mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-06 22:40:14 -05:00
Wired up Offer update Page
This commit is contained in:
parent
1382e34e42
commit
1e8176f596
3 changed files with 152 additions and 55 deletions
|
@ -1,4 +1,5 @@
|
|||
import {Meta, createQuery, createQueryWithId} from '../utils/api/hooks';
|
||||
import {Meta, createMutation, createQuery, createQueryWithId} from '../utils/api/hooks';
|
||||
import {updateQueryCache} from '../utils/api/updateQueries';
|
||||
|
||||
export type Offer = {
|
||||
id: string;
|
||||
|
@ -26,6 +27,10 @@ export interface OffersResponseType {
|
|||
offers: Offer[]
|
||||
}
|
||||
|
||||
export interface OfferEditResponseType extends OffersResponseType {
|
||||
meta?: Meta
|
||||
}
|
||||
|
||||
const dataType = 'OffersResponseType';
|
||||
|
||||
export const useBrowseOffers = createQuery<OffersResponseType>({
|
||||
|
@ -37,3 +42,14 @@ export const useBrowseOffersById = createQueryWithId<OffersResponseType>({
|
|||
dataType,
|
||||
path: `/offers/`
|
||||
});
|
||||
|
||||
export const useEditOffer = createMutation<OfferEditResponseType, Offer>({
|
||||
method: 'PUT',
|
||||
path: offer => `/offers/${offer.id}/`,
|
||||
body: offer => ({offers: [offer]}),
|
||||
updateQueries: {
|
||||
dataType,
|
||||
emberUpdateType: 'createOrUpdate',
|
||||
update: updateQueryCache('offers')
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,60 +1,97 @@
|
|||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
||||
import useForm, {ErrorMessages} from '../../../../hooks/useForm';
|
||||
import useHandleError from '../../../../utils/api/handleError';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {Button, Form, PreviewModalContent, TextArea, TextField} from '@tryghost/admin-x-design-system';
|
||||
import {Offer, useBrowseOffersById} from '../../../../api/offers';
|
||||
import {Button, Form, PreviewModalContent, TextArea, TextField, showToast} from '@tryghost/admin-x-design-system';
|
||||
import {Offer, useBrowseOffersById, useEditOffer} from '../../../../api/offers';
|
||||
import {RoutingModalProps} from '../../../providers/RoutingProvider';
|
||||
import {useEffect} from 'react';
|
||||
import {getHomepageUrl} from '../../../../api/site';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||
|
||||
const Sidebar: React.FC<{offer: Offer}> = ({offer}) => {
|
||||
return (
|
||||
<div className='pt-7'>
|
||||
<Form>
|
||||
<TextField
|
||||
hint='Visible to members on Stripe Checkout page.'
|
||||
placeholder='Black Friday'
|
||||
title='Name'
|
||||
value={offer?.name}
|
||||
/>
|
||||
<section className='mt-4'>
|
||||
<h2 className='mb-4 text-lg'>Portal Settings</h2>
|
||||
<div className='flex flex-col gap-6'>
|
||||
const Sidebar: React.FC<{
|
||||
clearError: (field: string) => void,
|
||||
errors: ErrorMessages,
|
||||
offer: Offer,
|
||||
updateOffer: (fields: Partial<Offer>) => void,
|
||||
validate: () => void}> = ({clearError, errors, offer, updateOffer, validate}) => {
|
||||
const {siteData} = useGlobalData();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const offerUrl = `${getHomepageUrl(siteData!)}/#/portal/offers/${offer?.code}`;
|
||||
const handleCopyClick = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(offerUrl);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 1000); // reset after 1 seconds
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='pt-7'>
|
||||
<Form>
|
||||
<TextField
|
||||
placeholder='Black Friday Special'
|
||||
title='Display title'
|
||||
value={offer?.display_title}
|
||||
error={Boolean(errors.name)}
|
||||
hint={errors.name || 'Visible to members on Stripe Checkout page'}
|
||||
placeholder='Black Friday'
|
||||
title='Name'
|
||||
value={offer?.name}
|
||||
onBlur={validate}
|
||||
onChange={e => updateOffer({name: e.target.value})}
|
||||
onKeyDown={() => clearError('name')}
|
||||
/>
|
||||
<TextField
|
||||
placeholder='black-friday'
|
||||
title='Offer code'
|
||||
value={offer?.code}
|
||||
/>
|
||||
<TextArea
|
||||
placeholder='Take advantage of this limited-time offer.'
|
||||
title='Display description'
|
||||
value={offer.display_description}
|
||||
/>
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<TextField
|
||||
placeholder='https://www.example.com'
|
||||
title='URL'
|
||||
type='url'
|
||||
value={`http://localhost:2368//#/portal/offers/${offer?.code}`}
|
||||
/>
|
||||
<Button color='green' fullWidth={true} label='Copy link' />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
<section className='mt-4'>
|
||||
<h2 className='mb-4 text-lg'>Portal Settings</h2>
|
||||
<div className='flex flex-col gap-6'>
|
||||
<TextField
|
||||
placeholder='Black Friday Special'
|
||||
title='Display title'
|
||||
value={offer?.display_title}
|
||||
onChange={e => updateOffer({display_title: e.target.value})}
|
||||
/>
|
||||
<TextField
|
||||
error={Boolean(errors.code)}
|
||||
hint={errors.code}
|
||||
placeholder='black-friday'
|
||||
title='Offer code'
|
||||
value={offer?.code}
|
||||
onBlur={validate}
|
||||
onChange={e => updateOffer({code: e.target.value})}
|
||||
onKeyDown={() => clearError('name')}
|
||||
/>
|
||||
<TextArea
|
||||
placeholder='Take advantage of this limited-time offer.'
|
||||
title='Display description'
|
||||
value={offer?.display_description}
|
||||
onChange={e => updateOffer({display_description: e.target.value})}
|
||||
/>
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<TextField
|
||||
disabled={Boolean(true)}
|
||||
placeholder='https://www.example.com'
|
||||
title='URL'
|
||||
type='url'
|
||||
value={offerUrl}
|
||||
/>
|
||||
<Button color={isCopied ? 'green' : 'black'} label={isCopied ? 'Copied!' : 'Copy code'} onClick={handleCopyClick} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EditOfferModal: React.FC<RoutingModalProps> = ({params}) => {
|
||||
const modal = useModal();
|
||||
const {updateRoute} = useRouting();
|
||||
const handleError = useHandleError();
|
||||
const hasOffers = useFeatureFlag('adminXOffers');
|
||||
let offer : Offer;
|
||||
const {mutateAsync: editOffer} = useEditOffer();
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasOffers) {
|
||||
|
@ -64,19 +101,62 @@ const EditOfferModal: React.FC<RoutingModalProps> = ({params}) => {
|
|||
}, [hasOffers, modal, updateRoute]);
|
||||
|
||||
const {data: {offers: offerById = []} = {}} = useBrowseOffersById(params?.id ? params?.id : '');
|
||||
if (offerById.length === 0) {
|
||||
return null;
|
||||
}
|
||||
offer = offerById[0];
|
||||
|
||||
const {formState, saveState, updateForm, setFormState, handleSave, validate, errors, clearError, okProps} = useForm({
|
||||
initialState: offerById[0],
|
||||
savingDelay: 500,
|
||||
onSave: async () => {
|
||||
await editOffer(formState);
|
||||
},
|
||||
onSaveError: handleError,
|
||||
onValidate: () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!formState?.name) {
|
||||
newErrors.name = 'Please enter a name';
|
||||
}
|
||||
|
||||
if (!formState?.code) {
|
||||
newErrors.code = 'Please enter a code';
|
||||
}
|
||||
|
||||
return newErrors;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setFormState(() => offerById[0]);
|
||||
}, [setFormState, offerById[0]]);
|
||||
|
||||
const updateOffer = (fields: Partial<Offer>) => {
|
||||
updateForm(state => ({...state, ...fields}));
|
||||
};
|
||||
|
||||
const sidebar = <Sidebar
|
||||
offer={offer}
|
||||
clearError={clearError}
|
||||
errors={errors}
|
||||
offer={formState}
|
||||
updateOffer={updateOffer}
|
||||
validate={validate}
|
||||
/>;
|
||||
|
||||
return <PreviewModalContent deviceSelector={false} okLabel='Update' sidebar={sidebar} size='full' title={offer?.name} onCancel={() => {
|
||||
modal.remove();
|
||||
updateRoute('offers/edit');
|
||||
}} />;
|
||||
return offerById ? <PreviewModalContent deviceSelector={false}
|
||||
dirty={saveState === 'unsaved'}
|
||||
okColor={okProps.color}
|
||||
okLabel={okProps.label || 'Save'}
|
||||
sidebar={sidebar} size='full' title='Offer'
|
||||
onCancel={() => {
|
||||
modal.remove();
|
||||
updateRoute('offers/edit');
|
||||
}}
|
||||
onOk={async () => {
|
||||
if (!(await handleSave({fakeWhenUnchanged: true}))) {
|
||||
showToast({
|
||||
type: 'pageError',
|
||||
message: 'Can\'t save offer, please double check that you\'ve filled all mandatory fields.'
|
||||
});
|
||||
}
|
||||
}} /> : null;
|
||||
};
|
||||
|
||||
export default NiceModal.create(EditOfferModal);
|
||||
|
|
|
@ -279,6 +279,7 @@ const fetchSettings = function () {
|
|||
const emberDataTypeMapping = {
|
||||
IntegrationsResponseType: {type: 'integration'},
|
||||
InvitesResponseType: {type: 'invite'},
|
||||
OffersResponseType: {type: 'offer'},
|
||||
NewslettersResponseType: {type: 'newsletter'},
|
||||
RecommendationResponseType: {type: 'recommendation'},
|
||||
SettingsResponseType: {type: 'setting', singleton: true},
|
||||
|
|
Loading…
Reference in a new issue