From f802113cd1063c8510e1956f46b9cc22cdf42c7e Mon Sep 17 00:00:00 2001
From: JCEEE <0xjceee@proton.me>
Date: Thu, 10 Sep 2026 11:27:27 +0100
Subject: [PATCH] loads of mobile ui layout upgrades
---
frontend/src/lib/components/Card.svelte | 31 +++--
frontend/src/lib/components/Chat.svelte | 2 +-
frontend/src/lib/components/Footer.svelte | 13 ++
.../src/lib/components/PricingPlans.svelte | 3 +
frontend/src/lib/components/Sidebar.svelte | 22 +++
frontend/src/lib/components/ViewHeader.svelte | 24 +++-
frontend/src/lib/components/icons.ts | 99 +++++++++-----
frontend/src/routes/+page.svelte | 5 +
frontend/src/routes/[fam]/+layout.svelte | 55 ++++++--
frontend/src/routes/[fam]/+page.svelte | 31 ++++-
.../src/routes/[fam]/[username]/+page.svelte | 125 +++++++++++++++---
.../[fam]/[username]/chores/+page.svelte | 85 +++++++++---
.../[fam]/[username]/ledger/+page.server.ts | 9 +-
.../[fam]/[username]/ledger/+page.svelte | 109 ++++++++++++---
.../[fam]/[username]/settings/+page.svelte | 3 +
frontend/src/routes/[fam]/join/+page.svelte | 6 +-
.../routes/[fam]/join/[username]/+page.svelte | 2 +-
17 files changed, 501 insertions(+), 123 deletions(-)
diff --git a/frontend/src/lib/components/Card.svelte b/frontend/src/lib/components/Card.svelte
index 8d2371e..74736f5 100644
--- a/frontend/src/lib/components/Card.svelte
+++ b/frontend/src/lib/components/Card.svelte
@@ -7,7 +7,15 @@
children,
class: className,
selected = false
- }: { cols?: 1 | 2 | 3 | 4 | 5 | 6; title?: string; accent?: string; scrollX?: boolean; children?: any; class?: string; selected?: boolean } = $props();
+ }: {
+ cols?: 1 | 2 | 3 | 4 | 5 | 6;
+ title?: string;
+ accent?: string;
+ scrollX?: boolean;
+ children?: any;
+ class?: string;
+ selected?: boolean;
+ } = $props();
\ No newline at end of file
+ /* Mobile (1-col grid): every card is a full row — declared last so it
+ overrides the base .card/.card-body rules (same specificity). */
+ @media (max-width: 639px) {
+ .card {
+ grid-column: span 1;
+ background: transparent;
+ border: none;
+ }
+ .card-body {
+ background: transparent;
+ padding: 0;
+ }
+ }
+
diff --git a/frontend/src/lib/components/Chat.svelte b/frontend/src/lib/components/Chat.svelte
index 8275d2b..3b51faf 100644
--- a/frontend/src/lib/components/Chat.svelte
+++ b/frontend/src/lib/components/Chat.svelte
@@ -298,7 +298,7 @@
/* Mobile: full-screen fixed layer. */
@media (max-width: 767.98px) {
.chat-panel {
- width: 100vw;
+ width: 100dvw;
}
}
.chat-head {
diff --git a/frontend/src/lib/components/Footer.svelte b/frontend/src/lib/components/Footer.svelte
index a3e0827..e42d35c 100644
--- a/frontend/src/lib/components/Footer.svelte
+++ b/frontend/src/lib/components/Footer.svelte
@@ -96,5 +96,18 @@
.footer-aside {
right: 1rem;
}
+ .footer-row {
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+ text-align: center;
+ }
+ .footer-star {
+ position: static;
+ }
+ .footer-aside {
+ position: static;
+ text-align: center;
+ }
}
diff --git a/frontend/src/lib/components/PricingPlans.svelte b/frontend/src/lib/components/PricingPlans.svelte
index 1266aca..c64e3cc 100644
--- a/frontend/src/lib/components/PricingPlans.svelte
+++ b/frontend/src/lib/components/PricingPlans.svelte
@@ -147,6 +147,9 @@
}
.price { margin: 0.55rem 0 0.15rem; line-height: 1; }
.amount { font-size: 2.4rem; font-weight: 800; color: #111827; }
+ @media (max-width: 639px) {
+ .amount { font-size: 1.5em; }
+ }
.period { font-size: 0.85rem; color: #9ca3af; margin-left: 0.3rem; }
.blurb { margin: 0 0 0.9rem; font-size: 0.85rem; color: #6b7280; }
diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte
index 36626dd..1fe05bb 100644
--- a/frontend/src/lib/components/Sidebar.svelte
+++ b/frontend/src/lib/components/Sidebar.svelte
@@ -11,6 +11,12 @@
prefsIcon
} from './icons';
import FamDone from './FamDone.svelte';
+ import { famStore } from '$lib/stores/fam.svelte';
+
+ // Outstanding claim requests (children asked → parent still to issue).
+ let requestedClaims = $derived(
+ famStore.initialized ? famStore.rewards.filter((r: any) => r.status === 'requested').length : 0
+ );
let { famName = '', session = null, isParent = false, role = 'child' } = $props();
@@ -62,6 +68,9 @@
{@html item.icon}
{item.label}
+ {#if isParent && item.label === 'Dashboard' && requestedClaims > 0}
+ {requestedClaims}
+ {/if}
{/each}
@@ -147,6 +156,19 @@
margin: 0 0.3rem;
white-space: nowrap;
transition: background 0.15s;
+ position: relative;
+ }
+ .nav-badge {
+ position: absolute;
+ top: 4px;
+ right: 6px;
+ background: #f59e0b;
+ color: #fff;
+ font-size: 0.65rem;
+ font-weight: 700;
+ line-height: 1;
+ padding: 2px 6px;
+ border-radius: 999px;
}
.nav-item:hover {
background: #3730a3;
diff --git a/frontend/src/lib/components/ViewHeader.svelte b/frontend/src/lib/components/ViewHeader.svelte
index 1c83dff..be5301c 100644
--- a/frontend/src/lib/components/ViewHeader.svelte
+++ b/frontend/src/lib/components/ViewHeader.svelte
@@ -73,6 +73,14 @@
diff --git a/frontend/src/routes/[fam]/+layout.svelte b/frontend/src/routes/[fam]/+layout.svelte
index 0189173..eaeef3e 100644
--- a/frontend/src/routes/[fam]/+layout.svelte
+++ b/frontend/src/routes/[fam]/+layout.svelte
@@ -126,7 +126,7 @@
});
$effect(() => {
- if (!session || !famStore.initialized) return;
+ if (!isParent || !session || !famStore.initialized) return;
const rewards = famStore.rewards;
if (!mounted) {
mounted = true;
@@ -180,7 +180,7 @@
s.active !== false)}
announcement={disabled && !activating ? accessMessage(accessReason, isParent) : ''}
>
@@ -191,7 +191,7 @@
{/if}
-
+
{@render children()}
@@ -218,7 +218,12 @@
{#if claimToast}
- {claimToast}
+
+ {claimToast} — view dashboard →
+
{/if}
@@ -232,17 +237,33 @@
diff --git a/frontend/src/routes/[fam]/[username]/+page.svelte b/frontend/src/routes/[fam]/[username]/+page.svelte
index 8e2501c..e0be904 100644
--- a/frontend/src/routes/[fam]/[username]/+page.svelte
+++ b/frontend/src/routes/[fam]/[username]/+page.svelte
@@ -144,10 +144,19 @@
}
}
+ // Same "Bonus name – prize" split as the ledger, so prize rewards don't
+ // duplicate the full label in both columns of a claim row.
+ function rewardLabelParts(r: any): { name: string; prize: string } {
+ const label = r.label || '';
+ const idx = label.indexOf(' – ');
+ if (idx === -1) return { name: label, prize: label };
+ return { name: label.slice(0, idx), prize: label.slice(idx + 3) };
+ }
+
function rewardLabel(r: any): string {
if (r.rewardType === 'cash') return `£${Number(r.value).toFixed(2)}`;
if (r.rewardType === 'points') return `${r.value} pts`;
- return r.label;
+ return rewardLabelParts(r).prize;
}
function manualConfigs() {
@@ -605,8 +614,14 @@
const existingReward = rewards.find(
(r) => r.bonusConfigId === cfg.id && r.memberId === memberId
);
+ // An earned reward only keeps lighting up the card while it's fresh
+ // (earned in the viewed week) or still outstanding (not yet claimed).
+ // Once claimed and the week rolls over, "Reward earned" disappears.
+ const rDate = (existingReward?.date || '').slice(0, 10);
+ const rewardInWeek = !!rDate && rDate >= weekStart && rDate <= weekEnd;
+ const rewardOutstanding = !!existingReward && existingReward.status !== 'claimed';
const achieved = existingReward
- ? true
+ ? rewardInWeek || rewardOutstanding
: isPercent
? totalPotential > 0 && ratio * 100 >= configured
: target > 0 && current >= target;
@@ -729,6 +744,12 @@
function headDay(): number {
return headDate().getDate();
}
+ function headDayOrdinal(): string {
+ const n = headDay();
+ if (n % 100 >= 11 && n % 100 <= 13) return `${n}th`;
+ const s = ['th', 'st', 'nd', 'rd'];
+ return `${n}${s[n % 10] || s[0]}`;
+ }
function headWeekday(): string {
return headDate().toLocaleDateString(undefined, { weekday: 'long' });
}
@@ -914,7 +935,6 @@