feat: decouple hero header from gallery layouts (#158)

- Add separate header_style setting (hero/standard/minimal/none) that can
  be combined with any layout type (grid/masonry/carousel/timeline/mosaic)
- Create HeroHeader and HeroDivider components for reusable hero section
- Add hero_divider_style setting (wave/straight/angle/curve/none)
- Add database migration for header_style and hero_divider_style columns
- Remove deprecated HeroGalleryLayout component
- Fix various TypeScript errors across the codebase:
  - Add missing type properties (css_template_id, updatedAt, justified settings)
  - Fix null handling for event_date and expires_at fields
  - Fix translation function calls and i18n config
  - Remove unused imports and variables
This commit is contained in:
Paul Nothaft
2026-02-01 22:44:28 +01:00
parent 397d33a95a
commit 7b8d8bd92b
35 changed files with 907 additions and 533 deletions
+2 -2
View File
@@ -195,7 +195,7 @@ export const AdminDashboard: React.FC = () => {
) : (
<div className="space-y-3">
{expiringEvents.slice(0, 5).map((event) => {
const daysLeft = differenceInDays(parseISO(event.expires_at), new Date());
const daysLeft = differenceInDays(parseISO(event.expires_at!), new Date());
return (
<div
@@ -216,7 +216,7 @@ export const AdminDashboard: React.FC = () => {
{t('admin.daysLeft', { count: daysLeft })}
</p>
<p className="text-xs text-neutral-500">
{t('gallery.expires')} {format(parseISO(event.expires_at), 'PP')}
{t('gallery.expires')} {format(parseISO(event.expires_at!), 'PP')}
</p>
</div>
</div>
+2 -1
View File
@@ -20,6 +20,7 @@ import { eventsService } from '../../services/events.service';
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
import { categoriesService } from '../../services/categories.service';
import { settingsService } from '../../services/settings.service';
import { publicSettingsService } from '../../services/publicSettings.service';
import { cssTemplatesService } from '../../services/cssTemplates.service';
import { eventTypesService } from '../../services/eventTypes.service';
import { useTranslation } from 'react-i18next';
@@ -152,7 +153,7 @@ export const CreateEventPage: React.FC = () => {
// Fetch public settings for field requirements
const { data: publicSettings } = useQuery({
queryKey: ['public-settings'],
queryFn: () => settingsService.getPublicSettings()
queryFn: () => publicSettingsService.getPublicSettings()
});
// Get field requirements (default to true if not set)
+3 -24
View File
@@ -60,7 +60,7 @@ import { buildResourceUrl } from '../../utils/url';
import { isGalleryPublic, normalizeRequirePassword } from '../../utils/accessControl';
import { archiveService } from '../../services/archive.service';
import { externalMediaService } from '../../services/externalMedia.service';
import { photosService, AdminPhoto, type PhotoFilters as PhotoFilterParams, type FeedbackFilters, type FilterSummary } from '../../services/photos.service';
import { photosService, AdminPhoto, type PhotoFilters as PhotoFilterParams, type FeedbackFilters } from '../../services/photos.service';
import { feedbackService, FeedbackSettings as FeedbackSettingsType } from '../../services/feedback.service';
import { cssTemplatesService, type EnabledTemplate } from '../../services/cssTemplates.service';
import { ThemeConfig, GALLERY_THEME_PRESETS } from '../../types/theme.types';
@@ -337,28 +337,6 @@ export const EventDetailsPage: React.FC = () => {
},
});
const applyThemeMutation = useMutation({
mutationFn: async ({ theme, presetName }: { theme: ThemeConfig; presetName: string }) => {
if (!id) {
throw new Error('Missing event identifier');
}
const colorThemeValue = presetName && presetName !== 'custom'
? presetName
: JSON.stringify(theme);
return eventsService.updateEvent(parseInt(id), { color_theme: colorThemeValue });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-event', id] });
toast.success(t('branding.themeApplied', 'Theme updated'));
},
onError: (error: any) => {
const message = error?.response?.data?.error || t('branding.themeApplyError', 'Failed to apply theme');
toast.error(message);
}
});
// Archive mutation
const archiveMutation = useMutation({
mutationFn: () => eventsService.archiveEvent(parseInt(id!)),
@@ -470,7 +448,7 @@ export const EventDetailsPage: React.FC = () => {
try {
const formData = new FormData();
formData.append('logo', file);
const response = await api.post(`/admin/events/${id}/logo`, formData, {
await api.post(`/admin/events/${id}/logo`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
toast.success(t('events.eventLogoUploaded', 'Event logo uploaded successfully'));
@@ -1745,6 +1723,7 @@ export const EventDetailsPage: React.FC = () => {
queryClient.invalidateQueries({ queryKey: ['admin-event', id] });
}}
onSelectionChange={setSelectedPhotoIds}
categories={categories}
/>
)}
+1 -1
View File
@@ -277,7 +277,7 @@ export const EventTypesPage: React.FC = () => {
{showCreateModal && (
<EventTypeModal
onClose={() => setShowCreateModal(false)}
onSubmit={(data) => createMutation.mutate(data)}
onSubmit={(data) => createMutation.mutate(data as CreateEventTypeData)}
isLoading={createMutation.isPending}
/>
)}
+1 -1
View File
@@ -30,7 +30,7 @@ const mockCategories = [
export const PreviewPage: React.FC = () => {
const { setTheme } = useTheme();
const [brandingSettings, setBrandingSettings] = useState<any>(null);
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null);
const [selectedCategoryId, setSelectedCategoryId] = useState<number | string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState<'date' | 'name' | 'size' | 'rating'>('date');