diff --git a/src/back/jdd-components/countdown/countdown.css b/src/back/jdd-components/countdown/countdown.css new file mode 100644 index 0000000..29ba59e --- /dev/null +++ b/src/back/jdd-components/countdown/countdown.css @@ -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; + } + } +} diff --git a/src/back/jdd-components/countdown/countdown.jdd.tsx b/src/back/jdd-components/countdown/countdown.jdd.tsx new file mode 100644 index 0000000..485f502 --- /dev/null +++ b/src/back/jdd-components/countdown/countdown.jdd.tsx @@ -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 ( +
+
+ {unit == "second" || unit == "minute" + ? number.toString().padStart(2, "0") + : number} +
+
{unit_label}
+
+ ); +} + +export class Countdown extends Component { + getArguments() { + return component_arguments; + } + + async toHTML({ + args: { date }, + classes, + jdd_context: { render_markdown, render_image, language }, + }: ComponentToHTMLArgs): Promise { + const { days, hours, minutes, seconds } = calculateDifference(date); + return ( +
+
+ {renderBlock({ number: days, unit: "day", unit_label: "days" })} + {renderBlock({ number: hours, unit: "hour", unit_label: "hours" })} + : + {renderBlock({ + number: minutes, + unit: "minute", + unit_label: "minutes", + })} + + : + + {renderBlock({ + number: seconds, + unit: "second", + unit_label: "seconds", + })} +
+
+ ); + } +} diff --git a/src/back/jdd-components/countdown/countdown.stimulus.ts b/src/back/jdd-components/countdown/countdown.stimulus.ts new file mode 100644 index 0000000..3130e45 --- /dev/null +++ b/src/back/jdd-components/countdown/countdown.stimulus.ts @@ -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"); + } +} diff --git a/src/back/jdd-components/countdown/difference.ts b/src/back/jdd-components/countdown/difference.ts new file mode 100644 index 0000000..b694db7 --- /dev/null +++ b/src/back/jdd-components/countdown/difference.ts @@ -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 }; +} diff --git a/src/front/controllers.ts b/src/front/controllers.ts index 2a22e3f..6c67d0b 100644 --- a/src/front/controllers.ts +++ b/src/front/controllers.ts @@ -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);