ce62b17898
Deploy to route.crispygoat.com / deploy (push) Successful in 4m2s
Makes the currently-signed-in irrigator visible while data is being
entered and confirmed, so the operator can verify the reading is
attributed to them at a glance.
- LogForm nav bar: title + initials avatar + name as a subtitle,
sign-out button moves from the form footer into the nav bar
(more discoverable, frees vertical space for the Submit button)
- SuccessScreen recap: 'Logged by {name}' row with avatar below
the timestamp, matches the log form styling
- MobileWaterApp threads irrigatorName through to both screens
The headgate list still shows 'Signed in as {name}' as a small
caption — left untouched to avoid duplicate UI.
Validation:
- tsc --noEmit clean
- eslint clean on all modified files
277 lines
9.8 KiB
TypeScript
277 lines
9.8 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* MobileWaterSuccessScreen — confirmation after a successful submit.
|
|
*
|
|
* Visual structure:
|
|
* - Centered success illustration: a green ring that draws on,
|
|
* with a checkmark that draws on ~280ms later (the same staggered
|
|
* reveal UIKit's success feedback uses).
|
|
* - Headline: "Reading logged."
|
|
* - Recap card: headgate name + measurement + unit + timestamp.
|
|
* - Two large primary actions, stacked:
|
|
* - "Log Another Entry" (green primary, refresh icon)
|
|
* - "Back to Headgates" (white secondary with thin border)
|
|
* - Subtle thank-you micro-copy at the bottom.
|
|
*
|
|
* The animations use the global `water-ring` + `water-draw` keyframes
|
|
* from `src/app/globals.css`.
|
|
*/
|
|
import { useEffect, useState } from "react";
|
|
import { Grid, Refresh } from "./icons";
|
|
import { formatClockTime, formatLongTimestamp, formatMeasurement } from "./format";
|
|
|
|
type Props = {
|
|
headgateName: string;
|
|
measurement: number;
|
|
unit: string;
|
|
timestamp: Date;
|
|
/** Name of the irrigator who logged the entry — shown in the recap
|
|
* so the operator can confirm the reading is attributed to them. */
|
|
irrigatorName?: string;
|
|
onLogAnother: () => void;
|
|
onBackToHeadgates: () => void;
|
|
};
|
|
|
|
export function MobileWaterSuccessScreen({
|
|
headgateName,
|
|
measurement,
|
|
unit,
|
|
timestamp,
|
|
irrigatorName,
|
|
onLogAnother,
|
|
onBackToHeadgates,
|
|
}: Props) {
|
|
// Reveal animation — staggered for ring → checkmark → content.
|
|
const [ringDone, setRingDone] = useState(false);
|
|
const [contentShown, setContentShown] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const ringTimer = window.setTimeout(() => setRingDone(true), 380);
|
|
const contentTimer = window.setTimeout(() => setContentShown(true), 480);
|
|
return () => {
|
|
window.clearTimeout(ringTimer);
|
|
window.clearTimeout(contentTimer);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
className="flex h-full flex-col bg-[#F2F2F7]"
|
|
style={{
|
|
paddingTop: "env(safe-area-inset-top)",
|
|
paddingBottom: "env(safe-area-inset-bottom)",
|
|
}}
|
|
>
|
|
{/* Brand strip — same as PIN screen for visual continuity. */}
|
|
<header
|
|
className="px-5 pb-3 pt-[max(env(safe-area-inset-top),1rem)]"
|
|
style={{
|
|
background:
|
|
"linear-gradient(180deg, rgba(255,255,255,0.92) 0%, rgba(242,242,247,0) 100%)",
|
|
backdropFilter: "blur(10px)",
|
|
WebkitBackdropFilter: "blur(10px)",
|
|
}}
|
|
>
|
|
<div className="mx-auto flex max-w-md items-center justify-center gap-2">
|
|
<span
|
|
className="text-[12px] font-bold tracking-[0.22em] text-[#14532d]"
|
|
style={{
|
|
fontFamily:
|
|
"-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif",
|
|
}}
|
|
>
|
|
TUXEDO WATER LOG
|
|
</span>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="flex flex-1 flex-col items-center justify-center px-6">
|
|
<div className="w-full max-w-sm text-center">
|
|
{/* Success ring + checkmark */}
|
|
<div className="relative mx-auto mb-8 h-24 w-24">
|
|
<svg
|
|
width="96"
|
|
height="96"
|
|
viewBox="0 0 96 96"
|
|
fill="none"
|
|
aria-hidden
|
|
focusable={false}
|
|
>
|
|
{/* Ring — circumference ~264px for r=42 */}
|
|
<circle
|
|
cx="48"
|
|
cy="48"
|
|
r="42"
|
|
stroke="#34C759"
|
|
strokeWidth="6"
|
|
strokeLinecap="round"
|
|
fill="none"
|
|
strokeDasharray="264"
|
|
strokeDashoffset="264"
|
|
style={{
|
|
animation:
|
|
"water-ring 420ms cubic-bezier(0.32, 0.72, 0, 1) forwards",
|
|
["--ring-from" as string]: "264",
|
|
}}
|
|
/>
|
|
{/* Checkmark — ~62px long */}
|
|
<path
|
|
d="M30 50 L43 63 L66 38"
|
|
stroke="#34C759"
|
|
strokeWidth="6"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
fill="none"
|
|
strokeDasharray="62"
|
|
strokeDashoffset="62"
|
|
style={{
|
|
animation:
|
|
"water-draw 320ms cubic-bezier(0.32, 0.72, 0, 1) 280ms forwards",
|
|
["--draw-from" as string]: "62",
|
|
}}
|
|
/>
|
|
</svg>
|
|
{/* Soft glow behind the ring — sells the celebration. */}
|
|
<div
|
|
aria-hidden
|
|
className="absolute inset-0 rounded-full"
|
|
style={{
|
|
background:
|
|
"radial-gradient(circle, rgba(52,199,89,0.18) 0%, rgba(52,199,89,0) 70%)",
|
|
opacity: ringDone ? 1 : 0,
|
|
transition: "opacity 280ms ease-out",
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Headline */}
|
|
<h1
|
|
className={[
|
|
"mb-2 text-[28px] font-bold leading-[1.15] tracking-tight text-[#1d1d1f]",
|
|
"transition-[opacity,transform] duration-300",
|
|
contentShown
|
|
? "translate-y-0 opacity-100"
|
|
: "translate-y-2 opacity-0",
|
|
].join(" ")}
|
|
style={{
|
|
fontFamily:
|
|
"-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif",
|
|
}}
|
|
>
|
|
Reading logged
|
|
</h1>
|
|
|
|
{/* Recap card */}
|
|
<div
|
|
className={[
|
|
"mx-auto mt-3 mb-8 max-w-xs rounded-[14px] bg-white px-5 py-4 text-left",
|
|
"shadow-[0_1px_2px_rgba(0,0,0,0.04),0_1px_0_rgba(0,0,0,0.02)]",
|
|
"transition-[opacity,transform] duration-300",
|
|
contentShown
|
|
? "translate-y-0 opacity-100"
|
|
: "translate-y-2 opacity-0",
|
|
].join(" ")}
|
|
style={{
|
|
transitionDelay: contentShown ? "80ms" : "0ms",
|
|
}}
|
|
>
|
|
<div className="flex items-baseline justify-between gap-2">
|
|
<span className="truncate text-[15px] font-medium text-[rgba(60,60,67,0.55)]">
|
|
{headgateName}
|
|
</span>
|
|
<span className="shrink-0 text-[13px] font-medium text-[rgba(60,60,67,0.45)]">
|
|
{formatClockTime(timestamp)}
|
|
</span>
|
|
</div>
|
|
<div className="mt-1 flex items-baseline gap-1.5">
|
|
<span
|
|
className="text-[36px] font-bold leading-[1.05] tracking-tight text-[#1d1d1f]"
|
|
style={{
|
|
fontFamily:
|
|
"-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif",
|
|
}}
|
|
>
|
|
{formatMeasurement(measurement)}
|
|
</span>
|
|
<span className="pb-1 text-[16px] font-semibold text-[rgba(60,60,67,0.55)]">
|
|
{unit}
|
|
</span>
|
|
</div>
|
|
<div className="mt-2 border-t border-[rgba(60,60,67,0.10)] pt-2 text-[12px] text-[rgba(60,60,67,0.45)]">
|
|
{formatLongTimestamp(timestamp)}
|
|
</div>
|
|
{irrigatorName ? (
|
|
<div className="mt-1.5 flex items-center gap-1.5 text-[12px] text-[rgba(60,60,67,0.55)]">
|
|
<span
|
|
aria-hidden
|
|
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-[#14532d] text-[9px] font-bold uppercase text-white"
|
|
>
|
|
{initials(irrigatorName)}
|
|
</span>
|
|
<span>Logged by {irrigatorName}</span>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
{/* Action buttons */}
|
|
<div
|
|
className={[
|
|
"flex flex-col gap-3",
|
|
"transition-[opacity,transform] duration-300",
|
|
contentShown
|
|
? "translate-y-0 opacity-100"
|
|
: "translate-y-3 opacity-0",
|
|
].join(" ")}
|
|
style={{
|
|
transitionDelay: contentShown ? "160ms" : "0ms",
|
|
}}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={onLogAnother}
|
|
className="flex h-[56px] w-full items-center justify-center gap-2 rounded-[14px] bg-[#34C759] text-[17px] font-semibold text-white active:scale-[0.985] active:opacity-90"
|
|
style={{
|
|
boxShadow:
|
|
"0 6px 18px rgba(52,199,89,0.32), inset 0 1px 0 rgba(255,255,255,0.18)",
|
|
}}
|
|
>
|
|
<Refresh size={20} />
|
|
Log Another Entry
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={onBackToHeadgates}
|
|
className="flex h-[56px] w-full items-center justify-center gap-2 rounded-[14px] border border-[rgba(60,60,67,0.18)] bg-white text-[17px] font-semibold text-[#14532d] active:scale-[0.985] active:bg-[rgba(120,120,128,0.08)]"
|
|
style={{
|
|
boxShadow: "0 1px 2px rgba(0,0,0,0.04)",
|
|
}}
|
|
>
|
|
<Grid size={20} />
|
|
Back to Headgates
|
|
</button>
|
|
</div>
|
|
|
|
<p className="mt-7 text-[13px] text-[rgba(60,60,67,0.45)]">
|
|
Thanks for keeping the records flowing.
|
|
</p>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default MobileWaterSuccessScreen;
|
|
|
|
/**
|
|
* Two-letter initials for the avatar chip in the recap.
|
|
* Mirrors the same helper used in the log form header so the avatar
|
|
* stays consistent across the two screens.
|
|
*/
|
|
function initials(name: string): string {
|
|
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
if (parts.length === 0) return "?";
|
|
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
} |