refactor: refactor subscriptions

This commit is contained in:
2025-06-03 02:21:49 +08:00
parent 5645645c5f
commit a3fd03d32a
30 changed files with 2612 additions and 2034 deletions

View File

@@ -32,8 +32,8 @@ import { Route as AppCredential3rdCreateImport } from './routes/_app/credential3
import { Route as AppBangumiManageImport } from './routes/_app/bangumi/manage'
import { Route as AppExploreFeedImport } from './routes/_app/_explore/feed'
import { Route as AppExploreExploreImport } from './routes/_app/_explore/explore'
import { Route as AppSubscriptionsEditSubscriptionIdImport } from './routes/_app/subscriptions/edit.$subscriptionId'
import { Route as AppSubscriptionsDetailSubscriptionIdImport } from './routes/_app/subscriptions/detail.$subscriptionId'
import { Route as AppSubscriptionsEditIdImport } from './routes/_app/subscriptions/edit.$id'
import { Route as AppSubscriptionsDetailIdImport } from './routes/_app/subscriptions/detail.$id'
import { Route as AppCredential3rdEditIdImport } from './routes/_app/credential3rd/edit.$id'
import { Route as AppCredential3rdDetailIdImport } from './routes/_app/credential3rd/detail.$id'
@@ -166,19 +166,17 @@ const AppExploreExploreRoute = AppExploreExploreImport.update({
getParentRoute: () => AppRouteRoute,
} as any)
const AppSubscriptionsEditSubscriptionIdRoute =
AppSubscriptionsEditSubscriptionIdImport.update({
id: '/edit/$subscriptionId',
path: '/edit/$subscriptionId',
getParentRoute: () => AppSubscriptionsRouteRoute,
} as any)
const AppSubscriptionsEditIdRoute = AppSubscriptionsEditIdImport.update({
id: '/edit/$id',
path: '/edit/$id',
getParentRoute: () => AppSubscriptionsRouteRoute,
} as any)
const AppSubscriptionsDetailSubscriptionIdRoute =
AppSubscriptionsDetailSubscriptionIdImport.update({
id: '/detail/$subscriptionId',
path: '/detail/$subscriptionId',
getParentRoute: () => AppSubscriptionsRouteRoute,
} as any)
const AppSubscriptionsDetailIdRoute = AppSubscriptionsDetailIdImport.update({
id: '/detail/$id',
path: '/detail/$id',
getParentRoute: () => AppSubscriptionsRouteRoute,
} as any)
const AppCredential3rdEditIdRoute = AppCredential3rdEditIdImport.update({
id: '/edit/$id',
@@ -357,18 +355,18 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppCredential3rdEditIdImport
parentRoute: typeof AppCredential3rdRouteImport
}
'/_app/subscriptions/detail/$subscriptionId': {
id: '/_app/subscriptions/detail/$subscriptionId'
path: '/detail/$subscriptionId'
fullPath: '/subscriptions/detail/$subscriptionId'
preLoaderRoute: typeof AppSubscriptionsDetailSubscriptionIdImport
'/_app/subscriptions/detail/$id': {
id: '/_app/subscriptions/detail/$id'
path: '/detail/$id'
fullPath: '/subscriptions/detail/$id'
preLoaderRoute: typeof AppSubscriptionsDetailIdImport
parentRoute: typeof AppSubscriptionsRouteImport
}
'/_app/subscriptions/edit/$subscriptionId': {
id: '/_app/subscriptions/edit/$subscriptionId'
path: '/edit/$subscriptionId'
fullPath: '/subscriptions/edit/$subscriptionId'
preLoaderRoute: typeof AppSubscriptionsEditSubscriptionIdImport
'/_app/subscriptions/edit/$id': {
id: '/_app/subscriptions/edit/$id'
path: '/edit/$id'
fullPath: '/subscriptions/edit/$id'
preLoaderRoute: typeof AppSubscriptionsEditIdImport
parentRoute: typeof AppSubscriptionsRouteImport
}
}
@@ -432,17 +430,15 @@ const AppSettingsRouteRouteWithChildren =
interface AppSubscriptionsRouteRouteChildren {
AppSubscriptionsCreateRoute: typeof AppSubscriptionsCreateRoute
AppSubscriptionsManageRoute: typeof AppSubscriptionsManageRoute
AppSubscriptionsDetailSubscriptionIdRoute: typeof AppSubscriptionsDetailSubscriptionIdRoute
AppSubscriptionsEditSubscriptionIdRoute: typeof AppSubscriptionsEditSubscriptionIdRoute
AppSubscriptionsDetailIdRoute: typeof AppSubscriptionsDetailIdRoute
AppSubscriptionsEditIdRoute: typeof AppSubscriptionsEditIdRoute
}
const AppSubscriptionsRouteRouteChildren: AppSubscriptionsRouteRouteChildren = {
AppSubscriptionsCreateRoute: AppSubscriptionsCreateRoute,
AppSubscriptionsManageRoute: AppSubscriptionsManageRoute,
AppSubscriptionsDetailSubscriptionIdRoute:
AppSubscriptionsDetailSubscriptionIdRoute,
AppSubscriptionsEditSubscriptionIdRoute:
AppSubscriptionsEditSubscriptionIdRoute,
AppSubscriptionsDetailIdRoute: AppSubscriptionsDetailIdRoute,
AppSubscriptionsEditIdRoute: AppSubscriptionsEditIdRoute,
}
const AppSubscriptionsRouteRouteWithChildren =
@@ -498,8 +494,8 @@ export interface FileRoutesByFullPath {
'/auth/oidc/callback': typeof AuthOidcCallbackRoute
'/credential3rd/detail/$id': typeof AppCredential3rdDetailIdRoute
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
'/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute
'/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
}
export interface FileRoutesByTo {
@@ -526,8 +522,8 @@ export interface FileRoutesByTo {
'/auth/oidc/callback': typeof AuthOidcCallbackRoute
'/credential3rd/detail/$id': typeof AppCredential3rdDetailIdRoute
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
'/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute
'/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
}
export interface FileRoutesById {
@@ -555,8 +551,8 @@ export interface FileRoutesById {
'/auth/oidc/callback': typeof AuthOidcCallbackRoute
'/_app/credential3rd/detail/$id': typeof AppCredential3rdDetailIdRoute
'/_app/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
'/_app/subscriptions/detail/$subscriptionId': typeof AppSubscriptionsDetailSubscriptionIdRoute
'/_app/subscriptions/edit/$subscriptionId': typeof AppSubscriptionsEditSubscriptionIdRoute
'/_app/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
'/_app/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
}
export interface FileRouteTypes {
@@ -585,8 +581,8 @@ export interface FileRouteTypes {
| '/auth/oidc/callback'
| '/credential3rd/detail/$id'
| '/credential3rd/edit/$id'
| '/subscriptions/detail/$subscriptionId'
| '/subscriptions/edit/$subscriptionId'
| '/subscriptions/detail/$id'
| '/subscriptions/edit/$id'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
@@ -612,8 +608,8 @@ export interface FileRouteTypes {
| '/auth/oidc/callback'
| '/credential3rd/detail/$id'
| '/credential3rd/edit/$id'
| '/subscriptions/detail/$subscriptionId'
| '/subscriptions/edit/$subscriptionId'
| '/subscriptions/detail/$id'
| '/subscriptions/edit/$id'
id:
| '__root__'
| '/'
@@ -639,8 +635,8 @@ export interface FileRouteTypes {
| '/auth/oidc/callback'
| '/_app/credential3rd/detail/$id'
| '/_app/credential3rd/edit/$id'
| '/_app/subscriptions/detail/$subscriptionId'
| '/_app/subscriptions/edit/$subscriptionId'
| '/_app/subscriptions/detail/$id'
| '/_app/subscriptions/edit/$id'
fileRoutesById: FileRoutesById
}
@@ -741,8 +737,8 @@ export const routeTree = rootRoute
"children": [
"/_app/subscriptions/create",
"/_app/subscriptions/manage",
"/_app/subscriptions/detail/$subscriptionId",
"/_app/subscriptions/edit/$subscriptionId"
"/_app/subscriptions/detail/$id",
"/_app/subscriptions/edit/$id"
]
},
"/auth/sign-in": {
@@ -798,12 +794,12 @@ export const routeTree = rootRoute
"filePath": "_app/credential3rd/edit.$id.tsx",
"parent": "/_app/credential3rd"
},
"/_app/subscriptions/detail/$subscriptionId": {
"filePath": "_app/subscriptions/detail.$subscriptionId.tsx",
"/_app/subscriptions/detail/$id": {
"filePath": "_app/subscriptions/detail.$id.tsx",
"parent": "/_app/subscriptions"
},
"/_app/subscriptions/edit/$subscriptionId": {
"filePath": "_app/subscriptions/edit.$subscriptionId.tsx",
"/_app/subscriptions/edit/$id": {
"filePath": "_app/subscriptions/edit.$id.tsx",
"parent": "/_app/subscriptions"
}
}

View File

@@ -80,13 +80,11 @@ function CredentialCreateRouteComponent() {
...form.value,
userAgent: form.value.userAgent || platformService.userAgent,
};
if (form.value.credentialType === Credential3rdTypeEnum.Mikan) {
await insertCredential3rd({
variables: {
data: value,
},
});
}
await insertCredential3rd({
variables: {
data: value,
},
});
},
});
@@ -118,16 +116,7 @@ function CredentialCreateRouteComponent() {
}}
className="space-y-6"
>
<form.Field
name="credentialType"
validators={{
onChange: ({ value }) => {
if (!value) {
return 'Please select the credential type';
}
},
}}
>
<form.Field name="credentialType">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Credential type *</Label>
@@ -147,7 +136,11 @@ function CredentialCreateRouteComponent() {
</SelectContent>
</Select>
{field.state.meta.errors && (
<FormFieldErrors errors={field.state.meta.errors} />
<FormFieldErrors
errors={field.state.meta.errors}
isDirty={field.state.meta.isDirty}
submissionAttempts={form.state.submissionAttempts}
/>
)}
</div>
)}
@@ -166,7 +159,11 @@ function CredentialCreateRouteComponent() {
autoComplete="off"
/>
{field.state.meta.errors && (
<FormFieldErrors errors={field.state.meta.errors} />
<FormFieldErrors
errors={field.state.meta.errors}
isDirty={field.state.meta.isDirty}
submissionAttempts={form.state.submissionAttempts}
/>
)}
</div>
)}
@@ -188,7 +185,11 @@ function CredentialCreateRouteComponent() {
autoComplete="off"
/>
{field.state.meta.errors && (
<FormFieldErrors errors={field.state.meta.errors} />
<FormFieldErrors
errors={field.state.meta.errors}
isDirty={field.state.meta.isDirty}
submissionAttempts={form.state.submissionAttempts}
/>
)}
</div>
)}
@@ -215,7 +216,11 @@ function CredentialCreateRouteComponent() {
Current default user agent: {platformService.userAgent}
</p>
{field.state.meta.errors && (
<FormFieldErrors errors={field.state.meta.errors} />
<FormFieldErrors
errors={field.state.meta.errors}
isDirty={field.state.meta.isDirty}
submissionAttempts={form.state.submissionAttempts}
/>
)}
</div>
)}

