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 <[email protected]>
This commit is contained in:
2025-07-10 22:19:15 +02:00
co-authored by Claude
parent 6438374258
commit 5328b4f73a
52 changed files with 3237 additions and 683 deletions
@@ -25,20 +25,40 @@ interface PhotoGridWithLayoutsProps {
photos: Photo[];
slug: string;
categoryId?: number | null;
isSelectionMode?: boolean;
selectedPhotos?: Set<number>;
onSelectionChange?: (photos: Set<number>) => void;
onToggleSelectionMode?: () => void;
showSelectionControls?: boolean;
eventName?: string;
eventLogo?: string | null;
}
export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
photos,
slug,
categoryId
categoryId,
isSelectionMode: parentSelectionMode,
selectedPhotos: parentSelectedPhotos,
onSelectionChange,
onToggleSelectionMode: parentToggleSelectionMode,
showSelectionControls = true,
eventName,
eventLogo
}) => {
const { t } = useTranslation();
const { theme } = useTheme();
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState<number | null>(null);
const [selectedPhotos, setSelectedPhotos] = useState<Set<number>>(new Set());
const [isSelectionMode, setIsSelectionMode] = useState(false);
const [localSelectedPhotos, setLocalSelectedPhotos] = useState<Set<number>>(new Set());
const [localSelectionMode, setLocalSelectionMode] = useState(false);
const downloadPhotoMutation = useDownloadPhoto();
// Use parent state if provided, otherwise use local state
const selectedPhotos = parentSelectedPhotos ?? localSelectedPhotos;
const isSelectionMode = parentSelectionMode ?? localSelectionMode;
const setSelectedPhotos = onSelectionChange ?? setLocalSelectedPhotos;
const toggleSelectionMode = parentToggleSelectionMode ?? (() => setLocalSelectionMode(!localSelectionMode));
// Clear selection when category changes
useEffect(() => {
setSelectedPhotos(new Set());
@@ -71,10 +91,6 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
});
};
const toggleSelectionMode = () => {
setIsSelectionMode(!isSelectionMode);
setSelectedPhotos(new Set());
};
const selectAll = () => {
setSelectedPhotos(new Set(photos.map(p => p.id)));
@@ -112,7 +128,11 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
// Clear selection after download
setSelectedPhotos(new Set());
setIsSelectionMode(false);
if (parentToggleSelectionMode) {
parentToggleSelectionMode();
} else {
setLocalSelectionMode(false);
}
} catch (error) {
toastify.error(t('gallery.downloadError'));
}
@@ -138,6 +158,8 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
selectedPhotos,
isSelectionMode,
onPhotoSelect: handlePhotoSelect,
eventName,
eventLogo,
};
let LayoutComponent;
@@ -163,8 +185,8 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
return (
<>
{/* Selection Mode Controls - Not shown for carousel layout */}
{photos.length > 1 && galleryLayout !== 'carousel' && (
{/* Selection Mode Controls - Not shown for carousel layout or when controls are hidden */}
{showSelectionControls && photos.length > 1 && galleryLayout !== 'carousel' && (
<div className="mb-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex items-center gap-2">
<Button
@@ -181,7 +203,7 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
variant="ghost"
size="sm"
onClick={() => {
setIsSelectionMode(true);
toggleSelectionMode();
selectAll();
}}
className="text-xs sm:text-sm"