import { Typewriter } from "./Typewriter"; import { useBars } from "~/context/bars"; import { useAuth } from "~/context/auth"; import { revalidateAuth } from "~/lib/auth-query"; import { onMount, createSignal, Show, For, onCleanup, type JSX } from "solid-js"; import { api } from "~/lib/api"; import { insertSoftHyphens, glitchText } from "~/lib/client-utils"; import GitHub from "./icons/GitHub"; import LinkedIn from "./icons/LinkedIn"; import { RecentCommits } from "./RecentCommits"; import { ActivityHeatmap } from "./ActivityHeatmap"; import { DarkModeToggle } from "./DarkModeToggle"; import { SkeletonBox, SkeletonText } from "./SkeletonLoader"; import { env } from "~/env/client"; import { A, useNavigate, useLocation } from "@solidjs/router"; import { BREAKPOINTS } from "~/config"; import { useSite } from "~/context/SiteContext"; import { NAV_CONFIG, BACK_TO_FRENO, filterNavByAuth, type NavItem, type NavIcon } from "~/lib/nav-config"; function formatDomainName(url: string): string { const domain = url.split("://")[1]?.split(":")[0] ?? url; const withoutWww = domain.replace(/^www\./i, ""); return withoutWww.charAt(0).toUpperCase() + withoutWww.slice(1); } function getThumbnailUrl(bannerPhoto: string | null): string { if (!bannerPhoto) return "/blueprint.jpg"; const match = bannerPhoto.match(/^(.+)(\.[^.]+)$/); if (match) { return `${match[1]}-small${match[2]}`; } return bannerPhoto; } interface GitCommit { sha: string; message: string; author: string; date: string; repo: string; url: string; } interface ContributionDay { date: string; count: number; } // Four independent cached promises — first RightBarContent instance to mount // starts each fetch; the second gets the already-in-flight promise. let ghCommitsPromise: Promise | null = null; let gtCommitsPromise: Promise | null = null; let ghActivityPromise: Promise | null = null; let gtActivityPromise: Promise | null = null; function getGhCommitsPromise(): Promise { return (ghCommitsPromise ??= api.gitActivity.getGitHubCommits .query({ limit: 6 }) .catch(() => [])); } function getGtCommitsPromise(): Promise { return (gtCommitsPromise ??= api.gitActivity.getGiteaCommits .query({ limit: 6 }) .catch(() => [])); } function getGhActivityPromise(): Promise { return (ghActivityPromise ??= api.gitActivity.getGitHubActivity .query() .catch(() => [])); } function getGtActivityPromise(): Promise { return (gtActivityPromise ??= api.gitActivity.getGiteaActivity .query() .catch(() => [])); } // ── Subdomain nav rendering (task 04) ───────────────────────────────────── // // The main site retains its bespoke LeftBar / RightBarContent rendering // unchanged (Recent Posts, auth-aware Account/Login/SignOut, admin links, // RecentCommits + ActivityHeatmap widgets, the "What's this?" glitch button). // Subdomain sites render a simplified, brand-colored shell that iterates // `NAV_CONFIG[site]` + a "back to freno.me" affordance, and deliberately // skips the web-auth (freno.me JWT) widgets — Nessa uses Clerk; Lineage uses // its mobile JWT; neither should surface web login state. /** Inline icon resolver keyed by `NavIcon` (kept out of the pure nav-config). */ function NavIconSvg(props: { icon?: NavIcon; size?: number }): JSX.Element { const size = () => props.size ?? 22; const cls = "shaker rounded-full p-2"; const common = (viewBox: string, path: JSX.Element) => ( {path} ); switch (props.icon) { case "home": return common( "0 0 576 512", ); case "blog": return common( "0 0 448 512", ); case "downloads": return common( "0 0 512 512", ); case "resume": return common( "0 0 384 512", ); case "contact": return common( "0 0 512 512", ); case "privacy": return common( "0 0 512 512", ); case "deletion": return common( "0 0 448 512", ); case "back": return common( "0 0 512 512", ); case "github": return ; case "linkedin": return ; default: return ; } } /** * Subdomain nav link — renders an internal (`A`) or external (`a`) link with * a resolved icon. Uses the same hover affordances as the main-site links. */ function SubdomainNavLink(props: { item: NavItem; onClick: () => void }) { const inner = ( <> {props.item.label} ); if (props.item.external) { return ( {inner} ); } return ( {inner} ); } /** The shared simplified nav list rendered by both subdomain bars. */ function SubdomainNavList(props: { onClick: () => void }) { const site = useSite(); // Auth items intentionally omitted — subdomains don't use the web (freno.me) // JWT auth (Nessa uses Clerk; Lineage uses its mobile JWT), so no subdomain // nav item sets showLoggedIn/showLoggedOut today. filterNavByAuth is therefore // a no-op right now but is kept so future admin items behave correctly // without re-touching the renderer. Filtering here keeps LeftBar + RightBar // consistent. return ( {(item) => } ); } /** Brand heading — display name in the site's brand color. */ function SubdomainBrand() { const site = useSite(); const accent = () => `color: ${site().brandColor}`; return (

