Merge pull request #241 from the-luap/fix/external-media-dimensions-and-email-colors
fix: external media dimensions, theme race condition, email color customization
This commit is contained in:
@@ -16,7 +16,6 @@ import { UserPhotoUpload } from './UserPhotoUpload';
|
||||
import type { FilterType } from './GalleryFilter';
|
||||
import { analyticsService } from '../../services/analytics.service';
|
||||
import { useDevToolsProtection } from '../../hooks/useDevToolsProtection';
|
||||
import { GALLERY_THEME_PRESETS } from '../../types/theme.types';
|
||||
import { api } from '../../config/api';
|
||||
import { Upload, Menu } from 'lucide-react';
|
||||
import { galleryService } from '../../services/gallery.service';
|
||||
@@ -44,7 +43,7 @@ interface GalleryViewProps {
|
||||
export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
||||
const { t } = useTranslation();
|
||||
const { logout } = useGalleryAuth();
|
||||
const { setTheme, theme } = useTheme();
|
||||
const { theme } = useTheme();
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<number | string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortBy, setSortBy] = useState<'date' | 'name' | 'size' | 'rating' | 'capture_date'>('date');
|
||||
@@ -275,62 +274,6 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
||||
setStaticHeroPhoto(defaultHeroPhoto);
|
||||
}, [selectedCategoryId, data?.categories, data?.photos, defaultHeroPhoto]);
|
||||
|
||||
// Apply theme when settings are loaded
|
||||
useEffect(() => {
|
||||
if (settingsData && data?.event) {
|
||||
let themeToApply = null;
|
||||
const fullEvent = data.event; // Use the full event data from API
|
||||
|
||||
if (fullEvent.color_theme) {
|
||||
try {
|
||||
// Check if it's a valid JSON string
|
||||
if (fullEvent.color_theme.startsWith('{')) {
|
||||
const eventTheme = JSON.parse(fullEvent.color_theme);
|
||||
themeToApply = eventTheme;
|
||||
} else {
|
||||
// Handle legacy theme names - check if it's a preset
|
||||
const preset = GALLERY_THEME_PRESETS[fullEvent.color_theme];
|
||||
if (preset) {
|
||||
themeToApply = preset.config;
|
||||
} else {
|
||||
// Unknown theme name, fall back to global theme
|
||||
if (settingsData.theme_config) {
|
||||
themeToApply = settingsData.theme_config;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Invalid theme format - use default
|
||||
// Fall back to global theme
|
||||
if (settingsData.theme_config) {
|
||||
themeToApply = settingsData.theme_config;
|
||||
}
|
||||
}
|
||||
} else if (settingsData.theme_config) {
|
||||
// No event theme, use global theme
|
||||
themeToApply = settingsData.theme_config;
|
||||
}
|
||||
|
||||
// Apply theme with a small delay to ensure it overrides any global theme
|
||||
if (themeToApply) {
|
||||
// Use setTimeout to ensure this runs after any global theme application
|
||||
const timer = setTimeout(() => {
|
||||
// If there's a hero photo, add it to gallery settings
|
||||
if (fullEvent.hero_photo_id && themeToApply.gallerySettings) {
|
||||
themeToApply.gallerySettings.heroImageId = fullEvent.hero_photo_id;
|
||||
// Apply hero photo ID to existing gallery settings
|
||||
} else if (fullEvent.hero_photo_id) {
|
||||
themeToApply.gallerySettings = { heroImageId: fullEvent.hero_photo_id };
|
||||
// Create gallery settings with hero photo ID
|
||||
}
|
||||
setTheme(themeToApply);
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}, [settingsData, data, setTheme]); // Use data instead of event prop
|
||||
|
||||
// Calculate days until expiration (null means never expires)
|
||||
const daysUntilExpiration = event.expires_at
|
||||
? differenceInDays(parseISO(event.expires_at), new Date())
|
||||
@@ -619,6 +562,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
||||
headerStyle={data?.event?.header_style || theme.headerStyle}
|
||||
heroDividerStyle={data?.event?.hero_divider_style || theme.heroDividerStyle || 'wave'}
|
||||
heroImageAnchor={data?.event?.hero_image_anchor || 'center'}
|
||||
welcomeMessage={event.welcome_message}
|
||||
onLogout={logout}
|
||||
/>
|
||||
|
||||
@@ -794,6 +738,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
||||
headerStyle={data?.event?.header_style || theme.headerStyle}
|
||||
heroDividerStyle={data?.event?.hero_divider_style || theme.heroDividerStyle || 'wave'}
|
||||
heroImageAnchor={data?.event?.hero_image_anchor || 'center'}
|
||||
welcomeMessage={event.welcome_message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -64,6 +64,8 @@ interface PhotoGridWithLayoutsProps {
|
||||
heroDividerStyle?: HeroDividerStyle;
|
||||
// Hero image anchor position (#162) – keyword or "X% Y%" focal point
|
||||
heroImageAnchor?: string;
|
||||
// Welcome message (per-event) for layouts that display it
|
||||
welcomeMessage?: string;
|
||||
// Logout callback for full-page layouts
|
||||
onLogout?: () => void;
|
||||
}
|
||||
@@ -97,6 +99,7 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
|
||||
headerStyle,
|
||||
heroDividerStyle = 'wave',
|
||||
heroImageAnchor = 'center',
|
||||
welcomeMessage,
|
||||
onLogout
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -226,6 +229,7 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
|
||||
heroLogoVisible,
|
||||
heroLogoSize,
|
||||
heroLogoPosition,
|
||||
welcomeMessage,
|
||||
onLogout,
|
||||
};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ interface CategoryScene {
|
||||
|
||||
interface GalleryStoryLayoutProps extends BaseGalleryLayoutProps {
|
||||
heroPhotoOverride?: Photo | null;
|
||||
welcomeMessage?: string;
|
||||
}
|
||||
|
||||
export const GalleryStoryLayout: React.FC<GalleryStoryLayoutProps> = ({
|
||||
@@ -55,6 +56,7 @@ export const GalleryStoryLayout: React.FC<GalleryStoryLayoutProps> = ({
|
||||
feedbackEnabled = false,
|
||||
feedbackOptions,
|
||||
heroPhotoOverride,
|
||||
welcomeMessage,
|
||||
onLogout
|
||||
}) => {
|
||||
// These props are passed by parent but we use our own feedback system, so mark as intentionally unused
|
||||
@@ -348,7 +350,7 @@ export const GalleryStoryLayout: React.FC<GalleryStoryLayoutProps> = ({
|
||||
<footer className="story-footer">
|
||||
<h2 className="story-footer-title">{t('gallery.thankYou', 'Thank You')}</h2>
|
||||
<p className="story-footer-text">
|
||||
{t('gallery.thankYouMessage', 'For being part of our story and making our special day unforgettable.')}
|
||||
{welcomeMessage || t('gallery.thankYouMessage', 'For being part of our story and making our special day unforgettable.')}
|
||||
</p>
|
||||
{allowDownloads && (
|
||||
<button className="story-footer-btn" onClick={handleDownloadAll}>
|
||||
|
||||
@@ -1963,6 +1963,13 @@
|
||||
"required": "erforderlich",
|
||||
"ignoreSslErrors": "SSL/TLS-Zertifikatfehler ignorieren",
|
||||
"ignoreSslWarning": "Warnung: Das Deaktivieren der Zertifikatüberprüfung macht die Verbindung anfällig für Man-in-the-Middle-Angriffe. Aktivieren Sie dies nur, wenn Sie dem SMTP-Server vertrauen und die Sicherheitsrisiken verstehen.",
|
||||
"brandingTitle": "E-Mail-Branding",
|
||||
"brandingDescription": "Passen Sie die Farben in E-Mail-Vorlagen an. Änderungen gelten für Kopfzeile, Schaltflächen, Links und Fußzeilen-Hintergrund.",
|
||||
"primaryColor": "Primärfarbe",
|
||||
"primaryColorHint": "Wird für Kopfzeile, Schaltflächen und Links verwendet",
|
||||
"secondaryColor": "Fußzeilen-Hintergrund",
|
||||
"secondaryColorHint": "Wird für den Hintergrund der Fußzeile verwendet",
|
||||
"saveEmailColors": "E-Mail-Farben speichern",
|
||||
"editor": {
|
||||
"bold": "Fett",
|
||||
"italic": "Kursiv",
|
||||
|
||||
@@ -1660,6 +1660,13 @@
|
||||
"required": "required",
|
||||
"ignoreSslErrors": "Ignore SSL/TLS certificate errors",
|
||||
"ignoreSslWarning": "Warning: Disabling certificate verification makes the connection vulnerable to man-in-the-middle attacks. Only enable this if you trust the SMTP server and understand the security implications.",
|
||||
"brandingTitle": "Email Branding",
|
||||
"brandingDescription": "Customize the colors used in email templates. Changes apply to the header bar, buttons, links, and footer background.",
|
||||
"primaryColor": "Primary Color",
|
||||
"primaryColorHint": "Used for header, buttons, and links",
|
||||
"secondaryColor": "Footer Background",
|
||||
"secondaryColorHint": "Used for footer section background",
|
||||
"saveEmailColors": "Save Email Colors",
|
||||
"editor": {
|
||||
"bold": "Bold",
|
||||
"italic": "Italic",
|
||||
|
||||
@@ -1636,6 +1636,13 @@
|
||||
"required": "obrigatorio",
|
||||
"ignoreSslErrors": "Ignorar erros de certificado SSL/TLS",
|
||||
"ignoreSslWarning": "Aviso: Desabilitar verificacao de certificado torna a conexao vulneravel a ataques man-in-the-middle. Ative somente se voce confia no servidor SMTP e entende as implicacoes de seguranca.",
|
||||
"brandingTitle": "Identidade Visual do E-mail",
|
||||
"brandingDescription": "Personalize as cores dos modelos de e-mail. As alteracoes se aplicam ao cabecalho, botoes, links e fundo do rodape.",
|
||||
"primaryColor": "Cor Primaria",
|
||||
"primaryColorHint": "Usada no cabecalho, botoes e links",
|
||||
"secondaryColor": "Fundo do Rodape",
|
||||
"secondaryColorHint": "Usada no fundo da secao de rodape",
|
||||
"saveEmailColors": "Salvar Cores do E-mail",
|
||||
"editor": {
|
||||
"bold": "Negrito",
|
||||
"italic": "Italico",
|
||||
|
||||
@@ -1636,6 +1636,13 @@
|
||||
"required": "обязательно",
|
||||
"ignoreSslErrors": "Игнорировать ошибки SSL/TLS-сертификата",
|
||||
"ignoreSslWarning": "Предупреждение: Отключение проверки сертификата делает соединение уязвимым для атак «человек посередине». Включайте только если доверяете SMTP-серверу и понимаете риски.",
|
||||
"brandingTitle": "Оформление писем",
|
||||
"brandingDescription": "Настройте цвета в шаблонах писем. Изменения применяются к шапке, кнопкам, ссылкам и фону подвала.",
|
||||
"primaryColor": "Основной цвет",
|
||||
"primaryColorHint": "Используется для шапки, кнопок и ссылок",
|
||||
"secondaryColor": "Фон подвала",
|
||||
"secondaryColorHint": "Используется для фона подвала письма",
|
||||
"saveEmailColors": "Сохранить цвета",
|
||||
"editor": {
|
||||
"bold": "Жирный",
|
||||
"italic": "Курсив",
|
||||
|
||||
@@ -156,6 +156,15 @@ export const GalleryPage: React.FC = () => {
|
||||
themeToApply = settingsData.theme_config;
|
||||
}
|
||||
|
||||
// Inject hero photo ID into theme gallery settings
|
||||
if (themeToApply && galleryInfo.hero_photo_id) {
|
||||
if (themeToApply.gallerySettings) {
|
||||
themeToApply.gallerySettings.heroImageId = galleryInfo.hero_photo_id;
|
||||
} else {
|
||||
themeToApply.gallerySettings = { heroImageId: galleryInfo.hero_photo_id };
|
||||
}
|
||||
}
|
||||
|
||||
// Apply theme
|
||||
if (themeToApply) {
|
||||
setTheme(themeToApply);
|
||||
|
||||
@@ -17,8 +17,10 @@ import { toast } from 'react-toastify';
|
||||
import { Button, Input, Card, Loading } from '../../components/common';
|
||||
import { EmailPreviewModal } from '../../components/admin/EmailPreviewModal';
|
||||
import { EmailTemplateEditor } from '../../components/admin/EmailTemplateEditor';
|
||||
import { Palette } from 'lucide-react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { emailService, type EmailConfig, type EmailTemplate } from '../../services/email.service';
|
||||
import { settingsService } from '../../services/settings.service';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const defaultTemplateKeys = [
|
||||
@@ -99,6 +101,8 @@ export const EmailConfigPage: React.FC = () => {
|
||||
htmlContent: '',
|
||||
textContent: ''
|
||||
});
|
||||
const [emailPrimaryColor, setEmailPrimaryColor] = useState('#5C8762');
|
||||
const [emailSecondaryColor, setEmailSecondaryColor] = useState('#f9f9f9');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// SMTP Configuration state
|
||||
@@ -119,6 +123,19 @@ export const EmailConfigPage: React.FC = () => {
|
||||
queryFn: () => emailService.getConfig(),
|
||||
});
|
||||
|
||||
// Fetch email branding colors from app settings
|
||||
const { data: allSettings } = useQuery({
|
||||
queryKey: ['admin-settings'],
|
||||
queryFn: () => settingsService.getAllSettings(),
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (allSettings) {
|
||||
if (allSettings.email_primary_color) setEmailPrimaryColor(allSettings.email_primary_color);
|
||||
if (allSettings.email_secondary_color) setEmailSecondaryColor(allSettings.email_secondary_color);
|
||||
}
|
||||
}, [allSettings]);
|
||||
|
||||
// Fetch email templates
|
||||
const { data: templates = [], isLoading: templatesLoading } = useQuery({
|
||||
queryKey: ['email-templates'],
|
||||
@@ -186,6 +203,25 @@ export const EmailConfigPage: React.FC = () => {
|
||||
}
|
||||
});
|
||||
|
||||
const saveEmailColorsMutation = useMutation({
|
||||
mutationFn: (colors: { email_primary_color: string; email_secondary_color: string }) =>
|
||||
settingsService.updateSettings(colors),
|
||||
onSuccess: () => {
|
||||
toast.success(t('toast.saveSuccess'));
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-settings'] });
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('toast.saveError'));
|
||||
}
|
||||
});
|
||||
|
||||
const handleSaveEmailColors = () => {
|
||||
saveEmailColorsMutation.mutate({
|
||||
email_primary_color: emailPrimaryColor,
|
||||
email_secondary_color: emailSecondaryColor,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveSmtp = () => {
|
||||
// Validate SMTP config
|
||||
if (!smtpConfig.smtp_host || !smtpConfig.smtp_port || !smtpConfig.from_email) {
|
||||
@@ -498,6 +534,76 @@ export const EmailConfigPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Branding - below SMTP settings */}
|
||||
{activeTab === 'smtp' && (
|
||||
<div className="mt-6">
|
||||
<Card padding="md">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Palette className="w-5 h-5 text-neutral-500" />
|
||||
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">{t('email.brandingTitle')}</h2>
|
||||
</div>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-6">{t('email.brandingDescription')}</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
|
||||
{t('email.primaryColor')}
|
||||
</label>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-2">{t('email.primaryColorHint')}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="color"
|
||||
value={emailPrimaryColor}
|
||||
onChange={(e) => setEmailPrimaryColor(e.target.value)}
|
||||
className="w-10 h-10 rounded border border-neutral-300 dark:border-neutral-600 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={emailPrimaryColor}
|
||||
onChange={(e) => setEmailPrimaryColor(e.target.value)}
|
||||
className="w-32"
|
||||
placeholder="#5C8762"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
|
||||
{t('email.secondaryColor')}
|
||||
</label>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-2">{t('email.secondaryColorHint')}</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="color"
|
||||
value={emailSecondaryColor}
|
||||
onChange={(e) => setEmailSecondaryColor(e.target.value)}
|
||||
className="w-10 h-10 rounded border border-neutral-300 dark:border-neutral-600 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={emailSecondaryColor}
|
||||
onChange={(e) => setEmailSecondaryColor(e.target.value)}
|
||||
className="w-32"
|
||||
placeholder="#f9f9f9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSaveEmailColors}
|
||||
isLoading={saveEmailColorsMutation.isPending}
|
||||
leftIcon={<Save className="w-5 h-5" />}
|
||||
>
|
||||
{t('email.saveEmailColors')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Templates Tab */}
|
||||
{activeTab === 'templates' && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
@@ -242,6 +242,8 @@ export const settingsService = {
|
||||
endpoint = '/admin/settings/branding';
|
||||
} else if (firstKey?.startsWith('seo_')) {
|
||||
endpoint = '/admin/settings/seo';
|
||||
} else if (firstKey?.startsWith('email_')) {
|
||||
endpoint = '/admin/settings/general';
|
||||
}
|
||||
|
||||
await api.put(endpoint, settings);
|
||||
|
||||
Reference in New Issue
Block a user