BLOG-45 Post content page #67
6
frontend/src/app.d.ts
vendored
6
frontend/src/app.d.ts
vendored
@ -3,7 +3,11 @@
|
|||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
|
||||||
|
interface Locals {
|
||||||
|
postListBloc: import('$lib/post/adapter/presenter/postListBloc').PostListBloc;
|
||||||
|
}
|
||||||
|
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
|
13
frontend/src/hooks.server.ts
Normal file
13
frontend/src/hooks.server.ts
Normal file
@ -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);
|
||||||
|
};
|
@ -5,12 +5,14 @@ export enum StatusType {
|
|||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IdleState {
|
export interface IdleState<T> {
|
||||||
status: StatusType.Idle;
|
status: StatusType.Idle;
|
||||||
|
data?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadingState {
|
export interface LoadingState<T> {
|
||||||
status: StatusType.Loading;
|
status: StatusType.Loading;
|
||||||
|
data?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SuccessState<T> {
|
export interface SuccessState<T> {
|
||||||
@ -18,9 +20,10 @@ export interface SuccessState<T> {
|
|||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorState {
|
export interface ErrorState<T> {
|
||||||
status: StatusType.Error;
|
status: StatusType.Error;
|
||||||
|
data?: T;
|
||||||
error: Error;
|
error: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AsyncState<T> = IdleState | LoadingState | SuccessState<T> | ErrorState;
|
export type AsyncState<T> = IdleState<T> | LoadingState<T> | SuccessState<T> | ErrorState<T>;
|
||||||
|
@ -56,6 +56,10 @@ export class ColorViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static rehydrate(props: DehydratedColorProps): ColorViewModel {
|
||||||
|
return new ColorViewModel(props);
|
||||||
|
}
|
||||||
|
|
||||||
get hex(): string {
|
get hex(): string {
|
||||||
const toHex = (value: number) => value.toString(16).padStart(2, '0');
|
const toHex = (value: number) => value.toString(16).padStart(2, '0');
|
||||||
return `#${toHex(this.red)}${toHex(this.green)}${toHex(this.blue)}${toHex(this.alpha)}`;
|
return `#${toHex(this.red)}${toHex(this.green)}${toHex(this.blue)}${toHex(this.alpha)}`;
|
||||||
@ -105,6 +109,15 @@ export class ColorViewModel {
|
|||||||
darken(amount: number): ColorViewModel {
|
darken(amount: number): ColorViewModel {
|
||||||
return this.lighten(-amount);
|
return this.lighten(-amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dehydrate(): DehydratedColorProps {
|
||||||
|
return {
|
||||||
|
red: this.red,
|
||||||
|
green: this.green,
|
||||||
|
blue: this.blue,
|
||||||
|
alpha: this.alpha
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Hsl {
|
interface Hsl {
|
||||||
@ -112,3 +125,10 @@ interface Hsl {
|
|||||||
s: number;
|
s: number;
|
||||||
l: number;
|
l: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DehydratedColorProps {
|
||||||
|
red: number;
|
||||||
|
green: number;
|
||||||
|
blue: number;
|
||||||
|
alpha: number;
|
||||||
|
}
|
||||||
|
@ -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';
|
import type { Label } from '$lib/post/domain/entity/label';
|
||||||
|
|
||||||
export class LabelViewModel {
|
export class LabelViewModel {
|
||||||
@ -19,4 +22,26 @@ export class LabelViewModel {
|
|||||||
color: ColorViewModel.fromEntity(label.color)
|
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;
|
||||||
}
|
}
|
||||||
|
@ -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';
|
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
|
||||||
|
|
||||||
export class PostInfoViewModel {
|
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 {
|
get formattedPublishedTime(): string {
|
||||||
return this.publishedTime.toISOString().slice(0, 10);
|
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;
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,48 @@
|
|||||||
import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
import { StatusType, type AsyncState } from '$lib/common/adapter/presenter/asyncState';
|
||||||
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
||||||
import type { GetAllPostUseCase } from '$lib/post/application/useCase/getAllPostsUseCase';
|
import type { GetAllPostUseCase } from '$lib/post/application/useCase/getAllPostsUseCase';
|
||||||
import { writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export type PostListState = AsyncState<readonly PostInfoViewModel[]>;
|
||||||
|
export type PostListEvent = PostListLoadedEvent;
|
||||||
|
|
||||||
export class PostListBloc {
|
export class PostListBloc {
|
||||||
constructor(private readonly getAllPostsUseCase: GetAllPostUseCase) {}
|
private readonly state = writable<PostListState>({
|
||||||
|
|
||||||
private readonly state = writable<AsyncState<readonly PostInfoViewModel[]>>({
|
|
||||||
status: StatusType.Idle
|
status: StatusType.Idle
|
||||||
});
|
});
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly getAllPostsUseCase: GetAllPostUseCase,
|
||||||
|
initialData?: readonly PostInfoViewModel[]
|
||||||
|
) {
|
||||||
|
this.state.set({
|
||||||
|
status: StatusType.Idle,
|
||||||
|
data: initialData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get subscribe() {
|
get subscribe() {
|
||||||
return this.state.subscribe;
|
return this.state.subscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(event: PostListEvent) {
|
async dispatch(event: PostListEvent): Promise<PostListState> {
|
||||||
switch (event.event) {
|
switch (event.event) {
|
||||||
case PostListEventType.PostListLoadedEvent:
|
case PostListEventType.PostListLoadedEvent:
|
||||||
this.loadPosts();
|
return this.loadPosts();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadPosts() {
|
private async loadPosts(): Promise<PostListState> {
|
||||||
this.state.set({ status: StatusType.Loading });
|
this.state.set({ status: StatusType.Loading, data: get(this.state).data });
|
||||||
const posts = await this.getAllPostsUseCase.execute();
|
const posts = await this.getAllPostsUseCase.execute();
|
||||||
const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post));
|
const postViewModels = posts.map((post) => PostInfoViewModel.fromEntity(post));
|
||||||
this.state.set({
|
const result: PostListState = {
|
||||||
status: StatusType.Success,
|
status: StatusType.Success,
|
||||||
data: postViewModels
|
data: postViewModels
|
||||||
});
|
};
|
||||||
|
|
||||||
|
this.state.set(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,5 +53,3 @@ export enum PostListEventType {
|
|||||||
export interface PostListLoadedEvent {
|
export interface PostListLoadedEvent {
|
||||||
event: PostListEventType.PostListLoadedEvent;
|
event: PostListEventType.PostListLoadedEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PostListEvent = PostListLoadedEvent;
|
|
||||||
|
@ -3,10 +3,12 @@ import type { PostApiService } from '$lib/post/adapter/gateway/postApiService';
|
|||||||
import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto';
|
import { PostInfoResponseDto } from '$lib/post/adapter/gateway/postInfoResponseDto';
|
||||||
|
|
||||||
export class PostApiServiceImpl implements PostApiService {
|
export class PostApiServiceImpl implements PostApiService {
|
||||||
|
constructor(private fetchFn: typeof fetch) {}
|
||||||
|
|
||||||
async getAllPosts(): Promise<PostInfoResponseDto[]> {
|
async getAllPosts(): Promise<PostInfoResponseDto[]> {
|
||||||
const url = new URL('post/all', Environment.API_BASE_URL);
|
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) {
|
if (!response.ok) {
|
||||||
return [];
|
return [];
|
||||||
|
13
frontend/src/lib/post/framework/ui/Label.svelte
Normal file
13
frontend/src/lib/post/framework/ui/Label.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { LabelViewModel } from '$lib/post/adapter/presenter/labelViewModel';
|
||||||
|
|
||||||
|
const { label }: { label: LabelViewModel } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-row items-center gap-x-1 rounded-full px-2 py-0.5"
|
||||||
|
style="background-color: {label.color.hex};"
|
||||||
|
>
|
||||||
|
<div class="size-2 rounded-full" style="background-color: {label.color.darken(0.2).hex};"></div>
|
||||||
|
<span>{label.name}</span>
|
||||||
|
</div>
|
@ -0,0 +1,6 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PostInfoViewModel } from "$lib/post/adapter/presenter/postInfoViewModel";
|
||||||
|
|
||||||
|
const { post }: { post: PostInfoViewModel } = $props();
|
||||||
|
</script>
|
||||||
|
|
@ -12,11 +12,9 @@
|
|||||||
|
|
||||||
<div class="container pb-10">
|
<div class="container pb-10">
|
||||||
<div class="py-9 text-center text-3xl font-bold text-gray-800 md:py-20 md:text-5xl">文章</div>
|
<div class="py-9 text-center text-3xl font-bold text-gray-800 md:py-20 md:text-5xl">文章</div>
|
||||||
{#if state.status === StatusType.Success}
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-y-8 lg:grid-cols-3">
|
||||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-y-8 lg:grid-cols-3">
|
{#each state.data ?? [] as postInfo (postInfo.id)}
|
||||||
{#each state.data as postInfo (postInfo.id)}
|
<PostPreview {postInfo} />
|
||||||
<PostPreview {postInfo} />
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,21 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { LabelViewModel } from '$lib/post/adapter/presenter/labelViewModel';
|
import type { LabelViewModel } from '$lib/post/adapter/presenter/labelViewModel';
|
||||||
|
import Label from '$lib/post/framework/ui/Label.svelte';
|
||||||
|
|
||||||
const { labels }: { labels: readonly LabelViewModel[] } = $props();
|
const { labels }: { labels: readonly LabelViewModel[] } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-row gap-x-2 text-xs">
|
<div class="flex flex-row gap-x-2 text-xs">
|
||||||
{#each labels.slice(0, 2) as label (label.id)}
|
{#each labels.slice(0, 2) as label (label.id)}
|
||||||
<div
|
<Label {label} />
|
||||||
class="flex flex-row items-center gap-x-1 rounded-full px-2 py-0.5"
|
|
||||||
style="background-color: {label.color.hex};"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="size-2 rounded-full"
|
|
||||||
style="background-color: {label.color.darken(0.2).hex};"
|
|
||||||
></div>
|
|
||||||
<span>{label.name}</span>
|
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
{#if labels.length > 2}
|
{#if labels.length > 2}
|
||||||
<div class="rounded-full bg-gray-200 px-2 py-0.5">
|
<div class="rounded-full bg-gray-200 px-2 py-0.5">
|
||||||
|
12
frontend/src/routes/post/+page.server.ts
Normal file
12
frontend/src/routes/post/+page.server.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { PostListEventType } from '$lib/post/adapter/presenter/postListBloc';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
const { postListBloc } = locals;
|
||||||
|
|
||||||
|
const state = await postListBloc.dispatch({ event: PostListEventType.PostListLoadedEvent });
|
||||||
|
|
||||||
|
return {
|
||||||
|
dehydratedData: state.data?.map((post) => post.dehydrate())
|
||||||
|
};
|
||||||
|
};
|
@ -3,13 +3,19 @@
|
|||||||
import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc';
|
import { PostListBloc } from '$lib/post/adapter/presenter/postListBloc';
|
||||||
import { GetAllPostUseCase } from '$lib/post/application/useCase/getAllPostsUseCase';
|
import { GetAllPostUseCase } from '$lib/post/application/useCase/getAllPostsUseCase';
|
||||||
import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl';
|
import { PostApiServiceImpl } from '$lib/post/framework/api/postApiServiceImpl';
|
||||||
import PostOverallPage from '$lib/post/framework/ui/PostOverallPage.svelte';
|
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
import { PostInfoViewModel } from '$lib/post/adapter/presenter/postInfoViewModel';
|
||||||
|
import PostOverallPage from '$lib/post/framework/ui/PostOverallPage.svelte';
|
||||||
|
|
||||||
const postApiService = new PostApiServiceImpl();
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
|
const initialData = data.dehydratedData?.map((post) => PostInfoViewModel.rehydrate(post));
|
||||||
|
|
||||||
|
const postApiService = new PostApiServiceImpl(fetch);
|
||||||
const postRepository = new PostRepositoryImpl(postApiService);
|
const postRepository = new PostRepositoryImpl(postApiService);
|
||||||
const getAllPostsUseCase = new GetAllPostUseCase(postRepository);
|
const getAllPostsUseCase = new GetAllPostUseCase(postRepository);
|
||||||
const postListBloc = new PostListBloc(getAllPostsUseCase);
|
const postListBloc = new PostListBloc(getAllPostsUseCase, initialData);
|
||||||
|
|
||||||
setContext(PostListBloc.name, postListBloc);
|
setContext(PostListBloc.name, postListBloc);
|
||||||
</script>
|
</script>
|
||||||
|
7
frontend/src/routes/post/[id]/+page.svelte
Normal file
7
frontend/src/routes/post/[id]/+page.svelte
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
|
const { data }: PageProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>{data.id}</div>
|
7
frontend/src/routes/post/[id]/+page.ts
Normal file
7
frontend/src/routes/post/[id]/+page.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ params }) => {
|
||||||
|
return {
|
||||||
|
id: params.id,
|
||||||
|
}
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user