Wizard When-step and the per-section Edit When page now expose an optional "Pause sending by" hour. Fire time IS the implicit start, so the deadline is the only thing the operator sets. When the bot's fan-out hasn't finished by that hour (in the reminder's timezone) the run pauses for resume — that runtime gating lands in a later phase; this commit just persists the hour and threads it through the wizard. HourSelect splits hour and AM/PM into two side-by-side <select>s and emits a single 0..23 value. to12Hour / from12Hour are pure helpers covered by 11 round-trip tests. Dashboard adjustments: * "WhatsApp accounts" card now reads Connected / Unpaired / Total. * "Reminders" card reads Active / Paused / Ended / Total. * "Recent runs" stat card removed (the Recent activity section below shows the same info). * Activity rows show absolute timestamp with AM/PM and relative time in tandem. Accounts list: * The page-level <h1>Accounts</h1> is hidden on mobile (the top bar already shows it), matching the Dashboard pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
"use client";
|
|
|
|
interface HourSelectProps {
|
|
value: number; // 0..23
|
|
onChange: (hour: number) => void;
|
|
ariaPrefix: string;
|
|
}
|
|
|
|
const HOURS_12H = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
|
type Period = "AM" | "PM";
|
|
|
|
/** Convert a 0..23 24-hour value to its 12-hour + period form. */
|
|
export function to12Hour(h: number): { hour12: number; period: Period } {
|
|
if (h === 0) return { hour12: 12, period: "AM" };
|
|
if (h === 12) return { hour12: 12, period: "PM" };
|
|
if (h < 12) return { hour12: h, period: "AM" };
|
|
return { hour12: h - 12, period: "PM" };
|
|
}
|
|
|
|
/** Convert a 12-hour + period pair back to a 0..23 24-hour value. */
|
|
export function from12Hour(hour12: number, period: Period): number {
|
|
if (period === "AM") return hour12 === 12 ? 0 : hour12;
|
|
return hour12 === 12 ? 12 : hour12 + 12;
|
|
}
|
|
|
|
const SELECT_CLASS =
|
|
"h-9 min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-sm transition-colors outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30";
|
|
|
|
/**
|
|
* Two side-by-side <select> menus: one picks the 12-hour value
|
|
* (1..12), the other picks AM/PM. They emit a single 0..23 value
|
|
* so callers don't have to think about the conversion.
|
|
*/
|
|
export function HourSelect({ value, onChange, ariaPrefix }: HourSelectProps) {
|
|
const { hour12, period } = to12Hour(value);
|
|
|
|
return (
|
|
<div className="flex items-center gap-1">
|
|
<select
|
|
aria-label={`${ariaPrefix} hour`}
|
|
value={hour12}
|
|
onChange={(e) => onChange(from12Hour(Number(e.target.value), period))}
|
|
className={SELECT_CLASS}
|
|
>
|
|
{HOURS_12H.map((h) => (
|
|
<option key={h} value={h}>
|
|
{h}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<select
|
|
aria-label={`${ariaPrefix} period`}
|
|
value={period}
|
|
onChange={(e) => onChange(from12Hour(hour12, e.target.value as Period))}
|
|
className={SELECT_CLASS}
|
|
>
|
|
<option value="AM">AM</option>
|
|
<option value="PM">PM</option>
|
|
</select>
|
|
</div>
|
|
);
|
|
}
|