This commit is contained in:
2026-01-13 00:00:22 +01:00
parent 38d0e7e0a0
commit 19081ec682
23 changed files with 5023 additions and 521 deletions

View File

@@ -11,16 +11,90 @@ export const Blockquote: React.FC<BlockquoteProps> = ({ children, className = ''
</blockquote>
);
export const CodeBlock: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '' }) => (
<pre className={`bg-slate-900 text-slate-100 p-4 rounded-lg overflow-x-auto mb-4 ${className}`}>
<code className="font-mono text-sm">
{children}
</code>
</pre>
);
interface CodeBlockProps {
children: string;
language?: string;
showLineNumbers?: boolean;
className?: string;
}
// Simple syntax highlighting for common languages
const highlightCode = (code: string, language: string): string => {
code = code.trim();
const patterns: Record<string, RegExp[]> = {
comment: [/#[^\n]*/g, /\/\/[^\n]*/g, /\/\*[\s\S]*?\*\//g],
string: [/["'`][^"'`]*["'`]/g],
number: [/\b\d+\b/g],
keyword: [
/\b(def|return|if|else|for|while|import|from|class|try|except|with|as|lambda|yield|async|await|pass|break|continue)\b/g,
/\b(function|const|let|var|if|else|for|while|return|import|export|class|new|try|catch|finally)\b/g,
/\b(package|import|class|public|private|protected|static|void|return|if|else|for|while|try|catch)\b/g
],
function: [/\b[a-zA-Z_]\w*(?=\s*\()/g],
operator: [/[\+\-\*\/=<>!&|]+/g],
punctuation: [/[\[\]{}(),;.:]/g],
tag: [/<\/?[a-zA-Z][^>]*>/g],
attr: [/\b[a-zA-Z-]+(?=\=)/g],
attrValue: [/="[^"]*"/g],
};
let highlighted = code;
const order = ['comment', 'string', 'number', 'keyword', 'function', 'operator', 'punctuation', 'tag', 'attr', 'attrValue'];
order.forEach(type => {
patterns[type].forEach(pattern => {
highlighted = highlighted.replace(pattern, match => {
return `<span class="token ${type}">${match}</span>`;
});
});
});
return highlighted;
};
export const CodeBlock: React.FC<CodeBlockProps> = ({
children,
language = 'text',
showLineNumbers = false,
className = ''
}) => {
const code = (typeof children === 'string' ? children : String(children)).trim();
const highlighted = language !== 'text' ? highlightCode(code, language) : code;
const lines = code.split('\n');
return (
<div className="relative my-6">
{language !== 'text' && (
<div className="absolute top-2 right-2 text-xs bg-slate-700 text-slate-300 px-2 py-1 rounded font-sans">
{language}
</div>
)}
<pre
className={`bg-slate-900 text-slate-100 p-5 rounded-lg overflow-x-auto border border-slate-700 font-mono text-sm leading-relaxed ${className} ${showLineNumbers ? 'pl-12' : ''}`}
>
{showLineNumbers ? (
<div className="relative">
<div className="absolute left-0 top-0 bottom-0 w-8 text-right text-slate-600 select-none pr-3 border-r border-slate-700">
{lines.map((_, i) => (
<div key={i} className="leading-relaxed">{i + 1}</div>
))}
</div>
<div className="pl-10">
<code dangerouslySetInnerHTML={{ __html: highlighted }} />
</div>
</div>
) : (
<code dangerouslySetInnerHTML={{ __html: highlighted }} />
)}
</pre>
</div>
);
};
export const InlineCode: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '' }) => (
<code className={`bg-slate-200 text-slate-900 px-1 py-0.5 rounded font-mono text-sm ${className}`}>
<code className={`bg-slate-100 text-pink-600 px-1.5 py-0.5 rounded font-mono text-sm border border-pink-200 ${className}`}>
{children}
</code>
);

View File

@@ -1,48 +0,0 @@
import React from 'react';
interface BlogPostCardProps {
title: string;
description: string;
date: string;
slug: string;
tags?: string[];
}
export const BlogPostCard: React.FC<BlogPostCardProps> = ({
title,
description,
date,
slug,
tags = []
}) => {
const formattedDate = new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
return (
<article className="border border-slate-200 rounded-lg p-6 hover:shadow-lg transition-shadow">
<a href={`/blog/${slug}`} className="block">
<h2 className="text-2xl font-bold text-slate-900 mb-2 hover:text-slate-700">
{title}
</h2>
<p className="text-slate-600 mb-3 leading-relaxed">
{description}
</p>
<div className="flex items-center justify-between text-sm text-slate-500">
<time dateTime={date}>{formattedDate}</time>
{tags.length > 0 && (
<div className="flex gap-2">
{tags.map(tag => (
<span key={tag} className="bg-slate-100 text-slate-700 px-2 py-1 rounded">
{tag}
</span>
))}
</div>
)}
</div>
</a>
</article>
);
};

View File

@@ -1,23 +0,0 @@
import React from 'react';
interface ContainerProps {
children: React.ReactNode;
className?: string;
maxWidth?: string;
}
export const Container: React.FC<ContainerProps> = ({
children,
className = '',
maxWidth = 'max-w-4xl'
}) => (
<div className={`mx-auto px-4 ${maxWidth} ${className}`}>
{children}
</div>
);
export const ArticleContainer: React.FC<ContainerProps> = ({ children, className = '' }) => (
<Container maxWidth="max-w-3xl" className={`py-8 ${className}`}>
{children}
</Container>
);

36
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,36 @@
import 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>
</div>
<span className="text-sm text-slate-900 font-medium">Marc Mintel</span>
</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>
</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>
);
};

54
src/components/Hero.tsx Normal file
View File

@@ -0,0 +1,54 @@
import { BookOpen, Code2, Terminal, Wrench } from 'lucide-react';
import React from 'react';
export const Hero: React.FC = () => {
return (
<section className="relative bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white py-20 md:py-28 overflow-hidden">
{/* Background pattern */}
<div className="absolute inset-0 opacity-10">
<div className="absolute inset-0" style={{
backgroundImage: 'radial-gradient(circle at 1px 1px, white 1px, transparent 0)',
backgroundSize: '40px 40px'
}} />
</div>
<div className="container relative z-10">
<div className="max-w-4xl mx-auto text-center">
{/* Main heading */}
<h1 className="text-4xl md:text-6xl font-bold mb-6 animate-slide-up">
Digital Problem Solver
</h1>
<p className="text-xl md:text-2xl text-slate-300 mb-8 leading-relaxed animate-fade-in">
I work on Digital problems and build tools, scripts, and systems to solve them.
</p>
{/* Quick stats or focus areas */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-10 animate-fade-in">
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-4">
<Code2 className="mx-auto mb-2 text-primary-400" size={24} />
<div className="text-sm font-mono text-slate-300">Code</div>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-4">
<Wrench className="mx-auto mb-2 text-primary-400" size={24} />
<div className="text-sm font-mono text-slate-300">Tools</div>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-4">
<Terminal className="mx-auto mb-2 text-primary-400" size={24} />
<div className="text-sm font-mono text-slate-300">Automation</div>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-4">
<BookOpen className="mx-auto mb-2 text-primary-400" size={24} />
<div className="text-sm font-mono text-slate-300">Learning</div>
</div>
</div>
{/* Topics */}
<div className="mt-8 text-sm text-slate-400 animate-fade-in">
<span className="font-semibold text-slate-300">Topics:</span> Vibe coding with AI Debugging Mac tools Automation Small scripts Learning notes FOSS
</div>
</div>
</div>
</section>
);
};

View File

@@ -0,0 +1,130 @@
---
import Tag from './Tag.astro';
interface Props {
post: {
title: string;
description: string;
date: string;
slug: string;
tags?: string[];
};
}
const { post } = Astro.props;
const { title, description, date, slug, tags = [] } = post;
const formattedDate = new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
// Calculate reading time
const wordCount = description.split(/\s+/).length;
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
---
<article class="post-card bg-white border border-slate-200 rounded-xl p-5 hover:border-blue-300 hover:shadow-md transition-all duration-200">
<div class="flex items-start justify-between mb-3">
<h3 class="text-lg font-serif font-medium text-slate-900 hover:text-blue-600 transition-colors leading-tight flex-1 mr-3">
{title}
</h3>
<span class="text-xs text-slate-500 font-sans whitespace-nowrap flex-shrink-0">
{formattedDate}
</span>
</div>
<p class="text-slate-600 leading-relaxed font-serif text-sm mb-3 line-clamp-3">
{description}
</p>
<div class="flex items-center justify-between">
<span class="text-xs text-slate-500 font-sans">
{readingTime} min
</span>
{tags.length > 0 && (
<div class="flex gap-1">
{tags.map((tag: string) => (
<span class="bg-slate-100 text-slate-700 px-2 py-0.5 rounded text-[10px] font-medium">
{tag}
</span>
))}
</div>
)}
</div>
</article>
<style>
/* Simple reveal animation */
@keyframes cardReveal {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.post-card {
animation: cardReveal 0.3s ease-out both;
}
/* Stagger for multiple cards */
.post-card:nth-child(1) { animation-delay: 0.05s; }
.post-card:nth-child(2) { animation-delay: 0.1s; }
.post-card:nth-child(3) { animation-delay: 0.15s; }
.post-card:nth-child(4) { animation-delay: 0.2s; }
.post-card:nth-child(5) { animation-delay: 0.25s; }
.post-card:nth-child(6) { animation-delay: 0.3s; }
/* Clean hover effects */
.post-card:hover {
transform: translateY(-1px);
}
/* Title underline on hover */
.post-card h3 {
position: relative;
display: inline-block;
}
.post-card h3::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 0;
height: 1px;
background: #3b82f6;
transition: width 0.2s ease;
}
.post-card:hover h3::after {
width: 100%;
}
/* Line clamp */
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Focus styles for accessibility */
.post-card:focus-within {
outline: 2px solid #3b82f6;
outline-offset: 2px;
transform: translateY(-1px);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
}
.post-card h3:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
border-radius: 2px;
}
</style>

View File

@@ -0,0 +1,141 @@
import React, { useState, useRef, useEffect } from 'react';
export const SearchBar: React.FC = () => {
const [query, setQuery] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const [isFocused, setIsFocused] = useState(false);
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setQuery(value);
// Trigger search functionality
if (typeof window !== 'undefined') {
const postsContainer = document.getElementById('posts-container');
const allPosts = postsContainer?.querySelectorAll('.post-card') as NodeListOf<HTMLElement>;
if (!allPosts) return;
const queryLower = value.toLowerCase().trim();
allPosts.forEach((post: HTMLElement) => {
const title = post.querySelector('h3')?.textContent?.toLowerCase() || '';
const description = post.querySelector('.post-excerpt')?.textContent?.toLowerCase() || '';
const tags = Array.from(post.querySelectorAll('.highlighter-tag'))
.map(tag => tag.textContent?.toLowerCase() || '')
.join(' ');
const searchableText = `${title} ${description} ${tags}`;
if (searchableText.includes(queryLower) || queryLower === '') {
post.style.display = 'block';
post.style.opacity = '1';
post.style.transform = 'scale(1)';
} else {
post.style.display = 'none';
post.style.opacity = '0';
post.style.transform = 'scale(0.95)';
}
});
}
};
const clearSearch = () => {
setQuery('');
if (inputRef.current) {
inputRef.current.value = '';
inputRef.current.focus();
}
// Reset all posts
if (typeof window !== 'undefined') {
const postsContainer = document.getElementById('posts-container');
const allPosts = postsContainer?.querySelectorAll('.post-card') as NodeListOf<HTMLElement>;
allPosts?.forEach((post: HTMLElement) => {
post.style.display = 'block';
post.style.opacity = '1';
post.style.transform = 'scale(1)';
});
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
clearSearch();
}
};
return (
<div className="relative w-full max-w-2xl mx-auto">
<div className={`relative flex items-center transition-all duration-300 ${
isFocused ? 'scale-[1.02]' : 'scale-100'
}`}>
{/* Search input wrapper */}
<div className="relative flex-1">
<input
ref={inputRef}
type="text"
placeholder="Search posts, topics, or tags..."
className={`w-full px-5 py-4 pl-12 text-base border-2 rounded-xl transition-all duration-300 font-sans focus:outline-none ${
isFocused
? 'border-blue-400 bg-white shadow-lg'
: 'border-slate-200 bg-white/90 hover:border-slate-300'
}`}
onInput={handleInput}
onKeyDown={handleKeyDown}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
aria-label="Search blog posts"
/>
{/* Search icon */}
<div className={`absolute left-4 top-1/2 -translate-y-1/2 transition-all duration-300 ${
isFocused ? 'text-blue-600 scale-110' : 'text-slate-400'
}`}>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
{/* Clear button */}
{query && (
<button
onClick={clearSearch}
className="absolute right-4 top-1/2 -translate-y-1/2 p-1.5 rounded-full hover:bg-slate-100 transition-colors duration-200 text-slate-500 hover:text-slate-700"
aria-label="Clear search"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
{/* Search hint */}
<div className="hidden md:flex items-center gap-2 ml-3 text-xs text-slate-500 font-sans">
<kbd className="bg-slate-100 px-2 py-1 rounded border border-slate-200">ESC</kbd>
<span>to clear</span>
</div>
</div>
{/* Active filter indicator */}
{query && (
<div className="mt-3 flex items-center justify-between text-sm">
<span className="text-slate-600">
Searching for: <span className="font-semibold text-blue-600">"{query}"</span>
</span>
<button
onClick={clearSearch}
className="text-slate-500 hover:text-blue-600 transition-colors duration-200 font-medium"
>
Show all posts
</button>
</div>
)}
{/* Results count (hidden by default, shown via JS) */}
<div id="search-results-count" className="mt-2 text-sm text-slate-500 font-sans hidden"></div>
</div>
);
};

125
src/components/Tag.astro Normal file
View File

@@ -0,0 +1,125 @@
---
interface Props {
tag: string;
index: number;
className?: string;
}
const { tag, index, className = '' } = Astro.props;
// Color mapping based on tag content for variety
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
};
const colorClass = getColorClass(tag);
---
<span class={`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`} data-tag={tag}>
<span class="relative z-10">{tag}</span>
<!-- Subtle underline animation -->
<span class="absolute bottom-0 left-0 w-0 h-0.5 bg-current transition-all duration-300 group-hover:w-full opacity-50"></span>
<!-- Micro-interaction: small dot that appears on hover -->
<span class="absolute top-1/2 left-1/2 w-0 h-0 bg-white/30 rounded-full transform -translate-x-1/2 -translate-y-1/2 transition-all duration-300 group-hover:w-1 group-hover:h-1"></span>
</span>
<style>
/* Enhanced hover effects */
.highlighter-tag {
transform: rotate(-1deg) translateY(0);
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.1);
}
.highlighter-tag:hover {
transform: rotate(-2deg) translateY(-2px) scale(1.05);
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.15);
}
/* Staggered entrance animation */
@keyframes tagPopIn {
from {
opacity: 0;
transform: rotate(-1deg) scale(0.8) translateY(5px);
}
to {
opacity: 1;
transform: rotate(-1deg) scale(1) translateY(0);
}
}
.highlighter-tag {
animation: tagPopIn 0.3s ease-out both;
animation-delay: calc(var(--tag-index, 0) * 0.05s);
}
/* Color variations with gradients */
.highlighter-yellow {
background: linear-gradient(135deg, rgba(255, 235, 59, 0.95) 0%, rgba(255, 213, 79, 0.95) 100%);
color: #3f2f00;
}
.highlighter-pink {
background: linear-gradient(135deg, rgba(255, 167, 209, 0.95) 0%, rgba(255, 122, 175, 0.95) 100%);
color: #3f0018;
}
.highlighter-green {
background: linear-gradient(135deg, rgba(129, 199, 132, 0.95) 0%, rgba(102, 187, 106, 0.95) 100%);
color: #002f0a;
}
.highlighter-blue {
background: linear-gradient(135deg, rgba(100, 181, 246, 0.95) 0%, rgba(66, 165, 245, 0.95) 100%);
color: #001f3f;
}
/* Hover glow effect */
.highlighter-tag:hover::before {
content: '';
position: absolute;
inset: -2px;
background: inherit;
filter: blur(8px);
opacity: 0.4;
z-index: -1;
border-radius: inherit;
}
/* Click effect */
.highlighter-tag:active {
transform: rotate(-1deg) translateY(0) scale(0.98);
transition: transform 0.1s ease;
}
/* Focus effect for accessibility */
.highlighter-tag:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
transform: rotate(-1deg) translateY(-2px) scale(1.05);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.3), 3px 3px 0 rgba(0, 0, 0, 0.15);
}
</style>
<script>
// Add click handler for tag filtering
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => {
const tags = document.querySelectorAll('.highlighter-tag');
tags.forEach(tagElement => {
tagElement.addEventListener('click', (e) => {
const tag = tagElement.getAttribute('data-tag');
if (tag && typeof (window as any).filterByTag === 'function') {
(window as any).filterByTag(tag);
}
});
});
});
}
</script>

View File

@@ -1,18 +1,14 @@
---
import '../styles/global.css';
import { Footer } from '../components/Footer';
import { Hero } from '../components/Hero';
interface Props {
title: string;
description?: string;
}
const { title, description = "Technical problem solver's blog - practical insights and learning notes" } = Astro.props;
// About info from context
const aboutInfo = {
name: "Marc Mintel",
role: "Technical problem solver",
email: "marc@mintel.me",
location: "Vulkaneifel, Germany"
};
---
<!DOCTYPE html>
@@ -20,81 +16,166 @@ const aboutInfo = {
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title} | {aboutInfo.name}</title>
<title>{title} | Marc Mintel</title>
<meta name="description" content={description} />
<meta name="generator" content={Astro.generator} />
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
</head>
<body class="bg-white text-slate-900 font-sans antialiased">
<div class="min-h-screen flex flex-col">
<!-- Header with About Info -->
<header class="bg-white border-b border-slate-200 sticky top-0 z-10">
<div class="max-w-4xl mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-xl font-bold text-slate-900">
<a href="/" class="hover:text-slate-700">{aboutInfo.name}</a>
</h1>
<p class="text-sm text-slate-600">{aboutInfo.role}</p>
</div>
<div class="text-right text-sm text-slate-600 hidden sm:block">
<p>{aboutInfo.email}</p>
<p>{aboutInfo.location}</p>
</div>
</div>
</div>
</header>
<body>
<!-- Single page container -->
<div class="min-h-screen bg-white">
<!-- Main Content -->
<main class="flex-grow">
<main class="container">
<slot />
</main>
<!-- Footer -->
<footer class="border-t border-slate-200 bg-slate-50">
<div class="max-w-4xl mx-auto px-4 py-6">
<p class="text-sm text-slate-600 text-center">
A public notebook of things I figured out, mistakes I made, and tools I tested.
</p>
</div>
</footer>
<Footer client:load />
<!-- Global Interactive Elements -->
<!-- Reading Progress Bar -->
<div id="global-reading-progress" class="reading-progress-bar" style="display: none;"></div>
<!-- Floating Back to Top Button -->
<button
id="global-back-to-top"
class="floating-back-to-top"
aria-label="Back to top"
style="display: none;"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</button>
</div>
<style is:global>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
<!-- Global JavaScript for interactive elements -->
<script>
// Global interactive elements manager
class GlobalInteractive {
readingProgress: HTMLElement | null;
backToTop: HTMLElement | null;
constructor() {
this.readingProgress = document.getElementById('global-reading-progress');
this.backToTop = document.getElementById('global-back-to-top');
this.init();
}
init() {
// Set up event listeners
window.addEventListener('scroll', () => this.handleScroll());
// Back to top click
this.backToTop?.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
// Show elements on first interaction
document.addEventListener('click', () => this.showElements(), { once: true });
document.addEventListener('scroll', () => this.showElements(), { once: true });
}
showElements() {
if (this.readingProgress) {
this.readingProgress.style.display = 'block';
}
if (this.backToTop) {
this.backToTop.style.display = 'flex';
}
}
handleScroll() {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
// Update reading progress
if (this.readingProgress && docHeight > 0) {
const progress = (scrollTop / docHeight) * 100;
this.readingProgress.style.transform = `scaleX(${progress / 100})`;
}
// Show/hide back to top button
if (this.backToTop) {
if (scrollTop > 300) {
this.backToTop.classList.add('visible');
} else {
this.backToTop.classList.remove('visible');
}
}
}
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
// Initialize when DOM is ready
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => {
new GlobalInteractive();
});
}
</script>
<style>
/* Additional global styles */
a {
color: #2563eb;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Smooth scrolling */
/* Smooth scroll behavior */
html {
scroll-behavior: smooth;
}
/* Focus styles for accessibility */
a:focus,
button:focus {
outline: 2px solid #2563eb;
outline-offset: 2px;
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
transition: background 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Selection color */
::selection {
background: #bfdbfe;
color: #1e40af;
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Print styles */
@media print {
.floating-back-to-top,
.keyboard-hint,
.reading-progress-bar {
display: none !important;
}
}
</style>
</body>
</html>
</html>

View File

@@ -1,133 +0,0 @@
---
import type { CollectionEntry } from 'astro:content';
import BaseLayout from './BaseLayout.astro';
interface Props {
post: CollectionEntry<'blog'>;
}
const { post } = Astro.props;
const { title, description, date, tags } = post.data;
const formattedDate = new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
---
<BaseLayout title={title} description={description}>
<article class="max-w-3xl mx-auto px-4 py-8">
<header class="mb-8">
<h1 class="text-4xl font-bold text-slate-900 mb-4 leading-tight">
{title}
</h1>
<div class="flex items-center justify-between text-sm text-slate-500 mb-4">
<time datetime={date}>{formattedDate}</time>
{tags && tags.length > 0 && (
<div class="flex gap-2">
{tags.map(tag => (
<span key={tag} class="bg-slate-100 text-slate-700 px-2 py-1 rounded text-xs">
{tag}
</span>
))}
</div>
)}
</div>
<p class="text-xl text-slate-600 leading-relaxed">
{description}
</p>
</header>
<div class="prose prose-slate max-w-none">
<slot />
</div>
</article>
</BaseLayout>
<style>
.prose {
line-height: 1.75;
}
.prose h2 {
font-size: 1.875rem;
font-weight: 700;
margin-top: 2rem;
margin-bottom: 1rem;
color: #0f172a;
line-height: 1.3;
}
.prose h3 {
font-size: 1.5rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #0f172a;
line-height: 1.4;
}
.prose p {
margin-bottom: 1.25rem;
color: #334155;
}
.prose ul, .prose ol {
margin-bottom: 1.25rem;
padding-left: 1.5rem;
}
.prose li {
margin-bottom: 0.5rem;
color: #334155;
}
.prose ul li {
list-style-type: disc;
}
.prose ol li {
list-style-type: decimal;
}
.prose blockquote {
border-left: 4px solid #94a3b8;
padding-left: 1rem;
font-style: italic;
color: #475569;
margin: 1.5rem 0;
}
.prose code {
background-color: #f1f5f9;
color: #0f172a;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-size: 0.875em;
font-family: 'Courier New', monospace;
}
.prose pre {
background-color: #0f172a;
color: #f1f5f9;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 1.25rem;
}
.prose pre code {
background: none;
color: inherit;
padding: 0;
}
.prose a {
color: #2563eb;
text-decoration: underline;
}
.prose a:hover {
color: #1d4ed8;
}
</style>

716
src/pages/blog/[slug].astro Normal file
View File

@@ -0,0 +1,716 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import Tag from '../../components/Tag.astro';
import { blogPosts } from '../../data/blogPosts';
import { CodeBlock, InlineCode } from '../../components/ArticleBlockquote';
import { H2, H3 } from '../../components/ArticleHeading';
import { Paragraph, LeadParagraph } from '../../components/ArticleParagraph';
import { UL, LI } from '../../components/ArticleList';
export async function getStaticPaths() {
return blogPosts.map(post => ({
params: { slug: post.slug },
props: { post }
}));
}
const { post } = Astro.props;
const formattedDate = new Date(post.date).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric'
});
// Calculate reading time
const wordCount = post.description.split(/\s+/).length + 100;
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
// Generate unique clap key for this post
const clapKey = `claps_${post.slug}`;
---
<BaseLayout title={post.title} description={post.description}>
<!-- Reading progress bar with gradient -->
<div id="reading-progress" class="fixed top-0 left-0 h-1 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 z-50 transition-all duration-100" style="width: 0%"></div>
<!-- Top navigation bar with back button and clap counter -->
<nav class="fixed top-0 left-0 right-0 z-40 bg-white/80 backdrop-blur-md border-b border-slate-200 transition-all duration-300" id="top-nav">
<div class="max-w-4xl mx-auto px-6 py-4 flex items-center justify-between">
<button
id="back-btn-top"
class="flex items-center gap-2 text-slate-700 hover:text-blue-600 transition-colors duration-200 group"
aria-label="Back to home"
>
<svg class="w-5 h-5 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
<span class="font-medium">Back</span>
</button>
<div class="flex items-center gap-3">
<span class="text-sm text-slate-500 font-sans hidden sm:inline">
{readingTime} min read
</span>
<button
id="share-btn-top"
class="share-button-top flex items-center gap-1.5 px-3 py-1.5 bg-slate-100 hover:bg-blue-50 border border-slate-200 hover:border-blue-300 rounded-full transition-all duration-200"
aria-label="Share this post"
>
<svg class="w-4 h-4 text-slate-600 group-hover:text-blue-600 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="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>
<button
id="clap-btn-top"
class="clap-button-top flex items-center gap-1.5 px-3 py-1.5 bg-slate-100 hover:bg-blue-50 border border-slate-200 hover:border-blue-300 rounded-full transition-all duration-200"
aria-label="Clap for this post"
data-clap-key={clapKey}
>
<svg class="w-4 h-4 text-slate-600 group-hover:text-blue-600 transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.789l5 2.5a2 2 0 001.794 0l5-2.5A2 2 0 0019 15.762v-5.43a2.5 2.5 0 00-1.292-2.182l-4.4-2.2a1.5 1.5 0 00-1.333 0l-4.4 2.2A2.5 2.5 0 002 10.333z"/>
</svg>
<span id="clap-count-top" class="text-sm font-medium text-slate-700">0</span>
</button>
</div>
</div>
</nav>
<!-- Main content with enhanced animations -->
<main id="post-content" class="pt-24">
<!-- Beautiful hero section -->
<section class="py-12 md:py-16">
<div class="max-w-3xl mx-auto px-6">
<div class="text-center">
<!-- Title -->
<h1
id="article-title"
class="text-3xl md:text-5xl font-serif font-bold text-slate-900 mb-6 leading-tight tracking-tight cursor-default"
>
{post.title}
</h1>
<!-- Elegant meta info -->
<div
id="article-meta"
class="flex flex-wrap items-center justify-center gap-4 text-sm text-slate-600 mb-6 font-sans"
>
<time datetime={post.date} class="flex items-center gap-1.5 px-3 py-1 bg-slate-50 rounded-full hover:bg-slate-100 transition-colors duration-200">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
<path d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zM4 8h12v8H4V8z"/>
</svg>
{formattedDate}
</time>
<span class="text-slate-400">•</span>
<span class="flex items-center gap-1.5 px-3 py-1 bg-slate-50 rounded-full hover:bg-slate-100 transition-colors duration-200">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
</svg>
{readingTime} min
</span>
</div>
<!-- Description with elegant styling -->
<p
id="article-description"
class="text-xl md:text-2xl text-slate-600 leading-relaxed font-serif italic mb-8 max-w-2xl mx-auto"
>
{post.description}
</p>
<!-- Tags using the Tag component -->
{post.tags && post.tags.length > 0 && (
<div id="article-tags" class="flex flex-wrap justify-center gap-2 mb-8">
{post.tags.map((tag: string, index: number) => (
<Tag tag={tag} index={index} className="text-xs" />
))}
</div>
)}
</div>
</div>
</section>
<!-- Article content with enhanced typography -->
<section class="max-w-3xl mx-auto px-6 pb-24">
<div class="prose prose-slate max-w-none">
<p>{post.description}</p>
{post.slug === 'first-note' && (
<>
<LeadParagraph>
This blog is a public notebook. It's where I document things I learn, problems I solve, and tools I test.
</LeadParagraph>
<H2>Why write in public?</H2>
<Paragraph>
I forget things. Writing them down helps. Making them public helps me think more clearly and might help someone else.
</Paragraph>
<H2>What to expect</H2>
<UL>
<LI>Short entries, usually under 500 words</LI>
<LI>Practical solutions to specific problems</LI>
<LI>Notes on tools and workflows</LI>
<LI>Mistakes and what I learned</LI>
</UL>
</>
)}
{post.slug === 'debugging-tips' && (
<>
<LeadParagraph>
Sometimes the simplest debugging tool is the best one. Print statements get a bad reputation, but they're often exactly what you need.
</LeadParagraph>
<H2>Why print statements work</H2>
<Paragraph>
Debuggers are powerful, but they change how your code runs. Print statements don't.
</Paragraph>
<CodeBlock language="python" showLineNumbers={true}>
{`def process_data(data):
print(f"Processing {len(data)} items")
result = expensive_operation(data)
print(f"Operation result: {result}")
return result`}
</CodeBlock>
</>
)}
</div>
<!-- Footer with elegant back button -->
<div class="mt-16 pt-8 border-t border-slate-200 text-center">
<button
id="back-btn-bottom"
class="inline-flex items-center gap-2 px-6 py-3 bg-white border-2 border-slate-200 text-slate-700 rounded-full hover:border-blue-400 hover:text-blue-600 hover:shadow-md transition-all duration-300 font-medium group"
>
<svg class="w-5 h-5 group-hover:-translate-x-1 transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to all posts
</button>
</div>
</section>
</main>
<!-- Back to top floating button -->
<button
id="back-to-top"
class="fixed bottom-6 right-6 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-full p-4 shadow-lg hover:from-blue-700 hover:to-purple-700 hover:shadow-xl hover:scale-110 active:scale-95 transition-all duration-300 opacity-0 pointer-events-none z-40"
aria-label="Back to top"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</button>
<script>
// Reading progress bar with smooth gradient
function updateReadingProgress() {
const section = document.querySelector('section:last-child') as HTMLElement;
const progressBar = document.getElementById('reading-progress');
if (!section || !progressBar) return;
const sectionTop = section.offsetTop;
const sectionHeight = section.offsetHeight;
const windowHeight = window.innerHeight;
const scrollTop = window.scrollY;
const progress = Math.min(
Math.max((scrollTop - sectionTop + windowHeight * 0.3) / (sectionHeight - windowHeight * 0.3), 0),
1
);
progressBar.style.width = `${progress * 100}%`;
// Update top nav appearance on scroll
const topNav = document.getElementById('top-nav');
if (topNav) {
if (scrollTop > 100) {
topNav.style.backdropFilter = 'blur(12px)';
topNav.style.backgroundColor = 'rgba(255, 255, 255, 0.95)';
} else {
topNav.style.backdropFilter = 'blur(8px)';
topNav.style.backgroundColor = 'rgba(255, 255, 255, 0.8)';
}
}
}
// Back to top button
function updateBackToTop() {
const backToTopBtn = document.getElementById('back-to-top');
if (!backToTopBtn) return;
if (window.scrollY > 300) {
backToTopBtn.style.opacity = '1';
backToTopBtn.style.pointerEvents = 'auto';
} else {
backToTopBtn.style.opacity = '0';
backToTopBtn.style.pointerEvents = 'none';
}
}
// Back to home with lovely transition
function setupBackNavigation() {
const backButtons = [
document.getElementById('back-btn-top'),
document.getElementById('back-btn-bottom')
];
const goHome = () => {
// Lovely exit animation
const content = document.getElementById('post-content');
if (content) {
content.style.transition = 'opacity 0.5s ease-out, transform 0.5s ease-out';
content.style.opacity = '0';
content.style.transform = 'translateY(20px) scale(0.98)';
}
// Fade out top nav
const topNav = document.getElementById('top-nav');
if (topNav) {
topNav.style.transition = 'opacity 0.4s ease-out';
topNav.style.opacity = '0';
}
// Create beautiful overlay
const overlay = document.createElement('div');
overlay.className = 'fixed inset-0 bg-gradient-to-br from-white via-slate-50 to-white z-[100] opacity-0 transition-opacity duration-500';
document.body.appendChild(overlay);
setTimeout(() => {
overlay.style.opacity = '1';
}, 100);
// Navigate after animation
setTimeout(() => {
window.location.href = '/?from=post';
}, 500);
};
backButtons.forEach(btn => {
if (btn) btn.addEventListener('click', goHome);
});
// Back to top click
const backToTopBtn = document.getElementById('back-to-top');
if (backToTopBtn) {
backToTopBtn.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
}
// Production-ready clap functionality with localStorage
function setupClapButtons() {
const clapBtnTop = document.getElementById('clap-btn-top');
const clapCountTop = document.getElementById('clap-count-top');
if (!clapBtnTop || !clapCountTop) return;
// Get clap key from data attribute
const clapKey = clapBtnTop.getAttribute('data-clap-key');
if (!clapKey) return;
// Load existing claps from localStorage
let claps = parseInt(localStorage.getItem(clapKey) || '0');
clapCountTop.textContent = claps.toString();
// Visual state if already clapped
if (claps > 0) {
clapBtnTop.classList.add('bg-blue-50', 'border-blue-300');
}
clapBtnTop.addEventListener('click', () => {
// Increment claps
claps++;
localStorage.setItem(clapKey, claps.toString());
clapCountTop.textContent = claps.toString();
// Visual feedback
clapBtnTop.classList.add('scale-110', 'bg-blue-100', 'border-blue-300');
setTimeout(() => {
clapBtnTop.classList.remove('scale-110', 'bg-blue-100', 'border-blue-300');
}, 300);
// Ripple effect
const ripple = document.createElement('span');
ripple.className = 'absolute inset-0 bg-blue-400/20 rounded-full scale-0 animate-ripple';
clapBtnTop.style.position = 'relative';
clapBtnTop.style.overflow = 'hidden';
clapBtnTop.appendChild(ripple);
setTimeout(() => ripple.remove(), 600);
});
}
// Share function - Web Share API or modal with share links
function setupShareButton() {
const shareBtn = document.getElementById('share-btn-top');
if (!shareBtn) return;
const url = window.location.href;
const title = document.title;
// Check if Web Share API is supported
if (navigator.share) {
// Use native share
shareBtn.addEventListener('click', async () => {
try {
await navigator.share({
title: title,
url: url
});
// Success feedback
shareBtn.classList.add('bg-green-50', 'border-green-300');
setTimeout(() => {
shareBtn.classList.remove('bg-green-50', 'border-green-300');
}, 1000);
} catch (err: unknown) {
// User cancelled - no feedback needed
if (err instanceof Error && err.name !== 'AbortError') {
console.error('Share failed:', err);
}
}
});
} else {
// Show modal with share links
shareBtn.addEventListener('click', () => {
showShareModal(title, url);
});
}
}
// Show share modal with links
function showShareModal(title: string, url: string) {
const encodedTitle = encodeURIComponent(title);
const encodedUrl = encodeURIComponent(url);
// Create modal
const modal = document.createElement('div');
modal.className = 'fixed inset-0 z-[200] flex items-center justify-center p-4';
modal.innerHTML = `
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity" id="share-modal-backdrop"></div>
<div class="relative bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 transform transition-all scale-100" id="share-modal-content">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-slate-900">Share this post</h3>
<button id="close-share-modal" class="p-2 hover:bg-slate-100 rounded-full transition-colors" aria-label="Close">
<svg class="w-5 h-5 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="space-y-3">
<a href="mailto:?subject=${encodedTitle}&body=${encodedUrl}"
class="flex items-center gap-3 p-3 rounded-lg hover:bg-slate-50 transition-colors group">
<div class="w-10 h-10 bg-blue-50 rounded-full flex items-center justify-center group-hover:bg-blue-100 transition-colors">
<svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"/>
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/>
</svg>
</div>
<div class="flex-1">
<div class="font-medium text-slate-900">Email</div>
<div class="text-sm text-slate-500">Send via email</div>
</div>
</a>
<a href="https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}"
target="_blank" rel="noopener noreferrer"
class="flex items-center gap-3 p-3 rounded-lg hover:bg-slate-50 transition-colors group">
<div class="w-10 h-10 bg-sky-50 rounded-full flex items-center justify-center group-hover:bg-sky-100 transition-colors">
<svg class="w-5 h-5 text-sky-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
</svg>
</div>
<div class="flex-1">
<div class="font-medium text-slate-900">X (Twitter)</div>
<div class="text-sm text-slate-500">Share on X</div>
</div>
</a>
<a href="https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}"
target="_blank" rel="noopener noreferrer"
class="flex items-center gap-3 p-3 rounded-lg hover:bg-slate-50 transition-colors group">
<div class="w-10 h-10 bg-blue-50 rounded-full flex items-center justify-center group-hover:bg-blue-100 transition-colors">
<svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</div>
<div class="flex-1">
<div class="font-medium text-slate-900">LinkedIn</div>
<div class="text-sm text-slate-500">Share professionally</div>
</div>
</a>
<button id="copy-link-btn"
class="flex items-center gap-3 p-3 rounded-lg hover:bg-slate-50 transition-colors group w-full text-left"
title="${url}">
<div class="w-10 h-10 bg-slate-50 rounded-full flex items-center justify-center group-hover:bg-slate-100 transition-colors">
<svg class="w-5 h-5 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
</div>
<div class="flex-1 overflow-hidden">
<div class="font-medium text-slate-900">Copy Link</div>
<div class="text-sm text-slate-500 truncate" id="copy-link-text">${url}</div>
</div>
</button>
</div>
</div>
`;
document.body.appendChild(modal);
document.body.style.overflow = 'hidden';
// Close modal handlers
const closeBtn = modal.querySelector('#close-share-modal');
const backdrop = modal.querySelector('#share-modal-backdrop');
const copyBtn = modal.querySelector('#copy-link-btn');
const closeModal = () => {
const content = modal.querySelector('#share-modal-content') as HTMLElement;
if (content) {
content.style.transform = 'scale(0.95)';
content.style.opacity = '0';
}
if (backdrop) {
(backdrop as HTMLElement).style.opacity = '0';
}
setTimeout(() => {
modal.remove();
document.body.style.overflow = '';
}, 200);
};
if (closeBtn) closeBtn.addEventListener('click', closeModal);
if (backdrop) backdrop.addEventListener('click', closeModal);
// Copy link functionality
if (copyBtn) {
copyBtn.addEventListener('click', async () => {
const displayText = document.getElementById('copy-link-text');
const originalText = displayText?.textContent || '';
try {
await navigator.clipboard.writeText(url);
if (displayText) {
displayText.textContent = '✓ Copied!';
displayText.classList.add('text-green-600');
setTimeout(() => {
displayText.textContent = originalText;
displayText.classList.remove('text-green-600');
}, 2000);
}
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = url;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
if (displayText) {
displayText.textContent = '✓ Copied!';
displayText.classList.add('text-green-600');
setTimeout(() => {
displayText.textContent = originalText;
displayText.classList.remove('text-green-600');
}, 2000);
}
} catch (err2) {
alert('Could not copy link. Please copy manually: ' + url);
}
document.body.removeChild(textArea);
}
});
}
}
// Initialize all
document.addEventListener('DOMContentLoaded', () => {
updateReadingProgress();
updateBackToTop();
setupBackNavigation();
setupClapButtons();
setupShareButton();
window.addEventListener('scroll', () => {
updateReadingProgress();
updateBackToTop();
});
});
</script>
<style>
/* Enhanced typography */
.prose-slate {
color: #334155;
}
.prose-slate p {
margin-bottom: 1.75rem;
line-height: 1.85;
font-size: 1.125rem;
}
.prose-slate h2 {
margin-top: 2.75rem;
margin-bottom: 1rem;
font-size: 1.75rem;
font-weight: 700;
color: #1e293b;
font-family: 'Inter', sans-serif;
letter-spacing: -0.025em;
}
.prose-slate h3 {
margin-top: 2rem;
margin-bottom: 0.75rem;
font-size: 1.5rem;
font-weight: 600;
color: #1e293b;
}
.prose-slate ul,
.prose-slate ol {
margin-bottom: 1.75rem;
padding-left: 1.5rem;
}
.prose-slate li {
margin-bottom: 0.55rem;
line-height: 1.75;
}
.prose-slate blockquote {
border-left: 4px solid #cbd5e1;
padding-left: 1.5rem;
font-style: italic;
color: #475569;
margin: 1.75rem 0;
font-size: 1.125rem;
background: linear-gradient(to right, #f8fafc, #ffffff);
padding: 1rem 1.5rem 1rem 1.5rem;
border-radius: 0 0.5rem 0.5rem 0;
}
.prose-slate code {
background: #f1f5f9;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-size: 0.9em;
color: #dc2626;
font-family: 'JetBrains Mono', monospace;
}
/* Smooth transitions */
a, button {
transition: all 0.2s ease;
}
/* Focus styles for all interactive elements */
a:focus,
button:focus,
.clap-button-top:focus,
.share-button-top:focus,
#back-btn-top:focus,
#back-btn-bottom:focus,
#back-to-top:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
}
/* High contrast focus for better visibility */
@media (prefers-contrast: high) {
a:focus,
button:focus {
outline: 3px solid #000;
outline-offset: 2px;
}
}
/* Reading progress */
#reading-progress {
transition: width 0.1s ease-out;
}
/* Top nav transitions */
#top-nav {
transition: backdrop-filter 0.3s ease, background-color 0.3s ease, opacity 0.4s ease;
}
/* Back to top hover */
#back-to-top:hover {
transform: translateY(-2px) scale(1.05);
}
/* Clap button animations */
.clap-button-top {
position: relative;
overflow: hidden;
}
/* Ripple animation */
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
.animate-ripple {
animation: ripple 0.6s ease-out;
}
/* Smooth scroll */
html {
scroll-behavior: smooth;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(to bottom, #cbd5e1, #94a3b8);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(to bottom, #94a3b8, #64748b);
}
/* Selection styling */
::selection {
background: linear-gradient(to right, #3b82f6, #8b5cf6);
color: white;
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Share modal animations */
#share-modal-backdrop {
transition: opacity 0.2s ease;
}
#share-modal-content {
transition: transform 0.2s ease, opacity 0.2s ease;
}
</style>
</BaseLayout>

View File

@@ -1,79 +0,0 @@
---
import BlogLayout from '../../layouts/BlogLayout.astro';
import { H2, H3 } from '../../components/ArticleHeading';
import { Paragraph, LeadParagraph } from '../../components/ArticleParagraph';
import { UL, LI } from '../../components/ArticleList';
import { CodeBlock, InlineCode } from '../../components/ArticleBlockquote';
const post = {
data: {
title: "Debugging with print statements",
description: "When printf debugging is actually the right tool",
date: new Date("2024-01-20"),
tags: ["debugging", "tools"]
}
};
---
<BlogLayout post={post}>
<LeadParagraph>
Sometimes the simplest debugging tool is the best one. Print statements get a bad reputation, but they're often exactly what you need.
</LeadParagraph>
<H2>Why print statements work</H2>
<Paragraph>
Debuggers are powerful, but they change how your code runs. Print statements don't. They let you see what's actually happening in the real execution flow.
</Paragraph>
<H3>When to use them</H3>
<UL>
<LI>Quick investigation of unexpected behavior</LI>
<LI>Understanding data flow through multiple functions</LI>
<LI>Checking state at specific points in time</LI>
<LI>When setting up a debugger feels like overkill</LI>
</UL>
<H2>Make them useful</H2>
<Paragraph>
Bad print statements create noise. Good ones tell you exactly what you need to know.
</Paragraph>
<CodeBlock>
{`def process_data(data):
# Bad: What does this even mean?
print("debug 1")
# Good: Clear context and value
print(f"Processing {len(data)} items")
result = expensive_operation(data)
# Good: Show the important intermediate result
print(f"Operation result: {result}")
return result`}
</CodeBlock>
<H3>What to print</H3>
<UL>
<LI>Variable values at key points</LI>
<LI>Function entry/exit with parameters</LI>
<LI>Loop iterations with counters</LI>
<LI>Conditional branches taken</LI>
<LI>Timing information for performance</LI>
</UL>
<H2>Temporary by design</H2>
<Paragraph>
The beauty of print statements is that they're temporary. Add them, get your answer, remove them. No setup, no cleanup, no commitment.
</Paragraph>
<Paragraph>
Sometimes the most sophisticated tool is the one you can use in 5 seconds and throw away in 10.
</Paragraph>
</BlogLayout>

View File

@@ -1,69 +0,0 @@
---
import BlogLayout from '../../layouts/BlogLayout.astro';
import { H2, H3 } from '../../components/ArticleHeading';
import { Paragraph, LeadParagraph } from '../../components/ArticleParagraph';
import { UL, LI } from '../../components/ArticleList';
import { Blockquote, InlineCode } from '../../components/ArticleBlockquote';
const post = {
data: {
title: "Starting this blog",
description: "Why I'm writing things down in public",
date: new Date("2024-01-15"),
tags: ["meta", "learning"]
}
};
---
<BlogLayout post={post}>
<LeadParagraph>
This blog is a public notebook. It's where I document things I learn, problems I solve, and tools I test.
</LeadParagraph>
<H2>Why write in public?</H2>
<Paragraph>
I forget things. Writing them down helps. Making them public helps me think more clearly and might help someone else.
</Paragraph>
<Paragraph>
The goal isn't to teach or impress. It's to document. If you find something useful here, great. If not, that's fine too.
</Paragraph>
<H2>What to expect</H2>
<UL>
<LI>Short entries, usually under 500 words</LI>
<LI>Practical solutions to specific problems</LI>
<LI>Notes on tools and workflows</LI>
<LI>Mistakes and what I learned</LI>
<LI>Occasional deep dives when needed</LI>
</UL>
<H2>How I work</H2>
<Paragraph>
My process is simple:
</Paragraph>
<UL>
<LI>I try things</LI>
<LI>I break things</LI>
<LI>I fix things</LI>
<LI>I write down what I learned</LI>
</UL>
<Blockquote>
Understanding doesn't expire. Finished projects do.
</Blockquote>
<H3>Tools I use</H3>
<Paragraph>
Mostly standard Unix tools, Python, shell scripts, and whatever gets the job done. I prefer <InlineCode>simple</InlineCode> over <InlineCode>clever</InlineCode>.
</Paragraph>
<Paragraph>
If you're building things and solving problems, maybe you'll find something useful here. If not, thanks for reading anyway.
</Paragraph>
</BlogLayout>

View File

@@ -1,49 +1,398 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { BlogPostCard } from '../components/BlogPostCard';
import MediumCard from '../components/MediumCard.astro';
import { SearchBar } from '../components/SearchBar';
import Tag from '../components/Tag.astro';
import { blogPosts } from '../data/blogPosts';
// Sort posts by date
const posts = [...blogPosts].sort((a, b) =>
const allPosts = [...blogPosts].sort((a, b) =>
new Date(b.date).getTime() - new Date(a.date).getTime()
);
// Get unique tags
const allTags = [...new Set(allPosts.flatMap(post => post.tags))];
---
<BaseLayout title="Blog" description="Technical problem solving blog - practical insights and learning notes">
<div class="max-w-4xl mx-auto px-4 py-8">
<!-- About Section -->
<section class="mb-12 pb-8 border-b border-slate-200">
<p class="text-lg text-slate-700 leading-relaxed">
I work on technical problems and build tools, scripts, and systems to solve them.
Sometimes that means code, sometimes automation, sometimes AI, sometimes something else.
The tool is secondary. The problem comes first.
</p>
<p class="text-sm text-slate-500 mt-4">
Topics: Vibe coding with AI • Debugging • Mac tools • Automation • Small scripts • Learning notes • FOSS
</p>
</section>
<BaseLayout title="Home" description="A public notebook of technical problem solving, mistakes, and learning notes">
<!-- Everything on ONE minimalist page -->
<!-- Blog Posts -->
<section>
<h2 class="text-2xl font-bold text-slate-900 mb-6">Recent Notes</h2>
{posts.length === 0 ? (
<div class="text-center py-12">
<p class="text-slate-500">No posts yet. Check back soon!</p>
<!-- Clean Hero Section -->
<section class="py-16 md:py-20">
<div class="max-w-3xl mx-auto px-6">
<!-- Personal intro -->
<div class="text-center mb-10 animate-fade-in">
<h1 class="text-4xl md:text-5xl font-serif font-light text-slate-900 tracking-tight mb-4">
Marc Mintel
</h1>
<p class="text-lg md:text-xl 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 class="flex items-center justify-center gap-4 text-sm text-slate-500 font-sans mt-4">
<span class="flex items-center gap-1">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><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>•</span>
<span>Digital problem solver</span>
</div>
</div>
</div>
</section>
<!-- Search -->
<section class="mb-8 mt-8">
<div id="search-container">
<SearchBar />
</div>
</section>
<!-- Topics -->
{allTags.length > 0 && (
<section class="mb-8">
<h2 class="text-lg font-semibold text-slate-800 mb-4">Topics</h2>
<div class="tag-cloud">
{allTags.map((tag, index) => (
<a
href="#"
data-tag={tag}
onclick={`filterByTag('${tag}'); return false;`}
class="inline-block"
>
<Tag tag={tag} index={index} />
</a>
))}
</div>
</section>
)}
<!-- All Posts -->
<section>
<div id="posts-container" class="grid-responsive">
{allPosts.length === 0 ? (
<div class="empty-state">
<p>No posts yet. Check back soon!</p>
</div>
) : (
<div class="space-y-4">
{posts.map(post => (
<BlogPostCard
title={post.title}
description={post.description}
date={post.date}
slug={post.slug}
tags={post.tags}
/>
))}
</div>
allPosts.map(post => (
<a
href={`/blog/${post.slug}`}
class="post-link"
data-slug={post.slug}
data-title={post.title}
data-description={post.description}
data-date={post.date}
data-tags={post.tags?.join(',')}
>
<MediumCard post={post} />
</a>
))
)}
</section>
</div>
</BaseLayout>
</div>
</section>
<!-- Transition overlay for smooth page transitions -->
<div id="transition-overlay" class="fixed inset-0 bg-white z-[100] pointer-events-none opacity-0 transition-opacity duration-500"></div>
<script>
// Enhanced client-side functionality with smooth transitions
document.addEventListener('DOMContentLoaded', () => {
const searchContainer = document.getElementById('search-container');
const postsContainer = document.getElementById('posts-container');
const allPosts = postsContainer?.querySelectorAll('.post-card') as NodeListOf<HTMLElement>;
const transitionOverlay = document.getElementById('transition-overlay');
// Search functionality
if (searchContainer) {
const searchInput = searchContainer.querySelector('input');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const query = (e.target as HTMLInputElement).value.toLowerCase().trim();
if (!allPosts) return;
allPosts.forEach((post: HTMLElement) => {
const title = post.querySelector('h3')?.textContent?.toLowerCase() || '';
const description = post.querySelector('.post-excerpt')?.textContent?.toLowerCase() || '';
const tags = Array.from(post.querySelectorAll('.highlighter-tag'))
.map(tag => tag.textContent?.toLowerCase() || '')
.join(' ');
const searchableText = `${title} ${description} ${tags}`;
if (searchableText.includes(query) || query === '') {
post.style.display = 'block';
post.style.opacity = '1';
post.style.transform = 'scale(1)';
} else {
post.style.display = 'none';
post.style.opacity = '0';
post.style.transform = 'scale(0.95)';
}
});
});
}
}
// Global function for tag filtering
(window as any).filterByTag = function(tag: string) {
const postsContainer = document.getElementById('posts-container');
const allPosts = postsContainer?.querySelectorAll('.post-card') as NodeListOf<HTMLElement>;
if (!allPosts) return;
const tagLower = tag.toLowerCase();
allPosts.forEach((post: HTMLElement) => {
const postTags = Array.from(post.querySelectorAll('.highlighter-tag'))
.map(t => t.textContent?.toLowerCase() || '');
if (postTags.includes(tagLower)) {
post.style.display = 'block';
post.style.opacity = '1';
post.style.transform = 'scale(1)';
} else {
post.style.display = 'none';
post.style.opacity = '0';
post.style.transform = 'scale(0.95)';
}
});
// Update search input to show active filter
const searchInput = document.querySelector('#search-container input');
if (searchInput) {
(searchInput as HTMLInputElement).value = `#${tag}`;
}
};
// Smooth transition to blog post with Framer Motion-style animation
const postLinks = document.querySelectorAll('.post-link') as NodeListOf<HTMLAnchorElement>;
postLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const href = link.getAttribute('href');
if (!href || !transitionOverlay) return;
// Get post data for potential future use
const slug = link.getAttribute('data-slug');
const title = link.getAttribute('data-title');
// Add click ripple effect to the card
const card = link.querySelector('.post-card') as HTMLElement;
if (card) {
card.style.transform = 'scale(0.98)';
card.style.transition = 'transform 0.2s ease';
setTimeout(() => {
card.style.transform = 'scale(1)';
}, 150);
}
// Framer Motion-style page transition
// 1. Fade in overlay
transitionOverlay.style.opacity = '1';
transitionOverlay.style.pointerEvents = 'auto';
// 2. Scale down current content
const mainContent = document.querySelector('main');
if (mainContent) {
mainContent.style.transition = 'transform 0.4s ease-out, opacity 0.4s ease-out';
mainContent.style.transform = 'scale(0.95)';
mainContent.style.opacity = '0';
}
// 3. Navigate after animation
setTimeout(() => {
window.location.href = href;
}, 400);
});
// Hover micro-interaction
link.addEventListener('mouseenter', () => {
const card = link.querySelector('.post-card') as HTMLElement;
if (card) {
card.style.transform = 'translateY(-2px)';
}
});
link.addEventListener('mouseleave', () => {
const card = link.querySelector('.post-card') as HTMLElement;
if (card) {
card.style.transform = 'translateY(0)';
}
});
});
// Handle incoming transition (when coming back from post)
const urlParams = new URLSearchParams(window.location.search);
const fromPost = urlParams.get('from');
if (fromPost && transitionOverlay) {
// Reverse animation - fade out overlay and scale up content
setTimeout(() => {
transitionOverlay.style.opacity = '0';
transitionOverlay.style.pointerEvents = 'none';
const mainContent = document.querySelector('main');
if (mainContent) {
mainContent.style.transition = 'transform 0.4s ease-out, opacity 0.4s ease-out';
mainContent.style.transform = 'scale(1)';
mainContent.style.opacity = '1';
}
}, 50);
// Clean up URL
window.history.replaceState({}, document.title, window.location.pathname);
}
// Stagger animation for posts on load
if (allPosts) {
allPosts.forEach((post, index) => {
post.style.opacity = '0';
post.style.transform = 'translateY(10px)';
post.style.transition = `opacity 0.4s ease-out ${index * 0.05}s, transform 0.4s ease-out ${index * 0.05}s`;
setTimeout(() => {
post.style.opacity = '1';
post.style.transform = 'translateY(0)';
}, 100);
});
}
});
</script>
<style>
/* Post link wrapper for smooth transitions */
.post-link {
display: block;
text-decoration: none;
color: inherit;
}
.post-link .post-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.post-link:hover .post-card {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
}
.post-link:active .post-card {
transform: translateY(0) scale(0.98);
}
/* Transition overlay */
#transition-overlay {
backdrop-filter: blur(10px);
}
/* Main content transition container */
main {
transform-origin: center top;
}
/* Smooth animations for all elements */
* {
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
/* Focus styles for all interactive elements */
a:focus,
button:focus,
.post-link:focus,
.highlighter-tag:focus,
input:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
}
/* Remove default outline in favor of custom styles */
a:focus-visible,
button:focus-visible,
input:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* High contrast mode support */
@media (prefers-contrast: high) {
a:focus,
button:focus,
input:focus {
outline: 3px solid #000;
outline-offset: 2px;
}
}
/* Fade in animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeIn 0.6s ease-out both;
}
/* Enhanced hover states for tags */
.tag-cloud a {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.tag-cloud a:hover {
transform: translateY(-1px) rotate(-1deg);
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.1);
}
/* Smooth scroll behavior */
html {
scroll-behavior: smooth;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
transition: background 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Selection color */
::selection {
background: #bfdbfe;
color: #1e40af;
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
</BaseLayout>

View File

@@ -0,0 +1,42 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { blogPosts } from '../../data/blogPosts';
import MediumCard from '../../components/MediumCard.astro';
export async function getStaticPaths() {
const allTags = [...new Set(blogPosts.flatMap(post => post.tags))];
return allTags.map(tag => ({
params: { tag },
props: { tag }
}));
}
const { tag } = Astro.props;
const posts = blogPosts.filter(post => post.tags.includes(tag));
---
<BaseLayout title={`Posts tagged "${tag}"`} description={`All posts tagged with ${tag}`}>
<div class="max-w-3xl mx-auto px-4 py-8">
<header class="mb-8">
<h1 class="text-3xl font-bold text-slate-900 mb-2">
Posts tagged <span class="highlighter-yellow">{tag}</span>
</h1>
<p class="text-slate-600">
{posts.length} post{posts.length === 1 ? '' : 's'}
</p>
</header>
<div class="space-y-4">
{posts.map(post => (
<MediumCard post={post} />
))}
</div>
<div class="mt-8 pt-6 border-t border-slate-200">
<a href="/" class="text-blue-600 hover:text-blue-800 inline-flex items-center">
← Back to home
</a>
</div>
</div>
</BaseLayout>

579
src/styles/global.css Normal file
View File

@@ -0,0 +1,579 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Medium-inspired clean reading experience */
@layer base {
html {
scroll-behavior: smooth;
}
body {
@apply bg-white text-slate-800 font-serif antialiased;
font-family: 'Georgia', 'Times New Roman', serif;
line-height: 1.75;
}
/* Typography */
h1, h2, h3, h4, h5, h6 {
@apply font-sans font-bold text-slate-900;
}
h1 {
@apply text-3xl md:text-4xl leading-tight mb-8;
font-family: 'Inter', sans-serif;
letter-spacing: -0.025em;
}
h2 {
@apply text-2xl md:text-3xl leading-tight mb-6 mt-12;
font-family: 'Inter', sans-serif;
letter-spacing: -0.025em;
}
h3 {
@apply text-xl md:text-2xl leading-tight mb-4 mt-8;
font-family: 'Inter', sans-serif;
letter-spacing: -0.025em;
}
h4 {
@apply text-lg md:text-xl leading-tight mb-3 mt-6;
font-family: 'Inter', sans-serif;
letter-spacing: -0.025em;
}
p {
@apply mb-6 text-base leading-relaxed text-slate-700;
}
.lead {
@apply text-xl md:text-2xl text-slate-600 mb-10 leading-relaxed;
font-weight: 400;
}
a {
@apply text-blue-600 hover:text-blue-800 transition-colors;
}
ul, ol {
@apply ml-6 mb-6;
}
li {
@apply mb-2;
}
code {
@apply bg-slate-100 px-1.5 py-0.5 rounded font-mono text-sm text-slate-700;
}
blockquote {
@apply border-l-4 border-slate-300 pl-6 italic text-slate-600 my-6;
}
/* Code formatting */
code {
@apply bg-slate-100 px-1.5 py-0.5 rounded font-mono text-sm text-slate-700;
}
/* Inline code */
:not(pre) > code {
@apply text-pink-600 bg-pink-50 border border-pink-200;
}
/* Code blocks */
pre {
@apply bg-slate-900 text-slate-100 p-4 rounded-lg overflow-x-auto my-6 border border-slate-700;
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.875rem;
line-height: 1.6;
}
pre code {
@apply bg-transparent text-slate-100 px-0 py-0 border-0;
font-family: inherit;
}
/* Syntax highlighting colors */
.token.comment { @apply text-slate-500 italic; }
.token.keyword { @apply text-purple-400 font-semibold; }
.token.string { @apply text-green-400; }
.token.number { @apply text-orange-400; }
.token.function { @apply text-blue-400; }
.token.operator { @apply text-slate-300; }
.token.punctuation { @apply text-slate-400; }
.token.class-name { @apply text-yellow-400 font-semibold; }
/* Line numbers wrapper */
.line-numbers {
counter-reset: line;
}
.line-numbers .line {
counter-increment: line;
position: relative;
padding-left: 2.5rem;
}
.line-numbers .line::before {
content: counter(line);
position: absolute;
left: 0;
width: 2rem;
text-align: right;
color: #64748b;
user-select: none;
}
/* Focus styles - Firefox compatible */
a:focus,
button:focus,
[tabindex]:focus,
.post-link:focus,
.highlighter-tag:focus,
.clap-button-top:focus,
.share-button-top:focus,
#back-btn-top:focus,
#back-btn-bottom:focus,
#back-to-top:focus,
input:focus {
outline: 2px solid #3b82f6;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2);
}
/* Remove default outline in favor of custom styles */
a:focus-visible,
button:focus-visible,
input:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* High contrast mode support */
@media (prefers-contrast: high) {
a:focus,
button:focus,
input:focus {
outline: 3px solid #000;
outline-offset: 2px;
}
}
}
/* Medium-inspired components */
@layer components {
.container {
@apply max-w-4xl mx-auto px-6 py-10;
}
.wide-container {
@apply max-w-5xl mx-auto px-6 py-12;
}
.narrow-container {
@apply max-w-2xl mx-auto px-6 py-8;
}
/* Header - removed for single page design */
/* Blog post card - refined with better spacing */
.post-card {
@apply mb-12 last:mb-0;
}
.post-card h3 {
@apply text-xl font-semibold mb-3 hover:text-blue-600 transition-colors cursor-pointer;
font-weight: 600;
}
.post-meta {
@apply text-sm text-slate-500 font-sans mb-4;
}
.post-excerpt {
@apply text-slate-700 mb-5 leading-relaxed;
}
.post-tags {
@apply flex flex-wrap gap-2;
}
.tag {
@apply text-xs font-sans bg-slate-100 text-slate-700 px-2 py-1 rounded hover:bg-slate-200 transition-colors cursor-pointer;
}
.read-more {
@apply text-sm font-semibold text-blue-600 hover:text-blue-800 font-sans inline-flex items-center;
}
/* Article page */
.article-header {
@apply mb-10;
}
.article-title {
@apply text-4xl md:text-5xl font-bold mb-4;
}
.article-meta {
@apply text-sm text-slate-500 font-sans mb-6;
}
.article-content {
@apply text-lg leading-relaxed;
}
.article-content p {
@apply mb-7;
}
.article-content h2 {
@apply text-2xl font-bold mt-10 mb-4;
}
.article-content h3 {
@apply text-xl font-bold mt-8 mb-3;
}
.article-content ul,
.article-content ol {
@apply ml-6 mb-7;
}
.article-content li {
@apply mb-2;
}
.article-content blockquote {
@apply border-l-4 border-slate-400 pl-6 italic text-slate-600 my-8 text-xl;
}
/* Enhanced code blocks for articles */
.article-content pre {
@apply bg-slate-900 text-slate-100 p-5 rounded-lg overflow-x-auto my-6 border border-slate-700;
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
font-size: 0.875rem;
line-height: 1.6;
position: relative;
}
.article-content pre code {
@apply bg-transparent px-0 py-0;
font-family: inherit;
white-space: pre;
display: block;
}
/* Line numbers for code blocks */
.article-content pre.line-numbers {
padding-left: 3.5rem;
}
.article-content pre.line-numbers::before {
content: attr(data-line-numbers);
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2.5rem;
background: #1e293b;
color: #64748b;
text-align: right;
padding: 1.25rem 0.5rem 1.25rem 0;
font-family: inherit;
font-size: inherit;
line-height: 1.6;
border-right: 1px solid #334155;
user-select: none;
overflow: hidden;
}
/* Inline code in articles */
.article-content p code,
.article-content li code,
.article-content blockquote code {
@apply bg-slate-100 text-pink-600 px-1.5 py-0.5 rounded font-mono text-sm border border-pink-200;
}
/* Syntax highlighting classes */
.article-content .token.comment { @apply text-slate-500 italic; }
.article-content .token.keyword { @apply text-purple-400 font-semibold; }
.article-content .token.string { @apply text-green-400; }
.article-content .token.number { @apply text-orange-400; }
.article-content .token.function { @apply text-blue-400; }
.article-content .token.operator { @apply text-slate-300; }
.article-content .token.punctuation { @apply text-slate-400; }
.article-content .token.class-name { @apply text-yellow-400 font-semibold; }
.article-content .token.tag { @apply text-red-400; }
.article-content .token.attr-name { @apply text-purple-400; }
.article-content .token.attr-value { @apply text-green-400; }
/* Code language badge */
.article-content pre::after {
content: attr(data-language);
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: #334155;
color: #e2e8f0;
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
font-family: 'Inter', sans-serif;
}
/* About page sections */
.about-section {
@apply mb-10 pb-8 border-b border-slate-200 last:border-0;
}
.about-section h2 {
@apply text-2xl font-bold mb-4;
}
.about-list {
@apply space-y-2;
}
.about-list li {
@apply flex items-start;
}
.about-list li::before {
content: "→";
@apply mr-2 text-blue-600 font-bold;
}
/* Footer - removed for single page design */
/* Simple button */
.btn {
@apply inline-block px-5 py-2 bg-slate-900 text-white font-sans font-medium hover:bg-slate-700 transition-colors no-underline rounded;
}
/* Highlighter-style tags - lovely note-taking style */
.highlighter-tag {
@apply inline-block text-xs font-bold px-2 py-0.5 rounded cursor-pointer transition-all duration-200;
position: relative;
transform: rotate(-1deg);
box-shadow: 2px 2px 0 rgba(0,0,0,0.1);
}
.highlighter-yellow {
@apply bg-yellow-300 text-yellow-900;
background: linear-gradient(180deg, rgba(255,235,59,0.9) 0%, rgba(255,213,79,0.9) 100%);
}
.highlighter-yellow:hover {
transform: rotate(-2deg) scale(1.1);
box-shadow: 3px 3px 0 rgba(0,0,0,0.15);
}
.highlighter-pink {
@apply bg-pink-300 text-pink-900;
background: linear-gradient(180deg, rgba(255,167,209,0.9) 0%, rgba(255,122,175,0.9) 100%);
}
.highlighter-pink:hover {
transform: rotate(-2deg) scale(1.1);
box-shadow: 3px 3px 0 rgba(0,0,0,0.15);
}
.highlighter-green {
@apply bg-green-300 text-green-900;
background: linear-gradient(180deg, rgba(129,199,132,0.9) 0%, rgba(102,187,106,0.9) 100%);
}
.highlighter-green:hover {
transform: rotate(-2deg) scale(1.1);
box-shadow: 3px 3px 0 rgba(0,0,0,0.15);
}
.highlighter-blue {
@apply bg-blue-300 text-blue-900;
background: linear-gradient(180deg, rgba(100,181,246,0.9) 0%, rgba(66,165,245,0.9) 100%);
}
.highlighter-blue:hover {
transform: rotate(-2deg) scale(1.1);
box-shadow: 3px 3px 0 rgba(0,0,0,0.15);
}
/* Scribble underline for emphasis */
.scribble-emphasis {
position: relative;
display: inline-block;
font-weight: 600;
}
.scribble-emphasis::after {
content: '';
position: absolute;
bottom: -2px;
left: -1px;
right: -1px;
height: 7px;
background: url("data:image/svg+xml,%3Csvg width='40' height='8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2 4 Q10 2, 20 4 T38 4' stroke='%23FFEB3B' stroke-width='3' fill='none' stroke-linecap='round'/%3E%3C/svg%3E") repeat-x;
background-size: 40px 8px;
opacity: 0.7;
z-index: -1;
}
/* Sticky note effect */
.sticky-note {
@apply p-4 rounded shadow-sm border;
background: linear-gradient(135deg, #fff9c4 0%, #fff59d 100%);
border-color: #f9a825;
transform: rotate(-0.5deg);
box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
}
.sticky-note:hover {
transform: rotate(0deg) scale(1.02);
box-shadow: 3px 3px 8px rgba(0,0,0,0.15);
}
/* Handwritten style */
.handwritten {
font-family: 'Comic Sans MS', 'Chalkboard SE', 'Comic Neue', cursive;
font-weight: 500;
letter-spacing: -0.3px;
}
/* Search box styling */
.search-box {
@apply w-full px-4 py-3 border-2 border-slate-200 rounded-lg focus:outline-none focus:border-blue-400 transition-colors;
background: rgba(255,255,255,0.9);
backdrop-filter: blur(10px);
}
.search-box::placeholder {
@apply text-slate-400;
}
/* Tag cloud */
.tag-cloud {
@apply flex flex-wrap gap-2 items-center;
}
/* Empty state */
.empty-state {
@apply text-center py-12 text-slate-500;
}
.empty-state svg {
@apply mx-auto mb-4 text-slate-300;
}
/* Line clamp utility for text truncation */
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Subtle gradient backgrounds */
.subtle-gradient {
background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(248,250,252,0.1) 100%);
}
/* Enhanced focus states */
.focus-ring {
@apply focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-white;
}
/* Reading progress indicator */
.reading-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
transform-origin: left;
z-index: 50;
}
/* Smooth animations */
.animate-fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Enhanced typography for better readability */
.text-balance {
text-wrap: balance;
}
/* Subtle shadow variations */
.shadow-soft {
box-shadow: 0 2px 8px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.08);
}
.shadow-hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.06), 0 2px 6px rgba(0,0,0,0.12);
}
/* Improved button styles */
.btn-primary {
@apply bg-blue-600 text-white font-sans font-medium px-6 py-3 rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-200;
}
.btn-secondary {
@apply bg-white text-slate-700 font-sans font-medium px-6 py-3 rounded-lg border border-slate-200 hover:bg-slate-50 focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 transition-all duration-200;
}
/* Status indicators */
.status-indicator {
@apply inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-medium;
}
.status-published {
@apply bg-green-100 text-green-800;
}
.status-draft {
@apply bg-yellow-100 text-yellow-800;
}
/* Responsive grid improvements */
.grid-responsive {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
/* Better spacing utilities */
.space-y-fluid > * + * {
margin-top: clamp(1rem, 2vw, 2rem);
}
/* Enhanced link styles */
.link-enhanced {
@apply text-blue-600 hover:text-blue-800 transition-colors duration-200 relative;
}
.link-enhanced::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
width: 0;
height: 2px;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
transition: width 0.3s ease;
}
.link-enhanced:hover::after {
width: 100%;
}
}