6 Blog post
This commit is contained in:
517
src/components/BlogPost.tsx
Normal file
517
src/components/BlogPost.tsx
Normal file
@@ -0,0 +1,517 @@
|
||||
import React from 'react'
|
||||
import { BlogPost } from '@/data/blogPosts'
|
||||
|
||||
interface BlogPostCardProps {
|
||||
post: BlogPost
|
||||
onReadMore: (post: BlogPost) => void
|
||||
}
|
||||
|
||||
export function BlogPostCard({ post, onReadMore }: BlogPostCardProps) {
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
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
|
||||
</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'
|
||||
}} />
|
||||
)}
|
||||
|
||||
{/* Category & Date */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<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)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h2 style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '28px',
|
||||
fontWeight: '900',
|
||||
color: '#1E1A17',
|
||||
marginBottom: '16px',
|
||||
lineHeight: '1.2'
|
||||
}}>
|
||||
{post.title}
|
||||
</h2>
|
||||
|
||||
{/* Excerpt */}
|
||||
<p style={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '16px',
|
||||
color: '#4A4A4A',
|
||||
lineHeight: '1.6',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
{post.excerpt}
|
||||
</p>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Read More Button */}
|
||||
<button
|
||||
onClick={() => onReadMore(post)}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
border: '2px solid #C89C2B',
|
||||
color: '#C89C2B',
|
||||
padding: '12px 24px',
|
||||
fontFamily: 'Staatliches, sans-serif',
|
||||
fontSize: '16px',
|
||||
letterSpacing: '0.1em',
|
||||
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'
|
||||
}}
|
||||
>
|
||||
Read Full Article →
|
||||
</button>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
interface BlogPostModalProps {
|
||||
post: BlogPost
|
||||
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)
|
||||
}
|
||||
|
||||
// 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={{
|
||||
fontFamily: 'Spectral, serif',
|
||||
fontSize: '16px',
|
||||
color: '#4A4A4A',
|
||||
lineHeight: '1.8',
|
||||
marginBottom: '16px'
|
||||
}} dangerouslySetInnerHTML={{ __html: formattedText }} />
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
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',
|
||||
overflowY: 'auto',
|
||||
padding: '48px',
|
||||
position: 'relative',
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.3)'
|
||||
}} onClick={(e) => e.stopPropagation()}>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '20px',
|
||||
right: '20px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
fontSize: '32px',
|
||||
cursor: 'pointer',
|
||||
color: '#8B7D6B',
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: '32px' }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<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)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 style={{
|
||||
fontFamily: 'Abril Fatface, serif',
|
||||
fontSize: '42px',
|
||||
fontWeight: '900',
|
||||
color: '#1E1A17',
|
||||
lineHeight: '1.2',
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
{post.title}
|
||||
</h1>
|
||||
|
||||
<div className="newspaper-rule" style={{
|
||||
width: '100px',
|
||||
height: '2px',
|
||||
backgroundColor: '#C89C2B',
|
||||
margin: '24px 0'
|
||||
}} />
|
||||
</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'
|
||||
}}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user