Files
minabot/src/lib/dashboard.ts
syntaxbullet ccd998ab04 Add service worker implementation and tests for PWA caching strategy
- Implemented a service worker (sw.js) that handles installation, activation, and fetch events.
- Added caching logic for public assets and a fallback for offline navigation.
- Created unit tests (sw.test.ts) to validate service worker behavior, including asset caching, navigation handling, and activation logic.
- Ensured that private API routes and third-party requests are excluded from caching.
2026-09-08 17:19:12 +02:00

63 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { HabitService } from "../habits/service";
import type { Schedule } from "../habits/contracts";
export type TodayHabit = ReturnType<HabitService["day"]>;
export type TodayResponse = {
date: string;
timezone: string;
habits: TodayHabit[];
due: number;
completed: number;
};
export async function habitRequest<T>(
path: string,
options?: RequestInit,
): Promise<T> {
if (typeof navigator !== "undefined" && navigator.onLine === false)
throw new Error("You’re offline. Reconnect and try again. Changes have not been saved or queued.");
const response = await fetch(`/api${path}`, options);
if (!response.ok) {
if (response.status === 401)
throw new Error("Your session has expired. Sign in again to continue.");
const body = await response.json().catch(() => null);
throw new Error(
(body?.error || "Could not reach your workspace. Please try again.") + (response.status >= 500 && body?.requestId ? ` Reference: ${body.requestId}` : ""),
);
}
return response.status === 204
? (undefined as T)
: (response.json() as Promise<T>);
}
export function scheduleLabel(schedule?: Schedule) {
if (!schedule || schedule.type === "daily") return "Every day";
if (schedule.type === "weekdays")
return schedule.days
.map((day) => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][day])
.join(", ");
if (schedule.type === "interval") return `Every ${schedule.every} days`;
return `Every ${schedule.every === 1 ? "week" : `${schedule.every} weeks`} · ${["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][schedule.weekday]}`;
}
export function formatTrackingDate(date: string) {
return new Date(`${date}T12:00:00Z`).toLocaleDateString("en", {
weekday: "long",
month: "long",
day: "numeric",
timeZone: "UTC",
});
}
/** Preserve unchanged row references when a background refresh returns new JSON objects. */
export function reconcileToday(current: TodayResponse | null, next: TodayResponse): TodayResponse {
if (!current || current.date !== next.date || current.timezone !== next.timezone) return next;
const previous = new Map(current.habits.map(habit => [habit.habitId, habit]));
const habits = next.habits.map(habit => {
const old = previous.get(habit.habitId);
return old && JSON.stringify(old) === JSON.stringify(habit) ? old : habit;
});
if (current.due === next.due && current.completed === next.completed && habits.length === current.habits.length && habits.every((habit, index) => habit === current.habits[index])) return current;
return { ...next, habits };
}