ci: migrate GitHub actions to Gitea actions
Deploy CV Site (andreknie.de) / deploy (push) Canceled after 0s
Deploy CV Site (andreknie.de) / deploy (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import React from 'react'
|
||||
import { useParams, Link } from 'react-router-dom'
|
||||
import { useContent } from '../hooks/useContent'
|
||||
import SEOHead from '../components/SEOHead'
|
||||
import { decodeHtml } from '../utils/decodeHtml'
|
||||
|
||||
export default function ArticleSingle() {
|
||||
const { slug } = useParams()
|
||||
const { items: posts } = useContent('posts', { pageSize: 1000 })
|
||||
const post = posts.find((p) => p.slug === slug)
|
||||
|
||||
if (!post) {
|
||||
return (
|
||||
<div className="page-container app-container" style={{ textAlign: 'center', paddingTop: '150px' }}>
|
||||
<h2>Artikel nicht gefunden</h2>
|
||||
<Link to="/artikel" className="primary-button" style={{ marginTop: '20px' }}>Zurück zur Übersicht</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const formattedDate = post.date
|
||||
? new Date(post.date).toLocaleDateString('de-DE', { year: 'numeric', month: 'long', day: 'numeric' })
|
||||
: ''
|
||||
|
||||
return (
|
||||
<div className="page-container article-single-page">
|
||||
<SEOHead
|
||||
title={`${post.title} | Dr. André Knie`}
|
||||
description={post.excerpt || post.title}
|
||||
/>
|
||||
<div className="app-container" style={{ maxWidth: '800px', margin: '0 auto', paddingTop: '120px' }}>
|
||||
<Link to="/artikel" style={{ color: 'var(--primary-color)', textDecoration: 'none', marginBottom: '20px', display: 'inline-block' }}>
|
||||
← Zurück zur Übersicht
|
||||
</Link>
|
||||
<article className="glass-panel" style={{ padding: '40px' }}>
|
||||
<header style={{ marginBottom: '30px' }}>
|
||||
{formattedDate && <p style={{ color: 'var(--text-secondary)', marginBottom: '10px' }}>{formattedDate}</p>}
|
||||
<h1 style={{ fontSize: '2.5rem', marginBottom: '20px' }}>{decodeHtml(post.title)}</h1>
|
||||
{post.url && (
|
||||
<a href={post.url} target={post.url.startsWith('/') ? "_self" : "_blank"} rel="noopener noreferrer" className="secondary-button" style={{ display: 'inline-block', marginBottom: '20px' }}>
|
||||
{post.url.includes('/podcast') ? 'Zum Podcast' : (post.linkText || 'Auf LinkedIn ansehen')}
|
||||
</a>
|
||||
)}
|
||||
</header>
|
||||
<div
|
||||
className="article-body content-body"
|
||||
dangerouslySetInnerHTML={{ __html: post.body }}
|
||||
/>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
.articles-page {
|
||||
padding-top: 120px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.articles-page .page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.articles-page .hero-title {
|
||||
font-size: clamp(2.5rem, 5vw, 4rem);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.articles-page .hero-title span {
|
||||
color: var(--color-primary, #646cff);
|
||||
background: linear-gradient(135deg, var(--color-primary, #646cff), var(--color-accent, #9089fc));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.articles-page .hero-subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text-muted, #aaa);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.articles-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 2.5rem;
|
||||
padding-bottom: 6rem;
|
||||
}
|
||||
|
||||
.article-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease, border-color 0.3s ease;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.article-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.article-image {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.article-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.article-card:hover .article-image img {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.article-content {
|
||||
padding: 1.8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.article-date {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-muted, #888);
|
||||
margin-bottom: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text, #fff);
|
||||
margin: 0 0 1rem;
|
||||
line-height: 1.4;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.article-card:hover .article-title {
|
||||
color: var(--color-primary, #646cff);
|
||||
}
|
||||
|
||||
.article-excerpt {
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-muted, #aaa);
|
||||
line-height: 1.6;
|
||||
margin: 0 0 1.5rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.article-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary, #646cff);
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
transition: color 0.2s ease, transform 0.2s ease;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.article-link:hover {
|
||||
color: var(--color-primary-light, #9089fc);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.no-articles {
|
||||
text-align: center;
|
||||
grid-column: 1 / -1;
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text-muted, #aaa);
|
||||
padding: 3rem 0;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useContent } from '../hooks/useContent'
|
||||
import SEOHead from '../components/SEOHead'
|
||||
import { decodeHtml } from '../utils/decodeHtml'
|
||||
import './Articles.css'
|
||||
|
||||
export default function Articles() {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const { items: posts } = useContent('posts', { pageSize: 100 })
|
||||
|
||||
const filteredPosts = posts?.filter(post => {
|
||||
if (!searchQuery) return true
|
||||
const query = searchQuery.toLowerCase()
|
||||
return (
|
||||
(post.title && post.title.toLowerCase().includes(query)) ||
|
||||
(post.excerpt && post.excerpt.toLowerCase().includes(query)) ||
|
||||
(post.body && post.body.toLowerCase().includes(query))
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="page-container articles-page">
|
||||
<SEOHead
|
||||
title="Artikel & Beiträge | Dr. André Knie"
|
||||
description="Aktuelle Artikel und LinkedIn-Beiträge zu den Themen Softwareentwicklung, Leadership und Technologie."
|
||||
/>
|
||||
<header className="page-header">
|
||||
<div className="app-container">
|
||||
<h1 className="hero-title">
|
||||
Meine <span>Artikel</span>
|
||||
</h1>
|
||||
<p className="hero-subtitle">
|
||||
Gedanken, Analysen und Erfahrungen rund um Software Engineering, Führung und Technologie.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="app-container" style={{ marginBottom: '40px' }}>
|
||||
<div style={{ maxWidth: '800px', margin: '0 auto', position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-secondary)' }}>
|
||||
<Search size={20} />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Artikel durchsuchen..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '16px 80px 16px 48px',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
background: 'var(--glass-bg)',
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: '1rem',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
<div style={{ position: 'absolute', right: '16px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
|
||||
{filteredPosts?.length || 0} / {posts?.length || 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="articles-grid app-container">
|
||||
{filteredPosts?.map((post) => (
|
||||
<article key={post.id || post.slug} className="article-card glass-card">
|
||||
{post.image && (
|
||||
<div className="article-image">
|
||||
<img src={post.image} alt={post.title} />
|
||||
</div>
|
||||
)}
|
||||
<div className="article-content">
|
||||
{post.date && (
|
||||
<span className="article-date">
|
||||
{new Date(post.date).toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
<h2 className="article-title">{decodeHtml(post.title)}</h2>
|
||||
<p className="article-excerpt">
|
||||
{decodeHtml(post.excerpt || (post.body ? post.body.replace(/<[^>]+>/g, '').substring(0, 150) + '...' : ''))}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '16px' }}>
|
||||
<a href={`/artikel/${post.slug}`} className="article-link">
|
||||
Weiterlesen →
|
||||
</a>
|
||||
{post.url && post.url.includes('/podcast') && (
|
||||
<a href={post.url} className="article-link" style={{ color: 'var(--accent-color)' }}>
|
||||
Zum Podcast →
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
{(!filteredPosts || filteredPosts.length === 0) && (
|
||||
<p className="no-articles">Keine Artikel für diese Suche gefunden.</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import { useSearchParams, Link } from 'react-router-dom'
|
||||
import SEOHead from '../components/SEOHead'
|
||||
|
||||
export default function Confirmation() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const status = searchParams.get('status')
|
||||
|
||||
const isSuccess = status === 'success'
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEOHead
|
||||
title={isSuccess ? "Vielen Dank" : "Ein Fehler ist aufgetreten"}
|
||||
description={isSuccess ? "Ihre Nachricht wurde erfolgreich gesendet." : "Es gab ein Problem beim Senden Ihrer Nachricht."}
|
||||
/>
|
||||
<main style={{ paddingTop: '120px', minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div className="app-container" style={{ textAlign: 'center', maxWidth: '600px' }}>
|
||||
{isSuccess ? (
|
||||
<>
|
||||
<div style={{ width: '80px', height: '80px', borderRadius: '50%', background: 'rgba(34, 197, 94, 0.1)', color: '#22c55e', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 24px' }}>
|
||||
<svg width="40" height="40" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 style={{ fontSize: '3rem', marginBottom: '16px' }}><span className="text-gradient">Vielen Dank!</span></h1>
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: '1.2rem', marginBottom: '32px' }}>
|
||||
Ihre Nachricht wurde erfolgreich gesendet. Ich werde mich so schnell wie möglich bei Ihnen melden.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ width: '80px', height: '80px', borderRadius: '50%', background: 'rgba(239, 68, 68, 0.1)', color: '#ef4444', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 24px' }}>
|
||||
<svg width="40" height="40" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 style={{ fontSize: '3rem', marginBottom: '16px' }}><span className="text-gradient">Ein Fehler ist aufgetreten</span></h1>
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: '1.2rem', marginBottom: '32px' }}>
|
||||
Leider gab es ein Problem beim Senden Ihrer Nachricht. Bitte versuchen Sie es später noch einmal oder kontaktieren Sie mich direkt per E-Mail.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<Link to="/" className="primary-button">Zurück zur Startseite</Link>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -38,8 +38,8 @@ export default function Datenschutz() {
|
||||
<p><strong style={{ color: 'var(--text-primary)' }}>9. Ihre Rechte</strong></p>
|
||||
<p>Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit. Wenden Sie sich an: <a href="mailto:datenschutz@d-hive.de">datenschutz@d-hive.de</a></p>
|
||||
|
||||
<p><strong style={{ color: 'var(--text-primary)' }}>10. Cookies</strong></p>
|
||||
<p>Diese Webseite verwendet keine Tracking-Cookies und keine Analyse-Tools von Drittanbietern. Es werden ausschließlich technisch notwendige Funktionen genutzt.</p>
|
||||
<p><strong style={{ color: 'var(--text-primary)' }}>10. Cookies und Webanalyse</strong></p>
|
||||
<p>Diese Webseite verwendet <strong>keine</strong> Cookies zur Analyse Ihres Nutzerverhaltens. Um die Nutzung unserer Website statistisch auszuwerten und das Angebot zu verbessern, setzen wir die Open-Source-Software <strong>Umami</strong> ein. Umami ist selbstgehostet (die Daten verlassen unsere Server nicht) und arbeitet zu 100% ohne Cookies und ohne die Speicherung von personenbezogenen Daten. Ihre IP-Adresse wird sofort anonymisiert. Ein Rückschluss auf Ihre Person ist nicht möglich.</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useContent, useContentTags } from '../hooks/useContent'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useContent } from '../hooks/useContent'
|
||||
import { formatDate } from '../utils/formatDate'
|
||||
import { decodeHtml } from '../utils/decodeHtml'
|
||||
|
||||
export default function Kniepunkt() {
|
||||
const [tag, setTag] = useState(null)
|
||||
const [page, setPage] = useState(1)
|
||||
const { items, totalPages, hasNext, hasPrev } = useContent('kniepunkt', { tag, page })
|
||||
const tags = useContentTags('kniepunkt')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const { items } = useContent('kniepunkt', { pageSize: 1000 })
|
||||
|
||||
const filteredItems = items.filter(item => {
|
||||
if (!searchQuery) return true
|
||||
const query = searchQuery.toLowerCase()
|
||||
return (
|
||||
(item.title && item.title.toLowerCase().includes(query)) ||
|
||||
(item.summary && item.summary.toLowerCase().includes(query)) ||
|
||||
(item.body && item.body.toLowerCase().includes(query))
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<main style={{ paddingTop: '120px', minHeight: '100vh' }}>
|
||||
@@ -17,33 +27,48 @@ export default function Kniepunkt() {
|
||||
<p className="section-subtitle">Wöchentliche KI-Kolumne. Nachrichten und Absurditäten rund um KI.</p>
|
||||
</div>
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', justifyContent: 'center', marginBottom: '40px' }}>
|
||||
<button onClick={() => setTag(null)} className={`secondary-button ${!tag ? 'active' : ''}`} style={{ padding: '8px 16px', fontSize: '0.85rem' }}>Alle</button>
|
||||
{tags.map(t => (
|
||||
<button key={t} onClick={() => { setTag(t); setPage(1) }} className={`secondary-button ${tag === t ? 'active' : ''}`} style={{ padding: '8px 16px', fontSize: '0.85rem' }}>{t}</button>
|
||||
))}
|
||||
<div style={{ maxWidth: '700px', margin: '0 auto 40px auto', position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-secondary)' }}>
|
||||
<Search size={20} />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Kniepunkte durchsuchen..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '16px 80px 16px 48px',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
background: 'var(--glass-bg)',
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: '1rem',
|
||||
outline: 'none'
|
||||
}}
|
||||
/>
|
||||
<div style={{ position: 'absolute', right: '16px', top: '50%', transform: 'translateY(-50%)', color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
|
||||
{filteredItems?.length || 0} / {items?.length || 0}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '700px', margin: '0 auto' }}>
|
||||
{items.map(issue => (
|
||||
<Link to={`/kniepunkt/${issue.slug}`} key={issue.slug} className="glass-panel" style={{ padding: '24px', display: 'block', transition: 'transform 0.3s ease' }}>
|
||||
<h3 style={{ marginBottom: '8px' }}>{issue.title}</h3>
|
||||
<p style={{ color: 'var(--text-secondary)', margin: 0, fontSize: '0.9rem' }}>{formatDate(issue.date)}</p>
|
||||
{issue.summary && <p style={{ color: 'var(--text-secondary)', margin: '8px 0 0', fontSize: '0.95rem' }}>{issue.summary}</p>}
|
||||
</Link>
|
||||
))}
|
||||
{items.length === 0 && <p style={{ textAlign: 'center', color: 'var(--text-secondary)' }}>Noch keine Ausgaben vorhanden.</p>}
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '16px', marginTop: '40px' }}>
|
||||
{hasPrev && <button onClick={() => setPage(p => p - 1)} className="secondary-button">Zurück</button>}
|
||||
<span style={{ color: 'var(--text-secondary)', alignSelf: 'center' }}>Seite {page} / {totalPages}</span>
|
||||
{hasNext && <button onClick={() => setPage(p => p + 1)} className="secondary-button">Weiter</button>}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '700px', margin: '0 auto' }}>
|
||||
{filteredItems.map(issue => (
|
||||
<Link to={`/kniepunkt/${issue.slug}`} key={issue.slug} className="glass-panel" style={{ padding: '0', display: 'flex', flexDirection: 'column', transition: 'transform 0.3s ease', overflow: 'hidden' }}>
|
||||
{issue.image && (
|
||||
<div style={{ width: '100%', height: '240px', overflow: 'hidden' }}>
|
||||
<img src={issue.image} alt={decodeHtml(issue.title)} style={{ width: '100%', height: '100%', objectFit: 'cover', borderBottom: '1px solid rgba(255,255,255,0.1)' }} />
|
||||
</div>
|
||||
)}
|
||||
<div style={{ padding: '24px' }}>
|
||||
<h3 style={{ marginBottom: '8px' }}>{decodeHtml(issue.title)}</h3>
|
||||
<p style={{ color: 'var(--text-secondary)', margin: 0, fontSize: '0.9rem' }}>{formatDate(issue.date)}</p>
|
||||
{issue.summary && <p style={{ color: 'var(--text-secondary)', margin: '8px 0 0', fontSize: '0.95rem' }}>{decodeHtml(issue.summary)}</p>}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{filteredItems.length === 0 && <p style={{ textAlign: 'center', color: 'var(--text-secondary)' }}>Keine Kniepunkte für diese Suche gefunden.</p>}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
||||
@@ -25,6 +25,11 @@ export default function KniepunktIssue() {
|
||||
<Link to="/kniepunkt" style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginBottom: '24px', display: 'inline-block' }}>← Alle Ausgaben</Link>
|
||||
<h1 style={{ marginBottom: '12px' }}>{issue.title}</h1>
|
||||
<p style={{ color: 'var(--text-secondary)', marginBottom: '40px' }}>{formatDate(issue.date)}</p>
|
||||
{issue.image && (
|
||||
<div style={{ marginBottom: '40px', borderRadius: '12px', overflow: 'hidden', boxShadow: '0 8px 24px rgba(0,0,0,0.1)' }}>
|
||||
<img src={issue.image} alt={issue.title} style={{ width: '100%', height: 'auto', display: 'block' }} />
|
||||
</div>
|
||||
)}
|
||||
<div className="content-body" dangerouslySetInnerHTML={{ __html: issue.body }} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user