fix: perf improvements

This commit is contained in:
diced
2026-04-27 17:51:00 -07:00
parent 87a2dfbda6
commit 15f5279ddb
24 changed files with 958 additions and 1107 deletions

View File

@@ -1,5 +1,6 @@
import { Response } from '@/lib/api/response'; import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi'; import { fetchApi } from '@/lib/fetchApi';
import useUser from '@/lib/client/hooks/useUser';
import { useTitle } from '@/lib/client/hooks/useTitle'; import { useTitle } from '@/lib/client/hooks/useTitle';
import { import {
Button, Button,
@@ -18,8 +19,8 @@ import {
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { notifications, showNotification } from '@mantine/notifications'; import { notifications, showNotification } from '@mantine/notifications';
import { IconLogin, IconPlus, IconUserPlus, IconX } from '@tabler/icons-react'; import { IconLogin, IconPlus, IconUserPlus, IconX } from '@tabler/icons-react';
import { useEffect, useState } from 'react'; import { useEffect } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Link, Navigate, useLocation, useNavigate } from 'react-router-dom';
import useSWR, { mutate } from 'swr'; import useSWR, { mutate } from 'swr';
import GenericError from '../../error/GenericError'; import GenericError from '../../error/GenericError';
import { getWebClient } from '@/lib/api/detect'; import { getWebClient } from '@/lib/api/detect';
@@ -31,8 +32,6 @@ export function Component() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const { const {
data: config, data: config,
error: configError, error: configError,
@@ -59,6 +58,8 @@ export function Component() {
}, },
); );
const { user, loading: userLoading } = useUser();
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
username: '', 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(() => { useEffect(() => {
if (!config) return; if (!config) return;
@@ -138,7 +128,11 @@ export function Component() {
} }
}; };
if (loading || configLoading) return <LoadingOverlay visible />; if (userLoading || configLoading) return <LoadingOverlay visible />;
if (user) {
return <Navigate to='/dashboard' replace />;
}
if (!config || configError) { if (!config || configError) {
return ( return (

View File

@@ -1,10 +1,32 @@
import type { File as DbFile } from '@/lib/db/models/file'; import type { File as DbFile } from '@/lib/db/models/file';
import { useCallback, useEffect, useState } from 'react'; import useSWR from 'swr';
import { isDbFile } from './useFileUrls'; import { isDbFile } from './useFileUrls';
const MAX_BYTES = 1 * 1024 * 1024; 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.'; 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<string>((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({ export default function useFileContent({
enabled, enabled,
file, file,
@@ -14,41 +36,32 @@ export default function useFileContent({
file: DbFile | File; file: DbFile | File;
fileUrl: string; fileUrl: string;
}) { }) {
const [content, setContent] = useState(''); const { data, error } = useSWR<string>(
() => {
if (!enabled) return null;
const loadText = useCallback(async () => { if (isDbFile(file)) return ['dbfile', file.id] as const;
try {
if (!isDbFile(file)) { const f = file as File;
const reader = new FileReader(); return ['blobfile', f.name] as const;
reader.onload = () => { },
const raw = reader.result as string; async () => {
setContent(raw.length > MAX_BYTES ? raw.slice(0, MAX_BYTES) + FILE_BIG : raw); if (!isDbFile(file)) return readBlobText(file as File);
};
reader.readAsText(file as File);
return;
}
if (file.size > MAX_BYTES) { if (file.size > MAX_BYTES) {
const res = await fetch(fileUrl, { headers: { Range: `bytes=0-${MAX_BYTES}` } }); const text = await readText(fileUrl);
if (!res.ok) throw new Error('Failed to fetch file'); return text + FILE_BIG;
const text = await res.text();
setContent(text + FILE_BIG);
return;
} }
const res = await fetch(fileUrl); return readText(fileUrl);
if (!res.ok) throw new Error('Failed to fetch file'); },
const text = await res.text(); {
setContent(text); revalidateOnFocus: false,
} catch { shouldRetryOnError: false,
setContent('Error loading file.'); },
} );
}, [file, fileUrl]);
useEffect(() => { if (error) return 'Error loading file.';
if (!enabled) return;
loadText();
}, [enabled, loadText]);
return content; return data ?? '';
} }

View File

@@ -189,7 +189,7 @@ export default function DashboardServerSettings() {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { data, isLoading } = useSWR<Response['/api/server/settings']>('/api/server/settings'); const { data } = useSWR<Response['/api/server/settings']>('/api/server/settings');
const [opened, { toggle }] = useDisclosure(false); const [opened, { toggle }] = useDisclosure(false);
const toSettingSection = useCallback((settingKey: string) => { const toSettingSection = useCallback((settingKey: string) => {
@@ -328,7 +328,7 @@ export default function DashboardServerSettings() {
</Box> </Box>
} }
> >
<SettingsComponent swr={{ data, isLoading }} /> <SettingsComponent />
</Suspense> </Suspense>
</Box> </Box>
) : ( ) : (

View File

@@ -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 { Button, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function Chunks({ export default function Chunks() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} bdrs='md' />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
chunksEnabled: true, chunksEnabled: data.settings.chunksEnabled,
chunksMax: '95mb', chunksMax: data.settings.chunksMax,
chunksSize: '25mb', chunksSize: data.settings.chunksSize,
}, },
enhanceGetInputProps: (payload: any): object => ({ enhanceGetInputProps: (payload: any): object => ({
disabled: disabled:
data?.tampered?.includes(payload.field) || data.tampered.includes(payload.field) ||
(payload.field !== 'chunksEnabled' && !form.values.chunksEnabled) || (payload.field !== 'chunksEnabled' && !form.values.chunksEnabled) ||
false, false,
}), }),
@@ -29,20 +36,7 @@ export default function Chunks({
const onSubmit = settingsOnSubmit(navigate, form); 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 ( return (
<>
<LoadingOverlay visible={isLoading} bdrs='md' />
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Stack gap='lg'> <Stack gap='lg'>
<Switch <Switch
@@ -72,6 +66,5 @@ export default function Chunks({
Save Save
</Button> </Button>
</form> </form>
</>
); );
} }

View File

@@ -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 { Button, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function Core({ export default function Core() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm<{ const form = useForm({
coreReturnHttpsUrls: boolean;
coreDefaultDomain: string | null | undefined;
coreTempDirectory: string;
coreTrustProxy: boolean;
}>({
initialValues: { initialValues: {
coreReturnHttpsUrls: false, coreReturnHttpsUrls: data.settings.coreReturnHttpsUrls,
coreDefaultDomain: '', coreDefaultDomain: data.settings.coreDefaultDomain,
coreTempDirectory: '/tmp/zipline', coreTempDirectory: data.settings.coreTempDirectory,
coreTrustProxy: false, coreTrustProxy: data.settings.coreTrustProxy,
}, },
enhanceGetInputProps: (payload) => ({ enhanceGetInputProps: (payload) => ({
disabled: data?.tampered?.includes(payload.field) || false, disabled: data.tampered.includes(payload.field) || false,
}), }),
}); });
@@ -40,21 +42,7 @@ export default function Core({
return settingsOnSubmit(navigate, form)(values); 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 ( return (
<>
<LoadingOverlay visible={isLoading} />
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Stack gap='lg'> <Stack gap='lg'>
<Switch <Switch
@@ -89,6 +77,5 @@ export default function Core({
Save Save
</Button> </Button>
</form> </form>
</>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Response } from '@/lib/api/response'; import type { Response } from '@/lib/api/response';
import { import {
Button, Button,
Collapse, Collapse,
@@ -13,24 +13,31 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
type DiscordEmbed = Record<string, any>; type DiscordEmbed = Record<string, any>;
export default function Discord({ export default function Discord() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const formMain = useForm({ const formMain = useForm({
initialValues: { initialValues: {
discordWebhookUrl: '', discordWebhookUrl: data.settings.discordWebhookUrl,
discordUsername: '', discordUsername: data.settings.discordUsername,
discordAvatarUrl: '', discordAvatarUrl: data.settings.discordAvatarUrl,
}, },
}); });
@@ -49,42 +56,46 @@ export default function Discord({
const formOnUpload = useForm({ const formOnUpload = useForm({
initialValues: { initialValues: {
discordOnUploadWebhookUrl: '', discordOnUploadWebhookUrl: data.settings.discordOnUploadWebhookUrl,
discordOnUploadUsername: '', discordOnUploadUsername: data.settings.discordOnUploadUsername,
discordOnUploadAvatarUrl: '', discordOnUploadAvatarUrl: data.settings.discordOnUploadAvatarUrl,
discordOnUploadContent: '', discordOnUploadContent: data.settings.discordOnUploadContent,
discordOnUploadEmbed: false, discordOnUploadEmbed: Boolean(data.settings.discordOnUploadEmbed),
discordOnUploadEmbedTitle: '', discordOnUploadEmbedTitle: (data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.title || '',
discordOnUploadEmbedDescription: '', discordOnUploadEmbedDescription:
discordOnUploadEmbedFooter: '', (data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.description || '',
discordOnUploadEmbedColor: '', discordOnUploadEmbedFooter: (data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.footer || '',
discordOnUploadEmbedThumbnail: false, discordOnUploadEmbedColor: (data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.color || '',
discordOnUploadEmbedImageOrVideo: false, discordOnUploadEmbedThumbnail: !!(data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.thumbnail,
discordOnUploadEmbedTimestamp: false, discordOnUploadEmbedImageOrVideo: !!(data.settings.discordOnUploadEmbed as DiscordEmbed | null)
discordOnUploadEmbedUrl: false, ?.imageOrVideo,
discordOnUploadEmbedTimestamp: !!(data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.timestamp,
discordOnUploadEmbedUrl: !!(data.settings.discordOnUploadEmbed as DiscordEmbed | null)?.url,
}, },
enhanceGetInputProps: (payload) => ({ enhanceGetInputProps: (payload) => ({
disabled: data?.tampered?.includes(payload.field) || false, disabled: data.tampered.includes(payload.field) || false,
}), }),
}); });
const formOnShorten = useForm({ const formOnShorten = useForm({
initialValues: { initialValues: {
discordOnShortenWebhookUrl: '', discordOnShortenWebhookUrl: data.settings.discordOnShortenWebhookUrl,
discordOnShortenUsername: '', discordOnShortenUsername: data.settings.discordOnShortenUsername,
discordOnShortenAvatarUrl: '', discordOnShortenAvatarUrl: data.settings.discordOnShortenAvatarUrl,
discordOnShortenContent: '', discordOnShortenContent: data.settings.discordOnShortenContent,
discordOnShortenEmbed: false, discordOnShortenEmbed: Boolean(data.settings.discordOnShortenEmbed),
discordOnShortenEmbedTitle: '', discordOnShortenEmbedTitle: (data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.title || '',
discordOnShortenEmbedDescription: '', discordOnShortenEmbedDescription:
discordOnShortenEmbedFooter: '', (data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.description || '',
discordOnShortenEmbedColor: '', discordOnShortenEmbedFooter: (data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.footer || '',
discordOnShortenEmbedTimestamp: false, discordOnShortenEmbedColor: (data.settings.discordOnShortenEmbed as DiscordEmbed | null)?.color || '',
discordOnShortenEmbedUrl: false, 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); 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 ( return (
<> <>
<LoadingOverlay visible={isLoading} />
<form onSubmit={formMain.onSubmit(onSubmitMain)}> <form onSubmit={formMain.onSubmit(onSubmitMain)}>
<TextInput <TextInput
label='Webhook URL' label='Webhook URL'

View File

@@ -1,19 +1,24 @@
import { Response } from '@/lib/api/response'; import type { Response } from '@/lib/api/response';
import { ActionIcon, LoadingOverlay, Paper, Table, Text, TextInput } from '@mantine/core'; import { ActionIcon, LoadingOverlay, Paper, Table, Text, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconPlus, IconTrash } from '@tabler/icons-react'; import { IconPlus, IconTrash } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function Domains({ export default function Domains() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { return (
data: Response['/api/server/settings'] | undefined; <>
isLoading: boolean; <LoadingOverlay visible={isLoading} />
}; {data ? <Form data={data} /> : null}
}) { </>
);
}
function Form({ data }: { data: Response['/api/server/settings'] }) {
const navigate = useNavigate(); const navigate = useNavigate();
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -24,7 +29,7 @@ export default function Domains({
const submitSettings = settingsOnSubmit(navigate, form); 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[]) { async function updateDomains(nextDomains: string[]) {
setSubmitting(true); setSubmitting(true);
@@ -56,7 +61,7 @@ export default function Domains({
return ( return (
<> <>
<LoadingOverlay visible={isLoading || submitting} /> <LoadingOverlay visible={submitting} />
<form onSubmit={addDomain}> <form onSubmit={addDomain}>
<TextInput <TextInput

View File

@@ -1,4 +1,4 @@
import { Response } from '@/lib/api/response'; import type { Response } from '@/lib/api/response';
import { import {
Anchor, Anchor,
Button, Button,
@@ -13,68 +13,53 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function Features({ export default function Features() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
featuresImageCompression: true, featuresImageCompression: data.settings.featuresImageCompression,
featuresRobotsTxt: true, featuresRobotsTxt: data.settings.featuresRobotsTxt,
featuresHealthcheck: true, featuresHealthcheck: data.settings.featuresHealthcheck,
featuresUserRegistration: false, featuresUserRegistration: data.settings.featuresUserRegistration,
featuresOauthRegistration: true, featuresOauthRegistration: data.settings.featuresOauthRegistration,
featuresDeleteOnMaxViews: true, featuresDeleteOnMaxViews: data.settings.featuresDeleteOnMaxViews,
featuresThumbnailsEnabled: true,
featuresThumbnailsNumberThreads: 4, featuresThumbnailsEnabled: data.settings.featuresThumbnailsEnabled,
featuresThumbnailsFormat: 'jpg', featuresThumbnailsNumberThreads: data.settings.featuresThumbnailsNumberThreads,
featuresThumbnailsInstantaneous: false, featuresThumbnailsFormat: data.settings.featuresThumbnailsFormat,
featuresMetricsEnabled: true, featuresThumbnailsInstantaneous: data.settings.featuresThumbnailsInstantaneous,
featuresMetricsAdminOnly: false,
featuresMetricsShowUserSpecific: true, featuresMetricsEnabled: data.settings.featuresMetricsEnabled,
featuresVersionChecking: true, featuresMetricsAdminOnly: data.settings.featuresMetricsAdminOnly,
featuresVersionAPI: 'https://zipline-version.diced.sh/', featuresMetricsShowUserSpecific: data.settings.featuresMetricsShowUserSpecific,
featuresVersionChecking: data.settings.featuresVersionChecking,
featuresVersionAPI: data.settings.featuresVersionAPI,
}, },
enhanceGetInputProps: (payload) => ({ enhanceGetInputProps: (payload) => ({
disabled: data?.tampered?.includes(payload.field) || false, disabled: data.tampered.includes(payload.field) || false,
}), }),
}); });
const onSubmit = settingsOnSubmit(navigate, form); 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 ( return (
<>
<LoadingOverlay visible={isLoading} />
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Stack gap='lg'> <Stack gap='lg'>
<Switch <Switch
@@ -198,6 +183,5 @@ export default function Features({
Save Save
</Button> </Button>
</form> </form>
</>
); );
} }

View File

@@ -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 { Button, LoadingOverlay, NumberInput, Select, Stack, Switch, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function Files({ export default function Files() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm<{ 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;
}>({
initialValues: { initialValues: {
filesRoute: '/u', filesRoute: data.settings.filesRoute,
filesLength: 6, filesLength: data.settings.filesLength,
filesDefaultFormat: 'random', filesDefaultFormat: data.settings.filesDefaultFormat,
filesDisabledExtensions: '', filesDisabledExtensions: data.settings.filesDisabledExtensions.join(', '),
filesMaxFileSize: '100mb', filesMaxFileSize: data.settings.filesMaxFileSize,
filesDefaultExpiration: '', filesDefaultExpiration: data.settings.filesDefaultExpiration,
filesMaxExpiration: '', filesMaxExpiration: data.settings.filesMaxExpiration,
filesAssumeMimetypes: false, filesAssumeMimetypes: data.settings.filesAssumeMimetypes,
filesDefaultDateFormat: 'YYYY-MM-DD_HH:mm:ss', filesDefaultDateFormat: data.settings.filesDefaultDateFormat,
filesRemoveGpsMetadata: false, filesRemoveGpsMetadata: data.settings.filesRemoveGpsMetadata,
filesRandomWordsNumAdjectives: 3, filesRandomWordsNumAdjectives: data.settings.filesRandomWordsNumAdjectives,
filesRandomWordsSeparator: '-', filesRandomWordsSeparator: data.settings.filesRandomWordsSeparator,
filesDefaultCompressionFormat: 'jpg', filesDefaultCompressionFormat: data.settings.filesDefaultCompressionFormat,
filesMaxFilesPerUpload: 1000, filesMaxFilesPerUpload: data.settings.filesMaxFilesPerUpload,
}, },
enhanceGetInputProps: (payload) => ({ enhanceGetInputProps: (payload) => ({
disabled: data?.tampered?.includes(payload.field) || false, disabled: data.tampered.includes(payload.field) || false,
}), }),
}); });
@@ -85,31 +77,7 @@ export default function Files({
return settingsOnSubmit(navigate, form)(values); 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 ( return (
<>
<LoadingOverlay visible={isLoading} />
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Stack gap='lg'> <Stack gap='lg'>
<Switch <Switch
@@ -222,6 +190,5 @@ export default function Files({
Save Save
</Button> </Button>
</form> </form>
</>
); );
} }

View File

@@ -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 { Button, LoadingOverlay, Stack, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function HttpWebhook({ export default function HttpWebhook() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
httpWebhookOnUpload: '', httpWebhookOnUpload: data.settings.httpWebhookOnUpload,
httpWebhookOnShorten: '', httpWebhookOnShorten: data.settings.httpWebhookOnShorten,
}, },
enhanceGetInputProps: (payload) => ({ enhanceGetInputProps: (payload) => ({
disabled: data?.tampered?.includes(payload.field) || false, disabled: data.tampered.includes(payload.field) || false,
}), }),
}); });
@@ -37,19 +44,7 @@ export default function HttpWebhook({
return settingsOnSubmit(navigate, form)(values); return settingsOnSubmit(navigate, form)(values);
}; };
useEffect(() => {
if (!data) return;
form.setValues({
httpWebhookOnUpload: data.settings.httpWebhookOnUpload ?? '',
httpWebhookOnShorten: data.settings.httpWebhookOnShorten ?? '',
});
}, [data]);
return ( return (
<>
<LoadingOverlay visible={isLoading} />
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Stack gap='lg'> <Stack gap='lg'>
<TextInput <TextInput
@@ -71,6 +66,5 @@ export default function HttpWebhook({
Save Save
</Button> </Button>
</form> </form>
</>
); );
} }

View File

@@ -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 { Button, LoadingOverlay, NumberInput, Stack, Switch } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function Invites({ export default function Invites() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
invitesEnabled: true, invitesEnabled: data.settings.invitesEnabled,
invitesLength: 6, invitesLength: data.settings.invitesLength,
}, },
enhanceGetInputProps: (payload: any): object => ({ enhanceGetInputProps: (payload: any): object => ({
disabled: disabled:
data?.tampered?.includes(payload.field) || data.tampered.includes(payload.field) ||
(payload.field !== 'invitesEnabled' && !form.values.invitesEnabled) || (payload.field !== 'invitesEnabled' && !form.values.invitesEnabled) ||
false, false,
}), }),
@@ -28,19 +35,7 @@ export default function Invites({
const onSubmit = settingsOnSubmit(navigate, form); const onSubmit = settingsOnSubmit(navigate, form);
useEffect(() => {
if (!data) return;
form.setValues({
invitesEnabled: data.settings.invitesEnabled ?? true,
invitesLength: data.settings.invitesLength ?? 6,
});
}, [data]);
return ( return (
<>
<LoadingOverlay visible={isLoading} />
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Stack gap='lg'> <Stack gap='lg'>
<Switch <Switch
@@ -63,6 +58,5 @@ export default function Invites({
Save Save
</Button> </Button>
</form> </form>
</>
); );
} }

View File

@@ -1,49 +1,41 @@
import { Response } from '@/lib/api/response'; import type { Response } from '@/lib/api/response';
import { Button, Divider, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core'; import { Button, Divider, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function Mfa({ export default function Mfa() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
mfaTotpEnabled: false, mfaTotpEnabled: data.settings.mfaTotpEnabled,
mfaTotpIssuer: 'Zipline', mfaTotpIssuer: data.settings.mfaTotpIssuer,
mfaPasskeysEnabled: false, mfaPasskeysEnabled: data.settings.mfaPasskeysEnabled,
mfaPasskeysRpID: '', mfaPasskeysRpID: data.settings.mfaPasskeysRpID,
mfaPasskeysOrigin: '', mfaPasskeysOrigin: data.settings.mfaPasskeysOrigin,
}, },
enhanceGetInputProps: (payload) => ({ enhanceGetInputProps: (payload) => ({
disabled: data?.tampered?.includes(payload.field) || false, disabled: data.tampered.includes(payload.field) || false,
}), }),
}); });
const onSubmit = settingsOnSubmit(navigate, form); 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 ( return (
<>
<LoadingOverlay visible={isLoading} />
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Stack gap='lg'> <Stack gap='lg'>
<Switch <Switch
@@ -85,6 +77,5 @@ export default function Mfa({
Save Save
</Button> </Button>
</form> </form>
</>
); );
} }

View File

@@ -1,4 +1,4 @@
import { Response } from '@/lib/api/response'; import type { Response } from '@/lib/api/response';
import { import {
Anchor, Anchor,
Button, Button,
@@ -13,45 +13,52 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function Oauth({ export default function Oauth() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
oauthBypassLocalLogin: false, oauthBypassLocalLogin: data.settings.oauthBypassLocalLogin,
oauthLoginOnly: false, oauthLoginOnly: data.settings.oauthLoginOnly,
oauthDiscordClientId: '', oauthDiscordClientId: data.settings.oauthDiscordClientId,
oauthDiscordClientSecret: '', oauthDiscordClientSecret: data.settings.oauthDiscordClientSecret,
oauthDiscordRedirectUri: '', oauthDiscordRedirectUri: data.settings.oauthDiscordRedirectUri,
oauthDiscordAllowedIds: '', oauthDiscordAllowedIds: data.settings.oauthDiscordAllowedIds.join(', '),
oauthDiscordDeniedIds: '', oauthDiscordDeniedIds: data.settings.oauthDiscordDeniedIds.join(', '),
oauthGoogleClientId: '', oauthGoogleClientId: data.settings.oauthGoogleClientId,
oauthGoogleClientSecret: '', oauthGoogleClientSecret: data.settings.oauthGoogleClientSecret,
oauthGoogleRedirectUri: '', oauthGoogleRedirectUri: data.settings.oauthGoogleRedirectUri,
oauthGithubClientId: '', oauthGithubClientId: data.settings.oauthGithubClientId,
oauthGithubClientSecret: '', oauthGithubClientSecret: data.settings.oauthGithubClientSecret,
oauthGithubRedirectUri: '', oauthGithubRedirectUri: data.settings.oauthGithubRedirectUri,
oauthOidcClientId: '', oauthOidcClientId: data.settings.oauthOidcClientId,
oauthOidcClientSecret: '', oauthOidcClientSecret: data.settings.oauthOidcClientSecret,
oauthOidcAuthorizeUrl: '', oauthOidcAuthorizeUrl: data.settings.oauthOidcAuthorizeUrl,
oauthOidcTokenUrl: '', oauthOidcTokenUrl: data.settings.oauthOidcTokenUrl,
oauthOidcUserinfoUrl: '', oauthOidcUserinfoUrl: data.settings.oauthOidcUserinfoUrl,
oauthOidcRedirectUri: '', oauthOidcRedirectUri: data.settings.oauthOidcRedirectUri,
}, },
enhanceGetInputProps: (payload) => ({ 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); 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 ( return (
<> <>
<LoadingOverlay visible={isLoading} />
<Text size='sm' c='dimmed' mb='md'> <Text size='sm' c='dimmed' mb='md'>
For OAuth to work, the &quot;OAuth Registration&quot; setting must be enabled in the{' '} For OAuth to work, the &quot;OAuth Registration&quot; setting must be enabled in the{' '}
<Anchor component={Link} to='/dashboard/admin/settings/features'> <Anchor component={Link} to='/dashboard/admin/settings/features'>

View File

@@ -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 { Button, ColorInput, Group, LoadingOverlay, Stack, Switch, Text, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy, IconRefresh } from '@tabler/icons-react'; import { IconDeviceFloppy, IconRefresh } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function PWA({ export default function PWA() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
pwaEnabled: false, pwaEnabled: data.settings.pwaEnabled,
pwaTitle: '', pwaTitle: data.settings.pwaTitle,
pwaShortName: '', pwaShortName: data.settings.pwaShortName,
pwaDescription: '', pwaDescription: data.settings.pwaDescription,
pwaThemeColor: '', pwaThemeColor: data.settings.pwaThemeColor,
pwaBackgroundColor: '', pwaBackgroundColor: data.settings.pwaBackgroundColor,
}, },
enhanceGetInputProps: (payload: any): object => ({ enhanceGetInputProps: (payload: any): object => ({
disabled: disabled:
data?.tampered?.includes(payload.field) || data.tampered.includes(payload.field) ||
(payload.field !== 'pwaEnabled' && !form.values.pwaEnabled) || (payload.field !== 'pwaEnabled' && !form.values.pwaEnabled) ||
false, 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 ( return (
<> <>
<LoadingOverlay visible={isLoading} />
<Text size='sm' c='dimmed' mb='md'> <Text size='sm' c='dimmed' mb='md'>
Refresh the page after enabling PWA to see any changes. Refresh the page after enabling PWA to see any changes.
</Text> </Text>

View File

@@ -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 { Button, LoadingOverlay, NumberInput, Stack, Switch, Text, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function Ratelimit({ export default function Ratelimit() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm<{ const form = useForm<{
ratelimitEnabled: boolean; ratelimitEnabled: boolean;
ratelimitMax: number; ratelimitMax: number;
ratelimitWindow: number | ''; ratelimitWindow: number | '' | null;
ratelimitAdminBypass: boolean; ratelimitAdminBypass: boolean;
ratelimitAllowList: string; ratelimitAllowList: string;
}>({ }>({
initialValues: { initialValues: {
ratelimitEnabled: true, ratelimitEnabled: data.settings.ratelimitEnabled,
ratelimitMax: 10, ratelimitMax: data.settings.ratelimitMax,
ratelimitWindow: '', ratelimitWindow: data.settings.ratelimitWindow,
ratelimitAdminBypass: false, ratelimitAdminBypass: data.settings.ratelimitAdminBypass,
ratelimitAllowList: '', ratelimitAllowList: data.settings.ratelimitAllowList.join(', '),
}, },
enhanceGetInputProps: (payload: any): object => ({ enhanceGetInputProps: (payload: any): object => ({
disabled: disabled:
data?.tampered?.includes(payload.field) || data.tampered.includes(payload.field) ||
(payload.field !== 'ratelimitEnabled' && !form.values.ratelimitEnabled) || (payload.field !== 'ratelimitEnabled' && !form.values.ratelimitEnabled) ||
false, false,
}), }),
@@ -55,22 +62,8 @@ export default function Ratelimit({
return settingsOnSubmit(navigate, form)(values); 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 ( return (
<> <>
<LoadingOverlay visible={isLoading} />
<Text size='sm' c='dimmed' mb='md'> <Text size='sm' c='dimmed' mb='md'>
All options require a restart to take effect. All options require a restart to take effect.
</Text> </Text>

View File

@@ -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 { Button, Code, LoadingOverlay, Stack, Text, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function Tasks({ export default function Tasks() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
tasksDeleteInterval: '30m', tasksDeleteInterval: data.settings.tasksDeleteInterval,
tasksClearInvitesInterval: '30m', tasksClearInvitesInterval: data.settings.tasksClearInvitesInterval,
tasksMaxViewsInterval: '30m', tasksMaxViewsInterval: data.settings.tasksMaxViewsInterval,
tasksThumbnailsInterval: '30m', tasksThumbnailsInterval: data.settings.tasksThumbnailsInterval,
tasksMetricsInterval: '30m', tasksMetricsInterval: data.settings.tasksMetricsInterval,
tasksCleanThumbnailsInterval: '1d', tasksCleanThumbnailsInterval: data.settings.tasksCleanThumbnailsInterval,
}, },
enhanceGetInputProps: (payload) => ({ enhanceGetInputProps: (payload) => ({
disabled: data?.tampered?.includes(payload.field) || false, disabled: data.tampered.includes(payload.field) || false,
}), }),
}); });
const onSubmit = settingsOnSubmit(navigate, form); 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 ( return (
<> <>
<LoadingOverlay visible={isLoading} />
<Text size='sm' c='dimmed' mb='md'> <Text size='sm' c='dimmed' mb='md'>
All options require a restart to take effect. Setting a value of <Code>0</Code> will disable the task. All options require a restart to take effect. Setting a value of <Code>0</Code> will disable the task.
</Text> </Text>

View File

@@ -1,43 +1,38 @@
import { Response } from '@/lib/api/response'; import type { Response } from '@/lib/api/response';
import { Button, LoadingOverlay, NumberInput, Stack, TextInput } from '@mantine/core'; import { Button, LoadingOverlay, NumberInput, Stack, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
export default function Urls({ export default function Urls() {
swr: { data, isLoading }, const { data, isLoading } = useServerSettings();
}: {
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; return (
}) { <>
<LoadingOverlay visible={isLoading} />
{data ? <Form data={data} isLoading={isLoading} /> : null}
</>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
urlsRoute: '/go', urlsRoute: data.settings.urlsRoute,
urlsLength: 6, urlsLength: data.settings.urlsLength,
}, },
enhanceGetInputProps: (payload) => ({ enhanceGetInputProps: (payload) => ({
disabled: data?.tampered?.includes(payload.field) || false, disabled: data.tampered.includes(payload.field) || false,
}), }),
}); });
const onSubmit = settingsOnSubmit(navigate, form); const onSubmit = settingsOnSubmit(navigate, form);
useEffect(() => {
if (!data) return;
form.setValues({
urlsRoute: data.settings.urlsRoute ?? '/go',
urlsLength: data.settings.urlsLength ?? 6,
});
}, [data]);
return ( return (
<>
<LoadingOverlay visible={isLoading} />
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Stack gap='lg'> <Stack gap='lg'>
<TextInput <TextInput
@@ -61,6 +56,5 @@ export default function Urls({
Save Save
</Button> </Button>
</form> </form>
</>
); );
} }

View File

@@ -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 { Button, JsonInput, LoadingOverlay, Stack, Switch, TextInput } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { IconDeviceFloppy } from '@tabler/icons-react'; import { IconDeviceFloppy } from '@tabler/icons-react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { settingsOnSubmit } from '../settingsOnSubmit'; import { settingsOnSubmit } from '../settingsOnSubmit';
import useServerSettings from '../useServerSettings';
const defaultExternalLinks = [ export default function Website() {
{ const { data, isLoading } = useServerSettings();
name: 'GitHub',
url: 'https://github.com/diced/zipline',
},
{
name: 'Documentation',
url: 'https://zipline.diced.sh',
},
];
export default function Website({ return (
swr: { data, isLoading }, <>
}: { <LoadingOverlay visible={isLoading} />
swr: { data: Response['/api/server/settings'] | undefined; isLoading: boolean }; {data ? <Form data={data} isLoading={isLoading} /> : null}
}) { </>
);
}
function Form({ data, isLoading }: { data: Response['/api/server/settings']; isLoading: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
websiteTitle: 'Zipline', websiteTitle: data.settings.websiteTitle,
websiteTitleLogo: '', websiteTitleLogo: data.settings.websiteTitleLogo,
websiteExternalLinks: JSON.stringify(defaultExternalLinks), websiteExternalLinks: JSON.stringify(data.settings.websiteExternalLinks, null, 2),
websiteLoginBackground: '', websiteLoginBackground: data.settings.websiteLoginBackground,
websiteLoginBackgroundBlur: true, websiteLoginBackgroundBlur: data.settings.websiteLoginBackgroundBlur,
websiteDefaultAvatar: '', websiteDefaultAvatar: data.settings.websiteDefaultAvatar,
websiteTos: '', websiteTos: data.settings.websiteTos,
websiteThemeDefault: 'system', websiteThemeDefault: data.settings.websiteThemeDefault,
websiteThemeDark: 'builtin:dark_gray', websiteThemeDark: data.settings.websiteThemeDark,
websiteThemeLight: 'builtin:light_gray', websiteThemeLight: data.settings.websiteThemeLight,
}, },
enhanceGetInputProps: (payload) => ({ 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 = sendValues.websiteTitleLogo =
values.websiteTitleLogo.trim() === '' ? null : values.websiteTitleLogo.trim(); values.websiteTitleLogo?.trim() === '' || !values.websiteTitleLogo?.trim()
? null
: values.websiteTitleLogo.trim();
sendValues.websiteLoginBackground = sendValues.websiteLoginBackground =
values.websiteLoginBackground.trim() === '' ? null : values.websiteLoginBackground.trim(); values.websiteLoginBackground?.trim() === '' || !values.websiteLoginBackground?.trim()
? null
: values.websiteLoginBackground.trim();
sendValues.websiteDefaultAvatar = sendValues.websiteDefaultAvatar =
values.websiteDefaultAvatar.trim() === '' ? null : values.websiteDefaultAvatar.trim(); values.websiteDefaultAvatar?.trim() === '' || !values.websiteDefaultAvatar?.trim()
sendValues.websiteTos = values.websiteTos.trim() === '' ? null : values.websiteTos.trim(); ? null
: values.websiteDefaultAvatar.trim();
sendValues.websiteTos =
values.websiteTos?.trim() === '' || !values.websiteTos?.trim() ? null : values.websiteTos.trim();
sendValues.websiteThemeDefault = values.websiteThemeDefault.trim(); sendValues.websiteThemeDefault = values.websiteThemeDefault.trim();
sendValues.websiteThemeDark = values.websiteThemeDark.trim(); sendValues.websiteThemeDark = values.websiteThemeDark.trim();
@@ -76,31 +79,7 @@ export default function Website({
return settingsOnSubmit(navigate, form)(sendValues); 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 ( return (
<>
<LoadingOverlay visible={isLoading} />
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
<Stack gap='lg'> <Stack gap='lg'>
<TextInput <TextInput
@@ -124,7 +103,14 @@ export default function Website({
minRows={1} minRows={1}
maxRows={7} maxRows={7}
autosize autosize
placeholder={JSON.stringify(defaultExternalLinks, null, 2)} placeholder={JSON.stringify(
[
{ name: 'GitHub', url: 'https://github.com/diced/zipline' },
{ name: 'Documentation', url: 'https://zipline.diced.sh' },
],
null,
2,
)}
{...form.getInputProps('websiteExternalLinks')} {...form.getInputProps('websiteExternalLinks')}
/> />
@@ -180,6 +166,5 @@ export default function Website({
Save Save
</Button> </Button>
</form> </form>
</>
); );
} }

View File

@@ -0,0 +1,6 @@
import type { Response } from '@/lib/api/response';
import useSWR from 'swr';
export default function useServerSettings() {
return useSWR<Response['/api/server/settings']>('/api/server/settings');
}

View File

@@ -25,7 +25,7 @@ import {
IconSettingsFilled, IconSettingsFilled,
IconX, IconX,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useEffect, useState } from 'react'; import { useState } from 'react';
export default function SettingsAvatar() { export default function SettingsAvatar() {
const user = useUserStore((state) => state.user); const user = useUserStore((state) => state.user);
@@ -36,14 +36,16 @@ export default function SettingsAvatar() {
const [avatar, setAvatar] = useState<File | null>(null); const [avatar, setAvatar] = useState<File | null>(null);
const [avatarSrc, setAvatarSrc] = useState<string | null>(null); const [avatarSrc, setAvatarSrc] = useState<string | null>(null);
useEffect(() => { const onAvatarChange = async (file: File | null) => {
(async () => { setAvatar(file);
if (!avatar) return;
const base64url = await readToDataURL(avatar); if (!file) {
setAvatarSrc(base64url); setAvatarSrc(null);
})(); return;
}, [avatar]); }
setAvatarSrc(await readToDataURL(file));
};
const saveAvatar = async () => { const saveAvatar = async () => {
if (!avatar) return; if (!avatar) return;
@@ -111,7 +113,7 @@ export default function SettingsAvatar() {
accept='image/*' accept='image/*'
placeholder='Upload new avatar...' placeholder='Upload new avatar...'
value={avatar} value={avatar}
onChange={(file) => setAvatar(file)} onChange={onAvatarChange}
leftSection={<IconPhotoUp size='1rem' />} leftSection={<IconPhotoUp size='1rem' />}
/> />

View File

@@ -1,3 +1,4 @@
import type { User } from '@/lib/db/models/user';
import { Response } from '@/lib/api/response'; import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi'; import { fetchApi } from '@/lib/fetchApi';
import { useUserStore } from '@/lib/client/store/user'; import { useUserStore } from '@/lib/client/store/user';
@@ -27,7 +28,6 @@ import {
IconDeviceFloppy, IconDeviceFloppy,
IconFileX, IconFileX,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useEffect } from 'react';
import { mutate } from 'swr'; import { mutate } from 'swr';
import { useShallow } from 'zustand/shallow'; import { useShallow } from 'zustand/shallow';
@@ -40,19 +40,34 @@ const alignIcons: Record<string, React.ReactNode> = {
export default function SettingsFileView() { export default function SettingsFileView() {
const [user, setUser] = useUserStore(useShallow((state) => [state.user, state.setUser])); const [user, setUser] = useUserStore(useShallow((state) => [state.user, state.setUser]));
if (!user) {
return (
<Paper withBorder p='sm'>
<Title order={2}>Viewing Files</Title>
<Text c='dimmed' mt='xs'>
Loading
</Text>
</Paper>
);
}
return <Form user={user} setUser={setUser} />;
}
function Form({ user, setUser }: { user: User; setUser: (u: User) => void }) {
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
enabled: user?.view.enabled ?? false, enabled: user.view.enabled || false,
content: user?.view.content ?? '', content: user.view.content || '',
embed: user?.view.embed ?? false, embed: user.view.embed || false,
embedTitle: user?.view.embedTitle ?? '', embedTitle: user.view.embedTitle || '',
embedDescription: user?.view.embedDescription ?? '', embedDescription: user.view.embedDescription || '',
embedSiteName: user?.view.embedSiteName ?? '', embedSiteName: user.view.embedSiteName || '',
embedColor: user?.view.embedColor ?? '', embedColor: user.view.embedColor || '',
align: user?.view.align ?? 'left', align: user.view.align || 'left',
showMimetype: user?.view.showMimetype ?? false, showMimetype: user.view.showMimetype || false,
showTags: user?.view.showTags ?? false, showTags: user.view.showTags || false,
showFolder: user?.view.showFolder ?? 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 ( return (
<Paper withBorder p='sm'> <Paper withBorder p='sm'>
<Title order={2}>Viewing Files</Title> <Title order={2}>Viewing Files</Title>

View File

@@ -1,3 +1,4 @@
import type { User } from '@/lib/db/models/user';
import { ApiError } from '@/lib/api/errors'; import { ApiError } from '@/lib/api/errors';
import { Response } from '@/lib/api/response'; import { Response } from '@/lib/api/response';
import { fetchApi } from '@/lib/fetchApi'; import { fetchApi } from '@/lib/fetchApi';
@@ -25,29 +26,36 @@ import {
IconUser, IconUser,
IconUserCancel, IconUserCancel,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { mutate } from 'swr'; import { mutate } from 'swr';
import useSWR from 'swr';
import { useShallow } from 'zustand/shallow'; import { useShallow } from 'zustand/shallow';
export default function SettingsUser() { export default function SettingsUser() {
const [user, setUser] = useUserStore(useShallow((state) => [state.user, state.setUser])); const [user, setUser] = useUserStore(useShallow((state) => [state.user, state.setUser]));
const [tokenShown, setTokenShown] = useState(false); const { data: tokenPayload } = useSWR<Response['/api/user/token']>('/api/user/token');
const [token, setToken] = useState('');
useEffect(() => { if (!user) {
(async () => { return (
const { data } = await fetchApi<Response['/api/user/token']>('/api/user/token'); <Paper withBorder p='sm'>
<Title order={2}>User</Title>
if (data) { <Text c='dimmed' size='sm' mt='sm'>
setToken(data.token || ''); Loading
</Text>
</Paper>
);
} }
})();
}, []); return <Form user={user} setUser={setUser} token={tokenPayload?.token ?? ''} />;
}
function Form({ user, setUser, token }: { user: User; setUser: (u: User) => void; token: string }) {
const [tokenShown, setTokenShown] = useState(false);
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
username: user?.username ?? '', username: user.username,
password: '', password: '',
}, },
validate: { validate: {
@@ -61,7 +69,7 @@ export default function SettingsUser() {
password?: string; 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(); if (values.password) send['password'] = values.password.trim();
const { data, error } = await fetchApi<Response['/api/user']>('/api/user', 'PATCH', send); const { data, error } = await fetchApi<Response['/api/user']>('/api/user', 'PATCH', send);
@@ -84,6 +92,7 @@ export default function SettingsUser() {
if (!data?.user) return; if (!data?.user) return;
mutate('/api/user'); mutate('/api/user');
mutate('/api/user/token');
setUser(data.user); setUser(data.user);
notifications.show({ notifications.show({
message: 'User updated', message: 'User updated',
@@ -96,7 +105,7 @@ export default function SettingsUser() {
<Paper withBorder p='sm'> <Paper withBorder p='sm'>
<Title order={2}>User</Title> <Title order={2}>User</Title>
<Text c='dimmed' size='sm' mb='sm'> <Text c='dimmed' size='sm' mb='sm'>
{user?.id} {user.id}
</Text> </Text>
<form onSubmit={form.onSubmit(onSubmit)}> <form onSubmit={form.onSubmit(onSubmit)}>
@@ -134,7 +143,7 @@ export default function SettingsUser() {
leftSection={<IconAsteriskSimple size='1rem' />} leftSection={<IconAsteriskSimple size='1rem' />}
/> />
<Button type='submit' mt='md' loading={!user} leftSection={<IconDeviceFloppy size='1rem' />}> <Button type='submit' mt='md' leftSection={<IconDeviceFloppy size='1rem' />}>
Save Save
</Button> </Button>
</form> </form>

View File

@@ -0,0 +1,24 @@
import type { Response } from '@/lib/api/response';
import useSWR from 'swr';
async function fetcher(url: string): Promise<Response['/api/user'] | null> {
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<Response['/api/user'] | null>('/api/user', fetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenHidden: false,
revalidateIfStale: false,
shouldRetryOnError: false,
});
return { user: data?.user, loading: isLoading };
}

View File

@@ -1,15 +1,16 @@
import useSWR from 'swr'; import useSWR from 'swr';
import { Response } from '../../api/response'; import { Response } from '../../api/response';
const f = async () => {
async function fetcher() {
const res = await fetch('/api/version'); const res = await fetch('/api/version');
if (!res.ok) throw new Error('Failed to fetch version'); if (!res.ok) throw new Error('Failed to fetch version');
const r = await res.json(); const r = await res.json();
return r; return r;
}; }
export default function useVersion() { export default function useVersion() {
const { isLoading, data } = useSWR<Response['/api/version'], Error>('/api/version', f, { const { isLoading, data } = useSWR<Response['/api/version'], Error>('/api/version', fetcher, {
refreshInterval: undefined, refreshInterval: undefined,
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateIfStale: false, revalidateIfStale: false,