DB ready
This commit is contained in:
parent
5856eda62b
commit
f36fe6276c
|
|
@ -12,18 +12,21 @@
|
||||||
API_PORT: 4005
|
API_PORT: 4005
|
||||||
API_BASE_URL: http://localhost:4005
|
API_BASE_URL: http://localhost:4005
|
||||||
NEXT_PUBLIC_API_BASE_URL: http://localhost:4005
|
NEXT_PUBLIC_API_BASE_URL: http://localhost:4005
|
||||||
DATABASE_URL: postgres://postgres:postgres@localhost:5433/claudia_blog
|
DATABASE_URL: postgres://postgres:postgres@postgres:5432/claudia_blog
|
||||||
CORS_ORIGINS: http://localhost:3005
|
CORS_ORIGINS: http://localhost:3005,http://172.25.182.67:3005
|
||||||
WATCHPACK_POLLING: "true"
|
WATCHPACK_POLLING: "true"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- node_modules:/app/node_modules
|
- node_modules:/app/node_modules
|
||||||
- uploads:/app/public/uploads
|
- uploads:/app/public/uploads
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
tty: true
|
tty: true
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:latest
|
image: postgres:latest
|
||||||
|
|
@ -31,14 +34,25 @@
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_DB: claudia_blog
|
POSTGRES_DB: claudia_blog
|
||||||
|
PGDATA: /tmp/pgdata
|
||||||
|
command:
|
||||||
|
- "postgres"
|
||||||
|
- "-c"
|
||||||
|
- "listen_addresses=*"
|
||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "0.0.0.0:5433:5432"
|
||||||
volumes:
|
healthcheck:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
node_modules:
|
node_modules:
|
||||||
postgres_data:
|
|
||||||
uploads:
|
uploads:
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,18 @@ app.get('/posts', async (_req, res) => {
|
||||||
app.get('/posts/:id', async (req, res) => {
|
app.get('/posts/:id', async (req, res) => {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
try {
|
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) {
|
if (result.rows.length === 0) {
|
||||||
return res.status(404).json({ error: 'Post not found' })
|
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 {
|
interface BlogPostCardProps {
|
||||||
post: BlogPost
|
post: BlogPost
|
||||||
onReadMore: (post: BlogPost) => void
|
|
||||||
isLatest: boolean
|
isLatest: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,14 +47,14 @@ const cardStyles = {
|
||||||
|
|
||||||
function formatDate(timestamp: string) {
|
function formatDate(timestamp: string) {
|
||||||
const date = new Date(timestamp)
|
const date = new Date(timestamp)
|
||||||
return date.toLocaleDateString(undefined, {
|
return date.toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BlogPostCard({ post, onReadMore, isLatest }: BlogPostCardProps) {
|
export function BlogPostCard({ post, isLatest }: BlogPostCardProps) {
|
||||||
const previewUrl = resolveMediaUrl(post.previewImage)
|
const previewUrl = resolveMediaUrl(post.previewImage)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -153,9 +152,8 @@ export function BlogPostCard({ post, onReadMore, isLatest }: BlogPostCardProps)
|
||||||
|
|
||||||
{post.excerpt && <p style={cardStyles.excerpt}>{post.excerpt}</p>}
|
{post.excerpt && <p style={cardStyles.excerpt}>{post.excerpt}</p>}
|
||||||
|
|
||||||
<button
|
<a
|
||||||
type="button"
|
href={`/blog/${post.slug}`}
|
||||||
onClick={() => onReadMore(post)}
|
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
@ -167,212 +165,14 @@ export function BlogPostCard({ post, onReadMore, isLatest }: BlogPostCardProps)
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
letterSpacing: '0.15em',
|
letterSpacing: '0.15em',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: '#1E1A17',
|
||||||
cursor: 'pointer'
|
cursor: 'pointer'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Read More
|
Read More
|
||||||
</button>
|
</a>
|
||||||
</article>
|
</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'
|
'use client'
|
||||||
|
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { BlogPost } from '@/types/blog'
|
import { BlogPost } from '@/types/blog'
|
||||||
import { ScrollEffects } from '@/components/ScrollEffects'
|
import { ScrollEffects } from '@/components/ScrollEffects'
|
||||||
import { FloatingElements } from '@/components/FloatingElements'
|
import { FloatingElements } from '@/components/FloatingElements'
|
||||||
import { ClientOnly } from '@/components/ClientOnly'
|
import { ClientOnly } from '@/components/ClientOnly'
|
||||||
import { BlogPostCard, BlogPostModal } from '@/components/BlogPost'
|
import { BlogPostCard } from '@/components/BlogPost'
|
||||||
|
|
||||||
type HomePageClientProps = {
|
type HomePageClientProps = {
|
||||||
posts: BlogPost[]
|
posts: BlogPost[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HomePageClient({ posts }: HomePageClientProps) {
|
export function HomePageClient({ posts }: HomePageClientProps) {
|
||||||
const [selectedBlogPost, setSelectedBlogPost] = useState<BlogPost | null>(null)
|
const { editorsPicks, regularPosts, latestPostId } = useMemo(() => {
|
||||||
|
|
||||||
const latestPostId = useMemo(() => {
|
|
||||||
if (!posts.length) {
|
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) {
|
if (!latestId) {
|
||||||
return current.id
|
return current.id
|
||||||
}
|
}
|
||||||
|
|
@ -28,6 +30,8 @@ export function HomePageClient({ posts }: HomePageClientProps) {
|
||||||
}
|
}
|
||||||
return new Date(current.createdAt) > new Date(latest.createdAt) ? current.id : latestId
|
return new Date(current.createdAt) > new Date(latest.createdAt) ? current.id : latestId
|
||||||
}, posts[0].id)
|
}, posts[0].id)
|
||||||
|
|
||||||
|
return { editorsPicks, regularPosts, latestPostId }
|
||||||
}, [posts])
|
}, [posts])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -125,7 +129,7 @@ export function HomePageClient({ posts }: HomePageClientProps) {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '800px',
|
maxWidth: '1200px',
|
||||||
margin: '0 auto'
|
margin: '0 auto'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -142,15 +146,39 @@ export function HomePageClient({ posts }: HomePageClientProps) {
|
||||||
</p>
|
</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
|
<BlogPostCard
|
||||||
key={post.id}
|
key={post.id}
|
||||||
post={post}
|
post={post}
|
||||||
isLatest={latestPostId === post.id}
|
isLatest={latestPostId === post.id}
|
||||||
onReadMore={setSelectedBlogPost}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -196,13 +224,36 @@ export function HomePageClient({ posts }: HomePageClientProps) {
|
||||||
>
|
>
|
||||||
(c) {new Date().getFullYear()} The Curated Finds - All rights reserved.
|
(c) {new Date().getFullYear()} The Curated Finds - All rights reserved.
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{selectedBlogPost && (
|
|
||||||
<BlogPostModal post={selectedBlogPost} onClose={() => setSelectedBlogPost(null)} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue