teasanctuary.ru/src/lib/components/CountdownClock.svelte

99 lines
3.2 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { onMount } from 'svelte';
import { dateDurationLong } from '$lib/util/Dates';
const FINAL_COUNTDOWN_MILLIS = 15 * 60 * 1000;
let { deadline }: { deadline: Date } = $props();
let deadlineMillis = $derived(deadline.valueOf());
let currentTime = $state(new Date().valueOf());
let timeLeft = $derived(Math.max(deadlineMillis - currentTime, 0));
let isFinalCountdown = $state(false);
$effect(() => {
// HACK: rndtrash: если дата обратного отчёта поменялась, то сбрасываем синхронизацию анимации
if (deadline || true) {
isFinalCountdown = false;
}
});
const padNumber = (n: number) => String(n).padStart(2, '0');
let secondsLeft = $derived(padNumber(Math.floor(timeLeft / 1000) % 60));
let minutesLeft = $derived(padNumber(Math.floor(timeLeft / 1000 / 60) % 60));
let hoursLeft = $derived(padNumber(Math.floor(timeLeft / 1000 / 60 / 60) % 24));
let daysLeft = $derived(Math.floor(timeLeft / 1000 / 60 / 60 / 24));
onMount(() => {
// HACK: rndtrash: NodeJS использует другой тип данных для интервалов
let interval: ReturnType<typeof setInterval> | undefined = undefined;
function updateTime() {
currentTime = new Date().valueOf();
isFinalCountdown = timeLeft <= FINAL_COUNTDOWN_MILLIS;
}
// В случае, если часы пререндернулись сервером, обновляем время вручную.
// Только потом используем специальную функцию, в том числе активирующую анимацию.
currentTime = new Date().valueOf();
interval = setInterval(updateTime, 1000);
return () => clearInterval(interval);
});
</script>
<div
class="flex flex-row flex-wrap items-center justify-center gap-2 text-2xl sm:gap-4 sm:text-4xl {isFinalCountdown
? 'final'
: ''}"
>
{#if daysLeft > 0}
<div class="digit digitBlock">{dateDurationLong.format({ days: daysLeft })}</div>
{/if}
<div class="align-center flex flex-row flex-nowrap gap-1 align-middle">
<div class="digit digitBlock">{hoursLeft[0]}</div>
<div class="digit digitBlock">{hoursLeft[1]}</div>
<div class="digit">:</div>
<div class="digit digitBlock">{minutesLeft[0]}</div>
<div class="digit digitBlock">{minutesLeft[1]}</div>
<div class="digit">:</div>
<div class="digit digitBlock">{secondsLeft[0]}</div>
<div class="digit digitBlock">{secondsLeft[1]}</div>
</div>
</div>
<style>
@import '$src/app.css';
.digit {
@apply pt-0.5 font-bold sm:pt-1 sm:pb-0.5;
}
.digitBlock {
/* rndtrash: паддинг снизу по-меньше из-за оптического центра ьуквы */
@apply font-disket rounded-md border-2 border-slate-950 bg-slate-50 px-1.5 text-slate-950 sm:rounded-xl;
box-shadow: 0 2px var(--color-slate-950);
}
@media (prefers-reduced-motion: no-preference) {
.final {
.digitBlock {
animation: finalCountdownText 1s ease-in-out infinite;
}
}
}
@keyframes finalCountdownText {
0% {
color: var(--color-slate-950);
}
1% {
color: var(--color-red-700);
}
50%,
100% {
color: var(--color-slate-950);
}
}
</style>