refactor: refactor webui
This commit is contained in:
6
apps/webui/src/views/routes/404.tsx
Normal file
6
apps/webui/src/views/routes/404.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { AppNotFoundComponent } from '@/views/components/layout/app-not-found';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/404')({
|
||||
component: AppNotFoundComponent,
|
||||
});
|
||||
15
apps/webui/src/views/routes/__root.tsx
Normal file
15
apps/webui/src/views/routes/__root.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type {
|
||||
RouteStateDataOption,
|
||||
RouterContext,
|
||||
} from '@/infra/routes/traits';
|
||||
import { Outlet, createRootRouteWithContext } from '@tanstack/react-router';
|
||||
import { Home } from 'lucide-react';
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
component: Outlet,
|
||||
staticData: {
|
||||
breadcrumb: {
|
||||
icon: Home,
|
||||
},
|
||||
} satisfies RouteStateDataOption,
|
||||
});
|
||||
15
apps/webui/src/views/routes/_app/_explore/explore.tsx
Normal file
15
apps/webui/src/views/routes/_app/_explore/explore.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/_app/_explore/explore')({
|
||||
component: ExploreRouteComponent,
|
||||
staticData: {
|
||||
breadcrumb: {
|
||||
label: 'Explore',
|
||||
},
|
||||
} satisfies RouteStateDataOption,
|
||||
});
|
||||
|
||||
function ExploreRouteComponent() {
|
||||
return <div>Hello "/_app/explore"!</div>;
|
||||
}
|
||||
15
apps/webui/src/views/routes/_app/_explore/feed.tsx
Normal file
15
apps/webui/src/views/routes/_app/_explore/feed.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/_app/_explore/feed')({
|
||||
component: FeedRouteComponent,
|
||||
staticData: {
|
||||
breadcrumb: {
|
||||
label: 'Feed',
|
||||
},
|
||||
} satisfies RouteStateDataOption,
|
||||
});
|
||||
|
||||
function FeedRouteComponent() {
|
||||
return <div>Hello "/_app/feed"!</div>;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useAuth } from '@/app/auth/hooks';
|
||||
import { type Fetcher, createGraphiQLFetcher } from '@graphiql/toolkit';
|
||||
import { createLazyFileRoute } from '@tanstack/react-router';
|
||||
import { GraphiQL } from 'graphiql';
|
||||
import { useCallback } from 'react';
|
||||
import 'graphiql/graphiql.css';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
export const Route = createLazyFileRoute('/_app/playground/graphql-api')({
|
||||
component: PlaygroundGraphQLApiRouteComponent,
|
||||
});
|
||||
|
||||
function PlaygroundGraphQLApiRouteComponent() {
|
||||
const { authProvider } = useAuth();
|
||||
|
||||
const fetcher: Fetcher = useCallback(
|
||||
async (props) => {
|
||||
const authHeaders = await firstValueFrom(authProvider.getAuthHeaders());
|
||||
return createGraphiQLFetcher({
|
||||
url: '/api/graphql',
|
||||
headers: authHeaders,
|
||||
})(props);
|
||||
},
|
||||
[authProvider]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-id="graphiql-playground-container"
|
||||
className="h-full overflow-hidden rounded-lg"
|
||||
>
|
||||
<GraphiQL fetcher={fetcher} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { buildLeafRouteStaticData } from '@/infra/routes/utils';
|
||||
import { AppSkeleton } from '@/views/components/layout/app-skeleton';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/_app/playground/graphql-api')({
|
||||
staticData: buildLeafRouteStaticData({ title: 'GraphQL Api' }),
|
||||
pendingComponent: AppSkeleton,
|
||||
});
|
||||
8
apps/webui/src/views/routes/_app/playground/route.tsx
Normal file
8
apps/webui/src/views/routes/_app/playground/route.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { buildVirtualBranchRouteOptions } from '@/infra/routes/utils';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/_app/playground')(
|
||||
buildVirtualBranchRouteOptions({
|
||||
title: 'Playground',
|
||||
})
|
||||
);
|
||||
16
apps/webui/src/views/routes/_app/route.tsx
Normal file
16
apps/webui/src/views/routes/_app/route.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { beforeLoadGuard } from '@/app/auth/guard';
|
||||
import { AppAside } from '@/views/components/layout/app-layout';
|
||||
import { Outlet, createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/_app')({
|
||||
component: AppLayoutRoute,
|
||||
beforeLoad: beforeLoadGuard,
|
||||
});
|
||||
|
||||
function AppLayoutRoute() {
|
||||
return (
|
||||
<AppAside extractBreadcrumbFromRoutes>
|
||||
<Outlet />
|
||||
</AppAside>
|
||||
);
|
||||
}
|
||||
13
apps/webui/src/views/routes/_app/settings/downloader.tsx
Normal file
13
apps/webui/src/views/routes/_app/settings/downloader.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { buildLeafRouteStaticData } from '@/infra/routes/utils';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/_app/settings/downloader')({
|
||||
component: SettingsDownloaderRouteComponent,
|
||||
staticData: buildLeafRouteStaticData({
|
||||
title: 'Downloader',
|
||||
}),
|
||||
});
|
||||
|
||||
function SettingsDownloaderRouteComponent() {
|
||||
return <div>Hello "/_app/settings/downloader"!</div>;
|
||||
}
|
||||
8
apps/webui/src/views/routes/_app/settings/route.tsx
Normal file
8
apps/webui/src/views/routes/_app/settings/route.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { buildVirtualBranchRouteOptions } from '@/infra/routes/utils';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/_app/settings')(
|
||||
buildVirtualBranchRouteOptions({
|
||||
title: 'Settings',
|
||||
})
|
||||
);
|
||||
234
apps/webui/src/views/routes/_app/subscriptions/create.tsx
Normal file
234
apps/webui/src/views/routes/_app/subscriptions/create.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { useAuth } from '@/app/auth/hooks';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { Button } from '@/views/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/views/components/ui/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/views/components/ui/form';
|
||||
import { Input } from '@/views/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/views/components/ui/select';
|
||||
import { Switch } from '@/views/components/ui/switch';
|
||||
import { gql, useMutation } from '@apollo/client';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const Route = createFileRoute('/_app/subscriptions/create')({
|
||||
component: SubscriptionCreateRouteComponent,
|
||||
staticData: {
|
||||
breadcrumb: { label: 'Create' },
|
||||
} satisfies RouteStateDataOption,
|
||||
});
|
||||
|
||||
type SubscriptionFormValues = {
|
||||
displayName: string;
|
||||
sourceUrl: string;
|
||||
category: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
const CREATE_SUBSCRIPTION_MUTATION = gql`
|
||||
mutation CreateSubscription($input: SubscriptionsInsertInput!) {
|
||||
subscriptionsCreateOne(data: $input) {
|
||||
id
|
||||
displayName
|
||||
sourceUrl
|
||||
enabled
|
||||
category
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function SubscriptionCreateRouteComponent() {
|
||||
const { userData } = useAuth();
|
||||
console.log(JSON.stringify(userData, null, 2));
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const form = useForm<SubscriptionFormValues>({
|
||||
defaultValues: {
|
||||
displayName: '',
|
||||
sourceUrl: '',
|
||||
category: 'Mikan',
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [createSubscription] = useMutation(CREATE_SUBSCRIPTION_MUTATION);
|
||||
|
||||
const onSubmit = async (data: SubscriptionFormValues) => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const response = await createSubscription({
|
||||
variables: {
|
||||
input: {
|
||||
category: data.category,
|
||||
displayName: data.displayName,
|
||||
sourceUrl: data.sourceUrl,
|
||||
enabled: data.enabled,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (response.errors) {
|
||||
throw new Error(
|
||||
response.errors[0]?.message || 'Failed to create subscription'
|
||||
);
|
||||
}
|
||||
|
||||
toast.success('Subscription created successfully');
|
||||
navigate({ to: '/subscriptions/manage' });
|
||||
} catch (error) {
|
||||
console.error('Failed to create subscription:', error);
|
||||
toast.error(
|
||||
`Subscription creation failed: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create Bangumi Subscription</CardTitle>
|
||||
<CardDescription>Add a new bangumi subscription source</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="category"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Source Type</FormLabel>
|
||||
<Select
|
||||
disabled
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue="Mikan"
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select source type" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="Mikan">Mikan</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Currently only Mikan source is supported
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="displayName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Display Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter subscription display name"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set an easily recognizable name for this subscription
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sourceUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Source URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter subscription source URL"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Copy the RSS subscription link from the source website, e.g.
|
||||
https://mikanani.me/RSS/Bangumi?bangumiId=3141&subgroupid=370
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">
|
||||
Enable Subscription
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Enable this subscription immediately after creation
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate({ to: '/subscriptions/manage' })}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={form.handleSubmit(onSubmit)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Creating...' : 'Create Subscription'}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute(
|
||||
'/_app/subscriptions/edit/$subscription-id'
|
||||
)({
|
||||
component: RouteComponent,
|
||||
staticData: {
|
||||
breadcrumb: { label: 'Edit' },
|
||||
} satisfies RouteStateDataOption,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/subscriptions/edit/$subscription-id"!</div>;
|
||||
}
|
||||
13
apps/webui/src/views/routes/_app/subscriptions/manage.tsx
Normal file
13
apps/webui/src/views/routes/_app/subscriptions/manage.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/_app/subscriptions/manage')({
|
||||
component: SubscriptionManage,
|
||||
staticData: {
|
||||
breadcrumb: { label: 'Manage' },
|
||||
} satisfies RouteStateDataOption,
|
||||
});
|
||||
|
||||
function SubscriptionManage() {
|
||||
return <div>Hello "/subscriptions/manage"!</div>;
|
||||
}
|
||||
8
apps/webui/src/views/routes/_app/subscriptions/route.tsx
Normal file
8
apps/webui/src/views/routes/_app/subscriptions/route.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { buildVirtualBranchRouteOptions } from '@/infra/routes/utils';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/_app/subscriptions')(
|
||||
buildVirtualBranchRouteOptions({
|
||||
title: 'Subscriptions',
|
||||
})
|
||||
);
|
||||
9
apps/webui/src/views/routes/about.tsx
Normal file
9
apps/webui/src/views/routes/about.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/about')({
|
||||
component: About,
|
||||
});
|
||||
|
||||
function About() {
|
||||
return <div class="p-2">Hello from About!</div>;
|
||||
}
|
||||
94
apps/webui/src/views/routes/auth/oidc/callback.tsx
Normal file
94
apps/webui/src/views/routes/auth/oidc/callback.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { authContextFromInjector } from '@/app/auth/context';
|
||||
import { useAuth } from '@/app/auth/hooks';
|
||||
import { AUTH_METHOD } from '@/infra/auth/defs';
|
||||
import { ProLink } from '@/views/components/ui/pro-link';
|
||||
import { Spinner } from '@/views/components/ui/spinner';
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { useAtom } from 'jotai/react';
|
||||
import { atomWithObservable } from 'jotai/utils';
|
||||
import { EventTypes } from 'oidc-client-rx';
|
||||
import { useMemo } from 'react';
|
||||
import { filter, map } from 'rxjs';
|
||||
|
||||
export const Route = createFileRoute('/auth/oidc/callback')({
|
||||
component: OidcCallbackRouteComponent,
|
||||
beforeLoad: ({ context }) => {
|
||||
const { authProvider } = authContextFromInjector(context.injector);
|
||||
if (authProvider.authMethod !== AUTH_METHOD.OIDC) {
|
||||
throw redirect({ to: '/' });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function OidcCallbackRouteComponent() {
|
||||
const { authService } = useAuth();
|
||||
|
||||
const isLoading = useAtom(
|
||||
useMemo(
|
||||
() =>
|
||||
atomWithObservable(() =>
|
||||
authService.checkAuthResultEvent$.pipe(map(Boolean))
|
||||
),
|
||||
[authService.checkAuthResultEvent$]
|
||||
)
|
||||
);
|
||||
|
||||
const checkAuthResultError = useAtom(
|
||||
useMemo(
|
||||
() =>
|
||||
atomWithObservable(() =>
|
||||
authService.checkAuthResultEvent$.pipe(
|
||||
filter(
|
||||
(
|
||||
e
|
||||
): e is {
|
||||
type: EventTypes.CheckingAuthFinishedWithError;
|
||||
value: string;
|
||||
} => e.type === EventTypes.CheckingAuthFinishedWithError
|
||||
),
|
||||
map((e) => e.value)
|
||||
)
|
||||
),
|
||||
[authService.checkAuthResultEvent$]
|
||||
)
|
||||
);
|
||||
|
||||
const renderBackToHome = () => {
|
||||
return (
|
||||
<ProLink
|
||||
to="/"
|
||||
className="inline-flex h-10 items-center rounded-md border border-gray-200 bg-white px-8 font-medium text-sm shadow-sm transition-colors hover:bg-gray-100 hover:text-gray-900 dark:border-gray-800 dark:bg-gray-950 dark:focus-visible:ring-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-50"
|
||||
>
|
||||
Return to website
|
||||
</ProLink>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-svh items-center px-4 py-12 sm:px-6 md:px-8 lg:px-12 xl:px-16">
|
||||
<div className="w-full space-y-6 text-center">
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-center font-bold text-4xl tracking-tighter sm:text-5xl">
|
||||
<Spinner variant="circle-filled" size="48" />
|
||||
</div>
|
||||
{isLoading && (
|
||||
<p className="text-gray-500">
|
||||
Processing OIDC authentication callback...
|
||||
</p>
|
||||
)}
|
||||
{checkAuthResultError && (
|
||||
<p className="text-gray-500">
|
||||
Failed to handle OIDC callback: {checkAuthResultError}
|
||||
</p>
|
||||
)}
|
||||
{!isLoading && !checkAuthResultError && (
|
||||
<p className="text-gray-500">
|
||||
Succeed to handle OIDC authentication callback.
|
||||
</p>
|
||||
)}
|
||||
{renderBackToHome()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
apps/webui/src/views/routes/auth/sign-in.tsx
Normal file
9
apps/webui/src/views/routes/auth/sign-in.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/auth/sign-in')({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/auth/sign-in"!</div>;
|
||||
}
|
||||
9
apps/webui/src/views/routes/auth/sign-up.tsx
Normal file
9
apps/webui/src/views/routes/auth/sign-up.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/auth/sign-up')({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/auth/sign-up"!</div>;
|
||||
}
|
||||
15
apps/webui/src/views/routes/index.tsx
Normal file
15
apps/webui/src/views/routes/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { AppAside } from '@/views/components/layout/app-layout';
|
||||
import { AppSkeleton } from '@/views/components/layout/app-skeleton';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: HomeRouteComponent,
|
||||
});
|
||||
|
||||
function HomeRouteComponent() {
|
||||
return (
|
||||
<AppAside extractBreadcrumbFromRoutes>
|
||||
<AppSkeleton />
|
||||
</AppAside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user