diff --git a/backend/src/controllers/tools.ts b/backend/src/controllers/tools.ts index 9918542e..b7b456cd 100644 --- a/backend/src/controllers/tools.ts +++ b/backend/src/controllers/tools.ts @@ -2,6 +2,7 @@ import type { Request, Response } from 'express' import { PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3' import _ from 'underscore' import { + filterDeepProperties, getDefaultData, getS3AndParams, streamToString @@ -19,19 +20,22 @@ export const getDefault = async (_: Request, res: Response) => { } } -export const saveUserConfig = async (req: Request, res: Response) => { +export const createUserConfig = async (req: Request, res: Response) => { try { const data = req.body + const tag = data.version || data.tag if (!data.walletAddress) { throw 'Wallet address is required' } + const defaultData = await getDefaultData() + const defaultDataContent = JSON.parse(defaultData).default + defaultDataContent.walletAddress = decodeURIComponent( + `https://${data.walletAddress}` + ) const { s3, params } = getS3AndParams(data.walletAddress) - // get defaults, get existing config, then overwrite values - const defaultData = await getDefaultData() - let fileContentString = '{}' try { // existing config @@ -51,23 +55,53 @@ export const saveUserConfig = async (req: Request, res: Response) => { } } - const changedValues = _.omit(data, function (value, key) { - return defaultData[key] === value - }) + let currentData = JSON.parse(fileContentString) - const currentData = Object.assign( - JSON.parse(defaultData), - JSON.parse(fileContentString) - ) - const fileContent = JSON.stringify( - Object.assign(currentData, ...[changedValues]) - ) + if (currentData?.default) { + currentData = Object.assign(filterDeepProperties(currentData), { + [tag]: defaultDataContent + }) + } else { + currentData = Object.assign( + { default: currentData }, + { + [tag]: defaultDataContent + } + ) + } + + const fileContent = JSON.stringify(currentData) + const extendedParams = { ...params, Body: fileContent } + + // save json to file + await s3.send(new PutObjectCommand(extendedParams)) + + res.status(200).send(currentData) + } catch (error) { + console.log(error) + res.status(500).send('An error occurred when fetching data') + } +} + +export const saveUserConfig = async (req: Request, res: Response) => { + try { + const data = req.body + + if (!data.walletAddress) { + throw 'Wallet address is required' + } + + const { s3, params } = getS3AndParams(data.walletAddress) + // filter data so we are saving only config and none of the extra params received + const fullConfig = JSON.parse(data?.fullconfig) + const filteredData = filterDeepProperties(fullConfig) + const fileContent = JSON.stringify(filteredData) const extendedParams = { ...params, Body: fileContent } await s3.send(new PutObjectCommand(extendedParams)) - res.status(200).send(data) + res.status(200).send(filteredData) } catch (err) { console.log(err) res.status(500).send('An error occurred when saving data') @@ -92,10 +126,49 @@ export const getUserConfig = async (req: Request, res: Response) => { data.Body as NodeJS.ReadableStream ) - const fileContent = Object.assign( + let fileContent = Object.assign( JSON.parse(defaultData), ...[JSON.parse(fileContentString)] ) + fileContent = filterDeepProperties(fileContent) + + res.status(200).send(fileContent) + } catch (error) { + const err = error as Error + if (err.name === 'NoSuchKey') { + // file / config not found, serve default + const defaultData = await getDefaultData() + res.status(200).send(defaultData) + } else { + console.log(error) + res.status(500).send('An error occurred while fetching data') + } + } +} + +export const getUserConfigByTag = async (req: Request, res: Response) => { + try { + const id = req.params.id + const tag = req.params.tag ?? 'default' + + if (!id) { + throw new S3FileNotFoundError('Wallet address is required') + } + + // ensure we have all keys w default values, user config will overwrite values that exist in saved json + const defaultDataResp = await getDefaultData() + const defaultData = JSON.parse(defaultDataResp)?.default + + const { s3, params } = getS3AndParams(id) + const data = await s3.send(new GetObjectCommand(params)) + // Convert the file stream to a string + const fileContentString = await streamToString( + data.Body as NodeJS.ReadableStream + ) + + const userConfig = JSON.parse(fileContentString) + const selectedConfig = userConfig[tag] ?? defaultData + const fileContent = Object.assign(defaultData, ...[selectedConfig]) res.status(200).send(fileContent) } catch (error) { @@ -114,5 +187,6 @@ export const getUserConfig = async (req: Request, res: Response) => { export default { getDefault, getUserConfig, + createUserConfig, saveUserConfig } diff --git a/backend/src/data/default_config.json b/backend/src/data/default_config.json index c7371cac..ec355349 100644 --- a/backend/src/data/default_config.json +++ b/backend/src/data/default_config.json @@ -1,30 +1,32 @@ { - "buttonFontName": "Arial", - "buttonText": "Support me", - "buttonBorder": "Light", - "buttonTextColor": "#ffffff", - "buttonBackgroundColor": "#ff808c", - "bannerFontName": "Arial", - "bannerFontSize": 16, - "bannerTitleText": "How to support?", - "bannerDescriptionText": "You can support this page and my work by a one time donation or proportional to the time you spend on this website through web monetization.", - "bannerSlideAnimation": "Down", - "bannerPosition": "Bottom", - "bannerTextColor": "#ffffff", - "bannerBackgroundColor": "#7f76b2", - "bannerBorder": "Light", - "widgetFontName": "Arial", - "widgetFontSize": 16, - "widgetDonateAmount": 1, - "widgetTitleText": "Future of support", - "widgetDescriptionText": "Experience the new way to support our content. Activate Web Monetization in your browser and support our work as you browse. Every visit helps us keep creating the content you love! You can also support us by a one time donation below!", - "widgetButtonText": "Support me", - "widgetButtonBackgroundColor": "#4ec6c0", - "widgetButtonTextColor": "#000000", - "widgetButtonBorder": "Light", - "widgetTextColor": "#000000", - "widgetBackgroundColor": "#ffffff", - "widgetTriggerBackgroundColor": "#ffffff", - "widgetTriggerIcon": "", - "css": "H4sIAAAAAAAAA61S227bMAz9lW5-WYFQCFIkA2ygQL8koCy64UZJrkR1Ngz_-2BtaRds6NP4JJE6Fx7IsIw4n2108zLEoDCgZ5nbp8QouzxnJQ-FdxlDhkyJh0_sx5gUg3Z9lJjaZqjVread684W1RjMD68wxORRFa0Q_Govt0CL_ffnFEtw8HuAB-uc7WxMjhIkdFxyuzcPX4-JfKcJQ2blGFoU2ZtjJsx0q2_QxxIUHOdRcN79ORPeNTQphcwxwIhztXgnaEmWD3ZqLoROKOfCUCMQSDHqgiJtoldK2q3_IuYwFr0S72t98NA4zltWbvk7F6p1zf10OnVr81KiUsU_On41g9DUBr1Af2FxXx7ul60DjhP1NbI-SvGhW5vzOdCkbyBzgaGI1POmZ1jJZ-gpKCXzrWTlYb5exwmOG_Lz_eby1uQ1uv8iUNFe4bA3SpOCxUyLx_TMATSO7WH7EOtPZC6FXcYCAAA=" + "default": { + "buttonFontName": "Arial", + "buttonText": "Support me", + "buttonBorder": "Light", + "buttonTextColor": "#ffffff", + "buttonBackgroundColor": "#ff808c", + "bannerFontName": "Arial", + "bannerFontSize": 16, + "bannerTitleText": "How to support?", + "bannerDescriptionText": "You can support this page and my work by a one time donation or proportional to the time you spend on this website through web monetization.", + "bannerSlideAnimation": "Down", + "bannerPosition": "Bottom", + "bannerTextColor": "#ffffff", + "bannerBackgroundColor": "#7f76b2", + "bannerBorder": "Light", + "widgetFontName": "Arial", + "widgetFontSize": 16, + "widgetDonateAmount": 1, + "widgetTitleText": "Future of support", + "widgetDescriptionText": "Experience the new way to support our content. Activate Web Monetization in your browser and support our work as you browse. Every visit helps us keep creating the content you love! You can also support us by a one time donation below!", + "widgetButtonText": "Support me", + "widgetButtonBackgroundColor": "#4ec6c0", + "widgetButtonTextColor": "#000000", + "widgetButtonBorder": "Light", + "widgetTextColor": "#000000", + "widgetBackgroundColor": "#ffffff", + "widgetTriggerBackgroundColor": "#ffffff", + "widgetTriggerIcon": "", + "css": "H4sIAAAAAAAAA61S227bMAz9lW5-WYFQCFIkA2ygQL8koCy64UZJrkR1Ngz_-2BtaRds6NP4JJE6Fx7IsIw4n2108zLEoDCgZ5nbp8QouzxnJQ-FdxlDhkyJh0_sx5gUg3Z9lJjaZqjVread684W1RjMD68wxORRFa0Q_Govt0CL_ffnFEtw8HuAB-uc7WxMjhIkdFxyuzcPX4-JfKcJQ2blGFoU2ZtjJsx0q2_QxxIUHOdRcN79ORPeNTQphcwxwIhztXgnaEmWD3ZqLoROKOfCUCMQSDHqgiJtoldK2q3_IuYwFr0S72t98NA4zltWbvk7F6p1zf10OnVr81KiUsU_On41g9DUBr1Af2FxXx7ul60DjhP1NbI-SvGhW5vzOdCkbyBzgaGI1POmZ1jJZ-gpKCXzrWTlYb5exwmOG_Lz_eby1uQ1uv8iUNFe4bA3SpOCxUyLx_TMATSO7WH7EOtPZC6FXcYCAAA=" + } } diff --git a/backend/src/routes/tools.ts b/backend/src/routes/tools.ts index 5f3572eb..c9aec8ee 100644 --- a/backend/src/routes/tools.ts +++ b/backend/src/routes/tools.ts @@ -2,13 +2,17 @@ import type { Router } from 'express' import { getDefault, getUserConfig, - saveUserConfig + createUserConfig, + saveUserConfig, + getUserConfigByTag } from '../controllers/tools.js' const userRoutes = (router: Router) => { router.get('/tools/default', getDefault) router.get('/tools/:id', getUserConfig) - router.post('/tools', saveUserConfig) + router.get('/tools/:id/:tag', getUserConfigByTag) + router.post('/tools', createUserConfig) + router.put('/tools', saveUserConfig) return router } diff --git a/backend/src/services/utils.ts b/backend/src/services/utils.ts index 695753d3..25c10f91 100644 --- a/backend/src/services/utils.ts +++ b/backend/src/services/utils.ts @@ -21,7 +21,7 @@ export const getS3AndParams = (walletAddress: string) => { }) }) - const fileKey = `${walletAddress + const fileKey = `${decodeURIComponent(walletAddress) .replace('$', '') .replace('https://', '')}.json` @@ -54,3 +54,44 @@ export const streamToString = ( readableStream.on('error', reject) }) } + +// return only properties that are at least levelCount deep +/* eslint-disable @typescript-eslint/no-explicit-any */ +export const filterDeepProperties = ( + obj: Record, + levelCount: number = 2 +): Record => { + const result: Record = {} + + const traverse = ( + current: any, + path: string[], + parent: Record + ) => { + if (typeof current === 'object' && current !== null) { + for (const key in current) { + if (Object.prototype.hasOwnProperty.call(current, key)) { + const newPath = [...path, key] + + if (typeof current[key] === 'object' && current[key] !== null) { + // Ensure parent structure exists + if (path.length === 0) { + if (!result[key]) result[key] = {} + traverse(current[key], newPath, result[key]) + } else { + if (!parent[key]) parent[key] = {} + traverse(current[key], newPath, parent[key]) + } + } else if (path.length >= levelCount - 1) { + // Only keep properties that are at least levelCount levels deep + if (!result[path[0]]) result[path[0]] = {} + result[path[0]][key] = current[key] + } + } + } + } + } + + traverse(obj, [], result) + return result +} diff --git a/eslint.config.js b/eslint.config.js index 6242319b..26eb6a50 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -39,6 +39,14 @@ export default [ mjs: 'always', jsx: 'always' } + ], + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + } ] } }, diff --git a/frontend/app/components/Button.tsx b/frontend/app/components/Button.tsx index 92375c41..2dc5c4c2 100644 --- a/frontend/app/components/Button.tsx +++ b/frontend/app/components/Button.tsx @@ -13,6 +13,7 @@ const buttonStyles = cva( 'text-sm hover:from-wm-purple hover:to-[#7f7fff] hover:text-white hover:bg-gradient-to-r', danger: 'disabled:bg-red-200 bg-red-500 hover:bg-red-600 shadow-md text-white', + icon: 'enabled:hover:bg-gradient-to-r enabled:hover:from-wm-green enabled:hover:to-wm-green-fade aspect-square shadow-md max-w-[30px] disabled:bg-gray-200 disabled:hover:bg-gray-200!', invisible: 'px-1 border-none text-white' }, size: { diff --git a/frontend/app/components/PageHeader.tsx b/frontend/app/components/PageHeader.tsx index b02a945a..ab9171de 100644 --- a/frontend/app/components/PageHeader.tsx +++ b/frontend/app/components/PageHeader.tsx @@ -1,19 +1,29 @@ import { useNavigate } from '@remix-run/react' import { cx } from 'class-variance-authority' import { availableTools } from '~/lib/presets.js' -import { Button, InfoWithTooltip } from './index.js' +import { Button, InfoWithTooltip, Select, SelectOption } from './index.js' import { Chevron } from './icons.js' export const PageHeader = ({ elementType, title, link, - setImportModalOpen + setImportModalOpen, + setNewVersionModalOpen, + setConfirmModalOpen, + versionOptions, + selectedVersion, + setSelectedVersion }: { elementType: string | undefined title: string link: string setImportModalOpen: React.Dispatch> + setNewVersionModalOpen: React.Dispatch> + setConfirmModalOpen: React.Dispatch> + versionOptions: SelectOption[] + selectedVersion: string + setSelectedVersion: (value: string) => void }) => { const navigate = useNavigate() @@ -33,6 +43,37 @@ export const PageHeader = ({ +
+ { + const newValue = e.target.value + if (/^[a-zA-Z0-9-_ ]*$/.test(newValue)) { + setVersionName(newValue) + } + }} + withBorder + /> +
+ +
+ +
+ + + + + + + + + ) +} diff --git a/frontend/app/components/modals/Script.tsx b/frontend/app/components/modals/Script.tsx index 9e1ee25b..680903d8 100644 --- a/frontend/app/components/modals/Script.tsx +++ b/frontend/app/components/modals/Script.tsx @@ -10,6 +10,7 @@ type ScriptModalProps = { tooltip?: string defaultType?: string scriptForDisplay: string + selectedVersion: string isOpen: boolean onClose: () => void } @@ -21,6 +22,7 @@ export const ScriptModal = ({ tooltip, defaultType, scriptForDisplay, + selectedVersion, isOpen, onClose }: ScriptModalProps) => { @@ -37,9 +39,11 @@ export const ScriptModal = ({ } useEffect(() => { - const script = scriptForDisplay.replace('[elements]', types.join('|')) + const script = scriptForDisplay + .replace('[elements]', types.join('|')) + .replace('[version]', selectedVersion) setProcessedScript(script) - }, [types, scriptForDisplay]) + }, [types, scriptForDisplay, selectedVersion]) return ( diff --git a/frontend/app/components/modals/index.tsx b/frontend/app/components/modals/index.tsx index 2f9b8007..ee2f91ea 100644 --- a/frontend/app/components/modals/index.tsx +++ b/frontend/app/components/modals/index.tsx @@ -1,2 +1,5 @@ +export * from './Confirm.js' export * from './Import.js' +export * from './Info.js' +export * from './NewVersion.js' export * from './Script.js' diff --git a/frontend/app/lib/apiClient.ts b/frontend/app/lib/apiClient.ts index 38db1942..08c0752b 100644 --- a/frontend/app/lib/apiClient.ts +++ b/frontend/app/lib/apiClient.ts @@ -8,6 +8,7 @@ export type ApiResponse = { readonly payload?: T readonly isFailure: false | true readonly errors?: Array + newversion?: false | string } const isProd = import.meta.env.PROD @@ -70,10 +71,40 @@ export class ApiClient { } } + public static async createUserConfig( + version: string, + walletAddress: string + ): Promise { + const tag = encodeURIComponent(version) + const wa = encodeURIComponent( + walletAddress.replace('$', '').replace('https://', '') + ) + const response = await axios.post( + `${apiUrl}tools`, + { walletAddress: wa, tag }, + { + httpsAgent + } + ) + + if (response.status === 200) { + return { + isFailure: false, + payload: response.data + } + } else { + return { + errors: [`status ${response.status}: ${response.statusText}`], + isFailure: true, + newversion: false + } + } + } + public static async saveUserConfig( configData: Partial ): Promise { - const response = await axios.post(`${apiUrl}tools`, configData, { + const response = await axios.put(`${apiUrl}tools`, configData, { httpsAgent }) diff --git a/frontend/app/lib/utils.ts b/frontend/app/lib/utils.ts index 4ee0b533..4dc6fe7f 100644 --- a/frontend/app/lib/utils.ts +++ b/frontend/app/lib/utils.ts @@ -319,3 +319,7 @@ export const getWebMonetizationLink = () => { return `Learn more here.` } } + +export const capitalizeFirstLetter = (string: string): string => { + return string.charAt(0).toUpperCase() + string.slice(1) +} diff --git a/frontend/app/lib/validate.server.ts b/frontend/app/lib/validate.server.ts index a2acb4a4..537089d0 100644 --- a/frontend/app/lib/validate.server.ts +++ b/frontend/app/lib/validate.server.ts @@ -7,6 +7,15 @@ export const walletSchema = z.object({ walletAddress: z.string().min(1, { message: 'Wallet address is required' }) }) +export const versionSchema = z.object({ + version: z.string().min(1, { message: 'Version is required' }) +}) + +// need a better definition & validation for this +export const fullConfigSchema = z.object({ + fullconfig: z.string().min(1, { message: 'Unknown error' }) +}) + export const createButtonSchema = z .object({ elementType: z.literal('button'), @@ -17,6 +26,7 @@ export const createButtonSchema = z buttonBackgroundColor: z.string().min(6) }) .merge(walletSchema) + .merge(versionSchema) export const createBannerSchema = z .object({ @@ -34,6 +44,7 @@ export const createBannerSchema = z bannerBorder: z.nativeEnum(CornerType) }) .merge(walletSchema) + .merge(versionSchema) export const createWidgetSchema = z .object({ @@ -51,6 +62,7 @@ export const createWidgetSchema = z widgetTriggerIcon: z.string().optional() }) .merge(walletSchema) + .merge(versionSchema) export const getElementSchema = (type: string) => { switch (type) { diff --git a/frontend/app/routes/create.$type.tsx b/frontend/app/routes/create.$type.tsx index b1124472..ece65164 100644 --- a/frontend/app/routes/create.$type.tsx +++ b/frontend/app/routes/create.$type.tsx @@ -3,14 +3,22 @@ import { Form, useActionData, useLoaderData, - useNavigation + useNavigation, + useSubmit } from '@remix-run/react' import { useEffect, useState } from 'react' -import { ImportModal, ScriptModal } from '~/components/modals/index.js' +import { + ConfirmModal, + ImportModal, + InfoModal, + NewVersionModal, + ScriptModal +} from '~/components/modals/index.js' import { ErrorPanel, NotFoundConfig, PageHeader, + SelectOption, ToolConfig, ToolPreview } from '~/components/index.js' @@ -19,11 +27,17 @@ import { type Message, messageStorage } from '~/lib/message.server.js' import { validConfigTypes } from '~/lib/presets.js' import { tooltips } from '~/lib/tooltips.js' import { ElementConfigType, ElementErrors } from '~/lib/types.js' -import { encodeAndCompressParameters, getIlpayCss } from '~/lib/utils.js' +import { + capitalizeFirstLetter, + encodeAndCompressParameters, + getIlpayCss +} from '~/lib/utils.js' import { createBannerSchema, createButtonSchema, createWidgetSchema, + fullConfigSchema, + versionSchema, walletSchema } from '~/lib/validate.server' @@ -37,7 +51,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { // get default config const apiResponse: ApiResponse = await ApiClient.getDefaultConfig() - const defaultConfig: ElementConfigType = apiResponse?.payload + const defaultConfig: ElementConfigType = apiResponse?.payload?.default const ilpayUrl = process.env.ILPAY_URL || '' const toolsUrl = process.env.FRONTEND_URL || '' @@ -54,41 +68,130 @@ export default function Create() { const [openWidget, setOpenWidget] = useState(false) const [toolConfig, setToolConfig] = useState(defaultConfig) + const [fullConfig, setFullConfig] = + useState>() const [modalOpen, setModalOpen] = useState(false) const [importModalOpen, setImportModalOpen] = useState(false) + const [newVersionModalOpen, setNewVersionModalOpen] = useState(false) + const [infoModalOpen, setInfoModalOpen] = useState(false) + const [confirmModalOpen, setConfirmModalOpen] = useState(false) + const [selectedVersion, setSelectedVersion] = useState('default') + const [versionOptions, setVersionOptions] = useState([ + { label: 'Default', value: 'default' } + ]) const wa = (toolConfig?.walletAddress || '') .replace('$', '') .replace('https://', '') - const scriptToDisplay = `` + const scriptToDisplay = `` + const submitForm = useSubmit() + + const onConfirm = () => { + if (fullConfig) { + const { [selectedVersion]: _, ...rest } = fullConfig + setFullConfig(rest) + const config = fullConfig['default'] + setToolConfig(config) + + const filteredOptions = versionOptions.filter( + (ver) => ver.value != selectedVersion + ) + setVersionOptions(filteredOptions) + setSelectedVersion('default') + setConfirmModalOpen(false) + + const formData = new FormData() + formData.append('intent', 'remove') + formData.append('version', 'default') + formData.append('fullconfig', JSON.stringify(rest)) + const defaultSet = rest.default as unknown as Record + Object.keys(defaultSet).map((key) => { + formData.append(key, defaultSet[key]) + }) + submitForm(formData, { method: 'post' }) + } + } useEffect(() => { const errors = Object.keys(response?.errors?.fieldErrors || {}) if (response && !errors.length && response.displayScript) { setModalOpen(true) + } else if ( + response && + response.apiResponse && + response.apiResponse.newversion + ) { + const versionLabels = Object.keys(response.apiResponse?.payload).map( + (key) => { + return { + label: capitalizeFirstLetter(key.replaceAll('-', ' ')), + value: key + } + } + ) + setVersionOptions(versionLabels) + setFullConfig(response.apiResponse.payload) + + const selVersion = response.apiResponse.newversion + setSelectedVersion(selVersion) + setNewVersionModalOpen(false) + setInfoModalOpen(true) } else if ( response && response.apiResponse && response.apiResponse.isFailure == false ) { - const config = response.apiResponse.payload - setToolConfig(config) + const versionLabels = Object.keys(response.apiResponse?.payload).map( + (key) => { + return { + label: capitalizeFirstLetter(key.replaceAll('-', ' ')), + value: key + } + } + ) + setVersionOptions(versionLabels) + + setFullConfig(response.apiResponse.payload) + setSelectedVersion('default') setImportModalOpen(false) + if (response.intent != 'remove') { + setInfoModalOpen(true) + } } }, [response]) + useEffect(() => { + const updatedFullConfig = { + ...fullConfig, + [selectedVersion]: toolConfig + } + setFullConfig(updatedFullConfig) + }, [toolConfig]) + + useEffect(() => { + if (fullConfig) { + const config = fullConfig[selectedVersion] + setToolConfig(config) + } + }, [selectedVersion]) + return (
- {validConfigTypes.includes(String(elementType)) ? ( + {toolConfig && validConfigTypes.includes(String(elementType)) ? (
-
+
+ +
@@ -117,6 +226,7 @@ export default function Create() { )} + setNewVersionModalOpen(false)} + errors={response?.errors} + toolConfig={toolConfig} + setToolConfig={setToolConfig} + /> + ver.value).join(', ')} + isOpen={infoModalOpen} + onClose={() => setInfoModalOpen(false)} + /> + setConfirmModalOpen(false)} + onConfirm={onConfirm} + />
) } @@ -141,7 +272,7 @@ export async function action({ request, params }: ActionFunctionArgs) { const formData = Object.fromEntries(await request.formData()) const intent = formData?.intent - let apiResponse: ApiResponse = { isFailure: true } + let apiResponse: ApiResponse = { isFailure: true, newversion: false } let displayScript: boolean = false const errors: ElementErrors = { fieldErrors: {}, @@ -153,13 +284,36 @@ export async function action({ request, params }: ActionFunctionArgs) { if (!result.success) { errors.fieldErrors = result.error.flatten().fieldErrors - return json({ errors, apiResponse, displayScript }, { status: 400 }) + return json( + { errors, apiResponse, displayScript, intent }, + { status: 400 } + ) } const payload = result.data apiResponse = await ApiClient.getUserConfig(payload.walletAddress) - return json({ errors, apiResponse, displayScript }, { status: 200 }) + return json({ errors, apiResponse, displayScript, intent }, { status: 200 }) + } else if (intent == 'newversion') { + const result = versionSchema.merge(walletSchema).safeParse(formData) + + if (!result.success) { + errors.fieldErrors = result.error.flatten().fieldErrors + return json( + { errors, apiResponse, displayScript, intent }, + { status: 400 } + ) + } + + const payload = result.data + const versionName = payload.version.replaceAll(' ', '-') + apiResponse = await ApiClient.createUserConfig( + versionName, + payload.walletAddress + ) + apiResponse.newversion = versionName + + return json({ errors, apiResponse, displayScript, intent }, { status: 200 }) } else { let currentSchema @@ -174,13 +328,16 @@ export async function action({ request, params }: ActionFunctionArgs) { default: currentSchema = createBannerSchema } - const result = currentSchema.safeParse( - Object.assign(formData, { ...{ elementType } }) - ) + const result = currentSchema + .merge(fullConfigSchema) + .safeParse(Object.assign(formData, { ...{ elementType } })) if (!result.success) { errors.fieldErrors = result.error.flatten().fieldErrors - return json({ errors, apiResponse, displayScript }, { status: 400 }) + return json( + { errors, apiResponse, displayScript, intent }, + { status: 400 } + ) } const payload = result.data @@ -194,8 +351,10 @@ export async function action({ request, params }: ActionFunctionArgs) { } apiResponse = await ApiClient.saveUserConfig(payload) - displayScript = true + if (intent != 'remove') { + displayScript = true + } - return json({ errors, apiResponse, displayScript }, { status: 200 }) + return json({ errors, apiResponse, displayScript, intent }, { status: 200 }) } } diff --git a/frontend/script/index.tsx b/frontend/script/index.tsx index 58564bbf..5b376aeb 100644 --- a/frontend/script/index.tsx +++ b/frontend/script/index.tsx @@ -3,7 +3,10 @@ const FRONTEND_URL = import.meta.env.VITE_SCRIPT_FRONTEND_URL const API_URL = import.meta.env.VITE_SCRIPT_API_URL const ILPAY_URL = import.meta.env.VITE_SCRIPT_ILPAY_URL -let paramTypes: string[] | undefined, paramWallet: string | undefined, urlWallet +let paramTypes: string[] | undefined, + paramWallet: string | undefined, + paramTag: string = 'default', + urlWallet // TODO: Have a defined interface for the config // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -17,6 +20,7 @@ if (currentScript) { const params = new URLSearchParams(scriptUrl.search) paramTypes = (params.get('types') || '').split('|') paramWallet = params.get('wa') || undefined + paramTag = params.get('tag') || 'default' urlWallet = encodeURIComponent(params.get('wa') || '') } @@ -25,7 +29,7 @@ if (!paramTypes || !paramWallet) { throw 'Missing parameters! Could not initialise WM Tools.' } -fetch(`${API_URL}tools/${urlWallet}`) +fetch(`${API_URL}tools/${urlWallet}/${paramTag}`) .then((response) => response.json()) .then((resp) => { const config = resp