From 365c9798786273445067c5bcd6a070d1a0078e69 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Mon, 13 Oct 2025 10:48:50 +0800 Subject: [PATCH 01/10] feat: implement authentication and user management features --- frontend/.env.example | 2 +- frontend/src/app.d.ts | 1 + frontend/src/hooks.server.ts | 17 +++++- .../auth/adapter/gateway/authApiService.ts | 5 ++ .../adapter/gateway/authRepositoryImpl.ts | 12 ++++ .../auth/adapter/gateway/userResponseDto.ts | 37 ++++++++++++ .../lib/auth/adapter/presenter/authBloc.ts | 59 +++++++++++++++++++ .../auth/adapter/presenter/authViewModel.ts | 33 +++++++++++ .../auth/adapter/presenter/userViewModel.ts | 43 ++++++++++++++ .../application/gateway/authRepository.ts | 5 ++ .../useCase/getCurrentUserUseCase.ts | 10 ++++ frontend/src/lib/auth/domain/entity/user.ts | 11 ++++ .../auth/framework/api/authApiServiceImpl.ts | 20 +++++++ .../adapter/gateway/postRepositoryImpl.ts | 2 +- .../{repository => gateway}/postRepository.ts | 0 .../application/useCase/getAllPostsUseCase.ts | 2 +- .../application/useCase/getPostUseCase.ts | 2 +- .../post/framework/api/postApiServiceImpl.ts | 6 +- frontend/src/routes/dashboard/+layout.svelte | 40 +++++++++++++ frontend/src/routes/dashboard/+page.svelte | 1 + frontend/src/routes/post/+page.svelte | 6 +- frontend/src/routes/post/[id]/+page.svelte | 6 +- 22 files changed, 307 insertions(+), 13 deletions(-) create mode 100644 frontend/src/lib/auth/adapter/gateway/authApiService.ts create mode 100644 frontend/src/lib/auth/adapter/gateway/authRepositoryImpl.ts create mode 100644 frontend/src/lib/auth/adapter/gateway/userResponseDto.ts create mode 100644 frontend/src/lib/auth/adapter/presenter/authBloc.ts create mode 100644 frontend/src/lib/auth/adapter/presenter/authViewModel.ts create mode 100644 frontend/src/lib/auth/adapter/presenter/userViewModel.ts create mode 100644 frontend/src/lib/auth/application/gateway/authRepository.ts create mode 100644 frontend/src/lib/auth/application/useCase/getCurrentUserUseCase.ts create mode 100644 frontend/src/lib/auth/domain/entity/user.ts create mode 100644 frontend/src/lib/auth/framework/api/authApiServiceImpl.ts rename frontend/src/lib/post/application/{repository => gateway}/postRepository.ts (100%) create mode 100644 frontend/src/routes/dashboard/+layout.svelte create mode 100644 frontend/src/routes/dashboard/+page.svelte diff --git a/frontend/.env.example b/frontend/.env.example index 76e9364..a80e001 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1 +1 @@ -PUBLIC_API_BASE_URL=http://127.0.0.1:5173/api/ +PUBLIC_API_BASE_URL=http://localhost:5173/api/ diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index a20b327..3d0dcb6 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -5,6 +5,7 @@ declare global { // interface Error {} interface Locals { + authBloc: import('$lib/auth/adapter/presenter/authBloc').AuthBloc; postListBloc: import('$lib/post/adapter/presenter/postListBloc').PostListBloc; postBloc: import('$lib/post/adapter/presenter/postBloc').PostBloc; } diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index 191c3d1..e6675b7 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -8,6 +8,14 @@ import { GetPostUseCase } from '$lib/post/application/useCase/getPostUseCase'; import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl'; import type { Handle } from '@sveltejs/kit'; import { Environment } from '$lib/environment'; +import { AuthApiServiceImpl } from '$lib/auth/framework/api/authApiServiceImpl'; +import { AuthRepositoryImpl } from '$lib/auth/adapter/gateway/authRepositoryImpl'; +import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService'; +import type { AuthRepository } from '$lib/auth/application/gateway/authRepository'; +import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; +import type { PostRepository } from '$lib/post/application/gateway/postRepository'; +import { AuthBloc } from '$lib/auth/adapter/presenter/authBloc'; +import { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase'; Sentry.init({ dsn: Environment.SENTRY_DSN, @@ -16,11 +24,16 @@ Sentry.init({ }); export const handle: Handle = sequence(Sentry.sentryHandle(), ({ event, resolve }) => { - const postApiService = new PostApiServiceImpl(event.fetch); - const postRepository = new PostRepositoryImpl(postApiService); + const authApiService: AuthApiService = new AuthApiServiceImpl(event.fetch); + const authRepository: AuthRepository = new AuthRepositoryImpl(authApiService); + const getCurrentUserUseCase = new GetCurrentUserUseCase(authRepository); + + const postApiService: PostApiService = new PostApiServiceImpl(event.fetch); + const postRepository: PostRepository = new PostRepositoryImpl(postApiService); const getAllPostsUseCase = new GetAllPostsUseCase(postRepository); const getPostUseCase = new GetPostUseCase(postRepository); + event.locals.authBloc = new AuthBloc(getCurrentUserUseCase); event.locals.postListBloc = new PostListBloc(getAllPostsUseCase); event.locals.postBloc = new PostBloc(getPostUseCase); diff --git a/frontend/src/lib/auth/adapter/gateway/authApiService.ts b/frontend/src/lib/auth/adapter/gateway/authApiService.ts new file mode 100644 index 0000000..92cb76f --- /dev/null +++ b/frontend/src/lib/auth/adapter/gateway/authApiService.ts @@ -0,0 +1,5 @@ +import type { UserResponseDto } from '$lib/auth/adapter/gateway/userResponseDto'; + +export interface AuthApiService { + getCurrentUser(): Promise; +} diff --git a/frontend/src/lib/auth/adapter/gateway/authRepositoryImpl.ts b/frontend/src/lib/auth/adapter/gateway/authRepositoryImpl.ts new file mode 100644 index 0000000..40ea61c --- /dev/null +++ b/frontend/src/lib/auth/adapter/gateway/authRepositoryImpl.ts @@ -0,0 +1,12 @@ +import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService'; +import type { AuthRepository } from '$lib/auth/application/gateway/authRepository'; +import type { User } from '$lib/auth/domain/entity/user'; + +export class AuthRepositoryImpl implements AuthRepository { + constructor(private readonly authApiService: AuthApiService) {} + + async getCurrentUser(): Promise { + const result = await this.authApiService.getCurrentUser(); + return result?.toEntity() ?? null; + } +} diff --git a/frontend/src/lib/auth/adapter/gateway/userResponseDto.ts b/frontend/src/lib/auth/adapter/gateway/userResponseDto.ts new file mode 100644 index 0000000..8f64369 --- /dev/null +++ b/frontend/src/lib/auth/adapter/gateway/userResponseDto.ts @@ -0,0 +1,37 @@ +import { User } from '$lib/auth/domain/entity/user'; +import z from 'zod'; + +export const UserResponseSchema = z.object({ + id: z.int32(), + displayed_name: z.string(), + email: z.email(), +}); + +export class UserResponseDto { + readonly id: number; + readonly displayedName: string; + readonly email: string; + + private constructor(props: { id: number; displayedName: string; email: string }) { + this.id = props.id; + this.displayedName = props.displayedName; + this.email = props.email; + } + + static fromJson(json: unknown): UserResponseDto { + const parsedJson = UserResponseSchema.parse(json); + return new UserResponseDto({ + id: parsedJson.id, + displayedName: parsedJson.displayed_name, + email: parsedJson.email, + }); + } + + toEntity(): User { + return new User({ + id: this.id, + name: this.displayedName, + email: this.email, + }); + } +} diff --git a/frontend/src/lib/auth/adapter/presenter/authBloc.ts b/frontend/src/lib/auth/adapter/presenter/authBloc.ts new file mode 100644 index 0000000..a793cf3 --- /dev/null +++ b/frontend/src/lib/auth/adapter/presenter/authBloc.ts @@ -0,0 +1,59 @@ +import { AuthViewModel } from '$lib/auth/adapter/presenter/authViewModel'; +import { UserViewModel } from '$lib/auth/adapter/presenter/userViewModel'; +import type { GetCurrentUserUseCase } from '$lib/auth/application/useCase/getCurrentUserUseCase'; +import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState'; +import { get, writable } from 'svelte/store'; + +export type AuthState = AsyncState; +export type AuthEvent = CurrentUserLoadedEvent; + +export class AuthBloc { + private readonly state = writable({ + status: StatusType.Idle, + }); + + constructor( + private readonly getCurrentUserUseCase: GetCurrentUserUseCase, + initialData?: AuthViewModel + ) { + this.state.set({ + status: StatusType.Idle, + data: initialData, + }); + } + + get subscribe() { + return this.state.subscribe; + } + + async dispatch(event: AuthEvent): Promise { + switch (event.event) { + case AuthEventType.CurrentUserLoadedEvent: + return this.loadCurrentUser(); + } + } + + private async loadCurrentUser(): Promise { + this.state.set({ status: StatusType.Loading, data: get(this.state).data }); + + const user = await this.getCurrentUserUseCase.execute(); + + const userViewModel = user ? UserViewModel.fromEntity(user) : null; + const authViewModel = AuthViewModel.fromEntity(userViewModel); + const result: AuthState = { + status: StatusType.Success, + data: authViewModel, + }; + + this.state.set(result); + return result; + } +} + +export enum AuthEventType { + CurrentUserLoadedEvent, +} + +interface CurrentUserLoadedEvent { + event: AuthEventType.CurrentUserLoadedEvent; +} diff --git a/frontend/src/lib/auth/adapter/presenter/authViewModel.ts b/frontend/src/lib/auth/adapter/presenter/authViewModel.ts new file mode 100644 index 0000000..ec983dc --- /dev/null +++ b/frontend/src/lib/auth/adapter/presenter/authViewModel.ts @@ -0,0 +1,33 @@ +import { UserViewModel, type DehydratedUserProps } from '$lib/auth/adapter/presenter/userViewModel'; + +export class AuthViewModel { + readonly user: UserViewModel | null; + + constructor(params: { user: UserViewModel | null }) { + this.user = params.user; + } + + static fromEntity(user: UserViewModel | null): AuthViewModel { + return new AuthViewModel({ user }); + } + + static rehydrate(props: DehydratedAuthProps): AuthViewModel { + return new AuthViewModel({ + user: props.user ? UserViewModel.rehydrate(props.user) : null, + }); + } + + dehydrate(): DehydratedAuthProps { + return { + user: this.user ? this.user.dehydrate() : null, + }; + } + + get isAuthenticated(): boolean { + return this.user !== null; + } +} + +export interface DehydratedAuthProps { + user: DehydratedUserProps | null; +} diff --git a/frontend/src/lib/auth/adapter/presenter/userViewModel.ts b/frontend/src/lib/auth/adapter/presenter/userViewModel.ts new file mode 100644 index 0000000..b3def38 --- /dev/null +++ b/frontend/src/lib/auth/adapter/presenter/userViewModel.ts @@ -0,0 +1,43 @@ +import type { User } from '$lib/auth/domain/entity/user'; + +export class UserViewModel { + readonly id: number; + readonly name: string; + readonly email: string; + + private constructor(props: { id: number; name: string; email: string }) { + this.id = props.id; + this.name = props.name; + this.email = props.email; + } + + static fromEntity(user: User): UserViewModel { + return new UserViewModel({ + id: user.id, + name: user.name, + email: user.email, + }); + } + + static rehydrate(props: DehydratedUserProps): UserViewModel { + return new UserViewModel({ + id: props.id, + name: props.name, + email: props.email, + }); + } + + dehydrate(): DehydratedUserProps { + return { + id: this.id, + name: this.name, + email: this.email, + }; + } +} + +export interface DehydratedUserProps { + id: number; + name: string; + email: string; +} diff --git a/frontend/src/lib/auth/application/gateway/authRepository.ts b/frontend/src/lib/auth/application/gateway/authRepository.ts new file mode 100644 index 0000000..fa3930b --- /dev/null +++ b/frontend/src/lib/auth/application/gateway/authRepository.ts @@ -0,0 +1,5 @@ +import type { User } from '$lib/auth/domain/entity/user'; + +export interface AuthRepository { + getCurrentUser(): Promise; +} diff --git a/frontend/src/lib/auth/application/useCase/getCurrentUserUseCase.ts b/frontend/src/lib/auth/application/useCase/getCurrentUserUseCase.ts new file mode 100644 index 0000000..0cc5711 --- /dev/null +++ b/frontend/src/lib/auth/application/useCase/getCurrentUserUseCase.ts @@ -0,0 +1,10 @@ +import type { AuthRepository } from '$lib/auth/application/gateway/authRepository'; +import type { User } from '$lib/auth/domain/entity/user'; + +export class GetCurrentUserUseCase { + constructor(private readonly authRepository: AuthRepository) {} + + async execute(): Promise { + return await this.authRepository.getCurrentUser(); + } +} diff --git a/frontend/src/lib/auth/domain/entity/user.ts b/frontend/src/lib/auth/domain/entity/user.ts new file mode 100644 index 0000000..43a2157 --- /dev/null +++ b/frontend/src/lib/auth/domain/entity/user.ts @@ -0,0 +1,11 @@ +export class User { + id: number; + name: string; + email: string; + + constructor(props: { id: number; name: string; email: string }) { + this.id = props.id; + this.name = props.name; + this.email = props.email; + } +} diff --git a/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts b/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts new file mode 100644 index 0000000..ed1ffb2 --- /dev/null +++ b/frontend/src/lib/auth/framework/api/authApiServiceImpl.ts @@ -0,0 +1,20 @@ +import type { AuthApiService } from '$lib/auth/adapter/gateway/authApiService'; +import { UserResponseDto } from '$lib/auth/adapter/gateway/userResponseDto'; +import { Environment } from '$lib/environment'; + +export class AuthApiServiceImpl implements AuthApiService { + constructor(private readonly fetchFn: typeof fetch) {} + + async getCurrentUser(): Promise { + const url = new URL('me', Environment.API_BASE_URL); + + const response = await this.fetchFn(url); + + if (!response.ok) { + return null; + } + + const json = await response.json(); + return UserResponseDto.fromJson(json); + } +} diff --git a/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts b/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts index 33723f3..e75dfed 100644 --- a/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts +++ b/frontend/src/lib/post/adapter/gateway/postRepositoryImpl.ts @@ -1,5 +1,5 @@ import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; -import type { PostRepository } from '$lib/post/application/repository/postRepository'; +import type { PostRepository } from '$lib/post/application/gateway/postRepository'; import type { Post } from '$lib/post/domain/entity/post'; import type { PostInfo } from '$lib/post/domain/entity/postInfo'; diff --git a/frontend/src/lib/post/application/repository/postRepository.ts b/frontend/src/lib/post/application/gateway/postRepository.ts similarity index 100% rename from frontend/src/lib/post/application/repository/postRepository.ts rename to frontend/src/lib/post/application/gateway/postRepository.ts diff --git a/frontend/src/lib/post/application/useCase/getAllPostsUseCase.ts b/frontend/src/lib/post/application/useCase/getAllPostsUseCase.ts index f55957c..b585fa5 100644 --- a/frontend/src/lib/post/application/useCase/getAllPostsUseCase.ts +++ b/frontend/src/lib/post/application/useCase/getAllPostsUseCase.ts @@ -1,4 +1,4 @@ -import type { PostRepository } from '$lib/post/application/repository/postRepository'; +import type { PostRepository } from '$lib/post/application/gateway/postRepository'; import type { PostInfo } from '$lib/post/domain/entity/postInfo'; export class GetAllPostsUseCase { diff --git a/frontend/src/lib/post/application/useCase/getPostUseCase.ts b/frontend/src/lib/post/application/useCase/getPostUseCase.ts index 7e2a60d..b13d017 100644 --- a/frontend/src/lib/post/application/useCase/getPostUseCase.ts +++ b/frontend/src/lib/post/application/useCase/getPostUseCase.ts @@ -1,4 +1,4 @@ -import type { PostRepository } from '$lib/post/application/repository/postRepository'; +import type { PostRepository } from '$lib/post/application/gateway/postRepository'; import type { Post } from '$lib/post/domain/entity/post'; export class GetPostUseCase { diff --git a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts index 3abeea5..d00a847 100644 --- a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts +++ b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts @@ -4,12 +4,12 @@ import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseD import { PostResponseDto } from '$lib/post/adapter/gateway/postResponseDto'; export class PostApiServiceImpl implements PostApiService { - constructor(private fetchFn: typeof fetch) {} + constructor(private readonly fetchFn: typeof fetch) {} async getAllPosts(): Promise { const url = new URL('post', Environment.API_BASE_URL); - const response = await this.fetchFn(url.href); + const response = await this.fetchFn(url); if (!response.ok) { return []; @@ -22,7 +22,7 @@ export class PostApiServiceImpl implements PostApiService { async getPost(id: string): Promise { const url = new URL(`post/${id}`, Environment.API_BASE_URL); - const response = await this.fetchFn(url.href); + const response = await this.fetchFn(url); if (!response.ok) { return null; diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte new file mode 100644 index 0000000..296fef2 --- /dev/null +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -0,0 +1,40 @@ + + +{#if hasError} +
Error
+{:else} + {@render children()} +{/if} diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000..9e01032 --- /dev/null +++ b/frontend/src/routes/dashboard/+page.svelte @@ -0,0 +1 @@ +
Dashboard
diff --git a/frontend/src/routes/post/+page.svelte b/frontend/src/routes/post/+page.svelte index 1fb1cf3..965f7ed 100644 --- a/frontend/src/routes/post/+page.svelte +++ b/frontend/src/routes/post/+page.svelte @@ -7,13 +7,15 @@ import type { PageProps } from './$types'; import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel'; import PostOverallPage from '$lib/post/framework/ui/PostOverallPage.svelte'; + import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; + import type { PostRepository } from '$lib/post/application/gateway/postRepository'; let { data }: PageProps = $props(); const initialData = data.dehydratedData?.map((post) => PostInfoViewModel.rehydrate(post)); - const postApiService = new PostApiServiceImpl(fetch); - const postRepository = new PostRepositoryImpl(postApiService); + const postApiService: PostApiService = new PostApiServiceImpl(fetch); + const postRepository: PostRepository = new PostRepositoryImpl(postApiService); const getAllPostsUseCase = new GetAllPostsUseCase(postRepository); const postListBloc = new PostListBloc(getAllPostsUseCase, initialData); diff --git a/frontend/src/routes/post/[id]/+page.svelte b/frontend/src/routes/post/[id]/+page.svelte index da4ea70..5e14b25 100644 --- a/frontend/src/routes/post/[id]/+page.svelte +++ b/frontend/src/routes/post/[id]/+page.svelte @@ -7,14 +7,16 @@ import { setContext } from 'svelte'; import type { PageProps } from './$types'; import PostContentPage from '$lib/post/framework/ui/PostContentPage.svelte'; + import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; + import type { PostRepository } from '$lib/post/application/gateway/postRepository'; const { data, params }: PageProps = $props(); const { id } = params; const initialData = PostViewModel.rehydrate(data.dehydratedData!); - const postApiService = new PostApiServiceImpl(fetch); - const postRepository = new PostRepositoryImpl(postApiService); + const postApiService: PostApiService = new PostApiServiceImpl(fetch); + const postRepository: PostRepository = new PostRepositoryImpl(postApiService); const getPostUseCase = new GetPostUseCase(postRepository); const postBloc = new PostBloc(getPostUseCase, initialData); -- 2.47.2 From f228a9bb4a012913db6d6add0e2d19a83aab8f09 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Mon, 13 Oct 2025 10:51:55 +0800 Subject: [PATCH 02/10] feat: add custom ErrorPage component for improved error handling --- .../lib/common/framework/ui/ErrorPage.svelte | 14 ++++++++++++++ frontend/src/routes/+error.svelte | 19 +++++-------------- frontend/src/routes/dashboard/+layout.svelte | 13 +++++++++---- 3 files changed, 28 insertions(+), 18 deletions(-) create mode 100644 frontend/src/lib/common/framework/ui/ErrorPage.svelte diff --git a/frontend/src/lib/common/framework/ui/ErrorPage.svelte b/frontend/src/lib/common/framework/ui/ErrorPage.svelte new file mode 100644 index 0000000..e097286 --- /dev/null +++ b/frontend/src/lib/common/framework/ui/ErrorPage.svelte @@ -0,0 +1,14 @@ +
+
+

