import { useCallback, useState } from "react"; export function useFieldVisibility() { const [touched, setTouched] = useState>({}); const [submitted, setSubmitted] = useState(false); const touch = useCallback((field: string) => { setTouched((prev) => (prev[field] ? prev : { ...prev, [field]: true })); }, []); const visible = useCallback( (field: string) => submitted || Boolean(touched[field]), [submitted, touched], ); const markSubmitted = useCallback(() => setSubmitted(true), []); return { touch, visible, markSubmitted }; } export function isRequiredString(value: string): boolean { return value.trim().length > 0; } export function isValidEmail(value: string): boolean { const trimmed = value.trim(); if (!trimmed) return false; return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed); } export function isValidPassword(value: string): boolean { return value.length >= 8; } /** Parses a non-negative decimal, or null if empty/invalid. */ export function parseNonNegativeNumber(value: string): number | null { const trimmed = value.trim(); if (!trimmed) return null; const n = Number(trimmed); if (Number.isNaN(n) || n < 0) return null; return n; } export function isValidTaxRate(value: string): boolean { const n = parseNonNegativeNumber(value); if (n === null) return false; return n <= 100; } export type LineItemInput = { description: string; hours: string; rate: string; }; export function validateLineItems(items: LineItemInput[]): string | null { if (items.length === 0) return "Add at least one line item"; for (const item of items) { if (!isRequiredString(item.description)) return "Each line needs a description"; if (parseNonNegativeNumber(item.hours) === null) return "Hours must be a valid number"; if (parseNonNegativeNumber(item.rate) === null) return "Rate must be a valid number"; } return null; }