feat: offline save-for-later + background sync for mark-cooked (v0.65.0)
Adds a "Save offline" button on the recipe page that force-refetches the current page so the service worker's cache picks up a fresh copy right now, plus a small IndexedDB-backed list (lib/offline-db.ts) of what's been saved. The /offline fallback page now reads that list and renders it instead of being a dead end with just a "go back" link. Also fixes the service worker's network-first fetch handler, which never wrote successful responses into its cache -- meaning the existing offline page's claim that "recently visited recipes are available" was never actually true. It populates the cache on every successful GET now. Background sync: marking a batch-cook dish as cooked while offline (the only existing mark-cooked call site in the app) now queues the request in IndexedDB instead of just failing, and registers a Background Sync (public/sw.js's "sync" listener replays the queue) for Chromium; lib/offline-queue.ts's online-event fallback covers Safari/Firefox, which never fire that event at all. Both replay paths read/write the same IndexedDB store so either one drains it. Also removes apps/web/public/manifest.json, a stale static manifest that layout.tsx used to link to before the previous commit pointed it at the real generated route (app/manifest.ts) -- it had gone unnoticed and unused since. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { Snowflake, Refrigerator, ChefHat, Check } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useLocale } from "@/lib/i18n/provider";
|
||||
import { stripMarkdown } from "@/lib/utils";
|
||||
import { queueOfflineAction } from "@/lib/offline-queue";
|
||||
|
||||
type Dish = {
|
||||
id: string;
|
||||
@@ -33,12 +34,31 @@ export function BatchCookDishes({ recipeId, dishes: initialDishes }: { recipeId:
|
||||
|
||||
async function markCooked(dishId: string) {
|
||||
setMarkingId(dishId);
|
||||
const body = { batchDishId: dishId, deductFromPantry: false };
|
||||
try {
|
||||
const res = await fetch(`/api/v1/recipes/${recipeId}/cooked`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ batchDishId: dishId, deductFromPantry: false }),
|
||||
});
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`/api/v1/recipes/${recipeId}/cooked`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
} catch {
|
||||
// fetch() throwing (rather than resolving with a non-ok status)
|
||||
// means the network itself is unreachable — queue it for the
|
||||
// service worker (or the online-event fallback) to replay later,
|
||||
// rather than treating it as a real failure.
|
||||
await queueOfflineAction({
|
||||
method: "POST",
|
||||
url: `/api/v1/recipes/${recipeId}/cooked`,
|
||||
body,
|
||||
description: `Mark dish ${dishId} as cooked`,
|
||||
});
|
||||
const now = new Date().toISOString();
|
||||
setDishes((prev) => prev.map((d) => (d.id === dishId ? { ...d, cookedAt: now } : d)));
|
||||
toast.info(t("batchCookMarkQueuedOffline"));
|
||||
return;
|
||||
}
|
||||
if (!res.ok) {
|
||||
toast.error(t("batchCookMarkFailed"));
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user