{site().displayName}

); } /** "Back to freno.me" affordance rendered on every subdomain site. */ function BackToFrenoLink() { return ( {BACK_TO_FRENO.label} ); } /** Inner content for the LeftBar on subdomain sites. */ function SubdomainLeftBarContent() { const { setLeftBarVisible } = useBars(); const handleLinkClick = () => { if ( typeof window !== "undefined" && window.innerWidth < BREAKPOINTS.MOBILE_MAX_WIDTH ) { setLeftBarVisible(false); } }; return (
{/* Auth items intentionally omitted — see SubdomainNavList. */}

{/* Mobile-only secondary column mirror of the right bar. */}
); } /** Inner content for the RightBar on subdomain sites (desktop only). */ function SubdomainRightBarContent() { const { setLeftBarVisible } = useBars(); const handleLinkClick = () => { if ( typeof window !== "undefined" && window.innerWidth < BREAKPOINTS.MOBILE_MAX_WIDTH ) { setLeftBarVisible(false); } }; return (


); } // ── Main-site RightBar (unchanged) ─────────────────────────────────────── function MainRightBarContent() { const { setLeftBarVisible } = useBars(); const [githubCommits, setGithubCommits] = createSignal([]); const [giteaCommits, setGiteaCommits] = createSignal([]); const [githubActivity, setGithubActivity] = createSignal( [] ); const [giteaActivity, setGiteaActivity] = createSignal([]); const [githubCommitsLoading, setGithubCommitsLoading] = createSignal(true); const [giteaCommitsLoading, setGiteaCommitsLoading] = createSignal(true); const handleLinkClick = () => { if ( typeof window !== "undefined" && window.innerWidth < BREAKPOINTS.MOBILE_MAX_WIDTH ) { setLeftBarVisible(false); } }; onMount(() => { setTimeout(() => { getGhCommitsPromise().then((commits) => { setGithubCommits(commits.slice(0, 3)); setGithubCommitsLoading(false); }); // Deduplicate Gitea against whatever GitHub has resolved by the time this lands getGtCommitsPromise().then((gtCommits) => { const ghShas = new Set(githubCommits().map((c) => c.sha)); setGiteaCommits( gtCommits.filter((c) => !ghShas.has(c.sha)).slice(0, 3) ); setGiteaCommitsLoading(false); }); getGhActivityPromise().then((activity) => setGithubActivity(activity)); getGtActivityPromise().then((activity) => setGiteaActivity(activity)); }, 0); }); return ( ); } export function RightBarContent() { const site = useSite(); return ( }> ); } // ── Main-site LeftBar content (unchanged) ──────────────────────────────── function MainLeftBarContent() { const { leftBarVisible, setLeftBarVisible } = useBars(); const location = useLocation(); const { isAuthenticated, email, isAdmin } = useAuth(); let ref: HTMLDivElement | undefined; const [recentPosts, setRecentPosts] = createSignal( undefined ); const [isMounted, setIsMounted] = createSignal(false); const [signOutLoading, setSignOutLoading] = createSignal(false); const [getLostText, setGetLostText] = createSignal("What's this?"); const [getLostVisible, setGetLostVisible] = createSignal(false); const handleLinkClick = () => { if ( typeof window !== "undefined" && window.innerWidth < BREAKPOINTS.MOBILE_MAX_WIDTH ) { setLeftBarVisible(false); } }; const handleSignOut = async () => { setSignOutLoading(true); try { await api.auth.signOut.mutate(); revalidateAuth(); // Clear auth state immediately window.location.href = "/"; } catch (error) { console.error("Sign out failed:", error); setSignOutLoading(false); } }; onMount(() => { setIsMounted(true); const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`"; const originalText = "What's this?"; let glitchInterval: NodeJS.Timeout; let animationFrame: number; setTimeout(() => { setGetLostVisible(true); let currentIndex = 0; let lastUpdate = 0; const updateInterval = 80; // ms between updates const revealAnimation = (timestamp: number) => { if (timestamp - lastUpdate >= updateInterval) { if (currentIndex <= originalText.length) { let displayText = originalText.substring(0, currentIndex); if (currentIndex < originalText.length) { const remaining = originalText.length - currentIndex; for (let i = 0; i < remaining; i++) { displayText += glitchChars[Math.floor(Math.random() * glitchChars.length)]; } } setGetLostText(displayText); currentIndex++; lastUpdate = timestamp; } else { setGetLostText(originalText); // Occasional glitch effect after reveal glitchInterval = glitchText(originalText, setGetLostText, 200, 80); return; } } animationFrame = requestAnimationFrame(revealAnimation); }; animationFrame = requestAnimationFrame(revealAnimation); }, 500); const fetchData = async () => { try { const posts = await api.blog.getRecentPosts.query(); setRecentPosts(posts as any[]); } catch (error) { console.error("Failed to fetch recent posts:", error); setRecentPosts([]); } }; setTimeout(() => { fetchData(); }, 0); }); const navigate = useNavigate(); return ( <>

