diff --git a/backend/src/modules/articles.dto.ts b/backend/src/modules/articles.dto.ts index be12635..870bc47 100644 --- a/backend/src/modules/articles.dto.ts +++ b/backend/src/modules/articles.dto.ts @@ -153,6 +153,10 @@ export class CreateLiveBlogDto { @IsOptional() @IsUUID() categoryId?: string; + + @IsOptional() + @IsString() + featuredImage?: string; } export class UpdateLiveBlogDto { @@ -187,6 +191,10 @@ export class UpdateLiveBlogDto { @IsOptional() @IsUUID() categoryId?: string; + + @IsOptional() + @IsString() + featuredImage?: string; } export class CreateLiveBlogUpdateDto { diff --git a/backend/src/modules/entities.ts b/backend/src/modules/entities.ts index 12f14a0..45eefe6 100644 --- a/backend/src/modules/entities.ts +++ b/backend/src/modules/entities.ts @@ -186,6 +186,9 @@ export class LiveBlog { @Column({ nullable: true }) categoryId: string; + @Column({ default: '' }) + featuredImage: string; + @Column({ default: 0 }) viewCount: number; diff --git a/backend/src/modules/strapi.service.ts b/backend/src/modules/strapi.service.ts index 721ba6d..859c9dc 100644 --- a/backend/src/modules/strapi.service.ts +++ b/backend/src/modules/strapi.service.ts @@ -17,6 +17,8 @@ interface StrapiArticle { publishedAt: string | null; createdAt: string; updatedAt: string; + img?: any; + media?: any[]; } interface StrapiLiveBlog { @@ -29,6 +31,8 @@ interface StrapiLiveBlog { publishedAt: string | null; createdAt: string; updatedAt: string; + img?: any; + media?: any[]; } interface StrapiResponse { @@ -71,13 +75,65 @@ export class StrapiService { return headers; } + private extractImageUrl(strapiArticle: StrapiArticle): string | undefined { + // Try to get image from img field first (single image) + let imageUrl: string | undefined; + + if (strapiArticle.img?.url) { + imageUrl = strapiArticle.img.url; + } else if (strapiArticle.media?.[0]?.url) { + // Try to get first image from media field (multiple images) + imageUrl = strapiArticle.media[0].url; + } + + if (!imageUrl) { + return undefined; + } + + // If URL is relative, prepend Strapi base URL + if (imageUrl.startsWith('/')) { + // Convert Docker service URL to localhost for frontend access + // Backend uses http://cms:1337 internally, but frontend needs http://localhost:1337 + const frontendStrapiUrl = this.strapiUrl.replace('cms:', 'localhost:'); + return `${frontendStrapiUrl}${imageUrl}`; + } + + return imageUrl; + } + + private extractLiveBlogImageUrl(strapiLiveBlog: StrapiLiveBlog): string | undefined { + // Try to get image from img field first (single image) + let imageUrl: string | undefined; + + if (strapiLiveBlog.img?.url) { + imageUrl = strapiLiveBlog.img.url; + } else if (strapiLiveBlog.media?.[0]?.url) { + // Try to get first image from media field (multiple images) + imageUrl = strapiLiveBlog.media[0].url; + } + + if (!imageUrl) { + return undefined; + } + + // If URL is relative, prepend Strapi base URL + if (imageUrl.startsWith('/')) { + // Convert Docker service URL to localhost for frontend access + // Backend uses http://cms:1337 internally, but frontend needs http://localhost:1337 + const frontendStrapiUrl = this.strapiUrl.replace('cms:', 'localhost:'); + return `${frontendStrapiUrl}${imageUrl}`; + } + + return imageUrl; + } + async syncArticles(): Promise { try { this.logger.log('Starting articles sync from Strapi...'); - const response = await lastValueFrom( + const response = await lastValueFrom( this.httpService.get>( - `${this.strapiUrl}/api/articles`, + `${this.strapiUrl}/api/articles?populate=*`, { headers: this.getHeaders(), }, @@ -88,6 +144,8 @@ export class StrapiService { let syncedCount = 0; for (const strapiArticle of strapiArticles) { + const imageUrl = this.extractImageUrl(strapiArticle); + const articleData: Partial = { title: strapiArticle.title, excerpt: strapiArticle.description, @@ -97,6 +155,7 @@ export class StrapiService { ? ArticleStatus.PUBLISHED : ArticleStatus.DRAFT, tags: [], + featuredImage: imageUrl, }; await this.articlesService.syncFromStrapi( @@ -122,9 +181,9 @@ export class StrapiService { try { this.logger.log(`Syncing single article from Strapi: ${strapiId}, event: ${event}`); - const response = await lastValueFrom( + const response = await lastValueFrom( this.httpService.get>( - `${this.strapiUrl}/api/articles/${strapiId}`, + `${this.strapiUrl}/api/articles/${strapiId}?populate=*`, { headers: this.getHeaders(), }, @@ -146,6 +205,8 @@ export class StrapiService { status = ArticleStatus.DRAFT; } + const imageUrl = this.extractImageUrl(strapiArticle); + const articleData: Partial = { title: strapiArticle.title, excerpt: strapiArticle.description, @@ -153,6 +214,7 @@ export class StrapiService { slug: strapiArticle.slug, status, tags: [], + featuredImage: imageUrl, }; await this.articlesService.syncFromStrapi( @@ -194,9 +256,9 @@ export class StrapiService { try { this.logger.log('Starting live blogs sync from Strapi...'); - const response = await lastValueFrom( + const response = await lastValueFrom( this.httpService.get>( - `${this.strapiUrl}/api/live-blogs`, + `${this.strapiUrl}/api/live-blogs?populate=*`, { headers: this.getHeaders(), }, @@ -207,11 +269,14 @@ export class StrapiService { let syncedCount = 0; for (const strapiLiveBlog of strapiLiveBlogs) { + const imageUrl = this.extractLiveBlogImageUrl(strapiLiveBlog); + const liveBlogData: Partial = { title: strapiLiveBlog.title, description: strapiLiveBlog.description, slug: strapiLiveBlog.slug, status: this.mapStrapiStatusToLiveBlogStatus(strapiLiveBlog.status), + featuredImage: imageUrl, }; await this.liveBlogService.syncFromStrapi( @@ -237,9 +302,9 @@ export class StrapiService { try { this.logger.log(`Syncing single live blog from Strapi: ${strapiId}, event: ${event}`); - const response = await lastValueFrom( + const response = await lastValueFrom( this.httpService.get>( - `${this.strapiUrl}/api/live-blogs/${strapiId}`, + `${this.strapiUrl}/api/live-blogs/${strapiId}?populate=*`, { headers: this.getHeaders(), }, @@ -257,11 +322,14 @@ export class StrapiService { status = LiveBlogStatus.DRAFT; } + const imageUrl = this.extractLiveBlogImageUrl(strapiLiveBlog); + const liveBlogData: Partial = { title: strapiLiveBlog.title, description: strapiLiveBlog.description, slug: strapiLiveBlog.slug, status, + featuredImage: imageUrl, }; await this.liveBlogService.syncFromStrapi( diff --git a/frontend/src/components/routes/ArticleDetailComponent.tsx b/frontend/src/components/routes/ArticleDetailComponent.tsx index 3b21dae..917f17a 100644 --- a/frontend/src/components/routes/ArticleDetailComponent.tsx +++ b/frontend/src/components/routes/ArticleDetailComponent.tsx @@ -66,11 +66,28 @@ export function ArticleDetailComponent({ id }: { id: string }) { {data.featuredImage && ( - {data.title} +
+ {data.title} { + console.error('Failed to load image:', data.featuredImage, e); + e.currentTarget.style.display = 'none'; + // Show fallback + const fallback = e.currentTarget.nextElementSibling as HTMLElement; + if (fallback) { + fallback.style.display = 'flex'; + } + }} + /> +
+ Image not available +
+
)}