From f36fe6276c4249f0c6583291d06dc7dee9a52a8d Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Tue, 30 Sep 2025 22:10:00 +0200 Subject: [PATCH] DB ready --- docker-compose.yml | 32 ++-- server/index.js | 13 +- src/app/blog/[slug]/page.tsx | 236 ++++++++++++++++++++++++++++++ src/components/BlogPost.tsx | 214 +-------------------------- src/components/HomePageClient.tsx | 89 ++++++++--- 5 files changed, 348 insertions(+), 236 deletions(-) create mode 100644 src/app/blog/[slug]/page.tsx diff --git a/docker-compose.yml b/docker-compose.yml index 25d2dee..1840e9b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,18 +12,21 @@ API_PORT: 4005 API_BASE_URL: http://localhost:4005 NEXT_PUBLIC_API_BASE_URL: http://localhost:4005 - DATABASE_URL: postgres://postgres:postgres@localhost:5433/claudia_blog - CORS_ORIGINS: http://localhost:3005 + DATABASE_URL: postgres://postgres:postgres@postgres:5432/claudia_blog + CORS_ORIGINS: http://localhost:3005,http://172.25.182.67:3005 WATCHPACK_POLLING: "true" volumes: - .:/app - node_modules:/app/node_modules - uploads:/app/public/uploads depends_on: - - postgres + postgres: + condition: service_healthy working_dir: /app tty: true stdin_open: true + networks: + - app-network postgres: image: postgres:latest @@ -31,14 +34,25 @@ POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: claudia_blog + PGDATA: /tmp/pgdata + command: + - "postgres" + - "-c" + - "listen_addresses=*" ports: - - "5433:5432" - volumes: - - postgres_data:/var/lib/postgresql/data + - "0.0.0.0:5433:5432" + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U postgres" ] + interval: 5s + timeout: 5s + retries: 5 + networks: + - app-network + +networks: + app-network: + driver: bridge volumes: node_modules: - postgres_data: uploads: - - diff --git a/server/index.js b/server/index.js index 5c9be02..839b695 100644 --- a/server/index.js +++ b/server/index.js @@ -211,7 +211,18 @@ app.get('/posts', async (_req, res) => { app.get('/posts/:id', async (req, res) => { const { id } = req.params try { - const result = await query('SELECT * FROM blog_posts WHERE id = $1', [id]) + // Try to parse as integer ID first, otherwise treat as slug + const numericId = parseInt(id, 10) + let result + + if (!isNaN(numericId) && numericId.toString() === id) { + // It's a numeric ID + result = await query('SELECT * FROM blog_posts WHERE id = $1', [numericId]) + } else { + // It's a slug + result = await query('SELECT * FROM blog_posts WHERE slug = $1', [id]) + } + if (result.rows.length === 0) { return res.status(404).json({ error: 'Post not found' }) } diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx new file mode 100644 index 0000000..78aa845 --- /dev/null +++ b/src/app/blog/[slug]/page.tsx @@ -0,0 +1,236 @@ +import { notFound } from 'next/navigation' +import { resolveMediaUrl } from '@/lib/media' +import { BlogPost, BlogPostSection } from '@/types/blog' + +async function getBlogPost(slug: string): Promise { + const baseUrl = process.env.API_BASE_URL || process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:4005' + + try { + const response = await fetch(`${baseUrl}/posts/${slug}`, { + cache: 'no-store', + next: { revalidate: 0 } + }) + + if (!response.ok) { + if (response.status === 404) { + return null + } + console.error(`[frontend] Failed to load post: ${response.status}`) + return null + } + + const payload = await response.json() + return payload.data || null + } catch (error) { + console.error('[frontend] Failed to load post', error) + return null + } +} + +function formatDate(timestamp: string) { + const date = new Date(timestamp) + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }) +} + +function renderSection(section: BlogPostSection) { + const imageUrl = resolveMediaUrl(section.image) + + return ( +
+ {section.text && ( +

+ {section.text} +

+ )} + {imageUrl && ( +
+ Blog section +
+ )} +
+ ) +} + +export default async function BlogPostPage({ params }: { params: { slug: string } }) { + const post = await getBlogPost(params.slug) + + if (!post) { + notFound() + } + + const heroImage = resolveMediaUrl(post.previewImage) + + return ( +
+
+
+ + ← Back + + + {heroImage && ( +
+ {post.title} +
+ )} + +
+
+ {post.isEditorsPick && ( + + Editor's Pick + + )} + + {formatDate(post.createdAt)} + +
+ +

+ {post.title} +

+ + {post.linkUrl && ( + + To Produkt + + )} +
+ +
+ {post.sections.map(section => renderSection(section))} +
+ + {post.footer && ( +
+

+ {post.footer} +

+
+ )} +
+
+
+ ) +} diff --git a/src/components/BlogPost.tsx b/src/components/BlogPost.tsx index 76459c5..f232685 100644 --- a/src/components/BlogPost.tsx +++ b/src/components/BlogPost.tsx @@ -4,7 +4,6 @@ import { BlogPost, BlogPostSection } from '@/types/blog' interface BlogPostCardProps { post: BlogPost - onReadMore: (post: BlogPost) => void isLatest: boolean } @@ -48,14 +47,14 @@ const cardStyles = { function formatDate(timestamp: string) { const date = new Date(timestamp) - return date.toLocaleDateString(undefined, { + return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) } -export function BlogPostCard({ post, onReadMore, isLatest }: BlogPostCardProps) { +export function BlogPostCard({ post, isLatest }: BlogPostCardProps) { const previewUrl = resolveMediaUrl(post.previewImage) return ( @@ -153,9 +152,8 @@ export function BlogPostCard({ post, onReadMore, isLatest }: BlogPostCardProps) {post.excerpt &&

{post.excerpt}

} - + ) } -interface BlogPostModalProps { - post: BlogPost - onClose: () => void -} - -function renderSection(section: BlogPostSection) { - const imageUrl = resolveMediaUrl(section.image) - - return ( -
- {section.text && ( -

- {section.text} -

- )} - {imageUrl && ( -
- Blog section -
- )} -
- ) -} - -export function BlogPostModal({ post, onClose }: BlogPostModalProps) { - const heroImage = resolveMediaUrl(post.previewImage) - - return ( -
-
event.stopPropagation()} - > - - - {heroImage && ( -
- {post.title} -
- )} - -
-
- {post.isEditorsPick && ( - - Editor's Pick - - )} - - {formatDate(post.createdAt)} - -
- -

- {post.title} -

- - {post.linkUrl && ( - - To Produkt - - )} -
- -
- {post.sections.map(section => renderSection(section))} -
- - {post.footer && ( -
-

- {post.footer} -

-
- )} -
-
- ) -} diff --git a/src/components/HomePageClient.tsx b/src/components/HomePageClient.tsx index b41196d..4c34e94 100644 --- a/src/components/HomePageClient.tsx +++ b/src/components/HomePageClient.tsx @@ -1,24 +1,26 @@ 'use client' -import { useMemo, useState } from 'react' +import { useMemo } from 'react' import { BlogPost } from '@/types/blog' import { ScrollEffects } from '@/components/ScrollEffects' import { FloatingElements } from '@/components/FloatingElements' import { ClientOnly } from '@/components/ClientOnly' -import { BlogPostCard, BlogPostModal } from '@/components/BlogPost' +import { BlogPostCard } from '@/components/BlogPost' type HomePageClientProps = { posts: BlogPost[] } export function HomePageClient({ posts }: HomePageClientProps) { - const [selectedBlogPost, setSelectedBlogPost] = useState(null) - - const latestPostId = useMemo(() => { + const { editorsPicks, regularPosts, latestPostId } = useMemo(() => { if (!posts.length) { - return null + return { editorsPicks: [], regularPosts: [], latestPostId: null } } - return posts.reduce((latestId, current) => { + + const editorsPicks = posts.filter(post => post.isEditorsPick) + const regularPosts = posts.filter(post => !post.isEditorsPick) + + const latestPostId = posts.reduce((latestId, current) => { if (!latestId) { return current.id } @@ -28,6 +30,8 @@ export function HomePageClient({ posts }: HomePageClientProps) { } return new Date(current.createdAt) > new Date(latest.createdAt) ? current.id : latestId }, posts[0].id) + + return { editorsPicks, regularPosts, latestPostId } }, [posts]) return ( @@ -125,7 +129,7 @@ export function HomePageClient({ posts }: HomePageClientProps) {
@@ -142,14 +146,38 @@ export function HomePageClient({ posts }: HomePageClientProps) {

)} - {posts.map(post => ( - - ))} + {editorsPicks.length > 0 && ( +
+ {editorsPicks.map(post => ( +
+ +
+ ))} +
+ )} + + {regularPosts.length > 0 && ( +
+ {regularPosts.map(post => ( + + ))} +
+ )}
@@ -196,13 +224,36 @@ export function HomePageClient({ posts }: HomePageClientProps) { > (c) {new Date().getFullYear()} The Curated Finds - All rights reserved.

+ - {selectedBlogPost && ( - setSelectedBlogPost(null)} /> - )} ) }