From c76c3f52f727b3b750b435145a3d43d7e17dc6d3 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Thu, 4 Jun 2026 01:07:43 +0100 Subject: [PATCH] feat(ui): loyalty stamp redesign and admin account restrictions Redesigned fuchsia-themed loyalty stamp card with procedural SVG flower-petal stamps (unique shape per slot via sine-wave perturbation), star cutout mask, hover animations, and gradient background. Admin accounts now see simplified account page with History, Referral, Danger Zone tabs and loyalty stamp card hidden. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- frontend/src/app.css | 8 + frontend/src/routes/account/+page.svelte | 286 +++++++++++++++-------- frontend/tailwind.config.js | 3 +- 3 files changed, 203 insertions(+), 94 deletions(-) diff --git a/frontend/src/app.css b/frontend/src/app.css index 0f20cf0..df65406 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -2,6 +2,8 @@ @import 'tw-animate-css'; +@import "shadcn-svelte/tailwind.css"; + @custom-variant dark (&:is(.dark *)); :root { @@ -119,6 +121,12 @@ body { @apply bg-background text-foreground; } + .maplibregl-popup-content { + @apply bg-transparent! shadow-none! p-0! rounded-none!; + } + .maplibregl-popup-tip { + @apply hidden!; + } } .calendar-container { diff --git a/frontend/src/routes/account/+page.svelte b/frontend/src/routes/account/+page.svelte index 4b05d97..7ad6d92 100644 --- a/frontend/src/routes/account/+page.svelte +++ b/frontend/src/routes/account/+page.svelte @@ -89,6 +89,32 @@ let pendingRedemption = $state(false); let uploadingPic = $state(false); + function getStampPath(slotNum: number): string { + const petals = 7 + (slotNum % 4); // 7, 8, 9, 10 petals + const amp = 2.5 + (slotNum % 3) * 0.5; // 2.5, 3.0, 3.5 amplitude + const phase = slotNum * 12; // phase offset in degrees + const R = 42; // base radius + const centerX = 50; + const centerY = 50; + + let path = ''; + const steps = 120; + for (let i = 0; i < steps; i++) { + const angleDeg = (i * 360) / steps; + const angleRad = (angleDeg * Math.PI) / 180; + const phaseRad = (phase * Math.PI) / 180; + const r = R + amp * Math.sin(petals * angleRad + phaseRad); + const x = (centerX + r * Math.cos(angleRad)).toFixed(1); + const y = (centerY + r * Math.sin(angleRad)).toFixed(1); + if (i === 0) { + path += `M ${x} ${y}`; + } else { + path += ` L ${x} ${y}`; + } + } + return path + ' Z'; + } + let notifPrefs = $state({ emailEnabled: true, smsEnabled: true, browserPushEnabled: true }); type SavedCard = { @@ -496,7 +522,7 @@ const now = new SvelteDate(); // Filter: Calculate end time (Start + Duration) and check if it's in the future - const activeOrFutureBookings = (data.bookings || []).filter((b: any) => { + const activeOrFutureBookings = (data.bookings || []).filter((b: Booking) => { const startTime = new SvelteDate(b.start_time); // Add duration (in ms) const endTime = new SvelteDate(startTime.getTime() + (b.duration_minutes || 0) * 60000); @@ -540,7 +566,7 @@ // FIX: Manually calculate amount_due for the list // The list API often returns 0 for amount_due/amount_paid, // so we derive it from total_amount. - bookings = bookings.map((b: any) => { + bookings = bookings.map((b: Booking) => { const total = b.total_amount || 0; const paid = b.amount_paid || 0; return { @@ -550,7 +576,7 @@ }); // SORT LOGIC: Unpaid first, then by most recent - bookings.sort((a: any, b: any) => { + bookings.sort((a: Booking, b: Booking) => { const aUnpaid = (a.amount_due || 0) > 0; const bUnpaid = (b.amount_due || 0) > 0; @@ -792,48 +818,52 @@ General - - + {/if} + {#if authStore.currentUser?.role !== 'admin'} + + + + + + + + Referral + + {/if} {#if canSaveCards} - + {#if authStore.currentUser?.role !== 'admin'} + +
+

Danger Zone

+

+ Once you delete your account, there is no going back. Please be certain. +

+ +
+ {/if} {/if} @@ -1597,6 +1690,7 @@ General + {#if authStore.currentUser?.role !== 'admin'} + {/if} {#if canSaveCards}