blog/frontend/src/lib/post/adapter/presenter/postInfoViewModel.ts
SquidSpirit 565df7aace
All checks were successful
Frontend CI / build (push) Successful in 1m26s
BLOG-125 Get post by sementic ID (#134)
### Description

#### Backend

- String and interger can be pass as `id` to `GET` `/post/{id}`

- For the posts existed, the default `semantic_id` for them will be `_id`. (e.g. `_1`, `_2`)

- Semantic ID should follow the rules:

  1. It shouldn't be an integer

  1. It should match the pattern: `^[0-9a-zA-Z_\-]+$`

  <br>

  |Semantic ID|Result|Note|
  |-|-|-|
  |12|X|against with `i`|
  |-3|X|against with `i`|
  |3.14|X|against with `ii`|
  |hello world|X|against with `ii`|
  |*EMPTY*|X|against with `ii`|
  |12_34-56|O||

#### Frontend

- The href of post preview card becomes the semantic ID.

### Package Changes

```toml
regex = "1.12.1"
```

### Screenshots

![截圖 2025-10-12 下午6.23.12.png](/attachments/67de1cd7-f584-40ad-9bbd-27f8bf6f1894)

### Reference

Resolves #125.

### Checklist

- [x] A milestone is set
- [x] The related issuse has been linked to this branch

Reviewed-on: #134
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
2025-10-12 18:28:58 +08:00

93 lines
2.5 KiB
TypeScript

import {
LabelViewModel,
type DehydratedLabelProps,
} from '$lib/post/adapter/presenter/labelViewModel';
import type { PostInfo } from '$lib/post/domain/entity/postInfo';
export class PostInfoViewModel {
readonly id: number;
readonly semanticId: string;
readonly title: string;
readonly description: string;
readonly previewImageUrl: URL;
readonly labels: readonly LabelViewModel[];
readonly publishedTime: Date | null;
private constructor(props: {
id: number;
semanticId: string;
title: string;
description: string;
previewImageUrl: URL;
labels: readonly LabelViewModel[];
publishedTime: Date | null;
}) {
this.id = props.id;
this.semanticId = props.semanticId;
this.title = props.title;
this.description = props.description;
this.previewImageUrl = props.previewImageUrl;
this.labels = props.labels;
this.publishedTime = props.publishedTime;
}
static fromEntity(postInfo: PostInfo): PostInfoViewModel {
return new PostInfoViewModel({
id: postInfo.id,
semanticId: postInfo.semanticId,
title: postInfo.title,
description: postInfo.description,
previewImageUrl: postInfo.previewImageUrl,
labels: postInfo.labels.map((label) => LabelViewModel.fromEntity(label)),
publishedTime: postInfo.publishedTime,
});
}
static rehydrate(props: DehydratedPostInfoProps): PostInfoViewModel {
let publishedTime: Date | null = null;
if (props.publishedTime !== null) {
publishedTime = new Date(props.publishedTime);
}
return new PostInfoViewModel({
id: props.id,
semanticId: props.semanticId,
title: props.title,
description: props.description,
previewImageUrl: new URL(props.previewImageUrl),
labels: props.labels.map((label) => LabelViewModel.rehydrate(label)),
publishedTime: publishedTime,
});
}
get isPublished(): boolean {
return this.publishedTime !== null;
}
get formattedPublishedTime(): string | null {
return this.publishedTime?.toISOString().slice(0, 10) ?? null;
}
dehydrate(): DehydratedPostInfoProps {
return {
id: this.id,
semanticId: this.semanticId,
title: this.title,
description: this.description,
previewImageUrl: this.previewImageUrl.href,
labels: this.labels.map((label) => label.dehydrate()),
publishedTime: this.publishedTime?.getTime() ?? null,
};
}
}
export interface DehydratedPostInfoProps {
id: number;
semanticId: string;
title: string;
description: string;
previewImageUrl: string;
labels: DehydratedLabelProps[];
publishedTime: number | null;
}