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
+332
View File
@@ -3,6 +3,8 @@
import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/state';
import { ViewHeader, CardGrid, Card, Button } from '$lib/components';
import TemplateIcon from '$lib/components/TemplateIcon.svelte';
import { ICON_NAMES } from '$lib/templateIcons';
import Footer from '$lib/components/Footer.svelte';
let { data, form } = $props();
@@ -20,6 +22,65 @@
canceled: 'Canceled',
none: 'None'
};
// ── Platform template management ──
type TplKind = 'chore' | 'reward';
let showTplModal = $state(false);
let editingTpl = $state<{ kind: TplKind; id: string } | null>(null);
let tplType = $state<TplKind>('chore');
const blankTpl = {
name: '',
description: '',
icon: 'Star',
color: '#6366f1',
defaultFrequency: 'daily',
defaultType: 'points',
defaultValue: 0,
target: 'individual',
bonusType: 'threshold',
thresholdType: 'points',
occurrence: 'recurring',
rewardType: 'points',
rewardValue: '',
criteriaValue: 0,
period: 'weekly',
isPocketMoney: false
};
let tplVals = $state({ ...blankTpl });
function openNewTpl(kind: TplKind) {
editingTpl = null;
tplType = kind;
tplVals = { ...blankTpl };
showTplModal = true;
}
function openEditTpl(kind: TplKind, rec: any) {
editingTpl = { kind, id: rec.id };
tplType = kind;
tplVals = {
name: rec.name || '',
description: rec.description || '',
icon: rec.icon || 'Star',
color: rec.color || '#6366f1',
defaultFrequency: rec.defaultFrequency || 'daily',
defaultType: rec.defaultType || 'points',
defaultValue: rec.defaultValue ?? 0,
target: rec.target || 'individual',
bonusType: rec.type || 'threshold',
thresholdType: rec.thresholdType || 'points',
occurrence: rec.occurrence || 'recurring',
rewardType: rec.rewardType || 'points',
rewardValue: rec.rewardValue ?? '',
criteriaValue: rec.criteriaValue ?? 0,
period: rec.period || 'weekly',
isPocketMoney: !!rec.isPocketMoney
};
showTplModal = true;
}
const allTemplates = $derived([
...(data.choreTemplates || []).map((t: any) => ({ ...t, kind: 'chore' as TplKind })),
...(data.bonusTemplates || []).map((t: any) => ({ ...t, kind: 'reward' as TplKind }))
]);
</script>
<div class="shell">
@@ -258,6 +319,184 @@
</Card>
</CardGrid>
<CardGrid>
<Card cols={3} title="Templates" accent="#8b5cf6">
<p class="muted">
Platform-owned chore &amp; reward droplets. Families assign these as read-only sources — they
can't create their own.
</p>
<div class="tpl-grid">
{#each allTemplates as t}
<div class="tpl-card" style="border-left-color:{t.color}">
<div class="tpl-head">
<TemplateIcon name={t.icon} size={20} color={t.color} />
<strong>{t.name}</strong>
</div>
<div class="tpl-meta">
<span class="badge">{t.kind}</span>
{#if t.isPocketMoney}<span class="badge pocket">Pocket Money</span>{/if}
</div>
<div class="tpl-actions">
<button class="link-btn" onclick={() => openEditTpl(t.kind, t)}>Edit</button>
<form
method="POST"
action="?/deleteTemplate"
use:enhance={() => {
return async ({ result, update }) => {
update();
};
}}
>
<input type="hidden" name="kind" value={t.kind} />
<input type="hidden" name="id" value={t.id} />
<button
type="submit"
class="link-btn danger"
onclick={(e) => {
if (!confirm('Delete this template?')) e.preventDefault();
}}>Delete</button
>
</form>
</div>
</div>
{/each}
{#if allTemplates.length === 0}
<p class="muted">No templates yet.</p>
{/if}
</div>
<div class="tpl-new">
<Button onclick={() => openNewTpl('chore')}>+ Chore template</Button>
<Button onclick={() => openNewTpl('reward')}>+ Reward template</Button>
</div>
</Card>
</CardGrid>
{#if showTplModal}
<div class="overlay" onclick={() => (showTplModal = false)} role="presentation">
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
<h3>{editingTpl ? 'Edit' : 'New'} {tplType} template</h3>
<form
method="POST"
action={editingTpl ? '?/updateTemplate' : '?/createTemplate'}
use:enhance={() => {
return async ({ result, update }) => {
if (result.type === 'success') showTplModal = false;
update();
};
}}
>
{#if editingTpl}
<input type="hidden" name="kind" value={editingTpl.kind} />
<input type="hidden" name="id" value={editingTpl.id} />
{/if}
<input type="hidden" name="type" value={tplType} />
<label>Name <input name="name" bind:value={tplVals.name} required /></label>
<label>Description <input name="description" bind:value={tplVals.description} /></label>
<div class="row">
<label>
Icon
<select name="icon" bind:value={tplVals.icon}>
{#each ICON_NAMES as n}<option value={n}>{n}</option>{/each}
</select>
</label>
<span class="icon-preview"><TemplateIcon name={tplVals.icon} size={22} color={tplVals.color} /></span>
<label>
Colour
<input type="color" name="color" bind:value={tplVals.color} />
</label>
</div>
{#if tplType === 'chore'}
<label>
Frequency
<select name="defaultFrequency" bind:value={tplVals.defaultFrequency}>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
</select>
</label>
<label>
Type
<select name="defaultType" bind:value={tplVals.defaultType}>
<option value="points">Points</option>
<option value="money">Money</option>
</select>
</label>
<label>
Default Value
<input type="number" step="any" name="defaultValue" bind:value={tplVals.defaultValue} />
</label>
{:else}
<label>
Target
<select name="target" bind:value={tplVals.target}>
<option value="individual">Individual</option>
<option value="competitive">Competition</option>
<option value="collaborative">Collaborative</option>
</select>
</label>
<label>
Type
<select name="bonusType" bind:value={tplVals.bonusType}>
<option value="threshold">Threshold</option>
<option value="count">Count</option>
<option value="manual">Manual</option>
</select>
</label>
{#if tplVals.bonusType === 'threshold'}
<label>
Threshold by
<select name="thresholdType" bind:value={tplVals.thresholdType}>
<option value="points">Points</option>
<option value="percent">% of chores done</option>
</select>
</label>
{/if}
<label>
Occurrence
<select name="occurrence" bind:value={tplVals.occurrence}>
<option value="recurring">Recurring</option>
<option value="once">Once</option>
</select>
</label>
<label>
Reward Type
<select name="rewardType" bind:value={tplVals.rewardType}>
<option value="points">Points</option>
<option value="cash">Cash</option>
<option value="prize">Prize</option>
</select>
</label>
<label>
Reward Value
<input name="rewardValue" bind:value={tplVals.rewardValue} />
</label>
<label>
Target Value
<input type="number" name="criteriaValue" bind:value={tplVals.criteriaValue} />
</label>
<label>
Period
<select name="period" bind:value={tplVals.period}>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</label>
<label class="check">
<input type="checkbox" name="isPocketMoney" value="true" bind:checked={tplVals.isPocketMoney} />
Pocket Money
</label>
{/if}
<div class="modal-actions">
<button type="button" onclick={() => (showTplModal = false)}>Cancel</button>
<button type="submit">{editingTpl ? 'Save' : 'Create'}</button>
</div>
</form>
</div>
</div>
{/if}
{/if}
</main>
@@ -669,4 +908,97 @@
border-bottom: none !important;
}
}
/* ── Template management ── */
.tpl-grid {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
margin: 0.75rem 0;
}
.tpl-card {
flex: 0 1 auto;
min-width: 180px;
background: #fff;
border: 1px solid #e5e7eb;
border-left: 4px solid #8b5cf6;
border-radius: 10px;
padding: 0.6rem 0.75rem;
}
.tpl-head {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.35rem;
}
.tpl-head strong { font-size: 0.9rem; }
.tpl-meta { display: flex; gap: 0.3rem; flex-wrap: wrap; margin-bottom: 0.4rem; }
.tpl-actions { display: flex; gap: 0.75rem; }
.tpl-new { display: flex; gap: 0.6rem; margin-top: 0.5rem; }
.row { display: flex; align-items: flex-end; gap: 0.75rem; flex-wrap: wrap; }
.icon-preview {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.badge.pocket { background: #dcfce7; color: #166534; }
.check { display: flex; align-items: center; gap: 0.4rem; }
/* ── Modal (shared) ── */
.overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.45);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
z-index: 50;
}
.modal {
background: #fff;
border-radius: 14px;
padding: 1.25rem 1.5rem;
width: min(440px, 100%);
max-height: 88vh;
overflow: auto;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.25);
}
.modal h3 { margin: 0 0 0.75rem; }
.modal label {
display: block;
margin-bottom: 0.6rem;
font-size: 0.85rem;
color: #374151;
}
.modal input,
.modal select {
display: block;
width: 100%;
margin-top: 0.25rem;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.9rem;
box-sizing: border-box;
}
.modal-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
margin-top: 0.5rem;
}
.modal-actions button {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
}
.modal-actions button[type='button'] { background: #e5e7eb; color: #374151; }
.modal-actions button[type='submit'] { background: #6366f1; color: #fff; }
</style>