192 lines
7.8 KiB
TypeScript
192 lines
7.8 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { MediumCard } from '../src/components/MediumCard';
|
|
import { SearchBar } from '../src/components/SearchBar';
|
|
import { Tag } from '../src/components/Tag';
|
|
import { blogPosts } from '../src/data/blogPosts';
|
|
|
|
export default function HomePage() {
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [filteredPosts, setFilteredPosts] = useState(blogPosts);
|
|
|
|
// Sort posts by date
|
|
const allPosts = [...blogPosts].sort((a, b) =>
|
|
new Date(b.date).getTime() - new Date(a.date).getTime()
|
|
);
|
|
|
|
// Get unique tags
|
|
const allTags = Array.from(new Set(allPosts.flatMap(post => post.tags || [])));
|
|
|
|
useEffect(() => {
|
|
const query = searchQuery.toLowerCase().trim();
|
|
if (query.startsWith('#')) {
|
|
const tag = query.slice(1);
|
|
setFilteredPosts(allPosts.filter(post =>
|
|
post.tags?.some(t => t.toLowerCase() === tag.toLowerCase())
|
|
));
|
|
} else {
|
|
setFilteredPosts(allPosts.filter(post => {
|
|
const title = post.title.toLowerCase();
|
|
const description = post.description.toLowerCase();
|
|
const tags = (post.tags || []).join(' ').toLowerCase();
|
|
return title.includes(query) || description.includes(query) || tags.includes(query);
|
|
}));
|
|
}
|
|
}, [searchQuery]);
|
|
|
|
const filterByTag = (tag: string) => {
|
|
setSearchQuery(`#${tag}`);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Clean Hero Section */}
|
|
<section className="pt-10 pb-8 md:pt-12 md:pb-10 relative overflow-hidden">
|
|
{/* Animated Background */}
|
|
<div className="absolute inset-0 bg-gradient-to-br from-white via-slate-50/30 to-blue-50/20 animate-gradient-shift"></div>
|
|
|
|
{/* Morphing Blob */}
|
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
<div className="w-48 h-48 bg-gradient-to-br from-blue-200/15 via-purple-200/10 to-indigo-200/15 animate-morph"></div>
|
|
</div>
|
|
|
|
{/* Animated Drawing Paths */}
|
|
<svg className="absolute inset-0 w-full h-full pointer-events-none" viewBox="0 0 100 100">
|
|
<path d="M10,50 Q50,10 90,50 T90,90" stroke="rgba(59,130,246,0.1)" strokeWidth="0.5" fill="none" className="animate-draw"></path>
|
|
<path d="M10,70 Q50,30 90,70" stroke="rgba(147,51,234,0.1)" strokeWidth="0.5" fill="none" className="animate-draw-delay"></path>
|
|
<path d="M20,20 Q50,80 80,20" stroke="rgba(16,185,129,0.1)" strokeWidth="0.5" fill="none" className="animate-draw-reverse"></path>
|
|
</svg>
|
|
|
|
{/* Floating Shapes */}
|
|
<div className="absolute top-10 left-10 w-20 h-20 bg-blue-100/20 rounded-full animate-float-1"></div>
|
|
<div className="absolute top-20 right-20 w-16 h-16 bg-indigo-100/20 rotate-45 animate-float-2"></div>
|
|
<div className="absolute bottom-20 left-1/4 w-12 h-12 bg-purple-100/20 rounded-full animate-float-3"></div>
|
|
<div className="absolute bottom-10 right-1/3 w-24 h-24 bg-cyan-100/20 animate-float-4"></div>
|
|
|
|
<div className="max-w-3xl mx-auto px-6 relative z-10">
|
|
<div className="text-center animate-fade-in">
|
|
<h1 className="text-3xl md:text-4xl font-serif font-light text-slate-900 tracking-tight mb-3">
|
|
Marc Mintel
|
|
</h1>
|
|
<p className="text-base md:text-lg text-slate-600 leading-relaxed font-serif italic">
|
|
"A public notebook of things I figured out, mistakes I made, and tools I tested."
|
|
</p>
|
|
<div className="flex items-center justify-center gap-3 text-[13px] text-slate-500 font-sans mt-3">
|
|
<span className="inline-flex items-center gap-1">
|
|
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"><path d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 12a4 4 0 110-8 4 4 0 010 8z"/></svg>
|
|
Vulkaneifel, Germany
|
|
</span>
|
|
<span aria-hidden="true">•</span>
|
|
<span>Digital problem solver</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Search */}
|
|
<section className="mb-8 mt-8">
|
|
<div id="search-container">
|
|
<SearchBar value={searchQuery} onChange={setSearchQuery} />
|
|
</div>
|
|
</section>
|
|
|
|
{/* Topics */}
|
|
{allTags.length > 0 && (
|
|
<section className="mb-8">
|
|
<h2 className="text-lg font-semibold text-slate-800 mb-4">Topics</h2>
|
|
<div className="tag-cloud flex flex-wrap gap-2">
|
|
{allTags.map((tag, index) => (
|
|
<button
|
|
key={tag}
|
|
onClick={() => filterByTag(tag)}
|
|
className="inline-block"
|
|
>
|
|
<Tag tag={tag} index={index} />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* All Posts */}
|
|
<section>
|
|
<div id="posts-container" className="not-prose grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-4">
|
|
{filteredPosts.length === 0 ? (
|
|
<div className="empty-state col-span-full">
|
|
<p>No posts found matching your criteria.</p>
|
|
</div>
|
|
) : (
|
|
filteredPosts.map(post => (
|
|
<MediumCard key={post.slug} post={post} />
|
|
))
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
<style jsx global>{`
|
|
@keyframes gradient-shift {
|
|
0%, 100% { background-position: 0% 50%; }
|
|
50% { background-position: 100% 50%; }
|
|
}
|
|
.animate-gradient-shift {
|
|
background-size: 200% 200%;
|
|
animation: gradient-shift 20s ease infinite;
|
|
}
|
|
@keyframes morph {
|
|
0%, 100% { border-radius: 50%; transform: scale(1) rotate(0deg); }
|
|
25% { border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; transform: scale(1.1) rotate(90deg); }
|
|
50% { border-radius: 20% 80% 20% 80% / 80% 20% 80% 20%; transform: scale(0.9) rotate(180deg); }
|
|
75% { border-radius: 70% 30% 50% 50% / 50% 70% 30% 50%; transform: scale(1.05) rotate(270deg); }
|
|
}
|
|
.animate-morph {
|
|
animation: morph 25s ease-in-out infinite;
|
|
}
|
|
@keyframes draw {
|
|
to { stroke-dashoffset: 0; }
|
|
}
|
|
.animate-draw {
|
|
stroke-dasharray: 200;
|
|
stroke-dashoffset: 200;
|
|
animation: draw 8s ease-in-out infinite alternate;
|
|
}
|
|
.animate-draw-delay {
|
|
stroke-dasharray: 200;
|
|
stroke-dashoffset: 200;
|
|
animation: draw 8s ease-in-out infinite alternate 4s;
|
|
}
|
|
.animate-draw-reverse {
|
|
stroke-dasharray: 200;
|
|
stroke-dashoffset: 200;
|
|
animation: draw 8s ease-in-out infinite alternate-reverse 2s;
|
|
}
|
|
@keyframes float-1 {
|
|
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
|
50% { transform: translateY(-20px) rotate(180deg); }
|
|
}
|
|
.animate-float-1 { animation: float-1 15s ease-in-out infinite; }
|
|
@keyframes float-2 {
|
|
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
|
50% { transform: translateY(-15px) rotate(90deg); }
|
|
}
|
|
.animate-float-2 { animation: float-2 18s ease-in-out infinite; }
|
|
@keyframes float-3 {
|
|
0%, 100% { transform: translateY(0px) scale(1); }
|
|
50% { transform: translateY(-10px) scale(1.1); }
|
|
}
|
|
.animate-float-3 { animation: float-3 12s ease-in-out infinite; }
|
|
@keyframes float-4 {
|
|
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
|
50% { transform: translateY(-25px) rotate(-90deg); }
|
|
}
|
|
.animate-float-4 { animation: float-4 20s ease-in-out infinite; }
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.animate-fade-in { animation: fadeIn 0.6s ease-out both; }
|
|
`}</style>
|
|
</>
|
|
);
|
|
}
|