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 } 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"; 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; } export function RightBarContent() { const { setLeftBarVisible } = useBars(); const [githubCommits, setGithubCommits] = createSignal([]); const [giteaCommits, setGiteaCommits] = createSignal([]); const [githubActivity, setGithubActivity] = createSignal( [] ); const [giteaActivity, setGiteaActivity] = createSignal([]); const [loading, setLoading] = createSignal(true); const handleLinkClick = () => { if ( typeof window !== "undefined" && window.innerWidth < BREAKPOINTS.MOBILE_MAX_WIDTH ) { setLeftBarVisible(false); } }; onMount(() => { const fetchData = async () => { try { // Fetch more commits to account for deduplication const [ghCommits, gtCommits, ghActivity, gtActivity] = await Promise.all([ api.gitActivity.getGitHubCommits .query({ limit: 6 }) .catch(() => []), api.gitActivity.getGiteaCommits.query({ limit: 6 }).catch(() => []), api.gitActivity.getGitHubActivity.query().catch(() => []), api.gitActivity.getGiteaActivity.query().catch(() => []) ]); // Take first 3 from GitHub const displayedGithubCommits = ghCommits.slice(0, 3); // Deduplicate Gitea commits - only against the 3 shown in GitHub section const githubShas = new Set(displayedGithubCommits.map((c) => c.sha)); const uniqueGiteaCommits = gtCommits.filter( (commit) => !githubShas.has(commit.sha) ); setGithubCommits(displayedGithubCommits); setGiteaCommits(uniqueGiteaCommits.slice(0, 3)); setGithubActivity(ghActivity); setGiteaActivity(gtActivity); } catch (error) { console.error("Failed to fetch git activity:", error); } finally { setLoading(false); } }; setTimeout(() => { fetchData(); }, 0); }); return ( ); } export function LeftBar() { 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 [windowWidth, setWindowWidth] = createSignal( typeof window !== "undefined" ? window.innerWidth : BREAKPOINTS.MOBILE_MAX_WIDTH ); 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 handleResize = () => { setWindowWidth(window.innerWidth); }; window.addEventListener("resize", handleResize); 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); 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); clearInterval(glitchInterval); if (animationFrame) cancelAnimationFrame(animationFrame); window.removeEventListener("resize", handleResize); }); } else { onCleanup(() => { clearInterval(glitchInterval); if (animationFrame) cancelAnimationFrame(animationFrame); window.removeEventListener("resize", handleResize); }); } 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(); 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 }; }; return ( ); } export function RightBar() { const { rightBarVisible } = useBars(); let ref: HTMLDivElement | undefined; return ( ); }