Skip to content

Commit e0962fb

Browse files
committed
feat: improve content loading
chore: update deps
1 parent 788d6c6 commit e0962fb

12 files changed

Lines changed: 142 additions & 175 deletions

File tree

frontend/bun.lock

Lines changed: 67 additions & 55 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,30 @@
2020
"pwa": "pwa-asset-generator static/favicon.svg static/pwa --background '#b4befe' --padding '10%' --icon-only --mstile --favicon --manifest static/manifest.webmanifest --index src/app.html --path-override /pwa"
2121
},
2222
"devDependencies": {
23-
"@eslint/compat": "^2.0.3",
23+
"@eslint/compat": "^2.0.5",
2424
"@eslint/js": "^10.0.1",
25-
"@iconify-json/lucide": "^1.2.98",
25+
"@iconify-json/lucide": "^1.2.102",
2626
"@sveltejs/adapter-cloudflare": "^7.2.8",
27-
"@sveltejs/kit": "^2.55.0",
27+
"@sveltejs/kit": "^2.57.1",
2828
"@sveltejs/vite-plugin-svelte": "^7.0.0",
2929
"@tailwindcss/vite": "^4.2.2",
30-
"@types/node": "^25.5.0",
30+
"@types/node": "^25.6.0",
3131
"@vite-pwa/sveltekit": "^1.1.0",
3232
"echarts": "^6.0.0",
33-
"eslint": "^10.1.0",
33+
"eslint": "^10.2.1",
3434
"eslint-config-prettier": "^10.1.8",
35-
"eslint-plugin-svelte": "^3.16.0",
36-
"globals": "^17.4.0",
37-
"prettier": "^3.8.1",
35+
"eslint-plugin-svelte": "^3.17.0",
36+
"globals": "^17.5.0",
37+
"prettier": "^3.8.3",
3838
"prettier-plugin-svelte": "^3.5.1",
39-
"svelte": "^5.54.0",
40-
"svelte-check": "^4.4.5",
39+
"svelte": "^5.55.4",
40+
"svelte-check": "^4.4.6",
4141
"svelte-echarts": "^1.0.0",
4242
"tailwindcss": "^4.2.2",
4343
"typescript": "^5.9.3",
44-
"typescript-eslint": "^8.57.1",
44+
"typescript-eslint": "^8.58.2",
4545
"unplugin-icons": "^23.0.1",
46-
"vite": "^8.0.1",
46+
"vite": "^8.0.9",
4747
"@prgm/sveltekit-progress-bar": "^3.0.2",
4848
"@catppuccin/tailwindcss": "^1.0.0"
4949
}

frontend/src/lib/charts/BarChart.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
<script lang="ts">
2+
import { browser } from '$app/environment';
23
import { Chart } from 'svelte-echarts';
34
import { init, use } from 'echarts/core';
45
import { BarChart } from 'echarts/charts';
56
import { TooltipComponent, GridComponent } from 'echarts/components';
67
import { CanvasRenderer } from 'echarts/renderers';
78
import type { UsageStat } from '$lib/types/dashboard';
89
import { createBarChartOptions } from './echarts';
10+
import Skeleton from '$lib/components/ui/Skeleton.svelte';
911
1012
use([BarChart, TooltipComponent, GridComponent, CanvasRenderer]);
1113
@@ -29,5 +31,9 @@
2931
</script>
3032

