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 { SegmentedControl } from "./SegmentedControl";
|
||||||
import type { FieldHeadgate, MobileUnit } from "./types";
|
import type { FieldHeadgate, MobileUnit } from "./types";
|
||||||
import { MOBILE_UNITS } 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 {
|
import {
|
||||||
formatLongTimestamp,
|
formatLongTimestamp,
|
||||||
formatMeasurement,
|
formatMeasurement,
|
||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
headgate: FieldHeadgate;
|
headgate: FieldHeadgate;
|
||||||
|
irrigatorName?: string;
|
||||||
onSubmit: (input: { measurement: number; unit: string; notes: string }) => Promise<void>;
|
onSubmit: (input: { measurement: number; unit: string; notes: string }) => Promise<void>;
|
||||||
onChangeHeadgate: () => void;
|
onChangeHeadgate: () => void;
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
@@ -54,6 +55,7 @@ const NOTES_MAX = 500;
|
|||||||
|
|
||||||
export function MobileWaterLogForm({
|
export function MobileWaterLogForm({
|
||||||
headgate,
|
headgate,
|
||||||
|
irrigatorName,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onChangeHeadgate,
|
onChangeHeadgate,
|
||||||
onLogout,
|
onLogout,
|
||||||
@@ -137,10 +139,9 @@ export function MobileWaterLogForm({
|
|||||||
>
|
>
|
||||||
{/* ── Top nav ────────────────────────────────────────────── */}
|
{/* ── Top nav ────────────────────────────────────────────── */}
|
||||||
<header
|
<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={{
|
style={{
|
||||||
paddingTop: "env(safe-area-inset-top)",
|
paddingTop: "calc(env(safe-area-inset-top) + 4px)",
|
||||||
height: "calc(44px + env(safe-area-inset-top))",
|
|
||||||
backdropFilter: "blur(20px) saturate(180%)",
|
backdropFilter: "blur(20px) saturate(180%)",
|
||||||
WebkitBackdropFilter: "blur(20px) saturate(180%)",
|
WebkitBackdropFilter: "blur(20px) saturate(180%)",
|
||||||
}}
|
}}
|
||||||
@@ -149,20 +150,42 @@ export function MobileWaterLogForm({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onChangeHeadgate}
|
onClick={onChangeHeadgate}
|
||||||
aria-label="Back to headgates"
|
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} />
|
<ChevronLeft size={22} />
|
||||||
</button>
|
</button>
|
||||||
<h1
|
|
||||||
className="absolute left-1/2 -translate-x-1/2 text-[17px] font-semibold text-[#1d1d1f]"
|
<div className="min-w-0 flex-1">
|
||||||
style={{
|
<h1
|
||||||
fontFamily:
|
className="truncate text-[17px] font-semibold leading-[1.15] text-[#1d1d1f]"
|
||||||
"-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif",
|
style={{
|
||||||
}}
|
fontFamily:
|
||||||
|
"-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Log Reading
|
||||||
|
</h1>
|
||||||
|
{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)]"
|
||||||
>
|
>
|
||||||
Log Reading
|
<Logout size={20} />
|
||||||
</h1>
|
</button>
|
||||||
<span className="w-9" />
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* ── Scrollable form body ───────────────────────────────── */}
|
{/* ── Scrollable form body ───────────────────────────────── */}
|
||||||
@@ -352,15 +375,6 @@ export function MobileWaterLogForm({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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;
|
export default MobileWaterLogForm;
|
||||||
@@ -213,6 +213,7 @@ export function MobileWaterApp() {
|
|||||||
<MobileWaterLogForm
|
<MobileWaterLogForm
|
||||||
key={`${selectedHeadgate.id}-${logFormKey}`}
|
key={`${selectedHeadgate.id}-${logFormKey}`}
|
||||||
headgate={selectedHeadgate}
|
headgate={selectedHeadgate}
|
||||||
|
irrigatorName={irrigatorName ?? undefined}
|
||||||
onSubmit={handleLogSubmit}
|
onSubmit={handleLogSubmit}
|
||||||
onChangeHeadgate={handleBackToHeadgates}
|
onChangeHeadgate={handleBackToHeadgates}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
@@ -230,6 +231,7 @@ export function MobileWaterApp() {
|
|||||||
measurement={submittedLog.measurement}
|
measurement={submittedLog.measurement}
|
||||||
unit={submittedLog.unit}
|
unit={submittedLog.unit}
|
||||||
timestamp={submittedLog.timestamp}
|
timestamp={submittedLog.timestamp}
|
||||||
|
irrigatorName={irrigatorName ?? undefined}
|
||||||
onLogAnother={handleLogAnother}
|
onLogAnother={handleLogAnother}
|
||||||
onBackToHeadgates={handleBackToHeadgates}
|
onBackToHeadgates={handleBackToHeadgates}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ type Props = {
|
|||||||
measurement: number;
|
measurement: number;
|
||||||
unit: string;
|
unit: string;
|
||||||
timestamp: Date;
|
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;
|
onLogAnother: () => void;
|
||||||
onBackToHeadgates: () => void;
|
onBackToHeadgates: () => void;
|
||||||
};
|
};
|
||||||
@@ -35,6 +38,7 @@ export function MobileWaterSuccessScreen({
|
|||||||
measurement,
|
measurement,
|
||||||
unit,
|
unit,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
irrigatorName,
|
||||||
onLogAnother,
|
onLogAnother,
|
||||||
onBackToHeadgates,
|
onBackToHeadgates,
|
||||||
}: Props) {
|
}: 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)]">
|
<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)}
|
{formatLongTimestamp(timestamp)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
@@ -247,4 +262,16 @@ export function MobileWaterSuccessScreen({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default 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