feat(water-log): surface irrigator name on log form + success screen
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
This commit is contained in:
Tyler
2026-07-01 20:04:41 -06:00
parent ac88584b8b
commit ce62b17898
3 changed files with 79 additions and 24 deletions
+42 -16
View File
@@ -34,7 +34,7 @@ import {
import { SegmentedControl } from "./SegmentedControl";
import type { FieldHeadgate, MobileUnit } from "./types";
import { MOBILE_UNITS } from "./types";
import { ChevronLeft, Clock, Gauge, Pencil, Pin, Spinner } from "./icons";
import { ChevronLeft, Clock, Gauge, Logout, Pencil, Pin, Spinner } from "./icons";
import {
formatLongTimestamp,
formatMeasurement,
@@ -43,6 +43,7 @@ import {
type Props = {
headgate: FieldHeadgate;
irrigatorName?: string;
onSubmit: (input: { measurement: number; unit: string; notes: string }) => Promise<void>;
onChangeHeadgate: () => void;
onLogout: () => void;
@@ -54,6 +55,7 @@ const NOTES_MAX = 500;
export function MobileWaterLogForm({
headgate,
irrigatorName,
onSubmit,
onChangeHeadgate,
onLogout,
@@ -137,10 +139,9 @@ export function MobileWaterLogForm({
>
{/* ── Top nav ────────────────────────────────────────────── */}
<header
className="sticky top-0 z-30 flex h-11 items-center border-b border-[rgba(60,60,67,0.12)] bg-[rgba(255,255,255,0.85)] px-2"
className="sticky top-0 z-30 flex items-center gap-2 border-b border-[rgba(60,60,67,0.12)] bg-[rgba(255,255,255,0.85)] px-2 py-2"
style={{
paddingTop: "env(safe-area-inset-top)",
height: "calc(44px + env(safe-area-inset-top))",
paddingTop: "calc(env(safe-area-inset-top) + 4px)",
backdropFilter: "blur(20px) saturate(180%)",
WebkitBackdropFilter: "blur(20px) saturate(180%)",
}}
@@ -149,12 +150,14 @@ export function MobileWaterLogForm({
type="button"
onClick={onChangeHeadgate}
aria-label="Back to headgates"
className="flex h-9 w-9 items-center justify-center rounded-full text-[#14532d] active:bg-[rgba(120,120,128,0.16)]"
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-[#14532d] active:bg-[rgba(120,120,128,0.16)]"
>
<ChevronLeft size={22} />
</button>
<div className="min-w-0 flex-1">
<h1
className="absolute left-1/2 -translate-x-1/2 text-[17px] font-semibold text-[#1d1d1f]"
className="truncate text-[17px] font-semibold leading-[1.15] text-[#1d1d1f]"
style={{
fontFamily:
"-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif",
@@ -162,7 +165,27 @@ export function MobileWaterLogForm({
>
Log Reading
</h1>
<span className="w-9" />
{irrigatorName ? (
<div className="mt-0.5 flex items-center gap-1.5 truncate text-[12px] text-[rgba(60,60,67,0.6)]">
<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 className="truncate">{irrigatorName}</span>
</div>
) : null}
</div>
<button
type="button"
onClick={onLogout}
aria-label="Sign out"
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-[#14532d] active:bg-[rgba(120,120,128,0.16)]"
>
<Logout size={20} />
</button>
</header>
{/* ── Scrollable form body ───────────────────────────────── */}
@@ -352,15 +375,6 @@ export function MobileWaterLogForm({
</>
)}
</button>
<div className="mt-2 flex items-center justify-center">
<button
type="button"
onClick={onLogout}
className="text-[12px] font-medium text-[rgba(60,60,67,0.45)] active:text-[#14532d]"
>
Sign out
</button>
</div>
</div>
</form>
</div>
@@ -377,4 +391,16 @@ function SectionHeader({ children }: { children: React.ReactNode }) {
);
}
/**
* Two-letter initials for the avatar chip in the nav bar. "Tyler
* Smith" → "TS", "Maria" → "MA". Falls back to "?" when there's no
* usable name.
*/
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();
}
export default MobileWaterLogForm;
@@ -213,6 +213,7 @@ export function MobileWaterApp() {
<MobileWaterLogForm
key={`${selectedHeadgate.id}-${logFormKey}`}
headgate={selectedHeadgate}
irrigatorName={irrigatorName ?? undefined}
onSubmit={handleLogSubmit}
onChangeHeadgate={handleBackToHeadgates}
onLogout={handleLogout}
@@ -230,6 +231,7 @@ export function MobileWaterApp() {
measurement={submittedLog.measurement}
unit={submittedLog.unit}
timestamp={submittedLog.timestamp}
irrigatorName={irrigatorName ?? undefined}
onLogAnother={handleLogAnother}
onBackToHeadgates={handleBackToHeadgates}
/>
@@ -26,6 +26,9 @@ type Props = {
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;
};
@@ -35,6 +38,7 @@ export function MobileWaterSuccessScreen({
measurement,
unit,
timestamp,
irrigatorName,
onLogAnother,
onBackToHeadgates,
}: Props) {
@@ -197,6 +201,17 @@ export function MobileWaterSuccessScreen({
<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 */}
@@ -248,3 +263,15 @@ export function MobileWaterSuccessScreen({
}
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();
}