loads of mobile ui layout upgrades

This commit is contained in:
JCEEE
2026-09-10 11:27:27 +01:00
parent 097106fa69
commit f802113cd1
17 changed files with 501 additions and 123 deletions
+5
View File
@@ -643,4 +643,9 @@
font-size: 2rem;
}
}
@media (max-width: 639px) {
h1 {
font-size: 1.5em;
}
}
</style>
+43 -12
View File
@@ -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 @@
<div class="app-shell">
<Sidebar {famName} session={data.session} {isParent} {role} />
<TopNav
{role}
role={data.session ? role : ''}
seasons={famStore.seasons.filter((s) => s.active !== false)}
announcement={disabled && !activating ? accessMessage(accessReason, isParent) : ''}
>
@@ -191,7 +191,7 @@
{/if}
</button>
</TopNav>
<main class="app-main">
<main class="app-main" class:join-page={page.url.pathname.split('/')[2] === 'join'}>
<div class="page-wrap">
<div class="page-content" class:locked>{@render children()}</div>
@@ -218,7 +218,12 @@
</div>
</main>
{#if claimToast}
<div class="claim-toast">{claimToast}</div>
<a
class="claim-toast"
href={`/${page.params.fam}/${data.session?.username || page.params.username || ''}`}
>
{claimToast} — view dashboard →
</a>
{/if}
<Footer sidebar />
</div>
@@ -232,17 +237,33 @@
<style>
.claim-toast {
position: fixed;
top: 3.5rem;
right: 1.5rem;
background: #059669;
color: white;
padding: 0.6rem 1rem;
border-radius: 8px;
top: 52px;
left: 220px;
right: 0;
background: #f59e0b;
color: #fff;
padding: 0.5rem 1.5rem;
font-size: 0.85rem;
z-index: 999;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
font-weight: 600;
z-index: 95;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
text-decoration: none;
animation: fadein 0.2s;
}
.claim-toast:hover {
background: #d97706;
}
@media (max-width: 1023px) {
.claim-toast {
left: 56px;
}
}
@media (max-width: 639px) {
.claim-toast {
left: 0;
padding: 0.5rem 1rem;
}
}
.app-shell {
min-height: 100vh;
display: flex;
@@ -283,6 +304,16 @@
margin-left: 56px;
}
}
@media (max-width: 639px) {
.app-main {
padding-left: 1rem;
padding-right: 1rem;
}
/* Join pages center their own content — no shell padding needed */
.app-main.join-page {
padding: 0;
}
}
.page-wrap {
position: relative;
min-height: 70vh;
+27 -4
View File
@@ -49,7 +49,6 @@
let tileChores = $derived(
summary?.summaries?.reduce((s: number, m: any) => s + (m.choresCompleted || 0), 0) ?? 0
);
// ─── All-time header stats (server-computed) ───
let allTimePts = $derived(Number(stats.allTimePts) || 0);
let allTimeCash = $derived(Number(stats.allTimeCash) || 0);
@@ -58,6 +57,9 @@
// ─── Goals completed (rewards from bonus configs, pocket money excluded) ───
// Derived from famStore so realtime updates (a goal met / revoked) flow in.
let rewards = $derived(famStore.initialized ? famStore.rewards : data.rewards || []);
// All-time goals achieved — every materialized bonus reward (prize / points /
// cash), including pocket money configs.
let tileGoals = $derived(rewards.filter((r: any) => r.bonusConfigId).length);
let bonusConfigs = $derived(
famStore.initialized ? famStore.bonusConfigs : data.bonusConfigs || []
);
@@ -326,6 +328,10 @@
<span class="tile-val">£{tileMoney.toFixed(2)}</span>
<span class="tile-lbl">cash this week</span>
</div>
<div class="tile tile-goals">
<span class="tile-val">{tileGoals}</span>
<span class="tile-lbl">goals achieved</span>
</div>
</div>
<CardGrid>
@@ -364,7 +370,7 @@
{@const scVal = avgDailyPointsFor(s)}
{@const scPct = scPot > 0 ? Math.min(100, Math.round((scVal / scPot) * 100)) : 0}
{@const goals = goalsByMember.get(s.memberId)}
<div class="column col-member">
<div class="column col-member" style="--mc: {s.memberColor}">
<div class="member-head">
<span class="dot" style="background:{s.memberColor}"></span>
<span class="member-name">{s.memberName}</span>
@@ -527,7 +533,8 @@
/* ── Tiles (matching child dashboard) ── */
.tiles {
display: grid;
grid-template-columns: repeat(3, 1fr);
/* members 1 unit wide; goals / points / cash 2 units each (1+2+2+2 on one row) */
grid-template-columns: 1fr 2fr 2fr 2fr;
gap: 0.75rem;
margin-bottom: 1rem;
}
@@ -535,6 +542,9 @@
.tiles {
grid-template-columns: repeat(2, 1fr);
}
.vh-num {
font-size: 1.5em;
}
}
.tile {
border-radius: 14px;
@@ -557,6 +567,9 @@
.tile-chores {
background: linear-gradient(135deg, #0284c7, #38bdf8);
}
.tile-goals {
background: linear-gradient(135deg, #f59e0b, #f97316);
}
.tile-val {
font-size: 1.6rem;
font-weight: 800;
@@ -584,7 +597,8 @@
}
.col-member {
border-top-color: #6366f1;
background: #eef2ff;
/* Translucent wash of the member's own colour (set via --mc inline) */
background: color-mix(in srgb, var(--mc, #6366f1) 12%, transparent);
}
.member-head {
@@ -705,4 +719,13 @@
text-align: center;
padding: 1.5rem;
}
/* Mobile: member columns stack full width (same convention as the chores
kanban) instead of the 450px horizontal scroll. Declared last so it wins
over the base .kanban rule. */
@media (max-width: 639px) {
.kanban {
grid-template-columns: 1fr;
min-width: 0;
}
}
</style>
+103 -22
View File
@@ -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 @@
</div>
<div class="gm-bottom">
<span class="gm-reward">{g.rewardText}</span>
<span class="gm-date">{formatDDMMYY(g.date)}</span>
</div>
</div>
{/each}
@@ -1047,7 +1067,7 @@
{/if}
</Card>
<Card cols={1} title="Claims" accent="#f59e0b">
<Card cols={1} title="Claims" accent="#f97316" class="claims-card">
{#if claimableRewards().length === 0}
<div class="claims-empty">
{@html checkCircleIcon}<span>No outstanding claims</span>
@@ -1106,7 +1126,7 @@
<p class="section-label">Requested</p>
{#each requested as r}
<form
class="flex justify-between items-center"
class="flex flex-wrap justify-between items-center"
method="POST"
action="?/claim"
use:enhance={() => {
@@ -1118,7 +1138,7 @@
<span class="claim-icon"
>{@html r.rewardType === 'cash' ? coinsIcon : giftIcon}</span
>
<span class="claim-label">{r.label}</span>
<span class="claim-label">{rewardLabelParts(r).name}</span>
<span class="claim-value">{rewardLabel(r)}</span>
</div>
<Button type="submit" size="sm" variant="primary">Issue</Button>
@@ -1133,7 +1153,7 @@
<span class="claim-icon"
>{@html r.rewardType === 'cash' ? coinsIcon : giftIcon}</span
>
<span class="claim-label">{r.label}</span>
<span class="claim-label">{rewardLabelParts(r).name}</span>
<span class="claim-value">
{rewardLabel(r)}
{#if paydayLocked(r)}
@@ -1265,8 +1285,8 @@
{/if}
</div>
<div class="hero-date">
<span class="hero-day">{headDay()}</span>
<span class="hero-weekday">{headWeekday()}</span>
<span class="hero-day">{headDayOrdinal()}</span>
<span class="hero-month">{headMonth()}</span>
</div>
</div>
@@ -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;
}
</style>
@@ -197,7 +197,7 @@
const isShared = assignTarget === 'shared';
const body: Record<string, unknown> = {
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 @@
<div class="template-section">
<div class="template-header">
<h3>Templates — Drag to assign</h3>
<button class="add-inline" onclick={openCreateChore}>+ Create a chore</button>
</div>
<div class="template-list">
{#each templates as t}
@@ -527,11 +542,11 @@
class="accordion-wrapper accordion-chores"
class:open={accordionState[m.id]?.chores ?? true}
>
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'chores')}>
<span class="accordion-title">
<span class="accordion-icon">{@html choresIcon}</span>
<span class="accordion-label">Chores</span>
</span>
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'chores')}>
<span class="accordion-title">
<span class="accordion-icon">{@html choresIcon}</span>
<span class="accordion-label">Chores</span>
</span>
<span class="accordion-chevron"
>{@html (accordionState[m.id]?.chores ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
>
@@ -619,8 +634,7 @@
<span class="accordion-label">Competitive Chores</span>
</span>
<span class="accordion-chevron"
>{@html
(accordionState.shared?.chores ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
>{@html (accordionState.shared?.chores ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
>
</button>
<span class="accordion-count" style="background:#6366f1"
@@ -720,12 +734,15 @@
<div
class="overlay"
onclick={(e) => {
if (e.target === e.currentTarget) { assignModal = null; assignStep = 1; }
if (e.target === e.currentTarget) {
assignModal = null;
assignStep = 1;
}
}}
role="presentation"
>
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
<h3>Assign: {assignModal.t.name}</h3>
<h3>{assignModal.t ? `Assign: ${assignModal.t.name}` : 'Create a chore'}</h3>
<!-- Step tabs -->
<div class="step-tabs" role="tablist">
@@ -743,14 +760,22 @@
{#if assignStep === 1}
<!-- Step 1: Who -->
<label>Name <input bind:value={assignName} placeholder="Task name" /></label>
<label>Description <input bind:value={assignDescription} placeholder="Optional description" /></label>
<label
>Description <input
bind:value={assignDescription}
placeholder="Optional description"
/></label
>
<label>
Assign to
<select bind:value={assignTarget} onchange={() => {
if (assignTarget === 'member' && !assignModal?.memberId && members.length) {
assignModal = { ...assignModal!, memberId: members[0].id };
}
}}>
<select
bind:value={assignTarget}
onchange={() => {
if (assignTarget === 'member' && !assignModal?.memberId && members.length) {
assignModal = { ...assignModal!, memberId: members[0].id };
}
}}
>
<option value="member">A specific member</option>
<option value="shared">Shared (all members)</option>
</select>
@@ -824,7 +849,13 @@
{#if assignStep > 1}
<button type="button" onclick={() => assignStep--}>Back</button>
{:else}
<button type="button" onclick={() => { assignModal = null; assignStep = 1; }}>Cancel</button>
<button
type="button"
onclick={() => {
assignModal = null;
assignStep = 1;
}}>Cancel</button
>
{/if}
{#if assignStep < 3}
<button type="button" class="primary" onclick={() => assignStep++}>Next</button>
@@ -1163,7 +1194,6 @@
width: 100%;
}
}
/* ── Kanban (3 columns, scrolls via Card scrollX) ── */
.kanban {
display: flex;
@@ -1849,4 +1879,23 @@
opacity: 0.5;
min-height: 200px;
}
/* Mobile: declared last so it wins over the base .kanban/.column rules
(same specificity, source order decides). */
@media (max-width: 639px) {
.template-list {
display: none;
}
.kanban {
flex-direction: column;
overflow-x: visible;
}
.column {
flex: none;
width: 100%;
min-height: auto;
}
.column-placeholder {
display: none;
}
}
</style>
@@ -5,14 +5,15 @@ export async function load(event) {
if (!event.locals.user) throw redirect(303, '/login');
const famId = event.locals.user.famId;
const s = servicesFor(event);
const [rewards, members, assigned, templates, completions] = await Promise.all([
const [rewards, members, assigned, templates, completions, fam] = await Promise.all([
s.crud.list('rewards', famId),
s.crud.list('members', famId),
s.crud.list('assigned-chores', famId),
s.crud.list('chore-templates', famId),
s.crud.list('completions', famId)
s.crud.list('completions', famId),
s.fam.get(famId)
]);
return { rewards, members, assigned, templates, completions };
return { rewards, members, assigned, templates, completions, fam };
}
export const actions = {
@@ -29,4 +30,4 @@ export const actions = {
return { error: e instanceof Error ? e.message : 'Failed to claim reward' };
}
}
};
};
@@ -2,7 +2,8 @@
import { enhance } from '$app/forms';
import { famStore } from '$lib/stores/fam.svelte';
import { ViewHeader, CardGrid, Card, Button } from '$lib/components';
import { formatDDMMYY } from '$lib/format';
import { formatDDMMYY, formatShortDate } from '$lib/format';
import { resolveTz, weekStart as tzWeekStart, addDaysStr } from '@shared/timezone';
import type { Member, AssignedChore, ChoreTemplate, Completion, Reward } from '$lib/types';
let { data } = $props();
@@ -24,6 +25,28 @@
famStore.initialized ? famStore.templates : ((data.templates || []) as ChoreTemplate[])
);
// ── Week navigation (mirrors the member dashboard: payday-anchored weeks) ──
// Anchor comes from data.fam at SSR; famStore takes over after init so a
// payday/timezone change in settings flows through too.
let fam = $derived(
famStore.initialized ? (famStore.fam as any) : ((data as any).fam as any) || {}
);
let famPayday = $derived(fam?.payday != null ? Number(fam.payday) : 1);
let famTz = $derived(resolveTz(fam?.timezone || 'auto'));
let currentWeek = $derived.by(() => tzWeekStart(famPayday, famTz));
let weekStart = $state(tzWeekStart(famPayday, famTz));
let weekEnd = $derived(addDaysStr(weekStart, 6));
let isCurrentWeek = $derived(weekStart === currentWeek);
function inWeek(dateStr: string | undefined): boolean {
const d = (dateStr || '').slice(0, 10);
return !!d && d >= weekStart && d <= weekEnd;
}
function navigateWeek(amount: number) {
weekStart = addDaysStr(weekStart, amount * 7);
}
let activeTab = $state('rewards');
let memberMap = $derived.by(() => new Map(members.map((m: any) => [m.id, m])));
@@ -43,10 +66,22 @@
}
// ── Rewards tab ──
// Rewards store the label as "Bonus name – prize" (built in bonuses.ts:
// cash → "Reading Bonus 3 – £5.00", points → "Reading Bonus 3 – 50",
// items → "Reading Bonus 3 – fluffy toy"). Split it so the description
// column shows the bonus name and the amount column shows the prize;
// labels without a separator keep the full text in both columns.
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 rewardAmount(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 rewardStatus(r: any): string {
@@ -71,8 +106,11 @@
})
);
let weekRewards = $derived(sortedRewards.filter((r: any) => inWeek(r.date)));
// Footer total matches the visible week, not all-time history.
let totalOutstanding = $derived(
rewards
weekRewards
.filter((r: any) => r.status !== 'claimed')
.filter((r: any) => r.rewardType === 'cash')
.reduce((sum: number, r: any) => sum + Number(r.value), 0)
@@ -93,6 +131,9 @@
[...todoCompletions].sort((a: any, b: any) => (b.date || '').localeCompare(a.date || ''))
);
let weekChoreCompletions = $derived(sortedChoreCompletions.filter((c: any) => inWeek(c.date)));
let weekTodoCompletions = $derived(sortedTodoCompletions.filter((c: any) => inWeek(c.date)));
let toast = $state('');
async function fire() {
@@ -125,6 +166,11 @@
active: activeTab,
onchange: (v: string) => (activeTab = v)
}}
weeknav={{
current: isCurrentWeek ? 'This week' : formatShortDate(weekStart),
onPrev: () => navigateWeek(-1),
onNext: () => navigateWeek(1)
}}
/>
{#if toast}
@@ -134,10 +180,11 @@
<CardGrid stackTablet>
{#if activeTab === 'rewards'}
<Card cols={3}>
{#if rewards.length === 0}
{#if weekRewards.length === 0}
<p class="empty">
No rewards yet. Points earned will automatically create bonus rewards when thresholds are
met.
{isCurrentWeek
? 'No rewards yet. Points earned will automatically create bonus rewards when thresholds are met.'
: `No rewards for the week of ${formatShortDate(weekStart)}.`}
</p>
{:else}
<table>
@@ -152,15 +199,19 @@
</tr>
</thead>
<tbody>
{#each sortedRewards as r}
{#each weekRewards as r}
{@const outstanding = isOutstanding(r)}
<tr class:claimed={!outstanding && !isRequested(r)} class:requested={isRequested(r)}>
<tr
class:claimed={!outstanding && !isRequested(r)}
class:requested={isRequested(r)}
style="--mc:{memberColor(r.memberId)}"
>
<td class="date">{r.date ? formatDDMMYY(r.date) : '—'}</td>
<td>
<span class="dot" style="background:{memberColor(r.memberId)}"></span>
{memberName(r.memberId)}
</td>
<td>{r.label}</td>
<td>{rewardLabelParts(r).name}</td>
<td class="num">{rewardAmount(r)}</td>
<td>
<span
@@ -208,8 +259,12 @@
</Card>
{:else if activeTab === 'chores'}
<Card cols={3} scrollX title="Chore completions">
{#if sortedChoreCompletions.length === 0}
<p class="empty">No chore completions yet.</p>
{#if weekChoreCompletions.length === 0}
<p class="empty">
{isCurrentWeek
? 'No chore completions yet.'
: `No chore completions for the week of ${formatShortDate(weekStart)}.`}
</p>
{:else}
<table>
<thead>
@@ -221,9 +276,9 @@
</tr>
</thead>
<tbody>
{#each sortedChoreCompletions as c}
{#each weekChoreCompletions as c}
{@const a = assignedMap.get(c.assignedChoreId)}
<tr>
<tr style="--mc:{memberColor(c.memberId)}">
<td class="date">{formatDDMMYY(c.date)}</td>
<td>
<span class="dot" style="background:{memberColor(c.memberId)}"></span>
@@ -242,8 +297,12 @@
</Card>
{:else}
<Card cols={3} scrollX title="Todo completions">
{#if sortedTodoCompletions.length === 0}
<p class="empty">No todos completed yet.</p>
{#if weekTodoCompletions.length === 0}
<p class="empty">
{isCurrentWeek
? 'No todos completed yet.'
: `No todos completed for the week of ${formatShortDate(weekStart)}.`}
</p>
{:else}
<table>
<thead>
@@ -255,9 +314,9 @@
</tr>
</thead>
<tbody>
{#each sortedTodoCompletions as c}
{#each weekTodoCompletions as c}
{@const a = assignedMap.get(c.assignedChoreId)}
<tr>
<tr style="--mc:{memberColor(c.memberId)}">
<td class="date">{formatDDMMYY(c.date)}</td>
<td>
<span class="dot" style="background:{memberColor(c.memberId)}"></span>
@@ -356,4 +415,20 @@
border-top: 2px solid #d1d5db;
padding-top: 0.6rem;
}
@media (max-width: 639px) {
/* Hide Member (2nd) and Status (5th, rewards only) columns */
th:nth-child(2),
td:nth-child(2) {
display: none;
}
th:nth-child(5),
td:nth-child(5) {
display: none;
}
/* Member colour as the entry background instead of the member column */
tbody tr,
tbody tr.requested {
background: color-mix(in srgb, var(--mc, #6366f1) 10%, transparent);
}
}
</style>
@@ -1161,4 +1161,7 @@
.resume-hint {
margin-top: 0.5rem;
}
@media (max-width: 639px) {
.code-display { font-size: 1.5em; }
}
</style>
+4 -2
View File
@@ -14,7 +14,9 @@
<main class="mx-auto flex min-h-screen max-w-md flex-col items-center justify-center px-6">
<section class="w-full rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-indigo-100 text-2xl">
<div
class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-indigo-100 text-2xl"
>
<span class="home-badge">{@html homeIcon}</span>
</div>
<h1 class="text-xl font-bold text-slate-900">Join {famSlug}</h1>
@@ -51,7 +53,7 @@
maxlength="6"
placeholder="6-digit code"
autocomplete="one-time-code"
class="w-full rounded-lg border border-slate-300 px-4 py-3 text-center text-2xl tracking-[0.5em] text-slate-900 outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200"
class="w-full rounded-lg border border-slate-300 px-4 py-3 text-center text-lg tracking-[0.5em] text-slate-900 outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200"
/>
{#if form?.error}
<p class="text-sm font-medium text-rose-600">{form.error}</p>
@@ -58,7 +58,7 @@
maxlength="6"
placeholder="6-digit code"
autocomplete="one-time-code"
class="w-full rounded-lg border border-slate-300 px-4 py-3 text-center text-2xl tracking-[0.5em] text-slate-900 outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200"
class="w-full rounded-lg border border-slate-300 px-4 py-3 text-center text-lg tracking-[0.5em] text-slate-900 outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200"
/>
{#if isParent}
<input