Compare commits

...

2 Commits

Author SHA1 Message Date
898ee86f91 feat: add semanticId to PostInfo, PostInfoViewModel, and PostInfoResponseDto; update PostPreview link
Some checks failed
Frontend CI / build (push) Failing after 53s
PR Title Check / pr-title-check (pull_request) Successful in 16s
2025-10-12 18:10:01 +08:00
6232105458 feat: update post ID type from number to string across the application 2025-10-12 18:03:29 +08:00
14 changed files with 30 additions and 19 deletions

1
frontend/.env.example Normal file
View File

@ -0,0 +1 @@
PUBLIC_API_BASE_URL=http://127.0.0.1:5173/api/

View File

@ -3,5 +3,5 @@ import type { PostResponseDto } from '$lib/post/adapter/gateway/postResponseDto'
export interface PostApiService { export interface PostApiService {
getAllPosts(): Promise<PostInfoResponseDto[]>; getAllPosts(): Promise<PostInfoResponseDto[]>;
getPost(id: number): Promise<PostResponseDto | null>; getPost(id: string): Promise<PostResponseDto | null>;
} }

View File

@ -4,6 +4,7 @@ import z from 'zod';
export const PostInfoResponseSchema = z.object({ export const PostInfoResponseSchema = z.object({
id: z.int32(), id: z.int32(),
semantic_id: z.string(),
title: z.string(), title: z.string(),
description: z.string(), description: z.string(),
preview_image_url: z.url(), preview_image_url: z.url(),
@ -13,6 +14,7 @@ export const PostInfoResponseSchema = z.object({
export class PostInfoResponseDto { export class PostInfoResponseDto {
readonly id: number; readonly id: number;
readonly semanticId: string;
readonly title: string; readonly title: string;
readonly description: string; readonly description: string;
readonly previewImageUrl: URL; readonly previewImageUrl: URL;
@ -21,6 +23,7 @@ export class PostInfoResponseDto {
private constructor(props: { private constructor(props: {
id: number; id: number;
semanticId: string;
title: string; title: string;
description: string; description: string;
previewImageUrl: URL; previewImageUrl: URL;
@ -28,6 +31,7 @@ export class PostInfoResponseDto {
publishedTime: Date | null; publishedTime: Date | null;
}) { }) {
this.id = props.id; this.id = props.id;
this.semanticId = props.semanticId;
this.title = props.title; this.title = props.title;
this.description = props.description; this.description = props.description;
this.previewImageUrl = props.previewImageUrl; this.previewImageUrl = props.previewImageUrl;
@ -45,6 +49,7 @@ export class PostInfoResponseDto {
return new PostInfoResponseDto({ return new PostInfoResponseDto({
id: parsedJson.id, id: parsedJson.id,
semanticId: parsedJson.semantic_id,
title: parsedJson.title, title: parsedJson.title,
description: parsedJson.description, description: parsedJson.description,
previewImageUrl: new URL(parsedJson.preview_image_url), previewImageUrl: new URL(parsedJson.preview_image_url),
@ -56,6 +61,7 @@ export class PostInfoResponseDto {
toEntity(): PostInfo { toEntity(): PostInfo {
return new PostInfo({ return new PostInfo({
id: this.id, id: this.id,
semanticId: this.semanticId,
title: this.title, title: this.title,
description: this.description, description: this.description,
previewImageUrl: this.previewImageUrl, previewImageUrl: this.previewImageUrl,

View File

@ -11,7 +11,7 @@ export class PostRepositoryImpl implements PostRepository {
return dtos.map((dto) => dto.toEntity()); return dtos.map((dto) => dto.toEntity());
} }
async getPost(id: number): Promise<Post | null> { async getPost(id: string): Promise<Post | null> {
const dto = await this.postApiService.getPost(id); const dto = await this.postApiService.getPost(id);
return dto?.toEntity() ?? null; return dto?.toEntity() ?? null;
} }

View File

@ -32,7 +32,7 @@ export class PostBloc {
} }
} }
private async loadPost(id: number): Promise<PostState> { private async loadPost(id: string): Promise<PostState> {
this.state.set({ status: StatusType.Loading, data: get(this.state).data }); this.state.set({ status: StatusType.Loading, data: get(this.state).data });
const post = await this.getPostUseCase.execute(id); const post = await this.getPostUseCase.execute(id);
@ -56,7 +56,7 @@ export enum PostEventType {
PostLoadedEvent PostLoadedEvent
} }
export interface PostLoadedEvent { interface PostLoadedEvent {
event: PostEventType.PostLoadedEvent; event: PostEventType.PostLoadedEvent;
id: number; id: string;
} }

View File

@ -6,6 +6,7 @@ import type { PostInfo } from '$lib/post/domain/entity/postInfo';
export class PostInfoViewModel { export class PostInfoViewModel {
readonly id: number; readonly id: number;
readonly semanticId: string;
readonly title: string; readonly title: string;
readonly description: string; readonly description: string;
readonly previewImageUrl: URL; readonly previewImageUrl: URL;
@ -14,6 +15,7 @@ export class PostInfoViewModel {
private constructor(props: { private constructor(props: {
id: number; id: number;
semanticId: string;
title: string; title: string;
description: string; description: string;
previewImageUrl: URL; previewImageUrl: URL;
@ -21,6 +23,7 @@ export class PostInfoViewModel {
publishedTime: Date | null; publishedTime: Date | null;
}) { }) {
this.id = props.id; this.id = props.id;
this.semanticId = props.semanticId;
this.title = props.title; this.title = props.title;
this.description = props.description; this.description = props.description;
this.previewImageUrl = props.previewImageUrl; this.previewImageUrl = props.previewImageUrl;
@ -31,6 +34,7 @@ export class PostInfoViewModel {
static fromEntity(postInfo: PostInfo): PostInfoViewModel { static fromEntity(postInfo: PostInfo): PostInfoViewModel {
return new PostInfoViewModel({ return new PostInfoViewModel({
id: postInfo.id, id: postInfo.id,
semanticId: postInfo.semanticId,
title: postInfo.title, title: postInfo.title,
description: postInfo.description, description: postInfo.description,
previewImageUrl: postInfo.previewImageUrl, previewImageUrl: postInfo.previewImageUrl,
@ -47,6 +51,7 @@ export class PostInfoViewModel {
return new PostInfoViewModel({ return new PostInfoViewModel({
id: props.id, id: props.id,
semanticId: props.semanticId,
title: props.title, title: props.title,
description: props.description, description: props.description,
previewImageUrl: new URL(props.previewImageUrl), previewImageUrl: new URL(props.previewImageUrl),
@ -66,6 +71,7 @@ export class PostInfoViewModel {
dehydrate(): DehydratedPostInfoProps { dehydrate(): DehydratedPostInfoProps {
return { return {
id: this.id, id: this.id,
semanticId: this.semanticId,
title: this.title, title: this.title,
description: this.description, description: this.description,
previewImageUrl: this.previewImageUrl.href, previewImageUrl: this.previewImageUrl.href,
@ -77,6 +83,7 @@ export class PostInfoViewModel {
export interface DehydratedPostInfoProps { export interface DehydratedPostInfoProps {
id: number; id: number;
semanticId: string;
title: string; title: string;
description: string; description: string;
previewImageUrl: string; previewImageUrl: string;

View File

@ -3,5 +3,5 @@ import type { PostInfo } from '$lib/post/domain/entity/postInfo';
export interface PostRepository { export interface PostRepository {
getAllPosts(): Promise<PostInfo[]>; getAllPosts(): Promise<PostInfo[]>;
getPost(id: number): Promise<Post | null>; getPost(id: string): Promise<Post | null>;
} }

View File

@ -4,7 +4,7 @@ import type { Post } from '$lib/post/domain/entity/post';
export class GetPostUseCase { export class GetPostUseCase {
constructor(private readonly postRepository: PostRepository) {} constructor(private readonly postRepository: PostRepository) {}
execute(id: number): Promise<Post | null> { execute(id: string): Promise<Post | null> {
return this.postRepository.getPost(id); return this.postRepository.getPost(id);
} }
} }

View File

@ -2,6 +2,7 @@ import type { Label } from '$lib/post/domain/entity/label';
export class PostInfo { export class PostInfo {
readonly id: number; readonly id: number;
readonly semanticId: string;
readonly title: string; readonly title: string;
readonly description: string; readonly description: string;
readonly previewImageUrl: URL; readonly previewImageUrl: URL;
@ -10,6 +11,7 @@ export class PostInfo {
constructor(props: { constructor(props: {
id: number; id: number;
semanticId: string;
title: string; title: string;
description: string; description: string;
previewImageUrl: URL; previewImageUrl: URL;
@ -17,6 +19,7 @@ export class PostInfo {
publishedTime: Date | null; publishedTime: Date | null;
}) { }) {
this.id = props.id; this.id = props.id;
this.semanticId = props.semanticId;
this.title = props.title; this.title = props.title;
this.description = props.description; this.description = props.description;
this.previewImageUrl = props.previewImageUrl; this.previewImageUrl = props.previewImageUrl;

View File

@ -19,7 +19,7 @@ export class PostApiServiceImpl implements PostApiService {
return json.map(PostInfoResponseDto.fromJson); return json.map(PostInfoResponseDto.fromJson);
} }
async getPost(id: number): Promise<PostResponseDto | null> { async getPost(id: string): Promise<PostResponseDto | null> {
const url = new URL(`post/${id}`, Environment.API_BASE_URL); const url = new URL(`post/${id}`, Environment.API_BASE_URL);
const response = await this.fetchFn(url.href); const response = await this.fetchFn(url.href);

View File

@ -7,7 +7,7 @@
import generateTitle from '$lib/common/framework/ui/generateTitle'; import generateTitle from '$lib/common/framework/ui/generateTitle';
import StructuredData from '$lib/post/framework/ui/StructuredData.svelte'; import StructuredData from '$lib/post/framework/ui/StructuredData.svelte';
const { id }: { id: number } = $props(); const { id }: { id: string } = $props();
const postBloc = getContext<PostBloc>(PostBloc.name); const postBloc = getContext<PostBloc>(PostBloc.name);
const state = $derived($postBloc); const state = $derived($postBloc);
@ -32,7 +32,7 @@
{/if} {/if}
{/if} {/if}
</svelte:head> </svelte:head>
<article class="container prose pb-10 prose-gray"> <article class="prose prose-gray container pb-10">
{#if state.data} {#if state.data}
<PostContentHeader postInfo={state.data.info} /> <PostContentHeader postInfo={state.data.info} />
<div class="max-w-3xl"> <div class="max-w-3xl">

View File

@ -17,7 +17,7 @@
} }
</script> </script>
<a class="flex cursor-pointer flex-col gap-y-6" href="/post/{postInfo.id}" title={postInfo.title}> <a class="flex cursor-pointer flex-col gap-y-6" href="/post/{postInfo.semanticId}" title={postInfo.title}>
<div class="relative aspect-video overflow-hidden rounded-2xl bg-gray-200"> <div class="relative aspect-video overflow-hidden rounded-2xl bg-gray-200">
<img <img
class="rounded-2xl object-cover transition-opacity duration-300 class="rounded-2xl object-cover transition-opacity duration-300

View File

@ -5,14 +5,9 @@ import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals, params }) => { export const load: PageServerLoad = async ({ locals, params }) => {
const { postBloc } = locals; const { postBloc } = locals;
const id = parseInt(params.id, 10);
if (isNaN(id) || id <= 0) {
error(400, { message: 'Invalid post ID' });
}
const state = await postBloc.dispatch({ const state = await postBloc.dispatch({
event: PostEventType.PostLoadedEvent, event: PostEventType.PostLoadedEvent,
id: id id: params.id
}); });
if (!state.data) { if (!state.data) {
error(404, { message: 'Post not found' }); error(404, { message: 'Post not found' });

View File

@ -9,8 +9,7 @@
import PostContentPage from '$lib/post/framework/ui/PostContentPage.svelte'; import PostContentPage from '$lib/post/framework/ui/PostContentPage.svelte';
const { data, params }: PageProps = $props(); const { data, params }: PageProps = $props();
const { id } = params;
const id = parseInt(params.id, 10);
const initialData = PostViewModel.rehydrate(data.dehydratedData!); const initialData = PostViewModel.rehydrate(data.dehydratedData!);