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).
This commit is contained in:
2026-06-01 16:05:54 +02:00
parent 9b5cfe5e49
commit f52d6b4d4b
3 changed files with 23 additions and 10 deletions
+2 -2
View File
@@ -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",
@@ -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({
+15 -5
View File
@@ -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();
}