refactor: rename project from wedding-photo-sharing to PicPeak
- 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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user