fun improvements (#1)
Some checks failed
Build and Deploy Blog / build-and-deploy-local (push) Failing after 11s

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-03-31 00:50:16 +00:00
parent 902521e1f3
commit 1ba5ee1b41
13 changed files with 424 additions and 66 deletions

85
components/badges.tsx Normal file
View File

@@ -0,0 +1,85 @@
import Link from "next/link";
interface BadgeProps {
href?: string;
children: React.ReactNode;
style?: React.CSSProperties;
}
function Badge({ href, children, style }: BadgeProps) {
const base = (
<div
className="flex items-center justify-center border border-white/40 text-center font-mono leading-tight select-none"
style={{ width: 88, height: 31, fontSize: 9, ...style }}
>
{children}
</div>
);
if (href) {
if (href.startsWith("http")) {
return (
<a href={href} target="_blank" rel="noopener noreferrer" className="hover:opacity-80 transition-opacity">
{base}
</a>
);
}
return (
<Link href={href} className="hover:opacity-80 transition-opacity">
{base}
</Link>
);
}
return base;
}
export default function Badges() {
return (
<div className="flex flex-wrap gap-1.5">
<Badge
href="https://nextjs.org"
style={{ background: "linear-gradient(135deg, #0d0d0d, #1a1a2e)", color: "#7dd3fc", borderColor: "#334155" }}
>
<span> Next.js</span>
</Badge>
<Badge
href="https://bun.sh"
style={{ background: "linear-gradient(135deg, #1a0a00, #2d1500)", color: "#fb923c", borderColor: "#7c2d12" }}
>
🍞 Powered by Bun
</Badge>
<Badge
style={{ background: "linear-gradient(135deg, #0c1a3a, #1e3a6e)", color: "#93c5fd", borderColor: "#1e40af" }}
>
<div>
<div>Best viewed</div>
<div>1024×768</div>
</div>
</Badge>
<Badge
href="/feed.xml"
style={{ background: "linear-gradient(135deg, #431407, #7c2d12)", color: "#fde68a", borderColor: "#b45309" }}
>
📡 Valid RSS
</Badge>
<Badge
style={{ background: "linear-gradient(135deg, #0f172a, #1e1b4b)", color: "#c4b5fd", borderColor: "#4c1d95" }}
>
Made with
</Badge>
<Badge
style={{ background: "linear-gradient(135deg, #0369a1, #0ea5e9)", color: "white", borderColor: "#38bdf8" }}
>
<div>
<div style={{ fontSize: 8 }}> Frutiger</div>
<div style={{ fontSize: 8 }}>Aero </div>
</div>
</Badge>
</div>
);
}

View File

@@ -0,0 +1,25 @@
"use client";
import { useRouter } from "next/navigation";
export default function GoBackButton() {
const router = useRouter();
const handleBack = () => {
if (window.history.length > 1) {
router.back();
} else {
router.push("/");
}
};
return (
<button
onClick={handleBack}
aria-label="Go back to previous page"
className="px-4 py-1.5 text-sm bg-gradient-to-b from-blue-100 to-blue-200 border border-blue-300 rounded hover:from-blue-200 hover:to-blue-300 transition-colors"
>
Go Back
</button>
);
}

View File

@@ -0,0 +1,33 @@
"use client";
import { useEffect, useState } from "react";
export default function ReadingProgress() {
const [progress, setProgress] = useState(0);
useEffect(() => {
const handleScroll = () => {
const scrollable = document.body.scrollHeight - window.innerHeight;
if (scrollable > 0) {
setProgress((window.scrollY / scrollable) * 100);
}
};
handleScroll();
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<div className="h-[3px] w-full bg-black/20">
<div
className="h-full transition-[width] duration-100"
style={{
width: `${progress}%`,
background:
"linear-gradient(90deg, rgba(255,255,255,0.9) 0%, rgba(180,230,255,0.75) 100%)",
}}
/>
</div>
);
}

View File

@@ -0,0 +1,64 @@
"use client";
import { useEffect, useState } from "react";
import type { Heading } from "@/lib/posts";
interface TableOfContentsProps {
headings: Heading[];
}
export default function TableOfContents({ headings }: TableOfContentsProps) {
const [activeSlug, setActiveSlug] = useState(() => headings[0]?.slug ?? "");
useEffect(() => {
if (headings.length === 0) return;
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
setActiveSlug(entry.target.id);
}
}
},
{ rootMargin: "0% 0% -70% 0%", threshold: 0 }
);
headings.forEach(({ slug }) => {
const el = document.getElementById(slug);
if (el) observer.observe(el);
});
return () => observer.disconnect();
}, [headings]);
if (headings.length === 0) return null;
return (
<nav>
<p className="text-xs font-bold uppercase tracking-widest text-blue-800 mb-3">
Contents
</p>
<ul className="space-y-1.5">
{headings.map(({ slug, text, level }) => (
<li
key={slug}
className={level === 3 ? "pl-3" : ""}
>
<a
href={`#${slug}`}
aria-current={activeSlug === slug ? "true" : undefined}
className={`text-sm leading-snug transition-colors duration-150 ${
activeSlug === slug
? "text-blue-700 font-semibold"
: "text-gray-600 hover:text-blue-600"
}`}
>
{text}
</a>
</li>
))}
</ul>
</nav>
);
}

View File

@@ -1,6 +1,7 @@
import Link from "next/link";
import React from "react";
import RSSIcon from "./rss-icon";
import ReadingProgress from "./reading-progress";
const CloseIcon = () => (
<svg
@@ -45,38 +46,47 @@ interface WindowProps {
title: string;
children: React.ReactNode;
className?: string;
showProgress?: boolean;
showRss?: boolean;
}
export default function Window({
title,
children,
className = "",
showProgress = false,
showRss = false,
}: WindowProps) {
return (
<div
className={`rounded-lg border border-white/30 bg-white/70 shadow-window backdrop-blur-xl ${className}`}
>
<div className="flex select-none items-center justify-between rounded-t-md bg-gradient-to-b from-blue-500 to-window-title px-4 py-1.5 font-bold text-white text-sm">
<div className="flex items-center space-x-2">
<span>{title}</span>
<Link href="/feed.xml" title="RSS Feed">
<RSSIcon className="h-4 w-4 opacity-80 hover:opacity-100" />
</Link>
</div>
<div className="flex items-center space-x-2">
<div className="flex h-4 w-4 items-center justify-center opacity-80">
<MinimizeIcon />
<div className="rounded-t-md overflow-hidden sticky top-0 z-10">
<div className="flex select-none items-center justify-between bg-gradient-to-b from-blue-500 to-window-title px-4 py-1.5 font-bold text-white text-sm">
<div className="flex items-center space-x-2">
<span>{title}</span>
{showRss && (
<Link href="/feed.xml" title="RSS Feed">
<RSSIcon className="h-4 w-4 opacity-80 hover:opacity-100" />
</Link>
)}
</div>
<div className="flex h-4 w-4 items-center justify-center opacity-80">
<MaximizeIcon />
<div className="flex items-center space-x-2">
<div className="flex h-4 w-4 items-center justify-center opacity-80">
<MinimizeIcon />
</div>
<div className="flex h-4 w-4 items-center justify-center opacity-80">
<MaximizeIcon />
</div>
<Link
href="/"
className="flex h-4 w-4 items-center justify-center opacity-80"
>
<CloseIcon />
</Link>
</div>
<Link
href="/"
className="flex h-4 w-4 items-center justify-center opacity-80"
>
<CloseIcon />
</Link>
</div>
{showProgress && <ReadingProgress />}
</div>
<div className="p-6">{children}</div>
</div>