From 1e07a245c514536ab4140a022f875a014e14404a Mon Sep 17 00:00:00 2001 From: Zhang Minghan Date: Fri, 19 Jan 2024 14:53:34 +0800 Subject: [PATCH] feat: support site announcement (#49) --- app/package.json | 1 + app/pnpm-lock.yaml | 143 +++++++++++++++++++++++- app/src/admin/api/info.ts | 5 +- app/src/components/Markdown.tsx | 27 +++-- app/src/components/app/Announcement.tsx | 54 +++++++++ app/src/components/app/AppProvider.tsx | 2 + app/src/conf/env.ts | 20 +++- app/src/events/announcement.ts | 5 + app/src/resources/i18n/cn.json | 2 + app/src/resources/i18n/en.json | 4 +- app/src/resources/i18n/ja.json | 4 +- app/src/resources/i18n/ru.json | 4 +- app/src/routes/admin/System.tsx | 1 + 13 files changed, 256 insertions(+), 16 deletions(-) create mode 100644 app/src/components/app/Announcement.tsx create mode 100644 app/src/events/announcement.ts diff --git a/app/package.json b/app/package.json index 4ff083d..95450c7 100644 --- a/app/package.json +++ b/app/package.json @@ -53,6 +53,7 @@ "react-router-dom": "^6.17.0", "react-syntax-highlighter": "^15.5.0", "rehype-katex": "^6.0.3", + "rehype-raw": "^7.0.0", "remark-breaks": "^4.0.0", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 86667a1..62298d6 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -128,6 +128,9 @@ dependencies: rehype-katex: specifier: ^6.0.3 version: 6.0.3 + rehype-raw: + specifier: ^7.0.0 + version: 7.0.0 remark-breaks: specifier: ^4.0.0 version: 4.0.0 @@ -2150,6 +2153,12 @@ packages: '@types/unist': 2.0.9 dev: false + /@types/hast@3.0.3: + resolution: {integrity: sha512-2fYGlaDy/qyLlhidX42wAH0KBi2TCjKMH8CHmBXgRlJ3Y+OXTiqsPQ6IWarZKwF1JoUcAJdPogv1d4b0COTpmQ==} + dependencies: + '@types/unist': 3.0.2 + dev: false + /@types/hoist-non-react-statics@3.3.4: resolution: {integrity: sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ==} dependencies: @@ -2393,7 +2402,6 @@ packages: /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - dev: true /@vitejs/plugin-react-swc@3.4.0(vite@4.5.0): resolution: {integrity: sha512-m7UaA4Uvz82N/0EOVpZL4XsFIakRqrFKeSNxa1FBLSXGvWrWRBwmZb4qxk+ZIVAZcW3c3dn5YosomDgx62XWcQ==} @@ -3497,6 +3505,19 @@ packages: web-namespaces: 2.0.1 dev: false + /hast-util-from-parse5@8.0.1: + resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} + dependencies: + '@types/hast': 3.0.3 + '@types/unist': 3.0.2 + devlop: 1.1.0 + hastscript: 8.0.0 + property-information: 6.3.0 + vfile: 6.0.1 + vfile-location: 5.0.2 + web-namespaces: 2.0.1 + dev: false + /hast-util-is-element@2.1.3: resolution: {integrity: sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==} dependencies: @@ -3514,6 +3535,42 @@ packages: '@types/hast': 2.3.7 dev: false + /hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + dependencies: + '@types/hast': 3.0.3 + dev: false + + /hast-util-raw@9.0.1: + resolution: {integrity: sha512-5m1gmba658Q+lO5uqL5YNGQWeh1MYWZbZmWrM5lncdcuiXuo5E2HT/CIOp0rLF8ksfSwiCVJ3twlgVRyTGThGA==} + dependencies: + '@types/hast': 3.0.3 + '@types/unist': 3.0.2 + '@ungap/structured-clone': 1.2.0 + hast-util-from-parse5: 8.0.1 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.1.0 + parse5: 7.1.2 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + dev: false + + /hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + dependencies: + '@types/hast': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.3.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + dev: false + /hast-util-to-text@3.1.2: resolution: {integrity: sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==} dependencies: @@ -3547,6 +3604,16 @@ packages: space-separated-tokens: 2.0.2 dev: false + /hastscript@8.0.0: + resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} + dependencies: + '@types/hast': 3.0.3 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 6.3.0 + space-separated-tokens: 2.0.2 + dev: false + /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -3582,6 +3649,10 @@ packages: void-elements: 3.1.0 dev: false + /html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + dev: false + /i18next@23.6.0: resolution: {integrity: sha512-z0Cxr0MGkt+kli306WS4nNNM++9cgt2b2VCMprY92j+AIab/oclgPxdwtTZVLP1zn5t5uo8M6uLsZmYrcjr3HA==} dependencies: @@ -4062,6 +4133,20 @@ packages: unist-util-visit: 4.1.2 dev: false + /mdast-util-to-hast@13.1.0: + resolution: {integrity: sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==} + dependencies: + '@types/hast': 3.0.3 + '@types/mdast': 4.0.3 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + dev: false + /mdast-util-to-markdown@1.5.0: resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} dependencies: @@ -4247,6 +4332,13 @@ packages: micromark-util-types: 1.1.0 dev: false + /micromark-util-character@2.0.1: + resolution: {integrity: sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==} + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + dev: false + /micromark-util-chunked@1.1.0: resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} dependencies: @@ -4287,6 +4379,10 @@ packages: resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} dev: false + /micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + dev: false + /micromark-util-html-tag-name@1.2.0: resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} dev: false @@ -4311,6 +4407,14 @@ packages: micromark-util-symbol: 1.1.0 dev: false + /micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + dependencies: + micromark-util-character: 2.0.1 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + dev: false + /micromark-util-subtokenize@1.1.0: resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} dependencies: @@ -4324,10 +4428,18 @@ packages: resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} dev: false + /micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + dev: false + /micromark-util-types@1.1.0: resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} dev: false + /micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + dev: false + /micromark@3.2.0: resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} dependencies: @@ -5098,6 +5210,14 @@ packages: unist-util-visit: 4.1.2 dev: false + /rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + dependencies: + '@types/hast': 3.0.3 + hast-util-raw: 9.0.1 + vfile: 6.0.1 + dev: false + /relateurl@0.2.7: resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} engines: {node: '>= 0.10'} @@ -5598,6 +5718,12 @@ packages: '@types/unist': 2.0.9 dev: false + /unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + dependencies: + '@types/unist': 3.0.2 + dev: false + /unist-util-remove-position@4.0.2: resolution: {integrity: sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==} dependencies: @@ -5639,6 +5765,14 @@ packages: unist-util-visit-parents: 5.1.3 dev: false + /unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + dev: false + /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} @@ -5740,6 +5874,13 @@ packages: vfile: 5.3.7 dev: false + /vfile-location@5.0.2: + resolution: {integrity: sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==} + dependencies: + '@types/unist': 3.0.2 + vfile: 6.0.1 + dev: false + /vfile-message@3.1.4: resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} dependencies: diff --git a/app/src/admin/api/info.ts b/app/src/admin/api/info.ts index 233d247..58c4060 100644 --- a/app/src/admin/api/info.ts +++ b/app/src/admin/api/info.ts @@ -1,5 +1,6 @@ import axios from "axios"; import { + setAnnouncement, setAppLogo, setAppName, setBlobEndpoint, @@ -11,6 +12,7 @@ export type SiteInfo = { logo: string; docs: string; file: string; + announcement: string; }; export async function getSiteInfo(): Promise { @@ -19,7 +21,7 @@ export async function getSiteInfo(): Promise { return response.data as SiteInfo; } catch (e) { console.warn(e); - return { title: "", logo: "", docs: "", file: "" }; + return { title: "", logo: "", docs: "", file: "", announcement: "" }; } } @@ -29,5 +31,6 @@ export function syncSiteInfo() { setAppLogo(info.logo); setDocsUrl(info.docs); setBlobEndpoint(info.file); + setAnnouncement(info.announcement); }); } diff --git a/app/src/components/Markdown.tsx b/app/src/components/Markdown.tsx index 2d1b9de..f15ffdc 100644 --- a/app/src/components/Markdown.tsx +++ b/app/src/components/Markdown.tsx @@ -5,6 +5,7 @@ import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import remarkBreaks from "remark-breaks"; import rehypeKatex from "rehype-katex"; +import rehypeRaw from "rehype-raw"; import { parseFile } from "./plugins/file.tsx"; import "@/assets/markdown/all.less"; import { useEffect, useMemo } from "react"; @@ -24,10 +25,12 @@ import { copyClipboard } from "@/utils/dom.ts"; import { useToast } from "./ui/use-toast.ts"; import { useTranslation } from "react-i18next"; import { parseProgressbar } from "@/components/plugins/progress.tsx"; +import { cn } from "@/components/ui/lib/utils.ts"; type MarkdownProps = { children: string; className?: string; + acceptHtml?: boolean; }; function doAction(dispatch: AppDispatch, url: string): boolean { @@ -69,7 +72,7 @@ function getSocialIcon(url: string) { } } -function MarkdownContent({ children, className }: MarkdownProps) { +function MarkdownContent({ children, className, acceptHtml }: MarkdownProps) { const dispatch = useDispatch(); const { t } = useTranslation(); const { toast } = useToast(); @@ -82,12 +85,18 @@ function MarkdownContent({ children, className }: MarkdownProps) { }); }, [children]); + const rehypePlugins = useMemo(() => { + const plugins = [rehypeKatex]; + return acceptHtml ? [...plugins, rehypeRaw] : plugins; + }, [acceptHtml]); + return ( {children}, - [props.children, props.className], + () => ( + + {children} + + ), + [props.children, props.className, props.acceptHtml], ); } diff --git a/app/src/components/app/Announcement.tsx b/app/src/components/app/Announcement.tsx new file mode 100644 index 0000000..aeb96a7 --- /dev/null +++ b/app/src/components/app/Announcement.tsx @@ -0,0 +1,54 @@ +import { useTranslation } from "react-i18next"; +import { useEffect, useState } from "react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { announcementEvent } from "@/events/announcement.ts"; +import { Bell, Check } from "lucide-react"; +import Markdown from "@/components/Markdown.tsx"; + +function Announcement() { + const { t } = useTranslation(); + const [announcement, setAnnouncement] = useState(""); + + useEffect(() => { + announcementEvent.bind((data: string) => setAnnouncement(data)); + }, []); + + return ( + setAnnouncement("")} + > + + + + +

{t("announcement")}

+
+ + {announcement} + +
+ + {t("close")} + + + {t("i-know")} + + +
+
+ ); +} + +export default Announcement; diff --git a/app/src/components/app/AppProvider.tsx b/app/src/components/app/AppProvider.tsx index 474f4c4..ac5effa 100644 --- a/app/src/components/app/AppProvider.tsx +++ b/app/src/components/app/AppProvider.tsx @@ -19,6 +19,7 @@ import { Model } from "@/api/types.ts"; import { ChargeProps, nonBilling } from "@/admin/charge.ts"; import { dispatchSubscriptionData } from "@/store/globals.ts"; import { marketEvent } from "@/events/market.ts"; +import Announcement from "@/components/app/Announcement.tsx"; function AppProvider() { const dispatch = useDispatch(); @@ -57,6 +58,7 @@ function AppProvider() { return ( <> + diff --git a/app/src/conf/env.ts b/app/src/conf/env.ts index bac7398..7d3d50f 100644 --- a/app/src/conf/env.ts +++ b/app/src/conf/env.ts @@ -1,4 +1,6 @@ import { updateDocumentTitle, updateFavicon } from "@/utils/dom.ts"; +import { getMemory, setMemory } from "@/utils/memory.ts"; +import { announcementEvent } from "@/events/announcement.ts"; export let appName = localStorage.getItem("app_name") || @@ -71,7 +73,7 @@ export function setAppName(name: string): void { * set the app name in localStorage */ name = name.trim() || "Chat Nio"; - localStorage.setItem("app_name", name); + setMemory("app_name", name); appName = name; updateDocumentTitle(name); @@ -82,7 +84,7 @@ export function setAppLogo(logo: string): void { * set the app logo in localStorage */ logo = logo.trim() || "/favicon.ico"; - localStorage.setItem("app_logo", logo); + setMemory("app_logo", logo); appLogo = logo; updateFavicon(logo); @@ -93,7 +95,7 @@ export function setDocsUrl(url: string): void { * set the docs url in localStorage */ url = url.trim() || "https://docs.chatnio.net"; - localStorage.setItem("docs_url", url); + setMemory("docs_url", url); docsEndpoint = url; } @@ -102,6 +104,16 @@ export function setBlobEndpoint(endpoint: string): void { * set the blob endpoint in localStorage */ endpoint = endpoint.trim() || "https://blob.chatnio.net"; - localStorage.setItem("blob_endpoint", endpoint); + setMemory("blob_endpoint", endpoint); blobEndpoint = endpoint; } + +export function setAnnouncement(announcement: string): void { + /** + * set the announcement in localStorage + */ + if (getMemory("announcement") === announcement) return; + setMemory("announcement", announcement); + + announcementEvent.emit(announcement); +} diff --git a/app/src/events/announcement.ts b/app/src/events/announcement.ts new file mode 100644 index 0000000..7933d25 --- /dev/null +++ b/app/src/events/announcement.ts @@ -0,0 +1,5 @@ +import { EventCommitter } from "@/events/struct.ts"; + +export const announcementEvent = new EventCommitter({ + name: "announcement", +}); diff --git a/app/src/resources/i18n/cn.json b/app/src/resources/i18n/cn.json index 14c69ac..037b0ec 100644 --- a/app/src/resources/i18n/cn.json +++ b/app/src/resources/i18n/cn.json @@ -43,6 +43,8 @@ "upward": "上移", "downward": "下移", "save": "保存", + "announcement": "站点公告", + "i-know": "我已知晓", "auth": { "username": "用户名", "username-placeholder": "请输入用户名", diff --git a/app/src/resources/i18n/en.json b/app/src/resources/i18n/en.json index 040f770..9be4307 100644 --- a/app/src/resources/i18n/en.json +++ b/app/src/resources/i18n/en.json @@ -550,5 +550,7 @@ "remove": "remove", "upward": "Top", "downward": "Move down", - "save": "Save" + "save": "Save", + "announcement": "Site Announcement", + "i-know": "Yes, I understand." } \ No newline at end of file diff --git a/app/src/resources/i18n/ja.json b/app/src/resources/i18n/ja.json index a7a8ae7..2569a71 100644 --- a/app/src/resources/i18n/ja.json +++ b/app/src/resources/i18n/ja.json @@ -550,5 +550,7 @@ "remove": "追放", "upward": "上へ移動", "downward": "下へ移動", - "save": "保存" + "save": "保存", + "announcement": "サイトのお知らせ", + "i-know": "私は知っています" } \ No newline at end of file diff --git a/app/src/resources/i18n/ru.json b/app/src/resources/i18n/ru.json index 7babadc..0e7008c 100644 --- a/app/src/resources/i18n/ru.json +++ b/app/src/resources/i18n/ru.json @@ -550,5 +550,7 @@ "remove": "Убрать", "upward": "Выше", "downward": "Ниже", - "save": "Сохранить" + "save": "Сохранить", + "announcement": "Объявление о площадке", + "i-know": "Мне известно о" } \ No newline at end of file diff --git a/app/src/routes/admin/System.tsx b/app/src/routes/admin/System.tsx index 8fe3c42..311bbf4 100644 --- a/app/src/routes/admin/System.tsx +++ b/app/src/routes/admin/System.tsx @@ -404,6 +404,7 @@ function Site({ data, dispatch, onChange }: CompProps) {