mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge branch 'dev' into alpha
This commit is contained in:
@@ -1,7 +1,3 @@
|
||||
import { createRequire } from 'module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
// Optional PostCSS configuration for applications that need it
|
||||
export const postcssConfig = {
|
||||
plugins: {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -8,22 +8,27 @@ export interface CurrencySelectorProps {
|
||||
disabled?: boolean;
|
||||
/** Validation error shown under the cards. */
|
||||
error?: string;
|
||||
/**
|
||||
* Offer USD alongside ETB. Import shipments only — export and domestic
|
||||
* traffic is invoiced in ETB, so the option stays hidden everywhere else.
|
||||
* USD is settled by bank transfer, never through the online gateway.
|
||||
*/
|
||||
allowUsd?: boolean;
|
||||
}
|
||||
|
||||
const OPTIONS = [
|
||||
{
|
||||
code: "USD",
|
||||
symbol: "$",
|
||||
name: "US Dollar",
|
||||
hint: "As quoted on the contract",
|
||||
},
|
||||
{
|
||||
code: "ETB",
|
||||
symbol: "Br",
|
||||
name: "Ethiopian Birr",
|
||||
hint: "Converted from the USD total",
|
||||
},
|
||||
] as const;
|
||||
const ETB_OPTION = {
|
||||
code: "ETB",
|
||||
symbol: "Br",
|
||||
name: "Ethiopian Birr",
|
||||
hint: "Pay online through the payment gateway",
|
||||
} as const;
|
||||
|
||||
const USD_OPTION = {
|
||||
code: "USD",
|
||||
symbol: "$",
|
||||
name: "US Dollar",
|
||||
hint: "Paid by bank transfer — send the slip to Finance",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Card-style USD/ETB billing-currency picker. Renders unselected when `value`
|
||||
@@ -34,7 +39,9 @@ export function CurrencySelector({
|
||||
onChange,
|
||||
disabled = false,
|
||||
error,
|
||||
allowUsd = false,
|
||||
}: CurrencySelectorProps) {
|
||||
const options = allowUsd ? [ETB_OPTION, USD_OPTION] : [ETB_OPTION];
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
@@ -44,7 +51,7 @@ export function CurrencySelector({
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{OPTIONS.map((o) => {
|
||||
{options.map((o) => {
|
||||
const selected = value === o.code;
|
||||
return (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user