Files
KamadoPool/ui/src/lib/SharesBar.svelte
T
satoshi 5e7be22e8e Add page transition animations and background wallpapers
Wrap page views in {#key} blocks with a fade-in animation for smooth
transitions. Add Demon Slayer wallpapers as fixed pseudo-element
backgrounds on main (Tanjiro for dashboard, Rengoku for accelerator)
with CSS crossfade. Fix section spacing lost by the page-enter wrapper
div. Minor tooltip text fixes.
2026-05-11 09:11:50 +03:00

197 lines
4.6 KiB
Svelte

<script lang="ts">
import { untrack } from "svelte";
import { snap } from "../stores/snapshot.svelte";
const data = $derived(snap.data!);
const sessionAcc = $derived(data.session_accepted ?? 0);
const sessionRej = $derived(data.session_rejected ?? 0);
const allAcc = $derived(data.alltime_accepted ?? 0);
const allRej = $derived(data.alltime_rejected ?? 0);
const sessionTotal = $derived(sessionAcc + sessionRej);
const sessionRejectPct = $derived(
sessionTotal > 0 ? (sessionRej / sessionTotal) * 100 : 0,
);
const allTotal = $derived(allAcc + allRej);
const allRejectPct = $derived(
allTotal > 0 ? (allRej / allTotal) * 100 : 0,
);
// Detect new accepted shares to trigger animation
let prevAccepted = $state(0);
let pulse = $state(false);
$effect(() => {
const cur = sessionAcc;
const prev = untrack(() => prevAccepted);
if (cur > 0 && prev > 0 && cur > prev) {
pulse = true;
setTimeout(() => { pulse = false; }, 700);
}
prevAccepted = cur;
});
function fmtCount(n: number): string {
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
return n.toLocaleString();
}
</script>
<section class="shares-bar" class:pulse>
<div class="slash-effect" aria-hidden="true"></div>
<div class="content">
<h3 class="title">Shares</h3>
<div class="stats">
<div class="group">
<span class="group-label">Session</span>
<span class="accepted">{fmtCount(sessionAcc)}</span>
<span class="sep">/</span>
<span class="rejected">{fmtCount(sessionRej)}</span>
{#if sessionTotal > 0}
<span class="pct" class:warn={sessionRejectPct > 1}>
{sessionRejectPct.toFixed(2)}% rejected
</span>
{/if}
</div>
<div class="divider"></div>
<div class="group">
<span class="group-label">All-time</span>
<span class="accepted">{fmtCount(allAcc)}</span>
<span class="sep">/</span>
<span class="rejected">{fmtCount(allRej)}</span>
{#if allTotal > 0}
<span class="pct" class:warn={allRejectPct > 1}>
{allRejectPct.toFixed(2)}% rejected
</span>
{/if}
</div>
</div>
</div>
</section>
<style>
.shares-bar {
position: relative;
overflow: hidden;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg-card);
padding: 1.1rem 1.5rem;
}
.content {
position: relative;
z-index: 1;
display: flex;
align-items: baseline;
justify-content: center;
gap: 2rem;
}
.title {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--fg);
}
.stats {
display: flex;
align-items: center;
gap: 2rem;
flex-wrap: wrap;
}
.group {
display: flex;
align-items: baseline;
gap: 0.5em;
}
.group-label {
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--fg-dim);
margin-right: 0.3em;
}
.accepted {
color: var(--good);
font-weight: 700;
font-size: 1.4rem;
font-variant-numeric: tabular-nums;
}
.rejected {
color: var(--bad);
font-weight: 700;
font-size: 1.4rem;
font-variant-numeric: tabular-nums;
}
.sep {
color: var(--fg-dim);
font-size: 1.1rem;
}
.pct {
font-size: 1.05rem;
color: var(--fg-dim);
margin-left: 0.4em;
}
.pct.warn {
color: var(--bad);
}
.divider {
width: 1px;
height: 2rem;
background: var(--border);
}
/* Demon Slayer flame-slash animation on new accepted share */
.slash-effect {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 122, 58, 0.0) 30%,
rgba(255, 122, 58, 0.15) 48%,
rgba(255, 200, 50, 0.25) 50%,
rgba(255, 122, 58, 0.15) 52%,
rgba(255, 122, 58, 0.0) 70%,
transparent 100%
);
}
.shares-bar.pulse .slash-effect {
animation: flame-slash 0.7s ease-out;
}
@keyframes flame-slash {
0% {
opacity: 1;
transform: translateX(-100%) scaleY(1);
}
40% {
opacity: 1;
transform: translateX(0%) scaleY(1.2);
}
100% {
opacity: 0;
transform: translateX(100%) scaleY(1);
}
}
.shares-bar.pulse {
border-color: rgba(255, 122, 58, 0.5);
box-shadow: 0 0 12px rgba(255, 122, 58, 0.2);
}
@media (max-width: 600px) {
.content {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.divider {
width: 100%;
height: 1px;
}
}
</style>