0
Fork 0
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:
Ronald Langeveld 2023-11-15 11:52:58 +07:00 committed by GitHub
parent 3bec13774b
commit 452e702530
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 136 additions and 58 deletions

View file

@ -18,19 +18,27 @@ export type Offer = {
redemption_count: number;
tier: {
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 {
meta?: Meta
offers: Offer[]
offers?: Offer[]
}
export interface OfferEditResponseType extends OffersResponseType {
meta?: Meta
}
export interface OfferAddResponseType {
meta?: Meta,
offers: NewOffer[]
}
const dataType = 'OffersResponseType';
export const useBrowseOffers = createQuery<OffersResponseType>({
@ -53,3 +61,14 @@ export const useEditOffer = createMutation<OfferEditResponseType, Offer>({
update: updateQueryCache('offers')
}
});
export const useAddOffer = createMutation<OfferAddResponseType, NewOffer>({
method: 'POST',
path: () => '/offers/',
body: offer => ({offers: [offer]}),
updateQueries: {
dataType,
emberUpdateType: 'createOrUpdate',
update: updateQueryCache('offers')
}
});

View file

@ -1,10 +1,12 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import PortalFrame from '../../membership/portal/PortalFrame';
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 {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers';
import {getTiersCadences} from '../../../../utils/getTiersCadences';
import {useAddOffer} from '@tryghost/admin-x-framework/api/offers';
import {useEffect, useState} from 'react';
import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useRouting} from '@tryghost/admin-x-framework/routing';
@ -206,6 +208,7 @@ const AddOfferModal = () => {
const {data: {tiers} = {}} = useBrowseTiers();
const activeTiers = getPaidActiveTiers(tiers || []);
const tierCadenceOptions = getTiersCadences(activeTiers);
const {mutateAsync: addOffer} = useAddOffer();
const [selectedTier, setSelectedTier] = useState({
tier: tierCadenceOptions[0] || {},
dataset: {
@ -214,34 +217,76 @@ const AddOfferModal = () => {
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>({
disableBackground: true,
name: '',
code: {
isDirty: false,
value: ''
const {formState, updateForm, handleSave, saveState, okProps} = useForm({
initialState: {
disableBackground: true,
name: '',
code: {
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: {
isDirty: false,
value: ''
onSave: async () => {
const dataset = {
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: '',
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'
onSaveError: () => {},
onValidate: () => {
return {};
},
savingDelay: 500
});
const amountOptions = [
{value: 'percentageOff', label: '%'},
{value: 'currencyOff', label: overrides.currency}
{value: 'currencyOff', label: formState.currency}
];
const handleTierChange = (tier: SelectOption) => {
@ -249,27 +294,26 @@ const AddOfferModal = () => {
tier,
dataset: parseData(tier.value)
});
setOverrides({
...overrides,
tierId: parseData(tier.value).id,
updateForm(state => ({
...state,
cadence: parseData(tier.value).period,
currency: parseData(tier.value).currency
});
currency: parseData(tier.value).currency,
tierId: parseData(tier.value).id
}));
};
const handleTypeChange = (type: string) => {
setOverrides({
...overrides,
updateForm(state => ({
...state,
type: type
});
}));
};
const handleAmountTypeChange = (amountType: string) => {
setOverrides({
...overrides,
updateForm(state => ({
...state,
amountType: amountType
});
}));
};
const handleTextInput = (
@ -277,16 +321,15 @@ const AddOfferModal = () => {
key: keyof offerPortalPreviewUrlTypes
) => {
const target = e.target as HTMLInputElement | HTMLTextAreaElement;
setOverrides((prevOverrides: offerPortalPreviewUrlTypes) => {
updateForm((state) => {
// 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
if (currentValue && typeof currentValue === 'object' && 'isDirty' in currentValue && 'value' in currentValue) {
// Determine if the field has been modified
return {
...prevOverrides,
...state,
[key]: {
...currentValue,
isDirty: true,
@ -296,7 +339,7 @@ const AddOfferModal = () => {
} else {
// For simple properties, update the value directly
return {
...prevOverrides,
...state,
[key]: target.value
};
}
@ -305,8 +348,7 @@ const AddOfferModal = () => {
const handleNameInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setOverrides((prevOverrides) => {
updateForm((prevOverrides) => {
let newOverrides = {...prevOverrides};
newOverrides.name = newValue;
if (!prevOverrides.code.isDirty) {
@ -327,17 +369,17 @@ const AddOfferModal = () => {
const handleTextAreaInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const target = e.target as HTMLTextAreaElement;
setOverrides({
...overrides,
updateForm(state => ({
...state,
displayDescription: target.value
});
}));
};
const handleDurationChange = (duration: string) => {
setOverrides({
...overrides,
updateForm(state => ({
...state,
duration: duration
});
}));
};
useEffect(() => {
@ -353,9 +395,9 @@ const AddOfferModal = () => {
};
useEffect(() => {
const newHref = getOfferPortalPreviewUrl(overrides, siteData.url);
const newHref = getOfferPortalPreviewUrl(formState, siteData.url);
setHref(newHref);
}, [overrides, siteData.url]);
}, [formState, siteData.url]);
const sidebar = <Sidebar
amountOptions={amountOptions as SelectOption[]}
@ -367,7 +409,7 @@ const AddOfferModal = () => {
handleTextInput={handleTextInput}
handleTierChange={handleTierChange}
handleTypeChange={handleTypeChange}
overrides={overrides}
overrides={formState}
selectedTier={selectedTier.tier}
tierOptions={tierCadenceOptions}
typeOptions={typeOptions}
@ -376,8 +418,25 @@ const AddOfferModal = () => {
const iframe = <PortalFrame
href={href}
/>;
return <PreviewModalContent cancelLabel='Cancel' deviceSelector={false} okLabel='Publish' preview={iframe} sidebar={sidebar} size='full' title='Offer' onCancel={cancelAddOffer} />;
return <PreviewModalContent
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);

View file

@ -130,11 +130,11 @@ const OffersModal = () => {
currency={offer?.currency || 'USD'}
duration={offer?.duration}
name={offer?.name}
offerId={offer?.id}
offerId={offer?.id ? offer.id : ''}
offerTier={offerTier}
redemptionCount={offer?.redemption_count}
redemptionCount={offer?.redemption_count ? offer.redemption_count : 0}
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><span className={`text-xs font-semibold uppercase ${discountColor}`}>{discountOffer}</span></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>
);
})}