From 0e6555cabe3a86aabf6bd24e6ace5a241568233f Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Wed, 19 Aug 2026 07:18:08 +0300 Subject: [PATCH 1/6] feat: Replace ProductUploadMedia with ReorderedMediaUpload component and update imports --- .../schemas/settings/maintenanceModeSchema.ts | 1 + .../(base)/(store)/products/[id]/page.tsx | 4 ++-- .../(base)/(store)/products/add/page.tsx | 4 ++-- ...loadMedia.tsx => ReorderedMediaUpload.tsx} | 24 ++++++++++++------- src/components/index.ts | 1 + src/components/products/index.ts | 1 - 6 files changed, 21 insertions(+), 14 deletions(-) rename src/components/{products/ProductUploadMedia.tsx => ReorderedMediaUpload.tsx} (83%) diff --git a/src/api/backend/schemas/settings/maintenanceModeSchema.ts b/src/api/backend/schemas/settings/maintenanceModeSchema.ts index 19d3144..b6f59e6 100644 --- a/src/api/backend/schemas/settings/maintenanceModeSchema.ts +++ b/src/api/backend/schemas/settings/maintenanceModeSchema.ts @@ -1,4 +1,5 @@ import z from 'zod'; + import { optionalLocalizedSchema } from '../localizedSchema'; export const maintenanceModeSchema = z.object({ diff --git a/src/app/(hydration)/(base)/(store)/products/[id]/page.tsx b/src/app/(hydration)/(base)/(store)/products/[id]/page.tsx index 148e85c..dc0013e 100644 --- a/src/app/(hydration)/(base)/(store)/products/[id]/page.tsx +++ b/src/app/(hydration)/(base)/(store)/products/[id]/page.tsx @@ -6,7 +6,7 @@ import { ProductForm, SelectProductRelations, ProductStatusSelectMenu, - ProductUploadMedia, + ReorderedMediaUpload, ProductVariants, LocalizedForm, ProductFormCTA, @@ -49,7 +49,7 @@ export default async function EditProduct(props: PagePropsWithParams<{ id: strin name="description" type="richtext" /> - + diff --git a/src/app/(hydration)/(base)/(store)/products/add/page.tsx b/src/app/(hydration)/(base)/(store)/products/add/page.tsx index db944dc..3b70816 100644 --- a/src/app/(hydration)/(base)/(store)/products/add/page.tsx +++ b/src/app/(hydration)/(base)/(store)/products/add/page.tsx @@ -5,7 +5,7 @@ import { ProductForm, SelectProductRelations, ProductStatusSelectMenu, - ProductUploadMedia, + ReorderedMediaUpload, ProductVariants, LocalizedForm, ProductFormCTA, @@ -34,7 +34,7 @@ export default async function AddProduct() { name="description" type="richtext" /> - + diff --git a/src/components/products/ProductUploadMedia.tsx b/src/components/ReorderedMediaUpload.tsx similarity index 83% rename from src/components/products/ProductUploadMedia.tsx rename to src/components/ReorderedMediaUpload.tsx index 2fdab6c..cb38731 100644 --- a/src/components/products/ProductUploadMedia.tsx +++ b/src/components/ReorderedMediaUpload.tsx @@ -5,7 +5,7 @@ import { HugeiconsIcon } from '@hugeicons/react'; import { DragDropVerticalIcon, Plus, X } from '@hugeicons/core-free-icons'; import { rectSortingStrategy } from '@dnd-kit/sortable'; -import { ProductSchema, MediaByFolder } from '@/api'; +import { MediaByFolder, MediaFolder } from '@/api'; import { Button, Field, @@ -16,13 +16,19 @@ import { type ReorderableItemProps, } from '@/components'; import { useDisclosure } from '@/hooks'; +import { cn } from '@/lib/utils'; -export const ProductUploadMedia = () => { - const form = useFormContext(); +interface ReorderedMediaUploadProps { + folder: F; + className?: string; +} + +export const ReorderedMediaUpload = ({ folder, className }: ReorderedMediaUploadProps) => { + const form = useFormContext<{ media: MediaByFolder[] }>(); const uploadMediaDisclosure = useDisclosure(); return ( -
+

Media

@@ -64,7 +70,7 @@ export const ProductUploadMedia = () => { state.setItems([...state.items, ...newMedia])} {...uploadMediaDisclosure} /> @@ -77,20 +83,20 @@ export const ProductUploadMedia = () => { ); }; -interface MediaCardProps extends MediaByFolder<'product'>, ReorderableItemProps { +interface MediaCardProps extends MediaByFolder, ReorderableItemProps { onRemove: () => void; } -const MediaCard = ({ +const MediaCard = ({ sortable: { containerProps, buttonProps: sortButtonProps }, onRemove, ...media -}: MediaCardProps) => { +}: MediaCardProps) => { return (
  • diff --git a/src/components/index.ts b/src/components/index.ts index 72c1215..a68e340 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -36,3 +36,4 @@ export * from './DateTimeSelector'; export * from './discounts'; export * from './StatusBadge'; export * from './NumberInput'; +export * from './ReorderedMediaUpload'; diff --git a/src/components/products/index.ts b/src/components/products/index.ts index ad124ae..598e885 100644 --- a/src/components/products/index.ts +++ b/src/components/products/index.ts @@ -2,7 +2,6 @@ export * from './ProductsList'; export * from './ProductsCTA'; export * from './productColumns'; export * from './ProductForm'; -export * from './ProductUploadMedia'; export * from './ProductVariants'; export * from './ProductStatusSelectMenu'; export * from './SelectProductRelations'; From f3ba8c24b7eb1cc2b23d815a3cd9b21bdb5908a8 Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Wed, 19 Aug 2026 07:18:47 +0300 Subject: [PATCH 2/6] feat: Enhance useSetAppSettingsMutation to include mediaIds in the mutation payload --- src/hooks/settings/useSetAppSettingsMutation.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/hooks/settings/useSetAppSettingsMutation.ts b/src/hooks/settings/useSetAppSettingsMutation.ts index 63fb164..d3505d3 100644 --- a/src/hooks/settings/useSetAppSettingsMutation.ts +++ b/src/hooks/settings/useSetAppSettingsMutation.ts @@ -16,6 +16,10 @@ import { useErrorHandler } from '../useErrorHandler'; import { useInvalidateAppSettingsQuery } from './useAppSettingsQuery'; import { useSetAppSettingData } from './useAppSettingQuery'; +interface AppSettingsSchemaWithMediaIds extends Omit { + mediaIds: number[]; +} + export const useSetAppSettingsMutation = (setting: S) => { const errorHandler = useErrorHandler(); @@ -29,7 +33,10 @@ export const useSetAppSettingsMutation = (setting: S) >({ mutationFn: (data) => gamersCoreAdmin - .put>(`/settings/${setting}`, data) + .put>(`/settings/${setting}`, { + ...data, + mediaIds: 'media' in data && data.media?.length ? data.media.map(({ id }) => id) : undefined, + }) .then((res) => res.data) .catch((err: AxiosError) => { throw errorHandler(err); From 8b0c8e0b37c98c9bdc325e3cb5cc6506708c5cfa Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Wed, 19 Aug 2026 07:25:26 +0300 Subject: [PATCH 3/6] feat: Add announcement settings and component for managing user announcements --- src/api/backend/const.ts | 3 +- .../schemas/settings/announcementSchema.ts | 13 ++ src/api/backend/schemas/settings/index.ts | 2 + src/api/backend/types.ts | 7 + src/app/(hydration)/(base)/settings/page.tsx | 4 +- src/components/settings/Announcement.tsx | 220 ++++++++++++++++++ src/components/settings/index.ts | 1 + 7 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 src/api/backend/schemas/settings/announcementSchema.ts create mode 100644 src/components/settings/Announcement.tsx diff --git a/src/api/backend/const.ts b/src/api/backend/const.ts index 1f4ce04..5c3b7c1 100644 --- a/src/api/backend/const.ts +++ b/src/api/backend/const.ts @@ -11,13 +11,14 @@ export const localeDir: Record = { export const mediaTypes = ['image', 'video', 'audio', 'raw'] as const; -export const mediaFolders = ['product', 'variant', 'brand', 'userReview'] as const; +export const mediaFolders = ['product', 'variant', 'brand', 'userReview', 'announcement'] as const; export const mediaFoldersTypeMap = { product: 'all', variant: 'image', brand: 'image', userReview: 'image', + announcement: 'all', } as const satisfies Record; export const mediaRawFormats = ['.pdf', '.csv', '.txt', '.zip', '.doc', '.docx', '.xls', '.xlsx'] as const; diff --git a/src/api/backend/schemas/settings/announcementSchema.ts b/src/api/backend/schemas/settings/announcementSchema.ts new file mode 100644 index 0000000..7dc2450 --- /dev/null +++ b/src/api/backend/schemas/settings/announcementSchema.ts @@ -0,0 +1,13 @@ +import z from 'zod'; + +import { optionalLocalizedSchema } from '../localizedSchema'; +import { MediaByFolder } from '../../types'; + +export const announcementSchema = z.object({ + enabled: z.boolean(), + message: optionalLocalizedSchema.optional(), + media: z.array(z.custom>()).optional(), + disableAt: z.string().nullable().optional(), +}); + +export type AnnouncementSchema = z.infer; diff --git a/src/api/backend/schemas/settings/index.ts b/src/api/backend/schemas/settings/index.ts index f7a20a9..36434bd 100644 --- a/src/api/backend/schemas/settings/index.ts +++ b/src/api/backend/schemas/settings/index.ts @@ -2,9 +2,11 @@ import z, { ZodObject } from 'zod'; import { AppSettingsKey } from '@/api'; import { maintenanceModeSchema } from './maintenanceModeSchema'; +import { announcementSchema } from './announcementSchema'; export const appSettingsSchemas = { maintenanceMode: maintenanceModeSchema, + announcement: announcementSchema, } as const satisfies Record; export type AppSettingsSchemas = { diff --git a/src/api/backend/types.ts b/src/api/backend/types.ts index a9f1930..a38ec53 100644 --- a/src/api/backend/types.ts +++ b/src/api/backend/types.ts @@ -277,6 +277,13 @@ export interface AppSettings { disableOnCountdownEnd?: boolean; countdown?: string; }; + announcement: { + enabled: boolean; + message: Localized; + mediaIds: number[]; + media?: Media<'image' | 'video'>[]; + disableAt?: string; + }; } export type AppSettingsKey = keyof AppSettings; diff --git a/src/app/(hydration)/(base)/settings/page.tsx b/src/app/(hydration)/(base)/settings/page.tsx index e5adafe..bb801b0 100644 --- a/src/app/(hydration)/(base)/settings/page.tsx +++ b/src/app/(hydration)/(base)/settings/page.tsx @@ -2,7 +2,7 @@ import { Metadata } from 'next'; import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'; import { useAppSettingsQuery } from '@/hooks'; -import { MaintenanceMode } from '@/components'; +import { Announcement, MaintenanceMode } from '@/components'; export const metadata: Metadata = { title: 'Gamers Core | Settings' }; @@ -14,6 +14,8 @@ export default async function Settings() { return ( + + ); } diff --git a/src/components/settings/Announcement.tsx b/src/components/settings/Announcement.tsx new file mode 100644 index 0000000..ffcb130 --- /dev/null +++ b/src/components/settings/Announcement.tsx @@ -0,0 +1,220 @@ +'use client'; + +import { Controller, SubmitHandler, useForm, useFormState } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useEffect } from 'react'; +import { toast } from 'sonner'; + +import { AppSettings, appSettingsSchemas, AppSettingsSchemas, localeDir, locales } from '@/api'; +import { Disclosure, useAppSettingQuery, useDisclosure, useFormatDate, useSetAppSettingsMutation } from '@/hooks'; +import { cn } from '@/lib/utils'; + +import { Button } from '../Button'; +import { Modal, ModalFooter } from '../Modal'; +import { Form } from '../Form'; +import { Field, FieldError, FieldLabel, FieldTitle, Switch } from '../ui'; +import { LocalizedForm } from '../LocalizedForm'; +import { DateTimeSelector } from '../DateTimeSelector'; +import { ReorderedMediaUpload } from '../ReorderedMediaUpload'; +import { HTMLRender } from '../HTMLRender'; + +export const Announcement = (props: AppSettings['announcement']) => { + const disclosure = useDisclosure(); + const formatDate = useFormatDate(); + + const announcementQuery = useAppSettingQuery('announcement'); + + const data = announcementQuery.data ?? props; + + return ( + <> +
    +
    +
    +
    +

    Announcement

    + +

    {data.enabled ? 'Enabled' : 'Disabled'}

    +
    + + +
    + + {data.enabled && ( +
    +
    + {locales.map( + (locale) => + data.message[locale] && ( +
    +

    {locale}

    + +
    + +
    +
    + ), + )} +
    + + {data.disableAt && ( +
    +
    +

    Disable at

    + +

    {formatDate(data.disableAt, "MMM d, yyyy 'at' h:mma")}

    +
    +
    + )} +
    + )} +
    +
    + + + + ); +}; + +const AnnouncementMessageHTML = HTMLRender('AnnouncementMessage'); + +interface AnnouncementModalProps extends Disclosure { + announcement: AppSettingsSchemas['announcement']; +} + +const defaultValues: AppSettingsSchemas['announcement'] = { + enabled: false, + message: { + en: '', + ar: '', + }, + disableAt: null, +}; + +export const AnnouncementModal = ({ announcement, ...disclosure }: AnnouncementModalProps) => { + const form = useForm({ + defaultValues, + resolver: zodResolver(appSettingsSchemas.announcement), + mode: 'onChange', + }); + const { isDirty } = useFormState({ control: form.control }); + + const setAnnouncementMutation = useSetAppSettingsMutation('announcement'); + + const onOpenChange = (open: boolean) => { + if (setAnnouncementMutation.isPending) return; + + if (!open) { + form.reset(defaultValues); + setAnnouncementMutation.reset(); + } + + disclosure.onOpenChange(open); + }; + + useEffect(() => { + if (!disclosure.open) return; + + form.reset(announcement); + }, [disclosure.open, announcement, form]); + + const onSubmit: SubmitHandler = async (data) => { + if (setAnnouncementMutation.isPending) return; + + await setAnnouncementMutation.mutateAsync( + { ...data, message: data.message }, + { + onSuccess: () => { + toast.success('Announcement configuration updated successfully.'); + + onOpenChange(false); + }, + onError: (validationErrors) => { + if (!validationErrors) return; + + validationErrors.errors.forEach((error) => { + form.setError(error.property, { message: error.messages[0] }); + }); + + toast.error('Please review the form and fix the errors before submitting again.'); + }, + }, + ); + }; + + const enabled = form.watch('enabled'); + + return ( + +
    + ( + + + Enable announcement + + + + + )} + /> + + {enabled && ( + <> + + +
    + name="message" type="richtext" /> + + +
    + + ( + <> + { + if (!date) return form.setValue('disableAt', null, { shouldValidate: true }); + + field.onChange(date.toISOString()); + }} + /> + + + + )} + /> + + )} + + + + + +
    + ); +}; diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index 7edd9ed..0073abf 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -1 +1,2 @@ export * from './MaintenanceMode'; +export * from './Announcement'; From 0a15c7a20827982a2653dc404b14b6b114ee83b1 Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Wed, 19 Aug 2026 08:42:44 +0300 Subject: [PATCH 4/6] feat: Add intervalHours field to announcement settings and update UI for interval input --- .../schemas/settings/announcementSchema.ts | 1 + src/api/backend/types.ts | 1 + src/components/settings/Announcement.tsx | 43 ++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/api/backend/schemas/settings/announcementSchema.ts b/src/api/backend/schemas/settings/announcementSchema.ts index 7dc2450..4e6ef42 100644 --- a/src/api/backend/schemas/settings/announcementSchema.ts +++ b/src/api/backend/schemas/settings/announcementSchema.ts @@ -8,6 +8,7 @@ export const announcementSchema = z.object({ message: optionalLocalizedSchema.optional(), media: z.array(z.custom>()).optional(), disableAt: z.string().nullable().optional(), + intervalHours: z.number().int().min(0).optional(), }); export type AnnouncementSchema = z.infer; diff --git a/src/api/backend/types.ts b/src/api/backend/types.ts index a38ec53..867c8d9 100644 --- a/src/api/backend/types.ts +++ b/src/api/backend/types.ts @@ -281,6 +281,7 @@ export interface AppSettings { enabled: boolean; message: Localized; mediaIds: number[]; + intervalHours: number; media?: Media<'image' | 'video'>[]; disableAt?: string; }; diff --git a/src/components/settings/Announcement.tsx b/src/components/settings/Announcement.tsx index ffcb130..e2d7501 100644 --- a/src/components/settings/Announcement.tsx +++ b/src/components/settings/Announcement.tsx @@ -12,7 +12,7 @@ import { cn } from '@/lib/utils'; import { Button } from '../Button'; import { Modal, ModalFooter } from '../Modal'; import { Form } from '../Form'; -import { Field, FieldError, FieldLabel, FieldTitle, Switch } from '../ui'; +import { Field, FieldError, FieldLabel, FieldTitle, Input, Switch } from '../ui'; import { LocalizedForm } from '../LocalizedForm'; import { DateTimeSelector } from '../DateTimeSelector'; import { ReorderedMediaUpload } from '../ReorderedMediaUpload'; @@ -72,6 +72,16 @@ export const Announcement = (props: AppSettings['announcement']) => {
  • )} + + {!!data.intervalHours && ( +
    +
    +

    Show every

    + +

    {data.intervalHours} hours

    +
    +
    + )} )} @@ -95,6 +105,7 @@ const defaultValues: AppSettingsSchemas['announcement'] = { ar: '', }, disableAt: null, + intervalHours: undefined, }; export const AnnouncementModal = ({ announcement, ...disclosure }: AnnouncementModalProps) => { @@ -200,6 +211,36 @@ export const AnnouncementModal = ({ announcement, ...disclosure }: AnnouncementM )} /> + + ( + + + Show every (hours) + + { + const value = e.target.value; + + if (!value) return form.setValue('intervalHours', undefined, { shouldValidate: true }); + + field.onChange(Number(value)); + }} + placeholder="Enter interval in hours" + /> + + + + + )} + /> )} From ba4384624dcdcf57f6e877e2465823eae0bf4dbb Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Wed, 19 Aug 2026 10:17:36 +0300 Subject: [PATCH 5/6] feat: Update mediaIds type to be optional in AppSettingsSchemaWithMediaIds --- src/hooks/settings/useSetAppSettingsMutation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/settings/useSetAppSettingsMutation.ts b/src/hooks/settings/useSetAppSettingsMutation.ts index d3505d3..eda0ebe 100644 --- a/src/hooks/settings/useSetAppSettingsMutation.ts +++ b/src/hooks/settings/useSetAppSettingsMutation.ts @@ -17,7 +17,7 @@ import { useInvalidateAppSettingsQuery } from './useAppSettingsQuery'; import { useSetAppSettingData } from './useAppSettingQuery'; interface AppSettingsSchemaWithMediaIds extends Omit { - mediaIds: number[]; + mediaIds: number[] | undefined; } export const useSetAppSettingsMutation = (setting: S) => { @@ -33,7 +33,7 @@ export const useSetAppSettingsMutation = (setting: S) >({ mutationFn: (data) => gamersCoreAdmin - .put>(`/settings/${setting}`, { + .put, AppSettingsSchemaWithMediaIds>(`/settings/${setting}`, { ...data, mediaIds: 'media' in data && data.media?.length ? data.media.map(({ id }) => id) : undefined, }) From fbfb914a2ee2bed6d135e3819370b5f27f0d481c Mon Sep 17 00:00:00 2001 From: OmarLawaty Date: Wed, 19 Aug 2026 10:25:04 +0300 Subject: [PATCH 6/6] feat: Add DOMPurify for sanitizing HTML in HTMLRender component and update package dependencies --- package-lock.json | 45 ++++++++++++++--------------------- package.json | 1 + src/api/backend/types.ts | 2 +- src/components/HTMLRender.tsx | 3 ++- 4 files changed, 22 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index e0b48f7..33cafa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.4.0", + "dompurify": "^3.4.13", "holy-loader": "^2.3.14", "husky": "^9.1.7", "input-otp": "^1.4.2", @@ -100,7 +101,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -501,7 +501,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -686,7 +685,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -745,7 +743,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -757,7 +754,6 @@ "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -931,7 +927,6 @@ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", - "peer": true, "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" @@ -1802,7 +1797,6 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -3724,7 +3718,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.101.0" }, @@ -3791,7 +3784,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.26.0.tgz", "integrity": "sha512-7jTed/RirIVsp+lLdLvGzGqF3EBGpnGHGYKOwz6t28V2BIJLAFdUhfEVdWie7xPxQNWK0TP+fPlsqZS0vxfHBg==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4014,7 +4006,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.26.0.tgz", "integrity": "sha512-EM8woyHDNKLEQ+lWUEoDtA4KrwP6fei/mYX1NxseMzKHHo7LFecx7wk6sovAXZrUvdML/yFBihgiMiO5VIsfkg==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4159,7 +4150,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.26.0.tgz", "integrity": "sha512-4wajuqnO2X0+LVvsBjW/xk3/tmdb16bNL939QhicAay4YYqXITeV2v3XJsryzmG4L5GkK1yLxvRGk4aLoxWrnA==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" @@ -4174,7 +4164,6 @@ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.26.0.tgz", "integrity": "sha512-q4RDeWwVrhOL0jJCGRgGxLSdjOYwzQ4h2InURZVhC66433ipcHd6f3bqSOhcXZ4r0sFmMNsuF7aZmUntjWLc7w==", "license": "MIT", - "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-commands": "^1.6.2", @@ -4380,7 +4369,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4390,11 +4378,17 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -4452,7 +4446,6 @@ "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", @@ -5071,7 +5064,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5547,7 +5539,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -6168,6 +6159,15 @@ "node": ">=0.10.0" } }, + "node_modules/dompurify": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -6491,7 +6491,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7714,7 +7713,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -10269,7 +10267,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10305,7 +10302,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10318,7 +10314,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.78.0.tgz", "integrity": "sha512-EEZqc+N23moyzTlz61Pj+JvcXo76ICkpfOZo8JZw+sM4+wLQGh6nI2Ms+PdMOYNluFu0ghlM7B8mCzhRYtJCnA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -11461,8 +11456,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.3", @@ -11525,7 +11519,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11771,7 +11764,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12229,7 +12221,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 91c3722..ecac1e1 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.4.0", + "dompurify": "^3.4.13", "holy-loader": "^2.3.14", "husky": "^9.1.7", "input-otp": "^1.4.2", diff --git a/src/api/backend/types.ts b/src/api/backend/types.ts index 867c8d9..2707d84 100644 --- a/src/api/backend/types.ts +++ b/src/api/backend/types.ts @@ -283,7 +283,7 @@ export interface AppSettings { mediaIds: number[]; intervalHours: number; media?: Media<'image' | 'video'>[]; - disableAt?: string; + disableAt?: string | null; }; } diff --git a/src/components/HTMLRender.tsx b/src/components/HTMLRender.tsx index 17129f5..bfd1bc9 100644 --- a/src/components/HTMLRender.tsx +++ b/src/components/HTMLRender.tsx @@ -1,4 +1,5 @@ import { memo } from 'react'; +import DOMPurify from 'dompurify'; import { cn } from '@/lib/utils'; @@ -10,7 +11,7 @@ interface HTMLRenderProps { export const HTMLRender = (displayName: string) => { const Component = memo(({ html, className, ...props }: HTMLRenderProps) => (