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:
@@ -356,7 +356,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
onSortChange(option.value as 'date' | 'name' | 'size' | 'rating');
|
||||
onSortChange(option.value as 'date' | 'name' | 'size' | 'rating' | 'capture_date');
|
||||
if (isMobile) onClose();
|
||||
}}
|
||||
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 }) => {
|
||||
const { t } = useTranslation();
|
||||
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 [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortBy, setSortBy] = useState<'date' | 'name' | 'size' | 'rating' | 'capture_date'>('date');
|
||||
const [defaultSortApplied, setDefaultSortApplied] = useState(false);
|
||||
const [brandingSettings, setBrandingSettings] = useState<any>(null);
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
@@ -103,6 +123,15 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
||||
}
|
||||
}, [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
|
||||
const disableRightClick = data?.event?.disable_right_click === true;
|
||||
const enableDevtoolsProtection = data?.event?.enable_devtools_protection === true;
|
||||
|
||||
@@ -25,8 +25,8 @@ interface PhotoFilterBarProps {
|
||||
onCategoryChange: (categoryId: number | string | null) => void;
|
||||
searchTerm: string;
|
||||
onSearchChange: (term: string) => void;
|
||||
sortBy: 'date' | 'name' | 'size' | 'rating';
|
||||
onSortChange: (sort: 'date' | 'name' | 'size' | 'rating') => void;
|
||||
sortBy: 'date' | 'name' | 'size' | 'rating' | 'capture_date';
|
||||
onSortChange: (sort: 'date' | 'name' | 'size' | 'rating' | 'capture_date') => void;
|
||||
photoCount: number;
|
||||
// Feedback filter props
|
||||
feedbackEnabled?: boolean;
|
||||
@@ -82,9 +82,10 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
||||
className="w-full md:w-auto text-sm md:text-base"
|
||||
>
|
||||
<span className="hidden md:inline">{t('common.sortBy')} </span>
|
||||
{sortBy === 'date' ? t('gallery.sortByDate').replace('Sort by ', '') :
|
||||
sortBy === 'name' ? t('gallery.sortByName').replace('Sort by ', '') :
|
||||
{sortBy === 'date' ? t('gallery.sortByDate').replace('Sort by ', '') :
|
||||
sortBy === 'name' ? t('gallery.sortByName').replace('Sort by ', '') :
|
||||
sortBy === 'size' ? t('gallery.sortBySize').replace('Sort by ', '') :
|
||||
sortBy === 'capture_date' ? t('photoSort.dateTaken', 'Date Taken') :
|
||||
t('gallery.sortByRating', 'Rating')}
|
||||
</Button>
|
||||
|
||||
@@ -101,6 +102,17 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
||||
>
|
||||
{t('gallery.sortByDate')}
|
||||
</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
|
||||
onClick={() => {
|
||||
onSortChange('name');
|
||||
|
||||
Reference in New Issue
Block a user