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 @@
{g.rewardText} - {formatDDMMYY(g.date)}
{/each} @@ -1047,7 +1067,7 @@ {/if} - + {#if claimableRewards().length === 0}
{@html checkCircleIcon}No outstanding claims @@ -1106,7 +1126,7 @@ {#each requested as r}
{ @@ -1118,7 +1138,7 @@ {@html r.rewardType === 'cash' ? coinsIcon : giftIcon} - {r.label} + {rewardLabelParts(r).name} {rewardLabel(r)}
@@ -1133,7 +1153,7 @@ {@html r.rewardType === 'cash' ? coinsIcon : giftIcon} - {r.label} + {rewardLabelParts(r).name} {rewardLabel(r)} {#if paydayLocked(r)} @@ -1265,8 +1285,8 @@ {/if}
- {headDay()} {headWeekday()} + {headDayOrdinal()} {headMonth()}
@@ -2224,11 +2244,15 @@ color: rgba(255, 255, 255, 0.7); } .hero-days { - font-size: 0.85rem; + font-size: 1rem; color: rgba(255, 255, 255, 0.9); + /* Own full-width line, centered under the hero content */ + flex: 1 1 100%; + text-align: center; + margin-top: 0.25rem; } .hero-days b { - font-size: 1.4rem; + font-size: 1.5rem; margin-right: 0.15rem; } .hero-nav { @@ -2483,20 +2507,25 @@ grid-template-columns: repeat(2, 1fr); } } - @media (max-width: 639px) { - .goal-mini-grid { - grid-template-columns: 1fr; - } - } .goal-mini { background: linear-gradient(135deg, #d1d3fa, #ccfbf1); border: 1px solid #a7f3d0; border-radius: 12px; padding: 0.55rem 0.7rem; display: flex; - flex-direction: column; - gap: 0.4rem; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 0.75rem; min-width: 0; + /* Own width as a query container, so tight cards drop the member name */ + container-type: inline-size; + } + /* Card narrower than 250px: hide the member name so the row doesn't squish */ + @container (max-width: 249px) { + .gm-member { + display: none; + } } .gm-top { display: flex; @@ -2526,18 +2555,14 @@ .gm-bottom { display: flex; justify-content: space-between; - align-items: center; gap: 0.4rem; + white-space: nowrap; } .gm-reward { font-weight: 800; color: #047857; font-size: 0.85rem; } - .gm-date { - color: #9ca3af; - font-size: 0.72rem; - } /* Goal celebration notice (child view) */ .celebrate-card.goal { @@ -2954,4 +2979,60 @@ font-size: 0.8rem; opacity: 0.85; } + /* Mobile: declared last so it wins over the base .goal-mini (.kanban) + rules (same specificity, source order decides). */ + @media (max-width: 639px) { + .goal-mini-grid { + grid-template-columns: 1fr; + } + .kanban { + grid-template-columns: 1fr; + } + /* Admin overview cards flow full width on phones — same convention as + the chores kanban (no horizontal scroll). */ + .kanban-scroll { + overflow-x: visible; + } + .kanban-inner { + min-width: 0; + } + /* Keep the orange wash visible: restore body padding for this card + only (the shared Card removes it on mobile). */ + :global(.card.claims-card .card-body) { + padding: 0.75rem; + } + /* Claim rows: icon/label/value stay on one line (label truncates + with ellipsis) and the Issue button becomes a full-width action + below — no squishing or overflow under 450px. */ + .member-claims form.flex { + flex-wrap: wrap; + row-gap: 0.5rem; + } + .member-claims form.flex > div { + flex: 1 1 100%; + min-width: 0; + flex-wrap: nowrap; + align-items: center; + } + /* :global — the button element is rendered by the shared Button + component, so a scoped selector never matches it. */ + .member-claims form.flex :global(button) { + flex: 1 1 100%; + width: 100%; + margin-left: 0; + } + .member-claims .claim-label { + white-space: nowrap; + } + .member-claims .claim-item { + flex-wrap: wrap; + } + } + /* Claims card — orange wash so pending requests are impossible to miss. + :global + !important deliberately override the shared Card's white/ + mobile-transparent backgrounds. */ + :global(.card.claims-card) { + background: linear-gradient(135deg, #ffedd5, #fdba74 70%, #fb923c) !important; + border-color: #f97316; + } diff --git a/frontend/src/routes/[fam]/[username]/chores/+page.svelte b/frontend/src/routes/[fam]/[username]/chores/+page.svelte index 8cb146b..c3ba61b 100644 --- a/frontend/src/routes/[fam]/[username]/chores/+page.svelte +++ b/frontend/src/routes/[fam]/[username]/chores/+page.svelte @@ -197,7 +197,7 @@ const isShared = assignTarget === 'shared'; const body: Record = { memberId: isShared ? '' : assignModal.memberId, - templateId: assignModal.t.id, + templateId: assignModal.t?.id || '', customName: assignName, description: assignDescription, color: assignColor, @@ -242,6 +242,20 @@ showTodoModal = true; } + function openCreateChore() { + assignModal = { t: null as any, memberId: members[0]?.id || '' }; + assignStep = 1; + assignTarget = 'member'; + assignName = ''; + assignDescription = ''; + assignColor = ''; + assignIcon = ''; + assignFreq = 'daily'; + assignType = 'points'; + assignValue = 10; + assignSeasonIds = seasonFilter !== 'all' ? [seasonFilter] : []; + } + // Sort todos by completeBy (soonest first), then by startDate function todoSort(a: AssignedChore, b: AssignedChore): number { const da = a.completeBy || '9999-99-99'; @@ -381,6 +395,7 @@

Templates — Drag to assign

+
{#each templates as t} @@ -527,11 +542,11 @@ class="accordion-wrapper accordion-chores" class:open={accordionState[m.id]?.chores ?? true} > - { - if (e.target === e.currentTarget) { assignModal = null; assignStep = 1; } + if (e.target === e.currentTarget) { + assignModal = null; + assignStep = 1; + } }} role="presentation" >