From 97bd6799546e19d78a3b45f3fe93c295d411688e Mon Sep 17 00:00:00 2001 From: JCEEE <0xjceee@proton.me> Date: Tue, 4 Aug 2026 15:32:59 +0100 Subject: [PATCH] tighten views --- MEMORY.md | 8 + frontend/src/lib/components/Card.svelte | 13 +- frontend/src/lib/components/ViewHeader.svelte | 25 +- frontend/src/routes/[fam]/+layout.svelte | 1 + frontend/src/routes/[fam]/+page.svelte | 383 ++++++++++---- .../src/routes/[fam]/[username]/+page.svelte | 80 ++- .../[fam]/[username]/bonuses/+page.svelte | 82 ++- .../[fam]/[username]/chores/+page.server.ts | 5 +- .../[fam]/[username]/chores/+page.svelte | 468 +++++++++++------- .../[fam]/[username]/preferences/+page.svelte | 2 +- .../[fam]/[username]/rewards/+page.svelte | 2 +- .../[fam]/[username]/settings/+page.svelte | 2 +- frontend/src/routes/admin/+page.svelte | 2 +- proxy/src/index.ts | 12 + 14 files changed, 776 insertions(+), 309 deletions(-) diff --git a/MEMORY.md b/MEMORY.md index 6a1b5d3..146ff0a 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -150,4 +150,12 @@ - **Problem**: the chores todo card rendered `due 050826` (DDMMYY code) — ambiguous/terrible for a due date. - **Fix**: added `formatShortDate()` to `frontend/src/lib/format.ts` — renders `5 Aug` (adds ` 26` when the year isn't the current one). Chores todo card now shows `due 5 Aug`. AGENTS.md date rule updated: DDMMYY for dense/range contexts, `formatShortDate()` for single human-readable dates like due dates. +### 2026-08-04 — Child-Dashboard Design Lead Applied to All Pages + +- **Design principal** (from `[fam]/[username]` child dash): gradient hero lead (`linear-gradient(135deg, #6366f1, #8b5cf6 55%, #a855f7)`, radius 16px, glow shadow, white text), gradient stat tiles, rounded white cards. +- **`ViewHeader` hero variant**: added `hero` prop to `frontend/src/lib/components/ViewHeader.svelte` — renders the title/subtitle/tools on the gradient hero (tabs/nav/sort get tinted-on-white styling). Off by default, so no behavior change elsewhere. +- **Applied `hero` to**: admin dashboard (fam name), chores, bonuses, rewards, settings, preferences, platform admin `/admin`. +- **Admin dashboard stat tiles**: added 4 gradient tiles (members / points / cash / chores done) reusing the child `.tiles`/`.tile` pattern + new `.tile-members`/`.tile-chores` colors, from a new `adminTiles` derived summing `summary.summaries`. Also fixed admin subtitle `Week of {YYYY-MM-DD}` → `Week of {DDMMYY}`. +- **Check**: frontend `svelte-check` stays at 12 baseline errors. Note: frontend dev server on :2080 was not running when verified (proxy :3456 up). + diff --git a/frontend/src/lib/components/Card.svelte b/frontend/src/lib/components/Card.svelte index 3899029..b3c1f1f 100644 --- a/frontend/src/lib/components/Card.svelte +++ b/frontend/src/lib/components/Card.svelte @@ -3,21 +3,23 @@ cols = 1, title, accent, + scrollX = false, children - }: { cols?: 1 | 2 | 3 | 4 | 5 | 6; title?: string; accent?: string; children?: any } = $props(); + }: { cols?: 1 | 2 | 3 | 4 | 5 | 6; title?: string; accent?: string; scrollX?: boolean; children?: any } = $props();
{#if title}
{title}
{/if} -
+
{@render children?.()}
@@ -29,6 +31,9 @@ border-radius: 10px; overflow: hidden; } + .card.scroll-x { + overflow: visible; + } .card.has-accent { border-top: 3px solid var(--card-accent, #6366f1); } @@ -45,4 +50,8 @@ .card-body { padding: 1rem; } + .card-body.scroll-x-body { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } diff --git a/frontend/src/lib/components/ViewHeader.svelte b/frontend/src/lib/components/ViewHeader.svelte index b1dadd1..c037292 100644 --- a/frontend/src/lib/components/ViewHeader.svelte +++ b/frontend/src/lib/components/ViewHeader.svelte @@ -1,14 +1,15 @@ -
+

{title}

{#if subtitle}

{subtitle}

{/if} @@ -68,4 +69,24 @@ padding: 0.3rem 0.6rem; border: 1px solid #d1d5db; border-radius: 5px; font-size: 0.85rem; background: #fff; } + + /* ── Hero lead (child-dashboard style) ── */ + .view-header.hero { + background: linear-gradient(135deg, #6366f1, #8b5cf6 55%, #a855f7); + border-radius: 16px; + padding: 1.25rem 1.5rem; + color: #fff; + box-shadow: 0 10px 30px rgba(99, 102, 241, 0.35); + } + .view-header.hero .view-title-group { margin-bottom: 0.25rem; } + .view-header.hero .view-title { color: #fff; } + .view-header.hero .view-subtitle { color: rgba(255, 255, 255, 0.85); } + .view-header.hero .view-tools { margin-top: 0.75rem; } + .view-header.hero .tab-bar { background: rgba(255, 255, 255, 0.16); } + .view-header.hero .tab-btn { color: rgba(255, 255, 255, 0.9); } + .view-header.hero .tab-btn:hover { color: #fff; } + .view-header.hero .tab-btn.active { background: #fff; color: #4338ca; } + .view-header.hero .nav-btn { background: rgba(255, 255, 255, 0.92); border-color: transparent; color: #4338ca; } + .view-header.hero .nav-label { color: #fff; } + .view-header.hero .sort-select { background: rgba(255, 255, 255, 0.92); color: #1e1b4b; } diff --git a/frontend/src/routes/[fam]/+layout.svelte b/frontend/src/routes/[fam]/+layout.svelte index d31c2ab..37b57a7 100644 --- a/frontend/src/routes/[fam]/+layout.svelte +++ b/frontend/src/routes/[fam]/+layout.svelte @@ -74,6 +74,7 @@ min-height: 100vh; display: flex; flex-direction: column; + background: whitesmoke; } .app-main { margin-left: 220px; diff --git a/frontend/src/routes/[fam]/+page.svelte b/frontend/src/routes/[fam]/+page.svelte index f957488..4b97a17 100644 --- a/frontend/src/routes/[fam]/+page.svelte +++ b/frontend/src/routes/[fam]/+page.svelte @@ -1,8 +1,9 @@ -

🏠 {famSlug}

+ -{#if summary?.summaries?.length} -

This week

- - - - {#each summary.summaries as s} - - - - - - {/each} - -
MemberPointsDone
{s.memberName}{s.pointsEarned}{s.choresCompleted}/{s.totalChores}
- -
-

Chores per day

- + +
+
+ {tileMembers} + members
- -
- {#each summary.summaries as s} -
- -
{s.memberName}
-
{s.pointsEarned} pts
-
{s.choresCompleted}/{s.totalChores} chores
-
- {/each} +
+ {tilePts} + points this week +
+
+ £{tileMoney.toFixed(2)} + cash this week +
+
+ {tileChores} + chores done
-{/if} - -

Members ({members?.length || 0})

-
- {#each members || [] as m} - - {m.name} - {m.deviceTokenHint || '—'} - - {/each}
-{#if templates?.length} -

Templates ({templates.length})

-
    - {#each templates as t} -
  • {t.name} — {t.defaultFrequency} — {t.defaultValue} {t.defaultType}
  • - {/each} -
-{/if} + + + + {#if !summary?.summaries?.length} +

No data yet this week

+ {:else} +
+ {#each summary.summaries as s} + {@const m = members.find((m2: any) => m2.id === s.memberId)} + {@const total = totalChoresFor(s.memberId)} + {@const pct = total > 0 ? Math.round((s.choresCompleted / total) * 100) : 0} +
+
+
+ + {s.memberName} + {#if m} + Kanban → + {/if} +
+
+ +
{pct}%
+
+
+
+
+ Points + {s.pointsEarned} +
+
+ Cash + £{Number(s.moneyEarned || 0).toFixed(2)} +
+
+ Done + {s.choresCompleted}/{total} +
+
+
+
+
+
+
+
+ {/each} +
+ {/if} +
+ + + + {#if summary?.summaries?.length} +
+ +
+ {:else} +

No data yet

+ {/if} +
+ + + + {#if templates.length === 0} +

No templates created

+ {:else} +
+ {#each templates as t} +
+ {t.name} + {t.defaultFrequency} · {t.defaultValue} {t.defaultType} +
+ {/each} +
+ {/if} +
+
diff --git a/frontend/src/routes/[fam]/[username]/+page.svelte b/frontend/src/routes/[fam]/[username]/+page.svelte index ddfae4c..c633002 100644 --- a/frontend/src/routes/[fam]/[username]/+page.svelte +++ b/frontend/src/routes/[fam]/[username]/+page.svelte @@ -65,6 +65,17 @@ famStore.initialized ? famStore.bonusConfigs : data.bonusConfigs || [] ); + // Admin overview stat tiles (summed across this week's member summaries). + let adminTiles = $derived.by(() => { + const sums = summary?.summaries || []; + return { + members: parentMembers.length, + pts: sums.reduce((t: number, s: any) => t + (Number(s.pointsEarned) || 0), 0), + money: sums.reduce((t: number, s: any) => t + (Number(s.moneyEarned) || 0), 0), + chores: sums.reduce((t: number, s: any) => t + (Number(s.choresCompleted) || 0), 0) + }; + }); + function totalChoresFor(memberId: string): number { return parentAssigned.filter((a: any) => a.memberId === memberId).length; } @@ -612,14 +623,37 @@ {#if toast}
{toast}
{/if} - + +
+
+ {adminTiles.members} + members +
+
+ {adminTiles.pts} + points this week +
+
+ £{adminTiles.money.toFixed(2)} + cash this week +
+
+ {adminTiles.chores} + chores done +
+
+ +
+
+ {#each parentMembers as m} {@const total = totalChoresFor(m.id)} @@ -838,6 +872,8 @@ {/if} +
+

@@ -1328,10 +1364,17 @@ .member-card { border: 1px solid #e5e7eb; - border-radius: 8px; - padding: 0.65rem; + border-left: 4px solid #6366f1; + border-radius: 10px; + padding: 0.65rem 0.85rem; background: white; margin-bottom: 0.6rem; + transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s; + } + .member-card:hover { + transform: translateY(-1px); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08); + border-color: #c7d2fe; } .member-card:last-child { margin-bottom: 0; @@ -1386,7 +1429,7 @@ } .bar-fill { height: 100%; - background: #6366f1; + background: linear-gradient(90deg, #6366f1, #8b5cf6); border-radius: 4px; transition: width 0.3s; } @@ -1430,11 +1473,16 @@ .trigger-card { border: 1px solid #e5e7eb; - border-radius: 8px; - padding: 0.65rem; + border-left: 3px solid #22c55e; + border-radius: 10px; + padding: 0.65rem 0.85rem; background: #f0fdf4; margin-bottom: 0.6rem; - border-left: 3px solid #22c55e; + transition: transform 0.15s, box-shadow 0.15s; + } + .trigger-card:hover { + transform: translateY(-1px); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08); } .trigger-card:last-child { margin-bottom: 0; @@ -1753,6 +1801,16 @@ color: #6d28d9; } + /* ── Kanban scroll for parent view cards ── */ + .kanban-scroll { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + margin-bottom: 1rem; + } + .kanban-inner { + min-width: 450px; + } + .tiles { display: grid; grid-template-columns: 1fr 1fr; @@ -1774,6 +1832,12 @@ .tile-pts { background: linear-gradient(135deg, #7c3aed, #a855f7); } + .tile-members { + background: linear-gradient(135deg, #6366f1, #8b5cf6); + } + .tile-chores { + background: linear-gradient(135deg, #0284c7, #38bdf8); + } .tile-val { font-size: 1.6rem; font-weight: 800; diff --git a/frontend/src/routes/[fam]/[username]/bonuses/+page.svelte b/frontend/src/routes/[fam]/[username]/bonuses/+page.svelte index 4733993..8f3b744 100644 --- a/frontend/src/routes/[fam]/[username]/bonuses/+page.svelte +++ b/frontend/src/routes/[fam]/[username]/bonuses/+page.svelte @@ -205,7 +205,7 @@ ); - +

@@ -262,10 +262,11 @@

No templates yet

{/if}
+
- +
@@ -730,18 +731,22 @@ font-size: 1rem; } .template-list { - display: flex; + display: grid; gap: 0.5rem; - flex-wrap: wrap; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); } .template-card { - background: #f8f9fa; - border: 2px solid #e5e7eb; - border-radius: 0.5rem; - padding: 0.5rem; + background: white; + border: 1px solid #e5e7eb; + border-left: 4px solid #8b5cf6; + border-radius: 10px; + padding: 0.65rem 0.85rem; cursor: grab; transition: all 0.2s; - min-width: 160px; + } + .template-card:hover { + transform: translateY(-1px); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08); } .template-card:active { cursor: grabbing; @@ -750,26 +755,33 @@ .kanban { display: grid; grid-template-columns: repeat(3, 1fr); - gap: 1rem; + gap: 0.75rem; + min-width: 450px; } .column { - background: #f9fafb; - border-radius: 0.5rem; + border: 1px solid #e5e7eb; + border-top: 4px solid #6366f1; + border-radius: 12px; padding: 0.75rem; min-height: 200px; + background: #fafbff; } + .member-col { border-top-color: #6366f1; background: #eef2ff; } + .comp-col { border-top-color: #f59e0b; background: #fffbeb; } + .collab-col { border-top-color: #10b981; background: #ecfdf5; } .column h3 { margin: 0 0 0.5rem; - font-size: 0.9rem; + font-size: 0.95rem; } .card-head { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 0.3rem; + margin-bottom: 0.4rem; } .card-head strong { font-size: 0.85rem; + color: #1f2937; } .badges { display: flex; @@ -807,22 +819,31 @@ color: #9ca3af; padding: 0 0.25rem; line-height: 1; + border-radius: 4px; } .del-btn:hover { color: #ef4444; + background: #fef2f2; } .empty { color: #9ca3af; - font-size: 0.8rem; + font-size: 0.85rem; text-align: center; - padding: 1rem 0; + padding: 1.5rem; } .col-card { background: white; - border: 2px solid #e5e7eb; - border-radius: 0.5rem; - padding: 0.5rem; - margin-bottom: 0.5rem; + border: 1px solid #e5e7eb; + border-left: 4px solid #6366f1; + border-radius: 10px; + padding: 0.65rem 0.85rem; + margin-bottom: 0.6rem; + transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s; + } + .col-card:hover { + transform: translateY(-1px); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08); + border-color: #c7d2fe; } .col-card.done { opacity: 0.75; @@ -834,11 +855,14 @@ .col-card-head { display: flex; justify-content: space-between; - align-items: center; + align-items: flex-start; + flex-wrap: wrap; + gap: 0.3rem; margin-bottom: 0.5rem; } .col-card-head strong { font-size: 0.85rem; + color: #1f2937; } .col-card-foot { margin-top: 0.5rem; @@ -848,30 +872,36 @@ .member-progress { display: flex; align-items: center; - gap: 0.3rem; + gap: 0.4rem; + margin-bottom: 0.3rem; } .mname { font-size: 0.8rem; - font-weight: 500; + font-weight: 600; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .bar-wrap { flex: 1; height: 8px; - background: #e5e7eb; + background: #f3f4f6; border-radius: 4px; overflow: hidden; } .bar-wrap.sm { width: 60px; + flex: none; } .bar-fill { height: 100%; - background: #6366f1; + background: linear-gradient(90deg, #6366f1, #8b5cf6); border-radius: 4px; transition: width 0.3s; } .bar-fill.met { - background: #10b981; + background: linear-gradient(90deg, #10b981, #34d399); } .stat { font-size: 0.7rem; diff --git a/frontend/src/routes/[fam]/[username]/chores/+page.server.ts b/frontend/src/routes/[fam]/[username]/chores/+page.server.ts index 74aec87..1e06cf9 100644 --- a/frontend/src/routes/[fam]/[username]/chores/+page.server.ts +++ b/frontend/src/routes/[fam]/[username]/chores/+page.server.ts @@ -4,13 +4,14 @@ import { hono } from '$lib/server/hono'; export async function load(event) { if (!event.locals.session) throw redirect(303, '/login'); const famId = event.locals.session.famId; - const [templates, members, assigned, seasons] = await Promise.all([ + const [templates, members, assigned, seasons, completions] = await Promise.all([ hono.admin.list(event, 'chore-templates', famId), hono.admin.list(event, 'members', famId), hono.admin.list(event, 'assigned-chores', famId), hono.admin.list(event, 'seasons', famId), + hono.admin.completions(event, famId), ]); - return { templates, members, assigned, seasons }; + return { templates, members, assigned, seasons, completions }; } export const actions = { diff --git a/frontend/src/routes/[fam]/[username]/chores/+page.svelte b/frontend/src/routes/[fam]/[username]/chores/+page.svelte index 67f264d..5cb637d 100644 --- a/frontend/src/routes/[fam]/[username]/chores/+page.svelte +++ b/frontend/src/routes/[fam]/[username]/chores/+page.svelte @@ -4,7 +4,11 @@ import { famStore } from '$lib/stores/fam.svelte'; import { ViewHeader, CardGrid, Card } from '$lib/components'; import { formatShortDate } from '$lib/format'; - import type { ChoreTemplate, AssignedChore, Member, Season } from '$lib/types'; + import type { ChoreTemplate, AssignedChore, Member, Season, Completion } from '$lib/types'; + + // ── Chevron SVG icons ── + const CHEVRON_DOWN = ''; + const CHEVRON_UP = ''; let { data, form } = $props(); @@ -61,6 +65,7 @@ ); let seasons = $state(famStore.initialized ? famStore.seasons : data.seasons || []); + let completions = $state((data.completions as Completion[]) || []); let filteredAssigned = $derived( seasonFilter === 'all' ? assigned : assigned.filter((a) => a.seasonIds?.includes(seasonFilter)) @@ -176,6 +181,11 @@ return d.toISOString().slice(0, 10); } + // Check if a todo has been completed by the child (todos don't reset daily like chores) + function isTodoCompleted(todoId: string): boolean { + return completions.some((c: Completion) => c.assignedChoreId === todoId); + } + // Traffic light for admin todo cards (same logic as child page) function todoUrgency(a: AssignedChore): 'blue' | 'green' | 'orange' | 'red' | 'black' { if (!a.completeBy) return 'blue'; @@ -285,28 +295,29 @@ } - + {#if form?.error}

{form.error}

{/if} -
- - -
- + -
-
-

Templates

- +
+
+

Templates — Drag to assign

+
+ + +
+
+
{#each templates as t}
handleDragStart(e, t.id)}>
openEditTemplate(t)} role="button" tabindex="0"> @@ -343,7 +354,14 @@

No templates yet

{/if}
+
+ +
+ + + +
{#each members as m}
- - - {#if accordionState[m.id]?.todos ?? true} - - {#each sortedTodosForMember(m.id) as a} - {@const urgency = todoUrgency(a)} -
openEdit(a)} - role="button" - tabindex="0" - > -
- {a.customName || 'Todo'} -
- {#if a.type === 'emoji'} - 🎯 emoji - {:else} - {a.value} {a.type} - {/if} - {#if a.completeBy} - due {formatShortDate(a.completeBy)} - {/if} -
-
- +
+ + {#if accordionState[m.id]?.todos ?? true} +
+ {#each sortedTodosForMember(m.id) as a} + {@const completed = isTodoCompleted(a.id)} + {@const urgency = todoUrgency(a)} +
!completed && openEdit(a)} + role="button" + tabindex={completed ? -1 : 0} > -
- {/each} - {/if} - - - - {#if accordionState[m.id]?.chores ?? true} - {#each assignedForMember(m.id) as a} - {@const tName = templateName(a.templateId)} -
openEdit(a)} role="button" tabindex="0"> -
- {a.customName || tName} - {a.frequency} - {a.type} - {#if seasonFilter !== 'all' && isGlobalChore(a)} - Add to +
+ {a.customName || 'Todo'} +
+ {#if a.type === 'emoji'} + 🎯 emoji + {:else} + {a.value} pts + {/if} + {#if a.completeBy} + due {formatShortDate(a.completeBy)} + {/if} +
+
+ {#if completed} + ✅ TBC completed + {:else} + {/if}
-
- {a.type === 'money' ? `£${Number(a.value).toFixed(2)}` : a.value} -
- -
- {/each} - {#if assignedForMember(m.id).length === 0} -

Drop a chore here

- {/if} + {/each} + +
{/if}
+ + +
+ + {#if accordionState[m.id]?.chores ?? true} +
+ {#each assignedForMember(m.id) as a} + {@const tName = templateName(a.templateId)} +
openEdit(a)} role="button" tabindex="0"> +
+ {a.customName || tName} + {a.frequency} + {a.type} + {#if seasonFilter !== 'all' && isGlobalChore(a)} + Add to + {/if} +
+
+ {a.type === 'money' ? `£${Number(a.value).toFixed(2)}` : `${a.value} pts`} +
+ +
+ {/each} + {#if assignedForMember(m.id).length === 0} +

Drop a chore here

+ {/if} +
+ {/if} +
+
{/each} + {#if members.length < 3} + {#each Array(3 - members.length) as _} +
+ {/each} + {/if}
- + + {#if showCreateModal} @@ -714,26 +748,48 @@ margin: 0.5rem 0; } - .kanban { + /* ── Template section (top, like bonuses) ── */ + .template-section { + margin-bottom: 0; + } + .template-header { display: flex; - gap: 1rem; - overflow-x: auto; - align-items: flex-start; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.75rem; + } + .template-header h3 { + margin: 0; + font-size: 1rem; + font-weight: 700; + } + .template-list { + display: grid; + gap: 0.5rem; + margin-bottom: 0.75rem; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + } + + /* ── Kanban (3 columns, scrolls via Card scrollX) ── */ + .kanban { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; + min-width: 450px; } .column { - flex: 1; - min-width: 200px; - max-width: 280px; - border: 2px solid #e5e7eb; - border-top-width: 4px; - border-radius: 8px; + border: 1px solid #e5e7eb; + border-top: 4px solid #6366f1; + border-radius: 12px; padding: 0.75rem; - background: #fafafa; + background: #fafbff; min-height: 200px; } .column h3 { margin: 0 0 0.75rem; - font-size: 1rem; + font-size: 0.95rem; display: flex; align-items: center; gap: 0.4rem; @@ -749,38 +805,58 @@ color: #9ca3af; font-size: 0.85rem; text-align: center; - padding: 1rem; - border: 1px dashed #d1d5db; - border-radius: 6px; + padding: 1.5rem; } .add-inline { + display: block; width: 100%; - margin-bottom: 0.5rem; - padding: 0.4rem; - background: transparent; - border: 1px dashed #d1d5db; - border-radius: 6px; - color: #6b7280; + margin-bottom: 0.35rem; + padding: 0.45rem; + background: rgba(99, 102, 241, 0.04); + border: 1px dashed #c7d2fe; + border-radius: 8px; + color: #6366f1; font-size: 0.8rem; font-weight: 600; cursor: pointer; + transition: background 0.15s, border-color 0.15s; + text-align: center; + } + .template-section > .add-inline { + margin-bottom: 0; + } + .accordion-add { + margin-bottom: 0; + margin-top: 0.35rem; + background: rgba(255,255,255,0.25); + border-color: rgba(255,255,255,0.4); + color: #fff; + } + .accordion-add:hover { + background: rgba(255,255,255,0.35); + border-color: rgba(255,255,255,0.6); + color: #fff; } .add-inline:hover { border-color: #6366f1; - color: #6366f1; background: #eef2ff; } .card { background: white; border: 1px solid #e5e7eb; - border-radius: 6px; + border-radius: 8px; padding: 0.5rem 0.75rem; - margin-bottom: 0.5rem; + margin-bottom: 0.4rem; display: flex; align-items: center; gap: 0.5rem; position: relative; + transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s; + } + .card:hover { + transform: translateY(-1px); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08); } .card-body { flex: 1; @@ -788,13 +864,14 @@ } .card-body strong { display: block; - font-size: 0.9rem; + font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + color: #1f2937; } .card-value { - font-size: 0.85rem; + font-size: 0.8rem; color: #6b7280; font-weight: 600; white-space: nowrap; @@ -824,15 +901,19 @@ color: #9ca3af; cursor: pointer; font-size: 1rem; - padding: 0 4px; + padding: 2px 6px; line-height: 1; flex-shrink: 0; + border-radius: 4px; + transition: color 0.15s, background 0.15s; } .edit-btn:hover { color: #6366f1; + background: #eef2ff; } .del-btn:hover { color: #dc2626; + background: #fef2f2; } .hint { color: #6b7280; @@ -843,18 +924,18 @@ display: flex; align-items: center; gap: 0.5rem; - margin-bottom: 1rem; } .season-filter label { - font-size: 0.85rem; - color: #374151; + font-size: 0.8rem; + color: #6b7280; font-weight: 500; } .season-filter select { - padding: 0.35rem 0.6rem; + padding: 0.3rem 0.5rem; border: 1px solid #d1d5db; border-radius: 6px; - font-size: 0.85rem; + font-size: 0.8rem; + background: #fff; } .badge.season { background: #e0f2fe; @@ -889,7 +970,7 @@ .template { cursor: grab; - border-left: 3px solid #6366f1; + border-left: 4px solid #8b5cf6; } .template:active { cursor: grabbing; @@ -897,10 +978,7 @@ } .assigned { cursor: pointer; - border-left: 3px solid #10b981; - } - .assigned:hover { - background: #f3f4f6; + border-left: 4px solid #10b981; } .overlay { @@ -962,38 +1040,45 @@ color: white; } - /* ── Accordion ── */ + /* ── Accordion wrappers ── */ + .accordion-wrapper { + background: #6366f1; + border-radius: 10px; + margin: 0.75rem 0 0.3rem; + overflow: hidden; + box-shadow: 0 2px 6px rgba(99, 102, 241, 0.25); + } + .accordion-wrapper.accordion-todos { + background: #2e8b57; + box-shadow: 0 2px 6px rgba(46, 139, 87, 0.25); + } .accordion-head { width: 100%; - padding: 0.4rem 0.6rem; - margin: 0.3rem 0 0.2rem; - background: #f3f4f6; - border: 1px solid #e5e7eb; - border-radius: 6px; + padding: 0.55rem 0.85rem; + border: none; + border-left: 4px solid rgba(0,0,0,0.2); + background: transparent; cursor: pointer; - font-size: 0.8rem; + font-size: 0.82rem; font-weight: 700; - color: #374151; + color: #fff; display: flex; justify-content: space-between; align-items: center; + transition: filter 0.15s; } .accordion-head:hover { - background: #e5e7eb; + filter: brightness(1.1); } - .accordion-todos { - background: #fffbeb; - border-color: #fcd34d; + .accordion-body { + padding: 0.5rem 0.65rem 0.65rem; + background: rgba(0,0,0,0.08); } - .accordion-todos:hover { - background: #fef3c7; - } - .accordion-arrow { - font-size: 0.7rem; - transition: transform 0.2s; - } - .accordion-arrow.open { - transform: rotate(180deg); + .accordion-chevron { + display: flex; + align-items: center; + flex-shrink: 0; + opacity: 0.8; } /* ── Todo admin cards ── */ @@ -1001,16 +1086,28 @@ background: white; border: 1px solid #e5e7eb; border-left: 4px solid #3b82f6; - border-radius: 6px; - padding: 0.4rem 0.6rem; + border-radius: 8px; + padding: 0.45rem 0.65rem; margin-bottom: 0.35rem; cursor: pointer; display: flex; align-items: center; gap: 0.4rem; + transition: transform 0.15s, box-shadow 0.15s, opacity 0.2s; } .todo-admin-card:hover { - filter: brightness(0.97); + transform: translateY(-1px); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08); + } + .todo-admin-card.todo-completed { + opacity: 0.55; + cursor: default; + background: #f0fdf4; + border-left-color: #10b981; + } + .todo-admin-card.todo-completed:hover { + transform: none; + box-shadow: none; } .todo-blue-bg { background: #eff6ff; @@ -1060,8 +1157,39 @@ font-weight: 700; } - .todo-badge { - background: #fef3c7; - color: #b45309; + .todo-completed-badge { + font-size: 0.65rem; + font-weight: 700; + color: #059669; + background: #d1fae5; + padding: 2px 6px; + border-radius: 4px; + flex-shrink: 0; + white-space: nowrap; + } + + /* ── Points badge ── */ + .badge-pts { + background: #e0e7ff; + color: #4338ca; + font-size: 0.68rem; + padding: 1px 6px; + border-radius: 10px; + font-weight: 600; + } + + /* ── Placeholder column ── */ + .column-placeholder { + border-top-color: #e5e7eb !important; + background: repeating-linear-gradient( + 45deg, + transparent, + transparent 8px, + rgba(0,0,0,0.02) 8px, + rgba(0,0,0,0.02) 16px + ); + border-style: dashed; + opacity: 0.5; + min-height: 200px; } diff --git a/frontend/src/routes/[fam]/[username]/preferences/+page.svelte b/frontend/src/routes/[fam]/[username]/preferences/+page.svelte index 65d3b68..3db86ef 100644 --- a/frontend/src/routes/[fam]/[username]/preferences/+page.svelte +++ b/frontend/src/routes/[fam]/[username]/preferences/+page.svelte @@ -11,7 +11,7 @@ const colors = ['#6366f1', '#ec4899', '#f59e0b', '#10b981', '#3b82f6', '#ef4444', '#8b5cf6', '#14b8a6']; - + {#if !data.verified} diff --git a/frontend/src/routes/[fam]/[username]/rewards/+page.svelte b/frontend/src/routes/[fam]/[username]/rewards/+page.svelte index fc40153..5f99ddd 100644 --- a/frontend/src/routes/[fam]/[username]/rewards/+page.svelte +++ b/frontend/src/routes/[fam]/[username]/rewards/+page.svelte @@ -58,7 +58,7 @@ ) - + {#if toast}
{toast}
diff --git a/frontend/src/routes/[fam]/[username]/settings/+page.svelte b/frontend/src/routes/[fam]/[username]/settings/+page.svelte index 9a8946b..acfb995 100644 --- a/frontend/src/routes/[fam]/[username]/settings/+page.svelte +++ b/frontend/src/routes/[fam]/[username]/settings/+page.svelte @@ -106,7 +106,7 @@ } - + diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 9bc98bd..9276ded 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -29,7 +29,7 @@
{:else} - + diff --git a/proxy/src/index.ts b/proxy/src/index.ts index 8b06cf1..72159be 100644 --- a/proxy/src/index.ts +++ b/proxy/src/index.ts @@ -518,6 +518,18 @@ app.delete("/api/admin/:famId/assigned-chores/:id", requireAdmin, async (c) => { } }); +// ── Admin: Completions ──────────────────────────── + +app.get("/api/admin/:famId/completions", requireAdmin, async (c) => { + try { + const { famId } = c.req.param(); + const data = await pb.getList("completions", `famId = '${famId}'`); + return c.json(data.items); + } catch (err) { + return handleError(c, err); + } +}); + // ── Admin CRUD: Seasons ────────────────────────── app.get("/api/admin/:famId/seasons", requireAdmin, async (c) => {