Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a959f204bd | |||
| 44524de4b9 | |||
| 0767fa4423 |
@@ -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>
|
||||
@@ -12,3 +12,4 @@ export { default as Chat } from './Chat.svelte';
|
||||
export { default as AuthShell } from './AuthShell.svelte';
|
||||
export { default as NoticeDialog } from './NoticeDialog.svelte';
|
||||
export { default as PricingPlans } from './PricingPlans.svelte';
|
||||
export { default as CheckboxGrid } from './CheckboxGrid.svelte';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import FamDone from '$lib/components/FamDone.svelte';
|
||||
import { homeIcon, checkCircleIcon, chevronRightIcon } from '$lib/components/icons';
|
||||
import { readShortcut, shortcutTarget, type Shortcut } from '$lib/shortcut';
|
||||
import CheckboxGrid from '$lib/components/CheckboxGrid.svelte';
|
||||
|
||||
let shortcut = $state<(Shortcut & { target: string }) | null>(null);
|
||||
|
||||
@@ -30,12 +31,13 @@
|
||||
</span>
|
||||
<span class="return-text">
|
||||
<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>
|
||||
</a>
|
||||
{/if}
|
||||
<!-- HERO -->
|
||||
<section class="hero">
|
||||
<CheckboxGrid />
|
||||
<div class="hero-content">
|
||||
<span class="hero-kicker">The family chore app</span>
|
||||
<h1 class="hero-title"><FamDone /></h1>
|
||||
@@ -273,6 +275,7 @@
|
||||
color: #eef2ff;
|
||||
padding: 6rem 1.5rem;
|
||||
text-align: center;
|
||||
clip-path: inset(0);
|
||||
}
|
||||
.hero::before,
|
||||
.hero::after {
|
||||
@@ -287,7 +290,7 @@
|
||||
height: 480px;
|
||||
top: -180px;
|
||||
left: -120px;
|
||||
background: rgba(67, 56, 202, 0.55);
|
||||
background: midnightblue;
|
||||
}
|
||||
.hero::after {
|
||||
width: 420px;
|
||||
@@ -341,21 +344,19 @@
|
||||
}
|
||||
.hero-cta {
|
||||
display: inline-block;
|
||||
background: #f59e0b;
|
||||
background: springgreen;
|
||||
color: #1e1b4b;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
padding: 0.8rem 1.75rem;
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.35);
|
||||
transition:
|
||||
transform 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
.hero-cta:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 28px rgba(245, 158, 11, 0.45);
|
||||
}
|
||||
.hero-cta-ghost {
|
||||
display: inline-block;
|
||||
|
||||
@@ -277,7 +277,7 @@ export const actions: Actions = {
|
||||
rewardType: (fd.get('rewardType') as string) || 'points',
|
||||
rewardValue: (fd.get('rewardValue') as string) || '',
|
||||
criteriaValue: Number(fd.get('criteriaValue') || 0),
|
||||
period: (fd.get('period') as string) || 'weekly'
|
||||
period: (fd.get('period') as string) || ''
|
||||
});
|
||||
}
|
||||
return { success: true };
|
||||
@@ -321,7 +321,7 @@ export const actions: Actions = {
|
||||
rewardType: (fd.get('rewardType') as string) || 'points',
|
||||
rewardValue: (fd.get('rewardValue') as string) || '',
|
||||
criteriaValue: Number(fd.get('criteriaValue') || 0),
|
||||
period: (fd.get('period') as string) || 'weekly'
|
||||
period: (fd.get('period') as string) || ''
|
||||
});
|
||||
}
|
||||
return { success: true };
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
rewardType: 'points',
|
||||
rewardValue: '',
|
||||
criteriaValue: 0,
|
||||
period: 'weekly'
|
||||
period: 'daily'
|
||||
};
|
||||
let tplVals = $state({ ...blankTpl });
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
rewardType: rec.rewardType || 'points',
|
||||
rewardValue: rec.rewardValue ?? '',
|
||||
criteriaValue: rec.criteriaValue ?? 0,
|
||||
period: rec.period || 'weekly'
|
||||
period: rec.period || ''
|
||||
};
|
||||
showTplModal = true;
|
||||
}
|
||||
@@ -554,18 +554,23 @@
|
||||
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>
|
||||
{#if tplVals.bonusType !== 'manual'}
|
||||
<label>
|
||||
Target Value
|
||||
<input type="number" name="criteriaValue" bind:value={tplVals.criteriaValue} />
|
||||
</label>
|
||||
{/if}
|
||||
{#if tplVals.bonusType === 'manual'}
|
||||
<label>
|
||||
Period
|
||||
<select name="period" bind:value={tplVals.period}>
|
||||
<option value="">Unlimited</option>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
</select>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
{/if}
|
||||
|
||||
|
||||
+1
-1
@@ -13,5 +13,5 @@
|
||||
"esbuild"
|
||||
]
|
||||
},
|
||||
"version": "1.9.1"
|
||||
"version": "1.9.2"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user