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
+57 -31
View File
@@ -8,7 +8,9 @@ import {
Clock,
Plus,
HardDrive,
Image
Image,
Archive,
Heart
} from 'lucide-react';
import { differenceInDays, parseISO } from 'date-fns';
import { useTranslation } from 'react-i18next';
@@ -44,6 +46,13 @@ export const AdminDashboard: React.FC = () => {
queryFn: () => adminService.getRecentActivity(10),
});
// Fetch system health
const { data: systemHealth } = useQuery({
queryKey: ['admin-system-health'],
queryFn: () => adminService.getSystemHealth(),
refetchInterval: 30000, // Refresh every 30 seconds
});
// Fetch events data for expiring events
const { data: eventsData, isLoading: eventsLoading } = useQuery({
queryKey: ['admin-events-summary'],
@@ -74,7 +83,7 @@ export const AdminDashboard: React.FC = () => {
return num.toString();
};
// Build statistics cards
// Build statistics cards - always show 8 cards in 2x4 grid
const stats: StatCard[] = [
{
title: t('admin.activeEvents'),
@@ -101,28 +110,34 @@ export const AdminDashboard: React.FC = () => {
icon: HardDrive,
color: 'text-purple-600',
},
{
title: t('admin.totalViews'),
value: formatNumber(dashboardStats?.totalViews || 0),
change: dashboardStats?.viewsTrend ? t('admin.percentFromLastWeek', { percent: `${dashboardStats.viewsTrend > 0 ? '+' : ''}${dashboardStats.viewsTrend}` }) : undefined,
icon: Eye,
color: 'text-indigo-600',
},
{
title: t('admin.downloads'),
value: formatNumber(dashboardStats?.totalDownloads || 0),
change: dashboardStats?.downloadsTrend ? t('admin.percentFromLastWeek', { percent: `${dashboardStats.downloadsTrend > 0 ? '+' : ''}${dashboardStats.downloadsTrend}` }) : undefined,
icon: Download,
color: 'text-pink-600',
},
{
title: t('admin.archivedEvents'),
value: dashboardStats?.archivedEvents || 0,
icon: Archive,
color: 'text-gray-600',
},
{
title: t('admin.systemHealth'),
value: systemHealth ? t(`admin.health.${systemHealth.overall}`) : t('admin.health.checking'),
icon: Heart,
color: systemHealth?.overall === 'healthy' ? 'text-green-600' : systemHealth?.overall === 'warning' ? 'text-yellow-600' : 'text-red-600',
},
];
// Add second row of stats if we have trend data
if (dashboardStats?.totalViews !== undefined) {
stats.push(
{
title: t('admin.totalViews'),
value: formatNumber(dashboardStats.totalViews),
change: dashboardStats.viewsTrend > 0 ? t('admin.percentFromLastWeek', { percent: `+${dashboardStats.viewsTrend}` }) : undefined,
icon: Eye,
color: 'text-indigo-600',
},
{
title: t('admin.downloads'),
value: formatNumber(dashboardStats.totalDownloads),
change: dashboardStats.downloadsTrend > 0 ? t('admin.percentFromLastWeek', { percent: `+${dashboardStats.downloadsTrend}` }) : undefined,
icon: Download,
color: 'text-pink-600',
}
);
}
return (
<div>
{/* Page Header */}
@@ -243,12 +258,31 @@ export const AdminDashboard: React.FC = () => {
return colors[type] || 'bg-gray-500';
};
// Format activity message with translations
const getActivityMessage = (): string => {
const translationKey = `admin.activities.${activity.type}`;
const params: Record<string, any> = {
eventName: activity.eventName || t('common.unknown'),
count: activity.metadata?.count || 0,
template: activity.metadata?.template_key || '',
categoryName: activity.metadata?.category_name || ''
};
// Check if translation exists
const translated = t(translationKey, params);
if (typeof translated === 'string') {
return translated;
}
// Fallback to unknown activity if translation not found
return t('admin.activities.unknown') as string;
};
return (
<div key={activity.id} className="flex items-start gap-3">
<div className={`w-2 h-2 rounded-full mt-1.5 flex-shrink-0 ${getActivityColor(activity.type)}`} />
<div className="flex-1 min-w-0">
<p className="text-sm text-neutral-900 break-words">
{adminService.formatActivityMessage(activity)}
{getActivityMessage()}
</p>
<p className="text-xs text-neutral-500">{activity.actorName}</p>
<p className="text-xs text-neutral-400 mt-1">
@@ -261,14 +295,6 @@ export const AdminDashboard: React.FC = () => {
)}
</div>
{recentActivity && recentActivity.length > 5 && (
<button
onClick={() => navigate('/admin/activity')}
className="w-full mt-4 text-sm text-primary-600 hover:text-primary-700 font-medium"
>
{t('admin.viewAllActivity')}
</button>
)}
</Card>
</div>
+33 -17
View File
@@ -270,21 +270,6 @@ export const BrandingPage: React.FC = () => {
</div>
</div>
<div className="mt-6 pt-6 border-t border-neutral-200">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={brandingSettings.watermark_enabled}
onChange={(e) => handleBrandingChange('watermark_enabled', e.target.checked)}
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
/>
<div>
<span className="text-sm font-medium text-neutral-900">{t('branding.enableWatermarks')}</span>
<p className="text-xs text-neutral-600">{t('branding.watermarkHelp')}</p>
</div>
</label>
</div>
<div className="mt-6 pt-6 border-t border-neutral-200">
<div>
<label className="block text-sm font-medium text-neutral-700 mb-2">
@@ -330,6 +315,21 @@ export const BrandingPage: React.FC = () => {
</div>
</div>
<div className="mt-6 pt-6 border-t border-neutral-200">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={brandingSettings.watermark_enabled}
onChange={(e) => handleBrandingChange('watermark_enabled', e.target.checked)}
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
/>
<div>
<span className="text-sm font-medium text-neutral-900">{t('branding.enableWatermarks')}</span>
<p className="text-xs text-neutral-600">{t('branding.watermarkHelp')}</p>
</div>
</label>
</div>
{/* Watermark Settings */}
{brandingSettings.watermark_enabled && (
<div className="mt-6 space-y-6 border-t border-neutral-200 pt-6">
@@ -422,7 +422,15 @@ export const BrandingPage: React.FC = () => {
step="10"
value={brandingSettings.watermark_opacity || 50}
onChange={(e) => handleBrandingChange('watermark_opacity', parseInt(e.target.value))}
className="w-full h-2 bg-neutral-200 rounded-lg appearance-none cursor-pointer slider"
className="w-full slider"
style={{
WebkitAppearance: 'none',
appearance: 'none',
height: '8px',
background: '#d4d4d4',
borderRadius: '4px',
outline: 'none'
}}
/>
<div className="flex justify-between text-xs text-neutral-500 mt-1">
<span>10%</span>
@@ -443,7 +451,15 @@ export const BrandingPage: React.FC = () => {
step="5"
value={brandingSettings.watermark_size || 15}
onChange={(e) => handleBrandingChange('watermark_size', parseInt(e.target.value))}
className="w-full h-2 bg-neutral-200 rounded-lg appearance-none cursor-pointer slider"
className="w-full slider"
style={{
WebkitAppearance: 'none',
appearance: 'none',
height: '8px',
background: '#d4d4d4',
borderRadius: '4px',
outline: 'none'
}}
/>
<div className="flex justify-between text-xs text-neutral-500 mt-1">
<span>5%</span>
@@ -235,10 +235,14 @@ export const CreateEventPageEnhanced: React.FC = () => {
};
const handlePresetChange = (presetName: string) => {
setFormData(prev => ({
...prev,
theme_preset: presetName
}));
const preset = GALLERY_THEME_PRESETS[presetName];
if (preset) {
setFormData(prev => ({
...prev,
theme_preset: presetName,
theme_config: preset.config
}));
}
};
return (
@@ -383,7 +387,7 @@ export const CreateEventPageEnhanced: React.FC = () => {
onChange={handleThemeChange}
presetName={formData.theme_preset}
onPresetChange={handlePresetChange}
isPreviewMode={false}
isPreviewMode={true}
showGalleryLayouts={true}
hideActions={true}
/>
+41 -134
View File
@@ -15,16 +15,14 @@ import {
CheckCircle,
Upload,
Image,
Key,
Palette,
Settings
Key
} from 'lucide-react';
import { parseISO, differenceInDays } from 'date-fns';
import { toast } from 'react-toastify';
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
import { Button, Input, Card, Loading } from '../../components/common';
import { PhotoUpload, EventCategoryManager, AdminPhotoGrid, AdminPhotoViewer, PhotoFilters, PasswordResetModal, ThemeDisplay, ThemeCustomizerEnhanced, ThemeEditorModal, HeroPhotoSelector, PhotoUploadModal } from '../../components/admin';
import { EventCategoryManager, AdminPhotoGrid, AdminPhotoViewer, PhotoFilters, PasswordResetModal, ThemeCustomizerEnhanced, ThemeDisplay, HeroPhotoSelector, PhotoUploadModal } from '../../components/admin';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { eventsService } from '../../services/events.service';
import { archiveService } from '../../services/archive.service';
@@ -60,10 +58,8 @@ export const EventDetailsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<'overview' | 'photos' | 'categories'>('overview');
const [selectedPhoto, setSelectedPhoto] = useState<{ photo: AdminPhoto; index: number } | null>(null);
const [showPasswordReset, setShowPasswordReset] = useState(false);
const [showThemeCustomizer, setShowThemeCustomizer] = useState(false);
const [currentTheme, setCurrentTheme] = useState<ThemeConfig | null>(null);
const [currentPresetName, setCurrentPresetName] = useState<string>('default');
const [showThemeEditorModal, setShowThemeEditorModal] = useState(false);
// Photo filters state
const [photoFilters, setPhotoFilters] = useState({
@@ -203,7 +199,7 @@ export const EventDetailsPage: React.FC = () => {
const handleSaveEdit = () => {
// Prepare color_theme - if we have a custom theme, serialize it
let themeToSave = editForm.color_theme;
if (currentTheme && (currentPresetName === 'custom' || showThemeCustomizer)) {
if (currentTheme && currentPresetName === 'custom') {
themeToSave = JSON.stringify(currentTheme);
} else if (currentPresetName && currentPresetName !== 'custom') {
// Use preset name for non-custom themes
@@ -256,34 +252,6 @@ export const EventDetailsPage: React.FC = () => {
}
};
const handleThemeModalSave = async (theme: ThemeConfig, presetName: string) => {
// Prepare theme for saving
let themeToSave: string;
if (presetName !== 'custom' && GALLERY_THEME_PRESETS[presetName]) {
// Save preset name for standard presets
themeToSave = presetName;
} else {
// Save full theme config for custom themes
themeToSave = JSON.stringify(theme);
}
try {
// Update the event with new theme
await eventsService.updateEvent(parseInt(id!), {
color_theme: themeToSave
});
// Invalidate queries to refresh the data
await queryClient.invalidateQueries({ queryKey: ['admin-event', id] });
// Close modal and show success
setShowThemeEditorModal(false);
toast.success(t('toast.themeUpdated'));
} catch (error) {
console.error('Failed to save theme:', error);
toast.error(t('toast.saveError'));
}
};
return (
<div>
@@ -492,73 +460,6 @@ export const EventDetailsPage: React.FC = () => {
/>
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1">
{t('events.galleryTheme')}
</label>
{!showThemeCustomizer ? (
<div className="space-y-2">
<select
value={currentPresetName}
onChange={(e) => {
const presetName = e.target.value;
setCurrentPresetName(presetName);
if (presetName !== 'custom') {
const preset = GALLERY_THEME_PRESETS[presetName];
if (preset) {
setCurrentTheme(preset.config);
setEditForm(prev => ({ ...prev, color_theme: presetName }));
}
}
}}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
{Object.entries(GALLERY_THEME_PRESETS).map(([key, preset]) => (
<option key={key} value={key}>
{preset.name}
</option>
))}
<option value="custom">{t('branding.customTheme')}</option>
</select>
<Button
variant="outline"
size="sm"
leftIcon={<Settings className="w-4 h-4" />}
onClick={() => setShowThemeCustomizer(true)}
className="w-full"
>
{t('branding.customizeTheme')}
</Button>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-neutral-600">{t('branding.customizingTheme')}</span>
<Button
variant="ghost"
size="sm"
onClick={() => setShowThemeCustomizer(false)}
>
{t('common.hide')}
</Button>
</div>
<ThemeCustomizerEnhanced
value={currentTheme || GALLERY_THEME_PRESETS.default.config}
onChange={setCurrentTheme}
presetName={currentPresetName}
onPresetChange={(presetName) => {
setCurrentPresetName(presetName);
if (presetName !== 'custom') {
setEditForm(prev => ({ ...prev, color_theme: presetName }));
}
}}
isPreviewMode={false}
showGalleryLayouts={true}
/>
</div>
)}
</div>
{/* Hero Photo Selection */}
<HeroPhotoSelector
photos={photos || []}
@@ -822,28 +723,44 @@ export const EventDetailsPage: React.FC = () => {
</div>
</Card>
{/* Gallery Theme */}
<Card padding="md">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-neutral-900">{t('events.galleryTheme')}</h2>
{!event.is_archived && (
<Button
variant="outline"
size="sm"
leftIcon={<Palette className="w-4 h-4" />}
onClick={() => setShowThemeEditorModal(true)}
>
{t('events.customizeTheme')}
</Button>
)}
</div>
<ThemeDisplay
theme={event.color_theme || GALLERY_THEME_PRESETS.default.config}
presetName={event.color_theme && !event.color_theme.startsWith('{') ? event.color_theme : undefined}
showDetails={true}
/>
</Card>
{/* Theme & Style */}
{isEditing && !event.is_archived && (
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('branding.themeAndStyle')}</h2>
<ThemeCustomizerEnhanced
value={currentTheme || GALLERY_THEME_PRESETS.default.config}
onChange={(theme) => {
setCurrentTheme(theme);
setEditForm(prev => ({ ...prev, color_theme: JSON.stringify(theme) }));
}}
presetName={currentPresetName}
onPresetChange={(presetName) => {
setCurrentPresetName(presetName);
if (presetName !== 'custom') {
const preset = GALLERY_THEME_PRESETS[presetName];
if (preset) {
setCurrentTheme(preset.config);
setEditForm(prev => ({ ...prev, color_theme: presetName }));
}
}
}}
isPreviewMode={false}
showGalleryLayouts={true}
/>
</Card>
)}
{/* Theme Display (when not editing) */}
{!isEditing && !event.is_archived && (
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('events.galleryTheme')}</h2>
<ThemeDisplay
theme={event.color_theme || GALLERY_THEME_PRESETS.default.config}
presetName={event.color_theme && !event.color_theme.startsWith('{') ? event.color_theme : undefined}
showDetails={true}
/>
</Card>
)}
{/* Archive Status */}
{event.is_archived ? (
@@ -995,16 +912,6 @@ export const EventDetailsPage: React.FC = () => {
/>
)}
{/* Theme Editor Modal */}
{showThemeEditorModal && (
<ThemeEditorModal
isOpen={showThemeEditorModal}
onClose={() => setShowThemeEditorModal(false)}
onSave={handleThemeModalSave}
currentTheme={event.color_theme || 'default'}
eventName={event.event_name}
/>
)}
</div>
);
};
+1 -1
View File
@@ -1,7 +1,7 @@
export { AdminLoginPage } from './AdminLoginPage';
export { AdminDashboard } from './AdminDashboard';
export { EventsListPage } from './EventsListPage';
export { CreateEventPageEnhanced as CreateEventPage } from './CreateEventPageEnhanced';
export { CreateEventPageEnhanced } from './CreateEventPageEnhanced';
export { EventDetailsPage } from './EventDetailsPage';
export { EmailConfigPage } from './EmailConfigPage';
export { ArchivesPage } from './ArchivesPage';
+2 -2
View File
@@ -46,7 +46,7 @@ export const LegalPage: React.FC = () => {
// Update page title
useEffect(() => {
if (page?.title) {
document.title = `${page.title} - Wedding Photo Sharing`;
document.title = `${page.title} - PicPeak`;
}
}, [page?.title]);
@@ -129,7 +129,7 @@ export const LegalPage: React.FC = () => {
</Link>
</div>
<p className="text-sm text-neutral-500 mt-4">
© 2024 Wedding Photo Sharing. All rights reserved.
© 2024 PicPeak. All rights reserved.
</p>
</div>
</footer>