Slave/blog #9

Merged
nobswebdev merged 14 commits from slave/blog into master 2026-09-16 20:15:08 +00:00
5 changed files with 82 additions and 31 deletions
Showing only changes of commit 9cd7059928 - Show all commits
@@ -1,6 +1,6 @@
import { NotFoundException } from '@nestjs/common';
import type { Repository } from 'typeorm';
import { resolveBlogImagePath } from '../../../config/uploadPaths';
import { getBlogImagePublicUrl, resolveBlogImagePath } from '../../../config/uploadPaths';
import { removeFileFromDisk } from '../../../utils/removeFileFromDisk';
import { BlogPost } from '../entities/BlogPost';
import { BlogPostsService } from './BlogPostsService';
@@ -58,7 +58,28 @@ describe('BlogPostsService', () => {
id: 'post-1',
title: '',
contentHtml: '',
isDraft: true
isDraft: true,
thumbnailUrl: null,
bannerUrl: null
})
);
});
it('maps image storage keys to public urls', async () => {
blogPostRepo.findOne.mockResolvedValue({
id: 'post-1',
title: 'Hello',
thumbnailStorageKey: 'thumb.webp',
bannerStorageKey: 'banner.webp',
contentHtml: '<p>Body</p>',
isDraft: false
});
await expect(service.findOne('post-1')).resolves.toEqual(
expect.objectContaining({
title: 'Hello',
thumbnailUrl: getBlogImagePublicUrl('thumb.webp'),
bannerUrl: getBlogImagePublicUrl('banner.webp')
})
);
});
@@ -1,12 +1,13 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { resolveBlogImagePath } from '../../../config/uploadPaths';
import { getBlogImagePublicUrl, resolveBlogImagePath } from '../../../config/uploadPaths';
import type { PaginatedResponse } from '../../../types/PaginatedResponse';
import { removeFileFromDisk } from '../../../utils/removeFileFromDisk';
import type { ListBlogPostsQueryDto } from '../dto/ListBlogPostsQueryDto';
import { UpdateBlogPostDto } from '../dto/UpdateBlogPostDto';
import { BlogPost } from '../entities/BlogPost';
import type { BlogPostExtended } from '../types/BlogPostExtended';
@Injectable()
export class BlogPostsService {
@@ -15,17 +16,19 @@ export class BlogPostsService {
private readonly blogPostRepo: Repository<BlogPost>
) {}
async findAll({ page = 1, limit = 20 }: ListBlogPostsQueryDto): Promise<PaginatedResponse<BlogPost>> {
async findAll({ page = 1, limit = 20 }: ListBlogPostsQueryDto): Promise<PaginatedResponse<BlogPostExtended>> {
const [items, total] = await this.blogPostRepo.findAndCount({
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit
});
return { items, total, page, limit };
const itemsExtended = items.map(post => this.toPostExtended(post));
return { items: itemsExtended, total, page, limit };
}
async createDraft(): Promise<BlogPost> {
async createDraft(): Promise<BlogPostExtended> {
const entity = this.blogPostRepo.create({
title: '',
contentHtml: '',
@@ -37,17 +40,13 @@ export class BlogPostsService {
return this.findOne(entity.id);
}
async findOne(id: string): Promise<BlogPost> {
const entity = await this.blogPostRepo.findOne({ where: { id } });
async findOne(id: string): Promise<BlogPostExtended> {
const entity = await this.findEntity(id);
if (!entity) {
throw new NotFoundException();
return this.toPostExtended(entity);
}
return entity;
}
async update(id: string, { title, contentHtml, isDraft }: UpdateBlogPostDto): Promise<BlogPost> {
async update(id: string, { title, contentHtml, isDraft }: UpdateBlogPostDto): Promise<BlogPostExtended> {
const exists = await this.blogPostRepo.exists({ where: { id } });
if (!exists) {
@@ -63,8 +62,8 @@ export class BlogPostsService {
return this.findOne(id);
}
async uploadThumbnail(id: string, filename: string): Promise<BlogPost> {
const post = await this.findOne(id);
async uploadThumbnail(id: string, filename: string): Promise<BlogPostExtended> {
const post = await this.findEntity(id);
const previousStorageKey = post.thumbnailStorageKey;
@@ -74,8 +73,8 @@ export class BlogPostsService {
return this.findOne(id);
}
async uploadBanner(id: string, filename: string): Promise<BlogPost> {
const post = await this.findOne(id);
async uploadBanner(id: string, filename: string): Promise<BlogPostExtended> {
const post = await this.findEntity(id);
const previousStorageKey = post.bannerStorageKey;
@@ -86,7 +85,7 @@ export class BlogPostsService {
}
async remove(id: string): Promise<void> {
const post = await this.findOne(id);
const post = await this.findEntity(id);
await this.blogPostRepo.delete(id);
@@ -96,6 +95,24 @@ export class BlogPostsService {
]);
}
private async findEntity(id: string): Promise<BlogPost> {
const entity = await this.blogPostRepo.findOne({ where: { id } });
if (!entity) {
throw new NotFoundException();
}
return entity;
}
private toPostExtended(post: BlogPost): BlogPostExtended {
return {
...post,
thumbnailUrl: post.thumbnailStorageKey ? getBlogImagePublicUrl(post.thumbnailStorageKey) : null,
bannerUrl: post.bannerStorageKey ? getBlogImagePublicUrl(post.bannerStorageKey) : null
};
}
private async removeBlogImageFile(storageKey: string | null): Promise<void> {
if (!storageKey) {
return;
@@ -0,0 +1,6 @@
import type { BlogPost } from '../entities/BlogPost';
export type BlogPostExtended = BlogPost & {
thumbnailUrl: string | null;
bannerUrl: string | null;
};
@@ -1626,12 +1626,17 @@ a:hover {
text-decoration: none;
}
.sf-blog-card__media {
aspect-ratio: 16 / 9;
background: var(--sf-surface-muted);
border-radius: var(--sf-radius-sm);
overflow: hidden;
}
.sf-blog-card__image {
width: 100%;
aspect-ratio: 16 / 9;
height: 100%;
object-fit: cover;
border-radius: var(--sf-radius-sm);
background: var(--sf-surface-muted);
}
.sf-blog-card__title {
@@ -1639,10 +1644,10 @@ a:hover {
}
.sf-blog-detail__banner {
width: 100vw;
margin-left: calc(50% - 50vw);
margin-bottom: var(--sf-space-4);
background: var(--sf-surface-muted);
border-radius: var(--sf-radius-sm);
overflow: hidden;
}
.sf-blog-detail__banner-image {
@@ -1,6 +1,7 @@
<li class='sf-card sf-blog-card'>
<a class='sf-blog-card__link' href='{{detailHref}}'>
{{#if thumbnailUrl}}
<div class='sf-blog-card__media'>
{{> storefront-image
class='sf-blog-card__image'
src=thumbnailUrl
@@ -8,6 +9,7 @@
height='1'
loading='lazy'
}}
</div>
{{/if}}
<strong class='sf-blog-card__title'>{{title}}</strong>
</a>