refactor(water-log): tone down report preview modal to production-appropriate GlassModal
Deploy to route.crispygoat.com / deploy (push) Successful in 3m55s
Deploy to route.crispygoat.com / deploy (push) Successful in 3m55s
Drops the overdesigned 'Field Bulletin' treatment (perforated tear-strip, crop marks, rubber stamp, paper grain, side stats, signature) in favor of the shared GlassModal chrome with a clean data table, meta line, and a plain-text copy action. Matches the rest of the admin UI.
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
* `src/actions/water-log/admin.ts`.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useReducer, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
} from "@/lib/water-log-reporting";
|
||||
import { WaterGauge, SeasonMark } from "@/components/water/icons";
|
||||
import AdminButton from "./design-system/AdminButton";
|
||||
import GlassModal from "@/components/admin/GlassModal";
|
||||
import { formatDate, formatDateTime } from "@/lib/format-date";
|
||||
|
||||
const DEFAULT_SEASON: IrrigationSeasonSettings = {
|
||||
@@ -76,7 +77,6 @@ type ReportPreviewData = {
|
||||
dateIso: string;
|
||||
recipientName?: string;
|
||||
inSeason: boolean;
|
||||
bulletinNumber: number;
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -493,13 +493,6 @@ export default function WaterLogAdminPanel({
|
||||
const unit =
|
||||
rows.find((r) => r.unit)?.unit ?? state.headgates[0]?.unit ?? "CFS";
|
||||
const now = new Date();
|
||||
// Deterministic-ish bulletin number from today's date + entry count so
|
||||
// it changes per-day but feels like a real reference code.
|
||||
const dayKey =
|
||||
state.today.replace(/-/g, "") ||
|
||||
`${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`;
|
||||
const bulletinNumber =
|
||||
(parseInt(dayKey, 10) % 1000) + (state.entries.length % 7) * 13 + 1;
|
||||
|
||||
if (rows.length === 0) {
|
||||
notify(
|
||||
@@ -518,7 +511,6 @@ export default function WaterLogAdminPanel({
|
||||
dateIso: state.today || now.toISOString().slice(0, 10),
|
||||
recipientName: state.recipientName.trim() || undefined,
|
||||
inSeason,
|
||||
bulletinNumber,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1832,12 +1824,10 @@ function DayInput({
|
||||
|
||||
// ─── Report Preview Modal ─────────────────────────────────────────────────
|
||||
//
|
||||
// A "Field Bulletin" — modeled after USDA Soil Conservation Service / USGS
|
||||
// survey bulletins. Cream paper, deep forest ink, monospaced data with
|
||||
// hairline rules, perforated tear-strip on the left, and a rotated
|
||||
// PREVIEW rubber stamp to keep it clearly distinct from the actual
|
||||
// deliverable. The composition is a single tactile sheet that opens with
|
||||
// a soft paper-curl motion and staggers each entry row in.
|
||||
// Simple, production-appropriate preview of the daily water report that
|
||||
// would be sent by the cron. Built on the shared GlassModal so ESC, focus
|
||||
// trapping, scroll-lock, and the standard admin header/close chrome all
|
||||
// match the rest of /admin.
|
||||
|
||||
function ReportPreviewModal({
|
||||
data,
|
||||
@@ -1848,30 +1838,11 @@ function ReportPreviewModal({
|
||||
onClose: () => void;
|
||||
notify: (msg: string, kind?: "ok" | "err") => void;
|
||||
}) {
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// ESC closes; lock body scroll while open.
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
document.addEventListener("keydown", onKey);
|
||||
const prevOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
// Move focus into the dialog for keyboard users.
|
||||
const t = setTimeout(() => dialogRef.current?.focus(), 30);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKey);
|
||||
document.body.style.overflow = prevOverflow;
|
||||
clearTimeout(t);
|
||||
};
|
||||
}, [data, onClose]);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const { rows, total, unit, dateLabel, dateIso, recipientName, inSeason, bulletinNumber } = data;
|
||||
const { rows, total, unit, dateLabel, dateIso, recipientName, inSeason } = data;
|
||||
const totalUnit = rows[0]?.unit ?? unit ?? "CFS";
|
||||
|
||||
async function handleCopy() {
|
||||
@@ -1890,275 +1861,65 @@ function ReportPreviewModal({
|
||||
}
|
||||
}
|
||||
|
||||
// Earliest entry time → "shift open", latest → "shift close" — a nice
|
||||
// touch pulled straight from irrigation logbook conventions.
|
||||
const times = rows
|
||||
.map((r) => new Date(r.logged_at))
|
||||
.filter((d) => !Number.isNaN(d.getTime()));
|
||||
const firstTime = times.length
|
||||
? times.reduce((a, b) => (a < b ? a : b))
|
||||
: null;
|
||||
const lastTime = times.length
|
||||
? times.reduce((a, b) => (a > b ? a : b))
|
||||
: null;
|
||||
const fmtTime = (d: Date | null) =>
|
||||
d
|
||||
? d.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: "—";
|
||||
|
||||
// Most active headgate (max sum of measurement) — useful color piece.
|
||||
const byHeadgate = new Map<string, number>();
|
||||
for (const r of rows) {
|
||||
byHeadgate.set(r.headgate_name, (byHeadgate.get(r.headgate_name) ?? 0) + r.measurement);
|
||||
}
|
||||
const topHeadgate = Array.from(byHeadgate.entries()).sort((a, b) => b[1] - a[1])[0];
|
||||
const subtitle =
|
||||
`${rows.length} ${rows.length === 1 ? "entry" : "entries"} · ` +
|
||||
`${total.toFixed(2)} ${totalUnit} total · ` +
|
||||
(inSeason ? "in season" : "out of season");
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Daily water report preview"
|
||||
className="wl-modal-backdrop fixed inset-0 z-50 flex items-start justify-center overflow-y-auto px-4 py-8 sm:py-14"
|
||||
onMouseDown={(e) => {
|
||||
// Click on the backdrop (not the paper) closes.
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
{/* Backdrop wash — deep forest tint with subtle radial vignette. */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="wl-modal-backdrop__wash fixed inset-0"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(120% 80% at 50% 30%, rgba(26,77,46,0.42) 0%, rgba(15,42,28,0.72) 60%, rgba(8,28,18,0.82) 100%)",
|
||||
backdropFilter: "blur(3px) saturate(0.9)",
|
||||
WebkitBackdropFilter: "blur(3px) saturate(0.9)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* The paper itself */}
|
||||
<div
|
||||
ref={dialogRef}
|
||||
tabIndex={-1}
|
||||
className="wl-modal-paper relative z-10 w-full max-w-2xl outline-none"
|
||||
style={{
|
||||
// soft drop shadow + warm paper gradient
|
||||
background:
|
||||
"linear-gradient(180deg, #fdfaf2 0%, #f7f1de 100%)",
|
||||
boxShadow:
|
||||
"0 1px 0 rgba(255,255,255,0.6) inset, 0 30px 60px -20px rgba(15,42,28,0.45), 0 18px 32px -22px rgba(15,42,28,0.6)",
|
||||
border: "1px solid #1a4d2e",
|
||||
}}
|
||||
>
|
||||
{/* Paper grain overlay — extremely subtle */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.10] mix-blend-multiply"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(circle at 25% 15%, rgba(122,90,38,0.22) 0, transparent 35%), radial-gradient(circle at 80% 80%, rgba(26,77,46,0.18) 0, transparent 40%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Perforated left tear-strip — like a printed form */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="wl-modal-perf absolute left-0 top-0 h-full w-3"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"radial-gradient(circle, #fdfaf2 1.2px, transparent 1.6px)",
|
||||
backgroundSize: "8px 12px",
|
||||
backgroundPosition: "2px 6px",
|
||||
opacity: 0.55,
|
||||
maskImage:
|
||||
"linear-gradient(180deg, transparent 0, black 6%, black 94%, transparent 100%)",
|
||||
WebkitMaskImage:
|
||||
"linear-gradient(180deg, transparent 0, black 6%, black 94%, transparent 100%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Crop marks at the four corners — printer's registration marks */}
|
||||
<CropMark className="absolute left-3 top-3" rotate={0} />
|
||||
<CropMark className="absolute right-3 top-3" rotate={90} />
|
||||
<CropMark className="absolute right-3 bottom-3" rotate={180} />
|
||||
<CropMark className="absolute left-3 bottom-3" rotate={270} />
|
||||
|
||||
{/* Header band — forest ink strip with cream mono caps */}
|
||||
<div
|
||||
className="relative flex items-stretch border-b border-[#1a4d2e]"
|
||||
style={{ background: "#1a4d2e", color: "#fdfaf2" }}
|
||||
>
|
||||
<div className="flex flex-1 flex-col justify-center gap-0.5 px-6 py-3">
|
||||
<div
|
||||
className="text-[10px] uppercase"
|
||||
style={{
|
||||
fontFamily: "var(--font-fragment-mono, monospace)",
|
||||
letterSpacing: "0.32em",
|
||||
}}
|
||||
>
|
||||
Tuxedo Ditch Co. · Water Log
|
||||
</div>
|
||||
<div
|
||||
className="text-[11px] uppercase opacity-80"
|
||||
style={{
|
||||
fontFamily: "var(--font-fragment-mono, monospace)",
|
||||
letterSpacing: "0.24em",
|
||||
}}
|
||||
>
|
||||
Daily Field Bulletin
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-col items-end justify-center border-l border-[#fdfaf2]/30 px-5 py-3 text-right"
|
||||
style={{
|
||||
fontFamily: "var(--font-fragment-mono, monospace)",
|
||||
letterSpacing: "0.18em",
|
||||
}}
|
||||
>
|
||||
<div className="text-[10px] uppercase opacity-80">Bulletin №</div>
|
||||
<div className="text-[15px] font-medium tracking-[0.18em]">
|
||||
{String(bulletinNumber).padStart(4, "0")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title row + PREVIEW stamp */}
|
||||
<div className="relative px-6 pb-2 pt-5 sm:px-8 sm:pt-6">
|
||||
<h2
|
||||
className="leading-[0.95] tracking-tight text-[#1a4d2e]"
|
||||
style={{
|
||||
fontFamily: "var(--font-display, Georgia, serif)",
|
||||
fontWeight: 600,
|
||||
fontSize: "clamp(34px, 5.4vw, 48px)",
|
||||
letterSpacing: "-0.02em",
|
||||
}}
|
||||
>
|
||||
Daily Water Report
|
||||
</h2>
|
||||
<p
|
||||
className="mt-1 text-[12px] uppercase tracking-[0.24em] text-[#57694e]"
|
||||
style={{ fontFamily: "var(--font-fragment-mono, monospace)" }}
|
||||
<GlassModal
|
||||
title="Daily water report — preview"
|
||||
subtitle={subtitle}
|
||||
onClose={onClose}
|
||||
maxWidth="max-w-2xl"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Meta line */}
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-[var(--admin-text-muted)]">
|
||||
<span>
|
||||
<span className="font-semibold text-[var(--admin-text-secondary)]">Date:</span>{" "}
|
||||
{dateLabel}
|
||||
<span className="mx-2 text-[#d4d9d3]">/</span>
|
||||
<span className="text-[#1a4d2e]">
|
||||
{dateIso.replace(/-/g, " · ")}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{/* Rotated rubber stamp — PREVIEW */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="wl-modal-stamp pointer-events-none absolute right-4 top-4 select-none sm:right-8 sm:top-5"
|
||||
style={{
|
||||
transform: "rotate(-9deg)",
|
||||
color: "#b1452a",
|
||||
fontFamily: "var(--font-display, Georgia, serif)",
|
||||
fontWeight: 800,
|
||||
letterSpacing: "0.18em",
|
||||
border: "2.5px double currentColor",
|
||||
padding: "4px 12px 5px",
|
||||
borderRadius: "3px",
|
||||
opacity: 0.78,
|
||||
boxShadow: "0 0 0 2px rgba(177,69,42,0.05)",
|
||||
fontSize: "16px",
|
||||
}}
|
||||
>
|
||||
PREVIEW
|
||||
<span
|
||||
className="ml-2 text-[10px] tracking-[0.3em]"
|
||||
style={{
|
||||
fontFamily: "var(--font-fragment-mono, monospace)",
|
||||
fontWeight: 500,
|
||||
opacity: 0.85,
|
||||
}}
|
||||
>
|
||||
ONLY
|
||||
<span className="hidden sm:inline text-[var(--admin-border)]">·</span>
|
||||
<span className="font-mono text-[var(--admin-text-secondary)]">
|
||||
{dateIso}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Meta strip — date / season / total / count */}
|
||||
<div className="relative grid grid-cols-2 gap-px border-y border-[#1a4d2e] bg-[#1a4d2e] sm:grid-cols-4">
|
||||
<MetaCell label="Date" value={dateLabel} mono />
|
||||
<MetaCell
|
||||
label="Season"
|
||||
value={inSeason ? "In season" : "Out of season"}
|
||||
accent={inSeason ? "green" : "amber"}
|
||||
/>
|
||||
<MetaCell
|
||||
label="Total volume"
|
||||
value={`${total.toFixed(2)} ${totalUnit}`}
|
||||
mono
|
||||
emphasis
|
||||
/>
|
||||
<MetaCell
|
||||
label="Entries"
|
||||
value={`${rows.length} ${rows.length === 1 ? "log" : "logs"}`}
|
||||
mono
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Salutation */}
|
||||
<div className="relative px-6 pt-5 sm:px-8">
|
||||
{recipientName ? (
|
||||
<p
|
||||
className="text-[#1a4d2e]"
|
||||
style={{
|
||||
fontFamily: "var(--font-display, Georgia, serif)",
|
||||
fontStyle: "italic",
|
||||
fontSize: "20px",
|
||||
lineHeight: 1.25,
|
||||
}}
|
||||
>
|
||||
{recipientName},
|
||||
</p>
|
||||
) : (
|
||||
<p
|
||||
className="text-[#57694e]"
|
||||
style={{
|
||||
fontFamily: "var(--font-fragment-mono, monospace)",
|
||||
fontSize: "11px",
|
||||
letterSpacing: "0.22em",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
To the irrigator —
|
||||
</p>
|
||||
)}
|
||||
<p
|
||||
className="mt-2 text-[13px] leading-relaxed text-[#3a4a36]"
|
||||
style={{ fontFamily: "var(--font-manrope, system-ui, sans-serif)" }}
|
||||
>
|
||||
Below is the day's record of measured flow at every headgate
|
||||
in service. Times are local; volumes are reported in the unit
|
||||
declared for each gate.{" "}
|
||||
<span className="text-[#a16207]">
|
||||
This bulletin is a preview — no message has been sent.
|
||||
<>
|
||||
<span className="hidden sm:inline text-[var(--admin-border)]">·</span>
|
||||
<span>
|
||||
<span className="font-semibold text-[var(--admin-text-secondary)]">
|
||||
Recipient:
|
||||
</span>{" "}
|
||||
{recipientName}
|
||||
</span>
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Preview banner — keep it clear this hasn't been sent */}
|
||||
<div
|
||||
className="rounded-lg px-3 py-2 text-xs"
|
||||
style={{
|
||||
backgroundColor: "var(--admin-warning-bg, #fef9c3)",
|
||||
color: "var(--admin-warning-text, #a16207)",
|
||||
}}
|
||||
>
|
||||
Preview only. No message has been sent. Daily-report delivery is
|
||||
handled by the scheduled{" "}
|
||||
<code className="font-mono">api/cron/water-report</code> endpoint.
|
||||
</div>
|
||||
|
||||
{/* Data table */}
|
||||
<div className="relative mt-5 px-6 sm:px-8">
|
||||
<div className="overflow-hidden rounded-sm border border-[#1a4d2e]/40 bg-[#fffaf0]/60">
|
||||
<table className="w-full border-collapse">
|
||||
<div className="overflow-hidden rounded-lg border border-[var(--admin-border)]">
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr
|
||||
className="border-b border-[#1a4d2e]/50"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, rgba(26,77,46,0.08) 0%, rgba(26,77,46,0.02) 100%)",
|
||||
}}
|
||||
>
|
||||
<Th className="w-[68px]">Time</Th>
|
||||
<tr style={{ backgroundColor: "var(--admin-bg-subtle)" }}>
|
||||
<Th className="w-[72px]">Time</Th>
|
||||
<Th>Headgate</Th>
|
||||
<Th>Irrigator</Th>
|
||||
<Th className="w-[86px] text-right">Volume</Th>
|
||||
<Th className="w-[28%]">Note</Th>
|
||||
<Th className="w-[88px] text-right">Volume</Th>
|
||||
<Th>Note</Th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -2167,283 +1928,92 @@ function ReportPreviewModal({
|
||||
"en-US",
|
||||
{ hour: "numeric", minute: "2-digit" },
|
||||
);
|
||||
const isTop =
|
||||
topHeadgate &&
|
||||
row.headgate_name === topHeadgate[0] &&
|
||||
row.measurement === topHeadgate[1];
|
||||
return (
|
||||
<tr
|
||||
key={i}
|
||||
className="wl-modal-row border-b border-[#1a4d2e]/15 transition-colors last:border-b-0 hover:bg-[#fdfaf2]"
|
||||
style={{
|
||||
animationDelay: `${120 + i * 35}ms`,
|
||||
}}
|
||||
className="border-t border-[var(--admin-border-light)]"
|
||||
>
|
||||
<Td className="text-[#3a4a36]">{time}</Td>
|
||||
<Td>
|
||||
<span
|
||||
className="inline-flex items-center gap-1.5 text-[#1a4d2e]"
|
||||
style={{
|
||||
fontFamily: "var(--font-fragment-mono, monospace)",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="inline-block h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
background: isTop ? "#ca8a04" : "#1a4d2e",
|
||||
opacity: isTop ? 1 : 0.55,
|
||||
}}
|
||||
/>
|
||||
{row.headgate_name}
|
||||
</span>
|
||||
<Td className="font-mono text-[var(--admin-text-secondary)]">
|
||||
{time}
|
||||
</Td>
|
||||
<Td className="font-medium text-[var(--admin-text-primary)]">
|
||||
{row.headgate_name}
|
||||
</Td>
|
||||
<Td className="text-[var(--admin-text-secondary)]">
|
||||
{row.user_name}
|
||||
</Td>
|
||||
<Td className="text-[#3a4a36]">{row.user_name}</Td>
|
||||
<Td className="text-right">
|
||||
<span
|
||||
className="text-[#1a4d2e]"
|
||||
style={{
|
||||
fontFamily: "var(--font-fragment-mono, monospace)",
|
||||
}}
|
||||
>
|
||||
<span className="font-mono font-semibold text-[var(--admin-text-primary)]">
|
||||
{row.measurement.toFixed(2)}
|
||||
</span>
|
||||
<span
|
||||
className="ml-1 text-[10px] text-[#57694e]"
|
||||
style={{
|
||||
fontFamily: "var(--font-fragment-mono, monospace)",
|
||||
letterSpacing: "0.08em",
|
||||
}}
|
||||
>
|
||||
</span>{" "}
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">
|
||||
{row.unit}
|
||||
</span>
|
||||
</Td>
|
||||
<Td className="text-[#57694e]">
|
||||
{row.notes ? (
|
||||
<span
|
||||
className="italic"
|
||||
style={{
|
||||
fontFamily:
|
||||
"var(--font-display, Georgia, serif)",
|
||||
}}
|
||||
>
|
||||
“{row.notes}”
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[#d4d9d3]">·</span>
|
||||
)}
|
||||
<Td className="text-[var(--admin-text-muted)]">
|
||||
{row.notes ?? <span className="opacity-40">—</span>}
|
||||
</Td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t-2 border-[#1a4d2e]">
|
||||
<Td className="!border-0 pt-2 text-[11px] uppercase tracking-[0.22em] text-[#57694e]">
|
||||
Day total
|
||||
</Td>
|
||||
<Td className="!border-0 pt-2">
|
||||
<span className="text-[#d4d9d3]">·</span>
|
||||
</Td>
|
||||
<Td className="!border-0 pt-2 text-right text-[11px] uppercase tracking-[0.22em] text-[#57694e]">
|
||||
Σ
|
||||
<tr
|
||||
className="border-t-2"
|
||||
style={{ borderColor: "var(--admin-border)" }}
|
||||
>
|
||||
<Td
|
||||
className="!border-0 pt-2 text-xs font-semibold uppercase tracking-wide text-[var(--admin-text-secondary)]"
|
||||
colSpan={3}
|
||||
>
|
||||
Total
|
||||
</Td>
|
||||
<Td className="!border-0 pt-2 text-right">
|
||||
<span
|
||||
className="text-[#1a4d2e]"
|
||||
style={{
|
||||
fontFamily: "var(--font-fragment-mono, monospace)",
|
||||
fontSize: "16px",
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.02em",
|
||||
}}
|
||||
>
|
||||
<span className="font-mono font-bold text-[var(--admin-text-primary)]">
|
||||
{total.toFixed(2)}
|
||||
</span>
|
||||
<span
|
||||
className="ml-1 text-[11px] text-[#57694e]"
|
||||
style={{
|
||||
fontFamily: "var(--font-fragment-mono, monospace)",
|
||||
letterSpacing: "0.08em",
|
||||
}}
|
||||
>
|
||||
</span>{" "}
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">
|
||||
{totalUnit}
|
||||
</span>
|
||||
</Td>
|
||||
<Td className="!border-0 pt-2 text-right text-[11px] text-[#57694e]">
|
||||
{rows.length} logs
|
||||
<Td className="!border-0 pt-2">
|
||||
<span className="opacity-40">—</span>
|
||||
</Td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Side stats — first/last entry, top headgate */}
|
||||
<div className="relative grid grid-cols-3 gap-px border-y border-[#1a4d2e]/40 bg-[#1a4d2e]/10 px-6 sm:px-8 mt-5">
|
||||
<SideStat label="First entry" value={fmtTime(firstTime)} />
|
||||
<SideStat label="Last entry" value={fmtTime(lastTime)} />
|
||||
<SideStat
|
||||
label="Top gate"
|
||||
value={topHeadgate ? topHeadgate[0] : "—"}
|
||||
sub={
|
||||
topHeadgate ? `${topHeadgate[1].toFixed(2)} ${totalUnit}` : ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer signature */}
|
||||
<div className="relative px-6 pb-6 pt-4 text-center sm:px-8">
|
||||
<div className="mx-auto mb-3 h-px w-24 bg-[#1a4d2e]/40" />
|
||||
<p
|
||||
className="text-[10px] uppercase tracking-[0.36em] text-[#57694e]"
|
||||
style={{ fontFamily: "var(--font-fragment-mono, monospace)" }}
|
||||
>
|
||||
— End of transmission —
|
||||
</p>
|
||||
<p
|
||||
className="mt-2 text-[11px] text-[#57694e]"
|
||||
style={{
|
||||
fontFamily: "var(--font-display, Georgia, serif)",
|
||||
fontStyle: "italic",
|
||||
}}
|
||||
>
|
||||
Filed by Route Commerce Water Log
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action bar — sticky to the bottom of the paper */}
|
||||
<div
|
||||
className="relative flex flex-col-reverse items-stretch gap-2 border-t border-[#1a4d2e] px-5 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-6"
|
||||
style={{ background: "rgba(26,77,46,0.04)" }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
{/* Actions */}
|
||||
<div className="flex flex-col-reverse items-stretch gap-2 pt-2 sm:flex-row sm:items-center sm:justify-end">
|
||||
<button type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-sm border border-[#1a4d2e]/40 px-4 py-2 text-[11px] uppercase tracking-[0.24em] text-[#57694e] transition-colors hover:border-[#1a4d2e] hover:text-[#1a4d2e]"
|
||||
style={{ fontFamily: "var(--font-fragment-mono, monospace)" }}
|
||||
className="rounded-lg border border-[var(--admin-border)] px-4 py-2 text-sm font-semibold text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] transition-colors"
|
||||
>
|
||||
Close · Esc
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
<button type="button"
|
||||
onClick={handleCopy}
|
||||
className="group inline-flex items-center justify-center gap-2 rounded-sm border border-[#1a4d2e] bg-[#1a4d2e] px-4 py-2 text-[11px] uppercase tracking-[0.24em] text-[#fdfaf2] transition-all hover:bg-[#163f25] active:translate-y-px"
|
||||
style={{ fontFamily: "var(--font-fragment-mono, monospace)" }}
|
||||
className="rounded-lg bg-[var(--admin-accent)] px-4 py-2 text-sm font-bold text-white hover:bg-[var(--admin-accent-hover)] transition-colors"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckMark /> Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CopyMark /> Copy plain text
|
||||
</>
|
||||
)}
|
||||
{copied ? "Copied" : "Copy plain text"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Component-local animations */}
|
||||
<style>{`
|
||||
@keyframes wl-modal-pop {
|
||||
0% { opacity: 0; transform: translateY(14px) scale(0.97); }
|
||||
60% { opacity: 1; transform: translateY(-2px) scale(1.005); }
|
||||
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
@keyframes wl-modal-fade {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
@keyframes wl-modal-row {
|
||||
0% { opacity: 0; transform: translateX(-6px); }
|
||||
100% { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
@keyframes wl-modal-stamp {
|
||||
0% { opacity: 0; transform: rotate(-9deg) scale(1.4); }
|
||||
60% { opacity: 0.78; transform: rotate(-9deg) scale(0.96); }
|
||||
100% { opacity: 0.78; transform: rotate(-9deg) scale(1); }
|
||||
}
|
||||
.wl-modal-backdrop { animation: wl-modal-fade 200ms ease-out both; }
|
||||
.wl-modal-paper {
|
||||
animation: wl-modal-pop 320ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
||||
}
|
||||
.wl-modal-row {
|
||||
animation: wl-modal-row 360ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
||||
opacity: 0;
|
||||
}
|
||||
.wl-modal-stamp { animation: wl-modal-stamp 480ms ease-out 220ms both; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.wl-modal-backdrop,
|
||||
.wl-modal-paper,
|
||||
.wl-modal-row,
|
||||
.wl-modal-stamp {
|
||||
animation: none !important;
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</GlassModal>
|
||||
);
|
||||
}
|
||||
|
||||
function MetaCell({
|
||||
label,
|
||||
value,
|
||||
mono = false,
|
||||
emphasis = false,
|
||||
accent,
|
||||
function Th({
|
||||
children,
|
||||
className = "",
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
mono?: boolean;
|
||||
emphasis?: boolean;
|
||||
accent?: "green" | "amber";
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const accentColor =
|
||||
accent === "green"
|
||||
? "#1a4d2e"
|
||||
: accent === "amber"
|
||||
? "#a16207"
|
||||
: undefined;
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5 bg-[#fdfaf2] px-4 py-2.5">
|
||||
<span
|
||||
className="text-[9px] uppercase tracking-[0.28em] text-[#57694e]"
|
||||
style={{ fontFamily: "var(--font-fragment-mono, monospace)" }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
emphasis
|
||||
? "text-[16px] text-[#1a4d2e]"
|
||||
: mono
|
||||
? "text-[13px] text-[#1a4d2e]"
|
||||
: "text-[13px] text-[#1a4d2e]"
|
||||
}
|
||||
style={{
|
||||
fontFamily: mono
|
||||
? "var(--font-fragment-mono, monospace)"
|
||||
: "var(--font-manrope, system-ui, sans-serif)",
|
||||
fontWeight: emphasis ? 600 : 500,
|
||||
letterSpacing: mono ? "0.02em" : undefined,
|
||||
color: accentColor,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Th({ children, className = "" }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<th
|
||||
className={`px-3 py-2 text-left text-[10px] uppercase tracking-[0.22em] text-[#1a4d2e] ${className}`}
|
||||
style={{ fontFamily: "var(--font-fragment-mono, monospace)" }}
|
||||
className={`px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wide text-[var(--admin-text-secondary)] ${className}`}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
@@ -2453,96 +2023,21 @@ function Th({ children, className = "" }: { children: React.ReactNode; className
|
||||
function Td({
|
||||
children,
|
||||
className = "",
|
||||
colSpan,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
colSpan?: number;
|
||||
}) {
|
||||
return (
|
||||
<td
|
||||
className={`px-3 py-2 text-[13px] text-[#1d1d1f] ${className}`}
|
||||
style={{
|
||||
fontFamily: "var(--font-manrope, system-ui, sans-serif)",
|
||||
verticalAlign: "top",
|
||||
}}
|
||||
colSpan={colSpan}
|
||||
className={`px-3 py-2 text-sm text-[var(--admin-text-primary)] ${className}`}
|
||||
>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
function SideStat({
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
sub?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5 bg-[#fdfaf2] px-4 py-2.5">
|
||||
<span
|
||||
className="text-[9px] uppercase tracking-[0.28em] text-[#57694e]"
|
||||
style={{ fontFamily: "var(--font-fragment-mono, monospace)" }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<span
|
||||
className="text-[14px] text-[#1a4d2e]"
|
||||
style={{
|
||||
fontFamily: "var(--font-fragment-mono, monospace)",
|
||||
letterSpacing: "0.02em",
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
{sub ? (
|
||||
<span
|
||||
className="text-[10px] text-[#57694e]"
|
||||
style={{
|
||||
fontFamily: "var(--font-fragment-mono, monospace)",
|
||||
letterSpacing: "0.06em",
|
||||
}}
|
||||
>
|
||||
{sub}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CropMark({ className = "", rotate = 0 }: { className?: string; rotate?: number }) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 10 10"
|
||||
className={className}
|
||||
style={{ transform: `rotate(${rotate}deg)`, opacity: 0.5 }}
|
||||
>
|
||||
<path d="M0 5 L10 5 M5 0 L5 10" stroke="#1a4d2e" strokeWidth="0.6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyMark() {
|
||||
return (
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4">
|
||||
<rect x="5" y="5" width="9" height="9" rx="1" />
|
||||
<path d="M11 5 V3 a1 1 0 0 0 -1 -1 H3 a1 1 0 0 0 -1 1 V10 a1 1 0 0 0 1 1 H5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckMark() {
|
||||
return (
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6">
|
||||
<path d="M3 8 L7 12 L13 4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Icons (16–20px, currentColor) ──────────────────────────────────────
|
||||
|
||||
function QrIcon() {
|
||||
|
||||
Reference in New Issue
Block a user