import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { ArrowLeft, ExternalLink, Calendar, Download, Archive, Edit2, Save, X, AlertTriangle, Copy, CheckCircle, Upload, Image, Key, Mail, MessageSquare, Lock, Eye, EyeOff } from 'lucide-react'; import { parseISO, differenceInDays } from 'date-fns'; import { toast } from 'react-toastify'; import { useLocalizedDate } from '../../hooks/useLocalizedDate'; import { Button, Input, Card, Loading } from '../../components/common'; import { EventCategoryManager, AdminPhotoGrid, AdminPhotoViewer, PhotoFilters, PasswordResetModal, ThemeCustomizerEnhanced, ThemeDisplay, HeroPhotoSelector, PhotoUploadModal, FeedbackSettings, FeedbackModerationPanel } from '../../components/admin'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { eventsService } from '../../services/events.service'; 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 } from '../../services/photos.service'; import { feedbackService, FeedbackSettings as FeedbackSettingsType } from '../../services/feedback.service'; import { ThemeConfig, GALLERY_THEME_PRESETS } from '../../types/theme.types'; const resolveShareLink = (link: string): string => { if (!link) return '#'; if (link.startsWith('http')) return link; if (link.startsWith('/')) return link; return `/gallery/${link}`; }; const ExternalFolderPicker: React.FC<{ value: string; onChange: (p: string) => void }> = ({ value, onChange }) => { const { t } = useTranslation(); const [entries, setEntries] = useState<{ path: string; entries: any[]; canNavigateUp: boolean } | null>(null); const [loading, setLoading] = useState(false); const [currentPath, setCurrentPath] = useState(value || ''); const load = async (p: string) => { try { setLoading(true); const res = await externalMediaService.list(p); setEntries(res); setCurrentPath(res.path); } finally { setLoading(false); } }; useEffect(() => { load(currentPath || ''); }, []); const navigateUp = () => { if (!entries?.canNavigateUp) return; const parts = (entries.path || '').split('/').filter(Boolean); parts.pop(); load(parts.join('/')); }; return (
/external-media/{entries?.path || ''}
{loading ? (
{t('common.loading', 'Loading...')}
) : (
{entries?.entries?.filter((e: any) => e.type === 'dir').map((e: any) => ( ))}
)} {value && (
{t('common.selected', 'Selected')}: /external-media/{value}
)}
); }; export const EventDetailsPage: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { t } = useTranslation(); const { format } = useLocalizedDate(); // Validate ID parameter React.useEffect(() => { if (!id || isNaN(parseInt(id))) { navigate('/admin/events'); } }, [id, navigate]); type EditFormState = { welcome_message: string; color_theme: string; expires_at: string; allow_user_uploads: boolean; upload_category_id: number | null; hero_photo_id: number | null; customer_name: string; source_mode: 'managed' | 'reference'; external_path: string; require_password: boolean; new_password: string; confirm_new_password: string; }; const [isEditing, setIsEditing] = useState(false); const [editForm, setEditForm] = useState({ welcome_message: '', color_theme: '', expires_at: '', allow_user_uploads: false, upload_category_id: null, hero_photo_id: null, customer_name: '', source_mode: 'managed', external_path: '', require_password: true, new_password: '', confirm_new_password: '', }); const [feedbackSettings, setFeedbackSettings] = useState({ feedback_enabled: false, allow_ratings: true, allow_likes: true, allow_comments: true, allow_favorites: true, require_name_email: false, moderate_comments: true, show_feedback_to_guests: true, enable_rate_limiting: false, rate_limit_window_minutes: 15, rate_limit_max_requests: 10, }); const [copiedLink, setCopiedLink] = useState(false); const [showPhotoUpload, setShowPhotoUpload] = useState(false); const [showExternalImport, setShowExternalImport] = useState(false); const [activeTab, setActiveTab] = useState<'overview' | 'photos' | 'categories'>('overview'); const [externalPath, setExternalPath] = useState(''); const [importing, setImporting] = useState(false); const [selectedPhoto, setSelectedPhoto] = useState<{ photo: AdminPhoto; index: number } | null>(null); const [showPasswordReset, setShowPasswordReset] = useState(false); const [showNewPassword, setShowNewPassword] = useState(false); const [currentTheme, setCurrentTheme] = useState(null); const [currentPresetName, setCurrentPresetName] = useState('default'); // Photo filters state const [photoFilters, setPhotoFilters] = useState({ category_id: undefined as number | null | undefined, search: '', sort: 'date', order: 'desc' as 'asc' | 'desc' }); // Fetch event details const { data: event, isLoading: eventLoading } = useQuery({ queryKey: ['admin-event', id], queryFn: () => eventsService.getEvent(parseInt(id!)), enabled: !!id, }); // Fetch feedback settings const { data: eventFeedbackSettings } = useQuery({ queryKey: ['admin-event-feedback-settings', id], queryFn: () => feedbackService.getEventFeedbackSettings(id!), enabled: !!id, }); // Update local feedback settings when fetched from server useEffect(() => { if (eventFeedbackSettings) { setFeedbackSettings(eventFeedbackSettings); } }, [eventFeedbackSettings]); // Statistics are now fetched with the event details from the admin API // Fetch photos (needed for both photos tab and hero photo selector) const { data: photos = [], isLoading: photosLoading, refetch: refetchPhotos } = useQuery({ queryKey: ['admin-event-photos', id, photoFilters], queryFn: () => photosService.getEventPhotos(parseInt(id!), photoFilters), enabled: !!id && (activeTab === 'photos' || isEditing), }); // Fetch categories for the event const { data: categories = [] } = useQuery({ queryKey: ['admin-event-categories', id], queryFn: async () => { const response = await eventsService.getEventCategories(parseInt(id!)); return response || []; }, enabled: !!id, }); // Update mutation const updateMutation = useMutation({ mutationFn: (data: any) => eventsService.updateEvent(parseInt(id!), data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-event', id] }); toast.success(t('toast.eventUpdated')); setIsEditing(false); }, onError: (error: any) => { if (error.response?.data?.errors) { const errorMessage = error.response.data.errors[0].msg + ' (field: ' + error.response.data.errors[0].path + ')'; toast.error(errorMessage); } else { toast.error(error.response?.data?.error || t('toast.saveError')); } }, }); // Archive mutation const archiveMutation = useMutation({ mutationFn: () => eventsService.archiveEvent(parseInt(id!)), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-event', id] }); toast.success(t('toast.eventArchived')); }, onError: () => { toast.error(t('errors.somethingWentWrong')); }, }); // Extend expiration mutation const extendMutation = useMutation({ mutationFn: (days: number) => { return eventsService.extendExpiration(parseInt(id!), days); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-event', id] }); toast.success(t('toast.saveSuccess')); }, onError: () => { toast.error(t('toast.saveError')); }, }); if (eventLoading || !event) { return (
); } const daysUntilExpiration = differenceInDays(parseISO(event.expires_at), new Date()); const isExpired = daysUntilExpiration <= 0; const isExpiring = daysUntilExpiration > 0 && daysUntilExpiration <= 7; const handleStartEdit = () => { setEditForm({ welcome_message: event.welcome_message || '', color_theme: event.color_theme || '', expires_at: format(parseISO(event.expires_at), 'yyyy-MM-dd'), allow_user_uploads: event.allow_user_uploads || false, upload_category_id: event.upload_category_id || null, hero_photo_id: event.hero_photo_id || null, customer_name: event.customer_name || '', source_mode: event.source_mode === 'reference' ? 'reference' : 'managed', external_path: event.external_path || '', require_password: normalizeRequirePassword(event.require_password), new_password: '', confirm_new_password: '', }); setShowNewPassword(false); // Set feedback settings if available if (eventFeedbackSettings) { setFeedbackSettings(eventFeedbackSettings); } // Parse theme configuration if (event.color_theme) { try { if (event.color_theme.startsWith('{')) { const parsedTheme = JSON.parse(event.color_theme); setCurrentTheme(parsedTheme); // Try to find matching preset const matchingPreset = Object.entries(GALLERY_THEME_PRESETS).find( ([_, preset]) => JSON.stringify(preset.config) === JSON.stringify(parsedTheme) ); setCurrentPresetName(matchingPreset ? matchingPreset[0] : 'custom'); } else { // Legacy theme name const preset = GALLERY_THEME_PRESETS[event.color_theme]; if (preset) { setCurrentTheme(preset.config); setCurrentPresetName(event.color_theme); } } } catch (e) { setCurrentTheme(GALLERY_THEME_PRESETS.default.config); setCurrentPresetName('default'); } } else { setCurrentTheme(GALLERY_THEME_PRESETS.default.config); setCurrentPresetName('default'); } setIsEditing(true); }; const handleSaveEdit = async () => { // Prepare color_theme - if we have a custom theme, serialize it let themeToSave = editForm.color_theme; if (currentTheme && currentPresetName === 'custom') { themeToSave = JSON.stringify(currentTheme); } else if (currentPresetName && currentPresetName !== 'custom') { // Use preset name for non-custom themes themeToSave = currentPresetName; } const externalPathToSave = editForm.external_path?.trim() || ''; const currentRequirePassword = normalizeRequirePassword(event.require_password); const requirePasswordChanged = editForm.require_password !== currentRequirePassword; if (editForm.require_password) { if (requirePasswordChanged && !editForm.new_password) { toast.error(t('events.newPasswordRequired', 'Please set a password before enabling protection.')); return; } if (editForm.new_password) { if (editForm.new_password.length < 6) { toast.error(t('validation.passwordMinLength')); return; } if (editForm.new_password !== editForm.confirm_new_password) { toast.error(t('validation.passwordsDoNotMatch')); return; } } } if (editForm.source_mode === 'reference' && !externalPathToSave) { toast.error(t('events.externalFolderRequired', 'Please select an external folder before saving.')); return; } // Clean up the data - remove undefined values const updateData: any = { expires_at: editForm.expires_at, allow_user_uploads: editForm.allow_user_uploads, require_password: editForm.require_password, }; // Only include fields that have defined values if (editForm.welcome_message !== undefined && editForm.welcome_message !== null) { updateData.welcome_message = editForm.welcome_message; } if (themeToSave) { updateData.color_theme = themeToSave; } if (editForm.upload_category_id !== undefined) { updateData.upload_category_id = editForm.upload_category_id; } if (editForm.hero_photo_id !== undefined) { updateData.hero_photo_id = editForm.hero_photo_id; } updateData.source_mode = editForm.source_mode; updateData.external_path = editForm.source_mode === 'reference' ? externalPathToSave : null; if (editForm.customer_name !== undefined && editForm.customer_name !== null) { updateData.customer_name = editForm.customer_name; } if (editForm.new_password) { updateData.password = editForm.new_password; } // Remove any keys with undefined values Object.keys(updateData).forEach(key => { if (updateData[key] === undefined) { delete updateData[key]; } }); // Event update with validation // Update event details updateMutation.mutate(updateData); // Update feedback settings separately try { await feedbackService.updateEventFeedbackSettings(id!, feedbackSettings); } catch (error) { // Error already handled by mutation } }; const handleCopyLink = async () => { try { // Check if share_link exists if (!event.share_link) { toast.error(t('errors.noShareLink', 'No share link available')); return; } // Try modern clipboard API first if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(event.share_link); } else { // Fallback for non-HTTPS contexts or older browsers const textArea = document.createElement('textarea'); textArea.value = event.share_link; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; textArea.style.top = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); const successful = document.execCommand('copy'); document.body.removeChild(textArea); if (!successful) { throw new Error('Copy failed'); } } setCopiedLink(true); setTimeout(() => setCopiedLink(false), 2000); toast.success(t('toast.linkCopied')); } catch (err) { console.error('Copy failed:', err); toast.error(t('errors.copyFailed', 'Failed to copy link. Please copy manually.')); } }; return (
{/* Page Header */}

{event.event_name}

{format(parseISO(event.event_date), 'PPP')} {event.event_type} {isGalleryPublic(event.require_password) ? t('events.publicAccess', 'Public access') : t('events.passwordProtected', 'Password protected')} {event.is_archived ? ( {t('events.archived')} ) : null}
{!event.is_archived && ( <> {isEditing ? ( <> ) : ( <> {feedbackSettings?.feedback_enabled && ( )} )} )} {event.share_link && !isEditing && ( {t('events.viewGallery')} )}
{/* Expiration Warning */} {!event.is_archived && (isExpired || isExpiring) && (

{isExpired ? t('events.eventExpiredMessage') : t('events.eventExpiresIn', { days: daysUntilExpiration }) }

{isExpired ? t('events.guestsCannotAccessGallery') : t('events.warningEmailsHaveBeenSent')}

{!isExpired && ( )}
)} {/* Tabs */}
{/* Tab Content */} {activeTab === 'overview' && (
{/* Left Column - Main Details */}
{/* Event Information */}

{t('events.eventInformation')}

{isEditing ? (