varierty of implementstions based on platform droplet issuance

This commit is contained in:
JCEEE
2026-08-26 10:17:32 +01:00
parent 7ebabc6e62
commit 8fea21704a
16 changed files with 1118 additions and 452 deletions
@@ -124,7 +124,7 @@
setTimeout(() => (toast = ''), 4000);
}
function handleResult({ result }, onSuccess = null) {
function handleResult({ result }: any, onSuccess: ((d: any) => void) | null = null) {
const d = result.data || {};
if (d.error) showToast(d.error);
else if (result.type === 'success') {
@@ -273,6 +273,38 @@
})
);
// Celebration badge — completed todos from the last 24h, dismissed per-device
// via localStorage (no PB storage; auto-expires after 24h).
let celebrateDismissed = $state<string[]>([]);
if (typeof localStorage !== 'undefined') {
celebrateDismissed = JSON.parse(localStorage.getItem('celebrateDismissed') || '[]');
}
function dismissCelebrate(id: string) {
if (!celebrateDismissed.includes(id)) {
celebrateDismissed = [...celebrateDismissed, id];
try {
localStorage.setItem('celebrateDismissed', JSON.stringify(celebrateDismissed));
} catch {}
}
}
let celebrations = $derived.by(() => {
const cutoff = Date.now() - 24 * 3600 * 1000;
return completions
.filter((c: Completion) => {
if (c.id.startsWith('optimistic-')) return false;
const at = c.completedAt ? new Date(c.completedAt).getTime() : 0;
if (!at || at < cutoff) return false;
const chore = assigned.find((a) => a.id === c.assignedChoreId);
return !!chore?.isTodo && !celebrateDismissed.includes(c.id);
})
.map((c: Completion) => {
const chore = assigned.find((a) => a.id === c.assignedChoreId);
const emoji =
chore?.type === 'emoji' ? chore.emoji || '🎉' : chore?.type === 'money' ? '💸' : '⭐';
return { id: c.id, emoji, name: chore?.customName || 'Todo' };
});
});
let dailyPending = $derived(
memberChores.filter((a) => a.frequency === 'daily' && !isCompleted(a.id, todayChild))
);
@@ -640,11 +672,13 @@
<div class="card-header">
<span class="dot" style="background:{m.color}"></span>
<span class="member-name">{m.name}</span>
<Button
href="/{famSlug}/{username}/bonuses?s=pocketmoney"
variant="secondary"
size="sm">Pocket money</Button
>
{#if !(pm && pm.rewardValue)}
<Button
href="/{famSlug}/{username}/bonuses?s=pocketmoney"
variant="secondary"
size="sm">Pocket money</Button
>
{/if}
</div>
<div class="stats">
<span>Points: {s?.pointsEarned ?? 0}</span>
@@ -1163,6 +1197,17 @@ const res = await fetch('/api/members', {
</div>
</div>
{:else}
{#if celebrations.length > 0}
<div class="celebrations">
{#each celebrations as cel}
<div class="celebrate-card">
<span class="celebrate-emoji">{cel.emoji}</span>
<span class="celebrate-text"><strong>{cel.name}</strong> complete — amazing work!</span>
<button class="celebrate-close" onclick={() => dismissCelebrate(cel.id)}>✕</button>
</div>
{/each}
</div>
{/if}
<div class="kanban">
<div class="column col-daily">
<h2>🎯 Daily ({dailyPending.length})</h2>
@@ -1206,7 +1251,7 @@ const res = await fetch('/api/members', {
<span class="checkbox">📋</span>
<span class="chore-name">{todo.customName || 'Todo'}</span>
<span class="todo-label">todo</span>
<span class="chore-value">🎯</span>
<span class="chore-value">{todo.emoji || '🎯'}</span>
</div>
{:else}
<button
@@ -1222,7 +1267,11 @@ const res = await fetch('/api/members', {
<span class="checkbox">📋</span>
<span class="chore-name">{todo.customName || 'Todo'}</span>
<span class="todo-label">todo</span>
<span class="chore-value">{todo.value} {todo.type}</span>
<span class="chore-value">
{todo.type === 'money'
? `£${Number(todo.value).toFixed(2)}`
: `${todo.value} pts`}
</span>
</button>
{/if}
{/each}
@@ -1879,6 +1928,48 @@ const res = await fetch('/api/members', {
}
/* ── Kanban ── */
.celebrations {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.celebrate-card {
display: flex;
align-items: center;
gap: 0.6rem;
background: linear-gradient(90deg, #fef3c7, #fce7f3);
border: 1px solid #fcd34d;
border-radius: 12px;
padding: 0.55rem 0.85rem;
animation: celebrate-pop 0.35s ease-out;
}
@keyframes celebrate-pop {
from {
transform: scale(0.9);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.celebrate-emoji {
font-size: 1.6rem;
}
.celebrate-text {
flex: 1;
font-size: 0.85rem;
color: #78350f;
}
.celebrate-close {
background: none;
border: none;
color: #92400e;
cursor: pointer;
font-size: 0.85rem;
padding: 0.15rem;
}
.kanban {
display: grid;
grid-template-columns: 1fr 1fr 1fr;