3133
<div class={className}>
32-
<Chart {init} {options} />
34+
{#if browser}
35+
<Chart {init} {options} />
36+
{:else}
37+
<Skeleton className="w-full h-full" />
38+
{/if}
3339
</div>

frontend/src/lib/charts/DateBarChart.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<script lang="ts">
2+
import { browser } from '$app/environment';
23
import { Chart } from 'svelte-echarts';
34
import { init, use } from 'echarts/core';
45
import { BarChart } from 'echarts/charts';
56
import { TooltipComponent, GridComponent } from 'echarts/components';
67
import { CanvasRenderer } from 'echarts/renderers';
78
import { createDateBarChartOptions } from './echarts';
9+
import Skeleton from '$lib/components/ui/Skeleton.svelte';
810
911
use([BarChart, TooltipComponent, GridComponent, CanvasRenderer]);
1012
@@ -30,5 +32,9 @@
3032
</script>
3133

3234
<div class={className}>
33-
<Chart {init} {options} />
35+
{#if browser}
36+
<Chart {init} {options} />
37+
{:else}
38+
<Skeleton className="w-full h-full" />
39+
{/if}
3440
</div>

frontend/src/lib/charts/PieChart.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
<script lang="ts">
2+
import { browser } from '$app/environment';
23
import { Chart } from 'svelte-echarts';
34
import { init, use } from 'echarts/core';
45
import { PieChart } from 'echarts/charts';
56
import { TooltipComponent, LegendComponent } from 'echarts/components';
67
import { CanvasRenderer } from 'echarts/renderers';
78
import type { UsageStat } from '$lib/types/dashboard';
89
import { createPieChartOptions } from './echarts';
10+
import Skeleton from '$lib/components/ui/Skeleton.svelte';
911
1012
use([PieChart, TooltipComponent, LegendComponent, CanvasRenderer]);
1113
@@ -22,5 +24,9 @@
2224
</script>
2325

2426
<div class={className}>
25-
<Chart {init} {options} />
27+
{#if browser}
28+
<Chart {init} {options} />
29+
{:else}
30+
<Skeleton className="w-full h-full" />
31+
{/if}
2632
</div>
Lines changed: 12 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,25 @@
1-
import { untrack } from 'svelte';
1+
import { navigating } from '$app/state';
22

3-
const RELOAD_SKELETON_DELAY_MS = 150;
3+
const SKELETON_DELAY_MS = 150;
44

5-
export interface DeferredDataState<T> {
6-
readonly data: T | null;
7-
readonly showSkeleton: boolean;
8-
readonly loadError: boolean;
9-
}
10-
11-
export function createDeferredData<T>(getPromise: () => Promise<T>): DeferredDataState<T> {
12-
let data = $state<T | null>(null);
13-
let showSkeleton = $state(true);
14-
let loadError = $state(false);
5+
export function useNavigationSkeleton() {
6+
let showSkeleton = $state(false);
7+
let timer: ReturnType<typeof setTimeout> | undefined;
158

169
$effect(() => {
17-
const promise = getPromise();
18-
let cancelled = false;
19-
let timer: ReturnType<typeof setTimeout> | undefined;
20-
21-
untrack(() => {
22-
loadError = false;
23-
24-
if (!data) {
10+
clearTimeout(timer);
11+
if (navigating.to) {
12+
timer = setTimeout(() => {
2513
showSkeleton = true;
26-
} else {
27-
timer = setTimeout(() => {
28-
if (!cancelled) showSkeleton = true;
29-
}, RELOAD_SKELETON_DELAY_MS);
30-
}
31-
});
32-
33-
promise
34-
.then((result) => {
35-
if (!cancelled) {
36-
clearTimeout(timer);
37-
data = result;
38-
showSkeleton = false;
39-
}
40-
})
41-
.catch(() => {
42-
clearTimeout(timer);
43-
if (!cancelled && !data) loadError = true;
44-
showSkeleton = false;
45-
});
46-
47-
return () => {
48-
cancelled = true;
49-
clearTimeout(timer);
50-
};
14+
}, SKELETON_DELAY_MS);
15+
} else {
16+
showSkeleton = false;
17+
}
5118
});
5219

5320
return {
54-
get data() {
55-
return data;
56-
},
5721
get showSkeleton() {
5822
return showSkeleton;
59-
},
60-
get loadError() {
61-
return loadError;
6223
}
6324
};
6425
}

frontend/src/routes/@[slug]/+page.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@ export const load: PageServerLoad = async ({ fetch, params }) => {
1616
};
1717

1818
return {
19-
profile: loadProfile()
19+
profile: await loadProfile()
2020
};
2121
};

frontend/src/routes/@[slug]/+page.svelte

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { replaceState } from '$app/navigation';
44
import { onMount } from 'svelte';
55
import type { PageData } from './$types';
6-
import { createDeferredData } from '$lib/utils/deferred-data.svelte';
6+
import { useNavigationSkeleton } from '$lib/utils/deferred-data.svelte';
77
import { Container, PageScaffold, SectionTitle, StatCard, EmptyState } from '$lib';
88
import { formatDuration } from '$lib/utils/time';
99
import LucideExternalLink from '~icons/lucide/external-link';
@@ -19,12 +19,12 @@
1919
2020
let { data }: Props = $props();
2121
22-
const deferred = createDeferredData(() => data.profile);
22+
const nav = useNavigationSkeleton();
2323
2424
onMount(() => {
2525
requestAnimationFrame(() => {
26-
if (deferred.data?.user.username && page.params.slug !== deferred.data.user.username) {
27-
replaceState(resolve('/@[slug]', { slug: deferred.data.user.username }), {});
26+
if (data.profile?.user.username && page.params.slug !== data.profile.user.username) {
27+
replaceState(resolve('/@[slug]', { slug: data.profile.user.username }), {});
2828
}
2929
});
3030
});
@@ -42,18 +42,18 @@
4242
};
4343
</script>
4444

45-
{#if deferred.showSkeleton}
45+
{#if nav.showSkeleton}
4646
<ProfileSkeleton />
47-
{:else if deferred.data === null}
47+
{:else if data.profile === null}
4848
<PageScaffold title="Not Found" showLastUpdated={false}>
4949
<EmptyState
5050
title="Profile not found"
5151
description="The user you're looking for doesn't exist :("
5252
className="mb-4"
5353
/>
5454
</PageScaffold>
55-
{:else if deferred.data}
56-
{@const profileData = deferred.data}
55+
{:else if data.profile}
56+
{@const profileData = data.profile}
5757
<PageScaffold title="@{profileData.user.username}" showLastUpdated={false}>
5858
<svelte:fragment slot="heading">
5959
<div class="flex items-center gap-4 mb-6">
@@ -131,12 +131,4 @@
131131
/>
132132
{/if}
133133
</PageScaffold>
134-
{:else if deferred.loadError}
135-
<PageScaffold title="Profile" showLastUpdated={false}>
136-
<EmptyState
137-
title="Failed to load profile"
138-
description="Something went wrong loading the profile data. Please try again."
139-
className="mb-4"
140-
/>
141-
</PageScaffold>
142134
{/if}

frontend/src/routes/dashboard/+page.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const load: PageServerLoad = async ({ fetch, depends, url, request }) =>
2727
};
2828

2929
return {
30-
dashboard: loadDashboard(),
30+
dashboard: await loadDashboard(),
3131
range
3232
};
3333
};

frontend/src/routes/dashboard/+page.svelte

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import type { Theme } from '$lib/stores/theme';
66
import type { PageData } from './$types';
77
import type { DashboardResponse } from '$lib/types/dashboard';
8-
import { createDeferredData } from '$lib/utils/deferred-data.svelte';
8+
import { useNavigationSkeleton } from '$lib/utils/deferred-data.svelte';
99
import { setupVisibilityRefresh } from '$lib/utils/refresh';
1010
import { Container, PageScaffold, SectionTitle, StatCard, ToggleGroup, EmptyState } from '$lib';
1111
import { noUnknownText, safeGraphData, safeText } from '$lib/utils/text';
@@ -23,10 +23,10 @@
2323
let selectedRange = $derived(data?.range || 'month');
2424
let loadedRange = $state('today');
2525
26-
const deferred = createDeferredData(() => data.dashboard);
26+
const nav = useNavigationSkeleton();
2727
2828
$effect(() => {
29-
if (deferred.data) {
29+
if (data.dashboard) {
3030
lastUpdatedAt = new Date();
3131
}
3232
});
@@ -67,40 +67,40 @@
6767
});
6868
</script>
6969

70-
{#if deferred.showSkeleton}
70+
{#if nav.showSkeleton}
7171
<DashboardSkeleton />
72-
{:else if deferred.data}
72+
{:else}
7373
{@const { topProjects, topLanguages, topEditors, topOperatingSystems } = getDerivedData(
74-
deferred.data
74+
data.dashboard
7575
)}
7676
<PageScaffold title="Dashboard" {lastUpdatedAt}>
7777
<!-- Time Range Filter -->
7878
<Container className="mb-4">
7979
<ToggleGroup options={rangeOptions} selected={selectedRange} onchange={handleRangeChange} />
8080
</Container>
8181

82-
{#if deferred.data.projects.length || deferred.data.languages.length || deferred.data.editors.length || deferred.data.operating_systems.length}
82+
{#if data.dashboard.projects.length || data.dashboard.languages.length || data.dashboard.editors.length || data.dashboard.operating_systems.length}
8383
<!-- Top Stats -->
8484
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
8585
<StatCard
8686
title="Total Time"
87-
value={deferred.data?.human_readable_total || 'None'}
87+
value={data.dashboard?.human_readable_total || 'None'}
8888
valueClass="text-xl font-semibold text-text"
8989
/>
9090
<StatCard
9191
title="Top Project"
92-
value={noUnknownText(deferred.data?.projects?.[0]?.name) || 'None'}
92+
value={noUnknownText(data.dashboard?.projects?.[0]?.name) || 'None'}
9393
valueClass="text-xl font-semibold text-text"
9494
/>
9595
<StatCard
9696
title="Top Language"
97-
value={safeText(deferred.data?.languages?.[0]?.name) || 'None'}
97+
value={safeText(data.dashboard?.languages?.[0]?.name) || 'None'}
9898
valueClass="text-xl font-semibold text-text"
9999
/>
100100
<StatCard
101101
title="Total Heartbeats"
102-
value={deferred.data?.total_heartbeats
103-
? deferred.data.total_heartbeats.toLocaleString()
102+
value={data.dashboard?.total_heartbeats
103+
? data.dashboard.total_heartbeats.toLocaleString()
104104
: '0'}
105105
valueClass="text-xl font-semibold text-text"
106106
/>
@@ -129,7 +129,7 @@
129129
{/if}
130130
</div>
131131

132-
{#if deferred.data.editors.length > 1 && deferred.data.operating_systems.length > 1}
132+
{#if data.dashboard.editors.length > 1 && data.dashboard.operating_systems.length > 1}
133133
<!-- Editors (Pie Chart) -->
134134
<div>
135135
<SectionTitle size="sm" className="mb-4">Editors</SectionTitle>
@@ -166,12 +166,4 @@
166166
/>
167167
{/if}
168168
</PageScaffold>
169-
{:else if deferred.loadError}
170-
<PageScaffold title="Dashboard" showLastUpdated={false}>
171-
<EmptyState
172-
title="Failed to load dashboard"
173-
description="Something went wrong loading your dashboard data. Please try again."
174-
className="mb-4"
175-
/>
176-
</PageScaffold>
177169
{/if}

0 commit comments

Comments
 (0)