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.
197 lines
4.6 KiB
Svelte
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>
|