DB ready
This commit is contained in:
parent
5856eda62b
commit
f36fe6276c
|
|
@ -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:
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<BlogPost | null> {
|
||||
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 (
|
||||
<div key={section.id} style={{ marginBottom: '32px' }}>
|
||||
{section.text && (
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '16px',
|
||||
color: '#4A4A4A',
|
||||
lineHeight: 1.8,
|
||||
marginBottom: imageUrl ? '18px' : 0
|
||||
}}
|
||||
>
|
||||
{section.text}
|
||||
</p>
|
||||
)}
|
||||
{imageUrl && (
|
||||
<div
|
||||
style={{
|
||||
border: '2px solid #8B7D6B',
|
||||
backgroundColor: 'white',
|
||||
padding: '6px',
|
||||
maxWidth: '420px',
|
||||
width: '100%',
|
||||
margin: '0 auto'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Blog section"
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
backgroundColor: '#F7F1E1'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
|
||||
const post = await getBlogPost(params.slug)
|
||||
|
||||
if (!post) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const heroImage = resolveMediaUrl(post.previewImage)
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', backgroundColor: '#F7F1E1' }}>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '820px',
|
||||
width: '100%',
|
||||
margin: '0 auto',
|
||||
padding: '48px 20px',
|
||||
backgroundColor: '#F7F1E1'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
backgroundColor: '#F7F1E1',
|
||||
border: '2px solid #8B7D6B',
|
||||
padding: '32px',
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.35)'
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href="/"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '16px',
|
||||
left: '16px',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #8B7D6B',
|
||||
backgroundColor: 'transparent',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
textDecoration: 'none',
|
||||
color: '#1E1A17'
|
||||
}}
|
||||
>
|
||||
← Back
|
||||
</a>
|
||||
|
||||
{heroImage && (
|
||||
<div
|
||||
style={{
|
||||
marginLeft: '-32px',
|
||||
marginRight: '-32px',
|
||||
marginTop: '-32px',
|
||||
borderBottom: '2px solid #8B7D6B'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={heroImage}
|
||||
alt={post.title}
|
||||
style={{ display: 'block', width: '100%', height: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<header style={{ marginTop: '24px', marginBottom: '32px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
alignItems: 'center',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
{post.isEditorsPick && (
|
||||
<span style={{
|
||||
backgroundColor: '#C89C2B',
|
||||
color: '#F7F1E1',
|
||||
padding: '6px 12px',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
letterSpacing: '0.15em',
|
||||
textTransform: 'uppercase'
|
||||
}}>
|
||||
Editor's Pick
|
||||
</span>
|
||||
)}
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
letterSpacing: '0.15em',
|
||||
color: '#8B7D6B',
|
||||
textTransform: 'uppercase'
|
||||
}}>
|
||||
{formatDate(post.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '42px',
|
||||
color: '#1E1A17',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
{post.linkUrl && (
|
||||
<a
|
||||
href={post.linkUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
backgroundColor: '#1E1A17',
|
||||
color: '#F7F1E1',
|
||||
padding: '12px 22px',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
letterSpacing: '0.15em',
|
||||
textTransform: 'uppercase',
|
||||
border: '2px solid #8B7D6B'
|
||||
}}
|
||||
>
|
||||
To Produkt
|
||||
</a>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div>
|
||||
{post.sections.map(section => renderSection(section))}
|
||||
</div>
|
||||
|
||||
{post.footer && (
|
||||
<footer style={{
|
||||
borderTop: '2px solid #8B7D6B',
|
||||
marginTop: '32px',
|
||||
paddingTop: '24px'
|
||||
}}>
|
||||
<p style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '15px',
|
||||
color: '#4A4A4A',
|
||||
lineHeight: 1.6
|
||||
}}>
|
||||
{post.footer}
|
||||
</p>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 && <p style={cardStyles.excerpt}>{post.excerpt}</p>}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onReadMore(post)}
|
||||
<a
|
||||
href={`/blog/${post.slug}`}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
|
|
@ -167,212 +165,14 @@ export function BlogPostCard({ post, onReadMore, isLatest }: BlogPostCardProps)
|
|||
fontSize: '12px',
|
||||
letterSpacing: '0.15em',
|
||||
textTransform: 'uppercase',
|
||||
textDecoration: 'none',
|
||||
color: '#1E1A17',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Read More
|
||||
</button>
|
||||
</a>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
interface BlogPostModalProps {
|
||||
post: BlogPost
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function renderSection(section: BlogPostSection) {
|
||||
const imageUrl = resolveMediaUrl(section.image)
|
||||
|
||||
return (
|
||||
<div key={section.id} style={{ marginBottom: '32px' }}>
|
||||
{section.text && (
|
||||
<p
|
||||
style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '16px',
|
||||
color: '#4A4A4A',
|
||||
lineHeight: 1.8,
|
||||
marginBottom: imageUrl ? '18px' : 0
|
||||
}}
|
||||
>
|
||||
{section.text}
|
||||
</p>
|
||||
)}
|
||||
{imageUrl && (
|
||||
<div
|
||||
style={{
|
||||
border: '2px solid #8B7D6B',
|
||||
backgroundColor: 'white',
|
||||
padding: '6px',
|
||||
maxWidth: '420px',
|
||||
width: '100%',
|
||||
margin: '0 auto'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt="Blog section"
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
backgroundColor: '#F7F1E1'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function BlogPostModal({ post, onClose }: BlogPostModalProps) {
|
||||
const heroImage = resolveMediaUrl(post.previewImage)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
overflowY: 'auto',
|
||||
padding: '48px 20px'
|
||||
}}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
maxWidth: '820px',
|
||||
width: '100%',
|
||||
backgroundColor: '#F7F1E1',
|
||||
border: '2px solid #8B7D6B',
|
||||
padding: '32px',
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.35)'
|
||||
}}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '16px',
|
||||
right: '16px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
aria-label="Close"
|
||||
>
|
||||
X
|
||||
</button>
|
||||
|
||||
{heroImage && (
|
||||
<div
|
||||
style={{
|
||||
marginLeft: '-32px',
|
||||
marginRight: '-32px',
|
||||
marginTop: '-32px',
|
||||
borderBottom: '2px solid #8B7D6B'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={heroImage}
|
||||
alt={post.title}
|
||||
style={{ display: 'block', width: '100%', height: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<header style={{ marginTop: '24px', marginBottom: '32px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
alignItems: 'center',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
{post.isEditorsPick && (
|
||||
<span style={{
|
||||
backgroundColor: '#C89C2B',
|
||||
color: '#F7F1E1',
|
||||
padding: '6px 12px',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
letterSpacing: '0.15em',
|
||||
textTransform: 'uppercase'
|
||||
}}>
|
||||
Editor's Pick
|
||||
</span>
|
||||
)}
|
||||
<span style={{
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
letterSpacing: '0.15em',
|
||||
color: '#8B7D6B',
|
||||
textTransform: 'uppercase'
|
||||
}}>
|
||||
{formatDate(post.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '42px',
|
||||
color: '#1E1A17',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
{post.title}
|
||||
</h2>
|
||||
|
||||
{post.linkUrl && (
|
||||
<a
|
||||
href={post.linkUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
backgroundColor: '#1E1A17',
|
||||
color: '#F7F1E1',
|
||||
padding: '12px 22px',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '12px',
|
||||
letterSpacing: '0.15em',
|
||||
textTransform: 'uppercase',
|
||||
border: '2px solid #8B7D6B'
|
||||
}}
|
||||
>
|
||||
To Produkt
|
||||
</a>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div>
|
||||
{post.sections.map(section => renderSection(section))}
|
||||
</div>
|
||||
|
||||
{post.footer && (
|
||||
<footer style={{
|
||||
borderTop: '2px solid #8B7D6B',
|
||||
marginTop: '32px',
|
||||
paddingTop: '24px'
|
||||
}}>
|
||||
<p style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '15px',
|
||||
color: '#4A4A4A',
|
||||
lineHeight: 1.6
|
||||
}}>
|
||||
{post.footer}
|
||||
</p>
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<BlogPost | null>(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) {
|
|||
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '800px',
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto'
|
||||
}}
|
||||
>
|
||||
|
|
@ -142,15 +146,39 @@ export function HomePageClient({ posts }: HomePageClientProps) {
|
|||
</p>
|
||||
)}
|
||||
|
||||
{posts.map(post => (
|
||||
{editorsPicks.length > 0 && (
|
||||
<div style={{ marginBottom: '48px' }}>
|
||||
{editorsPicks.map(post => (
|
||||
<div key={post.id} style={{ maxWidth: '800px', margin: '0 auto' }}>
|
||||
<BlogPostCard
|
||||
post={post}
|
||||
isLatest={latestPostId === post.id}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{regularPosts.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(350px, 1fr))',
|
||||
gap: '32px',
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto'
|
||||
}}
|
||||
>
|
||||
{regularPosts.map(post => (
|
||||
<BlogPostCard
|
||||
key={post.id}
|
||||
post={post}
|
||||
isLatest={latestPostId === post.id}
|
||||
onReadMore={setSelectedBlogPost}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -196,13 +224,36 @@ export function HomePageClient({ posts }: HomePageClientProps) {
|
|||
>
|
||||
(c) {new Date().getFullYear()} The Curated Finds - All rights reserved.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const password = prompt('Admin-Passwort eingeben:')
|
||||
if (password === 'timo') {
|
||||
window.location.href = '/admin'
|
||||
} else if (password !== null) {
|
||||
alert('Falsches Passwort')
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
marginTop: '16px',
|
||||
padding: '8px 16px',
|
||||
border: '1px solid #8B7D6B',
|
||||
backgroundColor: 'transparent',
|
||||
color: '#8B7D6B',
|
||||
fontFamily: 'Space Mono, monospace',
|
||||
fontSize: '10px',
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
cursor: 'pointer',
|
||||
opacity: 0.5
|
||||
}}
|
||||
>
|
||||
admin
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{selectedBlogPost && (
|
||||
<BlogPostModal post={selectedBlogPost} onClose={() => setSelectedBlogPost(null)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue