mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-28 21:33:24 +03: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:
parent
819ddccc72
commit
41ee387af2
@ -5,7 +5,7 @@ import {getOfferPortalPreviewUrl, offerPortalPreviewUrlTypes} from '../../../../
|
||||
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 {useEffect, useMemo, useState} from 'react';
|
||||
import {useForm} from '@tryghost/admin-x-framework/hooks';
|
||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||
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 = {
|
||||
tierOptions: SelectOption[];
|
||||
handleTierChange: (tier: SelectOption) => void;
|
||||
selectedTier: SelectOption;
|
||||
overrides: offerPortalPreviewUrlTypes
|
||||
handleTextInput: (e: React.ChangeEvent<HTMLInputElement>, key: keyof offerPortalPreviewUrlTypes) => void;
|
||||
overrides: formStateTypes;
|
||||
// handleTextInput: (e: React.ChangeEvent<HTMLInputElement>, key: keyof offerPortalPreviewUrlTypes) => void;
|
||||
amountOptions: SelectOption[];
|
||||
typeOptions: OfferType[];
|
||||
durationOptions: SelectOption[];
|
||||
@ -59,12 +99,16 @@ type SidebarProps = {
|
||||
handleAmountTypeChange: (amountType: string) => void;
|
||||
handleNameInput: (e: React.ChangeEvent<HTMLInputElement>) => 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,
|
||||
handleTierChange,
|
||||
selectedTier,
|
||||
handleTextInput,
|
||||
// handleTextInput,
|
||||
typeOptions,
|
||||
durationOptions,
|
||||
handleTypeChange,
|
||||
@ -73,6 +117,10 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
|
||||
handleAmountTypeChange,
|
||||
handleNameInput,
|
||||
handleTextAreaInput,
|
||||
handleDisplayTitleInput,
|
||||
handleDurationInMonthsInput,
|
||||
handleAmountInput,
|
||||
handleCodeInput,
|
||||
amountOptions}) => {
|
||||
const getFilteredDurationOptions = () => {
|
||||
// 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'>
|
||||
<TextField title='Amount off' type='number' onChange={(e) => {
|
||||
handleTextInput(e, 'discountAmount');
|
||||
<TextField title='Amount off' type='number' value={overrides.type === 'fixed' ? overrides.fixedAmount?.toString() : overrides.percentAmount?.toString()} onChange={(e) => {
|
||||
handleAmountInput(e);
|
||||
}} />
|
||||
<div className='absolute bottom-0 right-1.5 z-10'>
|
||||
<Select
|
||||
clearBg={true}
|
||||
controlClasses={{menu: 'w-20 right-0'}}
|
||||
options={amountOptions}
|
||||
selectedOption={overrides.amountType === 'percent' ? amountOptions[0] : amountOptions[1]}
|
||||
selectedOption={overrides.type === 'percent' ? amountOptions[0] : amountOptions[1]}
|
||||
onSelect={(e) => {
|
||||
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) => {
|
||||
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) => {
|
||||
handleTextInput(e, 'trialAmount');
|
||||
handleAmountInput(e);
|
||||
}} />
|
||||
}
|
||||
|
||||
@ -163,7 +211,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
|
||||
title='Display title'
|
||||
value={overrides.displayTitle.value}
|
||||
onChange={(e) => {
|
||||
handleTextInput(e, 'displayTitle');
|
||||
handleDisplayTitleInput(e);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
@ -171,7 +219,7 @@ const Sidebar: React.FC<SidebarProps> = ({tierOptions,
|
||||
title='Offer code'
|
||||
value={overrides.code.value}
|
||||
onChange={(e) => {
|
||||
handleTextInput(e, 'code');
|
||||
handleCodeInput(e);
|
||||
}}
|
||||
/>
|
||||
<TextArea
|
||||
@ -226,19 +274,22 @@ const AddOfferModal = () => {
|
||||
currency: tierCadenceOptions[0]?.value ? parseData(tierCadenceOptions[0]?.value).currency : ''
|
||||
}
|
||||
});
|
||||
const getDiscountAmount = (discount: number, dctype: string) => {
|
||||
if (dctype === 'percent') {
|
||||
return discount.toString();
|
||||
}
|
||||
if (dctype === 'fixed') {
|
||||
let calcDiscount = discount * 100;
|
||||
return calcDiscount.toString();
|
||||
}
|
||||
};
|
||||
|
||||
// const calculateAmount = useCallback(() => {
|
||||
// if (formState.type === 'fixed') {
|
||||
// return formState.fixedAmount;
|
||||
// } else if (formState.type === 'percent') {
|
||||
// return formState.percentAmount;
|
||||
// } else if (formState.type === 'trial') {
|
||||
// return formState.trialAmount;
|
||||
// } else {
|
||||
// return formState.amount; // default case
|
||||
// }
|
||||
// }, [f;
|
||||
|
||||
const {formState, updateForm, handleSave, saveState, okProps} = useForm({
|
||||
initialState: {
|
||||
disableBackground: true,
|
||||
disableBackground: false,
|
||||
name: '',
|
||||
code: {
|
||||
isDirty: false,
|
||||
@ -251,14 +302,15 @@ const AddOfferModal = () => {
|
||||
displayDescription: '',
|
||||
type: 'percent',
|
||||
cadence: selectedTier?.dataset?.period || '',
|
||||
trialAmount: 7,
|
||||
discountAmount: 0,
|
||||
amount: 0,
|
||||
duration: 'once',
|
||||
durationInMonths: 0,
|
||||
currency: selectedTier?.dataset?.currency || '',
|
||||
currency: selectedTier?.dataset?.currency || 'USD',
|
||||
status: 'active',
|
||||
tierId: selectedTier?.dataset?.id || '',
|
||||
amountType: 'percent'
|
||||
trialAmount: 7,
|
||||
fixedAmount: 0,
|
||||
percentAmount: 0
|
||||
},
|
||||
onSave: async () => {
|
||||
const dataset = {
|
||||
@ -267,7 +319,7 @@ const AddOfferModal = () => {
|
||||
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)),
|
||||
amount: calculateAmount(formState) || 0,
|
||||
duration: formState.type === 'trial' ? 'trial' : formState.duration,
|
||||
duration_in_months: Number(formState.durationInMonths),
|
||||
currency: formState.currency,
|
||||
@ -320,39 +372,37 @@ const AddOfferModal = () => {
|
||||
const handleAmountTypeChange = (amountType: string) => {
|
||||
updateForm(state => ({
|
||||
...state,
|
||||
amountType: amountType,
|
||||
type: amountType === 'percent' ? 'percent' : 'fixed' || state.type
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTextInput = (
|
||||
e: React.ChangeEvent<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
|
||||
const handleAmountInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
return {
|
||||
if (formState.type === 'fixed') {
|
||||
updateForm(state => ({
|
||||
...state,
|
||||
[key]: {
|
||||
...currentValue,
|
||||
isDirty: true,
|
||||
value: target.value
|
||||
}
|
||||
};
|
||||
fixedAmount: Number(target.value)
|
||||
}));
|
||||
} else if (formState.type === 'percent') {
|
||||
updateForm(state => ({
|
||||
...state,
|
||||
percentAmount: Number(target.value)
|
||||
}));
|
||||
} else {
|
||||
// For simple properties, update the value directly
|
||||
return {
|
||||
updateForm(state => ({
|
||||
...state,
|
||||
[key]: target.value
|
||||
};
|
||||
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>) => {
|
||||
@ -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 target = e.target as HTMLTextAreaElement;
|
||||
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(() => {
|
||||
if (!hasOffers) {
|
||||
modal.remove();
|
||||
@ -402,19 +476,54 @@ const AddOfferModal = () => {
|
||||
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(() => {
|
||||
const newHref = getOfferPortalPreviewUrl(formState, siteData.url);
|
||||
const newHref = getOfferPortalPreviewUrl(overrides, siteData.url);
|
||||
setHref(newHref);
|
||||
}, [formState, siteData.url]);
|
||||
}, [formState, siteData.url, formState.type, overrides]);
|
||||
|
||||
const sidebar = <Sidebar
|
||||
amountOptions={amountOptions as SelectOption[]}
|
||||
durationOptions={durationOptions}
|
||||
handleAmountInput={handleAmountInput}
|
||||
handleAmountTypeChange={handleAmountTypeChange}
|
||||
handleCodeInput={handleCodeInput}
|
||||
handleDisplayTitleInput={handleDisplayTitleInput}
|
||||
handleDurationChange={handleDurationChange}
|
||||
handleDurationInMonthsInput={handleDurationInMonthsInput}
|
||||
handleNameInput={handleNameInput}
|
||||
handleTextAreaInput={handleTextAreaInput}
|
||||
handleTextInput={handleTextInput}
|
||||
handleTierChange={handleTierChange}
|
||||
handleTypeChange={handleTypeChange}
|
||||
overrides={formState}
|
||||
|
@ -237,23 +237,17 @@ const EditOfferModal: React.FC<{id: string}> = ({id}) => {
|
||||
useEffect(() => {
|
||||
const dataset : offerPortalPreviewUrlTypes = {
|
||||
name: formState?.name || '',
|
||||
code: {
|
||||
value: formState?.code || ''
|
||||
},
|
||||
displayTitle: {
|
||||
value: formState?.display_title || ''
|
||||
},
|
||||
code: formState?.code || '',
|
||||
displayTitle: formState?.display_title || '',
|
||||
displayDescription: formState?.display_description || '',
|
||||
type: formState?.type || '',
|
||||
cadence: formState?.cadence || '',
|
||||
trialAmount: formState?.amount,
|
||||
discountAmount: formState?.amount,
|
||||
amount: formState?.amount,
|
||||
duration: formState?.duration || '',
|
||||
durationInMonths: formState?.duration_in_months || 0,
|
||||
currency: formState?.currency || '',
|
||||
status: formState?.status || '',
|
||||
tierId: formState?.tier.id || '',
|
||||
amountType: formState?.type === 'percent' ? 'percent' : 'amount'
|
||||
tierId: formState?.tier.id || ''
|
||||
};
|
||||
|
||||
const newHref = getOfferPortalPreviewUrl(dataset, siteData.url);
|
||||
|
@ -1,46 +1,29 @@
|
||||
export type offerPortalPreviewUrlTypes = {
|
||||
disableBackground?: boolean;
|
||||
name: string;
|
||||
code: {
|
||||
isDirty?: boolean;
|
||||
value: string;
|
||||
}
|
||||
displayTitle: {
|
||||
isDirty?: boolean;
|
||||
value: string;
|
||||
}
|
||||
displayDescription?: string;
|
||||
code: string;
|
||||
displayTitle: string;
|
||||
displayDescription: string;
|
||||
type: string;
|
||||
cadence: string;
|
||||
trialAmount?: number;
|
||||
discountAmount?: number;
|
||||
percentageOff?: number;
|
||||
amount: number;
|
||||
duration: string;
|
||||
durationInMonths: number;
|
||||
currency?: string;
|
||||
currency: string;
|
||||
status: string;
|
||||
tierId: string;
|
||||
amountType?: string;
|
||||
};
|
||||
|
||||
export const getOfferPortalPreviewUrl = (overrides:offerPortalPreviewUrlTypes, baseUrl: string) : string => {
|
||||
const {
|
||||
disableBackground = false,
|
||||
name,
|
||||
code = {
|
||||
isDirty: false,
|
||||
value: ''
|
||||
},
|
||||
displayTitle = {
|
||||
isDirty: false,
|
||||
value: ''
|
||||
},
|
||||
code,
|
||||
displayTitle = '',
|
||||
displayDescription = '',
|
||||
type,
|
||||
cadence,
|
||||
trialAmount = 7,
|
||||
discountAmount = 0,
|
||||
amountType,
|
||||
amount = 0,
|
||||
duration,
|
||||
durationInMonths,
|
||||
currency = 'usd',
|
||||
@ -51,30 +34,18 @@ export const getOfferPortalPreviewUrl = (overrides:offerPortalPreviewUrlTypes, b
|
||||
const portalBase = '/#/portal/preview/offer';
|
||||
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('code', encodeURIComponent(code.value));
|
||||
settingsParam.append('display_title', encodeURIComponent(displayTitle.value));
|
||||
settingsParam.append('code', encodeURIComponent(code));
|
||||
settingsParam.append('display_title', encodeURIComponent(displayTitle));
|
||||
settingsParam.append('display_description', encodeURIComponent(displayDescription));
|
||||
settingsParam.append('type', encodeURIComponent(type));
|
||||
settingsParam.append('cadence', encodeURIComponent(cadence));
|
||||
settingsParam.append('amount', encodeURIComponent(amount));
|
||||
settingsParam.append('duration', encodeURIComponent(duration));
|
||||
settingsParam.append('duration_in_months', encodeURIComponent(durationInMonths));
|
||||
settingsParam.append('currency', encodeURIComponent(currency));
|
||||
settingsParam.append('status', encodeURIComponent(status));
|
||||
settingsParam.append('tier_id', encodeURIComponent(tierId));
|
||||
settingsParam.append('amount', encodeURIComponent(type === 'trial' ? trialAmount.toString() : getDiscountAmount(discountAmount, amountType ? amountType : 'fixed') || '0'));
|
||||
|
||||
if (disableBackground) {
|
||||
settingsParam.append('disableBackground', 'true');
|
||||
|
Loading…
Reference in New Issue
Block a user