remove payday debug and add multiple ui fixes

This commit is contained in:
JCEEE
2026-08-28 19:32:32 +01:00
parent bcdd3805d5
commit d303eda038
19 changed files with 705 additions and 697 deletions
+4 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
let { variant = 'primary', size = 'md', href, onclick, children, ...rest }: { let { variant = 'primary', size = 'md', href, onclick, children, ...rest }: {
variant?: 'primary' | 'secondary' | 'ghost' | 'danger'; variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'success';
size?: 'sm' | 'md' | 'lg'; size?: 'sm' | 'md' | 'lg';
href?: string; href?: string;
onclick?: () => void; onclick?: () => void;
@@ -46,4 +46,7 @@
.btn-danger { background: #dc2626; color: #fff; border-color: #dc2626; } .btn-danger { background: #dc2626; color: #fff; border-color: #dc2626; }
.btn-danger:hover { background: #b91c1c; } .btn-danger:hover { background: #b91c1c; }
.btn-success { background: #059669; color: #fff; border-color: #059669; }
.btn-success:hover { background: #047857; }
</style> </style>
@@ -1,12 +1,18 @@
<script lang="ts"> <script lang="ts">
import { notices, type NoticeType } from '$lib/stores/notices.svelte'; import { notices, type NoticeType } from '$lib/stores/notices.svelte';
import {
checkCircleIcon,
alertTriangleIcon,
xCircleIcon,
infoIcon
} from '$lib/components/icons';
function icon(type: NoticeType) { function icon(type: NoticeType) {
switch (type) { switch (type) {
case 'success': return '✅'; case 'success': return checkCircleIcon;
case 'warning': return '⚠️'; case 'warning': return alertTriangleIcon;
case 'error': return '❌'; case 'error': return xCircleIcon;
default: return 'ℹ️'; default: return infoIcon;
} }
} }
</script> </script>
@@ -16,7 +22,7 @@
{#each notices.list as notice (notice.id)} {#each notices.list as notice (notice.id)}
<div class="notice notice-{notice.type}"> <div class="notice notice-{notice.type}">
<div class="notice-content"> <div class="notice-content">
<div class="notice-icon">{icon(notice.type)}</div> <div class="notice-icon">{@html icon(notice.type)}</div>
<div class="notice-text"> <div class="notice-text">
<h4>{notice.title}</h4> <h4>{notice.title}</h4>
{#if notice.message} {#if notice.message}
@@ -73,6 +79,14 @@
font-size: 1.1rem; font-size: 1.1rem;
flex-shrink: 0; flex-shrink: 0;
} }
.notice-icon :global(svg) {
width: 1.25rem;
height: 1.25rem;
}
.notice-success .notice-icon { color: #059669; }
.notice-warning .notice-icon { color: #d97706; }
.notice-error .notice-icon { color: #dc2626; }
.notice-info .notice-icon { color: #6366f1; }
.notice-text h4 { .notice-text h4 {
margin: 0 0 0.25rem; margin: 0 0 0.25rem;
font-size: 0.9rem; font-size: 0.9rem;
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { checkCircleIcon } from '$lib/components/icons';
interface Tier { interface Tier {
id: 'trial' | 'monthly' | 'yearly'; id: 'trial' | 'monthly' | 'yearly';
@@ -84,7 +85,7 @@
<ul> <ul>
{#each tier.features as f} {#each tier.features as f}
<li><span class="tick">✓</span>{f}</li> <li><span class="tick">{@html checkCircleIcon}</span>{f}</li>
{/each} {/each}
</ul> </ul>
</article> </article>
@@ -202,6 +203,7 @@
gap: 0.45rem; gap: 0.45rem;
} }
.tick { color: #059669; font-weight: 700; flex-shrink: 0; } .tick { color: #059669; font-weight: 700; flex-shrink: 0; }
.tick :global(svg) { width: 1.05em; height: 1.05em; vertical-align: -0.15em; }
.form-error { .form-error {
grid-column: 1 / -1; grid-column: 1 / -1;
+12
View File
@@ -2,6 +2,7 @@ export const dashboardIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fi
export const choresIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>' export const choresIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"/></svg>'
export const rewardsIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/></svg>' export const rewardsIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/></svg>'
export const moneyIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>' export const moneyIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>'
export const coinsIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="6" rx="8" ry="3"/><path d="M4 6v6c0 1.66 3.58 3 8 3s8-1.34 8-3V6"/><path d="M4 12v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6"/></svg>'
export const handCoinsIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 15h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 17"/><path d="m7 21 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"/><path d="M2 16l1 1h2"/><path d="M4 12l1 1h2"/><path d="M6 8l1 1h2"/><path d="M2 20l1.5-.5"/><circle cx="16" cy="8" r="2"/></svg>' export const handCoinsIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 15h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 17"/><path d="m7 21 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"/><path d="M2 16l1 1h2"/><path d="M4 12l1 1h2"/><path d="M6 8l1 1h2"/><path d="M2 20l1.5-.5"/><circle cx="16" cy="8" r="2"/></svg>'
export const gemIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 3h12l4 6-10 13L2 9Z"/><path d="M11 3 8 9l4 13 4-13-3-6"/><path d="M2 9h20"/></svg>' export const gemIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 3h12l4 6-10 13L2 9Z"/><path d="M11 3 8 9l4 13 4-13-3-6"/><path d="M2 9h20"/></svg>'
export const archiveIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></svg>' export const archiveIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/></svg>'
@@ -17,3 +18,14 @@ export const chatIcon = '<svg width="20" height="20" viewBox="0 0 24 24" fill="n
export const sendIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>' export const sendIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>'
export const checkCircleIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>' export const checkCircleIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>'
export const revokeIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>' export const revokeIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>'
export const targetIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg>'
export const calendarIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>'
export const sunIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>'
export const lockIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>'
export const clockIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>'
export const giftIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="8" width="18" height="4"/><path d="M12 8v13M12 8C12 8 9 3 6 5s0 3 3 3M12 8c0 0 3-5 6-3s0 3-3 3"/><path d="M5 12v9h14v-9"/></svg>'
export const chevronLeftIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="m15 6 -6 6 6 6"/></svg>'
export const chevronRightIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="m9 6 6 6 -6 6"/></svg>'
export const alertTriangleIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg>'
export const xCircleIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="m15 9-6 6M9 9l6 6"/></svg>'
export const infoIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 16v-4M12 8h.01"/></svg>'
+1 -9
View File
@@ -10,13 +10,6 @@ export async function myChores(pb: any, famId: string, memberId: string) {
pb.collection('bonus_configs').getFullList({ filter: `famId = '${famId}' && status = 'active'` }) pb.collection('bonus_configs').getFullList({ filter: `famId = '${famId}' && status = 'active'` })
]); ]);
const { payday, paydayTime, tz } = await famMeta(pb, famId); const { payday, paydayTime, tz } = await famMeta(pb, famId);
let settings: any = {};
try {
const s = await pb
.collection('settings')
.getFullList({ filter: `famId = '${famId}'` });
settings = s?.[0] || {};
} catch {}
return { return {
templates, templates,
assigned, assigned,
@@ -25,7 +18,6 @@ export async function myChores(pb: any, famId: string, memberId: string) {
bonusConfigs, bonusConfigs,
payday, payday,
paydayTime, paydayTime,
timezone: tz, timezone: tz
simulateEow: !!settings.simulateEow
}; };
} }
@@ -11,13 +11,6 @@ export async function myChores(pb: any, famId: string, memberId: string) {
pb.collection('bonus_configs').getFullList({ filter: `famId = '${famId}' && status = 'active'` }) pb.collection('bonus_configs').getFullList({ filter: `famId = '${famId}' && status = 'active'` })
]); ]);
const { payday, paydayTime, tz } = await famMeta(pb, famId); const { payday, paydayTime, tz } = await famMeta(pb, famId);
let settings: any = {};
try {
const s = await pb
.collection('settings')
.getFullList({ filter: `famId = '${famId}'` });
settings = s?.[0] || {};
} catch {}
return { return {
templates, templates,
assigned, assigned,
@@ -26,8 +19,7 @@ export async function myChores(pb: any, famId: string, memberId: string) {
bonusConfigs, bonusConfigs,
payday, payday,
paydayTime, paydayTime,
timezone: tz, timezone: tz
simulateEow: !!settings.simulateEow
}; };
} }
-143
View File
@@ -172,149 +172,6 @@ export async function weeklySummary(pb: any, famId: string) {
return { weekStart: ws, daysInWeek, summaries }; return { weekStart: ws, daysInWeek, summaries };
} }
export async function eowPreview(pb: any, famId: string) {
const { payday, tz } = await famMeta(pb, famId);
const ws = weekStart(payday, tz);
const we = periodEnd('weekly', ws);
const [members, assigned, completions, configs, rewards] = await Promise.all([
pb.collection('users').getFullList({ filter: `famId = '${famId}' && role = 'child'` }),
pb.collection('assigned_chores').getFullList({ filter: `famId = '${famId}'` }),
pb.collection('completions').getFullList({ filter: `famId = '${famId}' && date >= '${ws}'` }),
pb
.collection('bonus_configs')
.getFullList({ filter: `famId = '${famId}' && status = 'active'` })
.catch(() => []),
pb.collection('rewards').getFullList({ filter: `famId = '${famId}'` }).catch(() => [])
]);
let rewardPointsList: any[] = [];
let rewardCashList: any[] = [];
try {
[rewardPointsList, rewardCashList] = await Promise.all([
pb.collection('rewards').getFullList({
filter: `famId = '${famId}' && rewardType = 'points' && status = 'claimed' && date >= '${ws}'`
}),
pb.collection('rewards').getFullList({
filter: `famId = '${famId}' && rewardType = 'cash' && status = 'claimed' && date >= '${ws}'`
})
]);
} catch {}
const assignedList = assigned;
const completionsList = completions;
const summaries = members.map((m: any) => {
const mc = completionsList.filter((c: any) => c.memberId === m.id);
const weekPoints = mc.reduce((sum: number, c: any) => {
const ch = assignedList.find((a: any) => a.id === c.assignedChoreId);
return sum + (ch?.type === 'points' ? Number(ch.value) : 0);
}, 0);
const weekMoney = mc.reduce((sum: number, c: any) => {
const ch = assignedList.find((a: any) => a.id === c.assignedChoreId);
return sum + (ch?.type === 'money' ? Number(ch.value) : 0);
}, 0);
const bonusPoints = rewardPointsList
.filter((r: any) => r.memberId === m.id)
.reduce((sum: number, r: any) => sum + Number(r.value), 0);
const bonusMoney = rewardCashList
.filter((r: any) => r.memberId === m.id)
.reduce((sum: number, r: any) => sum + Number(r.value), 0);
return {
memberId: m.id,
memberName: m.name || m.username || m.id.slice(0, 6),
memberColor: m.color,
pointsEarned: weekPoints + bonusPoints,
moneyEarned: weekMoney + bonusMoney,
choresCompleted: mc.length,
bonusEarned: bonusPoints
};
});
const predictedRewards: any[] = [];
const existingRewards = rewards;
for (const cfg of configs) {
if (cfg.type === 'manual') continue;
const pStart = cfg.period ? periodStart(cfg.period, payday, tz) : '';
const pEnd = cfg.period ? periodEnd(cfg.period, pStart) : '';
const periodCompletions = cfg.period
? completionsList.filter(
(c: any) => (c.date || '').slice(0, 10) >= pStart && (c.date || '').slice(0, 10) <= pEnd
)
: completionsList;
const cfgRewards = existingRewards.filter((r: any) => r.bonusConfigId === cfg.id);
const tryEval = (sourceComps: any[]) => {
if (cfg.type === 'threshold')
return sourceComps.reduce((sum: number, c: any) => {
const ch = assignedList.find((a: any) => a.id === c.assignedChoreId);
return sum + (ch?.type === 'points' ? Number(ch.value) : 0);
}, 0);
if (cfg.type === 'count') return sourceComps.length;
return 0;
};
if (cfg.target === 'individual') {
const targets = cfg.memberId ? members.filter((m: any) => m.id === cfg.memberId) : members;
for (const m of targets) {
if (cfgRewards.some((r: any) => r.memberId === m.id)) continue;
const current = tryEval(periodCompletions.filter((c: any) => c.memberId === m.id));
if (cfg.criteriaValue > 0 && current >= Number(cfg.criteriaValue))
predictedRewards.push({
config: cfg.name,
memberName: m.name || m.id.slice(0, 6),
type: cfg.rewardType,
value: Number(cfg.rewardValue) || 0,
detail: `${cfg.type} ${current}/${cfg.criteriaValue}`
});
}
} else if (cfg.target === 'collaborative') {
if (cfgRewards.length) continue;
const allIds = members.map((m: any) => m.id);
const teamComps = periodCompletions.filter((c: any) => allIds.includes(c.memberId));
const current = tryEval(teamComps);
if (cfg.criteriaValue > 0 && current >= Number(cfg.criteriaValue))
predictedRewards.push({
config: cfg.name,
memberName: 'Everyone',
type: cfg.rewardType,
value: Number(cfg.rewardValue) || 0,
detail: `${cfg.type} ${current}/${cfg.criteriaValue}`
});
} else if (cfg.target === 'competitive') {
if (cfgRewards.length) continue;
const scored = members.map((m: any) => ({
memberId: m.id,
name: m.name,
current: tryEval(periodCompletions.filter((c: any) => c.memberId === m.id))
}));
const qualified = scored.filter((st: any) => st.current >= Number(cfg.criteriaValue));
const eligible = qualified.length ? qualified : scored.filter((st: any) => st.current > 0);
const winner = eligible.sort((aa: any, bb: any) => bb.current - aa.current)[0];
if (winner)
predictedRewards.push({
config: cfg.name,
memberName: winner.name,
type: cfg.rewardType,
value: Number(cfg.rewardValue) || 0,
detail: `winner ${winner.current} pts`
});
}
}
const nextWeekStart = addDaysStr(ws, 7);
return {
simulateEow: true,
weekStart: ws,
weekEnd: we,
nextWeekStart,
summaries,
predictedRewards,
completionsThisWeek: completionsList.length
};
}
export async function releaseWeek(pb: any, famId: string) { export async function releaseWeek(pb: any, famId: string) {
const { payday, paydayTime, tz } = await famMeta(pb, famId); const { payday, paydayTime, tz } = await famMeta(pb, famId);
const fams = await pb.collection('fams').getFullList({ filter: `id = '${famId}'` }); const fams = await pb.collection('fams').getFullList({ filter: `id = '${famId}'` });
@@ -35,7 +35,6 @@ export function createServices(
...(timezone !== undefined ? { timezone } : {}) ...(timezone !== undefined ? { timezone } : {})
}), }),
weeklySummary: (famId: string) => famSvc.weeklySummary(pb, famId), weeklySummary: (famId: string) => famSvc.weeklySummary(pb, famId),
eowPreview: (famId: string) => famSvc.eowPreview(pb, famId),
payday: (famId: string) => famSvc.releaseWeek(pb, famId), payday: (famId: string) => famSvc.releaseWeek(pb, famId),
completeWeek: (famId: string) => famSvc.completeWeek(pb, famId), completeWeek: (famId: string) => famSvc.completeWeek(pb, famId),
getProfile: (famId: string) => famSvc.getProfile(pb, famId, uid), getProfile: (famId: string) => famSvc.getProfile(pb, famId, uid),
+2 -3
View File
@@ -8,14 +8,13 @@ function resolveServerTz(tz?: string): string {
export async function getSettings(pb: any, famId: string) { export async function getSettings(pb: any, famId: string) {
const list = await pb.collection('settings').getFullList({ filter: `famId = '${famId}'` }); const list = await pb.collection('settings').getFullList({ filter: `famId = '${famId}'` });
const s = list?.[0] || {}; const s = list?.[0] || {};
return { simulateEow: !!s.simulateEow, webhookUrl: s.webhookUrl || '' }; return { webhookUrl: s.webhookUrl || '' };
} }
export async function updateSettings(pb: any, famId: string, data: Record<string, unknown>) { export async function updateSettings(pb: any, famId: string, data: Record<string, unknown>) {
const list = await pb.collection('settings').getFullList({ filter: `famId = '${famId}'` }); const list = await pb.collection('settings').getFullList({ filter: `famId = '${famId}'` });
const existing = list?.[0]; const existing = list?.[0];
const patch: Record<string, unknown> = {}; const patch: Record<string, unknown> = {};
if (data.simulateEow !== undefined) patch.simulateEow = !!data.simulateEow;
if (data.webhookUrl !== undefined) patch.webhookUrl = String(data.webhookUrl); if (data.webhookUrl !== undefined) patch.webhookUrl = String(data.webhookUrl);
let s; let s;
if (existing) { if (existing) {
@@ -23,5 +22,5 @@ export async function updateSettings(pb: any, famId: string, data: Record<string
} else { } else {
s = await pb.collection('settings').create({ famId, ...patch }); s = await pb.collection('settings').create({ famId, ...patch });
} }
return { simulateEow: !!s.simulateEow, webhookUrl: s.webhookUrl || '' }; return { webhookUrl: s.webhookUrl || '' };
} }
-1
View File
@@ -182,7 +182,6 @@ export interface Settings {
id: string; id: string;
famId: string; famId: string;
webhookUrl?: string; webhookUrl?: string;
simulateEow?: boolean;
} }
export interface ChatMessage { export interface ChatMessage {
+20 -10
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/Footer.svelte';
import { homeIcon } from '$lib/components/icons'; import { homeIcon, checkCircleIcon, chevronRightIcon } from '$lib/components/icons';
import { readShortcut, shortcutTarget, type Shortcut } from '$lib/shortcut'; import { readShortcut, shortcutTarget, type Shortcut } from '$lib/shortcut';
let shortcut = $state<(Shortcut & { target: string }) | null>(null); let shortcut = $state<(Shortcut & { target: string }) | null>(null);
@@ -29,7 +29,7 @@
</span> </span>
<span class="return-text"> <span class="return-text">
<span class="return-fam">{shortcut.famName}</span> <span class="return-fam">{shortcut.famName}</span>
<span class="return-sub">Jump back in →</span> <span class="return-sub">Jump back in {@html chevronRightIcon}</span>
</span> </span>
</a> </a>
{/if} {/if}
@@ -45,9 +45,9 @@
automatically — no spreadsheets, no nagging. automatically — no spreadsheets, no nagging.
</p> </p>
<ul class="hero-ticks"> <ul class="hero-ticks">
<li>Set up in under a minute</li> <li><span class="tick">{@html checkCircleIcon}</span>Set up in under a minute</li>
<li>Kids join with a simple invite code</li> <li><span class="tick">{@html checkCircleIcon}</span>Kids join with a simple invite code</li>
<li>Allowance lands on payday, automatically</li> <li><span class="tick">{@html checkCircleIcon}</span>Allowance lands on payday, automatically</li>
</ul> </ul>
</div> </div>
@@ -56,7 +56,7 @@
<p class="card-sub">Free to get going. Takes about a minute.</p> <p class="card-sub">Free to get going. Takes about a minute.</p>
<a class="submit" href="/signup">Create my family</a> <a class="submit" href="/signup">Create my family</a>
<p class="card-alt"> <p class="card-alt">
<a href="/pricing">See pricing →</a> <a href="/pricing">See pricing {@html chevronRightIcon}</a>
</p> </p>
<p class="card-alt"> <p class="card-alt">
Already have a family? <a href="/login">Log in</a> Already have a family? <a href="/login">Log in</a>
@@ -119,7 +119,7 @@
<div class="cta"> <div class="cta">
<p>Ready to make chores painless?</p> <p>Ready to make chores painless?</p>
<a class="cta-btn" href="/signup">Create your family</a> <a class="cta-btn" href="/signup">Create your family</a>
<p class="card-alt"><a href="/pricing">or see pricing →</a></p> <p class="card-alt"><a href="/pricing">or see pricing {@html chevronRightIcon}</a></p>
</div> </div>
</section> </section>
</main> </main>
@@ -230,10 +230,20 @@
gap: 0.6rem; gap: 0.6rem;
} }
.hero-ticks li::before { .hero-ticks li::before {
content: "✓"; content: "";
color: #4338ca; }
font-weight: 700; .hero-ticks .tick {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.15rem;
height: 1.15rem;
margin-right: 0.5rem; margin-right: 0.5rem;
color: #4338ca;
}
.hero-ticks .tick :global(svg) {
width: 1.1em;
height: 1.1em;
} }
/* SIGNUP CARD */ /* SIGNUP CARD */
@@ -35,7 +35,7 @@ export async function load(event) {
s.crud.list('rewards', famId), s.crud.list('rewards', famId),
s.crud.list('bonus-configs', famId), s.crud.list('bonus-configs', famId),
s.crud.list('completions', famId), s.crud.list('completions', famId),
s.settings.get(famId).catch(() => ({ simulateEow: false, webhookUrl: '' })) s.settings.get(famId)
]); ]);
return { return {
role: 'parent', role: 'parent',
@@ -105,8 +105,7 @@ export async function load(event) {
tallies: {} as Record<string, unknown>, tallies: {} as Record<string, unknown>,
payday: chores.payday, payday: chores.payday,
paydayTime: chores.paydayTime || '18:00', paydayTime: chores.paydayTime || '18:00',
timezone: chores.timezone || 'auto', timezone: chores.timezone || 'auto'
simulateEow: !!chores.simulateEow
}; };
} catch { } catch {
return empty; return empty;
@@ -114,33 +113,6 @@ export async function load(event) {
} }
export const actions = { export const actions = {
setEow: async (event) => {
if (!event.locals.user) throw redirect(303, '/login');
const famId = event.locals.user.famId;
const fd = await event.request.formData();
const on = fd.get('on') === 'true';
try {
const s = createServices(pbUser(event), event.locals.user);
const result = await s.settings.update(famId, { simulateEow: on });
return { simulateEow: result.simulateEow };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to update settings' };
}
},
previewEow: async (event) => {
if (!event.locals.user) throw redirect(303, '/login');
const famId = event.locals.user.famId;
try {
const s = createServices(pbUser(event), event.locals.user);
const preview = await s.fam.eowPreview(famId);
await s.settings.update(famId, { simulateEow: true });
return { preview, simulateEow: true };
} catch (e) {
return { error: e instanceof Error ? e.message : 'Failed to preview payday' };
}
},
claim: async (event) => { claim: async (event) => {
if (!event.locals.user) throw redirect(303, '/login'); if (!event.locals.user) throw redirect(303, '/login');
const famId = event.locals.user.famId; const famId = event.locals.user.famId;
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,7 @@
import { famStore } from '$lib/stores/fam.svelte'; import { famStore } from '$lib/stores/fam.svelte';
import { ViewHeader, CardGrid, Card } from '$lib/components'; import { ViewHeader, CardGrid, Card } from '$lib/components';
import TemplateIcon from '$lib/components/TemplateIcon.svelte'; import TemplateIcon from '$lib/components/TemplateIcon.svelte';
import { choresIcon, todoIcon } from '$lib/components/icons'; import { choresIcon, todoIcon, checkCircleIcon } from '$lib/components/icons';
import { formatHumanDate } from '$lib/format'; import { formatHumanDate } from '$lib/format';
import { TEMPLATE_COLORS, ICON_NAMES, accentBg, outlineColor } from '$lib/templateIcons'; import { TEMPLATE_COLORS, ICON_NAMES, accentBg, outlineColor } from '$lib/templateIcons';
import type { ChoreTemplate, AssignedChore, Member, Season, Completion } from '$lib/types'; import type { ChoreTemplate, AssignedChore, Member, Season, Completion } from '$lib/types';
@@ -48,8 +48,26 @@
// Curated celebration emojis for todos // Curated celebration emojis for todos
const EMOJI_CHOICES = [ const EMOJI_CHOICES = [
'🎉', '⭐', '🏆', '💪', '🧹', '📚', '🦸', '✨', '🌟', '🥇', '🎉',
'🎯', '💚', '🚀', '🦄', '🌈', '🍀', '🎨', '🎸', '⚽', '🍰' '⭐',
'🏆',
'💪',
'🧹',
'📚',
'🦸',
'✨',
'🌟',
'🥇',
'🎯',
'💚',
'🚀',
'🦄',
'🌈',
'🍀',
'🎨',
'🎸',
'⚽',
'🍰'
]; ];
let editFreq = $state('daily'); let editFreq = $state('daily');
@@ -68,9 +86,11 @@
// Platform-owned templates only — families assign these, they don't create. // Platform-owned templates only — families assign these, they don't create.
let templates = $derived( let templates = $derived(
((famStore.initialized ? (famStore.templates as ChoreTemplate[]) : (data.templates as ChoreTemplate[])) || []).filter( (
(t: ChoreTemplate) => t.global (famStore.initialized
) ? (famStore.templates as ChoreTemplate[])
: (data.templates as ChoreTemplate[])) || []
).filter((t: ChoreTemplate) => t.global)
); );
let members = $state( let members = $state(
famStore.initialized ? (famStore.members as Member[]) : (data.members as Member[]) || [] famStore.initialized ? (famStore.members as Member[]) : (data.members as Member[]) || []
@@ -85,7 +105,7 @@
// Only active seasons are selectable for assignment / filtering. // Only active seasons are selectable for assignment / filtering.
let activeSeasons = $derived((seasons || []).filter((s) => s.active !== false)); let activeSeasons = $derived((seasons || []).filter((s) => s.active !== false));
let completions = $state((data.completions as Completion[]) || []); let completions = $state((data.completions as Completion[]) || []);
// $effect(() => console.log(seasons, activeSeasons));
let filteredAssigned = $derived( let filteredAssigned = $derived(
seasonFilter === 'all' ? assigned : assigned.filter((a) => a.seasonIds?.includes(seasonFilter)) seasonFilter === 'all' ? assigned : assigned.filter((a) => a.seasonIds?.includes(seasonFilter))
); );
@@ -366,7 +386,10 @@
</h3> </h3>
<!-- TODOS accordion wrapper --> <!-- TODOS accordion wrapper -->
<div class="accordion-wrapper accordion-todos" class:open={accordionState[m.id]?.todos ?? true}> <div
class="accordion-wrapper accordion-todos"
class:open={accordionState[m.id]?.todos ?? true}
>
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'todos')}> <button class="accordion-head" onclick={() => toggleAccordion(m.id, 'todos')}>
<span class="accordion-title"> <span class="accordion-title">
<span class="accordion-icon">{@html todoIcon}</span> <span class="accordion-icon">{@html todoIcon}</span>
@@ -376,7 +399,9 @@
>{@html (accordionState[m.id]?.todos ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span >{@html (accordionState[m.id]?.todos ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
> >
</button> </button>
<span class="accordion-count" style="background:{m.color}">{todosForMember(m.id).length}</span> <span class="accordion-count" style="background:{m.color}"
>{todosForMember(m.id).length}</span
>
{#if accordionState[m.id]?.todos ?? true} {#if accordionState[m.id]?.todos ?? true}
<div class="accordion-body"> <div class="accordion-body">
{#each sortedTodosForMember(m.id) as a} {#each sortedTodosForMember(m.id) as a}
@@ -414,7 +439,8 @@
</div> </div>
</div> </div>
{#if completed} {#if completed}
<span class="todo-completed-badge">✅ TBC completed</span> <span class="todo-completed-badge">{@html checkCircleIcon} TBC completed</span
>
{:else} {:else}
<button <button
class="del-btn" class="del-btn"
@@ -440,7 +466,10 @@
</div> </div>
<!-- CHORES accordion wrapper --> <!-- CHORES accordion wrapper -->
<div class="accordion-wrapper accordion-chores" class:open={accordionState[m.id]?.chores ?? true}> <div
class="accordion-wrapper accordion-chores"
class:open={accordionState[m.id]?.chores ?? true}
>
<button class="accordion-head" onclick={() => toggleAccordion(m.id, 'chores')}> <button class="accordion-head" onclick={() => toggleAccordion(m.id, 'chores')}>
<span class="accordion-title"> <span class="accordion-title">
<span class="accordion-icon">{@html choresIcon}</span> <span class="accordion-icon">{@html choresIcon}</span>
@@ -450,7 +479,9 @@
>{@html (accordionState[m.id]?.chores ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span >{@html (accordionState[m.id]?.chores ?? true) ? CHEVRON_UP : CHEVRON_DOWN}</span
> >
</button> </button>
<span class="accordion-count" style="background:{m.color}">{assignedForMember(m.id).length}</span> <span class="accordion-count" style="background:{m.color}"
>{assignedForMember(m.id).length}</span
>
{#if accordionState[m.id]?.chores ?? true} {#if accordionState[m.id]?.chores ?? true}
<div class="accordion-body"> <div class="accordion-body">
{#each assignedForMember(m.id) as a} {#each assignedForMember(m.id) as a}
@@ -460,14 +491,20 @@
onclick={() => openEdit(a)} onclick={() => openEdit(a)}
role="button" role="button"
tabindex="0" tabindex="0"
style="border-left-color:{outlineColor(a.color)}; background:{accentBg(a.color)}" style="border-left-color:{outlineColor(a.color)}; background:{accentBg(
a.color
)}"
> >
<div class="card-body"> <div class="card-body">
<TemplateIcon name={a.icon} size={16} color={outlineColor(a.color)} /> <TemplateIcon name={a.icon} size={16} color={outlineColor(a.color)} />
<strong>{a.customName || tName}</strong> <strong>{a.customName || tName}</strong>
<span class="badge">{a.frequency}</span> <span class="badge">{a.frequency}</span>
{#each a.seasonIds || [] as sid} {#each a.seasonIds || [] as sid}
<span class="season-dot" title={seasonName(sid)} style="background:{seasonColor(sid)}"></span> <span
class="season-dot"
title={seasonName(sid)}
style="background:{seasonColor(sid)}"
></span>
{/each} {/each}
{#if (a.seasonIds || []).length === 0} {#if (a.seasonIds || []).length === 0}
<span class="badge season muted">All year</span> <span class="badge season muted">All year</span>
@@ -509,14 +546,16 @@
</CardGrid> </CardGrid>
<!-- Season pill selector (reused by assign + edit modals) --> <!-- Season pill selector (reused by assign + edit modals) -->
{#snippet seasonPills(list: Season[], selected: string[], set: (v: string[]) => void)} {#snippet seasonPills(list: Season[] = [], selected: string[] = [], set: (v: string[]) => void)}
<div class="season-pills"> <div class="season-pills" role="radiogroup" aria-label="Seasons">
<button <button
type="button" type="button"
class="season-pill" class="season-pill"
class:selected={selected.length === 0} class:selected={selected.length === 0}
style="--pill:#9ca3af" style="--pill:#9ca3af"
onclick={() => set([])}>All year</button role="radio"
aria-checked={selected.length === 0}
onclick={() => set([])}>Any</button
> >
{#each list as s} {#each list as s}
<button <button
@@ -524,6 +563,8 @@
class="season-pill" class="season-pill"
class:selected={selected[0] === s.id} class:selected={selected[0] === s.id}
style="--pill:{s.color}" style="--pill:{s.color}"
role="radio"
aria-checked={selected[0] === s.id}
onclick={() => set([s.id])}>{s.name}</button onclick={() => set([s.id])}>{s.name}</button
> >
{/each} {/each}
@@ -532,16 +573,25 @@
<!-- Assign from Template Modal --> <!-- Assign from Template Modal -->
{#if assignModal} {#if assignModal}
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) assignModal = null; }} role="presentation"> <div
class="overlay"
onclick={(e) => {
if (e.target === e.currentTarget) assignModal = null;
}}
role="presentation"
>
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog"> <div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
<h3>Assign: {assignModal.t.name}</h3> <h3>Assign: {assignModal.t.name}</h3>
<p class="hint"> <p class="hint">
Copies this platform template into {members.find((m) => m.id === assignModal?.memberId)?.name}'s Copies this platform template into {members.find((m) => m.id === assignModal?.memberId)
chores. Amend, then Assign. ?.name}'s chores. Amend, then Assign.
</p> </p>
<label>Name <input bind:value={assignName} placeholder="Task name" /></label> <label>Name <input bind:value={assignName} placeholder="Task name" /></label>
<label <label
>Description <input bind:value={assignDescription} placeholder="Optional description" /></label >Description <input
bind:value={assignDescription}
placeholder="Optional description"
/></label
> >
<div class="color-field"> <div class="color-field">
<span class="field-label">Colour</span> <span class="field-label">Colour</span>
@@ -556,7 +606,8 @@
? '#e5e7eb' ? '#e5e7eb'
: c.value}" : c.value}"
title={c.label} title={c.label}
onclick={() => (assignColor = c.value)}></button> onclick={() => (assignColor = c.value)}
></button>
{/each} {/each}
</div> </div>
</div> </div>
@@ -589,7 +640,7 @@
</label> </label>
<div class="season-field"> <div class="season-field">
<span class="field-label">Seasons</span> <span class="field-label">Seasons</span>
<seasonPills activeSeasons selected={assignSeasonIds} set={(v: string[]) => (assignSeasonIds = v)} /> {@render seasonPills(seasons, assignSeasonIds, (v: string[]) => (assignSeasonIds = v))}
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button type="button" onclick={() => (assignModal = null)}>Cancel</button> <button type="button" onclick={() => (assignModal = null)}>Cancel</button>
@@ -601,7 +652,13 @@
<!-- Edit Assignment Modal --> <!-- Edit Assignment Modal -->
{#if showEditModal && editingAssignment} {#if showEditModal && editingAssignment}
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) closeEdit(); }} role="presentation"> <div
class="overlay"
onclick={(e) => {
if (e.target === e.currentTarget) closeEdit();
}}
role="presentation"
>
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog"> <div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
<h3> <h3>
{#if editingAssignment.isTodo} {#if editingAssignment.isTodo}
@@ -619,7 +676,11 @@
</label> </label>
<label <label
>Description >Description
<input name="description" bind:value={editDescription} placeholder="Optional description" /></label <input
name="description"
bind:value={editDescription}
placeholder="Optional description"
/></label
> >
{#if editingAssignment.isTodo} {#if editingAssignment.isTodo}
@@ -639,7 +700,14 @@
{:else if editType === 'money'} {:else if editType === 'money'}
<label> <label>
Cash amount (£) Cash amount (£)
<input name="value" type="number" step="0.01" min="0.01" bind:value={editValue} required /> <input
name="value"
type="number"
step="0.01"
min="0.01"
bind:value={editValue}
required
/>
</label> </label>
{:else} {:else}
<input type="hidden" name="emoji" value={editEmoji} /> <input type="hidden" name="emoji" value={editEmoji} />
@@ -648,7 +716,8 @@
<button <button
type="button" type="button"
class="link-btn" class="link-btn"
onclick={() => (showTodoEmojiPicker = !showTodoEmojiPicker)}> onclick={() => (showTodoEmojiPicker = !showTodoEmojiPicker)}
>
{showTodoEmojiPicker ? 'Hide' : 'Choose'} {showTodoEmojiPicker ? 'Hide' : 'Choose'}
</button> </button>
</div> </div>
@@ -691,20 +760,21 @@
? '#e5e7eb' ? '#e5e7eb'
: c.value}" : c.value}"
title={c.label} title={c.label}
onclick={() => (editColor = c.value)}></button> onclick={() => (editColor = c.value)}
></button>
{/each} {/each}
</div> </div>
<input type="hidden" name="color" value={editColor} /> <input type="hidden" name="color" value={editColor} />
</div> </div>
<label <label class="icon-row"
><span class="icon-preview" ><span class="icon-preview"
><TemplateIcon name={editIcon} size={22} color={outlineColor(editColor)} /></span ><TemplateIcon name={editIcon} size={22} color={outlineColor(editColor)} /></span
> >
<select name="icon" bind:value={editIcon}> <select name="icon" bind:value={editIcon}>
<option value="">None</option> <option value="">None</option>
{#each ICON_NAMES as n}<option value={n}>{n}</option>{/each} {#each ICON_NAMES as n}<option value={n}>{n}</option>{/each}
</select> </select>
</label> </label>
<label> <label>
Frequency Frequency
<select name="frequency" bind:value={editFreq}> <select name="frequency" bind:value={editFreq}>
@@ -725,7 +795,7 @@
</label> </label>
<div class="season-field"> <div class="season-field">
<span class="field-label">Seasons</span> <span class="field-label">Seasons</span>
<seasonPills activeSeasons selected={editSeasonIds} set={(v: string[]) => (editSeasonIds = v)} /> {@render seasonPills(seasons, editSeasonIds, (v: string[]) => (editSeasonIds = v))}
</div> </div>
{/if} {/if}
<div class="modal-actions"> <div class="modal-actions">
@@ -739,7 +809,13 @@
<!-- Add Todo Modal --> <!-- Add Todo Modal -->
{#if showTodoModal && todoMemberId} {#if showTodoModal && todoMemberId}
<div class="overlay" onclick={(e) => { if (e.target === e.currentTarget) showTodoModal = false; }} role="presentation"> <div
class="overlay"
onclick={(e) => {
if (e.target === e.currentTarget) showTodoModal = false;
}}
role="presentation"
>
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog"> <div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
<h3>Add Todo for {members.find((m) => m.id === todoMemberId)?.name || 'Member'}</h3> <h3>Add Todo for {members.find((m) => m.id === todoMemberId)?.name || 'Member'}</h3>
<form <form
@@ -785,13 +861,24 @@
{:else if todoType === 'money'} {:else if todoType === 'money'}
<label> <label>
Cash amount (£) Cash amount (£)
<input name="value" type="number" step="0.01" min="0.01" bind:value={todoValue} required /> <input
name="value"
type="number"
step="0.01"
min="0.01"
bind:value={todoValue}
required
/>
</label> </label>
{:else} {:else}
<input type="hidden" name="emoji" value={todoEmoji} /> <input type="hidden" name="emoji" value={todoEmoji} />
<div class="emoji-field"> <div class="emoji-field">
<span class="emoji-preview">{todoEmoji}</span> <span class="emoji-preview">{todoEmoji}</span>
<button type="button" class="link-btn" onclick={() => (showTodoEmojiPicker = !showTodoEmojiPicker)}> <button
type="button"
class="link-btn"
onclick={() => (showTodoEmojiPicker = !showTodoEmojiPicker)}
>
{showTodoEmojiPicker ? 'Hide' : 'Choose'} {showTodoEmojiPicker ? 'Hide' : 'Choose'}
</button> </button>
</div> </div>
@@ -1464,6 +1551,13 @@
border-radius: 4px; border-radius: 4px;
flex-shrink: 0; flex-shrink: 0;
white-space: nowrap; white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 3px;
}
.todo-completed-badge :global(svg) {
width: 0.95em;
height: 0.95em;
} }
/* ── Points badge ── */ /* ── Points badge ── */
+9 -1
View File
@@ -2,6 +2,7 @@
import { page } from '$app/state'; import { page } from '$app/state';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { Button } from '$lib/components'; import { Button } from '$lib/components';
import { homeIcon } from '$lib/components/icons';
const famSlug = page.params.fam; const famSlug = page.params.fam;
let name = $state(''); let name = $state('');
@@ -14,7 +15,7 @@
<main class="mx-auto flex min-h-screen max-w-md flex-col items-center justify-center px-6"> <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"> <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> </div>
<h1 class="text-xl font-bold text-slate-900">Join {famSlug}</h1> <h1 class="text-xl font-bold text-slate-900">Join {famSlug}</h1>
<p class="mt-1 text-sm text-slate-500"> <p class="mt-1 text-sm text-slate-500">
@@ -63,3 +64,10 @@
</p> </p>
</section> </section>
</main> </main>
<style>
.home-badge :global(svg) {
width: 1.75rem;
height: 1.75rem;
}
</style>
@@ -2,6 +2,7 @@
import { page } from '$app/state'; import { page } from '$app/state';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { Button } from '$lib/components'; import { Button } from '$lib/components';
import { homeIcon } from '$lib/components/icons';
const famSlug = page.params.fam; const famSlug = page.params.fam;
const username = page.params.username; const username = page.params.username;
@@ -14,7 +15,7 @@
<main class="mx-auto flex min-h-screen max-w-md flex-col items-center justify-center px-6"> <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"> <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> </div>
<h1 class="text-xl font-bold text-slate-900">Welcome to {famSlug}!</h1> <h1 class="text-xl font-bold text-slate-900">Welcome to {famSlug}!</h1>
<p class="mt-1 text-sm text-slate-500"> <p class="mt-1 text-sm text-slate-500">
@@ -55,3 +56,9 @@
</p> </p>
</section> </section>
</main> </main>
<style>
.home-badge :global(svg) {
width: 1.75rem;
height: 1.75rem;
}
</style>
+3 -2
View File
@@ -6,6 +6,7 @@
import TemplateIcon from '$lib/components/TemplateIcon.svelte'; import TemplateIcon from '$lib/components/TemplateIcon.svelte';
import { ICON_NAMES, TEMPLATE_COLORS } from '$lib/templateIcons'; import { ICON_NAMES, TEMPLATE_COLORS } from '$lib/templateIcons';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/Footer.svelte';
import { homeIcon, chevronRightIcon } from '$lib/components/icons';
let { data, form } = $props(); let { data, form } = $props();
@@ -85,7 +86,7 @@
<div class="shell"> <div class="shell">
<header class="topbar"> <header class="topbar">
<a class="brand" href="/"><span class="brand-icon">🏠</span> <strong>FamChore</strong></a> <a class="brand" href="/"><span class="brand-icon">{@html homeIcon}</span> <strong>FamChore</strong></a>
<nav> <nav>
{#if data.authenticated} {#if data.authenticated}
<form method="POST" action="?/logout" use:enhance> <form method="POST" action="?/logout" use:enhance>
@@ -311,7 +312,7 @@
<span class="badge none">None</span> <span class="badge none">None</span>
{/if} {/if}
</td> </td>
<td class="row-actions" data-label="Actions"><a href="/{fam.slug}" class="link">View →</a></td> <td class="row-actions" data-label="Actions"><a href="/{fam.slug}" class="link">View {@html chevronRightIcon}</a></td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
+4 -3
View File
@@ -6,6 +6,7 @@
import { loadStripe, type StripeEmbeddedCheckout } from '@stripe/stripe-js'; import { loadStripe, type StripeEmbeddedCheckout } from '@stripe/stripe-js';
import { Button, PricingPlans } from '$lib/components'; import { Button, PricingPlans } from '$lib/components';
import Footer from '$lib/components/Footer.svelte'; import Footer from '$lib/components/Footer.svelte';
import { homeIcon, chevronLeftIcon, chevronRightIcon } from '$lib/components/icons';
let { data, form } = $props(); let { data, form } = $props();
@@ -58,7 +59,7 @@
<div class="shell"> <div class="shell">
<header class="topbar"> <header class="topbar">
<a class="brand" href="/"><span class="brand-icon">🏠</span> <strong>FamChore</strong></a> <a class="brand" href="/"><span class="brand-icon">{@html homeIcon}</span> <strong>FamChore</strong></a>
<nav> <nav>
<a href="/login" class="nav-link">Log in</a> <a href="/login" class="nav-link">Log in</a>
<a href="/signup" class="nav-cta">Get started</a> <a href="/signup" class="nav-cta">Get started</a>
@@ -77,13 +78,13 @@
<div class="checkout-card"> <div class="checkout-card">
<div class="checkout-head"> <div class="checkout-head">
<h2>Checkout — {checkoutTitle}</h2> <h2>Checkout — {checkoutTitle}</h2>
<Button variant="ghost" size="sm" onclick={resetCheckout}>← Back to plans</Button> <Button variant="ghost" size="sm" onclick={resetCheckout}>{@html chevronLeftIcon} Back to plans</Button>
</div> </div>
<div bind:this={checkoutEl} class="checkout-host"></div> <div bind:this={checkoutEl} class="checkout-host"></div>
<p class="hint"> <p class="hint">
You can close and go to your dashboard any time — access unlocks once payment completes. You can close and go to your dashboard any time — access unlocks once payment completes.
</p> </p>
<a href={data.famSlug ? `/${data.famSlug}` : '/'} class="dash-link">Go to dashboard →</a> <a href={data.famSlug ? `/${data.famSlug}` : '/'} class="dash-link">Go to dashboard {@html chevronRightIcon}</a>
</div> </div>
</div> </div>
{/if} {/if}
+1 -1
View File
@@ -184,7 +184,7 @@ export const SCHEMA_PLAN: CollectionPlanEntry[] = [
{ {
name: "settings", name: "settings",
build: (ids) => build: (ids) =>
col("settings", [rel("famId", ids.fams, true), text("webhookUrl"), bool("simulateEow")], { col("settings", [rel("famId", ids.fams, true), text("webhookUrl")], {
createRule: RULE_PARENT_WRITE, createRule: RULE_PARENT_WRITE,
updateRule: RULE_PARENT_SCOPED, updateRule: RULE_PARENT_SCOPED,
deleteRule: RULE_PARENT_SCOPED, deleteRule: RULE_PARENT_SCOPED,