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:
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Migration: Add optional event date and expiration settings
|
||||||
|
* These settings control whether event_date and expiration are required
|
||||||
|
* when creating new events, supporting non-event use cases like portraits.
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.up = async function(knex) {
|
||||||
|
// Add new settings for optional date and expiration
|
||||||
|
const settings = [
|
||||||
|
{ setting_key: 'event_require_event_date', setting_value: JSON.stringify(true), setting_type: 'boolean' },
|
||||||
|
{ setting_key: 'event_require_expiration', setting_value: JSON.stringify(true), setting_type: 'boolean' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const setting of settings) {
|
||||||
|
const exists = await knex('app_settings').where('setting_key', setting.setting_key).first();
|
||||||
|
if (!exists) {
|
||||||
|
await knex('app_settings').insert({
|
||||||
|
...setting,
|
||||||
|
updated_at: knex.fn.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make event_date and expires_at columns nullable
|
||||||
|
// PostgreSQL supports ALTER COLUMN ... DROP NOT NULL
|
||||||
|
// SQLite requires table recreation (handled differently)
|
||||||
|
const client = knex.client.config.client;
|
||||||
|
|
||||||
|
if (client === 'pg' || client === 'postgresql') {
|
||||||
|
// PostgreSQL: directly alter columns
|
||||||
|
await knex.raw('ALTER TABLE events ALTER COLUMN event_date DROP NOT NULL');
|
||||||
|
await knex.raw('ALTER TABLE events ALTER COLUMN expires_at DROP NOT NULL');
|
||||||
|
} else if (client === 'sqlite3' || client === 'better-sqlite3') {
|
||||||
|
// SQLite: columns are already effectively nullable in most cases
|
||||||
|
// SQLite doesn't enforce NOT NULL as strictly, and altering requires table recreation
|
||||||
|
// For safety, we'll skip the schema change for SQLite as it's complex
|
||||||
|
// The application logic will handle null values appropriately
|
||||||
|
console.log('SQLite detected - skipping schema alteration (columns will accept NULL values)');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = async function(knex) {
|
||||||
|
// Remove the settings
|
||||||
|
await knex('app_settings')
|
||||||
|
.whereIn('setting_key', [
|
||||||
|
'event_require_event_date',
|
||||||
|
'event_require_expiration'
|
||||||
|
])
|
||||||
|
.del();
|
||||||
|
|
||||||
|
// Note: We don't restore NOT NULL constraints as that could fail
|
||||||
|
// if there are existing NULL values in the database
|
||||||
|
};
|
||||||
@@ -168,8 +168,9 @@ async function galleryAuth(req, res, next) {
|
|||||||
return res.status(404).json({ error: 'Gallery not found or expired' });
|
return res.status(404).json({ error: 'Gallery not found or expired' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if gallery has expired
|
// Check if gallery has expired (only if expires_at is set)
|
||||||
if (new Date(event.expires_at) < new Date()) {
|
// Galleries with null expires_at never expire
|
||||||
|
if (event.expires_at && new Date(event.expires_at) < new Date()) {
|
||||||
return res.status(410).json({
|
return res.status(410).json({
|
||||||
error: 'Gallery has expired',
|
error: 'Gallery has expired',
|
||||||
code: 'GALLERY_EXPIRED'
|
code: 'GALLERY_EXPIRED'
|
||||||
|
|||||||
@@ -25,14 +25,18 @@ const getEventFieldRequirements = async () => {
|
|||||||
.whereIn('setting_key', [
|
.whereIn('setting_key', [
|
||||||
'event_require_customer_name',
|
'event_require_customer_name',
|
||||||
'event_require_customer_email',
|
'event_require_customer_email',
|
||||||
'event_require_admin_email'
|
'event_require_admin_email',
|
||||||
|
'event_require_event_date',
|
||||||
|
'event_require_expiration'
|
||||||
])
|
])
|
||||||
.select('setting_key', 'setting_value');
|
.select('setting_key', 'setting_value');
|
||||||
|
|
||||||
const requirements = {
|
const requirements = {
|
||||||
require_customer_name: true,
|
require_customer_name: true,
|
||||||
require_customer_email: true,
|
require_customer_email: true,
|
||||||
require_admin_email: true
|
require_admin_email: true,
|
||||||
|
require_event_date: true,
|
||||||
|
require_expiration: true
|
||||||
};
|
};
|
||||||
|
|
||||||
settings.forEach(s => {
|
settings.forEach(s => {
|
||||||
@@ -47,6 +51,8 @@ const getEventFieldRequirements = async () => {
|
|||||||
if (s.setting_key === 'event_require_customer_name') requirements.require_customer_name = value;
|
if (s.setting_key === 'event_require_customer_name') requirements.require_customer_name = value;
|
||||||
if (s.setting_key === 'event_require_customer_email') requirements.require_customer_email = value;
|
if (s.setting_key === 'event_require_customer_email') requirements.require_customer_email = value;
|
||||||
if (s.setting_key === 'event_require_admin_email') requirements.require_admin_email = value;
|
if (s.setting_key === 'event_require_admin_email') requirements.require_admin_email = value;
|
||||||
|
if (s.setting_key === 'event_require_event_date') requirements.require_event_date = value;
|
||||||
|
if (s.setting_key === 'event_require_expiration') requirements.require_expiration = value;
|
||||||
});
|
});
|
||||||
|
|
||||||
return requirements;
|
return requirements;
|
||||||
@@ -55,7 +61,9 @@ const getEventFieldRequirements = async () => {
|
|||||||
return {
|
return {
|
||||||
require_customer_name: true,
|
require_customer_name: true,
|
||||||
require_customer_email: true,
|
require_customer_email: true,
|
||||||
require_admin_email: true
|
require_admin_email: true,
|
||||||
|
require_event_date: true,
|
||||||
|
require_expiration: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -106,7 +114,7 @@ const hasCustomerContactColumns = async () => {
|
|||||||
router.post('/', adminAuth, requirePermission('events.create'), [
|
router.post('/', adminAuth, requirePermission('events.create'), [
|
||||||
body('event_type').isIn(['wedding', 'birthday', 'corporate', 'other']),
|
body('event_type').isIn(['wedding', 'birthday', 'corporate', 'other']),
|
||||||
body('event_name').notEmpty().trim(),
|
body('event_name').notEmpty().trim(),
|
||||||
body('event_date').isDate(),
|
body('event_date').optional().isDate(),
|
||||||
body('customer_name').optional().trim(),
|
body('customer_name').optional().trim(),
|
||||||
body('customer_email').optional().isEmail().normalizeEmail(),
|
body('customer_email').optional().isEmail().normalizeEmail(),
|
||||||
body('admin_email').optional().isEmail().normalizeEmail(),
|
body('admin_email').optional().isEmail().normalizeEmail(),
|
||||||
@@ -201,6 +209,9 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
if (fieldRequirements.require_admin_email && !admin_email) {
|
if (fieldRequirements.require_admin_email && !admin_email) {
|
||||||
validationErrors.push({ path: 'admin_email', msg: 'Admin email is required' });
|
validationErrors.push({ path: 'admin_email', msg: 'Admin email is required' });
|
||||||
}
|
}
|
||||||
|
if (fieldRequirements.require_event_date && !event_date) {
|
||||||
|
validationErrors.push({ path: 'event_date', msg: 'Event date is required' });
|
||||||
|
}
|
||||||
|
|
||||||
if (validationErrors.length > 0) {
|
if (validationErrors.length > 0) {
|
||||||
return res.status(400).json({ errors: validationErrors });
|
return res.status(400).json({ errors: validationErrors });
|
||||||
@@ -245,7 +256,10 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
.replace(/[^a-z0-9]/g, '-') // Replace non-alphanumeric with dash
|
.replace(/[^a-z0-9]/g, '-') // Replace non-alphanumeric with dash
|
||||||
.replace(/-+/g, '-') // Replace multiple dashes with single dash
|
.replace(/-+/g, '-') // Replace multiple dashes with single dash
|
||||||
.replace(/^-|-$/g, ''); // Remove leading/trailing dashes
|
.replace(/^-|-$/g, ''); // Remove leading/trailing dashes
|
||||||
const baseSlug = `${event_type}-${processedEventName}-${event_date}`;
|
|
||||||
|
// Use event_date in slug if provided, otherwise use random suffix
|
||||||
|
const slugSuffix = event_date || crypto.randomBytes(3).toString('hex');
|
||||||
|
const baseSlug = `${event_type}-${processedEventName}-${slugSuffix}`;
|
||||||
let slug = baseSlug;
|
let slug = baseSlug;
|
||||||
let counter = 1;
|
let counter = 1;
|
||||||
|
|
||||||
@@ -264,15 +278,20 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
: await bcrypt.hash(crypto.randomBytes(32).toString('hex'), getBcryptRounds());
|
: await bcrypt.hash(crypto.randomBytes(32).toString('hex'), getBcryptRounds());
|
||||||
|
|
||||||
// Calculate expiration date (days after event date)
|
// Calculate expiration date (days after event date)
|
||||||
// Parse YYYY-MM-DD format as local date to avoid timezone issues
|
// If expiration is not required, expires_at will be null (never expires)
|
||||||
let expires_at;
|
// If event_date is not provided, use current date as base for expiration
|
||||||
if (event_date.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
let expires_at = null;
|
||||||
const [year, month, day] = event_date.split('-').map(num => parseInt(num, 10));
|
if (fieldRequirements.require_expiration) {
|
||||||
expires_at = new Date(year, month - 1, day);
|
const baseDate = event_date || new Date().toISOString().split('T')[0];
|
||||||
} else {
|
// Parse YYYY-MM-DD format as local date to avoid timezone issues
|
||||||
expires_at = new Date(event_date);
|
if (baseDate.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
||||||
|
const [year, month, day] = baseDate.split('-').map(num => parseInt(num, 10));
|
||||||
|
expires_at = new Date(year, month - 1, day);
|
||||||
|
} else {
|
||||||
|
expires_at = new Date(baseDate);
|
||||||
|
}
|
||||||
|
expires_at.setDate(expires_at.getDate() + parseInt(expiration_days, 10));
|
||||||
}
|
}
|
||||||
expires_at.setDate(expires_at.getDate() + parseInt(expiration_days, 10));
|
|
||||||
|
|
||||||
// Create folder structure
|
// Create folder structure
|
||||||
const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
|
||||||
@@ -285,7 +304,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
slug,
|
slug,
|
||||||
event_type,
|
event_type,
|
||||||
event_name,
|
event_name,
|
||||||
event_date,
|
event_date: event_date || null,
|
||||||
...(customerColumnsAvailable ? { customer_name: customerName, customer_email: customerEmail } : {}),
|
...(customerColumnsAvailable ? { customer_name: customerName, customer_email: customerEmail } : {}),
|
||||||
host_name: customerName,
|
host_name: customerName,
|
||||||
host_email: customerEmail,
|
host_email: customerEmail,
|
||||||
@@ -295,7 +314,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
color_theme,
|
color_theme,
|
||||||
share_link: shareLinkToStore,
|
share_link: shareLinkToStore,
|
||||||
share_token: shareToken,
|
share_token: shareToken,
|
||||||
expires_at: expires_at.toISOString(),
|
expires_at: expires_at ? expires_at.toISOString() : null,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
created_by: req.admin.id,
|
created_by: req.admin.id,
|
||||||
allow_user_uploads,
|
allow_user_uploads,
|
||||||
@@ -350,7 +369,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
event_date: event_date, // Pass raw date - will be formatted by email processor
|
event_date: event_date, // Pass raw date - will be formatted by email processor
|
||||||
gallery_link: shareUrl,
|
gallery_link: shareUrl,
|
||||||
gallery_password: requirePassword ? password : 'No password required',
|
gallery_password: requirePassword ? password : 'No password required',
|
||||||
expiry_date: expires_at.toISOString(), // Pass ISO string - will be formatted by email processor
|
expiry_date: expires_at ? expires_at.toISOString() : null, // Pass ISO string - will be formatted by email processor
|
||||||
welcome_message: welcome_message || ''
|
welcome_message: welcome_message || ''
|
||||||
}),
|
}),
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
@@ -367,7 +386,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
customer_email: customerEmail,
|
customer_email: customerEmail,
|
||||||
require_password: requirePassword,
|
require_password: requirePassword,
|
||||||
share_link: shareUrl,
|
share_link: shareUrl,
|
||||||
expires_at: expires_at.toISOString(),
|
expires_at: expires_at ? expires_at.toISOString() : null,
|
||||||
created_at: new Date().toISOString()
|
created_at: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ router.get('/:slug/info', async (req, res) => {
|
|||||||
event_date: event.event_date,
|
event_date: event.event_date,
|
||||||
expires_at: event.expires_at,
|
expires_at: event.expires_at,
|
||||||
is_active: event.is_active,
|
is_active: event.is_active,
|
||||||
is_expired: !event.is_active || new Date(event.expires_at) < new Date(),
|
is_expired: !event.is_active || (event.expires_at && new Date(event.expires_at) < new Date()),
|
||||||
requires_password: requiresPassword,
|
requires_password: requiresPassword,
|
||||||
color_theme: event.color_theme,
|
color_theme: event.color_theme,
|
||||||
allow_downloads: !(event.allow_downloads === false || event.allow_downloads === 0 || event.allow_downloads === '0'),
|
allow_downloads: !(event.allow_downloads === false || event.allow_downloads === 0 || event.allow_downloads === '0'),
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ router.get('/', async (req, res) => {
|
|||||||
// Event field requirements
|
// Event field requirements
|
||||||
event_require_customer_name: settingsObject.event_require_customer_name !== false,
|
event_require_customer_name: settingsObject.event_require_customer_name !== false,
|
||||||
event_require_customer_email: settingsObject.event_require_customer_email !== false,
|
event_require_customer_email: settingsObject.event_require_customer_email !== false,
|
||||||
event_require_admin_email: settingsObject.event_require_admin_email !== false
|
event_require_admin_email: settingsObject.event_require_admin_email !== false,
|
||||||
|
event_require_event_date: settingsObject.event_require_event_date !== false,
|
||||||
|
event_require_expiration: settingsObject.event_require_expiration !== false
|
||||||
};
|
};
|
||||||
|
|
||||||
res.json(publicSettings);
|
res.json(publicSettings);
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ async function checkExpirations() {
|
|||||||
const warningDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); // 7 days from now
|
const warningDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); // 7 days from now
|
||||||
|
|
||||||
// Check for events needing warning emails
|
// Check for events needing warning emails
|
||||||
|
// Skip events with null expires_at (they never expire)
|
||||||
const eventsNeedingWarning = await db('events')
|
const eventsNeedingWarning = await db('events')
|
||||||
.where('is_active', formatBoolean(true))
|
.where('is_active', formatBoolean(true))
|
||||||
.where('is_archived', formatBoolean(false))
|
.where('is_archived', formatBoolean(false))
|
||||||
|
.whereNotNull('expires_at')
|
||||||
.where('expires_at', '<=', warningDate)
|
.where('expires_at', '<=', warningDate)
|
||||||
.where('expires_at', '>', now);
|
.where('expires_at', '>', now);
|
||||||
|
|
||||||
@@ -40,9 +42,11 @@ async function checkExpirations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for expired events
|
// Check for expired events
|
||||||
|
// Skip events with null expires_at (they never expire)
|
||||||
const expiredEvents = await db('events')
|
const expiredEvents = await db('events')
|
||||||
.where('is_active', formatBoolean(true))
|
.where('is_active', formatBoolean(true))
|
||||||
.where('is_archived', formatBoolean(false))
|
.where('is_archived', formatBoolean(false))
|
||||||
|
.whereNotNull('expires_at')
|
||||||
.where('expires_at', '<=', now);
|
.where('expires_at', '<=', now);
|
||||||
|
|
||||||
for (const event of expiredEvents) {
|
for (const event of expiredEvents) {
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export interface EventSettings {
|
|||||||
event_require_customer_name: boolean;
|
event_require_customer_name: boolean;
|
||||||
event_require_customer_email: boolean;
|
event_require_customer_email: boolean;
|
||||||
event_require_admin_email: boolean;
|
event_require_admin_email: boolean;
|
||||||
|
event_require_event_date: boolean;
|
||||||
|
event_require_expiration: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSettingsState() {
|
export function useSettingsState() {
|
||||||
@@ -109,7 +111,9 @@ export function useSettingsState() {
|
|||||||
const [eventSettings, setEventSettings] = useState<EventSettings>({
|
const [eventSettings, setEventSettings] = useState<EventSettings>({
|
||||||
event_require_customer_name: true,
|
event_require_customer_name: true,
|
||||||
event_require_customer_email: true,
|
event_require_customer_email: true,
|
||||||
event_require_admin_email: true
|
event_require_admin_email: true,
|
||||||
|
event_require_event_date: true,
|
||||||
|
event_require_expiration: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Account form state
|
// Account form state
|
||||||
@@ -178,7 +182,9 @@ export function useSettingsState() {
|
|||||||
setEventSettings({
|
setEventSettings({
|
||||||
event_require_customer_name: toBoolean(settings.event_require_customer_name, true),
|
event_require_customer_name: toBoolean(settings.event_require_customer_name, true),
|
||||||
event_require_customer_email: toBoolean(settings.event_require_customer_email, true),
|
event_require_customer_email: toBoolean(settings.event_require_customer_email, true),
|
||||||
event_require_admin_email: toBoolean(settings.event_require_admin_email, true)
|
event_require_admin_email: toBoolean(settings.event_require_admin_email, true),
|
||||||
|
event_require_event_date: toBoolean(settings.event_require_event_date, true),
|
||||||
|
event_require_expiration: toBoolean(settings.event_require_expiration, true)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [settings, i18n]);
|
}, [settings, i18n]);
|
||||||
|
|||||||
@@ -99,6 +99,56 @@ export const EventsTab: React.FC<EventsTabProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="flex items-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={eventSettings.event_require_event_date}
|
||||||
|
onChange={(e) => setEventSettings(prev => ({ ...prev, event_require_event_date: e.target.checked }))}
|
||||||
|
className="mt-1 w-4 h-4 text-primary-600 rounded focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-neutral-700">
|
||||||
|
{t('settings.events.requireEventDate', 'Require event date')}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-neutral-500 mt-1">
|
||||||
|
{t('settings.events.requireEventDateHelp', 'Event date must be provided when creating events')}
|
||||||
|
</p>
|
||||||
|
{!eventSettings.event_require_event_date && (
|
||||||
|
<p className="text-xs text-amber-600 mt-1 flex items-center gap-1">
|
||||||
|
<AlertCircle className="w-3 h-3" />
|
||||||
|
{t('settings.events.eventDateWarning', 'Gallery URLs will use random identifiers instead of dates')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="flex items-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={eventSettings.event_require_expiration}
|
||||||
|
onChange={(e) => setEventSettings(prev => ({ ...prev, event_require_expiration: e.target.checked }))}
|
||||||
|
className="mt-1 w-4 h-4 text-primary-600 rounded focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-neutral-700">
|
||||||
|
{t('settings.events.requireExpiration', 'Require expiration date')}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-neutral-500 mt-1">
|
||||||
|
{t('settings.events.requireExpirationHelp', 'Galleries must have an expiration date')}
|
||||||
|
</p>
|
||||||
|
{!eventSettings.event_require_expiration && (
|
||||||
|
<p className="text-xs text-amber-600 mt-1 flex items-center gap-1">
|
||||||
|
<AlertCircle className="w-3 h-3" />
|
||||||
|
{t('settings.events.expirationWarning', 'Galleries without expiration will remain active until manually archived')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
|
|||||||
@@ -759,6 +759,8 @@
|
|||||||
"themeAndStyle": "Design & Stil",
|
"themeAndStyle": "Design & Stil",
|
||||||
"galleryWillExpireOn": "Galerie läuft ab am {{date}}",
|
"galleryWillExpireOn": "Galerie läuft ab am {{date}}",
|
||||||
"expirationWarning": "Gäste erhalten 7 Tage vor Ablauf eine Warn-E-Mail.",
|
"expirationWarning": "Gäste erhalten 7 Tage vor Ablauf eine Warn-E-Mail.",
|
||||||
|
"noExpiration": "Kein Ablaufdatum",
|
||||||
|
"noExpirationHelp": "Diese Galerie bleibt aktiv, bis sie manuell archiviert wird.",
|
||||||
"userUploads": "Benutzer-Upload-Einstellungen",
|
"userUploads": "Benutzer-Upload-Einstellungen",
|
||||||
"allowUserUploads": "Gästen erlauben, Fotos hochzuladen",
|
"allowUserUploads": "Gästen erlauben, Fotos hochzuladen",
|
||||||
"allowUserUploadsHelp": "Ermöglichen Sie Gästen, ihre eigenen Fotos in diese Galerie hochzuladen",
|
"allowUserUploadsHelp": "Ermöglichen Sie Gästen, ihre eigenen Fotos in diese Galerie hochzuladen",
|
||||||
@@ -1048,6 +1050,12 @@
|
|||||||
"requireAdminEmail": "Admin-E-Mail erforderlich",
|
"requireAdminEmail": "Admin-E-Mail erforderlich",
|
||||||
"requireAdminEmailHelp": "Admin-E-Mail muss für neue Veranstaltungen angegeben werden",
|
"requireAdminEmailHelp": "Admin-E-Mail muss für neue Veranstaltungen angegeben werden",
|
||||||
"adminEmailWarning": "Erforderlich für den Erhalt von Veranstaltungsbenachrichtigungen",
|
"adminEmailWarning": "Erforderlich für den Erhalt von Veranstaltungsbenachrichtigungen",
|
||||||
|
"requireEventDate": "Veranstaltungsdatum erforderlich",
|
||||||
|
"requireEventDateHelp": "Veranstaltungsdatum muss beim Erstellen angegeben werden",
|
||||||
|
"eventDateWarning": "Galerie-URLs verwenden zufällige Kennungen anstelle von Daten",
|
||||||
|
"requireExpiration": "Ablaufdatum erforderlich",
|
||||||
|
"requireExpirationHelp": "Galerien müssen ein Ablaufdatum haben",
|
||||||
|
"expirationWarning": "Galerien ohne Ablaufdatum bleiben aktiv, bis sie manuell archiviert werden",
|
||||||
"saveSettings": "Veranstaltungseinstellungen speichern",
|
"saveSettings": "Veranstaltungseinstellungen speichern",
|
||||||
"noteTitle": "Hinweis",
|
"noteTitle": "Hinweis",
|
||||||
"noteText": "Diese Einstellungen betreffen nur die Erstellung neuer Veranstaltungen. Bestehende Veranstaltungen sind nicht betroffen. Standardmäßig sind alle Felder erforderlich."
|
"noteText": "Diese Einstellungen betreffen nur die Erstellung neuer Veranstaltungen. Bestehende Veranstaltungen sind nicht betroffen. Standardmäßig sind alle Felder erforderlich."
|
||||||
|
|||||||
@@ -433,6 +433,8 @@
|
|||||||
"expiresOn": "Expires on",
|
"expiresOn": "Expires on",
|
||||||
"galleryWillExpireOn": "Gallery will expire on {{date}}",
|
"galleryWillExpireOn": "Gallery will expire on {{date}}",
|
||||||
"expirationWarning": "Guests will receive a warning email 7 days before expiration.",
|
"expirationWarning": "Guests will receive a warning email 7 days before expiration.",
|
||||||
|
"noExpiration": "No Expiration",
|
||||||
|
"noExpirationHelp": "This gallery will remain active until manually archived.",
|
||||||
"userUploads": "User Upload Settings",
|
"userUploads": "User Upload Settings",
|
||||||
"allowUserUploads": "Allow guests to upload photos",
|
"allowUserUploads": "Allow guests to upload photos",
|
||||||
"allowUserUploadsHelp": "Enable guests to upload their own photos to this gallery",
|
"allowUserUploadsHelp": "Enable guests to upload their own photos to this gallery",
|
||||||
@@ -753,6 +755,12 @@
|
|||||||
"requireAdminEmail": "Require admin email",
|
"requireAdminEmail": "Require admin email",
|
||||||
"requireAdminEmailHelp": "Admin email must be provided for new events",
|
"requireAdminEmailHelp": "Admin email must be provided for new events",
|
||||||
"adminEmailWarning": "Required for receiving event notifications",
|
"adminEmailWarning": "Required for receiving event notifications",
|
||||||
|
"requireEventDate": "Require event date",
|
||||||
|
"requireEventDateHelp": "Event date must be provided when creating events",
|
||||||
|
"eventDateWarning": "Gallery URLs will use random identifiers instead of dates",
|
||||||
|
"requireExpiration": "Require expiration date",
|
||||||
|
"requireExpirationHelp": "Galleries must have an expiration date",
|
||||||
|
"expirationWarning": "Galleries without expiration will remain active until manually archived",
|
||||||
"saveSettings": "Save Event Settings",
|
"saveSettings": "Save Event Settings",
|
||||||
"noteTitle": "Note",
|
"noteTitle": "Note",
|
||||||
"noteText": "These settings only affect new event creation. Existing events are not affected. Default behavior requires all fields."
|
"noteText": "These settings only affect new event creation. Existing events are not affected. Default behavior requires all fields."
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
const requireCustomerName = publicSettings?.event_require_customer_name !== false;
|
const requireCustomerName = publicSettings?.event_require_customer_name !== false;
|
||||||
const requireCustomerEmail = publicSettings?.event_require_customer_email !== false;
|
const requireCustomerEmail = publicSettings?.event_require_customer_email !== false;
|
||||||
const requireAdminEmail = publicSettings?.event_require_admin_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
|
// Update default expiration days when settings are loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -201,7 +203,7 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
newErrors.event_name = t('validation.eventNameRequired');
|
newErrors.event_name = t('validation.eventNameRequired');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.event_date) {
|
if (requireEventDate && !formData.event_date) {
|
||||||
newErrors.event_date = t('validation.eventDateRequired');
|
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');
|
newErrors.expires_in_days = t('validation.expirationRange');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +269,7 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
event_type: formData.event_type,
|
event_type: formData.event_type,
|
||||||
event_name: formData.event_name,
|
event_name: formData.event_name,
|
||||||
event_date: formData.event_date,
|
event_date: formData.event_date || undefined,
|
||||||
customer_name: formData.customer_name,
|
customer_name: formData.customer_name,
|
||||||
customer_email: formData.customer_email,
|
customer_email: formData.customer_email,
|
||||||
admin_email: formData.admin_email,
|
admin_email: formData.admin_email,
|
||||||
@@ -275,7 +277,7 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
password: formData.require_password ? formData.password : undefined,
|
password: formData.require_password ? formData.password : undefined,
|
||||||
welcome_message: formData.welcome_message || '',
|
welcome_message: formData.welcome_message || '',
|
||||||
color_theme: JSON.stringify(formData.theme_config),
|
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,
|
allow_user_uploads: formData.allow_user_uploads,
|
||||||
upload_category_id: formData.upload_category_id,
|
upload_category_id: formData.upload_category_id,
|
||||||
css_template_id: formData.css_template_id,
|
css_template_id: formData.css_template_id,
|
||||||
@@ -394,7 +396,7 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="date"
|
type="date"
|
||||||
label={t('events.eventDate')}
|
label={requireEventDate ? t('events.eventDate') : `${t('events.eventDate')} (${t('common.optional')})`}
|
||||||
value={formData.event_date}
|
value={formData.event_date}
|
||||||
onChange={handleInputChange('event_date')}
|
onChange={handleInputChange('event_date')}
|
||||||
error={errors.event_date}
|
error={errors.event_date}
|
||||||
@@ -666,30 +668,42 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
{requireExpiration ? (
|
||||||
<label className="block text-sm font-medium text-neutral-700 mb-2">
|
<div>
|
||||||
{t('events.galleryExpiration')}
|
<label className="block text-sm font-medium text-neutral-700 mb-2">
|
||||||
</label>
|
{t('events.galleryExpiration')}
|
||||||
<div className="flex items-center gap-2">
|
</label>
|
||||||
<div className="w-32">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<div className="w-32">
|
||||||
type="number"
|
<Input
|
||||||
value={formData.expires_in_days}
|
type="number"
|
||||||
onChange={handleInputChange('expires_in_days')}
|
value={formData.expires_in_days}
|
||||||
error={errors.expires_in_days}
|
onChange={handleInputChange('expires_in_days')}
|
||||||
min={1}
|
error={errors.expires_in_days}
|
||||||
max={365}
|
min={1}
|
||||||
leftIcon={<Clock className="w-5 h-5" />}
|
max={365}
|
||||||
/>
|
leftIcon={<Clock className="w-5 h-5" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-neutral-600">{t('events.daysAfterEvent')}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{formData.event_date && (
|
) : (
|
||||||
<p className="mt-2 text-sm text-neutral-500">
|
<div className="rounded-md border border-blue-200 bg-blue-50 p-3">
|
||||||
{t('events.expiresOn')}: {format(addDays(new Date(formData.event_date), formData.expires_in_days))}
|
<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>
|
</p>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* User Upload Settings */}
|
{/* User Upload Settings */}
|
||||||
<div className="pt-4 border-t border-neutral-200">
|
<div className="pt-4 border-t border-neutral-200">
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ const normalizeEvent = (event: Event): Event => {
|
|||||||
interface CreateEventData {
|
interface CreateEventData {
|
||||||
event_type: string;
|
event_type: string;
|
||||||
event_name: string;
|
event_name: string;
|
||||||
event_date: string;
|
event_date?: string;
|
||||||
customer_name?: string;
|
customer_name?: string;
|
||||||
customer_email: string;
|
customer_email?: string;
|
||||||
admin_email: string;
|
admin_email?: string;
|
||||||
require_password?: boolean;
|
require_password?: boolean;
|
||||||
password?: string;
|
password?: string;
|
||||||
welcome_message?: string;
|
welcome_message?: string;
|
||||||
color_theme?: string;
|
color_theme?: string;
|
||||||
expiration_days: number;
|
expiration_days?: number;
|
||||||
allow_user_uploads?: boolean;
|
allow_user_uploads?: boolean;
|
||||||
upload_category_id?: number | null;
|
upload_category_id?: number | null;
|
||||||
feedback_enabled?: boolean;
|
feedback_enabled?: boolean;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export interface Event {
|
|||||||
slug: string;
|
slug: string;
|
||||||
event_type: string;
|
event_type: string;
|
||||||
event_name: string;
|
event_name: string;
|
||||||
event_date: string;
|
event_date: string | null;
|
||||||
customer_name?: string;
|
customer_name?: string;
|
||||||
customer_email: string;
|
customer_email: string;
|
||||||
admin_email: string;
|
admin_email: string;
|
||||||
@@ -12,7 +12,7 @@ export interface Event {
|
|||||||
color_theme?: string;
|
color_theme?: string;
|
||||||
share_link: string;
|
share_link: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
expires_at: string;
|
expires_at: string | null;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
is_archived: boolean;
|
is_archived: boolean;
|
||||||
archive_path?: string;
|
archive_path?: string;
|
||||||
@@ -46,8 +46,8 @@ export interface Event {
|
|||||||
export interface GalleryInfo {
|
export interface GalleryInfo {
|
||||||
event_name: string;
|
event_name: string;
|
||||||
event_type: string;
|
event_type: string;
|
||||||
event_date: string;
|
event_date: string | null;
|
||||||
expires_at: string;
|
expires_at: string | null;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
is_expired: boolean;
|
is_expired: boolean;
|
||||||
requires_password?: boolean;
|
requires_password?: boolean;
|
||||||
@@ -97,10 +97,10 @@ export interface GalleryData {
|
|||||||
id: number;
|
id: number;
|
||||||
event_name: string;
|
event_name: string;
|
||||||
event_type: string;
|
event_type: string;
|
||||||
event_date: string;
|
event_date: string | null;
|
||||||
welcome_message?: string;
|
welcome_message?: string;
|
||||||
color_theme?: string;
|
color_theme?: string;
|
||||||
expires_at: string;
|
expires_at: string | null;
|
||||||
allow_user_uploads?: boolean;
|
allow_user_uploads?: boolean;
|
||||||
upload_category_id?: number | null;
|
upload_category_id?: number | null;
|
||||||
hero_photo_id?: number | null;
|
hero_photo_id?: number | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user