Appearance
Core UI Features: Dashboard & Progress Tracking
Part 1 of 5 in the Core UI Features series.
Task 1 documented the infrastructure underneath Ediky — Firestore, SQLite, IPC. This series documents the layer students actually touch: the dashboard, notes, theming, search, notifications, and the performance work that keeps all of it fast. We start where every session starts — the dashboard.
What a student sees first
Dashboard.jsx (HomePage.jsx on desktop, a direct port) composes a stack of independent widgets under MainLayout: stat cards, a weekly activity chart (Recharts BarChart), a preparation breakdown (DSA / CS Fundamentals / Aptitude / CP), a grid of active-subject cards with per-subject percentages, an upcoming-OA panel, and a revision-due count. Each widget is its own component reading its own data source — nothing here is one big query.
That last point matters for accuracy: not every widget is wired to live data yet. SubjectsProgress.jsx is real, driven by useUserProgress(). StatsCards.jsx and WeeklyChart.jsx currently render fixture data while the aggregate-stats pipeline is finished, and the calendar-backed panels (CalendarPanel, upcoming-OA list) pull from the same event data that drives Scheduling & Reminders rather than a dashboard-specific source. Documenting that honestly is more useful than describing an idealized dashboard that doesn't match the repo — the architecture below is what's real today.
Real-time progress: web listener vs. desktop refresh
On web, useUserProgress opens a single Firestore onSnapshot on progress/{uid} when the dashboard mounts. Firestore pushes a new snapshot the instant any client writes a solve, and React re-renders with the new percentages — no polling, no manual refresh.
rendering diagram…
drag to pan · ctrl / ⌘ + scroll to zoom
Desktop can't hold an open socket to Firestore the way a browser tab can, so it doesn't try to. useUserProgress on desktop reads once from SQLite on mount (getMeta/setMeta) and stays put until something tells it to re-read. That "something" is a plain browser event — CONTENT_SYNCED_EVENT, dispatched on window after invoke("sync_content") finishes — not a Tauri backend push and not a polling loop. It's event-driven, just triggered by content sync rather than by Firestore's fanout. Desktop Features & Data Flows covers the SQLite side of that sync in full.
javascript
// Simplified real shape of useUserProgress (web)
function useUserProgress(uid) {
const [progress, setProgress] = useState(FALLBACK_PROGRESS);
useEffect(() => {
return onSnapshot(doc(db, "progress", uid), (snap) => {
setProgress(snap.data() ?? FALLBACK_PROGRESS);
});
}, [uid]);
return progress;
}
// Desktop equivalent: read once, refresh on a named DOM event
function useUserProgressDesktop(uid) {
const [progress, setProgress] = useState(FALLBACK_PROGRESS);
const load = useCallback(() => getMeta(uid).then(setProgress), [uid]);
useEffect(() => {
load();
window.addEventListener(CONTENT_SYNCED_EVENT, load);
return () => window.removeEventListener(CONTENT_SYNCED_EVENT, load);
}, [load]);
return progress;
}| Web | Desktop | |
|---|---|---|
| Data source | Firestore progress/{uid} | Local SQLite (getMeta) |
| Update trigger | onSnapshot fanout, instant | CONTENT_SYNCED_EVENT after a sync completes |
| Works offline | No | Yes |
| Cross-device | Instant | Only after next sync |
rendering diagram…
drag to pan · ctrl / ⌘ + scroll to zoom
Spaced repetition: what "revision due" actually means
src/lib/revision.js is not a flat "7 days later" rule — it's an expanding-interval schedule close to a simplified Anki. DEFAULT_INTERVALS = [1, 3, 7, 14, 30] (days). makeSchedule() chains concrete due-dates from an anchor date using those gaps; each slot is overdue, due-today, or upcoming depending on how dueAt compares to the start of today.
javascript
// Conceptual shape of the real algorithm
function makeSchedule(anchorDate, intervals = DEFAULT_INTERVALS) {
return intervals.map((days) => ({
dueAt: addDays(anchorDate, days),
status: "pending",
}));
}
function slotState(slot, now = new Date()) {
if (slot.status !== "pending") return slot.status;
if (slot.dueAt < startOfDay(now)) return "overdue";
if (isSameDay(slot.dueAt, now)) return "due-today";
return "upcoming";
}Completing a revision doesn't just tick a box — completeNextSlot() marks the earliest pending slot done, then recomputePendingTail() re-chains the remaining pending slots from the completion time. Revise early and the rest of the schedule shifts earlier too; revise late and it shifts later. undoLastCompletion() reverses the most recent completion for corrections. The dashboard's "revision due" count is just slots.filter(s => slotState(s) !== "upcoming").length across all tracked items.
Why circular progress and streaks
Circular progress indicators read as "almost done" faster than a percentage number — the Gestalt closure effect makes a 90%-filled ring feel more urgent to complete than "90%" in text. Streaks (revisionStreak: consecutive days with at least one completed revision, ending today or yesterday) exploit loss aversion — breaking a streak feels like losing something already earned, which is a stronger motivator than the abstract goal of "revise consistently." Color coding follows one consistent rule across the app: green for solved/on-track, amber for due-soon, red for overdue — the same three-state palette ThemeContext exposes as CSS variables, covered next in Dark Mode, Split-Pane & Search.
Self-assessment
Word count for this page: 852 words.
→ Next: Notes System & Annotations