From 23ec674e05290173673a8205e7af58904f712690 Mon Sep 17 00:00:00 2001 From: paul Date: Mon, 7 Jul 2025 15:59:48 +0200 Subject: [PATCH] Integrate branding and theme settings with database - Update BrandingPage to save settings to database instead of localStorage - Add public settings endpoint for galleries to fetch branding/theme - Update GalleryView to apply branding settings in footer - Apply theme settings from database to gallery pages - Support event-specific themes that override global settings - Ensure watermark and all branding settings are stored in database --- backend/server.js | 1 + backend/src/routes/publicSettings.js | 43 +++++++ .../src/components/gallery/GalleryView.tsx | 73 +++++++++-- frontend/src/pages/admin/BrandingPage.tsx | 120 ++++++++++++++---- 4 files changed, 201 insertions(+), 36 deletions(-) create mode 100644 backend/src/routes/publicSettings.js diff --git a/backend/server.js b/backend/server.js index b071d3e..b5b53c2 100644 --- a/backend/server.js +++ b/backend/server.js @@ -78,6 +78,7 @@ app.use('/api/events', eventRoutes); app.use('/api/gallery', galleryRoutes); app.use('/api/admin', adminRoutes); app.use('/api/admin/auth', adminAuthRoutes); +app.use('/api/public/settings', require('./src/routes/publicSettings')); // Error handling middleware app.use((err, req, res, next) => { diff --git a/backend/src/routes/publicSettings.js b/backend/src/routes/publicSettings.js new file mode 100644 index 0000000..2c2ddb0 --- /dev/null +++ b/backend/src/routes/publicSettings.js @@ -0,0 +1,43 @@ +const express = require('express'); +const { db } = require('../database/db'); +const router = express.Router(); + +// Get public settings (branding and theme) +router.get('/', async (req, res) => { + try { + // Fetch branding and theme settings + const settings = await db('app_settings') + .whereIn('setting_type', ['branding', 'theme']) + .select('setting_key', 'setting_value'); + + // Convert to object format + const settingsObject = {}; + settings.forEach(setting => { + try { + settingsObject[setting.setting_key] = setting.setting_value + ? JSON.parse(setting.setting_value) + : null; + } catch (e) { + // If parsing fails, use the raw value + settingsObject[setting.setting_key] = setting.setting_value; + } + }); + + // Return only safe public settings + const publicSettings = { + branding_company_name: settingsObject.branding_company_name || '', + branding_company_tagline: settingsObject.branding_company_tagline || '', + branding_support_email: settingsObject.branding_support_email || '', + branding_footer_text: settingsObject.branding_footer_text || '', + branding_watermark_enabled: settingsObject.branding_watermark_enabled || false, + theme_config: settingsObject.theme_config || null + }; + + res.json(publicSettings); + } catch (error) { + console.error('Public settings fetch error:', error); + res.status(500).json({ error: 'Failed to fetch settings' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/frontend/src/components/gallery/GalleryView.tsx b/frontend/src/components/gallery/GalleryView.tsx index b77e700..1c824bb 100644 --- a/frontend/src/components/gallery/GalleryView.tsx +++ b/frontend/src/components/gallery/GalleryView.tsx @@ -1,14 +1,16 @@ import React, { useState, useMemo, useEffect } from 'react'; import { Download, Grid, Square, LogOut, Calendar, Clock, Search, SortAsc } from 'lucide-react'; import { format, differenceInDays, parseISO } from 'date-fns'; +import { useQuery } from '@tanstack/react-query'; import { Button, Input, SkeletonGalleryGrid, Skeleton } from '../common'; -import { useGalleryAuth } from '../../contexts'; +import { useGalleryAuth, useTheme } from '../../contexts'; import { useGalleryPhotos, useDownloadAllPhotos } from '../../hooks/useGallery'; import { PhotoGrid } from './PhotoGrid'; import { ExpirationBanner } from './ExpirationBanner'; import { CountdownTimer } from './CountdownTimer'; import { analyticsService } from '../../services/analytics.service'; +import { api } from '../../config/api'; interface GalleryViewProps { slug: string; @@ -25,15 +27,58 @@ interface GalleryViewProps { export const GalleryView: React.FC = ({ slug, event }) => { const { logout } = useGalleryAuth(); + const { setTheme } = useTheme(); const [viewMode, setViewMode] = useState<'all' | 'collages' | 'individual'>('all'); const [searchTerm, setSearchTerm] = useState(''); const [sortBy, setSortBy] = useState<'date' | 'name' | 'size'>('date'); const [showSortMenu, setShowSortMenu] = useState(false); + const [brandingSettings, setBrandingSettings] = useState(null); // Fetch photos const { data, isLoading, error } = useGalleryPhotos(slug); const downloadAllMutation = useDownloadAllPhotos(); + // Fetch branding settings + const { data: settingsData } = useQuery({ + queryKey: ['gallery-settings'], + queryFn: async () => { + const response = await api.get('/api/public/settings'); + return response.data; + }, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + }); + + // Apply theme and branding settings + useEffect(() => { + if (settingsData) { + // Apply branding settings + setBrandingSettings({ + company_name: settingsData.branding_company_name || '', + company_tagline: settingsData.branding_company_tagline || '', + support_email: settingsData.branding_support_email || '', + footer_text: settingsData.branding_footer_text || '© 2024 Your Company. All rights reserved.', + watermark_enabled: settingsData.branding_watermark_enabled || false, + }); + + // Apply theme settings + if (settingsData.theme_config) { + setTheme(settingsData.theme_config); + } + } + }, [settingsData, setTheme]); + + // Apply event-specific theme if available + useEffect(() => { + if (event.color_theme) { + try { + const eventTheme = JSON.parse(event.color_theme); + setTheme(eventTheme); + } catch (e) { + console.error('Failed to parse event theme:', e); + } + } + }, [event.color_theme, setTheme]); + // Calculate days until expiration const daysUntilExpiration = differenceInDays(parseISO(event.expires_at), new Date()); const showUrgentWarning = daysUntilExpiration <= 7; @@ -305,15 +350,25 @@ export const GalleryView: React.FC = ({ slug, event }) => { {/* Footer */}
-

