diff --git a/frontend/src/app.css b/frontend/src/app.css index bbb74a3..e489be2 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -25,7 +25,7 @@ } body { - @apply bg-white font-sans text-base font-normal text-gray-600; + @apply bg-white font-sans text-base font-normal text-gray-800; } pre, diff --git a/frontend/src/lib/home/framework/ui/MottoAnimatedMark.svelte b/frontend/src/lib/home/framework/ui/MottoAnimatedMark.svelte index ea030e5..1ea9262 100644 --- a/frontend/src/lib/home/framework/ui/MottoAnimatedMark.svelte +++ b/frontend/src/lib/home/framework/ui/MottoAnimatedMark.svelte @@ -34,9 +34,9 @@ {text} diff --git a/frontend/src/lib/home/framework/ui/Terminal.svelte b/frontend/src/lib/home/framework/ui/Terminal.svelte index 73db390..22d2788 100644 --- a/frontend/src/lib/home/framework/ui/Terminal.svelte +++ b/frontend/src/lib/home/framework/ui/Terminal.svelte @@ -57,7 +57,7 @@ >
diff --git a/frontend/src/lib/home/framework/ui/TitleScreenAnimatedTags.svelte b/frontend/src/lib/home/framework/ui/TitleScreenAnimatedTags.svelte index b35dd60..5afc873 100644 --- a/frontend/src/lib/home/framework/ui/TitleScreenAnimatedTags.svelte +++ b/frontend/src/lib/home/framework/ui/TitleScreenAnimatedTags.svelte @@ -4,6 +4,7 @@ const tagsCollection = [ 'APP', 'C++', + 'Clean Architecture', 'Design Pattern', 'Docker', 'Flutter', @@ -12,7 +13,10 @@ 'LINER', 'Linux', 'Python', + 'React', + 'Rust', 'Squid', + 'Svelte', 'TypeScript', '中央大學', '全端', @@ -20,9 +24,7 @@ '前端', '後端', '教學', - '暴肝', '知識', - '碼農', '科技', '科普', '程式設計', @@ -64,7 +66,8 @@
LabelResponseDto.fromJson(JSON.stringify(label))), - publishedTime: new Date(parsedJson.published_time / 1000), + labels: parsedJson.labels.map((label) => LabelResponseDto.fromJson(label)), + publishedTime: new Date(parsedJson.published_time / 1000) }); } @@ -54,7 +54,7 @@ export class PostInfoResponseDto { description: this.description, previewImageUrl: this.previewImageUrl, labels: this.labels.map((label) => label.toEntity()), - publishedTime: this.publishedTime, + publishedTime: this.publishedTime }); } } diff --git a/frontend/src/lib/post/adapter/presenter/colorViewModel.ts b/frontend/src/lib/post/adapter/presenter/colorViewModel.ts new file mode 100644 index 0000000..827aa46 --- /dev/null +++ b/frontend/src/lib/post/adapter/presenter/colorViewModel.ts @@ -0,0 +1,114 @@ +import type { Color } from '$lib/post/domain/entity/color'; + +export class ColorViewModel { + readonly red: number; + readonly green: number; + readonly blue: number; + readonly alpha: number; + + private constructor(props: { red: number; green: number; blue: number; alpha: number }) { + this.red = props.red; + this.green = props.green; + this.blue = props.blue; + this.alpha = props.alpha; + } + + private static fromHsl(hsl: Hsl): ColorViewModel { + const { h, s, l } = hsl; + let r, g, b; + + if (s === 0) { + // achromatic (grayscale) + r = g = b = l; + } else { + const hue2rgb = (p: number, q: number, t: number) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + const h_norm = h / 360; + + r = hue2rgb(p, q, h_norm + 1 / 3); + g = hue2rgb(p, q, h_norm); + b = hue2rgb(p, q, h_norm - 1 / 3); + } + + return new ColorViewModel({ + red: Math.round(r * 255), + green: Math.round(g * 255), + blue: Math.round(b * 255), + alpha: 255 + }); + } + + static fromEntity(color: Color): ColorViewModel { + return new ColorViewModel({ + red: color.red, + green: color.green, + blue: color.blue, + alpha: color.alpha + }); + } + + 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)}`; + } + + private toHsl(): Hsl { + const r = this.red / 255; + const g = this.green / 255; + const b = this.blue / 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h = 0, + s = 0; + const l = (max + min) / 2; + + if (max === min) { + // achromatic (grayscale) + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return { h: h * 360, s: s, l: l }; + } + + lighten(amount: number): ColorViewModel { + const hsl = this.toHsl(); + hsl.l += amount; + hsl.l = Math.max(0, Math.min(1, hsl.l)); + return ColorViewModel.fromHsl(hsl); + } + + darken(amount: number): ColorViewModel { + return this.lighten(-amount); + } +} + +interface Hsl { + h: number; + s: number; + l: number; +} diff --git a/frontend/src/lib/post/adapter/presenter/labelViewModel.ts b/frontend/src/lib/post/adapter/presenter/labelViewModel.ts index 3ff7d46..4abebd3 100644 --- a/frontend/src/lib/post/adapter/presenter/labelViewModel.ts +++ b/frontend/src/lib/post/adapter/presenter/labelViewModel.ts @@ -1,19 +1,22 @@ +import { ColorViewModel } from '$lib/post/adapter/presenter/colorViewModel'; +import type { Label } from '$lib/post/domain/entity/label'; + export class LabelViewModel { readonly id: number; readonly name: string; - readonly color: string; + readonly color: ColorViewModel; - private constructor(props: { id: number; name: string; color: string }) { + private constructor(props: { id: number; name: string; color: ColorViewModel }) { this.id = props.id; this.name = props.name; this.color = props.color; } - static fromEntity(label: { id: number; name: string; color: string }): LabelViewModel { + static fromEntity(label: Label): LabelViewModel { return new LabelViewModel({ id: label.id, name: label.name, - color: label.color + color: ColorViewModel.fromEntity(label.color) }); } } diff --git a/frontend/src/lib/post/domain/entity/color.ts b/frontend/src/lib/post/domain/entity/color.ts new file mode 100644 index 0000000..8f6afa2 --- /dev/null +++ b/frontend/src/lib/post/domain/entity/color.ts @@ -0,0 +1,13 @@ +export class Color { + readonly red: number; + readonly green: number; + readonly blue: number; + readonly alpha: number; + + constructor(props: { red: number; green: number; blue: number; alpha: number }) { + this.red = props.red; + this.green = props.green; + this.blue = props.blue; + this.alpha = props.alpha; + } +} diff --git a/frontend/src/lib/post/domain/entity/label.ts b/frontend/src/lib/post/domain/entity/label.ts index a13de0c..c264ec6 100644 --- a/frontend/src/lib/post/domain/entity/label.ts +++ b/frontend/src/lib/post/domain/entity/label.ts @@ -1,9 +1,11 @@ +import type { Color } from '$lib/post/domain/entity/color'; + export class Label { readonly id: number; readonly name: string; - readonly color: string; + readonly color: Color; - constructor(props: { id: number; name: string; color: string }) { + constructor(props: { id: number; name: string; color: Color }) { this.id = props.id; this.name = props.name; this.color = props.color; diff --git a/frontend/src/lib/post/framework/ui/PostListPage.svelte b/frontend/src/lib/post/framework/ui/PostOverallPage.svelte similarity index 61% rename from frontend/src/lib/post/framework/ui/PostListPage.svelte rename to frontend/src/lib/post/framework/ui/PostOverallPage.svelte index 5b2d787..85f051f 100644 --- a/frontend/src/lib/post/framework/ui/PostListPage.svelte +++ b/frontend/src/lib/post/framework/ui/PostOverallPage.svelte @@ -1,23 +1,22 @@
文章
- {#if state.status === StatusType.Loading || state.status === StatusType.Idle} -
Loading
- {:else if state.status === StatusType.Success} -
{JSON.stringify(state.data)}
- {:else} -
Error loading posts
+ {#if state.status === StatusType.Success} +
+ {#each state.data as postInfo (postInfo.id)} + + {/each} +
{/if}
diff --git a/frontend/src/lib/post/framework/ui/PostPreview.svelte b/frontend/src/lib/post/framework/ui/PostPreview.svelte new file mode 100644 index 0000000..b51844a --- /dev/null +++ b/frontend/src/lib/post/framework/ui/PostPreview.svelte @@ -0,0 +1,41 @@ + + + +
+ {postInfo.title} + {#if isImageLoading || isImageError} +
+ {/if} +
+
+ + {postInfo.title} + {postInfo.description} + 查看更多 ⭢ +
+
diff --git a/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte b/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte new file mode 100644 index 0000000..d9e132a --- /dev/null +++ b/frontend/src/lib/post/framework/ui/PostPreviewLabels.svelte @@ -0,0 +1,25 @@ + + +
+ {#each labels.slice(0, 2) as label (label.id)} +
+
+ {label.name} +
+ {/each} + {#if labels.length > 2} +
+ +{labels.length - 2} +
+ {/if} +
diff --git a/frontend/src/routes/post/+page.svelte b/frontend/src/routes/post/+page.svelte index 370e0e7..3a1860d 100644 --- a/frontend/src/routes/post/+page.svelte +++ b/frontend/src/routes/post/+page.svelte @@ -3,7 +3,7 @@ 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 PostListPage from '$lib/post/framework/ui/PostListPage.svelte'; + import PostOverallPage from '$lib/post/framework/ui/PostOverallPage.svelte'; import { setContext } from 'svelte'; const postApiService = new PostApiServiceImpl(); @@ -14,4 +14,4 @@ setContext(PostListBloc.name, postListBloc); - +