mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Add countdown timer component and integrate phase deadlines in booking windows
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import { Clock } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export interface CountdownTimerProps {
|
||||
/** ISO timestamp the countdown targets. */
|
||||
deadline: string | null | undefined;
|
||||
/** Optional label shown before the time (e.g. "Window closes in"). */
|
||||
label?: string;
|
||||
/** Text shown once the deadline has passed. */
|
||||
expiredText?: string;
|
||||
/** Visual size of the time text. */
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
/** Colour once under this many seconds remain (urgency). Default 300 (5 min). */
|
||||
urgentUnderSeconds?: number;
|
||||
}
|
||||
|
||||
function pad(n: number): string {
|
||||
return String(n).padStart(2, "0");
|
||||
}
|
||||
|
||||
/** Break a remaining-milliseconds figure into a human string. */
|
||||
function formatRemaining(ms: number): string {
|
||||
const total = Math.floor(ms / 1000);
|
||||
const days = Math.floor(total / 86400);
|
||||
const hours = Math.floor((total % 86400) / 3600);
|
||||
const minutes = Math.floor((total % 3600) / 60);
|
||||
const seconds = total % 60;
|
||||
|
||||
if (days > 0) return `${days}d ${pad(hours)}h ${pad(minutes)}m`;
|
||||
if (hours > 0) return `${hours}h ${pad(minutes)}m ${pad(seconds)}s`;
|
||||
return `${pad(minutes)}m ${pad(seconds)}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export function CountdownTimer({
|
||||
deadline,
|
||||
label,
|
||||
expiredText = "Expired",
|
||||
size = "sm",
|
||||
urgentUnderSeconds = 300,
|
||||
}: CountdownTimerProps) {
|
||||
const [remaining, setRemaining] = useState<number | null>(() =>
|
||||
deadline ? new Date(deadline).getTime() - Date.now() : null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!deadline) {
|
||||
setRemaining(null);
|
||||
return;
|
||||
}
|
||||
const target = new Date(deadline).getTime();
|
||||
const tick = () => setRemaining(target - Date.now());
|
||||
tick();
|
||||
const id = setInterval(tick, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, [deadline]);
|
||||
|
||||
if (!deadline || remaining == null || Number.isNaN(remaining)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expired = remaining <= 0;
|
||||
const urgent = !expired && remaining <= urgentUnderSeconds * 1000;
|
||||
const color = expired ? "red.7" : urgent ? "orange.7" : "dimmed";
|
||||
|
||||
return (
|
||||
<Group gap={6} align="center" wrap="nowrap">
|
||||
<Clock size={size === "lg" ? 18 : 14} />
|
||||
{label && (
|
||||
<Text size={size} c="dimmed">
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
<Text size={size} fw={600} c={color}>
|
||||
{expired ? expiredText : formatRemaining(remaining)}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default CountdownTimer;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { CountdownTimer, default } from "./CountdownTimer";
|
||||
export type { CountdownTimerProps } from "./CountdownTimer";
|
||||
@@ -25,6 +25,9 @@ export { useFileViewer } from "./hooks/useFileViewer";
|
||||
export { OperationDatePicker } from "./components/OperationDatePicker";
|
||||
export type { OperationDatePickerProps } from "./components/OperationDatePicker";
|
||||
|
||||
export { CountdownTimer } from "./components/CountdownTimer";
|
||||
export type { CountdownTimerProps } from "./components/CountdownTimer";
|
||||
|
||||
export { Badge } from "./components/badge";
|
||||
// export type { BadgeProps } from "./components/badge";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user