feat(water-log): surface irrigator name on log form + success screen
Deploy to route.crispygoat.com / deploy (push) Successful in 4m2s
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:
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user