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 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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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>
|
||||||
|
{#if tplVals.bonusType !== 'manual'}
|
||||||
<label>
|
<label>
|
||||||
Target Value
|
Target Value
|
||||||
<input type="number" name="criteriaValue" bind:value={tplVals.criteriaValue} />
|
<input type="number" name="criteriaValue" bind:value={tplVals.criteriaValue} />
|
||||||
</label>
|
</label>
|
||||||
|
{/if}
|
||||||
|
{#if tplVals.bonusType === 'manual'}
|
||||||
<label>
|
<label>
|
||||||
Period
|
Period
|
||||||
<select name="period" bind:value={tplVals.period}>
|
<select name="period" bind:value={tplVals.period}>
|
||||||
|
<option value="">Unlimited</option>
|
||||||
<option value="daily">Daily</option>
|
<option value="daily">Daily</option>
|
||||||
<option value="weekly">Weekly</option>
|
<option value="weekly">Weekly</option>
|
||||||
<option value="monthly">Monthly</option>
|
<option value="monthly">Monthly</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -13,5 +13,5 @@
|
|||||||
"esbuild"
|
"esbuild"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"version": "1.9.1"
|
"version": "1.9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user