diff --git a/src/components/ActivityHeatmap.tsx b/src/components/ActivityHeatmap.tsx index e0672a7..c3ceacb 100644 --- a/src/components/ActivityHeatmap.tsx +++ b/src/components/ActivityHeatmap.tsx @@ -1,4 +1,5 @@ -import { Component, For, createMemo } from "solid-js"; +import { Component, For, createMemo, Show } from "solid-js"; +import { SkeletonBox } from "./SkeletonLoader"; interface ContributionDay { date: string; @@ -63,26 +64,50 @@ export const ActivityHeatmap: Component<{ return (

{props.title}

-
- - {(week) => ( -
- - {(day) => ( -
+ 0} + fallback={ +
+ {/* Skeleton grid matching heatmap dimensions */} +
+ + {() => ( +
+ + {() => } + +
)}
- )} - -
+ {/* Centered spinner overlay */} +
+ +
+
+ } + > +
+ + {(week) => ( +
+ + {(day) => ( +
+ )} + +
+ )} +
+
+
Less
diff --git a/src/components/Bars.tsx b/src/components/Bars.tsx index 670a903..04a0734 100644 --- a/src/components/Bars.tsx +++ b/src/components/Bars.tsx @@ -1,6 +1,13 @@ import { Typewriter } from "./Typewriter"; import { useBars } from "~/context/bars"; -import { onMount, createEffect, createSignal, Show, For } from "solid-js"; +import { + onMount, + createEffect, + createSignal, + Show, + For, + onCleanup +} from "solid-js"; import { api } from "~/lib/api"; import { TerminalSplash } from "./TerminalSplash"; import { insertSoftHyphens } from "~/lib/client-utils"; @@ -9,6 +16,7 @@ import LinkedIn from "./icons/LinkedIn"; import { RecentCommits } from "./RecentCommits"; import { ActivityHeatmap } from "./ActivityHeatmap"; import { DarkModeToggle } from "./DarkModeToggle"; +import { SkeletonBox, SkeletonText } from "./SkeletonLoader"; interface GitCommit { sha: string; @@ -33,25 +41,35 @@ export function RightBarContent() { const [giteaActivity, setGiteaActivity] = createSignal([]); const [loading, setLoading] = createSignal(true); - onMount(async () => { + onMount(() => { // Fetch all data client-side only to avoid hydration mismatch - try { - const [ghCommits, gtCommits, ghActivity, gtActivity] = await Promise.all([ - api.gitActivity.getGitHubCommits.query({ limit: 3 }).catch(() => []), - api.gitActivity.getGiteaCommits.query({ limit: 3 }).catch(() => []), - api.gitActivity.getGitHubActivity.query().catch(() => []), - api.gitActivity.getGiteaActivity.query().catch(() => []) - ]); + const fetchData = async () => { + try { + const [ghCommits, gtCommits, ghActivity, gtActivity] = + await Promise.all([ + api.gitActivity.getGitHubCommits + .query({ limit: 3 }) + .catch(() => []), + api.gitActivity.getGiteaCommits.query({ limit: 3 }).catch(() => []), + api.gitActivity.getGitHubActivity.query().catch(() => []), + api.gitActivity.getGiteaActivity.query().catch(() => []) + ]); - setGithubCommits(ghCommits); - setGiteaCommits(gtCommits); - setGithubActivity(ghActivity); - setGiteaActivity(gtActivity); - } catch (error) { - console.error("Failed to fetch git activity:", error); - } finally { - setLoading(false); - } + setGithubCommits(ghCommits); + setGiteaCommits(gtCommits); + setGithubActivity(ghActivity); + setGiteaActivity(gtActivity); + } catch (error) { + console.error("Failed to fetch git activity:", error); + } finally { + setLoading(false); + } + }; + + // Defer API calls to next tick to allow initial render to complete first + setTimeout(() => { + fetchData(); + }, 0); }); return ( @@ -166,43 +184,11 @@ export function LeftBar() { } }; - onMount(async () => { + onMount(() => { // Mark as mounted to avoid hydration mismatch setIsMounted(true); - // 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([]); - } - - // Fetch user info client-side only to avoid hydration mismatch - try { - const response = await fetch("/api/trpc/user.getProfile", { - method: "GET" - }); - - if (response.ok) { - const result = await response.json(); - if (result.result?.data) { - setUserInfo({ - email: result.result.data.email, - isAuthenticated: true - }); - } else { - setUserInfo({ email: null, isAuthenticated: false }); - } - } else { - setUserInfo({ email: null, isAuthenticated: false }); - } - } catch (error) { - console.error("Failed to fetch user info:", error); - setUserInfo({ email: null, isAuthenticated: false }); - } - + // Setup ResizeObserver FIRST (synchronous) - this allows bar sizing to happen immediately if (ref) { const updateSize = () => { actualWidth = ref?.offsetWidth || 0; @@ -282,13 +268,54 @@ export function LeftBar() { ref.addEventListener("touchend", handleTouchEnd, { passive: true }); ref.addEventListener("keydown", handleKeyDown); - return () => { + onCleanup(() => { resizeObserver.disconnect(); ref?.removeEventListener("touchstart", handleTouchStart); ref?.removeEventListener("touchend", handleTouchEnd); ref?.removeEventListener("keydown", handleKeyDown); - }; + }); } + + // Fetch data asynchronously AFTER sizing setup (non-blocking) + const fetchData = 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([]); + } + + // Fetch user info client-side only to avoid hydration mismatch + try { + const response = await fetch("/api/trpc/user.getProfile", { + method: "GET" + }); + + if (response.ok) { + const result = await response.json(); + if (result.result?.data) { + setUserInfo({ + email: result.result.data.email, + isAuthenticated: true + }); + } else { + setUserInfo({ email: null, isAuthenticated: false }); + } + } else { + setUserInfo({ email: null, isAuthenticated: false }); + } + } catch (error) { + console.error("Failed to fetch user info:", error); + setUserInfo({ email: null, isAuthenticated: false }); + } + }; + + // Defer API calls to next tick to allow initial render/sizing to complete first + setTimeout(() => { + fetchData(); + }, 0); }); // Update size when visibility changes @@ -365,7 +392,23 @@ export function LeftBar() {