diff --git a/frontend/src/routes/[fam]/[username]/+page.svelte b/frontend/src/routes/[fam]/[username]/+page.svelte index dfc0e18..acf20a6 100644 --- a/frontend/src/routes/[fam]/[username]/+page.svelte +++ b/frontend/src/routes/[fam]/[username]/+page.svelte @@ -202,9 +202,12 @@ const isCurrentWeek = $derived(weekStart === currentWeek); const daysLeft = $derived( - Math.round( - (new Date(weekEnd + 'T00:00:00').getTime() - new Date(todayIso + 'T00:00:00').getTime()) / - 86400000 + Math.max( + 0, + Math.round( + (new Date(weekEnd + 'T00:00:00').getTime() - new Date(todayIso + 'T00:00:00').getTime()) / + 86400000 + ) ) ); @@ -682,286 +685,266 @@ {:else} - -
-
- { - e.stopPropagation(); - showColorPicker = !showColorPicker; - }} - onkeydown={(e) => e.key === 'Enter' && (showColorPicker = !showColorPicker)} - > - {#if editingName} - { - if (e.key === 'Enter') { - editingName = false; - if (nameInput && nameInput !== memberName) { - const old = memberName; - memberName = nameInput; - try { - const res = await fetch('/api/members/me', { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'x-device-token': deviceToken, - 'x-device-famid': famId - }, - body: JSON.stringify({ name: nameInput }) - }); - if (!res.ok) memberName = old; - } catch { - memberName = old; - } - } - } else if (e.key === 'Escape') { - editingName = false; - nameInput = memberName; - } - }} - onblur={() => { - editingName = false; - nameInput = memberName; - }} - autofocus - /> - {:else} - { - nameInput = memberName; - editingName = true; - }} - onkeydown={(e) => e.key === 'Enter' && ((nameInput = memberName), (editingName = true))} - > - {memberName || username} - - {/if} -
- - {#if showColorPicker} - -
(showColorPicker = false)}> -
e.stopPropagation()}> -

Pick a color:

