giveme git commit message 50 char

This commit is contained in:
Marshal
2026-08-06 23:48:06 +00:00
parent 29f7d05800
commit 0f4aa1128f
12 changed files with 147 additions and 33 deletions

View File

@@ -1,4 +1,4 @@
import { Group, Text } from "@mantine/core";
import { Group, Loader, Text } from "@mantine/core";
import { Clock } from "lucide-react";
import { useEffect, useState } from "react";
@@ -9,6 +9,14 @@ export interface CountdownTimerProps {
label?: string;
/** Text shown once the deadline has passed. */
expiredText?: string;
/**
* Optional grace stage: once `deadline` passes, count down to this later
* ISO timestamp instead (e.g. the payment drain tail). `expiredText` then
* only shows after the grace deadline has also passed.
*/
graceDeadline?: string | null;
/** Label shown while counting down the grace stage (e.g. "Payment processing — closes in"). */
graceLabel?: string;
/** Visual size of the time text. */
size?: "xs" | "sm" | "md" | "lg";
/** Colour once under this many seconds remain (urgency). Default 300 (5 min). */
@@ -35,37 +43,56 @@ function formatRemaining(ms: number): string {
/**
* Live countdown to an ISO deadline. Ticks once a second, shows the remaining
* time (d/h/m/s), turns red when under `urgentUnderSeconds`, and shows
* `expiredText` once the deadline is in the past. Display only — enforcement
* lives server-side.
* `expiredText` once the deadline is in the past. When `graceDeadline` is set,
* a lapsed main deadline rolls into a calm second-stage countdown (spinner,
* no urgency colours) toward it before `expiredText` takes over. Display only
* — enforcement lives server-side.
*/
export function CountdownTimer({
deadline,
label,
expiredText = "Expired",
graceDeadline,
graceLabel,
size = "sm",
urgentUnderSeconds = 300,
}: CountdownTimerProps) {
const [remaining, setRemaining] = useState<number | null>(() =>
deadline ? new Date(deadline).getTime() - Date.now() : null,
);
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
if (!deadline) {
setRemaining(null);
return;
}
const target = new Date(deadline).getTime();
const tick = () => setRemaining(target - Date.now());
tick();
const id = setInterval(tick, 1000);
if (!deadline) return;
setNow(Date.now());
const id = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(id);
}, [deadline]);
}, [deadline, graceDeadline]);
if (!deadline || remaining == null || Number.isNaN(remaining)) {
return null;
if (!deadline) return null;
const target = new Date(deadline).getTime();
if (Number.isNaN(target)) return null;
const remaining = target - now;
const expired = remaining <= 0;
const graceTarget = graceDeadline ? new Date(graceDeadline).getTime() : NaN;
const graceRemaining = Number.isNaN(graceTarget) ? 0 : graceTarget - now;
const inGrace = expired && graceRemaining > 0;
if (inGrace) {
return (
<Group gap={6} align="center" wrap="nowrap">
<Loader size={size === "lg" ? 18 : 14} color="blue.7" />
{graceLabel && (
<Text size={size} c="dimmed">
{graceLabel}
</Text>
)}
<Text size={size} fw={600} c="blue.7">
{formatRemaining(graceRemaining)}
</Text>
</Group>
);
}
const expired = remaining <= 0;
const urgent = !expired && remaining <= urgentUnderSeconds * 1000;
const color = expired ? "red.7" : urgent ? "orange.7" : "dimmed";