Add 'apps/mobile/' from commit '5fa30f365f21531094cd4d2045042bb4f1370ac3'
git-subtree-dir: apps/mobile git-subtree-mainline:86f8987dffgit-subtree-split:5fa30f365f
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user