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 && (
+
+

+
+ )}
+
+ )
+}
+
+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.sections.map(section => renderSection(section))}
+
+
+ {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 && (
-
-

-
- )}
-
- )
-}
-
-export function BlogPostModal({ post, onClose }: BlogPostModalProps) {
- const heroImage = resolveMediaUrl(post.previewImage)
-
- return (
-
-
event.stopPropagation()}
- >
-
-
- {heroImage && (
-
-

-
- )}
-
-
-
-
- {post.sections.map(section => renderSection(section))}
-
-
- {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)} />
- )}
)
}