Compare commits

...

2 Commits

Author SHA1 Message Date
882b29d7a1 feat: task ui basic done 2025-06-13 04:02:01 +08:00
c60f6f511e feat: remove turbo 2025-06-13 00:09:18 +08:00
35 changed files with 2744 additions and 4411 deletions

14
.vscode/settings.json vendored
View File

@ -41,11 +41,11 @@
],
"rust-analyzer.cargo.features": "all",
// https://github.com/rust-lang/rust/issues/141540
"rust-analyzer.cargo.targetDir": "target/rust-analyzer",
"rust-analyzer.check.extraEnv": {
"CARGO_TARGET_DIR": "target/rust-analyzer"
},
"rust-analyzer.cargo.extraEnv": {
"CARGO_TARGET_DIR": "target/analyzer"
}
// "rust-analyzer.cargo.targetDir": "target/rust-analyzer",
// "rust-analyzer.check.extraEnv": {
// "CARGO_TARGET_DIR": "target/rust-analyzer"
// },
// "rust-analyzer.cargo.extraEnv": {
// "CARGO_TARGET_DIR": "target/analyzer"
// }
}

112
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,112 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "dev-all",
"dependsOn": [
"dev-webui",
"dev-recorder",
"dev-proxy",
"dev-codegen-wait",
"dev-deps",
],
"dependsOrder": "parallel",
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"group": "new-group",
"echo": true,
"reveal": "always",
"panel": "shared",
"clear": false
}
},
{
"label": "dev-webui",
"type": "shell",
"command": "just",
"args": [
"dev-webui"
],
"isBackground": true,
"problemMatcher": [],
"presentation": {
"panel": "dedicated",
"reveal": "always",
"focus": true,
"showReuseMessage": true,
"clear": true,
}
},
{
"label": "dev-deps",
"type": "shell",
"command": "just",
"args": [
"dev-deps"
],
"isBackground": true,
"problemMatcher": [],
"presentation": {
"panel": "dedicated",
"reveal": "never",
"focus": false,
"showReuseMessage": true,
"clear": true,
}
},
{
"label": "dev-codegen-wait",
"type": "shell",
"command": "just",
"args": [
"dev-codegen-wait"
],
"isBackground": true,
"problemMatcher": [],
"presentation": {
"panel": "dedicated",
"reveal": "never",
"focus": false,
"showReuseMessage": true,
"clear": true,
}
},
{
"label": "dev-recorder",
"type": "shell",
"command": "just",
"args": [
"dev-recorder"
],
"isBackground": true,
"problemMatcher": [],
"presentation": {
"panel": "dedicated",
"reveal": "never",
"focus": false,
"showReuseMessage": true,
"clear": true,
}
},
{
"label": "dev-proxy",
"type": "shell",
"command": "just",
"args": [
"dev-proxy",
],
"isBackground": true,
"problemMatcher": [],
"presentation": {
"panel": "dedicated",
"reveal": "never",
"focus": false,
"showReuseMessage": true,
"clear": true,
}
}
]
}

View File

@ -14,6 +14,8 @@ resolver = "2"
[profile.dev]
debug = 0
# https://github.com/rust-lang/rust/issues/141540
incremental = false
# [simd not supported by cranelift](https://github.com/rust-lang/rustc_codegen_cranelift/issues/171)
# codegen-backend = "cranelift"

View File

@ -6,13 +6,14 @@
"build": "email build",
"dev": "email dev --port 5003",
"export": "email export",
"clean": "git clean -xdf .cache .turbo dist node_modules",
"clean": "git clean -xdf .cache dist node_modules",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"@react-email/components": "0.0.31",
"@react-email/components": "^0.0.42",
"react": "^19.0.0",
"react-email": "3.0.4"
"react-email": "^4.0.16",
"@konobangu/email": "workspace:*"
},
"devDependencies": {
"@types/react": "19.0.1"

View File

@ -2,8 +2,12 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
"jsxImportSource": "react",
"module": "ESNext",
"moduleResolution": "bundler"
},
"references": [{ "path": "../../packages/email" }],
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@ -10,6 +10,6 @@
"keywords": [],
"license": "MIT",
"devDependencies": {
"whistle": "^2.9.93"
"whistle": "^2.9.99"
}
}

View File

