markdown img insert work as intended

This commit is contained in:
echo 2026-02-03 19:53:29 +01:00
parent 28609a6492
commit 6467e21019
9 changed files with 3138 additions and 1263 deletions

View File

@ -8,7 +8,7 @@ import {
IsBoolean, IsBoolean,
IsDate, IsDate,
} from 'class-validator'; } from 'class-validator';
import { ArticleStatus, LiveBlogStatus } from './entities'; import { ArticleStatus, LiveBlogStatus, ImagePosition, ImageSize } from './entities';
export class CreateArticleDto { export class CreateArticleDto {
@IsString() @IsString()
@ -49,6 +49,14 @@ export class CreateArticleDto {
@IsOptional() @IsOptional()
@IsUUID() @IsUUID()
categoryId?: string; categoryId?: string;
@IsOptional()
@IsEnum(ImagePosition)
imagePosition?: ImagePosition;
@IsOptional()
@IsEnum(ImageSize)
imageSize?: ImageSize;
} }
export class UpdateArticleDto { export class UpdateArticleDto {
@ -92,6 +100,14 @@ export class UpdateArticleDto {
@IsOptional() @IsOptional()
@IsUUID() @IsUUID()
categoryId?: string; categoryId?: string;
@IsOptional()
@IsEnum(ImagePosition)
imagePosition?: ImagePosition;
@IsOptional()
@IsEnum(ImageSize)
imageSize?: ImageSize;
} }
export class FindArticlesDto { export class FindArticlesDto {
@ -157,6 +173,14 @@ export class CreateLiveBlogDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
featuredImage?: string; featuredImage?: string;
@IsOptional()
@IsEnum(ImagePosition)
imagePosition?: ImagePosition;
@IsOptional()
@IsEnum(ImageSize)
imageSize?: ImageSize;
} }
export class UpdateLiveBlogDto { export class UpdateLiveBlogDto {
@ -195,6 +219,14 @@ export class UpdateLiveBlogDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
featuredImage?: string; featuredImage?: string;
@IsOptional()
@IsEnum(ImagePosition)
imagePosition?: ImagePosition;
@IsOptional()
@IsEnum(ImageSize)
imageSize?: ImageSize;
} }
export class CreateLiveBlogUpdateDto { export class CreateLiveBlogUpdateDto {

View File

@ -36,6 +36,19 @@ export enum LiveBlogStatus {
ARCHIVED = 'archived', ARCHIVED = 'archived',
} }
export enum ImagePosition {
TOP = 'top',
LEFT = 'left',
RIGHT = 'right',
NONE = 'none',
}
export enum ImageSize {
SMALL = 'small',
MEDIUM = 'medium',
LARGE = 'large',
}
@Entity('authors') @Entity('authors')
export class Author { export class Author {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
@ -114,6 +127,18 @@ export class Article {
@Column({ default: '' }) @Column({ default: '' })
featuredImage: string; featuredImage: string;
@Column({
type: 'text',
default: 'top',
})
imagePosition: ImagePosition;
@Column({
type: 'text',
default: 'medium',
})
imageSize: ImageSize;
@Column({ @Column({
type: 'text', type: 'text',
default: '[]', default: '[]',
@ -189,6 +214,18 @@ export class LiveBlog {
@Column({ default: '' }) @Column({ default: '' })
featuredImage: string; featuredImage: string;
@Column({
type: 'text',
default: 'top',
})
imagePosition: ImagePosition;
@Column({
type: 'text',
default: 'medium',
})
imageSize: ImageSize;
@Column({ default: 0 }) @Column({ default: 0 })
viewCount: number; viewCount: number;

View File

@ -5,7 +5,7 @@ import { lastValueFrom } from 'rxjs';
import { ArticlesService } from './articles.service'; import { ArticlesService } from './articles.service';
import { LiveBlogService } from './live-blog.service'; import { LiveBlogService } from './live-blog.service';
import { CreateArticleDto, CreateLiveBlogDto } from './articles.dto'; import { CreateArticleDto, CreateLiveBlogDto } from './articles.dto';
import { ArticleStatus, LiveBlogStatus } from './entities'; import { ArticleStatus, LiveBlogStatus, ImagePosition, ImageSize } from './entities';
interface StrapiArticle { interface StrapiArticle {
id: number; id: number;
@ -19,6 +19,8 @@ interface StrapiArticle {
updatedAt: string; updatedAt: string;
img?: any; img?: any;
media?: any[]; media?: any[];
imagePosition?: string;
imageSize?: string;
} }
interface StrapiLiveBlog { interface StrapiLiveBlog {
@ -33,6 +35,8 @@ interface StrapiLiveBlog {
updatedAt: string; updatedAt: string;
img?: any; img?: any;
media?: any[]; media?: any[];
imagePosition?: string;
imageSize?: string;
} }
interface StrapiResponse<T> { interface StrapiResponse<T> {
@ -156,6 +160,8 @@ export class StrapiService {
: ArticleStatus.DRAFT, : ArticleStatus.DRAFT,
tags: [], tags: [],
featuredImage: imageUrl, featuredImage: imageUrl,
imagePosition: (strapiArticle.imagePosition || 'top') as ImagePosition,
imageSize: (strapiArticle.imageSize || 'medium') as ImageSize,
}; };
await this.articlesService.syncFromStrapi( await this.articlesService.syncFromStrapi(
@ -215,6 +221,8 @@ export class StrapiService {
status, status,
tags: [], tags: [],
featuredImage: imageUrl, featuredImage: imageUrl,
imagePosition: (strapiArticle.imagePosition || 'top') as ImagePosition,
imageSize: (strapiArticle.imageSize || 'medium') as ImageSize,
}; };
await this.articlesService.syncFromStrapi( await this.articlesService.syncFromStrapi(
@ -277,6 +285,8 @@ export class StrapiService {
slug: strapiLiveBlog.slug, slug: strapiLiveBlog.slug,
status: this.mapStrapiStatusToLiveBlogStatus(strapiLiveBlog.status), status: this.mapStrapiStatusToLiveBlogStatus(strapiLiveBlog.status),
featuredImage: imageUrl, featuredImage: imageUrl,
imagePosition: (strapiLiveBlog.imagePosition || 'top') as ImagePosition,
imageSize: (strapiLiveBlog.imageSize || 'medium') as ImageSize,
}; };
await this.liveBlogService.syncFromStrapi( await this.liveBlogService.syncFromStrapi(
@ -330,6 +340,8 @@ export class StrapiService {
slug: strapiLiveBlog.slug, slug: strapiLiveBlog.slug,
status, status,
featuredImage: imageUrl, featuredImage: imageUrl,
imagePosition: (strapiLiveBlog.imagePosition || 'top') as ImagePosition,
imageSize: (strapiLiveBlog.imageSize || 'medium') as ImageSize,
}; };
await this.liveBlogService.syncFromStrapi( await this.liveBlogService.syncFromStrapi(

View File

@ -39,6 +39,16 @@
"videos", "videos",
"audios" "audios"
] ]
},
"imagePosition": {
"type": "enumeration",
"enum": ["top", "left", "right", "none"],
"default": "top"
},
"imageSize": {
"type": "enumeration",
"enum": ["small", "medium", "large"],
"default": "medium"
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,9 @@
"@tanstack/react-router": "^1.144.0", "@tanstack/react-router": "^1.144.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0" "react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",

View File

@ -1,6 +1,8 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import * as api from '@/lib/api' import * as api from '@/lib/api'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
export function ArticleDetailComponent({ id }: { id: string }) { export function ArticleDetailComponent({ id }: { id: string }) {
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
@ -65,8 +67,28 @@ export function ArticleDetailComponent({ id }: { id: string }) {
)} )}
</div> </div>
{data.featuredImage && ( {data.featuredImage && data.imagePosition !== 'none' && (
<div className="relative w-full h-64 md:h-96 mb-8"> <div className={`relative ${
data.imagePosition === 'top'
? 'w-full h-64 md:h-96 mb-8'
: data.imagePosition === 'left'
? 'float-none md:float-left mr-0 md:mr-6 mb-4 w-full md:w-auto'
: data.imagePosition === 'right'
? 'float-none md:float-right ml-0 md:ml-6 mb-4 w-full md:w-auto'
: ''
} ${
data.imagePosition === 'top'
? data.imageSize === 'small'
? 'h-32'
: data.imageSize === 'medium'
? 'h-48'
: 'h-64'
: data.imageSize === 'small'
? 'w-full md:w-48 h-32'
: data.imageSize === 'medium'
? 'w-full md:w-64 h-48'
: 'w-full md:w-96 h-64'
}`}>
<img <img
src={data.featuredImage} src={data.featuredImage}
alt={data.title} alt={data.title}
@ -90,8 +112,23 @@ export function ArticleDetailComponent({ id }: { id: string }) {
</div> </div>
)} )}
<div className="prose prose-slate max-w-none"> <div className="prose prose-slate max-w-none clear-both md:clear-none">
<p className="text-lg leading-relaxed mb-6">{data.content}</p> <div className="text-lg leading-relaxed mb-6">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
img: (props) => (
<img
{...props}
className="max-w-full h-auto rounded-lg my-4"
alt={props.alt || 'Article image'}
/>
)
}}
>
{data.content}
</ReactMarkdown>
</div>
</div> </div>
{data.tags && Array.isArray(data.tags) && data.tags.length > 0 && ( {data.tags && Array.isArray(data.tags) && data.tags.length > 0 && (

View File

@ -11,6 +11,8 @@ export interface Article {
excerpt: string | null; excerpt: string | null;
slug: string; slug: string;
featuredImage: string; featuredImage: string;
imagePosition: 'top' | 'left' | 'right' | 'none';
imageSize: 'small' | 'medium' | 'large';
tags: string[]; tags: string[];
status: 'draft' | 'published' | 'archived'; status: 'draft' | 'published' | 'archived';
views: number; views: number;
@ -126,6 +128,9 @@ export interface LiveBlog {
strapiId: string | null; strapiId: string | null;
authorId: string | null; authorId: string | null;
categoryId: string | null; categoryId: string | null;
featuredImage: string;
imagePosition: 'top' | 'left' | 'right' | 'none';
imageSize: 'small' | 'medium' | 'large';
viewCount: number; viewCount: number;
author?: { author?: {
id: string; id: string;

2759
package-lock.json generated

File diff suppressed because it is too large Load Diff