This commit is contained in:
2025-09-30 01:54:58 +02:00
parent 148cb6d283
commit 5856eda62b
16 changed files with 3340 additions and 1271 deletions

View File

@@ -1,163 +1,176 @@
import React from 'react'
import { BlogPost } from '@/data/blogPosts'
import React from 'react'
import { resolveMediaUrl } from '@/lib/media'
import { BlogPost, BlogPostSection } from '@/types/blog'
interface BlogPostCardProps {
post: BlogPost
onReadMore: (post: BlogPost) => void
isLatest: boolean
}
export function BlogPostCard({ post, onReadMore }: BlogPostCardProps) {
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date)
const cardStyles = {
container: {
position: 'relative' as const,
backgroundColor: 'white',
border: '2px solid #8B7D6B',
padding: '24px',
marginBottom: '32px',
boxShadow: '4px 4px 0 rgba(139, 125, 107, 0.2)'
},
imageWrapper: {
marginBottom: '24px',
marginLeft: '-24px',
marginRight: '-24px',
marginTop: '-24px',
height: '280px',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundColor: 'white',
borderBottom: '2px solid #8B7D6B'
},
title: {
fontFamily: 'Abril Fatface, serif',
fontSize: '28px',
fontWeight: 900,
color: '#1E1A17',
marginBottom: '16px',
lineHeight: 1.2
},
excerpt: {
fontFamily: 'Spectral, serif',
fontSize: '16px',
color: '#4A4A4A',
lineHeight: 1.6,
marginBottom: '20px'
}
}
function formatDate(timestamp: string) {
const date = new Date(timestamp)
return date.toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
export function BlogPostCard({ post, onReadMore, isLatest }: BlogPostCardProps) {
const previewUrl = resolveMediaUrl(post.previewImage)
return (
<article style={{
backgroundColor: 'white',
border: '2px solid #8B7D6B',
padding: '24px',
marginBottom: '32px',
position: 'relative',
boxShadow: '4px 4px 0 rgba(139, 125, 107, 0.2)'
}}>
{/* Featured Badge */}
{post.featured && (
<div style={{
position: 'absolute',
top: '-12px',
right: '24px',
backgroundColor: '#C89C2B',
color: '#F7F1E1',
padding: '4px 12px',
fontFamily: 'Pacifico, cursive',
fontSize: '14px',
transform: 'rotate(3deg)',
boxShadow: '2px 2px 4px rgba(0,0,0,0.2)'
}}>
Featured
<article style={cardStyles.container}>
{post.isEditorsPick && (
<div
style={{
position: 'absolute',
top: '32px',
left: '-60px',
padding: '8px 64px',
backgroundColor: '#C89C2B',
color: '#F7F1E1',
fontFamily: 'Space Mono, monospace',
fontSize: '12px',
letterSpacing: '0.2em',
textTransform: 'uppercase',
transform: 'rotate(-45deg)',
boxShadow: '0 4px 12px rgba(0,0,0,0.2)'
}}
>
Editor's Pick
</div>
)}
{/* Header Image */}
{(post.previewImage || post.images[0]) && (
<div style={{
marginBottom: '24px',
marginLeft: '-24px',
marginRight: '-24px',
marginTop: '-24px',
height: '300px',
backgroundImage: `url('${post.previewImage || post.images[0]}')`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundColor: 'white',
borderBottom: '2px solid #8B7D6B'
}} />
{isLatest && (
<div
style={{
position: 'absolute',
top: '16px',
right: '16px',
backgroundColor: '#1E1A17',
color: '#F7F1E1',
padding: '6px 12px',
fontFamily: 'Space Mono, monospace',
fontSize: '12px',
letterSpacing: '0.15em',
textTransform: 'uppercase'
}}
>
Last Blog Post
</div>
)}
{previewUrl && (
<div
style={{
...cardStyles.imageWrapper,
backgroundImage: `url('${previewUrl}')`
}}
/>
)}
{/* Category & Date */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '16px'
marginBottom: '12px'
}}>
<span style={{
fontFamily: 'Space Mono, monospace',
fontSize: '12px',
textTransform: 'uppercase',
letterSpacing: '0.15em',
color: '#C89C2B',
fontWeight: 'bold'
}}>
{post.category}
</span>
<span style={{
fontFamily: 'Space Mono, monospace',
fontSize: '12px',
color: '#8B7D6B'
}}>
{formatDate(post.datePublished)}
{formatDate(post.createdAt)}
</span>
</div>
{/* Title */}
<h2 style={{
fontFamily: 'Abril Fatface, serif',
fontSize: '28px',
fontWeight: '900',
color: '#1E1A17',
marginBottom: '16px',
lineHeight: '1.2'
}}>
{post.title}
</h2>
<h2 style={cardStyles.title}>{post.title}</h2>
{/* Excerpt */}
<p style={{
fontFamily: 'Spectral, serif',
fontSize: '16px',
color: '#4A4A4A',
lineHeight: '1.6',
marginBottom: '20px'
}}>
{post.excerpt}
</p>
{post.linkUrl && (
<div style={{ marginBottom: '16px' }}>
<a
href={post.linkUrl}
target="_blank"
rel="noopener noreferrer"
style={{
display: 'inline-block',
backgroundColor: '#1E1A17',
color: '#F7F1E1',
padding: '10px 18px',
fontFamily: 'Space Mono, monospace',
fontSize: '12px',
letterSpacing: '0.15em',
textTransform: 'uppercase',
border: '2px solid #8B7D6B'
}}
>
To Produkt
</a>
</div>
)}
{/* Tags */}
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px',
marginBottom: '20px'
}}>
{post.tags.slice(0, 3).map(tag => (
<span key={tag} style={{
backgroundColor: '#F7F1E1',
border: '1px solid #8B7D6B',
padding: '4px 8px',
fontFamily: 'Space Mono, monospace',
fontSize: '11px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
color: '#8B7D6B'
}}>
{tag}
</span>
))}
</div>
{post.excerpt && <p style={cardStyles.excerpt}>{post.excerpt}</p>}
{/* Read More Button */}
<button
type="button"
onClick={() => onReadMore(post)}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
padding: '12px 18px',
border: '2px solid #8B7D6B',
backgroundColor: 'transparent',
border: '2px solid #C89C2B',
color: '#C89C2B',
padding: '12px 24px',
fontFamily: 'Staatliches, sans-serif',
fontSize: '16px',
letterSpacing: '0.1em',
fontFamily: 'Space Mono, monospace',
fontSize: '12px',
letterSpacing: '0.15em',
textTransform: 'uppercase',
cursor: 'pointer',
transition: 'all 0.3s ease',
position: 'relative',
overflow: 'hidden'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#C89C2B'
e.currentTarget.style.color = '#F7F1E1'
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent'
e.currentTarget.style.color = '#C89C2B'
cursor: 'pointer'
}}
>
Read Full Article
Read More
</button>
</article>
)
@@ -168,350 +181,198 @@ interface BlogPostModalProps {
onClose: () => void
}
export function BlogPostModal({ post, onClose }: BlogPostModalProps) {
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date)
}
function renderSection(section: BlogPostSection) {
const imageUrl = resolveMediaUrl(section.image)
// Convert markdown-style formatting to HTML
const formatContent = (content: string) => {
return content
.split('\n\n')
.map((paragraph, index) => {
// Handle headers
if (paragraph.startsWith('## ')) {
return (
<h3 key={index} style={{
fontFamily: 'Abril Fatface, serif',
fontSize: '24px',
fontWeight: '900',
color: '#1E1A17',
marginTop: '32px',
marginBottom: '16px'
}}>
{paragraph.replace('## ', '')}
</h3>
)
}
if (paragraph.startsWith('### ')) {
return (
<h4 key={index} style={{
fontFamily: 'Staatliches, sans-serif',
fontSize: '20px',
color: '#C89C2B',
marginTop: '24px',
marginBottom: '12px'
}}>
{paragraph.replace('### ', '')}
</h4>
)
}
// Handle lists
if (paragraph.startsWith('- ')) {
const items = paragraph.split('\n').filter(line => line.startsWith('- '))
return (
<ul key={index} style={{
marginBottom: '16px',
paddingLeft: '24px'
}}>
{items.map((item, i) => (
<li key={i} style={{
fontFamily: 'Spectral, serif',
fontSize: '16px',
color: '#4A4A4A',
lineHeight: '1.8',
marginBottom: '8px'
}}>
{item.replace('- ', '').replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')}
</li>
))}
</ul>
)
}
// Regular paragraphs with bold text support
const formattedText = paragraph.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
return (
<p key={index} style={{
return (
<div key={section.id} style={{ marginBottom: '32px' }}>
{section.text && (
<p
style={{
fontFamily: 'Spectral, serif',
fontSize: '16px',
color: '#4A4A4A',
lineHeight: '1.8',
marginBottom: '16px'
}} dangerouslySetInnerHTML={{ __html: formattedText }} />
)
})
}
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',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(30, 26, 23, 0.8)',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
overflowY: 'auto'
}} onClick={onClose}>
<article style={{
backgroundColor: '#F7F1E1',
maxWidth: '900px',
width: '100%',
maxHeight: '90vh',
<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',
position: 'relative',
boxShadow: '0 10px 40px rgba(0,0,0,0.3)'
}} onClick={(e) => e.stopPropagation()}>
{/* Close Button */}
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: '20px',
right: '20px',
top: '16px',
right: '16px',
background: 'transparent',
border: 'none',
fontSize: '32px',
cursor: 'pointer',
color: '#8B7D6B',
zIndex: 10
fontSize: '24px',
cursor: 'pointer'
}}
aria-label="Close"
>
×
X
</button>
{/* Header */}
<div style={{ marginBottom: '32px' }}>
{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',
justifyContent: 'space-between',
gap: '16px',
alignItems: 'center',
marginBottom: '16px'
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',
textTransform: 'uppercase',
letterSpacing: '0.15em',
color: '#C89C2B',
fontWeight: 'bold'
color: '#8B7D6B',
textTransform: 'uppercase'
}}>
{post.category}
</span>
<span style={{
fontFamily: 'Space Mono, monospace',
fontSize: '12px',
color: '#8B7D6B'
}}>
{formatDate(post.datePublished)}
{formatDate(post.createdAt)}
</span>
</div>
<h1 style={{
<h2 style={{
fontFamily: 'Abril Fatface, serif',
fontSize: '42px',
fontWeight: '900',
color: '#1E1A17',
lineHeight: '1.2',
marginBottom: '24px'
marginBottom: '16px'
}}>
{post.title}
</h1>
</h2>
<div className="newspaper-rule" style={{
width: '100px',
height: '2px',
backgroundColor: '#C89C2B',
margin: '24px 0'
}} />
{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>
{/* Content with interleaved images */}
<div style={{ marginBottom: '32px' }}>
{(() => {
const blocks = post.content.split('\n\n')
const nodes: React.ReactNode[] = []
const totalBlocks = blocks.length
const totalImages = post.images.length
const step = totalImages > 0 ? Math.ceil(totalBlocks / (totalImages + 1)) : Infinity
let imageIndex = 0
blocks.forEach((paragraph, index) => {
// Headers
if (paragraph.startsWith('## ')) {
nodes.push(
<h3 key={`h3-${index}`} style={{
fontFamily: 'Abril Fatface, serif',
fontSize: '24px',
fontWeight: '900',
color: '#1E1A17',
marginTop: '32px',
marginBottom: '16px'
}}>
{paragraph.replace('## ', '')}
</h3>
)
} else if (paragraph.startsWith('### ')) {
nodes.push(
<h4 key={`h4-${index}`} style={{
fontFamily: 'Staatliches, sans-serif',
fontSize: '20px',
color: '#C89C2B',
marginTop: '24px',
marginBottom: '12px'
}}>
{paragraph.replace('### ', '')}
</h4>
)
} else if (paragraph.startsWith('- ')) {
const items = paragraph.split('\n').filter(line => line.startsWith('- '))
nodes.push(
<ul key={`ul-${index}`} style={{
marginBottom: '16px',
paddingLeft: '24px'
}}>
{items.map((item, i) => (
<li
key={`li-${index}-${i}`}
style={{
fontFamily: 'Spectral, serif',
fontSize: '16px',
color: '#4A4A4A',
lineHeight: '1.8',
marginBottom: '8px'
}}
dangerouslySetInnerHTML={{ __html: item.replace('- ', '').replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') }}
/>
))}
</ul>
)
} else {
// Paragraphs
const formattedText = paragraph.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
nodes.push(
<p key={`p-${index}`} style={{
fontFamily: 'Spectral, serif',
fontSize: '16px',
color: '#4A4A4A',
lineHeight: '1.8',
marginBottom: '16px'
}} dangerouslySetInnerHTML={{ __html: formattedText }} />
)
}
// Interleave images evenly through the content
if (totalImages > 0 && imageIndex < totalImages && (index + 1) % step === 0) {
nodes.push(
<div
key={`img-${imageIndex}`}
style={{
margin: '16px 0 32px',
display: 'flex',
justifyContent: 'center'
}}
>
<div
style={{
border: '2px solid #8B7D6B',
backgroundColor: 'white',
padding: '6px',
maxWidth: '360px',
width: '100%'
}}
>
<img
src={post.images[imageIndex]}
alt={`Image ${imageIndex + 1}`}
style={{
display: 'block',
width: '100%',
height: 'auto',
backgroundColor: '#F7F1E1'
}}
/>
</div>
</div>
)
imageIndex++
}
})
// If any images remain, append them at the end
while (imageIndex < totalImages) {
nodes.push(
<div
key={`img-tail-${imageIndex}`}
style={{
margin: '16px 0 32px',
display: 'flex',
justifyContent: 'center'
}}
>
<div
style={{
border: '2px solid #8B7D6B',
backgroundColor: 'white',
padding: '6px',
maxWidth: '360px',
width: '100%'
}}
>
<img
src={post.images[imageIndex]}
alt={`Image ${imageIndex + 1}`}
style={{
display: 'block',
width: '100%',
height: 'auto',
backgroundColor: '#F7F1E1'
}}
/>
</div>
</div>
)
imageIndex++
}
return nodes
})()}
</div>
{/* Tags */}
<div style={{
borderTop: '2px solid #8B7D6B',
paddingTop: '24px',
display: 'flex',
flexWrap: 'wrap',
gap: '8px'
}}>
{post.tags.map(tag => (
<span key={tag} style={{
backgroundColor: 'white',
border: '1px solid #8B7D6B',
padding: '6px 12px',
fontFamily: 'Space Mono, monospace',
fontSize: '12px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
color: '#8B7D6B'
{post.footer && (
<footer style={{
borderTop: '2px solid #8B7D6B',
marginTop: '32px',
paddingTop: '24px'
}}>
<p style={{
fontFamily: 'Spectral, serif',
fontSize: '15px',
color: '#4A4A4A',
lineHeight: 1.6
}}>
{tag}
</span>
))}
</div>
</article>
{post.footer}
</p>
</footer>
)}
</div>
</div>
)
}
}