import { Downasaur, Github, Home as HomeIcon, Image, ImageDelete, Mail, Notes, } from "@nsmr/pixelart-react"; import { useEffect, useState } from "react"; import { Link, Route, Routes, useNavigate, useParams } from "react-router-dom"; import { AskAI } from "@/components/ask-ai"; import { ProjectListItem } from "@/components/ProjectListItem"; import { TimeSince } from "@/components/time-since"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { type ProjectEntry, projectList, projects } from "@/projects"; import { locationPhotos, locations } from "@/travel"; import { ThemeToggle } from "./components/theme-toggle"; import { Button } from "./components/ui/button"; import { cn } from "./lib/utils"; const asciiFiles = [ "cat-sleep.txt", "polar-bear.txt", "penguin-surfboard.txt", "cat-shock.txt", "exclamation.txt", "fat-cat-head.txt", "grumpy-dog.txt", "cat-peek.txt", "cat-loaf.txt", ]; const homeTabs = ["work", "travel"] as const; type HomeTab = (typeof homeTabs)[number]; function App() { return ( } /> } /> } /> ); } export default App; function Home() { const isDevMode = import.meta.env.VITE_PUBLIC_DEV === "1"; const isTabsEnabled = import.meta.env.VITE_TABS === "1"; const navigate = useNavigate(); const [asciiArt, setAsciiArt] = useState(""); const [activeTab, setActiveTab] = useState("work"); const [activeIndex, setActiveIndex] = useState(null); const [activeLocation, setActiveLocation] = useState(null); const [activePhoto, setActivePhoto] = useState(null); const [hasPointerInteraction, setHasPointerInteraction] = useState(false); const [asciiFile] = useState( () => asciiFiles[Math.floor(Math.random() * asciiFiles.length)], ); const sortedProjects: ProjectEntry[] = [...projectList].sort( (a, b) => parseDate(b.metadata.date).getTime() - parseDate(a.metadata.date).getTime(), ); const visibleProjects = sortedProjects.filter( (project) => isDevMode || !project.metadata.hidden, ); useEffect(() => { let isActive = true; fetch(`/ascii/${asciiFile}`) .then((response) => response.text()) .then((text) => { if (isActive) setAsciiArt(text); }); return () => { isActive = false; }; }, [asciiFile]); useEffect(() => { setActiveIndex((prev) => { if (visibleProjects.length === 0) return null; if (prev === null) return null; return Math.min(prev, visibleProjects.length - 1); }); }, [visibleProjects.length]); useEffect(() => { if (visibleProjects.length === 0) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented || event.isComposing) return; if (event.metaKey || event.ctrlKey || event.altKey) return; if (isTabsEnabled && event.key === "Tab") { event.preventDefault(); setActiveTab((prev) => { const currentIndex = homeTabs.indexOf(prev); const safeIndex = currentIndex === -1 ? 0 : currentIndex; const nextIndex = (safeIndex + 1) % homeTabs.length; return homeTabs[nextIndex]; }); return; } if (isInteractiveTarget(event.target)) return; const key = event.key.length === 1 ? event.key.toLowerCase() : event.key; const isDesktop = window.matchMedia("(min-width: 768px)").matches; const columns = isDesktop ? 2 : 1; let delta = 0; if (key === "ArrowLeft" || key === "h") delta = -1; if (key === "ArrowRight" || key === "l") delta = 1; if (key === "ArrowUp" || key === "k") delta = -columns; if (key === "ArrowDown" || key === "j") delta = columns; if (delta !== 0) { event.preventDefault(); setActiveIndex((prev) => { if (prev === null) return 0; const next = Math.max( 0, Math.min(visibleProjects.length - 1, prev + delta), ); return next; }); return; } if (key === "Enter") { if (activeIndex === null) return; event.preventDefault(); const target = visibleProjects[activeIndex]; if (!target) return; navigate(`/projects/${target.metadata.slug}`); } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [activeIndex, navigate, visibleProjects]); useEffect(() => { const enablePointerInteraction = () => { setHasPointerInteraction(true); }; window.addEventListener("pointermove", enablePointerInteraction, { once: true, }); window.addEventListener("pointerdown", enablePointerInteraction, { once: true, }); return () => { window.removeEventListener("pointermove", enablePointerInteraction); window.removeEventListener("pointerdown", enablePointerInteraction); }; }, []); return ( {asciiArt ? ( {asciiArt} ) : null} Oliver Bryan hex248 / ob248@proton.me / CV Age: {isTabsEnabled ? ( setActiveTab(value as HomeTab)} className="w-full max-w-5xl gap-0" > Work Travel {visibleProjects.map((project, index) => ( ))} {locations.map((location, index) => ( <> { setActivePhoto(null); setActiveLocation((prev) => prev === index ? null : index, ); }} variant="dummy" size="sm" > {location.city}, {location.country} - {location.date} {activeLocation === index && (locationPhotos[location.id].length === 0 ? ( No photos available ) : ( {locationPhotos[location.id].map((photo) => ( { const path = `/travel/${location.city} ${location.country} ${location.date}/${photo}`; setActivePhoto((prev) => prev === path ? null : path, ); }} className={cn( "flex text-sm border cursor-pointer hover:border-accent items-center justify-start p-0 pl-2 ", )} variant="dummy" size="sm" > {photo} ))} {activePhoto ? ( ) : ( No photo selected )} ))} > ))} ) : ( {visibleProjects.map((project, index) => ( ))} )} arrows or hjkl, then enter ); } function isInteractiveTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) return false; if (target.isContentEditable) return true; const tagName = target.tagName; return ( tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT" || tagName === "BUTTON" || tagName === "A" ); } function ProjectRoute() { const { slug } = useParams(); if (!slug || !projects[slug]) return ; const { Component } = projects[slug]; return ; } function NotFound() { return ( ? 404 Not Found ); } // function NotFound() { // return ( // // // // // // // ); // } function parseDate(dateStr: string): Date { const lower = dateStr.toLowerCase(); if (lower.includes("q1")) return new Date("2023-01-01"); if (lower.includes("q2")) return new Date("2023-04-01"); if (lower.includes("q3")) return new Date("2023-07-01"); if (lower.includes("q4")) return new Date("2023-10-01"); const months: Record = { january: 0, february: 1, march: 2, april: 3, may: 4, june: 5, july: 6, august: 7, september: 8, october: 9, november: 10, december: 11, }; for (const [monthName, monthIndex] of Object.entries(months)) { if (lower.includes(monthName)) { const yearMatch = dateStr.match(/\b(20\d{2})\b/); if (yearMatch) { return new Date(Number.parseInt(yearMatch[1], 10), monthIndex, 1); } } } const yearMatch = dateStr.match(/\b(20\d{2})\b/); if (yearMatch) { return new Date(Number.parseInt(yearMatch[1], 10), 0, 1); } return new Date(0); }
{asciiArt}
arrows or hjkl, then enter