feat: add optional event date and expiration settings

Add global settings to make event_date and expiration optional when
creating galleries. This supports non-event use cases like portraits,
corporate shoots, etc.

New features:
- Settings toggles in Settings → Event Creation tab
- "Require event date" checkbox with warning about random URL identifiers
- "Require expiration date" checkbox with warning about manual archiving
- Galleries without date use random hex suffix in slug (e.g. portrait-smith-a1b2c3)
- Galleries without expiration never expire (stay active until archived)

Backend changes:
- New migration for settings and nullable columns
- Conditional validation based on settings
- Updated slug generation with random suffix fallback
- Updated expiration checker to skip null expires_at
- Updated gallery access control for null expiration

Frontend changes:
- New checkboxes in EventsTab with warnings
- Conditional event date field (shows optional label)
- No Expiration message when expiration disabled
- Updated types for nullable event_date and expires_at

Closes #118
This commit is contained in:
Paul Nothaft
2026-01-16 09:39:32 +01:00
parent 3e69579f5a
commit 2151147f2d
13 changed files with 226 additions and 61 deletions
+40 -26
View File
@@ -148,6 +148,8 @@ export const CreateEventPage: React.FC = () => {
const requireCustomerName = publicSettings?.event_require_customer_name !== false;
const requireCustomerEmail = publicSettings?.event_require_customer_email !== false;
const requireAdminEmail = publicSettings?.event_require_admin_email !== false;
const requireEventDate = publicSettings?.event_require_event_date !== false;
const requireExpiration = publicSettings?.event_require_expiration !== false;
// Update default expiration days when settings are loaded
useEffect(() => {
@@ -201,7 +203,7 @@ export const CreateEventPage: React.FC = () => {
newErrors.event_name = t('validation.eventNameRequired');
}
if (!formData.event_date) {
if (requireEventDate && !formData.event_date) {
newErrors.event_date = t('validation.eventDateRequired');
}
@@ -247,7 +249,7 @@ export const CreateEventPage: React.FC = () => {
}
}
if (formData.expires_in_days < 1 || formData.expires_in_days > 365) {
if (requireExpiration && (formData.expires_in_days < 1 || formData.expires_in_days > 365)) {
newErrors.expires_in_days = t('validation.expirationRange');
}
@@ -267,7 +269,7 @@ export const CreateEventPage: React.FC = () => {
const payload = {
event_type: formData.event_type,
event_name: formData.event_name,
event_date: formData.event_date,
event_date: formData.event_date || undefined,
customer_name: formData.customer_name,
customer_email: formData.customer_email,
admin_email: formData.admin_email,
@@ -275,7 +277,7 @@ export const CreateEventPage: React.FC = () => {
password: formData.require_password ? formData.password : undefined,
welcome_message: formData.welcome_message || '',
color_theme: JSON.stringify(formData.theme_config),
expiration_days: formData.expires_in_days,
expiration_days: requireExpiration ? formData.expires_in_days : undefined,
allow_user_uploads: formData.allow_user_uploads,
upload_category_id: formData.upload_category_id,
css_template_id: formData.css_template_id,
@@ -394,7 +396,7 @@ export const CreateEventPage: React.FC = () => {
<Input
type="date"
label={t('events.eventDate')}
label={requireEventDate ? t('events.eventDate') : `${t('events.eventDate')} (${t('common.optional')})`}
value={formData.event_date}
onChange={handleInputChange('event_date')}
error={errors.event_date}
@@ -666,30 +668,42 @@ export const CreateEventPage: React.FC = () => {
</div>
)}
<div>
<label className="block text-sm font-medium text-neutral-700 mb-2">
{t('events.galleryExpiration')}
</label>
<div className="flex items-center gap-2">
<div className="w-32">
<Input
type="number"
value={formData.expires_in_days}
onChange={handleInputChange('expires_in_days')}
error={errors.expires_in_days}
min={1}
max={365}
leftIcon={<Clock className="w-5 h-5" />}
/>
{requireExpiration ? (
<div>
<label className="block text-sm font-medium text-neutral-700 mb-2">
{t('events.galleryExpiration')}
</label>
<div className="flex items-center gap-2">
<div className="w-32">
<Input
type="number"
value={formData.expires_in_days}
onChange={handleInputChange('expires_in_days')}
error={errors.expires_in_days}
min={1}
max={365}
leftIcon={<Clock className="w-5 h-5" />}
/>
</div>
<span className="text-sm text-neutral-600">{t('events.daysAfterEvent')}</span>
</div>
<span className="text-sm text-neutral-600">{t('events.daysAfterEvent')}</span>
{formData.event_date && (
<p className="mt-2 text-sm text-neutral-500">
{t('events.expiresOn')}: {format(addDays(new Date(formData.event_date), formData.expires_in_days))}
</p>
)}
</div>
{formData.event_date && (
<p className="mt-2 text-sm text-neutral-500">
{t('events.expiresOn')}: {format(addDays(new Date(formData.event_date), formData.expires_in_days))}
) : (
<div className="rounded-md border border-blue-200 bg-blue-50 p-3">
<div className="flex items-center gap-2 text-blue-800">
<Clock className="w-4 h-4" />
<span className="text-sm font-medium">{t('events.noExpiration', 'No Expiration')}</span>
</div>
<p className="mt-1 text-xs text-blue-700">
{t('events.noExpirationHelp', 'This gallery will remain active until manually archived.')}
</p>
)}
</div>
</div>
)}
{/* User Upload Settings */}
<div className="pt-4 border-t border-neutral-200">