Skip to content

Commit 32a0e51

Browse files
refactor: streamline routing and enhance dashboard skeleton
Removed the AnimatePresence component from the routing structure in App.tsx for a more straightforward implementation. Introduced a DashboardSkeleton component in dashboard.tsx to improve user experience during data loading, providing a clearer visual indication of content loading states. Updated error handling in the Dashboard to utilize a StateBanner for better user feedback. Enhanced the News component to improve error messaging with localized text. Additionally, made minor adjustments to motion components and SQL schema for download tracking.
1 parent 572708e commit 32a0e51

12 files changed

Lines changed: 174 additions & 170 deletions

File tree

apps/desktop/src/App.tsx

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { AnimatePresence } from "motion/react";
21
import { type ReactNode, useEffect } from "react";
32
import { MemoryRouter, Route, Routes, useLocation, useNavigate } from "react-router";
43
import { SWRConfig } from "swr";
@@ -19,7 +18,6 @@ import { Weather } from "./features/weather/weather";
1918
import { ErrorBoundary } from "./shared/components/error-boundary";
2019
import { Header } from "./shared/components/header";
2120
import { HeaderSlotProvider } from "./shared/components/header-slot";
22-
import { PageTransition } from "./shared/components/motion";
2321
import { TabBar } from "./shared/components/tab-bar";
2422
import { SettingsProvider, useSettings } from "./shared/context/settings-context";
2523
import { UpdaterProvider } from "./shared/context/updater-context";
@@ -91,30 +89,28 @@ function Shell() {
9189
<div className="app-window flex flex-col">
9290
<TrayNavigation />
9391
<DismissOnEscape />
94-
<AnimatePresence mode="wait" initial={false}>
95-
<Routes location={location} key={location.pathname}>
96-
{ROUTES.map((route) => (
97-
<Route
98-
key={route.path}
99-
path={route.path}
100-
element={
101-
<PageTransition className="flex min-h-0 min-w-0 flex-1 flex-col">
102-
{route.path !== "/" && route.path !== "/weather" && (
103-
<Header title={t(route.titleKey)} />
104-
)}
105-
<main
106-
className={`min-w-0 flex-1 overflow-x-hidden overflow-y-auto ${
107-
route.path === "/" ? "p-3" : route.path === "/weather" ? "" : "p-2.5"
108-
}`}
109-
>
110-
<ErrorBoundary key={route.path}>{route.element}</ErrorBoundary>
111-
</main>
112-
</PageTransition>
113-
}
114-
/>
115-
))}
116-
</Routes>
117-
</AnimatePresence>
92+
<Routes location={location}>
93+
{ROUTES.map((route) => (
94+
<Route
95+
key={route.path}
96+
path={route.path}
97+
element={
98+
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
99+
{route.path !== "/" && route.path !== "/weather" && (
100+
<Header title={t(route.titleKey)} />
101+
)}
102+
<main
103+
className={`min-w-0 flex-1 overflow-x-hidden overflow-y-auto ${
104+
route.path === "/" ? "p-3" : route.path === "/weather" ? "" : "p-2.5"
105+
}`}
106+
>
107+
<ErrorBoundary key={route.path}>{route.element}</ErrorBoundary>
108+
</main>
109+
</div>
110+
}
111+
/>
112+
))}
113+
</Routes>
118114
<RadioMiniPlayer />
119115
<TabBar />
120116
</div>

apps/desktop/src/features/calendar/dashboard.tsx

Lines changed: 92 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { useEffect, useMemo, useState } from "react";
1+
import { useCallback, useEffect, useMemo, useState } from "react";
22
import { useNavigate } from "react-router";
33
import { Card } from "../../shared/components/card";
44
import { Icon } from "../../shared/components/icon";
55
import { MonthGrid } from "../../shared/components/month-grid";
6-
import { FadeUp, Stagger } from "../../shared/components/motion";
6+
import { SkeletonBlock } from "../../shared/components/skeleton";
7+
import { StateBanner } from "../../shared/components/state-banner";
78
import { useSettings } from "../../shared/context/settings-context";
89
import {
910
api,
@@ -18,6 +19,38 @@ import { ClockRow } from "./_components/clock-row";
1819
import { DateHeader } from "./_components/date-header";
1920
import { GlanceCards } from "./_components/glance-cards";
2021

22+
/**
23+
* The dashboard at its own shape, before the data lands.
24+
*
25+
* Sized to the real layout — date header, month grid, event row, glance pair —
26+
* so the popover opens at its final height instead of growing under the
27+
* pointer. This screen used to render a single `…` here, which told the user
28+
* nothing and then jumped.
29+
*/
30+
function DashboardSkeleton() {
31+
return (
32+
<div className="space-y-2.5" aria-hidden>
33+
<div className="surface-card p-2.5">
34+
<SkeletonBlock className="h-5 w-1/2" />
35+
<SkeletonBlock className="mt-1.5 h-3 w-1/3" />
36+
</div>
37+
<div className="surface-card calendar-panel">
38+
<SkeletonBlock className="mx-auto h-3 w-2/5" />
39+
<div className="mt-2.5 grid grid-cols-7 gap-1">
40+
{Array.from({ length: 42 }, (_, cell) => (
41+
// biome-ignore lint/suspicious/noArrayIndexKey: fixed grid, never reordered
42+
<SkeletonBlock key={cell} className="aspect-square w-full" />
43+
))}
44+
</div>
45+
</div>
46+
<div className="grid grid-cols-2 gap-2.5">
47+
<SkeletonBlock className="h-[72px] w-full rounded-[10px]" />
48+
<SkeletonBlock className="h-[72px] w-full rounded-[10px]" />
49+
</div>
50+
</div>
51+
);
52+
}
53+
2154
const PROVISIONAL_YEARS = new Set([2085, 2086, 2087, 2088, 2089, 2090]);
2255
const GREG_MONTHS = [
2356
"Jan",
@@ -77,7 +110,8 @@ export function Dashboard() {
77110
return keys;
78111
}, [plans]);
79112

80-
useEffect(() => {
113+
const reload = useCallback(() => {
114+
setError(null);
81115
api
82116
.today()
83117
.then((value) => {
@@ -95,6 +129,8 @@ export function Dashboard() {
95129
.catch(() => setPlans([]));
96130
}, []);
97131

132+
useEffect(reload, [reload]);
133+
98134
useEffect(() => {
99135
if (!cursor) return;
100136
api
@@ -126,90 +162,72 @@ export function Dashboard() {
126162
};
127163

128164
if (error) {
129-
return (
130-
<Card title={t("state.unavailable")}>
131-
<p className="text-text-secondary">{error}</p>
132-
</Card>
133-
);
134-
}
135-
if (!today || !month) {
136-
return <p className="text-text-muted"></p>;
165+
return <StateBanner state={{ status: "failed", message: error }} onRetry={reload} />;
137166
}
167+
if (!today || !month) return <DashboardSkeleton />;
138168

139169
const provisional = cursor && PROVISIONAL_YEARS.has(cursor.year);
140170
const upNext = upcoming[0];
141171

142172
return (
143-
<Stagger className="space-y-2.5">
144-
<FadeUp>
145-
<DateHeader today={today} />
146-
</FadeUp>
173+
<div className="space-y-2.5">
174+
<DateHeader today={today} />
147175

148176
{modules.clocksEnabled && modules.clocks.length > 0 && (
149-
<FadeUp>
150-
<ClockRow timeZones={modules.clocks} />
151-
</FadeUp>
177+
<ClockRow timeZones={modules.clocks} />
152178
)}
153-
154-
<FadeUp>
155-
<Card className="calendar-panel">
156-
<div className="mb-2 flex items-center justify-between gap-2">
157-
<button
158-
type="button"
159-
aria-label={t("calendar.previous-month")}
160-
onClick={() => step(-1)}
161-
className="icon-btn size-7"
162-
>
163-
<span className="text-[15px] leading-none"></span>
164-
</button>
165-
<span className="min-w-0 flex-1 truncate text-center text-[11px] font-semibold tracking-[0.01em] text-text-secondary">
166-
{month.title}
167-
{monthSpan ? ` · ${monthSpan}` : ""}
168-
</span>
169-
<button
170-
type="button"
171-
aria-label={t("calendar.next-month")}
172-
onClick={() => step(1)}
173-
className="icon-btn size-7"
174-
>
175-
<span className="text-[15px] leading-none"></span>
176-
</button>
177-
</div>
178-
<MonthGrid
179-
month={month}
180-
planDays={planDays}
181-
onSelect={(day) =>
182-
day.date && navigate(`/day?y=${day.date.year}&m=${day.date.month}&d=${day.date.day}`)
183-
}
184-
/>
185-
{provisional && (
186-
<p className="mt-2 text-[10px] text-text-muted">{t("calendar.provisional")}</p>
187-
)}
188-
</Card>
189-
</FadeUp>
190-
191-
{upNext && (
192-
<FadeUp>
179+
<Card className="calendar-panel">
180+
<div className="mb-2 flex items-center justify-between gap-2">
193181
<button
194182
type="button"
195-
onClick={() => navigate("/events")}
196-
className="surface-card flex w-full items-center gap-2 px-2.5 py-2 text-left transition-transform active:scale-[0.99]"
183+
aria-label={t("calendar.previous-month")}
184+
onClick={() => step(-1)}
185+
className="icon-btn size-7"
197186
>
198-
<Icon
199-
name="festival"
200-
className="size-3.5 shrink-0 text-[color:var(--color-accent-mark)]"
201-
/>
202-
<span className="min-w-0 flex-1 truncate text-[12px]">{upNext.name}</span>
203-
<span className="shrink-0 text-[11px] text-text-muted">
204-
{relativeText(upNext.days_away, t, numerals)}
205-
</span>
187+
<span className="text-[15px] leading-none"></span>
206188
</button>
207-
</FadeUp>
208-
)}
189+
<span className="min-w-0 flex-1 truncate text-center text-[11px] font-semibold tracking-[0.01em] text-text-secondary">
190+
{month.title}
191+
{monthSpan ? ` · ${monthSpan}` : ""}
192+
</span>
193+
<button
194+
type="button"
195+
aria-label={t("calendar.next-month")}
196+
onClick={() => step(1)}
197+
className="icon-btn size-7"
198+
>
199+
<span className="text-[15px] leading-none"></span>
200+
</button>
201+
</div>
202+
<MonthGrid
203+
month={month}
204+
planDays={planDays}
205+
onSelect={(day) =>
206+
day.date && navigate(`/day?y=${day.date.year}&m=${day.date.month}&d=${day.date.day}`)
207+
}
208+
/>
209+
{provisional && (
210+
<p className="mt-2 text-[10px] text-text-muted">{t("calendar.provisional")}</p>
211+
)}
212+
</Card>
209213

210-
<FadeUp>
211-
<GlanceCards />
212-
</FadeUp>
213-
</Stagger>
214+
{upNext && (
215+
<button
216+
type="button"
217+
onClick={() => navigate("/events")}
218+
className="surface-card flex w-full items-center gap-2 px-2.5 py-2 text-left transition-transform active:scale-[0.99]"
219+
>
220+
<Icon
221+
name="festival"
222+
className="size-3.5 shrink-0 text-[color:var(--color-accent-mark)]"
223+
/>
224+
<span className="min-w-0 flex-1 truncate text-[12px]">{upNext.name}</span>
225+
<span className="shrink-0 text-[11px] text-text-muted">
226+
{relativeText(upNext.days_away, t, numerals)}
227+
</span>
228+
</button>
229+
)}
230+
<GlanceCards />
231+
</div>
214232
);
215233
}

apps/desktop/src/features/news/news.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ export function News() {
163163
)}
164164

165165
{failed.length > 0 && (
166-
<p className="mt-2 text-[10px] text-text-muted">Could not reach {failed.join(", ")}.</p>
166+
<p className="mt-2 text-[11px] text-text-secondary">
167+
{t("news.could-not-reach").replace("{sources}", failed.join(", "))}
168+
</p>
167169
)}
168170

169171
{freshness && items.length > 0 && (

apps/desktop/src/i18n/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,5 +388,6 @@
388388
"error.render-failed-hint": "The rest of Sajilo is still working.",
389389
"state.failed": "Couldn't reach the source",
390390
"state.failed-hint": "Check your connection, then try again.",
391-
"state.not-yet-hint": "This fills in the next time Sajilo refreshes."
391+
"state.not-yet-hint": "This fills in the next time Sajilo refreshes.",
392+
"news.could-not-reach": "Could not reach {sources}."
392393
}

apps/desktop/src/i18n/ne.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,5 +388,6 @@
388388
"error.render-failed-hint": "साजिलोका अरू भाग चलिरहेका छन्।",
389389
"state.failed": "स्रोतसम्म पुग्न सकिएन",
390390
"state.failed-hint": "इन्टरनेट जडान जाँचेर फेरि प्रयास गर्नुहोस्।",
391-
"state.not-yet-hint": "साजिलोले अर्को पटक अद्यावधिक गर्दा देखिनेछ।"
391+
"state.not-yet-hint": "साजिलोले अर्को पटक अद्यावधिक गर्दा देखिनेछ।",
392+
"news.could-not-reach": "{sources} सम्म पुग्न सकिएन।"
392393
}

apps/desktop/src/shared/components/motion.tsx

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,9 @@
11
import { type HTMLMotionProps, motion } from "motion/react";
22
import type { ReactNode } from "react";
3-
import { easeOut, fadeIn, fadeUp, spring, stagger, useMotionEnabled } from "../lib/motion";
3+
import { fadeUp, spring, stagger, useMotionEnabled } from "../lib/motion";
44

55
type MotionDivProps = HTMLMotionProps<"div">;
66

7-
/** Route / panel enter — short fade + lift. */
8-
export function PageTransition({
9-
children,
10-
className,
11-
}: {
12-
children: ReactNode;
13-
className?: string;
14-
}) {
15-
const enabled = useMotionEnabled();
16-
if (!enabled) return <div className={className}>{children}</div>;
17-
18-
return (
19-
<motion.div
20-
className={className}
21-
initial={{ opacity: 0, y: 8 }}
22-
animate={{ opacity: 1, y: 0 }}
23-
exit={{ opacity: 0, y: -5 }}
24-
transition={{ ...spring.snappy, opacity: { duration: 0.18, ease: easeOut } }}
25-
>
26-
{children}
27-
</motion.div>
28-
);
29-
}
30-
317
/** Stagger children on mount (dashboard sections, lists). */
328
export function Stagger({ children, className }: { children: ReactNode; className?: string }) {
339
const enabled = useMotionEnabled();
@@ -59,21 +35,6 @@ export function FadeUp({
5935
);
6036
}
6137

62-
export function FadeIn({ children, className }: { children: ReactNode; className?: string }) {
63-
const enabled = useMotionEnabled();
64-
if (!enabled) return <div className={className}>{children}</div>;
65-
66-
return (
67-
<motion.div
68-
className={className}
69-
variants={fadeIn}
70-
transition={{ duration: 0.2, ease: easeOut }}
71-
>
72-
{children}
73-
</motion.div>
74-
);
75-
}
76-
7738
/** Interactive card / row — subtle scale on press. */
7839
export function Pressable({
7940
children,

apps/desktop/src/shared/components/skeleton.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
*/
1111
export function SkeletonBlock({ className }: { className?: string }) {
1212
return (
13-
<div
14-
className={`animate-pulse rounded bg-surface-hover${className ? ` ${className}` : ""}`}
15-
/>
13+
<div className={`animate-pulse rounded bg-surface-hover${className ? ` ${className}` : ""}`} />
1614
);
1715
}
1816

apps/desktop/src/shared/components/state-banner.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,7 @@ export function StateBanner({
5353
role="status"
5454
className="mb-2 flex items-center gap-1.5 rounded-lg border border-[color:var(--color-accent-mark)] bg-surface px-2 py-1.5 text-[11px] text-text"
5555
>
56-
<Icon
57-
name="clock"
58-
className="size-3.5 shrink-0 text-[color:var(--color-accent-mark)]"
59-
/>
56+
<Icon name="clock" className="size-3.5 shrink-0 text-[color:var(--color-accent-mark)]" />
6057
<span className="min-w-0 flex-1">
6158
{state.since ? `${t("state.stale-since")} ${state.since}` : t("state.stale")}
6259
</span>

0 commit comments

Comments
 (0)