feat: sort photos by capture date with configurable default sort (#283)
Add per-event default photo sort setting with 6 options: - Upload Date (Newest/Oldest First) - Date Taken (Newest/Oldest First) — uses EXIF captured_at - Filename (A-Z / Z-A) Backend: - Migration 077 adds default_photo_sort column to events table - Event create/update handlers accept and validate the setting - Gallery info endpoint returns default_photo_sort for frontend Frontend: - "Date Taken" added to gallery sort dropdown (alongside Date, Name, Size, Rating) - Gallery initializes with event's default sort instead of hardcoded "date" - "Default Photo Sort" dropdown in event create and edit forms - Photos without EXIF dates fall back to upload date i18n: All 5 locales (EN, DE, NL, PT, RU) updated with sort labels. Closes #283
This commit is contained in:
@@ -63,6 +63,8 @@ interface FormData {
|
||||
// Client access (#172)
|
||||
client_access_enabled: boolean;
|
||||
client_password: string;
|
||||
// Default photo sort
|
||||
default_photo_sort: string;
|
||||
}
|
||||
|
||||
// Fallback event types (used when API is unavailable)
|
||||
@@ -120,6 +122,7 @@ export const CreateEventPage: React.FC = () => {
|
||||
},
|
||||
client_access_enabled: false,
|
||||
client_password: '',
|
||||
default_photo_sort: 'upload_date_desc',
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
|
||||
@@ -319,6 +322,8 @@ export const CreateEventPage: React.FC = () => {
|
||||
// Client access (#172)
|
||||
client_access_enabled: formData.client_access_enabled,
|
||||
client_password: formData.client_access_enabled ? formData.client_password : undefined,
|
||||
// Default photo sort
|
||||
default_photo_sort: formData.default_photo_sort,
|
||||
};
|
||||
|
||||
createMutation.mutate(payload);
|
||||
@@ -751,6 +756,25 @@ export const CreateEventPage: React.FC = () => {
|
||||
</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) */}
|
||||
<div className="pt-4 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<label className="flex items-start gap-2">
|
||||
|
||||
@@ -176,6 +176,8 @@ export const EventDetailsPage: React.FC = () => {
|
||||
hero_image_anchor: string;
|
||||
// Photo cap
|
||||
photo_cap: number;
|
||||
// Default photo sort
|
||||
default_photo_sort: string;
|
||||
};
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
@@ -209,6 +211,8 @@ export const EventDetailsPage: React.FC = () => {
|
||||
hero_image_anchor: 'center',
|
||||
// Photo cap
|
||||
photo_cap: 0,
|
||||
// Default photo sort
|
||||
default_photo_sort: 'upload_date_desc',
|
||||
});
|
||||
const [feedbackSettings, setFeedbackSettings] = useState<FeedbackSettingsType>({
|
||||
feedback_enabled: false,
|
||||
@@ -442,6 +446,8 @@ export const EventDetailsPage: React.FC = () => {
|
||||
hero_image_anchor: event.hero_image_anchor || 'center',
|
||||
// Photo cap
|
||||
photo_cap: event.photo_cap || 0,
|
||||
// Default photo sort
|
||||
default_photo_sort: event.default_photo_sort || 'upload_date_desc',
|
||||
});
|
||||
|
||||
setShowNewPassword(false);
|
||||
@@ -577,6 +583,8 @@ export const EventDetailsPage: React.FC = () => {
|
||||
hero_image_anchor: editForm.hero_image_anchor,
|
||||
// Photo cap
|
||||
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: currentTheme?.headerStyle || 'standard',
|
||||
hero_divider_style: currentTheme?.heroDividerStyle || 'wave',
|
||||
@@ -1125,6 +1133,25 @@ export const EventDetailsPage: React.FC = () => {
|
||||
</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>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user