mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 15:50:11 -08:00
feat: all functionality added + client/ -> src/client/
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
import Markdown from '@/components/render/Markdown';
|
||||
import { Response } from '@/lib/api/response';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export default function Tos() {
|
||||
const { data: config } = useSWR<Response['/api/server/public']>('/api/server/public', {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenHidden: false,
|
||||
revalidateIfStale: false,
|
||||
});
|
||||
|
||||
return <Markdown md={config?.tos || ''} />;
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import Layout from '@/components/Layout';
|
||||
import DashboardHome from '@/components/pages/dashboard';
|
||||
import DashboardFiles from '@/components/pages/files';
|
||||
import DashboardFolders from '@/components/pages/folders';
|
||||
import DashboardInvites from '@/components/pages/invites';
|
||||
import DashboardMetrics from '@/components/pages/metrics';
|
||||
import DashboardServerSettings from '@/components/pages/serverSettings';
|
||||
import DashboardSettings from '@/components/pages/settings';
|
||||
import UploadFile from '@/components/pages/upload/File';
|
||||
import UploadText from '@/components/pages/upload/Text';
|
||||
import DashboardURLs from '@/components/pages/urls';
|
||||
import DashboardUsers from '@/components/pages/users';
|
||||
import { Response as ApiResponse } from '@/lib/api/response';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import Login from './pages/auth/login';
|
||||
import Logout from './pages/auth/logout';
|
||||
import Register from './pages/auth/register';
|
||||
import Tos from './pages/auth/tos';
|
||||
import ViewFolderId from './pages/folder/[id]';
|
||||
import ViewFolderIdUpload from './pages/folder/[id]/upload';
|
||||
import Root from './Root';
|
||||
|
||||
export async function dashboardLoader(): Promise<ApiResponse['/api/server/settings/web']> {
|
||||
const res = await fetch('/api/server/settings/web');
|
||||
if (!res.ok) {
|
||||
throw new Response('Failed to load settings', { status: res.status });
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
console.log('Loaded settings:', data);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
Component: () => <Root />,
|
||||
path: '/',
|
||||
children: [
|
||||
{
|
||||
path: '/auth',
|
||||
children: [
|
||||
{ path: 'login', Component: Login },
|
||||
{ path: 'logout', Component: Logout },
|
||||
{ path: 'register', Component: Register },
|
||||
{ path: 'tos', Component: Tos },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
Component: Layout,
|
||||
loader: dashboardLoader,
|
||||
children: [
|
||||
{ index: true, Component: DashboardHome },
|
||||
{ path: 'metrics', Component: DashboardMetrics },
|
||||
{ path: 'settings', Component: DashboardSettings },
|
||||
{ path: 'files', Component: DashboardFiles },
|
||||
{ path: 'folders', Component: DashboardFolders },
|
||||
{ path: 'urls', Component: DashboardURLs },
|
||||
{
|
||||
path: 'upload',
|
||||
children: [
|
||||
{ path: 'file', Component: UploadFile },
|
||||
{ path: 'text', Component: UploadText },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
children: [
|
||||
{ path: 'invites', Component: DashboardInvites },
|
||||
{ path: 'users', Component: DashboardUsers },
|
||||
{ path: 'settings', Component: DashboardServerSettings },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'folder/:id',
|
||||
loader: async ({ params }) => {
|
||||
const res = await fetch(`/api/server/folder/${params.id}`);
|
||||
if (!res.ok) {
|
||||
throw new Response('Folder not found', { status: 404 });
|
||||
}
|
||||
return {
|
||||
folder: await res.json(),
|
||||
};
|
||||
},
|
||||
Component: ViewFolderId,
|
||||
},
|
||||
{
|
||||
path: 'folder/:id/upload',
|
||||
loader: async ({ params }) => {
|
||||
const res = await fetch(`/api/server/folder/${params.id}?upload=true`);
|
||||
if (!res.ok) {
|
||||
throw new Response('Folder not found', { status: 404 });
|
||||
}
|
||||
return {
|
||||
folder: await res.json(),
|
||||
};
|
||||
},
|
||||
Component: ViewFolderIdUpload,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
@@ -4,12 +4,13 @@
|
||||
"license": "MIT",
|
||||
"version": "4.2.1",
|
||||
"scripts": {
|
||||
"build": "pnpm run --stream build:prisma && pnpm run --stream build:server && pnpm run --stream build:client",
|
||||
"build": "pnpm run --stream build:types && pnpm run --stream build:prisma && pnpm run --stream build:server && pnpm run --stream build:client",
|
||||
"build:types": "tsc",
|
||||
"build:prisma": "prisma generate --no-hints",
|
||||
"build:server": "tsup",
|
||||
"build:client": "vite build && pnpm run --stream \"/^build-ssr:.*/\"",
|
||||
"build-ssr:view": "vite build --ssr ssr-view/server.tsx -m ssr-view --outDir ../build/ssr --emptyOutDir=false",
|
||||
"build-ssr:view-url": "vite build --ssr ssr-view-url/server.tsx -m ssr-view-url --outDir ../build/ssr --emptyOutDir=false",
|
||||
"build-ssr:view": "vite build --ssr ssr-view/server.tsx -m ssr-view --outDir ../../build/ssr --emptyOutDir=false",
|
||||
"build-ssr:view-url": "vite build --ssr ssr-view-url/server.tsx -m ssr-view-url --outDir ../../build/ssr --emptyOutDir=false",
|
||||
"dev": "cross-env NODE_ENV=development DEBUG=zipline tsx --require dotenv/config --enable-source-maps ./src/server",
|
||||
"dev:nd": "cross-env NODE_ENV=development tsx --require dotenv/config --enable-source-maps ./src/server",
|
||||
"dev:inspector": "cross-env NODE_ENV=development DEBUG=zipline tsx --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./src/server",
|
||||
|
||||
11
src/client/error/DashboardErrorBoundary.tsx
Normal file
11
src/client/error/DashboardErrorBoundary.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import GenericError from './GenericError';
|
||||
|
||||
export default function DashboardErrorBoundary(props: Record<string, any>) {
|
||||
return (
|
||||
<GenericError
|
||||
title='Dashboard Client Error'
|
||||
message='Something went wrong while loading the dashboard. Please try again later, or report this issue if it persists.'
|
||||
details={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
27
src/client/error/GenericError.tsx
Normal file
27
src/client/error/GenericError.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Container, Paper, Stack, Text, Title } from '@mantine/core';
|
||||
|
||||
export default function GenericError({
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
}: {
|
||||
title?: string;
|
||||
message?: string;
|
||||
details?: Record<string, any>;
|
||||
}) {
|
||||
return (
|
||||
<Container my='lg'>
|
||||
<Stack gap='xs'>
|
||||
<Title order={5}>{title || 'An error occurred'}</Title>
|
||||
<Text c='dimmed'>
|
||||
{message || 'Something went wrong. Please try again later, or report this issue if it persists.'}
|
||||
</Text>
|
||||
{details && (
|
||||
<Paper withBorder px={3} py={3}>
|
||||
<pre style={{ margin: 0 }}>{JSON.stringify(details, null, 2)}</pre>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
11
src/client/error/RootErrorBoundary.tsx
Normal file
11
src/client/error/RootErrorBoundary.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import GenericError from './GenericError';
|
||||
|
||||
export default function RootErrorBoundary(props: Record<string, any>) {
|
||||
return (
|
||||
<GenericError
|
||||
title='Dashboard Client Error'
|
||||
message='Something went wrong while loading the dashboard. Please try again later, or report this issue if it persists.'
|
||||
details={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { hydrateRoot } from 'react-dom/client';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from './routes';
|
||||
|
||||
@@ -9,9 +8,9 @@ import '@mantine/dates/styles.css';
|
||||
import '@mantine/dropzone/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import 'mantine-datatable/styles.css';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
hydrateRoot(
|
||||
document.getElementById('root')!,
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
26
src/client/pages/404.tsx
Normal file
26
src/client/pages/404.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Button, Center, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconArrowLeft } from '@tabler/icons-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function FourOhFour() {
|
||||
return (
|
||||
<Center h='100vh'>
|
||||
<Stack>
|
||||
<Title order={1}>404</Title>
|
||||
<Text c='dimmed' mt='-md'>
|
||||
Page not found
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
component={Link}
|
||||
to='/auth/login'
|
||||
color='blue'
|
||||
fullWidth
|
||||
leftSection={<IconArrowLeft size='1rem' />}
|
||||
>
|
||||
Go home
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@@ -32,15 +32,17 @@ import {
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Link, redirect, useLocation, useNavigate } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
import GenericError from '../../error/GenericError';
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const query = new URLSearchParams(location.search);
|
||||
const { user, mutate } = useLogin();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
data: config,
|
||||
error: configError,
|
||||
@@ -147,7 +149,7 @@ export default function Login() {
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate('/dashboard', { replace: true });
|
||||
navigate('/dashboard');
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
@@ -158,7 +160,7 @@ export default function Login() {
|
||||
);
|
||||
|
||||
if (provider) {
|
||||
navigate(`/api/auth/oauth/${provider.toLowerCase()}`, { replace: true });
|
||||
redirect(`/api/auth/oauth/${provider.toLowerCase()}`);
|
||||
}
|
||||
}
|
||||
}, [willRedirect, config]);
|
||||
@@ -178,25 +180,22 @@ export default function Login() {
|
||||
}
|
||||
}, [passkeyErrored]);
|
||||
|
||||
if (configLoading) {
|
||||
return <LoadingOverlay visible />;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (config?.firstSetup) navigate('/auth/setup');
|
||||
}, [config]);
|
||||
|
||||
if (configError) {
|
||||
return (
|
||||
<Center h='100vh'>
|
||||
<Text c='red'>Failed to load configuration. Please try again later.</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
if (configLoading) return <LoadingOverlay visible />;
|
||||
|
||||
if (!config) {
|
||||
if (configError)
|
||||
return (
|
||||
<Center h='100vh'>
|
||||
<Text c='red'>Configuration is not available. Please try again later.</Text>
|
||||
</Center>
|
||||
<GenericError
|
||||
title='Error loading configuration'
|
||||
message='Could not load server configuration...'
|
||||
details={configError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!config) return <LoadingOverlay visible />;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useUserStore } from '@/lib/store/user';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { mutate } from 'swr';
|
||||
|
||||
export default function Logout() {
|
||||
const navigate = useNavigate();
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -19,6 +19,8 @@ export default function Logout() {
|
||||
setUser(null);
|
||||
mutate('/api/user', null);
|
||||
navigate('/auth/login');
|
||||
} else {
|
||||
navigate('/dashboard');
|
||||
}
|
||||
} else {
|
||||
navigate('/dashboard');
|
||||
@@ -18,12 +18,13 @@ import { useForm } from '@mantine/form';
|
||||
import { notifications } 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 { Link, redirect, useLocation, useNavigate } from 'react-router-dom';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import GenericError from '../../error/GenericError';
|
||||
|
||||
export default function Register() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [invite, setInvite] = useState<any>(null);
|
||||
@@ -53,19 +54,17 @@ export default function Register() {
|
||||
},
|
||||
});
|
||||
|
||||
// Check if already logged in
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch('/api/user');
|
||||
if (res.ok) {
|
||||
navigate('/auth/login');
|
||||
redirect('/dashboard');
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// Fetch invite if present
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (!code) return;
|
||||
@@ -75,13 +74,13 @@ export default function Register() {
|
||||
const json = await res.json();
|
||||
setInvite(json.invite);
|
||||
} else {
|
||||
navigate('/auth/login');
|
||||
redirect('/auth/login');
|
||||
}
|
||||
})();
|
||||
}, [code]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!code && !config?.features.userRegistration) {
|
||||
if (!config?.features.userRegistration) {
|
||||
navigate('/auth/login');
|
||||
}
|
||||
}, [code, config]);
|
||||
@@ -120,24 +119,19 @@ export default function Register() {
|
||||
});
|
||||
|
||||
mutate('/api/user');
|
||||
navigate('/dashboard');
|
||||
redirect('/dashboard');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <LoadingOverlay visible />;
|
||||
if (loading || configLoading) return <LoadingOverlay visible />;
|
||||
|
||||
if (!config || configError || configLoading) {
|
||||
if (!config || configError) {
|
||||
return (
|
||||
<Center h='100vh'>
|
||||
<Paper p='xl' shadow='xl' withBorder>
|
||||
<Title order={2} ta='center'>
|
||||
Failed to load configuration
|
||||
</Title>
|
||||
<Text ta='center' size='sm' c='dimmed'>
|
||||
Please try again later.
|
||||
</Text>
|
||||
</Paper>
|
||||
</Center>
|
||||
<GenericError
|
||||
title='Error loading configuration'
|
||||
message='Could not load server configuration...'
|
||||
details={configError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
232
src/client/pages/auth/setup.tsx
Normal file
232
src/client/pages/auth/setup.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import {
|
||||
Anchor,
|
||||
Button,
|
||||
Code,
|
||||
Group,
|
||||
Paper,
|
||||
PasswordInput,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Stepper,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconArrowBackUp, IconArrowForwardUp, IconCheck, IconX } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { mutate } from 'swr';
|
||||
|
||||
function LinkToDoc({ href, title, children }: { href: string; title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<Text>
|
||||
<Anchor href={href} target='_blank' rel='noopener noreferrer'>
|
||||
{title}
|
||||
</Anchor>{' '}
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Setup() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [active, setActive] = useState(0);
|
||||
const nextStep = () => setActive((current) => (current < 3 ? current + 1 : current));
|
||||
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
validate: {
|
||||
username: (value) => (value.length < 1 ? 'Username is required' : null),
|
||||
password: (value) => (value.length < 1 ? 'Password is required' : null),
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (values: typeof form.values) => {
|
||||
setLoading(true);
|
||||
|
||||
const { error } = await fetchApi('/api/setup', 'POST', {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: error.error,
|
||||
color: 'red',
|
||||
icon: <IconX size='1rem' />,
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
setActive(2);
|
||||
} else {
|
||||
notifications.show({
|
||||
title: 'Setup complete!',
|
||||
message: 'Logging in to new user...',
|
||||
color: 'green',
|
||||
loading: true,
|
||||
});
|
||||
|
||||
const { data, error } = await fetchApi<Response['/api/auth/login']>('/api/auth/login', 'POST', {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: error.error,
|
||||
color: 'red',
|
||||
icon: <IconX size='1rem' />,
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
setActive(2);
|
||||
} else {
|
||||
mutate('/api/user', data as Response['/api/user']);
|
||||
navigate('/dashboard');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper withBorder p='xs' m='sm'>
|
||||
<Stepper active={active} onStepClick={setActive} m='md'>
|
||||
<Stepper.Step label='Welcome!' description='Setup Zipline'>
|
||||
<Title>Welcome to Zipline!</Title>
|
||||
<SimpleGrid spacing='md' cols={{ base: 1, sm: 1 }}>
|
||||
<Paper withBorder p='sm' my='sm' h='100%'>
|
||||
<Title order={2}>Documentation</Title>
|
||||
<Text>Here are a couple of useful documentation links to get you started with Zipline:</Text>
|
||||
|
||||
<Stack mt='xs'>
|
||||
<LinkToDoc href='https://zipline.diced.sh/docs/config' title='Configuration'>
|
||||
Configuring Zipline to your needs
|
||||
</LinkToDoc>
|
||||
|
||||
<LinkToDoc href='https://zipline.diced.sh/docs/migrate' title='Migrate from v3 to v4'>
|
||||
Upgrading from a previous version of Zipline
|
||||
</LinkToDoc>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Paper withBorder p='sm' my='sm' h='100%'>
|
||||
<Title order={2}>Configuration</Title>
|
||||
|
||||
<Text>
|
||||
Most of Zipline's configuration is now managed through the dashboard. Once you login as
|
||||
a super-admin, you can click on your username in the top right corner and select
|
||||
"Server Settings" to configure your instance. The only exception to this is a few
|
||||
sensitive environment variables that must be set in order for Zipline to run. To change
|
||||
this, depending on the setup, you can either edit the <Code>.env</Code> or{' '}
|
||||
<Code>docker-compose.yml</Code> file.
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
To see all of the available environment variables, please refer to the documentation{' '}
|
||||
<Anchor
|
||||
href='https://zipline.diced.sh/docs/config'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
here.
|
||||
</Anchor>
|
||||
</Text>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button
|
||||
mt='xl'
|
||||
fullWidth
|
||||
rightSection={<IconArrowForwardUp size='1.25rem' />}
|
||||
size='lg'
|
||||
variant='default'
|
||||
onClick={nextStep}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stepper.Step>
|
||||
<Stepper.Step label='Create user' description='Create a super-admin account'>
|
||||
<Stack gap='lg'>
|
||||
<Title order={2}>Create your super-admin account</Title>
|
||||
|
||||
<TextInput
|
||||
label='Username'
|
||||
placeholder='Enter a username...'
|
||||
{...form.getInputProps('username')}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label='Password'
|
||||
placeholder='Enter a password...'
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Group justify='space-between' my='lg'>
|
||||
<Button
|
||||
leftSection={<IconArrowBackUp size='1.25rem' />}
|
||||
size='lg'
|
||||
variant='default'
|
||||
onClick={prevStep}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
rightSection={<IconArrowForwardUp size='1.25rem' />}
|
||||
size='lg'
|
||||
variant='default'
|
||||
onClick={nextStep}
|
||||
disabled={!form.isValid()}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Group>
|
||||
</Stepper.Step>
|
||||
<Stepper.Completed>
|
||||
<Title order={2}>Setup complete!</Title>
|
||||
|
||||
<Text>
|
||||
Clicking "Finish" below will create your super-admin account and log you in. You will
|
||||
be redirected to the dashboard shortly after that.
|
||||
</Text>
|
||||
<Group justify='space-between' my='lg'>
|
||||
<Button
|
||||
leftSection={<IconArrowBackUp size='1.25rem' />}
|
||||
size='lg'
|
||||
variant='default'
|
||||
onClick={prevStep}
|
||||
loading={loading}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
rightSection={<IconCheck size='1.25rem' />}
|
||||
size='lg'
|
||||
variant='default'
|
||||
loading={loading}
|
||||
onClick={() => form.onSubmit(onSubmit)()}
|
||||
>
|
||||
Finish
|
||||
</Button>
|
||||
</Group>
|
||||
</Stepper.Completed>
|
||||
</Stepper>
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
36
src/client/pages/auth/tos.tsx
Normal file
36
src/client/pages/auth/tos.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import Markdown from '@/components/render/Markdown';
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { Container, LoadingOverlay } from '@mantine/core';
|
||||
import useSWR from 'swr';
|
||||
import GenericError from '../../error/GenericError';
|
||||
|
||||
export default function Tos() {
|
||||
const {
|
||||
data: config,
|
||||
error,
|
||||
isLoading,
|
||||
} = useSWR<Response['/api/server/public']>('/api/server/public', {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
refreshWhenHidden: false,
|
||||
revalidateIfStale: false,
|
||||
});
|
||||
|
||||
if (isLoading) return <LoadingOverlay visible />;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<GenericError
|
||||
title='Error loading TOS'
|
||||
message='Could not load Terms of Service file...'
|
||||
details={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container my='lg'>
|
||||
<Markdown md={config?.tos || ''} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export default function ViewFolderId() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Container my='lg'>
|
||||
<Group>
|
||||
<Title order={1}>{folder.name}</Title>
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function ViewFolderIdUpload() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Container my='lg'>
|
||||
<ConfigProvider data={{ config: config as unknown as SafeConfig, codeMap: [] }}>
|
||||
<UploadFile title={`Upload files to ${folder.name}`} folder={folder.id} />
|
||||
<Center>
|
||||
@@ -24,7 +24,7 @@ import { IconDownload, IconExternalLink, IconInfoCircleFilled } from '@tabler/ic
|
||||
import * as sanitize from 'isomorphic-dompurify';
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useSsrData } from '../../../src/components/ZiplineSSRProvider';
|
||||
import { useSsrData } from '../../../components/ZiplineSSRProvider';
|
||||
import { getFile } from '../../ssr-view/server';
|
||||
|
||||
type SsrData = {
|
||||
161
src/client/routes.tsx
Normal file
161
src/client/routes.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import Layout from '@/components/Layout';
|
||||
import DashboardHome from '@/components/pages/dashboard';
|
||||
import DashboardFiles from '@/components/pages/files';
|
||||
import DashboardFolders from '@/components/pages/folders';
|
||||
import DashboardInvites from '@/components/pages/invites';
|
||||
import DashboardMetrics from '@/components/pages/metrics';
|
||||
import DashboardServerSettings from '@/components/pages/serverSettings';
|
||||
import DashboardSettings from '@/components/pages/settings';
|
||||
import UploadFile from '@/components/pages/upload/File';
|
||||
import UploadText from '@/components/pages/upload/Text';
|
||||
import DashboardURLs from '@/components/pages/urls';
|
||||
import DashboardUsers from '@/components/pages/users';
|
||||
import ViewUserFiles from '@/components/pages/users/ViewUserFiles';
|
||||
import { Response as ApiResponse } from '@/lib/api/response';
|
||||
import { createBrowserRouter, redirect } from 'react-router-dom';
|
||||
import FourOhFour from './pages/404';
|
||||
import Login from './pages/auth/login';
|
||||
import Logout from './pages/auth/logout';
|
||||
import Register from './pages/auth/register';
|
||||
import Tos from './pages/auth/tos';
|
||||
import ViewFolderId from './pages/folder/[id]';
|
||||
import ViewFolderIdUpload from './pages/folder/[id]/upload';
|
||||
import Root from './Root';
|
||||
import DashboardErrorBoundary from './error/DashboardErrorBoundary';
|
||||
import RootErrorBoundary from './error/RootErrorBoundary';
|
||||
import Setup from './pages/auth/setup';
|
||||
|
||||
export async function dashboardLoader() {
|
||||
const res = await fetch('/api/server/settings/web');
|
||||
if (!res.ok) {
|
||||
return redirect('/auth/login');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
console.log('Loaded settings:', data);
|
||||
|
||||
return data as ApiResponse['/api/server/settings/web'];
|
||||
}
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
Component: Root,
|
||||
|
||||
path: '/',
|
||||
children: [
|
||||
{
|
||||
ErrorBoundary: RootErrorBoundary,
|
||||
children: [
|
||||
{ path: '*', Component: FourOhFour },
|
||||
{
|
||||
path: '/auth',
|
||||
children: [
|
||||
{ path: 'login', Component: Login },
|
||||
{ path: 'logout', Component: Logout },
|
||||
{ path: 'register', Component: Register },
|
||||
{
|
||||
path: 'setup',
|
||||
Component: Setup,
|
||||
loader: async () => {
|
||||
const res = await fetch('/api/server/public');
|
||||
if (!res.ok) {
|
||||
throw new Response('Failed to fetch server settings', { status: res.status });
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (!data.firstSetup) return redirect('/auth/login');
|
||||
|
||||
return {};
|
||||
},
|
||||
},
|
||||
{ path: 'tos', Component: Tos },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
Component: Layout,
|
||||
loader: dashboardLoader,
|
||||
children: [
|
||||
{
|
||||
ErrorBoundary: DashboardErrorBoundary,
|
||||
children: [
|
||||
{ index: true, Component: DashboardHome },
|
||||
{ path: 'metrics', Component: DashboardMetrics },
|
||||
{ path: 'settings', Component: DashboardSettings },
|
||||
{ path: 'files', Component: DashboardFiles },
|
||||
{ path: 'folders', Component: DashboardFolders },
|
||||
{ path: 'urls', Component: DashboardURLs },
|
||||
{
|
||||
path: 'upload',
|
||||
children: [
|
||||
{ path: 'file', Component: UploadFile },
|
||||
{ path: 'text', Component: UploadText },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'admin',
|
||||
children: [
|
||||
{ path: 'invites', Component: DashboardInvites },
|
||||
{ path: 'settings', Component: DashboardServerSettings },
|
||||
{
|
||||
path: 'users',
|
||||
children: [
|
||||
{ index: true, Component: DashboardUsers },
|
||||
{
|
||||
path: ':id/files',
|
||||
loader: async ({ params }) => {
|
||||
const res = await fetch('/api/users/' + params.id);
|
||||
if (!res.ok) {
|
||||
console.log("can't get user", res.status);
|
||||
return redirect('/dashboard/admin/users');
|
||||
}
|
||||
|
||||
const user = await res.json();
|
||||
return { user };
|
||||
},
|
||||
Component: ViewUserFiles,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'folder/:id',
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
loader: async ({ params }) => {
|
||||
const res = await fetch(`/api/server/folder/${params.id}`);
|
||||
if (!res.ok) {
|
||||
throw new Response('Folder not found', { status: 404 });
|
||||
}
|
||||
return {
|
||||
folder: await res.json(),
|
||||
};
|
||||
},
|
||||
Component: ViewFolderId,
|
||||
},
|
||||
{
|
||||
path: 'upload',
|
||||
loader: async ({ params }) => {
|
||||
const res = await fetch(`/api/server/folder/${params.id}?upload=true`);
|
||||
if (!res.ok) {
|
||||
throw new Response('Folder not found', { status: 404 });
|
||||
}
|
||||
return {
|
||||
folder: await res.json(),
|
||||
};
|
||||
},
|
||||
Component: ViewFolderIdUpload,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
@@ -20,21 +20,7 @@ const initialData = (window as any)[ZIPLINE_SSR_PROP];
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<ZiplineSSRProvider ssrData={initialData}>
|
||||
<RouterProvider
|
||||
// router={createBrowserRouter([
|
||||
// {
|
||||
// path: '/view',
|
||||
// Component: () => <Root />,
|
||||
// children: [
|
||||
// {
|
||||
// path: ':id',
|
||||
// Component: () => <ViewFileId />,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ])}
|
||||
router={router}
|
||||
/>
|
||||
<RouterProvider router={router} />
|
||||
</ZiplineSSRProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import { dashboardLoader } from '../../client/routes';
|
||||
import { dashboardLoader } from '../client/routes';
|
||||
|
||||
type ConfigContextType = Awaited<ReturnType<typeof dashboardLoader>>;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ import { useState } from 'react';
|
||||
import ConfigProvider from './ConfigProvider';
|
||||
import VersionBadge from './VersionBadge';
|
||||
import { Link, useLoaderData } from 'react-router-dom';
|
||||
import { dashboardLoader } from '../../client/routes';
|
||||
import { dashboardLoader } from '../client/routes';
|
||||
|
||||
type NavLinks = {
|
||||
label: string;
|
||||
|
||||
@@ -15,7 +15,6 @@ export default function DashboardHome() {
|
||||
const { user } = useLogin();
|
||||
const { data: recent, isLoading: recentLoading } = useSWR<Response['/api/user/recent']>('/api/user/recent');
|
||||
const { data: stats, isLoading: statsLoading } = useSWR<Response['/api/user/stats']>('/api/user/stats');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import DashboardFile from '@/components/file/DashboardFile';
|
||||
import { useQueryState } from '@/lib/hooks/useQueryState';
|
||||
import {
|
||||
Accordion,
|
||||
Button,
|
||||
@@ -11,13 +13,11 @@ import {
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconFileUpload, IconFilesOff } from '@tabler/icons-react';
|
||||
import { useApiPagination } from '../useApiPagination';
|
||||
import DashboardFile from '@/components/file/DashboardFile';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import { useApiPagination } from '../useApiPagination';
|
||||
|
||||
export default function FavoriteFiles() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [page, setPage] = useQueryState('fpage', 1);
|
||||
|
||||
const { data, isLoading } = useApiPagination({
|
||||
page,
|
||||
|
||||
@@ -39,11 +39,12 @@ import {
|
||||
} from '@tabler/icons-react';
|
||||
import { DataTable } from 'mantine-datatable';
|
||||
import { useEffect, useReducer, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
import { bulkDelete, bulkFavorite } from '../bulk';
|
||||
import TagPill from '../tags/TagPill';
|
||||
import { useApiPagination } from '../useApiPagination';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useQueryState } from '@/lib/hooks/useQueryState';
|
||||
|
||||
type ReducerQuery = {
|
||||
state: { name: string; originalName: string; type: string; tags: string; id: string };
|
||||
@@ -185,8 +186,7 @@ export default function FileTable({ id }: { id?: string }) {
|
||||
'/api/user/folders?noincl=true',
|
||||
);
|
||||
|
||||
// const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
|
||||
const [page, setPage] = useState(1);
|
||||
const [page, setPage] = useQueryState('page', 1);
|
||||
const [perpage, setPerpage] = useState(20);
|
||||
const [sort, setSort] = useState<
|
||||
| 'id'
|
||||
|
||||
@@ -16,11 +16,12 @@ import { useEffect, useState } from 'react';
|
||||
import { useApiPagination } from '../useApiPagination';
|
||||
import DashboardFile from '@/components/file/DashboardFile';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useQueryState } from '@/lib/hooks/useQueryState';
|
||||
|
||||
const PER_PAGE_OPTIONS = [9, 12, 15, 30, 45];
|
||||
|
||||
export default function Files({ id }: { id?: string }) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [page, setPage] = useQueryState('page', 1);
|
||||
const [perpage, setPerpage] = useState(15);
|
||||
const [cachedPages, setCachedPages] = useState(1);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import DashboardFile from '@/components/file/DashboardFile';
|
||||
import { useQueryState } from '@/lib/hooks/useQueryState';
|
||||
import {
|
||||
Accordion,
|
||||
Button,
|
||||
@@ -12,12 +13,11 @@ import {
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconFileUpload, IconFilesOff } from '@tabler/icons-react';
|
||||
import { useApiPagination } from '../files/useApiPagination';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import { useApiPagination } from '../files/useApiPagination';
|
||||
|
||||
export default function FavoriteFiles() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [page, setPage] = useQueryState('fpage', 1);
|
||||
const { data, isLoading } = useApiPagination({
|
||||
page,
|
||||
favorite: true,
|
||||
|
||||
@@ -5,9 +5,15 @@ import { ActionIcon, Group, Title, Tooltip } from '@mantine/core';
|
||||
import FileTable from '../files/views/FileTable';
|
||||
import Files from '../files/views/Files';
|
||||
import { IconArrowBackUp } from '@tabler/icons-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useLoaderData } from 'react-router-dom';
|
||||
|
||||
export default function ViewFiles({ user }: { user: User }) {
|
||||
export default function ViewUserFiles() {
|
||||
const data = useLoaderData<{
|
||||
user: User;
|
||||
}>();
|
||||
if (!data) return null;
|
||||
|
||||
const { user } = data;
|
||||
if (!user) return null;
|
||||
|
||||
const view = useViewStore((state) => state.files);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
import { redirect } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
import type { Response } from '../api/response';
|
||||
import { useUserStore } from '../store/user';
|
||||
import { isAdministrator } from '../role';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { Response } from '../api/response';
|
||||
import { isAdministrator } from '../role';
|
||||
import { useUserStore } from '../store/user';
|
||||
|
||||
export default function useLogin(administratorOnly: boolean = false) {
|
||||
const navigate = useNavigate();
|
||||
const { data, error, isLoading, mutate } = useSWR<Response['/api/user']>('/api/user', {
|
||||
fallbackData: { user: undefined },
|
||||
});
|
||||
@@ -18,13 +17,13 @@ export default function useLogin(administratorOnly: boolean = false) {
|
||||
if (data?.user) {
|
||||
setUser(data.user);
|
||||
} else if (error) {
|
||||
navigate('/auth/login');
|
||||
redirect('/auth/login');
|
||||
}
|
||||
}, [data, error]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && administratorOnly && !isAdministrator(user.role)) {
|
||||
navigate('/dashboard');
|
||||
redirect('/dashboard');
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
|
||||
37
src/lib/hooks/useQueryState.ts
Normal file
37
src/lib/hooks/useQueryState.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
function parseValue<T>(value: string | null, defaultValue: T): T {
|
||||
if (value === null) return defaultValue;
|
||||
|
||||
if (typeof defaultValue === 'number') {
|
||||
const parsed = Number(value);
|
||||
return isNaN(parsed) ? defaultValue : (parsed as T);
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'boolean') {
|
||||
return (value === 'true') as T;
|
||||
}
|
||||
|
||||
return value as T;
|
||||
}
|
||||
|
||||
export function useQueryState<T>(key: string, defaultValue: T): [T, (value: T | null) => void] {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const rawValue = searchParams.get(key);
|
||||
const value: T = parseValue(rawValue, defaultValue);
|
||||
|
||||
const setValue = (newValue: T | null) => {
|
||||
setSearchParams((prev) => {
|
||||
const next = new URLSearchParams(prev);
|
||||
if (newValue === null) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.set(key, String(newValue));
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return [value, setValue];
|
||||
}
|
||||
@@ -177,6 +177,18 @@ async function main() {
|
||||
server.get('/', (_, res) => res.redirect('/dashboard', 301));
|
||||
}
|
||||
|
||||
server.setNotFoundHandler((req, res) => {
|
||||
if (req.url.startsWith('/api/')) {
|
||||
return res.status(404).send({
|
||||
message: `Route ${req.method}:${req.url} not found`,
|
||||
error: 'Not Found',
|
||||
statusCode: 404,
|
||||
});
|
||||
} else {
|
||||
return res.serveIndex();
|
||||
}
|
||||
});
|
||||
|
||||
server.setErrorHandler((error, _, res) => {
|
||||
if (error.statusCode) {
|
||||
res.status(error.statusCode);
|
||||
|
||||
@@ -19,6 +19,7 @@ async function vitePlugin(fastify: FastifyInstance) {
|
||||
|
||||
if (MODE === 'production') {
|
||||
fastify.decorate('serveIndex', route);
|
||||
fastify.decorateReply('serveIndex', serveIndex);
|
||||
|
||||
await fastify.register(fastifyStatic, {
|
||||
root: resolve('./build/client'),
|
||||
@@ -61,7 +62,7 @@ async function vitePlugin(fastify: FastifyInstance) {
|
||||
let render: (response: any, url: string) => Promise<ReturnType<typeof renderHtml>>;
|
||||
|
||||
if (MODE === 'development' && fastify.vite) {
|
||||
template = await readFile(resolve(`./client/ssr-${type}/`, 'index.html'), 'utf-8');
|
||||
template = await readFile(resolve(`./src/client/ssr-${type}/`, 'index.html'), 'utf-8');
|
||||
template = await fastify.vite.transformIndexHtml(url, template);
|
||||
render = (await fastify.vite.ssrLoadModule(`/ssr-${type}/server.tsx`)).render;
|
||||
} else {
|
||||
@@ -101,10 +102,13 @@ async function vitePlugin(fastify: FastifyInstance) {
|
||||
});
|
||||
|
||||
async function handler(_: FastifyRequest, reply: FastifyReply) {
|
||||
const template = await readFile(resolve('./build/client', 'index.html'), 'utf8');
|
||||
reply.type('text/html').send(template);
|
||||
return reply.serveIndex();
|
||||
}
|
||||
}
|
||||
|
||||
async function serveIndex(this: FastifyReply) {
|
||||
return this.sendFile('index.html', resolve('./build/client'));
|
||||
}
|
||||
}
|
||||
|
||||
export default fastifyPlugin(vitePlugin, {
|
||||
@@ -120,5 +124,6 @@ declare module 'fastify' {
|
||||
|
||||
interface FastifyReply {
|
||||
ssr: (type: 'view-url' | 'view') => Promise<void>;
|
||||
serveIndex: () => Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { Config } from '@/lib/config/validate';
|
||||
import { getZipline } from '@/lib/db/models/zipline';
|
||||
import { log } from '@/lib/logger';
|
||||
import enabled from '@/lib/oauth/enabled';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
@@ -33,6 +34,7 @@ export type ApiServerPublicResponse = {
|
||||
maxFileSize: string;
|
||||
};
|
||||
chunks: Config['chunks'];
|
||||
firstSetup: boolean;
|
||||
};
|
||||
|
||||
const logger = log('api').c('server').c('public');
|
||||
@@ -43,6 +45,8 @@ export const PATH = '/api/server/public';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get<{ Body: Body }>(PATH, async (req, res) => {
|
||||
const zipline = await getZipline();
|
||||
|
||||
const response: ApiServerPublicResponse = {
|
||||
oauth: {
|
||||
bypassLocalLogin: config.oauth.bypassLocalLogin,
|
||||
@@ -65,6 +69,7 @@ export default fastifyPlugin(
|
||||
maxFileSize: config.files.maxFileSize,
|
||||
},
|
||||
chunks: config.chunks,
|
||||
firstSetup: zipline.firstSetup,
|
||||
};
|
||||
|
||||
if (config.website.tos) {
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
"@/*": ["./src/*", "./generated/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "client/mount.js"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules", "uploads"]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineConfig(async (_) => {
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
entry: await glob('./src/**/*.ts', {
|
||||
ignore: ['./src/components/**/*.ts'],
|
||||
ignore: ['./src/components/**/*.ts', './src/client/**/*.(ts|tsx|html)'],
|
||||
}),
|
||||
shims: true,
|
||||
esbuildPlugins: [],
|
||||
|
||||
@@ -7,9 +7,9 @@ export default defineConfig(({ mode }) => {
|
||||
if (mode === 'development')
|
||||
return {
|
||||
plugins: [react()],
|
||||
root: 'client',
|
||||
root: './src/client',
|
||||
build: {
|
||||
outDir: '../build/client',
|
||||
outDir: '../../build/client',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
format: 'cjs',
|
||||
@@ -28,18 +28,15 @@ export default defineConfig(({ mode }) => {
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
root: 'client',
|
||||
root: './src/client',
|
||||
build: {
|
||||
outDir: '../build/client',
|
||||
outDir: '../../build/client',
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: path.resolve(__dirname, 'client/index.html'),
|
||||
'ssr-view': path.resolve(__dirname, 'client/ssr-view/index.html'),
|
||||
'ssr-view-url': path.resolve(__dirname, 'client/ssr-view-url/index.html'),
|
||||
main: path.resolve(__dirname, 'src/client/index.html'),
|
||||
'ssr-view': path.resolve(__dirname, 'src/client/ssr-view/index.html'),
|
||||
'ssr-view-url': path.resolve(__dirname, 'src/client/ssr-view-url/index.html'),
|
||||
},
|
||||
...(mode.startsWith('ssr') && {
|
||||
output: {
|
||||
@@ -57,5 +54,3 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Vite configuration loaded');
|
||||
|
||||
Reference in New Issue
Block a user