@ -267,6 +267,7 @@ where
Box::new(
move |context: &ResolverContext| -> SeaResult<Option<SeaValue>> {
let field_name = context.field().name();
tracing::warn!("field_name: {:?}", field_name);
if field_name == entity_create_one_mutation_field_name.as_str()
|| field_name == entity_create_batch_mutation_field_name.as_str()
{
@ -291,7 +292,7 @@ where
{
let entity_key = get_entity_key::<T>(context);
let entity_column_key = get_entity_column_key::<T>(context, column);
let column_name = context.entity_object.column_name.as_ref()(&entity_key, &entity_column_key);
context.guards.entity_guards.insert(
entity_key.clone(),
guard_entity_with_subscriber_id::<T>(context, column),
@ -311,13 +312,9 @@ where
generate_subscriber_id_filter_condition::<T>(context, column),
);
context.types.input_none_conversions.insert(
column_name.clone(),
entity_column_key.clone(),
generate_default_subscriber_id_input_conversion::<T>(context, column),
);
context
.entity_input
.insert_skips
.push(entity_column_key.clone());
context.entity_input.update_skips.push(entity_column_key);
}

View File

@ -121,6 +121,7 @@ pub fn build_schema(
builder.register_enumeration::<downloaders::DownloaderCategory>();
builder.register_enumeration::<downloads::DownloadMime>();
builder.register_enumeration::<credential_3rd::Credential3rdType>();
builder.register_enumeration::<subscriber_tasks::SubscriberTaskStatus>();
}
builder = register_subscriptions_to_schema_builder(builder);

View File

@ -1,7 +1,27 @@
use async_trait::async_trait;
use sea_orm::entity::prelude::*;
pub use crate::task::{SubscriberTask, SubscriberTaskType};
pub use crate::task::{
SubscriberTask, SubscriberTaskType, SubscriberTaskTypeEnum, SubscriberTaskTypeVariant,
SubscriberTaskTypeVariantIter,
};
#[derive(Clone, Debug, PartialEq, Eq, DeriveActiveEnum, EnumIter, DeriveDisplay)]
#[sea_orm(rs_type = "String", db_type = "Text")]
pub enum SubscriberTaskStatus {
#[sea_orm(string_value = "Pending")]
Pending,
#[sea_orm(string_value = "Scheduled")]
Scheduled,
#[sea_orm(string_value = "Running")]
Running,
#[sea_orm(string_value = "Done")]
Done,
#[sea_orm(string_value = "Failed")]
Failed,
#[sea_orm(string_value = "Killed")]
Killed,
}
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "subscriber_tasks")]
@ -11,7 +31,7 @@ pub struct Model {
pub subscriber_id: i32,
pub job: SubscriberTask,
pub task_type: SubscriberTaskType,
pub status: String,
pub status: SubscriberTaskStatus,
pub attempts: i32,
pub max_attempts: i32,
pub run_at: DateTimeUtc,

View File

@ -13,90 +13,87 @@
"dependencies": {
"@abraham/reflection": "^0.13.0",
"@apollo/client": "^3.13.8",
"@codemirror/language": "6.0.0",
"@corvu/drawer": "^0.2.3",
"@codemirror/language": "6.11.1",
"@corvu/drawer": "^0.2.4",
"@corvu/otp-field": "^0.1.4",
"@corvu/resizable": "^0.2.4",
"@graphiql/toolkit": "^0.11.1",
"@hookform/resolvers": "^5.0.1",
"@corvu/resizable": "^0.2.5",
"@graphiql/toolkit": "^0.11.3",
"@hookform/resolvers": "^5.1.1",
"@outposts/injection-js": "^2.5.1",
"@radix-ui/react-accordion": "^1.2.10",
"@radix-ui/react-alert-dialog": "^1.1.13",
"@radix-ui/react-aspect-ratio": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-collapsible": "^1.1.10",
"@radix-ui/react-context-menu": "^2.2.14",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-menubar": "^1.1.14",
"@radix-ui/react-navigation-menu": "^1.2.12",
"@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-progress": "^1.1.6",
"@radix-ui/react-radio-group": "^1.3.6",
"@radix-ui/react-scroll-area": "^1.2.8",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slider": "^1.3.4",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-switch": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.11",
"@radix-ui/react-toggle": "^1.1.8",
"@radix-ui/react-toggle-group": "^1.1.9",
"@radix-ui/react-tooltip": "^1.2.6",
"@rsbuild/plugin-react": "^1.2.0",
"@tanstack/react-form": "^1.12.1",
"@tanstack/react-query": "^5.75.6",
"@tanstack/react-router": "^1.112.13",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-context-menu": "^2.2.15",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.15",
"@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@rsbuild/plugin-react": "^1.3.2",
"@tanstack/react-form": "^1.12.3",
"@tanstack/react-query": "^5.80.7",
"@tanstack/react-table": "^8.21.3",
"@tanstack/router-devtools": "^1.112.13",
"@tanstack/store": "^0.7.1",
"arktype": "^2.1.6",
"chart.js": "^4.4.8",
"arktype": "^2.1.20",
"chart.js": "^4.4.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"graphiql": "^4.0.2",
"graphiql": "^4.1.2",
"graphql": "^16.11.0",
"input-otp": "^1.4.2",
"jotai": "^2.12.3",
"jotai": "^2.12.5",
"jotai-signal": "^0.9.0",
"lodash-es": "^4.17.21",
"lucide-react": "^0.512.0",
"lucide-react": "^0.514.0",
"oidc-client-rx": "0.1.0-alpha.9",
"react": "^19.1.0",
"react-day-picker": "9.6.0",
"react-day-picker": "9.7.0",
"react-dom": "^19.1.0",
"react-resizable-panels": "^3.0.1",
"react-resizable-panels": "^3.0.2",
"recharts": "^2.15.3",
"rxjs": "^7.8.2",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.0.6",
"tw-animate-css": "^1.2.7",
"type-fest": "^4.40.0",
"vaul": "^1.1.2"
"sonner": "^2.0.5",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.10",
"tw-animate-css": "^1.3.4",
"type-fest": "^4.41.0",
"vaul": "^1.1.2",
"es-toolkit": "^1.39.3",
"@tanstack/react-router": "^1.121.2"
},
"devDependencies": {
"@graphql-codegen/cli": "^5.0.6",
"@graphql-codegen/client-preset": "^4.8.1",
"@graphql-codegen/cli": "^5.0.7",
"@graphql-codegen/client-preset": "^4.8.2",
"@graphql-codegen/typescript": "^4.1.6",
"@graphql-typed-document-node/core": "^3.2.0",
"@parcel/watcher": "^2.5.1",
"@rsbuild/core": "^1.2.15",
"@tailwindcss/postcss": "^4.0.9",
"@tanstack/react-router": "^1.112.0",
"@tanstack/router-devtools": "^1.112.6",
"@tanstack/router-plugin": "^1.112.13",
"@types/lodash-es": "^4.17.12",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@rsbuild/core": "^1.3.22",
"@tailwindcss/postcss": "^4.1.10",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"chalk": "^5.4.1",
"commander": "^14.0.0",
"postcss": "^8.5.3"
"postcss": "^8.5.5",
"@tanstack/router-devtools": "^1.121.5",
"@tanstack/router-plugin": "^1.121.4"
}
}

View File

@ -1,6 +1,5 @@
"use client";
import type { Row } from "@tanstack/react-table";
import { MoreHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button";
@ -14,12 +13,11 @@ import {
} from "@/components/ui/dropdown-menu";
import type * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { ComponentProps, PropsWithChildren, useMemo } from "react";
import { ComponentProps, PropsWithChildren } from "react";
interface DataTableRowActionsProps<DataView, Id>
interface DropdownMenuActionsProps<Id>
extends ComponentProps<typeof DropdownMenuPrimitive.Root> {
row: Row<DataView>;
getId: (row: Row<DataView>) => Id;
id: Id;
showDetail?: boolean;
showEdit?: boolean;
showDelete?: boolean;
@ -28,9 +26,8 @@ interface DataTableRowActionsProps<DataView, Id>
onEdit?: (id: Id) => void;
}
export function DataTableRowActions<DataView, Id>({
row,
getId,
export function DropdownMenuActions<Id>({
id,
showDetail,
showDelete,
showEdit,
@ -39,8 +36,7 @@ export function DataTableRowActions<DataView, Id>({
onEdit,
children,
...rest
}: PropsWithChildren<DataTableRowActionsProps<DataView, Id>>) {
const id = useMemo(() => getId(row), [getId, row]);
}: PropsWithChildren<DropdownMenuActionsProps<Id>>) {
return (
<DropdownMenu {...rest}>
<DropdownMenuTrigger asChild>

View File

@ -4,7 +4,7 @@ import {
} from '@/infra/graphql/gql/graphql';
import { Injectable, inject } from '@outposts/injection-js';
import { ArkErrors } from 'arktype';
import { omit } from 'lodash-es';
import { omit } from 'es-toolkit';
import {
type MikanSubscriptionBangumiSourceUrl,
type MikanSubscriptionSeasonSourceUrl,

View File

@ -133,6 +133,7 @@ export type BangumiInsertInput = {
savePath?: InputMaybe<Scalars['String']['input']>;
season: Scalars['Int']['input'];
seasonRaw?: InputMaybe<Scalars['String']['input']>;
subscriberId: Scalars['Int']['input'];
updatedAt?: InputMaybe<Scalars['String']['input']>;
};
@ -266,6 +267,7 @@ export type Credential3rdInsertInput = {
credentialType: Credential3rdTypeEnum;
id?: InputMaybe<Scalars['Int']['input']>;
password?: InputMaybe<Scalars['String']['input']>;
subscriberId: Scalars['Int']['input'];
updatedAt?: InputMaybe<Scalars['String']['input']>;
userAgent?: InputMaybe<Scalars['String']['input']>;
username?: InputMaybe<Scalars['String']['input']>;
@ -448,6 +450,7 @@ export type DownloadersInsertInput = {
id?: InputMaybe<Scalars['Int']['input']>;
password: Scalars['String']['input'];
savePath: Scalars['String']['input'];
subscriberId: Scalars['Int']['input'];
updatedAt?: InputMaybe<Scalars['String']['input']>;
username: Scalars['String']['input'];
};
@ -563,6 +566,7 @@ export type DownloadsInsertInput = {
rawName: Scalars['String']['input'];
savePath?: InputMaybe<Scalars['String']['input']>;
status: DownloadStatusEnum;
subscriberId: Scalars['Int']['input'];
updatedAt?: InputMaybe<Scalars['String']['input']>;
url: Scalars['String']['input'];
};
@ -725,6 +729,7 @@ export type EpisodesInsertInput = {
season: Scalars['Int']['input'];
seasonRaw?: InputMaybe<Scalars['String']['input']>;
source?: InputMaybe<Scalars['String']['input']>;
subscriberId: Scalars['Int']['input'];
subtitle?: InputMaybe<Scalars['String']['input']>;
updatedAt?: InputMaybe<Scalars['String']['input']>;
};
@ -1191,6 +1196,16 @@ export type SubscriberIdFilterInput = {
eq?: InputMaybe<Scalars['Int']['input']>;
};
export const SubscriberTaskStatusEnum = {
Done: 'Done',
Failed: 'Failed',
Killed: 'Killed',
Pending: 'Pending',
Running: 'Running',
Scheduled: 'Scheduled'
} as const;
export type SubscriberTaskStatusEnum = typeof SubscriberTaskStatusEnum[keyof typeof SubscriberTaskStatusEnum];
export const SubscriberTaskTypeEnum = {
SyncOneSubscriptionFeedsFull: 'sync_one_subscription_feeds_full',
SyncOneSubscriptionFeedsIncremental: 'sync_one_subscription_feeds_incremental',
@ -1210,7 +1225,7 @@ export type SubscriberTasks = {
maxAttempts: Scalars['Int']['output'];
priority: Scalars['Int']['output'];
runAt: Scalars['String']['output'];
status: Scalars['String']['output'];
status: SubscriberTaskStatusEnum;
subscriber?: Maybe<Subscribers>;
subscriberId: Scalars['Int']['output'];
taskType: SubscriberTaskTypeEnum;
@ -1228,7 +1243,7 @@ export type SubscriberTasksBasic = {
maxAttempts: Scalars['Int']['output'];
priority: Scalars['Int']['output'];
runAt: Scalars['String']['output'];
status: Scalars['String']['output'];
status: SubscriberTaskStatusEnum;
subscriberId: Scalars['Int']['output'];
taskType: SubscriberTaskTypeEnum;
};
@ -1276,7 +1291,8 @@ export type SubscriberTasksInsertInput = {
maxAttempts: Scalars['Int']['input'];
priority: Scalars['Int']['input'];
runAt: Scalars['String']['input'];
status: Scalars['String']['input'];
status: SubscriberTaskStatusEnum;
subscriberId: Scalars['Int']['input'];
taskType: SubscriberTaskTypeEnum;
};
@ -1307,7 +1323,7 @@ export type SubscriberTasksUpdateInput = {
maxAttempts?: InputMaybe<Scalars['Int']['input']>;
priority?: InputMaybe<Scalars['Int']['input']>;
runAt?: InputMaybe<Scalars['String']['input']>;
status?: InputMaybe<Scalars['String']['input']>;
status?: InputMaybe<SubscriberTaskStatusEnum>;
taskType?: InputMaybe<SubscriberTaskTypeEnum>;
};
@ -1433,6 +1449,7 @@ export type SubscriptionBangumiFilterInput = {
export type SubscriptionBangumiInsertInput = {
bangumiId: Scalars['Int']['input'];
id?: InputMaybe<Scalars['Int']['input']>;
subscriberId: Scalars['Int']['input'];
subscriptionId: Scalars['Int']['input'];
};
@ -1515,6 +1532,7 @@ export type SubscriptionEpisodeFilterInput = {
export type SubscriptionEpisodeInsertInput = {
episodeId: Scalars['Int']['input'];
id?: InputMaybe<Scalars['Int']['input']>;
subscriberId: Scalars['Int']['input'];
subscriptionId: Scalars['Int']['input'];
};
@ -1627,6 +1645,7 @@ export type SubscriptionsInsertInput = {
enabled: Scalars['Boolean']['input'];
id?: InputMaybe<Scalars['Int']['input']>;
sourceUrl: Scalars['String']['input'];
subscriberId: Scalars['Int']['input'];
updatedAt?: InputMaybe<Scalars['String']['input']>;
};
@ -1790,7 +1809,7 @@ export type GetTasksQueryVariables = Exact<{
}>;
export type GetTasksQuery = { __typename?: 'Query', subscriberTasks: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, job: any, taskType: SubscriberTaskTypeEnum, status: string, attempts: number, maxAttempts: number, runAt: string, lastError?: string | null, lockAt?: string | null, lockBy?: string | null, doneAt?: string | null, priority: number }>, paginationInfo?: { __typename?: 'PaginationInfo', total: number, pages: number } | null } };
export type GetTasksQuery = { __typename?: 'Query', subscriberTasks: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, job: any, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum, attempts: number, maxAttempts: number, runAt: string, lastError?: string | null, lockAt?: string | null, lockBy?: string | null, doneAt?: string | null, priority: number }>, paginationInfo?: { __typename?: 'PaginationInfo', total: number, pages: number } | null } };
export const GetCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdOrderInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode<GetCredential3rdQuery, GetCredential3rdQueryVariables>;

File diff suppressed because it is too large Load Diff

View File

@ -39,7 +39,12 @@ import type {
} from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits';
import { useMutation, useQuery } from '@apollo/client';
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import {
createFileRoute,
useCanGoBack,
useNavigate,
useRouter,
} from '@tanstack/react-router';
import { ArrowLeft, Eye, EyeOff, Save, X } from 'lucide-react';
import { useCallback, useState } from 'react';
import { toast } from 'sonner';
@ -63,11 +68,17 @@ function FormView({
const togglePasswordVisibility = () => {
setShowPassword((prev) => !prev);
};
const router = useRouter();
const canGoBack = useCanGoBack();
const handleBack = () => {
if (canGoBack) {
router.history.back();
} else {
navigate({
to: '/credential3rd/manage',
});
}
};
const [updateCredential, { loading: updating }] = useMutation<

View File

@ -1,10 +1,10 @@
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { DataTablePagination } from '@/components/ui/data-table-pagination';
import { DataTableRowActions } from '@/components/ui/data-table-row-actions';
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
import { DialogTrigger } from '@/components/ui/dialog';
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
import { QueryErrorView } from '@/components/ui/query-error-view';
import { Skeleton } from '@/components/ui/skeleton';
import {
@ -231,9 +231,8 @@ function CredentialManageRouteComponent() {
{
id: 'actions',
cell: ({ row }) => (
<DataTableRowActions
row={row}
getId={(row) => row.original.id}
<DropdownMenuActions
id={row.original.id}
showEdit
showDelete
showDetail
@ -261,7 +260,7 @@ function CredentialManageRouteComponent() {
id={row.original.id}
/>
</Dialog>
</DataTableRowActions>
</DropdownMenuActions>
),
},
];

View File

@ -1,9 +1,9 @@
import { Button } from '@/components/ui/button';
import { DataTablePagination } from '@/components/ui/data-table-pagination';
import { DataTableRowActions } from '@/components/ui/data-table-row-actions';
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
import { QueryErrorView } from '@/components/ui/query-error-view';
import { Skeleton } from '@/components/ui/skeleton';
import { Switch } from '@/components/ui/switch';
@ -225,9 +225,8 @@ function SubscriptionManageRouteComponent() {
{
id: 'actions',
cell: ({ row }) => (
<DataTableRowActions
row={row}
getId={(row) => row.original.id}
<DropdownMenuActions
id={row.original.id}
showDetail
showEdit
showDelete
@ -253,7 +252,7 @@ function SubscriptionManageRouteComponent() {
</DialogTrigger>
<SubscriptionSyncDialogContent id={row.original.id} />
</Dialog>
</DataTableRowActions>
</DropdownMenuActions>
),
},
];

View File

@ -0,0 +1,44 @@
import { Badge } from '@/components/ui/badge';
import { SubscriberTaskStatusEnum } from '@/infra/graphql/gql/graphql';
import { AlertCircle, CheckCircle, Clock, Loader2 } from 'lucide-react';
export function getStatusBadge(status: SubscriberTaskStatusEnum) {
switch (status) {
case SubscriberTaskStatusEnum.Done:
return (
<Badge variant="secondary" className="bg-green-100 text-green-800">
<CheckCircle className="mr-1 h-3 w-3 capitalize" />
{status}
</Badge>
);
case SubscriberTaskStatusEnum.Running:
return (
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
<Loader2 className="mr-1 h-3 w-3 animate-spin capitalize" />
{status}
</Badge>
);
case SubscriberTaskStatusEnum.Killed:
case SubscriberTaskStatusEnum.Failed:
return (
<Badge variant="destructive">
<AlertCircle className="mr-1 h-3 w-3 capitalize" />
{status}
</Badge>
);
case SubscriberTaskStatusEnum.Scheduled:
case SubscriberTaskStatusEnum.Pending:
return (
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
<Clock className="mr-1 h-3 w-3 capitalize" />
{status}
</Badge>
);
default:
return (
<Badge variant="outline" className="capitalize">
{status}
</Badge>
);
}
}

View File

@ -1,5 +1,34 @@
import { DetailCardSkeleton } from '@/components/detail-card-skeleton';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
import { Label } from '@/components/ui/label';
import { QueryErrorView } from '@/components/ui/query-error-view';
import { Separator } from '@/components/ui/separator';
import { GET_TASKS } from '@/domains/recorder/schema/tasks';
import {
type GetTasksQuery,
type GetTasksQueryVariables,
SubscriberTaskStatusEnum,
} from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits';
import { createFileRoute } from '@tanstack/react-router';
import { useQuery } from '@apollo/client';
import {
createFileRoute,
useCanGoBack,
useNavigate,
useRouter,
} from '@tanstack/react-router';
import { format } from 'date-fns';
import { ArrowLeft, RefreshCw } from 'lucide-react';
import { getStatusBadge } from './-status-badge';
export const Route = createFileRoute('/_app/tasks/detail/$id')({
component: TaskDetailRouteComponent,
@ -9,5 +38,203 @@ export const Route = createFileRoute('/_app/tasks/detail/$id')({
});
function TaskDetailRouteComponent() {
return <div>Hello "/_app/tasks/detail/$id"!</div>;
const { id } = Route.useParams();
const navigate = useNavigate();
const router = useRouter();
const canGoBack = useCanGoBack();
const handleBack = () => {
if (canGoBack) {
router.history.back();
} else {
navigate({
to: '/tasks/manage',
});
}
};
const { data, loading, error, refetch } = useQuery<
GetTasksQuery,
GetTasksQueryVariables
>(GET_TASKS, {
variables: {
filters: {
id: {
eq: id,
},
},
pagination: {
page: {
page: 0,
limit: 1,
},
},
orderBy: {},
},
pollInterval: 5000, // Auto-refresh every 5 seconds for running tasks
});
const task = data?.subscriberTasks?.nodes?.[0];
if (loading) {
return <DetailCardSkeleton />;
}
if (error) {
return <QueryErrorView message={error.message} onRetry={refetch} />;
}
if (!task) {
return <DetailEmptyView message="Task not found" />;
}
return (
<div className="container mx-auto max-w-4xl py-6">
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"
onClick={handleBack}
className="h-8 w-8 p-0"
>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="font-bold text-2xl">Task Detail</h1>
<p className="mt-1 text-muted-foreground">View task #{task.id}</p>
</div>
</div>
<Button variant="outline" size="sm" onClick={() => refetch()}>
<RefreshCw className="h-4 w-4" />
Refresh
</Button>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Task Information</CardTitle>
<CardDescription className="mt-2">
View task execution details
</CardDescription>
</div>
<div className="flex items-center gap-2">
{getStatusBadge(task.status)}
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Basic Information */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label className="font-medium text-sm">Task ID</Label>
<div className="rounded-md bg-muted p-3">
<code className="text-sm">{task.id}</code>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Task Type</Label>
<div className="rounded-md bg-muted p-3">
<Badge variant="secondary">{task.taskType}</Badge>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Priority</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">{task.priority}</span>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Attempts</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{task.attempts} / {task.maxAttempts}
</span>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">
Scheduled Run Time
</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{format(new Date(task.runAt), 'yyyy-MM-dd HH:mm:ss')}
</span>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Done Time</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{task.doneAt
? format(new Date(task.doneAt), 'yyyy-MM-dd HH:mm:ss')
: '-'}
</span>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Lock Time</Label>
<div className="rounded-md bg-muted p-3">
<span className="text-sm">
{task.lockAt
? format(new Date(task.lockAt), 'yyyy-MM-dd HH:mm:ss')
: '-'}
</span>
</div>
</div>
<div className="space-y-2">
<Label className="font-medium text-sm">Lock By</Label>
<div className="rounded-md bg-muted p-3">
<code className="text-sm">{task.lockBy || '-'}</code>
</div>
</div>
</div>
{/* Job Details */}
{task.job && (
<>
<Separator />
<div className="space-y-2">
<Label className="font-medium text-sm">Job Details</Label>
<div className="rounded-md bg-muted p-3">
<pre className="overflow-x-auto whitespace-pre-wrap text-sm">
<code>{JSON.stringify(task.job, null, 2)}</code>
</pre>
</div>
</div>
</>
)}
{/* Error Information */}
{(task.status === SubscriberTaskStatusEnum.Failed ||
task.status === SubscriberTaskStatusEnum.Killed) &&
task.lastError && (
<>
<Separator />
<div className="space-y-2">
<Label className="font-medium text-sm">Last Error</Label>
<div className="rounded-md bg-destructive/10 p-3">
<p className="text-destructive text-sm">
{task.lastError}
</p>
</div>
</div>
</>
)}
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -1,12 +1,15 @@
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { DataTablePagination } from '@/components/ui/data-table-pagination';
import { DataTableRowActions } from '@/components/ui/data-table-row-actions';
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
import { QueryErrorView } from '@/components/ui/query-error-view';
import { Skeleton } from '@/components/ui/skeleton';
import { GET_TASKS, type TaskDto } from '@/domains/recorder/schema/tasks';
import type { GetTasksQuery } from '@/infra/graphql/gql/graphql';
import {
type GetTasksQuery,
SubscriberTaskStatusEnum,
} from '@/infra/graphql/gql/graphql';
import type { RouteStateDataOption } from '@/infra/routes/traits';
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
import { useQuery } from '@apollo/client';
@ -21,14 +24,10 @@ import {
useReactTable,
} from '@tanstack/react-table';
import { format } from 'date-fns';
import {
AlertCircle,
CheckCircle,
Clock,
Loader2,
RefreshCw,
} from 'lucide-react';
import { RefreshCw } from 'lucide-react';
import { useMemo, useState } from 'react';
import { getStatusBadge } from './-status-badge';
export const Route = createFileRoute('/_app/tasks/manage')({
component: TaskManageRouteComponent,
@ -87,111 +86,9 @@ function TaskManageRouteComponent() {
);
},
},
{
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => {
return getStatusBadge(row.original.status);
},
},
{
header: 'Priority',
accessorKey: 'priority',
cell: ({ row }) => {
return getPriorityBadge(row.original.priority);
},
},
{
header: 'Attempts',
accessorKey: 'attempts',
cell: ({ row }) => {
const attempts = row.original.attempts;
const maxAttempts = row.original.maxAttempts;
return (
<div className="text-sm">
{attempts} / {maxAttempts}
</div>
);
},
},
{
header: 'Run At',
accessorKey: 'runAt',
cell: ({ row }) => {
const runAt = row.original.runAt;
return (
<div className="text-sm">
{format(new Date(runAt), 'yyyy-MM-dd HH:mm:ss')}
</div>
);
},
},
{
header: 'Done At',
accessorKey: 'doneAt',
cell: ({ row }) => {
const doneAt = row.original.doneAt;
return (
<div className="text-sm">
{doneAt ? format(new Date(doneAt), 'yyyy-MM-dd HH:mm:ss') : '-'}
</div>
);
},
},
{
header: 'Last Error',
accessorKey: 'lastError',
cell: ({ row }) => {
const lastError = row.original.lastError;
return (
<div
className="max-w-xs truncate text-sm"
title={lastError || undefined}
>
{lastError || '-'}
</div>
);
},
},
{
header: 'Lock At',
accessorKey: 'lockAt',
cell: ({ row }) => {
const lockAt = row.original.lockAt;
return (
<div className="text-sm">
{lockAt ? format(new Date(lockAt), 'yyyy-MM-dd HH:mm:ss') : '-'}
</div>
);
},
},
{
header: 'Lock By',
accessorKey: 'lockBy',
cell: ({ row }) => {
const lockBy = row.original.lockBy;
return <div className="font-mono text-sm">{lockBy || '-'}</div>;
},
},
{
id: 'actions',
cell: ({ row }) => (
<DataTableRowActions
row={row}
getId={(row) => row.original.id}
showDetail
onDetail={() => {
navigate({
to: '/tasks/detail/$id',
params: { id: row.original.id },
});
}}
/>
),
},
];
return cs;
}, [navigate]);
}, []);
const table = useReactTable({
data: useMemo(() => (tasks?.nodes ?? []) as TaskDto[], [tasks]),
@ -226,8 +123,8 @@ function TaskManageRouteComponent() {
<div className="container mx-auto space-y-4 px-4">
<div className="flex items-center justify-between pt-4">
<div>
<h1 className="font-bold text-2xl">Subscription Management</h1>
<p className="text-muted-foreground">Manage your subscription</p>
<h1 className="font-bold text-2xl">Tasks Management</h1>
<p className="text-muted-foreground">Manage your tasks</p>
</div>
<Button onClick={() => refetch()} variant="outline" size="sm">
<RefreshCw className="h-4 w-4" />
@ -241,12 +138,12 @@ function TaskManageRouteComponent() {
))}
{!showSkeleton && table.getRowModel().rows?.length > 0 ? (
table.getRowModel().rows.map((row) => {
table.getRowModel().rows.map((row, index) => {
const task = row.original;
return (
<div
key={task.id}
className="space-y-3 rounded-lg border bg-card p-4"
key={`${task.id}-${index}`}
>
{/* Header with status and priority */}
<div className="flex items-center justify-between gap-2">
@ -259,10 +156,10 @@ function TaskManageRouteComponent() {
</div>
<div className="mt-1 flex items-center gap-2">
{getStatusBadge(task.status)}
<Badge variant="outline">Priority: {task.priority}</Badge>
<div className="mr-0 ml-auto">
<DataTableRowActions
row={row}
getId={(r) => r.original.id}
<DropdownMenuActions
id={task.id}
showDetail
onDetail={() => {
navigate({
@ -274,19 +171,6 @@ function TaskManageRouteComponent() {
</div>
</div>
{task.job && (
<div className="text-sm">
<span className="text-muted-foreground">Job: </span>
<br />
<span
className="whitespace-pre-wrap"
dangerouslySetInnerHTML={{
__html: JSON.stringify(task.job, null, 2),
}}
/>
</div>
)}
{/* Time info */}
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
@ -311,15 +195,34 @@ function TaskManageRouteComponent() {
</span>
</div>
{/* Priority */}
{/* Lock at */}
<div className="text-sm">
<span className="text-muted-foreground">Priority: </span>
<span>{task.priority}</span>
<span className="text-muted-foreground">Lock at: </span>
<span>
{task.lockAt
? format(new Date(task.lockAt), 'MM/dd HH:mm')
: '-'}
</span>
</div>
</div>
{task.job && (
<div className="text-sm">
<span className="text-muted-foreground">Job: </span>
<br />
<span
className="whitespace-pre-wrap"
dangerouslySetInnerHTML={{
__html: JSON.stringify(task.job, null, 2),
}}
/>
</div>
)}
{/* Error if exists */}
{task.status === 'error' && task.lastError && (
{(task.status === SubscriberTaskStatusEnum.Failed ||
task.status === SubscriberTaskStatusEnum.Killed) &&
task.lastError && (
<div className="rounded bg-destructive/10 p-2 text-destructive text-sm">
{task.lastError}
</div>
@ -336,42 +239,3 @@ function TaskManageRouteComponent() {
</div>
);
}
function getStatusBadge(status: string) {
switch (status.toLowerCase()) {
case 'completed':
case 'done':
return (
<Badge variant="secondary" className="bg-green-100 text-green-800">
<CheckCircle className="mr-1 h-3 w-3" />
Completed
</Badge>
);
case 'running':
case 'active':
return (
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
Running
</Badge>
);
case 'failed':
case 'error':
return (
<Badge variant="destructive">
<AlertCircle className="mr-1 h-3 w-3" />
Failed
</Badge>
);
case 'pending':
case 'waiting':
return (
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
<Clock className="mr-1 h-3 w-3" />
Pending
</Badge>
);
default:
return <Badge variant="outline">{status}</Badge>;
}
}

View File

@ -2,7 +2,7 @@ set windows-shell := ["pwsh.exe", "-c"]
set dotenv-load := true
prepare-dev:
cargo install sea-orm-cli cargo-llvm-cov cargo-nextest killport
cargo install sea-orm-cli cargo-llvm-cov cargo-nextest
# install watchexec
prepare-dev-testcontainers:
@ -32,14 +32,6 @@ dev-deps-clean:
dev-codegen:
pnpm run --filter=webui codegen
[unix]
dev-all:
zellij --layout dev.kdl
[windows]
dev-all:
pnpm run dev-all
dev-codegen-wait:
@until nc -z localhost 5001; do echo "Waiting for Recorder..."; sleep 1; done
pnpm run --filter=webui codegen-watch
@ -47,3 +39,10 @@ dev-codegen-wait:
dev-coverage:
cargo llvm-cov test --html
[unix]
dev-all:
zellij --layout dev.kdl
[windows]
dev-all:
@echo "zellij is not supported on Windows, please use vscode tasks 'dev-all'"

View File

@ -3,42 +3,45 @@
"version": "0.0.0",
"description": "Kono bangumi?",
"license": "MIT",
"workspaces": [
"packages/*",
"apps/*"
],
"workspaces": ["packages/*", "apps/*"],
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/dumtruck/konobangu.git"
},
"scripts": {
"dev-webui": "just dev-webui",
"dev-proxy": "just dev-proxy",
"dev-recorder": "just dev-recorder",
"dev-deps": "just dev-deps",
"dev-codegen-wait": "just dev-codegen-wait",
"dev-all": "turbo run //#dev-recorder //#dev-proxy //#dev-webui //#dev-deps //#dev-codegen-wait",
"lint": "ultracite lint",
"format": "ultracite format",
"bump-deps": "npx --yes npm-check-updates --deep -u -x react-day-picker && pnpm install",
"bump-deps": "npx --yes npm-check-updates --deep -u && pnpm install",
"clean": "git clean -xdf node_modules"
},
"packageManager": "pnpm@10.10.0",
"packageManager": "pnpm@10.12.1",
"engines": {
"node": ">=22"
},
"devDependencies": {
"@auto-it/all-contributors": "^11.3.0",
"@auto-it/first-time-contributor": "^11.3.0",
"@biomejs/biome": "1.9.4",
"@types/node": "^24.0.0",
"@types/node": "^24.0.1",
"cross-env": "^7.0.3",
"kill-port": "^2.0.1",
"npm-run-all": "^4.1.5",
"tsx": "^4.19.4",
"turbo": "^2.5.4",
"tsx": "^4.20.2",
"typescript": "^5.8.3",
"ultracite": "^4.2.8"
"ultracite": "^4.2.10"
},
"pnpm": {
"overrides": {
"codemirror-graphql>@codemirror/language": "^6.11.1"
},
"onlyBuiltDependencies": [
"@biomejs/biome",
"@parcel/watcher",
"@tailwindcss/oxide",
"bufferutil",
"core-js",
"esbuild",
"sharp",
"utf-8-validate"
]
}
}

View File

@ -10,6 +10,7 @@ export interface SendOptions {
export const konosend = {
emails: {
// biome-ignore lint/suspicious/useAwait: <explanation>
send: async (_props: SendOptions) => {
throw new Error('unimplemented');
},

View File

@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
"clean": "git clean -xdf .cache .turbo dist node_modules",
"clean": "git clean -xdf .cache dist node_modules",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {

View File

@ -2,8 +2,17 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"composite": true
"composite": true,
"jsx": "react-jsx",
"jsxImportSource": "react",
"module": "ESNext",
"moduleResolution": "bundler"
},
"include": ["./*.ts", "./*.tsx"],
"include": [
"./*.ts",
"./*.tsx",
"./templates/**/*.ts",
"./templates/**/*.tsx"
],
"exclude": ["node_modules"]
}

View File

@ -1,15 +0,0 @@
import type { Thing, WithContext } from 'schema-dts';
type JsonLdProps = {
code: WithContext<Thing>;
};
export const JsonLd = ({ code }: JsonLdProps) => (
<script
type="application/ld+json"
// biome-ignore lint/security/noDangerouslySetInnerHtml: "This is a JSON-LD script, not user-generated content."
dangerouslySetInnerHTML={{ __html: JSON.stringify(code) }}
/>
);
export * from 'schema-dts';

View File

@ -1,67 +0,0 @@
import merge from 'lodash.merge';
import type { Metadata } from 'next';
type MetadataGenerator = Omit<Metadata, 'description' | 'title'> & {
title: string;
description: string;
image?: string;
};
const applicationName = 'next-forge';
const author: Metadata['authors'] = {
name: 'Hayden Bleasel',
url: 'https://haydenbleasel.com/',
};
const publisher = 'Hayden Bleasel';
const twitterHandle = '@haydenbleasel';
export const createMetadata = ({
title,
description,
image,
...properties
}: MetadataGenerator): Metadata => {
const parsedTitle = `${title} | ${applicationName}`;
const defaultMetadata: Metadata = {
title: parsedTitle,
description,
applicationName,
authors: [author],
creator: author.name,
formatDetection: {
telephone: false,
},
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: parsedTitle,
},
openGraph: {
title: parsedTitle,
description,
type: 'website',
siteName: applicationName,
locale: 'en_US',
},
publisher,
twitter: {
card: 'summary_large_image',
creator: twitterHandle,
},
};
const metadata: Metadata = merge(defaultMetadata, properties);
if (image && metadata.openGraph) {
metadata.openGraph.images = [
{
url: image,
width: 1200,
height: 630,
alt: title,
},
];
}
return metadata;
};

View File

@ -1,21 +0,0 @@
{
"name": "@konobangu/seo",
"version": "0.0.0",
"private": true,
"scripts": {
"clean": "git clean -xdf .cache .turbo dist node_modules",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"lodash.merge": "^4.6.2",
"react": "^19.0.0",
"schema-dts": "^1.1.2"
},
"devDependencies": {
"@types/lodash.merge": "^4.6.9",
"@types/node": "22.10.1",
"@types/react": "19.0.1",
"@types/react-dom": "^19.0.3",
"next": "^15.1.4"
}
}

View File

@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"composite": true
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@ -8,11 +8,11 @@
"start": "tsx main.ts"
},
"dependencies": {
"@fastify/static": "^8.1.1",
"@fastify/static": "^8.2.0",
"bittorrent-tracker": "^11.2.1",
"fastify": "^5.2.2",
"tsx": "^4.19.2",
"webtorrent": "^2.5.19"
"fastify": "^5.4.0",
"tsx": "^4.20.2",
"webtorrent": "^2.6.8"
},
"devDependencies": {
"@types/create-torrent": "^5.0.2",

View File

@ -5,11 +5,11 @@
"main": "./index.js",
"type": "commonjs",
"scripts": {
"clean": "git clean -xdf .cache .turbo dist node_modules",
"clean": "git clean -xdf .cache dist node_modules",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vitest": "^2.1.8"
"@vitejs/plugin-react": "^4.5.2",
"vitest": "^3.2.3"
}
}

5020
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -10,9 +10,6 @@
{
"path": "./packages/email"
},
{
"path": "./packages/seo"
},
{
"path": "./packages/testing"
},

View File

@ -1,36 +0,0 @@
{
"$schema": "https://turborepo.com/schema.json",
"ui": "tui",
"tasks": {
"//#dev-webui": {
"cache": false,
"persistent": true,
"interactive": true,
"interruptible": true
},
"//#dev-proxy": {
"cache": false,
"persistent": true,
"interactive": true,
"interruptible": true
},
"//#dev-recorder": {
"cache": false,
"persistent": true,
"interactive": true,
"interruptible": true
},
"//#dev-deps": {
"cache": false,
"persistent": true,
"interactive": true,
"interruptible": true
},
"//#dev-codegen-wait": {
"cache": false,
"persistent": true,
"interactive": true,
"interruptible": true
}
}
}