404

+

+
+
+ Not +
+ Found. +

+
+
diff --git a/frontend/src/routes/+error.svelte b/frontend/src/routes/+error.svelte index 5881914..4d78c58 100644 --- a/frontend/src/routes/+error.svelte +++ b/frontend/src/routes/+error.svelte @@ -1,14 +1,5 @@ -
-
-

404

-

-
-
- Not -
- Found. -

-
-
+ + + diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 296fef2..5c2313c 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -8,6 +8,7 @@ import { onMount, setContext } from 'svelte'; import type { LayoutProps } from './$types'; import { StatusType } from '$lib/common/adapter/presenter/asyncState'; + import ErrorPage from '$lib/common/framework/ui/ErrorPage.svelte'; const { children }: LayoutProps = $props(); @@ -18,10 +19,12 @@ setContext(AuthBloc.name, authBloc); - const authState = $derived($authBloc); - onMount(() => authBloc.dispatch({ event: AuthEventType.CurrentUserLoadedEvent })); + const authState = $derived($authBloc); + const isLoading = $derived.by( + () => authState.status === StatusType.Loading || authState.status === StatusType.Idle + ); const hasError = $derived.by(() => { if (authState.status === StatusType.Error) { return true; @@ -33,8 +36,10 @@ }); -{#if hasError} -
Error
+{#if isLoading} +
+{:else if hasError} + {:else} {@render children()} {/if} -- 2.47.2 From 5e94cceb785b7e24fccf22460cf8df2f6ee339d0 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Mon, 13 Oct 2025 16:21:03 +0800 Subject: [PATCH 03/10] chore: update package manager version --- frontend/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index d82afd3..c1c0fff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,9 @@ "format": "prettier --write .", "lint": "prettier --check . && eslint ." }, + "dependencies": { + "@sentry/sveltekit": "^10.1.0" + }, "devDependencies": { "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", @@ -47,8 +50,5 @@ "esbuild" ] }, - "packageManager": "pnpm@10.12.4", - "dependencies": { - "@sentry/sveltekit": "^10.1.0" - } + "packageManager": "pnpm@10.17.1" } -- 2.47.2 From 07ab4ec15706f02e1c25cee8affdbb64871e2dc9 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Mon, 13 Oct 2025 16:52:13 +0800 Subject: [PATCH 04/10] feat: import shadcn --- frontend/components.json | 16 +++ frontend/package.json | 5 + frontend/pnpm-lock.yaml | 50 ++++++++ frontend/src/app.css | 119 ++++++++++++++++++ .../lib/common/framework/components/utils.ts | 13 ++ 5 files changed, 203 insertions(+) create mode 100644 frontend/components.json create mode 100644 frontend/src/lib/common/framework/components/utils.ts diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..97440fd --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/app.css", + "baseColor": "gray" + }, + "aliases": { + "components": "$lib/common/framework/components", + "utils": "$lib/common/framework/components/utils", + "ui": "$lib/common/framework/components/ui", + "hooks": "$lib/common/framework/components/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" +} diff --git a/frontend/package.json b/frontend/package.json index c1c0fff..5fe0ff5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", "@fortawesome/fontawesome-free": "^7.0.0", + "@lucide/svelte": "^0.545.0", "@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/adapter-node": "^5.2.13", "@sveltejs/kit": "^2.22.0", @@ -28,6 +29,7 @@ "@tailwindcss/vite": "^4.0.0", "@types/markdown-it": "^14.1.2", "@types/sanitize-html": "^2.16.0", + "clsx": "^2.1.1", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-svelte": "^3.0.0", @@ -39,7 +41,10 @@ "sanitize-html": "^2.17.0", "svelte": "^5.0.0", "svelte-check": "^4.0.0", + "tailwind-merge": "^3.3.1", + "tailwind-variants": "^3.1.1", "tailwindcss": "^4.0.0", + "tw-animate-css": "^1.4.0", "typescript": "^5.0.0", "typescript-eslint": "^8.20.0", "vite": "^7.0.4", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 65c56df..ecb9990 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@fortawesome/fontawesome-free': specifier: ^7.0.0 version: 7.0.0 + '@lucide/svelte': + specifier: ^0.545.0 + version: 0.545.0(svelte@5.36.13) '@sveltejs/adapter-auto': specifier: ^6.0.0 version: 6.0.1(@sveltejs/kit@2.25.1(@sveltejs/vite-plugin-svelte@6.1.0(svelte@5.36.13)(vite@7.0.5(@types/node@24.2.0)(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.36.13)(vite@7.0.5(@types/node@24.2.0)(jiti@2.4.2)(lightningcss@1.30.1))) @@ -45,6 +48,9 @@ importers: '@types/sanitize-html': specifier: ^2.16.0 version: 2.16.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 eslint: specifier: ^9.18.0 version: 9.31.0(jiti@2.4.2) @@ -78,9 +84,18 @@ importers: svelte-check: specifier: ^4.0.0 version: 4.3.0(picomatch@4.0.3)(svelte@5.36.13)(typescript@5.8.3) + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 + tailwind-variants: + specifier: ^3.1.1 + version: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.11) tailwindcss: specifier: ^4.0.0 version: 4.1.11 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 typescript: specifier: ^5.0.0 version: 5.8.3 @@ -416,6 +431,11 @@ packages: '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@lucide/svelte@0.545.0': + resolution: {integrity: sha512-RlAtWefx9MdpXaOMbx3Qv3/NqpeZKOIPxN2D0RBN2+op0opKly8VgYEEWZTT6Ow/zf7UwyTg6/0ExJlsVLK+8g==} + peerDependencies: + svelte: ^5 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2118,6 +2138,19 @@ packages: resolution: {integrity: sha512-LnSywHHQM/nJekC65d84T1Yo85IeCYN4AryWYPhTokSvcEAFdYFCfbMhX1mc0zHizT736QQj0nalUk+SXaWrEQ==} engines: {node: '>=18'} + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + + tailwind-variants@3.1.1: + resolution: {integrity: sha512-ftLXe3krnqkMHsuBTEmaVUXYovXtPyTK7ckEfDRXS8PBZx0bAUas+A0jYxuKA5b8qg++wvQ3d2MQ7l/xeZxbZQ==} + engines: {node: '>=16.x', pnpm: '>=7.x'} + peerDependencies: + tailwind-merge: '>=3.0.0' + tailwindcss: '*' + peerDependenciesMeta: + tailwind-merge: + optional: true + tailwindcss@4.1.11: resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} @@ -2159,6 +2192,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2561,6 +2597,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.4 + '@lucide/svelte@0.545.0(svelte@5.36.13)': + dependencies: + svelte: 5.36.13 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4284,6 +4324,14 @@ snapshots: magic-string: 0.30.17 zimmerframe: 1.1.2 + tailwind-merge@3.3.1: {} + + tailwind-variants@3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.11): + dependencies: + tailwindcss: 4.1.11 + optionalDependencies: + tailwind-merge: 3.3.1 + tailwindcss@4.1.11: {} tapable@2.2.2: {} @@ -4323,6 +4371,8 @@ snapshots: tslib@2.8.1: {} + tw-animate-css@1.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/frontend/src/app.css b/frontend/src/app.css index bc37462..6709bec 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,7 +1,126 @@ @import 'tailwindcss'; +@import 'tw-animate-css'; @plugin '@tailwindcss/typography'; @config "../tailwind.config.js"; +@custom-variant dark (&:is(.dark *)); + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.13 0.028 261.692); + --card: oklch(1 0 0); + --card-foreground: oklch(0.13 0.028 261.692); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.13 0.028 261.692); + --primary: oklch(0.21 0.034 264.665); + --primary-foreground: oklch(0.985 0.002 247.839); + --secondary: oklch(0.967 0.003 264.542); + --secondary-foreground: oklch(0.21 0.034 264.665); + --muted: oklch(0.967 0.003 264.542); + --muted-foreground: oklch(0.551 0.027 264.364); + --accent: oklch(0.967 0.003 264.542); + --accent-foreground: oklch(0.21 0.034 264.665); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.928 0.006 264.531); + --input: oklch(0.928 0.006 264.531); + --ring: oklch(0.707 0.022 261.325); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0.002 247.839); + --sidebar-foreground: oklch(0.13 0.028 261.692); + --sidebar-primary: oklch(0.21 0.034 264.665); + --sidebar-primary-foreground: oklch(0.985 0.002 247.839); + --sidebar-accent: oklch(0.967 0.003 264.542); + --sidebar-accent-foreground: oklch(0.21 0.034 264.665); + --sidebar-border: oklch(0.928 0.006 264.531); + --sidebar-ring: oklch(0.707 0.022 261.325); +} + +.dark { + --background: oklch(0.13 0.028 261.692); + --foreground: oklch(0.985 0.002 247.839); + --card: oklch(0.21 0.034 264.665); + --card-foreground: oklch(0.985 0.002 247.839); + --popover: oklch(0.21 0.034 264.665); + --popover-foreground: oklch(0.985 0.002 247.839); + --primary: oklch(0.928 0.006 264.531); + --primary-foreground: oklch(0.21 0.034 264.665); + --secondary: oklch(0.278 0.033 256.848); + --secondary-foreground: oklch(0.985 0.002 247.839); + --muted: oklch(0.278 0.033 256.848); + --muted-foreground: oklch(0.707 0.022 261.325); + --accent: oklch(0.278 0.033 256.848); + --accent-foreground: oklch(0.985 0.002 247.839); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.551 0.027 264.364); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.034 264.665); + --sidebar-foreground: oklch(0.985 0.002 247.839); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0.002 247.839); + --sidebar-accent: oklch(0.278 0.033 256.848); + --sidebar-accent-foreground: oklch(0.985 0.002 247.839); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.551 0.027 264.364); +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + @font-face { font-family: 'HackNerdMono'; src: url('/font/HackNerdMono.woff2') format('woff2'); diff --git a/frontend/src/lib/common/framework/components/utils.ts b/frontend/src/lib/common/framework/components/utils.ts new file mode 100644 index 0000000..55b3a91 --- /dev/null +++ b/frontend/src/lib/common/framework/components/utils.ts @@ -0,0 +1,13 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; -- 2.47.2 From 3dd5d711686b434ac642897124e074f6fe97249b Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Mon, 13 Oct 2025 18:44:08 +0800 Subject: [PATCH 05/10] feat: update Navbar structure and enhance layout with main tag; improve robots.txt for better crawling control --- frontend/src/lib/common/framework/ui/Navbar.svelte | 4 ++-- frontend/src/routes/+layout.svelte | 4 +++- frontend/static/robots.txt | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/common/framework/ui/Navbar.svelte b/frontend/src/lib/common/framework/ui/Navbar.svelte index e3586a8..a1d316e 100644 --- a/frontend/src/lib/common/framework/ui/Navbar.svelte +++ b/frontend/src/lib/common/framework/ui/Navbar.svelte @@ -3,7 +3,7 @@ import NavbarAction from '$lib/common/framework/ui/NavbarAction.svelte'; -
+
- + diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index f5d435c..2383ed1 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -12,6 +12,8 @@
- +
+ +