Booking flow: required terms and privacy checkbox with i18n
Also includes admin, dashboard, and API updates; PWA icon assets; and assorted layout and utility changes on dev.
This commit is contained in:
@@ -110,6 +110,10 @@ export default function BookingPage() {
|
||||
|
||||
const [errors, setErrors] = useState<Partial<Record<keyof BookingFormData, string>>>({});
|
||||
|
||||
// Terms & Privacy agreement (not persisted across page loads)
|
||||
const [agreedToTerms, setAgreedToTerms] = useState(false);
|
||||
const [termsError, setTermsError] = useState<string | null>(null);
|
||||
|
||||
const rucPattern = /^\d{6,10}$/;
|
||||
|
||||
// Format RUC input: digits only, max 10
|
||||
@@ -217,6 +221,13 @@ export default function BookingPage() {
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// Clear the terms error as soon as the user agrees
|
||||
useEffect(() => {
|
||||
if (agreedToTerms && termsError) {
|
||||
setTermsError(null);
|
||||
}
|
||||
}, [agreedToTerms, termsError]);
|
||||
|
||||
const formatDate = (dateStr: string) => formatDateLong(dateStr, locale as 'en' | 'es');
|
||||
const fmtTime = (dateStr: string) => formatTime(dateStr, locale as 'en' | 'es');
|
||||
|
||||
@@ -261,7 +272,20 @@ export default function BookingPage() {
|
||||
|
||||
setErrors(newErrors);
|
||||
setAttendeeErrors(newAttendeeErrors);
|
||||
return Object.keys(newErrors).length === 0 && Object.keys(newAttendeeErrors).length === 0;
|
||||
|
||||
let termsOk = true;
|
||||
if (!agreedToTerms) {
|
||||
setTermsError(t('booking.form.errors.termsRequired'));
|
||||
termsOk = false;
|
||||
} else {
|
||||
setTermsError(null);
|
||||
}
|
||||
|
||||
return (
|
||||
Object.keys(newErrors).length === 0 &&
|
||||
Object.keys(newAttendeeErrors).length === 0 &&
|
||||
termsOk
|
||||
);
|
||||
};
|
||||
|
||||
// Connect to SSE for real-time payment updates
|
||||
@@ -376,6 +400,10 @@ export default function BookingPage() {
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!agreedToTerms) {
|
||||
setTermsError(t('booking.form.errors.termsRequired'));
|
||||
return;
|
||||
}
|
||||
if (!event || !validateForm()) return;
|
||||
|
||||
setSubmitting(true);
|
||||
@@ -1323,13 +1351,58 @@ export default function BookingPage() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Terms & Privacy agreement */}
|
||||
<Card className="mb-6 p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
id="booking-terms-agree"
|
||||
type="checkbox"
|
||||
checked={agreedToTerms}
|
||||
onChange={(e) => setAgreedToTerms(e.target.checked)}
|
||||
aria-required="true"
|
||||
aria-invalid={termsError ? true : undefined}
|
||||
aria-describedby={termsError ? 'booking-terms-error' : undefined}
|
||||
className="h-5 w-5 mt-0.5 flex-shrink-0 accent-primary-yellow rounded focus:outline-none focus:ring-2 focus:ring-primary-yellow focus:ring-offset-2 cursor-pointer"
|
||||
/>
|
||||
<label
|
||||
htmlFor="booking-terms-agree"
|
||||
className="text-sm text-gray-500 leading-relaxed cursor-pointer select-none"
|
||||
>
|
||||
{t('booking.form.termsAgreePart1')}
|
||||
<Link
|
||||
href="/legal/terms-policy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-secondary-blue hover:text-brand-navy underline"
|
||||
>
|
||||
{t('booking.form.termsOfService')}
|
||||
</Link>
|
||||
{t('booking.form.termsAgreePart2')}
|
||||
<Link
|
||||
href="/legal/privacy-policy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-secondary-blue hover:text-brand-navy underline"
|
||||
>
|
||||
{t('booking.form.privacyPolicy')}
|
||||
</Link>
|
||||
{t('booking.form.termsAgreePart3')}
|
||||
</label>
|
||||
</div>
|
||||
{termsError && (
|
||||
<p id="booking-terms-error" className="mt-1.5 text-sm text-red-600">
|
||||
{termsError}
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
className="w-full"
|
||||
isLoading={submitting}
|
||||
disabled={paymentMethods.length === 0}
|
||||
disabled={paymentMethods.length === 0 || !agreedToTerms}
|
||||
>
|
||||
{formData.paymentMethod === 'cash'
|
||||
? t('booking.form.reserveSpot')
|
||||
@@ -1338,10 +1411,6 @@ export default function BookingPage() {
|
||||
: locale === 'es' ? 'Continuar al Pago' : 'Continue to Payment'
|
||||
}
|
||||
</Button>
|
||||
|
||||
<p className="text-center text-sm text-gray-500 mt-4">
|
||||
{t('booking.form.termsNote')}
|
||||
</p>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user