diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts index 1af894e..cac9cce 100644 --- a/frontend/src/app.d.ts +++ b/frontend/src/app.d.ts @@ -3,7 +3,11 @@ declare global { namespace App { // interface Error {} - // interface Locals {} + + interface Locals { + postListBloc: import('$lib/post/adapter/presenter/postListBloc').PostListBloc; + } + // interface PageData {} // interface PageState {} // interface Platform {} diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts new file mode 100644 index 0000000..b2b4de9 --- /dev/null +++ b/frontend/src/hooks.server.ts @@ -0,0 +1,13 @@ +import { PostRepositoryImpl } from '$lib/post/adapter/gateway/postRepositoryImpl'; +import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc'; +import { GetAllPostUseCase } from '$lib/post/application/useCase/getAllPostsUseCase'; +import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl'; +import type { Handle } from '@sveltejs/kit'; + +export const handle: Handle = ({ event, resolve }) => { + const postApiService = new PostApiServiceImpl(event.fetch); + const postRepository = new PostRepositoryImpl(postApiService); + const getAllPostsUseCase = new GetAllPostUseCase(postRepository); + event.locals.postListBloc = new PostListBloc(getAllPostsUseCase); + return resolve(event); +}; diff --git a/frontend/src/lib/common/adapter/presenter/asyncState.ts b/frontend/src/lib/common/adapter/presenter/asyncState.ts index b80e9af..a058935 100644 --- a/frontend/src/lib/common/adapter/presenter/asyncState.ts +++ b/frontend/src/lib/common/adapter/presenter/asyncState.ts @@ -5,12 +5,14 @@ export enum StatusType { Error } -export interface IdleState { +export interface IdleState { status: StatusType.Idle; + data?: T; } -export interface LoadingState { +export interface LoadingState { status: StatusType.Loading; + data?: T; } export interface SuccessState { @@ -18,9 +20,10 @@ export interface SuccessState { data: T; } -export interface ErrorState { +export interface ErrorState { status: StatusType.Error; + data?: T; error: Error; } -export type AsyncState = IdleState | LoadingState | SuccessState | ErrorState; +export type AsyncState = IdleState | LoadingState | SuccessState | ErrorState; diff --git a/frontend/src/lib/post/adapter/presenter/colorViewModel.ts b/frontend/src/lib/post/adapter/presenter/colorViewModel.ts index 827aa46..cf08d2f 100644 --- a/frontend/src/lib/post/adapter/presenter/colorViewModel.ts +++ b/frontend/src/lib/post/adapter/presenter/colorViewModel.ts @@ -56,6 +56,10 @@ export class ColorViewModel { }); } + static rehydrate(props: DehydratedColorProps): ColorViewModel { + return new ColorViewModel(props); + } + get hex(): string { const toHex = (value: number) => value.toString(16).padStart(2, '0'); return `#${toHex(this.red)}${toHex(this.green)}${toHex(this.blue)}${toHex(this.alpha)}`; @@ -105,6 +109,15 @@ export class ColorViewModel { darken(amount: number): ColorViewModel { return this.lighten(-amount); } + + dehydrate(): DehydratedColorProps { + return { + red: this.red, + green: this.green, + blue: this.blue, + alpha: this.alpha + }; + } } interface Hsl { @@ -112,3 +125,10 @@ interface Hsl { s: number; l: number; } + +export interface DehydratedColorProps { + red: number; + green: number; + blue: number; + alpha: number; +} diff --git a/frontend/src/lib/post/adapter/presenter/labelViewModel.ts b/frontend/src/lib/post/adapter/presenter/labelViewModel.ts index 4abebd3..c4ba633 100644 --- a/frontend/src/lib/post/adapter/presenter/labelViewModel.ts +++ b/frontend/src/lib/post/adapter/presenter/labelViewModel.ts @@ -1,4 +1,7 @@ -import { ColorViewModel } from '$lib/post/adapter/presenter/colorViewModel'; +import { + ColorViewModel, + type DehydratedColorProps +} from '$lib/post/adapter/presenter/colorViewModel'; import type { Label } from '$lib/post/domain/entity/label'; export class LabelViewModel { @@ -19,4 +22,26 @@ export class LabelViewModel { color: ColorViewModel.fromEntity(label.color) }); } + + static rehydrate(props: DehydratedLabelProps): LabelViewModel { + return new LabelViewModel({ + id: props.id, + name: props.name, + color: ColorViewModel.rehydrate(props.color) + }); + } + + dehydrate(): DehydratedLabelProps { + return { + id: this.id, + name: this.name, + color: this.color.dehydrate() + }; + } +} + +export interface DehydratedLabelProps { + id: number; + name: string; + color: DehydratedColorProps; } diff --git a/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts b/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts index 68ae230..670acb7 100644 --- a/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts +++ b/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts @@ -1,4 +1,7 @@ -import { LabelViewModel } from '$lib/post/adapter/presenter/labelViewModel'; +import { + LabelViewModel, + type DehydratedLabelProps +} from '$lib/post/adapter/presenter/labelViewModel'; import type { PostInfo } from '$lib/post/domain/entity/postInfo'; export class PostInfoViewModel { @@ -36,7 +39,38 @@ export class PostInfoViewModel { }); } + static rehydrate(props: DehydratedPostInfoProps): PostInfoViewModel { + return new PostInfoViewModel({ + id: props.id, + title: props.title, + description: props.description, + previewImageUrl: new URL(props.previewImageUrl), + labels: props.labels.map((label) => LabelViewModel.rehydrate(label)), + publishedTime: new Date(props.publishedTime) + }); + } + get formattedPublishedTime(): string { return this.publishedTime.toISOString().slice(0, 10); } + + dehydrate(): DehydratedPostInfoProps { + return { + id: this.id, + title: this.title, + description: this.description, + previewImageUrl: this.previewImageUrl.href, + labels: this.labels.map((label) => label.dehydrate()), + publishedTime: this.publishedTime.getTime() + }; + } +} + +export interface DehydratedPostInfoProps { + id: number; + title: string; + description: string; + previewImageUrl: string; + labels: DehydratedLabelProps[]; + publishedTime: number; } diff --git a/frontend/src/lib/post/adapter/presenter/postListBloc.ts b/frontend/src/lib/post/adapter/presenter/postListBloc.ts index 3bccf0d..11829fe 100644 --- a/frontend/src/lib/post/adapter/presenter/postListBloc.ts +++ b/frontend/src/lib/post/adapter/presenter/postListBloc.ts @@ -1,35 +1,48 @@ import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState'; import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel'; import type { GetAllPostUseCase } from '$lib/post/application/useCase/getAllPostsUseCase'; -import { writable } from 'svelte/store'; +import { get, writable } from 'svelte/store'; + +export type PostListState = AsyncState; +export type PostListEvent = PostListLoadedEvent; export class PostListBloc { - constructor(private readonly getAllPostsUseCase: GetAllPostUseCase) {} - - private readonly state = writable>({ + private readonly state = writable({ status: StatusType.Idle }); + constructor( + private readonly getAllPostsUseCase: GetAllPostUseCase, + initialData?: readonly PostInfoViewModel[] + ) { + this.state.set({ + status: StatusType.Idle, + data: initialData + }); + } + get subscribe() { return this.state.subscribe; } - dispatch(event: PostListEvent) { + async dispatch(event: PostListEvent): Promise { switch (event.event) { case PostListEventType.PostListLoadedEvent: - this.loadPosts(); - break; + return this.loadPosts(); } } - private async loadPosts() { - this.state.set({ status: StatusType.Loading }); + private async loadPosts(): Promise { + this.state.set({ status: StatusType.Loading, data: get(this.state).data }); const posts = await this.getAllPostsUseCase.execute(); const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post)); - this.state.set({ + const result: PostListState = { status: StatusType.Success, data: postViewModels - }); + }; + + this.state.set(result); + return result; } } @@ -40,5 +53,3 @@ export enum PostListEventType { export interface PostListLoadedEvent { event: PostListEventType.PostListLoadedEvent; } - -export type PostListEvent = PostListLoadedEvent; diff --git a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts index e1d4040..93091a2 100644 --- a/frontend/src/lib/post/framework/api/postApiServiceImpl.ts +++ b/frontend/src/lib/post/framework/api/postApiServiceImpl.ts @@ -3,10 +3,12 @@ import type { PostApiService } from '$lib/post/adapter/gateway/postApiService'; import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto'; export class PostApiServiceImpl implements PostApiService { + constructor(private fetchFn: typeof fetch) {} + async getAllPosts(): Promise { const url = new URL('post/all', Environment.API_BASE_URL); - const response = await fetch(url.href); + const response = await this.fetchFn(url.href); if (!response.ok) { return []; diff --git a/frontend/src/lib/post/framework/ui/Label.svelte b/frontend/src/lib/post/framework/ui/Label.svelte new file mode 100644 index 0000000..58763c6 --- /dev/null +++ b/frontend/src/lib/post/framework/ui/Label.svelte @@ -0,0 +1,13 @@ + + +
+
+ {label.name} +
diff --git a/frontend/src/lib/post/framework/ui/PostContentPage.svelte b/frontend/src/lib/post/framework/ui/PostContentPage.svelte new file mode 100644 index 0000000..f428674 --- /dev/null +++ b/frontend/src/lib/post/framework/ui/PostContentPage.svelte @@ -0,0 +1,6 @@ + + diff --git a/frontend/src/lib/post/framework/ui/PostOverallPage.svelte b/frontend/src/lib/post/framework/ui/PostOverallPage.svelte index c9d3153..19cdd4d 100644 --- a/frontend/src/lib/post/framework/ui/PostOverallPage.svelte +++ b/frontend/src/lib/post/framework/ui/PostOverallPage.svelte @@ -12,11 +12,9 @@
文章
- {#if state.status === StatusType.Success} -
- {#each state.data as postInfo (postInfo.id)} - - {/each} -
- {/if} +
+ {#each state.data ?? [] as postInfo (postInfo.id)} + + {/each} +
diff --git a/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte b/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte index d9e132a..470271e 100644 --- a/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte +++ b/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte @@ -1,21 +1,13 @@
{#each labels.slice(0, 2) as label (label.id)} -
-
- {label.name} -
+