Files
picpeak/frontend/src/components/gallery/GalleryLayout.tsx
T
paul a3638fe954
Test and Lint / backend-test (push) Successful in 1m11s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m17s
Version and Release / version-bump (push) Successful in 43s
Version and Release / trigger-drone (push) Successful in 3s
fix: remove hardcoded localhost URLs for production deployment
- Add URL utility functions for building resource URLs
- Update all components to use relative URLs in production
- Add production deployment documentation
- Update nginx config to proxy all required endpoints
- Add .env.production.example with proper configuration
2025-07-13 21:14:26 +02:00

380 lines
15 KiB
TypeScript

import React from 'react';
import { Link } from 'react-router-dom';
import { Calendar, Clock, Download, LogOut } from 'lucide-react';
import { parseISO } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
import { Button } from '../common';
import { DynamicFavicon } from '../common/DynamicFavicon';
import { useTheme } from '../../contexts/ThemeContext';
import { buildResourceUrl } from '../../utils/url';
interface GalleryLayoutProps {
event: {
event_name: string;
event_type?: string;
event_date?: string;
expires_at?: string;
};
brandingSettings?: {
company_name?: string;
company_tagline?: string;
support_email?: string;
footer_text?: string;
favicon_url?: string;
logo_url?: string;
};
showLogout?: boolean;
onLogout?: () => void;
showDownloadAll?: boolean;
onDownloadAll?: () => void;
isDownloading?: boolean;
headerExtra?: React.ReactNode;
menuButton?: React.ReactNode;
children: React.ReactNode;
}
export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
event,
brandingSettings,
showLogout = false,
onLogout,
showDownloadAll = false,
onDownloadAll,
isDownloading = false,
headerExtra,
menuButton,
children,
}) => {
const { t } = useTranslation();
const { format } = useLocalizedDate();
const { theme } = useTheme();
const isNonGridLayout = theme.galleryLayout && theme.galleryLayout !== 'grid' && theme.galleryLayout !== 'hero';
const fontFamily = theme.fontFamily || 'Inter, sans-serif';
const headingFontFamily = theme.headingFontFamily || fontFamily;
return (
<div className="min-h-screen bg-neutral-50">
{/* Dynamic Favicon */}
<DynamicFavicon />
{/* Header structure */}
<header className={`bg-white border-b border-neutral-200 sticky top-0 z-40 ${isNonGridLayout || theme.galleryLayout === 'hero' ? 'shadow-sm' : ''}`}>
{/* For non-grid layouts (excluding hero) - keep the current structure */}
{isNonGridLayout && (
<div className="bg-neutral-50 border-b border-neutral-200">
<div className="container py-2">
<div className="flex items-center justify-between">
{/* Left side - Menu button and other header extras */}
<div className="flex items-center gap-3">
{menuButton}
{headerExtra}
</div>
{/* Right side - Download and Logout */}
<div className="flex items-center gap-3">
{/* Download all button */}
{showDownloadAll && onDownloadAll && (
<Button
variant="primary"
size="sm"
leftIcon={<Download className="w-4 h-4" />}
onClick={onDownloadAll}
isLoading={isDownloading}
>
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
<span className="sm:hidden">{t('common.download')}</span>
</Button>
)}
{/* Logout button */}
{showLogout && onLogout && (
<Button
variant="outline"
size="sm"
leftIcon={<LogOut className="w-4 h-4" />}
onClick={onLogout}
className="sm:min-w-0"
>
<span className="hidden sm:inline">{t('common.logout')}</span>
</Button>
)}
</div>
</div>
</div>
</div>
)}
{/* For grid layout - everything in one bar */}
{!isNonGridLayout && theme.galleryLayout !== 'hero' && (
<div className="container py-3">
<div className="flex items-center justify-between gap-2 sm:gap-4">
{/* Left side - Menu button, Logo */}
<div className="flex items-center gap-2 sm:gap-4 flex-shrink-0">
{/* Menu button */}
{menuButton && (
<div className="flex-shrink-0">
{menuButton}
</div>
)}
{/* Logo - Show custom logo or fallback to PicPeak logo */}
<div className="flex-shrink-0">
<img
src={brandingSettings?.logo_url ?
buildResourceUrl(brandingSettings.logo_url) :
'/picpeak-logo-transparent.png'
}
alt={brandingSettings?.company_name || 'PicPeak'}
className="h-8 sm:h-10 lg:h-12 w-auto object-contain"
/>
</div>
</div>
{/* Center - Event info */}
<div className="flex-1 min-w-0 text-center sm:text-left">
<h1
className="text-base sm:text-lg lg:text-xl font-bold text-neutral-900 leading-tight truncate"
style={{ fontFamily: headingFontFamily }}
>
{event.event_name}
</h1>
{(event.event_date || event.expires_at) && (
<div className="hidden sm:flex flex-wrap gap-x-3 gap-y-1 mt-1 text-xs sm:text-sm text-neutral-600">
{event.event_date && (
<span className="flex items-center">
<Calendar className="w-3 h-3 sm:w-4 sm:h-4 mr-1 flex-shrink-0" />
<span>{format(parseISO(event.event_date), 'PP')}</span>
</span>
)}
{event.expires_at && (
<span className="flex items-center">
<Clock className="w-3 h-3 sm:w-4 sm:h-4 mr-1 flex-shrink-0" />
<span>{t('gallery.expires')} {format(parseISO(event.expires_at), 'PP')}</span>
</span>
)}
</div>
)}
</div>
{/* Right side - Action buttons */}
<div className="flex items-center gap-2 sm:gap-3 flex-shrink-0">
{/* Extra header items (upload button, etc.) */}
{headerExtra && (
<div className="hidden sm:block">
{headerExtra}
</div>
)}
{/* Download all button - hidden on mobile when sidebar is shown */}
{showDownloadAll && onDownloadAll && (
<Button
variant="primary"
size="sm"
leftIcon={<Download className="w-4 h-4" />}
onClick={onDownloadAll}
isLoading={isDownloading}
className="hidden sm:flex"
>
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
<span className="sm:hidden">{t('common.download')}</span>
</Button>
)}
{/* Logout button */}
{showLogout && onLogout && (
<Button
variant="outline"
size="sm"
leftIcon={<LogOut className="w-4 h-4" />}
onClick={onLogout}
className="sm:min-w-0"
>
<span className="hidden sm:inline">{t('common.logout')}</span>
</Button>
)}
</div>
</div>
{/* Mobile dates row */}
{(event.event_date || event.expires_at) && (
<div className="flex sm:hidden justify-center gap-x-3 mt-2 text-xs text-neutral-600">
{event.event_date && (
<span className="flex items-center">
<Calendar className="w-3 h-3 mr-1 flex-shrink-0" />
<span>{format(parseISO(event.event_date), 'PP')}</span>
</span>
)}
{event.expires_at && (
<span className="flex items-center">
<Clock className="w-3 h-3 mr-1 flex-shrink-0" />
<span>{t('gallery.expires')} {format(parseISO(event.expires_at), 'PP')}</span>
</span>
)}
</div>
)}
</div>
)}
{/* For hero layout - minimal header with just menu and logout */}
{theme.galleryLayout === 'hero' && (
<div className="container py-3">
<div className="flex items-center justify-between">
{/* Left side - Menu button */}
<div className="flex items-center gap-3">
{menuButton}
{headerExtra}
</div>
{/* Right side - Action buttons */}
<div className="flex items-center gap-3 flex-shrink-0">
{/* Download all button */}
{showDownloadAll && onDownloadAll && (
<Button
variant="primary"
size="sm"
leftIcon={<Download className="w-4 h-4" />}
onClick={onDownloadAll}
isLoading={isDownloading}
>
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
<span className="sm:hidden">{t('common.download')}</span>
</Button>
)}
{/* Logout button */}
{showLogout && onLogout && (
<Button
variant="outline"
size="sm"
leftIcon={<LogOut className="w-4 h-4" />}
onClick={onLogout}
className="sm:min-w-0"
>
<span className="hidden sm:inline">{t('common.logout')}</span>
</Button>
)}
</div>
</div>
</div>
)}
</header>
{/* Hero Header for non-grid layouts (excluding hero layout which has its own) */}
{isNonGridLayout && (
<div
className="relative text-white overflow-hidden"
style={{
backgroundColor: theme.accentColor || '#22c55e',
backgroundImage: theme.backgroundPattern !== 'none'
? `url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23ffffff' fill-opacity='0.03'%3E%3Cpath d='M0 40L40 0H20L0 20M40 40V20L20 40'/%3E%3C/g%3E%3C/svg%3E")`
: undefined
}}
>
<div className="container py-12 sm:py-16 lg:py-20 relative z-10">
<div className="text-center max-w-4xl mx-auto">
{/* Logo - Show custom logo or fallback to PicPeak logo */}
<div className="mb-6">
<img
src={brandingSettings?.logo_url ?
buildResourceUrl(brandingSettings.logo_url) :
'/picpeak-logo-transparent.png'
}
alt={brandingSettings?.company_name || 'PicPeak'}
className="h-16 sm:h-20 lg:h-24 w-auto object-contain mx-auto"
style={{
filter: 'brightness(0) invert(1) drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3))'
}}
/>
</div>
{/* Event Name */}
<h1
className="text-3xl sm:text-4xl lg:text-5xl font-bold mb-4"
style={{
fontFamily: headingFontFamily,
textShadow: '0 2px 4px rgba(0, 0, 0, 0.3)'
}}
>
{event.event_name}
</h1>
{/* Event Details */}
{(event.event_date || event.expires_at) && (
<div className="flex flex-wrap items-center justify-center gap-4 sm:gap-6 text-white/80" style={{ textShadow: '0 1px 3px rgba(0, 0, 0, 0.3)' }}>
{event.event_date && (
<span className="flex items-center text-lg">
<Calendar className="w-5 h-5 mr-2" />
{format(parseISO(event.event_date), 'PP')}
</span>
)}
{event.expires_at && (
<span className="flex items-center text-lg">
<Clock className="w-5 h-5 mr-2" />
{t('gallery.expires')} {format(parseISO(event.expires_at), 'PP')}
</span>
)}
</div>
)}
</div>
</div>
{/* Decorative bottom wave */}
<div className="absolute bottom-0 left-0 right-0">
<svg className="w-full h-12 sm:h-16" viewBox="0 0 1200 120" preserveAspectRatio="none">
<path d="M0,60 C150,90 350,30 600,60 C850,90 1050,30 1200,60 L1200,120 L0,120 Z"
fill="var(--color-background, #fafafa)" />
</svg>
</div>
</div>
)}
{/* Main Content */}
<main className="container">{children}</main>
{/* Footer */}
<footer className="mt-8 sm:mt-12 py-6 sm:py-8 border-t border-neutral-200">
<div className="container text-center px-4">
{brandingSettings?.support_email && (
<p className="text-xs sm:text-sm text-neutral-600 mb-2">
{t('gallery.needHelp')}{' '}
<a
href={`mailto:${brandingSettings.support_email}`}
className="text-primary-600 hover:text-primary-700 break-all"
>
{brandingSettings.support_email}
</a>
</p>
)}
<p className="text-xs sm:text-sm text-neutral-500">
{brandingSettings?.footer_text || '© 2024 Your Company. All rights reserved.'} | Powered by <span className="font-semibold">PicPeak</span>
</p>
{brandingSettings?.company_name && brandingSettings?.company_tagline && (
<p className="text-xs text-neutral-400 mt-2">
{brandingSettings.company_name} - {brandingSettings.company_tagline}
</p>
)}
{/* Legal Links */}
<div className="mt-4 flex items-center justify-center gap-4">
<Link
to="/impressum"
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors"
>
{t('legal.impressum')}
</Link>
<span className="text-xs text-neutral-400">|</span>
<Link
to="/datenschutz"
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors"
>
{t('legal.datenschutz')}
</Link>
</div>
</div>
</footer>
</div>
);
};
GalleryLayout.displayName = 'GalleryLayout';