Skip to content

Commit

Permalink
Updated portal popup to support selecting and updating tiers (#17192)
Browse files Browse the repository at this point in the history
  • Loading branch information
binary-koan authored Jul 4, 2023
1 parent 7f9f467 commit 1cc55ed
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ type Story = StoryObj<typeof Checkbox>;

export const Default: Story = {
args: {
label: 'Checkbox 1',
id: 'my-radio-button'
label: 'Checkbox 1'
}
};

Expand Down
12 changes: 7 additions & 5 deletions apps/admin-x-settings/src/admin-x-ds/global/form/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import Heading from '../Heading';
import Hint from '../Hint';
import React, {useEffect, useState} from 'react';
import React, {useEffect, useId, useState} from 'react';
import Separator from '../Separator';

interface CheckboxProps {
id: string;
title?: string;
label: string;
value: string;
onChange: (checked: boolean) => void;
error?:boolean;
disabled?: boolean;
error?: boolean;
hint?: React.ReactNode;
checked?: boolean;
separator?: boolean;
}

const Checkbox: React.FC<CheckboxProps> = ({id, title, label, value, onChange, error, hint, checked, separator}) => {
const Checkbox: React.FC<CheckboxProps> = ({title, label, value, onChange, disabled, error, hint, checked, separator}) => {
const id = useId();
const [isChecked, setIsChecked] = useState(checked);

useEffect(() => {
Expand All @@ -36,6 +37,7 @@ const Checkbox: React.FC<CheckboxProps> = ({id, title, label, value, onChange, e
<input
checked={isChecked}
className="relative float-left mt-[3px] h-4 w-4 appearance-none border-2 border-solid border-grey-300 outline-none checked:border-green checked:bg-green checked:after:absolute checked:after:-mt-px checked:after:ml-[3px] checked:after:block checked:after:h-[11px] checked:after:w-[6px] checked:after:rotate-45 checked:after:border-[2px] checked:after:border-l-0 checked:after:border-t-0 checked:after:border-solid checked:after:border-white checked:after:bg-transparent checked:after:content-[''] hover:cursor-pointer focus:shadow-none focus:transition-[border-color_0.2s] dark:border-grey-600 dark:checked:border-green dark:checked:bg-green"
disabled={disabled}
id={id}
type='checkbox'
value={value}
Expand All @@ -52,4 +54,4 @@ const Checkbox: React.FC<CheckboxProps> = ({id, title, label, value, onChange, e
);
};

export default Checkbox;
export default Checkbox;
12 changes: 10 additions & 2 deletions apps/admin-x-settings/src/components/providers/ServiceProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, {createContext, useContext, useMemo} from 'react';
import setupGhostApi from '../../utils/api';
import useDataService, {DataService, bulkEdit} from '../../utils/dataService';
import useSearchService, {SearchService} from '../../utils/search';
import {OfficialTheme} from '../../models/themes';
import {Tier} from '../../types/api';

export interface FileService {
uploadImage: (file: File) => Promise<string>;
Expand All @@ -11,6 +13,7 @@ interface ServicesContextProps {
fileService: FileService|null;
officialThemes: OfficialTheme[];
search: SearchService
tiers: DataService<Tier>
}

interface ServicesProviderProps {
Expand All @@ -23,7 +26,8 @@ const ServicesContext = createContext<ServicesContextProps>({
api: setupGhostApi({ghostVersion: ''}),
fileService: null,
officialThemes: [],
search: {filter: '', setFilter: () => {}, checkVisible: () => true}
search: {filter: '', setFilter: () => {}, checkVisible: () => true},
tiers: {data: [], update: async () => {}}
});

const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersion, officialThemes}) => {
Expand All @@ -35,13 +39,15 @@ const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersi
}
}), [apiService]);
const search = useSearchService();
const tiers = useDataService({key: 'tiers', browse: apiService.tiers.browse, edit: bulkEdit('tiers', apiService.tiers.edit)});

return (
<ServicesContext.Provider value={{
api: apiService,
fileService,
officialThemes,
search
search,
tiers
}}>
{children}
</ServicesContext.Provider>
Expand All @@ -57,3 +63,5 @@ export const useApi = () => useServices().api;
export const useOfficialThemes = () => useServices().officialThemes;

export const useSearch = () => useServices().search;

export const useTiers = () => useServices().tiers;
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, {createContext, useCallback, useContext, useEffect, useState} from 'react';
import {Config, Setting, SiteData} from '../../types/api';
import {ServicesContext} from './ServiceProvider';
import {Setting, SiteData} from '../../types/api';

// Define the Settings Context
interface SettingsContextProps {
settings: Setting[] | null;
saveSettings: (updatedSettings: Setting[]) => Promise<Setting[]>;
siteData: SiteData | null;
config: Config | null;
}

interface SettingsProviderProps {
Expand All @@ -16,6 +17,7 @@ interface SettingsProviderProps {
const SettingsContext = createContext<SettingsContextProps>({
settings: null,
siteData: null,
config: null,
saveSettings: async () => []
});

Expand Down Expand Up @@ -79,18 +81,23 @@ function deserializeSettings(settings: Setting[]): Setting[] {
// Create a Settings Provider component
const SettingsProvider: React.FC<SettingsProviderProps> = ({children}) => {
const {api} = useContext(ServicesContext);
const [settings, setSettings] = useState <Setting[] | null> (null);
const [siteData, setSiteData] = useState <SiteData | null> (null);
const [settings, setSettings] = useState<Setting[] | null> (null);
const [siteData, setSiteData] = useState<SiteData | null> (null);
const [config, setConfig] = useState<Config | null> (null);

useEffect(() => {
const fetchSettings = async (): Promise<void> => {
try {
// Make an API call to fetch the settings
const data = await api.settings.browse();
const siteDataRes = await api.site.browse();

setSettings(serialiseSettingsData(data.settings));
setSiteData(siteDataRes.site);
const [settingsData, siteDataResponse, configData] = await Promise.all([
api.settings.browse(),
api.site.browse(),
api.config.browse()
]);

setSettings(serialiseSettingsData(settingsData.settings));
setSiteData(siteDataResponse.site);
setConfig(configData.config);
} catch (error) {
// Log error in settings API
}
Expand Down Expand Up @@ -120,7 +127,7 @@ const SettingsProvider: React.FC<SettingsProviderProps> = ({children}) => {
// Provide the settings and the saveSettings function to the children components
return (
<SettingsContext.Provider value={{
settings, saveSettings, siteData
settings, saveSettings, siteData, config
}}>
{children}
</SettingsContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@ import AccountPage from './portal/AccountPage';
import LookAndFeel from './portal/LookAndFeel';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import PortalPreview from './portal/PortalPreview';
import React, {useState} from 'react';
import React, {useContext, useState} from 'react';
import SignupOptions from './portal/SignupOptions';
import TabView, {Tab} from '../../../admin-x-ds/global/TabView';
import useSettingGroup from '../../../hooks/useSettingGroup';
import useForm, {Dirtyable} from '../../../hooks/useForm';
import {PreviewModalContent} from '../../../admin-x-ds/global/modal/PreviewModal';
import {Setting, SettingValue} from '../../../types/api';
import {Setting, SettingValue, Tier} from '../../../types/api';
import {SettingsContext} from '../../providers/SettingsProvider';
import {useTiers} from '../../providers/ServiceProvider';

const Sidebar: React.FC<{
localSettings: Setting[]
updateSetting: (key: string, setting: SettingValue) => void
}> = ({localSettings, updateSetting}) => {
localTiers: Tier[]
updateTier: (tier: Tier) => void
}> = ({localSettings, updateSetting, localTiers, updateTier}) => {
const [selectedTab, setSelectedTab] = useState('signupOptions');

const tabs: Tab[] = [
{
id: 'signupOptions',
title: 'Signup options',
contents: <SignupOptions localSettings={localSettings} updateSetting={updateSetting} />
contents: <SignupOptions localSettings={localSettings} localTiers={localTiers} updateSetting={updateSetting} updateTier={updateTier} />
},
{
id: 'lookAndFeel',
Expand Down Expand Up @@ -48,13 +52,44 @@ const PortalModal: React.FC = () => {
const modal = useModal();

const [selectedPreviewTab, setSelectedPreviewTab] = useState('signup');
const {localSettings, updateSetting, handleSave, saveState} = useSettingGroup();
const {settings, saveSettings} = useContext(SettingsContext);
const {data: tiers, update: updateTiers} = useTiers();

const {formState, saveState, handleSave, updateForm} = useForm({
initialState: {
settings: settings as Dirtyable<Setting>[],
tiers: tiers as Dirtyable<Tier>[]
},

onSave: async () => {
await updateTiers(formState.tiers.filter(tier => tier.dirty));
await saveSettings(formState.settings.filter(setting => setting.dirty));
}
});

const updateSetting = (key: string, value: SettingValue) => {
updateForm(state => ({
...state,
settings: state.settings.map(setting => (
setting.key === key ? {...setting, value, dirty: true} : setting
))
}));
};

const updateTier = (newTier: Tier) => {
updateForm(state => ({
...state,
tiers: state.tiers.map(tier => (
tier.id === newTier.id ? {...newTier, dirty: true} : tier
))
}));
};

const onSelectURL = (id: string) => {
setSelectedPreviewTab(id);
};

const sidebar = <Sidebar localSettings={localSettings} updateSetting={updateSetting} />;
const sidebar = <Sidebar localSettings={formState.settings} localTiers={formState.tiers} updateSetting={updateSetting} updateTier={updateTier} />;
const preview = <PortalPreview selectedTab={selectedPreviewTab} />;

let previewTabs: Tab[] = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,67 @@
import React from 'react';
import Checkbox from '../../../../admin-x-ds/global/form/Checkbox';
import Heading from '../../../../admin-x-ds/global/Heading';
import React, {useContext} from 'react';
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
import {Setting, SettingValue} from '../../../../types/api';
import {getSettingValues} from '../../../../utils/helpers';
import {Setting, SettingValue, Tier} from '../../../../types/api';
import {SettingsContext} from '../../../providers/SettingsProvider';
import {checkStripeEnabled, getSettingValues} from '../../../../utils/helpers';

const SignupOptions: React.FC<{
localSettings: Setting[]
updateSetting: (key: string, setting: SettingValue) => void
}> = ({localSettings, updateSetting}) => {
const [membersSignupAccess, portalName, portalSignupCheckboxRequired] = getSettingValues(localSettings, ['members_signup_access', 'portal_name', 'portal_signup_checkbox_required']);
localTiers: Tier[]
updateTier: (tier: Tier) => void
}> = ({localSettings, updateSetting, localTiers, updateTier}) => {
const {config} = useContext(SettingsContext);

const [membersSignupAccess, portalName, portalSignupCheckboxRequired, portalPlansJson] = getSettingValues(localSettings, ['members_signup_access', 'portal_name', 'portal_signup_checkbox_required', 'portal_plans']);
const portalPlans = JSON.parse(portalPlansJson?.toString() || '[]') as string[];

const togglePlan = (plan: string) => {
const index = portalPlans.indexOf(plan);

if (index === -1) {
portalPlans.push(plan);
} else {
portalPlans.splice(index, 1);
}

updateSetting('portal_plans', JSON.stringify(portalPlans));
};

// This is a bit unclear in current admin, maybe we should add a message if the settings are disabled?
const isDisabled = membersSignupAccess !== 'all';

const isStripeEnabled = checkStripeEnabled(localSettings, config!);

return <>
<Toggle
checked={Boolean(portalName)}
disabled={isDisabled}
label='Display name in signup form'
onChange={e => updateSetting('portal_name', e.target.checked)}
/>
<div>TODO: Tiers available at signup</div>

<Heading level={6} grey>Tiers available at signup</Heading>
<Checkbox checked={portalPlans.includes('free')} disabled={isDisabled} label='Free' value='free' onChange={() => togglePlan('free')} />

{isStripeEnabled && localTiers.map(tier => (
<Checkbox
checked={tier.visibility === 'public'}
label={tier.name}
value={tier.id}
onChange={checked => updateTier({...tier, visibility: checked ? 'public' : 'none'})}
/>
))}

{isStripeEnabled && localTiers.some(tier => tier.visibility === 'public') && (
<>
<Heading level={6} grey>Prices available at signup</Heading>
<Checkbox checked={portalPlans.includes('monthly')} disabled={isDisabled} label='Monthly' value='monthly' onChange={() => togglePlan('monthly')} />
<Checkbox checked={portalPlans.includes('yearly')} disabled={isDisabled} label='Yearly' value='yearly' onChange={() => togglePlan('yearly')} />
</>
)}

<div>TODO: Display notice at signup (Koenig)</div>
<Toggle
checked={Boolean(portalSignupCheckboxRequired)}
Expand Down
4 changes: 4 additions & 0 deletions apps/admin-x-settings/src/hooks/useForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import {useCallback, useEffect, useState} from 'react';

export type Dirtyable<Data> = Data & {
dirty?: boolean;
}

export type SaveState = 'unsaved' | 'saving' | 'saved' | 'error' | '';

export interface FormHook<State> {
Expand Down
4 changes: 4 additions & 0 deletions apps/admin-x-settings/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export type Setting = {
value: SettingValue;
}

export type Config = {
[key: string]: any;
}

export type User = {
id: string;
name: string;
Expand Down
Loading

0 comments on commit 1cc55ed

Please sign in to comment.