fun improvements (#1)
Some checks failed
Build and Deploy Blog / build-and-deploy-local (push) Failing after 11s
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:
85
components/badges.tsx
Normal file
85
components/badges.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
components/go-back-button.tsx
Normal file
25
components/go-back-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
33
components/reading-progress.tsx
Normal file
33
components/reading-progress.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
64
components/table-of-contents.tsx
Normal file
64
components/table-of-contents.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user