Add Countdown component

This commit is contained in:
Kuba Orlik 2025-03-01 21:59:27 +01:00
parent 8f382823ad
commit a83d651a52
5 changed files with 193 additions and 0 deletions

View File

@ -0,0 +1,58 @@
@keyframes countdown-tick {
0% {
opacity: 1;
}
49% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 0;
}
}
.countdown {
--height: 60px;
font-size: 32px;
.countdown__wrapper {
display: flex;
flex-flow: row nowrap;
justify-content: center;
}
.countdown-number {
background-color: var(--color-brand-text-bg);
color: var(--color-brand-text-fg);
display: inline-flex;
flex-flow: column;
height: var(--height);
padding: 8px 16px;
margin: 4px;
text-align: center;
.countdown-number__unit {
font-size: 16px;
}
}
.separator {
display: flex;
height: var(--height);
align-items: center;
}
.separator--seconds {
animation: countdown-tick linear infinite 2s;
}
@container (max-width: 400px) {
.separator--seconds,
[data-unit="second"] {
display: none;
}
}
}

View File

@ -0,0 +1,72 @@
import { TempstreamJSX } from "tempstream";
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
import { Component, ComponentArguments } from "@sealcode/jdd";
import { calculateDifference } from "./difference.js";
const component_arguments = {
date: new ComponentArguments.ShortText().setExampleValues(["2025-08-05"]),
} as const;
function renderBlock({
number,
unit,
unit_label,
}: {
number: number;
unit: "day" | "hour" | "minute" | "second";
unit_label: string;
}) {
return (
<div class="countdown-number" data-unit={unit}>
<div class="countdown-number__digits" data-countdown-target={unit}>
{unit == "second" || unit == "minute"
? number.toString().padStart(2, "0")
: number}
</div>
<div class="countdown-number__unit">{unit_label}</div>
</div>
);
}
export class Countdown extends Component<typeof component_arguments> {
getArguments() {
return component_arguments;
}
async toHTML({
args: { date },
classes,
jdd_context: { render_markdown, render_image, language },
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
const { days, hours, minutes, seconds } = calculateDifference(date);
return (
<div
class={["countdown", ...classes]}
data-controller="countdown"
data-countdown-date-value={date}
>
<div class="countdown__wrapper">
{renderBlock({ number: days, unit: "day", unit_label: "days" })}
{renderBlock({ number: hours, unit: "hour", unit_label: "hours" })}
<span class="separator">:</span>
{renderBlock({
number: minutes,
unit: "minute",
unit_label: "minutes",
})}
<span
class="separator separator--seconds"
data-countdown-target="ticker"
>
:
</span>
{renderBlock({
number: seconds,
unit: "second",
unit_label: "seconds",
})}
</div>
</div>
);
}
}

View File

@ -0,0 +1,40 @@
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
import { Controller } from "stimulus";
import { calculateDifference } from "./difference.js";
export default class Countdown extends Controller {
currentIndex = 0;
interval_id: number;
declare dateValue: string;
static values = {
date: String,
};
static targets = ["day", "hour", "minute", "second", "ticker"];
declare dayTarget: HTMLDivElement;
declare hourTarget: HTMLDivElement;
declare minuteTarget: HTMLDivElement;
declare secondTarget: HTMLDivElement;
declare tickerTarget: HTMLDivElement;
async connect() {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
this.interval_id = setInterval(() => this.tick(), 1000) as unknown as number;
this.tickerTarget.style.setProperty("animation", "none"); // we're gonna animate it ourselves to keep it in sync with the js-changed seconds countdown
}
async disconnect() {
clearInterval(this.interval_id);
}
tick() {
const { days, hours, minutes, seconds } = calculateDifference(this.dateValue);
this.dayTarget.textContent = days.toString();
this.hourTarget.textContent = hours.toString();
this.minuteTarget.textContent = minutes.toString().padStart(2, "0");
this.secondTarget.textContent = seconds.toString().padStart(2, "0");
this.tickerTarget.style.setProperty("opacity", seconds % 2 ? "0" : "1");
}
}

View File

@ -0,0 +1,20 @@
const second = 1000;
const minute = 60 * second;
const hour = 60 * minute;
const day = 24 * hour;
export function calculateDifference(date: string) {
const now = new Date();
const target_date = new Date(date);
const distance = target_date.getTime() - now.getTime();
if (distance <= 0) {
return { days: 0, hours: 0, minutes: 0, seconds: 0 };
}
const days = Math.floor(distance / day);
const hours = Math.floor((distance - days * day) / hour);
const minutes = Math.floor((distance - days * day - hours * hour) / minute);
const seconds = Math.floor(
(distance - days * day - hours * hour - minutes * minute) / second
);
return { days, hours, minutes, seconds };
}

View File

@ -7,6 +7,9 @@ const application = Application.start();
import { default as AutoscrollingImages } from "./../back/jdd-components/autoscrolling-images/autoscrolling-images.stimulus.js";
application.register("autoscrolling-images", AutoscrollingImages);
import { default as Countdown } from "./../back/jdd-components/countdown/countdown.stimulus.js";
application.register("countdown", Countdown);
import { default as MapWithPins } from "./../back/jdd-components/map-with-pins/map-with-pins.stimulus.js";
application.register("map-with-pins", MapWithPins);