enhancment complete with phase 10

This commit is contained in:
dimitar committed 2026-03-10 05:37:22 +01:00
1 parent 2c4927478a
commit 6adb541967
44 files changed
+7820 -845

No files matched your search

+224
View File
@@ -0,0 +1,224 @@
import { z, ZodError } from "zod";
/**
* Format Zod validation errors into user-friendly messages
*
* @param error - The ZodError to format
* @returns Object with field paths as keys and error messages as values
*
* @example
* const errors = formatZodErrors(zodError)
* // { "email": ["Invalid email address"], "password": ["Password too short"] }
*/
export function formatZodErrors(error: ZodError): Record<string, string[]> {
const formatted: Record<string, string[]> = {};
for (const issue of error.issues) {
const path = issue.path.join(".") || "root";
if (!formatted[path]) {
formatted[path] = [];
}
formatted[path].push(issue.message);
}
return formatted;
}
/**
* Validate data against a Zod schema
* Throws an error if validation fails
*
* @param schema - The Zod schema to validate against
* @param data - The data to validate
* @returns The validated and typed data
*
* @example
* try {
* const validUser = validateData(userSchema, userData)
* // Use validUser (type-safe)
* } catch (error) {
* console.error('Validation failed:', error.message)
* }
*/
export function validateData<T>(schema: z.ZodSchema<T>, data: unknown): T {
return schema.parse(data);
}
/**
* Safe validation that returns a result object instead of throwing
*
* @param schema - The Zod schema to validate against
* @param data - The data to validate
* @returns Success object with validated data or failure object with errors
*
* @example
* const result = safeValidate(userSchema, userData)
* if (!result.success) {
* Alert.alert('Validation Error', getFirstError(result.errors))
* return
* }
* const { email, password } = result.data
*/
export function safeValidate<T>(
schema: z.ZodSchema<T>,
data: unknown,
):
| { success: true; data: T }
| { success: false; errors: Record<string, string[]> } {
try {
const validData = schema.parse(data);
return { success: true, data: validData };
} catch (error) {
if (error instanceof ZodError) {
return { success: false, errors: formatZodErrors(error) };
}
throw error;
}
}
/**
* Get first error message for display in UI
*
* @param errors - The formatted validation errors
* @returns The first error message found
*
* @example
* const firstError = getFirstError(errors)
* Alert.alert('Error', firstError)
*/
export function getFirstError(errors: Record<string, string[]>): string {
const firstField = Object.keys(errors)[0];
return errors[firstField]?.[0] || "Validation error";
}
/**
* Get all error messages as a flat array
*
* @param errors - The formatted validation errors
* @returns Array of all error messages
*
* @example
* const allErrors = getAllErrors(errors)
* allErrors.forEach(err => console.error(err))
*/
export function getAllErrors(errors: Record<string, string[]>): string[] {
return Object.values(errors).flat();
}
/**
* Get errors for a specific field
*
* @param errors - The formatted validation errors
* @param fieldPath - The field path (e.g., "email", "address.city")
* @returns Array of error messages for the field, or empty array if none
*
* @example
* const emailErrors = getFieldErrors(errors, 'email')
* if (emailErrors.length > 0) {
* setEmailErrorText(emailErrors[0])
* }
*/
export function getFieldErrors(
errors: Record<string, string[]>,
fieldPath: string,
): string[] {
return errors[fieldPath] || [];
}
/**
* Check if a specific field has validation errors
*
* @param errors - The formatted validation errors
* @param fieldPath - The field path to check
* @returns True if the field has errors
*
* @example
* const hasEmailError = hasFieldError(errors, 'email')
* <TextInput error={hasEmailError} />
*/
export function hasFieldError(
errors: Record<string, string[]>,
fieldPath: string,
): boolean {
return fieldPath in errors && errors[fieldPath].length > 0;
}
/**
* Format validation errors for display in a form
* Returns an object suitable for form error state
*
* @param errors - The formatted validation errors
* @returns Object with field paths as keys and first error message as value
*
* @example
* const formErrors = formatFormErrors(errors)
* setErrors(formErrors)
* // { email: "Invalid email address", password: "Password too short" }
*/
export function formatFormErrors(
errors: Record<string, string[]>,
): Record<string, string> {
const formatted: Record<string, string> = {};
for (const [field, messages] of Object.entries(errors)) {
if (messages.length > 0) {
formatted[field] = messages[0];
}
}
return formatted;
}
/**
* Validate form data before submission
* Useful for client-side form validation
*
* @param schema - The Zod schema to validate against
* @param formData - The form data object
* @returns Validation result with formatted errors for forms
*
* @example
* const handleSubmit = () => {
* const result = validateForm(goalSchema, {
* title: goalTitle,
* description: goalDescription,
* targetValue: parseFloat(targetValue),
* })
*
* if (!result.success) {
* setFormErrors(result.formErrors)
* Alert.alert('Validation Error', result.message)
* return
* }
*
* // Submit result.data
* }
*/
export function validateForm<T>(
schema: z.ZodSchema<T>,
formData: unknown,
):
| {
success: true;
data: T;
formErrors?: never;
message?: never;
}
| {
success: false;
data?: never;
formErrors: Record<string, string>;
message: string;
} {
const result = safeValidate(schema, formData);
if (result.success) {
return { success: true, data: result.data };
}
return {
success: false,
formErrors: formatFormErrors(result.errors),
message: getFirstError(result.errors),
};
}
+232
View File
@@ -0,0 +1,232 @@
import { z } from "zod";
// ============================================================================
// Common validation rules
// ============================================================================
export const emailSchema = z.string().email("Invalid email address");
export const passwordSchema = z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
.regex(/[0-9]/, "Password must contain at least one number");
export const phoneSchema = z
.string()
.regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number format")
.optional();
export const dateTimeSchema = z.string().datetime("Invalid datetime format");
// ============================================================================
// User schemas
// ============================================================================
export const userRoleSchema = z.enum([
"client",
"trainer",
"admin",
"superAdmin",
]);
export const userSchema = z.object({
email: emailSchema,
password: passwordSchema,
firstName: z.string().min(1, "First name is required").max(50),
lastName: z.string().min(1, "Last name is required").max(50),
phone: phoneSchema,
role: userRoleSchema.default("client"),
});
export const userUpdateSchema = userSchema.partial();
export const userLoginSchema = z.object({
email: emailSchema,
password: z.string().min(1, "Password is required"),
});
// ============================================================================
// Fitness Goal schemas
// ============================================================================
export const goalTypeSchema = z.enum([
"weight_target",
"strength_milestone",
"endurance_target",
"flexibility_goal",
"habit_building",
"custom",
]);
export const prioritySchema = z.enum(["low", "medium", "high"]);
export const goalStatusSchema = z.enum(["active", "completed", "abandoned"]);
export const fitnessGoalSchema = z.object({
userId: z.string().min(1, "User ID is required"),
goalType: goalTypeSchema,
title: z.string().min(1, "Title is required").max(100),
description: z.string().max(500).optional(),
targetValue: z.number().positive("Target value must be positive").optional(),
currentValue: z
.number()
.nonnegative("Current value cannot be negative")
.optional(),
unit: z.string().max(20).optional(),
targetDate: dateTimeSchema.optional(),
priority: prioritySchema.default("medium"),
status: goalStatusSchema.default("active"),
notes: z.string().max(1000).optional(),
});
export const fitnessGoalUpdateSchema = fitnessGoalSchema.partial().omit({
userId: true,
});
export const fitnessGoalCreateSchema = fitnessGoalSchema.omit({
userId: true,
});
// ============================================================================
// Fitness Profile schemas
// ============================================================================
export const genderSchema = z.enum([
"male",
"female",
"other",
"prefer_not_to_say",
]);
export const activityLevelSchema = z.enum([
"sedentary",
"lightly_active",
"moderately_active",
"very_active",
"extremely_active",
]);
export const fitnessProfileSchema = z.object({
userId: z.string().min(1, "User ID is required"),
height: z
.number()
.positive("Height must be positive")
.max(300, "Invalid height value"),
weight: z
.number()
.positive("Weight must be positive")
.max(500, "Invalid weight value"),
age: z
.number()
.int("Age must be an integer")
.positive("Age must be positive")
.min(13, "Must be at least 13 years old")
.max(120, "Invalid age value"),
gender: genderSchema.optional(),
activityLevel: activityLevelSchema.optional(),
fitnessGoals: z
.array(z.string())
.max(10, "Maximum 10 goals allowed")
.optional(),
medicalConditions: z.string().max(1000).optional(),
allergies: z.string().max(500).optional(),
injuries: z.string().max(500).optional(),
preferences: z.string().max(1000).optional(),
});
export const fitnessProfileUpdateSchema = fitnessProfileSchema.partial().omit({
userId: true,
});
export const fitnessProfileCreateSchema = fitnessProfileSchema.omit({
userId: true,
});
// ============================================================================
// Recommendation schemas
// ============================================================================
export const recommendationTypeSchema = z.enum([
"workout",
"nutrition",
"lifestyle",
"recovery",
]);
export const recommendationStatusSchema = z.enum([
"pending",
"accepted",
"rejected",
"completed",
]);
export const recommendationSchema = z.object({
userId: z.string().min(1, "User ID is required"),
type: recommendationTypeSchema,
title: z.string().min(1, "Title is required").max(200),
description: z.string().min(1, "Description is required").max(2000),
reasoning: z.string().max(1000).optional(),
priority: prioritySchema.default("medium"),
status: recommendationStatusSchema.default("pending"),
metadata: z.record(z.unknown()).optional(),
aiGenerated: z.boolean().default(false),
});
export const recommendationUpdateSchema = recommendationSchema.partial().omit({
userId: true,
});
// ============================================================================
// Attendance schemas
// ============================================================================
export const attendanceSchema = z.object({
userId: z.string().min(1, "User ID is required"),
gymId: z.string().min(1, "Gym ID is required"),
checkInTime: dateTimeSchema,
checkOutTime: dateTimeSchema.optional(),
});
export const checkInSchema = z.object({
userId: z.string().min(1, "User ID is required"),
gymId: z.string().min(1, "Gym ID is required"),
});
export const checkOutSchema = z.object({
userId: z.string().min(1, "User ID is required"),
attendanceId: z.string().min(1, "Attendance ID is required"),
});
// ============================================================================
// Type exports (inferred from schemas)
// ============================================================================
export type User = z.infer<typeof userSchema>;
export type UserUpdate = z.infer<typeof userUpdateSchema>;
export type UserLogin = z.infer<typeof userLoginSchema>;
export type UserRole = z.infer<typeof userRoleSchema>;
export type FitnessGoal = z.infer<typeof fitnessGoalSchema>;
export type FitnessGoalUpdate = z.infer<typeof fitnessGoalUpdateSchema>;
export type FitnessGoalCreate = z.infer<typeof fitnessGoalCreateSchema>;
export type GoalType = z.infer<typeof goalTypeSchema>;
export type GoalStatus = z.infer<typeof goalStatusSchema>;
export type FitnessProfile = z.infer<typeof fitnessProfileSchema>;
export type FitnessProfileUpdate = z.infer<typeof fitnessProfileUpdateSchema>;
export type FitnessProfileCreate = z.infer<typeof fitnessProfileCreateSchema>;
export type Gender = z.infer<typeof genderSchema>;
export type ActivityLevel = z.infer<typeof activityLevelSchema>;
export type Recommendation = z.infer<typeof recommendationSchema>;
export type RecommendationUpdate = z.infer<typeof recommendationUpdateSchema>;
export type RecommendationType = z.infer<typeof recommendationTypeSchema>;
export type RecommendationStatus = z.infer<typeof recommendationStatusSchema>;
export type Attendance = z.infer<typeof attendanceSchema>;
export type CheckIn = z.infer<typeof checkInSchema>;
export type CheckOut = z.infer<typeof checkOutSchema>;
export type Priority = z.infer<typeof prioritySchema>;