refactor: rename project from wedding-photo-sharing to PicPeak
Create Release / check-version-change (push) Successful in 2m25s
Automatic Version Bump / version-bump (push) Failing after 8m2s
Create Release / create-release (push) Has been skipped

- Update Docker image names and network configurations
- Rename package.json project names to picpeak-backend/frontend
- Update CI/CD configurations (Drone CI and GitHub Actions)
- Update documentation and setup scripts
- Update application branding in source code
- Change default database name to picpeak
- Update PM2 ecosystem config

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-12 09:22:14 +02:00
parent d065132bb7
commit 288b0c25e6
42 changed files with 1116 additions and 422 deletions
@@ -29,6 +29,7 @@ interface GalleryLayoutProps {
onDownloadAll?: () => void;
isDownloading?: boolean;
headerExtra?: React.ReactNode;
menuButton?: React.ReactNode;
children: React.ReactNode;
}
@@ -41,6 +42,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
onDownloadAll,
isDownloading = false,
headerExtra,
menuButton,
children,
}) => {
const { t } = useTranslation();
@@ -65,6 +67,7 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
<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>
@@ -106,8 +109,15 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
{!isNonGridLayout && theme.galleryLayout !== 'hero' && (
<div className="container py-3">
<div className="flex items-center justify-between gap-2 sm:gap-4">
{/* Left side - Logo and menu button on mobile */}
{/* 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
@@ -119,11 +129,6 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
className="h-8 sm:h-10 lg:h-12 w-auto object-contain"
/>
</div>
{/* Menu button on mobile */}
<div className="flex-shrink-0 sm:hidden">
{headerExtra}
</div>
</div>
{/* Center - Event info */}
@@ -154,10 +159,12 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
{/* Right side - Action buttons */}
<div className="flex items-center gap-2 sm:gap-3 flex-shrink-0">
{/* Menu button on desktop */}
<div className="hidden sm:block">
{headerExtra}
</div>
{/* 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 && (
@@ -214,7 +221,8 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
<div className="container py-3">
<div className="flex items-center justify-between">
{/* Left side - Menu button */}
<div className="flex-shrink-0">
<div className="flex items-center gap-3">
{menuButton}
{headerExtra}
</div>
@@ -273,21 +281,27 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
'/picpeak-logo-transparent.png'
}
alt={brandingSettings?.company_name || 'PicPeak'}
className="h-16 sm:h-20 lg:h-24 w-auto object-contain mx-auto filter brightness-0 invert"
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 }}
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">
<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" />
+59 -32
View File
@@ -18,6 +18,7 @@ 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';
import { useWatermarkSettings } from '../../hooks/useWatermarkSettings';
interface GalleryViewProps {
slug: string;
@@ -48,9 +49,21 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
const [isSelectionMode, setIsSelectionMode] = useState(false);
const [selectedPhotos, setSelectedPhotos] = useState<Set<number>>(new Set());
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const { watermarkEnabled } = useWatermarkSettings();
// Fetch photos
const { data, isLoading, error } = useGalleryPhotos(slug);
// Debug logging
useEffect(() => {
console.log('Event prop:', event);
console.log('Event prop hero_photo_id:', event?.hero_photo_id);
if (data) {
console.log('Gallery data:', data);
console.log('Event data from API:', data.event);
console.log('Hero photo ID from API:', data.event?.hero_photo_id);
}
}, [data, event]);
const downloadAllMutation = useDownloadAllPhotos();
// Handle window resize
@@ -88,18 +101,19 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
// Apply theme when settings are loaded
useEffect(() => {
if (settingsData && event) {
if (settingsData && data?.event) {
let themeToApply = null;
const fullEvent = data.event; // Use the full event data from API
if (event.color_theme) {
if (fullEvent.color_theme) {
try {
// Check if it's a valid JSON string
if (event.color_theme.startsWith('{')) {
const eventTheme = JSON.parse(event.color_theme);
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[event.color_theme];
const preset = GALLERY_THEME_PRESETS[fullEvent.color_theme];
if (preset) {
themeToApply = preset.config;
} else {
@@ -126,10 +140,12 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
// 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 (event.hero_photo_id && themeToApply.gallerySettings) {
themeToApply.gallerySettings.heroImageId = event.hero_photo_id;
} else if (event.hero_photo_id) {
themeToApply.gallerySettings = { heroImageId: event.hero_photo_id };
if (fullEvent.hero_photo_id && themeToApply.gallerySettings) {
themeToApply.gallerySettings.heroImageId = fullEvent.hero_photo_id;
console.log('Setting hero photo ID in existing gallery settings:', fullEvent.hero_photo_id);
} else if (fullEvent.hero_photo_id) {
themeToApply.gallerySettings = { heroImageId: fullEvent.hero_photo_id };
console.log('Creating gallery settings with hero photo ID:', fullEvent.hero_photo_id);
}
setTheme(themeToApply);
}, 0);
@@ -137,7 +153,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
return () => clearTimeout(timer);
}
}
}, [settingsData, event, setTheme]); // Include all dependencies
}, [settingsData, data, setTheme]); // Use data instead of event prop
// Calculate days until expiration
const daysUntilExpiration = differenceInDays(parseISO(event.expires_at), new Date());
@@ -175,8 +191,17 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
}
});
// Transform URLs for watermarks if enabled
if (watermarkEnabled) {
photos = photos.map(photo => ({
...photo,
url: `/api/gallery/${slug}/photo/${photo.id}`,
thumbnail_url: `/api/gallery/${slug}/photo/${photo.id}` // Use watermarked version for thumbnails too
}));
}
return photos;
}, [data?.photos, selectedCategoryId, searchTerm, sortBy]);
}, [data?.photos, selectedCategoryId, searchTerm, sortBy, watermarkEnabled, slug]);
const handleDownloadAll = () => {
downloadAllMutation.mutate(slug);
@@ -307,7 +332,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
totalPhotos={data?.photos.length || 0}
isMobile={isMobile}
galleryLayout={theme.galleryLayout}
allowUploads={event.allow_user_uploads}
allowUploads={data?.event?.allow_user_uploads || event?.allow_user_uploads || false}
onUploadClick={() => setShowUploadModal(true)}
/>
) : null}
@@ -320,23 +345,24 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
showDownloadAll={!showSidebar}
onDownloadAll={handleDownloadAll}
isDownloading={downloadAllMutation.isPending}
menuButton={showSidebar ? (
<Button
variant="ghost"
size="sm"
leftIcon={<Menu className="w-4 h-4" />}
onClick={() => setSidebarOpen(!sidebarOpen)}
aria-label={t('gallery.toggleMenu')}
>
<span className="hidden sm:inline">{t('common.menu')}</span>
</Button>
) : undefined}
headerExtra={(() => {
const items = [];
if (showSidebar) {
items.push(
<Button
key="menu-button"
variant="ghost"
size="sm"
leftIcon={<Menu className="w-4 h-4" />}
onClick={() => setSidebarOpen(!sidebarOpen)}
aria-label={t('gallery.toggleMenu')}
>
<span className="hidden sm:inline">{t('common.menu')}</span>
</Button>
);
}
console.log('Header extra - data loaded:', !!data);
console.log('Header extra - allow uploads:', data?.event?.allow_user_uploads);
console.log('Header extra - showSidebar:', showSidebar);
console.log('Header extra - isMobile:', isMobile);
if (daysUntilExpiration <= 1 && daysUntilExpiration > 0) {
items.push(
@@ -345,7 +371,8 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
}
// Upload button only on desktop when sidebar is shown
if (event.allow_user_uploads && showSidebar && !isMobile) {
const allowUploads = data?.event?.allow_user_uploads || event?.allow_user_uploads;
if (allowUploads && showSidebar && !isMobile) {
items.push(
<Button
key="upload-button"
@@ -360,7 +387,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
}
// Upload button for non-sidebar layouts
if (event.allow_user_uploads && !showSidebar) {
if (allowUploads && !showSidebar) {
items.push(
<Button
key="upload-button"
@@ -376,7 +403,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
);
}
return <>{items}</>;
return items.length > 0 ? <>{items}</> : null;
})()}
>
{/* Expiration Banner */}
@@ -420,10 +447,10 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
</div>
{/* Upload Modal */}
{showUploadModal && (
{showUploadModal && (data?.event?.allow_user_uploads || event?.allow_user_uploads) && (
<UserPhotoUpload
eventId={event.id}
categoryId={event.upload_category_id}
eventId={data?.event?.id || event?.id}
categoryId={data?.event?.upload_category_id || event?.upload_category_id}
onUploadComplete={() => {
setShowUploadModal(false);
// Refetch photos after upload
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { Upload, X, CheckCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-toastify';
import { Button, Card, CardContent } from '../common';
import { Button } from '../common';
import { api } from '../../config/api';
interface UserPhotoUploadProps {
@@ -114,7 +114,7 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-end sm:items-center justify-center z-50 p-0 sm:p-4">
<div className="w-full sm:max-w-2xl bg-white flex flex-col max-h-[100vh] sm:max-h-[90vh] rounded-t-2xl sm:rounded-2xl shadow-xl">
<div className="w-full sm:max-w-2xl bg-white flex flex-col max-h-[100vh] sm:max-h-[90vh] rounded-2xl shadow-xl overflow-hidden">
{/* Fixed Header */}
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-neutral-200 flex-shrink-0">
<h2 className="text-lg sm:text-xl font-semibold text-neutral-900">{t('upload.uploadPhotos')}</h2>
@@ -31,24 +31,49 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
const { format } = useLocalizedDate();
const { theme } = useTheme();
const [heroPhoto, setHeroPhoto] = useState<Photo | null>(null);
const [hasInitialized, setHasInitialized] = useState(false);
const gallerySettings = theme.gallerySettings || {};
const overlayOpacity = gallerySettings.heroOverlayOpacity || 0.3;
// Select hero photo (first photo or specified one)
// Reset initialization when heroImageId changes
useEffect(() => {
if (gallerySettings.heroImageId) {
setHasInitialized(false);
}
}, [gallerySettings.heroImageId]);
// Select hero photo (admin-selected or first photo only if gallery was empty)
useEffect(() => {
if (photos.length > 0) {
const heroId = gallerySettings.heroImageId;
const hero = heroId ? photos.find(p => p.id === heroId) : photos[0];
setHeroPhoto(hero || photos[0]);
console.log('HeroGalleryLayout - heroImageId:', heroId, 'photos:', photos.length);
// If admin has selected a specific hero image, always use it
if (heroId) {
const adminSelectedHero = photos.find(p => p.id === heroId);
console.log('Looking for hero photo with ID:', heroId, 'Found:', adminSelectedHero?.filename);
if (adminSelectedHero) {
setHeroPhoto(adminSelectedHero);
setHasInitialized(true);
return;
}
}
// Only auto-select first photo on initial load when gallery was empty
// This prevents changing the hero when new photos are uploaded
if (!hasInitialized) {
setHeroPhoto(photos[0]);
setHasInitialized(true);
}
}
}, [photos, gallerySettings.heroImageId]);
}, [photos, gallerySettings.heroImageId, hasInitialized]);
if (!heroPhoto) return null;
const remainingPhotos = photos.filter(p => p.id !== heroPhoto.id);
return (
<div className="relative">
<div className="relative -mt-6">
{/* Hero Section */}
<div className="relative h-[60vh] sm:h-[70vh] lg:h-[80vh] -mx-4 sm:-mx-6 lg:-mx-8 mb-8">
<AuthenticatedImage
@@ -67,17 +92,20 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
{/* Hero Content */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center px-4">
{/* Logo */}
{eventLogo && (
<div className="mb-6">
<img
src={eventLogo}
alt="Event logo"
className="h-20 sm:h-24 lg:h-32 mx-auto drop-shadow-lg filter brightness-0 invert"
style={{ filter: 'drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))' }}
/>
</div>
)}
{/* Logo - Show custom logo or fallback to PicPeak logo */}
<div className="mb-6">
<img
src={eventLogo ?
`${import.meta.env.VITE_API_URL || 'http://localhost:3001'}${eventLogo}` :
'/picpeak-logo-transparent.png'
}
alt="Event logo"
className="h-20 sm:h-24 lg:h-32 mx-auto"
style={{
filter: 'brightness(0) invert(1) drop-shadow(0 4px 6px rgba(0, 0, 0, 0.5))'
}}
/>
</div>
{/* Event Title */}
{eventName && (