View File

@@ -91,7 +91,7 @@ function Credential3rdDetailRouteComponent() {
<div>
<h1 className="font-bold text-2xl">Credential detail</h1>
<p className="mt-1 text-muted-foreground">
View and manage credential #{credential.id}
View credential #{credential.id}
</p>
</div>
</div>

View File

@@ -117,9 +117,9 @@ function FormView({
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="font-bold text-2xl">Credential detail</h1>
<h1 className="font-bold text-2xl">Credential edit</h1>
<p className="mt-1 text-muted-foreground">
View and manage credential #{credential.id}
Edit credential #{credential.id}
</p>
</div>
</div>

View File

@@ -22,6 +22,7 @@ import type { GetCredential3rdQuery } from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits';
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
import { useEvent } from '@/presentation/hooks/use-event';
import { cn } from '@/presentation/utils';
import { useMutation, useQuery } from '@apollo/client';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import {
@@ -332,14 +333,24 @@ function CredentialManageRouteComponent() {
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
{row.getVisibleCells().map((cell) => {
const isPinned = cell.column.getIsPinned();
return (
<TableCell
key={cell.id}
className={cn({
'sticky z-1 bg-background shadow-xs': isPinned,
'right-0': isPinned === 'right',
'left-0': isPinned === 'left',
})}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
);
})}
</TableRow>
))
) : (

View File

@@ -0,0 +1,93 @@
import { Button } from '@/components/ui/button';
import { SelectContent, SelectItem } from '@/components/ui/select';
import { GET_CREDENTIAL_3RD } from '@/domains/recorder/schema/credential3rd';
import {
type Credential3rdTypeEnum,
type GetCredential3rdQuery,
type GetCredential3rdQueryVariables,
OrderByEnum,
} from '@/infra/graphql/gql/graphql';
import { useQuery } from '@apollo/client';
import { AlertCircle, Loader2, RefreshCw } from 'lucide-react';
import type { ComponentProps } from 'react';
export interface Credential3rdSelectContentProps
extends ComponentProps<typeof SelectContent> {
credentialType: Credential3rdTypeEnum;
}
export function Credential3rdSelectContent({
credentialType,
...props
}: Credential3rdSelectContentProps) {
const { data, loading, error, refetch } = useQuery<
GetCredential3rdQuery,
GetCredential3rdQueryVariables
>(GET_CREDENTIAL_3RD, {
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-and-network',
variables: {
filters: {
credentialType: {
eq: credentialType,
},
},
orderBy: {
createdAt: OrderByEnum.Desc,
},
pagination: {
page: {
page: 0,
limit: 100,
},
},
},
});
const credentials = data?.credential3rd?.nodes ?? [];
return (
<SelectContent {...props}>
{loading && (
<div className="flex items-center justify-center py-6">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="ml-2 text-muted-foreground text-sm">Loading...</span>
</div>
)}
{error && (
<div className="flex flex-col items-center gap-2 py-6">
<div className="flex items-center text-destructive">
<AlertCircle className="h-4 w-4" />
<span className="ml-2 text-sm">Failed to load credentials</span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => refetch()}
className="flex items-center gap-1"
>
<RefreshCw className="h-3 w-3" />
Retry
</Button>
</div>
)}
{!loading &&
!error &&
(credentials.length === 0 ? (
<div className="py-6 text-center">
<span className="text-muted-foreground text-sm">
No credentials found
</span>
</div>
) : (
credentials.map((credential) => (
<SelectItem key={credential.id} value={credential.id.toString()}>
{credential.username}
</SelectItem>
))
))}
</SelectContent>
);
}

View File

@@ -1,23 +1,14 @@
import { useAuth } from '@/app/auth/hooks';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { FormFieldErrors } from '@/components/ui/form-field-errors';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
@@ -26,13 +17,28 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { useAppForm } from '@/components/ui/tanstack-form';
import { MikanSeasonEnum } from '@/domains/recorder/schema/mikan';
import {
INSERT_SUBSCRIPTION,
type SubscriptionInsertForm,
SubscriptionInsertFormSchema,
} from '@/domains/recorder/schema/subscriptions';
import { SubscriptionService } from '@/domains/recorder/services/subscription.service';
import { useInject } from '@/infra/di/inject';
import {
Credential3rdTypeEnum,
type InsertSubscriptionMutation,
type InsertSubscriptionMutationVariables,
SubscriptionCategoryEnum,
} from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits';
import { gql, useMutation } from '@apollo/client';
import { 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 { Loader2, Save } from 'lucide-react';
import { toast } from 'sonner';
import { Credential3rdSelectContent } from './-credential3rd-select';
export const Route = createFileRoute('/_app/subscriptions/create')({
component: SubscriptionCreateRouteComponent,
@@ -41,194 +47,312 @@ export const Route = createFileRoute('/_app/subscriptions/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 { authData } = useAuth();
console.log(JSON.stringify(authData, null, 2));
const [isSubmitting, setIsSubmitting] = useState(false);
const navigate = useNavigate();
const form = useForm<SubscriptionFormValues>({
defaultValues: {
displayName: '',
sourceUrl: '',
category: 'mikan',
enabled: true,
const subscriptionService = useInject(SubscriptionService);
const [insertSubscription, { loading }] = useMutation<
InsertSubscriptionMutation['subscriptionsCreateOne'],
InsertSubscriptionMutationVariables
>(INSERT_SUBSCRIPTION, {
onCompleted(data) {
toast.success('Subscription created');
navigate({
to: '/subscriptions/detail/$id',
params: { id: `${data.id}` },
});
},
onError(error) {
toast.error('Failed to create subscription', {
description: error.message,
});
},
});
const [createSubscription] = useMutation(CREATE_SUBSCRIPTION_MUTATION);
const onSubmit = async (data: SubscriptionFormValues) => {
try {
setIsSubmitting(true);
const response = await createSubscription({
const form = useAppForm({
defaultValues: {
displayName: '',
category: undefined,
enabled: true,
sourceUrl: '',
credentialId: '',
year: undefined,
seasonStr: '',
} as unknown as SubscriptionInsertForm,
validators: {
onBlur: SubscriptionInsertFormSchema,
onSubmit: SubscriptionInsertFormSchema,
},
onSubmit: async (form) => {
const input = subscriptionService.transformInsertFormToInput(form.value);
await insertSubscription({
variables: {
input: {
category: data.category,
displayName: data.displayName,
sourceUrl: data.sourceUrl,
enabled: data.enabled,
},
data: input,
},
});
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>
<div className="container mx-auto max-w-2xl py-6">
<div className="mb-6 flex items-center gap-4">
<div>
<h1 className="font-bold text-2xl">Create Bangumi Subscription</h1>
<p className="mt-1 text-muted-foreground">
Add a new bangumi subscription source
</p>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>Subscription information</CardTitle>
<CardDescription className="mt-2">
Please fill in the information of the bangumi subscription source.
</CardDescription>
</CardHeader>
<CardContent>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-6"
>
<form.Field name="displayName">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Display Name *</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Please enter display name"
autoComplete="off"
/>
{field.state.meta.errors && (
<FormFieldErrors
errors={field.state.meta.errors}
isDirty={field.state.meta.isDirty}
submissionAttempts={form.state.submissionAttempts}
/>
)}
</div>
)}
</form.Field>
<form.Field name="category">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Category *</Label>
<Select
disabled
value={field.value}
onValueChange={field.onChange}
defaultValue="mikan"
value={field.state.value}
onValueChange={(value) =>
field.handleChange(
value as SubscriptionInsertForm['category']
)
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select source type" />
</SelectTrigger>
</FormControl>
<SelectTrigger>
<SelectValue placeholder="Select subscription category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="mikan">mikan</SelectItem>
<SelectItem value={SubscriptionCategoryEnum.MikanBangumi}>
Mikan Bangumi Subscription
</SelectItem>
<SelectItem value={SubscriptionCategoryEnum.MikanSeason}>
Mikan Season Subscription
</SelectItem>
<SelectItem
value={SubscriptionCategoryEnum.MikanSubscriber}
>
Mikan Subscriber Subscription
</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}
{field.state.meta.errors && (
<FormFieldErrors
errors={field.state.meta.errors}
isDirty={field.state.meta.isDirty}
submissionAttempts={form.state.submissionAttempts}
/>
</FormControl>
<FormDescription>
Set an easily recognizable name for this subscription
</FormDescription>
<FormMessage />
</FormItem>
)}
</div>
)}
/>
<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">
</form.Field>
<form.Subscribe selector={(state) => state.values.category}>
{(category) => {
if (category === SubscriptionCategoryEnum.MikanSeason) {
return (
<>
<form.Field name="credentialId">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Credential ID *</Label>
<Select
value={field.state.value.toString()}
onValueChange={(value) =>
field.handleChange(Number.parseInt(value))
}
>
<SelectTrigger>
<SelectValue placeholder="Select credential" />
</SelectTrigger>
<Credential3rdSelectContent
credentialType={Credential3rdTypeEnum.Mikan}
/>
</Select>
{field.state.meta.errors && (
<FormFieldErrors
errors={field.state.meta.errors}
isDirty={field.state.meta.isDirty}
submissionAttempts={
form.state.submissionAttempts
}
/>
)}
</div>
)}
</form.Field>
<form.Field name="year">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Year *</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
type="number"
min={1970}
onBlur={field.handleBlur}
onChange={(e) =>
field.handleChange(
Number.parseInt(e.target.value)
)
}
placeholder="Please enter full year (e.g. 2025)"
autoComplete="off"
/>
{field.state.meta.errors && (
<FormFieldErrors
errors={field.state.meta.errors}
isDirty={field.state.meta.isDirty}
submissionAttempts={
form.state.submissionAttempts
}
/>
)}
</div>
)}
</form.Field>
<form.Field name="seasonStr">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Season *</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Select season" />
</SelectTrigger>
<SelectContent>
<SelectItem value={MikanSeasonEnum.Spring}>
Spring
</SelectItem>
<SelectItem value={MikanSeasonEnum.Summer}>
Summer
</SelectItem>
<SelectItem value={MikanSeasonEnum.Autumn}>
Autumn
</SelectItem>
<SelectItem value={MikanSeasonEnum.Winter}>
Winter
</SelectItem>
</SelectContent>
</Select>
{field.state.meta.errors && (
<FormFieldErrors
errors={field.state.meta.errors}
isDirty={field.state.meta.isDirty}
submissionAttempts={
form.state.submissionAttempts
}
/>
)}
</div>
)}
</form.Field>
</>
);
}
return (
<form.Field name="sourceUrl">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Source URL *</Label>
<Input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
placeholder="Please enter source URL"
autoComplete="off"
/>
{field.state.meta.errors && (
<FormFieldErrors
errors={field.state.meta.errors}
isDirty={field.state.meta.isDirty}
submissionAttempts={form.state.submissionAttempts}
/>
)}
</div>
)}
</form.Field>
);
}}
</form.Subscribe>
<form.Field name="enabled">
{(field) => (
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<FormLabel className="text-base">
Enable Subscription
</FormLabel>
<FormDescription>
Enable this subscription immediately after creation
</FormDescription>
<Label htmlFor={field.name}>Enabled</Label>
<div className="text-muted-foreground text-sm">
Enable this subscription
</div>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
<Switch
id={field.name}
checked={field.state.value}
onCheckedChange={(checked) => field.handleChange(checked)}
/>
</div>
)}
/>
</form.Field>
<div className="flex gap-3 pt-4">
<form.Subscribe selector={(state) => [state.isSubmitting]}>
{([isSubmitting]) => (
<Button type="submit" disabled={loading} className="flex-1">
{loading || isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Create subscription
</>
)}
</Button>
)}
</form.Subscribe>
</div>
</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>
</CardContent>
</Card>
</div>
);
}

View File

@@ -3,19 +3,17 @@ import { useQuery } from '@apollo/client';
import { createFileRoute } from '@tanstack/react-router';
import { GET_SUBSCRIPTION_DETAIL } from '../../../../domains/recorder/schema/subscriptions.js';
export const Route = createFileRoute(
'/_app/subscriptions/detail/$subscriptionId'
)({
export const Route = createFileRoute('/_app/subscriptions/detail/$id')({
component: DetailRouteComponent,
});
function DetailRouteComponent() {
const { subscriptionId } = Route.useParams();
const { id } = Route.useParams();
const { data, loading, error } = useQuery<GetSubscriptionDetailQuery>(
GET_SUBSCRIPTION_DETAIL,
{
variables: {
id: Number.parseInt(subscriptionId),
id: Number.parseInt(id),
},
}
);

View File

@@ -1,9 +1,7 @@
import type { RouteStateDataOption } from '@/infra/routes/traits';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute(
'/_app/subscriptions/edit/$subscriptionId'
)({
export const Route = createFileRoute('/_app/subscriptions/edit/$id')({
component: RouteComponent,
staticData: {
breadcrumb: { label: 'Edit' },
@@ -11,5 +9,6 @@ export const Route = createFileRoute(
});
function RouteComponent() {
return <div>Hello "/subscriptions/edit/$subscription-id"!</div>;
const { id } = Route.useParams();
return <div>Hello "/subscriptions/edit/$id"!</div>;
}

View File

@@ -26,6 +26,7 @@ import type {
import type { RouteStateDataOption } from '@/infra/routes/traits';
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
import { useEvent } from '@/presentation/hooks/use-event';
import { cn } from '@/presentation/utils';
import { useMutation, useQuery } from '@apollo/client';
import { createFileRoute } from '@tanstack/react-router';
import { useNavigate } from '@tanstack/react-router';
@@ -40,6 +41,7 @@ import {
getPaginationRowModel,
useReactTable,
} from '@tanstack/react-table';
import { format } from 'date-fns';
import { Plus } from 'lucide-react';
import { useMemo, useState } from 'react';
import { toast } from 'sonner';
@@ -54,7 +56,10 @@ export const Route = createFileRoute('/_app/subscriptions/manage')({
function SubscriptionManageRouteComponent() {
const navigate = useNavigate();
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
createdAt: false,
updatedAt: false,
});
const [sorting, setSorting] = useState<SortingState>([]);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
@@ -181,6 +186,30 @@ function SubscriptionManageRouteComponent() {
);
},
},
{
header: 'Created At',
accessorKey: 'createdAt',
cell: ({ row }) => {
const createdAt = row.original.createdAt;
return (
<div className="text-sm">
{format(new Date(createdAt), 'yyyy-MM-dd HH:mm:ss')}
</div>
);
},
},
{
header: 'Updated At',
accessorKey: 'updatedAt',
cell: ({ row }) => {
const updatedAt = row.original.updatedAt;
return (
<div className="text-sm">
{format(new Date(updatedAt), 'yyyy-MM-dd HH:mm:ss')}
</div>
);
},
},
{
id: 'actions',
cell: ({ row }) => (
@@ -192,14 +221,14 @@ function SubscriptionManageRouteComponent() {
showDelete
onDetail={() => {
navigate({
to: '/subscriptions/detail/$subscriptionId',
params: { subscriptionId: `${row.original.id}` },
to: '/subscriptions/detail/$id',
params: { id: `${row.original.id}` },
});
}}
onEdit={() => {
navigate({
to: '/subscriptions/edit/$subscriptionId',
params: { subscriptionId: `${row.original.id}` },
to: '/subscriptions/edit/$id',
params: { id: `${row.original.id}` },
});
}}
onDelete={handleDeleteRecord(row)}
@@ -220,11 +249,17 @@ function SubscriptionManageRouteComponent() {
onColumnVisibilityChange: setColumnVisibility,
pageCount: subscriptions?.paginationInfo?.pages,
rowCount: subscriptions?.paginationInfo?.total,
enableColumnPinning: true,
state: {
pagination,
sorting,
columnVisibility,
},
initialState: {
columnPinning: {
right: ['actions'],
},
},
});
if (error) {
@@ -284,14 +319,24 @@ function SubscriptionManageRouteComponent() {
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
{row.getVisibleCells().map((cell) => {
const isPinned = cell.column.getIsPinned();
return (
<TableCell
key={cell.id}
className={cn({
'sticky z-1 bg-background shadow-xs': isPinned,
'right-0': isPinned === 'right',
'left-0': isPinned === 'left',
})}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
);
})}
</TableRow>
))
) : (