blog design
This commit is contained in:
@@ -95,19 +95,19 @@ export const BlogPostClient: React.FC<BlogPostClientProps> = ({ readingTime, tit
|
||||
<svg className="w-5 h-5 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
<span className="font-medium">Back</span>
|
||||
<span className="font-bold uppercase tracking-widest text-xs">Zurück</span>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-slate-500 font-sans hidden sm:inline">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-400 hidden sm:inline">
|
||||
{readingTime} min read
|
||||
</span>
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="share-button-top flex items-center gap-1.5 px-3 py-1.5 bg-slate-100 hover:bg-slate-200 border border-slate-200 hover:border-slate-300 rounded-full transition-all duration-200"
|
||||
className="share-button-top flex items-center gap-1.5 px-3 py-1.5 bg-slate-50 hover:bg-slate-900 hover:text-white border border-slate-100 hover:border-slate-900 transition-all duration-200"
|
||||
aria-label="Share this post"
|
||||
>
|
||||
<svg className="w-4 h-4 text-slate-600 group-hover:text-slate-900 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/>
|
||||
</svg>
|
||||
</button>
|
||||
@@ -115,15 +115,15 @@ export const BlogPostClient: React.FC<BlogPostClientProps> = ({ readingTime, tit
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="mt-16 pt-8 border-t border-slate-200 text-center">
|
||||
<div className="mt-16 pt-8 border-t border-slate-50 text-center">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-white border-2 border-slate-200 text-slate-700 rounded-full hover:border-slate-900 hover:text-slate-900 hover:shadow-md transition-all duration-300 font-medium group"
|
||||
className="inline-flex items-center gap-2 px-8 py-4 bg-white border-2 border-slate-900 text-slate-900 hover:bg-slate-900 hover:text-white transition-all duration-300 font-bold uppercase tracking-widest text-xs group"
|
||||
>
|
||||
<svg className="w-5 h-5 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Back to all posts
|
||||
Alle Beiträge
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="border-t border-slate-200 py-12 mt-20 bg-gradient-to-b from-white to-slate-50">
|
||||
<div className="max-w-3xl mx-auto px-6">
|
||||
{/* Main footer content - all centered */}
|
||||
<div className="flex flex-col items-center justify-center gap-4 mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-6 h-6 bg-slate-900 rounded-full flex items-center justify-center">
|
||||
<span className="text-white text-xs font-bold">M</span>
|
||||
<footer className="py-24 mt-48 border-t border-slate-100">
|
||||
<div className="narrow-container">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-end">
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-slate-900 flex items-center justify-center">
|
||||
<span className="text-white text-sm font-bold">M</span>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-slate-900 tracking-tighter">Marc Mintel</span>
|
||||
</div>
|
||||
<span className="text-sm text-slate-900 font-medium">Marc Mintel</span>
|
||||
<p className="text-2xl text-slate-400 font-serif italic leading-tight max-w-xs">
|
||||
Digitale Systeme ohne Overhead.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-slate-600 font-serif italic text-center max-w-md">
|
||||
Write things down. Don't forget. Maybe help someone.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="text-slate-500 font-sans">© {currentYear}</span>
|
||||
<div className="flex flex-col md:items-end gap-4 text-sm font-mono text-slate-300 uppercase tracking-widest">
|
||||
<span>© {currentYear}</span>
|
||||
<div className="flex gap-8">
|
||||
<a href="mailto:marc@mintel.me" className="hover:text-slate-900 transition-colors no-underline">Email</a>
|
||||
<a href="https://github.com/marcmintel" className="hover:text-slate-900 transition-colors no-underline">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtle tagline */}
|
||||
<div className="text-center pt-6 border-t border-slate-200/50">
|
||||
<p className="text-xs text-slate-400 font-sans">
|
||||
A public notebook of digital problem solving
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -11,12 +11,12 @@ export const Header: React.FC = () => {
|
||||
|
||||
return (
|
||||
<header className="bg-white/80 backdrop-blur-md sticky top-0 z-50">
|
||||
<div className="max-w-4xl mx-auto px-6 py-8 flex items-center justify-between">
|
||||
<div className="narrow-container py-8 flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-3 group">
|
||||
<div className="w-10 h-10 bg-slate-900 rounded-sm flex items-center justify-center group-hover:bg-slate-700 transition-colors">
|
||||
<div className="w-10 h-10 bg-slate-900 flex items-center justify-center group-hover:bg-slate-700 transition-colors">
|
||||
<span className="text-white text-lg font-bold">M</span>
|
||||
</div>
|
||||
<span className="text-slate-900 font-bold tracking-tight text-xl">Marc Mintel</span>
|
||||
<span className="text-slate-900 font-bold tracking-tighter text-2xl">Marc Mintel</span>
|
||||
</Link>
|
||||
|
||||
<nav className="flex items-center gap-8">
|
||||
@@ -38,7 +38,7 @@ export const Header: React.FC = () => {
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="text-[10px] font-bold uppercase tracking-[0.2em] text-slate-900 bg-white border border-slate-200 rounded-full px-5 py-2.5 hover:border-slate-900 hover:shadow-sm transition-all duration-300"
|
||||
className="text-[10px] font-bold uppercase tracking-[0.2em] text-slate-900 border-2 border-slate-900 px-5 py-2.5 hover:bg-slate-900 hover:text-white transition-all duration-300"
|
||||
>
|
||||
Anfrage
|
||||
</Link>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface Post {
|
||||
@@ -14,7 +14,7 @@ interface MediumCardProps {
|
||||
}
|
||||
|
||||
export const MediumCard: React.FC<MediumCardProps> = ({ post }) => {
|
||||
const { title, description, date, slug, tags = [] } = post;
|
||||
const { title, description, date, slug } = post;
|
||||
|
||||
const formattedDate = new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
@@ -22,43 +22,24 @@ export const MediumCard: React.FC<MediumCardProps> = ({ post }) => {
|
||||
year: 'numeric',
|
||||
});
|
||||
|
||||
const wordCount = description.split(/\s+/).length;
|
||||
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
|
||||
const markerSeed = Math.abs(title.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0)) % 7;
|
||||
|
||||
return (
|
||||
<Link href={`/blog/${slug}`} className="post-link block group">
|
||||
<article className="post-card bg-white border border-slate-200/80 rounded-lg px-4 py-3 transition-all duration-200 group-hover:border-slate-300 group-hover:shadow-sm">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<h3 className="m-0 text-sm font-semibold leading-snug tracking-tight">
|
||||
<span
|
||||
className="relative inline-block text-slate-900 marker-title"
|
||||
style={{ '--marker-seed': markerSeed } as React.CSSProperties}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</h3>
|
||||
<time className="text-[11px] text-slate-500 tabular-nums whitespace-nowrap leading-none pt-0.5">
|
||||
{formattedDate}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
<p className="post-excerpt mt-2 mb-0 text-[13px] leading-relaxed text-slate-600 line-clamp-3">
|
||||
<Link href={`/blog/${slug}`} className="group block">
|
||||
<article className="space-y-3 py-8 border-b border-slate-50 group-hover:border-slate-900 transition-colors">
|
||||
<time className="text-[10px] font-mono text-slate-300 uppercase tracking-widest group-hover:text-slate-900 transition-colors">
|
||||
{formattedDate}
|
||||
</time>
|
||||
|
||||
<h3 className="text-3xl font-bold text-slate-900 tracking-tighter group-hover:text-slate-900 transition-colors">
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
<p className="text-xl text-slate-500 font-serif italic leading-tight line-clamp-2">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
<div className="mt-3 flex items-center justify-between gap-3">
|
||||
<span className="text-[11px] text-slate-500 tabular-nums leading-none">{readingTime} min</span>
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div className="flex flex-wrap items-center justify-end gap-1">
|
||||
{tags.slice(0, 3).map((tag: string) => (
|
||||
<span key={tag} className="inline-flex items-center rounded-full bg-slate-100/80 border border-slate-200/60 px-2 py-0.5 text-[11px] text-slate-700 leading-none">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4 flex items-center gap-4 text-slate-900 font-bold text-sm group/link">
|
||||
Lesen
|
||||
<div className="w-8 h-px bg-slate-900 group-hover:w-12 transition-all"></div>
|
||||
</div>
|
||||
</article>
|
||||
</Link>
|
||||
|
||||
53
src/components/PageHeader.tsx
Normal file
53
src/components/PageHeader.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Reveal } from './Reveal';
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: React.ReactNode;
|
||||
description?: string;
|
||||
backLink?: {
|
||||
href: string;
|
||||
label: string;
|
||||
};
|
||||
backgroundSymbol?: string;
|
||||
}
|
||||
|
||||
export const PageHeader: React.FC<PageHeaderProps> = ({
|
||||
title,
|
||||
description,
|
||||
backLink,
|
||||
backgroundSymbol
|
||||
}) => {
|
||||
return (
|
||||
<section className="narrow-container relative">
|
||||
{backgroundSymbol && (
|
||||
<div className="absolute -left-24 -top-24 text-[20rem] font-bold text-slate-50 select-none -z-10">
|
||||
{backgroundSymbol}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{backLink && (
|
||||
<Link href={backLink.href} className="inline-flex items-center gap-2 text-slate-300 hover:text-slate-900 mb-16 transition-colors font-mono text-xs uppercase tracking-[0.3em]">
|
||||
<ArrowLeft className="w-3 h-3" /> {backLink.label}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<div className="space-y-16">
|
||||
<Reveal>
|
||||
<h1 className="text-6xl md:text-8xl font-bold text-slate-900 tracking-tighter leading-[0.9]">
|
||||
{title}
|
||||
</h1>
|
||||
</Reveal>
|
||||
|
||||
{description && (
|
||||
<Reveal delay={0.2}>
|
||||
<p className="text-2xl md:text-3xl text-slate-500 font-serif italic leading-tight max-w-2xl">
|
||||
{description}
|
||||
</p>
|
||||
</Reveal>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
interface SearchBarProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
size?: 'small' | 'large';
|
||||
}
|
||||
|
||||
export const SearchBar: React.FC<SearchBarProps> = ({ value: propValue, onChange }) => {
|
||||
export const SearchBar: React.FC<SearchBarProps> = ({ value: propValue, onChange, size = 'small' }) => {
|
||||
const [internalValue, setInternalValue] = useState('');
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
@@ -41,39 +43,35 @@ export const SearchBar: React.FC<SearchBarProps> = ({ value: propValue, onChange
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-2xl mx-auto">
|
||||
<div className="relative w-full">
|
||||
<div className="relative flex items-center">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
value={value}
|
||||
className={`w-full px-3 py-2 text-[14px] border border-slate-200 rounded-md bg-transparent transition-colors font-sans focus:outline-none ${
|
||||
isFocused ? 'bg-white border-slate-300' : 'hover:border-slate-300'
|
||||
}`}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
aria-label="Search blog posts"
|
||||
/>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="Suchen..."
|
||||
value={value}
|
||||
className={`w-full px-0 py-2 font-bold text-slate-900 bg-transparent border-b-2 transition-all focus:outline-none placeholder:text-slate-100 ${
|
||||
size === 'large' ? 'text-2xl md:text-4xl py-4 border-b-4' : 'text-lg'
|
||||
} ${
|
||||
isFocused ? 'border-slate-900' : 'border-slate-100'
|
||||
}`}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
aria-label="Search blog posts"
|
||||
/>
|
||||
|
||||
{value && (
|
||||
<button
|
||||
onClick={clearSearch}
|
||||
className="absolute right-1.5 top-1/2 -translate-y-1/2 h-7 px-2 inline-flex items-center justify-center rounded text-[12px] text-slate-500 hover:text-slate-700 hover:bg-slate-100 transition-colors"
|
||||
aria-label="Clear search"
|
||||
type="button"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex items-center gap-2 ml-3 text-xs text-slate-400 font-sans">
|
||||
<kbd className="bg-slate-100 px-2 py-1 rounded border border-slate-200">ESC</kbd>
|
||||
</div>
|
||||
{value && (
|
||||
<button
|
||||
onClick={clearSearch}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 text-[10px] font-bold uppercase tracking-widest text-slate-400 hover:text-slate-900 transition-colors"
|
||||
aria-label="Clear search"
|
||||
type="button"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
36
src/components/Section.tsx
Normal file
36
src/components/Section.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from 'react';
|
||||
import { Reveal } from './Reveal';
|
||||
|
||||
interface SectionProps {
|
||||
number?: string;
|
||||
title?: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
numberPosition?: 'left' | 'right';
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const Section: React.FC<SectionProps> = ({
|
||||
number,
|
||||
title,
|
||||
children,
|
||||
className = "",
|
||||
numberPosition = 'left',
|
||||
delay = 0
|
||||
}) => {
|
||||
return (
|
||||
<section className={`narrow-container relative ${className}`}>
|
||||
{number && (
|
||||
<div className={`absolute ${numberPosition === 'left' ? '-left-24' : '-right-24'} -top-12 text-[15rem] font-bold text-slate-50 select-none -z-10`}>
|
||||
{number}
|
||||
</div>
|
||||
)}
|
||||
{title && (
|
||||
<Reveal delay={delay}>
|
||||
<h2 className="text-sm font-bold uppercase tracking-[0.3em] text-slate-400 mb-24">{title}</h2>
|
||||
</Reveal>
|
||||
)}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,4 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface TagProps {
|
||||
@@ -9,25 +7,13 @@ interface TagProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const getColorClass = (tag: string) => {
|
||||
const tagLower = tag.toLowerCase();
|
||||
if (tagLower.includes('meta') || tagLower.includes('learning')) return 'highlighter-yellow';
|
||||
if (tagLower.includes('debug') || tagLower.includes('tools')) return 'highlighter-pink';
|
||||
if (tagLower.includes('ai') || tagLower.includes('automation')) return 'highlighter-blue';
|
||||
if (tagLower.includes('script') || tagLower.includes('code')) return 'highlighter-green';
|
||||
return 'highlighter-yellow'; // default
|
||||
};
|
||||
|
||||
export const Tag: React.FC<TagProps> = ({ tag, index, className = '' }) => {
|
||||
const colorClass = getColorClass(tag);
|
||||
|
||||
export const Tag: React.FC<TagProps> = ({ tag, className = '' }) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/tags/${tag}`}
|
||||
className={`highlighter-tag ${colorClass} ${className} inline-block text-xs font-bold px-2.5 py-1 rounded cursor-pointer transition-all duration-200 relative overflow-hidden group`}
|
||||
style={{ '--tag-index': index } as React.CSSProperties}
|
||||
className={`inline-block text-[10px] font-bold uppercase tracking-[0.2em] text-slate-400 border border-slate-100 px-3 py-1.5 hover:border-slate-900 hover:text-slate-900 transition-all duration-300 ${className}`}
|
||||
>
|
||||
<span className="relative z-10">{tag}</span>
|
||||
{tag}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user