Leaflet Blog in Deno Fresh

nav and about

Changed files
+203 -83
components
islands
routes
+5 -5
components/footer.tsx
···
export function Footer() {
return (
-
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href={`https://bsky.app/profile/${env.NEXT_PUBLIC_BSKY_DID}`}
target="_blank"
rel="noopener noreferrer"
···
width={16}
height={16}
viewBox="0 0 24 24"
-
className="fill-black dark:fill-white"
>
<path d={BlueskyIcon.path} />
</svg>
Bluesky
</a>
<a
-
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://github.com/knotbin"
target="_blank"
rel="noopener noreferrer"
···
width={16}
height={16}
viewBox="0 0 24 24"
-
className="fill-black dark:fill-white"
>
<path d={GithubIcon.path} />
</svg>
···
export function Footer() {
return (
+
<footer class="py-8 flex gap-6 flex-wrap items-center justify-center text-sm">
<a
+
class="flex items-center gap-2 hover:underline hover:underline-offset-4"
href={`https://bsky.app/profile/${env.NEXT_PUBLIC_BSKY_DID}`}
target="_blank"
rel="noopener noreferrer"
···
width={16}
height={16}
viewBox="0 0 24 24"
+
class="fill-black dark:fill-white"
>
<path d={BlueskyIcon.path} />
</svg>
Bluesky
</a>
<a
+
class="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://github.com/knotbin"
target="_blank"
rel="noopener noreferrer"
···
width={16}
height={16}
viewBox="0 0 24 24"
+
class="fill-black dark:fill-white"
>
<path d={GithubIcon.path} />
</svg>
+20 -12
components/post-list-item.tsx
···
timeoutRef.current = setTimeout(() => {
setIsHovered(false);
setIsLeaving(false);
-
}, 300); // Match the animation duration
};
return (
···
{isHovered && (
<div
className={cx(
-
"fixed inset-0 pointer-events-none z-0 overflow-hidden flex items-center",
isLeaving ? "animate-fade-out" : "animate-fade-in",
)}
>
-
<div className="absolute whitespace-nowrap animate-marquee font-serif font-medium uppercase overflow-visible flex items-center justify-center leading-none">
-
{Array(10).fill(post.title).join(" · ")}
</div>
</div>
)}
<a
href={`/post/${rkey}`}
-
className="w-full group"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
-
<article className="w-full flex flex-row border-b items-stretch relative transition-color backdrop-blur-sm hover:bg-slate-700/5 dark:hover:bg-slate-200/10">
-
<div className="w-1.5 diagonal-pattern shrink-0 opacity-20 group-hover:opacity-100 transition-opacity" />
-
<div className="flex-1 py-2 px-4 z-10 relative">
-
<Title className="text-lg" level="h3">
{post.title}
</Title>
<PostInfo
content={post.content}
createdAt={post.createdAt}
-
className="text-xs mt-1"
-
>
-
</PostInfo>
</div>
</article>
</a>
···
timeoutRef.current = setTimeout(() => {
setIsHovered(false);
setIsLeaving(false);
+
}, 300); // Match animation duration
};
return (
···
{isHovered && (
<div
className={cx(
+
"fixed inset-0 pointer-events-none z-0",
isLeaving ? "animate-fade-out" : "animate-fade-in",
)}
>
+
<div className="h-full w-full pt-[120px] flex items-center justify-center">
+
<div className="whitespace-nowrap animate-marquee font-serif font-medium uppercase leading-[0.8] text-[20vw] opacity-[0.015] -rotate-12">
+
{Array(3).fill(post.title).join(" · ")}
+
</div>
</div>
</div>
)}
<a
href={`/post/${rkey}`}
+
className="w-full group block"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
+
<article className="w-full flex flex-row border-b items-stretch relative transition-colors duration-300 ease-[cubic-bezier(0.33,0,0.67,1)] backdrop-blur-sm hover:bg-slate-700/5 dark:hover:bg-slate-200/10">
+
<div className="w-1.5 diagonal-pattern shrink-0 opacity-20 group-hover:opacity-100 transition-opacity duration-300 ease-[cubic-bezier(0.33,0,0.67,1)]" />
+
<div className="flex-1 py-2 px-4 z-10 relative w-full">
+
<Title className="text-lg w-full" level="h3">
{post.title}
</Title>
<PostInfo
content={post.content}
createdAt={post.createdAt}
+
className="text-xs mt-1 w-full"
+
/>
+
<div className="grid transition-[grid-template-rows,opacity] duration-300 ease-[cubic-bezier(0.33,0,0.67,1)] grid-rows-[0fr] group-hover:grid-rows-[1fr] opacity-0 group-hover:opacity-100 mt-2">
+
<div className="overflow-hidden">
+
<p className="text-sm text-slate-600 dark:text-slate-300 line-clamp-3 break-words">
+
{post.content.substring(0, 280)}
+
</p>
+
</div>
+
</div>
</div>
</article>
</a>
+4
fresh.gen.ts
···
import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx";
import * as $index from "./routes/index.tsx";
import * as $post_slug_ from "./routes/post/[slug].tsx";
import * as $rss from "./routes/rss.ts";
import * as $CommentSection from "./islands/CommentSection.tsx";
import * as $post_list from "./islands/post-list.tsx";
import type { Manifest } from "$fresh/server.ts";
···
routes: {
"./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app,
"./routes/index.tsx": $index,
"./routes/post/[slug].tsx": $post_slug_,
"./routes/rss.ts": $rss,
},
islands: {
"./islands/CommentSection.tsx": $CommentSection,
"./islands/post-list.tsx": $post_list,
},
baseUrl: import.meta.url,
···
import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx";
+
import * as $about from "./routes/about.tsx";
import * as $index from "./routes/index.tsx";
import * as $post_slug_ from "./routes/post/[slug].tsx";
import * as $rss from "./routes/rss.ts";
import * as $CommentSection from "./islands/CommentSection.tsx";
+
import * as $layout from "./islands/layout.tsx";
import * as $post_list from "./islands/post-list.tsx";
import type { Manifest } from "$fresh/server.ts";
···
routes: {
"./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app,
+
"./routes/about.tsx": $about,
"./routes/index.tsx": $index,
"./routes/post/[slug].tsx": $post_slug_,
"./routes/rss.ts": $rss,
},
islands: {
"./islands/CommentSection.tsx": $CommentSection,
+
"./islands/layout.tsx": $layout,
"./islands/post-list.tsx": $post_list,
},
baseUrl: import.meta.url,
+70
islands/layout.tsx
···
···
+
import { Footer } from "../components/footer.tsx";
+
import type { ComponentChildren } from "preact";
+
import { useEffect, useState } from "preact/hooks";
+
+
export function Layout({ children }: { children: ComponentChildren }) {
+
const [isScrolled, setIsScrolled] = useState(false);
+
+
// Get current path to determine active nav item
+
const path = typeof window !== "undefined" ? window.location.pathname : "";
+
const isActive = (href: string) => {
+
if (href === "/") {
+
return path === "/" || path.startsWith("/post/");
+
}
+
return path === href;
+
};
+
+
useEffect(() => {
+
const handleScroll = () => {
+
setIsScrolled(window.scrollY > 0);
+
};
+
+
window.addEventListener("scroll", handleScroll);
+
handleScroll(); // Check initial scroll position
+
+
return () => window.removeEventListener("scroll", handleScroll);
+
}, []);
+
+
return (
+
<div class="flex flex-col min-h-dvh">
+
<nav class="w-full sticky top-0 z-50 backdrop-blur-sm bg-white/80 dark:bg-black/80 transition-[padding,border-color] duration-200">
+
<div class="relative">
+
<div
+
class="absolute inset-x-0 bottom-0 h-2 diagonal-pattern opacity-0 transition-opacity duration-300"
+
style={{ opacity: isScrolled ? 0.25 : 0 }}
+
/>
+
<div class="max-w-screen-2xl mx-auto px-8 py-5 flex justify-between items-center">
+
<div class="flex items-center gap-7">
+
<a href="/" class="font-serif text-xl">
+
knotbin
+
</a>
+
<div class="h-4 w-px bg-slate-200 dark:bg-slate-700"></div>
+
<div class="text-base flex items-center gap-7">
+
<a href="/" class="relative group" data-current={isActive("/")}>
+
<span class="opacity-50 group-hover:opacity-100 group-data-[current=true]:opacity-100 transition-opacity">
+
blog
+
</span>
+
<div class="absolute bottom-0 left-0 w-full h-px bg-current origin-left scale-x-0 group-hover:scale-x-100 group-data-[current=true]:scale-x-100 transition-transform duration-300 ease-in-out" />
+
</a>
+
<a
+
href="/about"
+
class="relative group"
+
data-current={isActive("/about")}
+
>
+
<span class="opacity-50 group-hover:opacity-100 group-data-[current=true]:opacity-100 transition-opacity">
+
about
+
</span>
+
<div class="absolute bottom-0 left-0 w-full h-px bg-current origin-left scale-x-0 group-hover:scale-x-100 group-data-[current=true]:scale-x-100 transition-transform duration-300 ease-in-out" />
+
</a>
+
</div>
+
</div>
+
</div>
+
</div>
+
</nav>
+
+
<main class="flex-1">{children}</main>
+
+
<Footer />
+
</div>
+
);
+
}
+14 -15
routes/_404.tsx
···
import { Head } from "$fresh/runtime.ts";
export default function Error404() {
return (
···
<Head>
<title>404 - Page not found</title>
</Head>
-
<div class="px-4 py-8 mx-auto bg-[#86efac]">
-
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
-
<img
-
class="my-6"
-
src="/logo.svg"
-
width="128"
-
height="128"
-
alt="the Fresh logo: a sliced lemon dripping with juice"
-
/>
-
<h1 class="text-4xl font-bold">404 - Page not found</h1>
-
<p class="my-4">
-
The page you were looking for doesn't exist.
-
</p>
-
<a href="/" class="underline">Go back home</a>
</div>
-
</div>
</>
);
}
···
+
import { Title } from "../components/typography.tsx";
import { Head } from "$fresh/runtime.ts";
+
import { Layout } from "../islands/layout.tsx";
export default function Error404() {
return (
···
<Head>
<title>404 - Page not found</title>
</Head>
+
<Layout>
+
<div class="flex-1 flex items-center justify-center">
+
<div class="p-8 pb-20 sm:p-20 text-center">
+
<Title class="font-serif-italic text-4xl sm:text-5xl lowercase mb-6">
+
Page not found.
+
</Title>
+
<p class="my-4">The page you were looking for doesn't exist.</p>
+
<a href="/" class="underline">
+
Go back home
+
</a>
+
</div>
</div>
+
</Layout>
</>
);
}
+38
routes/about.tsx
···
···
+
import { Title } from "../components/typography.tsx";
+
import { Head } from "$fresh/runtime.ts";
+
import { Layout } from "../islands/layout.tsx";
+
+
export default function About() {
+
return (
+
<>
+
<Head>
+
<title>About - knotbin</title>
+
</Head>
+
<Layout>
+
<div class="p-8 pb-20 gap-16 sm:p-20">
+
<div class="max-w-[600px] mx-auto">
+
<Title class="font-serif-italic text-4xl sm:text-5xl lowercase mb-12">
+
About
+
</Title>
+
+
<div class="prose prose-slate dark:prose-invert space-y-8">
+
<p>
+
I'm a fifteen year old software developer. I'm experienced in
+
iOS development, and a winner of the 2024 Apple Swift Student
+
Challenge. I'm very interested in decentralized systems and AT
+
Protocol in particular. I love designing and building beautiful
+
interfaces, and learning about amazing systems.
+
</p>
+
+
<p>
+
Currently, I'm working with Spark to build a shortform video
+
platform on the AT Protocol. I'm also working on my own
+
projects, and always thinking about big ideas and small details.
+
</p>
+
</div>
+
</div>
+
</div>
+
</Layout>
+
</>
+
);
+
}
+18 -19
routes/index.tsx
···
-
import { Footer } from "../components/footer.tsx";
import PostList from "../islands/post-list.tsx";
import { Title } from "../components/typography.tsx";
import { getPosts } from "../lib/api.ts";
export const dynamic = "force-static";
export const revalidate = 3600; // 1 hour
···
];
function getRandomTagline() {
-
return stupidSelfDeprecatingTaglinesToTryToPretendImSelfAware[Math.floor(Math.random() * stupidSelfDeprecatingTaglinesToTryToPretendImSelfAware.length)];
}
export default async function Home() {
···
const tagline = getRandomTagline();
return (
-
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-dvh p-8 pb-20 gap-16 sm:p-20">
-
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start w-full max-w-[600px]">
-
<div className="self-center flex flex-col">
-
<div className="relative">
-
<Title className="m-0 mb-6 font-serif-italic text-4xl sm:text-5xl lowercase">
-
knotbin
-
</Title>
-
<span className="absolute bottom-3 -right-2 font-bold text-xs opacity-50 text-right whitespace-nowrap">
-
{tagline}
-
</span>
-
</div>
-
</div>
-
<div className="flex flex-col gap-4 w-full">
-
<PostList posts={posts} />
</div>
-
</main>
-
<Footer />
-
</div>
);
}
···
import PostList from "../islands/post-list.tsx";
import { Title } from "../components/typography.tsx";
import { getPosts } from "../lib/api.ts";
+
import { Layout } from "../islands/layout.tsx";
export const dynamic = "force-static";
export const revalidate = 3600; // 1 hour
···
];
function getRandomTagline() {
+
return stupidSelfDeprecatingTaglinesToTryToPretendImSelfAware[
+
Math.floor(
+
Math.random() *
+
stupidSelfDeprecatingTaglinesToTryToPretendImSelfAware.length,
+
)
+
];
}
export default async function Home() {
···
const tagline = getRandomTagline();
return (
+
<Layout>
+
<div class="p-8 pb-20 gap-16 sm:p-20">
+
<div class="max-w-[600px] mx-auto">
+
<Title class="font-serif-italic text-4xl sm:text-5xl lowercase mb-12">
+
Knotbin
+
</Title>
+
<div class="space-y-4 w-full">
+
<PostList posts={posts} />
+
</div>
</div>
+
</div>
+
</Layout>
);
}
+34 -32
routes/post/[slug].tsx
···
import { CSS, render } from "@deno/gfm";
import { Handlers, PageProps } from "$fresh/server.ts";
-
import { Footer } from "../../components/footer.tsx";
import { PostInfo } from "../../components/post-info.tsx";
import { Title } from "../../components/typography.tsx";
import { getPost } from "../../lib/api.ts";
···
/>
</Head>
-
<div className="grid grid-rows-[20px_1fr_20px] justify-items-center min-h-dvh py-8 px-4 xs:px-8 pb-20 gap-16 sm:p-20">
-
<link rel="alternate" href={post.uri} />
-
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start w-full max-w-[600px] overflow-hidden">
-
<article className="w-full space-y-8">
-
<div className="space-y-4 w-full">
-
<a
-
href="/"
-
className="hover:underline hover:underline-offset-4 font-medium"
-
>
-
Back
-
</a>
-
<Title>{post.value.title}</Title>
-
<PostInfo
-
content={post.value.content}
-
createdAt={post.value.createdAt}
-
includeAuthor
-
className="text-sm"
-
/>
-
<div className="diagonal-pattern w-full h-3" />
-
</div>
-
<div className="[&>.bluesky-embed]:mt-8 [&>.bluesky-embed]:mb-0">
-
{/* Render GFM HTML via dangerouslySetInnerHTML */}
-
<div
-
class="mt-8 markdown-body"
-
dangerouslySetInnerHTML={{ __html: render(post.value.content) }}
-
/>
-
</div>
-
</article>
-
</main>
-
<Footer />
-
</div>
</>
);
}
···
import { CSS, render } from "@deno/gfm";
import { Handlers, PageProps } from "$fresh/server.ts";
+
import { Layout } from "../../islands/layout.tsx";
import { PostInfo } from "../../components/post-info.tsx";
import { Title } from "../../components/typography.tsx";
import { getPost } from "../../lib/api.ts";
···
/>
</Head>
+
<Layout>
+
<div class="p-8 pb-20 gap-16 sm:p-20">
+
<link rel="alternate" href={post.uri} />
+
<div class="max-w-[600px] mx-auto">
+
<a
+
href="/"
+
class="hover:underline hover:underline-offset-4 font-medium block mb-8"
+
>
+
Back
+
</a>
+
<article class="w-full space-y-8">
+
<div class="space-y-4 w-full">
+
<Title>{post.value.title}</Title>
+
<PostInfo
+
content={post.value.content}
+
createdAt={post.value.createdAt}
+
includeAuthor
+
class="text-sm"
+
/>
+
<div class="diagonal-pattern w-full h-3" />
+
</div>
+
<div class="[&>.bluesky-embed]:mt-8 [&>.bluesky-embed]:mb-0">
+
<div
+
class="mt-8 markdown-body"
+
dangerouslySetInnerHTML={{
+
__html: render(post.value.content),
+
}}
+
/>
+
</div>
+
</article>
+
</div>
+
</div>
+
</Layout>
</>
);
}