diff --git a/packages/pl-fe/src/features/pl-fe-config/components/crypto-address-input.tsx b/packages/pl-fe/src/features/pl-fe-config/components/crypto-address-input.tsx index 9a6f6beba..1e89bdf9d 100644 --- a/packages/pl-fe/src/features/pl-fe-config/components/crypto-address-input.tsx +++ b/packages/pl-fe/src/features/pl-fe-config/components/crypto-address-input.tsx @@ -17,7 +17,7 @@ const CryptoAddressInput: StreamfieldComponent = ({ value, onChan const intl = useIntl(); const handleChange = (key: 'ticker' | 'address' | 'note'): React.ChangeEventHandler => e => { - onChange(value.set(key, e.currentTarget.value)); + onChange({ ...value, [key]: e.currentTarget.value }); }; return ( diff --git a/packages/pl-fe/src/features/pl-fe-config/components/footer-link-input.tsx b/packages/pl-fe/src/features/pl-fe-config/components/footer-link-input.tsx index d13d6ffd6..b5684fc69 100644 --- a/packages/pl-fe/src/features/pl-fe-config/components/footer-link-input.tsx +++ b/packages/pl-fe/src/features/pl-fe-config/components/footer-link-input.tsx @@ -16,7 +16,7 @@ const PromoPanelInput: StreamfieldComponent = ({ value, onChange }) const intl = useIntl(); const handleChange = (key: 'title' | 'url'): React.ChangeEventHandler => e => { - onChange(value.set(key, e.currentTarget.value)); + onChange({ ...value, [key]: e.currentTarget.value }); }; return ( diff --git a/packages/pl-fe/src/features/pl-fe-config/components/promo-panel-input.tsx b/packages/pl-fe/src/features/pl-fe-config/components/promo-panel-input.tsx index 162117446..a6db0ee5e 100644 --- a/packages/pl-fe/src/features/pl-fe-config/components/promo-panel-input.tsx +++ b/packages/pl-fe/src/features/pl-fe-config/components/promo-panel-input.tsx @@ -19,11 +19,11 @@ const PromoPanelInput: StreamfieldComponent = ({ value, onChange const intl = useIntl(); const handleIconChange = (icon: string) => { - onChange(value.set('icon', icon)); + onChange({ ...value, icon }); }; const handleChange = (key: 'text' | 'url'): React.ChangeEventHandler => e => { - onChange(value.set(key, e.currentTarget.value)); + onChange({ ...value, [key]: e.currentTarget.value }); }; return ( diff --git a/packages/pl-fe/src/features/pl-fe-config/index.tsx b/packages/pl-fe/src/features/pl-fe-config/index.tsx index 25f583318..0799ef402 100644 --- a/packages/pl-fe/src/features/pl-fe-config/index.tsx +++ b/packages/pl-fe/src/features/pl-fe-config/index.tsx @@ -1,4 +1,4 @@ -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { create } from 'mutative'; import React, { useState, useEffect, useMemo } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import * as v from 'valibot'; @@ -22,7 +22,7 @@ import ThemeSelector from 'pl-fe/features/ui/components/theme-selector'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useFeatures } from 'pl-fe/hooks/use-features'; -import { plFeConfigSchema } from 'pl-fe/normalizers/pl-fe/pl-fe-config'; +import { cryptoAddressSchema, footerItemSchema, plFeConfigSchema, promoPanelItemSchema, type PlFeConfig } from 'pl-fe/normalizers/pl-fe/pl-fe-config'; import toast from 'pl-fe/toast'; import CryptoAddressInput from './components/crypto-address-input'; @@ -55,18 +55,11 @@ const messages = defineMessages({ sentryDsnHint: { id: 'plfe_config.sentry_dsn_hint', defaultMessage: 'DSN URL for error reporting. Works with Sentry and GlitchTip.' }, }); -type ValueGetter = (e: React.ChangeEvent) => any; -type Template = ImmutableMap; -type ConfigPath = Array; +type ValueGetter = (e: React.ChangeEvent) => T2; +type StreamItemConfigPath = ['promoPanel', 'items'] | ['navlinks', 'homeFooter'] | ['cryptoAddresses']; type ThemeChangeHandler = (theme: string) => void; -const templates: Record = { - promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }), - footerItem: ImmutableMap({ title: '', url: '' }), - cryptoAddress: ImmutableMap({ ticker: '', address: '', note: '' }), -}; - -const PlFeConfig: React.FC = () => { +const PlFeConfigEditor: React.FC = () => { const intl = useIntl(); const dispatch = useAppDispatch(); @@ -75,26 +68,25 @@ const PlFeConfig: React.FC = () => { const initialData = useAppSelector(state => state.plfe); const [isLoading, setLoading] = useState(false); - const [data, setData] = useState(initialData); + const [data, setData] = useState(v.parse(plFeConfigSchema, initialData)); const [jsonEditorExpanded, setJsonEditorExpanded] = useState(false); const [rawJSON, setRawJSON] = useState(JSON.stringify(initialData, null, 2)); const [jsonValid, setJsonValid] = useState(true); const plFe = useMemo(() => v.parse(plFeConfigSchema, data), [data]); - const setConfig = (path: ConfigPath, value: any) => { - const newData = data.setIn(path, value); + const setConfig = (newData: PlFeConfig) => { setData(newData); setJsonValid(true); }; - const putConfig = (newData: any) => { + const putConfig = (newData: PlFeConfig) => { setData(newData); setJsonValid(true); }; const handleSubmit: React.FormEventHandler = (e) => { - dispatch(updatePlFeConfig(data.toJS())).then(() => { + dispatch(updatePlFeConfig(data)).then(() => { setLoading(false); toast.success(intl.formatMessage(messages.saved)); }).catch(() => { @@ -104,15 +96,20 @@ const PlFeConfig: React.FC = () => { e.preventDefault(); }; - const handleChange = (path: ConfigPath, getValue: ValueGetter): React.ChangeEventHandler => e => { - setConfig(path, getValue(e)); + const handleChange = (path: keyof PlFeConfig, getValue: ValueGetter): React.ChangeEventHandler => e => { + const newData: PlFeConfig = { ...data, [path]: getValue(e) }; + setConfig(newData); }; - const handleThemeChange = (path: ConfigPath): ThemeChangeHandler => theme => { - setConfig(path, theme); + const handleThemeChange: ThemeChangeHandler = (theme) => { + const newData = create(data, (draft) => { + if (!draft.defaultSettings) draft.defaultSettings = {}; + draft.defaultSettings.themeMode = theme; + }); + setConfig(newData); }; - const handleFileChange = (path: ConfigPath): React.ChangeEventHandler => e => { + const handleFileChange = (path: keyof PlFeConfig): React.ChangeEventHandler => e => { const file = e.target.files?.item(0); if (file) { @@ -122,19 +119,40 @@ const PlFeConfig: React.FC = () => { } }; - const handleStreamItemChange = (path: ConfigPath) => (values: any[]) => { - setConfig(path, ImmutableList(values)); + const handleStreamItemChange = (path: StreamItemConfigPath) => (values: any[]) => { + const newData = create(data, (draft) => { + if (path[0] === 'cryptoAddresses') { + draft.cryptoAddresses = values; + } else { + // @ts-ignore + draft[path[0]][path[1]] = values; + } + }); + setConfig(newData); }; - const addStreamItem = (path: ConfigPath, template: Template) => () => { - let items = data; - path.forEach(key => items = items?.[key] || []); - setConfig(path, items.push(template)); + const addStreamItem = (path: StreamItemConfigPath, schema: v.BaseSchema>) => () => { + const newData = create(data, (draft) => { + if (path[0] === 'cryptoAddresses') { + draft.cryptoAddresses.push(v.parse(cryptoAddressSchema, {})); + } else { + // @ts-ignore + draft[path[0]][path[1]].push(v.parse(schema, {})); + } + }); + setConfig(newData); }; - const deleteStreamItem = (path: ConfigPath) => (i: number) => { - const newData = data.deleteIn([...path, i]); - setData(newData); + const deleteStreamItem = (path: StreamItemConfigPath) => (i: number) => { + const newData = create(data, (draft) => { + if (path[0] === 'cryptoAddresses') { + draft.cryptoAddresses = draft.cryptoAddresses.filter((_, index) => index !== i); + } else { + // @ts-ignore + draft[path[0]][path[1]] = draft[path[0]][path[1]].filter((_, index) => index !== i); + } + }); + setConfig(newData); }; const handleEditJSON: React.ChangeEventHandler = e => { @@ -144,7 +162,7 @@ const PlFeConfig: React.FC = () => { const toggleJSONEditor = (expanded: boolean) => setJsonEditorExpanded(expanded); useEffect(() => { - putConfig(initialData); + putConfig(v.parse(plFeConfigSchema, initialData)); }, [initialData]); useEffect(() => { @@ -153,7 +171,7 @@ const PlFeConfig: React.FC = () => { useEffect(() => { try { - const data = fromJS(JSON.parse(rawJSON)); + const data = v.parse(plFeConfigSchema, JSON.parse(rawJSON)); putConfig(data); } catch { setJsonValid(false); @@ -171,7 +189,7 @@ const PlFeConfig: React.FC = () => { hintText={