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
This commit is contained in:
@@ -78,6 +78,7 @@ app.use('/api/events', eventRoutes);
|
|||||||
app.use('/api/gallery', galleryRoutes);
|
app.use('/api/gallery', galleryRoutes);
|
||||||
app.use('/api/admin', adminRoutes);
|
app.use('/api/admin', adminRoutes);
|
||||||
app.use('/api/admin/auth', adminAuthRoutes);
|
app.use('/api/admin/auth', adminAuthRoutes);
|
||||||
|
app.use('/api/public/settings', require('./src/routes/publicSettings'));
|
||||||
|
|
||||||
// Error handling middleware
|
// Error handling middleware
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import { Download, Grid, Square, LogOut, Calendar, Clock, Search, SortAsc } from 'lucide-react';
|
import { Download, Grid, Square, LogOut, Calendar, Clock, Search, SortAsc } from 'lucide-react';
|
||||||
import { format, differenceInDays, parseISO } from 'date-fns';
|
import { format, differenceInDays, parseISO } from 'date-fns';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { Button, Input, SkeletonGalleryGrid, Skeleton } from '../common';
|
import { Button, Input, SkeletonGalleryGrid, Skeleton } from '../common';
|
||||||
import { useGalleryAuth } from '../../contexts';
|
import { useGalleryAuth, useTheme } from '../../contexts';
|
||||||
import { useGalleryPhotos, useDownloadAllPhotos } from '../../hooks/useGallery';
|
import { useGalleryPhotos, useDownloadAllPhotos } from '../../hooks/useGallery';
|
||||||
import { PhotoGrid } from './PhotoGrid';
|
import { PhotoGrid } from './PhotoGrid';
|
||||||
import { ExpirationBanner } from './ExpirationBanner';
|
import { ExpirationBanner } from './ExpirationBanner';
|
||||||
import { CountdownTimer } from './CountdownTimer';
|
import { CountdownTimer } from './CountdownTimer';
|
||||||
import { analyticsService } from '../../services/analytics.service';
|
import { analyticsService } from '../../services/analytics.service';
|
||||||
|
import { api } from '../../config/api';
|
||||||
|
|
||||||
interface GalleryViewProps {
|
interface GalleryViewProps {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -25,15 +27,58 @@ interface GalleryViewProps {
|
|||||||
|
|
||||||
export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
||||||
const { logout } = useGalleryAuth();
|
const { logout } = useGalleryAuth();
|
||||||
|
const { setTheme } = useTheme();
|
||||||
const [viewMode, setViewMode] = useState<'all' | 'collages' | 'individual'>('all');
|
const [viewMode, setViewMode] = useState<'all' | 'collages' | 'individual'>('all');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [sortBy, setSortBy] = useState<'date' | 'name' | 'size'>('date');
|
const [sortBy, setSortBy] = useState<'date' | 'name' | 'size'>('date');
|
||||||
const [showSortMenu, setShowSortMenu] = useState(false);
|
const [showSortMenu, setShowSortMenu] = useState(false);
|
||||||
|
const [brandingSettings, setBrandingSettings] = useState<any>(null);
|
||||||
|
|
||||||
// Fetch photos
|
// Fetch photos
|
||||||
const { data, isLoading, error } = useGalleryPhotos(slug);
|
const { data, isLoading, error } = useGalleryPhotos(slug);
|
||||||
const downloadAllMutation = useDownloadAllPhotos();
|
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
|
// Calculate days until expiration
|
||||||
const daysUntilExpiration = differenceInDays(parseISO(event.expires_at), new Date());
|
const daysUntilExpiration = differenceInDays(parseISO(event.expires_at), new Date());
|
||||||
const showUrgentWarning = daysUntilExpiration <= 7;
|
const showUrgentWarning = daysUntilExpiration <= 7;
|
||||||
@@ -305,15 +350,25 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="mt-12 py-8 border-t border-neutral-200">
|
<footer className="mt-12 py-8 border-t border-neutral-200">
|
||||||
<div className="container text-center">
|
<div className="container text-center">
|
||||||
<p className="text-sm text-neutral-600">
|
{brandingSettings?.support_email && (
|
||||||
Need help? Contact the event organizer at{' '}
|
<p className="text-sm text-neutral-600 mb-2">
|
||||||
|
Need help? Contact us at{' '}
|
||||||
<a
|
<a
|
||||||
href={`mailto:${data.event.event_name}`}
|
href={`mailto:${brandingSettings.support_email}`}
|
||||||
className="text-primary-600 hover:text-primary-700"
|
className="text-primary-600 hover:text-primary-700"
|
||||||
>
|
>
|
||||||
support email
|
{brandingSettings.support_email}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-sm text-neutral-500">
|
||||||
|
{brandingSettings?.footer_text || '© 2024 Your Company. All rights reserved.'}
|
||||||
|
</p>
|
||||||
|
{brandingSettings?.company_name && brandingSettings?.company_tagline && (
|
||||||
|
<p className="text-xs text-neutral-400 mt-2">
|
||||||
|
{brandingSettings.company_name} - {brandingSettings.company_tagline}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,79 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Save, Eye, Palette } from 'lucide-react';
|
import { Save, Eye, Palette } from 'lucide-react';
|
||||||
import { toast } from 'react-toastify';
|
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 { ThemeCustomizer } from '../../components/admin/ThemeCustomizer';
|
||||||
import { useTheme, type ThemeConfig } from '../../contexts/ThemeContext';
|
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 = () => {
|
export const BrandingPage: React.FC = () => {
|
||||||
const { theme, setTheme, themeName, setThemeByName } = useTheme();
|
const { theme, setTheme, themeName, setThemeByName } = useTheme();
|
||||||
const [brandingSettings, setBrandingSettings] = useState({
|
const [brandingSettings, setBrandingSettings] = useState({
|
||||||
companyName: localStorage.getItem('branding-company-name') || '',
|
company_name: '',
|
||||||
companyTagline: localStorage.getItem('branding-company-tagline') || '',
|
company_tagline: '',
|
||||||
footerText: localStorage.getItem('branding-footer-text') || '© 2024 Your Company. All rights reserved.',
|
footer_text: '© 2024 Your Company. All rights reserved.',
|
||||||
supportEmail: localStorage.getItem('branding-support-email') || '',
|
support_email: '',
|
||||||
watermarkEnabled: localStorage.getItem('branding-watermark-enabled') === 'true',
|
watermark_enabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [currentTheme, setCurrentTheme] = useState<ThemeConfig>(theme);
|
const [currentTheme, setCurrentTheme] = useState<ThemeConfig>(theme);
|
||||||
const [currentThemeName, setCurrentThemeName] = useState(themeName);
|
const [currentThemeName, setCurrentThemeName] = useState(themeName);
|
||||||
const [isPreviewMode, setIsPreviewMode] = useState(false);
|
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) => {
|
const handleBrandingChange = (key: string, value: any) => {
|
||||||
setBrandingSettings(prev => ({ ...prev, [key]: value }));
|
setBrandingSettings(prev => ({ ...prev, [key]: value }));
|
||||||
};
|
};
|
||||||
@@ -37,16 +92,19 @@ export const BrandingPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = async () => {
|
||||||
// Save branding settings to localStorage
|
try {
|
||||||
Object.entries(brandingSettings).forEach(([key, value]) => {
|
// Save branding settings to database
|
||||||
localStorage.setItem(`branding-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`, String(value));
|
await brandingMutation.mutateAsync(brandingSettings);
|
||||||
});
|
|
||||||
|
|
||||||
// Apply theme
|
// Save theme settings to database
|
||||||
|
await themeMutation.mutateAsync(currentTheme);
|
||||||
|
|
||||||
|
// Apply theme globally
|
||||||
setTheme(currentTheme);
|
setTheme(currentTheme);
|
||||||
|
} catch (error) {
|
||||||
toast.success('Branding settings saved successfully!');
|
console.error('Failed to save settings:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePreview = () => {
|
const handlePreview = () => {
|
||||||
@@ -63,6 +121,14 @@ export const BrandingPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
|
<Loading size="lg" text="Loading branding settings..." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<div>
|
<div>
|
||||||
@@ -96,23 +162,23 @@ export const BrandingPage: React.FC = () => {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<Input
|
<Input
|
||||||
label="Company Name"
|
label="Company Name"
|
||||||
value={brandingSettings.companyName}
|
value={brandingSettings.company_name}
|
||||||
onChange={(e) => handleBrandingChange('companyName', e.target.value)}
|
onChange={(e) => handleBrandingChange('company_name', e.target.value)}
|
||||||
placeholder="Your Photography Studio"
|
placeholder="Your Photography Studio"
|
||||||
helperText="Displayed in email notifications and footers"
|
helperText="Displayed in email notifications and footers"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Company Tagline"
|
label="Company Tagline"
|
||||||
value={brandingSettings.companyTagline}
|
value={brandingSettings.company_tagline}
|
||||||
onChange={(e) => handleBrandingChange('companyTagline', e.target.value)}
|
onChange={(e) => handleBrandingChange('company_tagline', e.target.value)}
|
||||||
placeholder="Capturing moments that last forever"
|
placeholder="Capturing moments that last forever"
|
||||||
helperText="Optional tagline for branding"
|
helperText="Optional tagline for branding"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Support Email"
|
label="Support Email"
|
||||||
type="email"
|
type="email"
|
||||||
value={brandingSettings.supportEmail}
|
value={brandingSettings.support_email}
|
||||||
onChange={(e) => handleBrandingChange('supportEmail', e.target.value)}
|
onChange={(e) => handleBrandingChange('support_email', e.target.value)}
|
||||||
placeholder="support@yourcompany.com"
|
placeholder="support@yourcompany.com"
|
||||||
helperText="Contact email for gallery visitors"
|
helperText="Contact email for gallery visitors"
|
||||||
/>
|
/>
|
||||||
@@ -121,8 +187,8 @@ export const BrandingPage: React.FC = () => {
|
|||||||
Footer Text
|
Footer Text
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={brandingSettings.footerText}
|
value={brandingSettings.footer_text}
|
||||||
onChange={(e) => handleBrandingChange('footerText', e.target.value)}
|
onChange={(e) => handleBrandingChange('footer_text', e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||||
rows={2}
|
rows={2}
|
||||||
placeholder="© 2024 Your Company. All rights reserved."
|
placeholder="© 2024 Your Company. All rights reserved."
|
||||||
@@ -134,8 +200,8 @@ export const BrandingPage: React.FC = () => {
|
|||||||
<label className="flex items-center gap-3 cursor-pointer">
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={brandingSettings.watermarkEnabled}
|
checked={brandingSettings.watermark_enabled}
|
||||||
onChange={(e) => handleBrandingChange('watermarkEnabled', e.target.checked)}
|
onChange={(e) => handleBrandingChange('watermark_enabled', e.target.checked)}
|
||||||
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
|
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user