diff --git a/RestroHub-FrontEnd/src/components/admin/menu/menuCard/CategoryFormModal.jsx b/RestroHub-FrontEnd/src/components/admin/menu/menuCard/CategoryFormModal.jsx index e1effa26..82013ce7 100644 --- a/RestroHub-FrontEnd/src/components/admin/menu/menuCard/CategoryFormModal.jsx +++ b/RestroHub-FrontEnd/src/components/admin/menu/menuCard/CategoryFormModal.jsx @@ -10,6 +10,7 @@ const CategoryFormModal = ({ isOpen, onClose }) => { }); const [submitting, setSubmitting] = useState(false); + const [fieldErrors, setFieldErrors] = useState({}); const updateField = (field, value) => { setFormData(prev => ({ @@ -20,6 +21,16 @@ const CategoryFormModal = ({ isOpen, onClose }) => { const handleSubmit = async (e) => { e.preventDefault(); + // client-side validation + const errs = {}; + if (!formData.name.trim()) errs.name = 'Category name is required'; + else if (formData.name.trim().length < 2) errs.name = 'Minimum 2 characters'; + + if (Object.keys(errs).length) { + setFieldErrors(errs); + return; + } + try { setSubmitting(true); @@ -82,16 +93,22 @@ const CategoryFormModal = ({ isOpen, onClose }) => { updateField("name", e.target.value)} - className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl + onChange={(e) => { + updateField("name", e.target.value); + setFieldErrors((p) => ({ ...p, name: undefined })); + }} + className={`w-full px-4 py-3 bg-gray-50 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent focus:bg-white outline-none transition-all text-gray-800 - placeholder:text-gray-400" + placeholder:text-gray-400 ${fieldErrors.name ? 'border-red-500' : 'border-gray-200'}`} placeholder="e.g. Starters" - required + aria-required="true" + aria-invalid={fieldErrors.name ? 'true' : 'false'} + aria-describedby={fieldErrors.name ? 'err-cat-name' : undefined} minLength={2} maxLength={50} /> + {fieldErrors.name &&

{fieldErrors.name}

} {/* Description */} diff --git a/RestroHub-FrontEnd/src/components/admin/menu/menuCard/FoodItemFormModal.jsx b/RestroHub-FrontEnd/src/components/admin/menu/menuCard/FoodItemFormModal.jsx index ec410b39..f88cd1b7 100644 --- a/RestroHub-FrontEnd/src/components/admin/menu/menuCard/FoodItemFormModal.jsx +++ b/RestroHub-FrontEnd/src/components/admin/menu/menuCard/FoodItemFormModal.jsx @@ -235,9 +235,10 @@ const MenuFormModal = ({ isOpen, onClose, editingItem, allCategories }) => { placeholder:text-gray-400" placeholder="e.g. Paneer Tikka, Chicken Biryani" required - aria-invalid={Boolean(errors.name)} + aria-invalid={Boolean(errors.name)} + aria-describedby={errors.name ? 'err-food-name' : undefined} /> - {errors.name &&

{errors.name}

} + {errors.name &&

{errors.name}

} {/* Price */} @@ -263,9 +264,10 @@ const MenuFormModal = ({ isOpen, onClose, editingItem, allCategories }) => { placeholder="250.00" required aria-invalid={Boolean(errors.price)} + aria-describedby={errors.price ? 'err-food-price' : undefined} /> - {errors.price &&

{errors.price}

} + {errors.price &&

{errors.price}

} @@ -285,10 +287,11 @@ const MenuFormModal = ({ isOpen, onClose, editingItem, allCategories }) => { placeholder:text-gray-400 resize-none" placeholder="Describe the food item, ingredients, taste..." aria-invalid={Boolean(errors.description)} + aria-describedby={errors.description ? 'err-food-description' : undefined} />
{errors.description ? ( -

{errors.description}

+

{errors.description}

) : ( )} @@ -314,7 +317,8 @@ const MenuFormModal = ({ isOpen, onClose, editingItem, allCategories }) => { focus:border-transparent focus:bg-white outline-none transition-all text-gray-800 appearance-none cursor-pointer" required - aria-invalid={Boolean(errors.categoryId)} + aria-invalid={Boolean(errors.categoryId)} + aria-describedby={errors.categoryId ? 'err-food-category' : undefined} >
- {errors.categoryId &&

{errors.categoryId}

} + {errors.categoryId &&

{errors.categoryId}

} {/* Image Upload */} @@ -378,7 +382,7 @@ const MenuFormModal = ({ isOpen, onClose, editingItem, allCategories }) => { className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" /> - {errors.imageFile &&

{errors.imageFile}

} + {errors.imageFile &&

{errors.imageFile}

} @@ -399,9 +403,10 @@ const MenuFormModal = ({ isOpen, onClose, editingItem, allCategories }) => { placeholder:text-gray-400 disabled:opacity-60" placeholder="https://example.com/food-image.jpg" aria-invalid={Boolean(errors.imageUrl)} + aria-describedby={errors.imageUrl ? 'err-food-imageurl' : undefined} /> {errors.imageUrl ? ( -

{errors.imageUrl}

+

{errors.imageUrl}

) : (

Use a URL when no image file is uploaded.

)} diff --git a/RestroHub-FrontEnd/src/components/admin/profile/profileComponents/PersonalInfoCard.jsx b/RestroHub-FrontEnd/src/components/admin/profile/profileComponents/PersonalInfoCard.jsx index 468bc2a7..6942bc02 100644 --- a/RestroHub-FrontEnd/src/components/admin/profile/profileComponents/PersonalInfoCard.jsx +++ b/RestroHub-FrontEnd/src/components/admin/profile/profileComponents/PersonalInfoCard.jsx @@ -35,6 +35,18 @@ const PersonalInfoCard = ({ profile, onSave }) => { const handleSubmit = async (e) => { e.preventDefault(); + // client-side validation + const errs = {}; + if (!formData.firstName.trim()) errs.firstName = 'First name is required'; + if (!formData.phoneNumber.trim()) errs.phoneNumber = 'Phone number is required'; + else if (!/^\+?[0-9\s-]{7,15}$/.test(formData.phoneNumber)) errs.phoneNumber = 'Enter a valid phone number'; + + if (Object.keys(errs).length) { + setSaving(false); + setFieldErrors(errs); + return; + } + try { setSaving(true); if (onSave) { @@ -81,6 +93,8 @@ const PersonalInfoCard = ({ profile, onSave }) => { const labelClass = 'mb-1.5 block text-sm font-medium text-gray-800'; + const [fieldErrors, setFieldErrors] = useState({}); + // View Mode Row const InfoRow = ({ label, value }) => (
@@ -135,11 +149,18 @@ const PersonalInfoCard = ({ profile, onSave }) => { updateField('firstName', e.target.value)} - className={inputClass} + onChange={(e) => { + updateField('firstName', e.target.value); + setFieldErrors((p) => ({ ...p, firstName: undefined })); + }} + className={`${inputClass} ${fieldErrors.firstName ? 'border-red-500' : 'border-gray-200'}`} placeholder="First name" required + aria-required="true" + aria-invalid={fieldErrors.firstName ? 'true' : 'false'} + aria-describedby={fieldErrors.firstName ? 'err-personal-firstname' : undefined} /> + {fieldErrors.firstName &&

{fieldErrors.firstName}

}
@@ -170,11 +191,18 @@ const PersonalInfoCard = ({ profile, onSave }) => { updateField('phoneNumber', e.target.value)} - className={inputClass} + onChange={(e) => { + updateField('phoneNumber', e.target.value); + setFieldErrors((p) => ({ ...p, phoneNumber: undefined })); + }} + className={`${inputClass} ${fieldErrors.phoneNumber ? 'border-red-500' : 'border-gray-200'}`} placeholder="9876543210" required + aria-required="true" + aria-invalid={fieldErrors.phoneNumber ? 'true' : 'false'} + aria-describedby={fieldErrors.phoneNumber ? 'err-personal-phone' : undefined} /> + {fieldErrors.phoneNumber &&

{fieldErrors.phoneNumber}

}
diff --git a/RestroHub-FrontEnd/src/components/admin/profile/profileComponents/SecurityCard.jsx b/RestroHub-FrontEnd/src/components/admin/profile/profileComponents/SecurityCard.jsx index ef99cddf..8f7cd378 100644 --- a/RestroHub-FrontEnd/src/components/admin/profile/profileComponents/SecurityCard.jsx +++ b/RestroHub-FrontEnd/src/components/admin/profile/profileComponents/SecurityCard.jsx @@ -65,6 +65,9 @@ const SecurityCard = () => { className={`${inputClass} ${error ? 'border-red-300' : 'border-gray-200'}`} placeholder="••••••••" required + aria-required="true" + aria-invalid={error ? 'true' : 'false'} + aria-describedby={error ? `err-${field}` : undefined} /> {error && ( -

+

{error}

)} diff --git a/RestroHub-FrontEnd/src/components/admin/store/branch/BranchFormModal.jsx b/RestroHub-FrontEnd/src/components/admin/store/branch/BranchFormModal.jsx index 3a342a45..a04d824c 100644 --- a/RestroHub-FrontEnd/src/components/admin/store/branch/BranchFormModal.jsx +++ b/RestroHub-FrontEnd/src/components/admin/store/branch/BranchFormModal.jsx @@ -22,6 +22,7 @@ const BranchFormModal = ({ isOpen, onClose, editingBranch, restaurantId }) => { }); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); + const [fieldErrors, setFieldErrors] = useState({}); // ========== POPULATE FORM ON EDIT ========== useEffect(() => { @@ -61,6 +62,20 @@ const BranchFormModal = ({ isOpen, onClose, editingBranch, restaurantId }) => { e.preventDefault(); setSubmitting(true); setError(null); + setFieldErrors({}); + + const errs = {}; + if (!formData.name.trim()) errs.name = 'Branch name is required'; + if (!formData.add1.trim()) errs.add1 = 'Address line 1 is required'; + if (!formData.city.trim()) errs.city = 'City is required'; + if (!formData.state.trim()) errs.state = 'State is required'; + if (!formData.postalCode.trim()) errs.postalCode = 'Postal code is required'; + + if (Object.keys(errs).length) { + setFieldErrors(errs); + setSubmitting(false); + return; + } try { // Build payload matching BranchRequestDTO @@ -161,16 +176,23 @@ const BranchFormModal = ({ isOpen, onClose, editingBranch, restaurantId }) => { updateField('name', e.target.value)} - className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl + onChange={(e) => { + updateField('name', e.target.value); + setFieldErrors((p) => ({ ...p, name: undefined })); + }} + className={`w-full px-4 py-3 bg-gray-50 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent focus:bg-white outline-none transition-all text-gray-800 - placeholder:text-gray-400" + placeholder:text-gray-400 ${fieldErrors.name ? 'border-red-500' : 'border-gray-200'}`} + aria-required="true" + aria-invalid={fieldErrors.name ? 'true' : 'false'} + aria-describedby={fieldErrors.name ? 'err-branch-name' : undefined} placeholder="e.g., Main Branch, Downtown Outlet" required minLength={2} maxLength={100} /> + {fieldErrors.name &&

{fieldErrors.name}

} {/* Description */} @@ -211,15 +233,22 @@ const BranchFormModal = ({ isOpen, onClose, editingBranch, restaurantId }) => { updateField('add1', e.target.value)} - className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl + onChange={(e) => { + updateField('add1', e.target.value); + setFieldErrors((p) => ({ ...p, add1: undefined })); + }} + className={`w-full px-4 py-3 bg-gray-50 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent focus:bg-white outline-none transition-all text-gray-800 - placeholder:text-gray-400" + placeholder:text-gray-400 ${fieldErrors.add1 ? 'border-red-500' : 'border-gray-200'}`} + aria-required="true" + aria-invalid={fieldErrors.add1 ? 'true' : 'false'} + aria-describedby={fieldErrors.add1 ? 'err-branch-add1' : undefined} placeholder="Street address, building number" required maxLength={255} /> + {fieldErrors.add1 &&

{fieldErrors.add1}

} {/* Address Line 2 */} @@ -254,15 +283,22 @@ const BranchFormModal = ({ isOpen, onClose, editingBranch, restaurantId }) => { updateField('city', e.target.value)} - className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl - focus:ring-2 focus:ring-blue-500 focus:border-transparent - focus:bg-white outline-none transition-all text-gray-800 - placeholder:text-gray-400" + onChange={(e) => { + updateField('city', e.target.value); + setFieldErrors((p) => ({ ...p, city: undefined })); + }} + className={`w-full px-4 py-3 bg-gray-50 rounded-xl + focus:ring-2 focus:ring-blue-500 focus:border-transparent + focus:bg-white outline-none transition-all text-gray-800 + placeholder:text-gray-400 ${fieldErrors.city ? 'border-red-500' : 'border-gray-200'}`} + aria-required="true" + aria-invalid={fieldErrors.city ? 'true' : 'false'} + aria-describedby={fieldErrors.city ? 'err-branch-city' : undefined} placeholder="e.g., Mumbai" required maxLength={100} /> + {fieldErrors.city &&

{fieldErrors.city}

} {/* State */} @@ -275,15 +311,22 @@ const BranchFormModal = ({ isOpen, onClose, editingBranch, restaurantId }) => { updateField('state', e.target.value)} - className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl - focus:ring-2 focus:ring-blue-500 focus:border-transparent - focus:bg-white outline-none transition-all text-gray-800 - placeholder:text-gray-400" + onChange={(e) => { + updateField('state', e.target.value); + setFieldErrors((p) => ({ ...p, state: undefined })); + }} + className={`w-full px-4 py-3 bg-gray-50 rounded-xl + focus:ring-2 focus:ring-blue-500 focus:border-transparent + focus:bg-white outline-none transition-all text-gray-800 + placeholder:text-gray-400 ${fieldErrors.state ? 'border-red-500' : 'border-gray-200'}`} + aria-required="true" + aria-invalid={fieldErrors.state ? 'true' : 'false'} + aria-describedby={fieldErrors.state ? 'err-branch-state' : undefined} placeholder="e.g., Maharashtra" required maxLength={100} /> + {fieldErrors.state &&

{fieldErrors.state}

} @@ -299,15 +342,22 @@ const BranchFormModal = ({ isOpen, onClose, editingBranch, restaurantId }) => { updateField('postalCode', e.target.value)} - className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl - focus:ring-2 focus:ring-blue-500 focus:border-transparent - focus:bg-white outline-none transition-all text-gray-800 - placeholder:text-gray-400" + onChange={(e) => { + updateField('postalCode', e.target.value); + setFieldErrors((p) => ({ ...p, postalCode: undefined })); + }} + className={`w-full px-4 py-3 bg-gray-50 rounded-xl + focus:ring-2 focus:ring-blue-500 focus:border-transparent + focus:bg-white outline-none transition-all text-gray-800 + placeholder:text-gray-400 ${fieldErrors.postalCode ? 'border-red-500' : 'border-gray-200'}`} + aria-required="true" + aria-invalid={fieldErrors.postalCode ? 'true' : 'false'} + aria-describedby={fieldErrors.postalCode ? 'err-branch-postal' : undefined} placeholder="e.g., 400001" required maxLength={20} /> + {fieldErrors.postalCode &&

{fieldErrors.postalCode}

} {/* Country */} diff --git a/RestroHub-FrontEnd/src/components/admin/store/tables/TableFormModal.jsx b/RestroHub-FrontEnd/src/components/admin/store/tables/TableFormModal.jsx index c44c2beb..3fa35e72 100644 --- a/RestroHub-FrontEnd/src/components/admin/store/tables/TableFormModal.jsx +++ b/RestroHub-FrontEnd/src/components/admin/store/tables/TableFormModal.jsx @@ -12,6 +12,7 @@ const TableFormModal = ({ isOpen, onClose, onSaved, branchId, editingTable }) => }); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); + const [fieldErrors, setFieldErrors] = useState({}); useEffect(() => { if (editingTable) { @@ -31,6 +32,18 @@ const TableFormModal = ({ isOpen, onClose, onSaved, branchId, editingTable }) => e.preventDefault(); setSubmitting(true); setError(null); + setFieldErrors({}); + + const errs = {}; + if (!formData.number) errs.number = 'Table number is required'; + if (!formData.capacity) errs.capacity = 'Seating capacity is required'; + else if (Number(formData.capacity) <= 0) errs.capacity = 'Enter a valid capacity'; + + if (Object.keys(errs).length) { + setFieldErrors(errs); + setSubmitting(false); + return; + } try { const payload = { @@ -113,11 +126,17 @@ const TableFormModal = ({ isOpen, onClose, onSaved, branchId, editingTable }) => type="number" min="1" value={formData.number} - onChange={(e) => updateField('number', e.target.value)} - className={inputClass} + onChange={(e) => { + updateField('number', e.target.value); + setFieldErrors((p) => ({ ...p, number: undefined })); + }} + className={`${inputClass} ${fieldErrors.number ? 'border-red-500' : ''}`} placeholder="9" - required + aria-required="true" + aria-invalid={fieldErrors.number ? 'true' : 'false'} + aria-describedby={fieldErrors.number ? 'err-table-number' : undefined} /> + {fieldErrors.number &&

{fieldErrors.number}

}
{/* UPI ID */} @@ -89,13 +106,17 @@ const UPIFormModal = ({ isOpen, onClose }) => { - setFormData({ ...formData, upiId: e.target.value }) - } - className={inputClass} + onChange={(e) => { + setFormData({ ...formData, upiId: e.target.value }); + setFieldErrors((p) => ({ ...p, upiId: undefined })); + }} + className={`${fieldErrors.upiId ? 'border-red-500' : 'border-gray-200'} ${inputClass}`} placeholder="yourname@paytm" - required + aria-required="true" + aria-invalid={fieldErrors.upiId ? 'true' : 'false'} + aria-describedby={fieldErrors.upiId ? 'err-upi-id' : undefined} /> + {fieldErrors.upiId &&

{fieldErrors.upiId}

}

Enter your UPI ID from Paytm, PhonePe, GPay, etc. diff --git a/RestroHub-FrontEnd/src/components/customer/ReservationsSection.jsx b/RestroHub-FrontEnd/src/components/customer/ReservationsSection.jsx index c9f640fd..9bb9c1e3 100644 --- a/RestroHub-FrontEnd/src/components/customer/ReservationsSection.jsx +++ b/RestroHub-FrontEnd/src/components/customer/ReservationsSection.jsx @@ -22,6 +22,7 @@ const ReservationsSection = () => { const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(null); const [confirmationNumber, setConfirmationNumber] = useState(null); + const [fieldErrors, setFieldErrors] = useState({}); if (!siteData) return null; @@ -37,6 +38,26 @@ const ReservationsSection = () => { e.preventDefault(); setSubmitting(true); setError(null); + setFieldErrors({}); + + // Client-side validation + const errors = {}; + if (!formData.name.trim()) errors.name = 'Name is required'; + if (!formData.email.trim()) errors.email = 'Email is required'; + else if (!/^\S+@\S+\.\S+$/.test(formData.email)) errors.email = 'Enter a valid email address'; + if (!formData.phone.trim()) errors.phone = 'Phone number is required'; + else if (!/^\+?[0-9\s-]{7,15}$/.test(formData.phone)) errors.phone = 'Enter a valid phone number'; + if (!formData.date) errors.date = 'Select a date'; + else if (new Date(formData.date) < new Date(getMinDate())) errors.date = 'Date cannot be in the past'; + if (!formData.time) errors.time = 'Select a time slot'; + if (!formData.guests) errors.guests = 'Select number of guests'; + + if (Object.keys(errors).length) { + setFieldErrors(errors); + setError('Please fix the highlighted fields and try again.'); + setSubmitting(false); + return; + } try { const response = await ApiService.submitReservation(formData); @@ -53,6 +74,7 @@ const ReservationsSection = () => { guests: '', requests: '' }); + setFieldErrors({}); } } catch (err) { setError('Failed to submit reservation. Please try again.'); @@ -123,60 +145,117 @@ const ReservationsSection = () => { )}

- - +
+ + {fieldErrors.name && ( +

{fieldErrors.name}

+ )} +
+ +
+ + {fieldErrors.email && ( +

{fieldErrors.email}

+ )} +
+
+ +
+
+ + {fieldErrors.phone && ( +

{fieldErrors.phone}

+ )} +
- - - +
+ + {fieldErrors.date && ( +

{fieldErrors.date}

+ )} +
+ +
+ + {fieldErrors.time && ( +

{fieldErrors.time}

+ )} +
+ +
+ + {fieldErrors.guests && ( +

{fieldErrors.guests}

+ )} +