import { Typewriter } from "./Typewriter"; import { useBars } from "~/context/bars"; import { onMount, createEffect, createSignal, Show, For } from "solid-js"; import { api } from "~/lib/api"; import { TerminalSplash } from "./TerminalSplash"; import { insertSoftHyphens } from "~/lib/client-utils"; import GitHub from "./icons/GitHub"; import LinkedIn from "./icons/LinkedIn"; import MoonIcon from "./icons/MoonIcon"; import SunIcon from "./icons/SunIcon"; export function RightBarContent() { const [isDark, setIsDark] = createSignal(false); onMount(() => { const prefersDark = window.matchMedia( "(prefers-color-scheme: dark)" ).matches; if (prefersDark) { setIsDark(true); document.documentElement.classList.add("dark"); document.documentElement.classList.remove("light"); } else { setIsDark(false); document.documentElement.classList.add("light"); document.documentElement.classList.remove("dark"); } }); const toggleDarkMode = () => { const newDarkMode = !isDark(); setIsDark(newDarkMode); if (newDarkMode) { document.documentElement.classList.add("dark"); document.documentElement.classList.remove("light"); } else { document.documentElement.classList.add("light"); document.documentElement.classList.remove("dark"); } }; return (
{/* Dark Mode Toggle */}
); } export function LeftBar() { const { setLeftBarSize, leftBarSize, leftBarVisible, setLeftBarVisible } = useBars(); let ref: HTMLDivElement | undefined; let actualWidth = 0; let touchStartX = 0; let touchStartY = 0; const [recentPosts, setRecentPosts] = createSignal( undefined ); onMount(async () => { // Fetch recent posts only on client side to avoid hydration mismatch try { const posts = await api.blog.getRecentPosts.query(); setRecentPosts(posts as any[]); } catch (error) { console.error("Failed to fetch recent posts:", error); setRecentPosts([]); } if (ref) { const updateSize = () => { actualWidth = ref?.offsetWidth || 0; setLeftBarSize(leftBarVisible() ? actualWidth : 0); }; updateSize(); const resizeObserver = new ResizeObserver((entries) => { // Use requestAnimationFrame to avoid ResizeObserver loop error requestAnimationFrame(() => { actualWidth = ref?.offsetWidth || 0; setLeftBarSize(leftBarVisible() ? actualWidth : 0); }); }); resizeObserver.observe(ref); // Swipe-to-dismiss gesture on sidebar itself (mobile only) const handleTouchStart = (e: TouchEvent) => { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; }; const handleTouchEnd = (e: TouchEvent) => { const isMobile = window.innerWidth < 768; if (!isMobile) return; // Only allow dismiss on mobile const touchEndX = e.changedTouches[0].clientX; const touchEndY = e.changedTouches[0].clientY; const deltaX = touchEndX - touchStartX; const deltaY = touchEndY - touchStartY; // Only trigger if horizontal swipe is dominant if (Math.abs(deltaX) > Math.abs(deltaY)) { // Swipe left to dismiss (at least 50px) if (deltaX < -50 && leftBarVisible()) { setLeftBarVisible(false); } } }; // Focus trap for accessibility on mobile const handleKeyDown = (e: KeyboardEvent) => { const isMobile = window.innerWidth < 768; 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) { // Shift+Tab - going backwards if (document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } } else { // Tab - going forwards if (document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } } }; ref.addEventListener("touchstart", handleTouchStart, { passive: true }); ref.addEventListener("touchend", handleTouchEnd, { passive: true }); ref.addEventListener("keydown", handleKeyDown); return () => { resizeObserver.disconnect(); ref?.removeEventListener("touchstart", handleTouchStart); ref?.removeEventListener("touchend", handleTouchEnd); ref?.removeEventListener("keydown", handleKeyDown); }; } }); // Update size when visibility changes createEffect(() => { setLeftBarSize(leftBarVisible() ? actualWidth : 0); }); // Auto-focus first element when sidebar opens on mobile createEffect(() => { const isMobile = window.innerWidth < 768; if (leftBarVisible() && isMobile && ref) { const firstFocusable = ref.querySelector( "a[href], button:not([disabled]), input:not([disabled])" ) as HTMLElement; if (firstFocusable) { // Small delay to ensure animation has started setTimeout(() => firstFocusable.focus(), 100); } } }); return ( ); } export function RightBar() { const { setRightBarSize, rightBarSize, rightBarVisible } = useBars(); let ref: HTMLDivElement | undefined; let actualWidth = 0; onMount(() => { if (ref) { const updateSize = () => { actualWidth = ref?.offsetWidth || 0; setRightBarSize(rightBarVisible() ? actualWidth : 0); }; updateSize(); const resizeObserver = new ResizeObserver((entries) => { requestAnimationFrame(() => { actualWidth = ref?.offsetWidth || 0; setRightBarSize(rightBarVisible() ? actualWidth : 0); }); }); resizeObserver.observe(ref); return () => { resizeObserver.disconnect(); }; } }); createEffect(() => { setRightBarSize(rightBarVisible() ? actualWidth : 0); }); return ( ); }