Compare commits

..

3 Commits

Author SHA1 Message Date
JCEEE a959f204bd cleanup period confusion 2026-09-04 10:22:51 +01:00
JCEEE 44524de4b9 1.9.2 2026-09-04 09:41:37 +01:00
JCEEE 0767fa4423 add hero component 2026-09-04 09:41:20 +01:00
6 changed files with 246 additions and 22 deletions
@@ -0,0 +1,217 @@
<script lang="ts">
import { onMount } from 'svelte';
const {
cellSize = 32,
gap = 8,
color = 'rgba(200, 210, 255, 0.15)',
tickColor = 'rgba(200, 210, 255, 0.55)',
rotation = -8,
waveSpeed = 1.6
}: {
cellSize?: number;
gap?: number;
color?: string;
tickColor?: string;
rotation?: number;
waveSpeed?: number;
} = $props();
let containerEl: HTMLDivElement;
let columns = $state(0);
let rows = $state(0);
let mounted = $state(false);
let phase = $state(0);
let rafId = 0;
let lastTime = 0;
const seed = Math.random() * 1000;
function seededRandom(i: number, j: number): number {
const x = Math.sin(seed + i * 127.1 + j * 311.7) * 43758.5453;
return x - Math.floor(x);
}
function calcGrid() {
if (!containerEl) return;
const rect = containerEl.getBoundingClientRect();
const w = rect.width;
const h = rect.height;
columns = Math.ceil(w / (cellSize + gap)) + 1;
rows = Math.ceil(h / (cellSize + gap)) + 1;
}
function gridRows(): number {
return rows + 8;
}
function gridStart(): number {
return -4;
}
function getCellRotation(i: number, j: number): number {
const r = seededRandom(i, j);
return (r - 0.5) * 16;
}
function getRowOffset(i: number): number {
return i % 2 === 1 ? (cellSize + gap) / 2 : 0;
}
function getCellOpacity(i: number, j: number, p: number): number {
const centerJ = (j + 0.5) / columns;
const wave = Math.sin((centerJ * columns * 0.3) - i * 0.35 + p);
const threshold = (wave + 1) / 2;
const cellRand = seededRandom(i, j);
return cellRand < threshold ? 1 : 0;
}
function isGreenCell(i: number, j: number): boolean {
return seededRandom(i + 500, j + 500) < 0.15;
}
function getTickColor(i: number, j: number): string {
return isGreenCell(i, j) ? 'rgba(16, 185, 129, 0.6)' : tickColor;
}
function getBoxColor(i: number, j: number): string {
return isGreenCell(i, j) ? 'rgba(16, 185, 129, 0.35)' : color;
}
function getBoxRadius(i: number, j: number): string {
return isGreenCell(i, j) ? '50%' : '4';
}
function animate(time: number) {
if (!lastTime) lastTime = time;
const dt = (time - lastTime) / 1000;
lastTime = time;
phase += dt * waveSpeed;
rafId = requestAnimationFrame(animate);
}
onMount(() => {
mounted = true;
calcGrid();
const ro = new ResizeObserver(() => calcGrid());
ro.observe(containerEl);
rafId = requestAnimationFrame(animate);
return () => {
ro.disconnect();
cancelAnimationFrame(rafId);
};
});
</script>
<div class="checkbox-grid-container" bind:this={containerEl}>
{#if mounted}
<div
class="checkbox-grid"
style="
--grid-cols: {columns};
--cell-size: {cellSize}px;
--cell-gap: {gap}px;
--container-rotation: {rotation}deg;
"
>
{#each Array(gridRows()) as _, i}
{#each Array(columns) as _, j}
{@const di = i + gridStart()}
{@const cellRotation = getCellRotation(di, j)}
{@const rowOffset = getRowOffset(di)}
{@const opacity = getCellOpacity(di, j, phase)}
{@const cellTickColor = getTickColor(di, j)}
{@const cellBoxColor = getBoxColor(di, j)}
{@const boxRadius = getBoxRadius(di, j)}
<div
class="checkbox-cell"
style="
--cell-rotation: {cellRotation}deg;
--row-offset: {rowOffset}px;
opacity: {opacity};
transition: opacity 0.8s ease;
"
>
<svg
viewBox="0 0 24 24"
width={cellSize}
height={cellSize}
class="checkbox-svg"
>
<rect
x="1" y="1"
width="22" height="22"
rx={boxRadius} ry={boxRadius}
fill="none"
stroke={cellBoxColor}
stroke-width="1.5"
/>
{#if opacity > 0.5}
<path
d="M6 12.5 L10.5 17 L18 7.5"
fill="none"
stroke={cellTickColor}
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
class="tick-path"
/>
{/if}
</svg>
</div>
{/each}
{/each}
</div>
{/if}
</div>
<style>
.checkbox-grid-container {
position: fixed;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.checkbox-grid {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(var(--container-rotation));
display: grid;
grid-template-columns: repeat(var(--grid-cols), var(--cell-size));
gap: var(--cell-gap);
}
.checkbox-cell {
width: var(--cell-size);
height: var(--cell-size);
transform: translateX(var(--row-offset, 0px)) rotate(var(--cell-rotation));
display: flex;
align-items: center;
justify-content: center;
}
.checkbox-svg {
display: block;
}
.tick-path {
animation: tick-draw 0.3s ease forwards;
}
@keyframes tick-draw {
from {
stroke-dasharray: 40;
stroke-dashoffset: 40;
}
to {
stroke-dasharray: 40;
stroke-dashoffset: 0;
}
}
</style>
+1
View File
@@ -12,3 +12,4 @@ export { default as Chat } from './Chat.svelte';
export { default as AuthShell } from './AuthShell.svelte'; export { default as AuthShell } from './AuthShell.svelte';
export { default as NoticeDialog } from './NoticeDialog.svelte'; export { default as NoticeDialog } from './NoticeDialog.svelte';
export { default as PricingPlans } from './PricingPlans.svelte'; export { default as PricingPlans } from './PricingPlans.svelte';
export { default as CheckboxGrid } from './CheckboxGrid.svelte';
+6 -5
View File
@@ -4,6 +4,7 @@
import FamDone from '$lib/components/FamDone.svelte'; import FamDone from '$lib/components/FamDone.svelte';
import { homeIcon, checkCircleIcon, chevronRightIcon } 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';
import CheckboxGrid from '$lib/components/CheckboxGrid.svelte';
let shortcut = $state<(Shortcut & { target: string }) | null>(null); let shortcut = $state<(Shortcut & { target: string }) | null>(null);
@@ -30,12 +31,13 @@
</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 {@html chevronRightIcon}</span> <span class="return-sub">Jump back in</span>
</span> </span>
</a> </a>
{/if} {/if}
<!-- HERO --> <!-- HERO -->
<section class="hero"> <section class="hero">
<CheckboxGrid />
<div class="hero-content"> <div class="hero-content">
<span class="hero-kicker">The family chore app</span> <span class="hero-kicker">The family chore app</span>
<h1 class="hero-title"><FamDone /></h1> <h1 class="hero-title"><FamDone /></h1>
@@ -273,6 +275,7 @@
color: #eef2ff; color: #eef2ff;
padding: 6rem 1.5rem; padding: 6rem 1.5rem;
text-align: center; text-align: center;
clip-path: inset(0);
} }
.hero::before, .hero::before,
.hero::after { .hero::after {
@@ -287,7 +290,7 @@
height: 480px; height: 480px;
top: -180px; top: -180px;
left: -120px; left: -120px;
background: rgba(67, 56, 202, 0.55); background: midnightblue;
} }
.hero::after { .hero::after {
width: 420px; width: 420px;
@@ -341,21 +344,19 @@
} }
.hero-cta { .hero-cta {
display: inline-block; display: inline-block;
background: #f59e0b; background: springgreen;
color: #1e1b4b; color: #1e1b4b;
text-decoration: none; text-decoration: none;
font-weight: 700; font-weight: 700;
font-size: 1rem; font-size: 1rem;
padding: 0.8rem 1.75rem; padding: 0.8rem 1.75rem;
border-radius: 999px; border-radius: 999px;
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.35);
transition: transition:
transform 0.15s, transform 0.15s,
box-shadow 0.15s; box-shadow 0.15s;
} }
.hero-cta:hover { .hero-cta:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 10px 28px rgba(245, 158, 11, 0.45);
} }
.hero-cta-ghost { .hero-cta-ghost {
display: inline-block; display: inline-block;
+2 -2
View File
@@ -277,7 +277,7 @@ export const actions: Actions = {
rewardType: (fd.get('rewardType') as string) || 'points', rewardType: (fd.get('rewardType') as string) || 'points',
rewardValue: (fd.get('rewardValue') as string) || '', rewardValue: (fd.get('rewardValue') as string) || '',
criteriaValue: Number(fd.get('criteriaValue') || 0), criteriaValue: Number(fd.get('criteriaValue') || 0),
period: (fd.get('period') as string) || 'weekly' period: (fd.get('period') as string) || ''
}); });
} }
return { success: true }; return { success: true };
@@ -321,7 +321,7 @@ export const actions: Actions = {
rewardType: (fd.get('rewardType') as string) || 'points', rewardType: (fd.get('rewardType') as string) || 'points',
rewardValue: (fd.get('rewardValue') as string) || '', rewardValue: (fd.get('rewardValue') as string) || '',
criteriaValue: Number(fd.get('criteriaValue') || 0), criteriaValue: Number(fd.get('criteriaValue') || 0),
period: (fd.get('period') as string) || 'weekly' period: (fd.get('period') as string) || ''
}); });
} }
return { success: true }; return { success: true };
+19 -14
View File
@@ -45,7 +45,7 @@
rewardType: 'points', rewardType: 'points',
rewardValue: '', rewardValue: '',
criteriaValue: 0, criteriaValue: 0,
period: 'weekly' period: 'daily'
}; };
let tplVals = $state({ ...blankTpl }); let tplVals = $state({ ...blankTpl });
@@ -73,7 +73,7 @@
rewardType: rec.rewardType || 'points', rewardType: rec.rewardType || 'points',
rewardValue: rec.rewardValue ?? '', rewardValue: rec.rewardValue ?? '',
criteriaValue: rec.criteriaValue ?? 0, criteriaValue: rec.criteriaValue ?? 0,
period: rec.period || 'weekly' period: rec.period || ''
}; };
showTplModal = true; showTplModal = true;
} }
@@ -554,18 +554,23 @@
Reward Value Reward Value
<input name="rewardValue" bind:value={tplVals.rewardValue} /> <input name="rewardValue" bind:value={tplVals.rewardValue} />
</label> </label>
<label> {#if tplVals.bonusType !== 'manual'}
Target Value <label>
<input type="number" name="criteriaValue" bind:value={tplVals.criteriaValue} /> Target Value
</label> <input type="number" name="criteriaValue" bind:value={tplVals.criteriaValue} />
<label> </label>
Period {/if}
<select name="period" bind:value={tplVals.period}> {#if tplVals.bonusType === 'manual'}
<option value="daily">Daily</option> <label>
<option value="weekly">Weekly</option> Period
<option value="monthly">Monthly</option> <select name="period" bind:value={tplVals.period}>
</select> <option value="">Unlimited</option>
</label> <option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</label>
{/if}
{/if} {/if}
+1 -1
View File
@@ -13,5 +13,5 @@
"esbuild" "esbuild"
] ]
}, },
"version": "1.9.1" "version": "1.9.2"
} }