Compare commits

...

2 Commits

Author SHA1 Message Date
JCEEE 8e4eef06f5 add payday anim 2026-09-20 17:58:38 +01:00
JCEEE 1609e11c48 add new save button 2026-09-20 17:52:53 +01:00
2 changed files with 137 additions and 0 deletions
@@ -0,0 +1,32 @@
<script lang="ts">
import Button from './Button.svelte';
let {
label,
successLabel = 'Saved ✓',
variant = 'primary',
size = 'sm',
...rest
}: {
label: string;
successLabel?: string;
variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'success' | 'purple';
size?: 'sm' | 'md' | 'lg';
[key: string]: unknown;
} = $props();
let saved = $state(false);
let timer: ReturnType<typeof setTimeout> | undefined;
// Parent forms call this (via bind:this) from their use:enhance callback
// on result.type === 'success'. Green for 4s, then back to normal.
export function flash() {
saved = true;
if (timer) clearTimeout(timer);
timer = setTimeout(() => (saved = false), 4000);
}
</script>
<Button type="submit" {size} variant={saved ? 'success' : variant} {...rest}>
{saved ? successLabel : label}
</Button>
@@ -248,6 +248,15 @@
// ─── Payday countdown (child view) ───
let nowMs = $state(Date.now());
let eowFired = $state(false);
// Nyan-style payday party: rainbow banner + confetti when payday unlocks.
let paydayParty = $state(false);
let partyTimer: ReturnType<typeof setTimeout> | undefined;
function startPaydayParty() {
paydayParty = true;
fire();
if (partyTimer) clearTimeout(partyTimer);
partyTimer = setTimeout(() => (paydayParty = false), 15000);
}
$effect(() => {
const id = setInterval(() => (nowMs = Date.now()), 1000);
return () => clearInterval(id);
@@ -269,6 +278,7 @@
if (!pbToken || !famId) return;
eowFired = true;
memberApi.payday(famId).catch(() => {});
startPaydayParty();
});
function paydayWeekStart(): string {
@@ -1273,6 +1283,16 @@
</div>
</div>
{/if}
{#if paydayParty}
<div class="payday-party" role="status" onclick={() => (paydayParty = false)}>
<span class="party-cat">🐖</span>
<div class="party-body">
<p class="party-title">PAYDAY!!</p>
<p class="party-sub">Your cash is ready — go collect it! 🎉</p>
</div>
<span class="party-stars">🌈⭐🌈</span>
</div>
{/if}
<!-- HERO -->
<header class="hero">
<svg
@@ -3107,6 +3127,91 @@
font-size: 0.8rem;
opacity: 0.85;
}
/* ── Payday party: nyan-style colour-cycling celebration ── */
.payday-party {
display: flex;
align-items: center;
gap: 0.8rem;
margin-bottom: 1rem;
padding: 1rem 1.2rem;
border-radius: 14px;
color: #fff;
cursor: pointer;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
background: linear-gradient(
90deg,
#ef4444,
#f97316,
#eab308,
#22c55e,
#3b82f6,
#a855f7,
#ec4899,
#ef4444
);
background-size: 300% 100%;
animation:
nyan-slide 3s linear infinite,
party-pop 0.45s ease-out;
box-shadow: 0 8px 24px rgba(168, 85, 247, 0.45);
}
@keyframes nyan-slide {
to {
background-position: 300% 0;
}
}
@keyframes party-pop {
from {
transform: scale(0.85);
}
to {
transform: scale(1);
}
}
.party-cat {
font-size: 2.6rem;
animation: party-bounce 0.55s ease-in-out infinite alternate;
}
@keyframes party-bounce {
from {
transform: translateY(-4px) rotate(-10deg);
}
to {
transform: translateY(4px) rotate(10deg);
}
}
.party-body {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.party-title {
margin: 0;
font-size: 1.5rem;
font-weight: 900;
letter-spacing: 0.1em;
animation: party-wiggle 1s ease-in-out infinite;
}
@keyframes party-wiggle {
0%,
100% {
transform: rotate(-2deg);
}
50% {
transform: rotate(2deg);
}
}
.party-sub {
margin: 0;
font-size: 0.85rem;
opacity: 0.95;
}
.party-stars {
margin-left: auto;
font-size: 1.4rem;
animation: party-bounce 0.55s ease-in-out infinite alternate-reverse;
}
/* Mobile: declared last so it wins over the base .goal-mini (.kanban)
rules (same specificity, source order decides). */
@media (max-width: 639px) {