- Need help? Contact the event organizer at{' '} - - support email - + {brandingSettings?.support_email && ( +

+ Need help? Contact us at{' '} + + {brandingSettings.support_email} + +

+ )} +

+ {brandingSettings?.footer_text || '© 2024 Your Company. All rights reserved.'}

+ {brandingSettings?.company_name && brandingSettings?.company_tagline && ( +

+ {brandingSettings.company_name} - {brandingSettings.company_tagline} +

+ )}
diff --git a/frontend/src/pages/admin/BrandingPage.tsx b/frontend/src/pages/admin/BrandingPage.tsx index 98d3c6f..9ba318e 100644 --- a/frontend/src/pages/admin/BrandingPage.tsx +++ b/frontend/src/pages/admin/BrandingPage.tsx @@ -1,24 +1,79 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Save, Eye, Palette } from 'lucide-react'; import { toast } from 'react-toastify'; -import { Button, Card, Input, ErrorBoundary } from '../../components/common'; +import { Button, Card, Input, ErrorBoundary, Loading } from '../../components/common'; import { ThemeCustomizer } from '../../components/admin/ThemeCustomizer'; import { useTheme, type ThemeConfig } from '../../contexts/ThemeContext'; +import { useQuery, useMutation } from '@tanstack/react-query'; +import { settingsService } from '../../services/settings.service'; export const BrandingPage: React.FC = () => { const { theme, setTheme, themeName, setThemeByName } = useTheme(); const [brandingSettings, setBrandingSettings] = useState({ - companyName: localStorage.getItem('branding-company-name') || '', - companyTagline: localStorage.getItem('branding-company-tagline') || '', - footerText: localStorage.getItem('branding-footer-text') || '© 2024 Your Company. All rights reserved.', - supportEmail: localStorage.getItem('branding-support-email') || '', - watermarkEnabled: localStorage.getItem('branding-watermark-enabled') === 'true', + company_name: '', + company_tagline: '', + footer_text: '© 2024 Your Company. All rights reserved.', + support_email: '', + watermark_enabled: false, }); const [currentTheme, setCurrentTheme] = useState(theme); const [currentThemeName, setCurrentThemeName] = useState(themeName); const [isPreviewMode, setIsPreviewMode] = useState(false); + // Fetch current settings + const { data: settings, isLoading } = useQuery({ + queryKey: ['admin-settings', 'branding'], + queryFn: () => settingsService.getSettingsByType('branding'), + }); + + // Fetch theme settings + const { data: themeSettings } = useQuery({ + queryKey: ['admin-settings', 'theme'], + queryFn: () => settingsService.getSettingsByType('theme'), + }); + + // Update branding mutation + const brandingMutation = useMutation({ + mutationFn: settingsService.updateBranding, + onSuccess: () => { + toast.success('Branding settings saved successfully!'); + }, + onError: () => { + toast.error('Failed to save branding settings'); + }, + }); + + // Update theme mutation + const themeMutation = useMutation({ + mutationFn: settingsService.updateTheme, + onSuccess: () => { + toast.success('Theme settings saved successfully!'); + }, + onError: () => { + toast.error('Failed to save theme settings'); + }, + }); + + // Initialize settings from database + useEffect(() => { + if (settings) { + const formatted = settingsService.formatBrandingSettings(settings); + setBrandingSettings(formatted); + } + }, [settings]); + + // Initialize theme from database + useEffect(() => { + if (themeSettings) { + const formatted = settingsService.formatThemeSettings(themeSettings); + if (formatted && Object.keys(formatted).length > 0) { + setCurrentTheme(formatted); + setTheme(formatted); + } + } + }, [themeSettings]); + const handleBrandingChange = (key: string, value: any) => { setBrandingSettings(prev => ({ ...prev, [key]: value })); }; @@ -37,16 +92,19 @@ export const BrandingPage: React.FC = () => { } }; - const handleSave = () => { - // Save branding settings to localStorage - Object.entries(brandingSettings).forEach(([key, value]) => { - localStorage.setItem(`branding-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`, String(value)); - }); - - // Apply theme - setTheme(currentTheme); - - toast.success('Branding settings saved successfully!'); + const handleSave = async () => { + try { + // Save branding settings to database + await brandingMutation.mutateAsync(brandingSettings); + + // Save theme settings to database + await themeMutation.mutateAsync(currentTheme); + + // Apply theme globally + setTheme(currentTheme); + } catch (error) { + console.error('Failed to save settings:', error); + } }; const handlePreview = () => { @@ -63,6 +121,14 @@ export const BrandingPage: React.FC = () => { } }; + if (isLoading) { + return ( +
+ +
+ ); + } + return (
@@ -96,23 +162,23 @@ export const BrandingPage: React.FC = () => {
handleBrandingChange('companyName', e.target.value)} + value={brandingSettings.company_name} + onChange={(e) => handleBrandingChange('company_name', e.target.value)} placeholder="Your Photography Studio" helperText="Displayed in email notifications and footers" /> handleBrandingChange('companyTagline', e.target.value)} + value={brandingSettings.company_tagline} + onChange={(e) => handleBrandingChange('company_tagline', e.target.value)} placeholder="Capturing moments that last forever" helperText="Optional tagline for branding" /> handleBrandingChange('supportEmail', e.target.value)} + value={brandingSettings.support_email} + onChange={(e) => handleBrandingChange('support_email', e.target.value)} placeholder="support@yourcompany.com" helperText="Contact email for gallery visitors" /> @@ -121,8 +187,8 @@ export const BrandingPage: React.FC = () => { Footer Text