feat: live updates on shared shopping lists

Polls item state every 4s and merges it into the list view, guarding
any item with an in-flight local edit so a poll landing mid-edit can't
stomp on it. No websocket/SSE infra exists in this app yet, so this
follows the same polling convention already used for notifications and
message threads.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Arnaud
2026-07-13 12:27:48 +02:00
parent 70eb565eec
commit b1f4bba6dd
6 changed files with 93 additions and 4 deletions
@@ -6,6 +6,36 @@ import { getShoppingListAccess, canWriteShoppingList } from "@/lib/shopping-list
import { guessAisle } from "@/lib/grocery-categories";
import { notifyShoppingListMembers } from "@/lib/shopping-list-notify";
// Polled by the list view (components/meal-plan/shopping-list-view.tsx) so
// collaborators see each other's checked/added/reordered items without a
// manual refresh — no push/websocket infra in this app, so a short poll on
// the same shape the page's initial server-render already uses.
export async function GET(_req: NextRequest, { params }: Params) {
const { session, response } = await requireSession();
if (response) return response;
const { id } = await params;
const access = await getShoppingListAccess(id, session!.user.id);
if (!access) return NextResponse.json({ error: "Not found" }, { status: 404 });
const items = await db.query.shoppingListItems.findMany({
where: eq(shoppingListItems.listId, id),
orderBy: (t, { asc }) => [asc(t.aisle), asc(t.sortOrder), asc(t.rawName)],
});
return NextResponse.json({
items: items.map((i) => ({
id: i.id,
rawName: i.rawName,
quantity: i.quantity,
unit: i.unit,
aisle: i.aisle,
checked: i.checked,
sortOrder: i.sortOrder,
})),
});
}
const AddItemsSchema = z.object({
items: z.array(z.object({
rawName: z.string().min(1),