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:
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { Download, Check } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { isRecipeSavedOffline, saveRecipeOffline, unsaveRecipeOffline } from "@/lib/offline-db";
|
||||
|
||||
export function SaveOfflineButton({ recipeId, recipeTitle }: { recipeId: string; recipeTitle: string }) {
|
||||
const pathname = usePathname();
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
isRecipeSavedOffline(recipeId).then(setSaved).catch(() => {});
|
||||
}, [recipeId]);
|
||||
|
||||
async function toggle() {
|
||||
setBusy(true);
|
||||
try {
|
||||
if (saved) {
|
||||
await unsaveRecipeOffline(recipeId);
|
||||
setSaved(false);
|
||||
toast.success("Removed from offline recipes");
|
||||
return;
|
||||
}
|
||||
// Re-fetching the current page (rather than relying on the visit that
|
||||
// already happened) forces the service worker's fetch handler to pin
|
||||
// a fresh copy in its cache right now, regardless of how the normal
|
||||
// cache eviction/versioning plays out later.
|
||||
await fetch(pathname, { cache: "reload" });
|
||||
await saveRecipeOffline(recipeId, recipeTitle);
|
||||
setSaved(true);
|
||||
toast.success("Saved for offline — photos and linked pages aren't included");
|
||||
} catch {
|
||||
toast.error("Couldn't save this recipe for offline use");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button type="button" variant="outline" size="sm" disabled={busy} onClick={() => { void toggle(); }}>
|
||||
{saved ? <Check className="h-3.5 w-3.5" /> : <Download className="h-3.5 w-3.5" />}
|
||||
{saved ? "Saved offline" : "Save offline"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user