Files
picpeak/frontend/src/components/gallery/GalleryLayout.tsx
T
paul d8fb4c9565 Make gallery page fully mobile responsive
GalleryLayout improvements:
- Stack header elements vertically on mobile
- Hide company branding on small screens
- Make dates stack vertically
- Responsive text sizes and padding
- Icon-only logout button on mobile
- Improved button layout with proper wrapping

PhotoFilterBar improvements:
- Stack search and sort vertically on mobile
- Full-width sort button on mobile
- Horizontally scrollable category filters
- Responsive text sizes
- Mobile-friendly dropdown positioning

PhotoGrid improvements:
- Responsive selection controls
- Touch-friendly photo overlays
- Larger selection checkboxes on mobile
- Improved button text for small screens
- Responsive gaps between photos

Gallery grid CSS:
- Smaller gaps on mobile devices
- Maintains 2 columns on smallest screens

GalleryPage login:
- Responsive padding and margins
- Smaller text and icon sizes on mobile
- Better card spacing
- Responsive form elements

UserPhotoUpload modal:
- Full-screen modal on mobile (slides up from bottom)
- Responsive padding and text sizes
- Mobile-optimized upload area
- Sticky footer on mobile

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-09 09:53:24 +02:00

176 lines
6.8 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';
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;
children: React.ReactNode;
}
export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
event,
brandingSettings,
showLogout = false,
onLogout,
showDownloadAll = false,
onDownloadAll,
isDownloading = false,
headerExtra,
children,
}) => {
const { t } = useTranslation();
const { format } = useLocalizedDate();
return (
<div className="min-h-screen bg-neutral-50">
{/* Dynamic Favicon */}
<DynamicFavicon />
{/* Header */}
<header className="bg-white border-b border-neutral-200 sticky top-0 z-40">
<div className="container py-3 sm:py-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="flex items-start sm:items-center gap-3 sm:gap-4">
{/* Company logo */}
{brandingSettings?.logo_url && (
<div className="hidden sm:block pr-4 border-r border-neutral-200 flex-shrink-0">
<img
src={`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${brandingSettings.logo_url}`}
alt={brandingSettings.company_name || 'Company Logo'}
className="h-10 sm:h-12 w-auto object-contain"
/>
</div>
)}
{/* Company branding */}
{!brandingSettings?.logo_url && brandingSettings?.company_name && (
<div className="hidden sm:block pr-4 border-r border-neutral-200 flex-shrink-0">
<h2 className="text-base sm:text-lg font-semibold text-neutral-800">{brandingSettings.company_name}</h2>
{brandingSettings.company_tagline && (
<p className="hidden lg:block text-xs text-neutral-600">{brandingSettings.company_tagline}</p>
)}
</div>
)}
<div className="flex-1">
<h1 className="text-xl sm:text-2xl font-bold text-neutral-900 leading-tight">{event.event_name}</h1>
{(event.event_date || event.expires_at) && (
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4 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" />
<span className="truncate">{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" />
<span className="truncate">{t('gallery.expires')} {format(parseISO(event.expires_at), 'PP')}</span>
</span>
)}
</div>
)}
</div>
</div>
<div className="flex items-center gap-2 flex-wrap sm:flex-nowrap">
{headerExtra}
{showDownloadAll && onDownloadAll && (
<Button
variant="primary"
size="md"
leftIcon={<Download className="w-4 h-4" />}
onClick={onDownloadAll}
isLoading={isDownloading}
className="flex-1 sm:flex-initial text-sm sm:text-base"
>
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
<span className="sm:hidden">{t('common.downloadAll')}</span>
</Button>
)}
{showLogout && onLogout && (
<Button
variant="outline"
size="md"
leftIcon={<LogOut className="w-4 h-4" />}
onClick={onLogout}
className="text-sm sm:text-base"
>
<span className="hidden sm:inline">{t('common.logout')}</span>
<span className="sm:hidden"><LogOut className="w-4 h-4" /></span>
</Button>
)}
</div>
</div>
</div>
</header>
{/* 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.'}
</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';