Fix brand theme application and add comprehensive translations
- Fixed theme not being reflected on gallery and admin login pages - Created GlobalThemeProvider to apply themes globally - Updated gallery and admin login pages to use dynamic CSS variables - Added complete translations for all admin sections in English and German: - Notifications management - Event view and creation - Photo upload functionality - Category management - Archive page view - Analytics dashboard - Branding and theme settings - System settings - CMS page management - Email configuration - Fixed admin photo management display issues - Fixed photo upload category assignment - Added password reset functionality for galleries - Improved error handling and user feedback 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -65,10 +65,15 @@ class AnalyticsService {
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
// Check if analytics is initialized
|
||||
isInitialized() {
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
// Track custom events
|
||||
track(eventName: string, eventData?: Record<string, any>) {
|
||||
if (!this.initialized || !window.umami) {
|
||||
console.warn('Umami Analytics not initialized');
|
||||
// Silently ignore if not initialized
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,7 +84,7 @@ class AnalyticsService {
|
||||
// Track page views manually
|
||||
trackPageView(url?: string, referrer?: string) {
|
||||
if (!this.initialized || !window.umami) {
|
||||
console.warn('Umami Analytics not initialized');
|
||||
// Silently ignore if not initialized
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ import type { LoginResponse, GalleryAuthResponse } from '../types';
|
||||
|
||||
export const authService = {
|
||||
// Admin authentication
|
||||
async adminLogin(credentials: { email: string; password: string }): Promise<LoginResponse> {
|
||||
async adminLogin(credentials: { email: string; password: string; recaptchaToken?: string | null }): Promise<LoginResponse> {
|
||||
// Backend expects 'username' field, but we accept email
|
||||
const response = await api.post<LoginResponse>('/api/auth/admin/login', {
|
||||
username: credentials.email,
|
||||
password: credentials.password
|
||||
password: credentials.password,
|
||||
recaptchaToken: credentials.recaptchaToken
|
||||
});
|
||||
|
||||
setAuthToken(response.data.token, true);
|
||||
@@ -20,10 +21,11 @@ export const authService = {
|
||||
},
|
||||
|
||||
// Gallery authentication
|
||||
async verifyGalleryPassword(slug: string, password: string): Promise<GalleryAuthResponse> {
|
||||
async verifyGalleryPassword(slug: string, password: string, recaptchaToken?: string | null): Promise<GalleryAuthResponse> {
|
||||
const response = await api.post<GalleryAuthResponse>('/api/auth/gallery/verify', {
|
||||
slug,
|
||||
password,
|
||||
recaptchaToken
|
||||
});
|
||||
|
||||
setAuthToken(response.data.token, false);
|
||||
|
||||
@@ -80,6 +80,20 @@ export const eventsService = {
|
||||
await api.post(`/api/admin/events/${id}/archive`);
|
||||
},
|
||||
|
||||
// Bulk archive events (admin)
|
||||
async bulkArchiveEvents(eventIds: number[]): Promise<{
|
||||
message: string;
|
||||
results: {
|
||||
successful: Array<{ id: number; name: string }>;
|
||||
failed: Array<{ id: number; name: string; error: string }>;
|
||||
};
|
||||
}> {
|
||||
const response = await api.post('/api/admin/events/bulk-archive', {
|
||||
eventIds,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Extend event expiration (admin)
|
||||
async extendExpiration(id: number, days: number): Promise<Event> {
|
||||
const response = await api.post<Event>(`/api/events/${id}/extend`, {
|
||||
@@ -87,4 +101,16 @@ export const eventsService = {
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get event categories
|
||||
async getEventCategories(eventId: number): Promise<Array<{ id: number; name: string; slug: string }>> {
|
||||
const response = await api.get(`/api/admin/categories/event/${eventId}`);
|
||||
return response.data || [];
|
||||
},
|
||||
|
||||
// Reset event password
|
||||
async resetPassword(eventId: number, sendEmail: boolean = true): Promise<{ message: string; newPassword: string; emailSent: boolean }> {
|
||||
const response = await api.post(`/api/admin/events/${eventId}/reset-password`, { sendEmail });
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -6,4 +6,5 @@ export { analyticsService } from './analytics.service';
|
||||
export { archiveService } from './archive.service';
|
||||
export { emailService } from './email.service';
|
||||
export { settingsService } from './settings.service';
|
||||
export { cmsService } from './cms.service';
|
||||
export { cmsService } from './cms.service';
|
||||
export { notificationsService } from './notifications.service';
|
||||
@@ -0,0 +1,101 @@
|
||||
import { api } from '../config/api';
|
||||
|
||||
export interface Notification {
|
||||
id: number;
|
||||
type: string;
|
||||
actorType: string;
|
||||
actorName: string;
|
||||
eventName?: string;
|
||||
eventId?: number;
|
||||
metadata: Record<string, any>;
|
||||
createdAt: string;
|
||||
readAt?: string;
|
||||
isRead: boolean;
|
||||
}
|
||||
|
||||
export interface NotificationsResponse {
|
||||
notifications: Notification[];
|
||||
unreadCount: number;
|
||||
}
|
||||
|
||||
export const notificationsService = {
|
||||
// Get notifications
|
||||
async getNotifications(includeRead: boolean = false, limit: number = 20): Promise<NotificationsResponse> {
|
||||
const response = await api.get('/api/admin/notifications', {
|
||||
params: { includeRead, limit }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Mark single notification as read
|
||||
async markAsRead(notificationId: number): Promise<void> {
|
||||
await api.put(`/api/admin/notifications/${notificationId}/read`);
|
||||
},
|
||||
|
||||
// Mark all notifications as read
|
||||
async markAllAsRead(): Promise<void> {
|
||||
await api.put('/api/admin/notifications/read-all');
|
||||
},
|
||||
|
||||
// Clear old notifications
|
||||
async clearOldNotifications(): Promise<{ deletedCount: number }> {
|
||||
const response = await api.delete('/api/admin/notifications/clear-old');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Format notification message
|
||||
formatNotificationMessage(notification: Notification): string {
|
||||
switch (notification.type) {
|
||||
case 'event_created':
|
||||
return `New event "${notification.eventName}" was created`;
|
||||
case 'event_archived':
|
||||
return `Event "${notification.eventName}" was archived`;
|
||||
case 'photos_uploaded':
|
||||
return `${notification.metadata.count || 0} photos uploaded to "${notification.eventName}"`;
|
||||
case 'event_expiring':
|
||||
return `Event "${notification.eventName}" expires in ${notification.metadata.days || 0} days`;
|
||||
case 'event_expired':
|
||||
return `Event "${notification.eventName}" has expired`;
|
||||
case 'password_changed':
|
||||
return `Password changed by ${notification.actorName}`;
|
||||
case 'settings_updated':
|
||||
return `${notification.metadata.type || 'System'} settings updated`;
|
||||
case 'email_template_updated':
|
||||
return `Email template "${notification.metadata.template}" updated`;
|
||||
case 'bulk_download':
|
||||
return `${notification.metadata.count || 0} photos downloaded from "${notification.eventName}"`;
|
||||
case 'storage_warning':
|
||||
return `Storage usage at ${notification.metadata.percentage || 0}%`;
|
||||
default:
|
||||
return notification.metadata.message || 'System notification';
|
||||
}
|
||||
},
|
||||
|
||||
// Get notification icon and color
|
||||
getNotificationStyle(type: string): { icon: string; color: string } {
|
||||
switch (type) {
|
||||
case 'event_created':
|
||||
return { icon: 'Calendar', color: 'text-blue-600' };
|
||||
case 'event_archived':
|
||||
return { icon: 'Archive', color: 'text-green-600' };
|
||||
case 'photos_uploaded':
|
||||
return { icon: 'Image', color: 'text-purple-600' };
|
||||
case 'event_expiring':
|
||||
return { icon: 'AlertCircle', color: 'text-amber-600' };
|
||||
case 'event_expired':
|
||||
return { icon: 'Clock', color: 'text-red-600' };
|
||||
case 'password_changed':
|
||||
return { icon: 'Lock', color: 'text-indigo-600' };
|
||||
case 'settings_updated':
|
||||
return { icon: 'Settings', color: 'text-gray-600' };
|
||||
case 'email_template_updated':
|
||||
return { icon: 'Mail', color: 'text-teal-600' };
|
||||
case 'bulk_download':
|
||||
return { icon: 'Download', color: 'text-cyan-600' };
|
||||
case 'storage_warning':
|
||||
return { icon: 'Database', color: 'text-orange-600' };
|
||||
default:
|
||||
return { icon: 'Bell', color: 'text-gray-600' };
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { api } from '../config/api';
|
||||
|
||||
export interface AdminPhoto {
|
||||
id: number;
|
||||
filename: string;
|
||||
path: string;
|
||||
url: string;
|
||||
thumbnail_url: string | null;
|
||||
type: string;
|
||||
category_id: number | null;
|
||||
category_name: string | null;
|
||||
category_slug: string | null;
|
||||
size: number;
|
||||
uploaded_at: string;
|
||||
view_count?: number;
|
||||
download_count?: number;
|
||||
}
|
||||
|
||||
export interface PhotoFilters {
|
||||
category_id?: number | null;
|
||||
type?: string;
|
||||
search?: string;
|
||||
sort?: 'date' | 'name' | 'size';
|
||||
order?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
class PhotosService {
|
||||
async getEventPhotos(eventId: number, filters?: PhotoFilters): Promise<AdminPhoto[]> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (filters) {
|
||||
if (filters.category_id !== undefined) {
|
||||
params.append('category_id', filters.category_id?.toString() || '');
|
||||
}
|
||||
if (filters.type) params.append('type', filters.type);
|
||||
if (filters.search) params.append('search', filters.search);
|
||||
if (filters.sort) params.append('sort', filters.sort);
|
||||
if (filters.order) params.append('order', filters.order);
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `/api/admin/events/${eventId}/photos${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await api.get(url);
|
||||
|
||||
// Return photos as-is, URLs are already relative API paths
|
||||
return response.data.photos;
|
||||
}
|
||||
|
||||
async deletePhoto(eventId: number, photoId: number): Promise<void> {
|
||||
await api.delete(`/api/admin/events/${eventId}/photos/${photoId}`);
|
||||
}
|
||||
|
||||
async deletePhotos(eventId: number, photoIds: number[]): Promise<void> {
|
||||
await api.post(`/api/admin/events/${eventId}/photos/bulk-delete`, { photoIds });
|
||||
}
|
||||
|
||||
async updatePhotoCategory(eventId: number, photoId: number, categoryId: number | null): Promise<void> {
|
||||
await api.patch(`/api/admin/events/${eventId}/photos/${photoId}`, { category_id: categoryId });
|
||||
}
|
||||
|
||||
async updatePhotosCategory(eventId: number, photoIds: number[], categoryId: number | null): Promise<void> {
|
||||
await api.post(`/api/admin/events/${eventId}/photos/bulk-update`, {
|
||||
photoIds,
|
||||
updates: { category_id: categoryId }
|
||||
});
|
||||
}
|
||||
|
||||
async downloadPhoto(eventId: number, photoId: number, filename: string): Promise<void> {
|
||||
const response = await api.get(`/api/admin/events/${eventId}/photos/${photoId}/download`, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
|
||||
export const photosService = new PhotosService();
|
||||
Reference in New Issue
Block a user