99 lines
3.2 KiB
Svelte
99 lines
3.2 KiB
Svelte
<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>
|