Merge pull request #286 from the-luap/feat/photo-sort-by-capture-date
feat: sort photos by capture date with configurable default sort (#283)
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
exports.up = async function(knex) {
|
||||||
|
const hasColumn = await knex.schema.hasColumn('events', 'default_photo_sort');
|
||||||
|
if (!hasColumn) {
|
||||||
|
await knex.schema.alterTable('events', (table) => {
|
||||||
|
table.string('default_photo_sort', 50).defaultTo('upload_date_desc');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = async function(knex) {
|
||||||
|
const hasColumn = await knex.schema.hasColumn('events', 'default_photo_sort');
|
||||||
|
if (hasColumn) {
|
||||||
|
await knex.schema.alterTable('events', (table) => {
|
||||||
|
table.dropColumn('default_photo_sort');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -259,7 +259,12 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
body('hero_image_anchor').optional().custom(validateHeroImageAnchor),
|
body('hero_image_anchor').optional().custom(validateHeroImageAnchor),
|
||||||
// Client access settings (#172)
|
// Client access settings (#172)
|
||||||
body('client_access_enabled').optional().isBoolean(),
|
body('client_access_enabled').optional().isBoolean(),
|
||||||
body('client_password').optional().isString()
|
body('client_password').optional().isString(),
|
||||||
|
body('default_photo_sort').optional().isIn([
|
||||||
|
'upload_date_desc', 'upload_date_asc',
|
||||||
|
'capture_date_desc', 'capture_date_asc',
|
||||||
|
'filename_asc', 'filename_desc'
|
||||||
|
])
|
||||||
], async (req, res) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
logger.debug('Create event request body', { body: req.body });
|
logger.debug('Create event request body', { body: req.body });
|
||||||
@@ -314,7 +319,9 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
client_access_enabled = false,
|
client_access_enabled = false,
|
||||||
client_password = null,
|
client_password = null,
|
||||||
// Draft mode
|
// Draft mode
|
||||||
is_draft = true
|
is_draft = true,
|
||||||
|
// Default photo sort
|
||||||
|
default_photo_sort = 'upload_date_desc'
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const customerName = getCustomerNameFromPayload(req.body);
|
const customerName = getCustomerNameFromPayload(req.body);
|
||||||
@@ -483,6 +490,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
|
|||||||
hero_image_anchor: hero_image_anchor || 'center',
|
hero_image_anchor: hero_image_anchor || 'center',
|
||||||
photo_cap: photo_cap || null,
|
photo_cap: photo_cap || null,
|
||||||
is_draft: formatBoolean(parseBooleanInput(is_draft, true)),
|
is_draft: formatBoolean(parseBooleanInput(is_draft, true)),
|
||||||
|
default_photo_sort: default_photo_sort || 'upload_date_desc',
|
||||||
// Client access (#172)
|
// Client access (#172)
|
||||||
client_access_enabled: formatBoolean(client_access_enabled),
|
client_access_enabled: formatBoolean(client_access_enabled),
|
||||||
...(client_access_enabled && client_password ? {
|
...(client_access_enabled && client_password ? {
|
||||||
@@ -856,7 +864,12 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), requireEventOwne
|
|||||||
// Client access settings (#172)
|
// Client access settings (#172)
|
||||||
body('client_access_enabled').optional().isBoolean(),
|
body('client_access_enabled').optional().isBoolean(),
|
||||||
body('client_password').optional().isString(),
|
body('client_password').optional().isString(),
|
||||||
body('regenerate_client_token').optional().isBoolean()
|
body('regenerate_client_token').optional().isBoolean(),
|
||||||
|
body('default_photo_sort').optional().isIn([
|
||||||
|
'upload_date_desc', 'upload_date_asc',
|
||||||
|
'capture_date_desc', 'capture_date_asc',
|
||||||
|
'filename_asc', 'filename_desc'
|
||||||
|
])
|
||||||
], async (req, res) => {
|
], async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ router.get('/:slug/info', async (req, res) => {
|
|||||||
'header_style',
|
'header_style',
|
||||||
'hero_divider_style',
|
'hero_divider_style',
|
||||||
'hero_image_anchor',
|
'hero_image_anchor',
|
||||||
'is_draft'
|
'is_draft',
|
||||||
|
'default_photo_sort'
|
||||||
)
|
)
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
@@ -182,7 +183,8 @@ router.get('/:slug/info', async (req, res) => {
|
|||||||
hero_logo_url: event.hero_logo_url || null,
|
hero_logo_url: event.hero_logo_url || null,
|
||||||
header_style: event.header_style || 'standard',
|
header_style: event.header_style || 'standard',
|
||||||
hero_divider_style: event.hero_divider_style || 'wave',
|
hero_divider_style: event.hero_divider_style || 'wave',
|
||||||
hero_image_anchor: event.hero_image_anchor || 'center'
|
hero_image_anchor: event.hero_image_anchor || 'center',
|
||||||
|
default_photo_sort: event.default_photo_sort || 'upload_date_desc'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching gallery info:', error);
|
console.error('Error fetching gallery info:', error);
|
||||||
@@ -397,6 +399,7 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
|||||||
header_style: req.event.header_style || 'standard',
|
header_style: req.event.header_style || 'standard',
|
||||||
hero_divider_style: req.event.hero_divider_style || 'wave',
|
hero_divider_style: req.event.hero_divider_style || 'wave',
|
||||||
hero_image_anchor: req.event.hero_image_anchor || 'center',
|
hero_image_anchor: req.event.hero_image_anchor || 'center',
|
||||||
|
default_photo_sort: req.event.default_photo_sort || 'upload_date_desc',
|
||||||
...protectionSettings
|
...protectionSettings
|
||||||
},
|
},
|
||||||
categories: categories,
|
categories: categories,
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
|
|||||||
<button
|
<button
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSortChange(option.value as 'date' | 'name' | 'size' | 'rating');
|
onSortChange(option.value as 'date' | 'name' | 'size' | 'rating' | 'capture_date');
|
||||||
if (isMobile) onClose();
|
if (isMobile) onClose();
|
||||||
}}
|
}}
|
||||||
className={`
|
className={`
|
||||||
|
|||||||
@@ -42,6 +42,25 @@ interface GalleryViewProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert default_photo_sort DB value to internal sortBy state
|
||||||
|
const parseDefaultPhotoSort = (defaultSort?: string): { sortBy: 'date' | 'name' | 'size' | 'rating' | 'capture_date'; sortDesc: boolean } => {
|
||||||
|
switch (defaultSort) {
|
||||||
|
case 'upload_date_asc':
|
||||||
|
return { sortBy: 'date', sortDesc: false };
|
||||||
|
case 'capture_date_desc':
|
||||||
|
return { sortBy: 'capture_date', sortDesc: true };
|
||||||
|
case 'capture_date_asc':
|
||||||
|
return { sortBy: 'capture_date', sortDesc: false };
|
||||||
|
case 'filename_asc':
|
||||||
|
return { sortBy: 'name', sortDesc: false };
|
||||||
|
case 'filename_desc':
|
||||||
|
return { sortBy: 'name', sortDesc: true };
|
||||||
|
case 'upload_date_desc':
|
||||||
|
default:
|
||||||
|
return { sortBy: 'date', sortDesc: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { logout, isClient } = useGalleryAuth();
|
const { logout, isClient } = useGalleryAuth();
|
||||||
@@ -50,6 +69,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
|||||||
const [selectedCategoryId, setSelectedCategoryId] = useState<number | string | null>(null);
|
const [selectedCategoryId, setSelectedCategoryId] = useState<number | string | null>(null);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [sortBy, setSortBy] = useState<'date' | 'name' | 'size' | 'rating' | 'capture_date'>('date');
|
const [sortBy, setSortBy] = useState<'date' | 'name' | 'size' | 'rating' | 'capture_date'>('date');
|
||||||
|
const [defaultSortApplied, setDefaultSortApplied] = useState(false);
|
||||||
const [brandingSettings, setBrandingSettings] = useState<any>(null);
|
const [brandingSettings, setBrandingSettings] = useState<any>(null);
|
||||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
@@ -103,6 +123,15 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
|||||||
}
|
}
|
||||||
}, [data?.event?.protection_level]);
|
}, [data?.event?.protection_level]);
|
||||||
|
|
||||||
|
// Apply default photo sort from event settings
|
||||||
|
useEffect(() => {
|
||||||
|
if (!defaultSortApplied && data?.event?.default_photo_sort) {
|
||||||
|
const { sortBy: defaultSortBy } = parseDefaultPhotoSort(data.event.default_photo_sort);
|
||||||
|
setSortBy(defaultSortBy);
|
||||||
|
setDefaultSortApplied(true);
|
||||||
|
}
|
||||||
|
}, [data?.event?.default_photo_sort, defaultSortApplied]);
|
||||||
|
|
||||||
// Get individual protection settings from event
|
// Get individual protection settings from event
|
||||||
const disableRightClick = data?.event?.disable_right_click === true;
|
const disableRightClick = data?.event?.disable_right_click === true;
|
||||||
const enableDevtoolsProtection = data?.event?.enable_devtools_protection === true;
|
const enableDevtoolsProtection = data?.event?.enable_devtools_protection === true;
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ interface PhotoFilterBarProps {
|
|||||||
onCategoryChange: (categoryId: number | string | null) => void;
|
onCategoryChange: (categoryId: number | string | null) => void;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
onSearchChange: (term: string) => void;
|
onSearchChange: (term: string) => void;
|
||||||
sortBy: 'date' | 'name' | 'size' | 'rating';
|
sortBy: 'date' | 'name' | 'size' | 'rating' | 'capture_date';
|
||||||
onSortChange: (sort: 'date' | 'name' | 'size' | 'rating') => void;
|
onSortChange: (sort: 'date' | 'name' | 'size' | 'rating' | 'capture_date') => void;
|
||||||
photoCount: number;
|
photoCount: number;
|
||||||
// Feedback filter props
|
// Feedback filter props
|
||||||
feedbackEnabled?: boolean;
|
feedbackEnabled?: boolean;
|
||||||
@@ -85,6 +85,7 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
|||||||
{sortBy === 'date' ? t('gallery.sortByDate').replace('Sort by ', '') :
|
{sortBy === 'date' ? t('gallery.sortByDate').replace('Sort by ', '') :
|
||||||
sortBy === 'name' ? t('gallery.sortByName').replace('Sort by ', '') :
|
sortBy === 'name' ? t('gallery.sortByName').replace('Sort by ', '') :
|
||||||
sortBy === 'size' ? t('gallery.sortBySize').replace('Sort by ', '') :
|
sortBy === 'size' ? t('gallery.sortBySize').replace('Sort by ', '') :
|
||||||
|
sortBy === 'capture_date' ? t('photoSort.dateTaken', 'Date Taken') :
|
||||||
t('gallery.sortByRating', 'Rating')}
|
t('gallery.sortByRating', 'Rating')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -101,6 +102,17 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
|||||||
>
|
>
|
||||||
{t('gallery.sortByDate')}
|
{t('gallery.sortByDate')}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onSortChange('capture_date');
|
||||||
|
setShowSortMenu(false);
|
||||||
|
}}
|
||||||
|
className={`w-full text-left px-4 py-2 text-sm hover:bg-black/10 ${
|
||||||
|
sortBy === 'capture_date' ? 'text-primary-600 bg-primary-50' : 'text-muted-theme'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t('photoSort.dateTaken', 'Date Taken')}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onSortChange('name');
|
onSortChange('name');
|
||||||
|
|||||||
@@ -2396,5 +2396,15 @@
|
|||||||
"sanitizationWarning": "Einige CSS-Muster wurden aus Sicherheitsgründen blockiert",
|
"sanitizationWarning": "Einige CSS-Muster wurden aus Sicherheitsgründen blockiert",
|
||||||
"reset": "Vorlage auf Standard zurückgesetzt",
|
"reset": "Vorlage auf Standard zurückgesetzt",
|
||||||
"resetFailed": "Vorlage konnte nicht zurückgesetzt werden"
|
"resetFailed": "Vorlage konnte nicht zurückgesetzt werden"
|
||||||
|
},
|
||||||
|
"photoSort": {
|
||||||
|
"defaultSort": "Standard-Fotosortierung",
|
||||||
|
"uploadDateNewest": "Hochladedatum (Neueste zuerst)",
|
||||||
|
"uploadDateOldest": "Hochladedatum (Älteste zuerst)",
|
||||||
|
"captureDateNewest": "Aufnahmedatum (Neueste zuerst)",
|
||||||
|
"captureDateOldest": "Aufnahmedatum (Älteste zuerst)",
|
||||||
|
"filenameAZ": "Dateiname (A-Z)",
|
||||||
|
"filenameZA": "Dateiname (Z-A)",
|
||||||
|
"dateTaken": "Aufnahmedatum"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2396,5 +2396,15 @@
|
|||||||
"exportSelected": "Export {{count}} selected",
|
"exportSelected": "Export {{count}} selected",
|
||||||
"exportFiltered": "Export filtered photos",
|
"exportFiltered": "Export filtered photos",
|
||||||
"hint": "Select photos or apply filters to export"
|
"hint": "Select photos or apply filters to export"
|
||||||
|
},
|
||||||
|
"photoSort": {
|
||||||
|
"defaultSort": "Default Photo Sort",
|
||||||
|
"uploadDateNewest": "Upload Date (Newest First)",
|
||||||
|
"uploadDateOldest": "Upload Date (Oldest First)",
|
||||||
|
"captureDateNewest": "Date Taken (Newest First)",
|
||||||
|
"captureDateOldest": "Date Taken (Oldest First)",
|
||||||
|
"filenameAZ": "Filename (A-Z)",
|
||||||
|
"filenameZA": "Filename (Z-A)",
|
||||||
|
"dateTaken": "Date Taken"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2396,5 +2396,15 @@
|
|||||||
"linkLabel": "Klanttoegangslink",
|
"linkLabel": "Klanttoegangslink",
|
||||||
"regenerateToken": "Link opnieuw genereren",
|
"regenerateToken": "Link opnieuw genereren",
|
||||||
"tokenRegenerated": "Klanttoegangslink opnieuw gegenereerd"
|
"tokenRegenerated": "Klanttoegangslink opnieuw gegenereerd"
|
||||||
|
},
|
||||||
|
"photoSort": {
|
||||||
|
"defaultSort": "Standaard fotovolgorde",
|
||||||
|
"uploadDateNewest": "Uploaddatum (Nieuwste eerst)",
|
||||||
|
"uploadDateOldest": "Uploaddatum (Oudste eerst)",
|
||||||
|
"captureDateNewest": "Opnamedatum (Nieuwste eerst)",
|
||||||
|
"captureDateOldest": "Opnamedatum (Oudste eerst)",
|
||||||
|
"filenameAZ": "Bestandsnaam (A-Z)",
|
||||||
|
"filenameZA": "Bestandsnaam (Z-A)",
|
||||||
|
"dateTaken": "Opnamedatum"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2396,5 +2396,15 @@
|
|||||||
"exportSelected": "Exportar {{count}} selecionados",
|
"exportSelected": "Exportar {{count}} selecionados",
|
||||||
"exportFiltered": "Exportar fotos filtradas",
|
"exportFiltered": "Exportar fotos filtradas",
|
||||||
"hint": "Selecione fotos ou aplique filtros para exportar"
|
"hint": "Selecione fotos ou aplique filtros para exportar"
|
||||||
|
},
|
||||||
|
"photoSort": {
|
||||||
|
"defaultSort": "Ordenação padrão das fotos",
|
||||||
|
"uploadDateNewest": "Data de envio (Mais recentes)",
|
||||||
|
"uploadDateOldest": "Data de envio (Mais antigas)",
|
||||||
|
"captureDateNewest": "Data da captura (Mais recentes)",
|
||||||
|
"captureDateOldest": "Data da captura (Mais antigas)",
|
||||||
|
"filenameAZ": "Nome do arquivo (A-Z)",
|
||||||
|
"filenameZA": "Nome do arquivo (Z-A)",
|
||||||
|
"dateTaken": "Data da captura"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2396,5 +2396,15 @@
|
|||||||
"exportSelected": "Экспортировать {{count}} выбранных",
|
"exportSelected": "Экспортировать {{count}} выбранных",
|
||||||
"exportFiltered": "Экспортировать отфильтрованные фото",
|
"exportFiltered": "Экспортировать отфильтрованные фото",
|
||||||
"hint": "Выберите фото или примените фильтры для экспорта"
|
"hint": "Выберите фото или примените фильтры для экспорта"
|
||||||
|
},
|
||||||
|
"photoSort": {
|
||||||
|
"defaultSort": "Сортировка фото по умолчанию",
|
||||||
|
"uploadDateNewest": "Дата загрузки (Сначала новые)",
|
||||||
|
"uploadDateOldest": "Дата загрузки (Сначала старые)",
|
||||||
|
"captureDateNewest": "Дата съёмки (Сначала новые)",
|
||||||
|
"captureDateOldest": "Дата съёмки (Сначала старые)",
|
||||||
|
"filenameAZ": "Имя файла (А-Я)",
|
||||||
|
"filenameZA": "Имя файла (Я-А)",
|
||||||
|
"dateTaken": "Дата съёмки"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ interface FormData {
|
|||||||
// Client access (#172)
|
// Client access (#172)
|
||||||
client_access_enabled: boolean;
|
client_access_enabled: boolean;
|
||||||
client_password: string;
|
client_password: string;
|
||||||
|
// Default photo sort
|
||||||
|
default_photo_sort: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback event types (used when API is unavailable)
|
// Fallback event types (used when API is unavailable)
|
||||||
@@ -120,6 +122,7 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
client_access_enabled: false,
|
client_access_enabled: false,
|
||||||
client_password: '',
|
client_password: '',
|
||||||
|
default_photo_sort: 'upload_date_desc',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
|
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
|
||||||
@@ -319,6 +322,8 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
// Client access (#172)
|
// Client access (#172)
|
||||||
client_access_enabled: formData.client_access_enabled,
|
client_access_enabled: formData.client_access_enabled,
|
||||||
client_password: formData.client_access_enabled ? formData.client_password : undefined,
|
client_password: formData.client_access_enabled ? formData.client_password : undefined,
|
||||||
|
// Default photo sort
|
||||||
|
default_photo_sort: formData.default_photo_sort,
|
||||||
};
|
};
|
||||||
|
|
||||||
createMutation.mutate(payload);
|
createMutation.mutate(payload);
|
||||||
@@ -751,6 +756,25 @@ export const CreateEventPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Default Photo Sort */}
|
||||||
|
<div className="pt-4 border-t border-neutral-200 dark:border-neutral-700">
|
||||||
|
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
|
||||||
|
{t('photoSort.defaultSort', 'Default Photo Sort')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.default_photo_sort}
|
||||||
|
onChange={(e) => setFormData({ ...formData, default_photo_sort: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
>
|
||||||
|
<option value="upload_date_desc">{t('photoSort.uploadDateNewest', 'Upload Date (Newest First)')}</option>
|
||||||
|
<option value="upload_date_asc">{t('photoSort.uploadDateOldest', 'Upload Date (Oldest First)')}</option>
|
||||||
|
<option value="capture_date_desc">{t('photoSort.captureDateNewest', 'Date Taken (Newest First)')}</option>
|
||||||
|
<option value="capture_date_asc">{t('photoSort.captureDateOldest', 'Date Taken (Oldest First)')}</option>
|
||||||
|
<option value="filename_asc">{t('photoSort.filenameAZ', 'Filename (A-Z)')}</option>
|
||||||
|
<option value="filename_desc">{t('photoSort.filenameZA', 'Filename (Z-A)')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Client Access (#172) */}
|
{/* Client Access (#172) */}
|
||||||
<div className="pt-4 border-t border-neutral-200 dark:border-neutral-700">
|
<div className="pt-4 border-t border-neutral-200 dark:border-neutral-700">
|
||||||
<label className="flex items-start gap-2">
|
<label className="flex items-start gap-2">
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
hero_image_anchor: string;
|
hero_image_anchor: string;
|
||||||
// Photo cap
|
// Photo cap
|
||||||
photo_cap: number;
|
photo_cap: number;
|
||||||
|
// Default photo sort
|
||||||
|
default_photo_sort: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
@@ -209,6 +211,8 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
hero_image_anchor: 'center',
|
hero_image_anchor: 'center',
|
||||||
// Photo cap
|
// Photo cap
|
||||||
photo_cap: 0,
|
photo_cap: 0,
|
||||||
|
// Default photo sort
|
||||||
|
default_photo_sort: 'upload_date_desc',
|
||||||
});
|
});
|
||||||
const [feedbackSettings, setFeedbackSettings] = useState<FeedbackSettingsType>({
|
const [feedbackSettings, setFeedbackSettings] = useState<FeedbackSettingsType>({
|
||||||
feedback_enabled: false,
|
feedback_enabled: false,
|
||||||
@@ -442,6 +446,8 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
hero_image_anchor: event.hero_image_anchor || 'center',
|
hero_image_anchor: event.hero_image_anchor || 'center',
|
||||||
// Photo cap
|
// Photo cap
|
||||||
photo_cap: event.photo_cap || 0,
|
photo_cap: event.photo_cap || 0,
|
||||||
|
// Default photo sort
|
||||||
|
default_photo_sort: event.default_photo_sort || 'upload_date_desc',
|
||||||
});
|
});
|
||||||
|
|
||||||
setShowNewPassword(false);
|
setShowNewPassword(false);
|
||||||
@@ -577,6 +583,8 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
hero_image_anchor: editForm.hero_image_anchor,
|
hero_image_anchor: editForm.hero_image_anchor,
|
||||||
// Photo cap
|
// Photo cap
|
||||||
photo_cap: editForm.photo_cap > 0 ? editForm.photo_cap : null,
|
photo_cap: editForm.photo_cap > 0 ? editForm.photo_cap : null,
|
||||||
|
// Default photo sort
|
||||||
|
default_photo_sort: editForm.default_photo_sort,
|
||||||
// Header style settings (decoupled from layout, #158)
|
// Header style settings (decoupled from layout, #158)
|
||||||
header_style: currentTheme?.headerStyle || 'standard',
|
header_style: currentTheme?.headerStyle || 'standard',
|
||||||
hero_divider_style: currentTheme?.heroDividerStyle || 'wave',
|
hero_divider_style: currentTheme?.heroDividerStyle || 'wave',
|
||||||
@@ -1125,6 +1133,25 @@ export const EventDetailsPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Default Photo Sort */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||||
|
{t('photoSort.defaultSort', 'Default Photo Sort')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={editForm.default_photo_sort}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, default_photo_sort: e.target.value }))}
|
||||||
|
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
>
|
||||||
|
<option value="upload_date_desc">{t('photoSort.uploadDateNewest', 'Upload Date (Newest First)')}</option>
|
||||||
|
<option value="upload_date_asc">{t('photoSort.uploadDateOldest', 'Upload Date (Oldest First)')}</option>
|
||||||
|
<option value="capture_date_desc">{t('photoSort.captureDateNewest', 'Date Taken (Newest First)')}</option>
|
||||||
|
<option value="capture_date_asc">{t('photoSort.captureDateOldest', 'Date Taken (Oldest First)')}</option>
|
||||||
|
<option value="filename_asc">{t('photoSort.filenameAZ', 'Filename (A-Z)')}</option>
|
||||||
|
<option value="filename_desc">{t('photoSort.filenameZA', 'Filename (Z-A)')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="flex items-center">
|
<label className="flex items-center">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ interface CreateEventData {
|
|||||||
moderate_comments?: boolean;
|
moderate_comments?: boolean;
|
||||||
show_feedback_to_guests?: boolean;
|
show_feedback_to_guests?: boolean;
|
||||||
photo_cap?: number | null;
|
photo_cap?: number | null;
|
||||||
|
default_photo_sort?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateEventData {
|
interface UpdateEventData {
|
||||||
@@ -60,6 +61,7 @@ interface UpdateEventData {
|
|||||||
source_mode?: 'managed' | 'reference';
|
source_mode?: 'managed' | 'reference';
|
||||||
external_path?: string | null;
|
external_path?: string | null;
|
||||||
photo_cap?: number | null;
|
photo_cap?: number | null;
|
||||||
|
default_photo_sort?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventsListResponse {
|
interface EventsListResponse {
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export interface Event {
|
|||||||
// Client access (#172)
|
// Client access (#172)
|
||||||
client_access_enabled?: boolean;
|
client_access_enabled?: boolean;
|
||||||
client_share_token?: string;
|
client_share_token?: string;
|
||||||
|
// Default photo sort order
|
||||||
|
default_photo_sort?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GalleryAccessLevel = 'guest' | 'client';
|
export type GalleryAccessLevel = 'guest' | 'client';
|
||||||
@@ -73,6 +75,7 @@ export interface GalleryInfo {
|
|||||||
is_expired: boolean;
|
is_expired: boolean;
|
||||||
requires_password?: boolean;
|
requires_password?: boolean;
|
||||||
color_theme?: string;
|
color_theme?: string;
|
||||||
|
default_photo_sort?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Photo {
|
export interface Photo {
|
||||||
@@ -151,6 +154,8 @@ export interface GalleryData {
|
|||||||
hero_divider_style?: 'wave' | 'straight' | 'angle' | 'curve' | 'none';
|
hero_divider_style?: 'wave' | 'straight' | 'angle' | 'curve' | 'none';
|
||||||
// Hero image anchor position (#162) – keyword or "X% Y%" focal point
|
// Hero image anchor position (#162) – keyword or "X% Y%" focal point
|
||||||
hero_image_anchor?: string;
|
hero_image_anchor?: string;
|
||||||
|
// Default photo sort order
|
||||||
|
default_photo_sort?: string;
|
||||||
};
|
};
|
||||||
categories?: PhotoCategory[];
|
categories?: PhotoCategory[];
|
||||||
photos: Photo[];
|
photos: Photo[];
|
||||||
|
|||||||
Reference in New Issue
Block a user