BLOG-126 Post management (list and create) #139
@ -12,8 +12,8 @@ pub struct CreatePostRequestDto {
|
|||||||
pub content: String,
|
pub content: String,
|
||||||
pub label_ids: Vec<i32>,
|
pub label_ids: Vec<i32>,
|
||||||
|
|
||||||
#[schema(format = Uri)]
|
#[schema(required, format = Uri)]
|
||||||
pub preview_image_url: String,
|
pub preview_image_url: Option<String>,
|
||||||
|
|
||||||
#[schema(required, format = DateTime)]
|
#[schema(required, format = DateTime)]
|
||||||
pub published_time: Option<String>,
|
pub published_time: Option<String>,
|
||||||
|
@ -14,7 +14,7 @@ pub struct PostInfoResponseDto {
|
|||||||
pub labels: Vec<LabelResponseDto>,
|
pub labels: Vec<LabelResponseDto>,
|
||||||
|
|
||||||
#[schema(format = Uri)]
|
#[schema(format = Uri)]
|
||||||
pub preview_image_url: String,
|
pub preview_image_url: Option<String>,
|
||||||
|
|
||||||
#[schema(format = DateTime)]
|
#[schema(format = DateTime)]
|
||||||
pub published_time: Option<String>,
|
pub published_time: Option<String>,
|
||||||
|
@ -12,8 +12,8 @@ pub struct UpdatePostRequestDto {
|
|||||||
pub content: String,
|
pub content: String,
|
||||||
pub label_ids: Vec<i32>,
|
pub label_ids: Vec<i32>,
|
||||||
|
|
||||||
#[schema(format = Uri)]
|
#[schema(required, format = Uri)]
|
||||||
pub preview_image_url: String,
|
pub preview_image_url: Option<String>,
|
||||||
|
|
||||||
#[schema(required, format = DateTime)]
|
#[schema(required, format = DateTime)]
|
||||||
pub published_time: Option<String>,
|
pub published_time: Option<String>,
|
||||||
|
@ -7,7 +7,7 @@ pub struct PostInfoMapper {
|
|||||||
pub semantic_id: String,
|
pub semantic_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub preview_image_url: String,
|
pub preview_image_url: Option<String>,
|
||||||
pub published_time: Option<NaiveDateTime>,
|
pub published_time: Option<NaiveDateTime>,
|
||||||
pub labels: Vec<LabelMapper>,
|
pub labels: Vec<LabelMapper>,
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ pub struct PostInfo {
|
|||||||
pub semantic_id: String,
|
pub semantic_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub preview_image_url: String,
|
pub preview_image_url: Option<String>,
|
||||||
pub labels: Vec<Label>,
|
pub labels: Vec<Label>,
|
||||||
pub published_time: Option<DateTime<Utc>>,
|
pub published_time: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ pub struct PostInfoWithLabelRecord {
|
|||||||
pub semantic_id: String,
|
pub semantic_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub preview_image_url: String,
|
pub preview_image_url: Option<String>,
|
||||||
pub published_time: Option<NaiveDateTime>,
|
pub published_time: Option<NaiveDateTime>,
|
||||||
|
|
||||||
pub label_id: Option<i32>,
|
pub label_id: Option<i32>,
|
||||||
|
@ -6,7 +6,7 @@ pub struct PostWithLabelRecord {
|
|||||||
pub semantic_id: String,
|
pub semantic_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub preview_image_url: String,
|
pub preview_image_url: Option<String>,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub published_time: Option<NaiveDateTime>,
|
pub published_time: Option<NaiveDateTime>,
|
||||||
|
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- Revert post.preview_image_url to NOT NULL
|
||||||
|
ALTER TABLE "post" ALTER COLUMN "preview_image_url" SET NOT NULL;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- Make post.preview_image_url nullable
|
||||||
|
ALTER TABLE "post" ALTER COLUMN "preview_image_url" DROP NOT NULL;
|
@ -6,7 +6,7 @@ export class CreatePostRequestDto {
|
|||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly content: string;
|
readonly content: string;
|
||||||
readonly labelIds: number[];
|
readonly labelIds: number[];
|
||||||
readonly previewImageUrl: URL;
|
readonly previewImageUrl: URL | null;
|
||||||
readonly publishedTime: Date | null;
|
readonly publishedTime: Date | null;
|
||||||
|
|
||||||
private constructor(props: {
|
private constructor(props: {
|
||||||
@ -15,7 +15,7 @@ export class CreatePostRequestDto {
|
|||||||
description: string;
|
description: string;
|
||||||
content: string;
|
content: string;
|
||||||
labelIds: number[];
|
labelIds: number[];
|
||||||
previewImageUrl: URL;
|
previewImageUrl: URL | null;
|
||||||
publishedTime: Date | null;
|
publishedTime: Date | null;
|
||||||
}) {
|
}) {
|
||||||
this.semanticId = props.semanticId;
|
this.semanticId = props.semanticId;
|
||||||
@ -33,7 +33,7 @@ export class CreatePostRequestDto {
|
|||||||
description: '',
|
description: '',
|
||||||
content: '',
|
content: '',
|
||||||
labelIds: [],
|
labelIds: [],
|
||||||
previewImageUrl: new URL('https://example.com'),
|
previewImageUrl: null,
|
||||||
publishedTime: null,
|
publishedTime: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ export const PostInfoResponseSchema = z.object({
|
|||||||
semantic_id: z.string(),
|
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().nullable(),
|
||||||
labels: z.array(LabelResponseSchema),
|
labels: z.array(LabelResponseSchema),
|
||||||
published_time: z.iso.datetime({ offset: true }).nullable(),
|
published_time: z.iso.datetime({ offset: true }).nullable(),
|
||||||
});
|
});
|
||||||
@ -17,7 +17,7 @@ export class PostInfoResponseDto {
|
|||||||
readonly semanticId: string;
|
readonly semanticId: string;
|
||||||
readonly title: string;
|
readonly title: string;
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly previewImageUrl: URL;
|
readonly previewImageUrl: URL | null;
|
||||||
readonly labels: readonly LabelResponseDto[];
|
readonly labels: readonly LabelResponseDto[];
|
||||||
readonly publishedTime: Date | null;
|
readonly publishedTime: Date | null;
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ export class PostInfoResponseDto {
|
|||||||
semanticId: string;
|
semanticId: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
previewImageUrl: URL;
|
previewImageUrl: URL | null;
|
||||||
labels: LabelResponseDto[];
|
labels: LabelResponseDto[];
|
||||||
publishedTime: Date | null;
|
publishedTime: Date | null;
|
||||||
}) {
|
}) {
|
||||||
@ -47,12 +47,17 @@ export class PostInfoResponseDto {
|
|||||||
published_time = new Date(parsedJson.published_time);
|
published_time = new Date(parsedJson.published_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let preview_image_url: URL | null = null;
|
||||||
|
if (parsedJson.preview_image_url !== null) {
|
||||||
|
preview_image_url = new URL(parsedJson.preview_image_url);
|
||||||
|
}
|
||||||
|
|
||||||
return new PostInfoResponseDto({
|
return new PostInfoResponseDto({
|
||||||
id: parsedJson.id,
|
id: parsedJson.id,
|
||||||
semanticId: parsedJson.semantic_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: preview_image_url,
|
||||||
labels: parsedJson.labels.map((label) => LabelResponseDto.fromJson(label)),
|
labels: parsedJson.labels.map((label) => LabelResponseDto.fromJson(label)),
|
||||||
publishedTime: published_time,
|
publishedTime: published_time,
|
||||||
});
|
});
|
||||||
|
@ -9,7 +9,7 @@ export class PostInfoViewModel {
|
|||||||
readonly semanticId: string;
|
readonly semanticId: string;
|
||||||
readonly title: string;
|
readonly title: string;
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly previewImageUrl: URL;
|
readonly previewImageUrl: URL | null;
|
||||||
readonly labels: readonly LabelViewModel[];
|
readonly labels: readonly LabelViewModel[];
|
||||||
readonly publishedTime: Date | null;
|
readonly publishedTime: Date | null;
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ export class PostInfoViewModel {
|
|||||||
semanticId: string;
|
semanticId: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
previewImageUrl: URL;
|
previewImageUrl: URL | null;
|
||||||
labels: readonly LabelViewModel[];
|
labels: readonly LabelViewModel[];
|
||||||
publishedTime: Date | null;
|
publishedTime: Date | null;
|
||||||
}) {
|
}) {
|
||||||
@ -49,12 +49,17 @@ export class PostInfoViewModel {
|
|||||||
publishedTime = new Date(props.publishedTime);
|
publishedTime = new Date(props.publishedTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let previewImageUrl: URL | null = null;
|
||||||
|
if (props.previewImageUrl) {
|
||||||
|
previewImageUrl = new URL(props.previewImageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
return new PostInfoViewModel({
|
return new PostInfoViewModel({
|
||||||
id: props.id,
|
id: props.id,
|
||||||
semanticId: props.semanticId,
|
semanticId: props.semanticId,
|
||||||
title: props.title,
|
title: props.title,
|
||||||
description: props.description,
|
description: props.description,
|
||||||
previewImageUrl: new URL(props.previewImageUrl),
|
previewImageUrl: previewImageUrl,
|
||||||
labels: props.labels.map((label) => LabelViewModel.rehydrate(label)),
|
labels: props.labels.map((label) => LabelViewModel.rehydrate(label)),
|
||||||
publishedTime: publishedTime,
|
publishedTime: publishedTime,
|
||||||
});
|
});
|
||||||
@ -74,7 +79,7 @@ export class PostInfoViewModel {
|
|||||||
semanticId: this.semanticId,
|
semanticId: this.semanticId,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
previewImageUrl: this.previewImageUrl.href,
|
previewImageUrl: this.previewImageUrl?.href ?? null,
|
||||||
labels: this.labels.map((label) => label.dehydrate()),
|
labels: this.labels.map((label) => label.dehydrate()),
|
||||||
publishedTime: this.publishedTime?.getTime() ?? null,
|
publishedTime: this.publishedTime?.getTime() ?? null,
|
||||||
};
|
};
|
||||||
@ -86,7 +91,7 @@ export interface DehydratedPostInfoProps {
|
|||||||
semanticId: string;
|
semanticId: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
previewImageUrl: string;
|
previewImageUrl: string | null;
|
||||||
labels: DehydratedLabelProps[];
|
labels: DehydratedLabelProps[];
|
||||||
publishedTime: number | null;
|
publishedTime: number | null;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ export class PostInfo {
|
|||||||
readonly semanticId: string;
|
readonly semanticId: string;
|
||||||
readonly title: string;
|
readonly title: string;
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly previewImageUrl: URL;
|
readonly previewImageUrl: URL | null;
|
||||||
readonly labels: readonly Label[];
|
readonly labels: readonly Label[];
|
||||||
readonly publishedTime: Date | null;
|
readonly publishedTime: Date | null;
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ export class PostInfo {
|
|||||||
semanticId: string;
|
semanticId: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
previewImageUrl: URL;
|
previewImageUrl: URL | null;
|
||||||
labels: readonly Label[];
|
labels: readonly Label[];
|
||||||
publishedTime: Date | null;
|
publishedTime: Date | null;
|
||||||
}) {
|
}) {
|
||||||
|
@ -14,27 +14,28 @@
|
|||||||
|
|
||||||
const md = markdownit();
|
const md = markdownit();
|
||||||
const parsedContent = $derived(state.data?.content ? md.render(state.data.content) : '');
|
const parsedContent = $derived(state.data?.content ? md.render(state.data.content) : '');
|
||||||
|
const postInfo = $derived(state.data?.info);
|
||||||
|
|
||||||
onMount(() => postBloc.dispatch({ event: PostEventType.PostLoadedEvent, id: id }));
|
onMount(() => postBloc.dispatch({ event: PostEventType.PostLoadedEvent, id: id }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{generateTitle(state.data?.info.title)}</title>
|
<title>{generateTitle(postInfo?.title)}</title>
|
||||||
{#if state.data}
|
{#if postInfo}
|
||||||
<meta name="description" content={state.data.info.description} />
|
<meta name="description" content={postInfo.description} />
|
||||||
{#if state.data.info.isPublished}
|
{#if postInfo.isPublished}
|
||||||
<StructuredData
|
<StructuredData
|
||||||
headline={state.data.info.title}
|
headline={postInfo.title}
|
||||||
description={state.data.info.description}
|
description={postInfo.description}
|
||||||
datePublished={state.data.info.publishedTime!}
|
datePublished={postInfo.publishedTime!}
|
||||||
image={state.data.info.previewImageUrl}
|
image={postInfo.previewImageUrl}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
<article class="content-container prose pb-10 prose-gray">
|
<article class="content-container prose prose-gray pb-10">
|
||||||
{#if state.data}
|
{#if postInfo}
|
||||||
<PostContentHeader postInfo={state.data.info} />
|
<PostContentHeader {postInfo} />
|
||||||
<div class="max-w-3xl">
|
<div class="max-w-3xl">
|
||||||
<hr />
|
<hr />
|
||||||
<SafeHtml html={parsedContent} />
|
<SafeHtml html={parsedContent} />
|
||||||
|
@ -7,11 +7,11 @@
|
|||||||
let isImageLoading = $state(true);
|
let isImageLoading = $state(true);
|
||||||
let isImageError = $state(false);
|
let isImageError = $state(false);
|
||||||
|
|
||||||
function handleImageLoad() {
|
function onImageLoad() {
|
||||||
isImageLoading = false;
|
isImageLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleImageError() {
|
function onImageError() {
|
||||||
isImageLoading = false;
|
isImageLoading = false;
|
||||||
isImageError = true;
|
isImageError = true;
|
||||||
}
|
}
|
||||||
@ -27,10 +27,10 @@
|
|||||||
class="rounded-2xl object-cover transition-opacity duration-300
|
class="rounded-2xl object-cover transition-opacity duration-300
|
||||||
{isImageLoading ? 'opacity-0' : 'opacity-100'}
|
{isImageLoading ? 'opacity-0' : 'opacity-100'}
|
||||||
{isImageError ? 'hidden' : ''}"
|
{isImageError ? 'hidden' : ''}"
|
||||||
src={postInfo.previewImageUrl.href}
|
src={postInfo.previewImageUrl?.href}
|
||||||
alt={postInfo.title}
|
alt={postInfo.title}
|
||||||
onload={handleImageLoad}
|
onload={onImageLoad}
|
||||||
onerror={handleImageError}
|
onerror={onImageError}
|
||||||
/>
|
/>
|
||||||
{#if isImageLoading || isImageError}
|
{#if isImageLoading || isImageError}
|
||||||
<div class="absolute inset-0 flex items-center justify-center bg-gray-200"></div>
|
<div class="absolute inset-0 flex items-center justify-center bg-gray-200"></div>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
headline: string;
|
headline: string;
|
||||||
description: string;
|
description: string;
|
||||||
datePublished: Date;
|
datePublished: Date;
|
||||||
image: URL;
|
image: URL | null;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const structuredData = $derived({
|
const structuredData = $derived({
|
||||||
@ -19,7 +19,7 @@
|
|||||||
headline: headline,
|
headline: headline,
|
||||||
description: description,
|
description: description,
|
||||||
datePublished: datePublished.toISOString(),
|
datePublished: datePublished.toISOString(),
|
||||||
image: image.href,
|
image: image?.href,
|
||||||
});
|
});
|
||||||
|
|
||||||
const jsonLdScript = $derived(
|
const jsonLdScript = $derived(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user