-
- {#each ['#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#3b82f6', '#6366f1', '#a855f7', '#ec4899', '#78716c'] as color} - - {/each} -
-
-
- {/if} - -
-
- {headDay()} - {headMonth()} -
-
- {daysLeft} - days left -
-
- -
- - - -
-
- + {#if loading}

Loading...

{:else if error}

{error}

{:else} - - - - {#if weeklyTotal > 0} - {@const wpct = Math.max(0, Math.min(100, weeklyPct))} -
{weeklyDone}/{weeklyTotal} done
-
-
-
-
- {:else} -

No chores assigned

- {/if} -
+ +
+
+
+ { + e.stopPropagation(); + showColorPicker = !showColorPicker; + }} + onkeydown={(e) => e.key === 'Enter' && (showColorPicker = !showColorPicker)} + > + {#if editingName} + { + if (e.key === 'Enter') { + editingName = false; + if (nameInput && nameInput !== memberName) { + const old = memberName; + memberName = nameInput; + try { + const res = await fetch('/api/members/me', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'x-device-token': deviceToken, + 'x-device-famid': famId + }, + body: JSON.stringify({ name: nameInput }) + }); + if (!res.ok) memberName = old; + } catch { + memberName = old; + } + } + } else if (e.key === 'Escape') { + editingName = false; + nameInput = memberName; + } + }} + onblur={() => { + editingName = false; + nameInput = memberName; + }} + autofocus + /> + {:else} + { + nameInput = memberName; + editingName = true; + }} + onkeydown={(e) => + e.key === 'Enter' && ((nameInput = memberName), (editingName = true))} + > + {memberName || username} + + {/if} +
+
+ {headDay()} + {headMonth()} +
+
- - {#if weekBonusTallies.length === 0} -

No bonuses yet

- {:else} -
- {#each weekBonusTallies as t} -
- {t.name} - {t.count} -
- {/each} -
- {/if} -
- - -
-
- Cash - ยฃ{allTimeCash.toFixed(2)} -
-
- Points - {allTimePoints} + {#if showColorPicker} + +
(showColorPicker = false)}> +
e.stopPropagation()}> +

Pick a color:

+
+ {#each ['#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#3b82f6', '#6366f1', '#a855f7', '#ec4899', '#78716c'] as color} + + {/each} +
- + {/if} - -
ยฃ{pendingCash.toFixed(2)}
-

pending this week

-
+
+ {weeklyTotal > 0 ? weeklyPct : 0}% +
+
+
+ + {weeklyTotal > 0 ? `${weeklyDone}/${weeklyTotal} done` : 'no chores yet'} + +
- -
{pendingPoints}
-

pending this week

-
- - - {#if pendingRewards.length === 0} -

Nothing to claim

+
+ {#if weekBonusTallies.length > 0} +
+ {#each weekBonusTallies as t} + ๐ŸŽ‰ {t.name} ร—{t.count} + {/each} +
{:else} + no bonuses earned yet + {/if} +
{daysLeft} days left
+
+ + +
+ + +
+
+ ยฃ{pendingCash.toFixed(2)} + pending cash +
+
+ {pendingPoints} + pending points +
+
+ + +
+
+

๐ŸŽฏ Daily ({dailyPending.length})

+ {#if dailyPending.length === 0} +

All done!

+ {:else} + {#each dailyPending as chore} + + {/each} + {/if} +
+
+

๐Ÿ“… Weekly ({weeklyPending.length})

+ {#if weeklyPending.length === 0} +

All done!

+ {:else} + {#each weeklyPending as chore} + + {/each} + {/if} +
+
+

โœ… Done ({completedToday.length})

+ {#if completedToday.length === 0} +

Nothing yet

+ {:else} + {#each completedToday as c} + {@const chore = assigned.find((a) => a.id === c.assignedChoreId)} + {#if chore} + + {/if} + {/each} + {/if} +
+
+ + +
+
+ ๐Ÿ’ฐ Wallet + {#if allTimeCash > 0 || allTimePoints > 0} + + ยฃ{allTimeCash.toFixed(2)} + ยท + {allTimePoints} pts + + {/if} +
+ {#if pendingRewards.length === 0} +

No rewards waiting

+ {:else} +
{#each pendingRewards as r} - {@const isRequested = r.status === 'requested'} -
- - {#if isRequested}๐Ÿ•“ requested{:else}โ–ช๏ธ claimable{/if} - - {r.label} - + {@const isReq = r.status === 'requested'} +
+ {r.label} + {#if r.rewardType === 'cash'} ยฃ{Number(r.value).toFixed(2)} {:else if r.rewardType === 'prize'} - ๐ŸŽ {r.value} + ๐ŸŽ {:else} {r.value} pts {/if} - {#if isRequested} - waiting + {#if isReq} + โณ waiting {:else} - Request - {/if}
{/each} - {/if} - - - - - - -
-
-

Daily Pending ({dailyPending.length})

- {#if dailyPending.length === 0} -

All done!

- {:else} - {#each dailyPending as chore} - - {/each} - {/if} -
- -
-

Weekly Pending ({weeklyPending.length})

- {#if weeklyPending.length === 0} -

All done!

- {:else} - {#each weeklyPending as chore} - - {/each} - {/if} -
- -
-

Completed Today ({completedToday.length})

- {#if completedToday.length === 0} -

Nothing yet

- {:else} - {#each completedToday as c} - {@const chore = assigned.find((a) => a.id === c.assignedChoreId)} - {#if chore} - - {/if} - {/each} - {/if} -
-
-
+ {/if} +
{/if} {/if} @@ -1207,68 +1190,6 @@ text-align: center; } - .bar-wrap.lg { - height: 14px; - } - - .kanban { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 0.75rem; - } - .column { - border: 1px solid #e5e7eb; - border-radius: 8px; - padding: 0.75rem; - background: #f9fafb; - } - .column h2 { - margin: 0 0 0.75rem; - font-size: 0.95rem; - } - - .chore { - display: flex; - align-items: center; - gap: 0.4rem; - width: 100%; - padding: 0.5rem 0.6rem; - margin-bottom: 0.4rem; - border: 1px solid #e5e7eb; - border-radius: 6px; - background: white; - cursor: pointer; - text-align: left; - font-size: 0.85rem; - } - .chore:hover { - background: #f3f4f6; - } - .chore:disabled { - opacity: 0.5; - cursor: not-allowed; - } - .done { - opacity: 0.65; - } - .checkbox { - font-size: 1rem; - flex-shrink: 0; - } - .chore-name { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .chore-value { - font-size: 0.75rem; - color: #6b7280; - font-weight: 600; - white-space: nowrap; - } - .section-label { font-size: 0.75rem; font-weight: 600; @@ -1296,10 +1217,6 @@ cursor: pointer; flex-shrink: 0; } - .name-display { - cursor: pointer; - border-bottom: 1px dashed #d1d5db; - } .name-edit { font-size: 1.2rem; padding: 0.1rem 0.3rem; @@ -1345,221 +1262,344 @@ transform: scale(1.1); } - .wk-head { + /* โ”€โ”€ Child view โ”€โ”€ */ + + .hero { + background: linear-gradient(135deg, #6366f1, #8b5cf6 55%, #a855f7); + border-radius: 16px; + padding: 1.25rem 1.5rem; + margin-bottom: 1rem; + color: #fff; + box-shadow: 0 10px 30px rgba(99, 102, 241, 0.35); + } + .hero-top { display: flex; align-items: center; - gap: 1.5rem; - flex-wrap: wrap; justify-content: space-between; - margin-bottom: 1.25rem; + gap: 0.75rem; + margin-bottom: 1rem; } - .wk-head-main { + .hero-id { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.6rem; + min-width: 0; } - .wk-date { - display: flex; - align-items: center; - gap: 1.25rem; + .hero-dot { + border-color: rgba(255, 255, 255, 0.6); + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.35); } - .wk-date-left { + .hero-name { + font-size: 1.5rem; + font-weight: 800; + color: #fff; + cursor: pointer; + border-bottom: 1px dashed rgba(255, 255, 255, 0.4); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .name-edit-dark { + background: rgba(255, 255, 255, 0.92); + color: #1e1b4b; + border-color: #fff; + font-size: 1.2rem; + } + .hero-date { display: flex; align-items: baseline; - gap: 0.5rem; + gap: 0.45rem; + flex-shrink: 0; } - .wk-day { - font-size: 3.5rem; + .hero-day { + font-size: 1.7rem; font-weight: 800; line-height: 1; - color: #111827; + color: #fff; } - .wk-month { - font-size: 1rem; - color: #6b7280; + .hero-month { + font-size: 0.85rem; font-weight: 600; + color: rgba(255, 255, 255, 0.85); } - .wk-days-left { + .hero-progress { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.7rem; + } + .hero-pct { + font-size: 1.5rem; + font-weight: 800; + color: #fff; + flex-shrink: 0; + min-width: 3.4rem; + } + .hero-bar { + flex: 1; + height: 12px; + background: rgba(255, 255, 255, 0.28); + } + .hero-fill { + background: #fff; + } + .hero-count { + font-size: 0.8rem; + font-weight: 700; + color: rgba(255, 255, 255, 0.95); + flex-shrink: 0; + white-space: nowrap; + } + .hero-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 0.9rem; + flex-wrap: wrap; + } + .hero-pills { + display: flex; + gap: 0.4rem; + flex-wrap: wrap; + } + .hero-pill { + font-size: 0.75rem; + font-weight: 700; + padding: 3px 10px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.2); + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.5); + } + .hero-null { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.7); + } + .hero-days { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.9); + } + .hero-days b { + font-size: 1.4rem; + margin-right: 0.15rem; + } + .hero-nav { + display: flex; + gap: 0.4rem; + } + .hero-nav-btn { + font-size: 0.85rem; + font-weight: 700; + padding: 0.35rem 0.9rem; + border: none; + border-radius: 8px; + background: rgba(255, 255, 255, 0.2); + color: #fff; + cursor: pointer; + } + .hero-nav-btn:hover { + background: rgba(255, 255, 255, 0.32); + } + .hero-nav-btn.cur { + flex: 1; + background: #fff; + color: #6d28d9; + } + + .tiles { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.75rem; + margin-bottom: 1rem; + } + .tile { + border-radius: 14px; + padding: 0.9rem 1.1rem; + color: #fff; display: flex; flex-direction: column; - align-items: center; - padding-left: 1.25rem; - border-left: 2px solid #e5e7eb; + gap: 0.15rem; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); } - .wk-days-num { - font-size: 2.5rem; + .tile-cash { + background: linear-gradient(135deg, #059669, #10b981); + } + .tile-pts { + background: linear-gradient(135deg, #7c3aed, #a855f7); + } + .tile-val { + font-size: 1.6rem; font-weight: 800; - line-height: 1; - color: #6366f1; } - .wk-days-label { + .tile-lbl { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.06em; + opacity: 0.9; + } + + /* โ”€โ”€ Kanban โ”€โ”€ */ + .kanban { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 0.75rem; + margin-bottom: 1rem; + } + .column { + border: 1px solid #e5e7eb; + border-top: 4px solid #6366f1; + border-radius: 12px; + padding: 0.75rem; + background: #fafbff; + } + .column h2 { + margin: 0 0 0.75rem; + font-size: 0.95rem; + } + .col-daily { + border-top-color: #6366f1; + background: #eef2ff; + } + .col-weekly { + border-top-color: #f59e0b; + background: #fffbeb; + } + .col-done { + border-top-color: #10b981; + background: #ecfdf5; + } + + .chore { + display: flex; + align-items: center; + gap: 0.4rem; + width: 100%; + padding: 0.5rem 0.6rem; + margin-bottom: 0.4rem; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: white; + cursor: pointer; + text-align: left; + font-size: 0.85rem; + } + .chore:hover { + transform: translateY(-1px); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.08); + border-color: #c7d2fe; + } + .chore:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + box-shadow: none; + } + .done { + opacity: 0.7; + } + .checkbox { + font-size: 1rem; + flex-shrink: 0; + } + .chore-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .chore-value { font-size: 0.75rem; color: #6b7280; + font-weight: 600; + white-space: nowrap; + } + + .wallet { + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 14px; + padding: 1rem 1.25rem; + } + .wallet-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + margin-bottom: 0.5rem; + } + .wallet-lbl { + font-size: 0.8rem; + font-weight: 800; + color: #374151; text-transform: uppercase; letter-spacing: 0.05em; } - .wk-nav { + .wallet-total { display: flex; gap: 0.4rem; align-items: center; - } - .wk-nav-btn { - font-size: 0.8rem; - padding: 0.4rem 0.7rem; - border: 1px solid #d1d5db; - border-radius: 6px; - background: white; - cursor: pointer; - font-weight: 600; - color: #374151; - } - .wk-nav-btn:hover { - background: #f3f4f6; - } - .wk-nav-btn.active { - background: #6366f1; - color: white; - border-color: #6366f1; - } - - .big-stat { - font-size: 2.2rem; - font-weight: 800; - line-height: 1.1; - margin-bottom: 0.25rem; - } - .big-stat.cash { - color: #059669; - } - .big-stat.points { - color: #7c3aed; - } - .big-sub { - font-size: 1rem; - font-weight: 600; - color: #6b7280; - } - .row-spacer { - height: 1.25rem; - } - .stat-sub { - font-size: 0.8rem; - color: #6b7280; - margin: 0; - } - - .bonus-tallies { - display: flex; - flex-direction: column; - gap: 0.5rem; - } - .bonus-tally { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; - padding: 0.4rem 0.6rem; - border: 1px solid #e5e7eb; - border-left: 4px solid #f59e0b; - border-radius: 8px; - background: white; - } - .bt-name { - font-size: 0.85rem; - font-weight: 600; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .bt-count { font-size: 0.9rem; - font-weight: 700; + font-weight: 800; + } + .wallet-cash { + color: #059669; + } + .wallet-pts { color: #7c3aed; } - - .alltime { + .wallet-dot { + color: #d1d5db; + } + .wallet-empty { + margin: 0; + font-size: 0.85rem; + color: #9ca3af; + } + .wallet-rewards { display: flex; flex-direction: column; gap: 0.5rem; } - .alltime-row { - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid #f3f4f6; - padding-bottom: 0.4rem; - } - .alltime-row:last-child { - border-bottom: none; - } - .alltime-label { - font-size: 0.85rem; - color: #6b7280; - } - .alltime-value { - font-size: 1.1rem; - font-weight: 700; - } - .alltime-value.cash { - color: #059669; - } - .alltime-value.points { - color: #7c3aed; - } - - .claim-list-row { + .wallet-reward { display: flex; align-items: center; - gap: 0.5rem; - padding: 0.5rem 0; - border-bottom: 1px solid #f3f4f6; + gap: 0.6rem; + padding: 0.6rem 0.8rem; + border-radius: 10px; + background: #fafbfc; + border: 1px solid #eeeff2; } - .claim-list-row:last-child { - border-bottom: none; - } - .cl-status { - font-size: 0.7rem; - font-weight: 600; - min-width: 5.5rem; - } - .cl-label { + .wr-label { flex: 1; - font-size: 0.85rem; font-weight: 600; + font-size: 0.85rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .cl-value { - font-size: 0.85rem; - font-weight: 700; + .wr-val { + font-weight: 800; color: #059669; - white-space: nowrap; } - .cl-wait { - font-size: 0.7rem; - padding: 2px 8px; - border-radius: 10px; - background: #dbeafe; - color: #1e40af; + .wr-pending { + font-size: 0.75rem; font-weight: 600; + color: #6b7280; } - .cl-cta { + .wr-cta { font-size: 0.72rem; - padding: 3px 10px; - background: #6366f1; - color: white; + font-weight: 700; + padding: 3px 12px; + background: linear-gradient(135deg, #6366f1, #8b5cf6); + color: #fff; border: none; - border-radius: 5px; + border-radius: 6px; cursor: pointer; - font-weight: 600; } - .cl-cta:hover { - background: #4f46e5; - } - .claim-list-row.requested { - opacity: 0.7; + .wr-cta:hover { + filter: brightness(1.1); }