Enhance email templates with clickable links, branding, and improved styling

- Add clickable gallery links in all email templates
- Include application logo in email header and footer (custom or PicPeak default)
- Redesign emails with professional styling matching gallery login page
  - Gray background with white content box
  - PicPeak green header with centered logo
  - Clean typography and proper spacing
  - Responsive design for mobile devices
  - Styled call-to-action buttons
  - Footer with branding and copyright
- Update email processor to fetch branding settings dynamically
- Use proper API URLs for logo images in emails

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-10 22:19:15 +02:00
parent 6438374258
commit 5328b4f73a
52 changed files with 3237 additions and 683 deletions
@@ -11,6 +11,7 @@ import {
EyeOff
} from 'lucide-react';
import { format, addDays } from 'date-fns';
import { enUS, de } from 'date-fns/locale';
import { toast } from 'react-toastify';
import { Button, Input, Card } from '../../components/common';
@@ -18,6 +19,7 @@ import { ThemeCustomizerEnhanced } from '../../components/admin/ThemeCustomizerE
import { useMutation, useQuery } from '@tanstack/react-query';
import { eventsService } from '../../services/events.service';
import { categoriesService } from '../../services/categories.service';
import { settingsService } from '../../services/settings.service';
import { useTranslation } from 'react-i18next';
import { ThemeConfig, GALLERY_THEME_PRESETS } from '../../types/theme.types';
@@ -25,6 +27,7 @@ interface FormData {
event_type: string;
event_name: string;
event_date: string;
host_name: string;
host_email: string;
admin_email: string;
password: string;
@@ -53,7 +56,7 @@ const EVENT_TYPES = [
export const CreateEventPageEnhanced: React.FC = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const { t, i18n } = useTranslation();
const isMountedRef = useRef(true);
const [showThemeCustomizer, setShowThemeCustomizer] = useState(false);
// const [showPreview, setShowPreview] = useState(false);
@@ -68,6 +71,7 @@ export const CreateEventPageEnhanced: React.FC = () => {
event_type: 'wedding',
event_name: '',
event_date: format(new Date(), 'yyyy-MM-dd'),
host_name: '',
host_email: '',
admin_email: '',
password: '',
@@ -89,6 +93,22 @@ export const CreateEventPageEnhanced: React.FC = () => {
queryFn: () => categoriesService.getGlobalCategories()
});
// Fetch default settings
const { data: settings } = useQuery({
queryKey: ['admin-settings'],
queryFn: () => settingsService.getAllSettings()
});
// Update default expiration days when settings are loaded
useEffect(() => {
if (settings?.general_default_expiration_days) {
setFormData(prev => ({
...prev,
expires_in_days: settings.general_default_expiration_days
}));
}
}, [settings]);
// Update theme when event type changes
useEffect(() => {
const recommendedPreset = EVENT_TYPE_PRESETS[formData.event_type];
@@ -111,8 +131,21 @@ export const CreateEventPageEnhanced: React.FC = () => {
},
onError: (error: any) => {
console.error('Create event error:', error);
console.error('Error response:', error.response?.data);
console.error('Error status:', error.response?.status);
console.error('Full error object:', JSON.stringify(error.response, null, 2));
const errorMessage = error.response?.data?.error || error.message || t('errors.eventCreationFailed');
toast.error(errorMessage);
// If validation errors exist, show them
if (error.response?.data?.errors) {
const validationErrors = error.response.data.errors;
console.error('Validation errors:', validationErrors);
validationErrors.forEach((err: any) => {
toast.error(`${err.param}: ${err.msg}`);
});
} else {
toast.error(errorMessage);
}
},
});
@@ -127,6 +160,10 @@ export const CreateEventPageEnhanced: React.FC = () => {
newErrors.event_date = t('validation.eventDateRequired');
}
if (!formData.host_name) {
newErrors.host_name = t('validation.hostNameRequired');
}
if (!formData.host_email) {
newErrors.host_email = t('validation.hostEmailRequired');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.host_email)) {
@@ -164,10 +201,11 @@ export const CreateEventPageEnhanced: React.FC = () => {
return;
}
createMutation.mutate({
const payload = {
event_type: formData.event_type,
event_name: formData.event_name,
event_date: formData.event_date,
host_name: formData.host_name,
host_email: formData.host_email,
admin_email: formData.admin_email,
password: formData.password,
@@ -176,7 +214,10 @@ export const CreateEventPageEnhanced: React.FC = () => {
expiration_days: formData.expires_in_days,
allow_user_uploads: formData.allow_user_uploads,
upload_category_id: formData.upload_category_id,
});
};
console.log('Submitting payload:', payload);
createMutation.mutate(payload);
};
const handleInputChange = (field: keyof FormData) => (
@@ -354,16 +395,27 @@ export const CreateEventPageEnhanced: React.FC = () => {
{t('events.accessAndSecurity')}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
type="email"
label={t('events.hostEmail')}
placeholder={t('events.hostEmailPlaceholder')}
value={formData.host_email}
onChange={handleInputChange('host_email')}
error={errors.host_email}
leftIcon={<Mail className="w-5 h-5" />}
/>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label={t('events.hostName')}
placeholder={t('events.hostNamePlaceholder')}
value={formData.host_name}
onChange={handleInputChange('host_name')}
error={errors.host_name}
leftIcon={<Calendar className="w-5 h-5" />}
/>
<Input
type="email"
label={t('events.hostEmail')}
placeholder={t('events.hostEmailPlaceholder')}
value={formData.host_email}
onChange={handleInputChange('host_email')}
error={errors.host_email}
leftIcon={<Mail className="w-5 h-5" />}
/>
</div>
<Input
type="email"
@@ -411,22 +463,23 @@ export const CreateEventPageEnhanced: React.FC = () => {
<label className="block text-sm font-medium text-neutral-700 mb-2">
{t('events.galleryExpiration')}
</label>
<div className="flex items-center gap-4">
<Input
type="number"
value={formData.expires_in_days}
onChange={handleInputChange('expires_in_days')}
error={errors.expires_in_days}
min={1}
max={365}
leftIcon={<Clock className="w-5 h-5" />}
className="w-32"
/>
<div className="flex items-center gap-2">
<div className="w-32">
<Input
type="number"
value={formData.expires_in_days}
onChange={handleInputChange('expires_in_days')}
error={errors.expires_in_days}
min={1}
max={365}
leftIcon={<Clock className="w-5 h-5" />}
/>
</div>
<span className="text-sm text-neutral-600">{t('events.daysAfterEvent')}</span>
</div>
{formData.event_date && (
<p className="mt-2 text-sm text-neutral-500">
{t('events.expiresOn')}: {format(addDays(new Date(formData.event_date), formData.expires_in_days), 'PPP')}
{t('events.expiresOn')}: {format(addDays(new Date(formData.event_date), formData.expires_in_days), 'PPP', { locale: i18n.language === 'de' ? de : enUS })}
</p>
)}
</div>