diff --git a/src/App.tsx b/src/App.tsx index c0b9548..87e791b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,12 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { Link, Navigate, Route, Routes, useLocation, useParams } from "react-router-dom"; +import { + Link, + NavLink, + Route, + Routes, + useLocation, + useParams +} from "react-router-dom"; import { BLOG_DEFAULT_DESCRIPTION, BLOG_NAME, @@ -40,11 +47,19 @@ type MetaInput = { const LOADER_FRAMES = [load1, load2, load3, load4, load5, load6]; const FORCE_LOADER_PREVIEW = false; const TWITTER_WIDGETS_SCRIPT_ID = "twitter-widgets-script"; +const ARTIFACT_CATEGORY = "artifact"; +const ARTIFACTS_PATH = "/artifacts"; let twitterWidgetsPromise: Promise | null = null; const toAbsoluteUrl = (path = "/"): string => new URL(path, BLOG_SITE_URL).toString(); +const hasCategory = (categories: string[] | undefined, category: string): boolean => + (categories ?? []).some((entry) => entry.trim().toLowerCase() === category); + +const isArtifactPost = (post: ManifestPost): boolean => + hasCategory(post.categories, ARTIFACT_CATEGORY); + const ensureTwitterWidgets = (): Promise => { if (window.twttr?.widgets?.load) return Promise.resolve(); if (twitterWidgetsPromise) return twitterWidgetsPromise; @@ -212,6 +227,9 @@ function App() { }; }, []); + const articlePosts = useMemo(() => posts.filter((post) => !isArtifactPost(post)), [posts]); + const artifactPosts = useMemo(() => posts.filter(isArtifactPost), [posts]); + return (
@@ -226,6 +244,21 @@ function App() { {BLOG_NAME} +
@@ -235,13 +268,20 @@ function App() { } + element={} + /> + } /> } /> - } /> + } + /> )}
@@ -330,6 +370,55 @@ function IndexPage({ ); } +function ArtifactsPage({ + posts, + state, + error +}: { + posts: ManifestPost[]; + state: LoadState; + error: string; +}) { + usePageMetadata({ + title: `Artifacts | ${BLOG_NAME}`, + description: "Raw, unpolished LLM outputs.", + path: ARTIFACTS_PATH, + type: "website" + }); + + if (state === "loading") return ; + if (state === "error") return

Error: {error}

; + if (posts.length === 0) return

No artifacts yet.

; + + return ( +
+
+

Artifacts

+

Raw, unpolished LLM outputs.

+
+
    + {posts.map((post) => { + const publishedDate = getReadableDate(post.publishedAt); + const description = post.description || post.excerpt || "No description provided."; + + return ( +
  1. + + {post.title} + +
    + {publishedDate && {publishedDate}} + codex +
    +

    {description}

    +
  2. + ); + })} +
+
+ ); +} + function PostPage({ posts, state, @@ -339,7 +428,8 @@ function PostPage({ state: LoadState; manifestError: string; }) { - const { slug = "" } = useParams(); + const params = useParams(); + const slug = params.slug ?? params["*"] ?? ""; const location = useLocation(); const post = useMemo(() => posts.find((entry) => entry.slug === slug), [posts, slug]); const [html, setHtml] = useState(""); @@ -399,6 +489,10 @@ function PostPage({ postFrontmatter.excerpt || post?.excerpt || BLOG_DEFAULT_DESCRIPTION; + const categories = postFrontmatter.categories?.length + ? postFrontmatter.categories + : post?.categories ?? []; + const isArtifact = hasCategory(categories, ARTIFACT_CATEGORY); const bannerTxId = postFrontmatter.banner || post?.frontmatter?.banner || post?.bannerTxId; const publishedDate = getReadableDate(postFrontmatter.date || post?.publishedAt); const updatedDate = getReadableDate(postFrontmatter.updated || post?.updated || undefined); @@ -423,7 +517,7 @@ function PostPage({ title: metadataTitle, description: metadataDescription, path: location.pathname, - image: bannerTxId ? arweaveUrl(bannerTxId) : undefined, + image: bannerTxId && !isArtifact ? arweaveUrl(bannerTxId) : undefined, type: post ? "article" : "website", publishedTime: post ? postFrontmatter.date || post.publishedAt : undefined, modifiedTime: post ? postFrontmatter.updated || post.updated || undefined : undefined @@ -436,13 +530,14 @@ function PostPage({ if (postState === "error") return

Error: {postError}

; return ( -
+

{title}

{description}

{publishedDate && Published {publishedDate}} {updatedDate && Updated {updatedDate}} + {isArtifact && codex} {post.readingTime && {post.readingTime} min read} {post.wordCount && {post.wordCount} words} {post.postTxId.slice(0, 8)}... @@ -453,7 +548,7 @@ function PostPage({
- {bannerTxId && ( + {bannerTxId && !isArtifact && (
{`${title}
diff --git a/src/styles.css b/src/styles.css index 7037d4c..ac0a758 100644 --- a/src/styles.css +++ b/src/styles.css @@ -60,6 +60,8 @@ a:hover { .header-row { display: flex; align-items: center; + justify-content: space-between; + gap: 18px; min-height: 72px; padding: 0 24px; } @@ -113,6 +115,27 @@ a:hover { background: var(--brand-green); } +.site-nav { + display: flex; + align-items: center; + gap: 18px; + font-size: 0.8rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.nav-link { + color: #444444; + text-decoration: none; + border-bottom: 1px solid transparent; +} + +.nav-link:hover, +.nav-link-active { + color: #111111; + border-bottom-color: #111111; +} + .content { max-width: 100%; } @@ -224,6 +247,68 @@ a:hover { transform: translateY(-50%); } +.artifact-index { + padding: 0 24px 52px; +} + +.artifact-index-header { + padding: 28px 0 22px; + border-bottom: 1px solid var(--line); +} + +.artifact-index-header h1 { + margin: 0; + font-size: clamp(2rem, 5vw, 3rem); + font-weight: 500; + line-height: 1; + letter-spacing: -0.03em; +} + +.artifact-index-header p { + margin: 12px 0 0; + color: #262626; +} + +.artifact-list { + list-style: none; + margin: 0; + padding: 0; +} + +.artifact-list-item { + padding: 18px 0 20px; + border-bottom: 1px solid var(--line-soft); +} + +.artifact-link { + display: inline-block; + font-size: 1.12rem; + font-weight: 500; + line-height: 1.25; + text-decoration: none; +} + +.artifact-link:hover { + text-decoration: underline; +} + +.artifact-item-meta { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 7px; + color: var(--muted); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + font-size: 0.75rem; + text-transform: lowercase; +} + +.artifact-list-item p { + margin: 9px 0 0; + color: #222222; +} + .post { border-top: 0; } @@ -348,6 +433,34 @@ a:hover { border: 0; } +.post-artifact .post-header { + padding-bottom: 24px; + border-bottom: 1px solid var(--line); +} + +.post-artifact .post-header h1 { + max-width: 30ch; +} + +.post-artifact .article { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + font-size: 0.94rem; + line-height: 1.62; +} + +.post-artifact .article p, +.post-artifact .article li, +.post-artifact .article blockquote { + font-size: 0.94rem; +} + +.post-artifact .article h2, +.post-artifact .article h3, +.post-artifact .article h4 { + font-family: "DM Sans", sans-serif; +} + @media (max-width: 860px) { .page { width: 100%; @@ -362,6 +475,7 @@ a:hover { } .post-card, + .artifact-index, .post-header, .post-hero, .article, @@ -382,3 +496,17 @@ a:hover { font-size: 1.38rem; } } + +@media (max-width: 520px) { + .header-row { + align-items: flex-start; + flex-direction: column; + justify-content: center; + padding-top: 14px; + padding-bottom: 14px; + } + + .site-nav { + gap: 14px; + } +}