0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Fixed Offers portal preview edge cases (#19124)

no issue

- cleaned up offers portal preview.
- fixes a few logic errors and potential edge cases and making it easier
to maintain.
This commit is contained in:
Ronald Langeveld 2023-11-24 12:32:45 +02:00 committed by GitHub
parent 819ddccc72
commit 41ee387af2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 182 additions and 108 deletions

View file

@ -5,7 +5,7 @@ import {getOfferPortalPreviewUrl, offerPortalPreviewUrlTypes} from '../../../../
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 {useAddOffer} from '@tryghost/admin-x-framework/api/offers';
import {useEffect, useState} from 'react'; import {useEffect, useMemo, useState} from 'react';
import {useForm} from '@tryghost/admin-x-framework/hooks'; import {useForm} from '@tryghost/admin-x-framework/hooks';
import {useGlobalData} from '../../../providers/GlobalDataProvider'; import {useGlobalData} from '../../../providers/GlobalDataProvider';
import {useModal} from '@ebay/nice-modal-react'; import {useModal} from '@ebay/nice-modal-react';
@ -45,12 +45,52 @@ const ButtonSelect: React.FC<{type: OfferType, checked: boolean, onClick: () =>
); );
}; };
type formStateTypes = {
disableBackground?: boolean;
name: string;
code: {
isDirty: boolean;
value: string;
};
displayTitle: {
isDirty: boolean;
value: string;
};
displayDescription: string;
type: string;
cadence: string;
amount: number;
duration: string;
durationInMonths: number;
currency: string;
status: string;
tierId: string;
fixedAmount?: number;
trialAmount?: number;
percentAmount?: number;
};
const calculateAmount = (formState: formStateTypes): number => {
const {fixedAmount = 0, percentAmount = 0, trialAmount = 0, amount = 0} = formState;
switch (formState.type) {
case 'fixed':
return fixedAmount * 100;
case 'percent':
return percentAmount;
case 'trial':
return trialAmount;
default:
return amount;
}
};
type SidebarProps = { type SidebarProps = {
tierOptions: SelectOption[]; tierOptions: SelectOption[];
handleTierChange: (tier: SelectOption) => void; handleTierChange: (tier: SelectOption) => void;
selectedTier: SelectOption; selectedTier: SelectOption;
overrides: offerPortalPreviewUrlTypes overrides: formStateTypes;
handleTextInput: (e: React.ChangeEvent<HTMLInputElement>, key: keyof offerPortalPreviewUrlTypes) => void; // handleTextInput: (e: React.ChangeEvent<HTMLInputElement>, key: keyof offerPortalPreviewUrlTypes) => void;
amountOptions: SelectOption[]; amountOptions: SelectOption[];
typeOptions: OfferType[]; typeOptions: OfferType[];
durationOptions: SelectOption[]; durationOptions: SelectOption[];
@ -59,12 +99,16 @@ type SidebarProps = {
handleAmountTypeChange: (amountType: string) => void; handleAmountTypeChange: (amountType: string) => void;
handleNameInput: (e: React.ChangeEvent<HTMLInputElement>) => void; handleNameInput: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleTextAreaInput: (e: React.ChangeEvent<HTMLTextAreaElement>) => void; handleTextAreaInput: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
handleDisplayTitleInput: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleAmountInput: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleDurationInMonthsInput: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleCodeInput: (e: React.ChangeEvent<HTMLInputElement>) => void;
}; };
const Sidebar: React.FC<SidebarProps> = ({tierOptions, const Sidebar: React.FC<SidebarProps> = ({tierOptions,
handleTierChange, handleTierChange,
selectedTier, selectedTier,
handleTextInput, // handleTextInput,
typeOptions, typeOptions,
durationOptions, durationOptions,
handleTypeChange, handleTypeChange,
@ -73,6 +117,10 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
handleAmountTypeChange, handleAmountTypeChange,
handleNameInput, handleNameInput,
handleTextAreaInput, handleTextAreaInput,
handleDisplayTitleInput,
handleDurationInMonthsInput,
handleAmountInput,
handleCodeInput,
amountOptions}) => { amountOptions}) => {
const getFilteredDurationOptions = () => { const getFilteredDurationOptions = () => {
// Check if the selected tier's cadence is 'yearly' // Check if the selected tier's cadence is 'yearly'
@ -117,15 +165,15 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
/> />
{ {
overrides.type !== 'trial' && <> <div className='relative'> overrides.type !== 'trial' && <> <div className='relative'>
<TextField title='Amount off' type='number' onChange={(e) => { <TextField title='Amount off' type='number' value={overrides.type === 'fixed' ? overrides.fixedAmount?.toString() : overrides.percentAmount?.toString()} onChange={(e) => {
handleTextInput(e, 'discountAmount'); handleAmountInput(e);
}} /> }} />
<div className='absolute bottom-0 right-1.5 z-10'> <div className='absolute bottom-0 right-1.5 z-10'>
<Select <Select
clearBg={true} clearBg={true}
controlClasses={{menu: 'w-20 right-0'}} controlClasses={{menu: 'w-20 right-0'}}
options={amountOptions} options={amountOptions}
selectedOption={overrides.amountType === 'percent' ? amountOptions[0] : amountOptions[1]} selectedOption={overrides.type === 'percent' ? amountOptions[0] : amountOptions[1]}
onSelect={(e) => { onSelect={(e) => {
handleAmountTypeChange(e?.value || ''); handleAmountTypeChange(e?.value || '');
}} }}
@ -141,7 +189,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
{ {
overrides.duration === 'repeating' && <TextField title='Duration in months' type='number' onChange={(e) => { overrides.duration === 'repeating' && <TextField title='Duration in months' type='number' onChange={(e) => {
handleTextInput(e, 'durationInMonths'); handleDurationInMonthsInput(e);
}} /> }} />
} }
</> </>
@ -149,7 +197,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
{ {
overrides.type === 'trial' && <TextField title='Trial duration' type='number' value={overrides.trialAmount?.toString()} onChange={(e) => { overrides.type === 'trial' && <TextField title='Trial duration' type='number' value={overrides.trialAmount?.toString()} onChange={(e) => {
handleTextInput(e, 'trialAmount'); handleAmountInput(e);
}} /> }} />
} }
@ -163,7 +211,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
title='Display title' title='Display title'
value={overrides.displayTitle.value} value={overrides.displayTitle.value}
onChange={(e) => { onChange={(e) => {
handleTextInput(e, 'displayTitle'); handleDisplayTitleInput(e);
}} }}
/> />
<TextField <TextField
@ -171,7 +219,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
title='Offer code' title='Offer code'
value={overrides.code.value} value={overrides.code.value}
onChange={(e) => { onChange={(e) => {
handleTextInput(e, 'code'); handleCodeInput(e);
}} }}
/> />
<TextArea <TextArea
@ -226,19 +274,22 @@ 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 === 'percent') { // const calculateAmount = useCallback(() => {
return discount.toString(); // if (formState.type === 'fixed') {
} // return formState.fixedAmount;
if (dctype === 'fixed') { // } else if (formState.type === 'percent') {
let calcDiscount = discount * 100; // return formState.percentAmount;
return calcDiscount.toString(); // } else if (formState.type === 'trial') {
} // return formState.trialAmount;
}; // } else {
// return formState.amount; // default case
// }
// }, [f;
const {formState, updateForm, handleSave, saveState, okProps} = useForm({ const {formState, updateForm, handleSave, saveState, okProps} = useForm({
initialState: { initialState: {
disableBackground: true, disableBackground: false,
name: '', name: '',
code: { code: {
isDirty: false, isDirty: false,
@ -251,14 +302,15 @@ const AddOfferModal = () => {
displayDescription: '', displayDescription: '',
type: 'percent', type: 'percent',
cadence: selectedTier?.dataset?.period || '', cadence: selectedTier?.dataset?.period || '',
trialAmount: 7, amount: 0,
discountAmount: 0,
duration: 'once', duration: 'once',
durationInMonths: 0, durationInMonths: 0,
currency: selectedTier?.dataset?.currency || '', currency: selectedTier?.dataset?.currency || 'USD',
status: 'active', status: 'active',
tierId: selectedTier?.dataset?.id || '', tierId: selectedTier?.dataset?.id || '',
amountType: 'percent' trialAmount: 7,
fixedAmount: 0,
percentAmount: 0
}, },
onSave: async () => { onSave: async () => {
const dataset = { const dataset = {
@ -267,7 +319,7 @@ const AddOfferModal = () => {
display_title: formState.displayTitle.value, display_title: formState.displayTitle.value,
display_description: formState.displayDescription, display_description: formState.displayDescription,
cadence: formState.cadence, cadence: formState.cadence,
amount: formState.type === 'trial' ? Number(getDiscountAmount(formState.trialAmount, formState.amountType)) : Number(getDiscountAmount(formState.discountAmount, formState.amountType)), amount: calculateAmount(formState) || 0,
duration: formState.type === 'trial' ? 'trial' : formState.duration, duration: formState.type === 'trial' ? 'trial' : formState.duration,
duration_in_months: Number(formState.durationInMonths), duration_in_months: Number(formState.durationInMonths),
currency: formState.currency, currency: formState.currency,
@ -320,39 +372,37 @@ const AddOfferModal = () => {
const handleAmountTypeChange = (amountType: string) => { const handleAmountTypeChange = (amountType: string) => {
updateForm(state => ({ updateForm(state => ({
...state, ...state,
amountType: amountType,
type: amountType === 'percent' ? 'percent' : 'fixed' || state.type type: amountType === 'percent' ? 'percent' : 'fixed' || state.type
})); }));
}; };
const handleTextInput = ( const handleAmountInput = (e: React.ChangeEvent<HTMLInputElement>) => {
e: React.ChangeEvent<HTMLInputElement>, const target = e.target as HTMLInputElement;
key: keyof offerPortalPreviewUrlTypes
) => {
const target = e.target as HTMLInputElement | HTMLTextAreaElement;
updateForm((state) => {
// Extract the current value for the 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 { if (formState.type === 'fixed') {
...state, updateForm(state => ({
[key]: { ...state,
...currentValue, fixedAmount: Number(target.value)
isDirty: true, }));
value: target.value } else if (formState.type === 'percent') {
} updateForm(state => ({
}; ...state,
} else { percentAmount: Number(target.value)
// For simple properties, update the value directly }));
return { } else {
...state, updateForm(state => ({
[key]: target.value ...state,
}; amount: Number(target.value)
} }));
}); }
};
const handleDurationInMonthsInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
updateForm(state => ({
...state,
durationInMonths: Number(target.value)
}));
}; };
const handleNameInput = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNameInput = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -376,6 +426,18 @@ const AddOfferModal = () => {
}); });
}; };
const handleDisplayTitleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
updateForm(state => ({
...state,
displayTitle: {
...state.displayTitle,
isDirty: true,
value: target.value
}
}));
};
const handleTextAreaInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleTextAreaInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const target = e.target as HTMLTextAreaElement; const target = e.target as HTMLTextAreaElement;
updateForm(state => ({ updateForm(state => ({
@ -391,6 +453,18 @@ const AddOfferModal = () => {
})); }));
}; };
const handleCodeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const target = e.target as HTMLInputElement;
updateForm(state => ({
...state,
code: {
...state.code,
isDirty: true,
value: target.value
}
}));
};
useEffect(() => { useEffect(() => {
if (!hasOffers) { if (!hasOffers) {
modal.remove(); modal.remove();
@ -402,19 +476,54 @@ const AddOfferModal = () => {
updateRoute('offers/edit'); updateRoute('offers/edit');
}; };
// const overrides : offerPortalPreviewUrlTypes = {
// name: formState.name,
// code: formState.code.value,
// displayTitle: formState.displayTitle.value,
// displayDescription: formState.displayDescription,
// type: formState.type,
// cadence: formState.cadence,
// amount: calculateAmount(),
// duration: formState.type === 'trial' ? 'trial' : formState.duration,
// durationInMonths: formState.durationInMonths,
// currency: formState.currency,
// status: formState.status,
// tierId: formState.tierId
// };
const overrides : offerPortalPreviewUrlTypes = useMemo(() => {
return {
name: formState.name,
code: formState.code.value,
displayTitle: formState.displayTitle.value,
displayDescription: formState.displayDescription,
type: formState.type,
cadence: formState.cadence,
amount: calculateAmount(formState) || 0,
duration: formState.type === 'trial' ? 'trial' : formState.duration,
durationInMonths: formState.durationInMonths,
currency: formState.currency,
status: formState.status,
tierId: formState.tierId
};
}, [formState]);
useEffect(() => { useEffect(() => {
const newHref = getOfferPortalPreviewUrl(formState, siteData.url); const newHref = getOfferPortalPreviewUrl(overrides, siteData.url);
setHref(newHref); setHref(newHref);
}, [formState, siteData.url]); }, [formState, siteData.url, formState.type, overrides]);
const sidebar = <Sidebar const sidebar = <Sidebar
amountOptions={amountOptions as SelectOption[]} amountOptions={amountOptions as SelectOption[]}
durationOptions={durationOptions} durationOptions={durationOptions}
handleAmountInput={handleAmountInput}
handleAmountTypeChange={handleAmountTypeChange} handleAmountTypeChange={handleAmountTypeChange}
handleCodeInput={handleCodeInput}
handleDisplayTitleInput={handleDisplayTitleInput}
handleDurationChange={handleDurationChange} handleDurationChange={handleDurationChange}
handleDurationInMonthsInput={handleDurationInMonthsInput}
handleNameInput={handleNameInput} handleNameInput={handleNameInput}
handleTextAreaInput={handleTextAreaInput} handleTextAreaInput={handleTextAreaInput}
handleTextInput={handleTextInput}
handleTierChange={handleTierChange} handleTierChange={handleTierChange}
handleTypeChange={handleTypeChange} handleTypeChange={handleTypeChange}
overrides={formState} overrides={formState}

View file

@ -237,23 +237,17 @@ const EditOfferModal: React.FC<{id: string}> = ({id}) => {
useEffect(() => { useEffect(() => {
const dataset : offerPortalPreviewUrlTypes = { const dataset : offerPortalPreviewUrlTypes = {
name: formState?.name || '', name: formState?.name || '',
code: { code: formState?.code || '',
value: formState?.code || '' displayTitle: formState?.display_title || '',
},
displayTitle: {
value: formState?.display_title || ''
},
displayDescription: formState?.display_description || '', displayDescription: formState?.display_description || '',
type: formState?.type || '', type: formState?.type || '',
cadence: formState?.cadence || '', cadence: formState?.cadence || '',
trialAmount: formState?.amount, amount: formState?.amount,
discountAmount: formState?.amount,
duration: formState?.duration || '', duration: formState?.duration || '',
durationInMonths: formState?.duration_in_months || 0, durationInMonths: formState?.duration_in_months || 0,
currency: formState?.currency || '', currency: formState?.currency || '',
status: formState?.status || '', status: formState?.status || '',
tierId: formState?.tier.id || '', tierId: formState?.tier.id || ''
amountType: formState?.type === 'percent' ? 'percent' : 'amount'
}; };
const newHref = getOfferPortalPreviewUrl(dataset, siteData.url); const newHref = getOfferPortalPreviewUrl(dataset, siteData.url);

View file

@ -1,46 +1,29 @@
export type offerPortalPreviewUrlTypes = { export type offerPortalPreviewUrlTypes = {
disableBackground?: boolean; disableBackground?: boolean;
name: string; name: string;
code: { code: string;
isDirty?: boolean; displayTitle: string;
value: string; displayDescription: string;
}
displayTitle: {
isDirty?: boolean;
value: string;
}
displayDescription?: string;
type: string; type: string;
cadence: string; cadence: string;
trialAmount?: number; amount: number;
discountAmount?: number;
percentageOff?: number;
duration: string; duration: string;
durationInMonths: number; durationInMonths: number;
currency?: string; currency: string;
status: string; status: string;
tierId: string; tierId: string;
amountType?: string;
}; };
export const getOfferPortalPreviewUrl = (overrides:offerPortalPreviewUrlTypes, baseUrl: string) : string => { export const getOfferPortalPreviewUrl = (overrides:offerPortalPreviewUrlTypes, baseUrl: string) : string => {
const { const {
disableBackground = false, disableBackground = false,
name, name,
code = { code,
isDirty: false, displayTitle = '',
value: ''
},
displayTitle = {
isDirty: false,
value: ''
},
displayDescription = '', displayDescription = '',
type, type,
cadence, cadence,
trialAmount = 7, amount = 0,
discountAmount = 0,
amountType,
duration, duration,
durationInMonths, durationInMonths,
currency = 'usd', currency = 'usd',
@ -51,30 +34,18 @@ export const getOfferPortalPreviewUrl = (overrides:offerPortalPreviewUrlTypes, b
const portalBase = '/#/portal/preview/offer'; const portalBase = '/#/portal/preview/offer';
const settingsParam = new URLSearchParams(); const settingsParam = new URLSearchParams();
settingsParam.append('type', encodeURIComponent(type));
const getDiscountAmount = (discount: number, dctype: string) => {
if (dctype === 'percent') {
return discount.toString();
}
if (dctype === 'fixed') {
settingsParam.append('type', encodeURIComponent('fixed'));
let calcDiscount = discount * 100;
return calcDiscount.toString();
}
};
settingsParam.append('name', encodeURIComponent(name)); settingsParam.append('name', encodeURIComponent(name));
settingsParam.append('code', encodeURIComponent(code.value)); settingsParam.append('code', encodeURIComponent(code));
settingsParam.append('display_title', encodeURIComponent(displayTitle.value)); settingsParam.append('display_title', encodeURIComponent(displayTitle));
settingsParam.append('display_description', encodeURIComponent(displayDescription)); settingsParam.append('display_description', encodeURIComponent(displayDescription));
settingsParam.append('type', encodeURIComponent(type));
settingsParam.append('cadence', encodeURIComponent(cadence)); settingsParam.append('cadence', encodeURIComponent(cadence));
settingsParam.append('amount', encodeURIComponent(amount));
settingsParam.append('duration', encodeURIComponent(duration)); settingsParam.append('duration', encodeURIComponent(duration));
settingsParam.append('duration_in_months', encodeURIComponent(durationInMonths)); settingsParam.append('duration_in_months', encodeURIComponent(durationInMonths));
settingsParam.append('currency', encodeURIComponent(currency)); settingsParam.append('currency', encodeURIComponent(currency));
settingsParam.append('status', encodeURIComponent(status)); settingsParam.append('status', encodeURIComponent(status));
settingsParam.append('tier_id', encodeURIComponent(tierId)); settingsParam.append('tier_id', encodeURIComponent(tierId));
settingsParam.append('amount', encodeURIComponent(type === 'trial' ? trialAmount.toString() : getDiscountAmount(discountAmount, amountType ? amountType : 'fixed') || '0'));
if (disableBackground) { if (disableBackground) {
settingsParam.append('disableBackground', 'true'); settingsParam.append('disableBackground', 'true');