Leaflet Blog in Deno Fresh

nav and about

Changed files
+203 -83
components
islands
routes
+5 -5
components/footer.tsx
··· 4 4 5 5 export function Footer() { 6 6 return ( 7 - <footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center"> 7 + <footer class="py-8 flex gap-6 flex-wrap items-center justify-center text-sm"> 8 8 <a 9 - className="flex items-center gap-2 hover:underline hover:underline-offset-4" 9 + class="flex items-center gap-2 hover:underline hover:underline-offset-4" 10 10 href={`https://bsky.app/profile/${env.NEXT_PUBLIC_BSKY_DID}`} 11 11 target="_blank" 12 12 rel="noopener noreferrer" ··· 15 15 width={16} 16 16 height={16} 17 17 viewBox="0 0 24 24" 18 - className="fill-black dark:fill-white" 18 + class="fill-black dark:fill-white" 19 19 > 20 20 <path d={BlueskyIcon.path} /> 21 21 </svg> 22 22 Bluesky 23 23 </a> 24 24 <a 25 - className="flex items-center gap-2 hover:underline hover:underline-offset-4" 25 + class="flex items-center gap-2 hover:underline hover:underline-offset-4" 26 26 href="https://github.com/knotbin" 27 27 target="_blank" 28 28 rel="noopener noreferrer" ··· 31 31 width={16} 32 32 height={16} 33 33 viewBox="0 0 24 24" 34 - className="fill-black dark:fill-white" 34 + class="fill-black dark:fill-white" 35 35 > 36 36 <path d={GithubIcon.path} /> 37 37 </svg>
+20 -12
components/post-list-item.tsx
··· 41 41 timeoutRef.current = setTimeout(() => { 42 42 setIsHovered(false); 43 43 setIsLeaving(false); 44 - }, 300); // Match the animation duration 44 + }, 300); // Match animation duration 45 45 }; 46 46 47 47 return ( ··· 49 49 {isHovered && ( 50 50 <div 51 51 className={cx( 52 - "fixed inset-0 pointer-events-none z-0 overflow-hidden flex items-center", 52 + "fixed inset-0 pointer-events-none z-0", 53 53 isLeaving ? "animate-fade-out" : "animate-fade-in", 54 54 )} 55 55 > 56 - <div className="absolute whitespace-nowrap animate-marquee font-serif font-medium uppercase overflow-visible flex items-center justify-center leading-none"> 57 - {Array(10).fill(post.title).join(" · ")} 56 + <div className="h-full w-full pt-[120px] flex items-center justify-center"> 57 + <div className="whitespace-nowrap animate-marquee font-serif font-medium uppercase leading-[0.8] text-[20vw] opacity-[0.015] -rotate-12"> 58 + {Array(3).fill(post.title).join(" · ")} 59 + </div> 58 60 </div> 59 61 </div> 60 62 )} 61 63 <a 62 64 href={`/post/${rkey}`} 63 - className="w-full group" 65 + className="w-full group block" 64 66 onMouseEnter={handleMouseEnter} 65 67 onMouseLeave={handleMouseLeave} 66 68 > 67 - <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"> 68 - <div className="w-1.5 diagonal-pattern shrink-0 opacity-20 group-hover:opacity-100 transition-opacity" /> 69 - <div className="flex-1 py-2 px-4 z-10 relative"> 70 - <Title className="text-lg" level="h3"> 69 + <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"> 70 + <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)]" /> 71 + <div className="flex-1 py-2 px-4 z-10 relative w-full"> 72 + <Title className="text-lg w-full" level="h3"> 71 73 {post.title} 72 74 </Title> 73 75 <PostInfo 74 76 content={post.content} 75 77 createdAt={post.createdAt} 76 - className="text-xs mt-1" 77 - > 78 - </PostInfo> 78 + className="text-xs mt-1 w-full" 79 + /> 80 + <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"> 81 + <div className="overflow-hidden"> 82 + <p className="text-sm text-slate-600 dark:text-slate-300 line-clamp-3 break-words"> 83 + {post.content.substring(0, 280)} 84 + </p> 85 + </div> 86 + </div> 79 87 </div> 80 88 </article> 81 89 </a>
+4
fresh.gen.ts
··· 4 4 5 5 import * as $_404 from "./routes/_404.tsx"; 6 6 import * as $_app from "./routes/_app.tsx"; 7 + import * as $about from "./routes/about.tsx"; 7 8 import * as $index from "./routes/index.tsx"; 8 9 import * as $post_slug_ from "./routes/post/[slug].tsx"; 9 10 import * as $rss from "./routes/rss.ts"; 10 11 import * as $CommentSection from "./islands/CommentSection.tsx"; 12 + import * as $layout from "./islands/layout.tsx"; 11 13 import * as $post_list from "./islands/post-list.tsx"; 12 14 import type { Manifest } from "$fresh/server.ts"; 13 15 ··· 15 17 routes: { 16 18 "./routes/_404.tsx": $_404, 17 19 "./routes/_app.tsx": $_app, 20 + "./routes/about.tsx": $about, 18 21 "./routes/index.tsx": $index, 19 22 "./routes/post/[slug].tsx": $post_slug_, 20 23 "./routes/rss.ts": $rss, 21 24 }, 22 25 islands: { 23 26 "./islands/CommentSection.tsx": $CommentSection, 27 + "./islands/layout.tsx": $layout, 24 28 "./islands/post-list.tsx": $post_list, 25 29 }, 26 30 baseUrl: import.meta.url,
+70
islands/layout.tsx
··· 1 + import { Footer } from "../components/footer.tsx"; 2 + import type { ComponentChildren } from "preact"; 3 + import { useEffect, useState } from "preact/hooks"; 4 + 5 + export function Layout({ children }: { children: ComponentChildren }) { 6 + const [isScrolled, setIsScrolled] = useState(false); 7 + 8 + // Get current path to determine active nav item 9 + const path = typeof window !== "undefined" ? window.location.pathname : ""; 10 + const isActive = (href: string) => { 11 + if (href === "/") { 12 + return path === "/" || path.startsWith("/post/"); 13 + } 14 + return path === href; 15 + }; 16 + 17 + useEffect(() => { 18 + const handleScroll = () => { 19 + setIsScrolled(window.scrollY > 0); 20 + }; 21 + 22 + window.addEventListener("scroll", handleScroll); 23 + handleScroll(); // Check initial scroll position 24 + 25 + return () => window.removeEventListener("scroll", handleScroll); 26 + }, []); 27 + 28 + return ( 29 + <div class="flex flex-col min-h-dvh"> 30 + <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"> 31 + <div class="relative"> 32 + <div 33 + class="absolute inset-x-0 bottom-0 h-2 diagonal-pattern opacity-0 transition-opacity duration-300" 34 + style={{ opacity: isScrolled ? 0.25 : 0 }} 35 + /> 36 + <div class="max-w-screen-2xl mx-auto px-8 py-5 flex justify-between items-center"> 37 + <div class="flex items-center gap-7"> 38 + <a href="/" class="font-serif text-xl"> 39 + knotbin 40 + </a> 41 + <div class="h-4 w-px bg-slate-200 dark:bg-slate-700"></div> 42 + <div class="text-base flex items-center gap-7"> 43 + <a href="/" class="relative group" data-current={isActive("/")}> 44 + <span class="opacity-50 group-hover:opacity-100 group-data-[current=true]:opacity-100 transition-opacity"> 45 + blog 46 + </span> 47 + <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" /> 48 + </a> 49 + <a 50 + href="/about" 51 + class="relative group" 52 + data-current={isActive("/about")} 53 + > 54 + <span class="opacity-50 group-hover:opacity-100 group-data-[current=true]:opacity-100 transition-opacity"> 55 + about 56 + </span> 57 + <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" /> 58 + </a> 59 + </div> 60 + </div> 61 + </div> 62 + </div> 63 + </nav> 64 + 65 + <main class="flex-1">{children}</main> 66 + 67 + <Footer /> 68 + </div> 69 + ); 70 + }
+14 -15
routes/_404.tsx
··· 1 + import { Title } from "../components/typography.tsx"; 1 2 import { Head } from "$fresh/runtime.ts"; 3 + import { Layout } from "../islands/layout.tsx"; 2 4 3 5 export default function Error404() { 4 6 return ( ··· 6 8 <Head> 7 9 <title>404 - Page not found</title> 8 10 </Head> 9 - <div class="px-4 py-8 mx-auto bg-[#86efac]"> 10 - <div class="max-w-screen-md mx-auto flex flex-col items-center justify-center"> 11 - <img 12 - class="my-6" 13 - src="/logo.svg" 14 - width="128" 15 - height="128" 16 - alt="the Fresh logo: a sliced lemon dripping with juice" 17 - /> 18 - <h1 class="text-4xl font-bold">404 - Page not found</h1> 19 - <p class="my-4"> 20 - The page you were looking for doesn't exist. 21 - </p> 22 - <a href="/" class="underline">Go back home</a> 11 + <Layout> 12 + <div class="flex-1 flex items-center justify-center"> 13 + <div class="p-8 pb-20 sm:p-20 text-center"> 14 + <Title class="font-serif-italic text-4xl sm:text-5xl lowercase mb-6"> 15 + Page not found. 16 + </Title> 17 + <p class="my-4">The page you were looking for doesn't exist.</p> 18 + <a href="/" class="underline"> 19 + Go back home 20 + </a> 21 + </div> 23 22 </div> 24 - </div> 23 + </Layout> 25 24 </> 26 25 ); 27 26 }
+38
routes/about.tsx
··· 1 + import { Title } from "../components/typography.tsx"; 2 + import { Head } from "$fresh/runtime.ts"; 3 + import { Layout } from "../islands/layout.tsx"; 4 + 5 + export default function About() { 6 + return ( 7 + <> 8 + <Head> 9 + <title>About - knotbin</title> 10 + </Head> 11 + <Layout> 12 + <div class="p-8 pb-20 gap-16 sm:p-20"> 13 + <div class="max-w-[600px] mx-auto"> 14 + <Title class="font-serif-italic text-4xl sm:text-5xl lowercase mb-12"> 15 + About 16 + </Title> 17 + 18 + <div class="prose prose-slate dark:prose-invert space-y-8"> 19 + <p> 20 + I'm a fifteen year old software developer. I'm experienced in 21 + iOS development, and a winner of the 2024 Apple Swift Student 22 + Challenge. I'm very interested in decentralized systems and AT 23 + Protocol in particular. I love designing and building beautiful 24 + interfaces, and learning about amazing systems. 25 + </p> 26 + 27 + <p> 28 + Currently, I'm working with Spark to build a shortform video 29 + platform on the AT Protocol. I'm also working on my own 30 + projects, and always thinking about big ideas and small details. 31 + </p> 32 + </div> 33 + </div> 34 + </div> 35 + </Layout> 36 + </> 37 + ); 38 + }
+18 -19
routes/index.tsx
··· 1 - import { Footer } from "../components/footer.tsx"; 2 1 import PostList from "../islands/post-list.tsx"; 3 2 import { Title } from "../components/typography.tsx"; 4 3 import { getPosts } from "../lib/api.ts"; 4 + import { Layout } from "../islands/layout.tsx"; 5 5 6 6 export const dynamic = "force-static"; 7 7 export const revalidate = 3600; // 1 hour ··· 29 29 ]; 30 30 31 31 function getRandomTagline() { 32 - return stupidSelfDeprecatingTaglinesToTryToPretendImSelfAware[Math.floor(Math.random() * stupidSelfDeprecatingTaglinesToTryToPretendImSelfAware.length)]; 32 + return stupidSelfDeprecatingTaglinesToTryToPretendImSelfAware[ 33 + Math.floor( 34 + Math.random() * 35 + stupidSelfDeprecatingTaglinesToTryToPretendImSelfAware.length, 36 + ) 37 + ]; 33 38 } 34 39 35 40 export default async function Home() { ··· 37 42 const tagline = getRandomTagline(); 38 43 39 44 return ( 40 - <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"> 41 - <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start w-full max-w-[600px]"> 42 - <div className="self-center flex flex-col"> 43 - <div className="relative"> 44 - <Title className="m-0 mb-6 font-serif-italic text-4xl sm:text-5xl lowercase"> 45 - knotbin 46 - </Title> 47 - <span className="absolute bottom-3 -right-2 font-bold text-xs opacity-50 text-right whitespace-nowrap"> 48 - {tagline} 49 - </span> 50 - </div> 51 - </div> 45 + <Layout> 46 + <div class="p-8 pb-20 gap-16 sm:p-20"> 47 + <div class="max-w-[600px] mx-auto"> 48 + <Title class="font-serif-italic text-4xl sm:text-5xl lowercase mb-12"> 49 + Knotbin 50 + </Title> 52 51 53 - <div className="flex flex-col gap-4 w-full"> 54 - <PostList posts={posts} /> 52 + <div class="space-y-4 w-full"> 53 + <PostList posts={posts} /> 54 + </div> 55 55 </div> 56 - </main> 57 - <Footer /> 58 - </div> 56 + </div> 57 + </Layout> 59 58 ); 60 59 }
+34 -32
routes/post/[slug].tsx
··· 2 2 import { CSS, render } from "@deno/gfm"; 3 3 import { Handlers, PageProps } from "$fresh/server.ts"; 4 4 5 - import { Footer } from "../../components/footer.tsx"; 5 + import { Layout } from "../../islands/layout.tsx"; 6 6 import { PostInfo } from "../../components/post-info.tsx"; 7 7 import { Title } from "../../components/typography.tsx"; 8 8 import { getPost } from "../../lib/api.ts"; ··· 117 117 /> 118 118 </Head> 119 119 120 - <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"> 121 - <link rel="alternate" href={post.uri} /> 122 - <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start w-full max-w-[600px] overflow-hidden"> 123 - <article className="w-full space-y-8"> 124 - <div className="space-y-4 w-full"> 125 - <a 126 - href="/" 127 - className="hover:underline hover:underline-offset-4 font-medium" 128 - > 129 - Back 130 - </a> 131 - <Title>{post.value.title}</Title> 132 - <PostInfo 133 - content={post.value.content} 134 - createdAt={post.value.createdAt} 135 - includeAuthor 136 - className="text-sm" 137 - /> 138 - <div className="diagonal-pattern w-full h-3" /> 139 - </div> 140 - <div className="[&>.bluesky-embed]:mt-8 [&>.bluesky-embed]:mb-0"> 141 - {/* Render GFM HTML via dangerouslySetInnerHTML */} 142 - <div 143 - class="mt-8 markdown-body" 144 - dangerouslySetInnerHTML={{ __html: render(post.value.content) }} 145 - /> 146 - </div> 147 - </article> 148 - </main> 149 - <Footer /> 150 - </div> 120 + <Layout> 121 + <div class="p-8 pb-20 gap-16 sm:p-20"> 122 + <link rel="alternate" href={post.uri} /> 123 + <div class="max-w-[600px] mx-auto"> 124 + <a 125 + href="/" 126 + class="hover:underline hover:underline-offset-4 font-medium block mb-8" 127 + > 128 + Back 129 + </a> 130 + <article class="w-full space-y-8"> 131 + <div class="space-y-4 w-full"> 132 + <Title>{post.value.title}</Title> 133 + <PostInfo 134 + content={post.value.content} 135 + createdAt={post.value.createdAt} 136 + includeAuthor 137 + class="text-sm" 138 + /> 139 + <div class="diagonal-pattern w-full h-3" /> 140 + </div> 141 + <div class="[&>.bluesky-embed]:mt-8 [&>.bluesky-embed]:mb-0"> 142 + <div 143 + class="mt-8 markdown-body" 144 + dangerouslySetInnerHTML={{ 145 + __html: render(post.value.content), 146 + }} 147 + /> 148 + </div> 149 + </article> 150 + </div> 151 + </div> 152 + </Layout> 151 153 </> 152 154 ); 153 155 }