mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Wired up DB to new Offers Modal (#18980)
refs https://github.com/TryGhost/Product/issues/4147 --- <!-- Leave the line below if you'd like GitHub Copilot to generate a summary from your commit --> <!-- copilot:summary --> ### <samp>🤖 Generated by Copilot at b18d725</samp> This pull request adds the functionality to create new offers for members in the admin-x-settings app. It uses custom hooks to handle the form logic and the API mutation, and updates the type definitions and the UI components accordingly. It also modifies the `offers.ts` file in the admin-x-framework to support the new feature. --------- Co-authored-by: Chris Raible <chris@ghost.org> Co-authored-by: Sodbileg Gansukh <sodbileg.gansukh@gmail.com>
This commit is contained in:
parent
3bec13774b
commit
452e702530
3 changed files with 136 additions and 58 deletions
|
@ -18,19 +18,27 @@ export type Offer = {
|
||||||
redemption_count: number;
|
redemption_count: number;
|
||||||
tier: {
|
tier: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PartialNewOffer = Omit<Offer, 'redemption_count'>;
|
||||||
|
export type NewOffer = Partial<Pick<PartialNewOffer, 'id'>> & Omit<PartialNewOffer, 'id'>;
|
||||||
|
|
||||||
export interface OffersResponseType {
|
export interface OffersResponseType {
|
||||||
meta?: Meta
|
meta?: Meta
|
||||||
offers: Offer[]
|
offers?: Offer[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OfferEditResponseType extends OffersResponseType {
|
export interface OfferEditResponseType extends OffersResponseType {
|
||||||
meta?: Meta
|
meta?: Meta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OfferAddResponseType {
|
||||||
|
meta?: Meta,
|
||||||
|
offers: NewOffer[]
|
||||||
|
}
|
||||||
|
|
||||||
const dataType = 'OffersResponseType';
|
const dataType = 'OffersResponseType';
|
||||||
|
|
||||||
export const useBrowseOffers = createQuery<OffersResponseType>({
|
export const useBrowseOffers = createQuery<OffersResponseType>({
|
||||||
|
@ -53,3 +61,14 @@ export const useEditOffer = createMutation<OfferEditResponseType, Offer>({
|
||||||
update: updateQueryCache('offers')
|
update: updateQueryCache('offers')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useAddOffer = createMutation<OfferAddResponseType, NewOffer>({
|
||||||
|
method: 'POST',
|
||||||
|
path: () => '/offers/',
|
||||||
|
body: offer => ({offers: [offer]}),
|
||||||
|
updateQueries: {
|
||||||
|
dataType,
|
||||||
|
emberUpdateType: 'createOrUpdate',
|
||||||
|
update: updateQueryCache('offers')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||||
import PortalFrame from '../../membership/portal/PortalFrame';
|
import PortalFrame from '../../membership/portal/PortalFrame';
|
||||||
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
||||||
import {Form, Icon, PreviewModalContent, Select, SelectOption, TextArea, TextField} from '@tryghost/admin-x-design-system';
|
import useForm from '../../../../hooks/useForm';
|
||||||
|
import {Form, Icon, PreviewModalContent, Select, SelectOption, TextArea, TextField, showToast} from '@tryghost/admin-x-design-system';
|
||||||
import {getOfferPortalPreviewUrl, offerPortalPreviewUrlTypes} from '../../../../utils/getOffersPortalPreviewUrl';
|
import {getOfferPortalPreviewUrl, offerPortalPreviewUrlTypes} from '../../../../utils/getOffersPortalPreviewUrl';
|
||||||
import {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers';
|
import {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers';
|
||||||
import {getTiersCadences} from '../../../../utils/getTiersCadences';
|
import {getTiersCadences} from '../../../../utils/getTiersCadences';
|
||||||
|
import {useAddOffer} from '@tryghost/admin-x-framework/api/offers';
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||||
|
@ -206,6 +208,7 @@ const AddOfferModal = () => {
|
||||||
const {data: {tiers} = {}} = useBrowseTiers();
|
const {data: {tiers} = {}} = useBrowseTiers();
|
||||||
const activeTiers = getPaidActiveTiers(tiers || []);
|
const activeTiers = getPaidActiveTiers(tiers || []);
|
||||||
const tierCadenceOptions = getTiersCadences(activeTiers);
|
const tierCadenceOptions = getTiersCadences(activeTiers);
|
||||||
|
const {mutateAsync: addOffer} = useAddOffer();
|
||||||
const [selectedTier, setSelectedTier] = useState({
|
const [selectedTier, setSelectedTier] = useState({
|
||||||
tier: tierCadenceOptions[0] || {},
|
tier: tierCadenceOptions[0] || {},
|
||||||
dataset: {
|
dataset: {
|
||||||
|
@ -214,34 +217,76 @@ const AddOfferModal = () => {
|
||||||
currency: tierCadenceOptions[0]?.value ? parseData(tierCadenceOptions[0]?.value).currency : ''
|
currency: tierCadenceOptions[0]?.value ? parseData(tierCadenceOptions[0]?.value).currency : ''
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const getDiscountAmount = (discount: number, dctype: string) => {
|
||||||
|
if (dctype === 'percentageOff') {
|
||||||
|
return discount.toString();
|
||||||
|
}
|
||||||
|
if (dctype === 'currencyOff') {
|
||||||
|
let calcDiscount = discount * 100;
|
||||||
|
return calcDiscount.toString();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const [overrides, setOverrides] = useState<offerPortalPreviewUrlTypes>({
|
const {formState, updateForm, handleSave, saveState, okProps} = useForm({
|
||||||
disableBackground: true,
|
initialState: {
|
||||||
name: '',
|
disableBackground: true,
|
||||||
code: {
|
name: '',
|
||||||
isDirty: false,
|
code: {
|
||||||
value: ''
|
isDirty: false,
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
displayTitle: {
|
||||||
|
isDirty: false,
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
displayDescription: '',
|
||||||
|
type: 'percent',
|
||||||
|
cadence: selectedTier?.dataset?.period || '',
|
||||||
|
trialAmount: 7,
|
||||||
|
discountAmount: 0,
|
||||||
|
duration: 'once',
|
||||||
|
durationInMonths: 0,
|
||||||
|
currency: selectedTier?.dataset?.currency || '',
|
||||||
|
status: 'active',
|
||||||
|
tierId: selectedTier?.dataset?.id || '',
|
||||||
|
amountType: 'percentageOff'
|
||||||
},
|
},
|
||||||
displayTitle: {
|
onSave: async () => {
|
||||||
isDirty: false,
|
const dataset = {
|
||||||
value: ''
|
name: formState.name,
|
||||||
|
code: formState.code.value,
|
||||||
|
display_title: formState.displayTitle.value,
|
||||||
|
display_description: formState.displayDescription,
|
||||||
|
cadence: formState.cadence,
|
||||||
|
amount: formState.type === 'trial' ? Number(getDiscountAmount(formState.trialAmount, formState.amountType)) : Number(getDiscountAmount(formState.discountAmount, formState.amountType)),
|
||||||
|
duration: formState.type === 'trial' ? 'trial' : formState.duration,
|
||||||
|
duration_in_months: formState.durationInMonths,
|
||||||
|
currency: formState.currency,
|
||||||
|
status: formState.status,
|
||||||
|
tier: {
|
||||||
|
id: formState.tierId
|
||||||
|
},
|
||||||
|
type: formState.type,
|
||||||
|
currency_restriction: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await addOffer(dataset);
|
||||||
|
|
||||||
|
if (response && response.offers && response.offers.length > 0) {
|
||||||
|
modal.remove();
|
||||||
|
updateRoute(`offers/${response.offers[0].id}`);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
displayDescription: '',
|
onSaveError: () => {},
|
||||||
type: 'percent',
|
onValidate: () => {
|
||||||
cadence: selectedTier?.dataset?.period || '',
|
return {};
|
||||||
trialAmount: 7,
|
},
|
||||||
discountAmount: 0,
|
savingDelay: 500
|
||||||
duration: 'once',
|
|
||||||
durationInMonths: 0,
|
|
||||||
currency: selectedTier?.dataset?.currency || '',
|
|
||||||
status: 'active',
|
|
||||||
tierId: selectedTier?.dataset?.id || '',
|
|
||||||
amountType: 'percentageOff'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const amountOptions = [
|
const amountOptions = [
|
||||||
{value: 'percentageOff', label: '%'},
|
{value: 'percentageOff', label: '%'},
|
||||||
{value: 'currencyOff', label: overrides.currency}
|
{value: 'currencyOff', label: formState.currency}
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleTierChange = (tier: SelectOption) => {
|
const handleTierChange = (tier: SelectOption) => {
|
||||||
|
@ -249,27 +294,26 @@ const AddOfferModal = () => {
|
||||||
tier,
|
tier,
|
||||||
dataset: parseData(tier.value)
|
dataset: parseData(tier.value)
|
||||||
});
|
});
|
||||||
|
updateForm(state => ({
|
||||||
setOverrides({
|
...state,
|
||||||
...overrides,
|
|
||||||
tierId: parseData(tier.value).id,
|
|
||||||
cadence: parseData(tier.value).period,
|
cadence: parseData(tier.value).period,
|
||||||
currency: parseData(tier.value).currency
|
currency: parseData(tier.value).currency,
|
||||||
});
|
tierId: parseData(tier.value).id
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTypeChange = (type: string) => {
|
const handleTypeChange = (type: string) => {
|
||||||
setOverrides({
|
updateForm(state => ({
|
||||||
...overrides,
|
...state,
|
||||||
type: type
|
type: type
|
||||||
});
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAmountTypeChange = (amountType: string) => {
|
const handleAmountTypeChange = (amountType: string) => {
|
||||||
setOverrides({
|
updateForm(state => ({
|
||||||
...overrides,
|
...state,
|
||||||
amountType: amountType
|
amountType: amountType
|
||||||
});
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTextInput = (
|
const handleTextInput = (
|
||||||
|
@ -277,16 +321,15 @@ const AddOfferModal = () => {
|
||||||
key: keyof offerPortalPreviewUrlTypes
|
key: keyof offerPortalPreviewUrlTypes
|
||||||
) => {
|
) => {
|
||||||
const target = e.target as HTMLInputElement | HTMLTextAreaElement;
|
const target = e.target as HTMLInputElement | HTMLTextAreaElement;
|
||||||
setOverrides((prevOverrides: offerPortalPreviewUrlTypes) => {
|
updateForm((state) => {
|
||||||
// Extract the current value for the key
|
// Extract the current value for the key
|
||||||
const currentValue = prevOverrides[key];
|
const currentValue = (state as offerPortalPreviewUrlTypes)[key];
|
||||||
|
|
||||||
// Check if the current value is an object and has 'isDirty' and 'value' properties
|
// Check if the current value is an object and has 'isDirty' and 'value' properties
|
||||||
if (currentValue && typeof currentValue === 'object' && 'isDirty' in currentValue && 'value' in currentValue) {
|
if (currentValue && typeof currentValue === 'object' && 'isDirty' in currentValue && 'value' in currentValue) {
|
||||||
// Determine if the field has been modified
|
// Determine if the field has been modified
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prevOverrides,
|
...state,
|
||||||
[key]: {
|
[key]: {
|
||||||
...currentValue,
|
...currentValue,
|
||||||
isDirty: true,
|
isDirty: true,
|
||||||
|
@ -296,7 +339,7 @@ const AddOfferModal = () => {
|
||||||
} else {
|
} else {
|
||||||
// For simple properties, update the value directly
|
// For simple properties, update the value directly
|
||||||
return {
|
return {
|
||||||
...prevOverrides,
|
...state,
|
||||||
[key]: target.value
|
[key]: target.value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -305,8 +348,7 @@ const AddOfferModal = () => {
|
||||||
|
|
||||||
const handleNameInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleNameInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
|
updateForm((prevOverrides) => {
|
||||||
setOverrides((prevOverrides) => {
|
|
||||||
let newOverrides = {...prevOverrides};
|
let newOverrides = {...prevOverrides};
|
||||||
newOverrides.name = newValue;
|
newOverrides.name = newValue;
|
||||||
if (!prevOverrides.code.isDirty) {
|
if (!prevOverrides.code.isDirty) {
|
||||||
|
@ -327,17 +369,17 @@ const AddOfferModal = () => {
|
||||||
|
|
||||||
const handleTextAreaInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleTextAreaInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const target = e.target as HTMLTextAreaElement;
|
const target = e.target as HTMLTextAreaElement;
|
||||||
setOverrides({
|
updateForm(state => ({
|
||||||
...overrides,
|
...state,
|
||||||
displayDescription: target.value
|
displayDescription: target.value
|
||||||
});
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDurationChange = (duration: string) => {
|
const handleDurationChange = (duration: string) => {
|
||||||
setOverrides({
|
updateForm(state => ({
|
||||||
...overrides,
|
...state,
|
||||||
duration: duration
|
duration: duration
|
||||||
});
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -353,9 +395,9 @@ const AddOfferModal = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newHref = getOfferPortalPreviewUrl(overrides, siteData.url);
|
const newHref = getOfferPortalPreviewUrl(formState, siteData.url);
|
||||||
setHref(newHref);
|
setHref(newHref);
|
||||||
}, [overrides, siteData.url]);
|
}, [formState, siteData.url]);
|
||||||
|
|
||||||
const sidebar = <Sidebar
|
const sidebar = <Sidebar
|
||||||
amountOptions={amountOptions as SelectOption[]}
|
amountOptions={amountOptions as SelectOption[]}
|
||||||
|
@ -367,7 +409,7 @@ const AddOfferModal = () => {
|
||||||
handleTextInput={handleTextInput}
|
handleTextInput={handleTextInput}
|
||||||
handleTierChange={handleTierChange}
|
handleTierChange={handleTierChange}
|
||||||
handleTypeChange={handleTypeChange}
|
handleTypeChange={handleTypeChange}
|
||||||
overrides={overrides}
|
overrides={formState}
|
||||||
selectedTier={selectedTier.tier}
|
selectedTier={selectedTier.tier}
|
||||||
tierOptions={tierCadenceOptions}
|
tierOptions={tierCadenceOptions}
|
||||||
typeOptions={typeOptions}
|
typeOptions={typeOptions}
|
||||||
|
@ -376,8 +418,25 @@ const AddOfferModal = () => {
|
||||||
const iframe = <PortalFrame
|
const iframe = <PortalFrame
|
||||||
href={href}
|
href={href}
|
||||||
/>;
|
/>;
|
||||||
|
return <PreviewModalContent
|
||||||
return <PreviewModalContent cancelLabel='Cancel' deviceSelector={false} okLabel='Publish' preview={iframe} sidebar={sidebar} size='full' title='Offer' onCancel={cancelAddOffer} />;
|
cancelLabel='Cancel'
|
||||||
|
deviceSelector={false}
|
||||||
|
dirty={saveState === 'unsaved'}
|
||||||
|
okColor={okProps.color}
|
||||||
|
okLabel='Publish'
|
||||||
|
preview={iframe}
|
||||||
|
sidebar={sidebar}
|
||||||
|
size='full'
|
||||||
|
title='Offer'
|
||||||
|
onCancel={cancelAddOffer}
|
||||||
|
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.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NiceModal.create(AddOfferModal);
|
export default NiceModal.create(AddOfferModal);
|
||||||
|
|
|
@ -130,11 +130,11 @@ const OffersModal = () => {
|
||||||
currency={offer?.currency || 'USD'}
|
currency={offer?.currency || 'USD'}
|
||||||
duration={offer?.duration}
|
duration={offer?.duration}
|
||||||
name={offer?.name}
|
name={offer?.name}
|
||||||
offerId={offer?.id}
|
offerId={offer?.id ? offer.id : ''}
|
||||||
offerTier={offerTier}
|
offerTier={offerTier}
|
||||||
redemptionCount={offer?.redemption_count}
|
redemptionCount={offer?.redemption_count ? offer.redemption_count : 0}
|
||||||
type={offer?.type as OfferType}
|
type={offer?.type as OfferType}
|
||||||
onClick={() => handleOfferEdit(offer?.id)}
|
onClick={() => handleOfferEdit(offer?.id ? offer.id : '')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -156,7 +156,7 @@ const OffersModal = () => {
|
||||||
<td>{offerTier.name} {getOfferCadence(offer.cadence)}</td>
|
<td>{offerTier.name} {getOfferCadence(offer.cadence)}</td>
|
||||||
<td><span className={`text-xs font-semibold uppercase ${discountColor}`}>{discountOffer}</span></td>
|
<td><span className={`text-xs font-semibold uppercase ${discountColor}`}>{discountOffer}</span></td>
|
||||||
<td>{updatedPriceWithCurrency}{originalPriceWithCurrency}</td>
|
<td>{updatedPriceWithCurrency}{originalPriceWithCurrency}</td>
|
||||||
<td><a className='hover:underline' href={createRedemptionFilterUrl(offer.id)}>{offer.redemption_count}</a></td>
|
<td><a className='hover:underline' href={createRedemptionFilterUrl(offer.id ? offer.id : '')}>{offer.redemption_count}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
Loading…
Add table
Reference in a new issue