diff --git a/src/client/pages/auth/register.tsx b/src/client/pages/auth/register.tsx index ab19b12f..5520fd4d 100644 --- a/src/client/pages/auth/register.tsx +++ b/src/client/pages/auth/register.tsx @@ -1,5 +1,6 @@ import { Response } from '@/lib/api/response'; import { fetchApi } from '@/lib/fetchApi'; +import useUser from '@/lib/client/hooks/useUser'; import { useTitle } from '@/lib/client/hooks/useTitle'; import { Button, @@ -18,8 +19,8 @@ import { import { useForm } from '@mantine/form'; import { notifications, showNotification } from '@mantine/notifications'; import { IconLogin, IconPlus, IconUserPlus, IconX } from '@tabler/icons-react'; -import { useEffect, useState } from 'react'; -import { Link, useLocation, useNavigate } from 'react-router-dom'; +import { useEffect } from 'react'; +import { Link, Navigate, useLocation, useNavigate } from 'react-router-dom'; import useSWR, { mutate } from 'swr'; import GenericError from '../../error/GenericError'; import { getWebClient } from '@/lib/api/detect'; @@ -31,8 +32,6 @@ export function Component() { const location = useLocation(); const navigate = useNavigate(); - const [loading, setLoading] = useState(true); - const { data: config, error: configError, @@ -59,6 +58,8 @@ export function Component() { }, ); + const { user, loading: userLoading } = useUser(); + const form = useForm({ initialValues: { username: '', @@ -74,17 +75,6 @@ export function Component() { }), }); - useEffect(() => { - (async () => { - const res = await fetch('/api/user'); - if (res.ok) { - navigate('/dashboard'); - } else { - setLoading(false); - } - })(); - }, []); - useEffect(() => { if (!config) return; @@ -138,7 +128,11 @@ export function Component() { } }; - if (loading || configLoading) return ; + if (userLoading || configLoading) return ; + + if (user) { + return ; + } if (!config || configError) { return ( diff --git a/src/components/file/DashboardFileType/useFileContent.ts b/src/components/file/DashboardFileType/useFileContent.ts index a1b2bb7e..a9eb470d 100644 --- a/src/components/file/DashboardFileType/useFileContent.ts +++ b/src/components/file/DashboardFileType/useFileContent.ts @@ -1,10 +1,32 @@ import type { File as DbFile } from '@/lib/db/models/file'; -import { useCallback, useEffect, useState } from 'react'; +import useSWR from 'swr'; import { isDbFile } from './useFileUrls'; const MAX_BYTES = 1 * 1024 * 1024; const FILE_BIG = '\n...\nThe file is too big to display click the download icon to view/download it.'; +async function readBlobText(file: File) { + const raw = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = () => reject(new Error('Failed to read file')); + reader.onload = () => resolve((reader.result ?? '') as string); + reader.readAsText(file); + }); + + return raw.length > MAX_BYTES ? raw.slice(0, MAX_BYTES) + FILE_BIG : raw; +} + +async function readText(fileUrl: string) { + const res = await fetch(fileUrl, { + headers: { + Range: `bytes=0-${MAX_BYTES}`, + }, + }); + + if (!res.ok) throw new Error('Failed to fetch file'); + return await res.text(); +} + export default function useFileContent({ enabled, file, @@ -14,41 +36,32 @@ export default function useFileContent({ file: DbFile | File; fileUrl: string; }) { - const [content, setContent] = useState(''); + const { data, error } = useSWR( + () => { + if (!enabled) return null; - const loadText = useCallback(async () => { - try { - if (!isDbFile(file)) { - const reader = new FileReader(); - reader.onload = () => { - const raw = reader.result as string; - setContent(raw.length > MAX_BYTES ? raw.slice(0, MAX_BYTES) + FILE_BIG : raw); - }; - reader.readAsText(file as File); - return; - } + if (isDbFile(file)) return ['dbfile', file.id] as const; + + const f = file as File; + return ['blobfile', f.name] as const; + }, + async () => { + if (!isDbFile(file)) return readBlobText(file as File); if (file.size > MAX_BYTES) { - const res = await fetch(fileUrl, { headers: { Range: `bytes=0-${MAX_BYTES}` } }); - if (!res.ok) throw new Error('Failed to fetch file'); - const text = await res.text(); - setContent(text + FILE_BIG); - return; + const text = await readText(fileUrl); + return text + FILE_BIG; } - const res = await fetch(fileUrl); - if (!res.ok) throw new Error('Failed to fetch file'); - const text = await res.text(); - setContent(text); - } catch { - setContent('Error loading file.'); - } - }, [file, fileUrl]); + return readText(fileUrl); + }, + { + revalidateOnFocus: false, + shouldRetryOnError: false, + }, + ); - useEffect(() => { - if (!enabled) return; - loadText(); - }, [enabled, loadText]); + if (error) return 'Error loading file.'; - return content; + return data ?? ''; } diff --git a/src/components/pages/serverSettings/index.tsx b/src/components/pages/serverSettings/index.tsx index df7ee941..519034e8 100644 --- a/src/components/pages/serverSettings/index.tsx +++ b/src/components/pages/serverSettings/index.tsx @@ -189,7 +189,7 @@ export default function DashboardServerSettings() { const location = useLocation(); const navigate = useNavigate(); - const { data, isLoading } = useSWR('/api/server/settings'); + const { data } = useSWR('/api/server/settings'); const [opened, { toggle }] = useDisclosure(false); const toSettingSection = useCallback((settingKey: string) => { @@ -328,7 +328,7 @@ export default function DashboardServerSettings() { } > - + ) : ( diff --git a/src/components/pages/serverSettings/parts/Chunks.tsx b/src/components/pages/serverSettings/parts/Chunks.tsx index 26b4624f..8c2017fc 100644 --- a/src/components/pages/serverSettings/parts/Chunks.tsx +++ b/src/components/pages/serverSettings/parts/Chunks.tsx @@ -1,27 +1,34 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Button, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; -export default function Chunks({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { +export default function Chunks() { + const { data, isLoading } = useServerSettings(); + + return ( + <> + + {data ?
: null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); const form = useForm({ initialValues: { - chunksEnabled: true, - chunksMax: '95mb', - chunksSize: '25mb', + chunksEnabled: data.settings.chunksEnabled, + chunksMax: data.settings.chunksMax, + chunksSize: data.settings.chunksSize, }, enhanceGetInputProps: (payload: any): object => ({ disabled: - data?.tampered?.includes(payload.field) || + data.tampered.includes(payload.field) || (payload.field !== 'chunksEnabled' && !form.values.chunksEnabled) || false, }), @@ -29,49 +36,35 @@ export default function Chunks({ const onSubmit = settingsOnSubmit(navigate, form); - useEffect(() => { - if (!data) return; - - form.setValues({ - chunksEnabled: data.settings.chunksEnabled ?? true, - chunksMax: data.settings.chunksMax ?? '', - chunksSize: data.settings.chunksSize ?? '', - }); - }, [data]); - return ( - <> - + + + - - - + - + + - - - - - - + + ); } diff --git a/src/components/pages/serverSettings/parts/Core.tsx b/src/components/pages/serverSettings/parts/Core.tsx index 62fd421b..613c250b 100644 --- a/src/components/pages/serverSettings/parts/Core.tsx +++ b/src/components/pages/serverSettings/parts/Core.tsx @@ -1,32 +1,34 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Button, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; -export default function Core({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { +export default function Core() { + const { data, isLoading } = useServerSettings(); + + return ( + <> + + {data ?
: null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); - const form = useForm<{ - coreReturnHttpsUrls: boolean; - coreDefaultDomain: string | null | undefined; - coreTempDirectory: string; - coreTrustProxy: boolean; - }>({ + const form = useForm({ initialValues: { - coreReturnHttpsUrls: false, - coreDefaultDomain: '', - coreTempDirectory: '/tmp/zipline', - coreTrustProxy: false, + coreReturnHttpsUrls: data.settings.coreReturnHttpsUrls, + coreDefaultDomain: data.settings.coreDefaultDomain, + coreTempDirectory: data.settings.coreTempDirectory, + coreTrustProxy: data.settings.coreTrustProxy, }, enhanceGetInputProps: (payload) => ({ - disabled: data?.tampered?.includes(payload.field) || false, + disabled: data.tampered.includes(payload.field) || false, }), }); @@ -40,55 +42,40 @@ export default function Core({ return settingsOnSubmit(navigate, form)(values); }; - useEffect(() => { - if (!data) return; - - form.setValues({ - coreReturnHttpsUrls: data.settings.coreReturnHttpsUrls ?? false, - coreDefaultDomain: data.settings.coreDefaultDomain ?? '', - coreTempDirectory: data.settings.coreTempDirectory ?? '/tmp/zipline', - coreTrustProxy: data.settings.coreTrustProxy ?? false, - }); - }, [data]); - return ( - <> - + + + - - - + - + - + + - - - - - - + + ); } diff --git a/src/components/pages/serverSettings/parts/Discord.tsx b/src/components/pages/serverSettings/parts/Discord.tsx index 1bc1d56a..05e8b059 100644 --- a/src/components/pages/serverSettings/parts/Discord.tsx +++ b/src/components/pages/serverSettings/parts/Discord.tsx @@ -1,4 +1,4 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Button, Collapse, @@ -13,24 +13,31 @@ import { } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; type DiscordEmbed = Record; -export default function Discord({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { +export default function Discord() { + const { data, isLoading } = useServerSettings(); + + return ( + <> + + {data ?
: null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); const formMain = useForm({ initialValues: { - discordWebhookUrl: '', - discordUsername: '', - discordAvatarUrl: '', + discordWebhookUrl: data.settings.discordWebhookUrl, + discordUsername: data.settings.discordUsername, + discordAvatarUrl: data.settings.discordAvatarUrl, }, }); @@ -49,42 +56,46 @@ export default function Discord({ const formOnUpload = useForm({ initialValues: { - discordOnUploadWebhookUrl: '', - discordOnUploadUsername: '', - discordOnUploadAvatarUrl: '', + discordOnUploadWebhookUrl: data.settings.discordOnUploadWebhookUrl, + discordOnUploadUsername: data.settings.discordOnUploadUsername, + discordOnUploadAvatarUrl: data.settings.discordOnUploadAvatarUrl, - discordOnUploadContent: '', + discordOnUploadContent: data.settings.discordOnUploadContent, - discordOnUploadEmbed: false, - discordOnUploadEmbedTitle: '', - discordOnUploadEmbedDescription: '', - discordOnUploadEmbedFooter: '', - discordOnUploadEmbedColor: '', - discordOnUploadEmbedThumbnail: false, - discordOnUploadEmbedImageOrVideo: false, - discordOnUploadEmbedTimestamp: false, - discordOnUploadEmbedUrl: false, + discordOnUploadEmbed: Boolean(data.settings.discordOnUploadEmbed), + discordOnUploadEmbedTitle: (data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.title || '', + discordOnUploadEmbedDescription: + (data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.description || '', + discordOnUploadEmbedFooter: (data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.footer || '', + discordOnUploadEmbedColor: (data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.color || '', + discordOnUploadEmbedThumbnail: !!(data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.thumbnail, + discordOnUploadEmbedImageOrVideo: !!(data.settings.discordOnUploadEmbed as DiscordEmbed | null) + ?.imageOrVideo, + discordOnUploadEmbedTimestamp: !!(data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.timestamp, + discordOnUploadEmbedUrl: !!(data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.url, }, enhanceGetInputProps: (payload) => ({ - disabled: data?.tampered?.includes(payload.field) || false, + disabled: data.tampered.includes(payload.field) || false, }), }); const formOnShorten = useForm({ initialValues: { - discordOnShortenWebhookUrl: '', - discordOnShortenUsername: '', - discordOnShortenAvatarUrl: '', + discordOnShortenWebhookUrl: data.settings.discordOnShortenWebhookUrl, + discordOnShortenUsername: data.settings.discordOnShortenUsername, + discordOnShortenAvatarUrl: data.settings.discordOnShortenAvatarUrl, - discordOnShortenContent: '', + discordOnShortenContent: data.settings.discordOnShortenContent, - discordOnShortenEmbed: false, - discordOnShortenEmbedTitle: '', - discordOnShortenEmbedDescription: '', - discordOnShortenEmbedFooter: '', - discordOnShortenEmbedColor: '', - discordOnShortenEmbedTimestamp: false, - discordOnShortenEmbedUrl: false, + discordOnShortenEmbed: Boolean(data.settings.discordOnShortenEmbed), + discordOnShortenEmbedTitle: (data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.title || '', + discordOnShortenEmbedDescription: + (data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.description || '', + discordOnShortenEmbedFooter: (data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.footer || '', + discordOnShortenEmbedColor: (data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.color || '', + discordOnShortenEmbedTimestamp: !!(data.settings.discordOnShortenEmbed as DiscordEmbed | null) + ?.timestamp, + discordOnShortenEmbedUrl: !!(data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.url, }, }); @@ -123,56 +134,8 @@ export default function Discord({ return settingsOnSubmit(navigate, type === 'upload' ? formOnUpload : formOnShorten)(sendValues); }; - useEffect(() => { - if (!data) return; - - formMain.setValues({ - discordWebhookUrl: data.settings.discordWebhookUrl ?? '', - discordUsername: data.settings.discordUsername ?? '', - discordAvatarUrl: data.settings.discordAvatarUrl ?? '', - }); - - formOnUpload.setValues({ - discordOnUploadWebhookUrl: data.settings.discordOnUploadWebhookUrl ?? '', - discordOnUploadUsername: data.settings.discordOnUploadUsername ?? '', - discordOnUploadAvatarUrl: data.settings.discordOnUploadAvatarUrl ?? '', - - discordOnUploadContent: data.settings.discordOnUploadContent ?? '', - discordOnUploadEmbed: data.settings.discordOnUploadEmbed ? true : false, - discordOnUploadEmbedTitle: (data.settings.discordOnUploadEmbed as DiscordEmbed)?.title ?? '', - discordOnUploadEmbedDescription: - (data.settings.discordOnUploadEmbed as DiscordEmbed)?.description ?? '', - discordOnUploadEmbedFooter: (data.settings.discordOnUploadEmbed as DiscordEmbed)?.footer ?? '', - discordOnUploadEmbedColor: (data.settings.discordOnUploadEmbed as DiscordEmbed)?.color ?? '', - discordOnUploadEmbedThumbnail: (data.settings.discordOnUploadEmbed as DiscordEmbed)?.thumbnail ?? false, - discordOnUploadEmbedImageOrVideo: - (data.settings.discordOnUploadEmbed as DiscordEmbed)?.imageOrVideo ?? false, - discordOnUploadEmbedTimestamp: (data.settings.discordOnUploadEmbed as DiscordEmbed)?.timestamp ?? false, - discordOnUploadEmbedUrl: (data.settings.discordOnUploadEmbed as DiscordEmbed)?.url ?? false, - }); - - formOnShorten.setValues({ - discordOnShortenWebhookUrl: data.settings.discordOnShortenWebhookUrl ?? '', - discordOnShortenUsername: data.settings.discordOnShortenUsername ?? '', - discordOnShortenAvatarUrl: data.settings.discordOnShortenAvatarUrl ?? '', - - discordOnShortenContent: data.settings.discordOnShortenContent ?? '', - discordOnShortenEmbed: data.settings.discordOnShortenEmbed ? true : false, - discordOnShortenEmbedTitle: (data.settings.discordOnShortenEmbed as DiscordEmbed)?.title ?? '', - discordOnShortenEmbedDescription: - (data.settings.discordOnShortenEmbed as DiscordEmbed)?.description ?? '', - discordOnShortenEmbedFooter: (data.settings.discordOnShortenEmbed as DiscordEmbed)?.footer ?? '', - discordOnShortenEmbedColor: (data.settings.discordOnShortenEmbed as DiscordEmbed)?.color ?? '', - discordOnShortenEmbedTimestamp: - (data.settings.discordOnShortenEmbed as DiscordEmbed)?.timestamp ?? false, - discordOnShortenEmbedUrl: (data.settings.discordOnShortenEmbed as DiscordEmbed)?.url ?? false, - }); - }, [data]); - return ( <> - - + + {data ? : null} + + ); +} + +function Form({ data }: { data: Response['/api/server/settings'] }) { const navigate = useNavigate(); const [submitting, setSubmitting] = useState(false); @@ -24,7 +29,7 @@ export default function Domains({ const submitSettings = settingsOnSubmit(navigate, form); - const domains = Array.isArray(data?.settings.domains) ? data!.settings.domains.map(String) : []; + const domains = data.settings.domains.map(String); async function updateDomains(nextDomains: string[]) { setSubmitting(true); @@ -56,7 +61,7 @@ export default function Domains({ return ( <> - + + + {data ? : null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); const form = useForm({ initialValues: { - featuresImageCompression: true, - featuresRobotsTxt: true, - featuresHealthcheck: true, - featuresUserRegistration: false, - featuresOauthRegistration: true, - featuresDeleteOnMaxViews: true, - featuresThumbnailsEnabled: true, - featuresThumbnailsNumberThreads: 4, - featuresThumbnailsFormat: 'jpg', - featuresThumbnailsInstantaneous: false, - featuresMetricsEnabled: true, - featuresMetricsAdminOnly: false, - featuresMetricsShowUserSpecific: true, - featuresVersionChecking: true, - featuresVersionAPI: 'https://zipline-version.diced.sh/', + featuresImageCompression: data.settings.featuresImageCompression, + featuresRobotsTxt: data.settings.featuresRobotsTxt, + featuresHealthcheck: data.settings.featuresHealthcheck, + featuresUserRegistration: data.settings.featuresUserRegistration, + featuresOauthRegistration: data.settings.featuresOauthRegistration, + featuresDeleteOnMaxViews: data.settings.featuresDeleteOnMaxViews, + + featuresThumbnailsEnabled: data.settings.featuresThumbnailsEnabled, + featuresThumbnailsNumberThreads: data.settings.featuresThumbnailsNumberThreads, + featuresThumbnailsFormat: data.settings.featuresThumbnailsFormat, + featuresThumbnailsInstantaneous: data.settings.featuresThumbnailsInstantaneous, + + featuresMetricsEnabled: data.settings.featuresMetricsEnabled, + featuresMetricsAdminOnly: data.settings.featuresMetricsAdminOnly, + featuresMetricsShowUserSpecific: data.settings.featuresMetricsShowUserSpecific, + + featuresVersionChecking: data.settings.featuresVersionChecking, + featuresVersionAPI: data.settings.featuresVersionAPI, }, enhanceGetInputProps: (payload) => ({ - disabled: data?.tampered?.includes(payload.field) || false, + disabled: data.tampered.includes(payload.field) || false, }), }); const onSubmit = settingsOnSubmit(navigate, form); - useEffect(() => { - if (!data) return; - - form.setValues({ - featuresImageCompression: data.settings.featuresImageCompression ?? true, - featuresRobotsTxt: data.settings.featuresRobotsTxt ?? true, - featuresHealthcheck: data.settings.featuresHealthcheck ?? true, - featuresUserRegistration: data.settings.featuresUserRegistration ?? false, - featuresOauthRegistration: data.settings.featuresOauthRegistration ?? true, - featuresDeleteOnMaxViews: data.settings.featuresDeleteOnMaxViews ?? true, - featuresThumbnailsEnabled: data.settings.featuresThumbnailsEnabled ?? true, - featuresThumbnailsNumberThreads: data.settings.featuresThumbnailsNumberThreads ?? 4, - featuresThumbnailsFormat: data.settings.featuresThumbnailsFormat ?? 'jpg', - featuresThumbnailsInstantaneous: data.settings.featuresThumbnailsInstantaneous ?? false, - featuresMetricsEnabled: data.settings.featuresMetricsEnabled ?? true, - featuresMetricsAdminOnly: data.settings.featuresMetricsAdminOnly ?? false, - featuresMetricsShowUserSpecific: data.settings.featuresMetricsShowUserSpecific ?? true, - featuresVersionChecking: data.settings.featuresVersionChecking ?? true, - featuresVersionAPI: data.settings.featuresVersionAPI ?? 'https://zipline-version.diced.sh/', - }); - }, [data]); - return ( - <> - + + + - - + + + + + + + + + + + + + + + + + + + - + - + - + - - - - - - - The URL of the version checking server. The default is{' '} - - https://zipline-version.diced.sh - - . Visit the{' '} - - GitHub - {' '} - to host your own version checking server. - - } - placeholder='https://zipline-version.diced.sh/' - {...form.getInputProps('featuresVersionAPI')} - /> - - - - - + + ); } diff --git a/src/components/pages/serverSettings/parts/Files.tsx b/src/components/pages/serverSettings/parts/Files.tsx index 9a77f6a7..c1cbd6ee 100644 --- a/src/components/pages/serverSettings/parts/Files.tsx +++ b/src/components/pages/serverSettings/parts/Files.tsx @@ -1,52 +1,44 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Button, LoadingOverlay, NumberInput, Select, Stack, Switch, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; -export default function Files({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { +export default function Files() { + const { data, isLoading } = useServerSettings(); + + return ( + <> + + {data ?
: null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); - const form = useForm<{ - filesRoute: string; - filesLength: number; - filesDefaultFormat: string; - filesDisabledExtensions: string; - filesMaxFileSize: string; - filesDefaultExpiration: string | null; - filesMaxExpiration: string | null; - filesAssumeMimetypes: boolean; - filesDefaultDateFormat: string; - filesRemoveGpsMetadata: boolean; - filesRandomWordsNumAdjectives: number; - filesRandomWordsSeparator: string; - filesDefaultCompressionFormat: string; - filesMaxFilesPerUpload: number; - }>({ + const form = useForm({ initialValues: { - filesRoute: '/u', - filesLength: 6, - filesDefaultFormat: 'random', - filesDisabledExtensions: '', - filesMaxFileSize: '100mb', - filesDefaultExpiration: '', - filesMaxExpiration: '', - filesAssumeMimetypes: false, - filesDefaultDateFormat: 'YYYY-MM-DD_HH:mm:ss', - filesRemoveGpsMetadata: false, - filesRandomWordsNumAdjectives: 3, - filesRandomWordsSeparator: '-', - filesDefaultCompressionFormat: 'jpg', - filesMaxFilesPerUpload: 1000, + filesRoute: data.settings.filesRoute, + filesLength: data.settings.filesLength, + filesDefaultFormat: data.settings.filesDefaultFormat, + filesDisabledExtensions: data.settings.filesDisabledExtensions.join(', '), + filesMaxFileSize: data.settings.filesMaxFileSize, + filesDefaultExpiration: data.settings.filesDefaultExpiration, + filesMaxExpiration: data.settings.filesMaxExpiration, + filesAssumeMimetypes: data.settings.filesAssumeMimetypes, + filesDefaultDateFormat: data.settings.filesDefaultDateFormat, + filesRemoveGpsMetadata: data.settings.filesRemoveGpsMetadata, + filesRandomWordsNumAdjectives: data.settings.filesRandomWordsNumAdjectives, + filesRandomWordsSeparator: data.settings.filesRandomWordsSeparator, + filesDefaultCompressionFormat: data.settings.filesDefaultCompressionFormat, + filesMaxFilesPerUpload: data.settings.filesMaxFilesPerUpload, }, enhanceGetInputProps: (payload) => ({ - disabled: data?.tampered?.includes(payload.field) || false, + disabled: data.tampered.includes(payload.field) || false, }), }); @@ -85,143 +77,118 @@ export default function Files({ return settingsOnSubmit(navigate, form)(values); }; - useEffect(() => { - if (!data) return; - - form.setValues({ - filesRoute: data.settings.filesRoute ?? '/u', - filesLength: data.settings.filesLength ?? 6, - filesDefaultFormat: data.settings.filesDefaultFormat ?? 'random', - filesDisabledExtensions: data.settings.filesDisabledExtensions.join(', ') ?? '', - filesMaxFileSize: data.settings.filesMaxFileSize ?? '100mb', - filesDefaultExpiration: data.settings.filesDefaultExpiration ?? '', - filesMaxExpiration: data.settings.filesMaxExpiration ?? '', - filesAssumeMimetypes: data.settings.filesAssumeMimetypes ?? false, - filesDefaultDateFormat: data.settings.filesDefaultDateFormat ?? 'YYYY-MM-DD_HH:mm:ss', - filesRemoveGpsMetadata: data.settings.filesRemoveGpsMetadata ?? false, - filesRandomWordsNumAdjectives: data.settings.filesRandomWordsNumAdjectives ?? 3, - filesRandomWordsSeparator: data.settings.filesRandomWordsSeparator ?? '-', - filesDefaultCompressionFormat: data.settings.filesDefaultCompressionFormat ?? 'jpg', - filesMaxFilesPerUpload: data.settings.filesMaxFilesPerUpload ?? 1000, - }); - }, [data]); - return ( - <> - + + + - - - + - + - + - + + - + - + - + - + - + - + - + + + - - - - - - + + ); } diff --git a/src/components/pages/serverSettings/parts/HttpWebhook.tsx b/src/components/pages/serverSettings/parts/HttpWebhook.tsx index 20b4fd04..5d8f51d0 100644 --- a/src/components/pages/serverSettings/parts/HttpWebhook.tsx +++ b/src/components/pages/serverSettings/parts/HttpWebhook.tsx @@ -1,25 +1,32 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Button, LoadingOverlay, Stack, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; -export default function HttpWebhook({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { +export default function HttpWebhook() { + const { data, isLoading } = useServerSettings(); + + return ( + <> + + {data ?
: null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); const form = useForm({ initialValues: { - httpWebhookOnUpload: '', - httpWebhookOnShorten: '', + httpWebhookOnUpload: data.settings.httpWebhookOnUpload, + httpWebhookOnShorten: data.settings.httpWebhookOnShorten, }, enhanceGetInputProps: (payload) => ({ - disabled: data?.tampered?.includes(payload.field) || false, + disabled: data.tampered.includes(payload.field) || false, }), }); @@ -37,40 +44,27 @@ export default function HttpWebhook({ return settingsOnSubmit(navigate, form)(values); }; - useEffect(() => { - if (!data) return; - - form.setValues({ - httpWebhookOnUpload: data.settings.httpWebhookOnUpload ?? '', - httpWebhookOnShorten: data.settings.httpWebhookOnShorten ?? '', - }); - }, [data]); - return ( - <> - + + + - - - + + - - - - - - + + ); } diff --git a/src/components/pages/serverSettings/parts/Invites.tsx b/src/components/pages/serverSettings/parts/Invites.tsx index beb36764..63cc577c 100644 --- a/src/components/pages/serverSettings/parts/Invites.tsx +++ b/src/components/pages/serverSettings/parts/Invites.tsx @@ -1,26 +1,33 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Button, LoadingOverlay, NumberInput, Stack, Switch } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; -export default function Invites({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { +export default function Invites() { + const { data, isLoading } = useServerSettings(); + + return ( + <> + + {data ?
: null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); const form = useForm({ initialValues: { - invitesEnabled: true, - invitesLength: 6, + invitesEnabled: data.settings.invitesEnabled, + invitesLength: data.settings.invitesLength, }, enhanceGetInputProps: (payload: any): object => ({ disabled: - data?.tampered?.includes(payload.field) || + data.tampered.includes(payload.field) || (payload.field !== 'invitesEnabled' && !form.values.invitesEnabled) || false, }), @@ -28,41 +35,28 @@ export default function Invites({ const onSubmit = settingsOnSubmit(navigate, form); - useEffect(() => { - if (!data) return; - - form.setValues({ - invitesEnabled: data.settings.invitesEnabled ?? true, - invitesLength: data.settings.invitesLength ?? 6, - }); - }, [data]); - return ( - <> - + + + - - - + + - - - - - - + + ); } diff --git a/src/components/pages/serverSettings/parts/Mfa.tsx b/src/components/pages/serverSettings/parts/Mfa.tsx index e74f8b74..54faa992 100644 --- a/src/components/pages/serverSettings/parts/Mfa.tsx +++ b/src/components/pages/serverSettings/parts/Mfa.tsx @@ -1,90 +1,81 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Button, Divider, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; -export default function Mfa({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { +export default function Mfa() { + const { data, isLoading } = useServerSettings(); + + return ( + <> + + {data ?
: null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); const form = useForm({ initialValues: { - mfaTotpEnabled: false, - mfaTotpIssuer: 'Zipline', - mfaPasskeysEnabled: false, - mfaPasskeysRpID: '', - mfaPasskeysOrigin: '', + mfaTotpEnabled: data.settings.mfaTotpEnabled, + mfaTotpIssuer: data.settings.mfaTotpIssuer, + mfaPasskeysEnabled: data.settings.mfaPasskeysEnabled, + mfaPasskeysRpID: data.settings.mfaPasskeysRpID, + mfaPasskeysOrigin: data.settings.mfaPasskeysOrigin, }, enhanceGetInputProps: (payload) => ({ - disabled: data?.tampered?.includes(payload.field) || false, + disabled: data.tampered.includes(payload.field) || false, }), }); const onSubmit = settingsOnSubmit(navigate, form); - useEffect(() => { - if (!data) return; - - form.setValues({ - mfaTotpEnabled: data.settings.mfaTotpEnabled ?? false, - mfaTotpIssuer: data.settings.mfaTotpIssuer ?? 'Zipline', - mfaPasskeysEnabled: data.settings.mfaPasskeysEnabled ?? false, - mfaPasskeysRpID: data.settings.mfaPasskeysRpID ?? '', - mfaPasskeysOrigin: data.settings.mfaPasskeysOrigin ?? '', - }); - }, [data]); - return ( - <> - + + + - - - + - + - + - + + + - - - - - - - + + ); } diff --git a/src/components/pages/serverSettings/parts/Oauth.tsx b/src/components/pages/serverSettings/parts/Oauth.tsx index f9c7312a..4162a767 100644 --- a/src/components/pages/serverSettings/parts/Oauth.tsx +++ b/src/components/pages/serverSettings/parts/Oauth.tsx @@ -1,4 +1,4 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Anchor, Button, @@ -13,45 +13,52 @@ import { } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; -export default function Oauth({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { +export default function Oauth() { + const { data, isLoading } = useServerSettings(); + + return ( + <> + + {data ?
: null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); const form = useForm({ initialValues: { - oauthBypassLocalLogin: false, - oauthLoginOnly: false, + oauthBypassLocalLogin: data.settings.oauthBypassLocalLogin, + oauthLoginOnly: data.settings.oauthLoginOnly, - oauthDiscordClientId: '', - oauthDiscordClientSecret: '', - oauthDiscordRedirectUri: '', - oauthDiscordAllowedIds: '', - oauthDiscordDeniedIds: '', + oauthDiscordClientId: data.settings.oauthDiscordClientId, + oauthDiscordClientSecret: data.settings.oauthDiscordClientSecret, + oauthDiscordRedirectUri: data.settings.oauthDiscordRedirectUri, + oauthDiscordAllowedIds: data.settings.oauthDiscordAllowedIds.join(', '), + oauthDiscordDeniedIds: data.settings.oauthDiscordDeniedIds.join(', '), - oauthGoogleClientId: '', - oauthGoogleClientSecret: '', - oauthGoogleRedirectUri: '', + oauthGoogleClientId: data.settings.oauthGoogleClientId, + oauthGoogleClientSecret: data.settings.oauthGoogleClientSecret, + oauthGoogleRedirectUri: data.settings.oauthGoogleRedirectUri, - oauthGithubClientId: '', - oauthGithubClientSecret: '', - oauthGithubRedirectUri: '', + oauthGithubClientId: data.settings.oauthGithubClientId, + oauthGithubClientSecret: data.settings.oauthGithubClientSecret, + oauthGithubRedirectUri: data.settings.oauthGithubRedirectUri, - oauthOidcClientId: '', - oauthOidcClientSecret: '', - oauthOidcAuthorizeUrl: '', - oauthOidcTokenUrl: '', - oauthOidcUserinfoUrl: '', - oauthOidcRedirectUri: '', + oauthOidcClientId: data.settings.oauthOidcClientId, + oauthOidcClientSecret: data.settings.oauthOidcClientSecret, + oauthOidcAuthorizeUrl: data.settings.oauthOidcAuthorizeUrl, + oauthOidcTokenUrl: data.settings.oauthOidcTokenUrl, + oauthOidcUserinfoUrl: data.settings.oauthOidcUserinfoUrl, + oauthOidcRedirectUri: data.settings.oauthOidcRedirectUri, }, enhanceGetInputProps: (payload) => ({ - disabled: data?.tampered?.includes(payload.field) || false, + disabled: data.tampered.includes(payload.field) || false, }), }); @@ -90,44 +97,8 @@ export default function Oauth({ return settingsOnSubmit(navigate, form)(values); }; - useEffect(() => { - if (!data) return; - - form.setValues({ - oauthBypassLocalLogin: data.settings.oauthBypassLocalLogin ?? false, - oauthLoginOnly: data.settings.oauthLoginOnly ?? false, - - oauthDiscordClientId: data.settings.oauthDiscordClientId ?? '', - oauthDiscordClientSecret: data.settings.oauthDiscordClientSecret ?? '', - oauthDiscordRedirectUri: data.settings.oauthDiscordRedirectUri ?? '', - oauthDiscordAllowedIds: data.settings.oauthDiscordAllowedIds - ? data.settings.oauthDiscordAllowedIds.join(', ') - : '', - oauthDiscordDeniedIds: data.settings.oauthDiscordDeniedIds - ? data.settings.oauthDiscordDeniedIds.join(', ') - : '', - - oauthGoogleClientId: data.settings.oauthGoogleClientId ?? '', - oauthGoogleClientSecret: data.settings.oauthGoogleClientSecret ?? '', - oauthGoogleRedirectUri: data.settings.oauthGoogleRedirectUri ?? '', - - oauthGithubClientId: data.settings.oauthGithubClientId ?? '', - oauthGithubClientSecret: data.settings.oauthGithubClientSecret ?? '', - oauthGithubRedirectUri: data.settings.oauthGithubRedirectUri ?? '', - - oauthOidcClientId: data.settings.oauthOidcClientId ?? '', - oauthOidcClientSecret: data.settings.oauthOidcClientSecret ?? '', - oauthOidcAuthorizeUrl: data.settings.oauthOidcAuthorizeUrl ?? '', - oauthOidcTokenUrl: data.settings.oauthOidcTokenUrl ?? '', - oauthOidcUserinfoUrl: data.settings.oauthOidcUserinfoUrl ?? '', - oauthOidcRedirectUri: data.settings.oauthOidcRedirectUri ?? '', - }); - }, [data]); - return ( <> - - For OAuth to work, the "OAuth Registration" setting must be enabled in the{' '} diff --git a/src/components/pages/serverSettings/parts/PWA.tsx b/src/components/pages/serverSettings/parts/PWA.tsx index c7253ce1..bcdbaed7 100644 --- a/src/components/pages/serverSettings/parts/PWA.tsx +++ b/src/components/pages/serverSettings/parts/PWA.tsx @@ -1,30 +1,37 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Button, ColorInput, Group, LoadingOverlay, Stack, Switch, Text, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy, IconRefresh } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; -export default function PWA({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { +export default function PWA() { + const { data, isLoading } = useServerSettings(); + + return ( + <> + + {data ? : null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); const form = useForm({ initialValues: { - pwaEnabled: false, - pwaTitle: '', - pwaShortName: '', - pwaDescription: '', - pwaThemeColor: '', - pwaBackgroundColor: '', + pwaEnabled: data.settings.pwaEnabled, + pwaTitle: data.settings.pwaTitle, + pwaShortName: data.settings.pwaShortName, + pwaDescription: data.settings.pwaDescription, + pwaThemeColor: data.settings.pwaThemeColor, + pwaBackgroundColor: data.settings.pwaBackgroundColor, }, enhanceGetInputProps: (payload: any): object => ({ disabled: - data?.tampered?.includes(payload.field) || + data.tampered.includes(payload.field) || (payload.field !== 'pwaEnabled' && !form.values.pwaEnabled) || false, }), @@ -48,23 +55,8 @@ export default function PWA({ }); }; - useEffect(() => { - if (!data) return; - - form.setValues({ - pwaEnabled: data.settings.pwaEnabled ?? false, - pwaTitle: data.settings.pwaTitle ?? '', - pwaShortName: data.settings.pwaShortName ?? '', - pwaDescription: data.settings.pwaDescription ?? '', - pwaThemeColor: data.settings.pwaThemeColor ?? '', - pwaBackgroundColor: data.settings.pwaBackgroundColor ?? '', - }); - }, [data]); - return ( <> - - Refresh the page after enabling PWA to see any changes. diff --git a/src/components/pages/serverSettings/parts/Ratelimit.tsx b/src/components/pages/serverSettings/parts/Ratelimit.tsx index 6f8fecd3..5de00762 100644 --- a/src/components/pages/serverSettings/parts/Ratelimit.tsx +++ b/src/components/pages/serverSettings/parts/Ratelimit.tsx @@ -1,35 +1,42 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Button, LoadingOverlay, NumberInput, Stack, Switch, Text, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; -export default function Ratelimit({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { +export default function Ratelimit() { + const { data, isLoading } = useServerSettings(); + + return ( + <> + + {data ? : null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); const form = useForm<{ ratelimitEnabled: boolean; ratelimitMax: number; - ratelimitWindow: number | ''; + ratelimitWindow: number | '' | null; ratelimitAdminBypass: boolean; ratelimitAllowList: string; }>({ initialValues: { - ratelimitEnabled: true, - ratelimitMax: 10, - ratelimitWindow: '', - ratelimitAdminBypass: false, - ratelimitAllowList: '', + ratelimitEnabled: data.settings.ratelimitEnabled, + ratelimitMax: data.settings.ratelimitMax, + ratelimitWindow: data.settings.ratelimitWindow, + ratelimitAdminBypass: data.settings.ratelimitAdminBypass, + ratelimitAllowList: data.settings.ratelimitAllowList.join(', '), }, enhanceGetInputProps: (payload: any): object => ({ disabled: - data?.tampered?.includes(payload.field) || + data.tampered.includes(payload.field) || (payload.field !== 'ratelimitEnabled' && !form.values.ratelimitEnabled) || false, }), @@ -55,22 +62,8 @@ export default function Ratelimit({ return settingsOnSubmit(navigate, form)(values); }; - useEffect(() => { - if (!data) return; - - form.setValues({ - ratelimitEnabled: data.settings.ratelimitEnabled ?? true, - ratelimitMax: data.settings.ratelimitMax ?? 10, - ratelimitWindow: data.settings.ratelimitWindow ?? '', - ratelimitAdminBypass: data.settings.ratelimitAdminBypass ?? false, - ratelimitAllowList: data.settings.ratelimitAllowList.join(', ') ?? '', - }); - }, [data]); - return ( <> - - All options require a restart to take effect. diff --git a/src/components/pages/serverSettings/parts/Tasks.tsx b/src/components/pages/serverSettings/parts/Tasks.tsx index db092911..f5e5f075 100644 --- a/src/components/pages/serverSettings/parts/Tasks.tsx +++ b/src/components/pages/serverSettings/parts/Tasks.tsx @@ -1,51 +1,43 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Button, Code, LoadingOverlay, Stack, Text, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; -export default function Tasks({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { +export default function Tasks() { + const { data, isLoading } = useServerSettings(); + + return ( + <> + + {data ? : null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); const form = useForm({ initialValues: { - tasksDeleteInterval: '30m', - tasksClearInvitesInterval: '30m', - tasksMaxViewsInterval: '30m', - tasksThumbnailsInterval: '30m', - tasksMetricsInterval: '30m', - tasksCleanThumbnailsInterval: '1d', + tasksDeleteInterval: data.settings.tasksDeleteInterval, + tasksClearInvitesInterval: data.settings.tasksClearInvitesInterval, + tasksMaxViewsInterval: data.settings.tasksMaxViewsInterval, + tasksThumbnailsInterval: data.settings.tasksThumbnailsInterval, + tasksMetricsInterval: data.settings.tasksMetricsInterval, + tasksCleanThumbnailsInterval: data.settings.tasksCleanThumbnailsInterval, }, enhanceGetInputProps: (payload) => ({ - disabled: data?.tampered?.includes(payload.field) || false, + disabled: data.tampered.includes(payload.field) || false, }), }); const onSubmit = settingsOnSubmit(navigate, form); - useEffect(() => { - if (!data) return; - - form.setValues({ - tasksDeleteInterval: data.settings.tasksDeleteInterval ?? '30m', - tasksClearInvitesInterval: data.settings.tasksClearInvitesInterval ?? '30m', - tasksMaxViewsInterval: data.settings.tasksMaxViewsInterval ?? '30m', - tasksThumbnailsInterval: data.settings.tasksThumbnailsInterval ?? '30m', - tasksMetricsInterval: data.settings.tasksMetricsInterval ?? '30m', - tasksCleanThumbnailsInterval: data.settings.tasksCleanThumbnailsInterval ?? '1d', - }); - }, [data]); - return ( <> - - All options require a restart to take effect. Setting a value of 0 will disable the task. diff --git a/src/components/pages/serverSettings/parts/Urls.tsx b/src/components/pages/serverSettings/parts/Urls.tsx index 01eb823b..3b3bb680 100644 --- a/src/components/pages/serverSettings/parts/Urls.tsx +++ b/src/components/pages/serverSettings/parts/Urls.tsx @@ -1,66 +1,60 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Button, LoadingOverlay, NumberInput, Stack, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; -export default function Urls({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { +export default function Urls() { + const { data, isLoading } = useServerSettings(); + + return ( + <> + + {data ? : null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); const form = useForm({ initialValues: { - urlsRoute: '/go', - urlsLength: 6, + urlsRoute: data.settings.urlsRoute, + urlsLength: data.settings.urlsLength, }, enhanceGetInputProps: (payload) => ({ - disabled: data?.tampered?.includes(payload.field) || false, + disabled: data.tampered.includes(payload.field) || false, }), }); const onSubmit = settingsOnSubmit(navigate, form); - useEffect(() => { - if (!data) return; - - form.setValues({ - urlsRoute: data.settings.urlsRoute ?? '/go', - urlsLength: data.settings.urlsLength ?? 6, - }); - }, [data]); - return ( - <> - + + + - - - + + - - - - - - + + ); } diff --git a/src/components/pages/serverSettings/parts/Website.tsx b/src/components/pages/serverSettings/parts/Website.tsx index 20065e2b..0fd15c14 100644 --- a/src/components/pages/serverSettings/parts/Website.tsx +++ b/src/components/pages/serverSettings/parts/Website.tsx @@ -1,45 +1,41 @@ -import { Response } from '@/lib/api/response'; +import type { Response } from '@/lib/api/response'; import { Button, JsonInput, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core'; import { useForm } from '@mantine/form'; import { IconDeviceFloppy } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { settingsOnSubmit } from '../settingsOnSubmit'; +import useServerSettings from '../useServerSettings'; -const defaultExternalLinks = [ - { - name: 'GitHub', - url: 'https://github.com/diced/zipline', - }, - { - name: 'Documentation', - url: 'https://zipline.diced.sh', - }, -]; +export default function Website() { + const { data, isLoading } = useServerSettings(); -export default function Website({ - swr: { data, isLoading }, -}: { - swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; -}) { + return ( + <> + + {data ?
: null} + + ); +} + +function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) { const navigate = useNavigate(); const form = useForm({ initialValues: { - websiteTitle: 'Zipline', - websiteTitleLogo: '', - websiteExternalLinks: JSON.stringify(defaultExternalLinks), - websiteLoginBackground: '', - websiteLoginBackgroundBlur: true, - websiteDefaultAvatar: '', - websiteTos: '', + websiteTitle: data.settings.websiteTitle, + websiteTitleLogo: data.settings.websiteTitleLogo, + websiteExternalLinks: JSON.stringify(data.settings.websiteExternalLinks, null, 2), + websiteLoginBackground: data.settings.websiteLoginBackground, + websiteLoginBackgroundBlur: data.settings.websiteLoginBackgroundBlur, + websiteDefaultAvatar: data.settings.websiteDefaultAvatar, + websiteTos: data.settings.websiteTos, - websiteThemeDefault: 'system', - websiteThemeDark: 'builtin:dark_gray', - websiteThemeLight: 'builtin:light_gray', + websiteThemeDefault: data.settings.websiteThemeDefault, + websiteThemeDark: data.settings.websiteThemeDark, + websiteThemeLight: data.settings.websiteThemeLight, }, enhanceGetInputProps: (payload) => ({ - disabled: data?.tampered?.includes(payload.field) || false, + disabled: data.tampered.includes(payload.field) || false, }), }); @@ -59,12 +55,19 @@ export default function Website({ } sendValues.websiteTitleLogo = - values.websiteTitleLogo.trim() === '' ? null : values.websiteTitleLogo.trim(); + values.websiteTitleLogo?.trim() === '' || !values.websiteTitleLogo?.trim() + ? null + : values.websiteTitleLogo.trim(); sendValues.websiteLoginBackground = - values.websiteLoginBackground.trim() === '' ? null : values.websiteLoginBackground.trim(); + values.websiteLoginBackground?.trim() === '' || !values.websiteLoginBackground?.trim() + ? null + : values.websiteLoginBackground.trim(); sendValues.websiteDefaultAvatar = - values.websiteDefaultAvatar.trim() === '' ? null : values.websiteDefaultAvatar.trim(); - sendValues.websiteTos = values.websiteTos.trim() === '' ? null : values.websiteTos.trim(); + values.websiteDefaultAvatar?.trim() === '' || !values.websiteDefaultAvatar?.trim() + ? null + : values.websiteDefaultAvatar.trim(); + sendValues.websiteTos = + values.websiteTos?.trim() === '' || !values.websiteTos?.trim() ? null : values.websiteTos.trim(); sendValues.websiteThemeDefault = values.websiteThemeDefault.trim(); sendValues.websiteThemeDark = values.websiteThemeDark.trim(); @@ -76,110 +79,92 @@ export default function Website({ return settingsOnSubmit(navigate, form)(sendValues); }; - useEffect(() => { - if (!data) return; - - form.setValues({ - websiteTitle: data.settings.websiteTitle ?? 'Zipline', - websiteTitleLogo: data.settings.websiteTitleLogo ?? '', - websiteExternalLinks: JSON.stringify( - data.settings.websiteExternalLinks ?? defaultExternalLinks, - null, - 2, - ), - websiteLoginBackground: data.settings.websiteLoginBackground ?? '', - websiteLoginBackgroundBlur: data.settings.websiteLoginBackgroundBlur ?? true, - websiteDefaultAvatar: data.settings.websiteDefaultAvatar ?? '', - websiteTos: data.settings.websiteTos ?? '', - websiteThemeDefault: data.settings.websiteThemeDefault ?? 'system', - websiteThemeDark: data.settings.websiteThemeDark ?? 'builtin:dark_gray', - websiteThemeLight: data.settings.websiteThemeLight ?? 'builtin:light_gray', - }); - }, [data]); - return ( - <> - + + + - - - + - + - + - + - + - + - + - + - - - - - - - + + + + ); } diff --git a/src/components/pages/serverSettings/useServerSettings.ts b/src/components/pages/serverSettings/useServerSettings.ts new file mode 100644 index 00000000..2b9dacb6 --- /dev/null +++ b/src/components/pages/serverSettings/useServerSettings.ts @@ -0,0 +1,6 @@ +import type { Response } from '@/lib/api/response'; +import useSWR from 'swr'; + +export default function useServerSettings() { + return useSWR('/api/server/settings'); +} diff --git a/src/components/pages/settings/parts/SettingsAvatar.tsx b/src/components/pages/settings/parts/SettingsAvatar.tsx index 73308187..de15771d 100644 --- a/src/components/pages/settings/parts/SettingsAvatar.tsx +++ b/src/components/pages/settings/parts/SettingsAvatar.tsx @@ -25,7 +25,7 @@ import { IconSettingsFilled, IconX, } from '@tabler/icons-react'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; export default function SettingsAvatar() { const user = useUserStore((state) => state.user); @@ -36,14 +36,16 @@ export default function SettingsAvatar() { const [avatar, setAvatar] = useState(null); const [avatarSrc, setAvatarSrc] = useState(null); - useEffect(() => { - (async () => { - if (!avatar) return; + const onAvatarChange = async (file: File | null) => { + setAvatar(file); - const base64url = await readToDataURL(avatar); - setAvatarSrc(base64url); - })(); - }, [avatar]); + if (!file) { + setAvatarSrc(null); + return; + } + + setAvatarSrc(await readToDataURL(file)); + }; const saveAvatar = async () => { if (!avatar) return; @@ -111,7 +113,7 @@ export default function SettingsAvatar() { accept='image/*' placeholder='Upload new avatar...' value={avatar} - onChange={(file) => setAvatar(file)} + onChange={onAvatarChange} leftSection={} /> diff --git a/src/components/pages/settings/parts/SettingsFileView.tsx b/src/components/pages/settings/parts/SettingsFileView.tsx index eddb39d4..7c9aae00 100644 --- a/src/components/pages/settings/parts/SettingsFileView.tsx +++ b/src/components/pages/settings/parts/SettingsFileView.tsx @@ -1,3 +1,4 @@ +import type { User } from '@/lib/db/models/user'; import { Response } from '@/lib/api/response'; import { fetchApi } from '@/lib/fetchApi'; import { useUserStore } from '@/lib/client/store/user'; @@ -27,7 +28,6 @@ import { IconDeviceFloppy, IconFileX, } from '@tabler/icons-react'; -import { useEffect } from 'react'; import { mutate } from 'swr'; import { useShallow } from 'zustand/shallow'; @@ -40,19 +40,34 @@ const alignIcons: Record = { export default function SettingsFileView() { const [user, setUser] = useUserStore(useShallow((state) => [state.user, state.setUser])); + if (!user) { + return ( + + Viewing Files + + Loading… + + + ); + } + + return
; +} + +function Form({ user, setUser }: { user: User; setUser: (u: User) => void }) { const form = useForm({ initialValues: { - enabled: user?.view.enabled ?? false, - content: user?.view.content ?? '', - embed: user?.view.embed ?? false, - embedTitle: user?.view.embedTitle ?? '', - embedDescription: user?.view.embedDescription ?? '', - embedSiteName: user?.view.embedSiteName ?? '', - embedColor: user?.view.embedColor ?? '', - align: user?.view.align ?? 'left', - showMimetype: user?.view.showMimetype ?? false, - showTags: user?.view.showTags ?? false, - showFolder: user?.view.showFolder ?? false, + enabled: user.view.enabled || false, + content: user.view.content || '', + embed: user.view.embed || false, + embedTitle: user.view.embedTitle || '', + embedDescription: user.view.embedDescription || '', + embedSiteName: user.view.embedSiteName || '', + embedColor: user.view.embedColor || '', + align: user.view.align || 'left', + showMimetype: user.view.showMimetype || false, + showTags: user.view.showTags || false, + showFolder: user.view.showFolder || false, }, }); @@ -95,24 +110,6 @@ export default function SettingsFileView() { }); }; - useEffect(() => { - if (user) { - form.setValues({ - enabled: user.view.enabled || false, - content: user.view.content || '', - embed: user.view.embed || false, - embedTitle: user.view.embedTitle || '', - embedDescription: user.view.embedDescription || '', - embedSiteName: user.view.embedSiteName || '', - embedColor: user.view.embedColor || '', - align: user.view.align || 'left', - showMimetype: user.view.showMimetype || false, - showTags: user.view.showTags || false, - showFolder: user.view.showFolder || false, - }); - } - }, [user]); - return ( Viewing Files diff --git a/src/components/pages/settings/parts/SettingsUser.tsx b/src/components/pages/settings/parts/SettingsUser.tsx index 5918e188..417b2336 100644 --- a/src/components/pages/settings/parts/SettingsUser.tsx +++ b/src/components/pages/settings/parts/SettingsUser.tsx @@ -1,3 +1,4 @@ +import type { User } from '@/lib/db/models/user'; import { ApiError } from '@/lib/api/errors'; import { Response } from '@/lib/api/response'; import { fetchApi } from '@/lib/fetchApi'; @@ -25,29 +26,36 @@ import { IconUser, IconUserCancel, } from '@tabler/icons-react'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { mutate } from 'swr'; +import useSWR from 'swr'; import { useShallow } from 'zustand/shallow'; export default function SettingsUser() { const [user, setUser] = useUserStore(useShallow((state) => [state.user, state.setUser])); + const { data: tokenPayload } = useSWR('/api/user/token'); + + if (!user) { + return ( + + User + + Loading… + + + ); + } + + return ; +} + +function Form({ user, setUser, token }: { user: User; setUser: (u: User) => void; token: string }) { const [tokenShown, setTokenShown] = useState(false); - const [token, setToken] = useState(''); - - useEffect(() => { - (async () => { - const { data } = await fetchApi('/api/user/token'); - - if (data) { - setToken(data.token || ''); - } - })(); - }, []); const form = useForm({ initialValues: { - username: user?.username ?? '', + username: user.username, password: '', }, validate: { @@ -61,7 +69,7 @@ export default function SettingsUser() { password?: string; } = {}; - if (values.username !== user?.username) send['username'] = values.username.trim(); + if (values.username !== user.username) send['username'] = values.username.trim(); if (values.password) send['password'] = values.password.trim(); const { data, error } = await fetchApi('/api/user', 'PATCH', send); @@ -84,6 +92,7 @@ export default function SettingsUser() { if (!data?.user) return; mutate('/api/user'); + mutate('/api/user/token'); setUser(data.user); notifications.show({ message: 'User updated', @@ -96,7 +105,7 @@ export default function SettingsUser() { User - {user?.id} + {user.id} @@ -134,7 +143,7 @@ export default function SettingsUser() { leftSection={} /> - diff --git a/src/lib/client/hooks/useUser.ts b/src/lib/client/hooks/useUser.ts new file mode 100644 index 00000000..d413a229 --- /dev/null +++ b/src/lib/client/hooks/useUser.ts @@ -0,0 +1,24 @@ +import type { Response } from '@/lib/api/response'; +import useSWR from 'swr'; + +async function fetcher(url: string): Promise { + const res = await fetch(url); + if (!res.ok) return null; + + return res.json(); +} + +export default function useUser(): { + user: Response['/api/user']['user'] | undefined; + loading: boolean; +} { + const { data, isLoading } = useSWR('/api/user', fetcher, { + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenHidden: false, + revalidateIfStale: false, + shouldRetryOnError: false, + }); + + return { user: data?.user, loading: isLoading }; +} diff --git a/src/lib/client/hooks/useVersion.ts b/src/lib/client/hooks/useVersion.ts index 82260192..be721fee 100644 --- a/src/lib/client/hooks/useVersion.ts +++ b/src/lib/client/hooks/useVersion.ts @@ -1,15 +1,16 @@ import useSWR from 'swr'; import { Response } from '../../api/response'; -const f = async () => { + +async function fetcher() { const res = await fetch('/api/version'); if (!res.ok) throw new Error('Failed to fetch version'); const r = await res.json(); return r; -}; +} export default function useVersion() { - const { isLoading, data } = useSWR('/api/version', f, { + const { isLoading, data } = useSWR('/api/version', fetcher, { refreshInterval: undefined, revalidateOnFocus: false, revalidateIfStale: false,