Files
beenvoice-app/lib/form-validation.ts
T
soconnorandCursor 6762a9bff3 Move production to beenvoice.app with migrated accounts, refreshed auth and timer UX, and expanded invoice flows.
Official URL migration preserves sessions, shortcuts prefs, and last clock-in client; auth screens match web with legal links; time clock and invoice editor/send flows are updated for the new domain and UI patterns.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 03:40:48 -04:00

72 lines
2.1 KiB
TypeScript

import { useCallback, useState } from "react";
export function useFieldVisibility() {
const [touched, setTouched] = useState<Record<string, boolean>>({});
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 null;
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;
}
export function validateLineItemsForSend(items: LineItemInput[]): string | null {
if (items.length === 0) return "Add at least one line item before sending";
return validateLineItems(items);
}