Compare commits
10 Commits
8abb3899a6
...
7e97459c93
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e97459c93 | |||
| ce915d666b | |||
| 2d6239d2d9 | |||
| 893760751b | |||
| 7faaac2081 | |||
| b2a611ce5b | |||
| 04ada8ad51 | |||
| 86d76d39ac | |||
| 6de2a1c9fa | |||
| c20b33e6af |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -39,3 +39,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# brainstorm
|
||||
.superpowers/
|
||||
|
||||
@@ -5,7 +5,7 @@ import GoBackButton from "@/components/go-back-button";
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="mx-auto max-w-md pt-16">
|
||||
<Window title="Windows cannot find this page">
|
||||
<Window title="Oopsie doopsie! You're lost!">
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="flex-shrink-0 flex h-10 w-10 items-center justify-center rounded-full bg-red-600 shadow-inner">
|
||||
<svg
|
||||
|
||||
27
app/page.tsx
27
app/page.tsx
@@ -1,12 +1,20 @@
|
||||
import { getSortedPostsData, PostMeta } from "../lib/posts";
|
||||
import Link from "next/link";
|
||||
import Window from "../components/window";
|
||||
import Badges from "../components/badges";
|
||||
|
||||
function isNew(dateStr: string): boolean {
|
||||
const postDate = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diffDays = (now.getTime() - postDate.getTime()) / (1000 * 60 * 60 * 24);
|
||||
return diffDays <= 30;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const allPostsData: PostMeta[] = getSortedPostsData();
|
||||
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
<div className="space-y-6">
|
||||
<header className="text-center">
|
||||
<h1 className="text-5xl font-bold text-white [text-shadow:_2px_2px_4px_rgb(0_0_0_/_40%)]">
|
||||
Gabriel's Kaszewski Blog
|
||||
@@ -17,16 +25,25 @@ export default function Home() {
|
||||
</header>
|
||||
|
||||
<section>
|
||||
{/* The list of posts is displayed inside our custom Window component */}
|
||||
<Window title="Blog Posts">
|
||||
<Window title="Blog Posts" showRss>
|
||||
{allPostsData.length > 0 ? (
|
||||
<ul className="space-y-4">
|
||||
{allPostsData.map(({ id, date, title }) => (
|
||||
{allPostsData.map(({ id, date, title, wip }) => (
|
||||
<li key={id}>
|
||||
<Link
|
||||
href={`/posts/${id}`}
|
||||
className="block rounded-md bg-white/50 p-4 transition-all duration-200 hover:bg-white/80 hover:shadow-md"
|
||||
className="relative block overflow-hidden rounded-md bg-white/50 p-4 transition-all duration-200 hover:bg-white/80 hover:shadow-md"
|
||||
>
|
||||
{isNew(date) && !wip && (
|
||||
<div className="animate-blink absolute top-[10px] right-[-24px] w-[100px] rotate-[35deg] bg-[#e05010] py-0.5 text-center text-[9px] font-bold text-white shadow-sm">
|
||||
NEW!
|
||||
</div>
|
||||
)}
|
||||
{wip && (
|
||||
<div className="absolute top-[10px] right-[-24px] w-[100px] rotate-[35deg] bg-amber-500 py-0.5 text-center text-[9px] font-bold text-white shadow-sm">
|
||||
🚧 WIP
|
||||
</div>
|
||||
)}
|
||||
<h3 className="font-bold text-lg text-blue-800">{title}</h3>
|
||||
<small className="text-gray-600">
|
||||
{new Date(date).toLocaleDateString("en-US", {
|
||||
|
||||
@@ -8,9 +8,7 @@ import rehypePrettyCode from "rehype-pretty-code";
|
||||
import rehypeSlug from "rehype-slug";
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
slug: string;
|
||||
};
|
||||
params: Promise<{ slug: string }>;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
@@ -19,46 +17,48 @@ export async function generateStaticParams() {
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps) {
|
||||
const postData = await getPostData(params.slug);
|
||||
const { slug } = await params;
|
||||
const postData = await getPostData(slug);
|
||||
return {
|
||||
title: `${postData.title} | Gabriel's Kaszewski Blog`,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Post({ params }: PageProps) {
|
||||
const postData: PostData = await getPostData(params.slug);
|
||||
const { slug } = await params;
|
||||
const postData: PostData = await getPostData(slug);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<Window title={postData.title} showProgress>
|
||||
<article>
|
||||
<div className="mb-4 text-gray-500 flex flex-col gap-1">
|
||||
<span>
|
||||
{new Date(postData.date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</span>
|
||||
<span className="text-sm text-gray-400">{postData.readingTime}</span>
|
||||
</div>
|
||||
|
||||
{postData.wip && (
|
||||
<div className="mb-6 flex items-center gap-3 rounded-lg border border-amber-300 bg-amber-50/70 px-4 py-3 text-sm text-amber-800">
|
||||
<span className="text-lg">🚧</span>
|
||||
<span>This post is a work in progress — content may change.</span>
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="lg:flex lg:gap-4 lg:items-start">
|
||||
<Window title={postData.title} showProgress className="lg:flex-1 min-w-0">
|
||||
<article>
|
||||
<div className="mb-4 text-gray-500 flex flex-col gap-1">
|
||||
<span>
|
||||
{new Date(postData.date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</span>
|
||||
<span className="text-sm text-gray-400">{postData.readingTime}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile TOC — shown above article, hidden on lg+ */}
|
||||
{postData.headings.length > 0 && (
|
||||
<div className="lg:hidden mb-6">
|
||||
<TableOfContents headings={postData.headings} />
|
||||
</div>
|
||||
)}
|
||||
{postData.wip && (
|
||||
<div className="mb-6 flex items-center gap-3 rounded-lg border border-amber-300 bg-amber-50/70 px-4 py-3 text-sm text-amber-800">
|
||||
<span className="text-lg">🚧</span>
|
||||
<span>This post is a work in progress — content may change.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="lg:flex lg:gap-8 lg:items-start">
|
||||
<div className="prose lg:prose-lg max-w-none flex-1 min-w-0">
|
||||
{/* Mobile TOC — inline above article, hidden on lg+ */}
|
||||
{postData.headings.length > 0 && (
|
||||
<div className="lg:hidden mb-6 bg-white/20 backdrop-blur border border-white/30 rounded-lg p-4">
|
||||
<TableOfContents headings={postData.headings} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="prose lg:prose-lg max-w-none">
|
||||
<MDXRemote
|
||||
source={postData.content}
|
||||
options={{
|
||||
@@ -77,16 +77,18 @@ export default async function Post({ params }: PageProps) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
</Window>
|
||||
|
||||
{/* Desktop TOC sidebar — hidden on mobile */}
|
||||
{postData.headings.length > 0 && (
|
||||
<div className="hidden lg:block w-52 flex-shrink-0">
|
||||
<TableOfContents headings={postData.headings} />
|
||||
</div>
|
||||
)}
|
||||
{/* Desktop TOC — separate Aero Window, sticky on the right */}
|
||||
{postData.headings.length > 0 && (
|
||||
<div className="hidden lg:block w-56 flex-shrink-0 sticky top-8">
|
||||
<Window title="Contents">
|
||||
<TableOfContents headings={postData.headings} />
|
||||
</Window>
|
||||
</div>
|
||||
</article>
|
||||
</Window>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<Link
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -4,9 +4,19 @@ 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={() => router.back()}
|
||||
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
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function TableOfContents({ headings }: TableOfContentsProps) {
|
||||
if (headings.length === 0) return null;
|
||||
|
||||
return (
|
||||
<nav className="bg-white/20 backdrop-blur border border-white/30 rounded-lg p-4 lg:sticky lg:top-8">
|
||||
<nav>
|
||||
<p className="text-xs font-bold uppercase tracking-widest text-blue-800 mb-3">
|
||||
Contents
|
||||
</p>
|
||||
|
||||
@@ -47,6 +47,7 @@ interface WindowProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
showProgress?: boolean;
|
||||
showRss?: boolean;
|
||||
}
|
||||
|
||||
export default function Window({
|
||||
@@ -54,18 +55,21 @@ export default function Window({
|
||||
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="rounded-t-md overflow-hidden">
|
||||
<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>
|
||||
<Link href="/feed.xml" title="RSS Feed">
|
||||
<RSSIcon className="h-4 w-4 opacity-80 hover:opacity-100" />
|
||||
</Link>
|
||||
{showRss && (
|
||||
<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">
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import readingTime from 'reading-time';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
||||
|
||||
@@ -90,6 +91,7 @@ export function getAllPostIds() {
|
||||
|
||||
export async function getPostData(id: string): Promise<PostData> {
|
||||
const fullPath = path.join(postsDirectory, `${id}.mdx`);
|
||||
if (!fs.existsSync(fullPath)) notFound();
|
||||
const fileContents = fs.readFileSync(fullPath, 'utf8');
|
||||
const matterResult = matter(fileContents);
|
||||
const stats = readingTime(matterResult.content);
|
||||
|
||||
Reference in New Issue
Block a user