feat: add featured event with automatic fallback

- Add featured_event_id to site_settings (schema + migration)
- Backend: featured event logic in /events/next/upcoming with auto-unset when event ends
- Site settings: PUT supports featuredEventId, add PUT /featured-event for admin
- Admin events: Set as featured checkbox in editor, star toggle in list, featured badge
- Admin settings: Featured Event section with current event and remove/change links
- API: siteSettingsApi.setFeaturedEvent(), Event.isFeatured, SiteSettings.featuredEventId
- Homepage/linktree unchanged: still use getNextUpcoming (now returns featured or fallback)
This commit is contained in:
Michilis
2026-02-03 19:24:00 +00:00
parent 0fd8172e04
commit 0c142884c7
9 changed files with 421 additions and 78 deletions

View File

@@ -110,43 +110,12 @@ export default function BookingPage() {
const [errors, setErrors] = useState<Partial<Record<keyof BookingFormData, string>>>({});
// RUC validation using modulo 11 algorithm
const validateRucCheckDigit = (ruc: string): boolean => {
const match = ruc.match(/^(\d{6,8})-(\d)$/);
if (!match) return false;
const baseNumber = match[1];
const checkDigit = parseInt(match[2], 10);
// Modulo 11 algorithm for Paraguayan RUC
const weights = [2, 3, 4, 5, 6, 7, 2, 3];
let sum = 0;
const digits = baseNumber.split('').reverse();
for (let i = 0; i < digits.length; i++) {
sum += parseInt(digits[i], 10) * weights[i];
}
const remainder = sum % 11;
const expectedCheckDigit = remainder < 2 ? 0 : 11 - remainder;
return checkDigit === expectedCheckDigit;
};
const rucPattern = /^\d{6,10}$/;
// Format RUC input: auto-insert hyphen before last digit
// Format RUC input: digits only, max 10
const formatRuc = (value: string): string => {
// Remove non-numeric characters
const digits = value.replace(/\D/g, '');
// Limit to 9 digits (8 base + 1 check)
const limited = digits.slice(0, 9);
// Auto-insert hyphen before last digit if we have more than 6 digits
if (limited.length > 6) {
return `${limited.slice(0, -1)}-${limited.slice(-1)}`;
}
return limited;
const digits = value.replace(/\D/g, '').slice(0, 10);
return digits;
};
// Handle RUC input change
@@ -160,19 +129,12 @@ export default function BookingPage() {
}
};
// Validate RUC on blur
// Validate RUC on blur (optional field: 610 digits)
const handleRucBlur = () => {
if (!formData.ruc) return; // Optional field, no validation if empty
const rucPattern = /^[0-9]{6,8}-[0-9]{1}$/;
if (!rucPattern.test(formData.ruc)) {
if (!formData.ruc) return;
const digits = formData.ruc.replace(/\D/g, '');
if (digits.length > 0 && !rucPattern.test(digits)) {
setErrors({ ...errors, ruc: t('booking.form.errors.rucInvalidFormat') });
return;
}
if (!validateRucCheckDigit(formData.ruc)) {
setErrors({ ...errors, ruc: t('booking.form.errors.rucInvalidCheckDigit') });
}
};
@@ -275,13 +237,11 @@ export default function BookingPage() {
newErrors.phone = t('booking.form.errors.phoneTooShort');
}
// RUC validation (optional field - only validate if filled)
// RUC validation (optional field - 610 digits if filled)
if (formData.ruc.trim()) {
const rucPattern = /^[0-9]{6,8}-[0-9]{1}$/;
if (!rucPattern.test(formData.ruc)) {
const digits = formData.ruc.replace(/\D/g, '');
if (!/^\d{6,10}$/.test(digits)) {
newErrors.ruc = t('booking.form.errors.rucInvalidFormat');
} else if (!validateRucCheckDigit(formData.ruc)) {
newErrors.ruc = t('booking.form.errors.rucInvalidCheckDigit');
}
}
@@ -429,7 +389,7 @@ export default function BookingPage() {
phone: formData.phone,
preferredLanguage: formData.preferredLanguage,
paymentMethod: formData.paymentMethod,
...(formData.ruc.trim() && { ruc: formData.ruc }),
...(formData.ruc.trim() && { ruc: formData.ruc.replace(/\D/g, '') }),
// Include attendees array for multi-ticket bookings
...(allAttendees.length > 1 && { attendees: allAttendees }),
});