From f52d6b4d4bb91cb4ad955116bbc9a3eb99b81cc9 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Mon, 1 Jun 2026 16:05:54 +0200 Subject: [PATCH] perf(tasks): show completion sticker instantly Roll the sticker client-side and render the popup immediately on completion instead of waiting for the POST roundtrip + DB writes to return it. Preload the sticker image so the cat is decoded before the bounce-in finishes. The chosen stickerId is sent to the server, which persists it (falling back to a server-side roll if missing/invalid). --- package.json | 4 ++-- src/routes/api/tasks/[id]/complete/+server.ts | 9 ++++++--- src/routes/tasks/+page.svelte | 20 ++++++++++++++----- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 3d91ac58..cec3f055 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.95.2", + "version": "1.95.4", "private": true, "type": "module", "scripts": { @@ -8,7 +8,7 @@ "dev": "vite dev", "prebuild": "bash scripts/subset-emoji-font.sh && pnpm exec vite-node scripts/generate-mystery-verses.ts && pnpm exec vite-node scripts/download-models.ts && pnpm exec vite-node scripts/generate-loyalty-cards.ts && pnpm exec vite-node scripts/generate-error-quotes.ts && pnpm exec vite-node scripts/build-hikes.ts && pnpm exec vite-node scripts/build-private-images.ts", "build": "vite build", - "postbuild": "pnpm exec vite-node scripts/build-error-page.ts", + "postbuild": "pnpm exec vite-node scripts/build-error-page.ts && pnpm exec vite-node scripts/precompress.ts", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", diff --git a/src/routes/api/tasks/[id]/complete/+server.ts b/src/routes/api/tasks/[id]/complete/+server.ts index ba442db1..1824a2d1 100644 --- a/src/routes/api/tasks/[id]/complete/+server.ts +++ b/src/routes/api/tasks/[id]/complete/+server.ts @@ -3,7 +3,7 @@ import { Task } from '$models/Task'; import { TaskCompletion } from '$models/TaskCompletion'; import { dbConnect } from '$utils/db'; import { error, json } from '@sveltejs/kit'; -import { getStickerForTags } from '$lib/utils/stickers'; +import { getStickerForTags, getStickerById } from '$lib/utils/stickers'; import { addDays } from 'date-fns'; function getNextDueDate(completedAt: Date, frequencyType: string, customDays?: number): Date { @@ -37,8 +37,11 @@ export const POST: RequestHandler = async ({ params, request, locals }) => { const now = new Date(); - // Award a sticker based on task tags and difficulty - const sticker = getStickerForTags(task.tags, task.difficulty || 'medium'); + // Award a sticker. The client rolls + displays it optimistically and passes + // the id here; fall back to a server-side roll if it's missing or invalid. + const sticker = + (typeof body.stickerId === 'string' && getStickerById(body.stickerId)) || + getStickerForTags(task.tags, task.difficulty || 'medium'); // Record the completion const completion = await TaskCompletion.create({ diff --git a/src/routes/tasks/+page.svelte b/src/routes/tasks/+page.svelte index 3ae7f6df..fb808fed 100644 --- a/src/routes/tasks/+page.svelte +++ b/src/routes/tasks/+page.svelte @@ -27,6 +27,7 @@ import { flip } from 'svelte/animate'; import TaskForm from '$lib/components/tasks/TaskForm.svelte'; import StickerPopup from '$lib/components/tasks/StickerPopup.svelte'; + import { getStickerForTags } from '$lib/utils/stickers'; import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte'; let { data } = $props(); @@ -112,16 +113,25 @@ * @param {string} [forUser] */ async function completeTask(task, forUser) { + // Roll the sticker client-side and show it immediately — don't wait on the + // POST roundtrip (DB writes) just to learn which sticker to display. + const sticker = getStickerForTags(task.tags, task.difficulty || 'medium'); + // Warm the image cache so the cat is decoded by the time the popup finishes + // its bounce-in, instead of fading into an empty circle. + if (typeof Image !== 'undefined') { + const img = new Image(); + img.src = `/stickers/${sticker.image}`; + } + awardedSticker = sticker; + completeForTaskId = null; + + // Persist in the background; tell the server which sticker we showed. const res = await fetch(`/api/tasks/${task._id}/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(forUser ? { completedFor: forUser } : {}) + body: JSON.stringify({ stickerId: sticker.id, ...(forUser ? { completedFor: forUser } : {}) }) }); if (!res.ok) return; - const result = await res.json(); - - awardedSticker = result.sticker; - completeForTaskId = null; await refreshTasks(); }