{formatDomainName(env.VITE_DOMAIN)}


); } export function LeftBar() { const { leftBarVisible, setLeftBarVisible } = useBars(); const site = useSite(); let ref: HTMLDivElement | undefined; const [windowWidth, setWindowWidth] = createSignal( typeof window !== "undefined" ? window.innerWidth : BREAKPOINTS.MOBILE_MAX_WIDTH ); onMount(() => { const handleResize = () => { setWindowWidth(window.innerWidth); }; window.addEventListener("resize", handleResize); if (ref) { const handleKeyDown = (e: KeyboardEvent) => { const isMobile = window.innerWidth < BREAKPOINTS.MOBILE_MAX_WIDTH; if (!isMobile || !leftBarVisible()) return; if (e.key === "Tab") { const focusableElements = ref?.querySelectorAll( 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' ); if (!focusableElements || focusableElements.length === 0) return; const firstElement = focusableElements[0] as HTMLElement; const lastElement = focusableElements[ focusableElements.length - 1 ] as HTMLElement; if (e.shiftKey) { if (document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } } else { if (document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } } }; ref.addEventListener("keydown", handleKeyDown); onCleanup(() => { ref?.removeEventListener("keydown", handleKeyDown); window.removeEventListener("resize", handleResize); }); } else { onCleanup(() => { window.removeEventListener("resize", handleResize); }); } }); const getMainNavStyles = () => { const baseStyles = { "transition-timing-function": "cubic-bezier(0.4, 0, 0.2, 1)", width: "250px", "padding-top": "env(safe-area-inset-top)", "padding-bottom": "env(safe-area-inset-bottom)" }; const shadowStyle = windowWidth() >= BREAKPOINTS.MOBILE_MAX_WIDTH ? { "box-shadow": "inset -6px 0 16px -6px rgba(0, 0, 0, 0.1)" } : { "box-shadow": "0 10px 10px 0 rgba(0, 0, 0, 0.2)" }; return { ...baseStyles, ...shadowStyle }; }; // Subdomain sites sport an accent border / shadow tinted by the brand color // ("bars render appropriately styled per site — brand color hint from // SITE_CONFIG"). Main keeps the existing neutral styling. const accentBorder = () => site().id === "main" ? undefined : { "border-color": site().brandColor }; return ( ); } export function RightBar() { const { rightBarVisible } = useBars(); let ref: HTMLDivElement | undefined; return ( ); }