feat: optional customer phone field gated by global toggle (#322)
Adds a `customer_phone` column on events plus an `event_phone_field_enabled` admin setting (default off) that surfaces the input in the create-event and event-detail forms. Designed for downstream automation tooling — once exposed via the upcoming public API, n8n / similar can pick it up to deliver gallery links over WhatsApp, SMS, etc. - Migration 080 adds the column + seeds the setting as false. Existing deployments see no UI change unless the admin opts in via Settings → Events. - Backend strips the field server-side when the toggle is off (defence in depth against form bypass). - Frontend renders the input only when the public-settings flag is true; always optional even then. - publicSettings + EventSettings types extended; CreateEventPage and EventDetailsPage wired to read the toggle and submit the value.
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
const { addColumnIfNotExists } = require('../helpers');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* #322 — optional phone-number field on events. Off by default; surfaced
|
||||||
|
* only when the global `event_phone_field_enabled` app setting is true,
|
||||||
|
* so existing deployments see no UI change unless the admin opts in.
|
||||||
|
*/
|
||||||
|
exports.up = async function up(knex) {
|
||||||
|
await addColumnIfNotExists(knex, 'events', 'customer_phone', (table) => {
|
||||||
|
table.string('customer_phone', 32).nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed the global enable flag (default false).
|
||||||
|
const exists = await knex('app_settings')
|
||||||
|
.where('setting_key', 'event_phone_field_enabled')
|
||||||
|
.first();
|
||||||
|
if (!exists) {
|
||||||
|
await knex('app_settings').insert({
|
||||||
|
setting_key: 'event_phone_field_enabled',
|
||||||
|
setting_value: JSON.stringify(false),
|
||||||
|
setting_type: 'boolean'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = async function down(knex) {
|
||||||
|
if (await knex.schema.hasColumn('events', 'customer_phone')) {
|
||||||
|
await knex.schema.alterTable('events', (table) => {
|
||||||
|
table.dropColumn('customer_phone');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await knex('app_settings').where('setting_key', 'event_phone_field_enabled').delete();
|
||||||
|
};
|
||||||
@@ -185,6 +185,25 @@ const getBrandingDefaults = async () => {
|
|||||||
// Use parseStringInput from shared parsers for customer data extraction
|
// Use parseStringInput from shared parsers for customer data extraction
|
||||||
const getCustomerNameFromPayload = (payload = {}) => parseStringInput(payload.customer_name);
|
const getCustomerNameFromPayload = (payload = {}) => parseStringInput(payload.customer_name);
|
||||||
const getCustomerEmailFromPayload = (payload = {}) => parseStringInput(payload.customer_email);
|
const getCustomerEmailFromPayload = (payload = {}) => parseStringInput(payload.customer_email);
|
||||||
|
const getCustomerPhoneFromPayload = (payload = {}) => parseStringInput(payload.customer_phone);
|
||||||
|
|
||||||
|
// Whether the global "phone field" toggle (#322) is enabled. Cached for
|
||||||
|
// the request via a module-level read; drift is acceptable since this
|
||||||
|
// only governs whether to persist the field, not security boundaries.
|
||||||
|
const isPhoneFieldEnabled = async () => {
|
||||||
|
try {
|
||||||
|
const row = await db('app_settings').where('setting_key', 'event_phone_field_enabled').first();
|
||||||
|
if (!row) return false;
|
||||||
|
let value = row.setting_value;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
try { value = JSON.parse(value); } catch { /* keep raw */ }
|
||||||
|
}
|
||||||
|
return value === true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('Failed to read event_phone_field_enabled', { error: error.message });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const mapEventForApi = (event) => {
|
const mapEventForApi = (event) => {
|
||||||
if (!event || typeof event !== 'object') {
|
if (!event || typeof event !== 'object') {
|
||||||
@@ -196,6 +215,7 @@ const mapEventForApi = (event) => {
|
|||||||
host_email,
|
host_email,
|
||||||
customer_name,
|
customer_name,
|
||||||
customer_email,
|
customer_email,
|
||||||
|
customer_phone,
|
||||||
password_hash: _ph,
|
password_hash: _ph,
|
||||||
client_password_hash: _cph,
|
client_password_hash: _cph,
|
||||||
...rest
|
...rest
|
||||||
@@ -204,7 +224,8 @@ const mapEventForApi = (event) => {
|
|||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
customer_name: customer_name ?? host_name ?? null,
|
customer_name: customer_name ?? host_name ?? null,
|
||||||
customer_email: customer_email ?? host_email ?? null
|
customer_email: customer_email ?? host_email ?? null,
|
||||||
|
customer_phone: customer_phone ?? null
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -239,6 +260,9 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
body('event_date').optional({ values: 'falsy' }).isDate(),
|
body('event_date').optional({ values: 'falsy' }).isDate(),
|
||||||
body('customer_name').optional().trim(),
|
body('customer_name').optional().trim(),
|
||||||
body('customer_email').optional({ values: 'falsy' }).isEmail().normalizeEmail(),
|
body('customer_email').optional({ values: 'falsy' }).isEmail().normalizeEmail(),
|
||||||
|
body('customer_phone').optional({ nullable: true, checkFalsy: true })
|
||||||
|
.isString().trim()
|
||||||
|
.isLength({ max: 32 }).withMessage('Phone number must be at most 32 characters'),
|
||||||
body('admin_email').optional({ values: 'falsy' }).isEmail().normalizeEmail(),
|
body('admin_email').optional({ values: 'falsy' }).isEmail().normalizeEmail(),
|
||||||
body('require_password').optional().isBoolean(),
|
body('require_password').optional().isBoolean(),
|
||||||
body('password').optional().isString().custom((value, { req }) => {
|
body('password').optional().isString().custom((value, { req }) => {
|
||||||
@@ -354,6 +378,11 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
|
|
||||||
const customerName = getCustomerNameFromPayload(req.body);
|
const customerName = getCustomerNameFromPayload(req.body);
|
||||||
const customerEmail = getCustomerEmailFromPayload(req.body);
|
const customerEmail = getCustomerEmailFromPayload(req.body);
|
||||||
|
// Phone field is opt-in via the global setting (#322). If disabled,
|
||||||
|
// ignore whatever the client posted — defence in depth against form
|
||||||
|
// bypass.
|
||||||
|
const phoneEnabled = await isPhoneFieldEnabled();
|
||||||
|
const customerPhone = phoneEnabled ? getCustomerPhoneFromPayload(req.body) : null;
|
||||||
|
|
||||||
const customerColumnsAvailable = await hasCustomerContactColumns();
|
const customerColumnsAvailable = await hasCustomerContactColumns();
|
||||||
|
|
||||||
@@ -509,6 +538,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
event_name,
|
event_name,
|
||||||
event_date: event_date || null,
|
event_date: event_date || null,
|
||||||
...(customerColumnsAvailable ? { customer_name: customerName, customer_email: customerEmail } : {}),
|
...(customerColumnsAvailable ? { customer_name: customerName, customer_email: customerEmail } : {}),
|
||||||
|
...(customerPhone ? { customer_phone: customerPhone } : {}),
|
||||||
host_name: customerName || null,
|
host_name: customerName || null,
|
||||||
host_email: customerEmail || null,
|
host_email: customerEmail || null,
|
||||||
admin_email: admin_email || null,
|
admin_email: admin_email || null,
|
||||||
@@ -863,6 +893,9 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), requireEventOwne
|
|||||||
body('allow_user_uploads').optional().isBoolean(),
|
body('allow_user_uploads').optional().isBoolean(),
|
||||||
body('customer_name').optional({ nullable: true, checkFalsy: true }).trim(),
|
body('customer_name').optional({ nullable: true, checkFalsy: true }).trim(),
|
||||||
body('customer_email').optional().isEmail().normalizeEmail(),
|
body('customer_email').optional().isEmail().normalizeEmail(),
|
||||||
|
body('customer_phone').optional({ nullable: true, checkFalsy: true })
|
||||||
|
.isString().trim()
|
||||||
|
.isLength({ max: 32 }).withMessage('Phone number must be at most 32 characters'),
|
||||||
body('upload_category_id').optional().custom((value) => {
|
body('upload_category_id').optional().custom((value) => {
|
||||||
// Accept null, undefined, or integer values
|
// Accept null, undefined, or integer values
|
||||||
if (value === null || value === undefined) return true;
|
if (value === null || value === undefined) return true;
|
||||||
@@ -961,6 +994,19 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), requireEventOwne
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phone is gated on the global toggle (#322). Strip from the update
|
||||||
|
// unconditionally if disabled — even null/clear is rejected so an
|
||||||
|
// admin can't accidentally write to a field they've turned off.
|
||||||
|
if (Object.prototype.hasOwnProperty.call(updates, 'customer_phone')) {
|
||||||
|
const phoneEnabled = await isPhoneFieldEnabled();
|
||||||
|
if (!phoneEnabled) {
|
||||||
|
delete updates.customer_phone;
|
||||||
|
} else {
|
||||||
|
const nextPhone = getCustomerPhoneFromPayload(updates);
|
||||||
|
updates.customer_phone = nextPhone || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const hasRequirePasswordUpdate = Object.prototype.hasOwnProperty.call(updates, 'require_password');
|
const hasRequirePasswordUpdate = Object.prototype.hasOwnProperty.call(updates, 'require_password');
|
||||||
let requirePasswordUpdate;
|
let requirePasswordUpdate;
|
||||||
if (hasRequirePasswordUpdate) {
|
if (hasRequirePasswordUpdate) {
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ router.get('/', async (req, res) => {
|
|||||||
.orWhereIn('setting_key', [
|
.orWhereIn('setting_key', [
|
||||||
'seo_meta_noindex', 'seo_meta_nofollow', 'seo_meta_noai',
|
'seo_meta_noindex', 'seo_meta_nofollow', 'seo_meta_noai',
|
||||||
'event_default_require_password',
|
'event_default_require_password',
|
||||||
'gallery_show_filter_bar'
|
'gallery_show_filter_bar',
|
||||||
|
'event_phone_field_enabled'
|
||||||
]);
|
]);
|
||||||
})
|
})
|
||||||
.select('setting_key', 'setting_value');
|
.select('setting_key', 'setting_value');
|
||||||
@@ -84,6 +85,8 @@ router.get('/', async (req, res) => {
|
|||||||
event_require_expiration: settingsObject.event_require_expiration !== false,
|
event_require_expiration: settingsObject.event_require_expiration !== false,
|
||||||
// Default value for "Require password" toggle in event creation form
|
// Default value for "Require password" toggle in event creation form
|
||||||
event_default_require_password: settingsObject.event_default_require_password !== false,
|
event_default_require_password: settingsObject.event_default_require_password !== false,
|
||||||
|
// Phone-number field on events is opt-in (#322).
|
||||||
|
event_phone_field_enabled: settingsObject.event_phone_field_enabled === true,
|
||||||
// Whether to show the search/sort filter bar in public galleries (default: true)
|
// Whether to show the search/sort filter bar in public galleries (default: true)
|
||||||
gallery_show_filter_bar: settingsObject.gallery_show_filter_bar !== false,
|
gallery_show_filter_bar: settingsObject.gallery_show_filter_bar !== false,
|
||||||
// Upload settings (safe to expose - needed for client-side validation)
|
// Upload settings (safe to expose - needed for client-side validation)
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export interface EventSettings {
|
|||||||
event_require_expiration: boolean;
|
event_require_expiration: boolean;
|
||||||
event_default_require_password: boolean;
|
event_default_require_password: boolean;
|
||||||
gallery_show_filter_bar: boolean;
|
gallery_show_filter_bar: boolean;
|
||||||
|
event_phone_field_enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SeoSettings {
|
export interface SeoSettings {
|
||||||
@@ -127,7 +128,8 @@ export function useSettingsState() {
|
|||||||
event_require_event_date: true,
|
event_require_event_date: true,
|
||||||
event_require_expiration: true,
|
event_require_expiration: true,
|
||||||
event_default_require_password: true,
|
event_default_require_password: true,
|
||||||
gallery_show_filter_bar: true
|
gallery_show_filter_bar: true,
|
||||||
|
event_phone_field_enabled: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// SEO settings state
|
// SEO settings state
|
||||||
@@ -212,7 +214,8 @@ export function useSettingsState() {
|
|||||||
event_require_event_date: toBoolean(settings.event_require_event_date, true),
|
event_require_event_date: toBoolean(settings.event_require_event_date, true),
|
||||||
event_require_expiration: toBoolean(settings.event_require_expiration, true),
|
event_require_expiration: toBoolean(settings.event_require_expiration, true),
|
||||||
event_default_require_password: toBoolean(settings.event_default_require_password, true),
|
event_default_require_password: toBoolean(settings.event_default_require_password, true),
|
||||||
gallery_show_filter_bar: toBoolean(settings.gallery_show_filter_bar, true)
|
gallery_show_filter_bar: toBoolean(settings.gallery_show_filter_bar, true),
|
||||||
|
event_phone_field_enabled: toBoolean(settings.event_phone_field_enabled, false)
|
||||||
});
|
});
|
||||||
|
|
||||||
setSeoSettings({
|
setSeoSettings({
|
||||||
|
|||||||
@@ -187,6 +187,25 @@ 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_phone_field_enabled}
|
||||||
|
onChange={(e) => setEventSettings(prev => ({ ...prev, event_phone_field_enabled: 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 dark:text-neutral-300">
|
||||||
|
{t('settings.events.enablePhoneField', 'Enable phone number field')}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
|
||||||
|
{t('settings.events.enablePhoneFieldHelp', 'Adds an optional phone number input to the event form. Useful for downstream automations like WhatsApp delivery via n8n. Always optional even when enabled.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ interface FormData {
|
|||||||
event_date: string;
|
event_date: string;
|
||||||
customer_name: string;
|
customer_name: string;
|
||||||
customer_email: string;
|
customer_email: string;
|
||||||
|
customer_phone: string;
|
||||||
admin_email: string;
|
admin_email: string;
|
||||||
require_password: boolean;
|
require_password: boolean;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -95,6 +96,7 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
event_date: new Date().toISOString().split('T')[0], // Initialize with ISO date format
|
event_date: new Date().toISOString().split('T')[0], // Initialize with ISO date format
|
||||||
customer_name: '',
|
customer_name: '',
|
||||||
customer_email: '',
|
customer_email: '',
|
||||||
|
customer_phone: '',
|
||||||
admin_email: '',
|
admin_email: '',
|
||||||
require_password: true,
|
require_password: true,
|
||||||
password: '',
|
password: '',
|
||||||
@@ -177,6 +179,7 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
// Get field requirements (default to true if not set)
|
// Get field requirements (default to true if not set)
|
||||||
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 phoneFieldEnabled = publicSettings?.event_phone_field_enabled === true;
|
||||||
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 requireEventDate = publicSettings?.event_require_event_date !== false;
|
||||||
const requireExpiration = publicSettings?.event_require_expiration !== false;
|
const requireExpiration = publicSettings?.event_require_expiration !== false;
|
||||||
@@ -351,6 +354,7 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
event_date: formData.event_date || undefined,
|
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,
|
||||||
|
...(phoneFieldEnabled && formData.customer_phone ? { customer_phone: formData.customer_phone.trim() } : {}),
|
||||||
admin_email: formData.admin_email,
|
admin_email: formData.admin_email,
|
||||||
require_password: formData.require_password,
|
require_password: formData.require_password,
|
||||||
password: formData.require_password ? formData.password : undefined,
|
password: formData.require_password ? formData.password : undefined,
|
||||||
@@ -654,6 +658,16 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{phoneFieldEnabled && (
|
||||||
|
<Input
|
||||||
|
type="tel"
|
||||||
|
label={`${t('events.customerPhone', 'Customer Phone')} (${t('common.optional')})`}
|
||||||
|
placeholder={t('events.customerPhonePlaceholder', '+1 555 555 1234')}
|
||||||
|
value={formData.customer_phone}
|
||||||
|
onChange={handleInputChange('customer_phone')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
label={requireAdminEmail ? t('events.adminEmail') : `${t('events.adminEmail')} (${t('common.optional')})`}
|
label={requireAdminEmail ? t('events.adminEmail') : `${t('events.adminEmail')} (${t('common.optional')})`}
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
hero_photo_id: number | null;
|
hero_photo_id: number | null;
|
||||||
customer_name: string;
|
customer_name: string;
|
||||||
customer_email: string;
|
customer_email: string;
|
||||||
|
customer_phone: string;
|
||||||
source_mode: 'managed' | 'reference';
|
source_mode: 'managed' | 'reference';
|
||||||
external_path: string;
|
external_path: string;
|
||||||
require_password: boolean;
|
require_password: boolean;
|
||||||
@@ -184,6 +185,7 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
hero_photo_id: null,
|
hero_photo_id: null,
|
||||||
customer_name: '',
|
customer_name: '',
|
||||||
customer_email: '',
|
customer_email: '',
|
||||||
|
customer_phone: '',
|
||||||
source_mode: 'managed',
|
source_mode: 'managed',
|
||||||
external_path: '',
|
external_path: '',
|
||||||
require_password: true,
|
require_password: true,
|
||||||
@@ -338,6 +340,7 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
queryFn: () => publicSettingsService.getPublicSettings(),
|
queryFn: () => publicSettingsService.getPublicSettings(),
|
||||||
});
|
});
|
||||||
const requireExpiration = publicSettings?.event_require_expiration !== false;
|
const requireExpiration = publicSettings?.event_require_expiration !== false;
|
||||||
|
const phoneFieldEnabled = publicSettings?.event_phone_field_enabled === true;
|
||||||
|
|
||||||
// Fetch categories for the event
|
// Fetch categories for the event
|
||||||
const { data: categories = [] } = useQuery({
|
const { data: categories = [] } = useQuery({
|
||||||
@@ -430,6 +433,7 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
hero_photo_id: event.hero_photo_id || null,
|
hero_photo_id: event.hero_photo_id || null,
|
||||||
customer_name: event.customer_name || '',
|
customer_name: event.customer_name || '',
|
||||||
customer_email: event.customer_email || '',
|
customer_email: event.customer_email || '',
|
||||||
|
customer_phone: (event as any).customer_phone || '',
|
||||||
source_mode: event.source_mode === 'reference' ? 'reference' : 'managed',
|
source_mode: event.source_mode === 'reference' ? 'reference' : 'managed',
|
||||||
external_path: event.external_path || '',
|
external_path: event.external_path || '',
|
||||||
require_password: normalizeRequirePassword(event.require_password),
|
require_password: normalizeRequirePassword(event.require_password),
|
||||||
@@ -617,6 +621,11 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
if (editForm.customer_email !== undefined && editForm.customer_email !== null && editForm.customer_email.trim()) {
|
if (editForm.customer_email !== undefined && editForm.customer_email !== null && editForm.customer_email.trim()) {
|
||||||
updateData.customer_email = editForm.customer_email;
|
updateData.customer_email = editForm.customer_email;
|
||||||
}
|
}
|
||||||
|
if (editForm.customer_phone !== undefined) {
|
||||||
|
// Send empty string as null so an admin can clear the field. Backend
|
||||||
|
// strips this entirely if the global phone-field toggle is off.
|
||||||
|
updateData.customer_phone = editForm.customer_phone.trim() || null;
|
||||||
|
}
|
||||||
|
|
||||||
if (editForm.new_password) {
|
if (editForm.new_password) {
|
||||||
updateData.password = editForm.new_password;
|
updateData.password = editForm.new_password;
|
||||||
@@ -970,6 +979,20 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{phoneFieldEnabled && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||||
|
{t('events.customerPhone', 'Customer Phone')} ({t('common.optional')})
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="tel"
|
||||||
|
value={editForm.customer_phone}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, customer_phone: e.target.value }))}
|
||||||
|
placeholder={t('events.customerPhonePlaceholder', '+1 555 555 1234')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||||
{t('events.expirationDate')}
|
{t('events.expirationDate')}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export interface PublicSettings {
|
|||||||
event_require_expiration?: boolean;
|
event_require_expiration?: boolean;
|
||||||
event_default_require_password?: boolean;
|
event_default_require_password?: boolean;
|
||||||
gallery_show_filter_bar?: boolean;
|
gallery_show_filter_bar?: boolean;
|
||||||
|
event_phone_field_enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const publicSettingsService = {
|
export const publicSettingsService = {
|
||||||
|
|||||||
Reference in New Issue
Block a user