Compare commits

...

7 Commits

Author SHA1 Message Date
Gitea Actions Bot 591a119ab3 chore: bump version to 1.0.93 (backend + frontend)
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-24 13:24:16 +00:00
paul c7cb96aaab fix: resolve multiple feedback management issues
Mirror to GitHub / mirror (push) Successful in 44s
Test and Lint / backend-test (push) Successful in 1m22s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m1s
Version and Release / version-bump (push) Successful in 40s
Version and Release / trigger-drone (push) Successful in 3s
- Hide "Manage Feedback" button when feedback is disabled for an event
- Fix 500 error on feedback API endpoint by adding null-safe operators
- Fix TypeError on analytics page by calculating average_rating in backend
- Fix password validation for event creation by properly awaiting async validation
- Add proper null checks and fallbacks for feedback statistics

These fixes ensure:
- Date passwords like "19.07.2025" work with simple password complexity settings
- Feedback management page loads without errors
- Analytics display correctly even with no feedback data

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 15:19:06 +02:00
Gitea Actions Bot 94778dca55 chore: bump frontend version to 1.0.92
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-24 12:22:36 +00:00
paul e9a2508750 fix: add missing route for feedback management page
Mirror to GitHub / mirror (push) Successful in 50s
Test and Lint / backend-test (push) Successful in 1m37s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m9s
Version and Release / version-bump (push) Successful in 40s
Version and Release / trigger-drone (push) Successful in 3s
- Added /admin/events/:id/feedback route to App.tsx
- This fixes the empty page issue when navigating to feedback management
- EventFeedbackPage component was already implemented but route was missing

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 14:17:00 +02:00
Gitea Actions Bot 27f551ef3a chore: bump frontend version to 1.0.91
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-07-24 11:52:24 +00:00
paul 81c55261d6 feat: add feedback management enhancements
Mirror to GitHub / mirror (push) Successful in 43s
Test and Lint / backend-test (push) Successful in 1m45s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m8s
Version and Release / version-bump (push) Successful in 44s
Version and Release / trigger-drone (push) Successful in 3s
- Add German translations for event dropdown menu actions
- Add feedback settings to event edit form
- Hide comment button in gallery when feedback is disabled
- Add feedback moderation panel to event details page

Implements:
1. German translation for three dots menu actions (viewDetails, archiveEventAction, etc.)
2. Feedback enable option now visible when editing existing events
3. Comment button in photo lightbox only shows when feedback is enabled
4. Inline comment moderation in admin event detail view

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 13:46:51 +02:00
paul bf68344f0c fix: use plugins/gitea-release for Drone CI/CD
Mirror to GitHub / mirror (push) Successful in 48s
Test and Lint / backend-test (push) Successful in 1m50s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m41s
- Replace plugins/github-release with plugins/gitea-release
- Fix API endpoint compatibility issue (was using GitHub API v3)
- Update base_url to gitea.local.nothaft.cloud
- Change secret from GITHUB_TOKEN to GITEA_TOKEN
- Update release notes to reference local Gitea URLs

This fixes the 401 authentication error when creating releases
on Gitea instances.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 13:19:25 +02:00
18 changed files with 365 additions and 43 deletions
+13 -12
View File
@@ -108,14 +108,15 @@ steps:
- VERSION=${DRONE_TAG}
- VITE_API_URL=${VITE_API_URL:-/api}
# -------- NEW: Create GitHub Release --------
- name: github-release
image: plugins/github-release
# -------- NEW: Create Gitea Release --------
- name: gitea-release
image: plugins/gitea-release
when:
event: tag
settings:
api_key:
from_secret: GITHUB_TOKEN
from_secret: GITEA_TOKEN
base_url: https://gitea.local.nothaft.cloud
files: []
title: PicPeak ${DRONE_TAG}
note: |
@@ -123,27 +124,27 @@ steps:
## 🐳 Docker Images
This release includes Docker images published to GitHub Container Registry:
This release includes Docker images published to local registry:
```bash
# Backend
docker pull ghcr.io/the-luap/picpeak-backend:${DRONE_TAG}
docker pull ghcr.io/the-luap/picpeak-backend:latest
docker pull registry.local.nothaft.cloud/picpeak-backend:${DRONE_TAG}
docker pull registry.local.nothaft.cloud/picpeak-backend:latest
# Frontend
docker pull ghcr.io/the-luap/picpeak-frontend:${DRONE_TAG}
docker pull ghcr.io/the-luap/picpeak-frontend:latest
docker pull registry.local.nothaft.cloud/picpeak-frontend:${DRONE_TAG}
docker pull registry.local.nothaft.cloud/picpeak-frontend:latest
```
## 📦 What's New
See the [README](https://github.com/the-luap/picpeak#readme) for features and documentation.
See the [README](https://gitea.nothaft.cloud/paul/picpeak#readme) for features and documentation.
## 🚀 Quick Start
```bash
# Clone and deploy
git clone https://github.com/the-luap/picpeak.git
git clone https://gitea.nothaft.cloud/paul/picpeak.git
cd picpeak
# Use the tagged version
@@ -152,7 +153,7 @@ steps:
---
For detailed deployment instructions, see the [Deployment Guide](https://github.com/the-luap/picpeak/blob/main/DEPLOYMENT.md).
For detailed deployment instructions, see the Deployment Guide in the repository.
trigger:
event:
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-backend",
"version": "1.0.84",
"version": "1.0.93",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-backend",
"version": "1.0.84",
"version": "1.0.93",
"dependencies": {
"@aws-sdk/client-s3": "^3.850.0",
"@aws-sdk/lib-storage": "^3.850.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "picpeak-backend",
"version": "1.0.84",
"version": "1.0.93",
"description": "Backend for PicPeak event photo sharing platform",
"main": "server.js",
"scripts": {
+1 -1
View File
@@ -42,7 +42,7 @@ router.post('/', adminAuth, [
} = req.body;
// Validate password strength for gallery
const passwordValidation = validatePasswordInContext(password, 'gallery', {
const passwordValidation = await validatePasswordInContext(password, 'gallery', {
eventName: event_name
});
+1 -1
View File
@@ -53,7 +53,7 @@ router.post('/', adminAuth, [
} = req.body;
// Validate password strength
const passwordValidation = validatePasswordInContext(password, 'gallery', {
const passwordValidation = await validatePasswordInContext(password, 'gallery', {
eventName: event_name
});
+31 -3
View File
@@ -123,8 +123,8 @@ router.get('/events/:eventId/feedback',
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: totalCount.count || 0,
pages: Math.ceil((totalCount.count || 0) / limit)
total: totalCount?.count || 0,
pages: Math.ceil((totalCount?.count || 0) / limit)
}
});
} catch (error) {
@@ -182,7 +182,35 @@ router.get('/events/:eventId/feedback-analytics',
const { eventId } = req.params;
// Get summary statistics
const summary = await feedbackService.getEventFeedbackSummary(eventId);
const summaryData = await feedbackService.getEventFeedbackSummary(eventId);
// Calculate average rating and other summary stats
const avgRatingResult = await db('photo_feedback')
.where('event_id', eventId)
.where('feedback_type', 'rating')
.avg('rating as average_rating')
.first();
const pendingModeration = await db('photo_feedback')
.where('event_id', eventId)
.where('feedback_type', 'comment')
.where('is_approved', false)
.where('is_hidden', false)
.count('* as count')
.first();
const summary = {
average_rating: parseFloat(avgRatingResult?.average_rating || 0),
total_ratings: summaryData.stats?.total_ratings || 0,
total_likes: summaryData.stats?.total_likes || 0,
total_comments: summaryData.stats?.total_comments || 0,
total_favorites: summaryData.stats?.total_favorites || 0,
pending_moderation: pendingModeration?.count || 0,
total_feedback: (summaryData.stats?.total_ratings || 0) +
(summaryData.stats?.total_likes || 0) +
(summaryData.stats?.total_comments || 0) +
(summaryData.stats?.total_favorites || 0)
};
// Get top-rated photos
const topRated = await db('photos')
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "picpeak-frontend",
"version": "1.0.90",
"version": "1.0.93",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picpeak-frontend",
"version": "1.0.90",
"version": "1.0.93",
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"@tiptap/extension-character-count": "^2.26.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "picpeak-frontend",
"private": true,
"version": "1.0.90",
"version": "1.0.93",
"type": "module",
"scripts": {
"dev": "vite",
+2
View File
@@ -16,6 +16,7 @@ import {
EventsListPage,
CreateEventPageEnhanced as CreateEventPage,
EventDetailsPage,
EventFeedbackPage,
EmailConfigPage,
ArchivesPage,
AnalyticsPage,
@@ -120,6 +121,7 @@ function App() {
<Route path="events" element={<EventsListPage />} />
<Route path="events/new" element={<CreateEventPage />} />
<Route path="events/:id" element={<EventDetailsPage />} />
<Route path="events/:id/feedback" element={<EventFeedbackPage />} />
<Route path="archives" element={<ArchivesPage />} />
<Route path="email" element={<EmailConfigPage />} />
<Route path="analytics" element={<AnalyticsPage />} />
@@ -0,0 +1,205 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
MessageSquare,
Eye,
EyeOff,
Trash2,
AlertCircle,
CheckCircle,
Clock,
User
} from 'lucide-react';
import { parseISO } from 'date-fns';
import { toast } from 'react-toastify';
import { Card, Loading, Button } from '../common';
import { feedbackService } from '../../services/feedback.service';
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
interface FeedbackModerationPanelProps {
eventId: number;
className?: string;
compact?: boolean;
maxItems?: number;
}
export const FeedbackModerationPanel: React.FC<FeedbackModerationPanelProps> = ({
eventId,
className = '',
compact = false,
maxItems = 5
}) => {
const { t } = useTranslation();
const { format } = useLocalizedDate();
const queryClient = useQueryClient();
const [showAll, setShowAll] = useState(false);
// Fetch pending feedback
const { data: feedbackData, isLoading } = useQuery({
queryKey: ['event-feedback-moderation', eventId],
queryFn: () => feedbackService.getEventFeedback(eventId.toString(), {
type: 'comment',
status: 'pending',
limit: showAll ? 100 : maxItems
}),
refetchInterval: 30000 // Refresh every 30 seconds
});
// Moderation mutation
const moderateMutation = useMutation({
mutationFn: ({ feedbackId, action }: { feedbackId: string; action: 'approve' | 'hide' | 'reject' }) =>
feedbackService.moderateFeedback(feedbackId, action),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['event-feedback-moderation', eventId] });
toast.success(t('feedback.moderationSuccess'));
}
});
// Delete mutation
const deleteMutation = useMutation({
mutationFn: (feedbackId: string) => feedbackService.deleteFeedback(feedbackId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['event-feedback-moderation', eventId] });
toast.success(t('feedback.deleted'));
}
});
if (isLoading) {
return (
<Card className={className}>
<div className="p-6">
<Loading />
</div>
</Card>
);
}
const pendingComments = feedbackData?.feedback || [];
const hasPending = pendingComments.length > 0;
return (
<Card className={className}>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-neutral-900">
{t('feedback.pendingModeration', 'Pending Moderation')}
</h2>
{hasPending && (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
{pendingComments.length} {t('feedback.pending', 'pending')}
</span>
)}
</div>
{!hasPending ? (
<div className="text-center py-8">
<CheckCircle className="w-12 h-12 text-green-500 mx-auto mb-3" />
<p className="text-neutral-600">{t('feedback.noPendingComments', 'No comments pending moderation')}</p>
</div>
) : (
<div className="space-y-3">
{pendingComments.slice(0, showAll ? undefined : maxItems).map((item) => (
<div key={item.id} className="border border-neutral-200 rounded-lg p-4 hover:bg-neutral-50">
<div className="flex items-start gap-3">
<div className="flex-shrink-0">
<div className="w-10 h-10 bg-neutral-100 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-neutral-600" />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<div className="flex items-center gap-2 text-sm">
<span className="font-medium text-neutral-900">
{item.guest_name || t('feedback.anonymous', 'Anonymous')}
</span>
<span className="text-neutral-500"></span>
<span className="text-neutral-500">
{format(parseISO(item.created_at), 'MMM d, h:mm a')}
</span>
</div>
<p className="mt-1 text-sm text-neutral-700">{item.comment}</p>
{item.photo_filename && (
<p className="mt-1 text-xs text-neutral-500">
{t('feedback.onPhoto', 'On photo')}: {item.photo_filename}
</p>
)}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2 mt-3">
<Button
size="sm"
variant="ghost"
leftIcon={<CheckCircle className="w-4 h-4" />}
onClick={() => moderateMutation.mutate({
feedbackId: item.id.toString(),
action: 'approve'
})}
isLoading={moderateMutation.isPending}
>
{t('feedback.approve', 'Approve')}
</Button>
<Button
size="sm"
variant="ghost"
leftIcon={<EyeOff className="w-4 h-4" />}
onClick={() => moderateMutation.mutate({
feedbackId: item.id.toString(),
action: 'hide'
})}
isLoading={moderateMutation.isPending}
>
{t('feedback.hide', 'Hide')}
</Button>
<Button
size="sm"
variant="ghost"
leftIcon={<Trash2 className="w-4 h-4" />}
onClick={() => {
if (confirm(t('feedback.confirmDelete', 'Are you sure you want to delete this comment?'))) {
deleteMutation.mutate(item.id.toString());
}
}}
isLoading={deleteMutation.isPending}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
{t('common.delete', 'Delete')}
</Button>
</div>
</div>
</div>
</div>
))}
{pendingComments.length > maxItems && !showAll && (
<button
onClick={() => setShowAll(true)}
className="w-full text-center py-2 text-sm text-primary-600 hover:text-primary-700 font-medium"
>
{t('feedback.showAll', 'Show all {{count}} pending comments', { count: pendingComments.length })}
</button>
)}
</div>
)}
{/* Quick link to full feedback page */}
<div className="mt-4 pt-4 border-t border-neutral-200">
<a
href={`/admin/events/${eventId}/feedback`}
className="text-sm text-primary-600 hover:text-primary-700 font-medium flex items-center gap-1"
>
<MessageSquare className="w-4 h-4" />
{t('feedback.viewAllFeedback', 'View all feedback & settings')}
</a>
</div>
</div>
</Card>
);
};
FeedbackModerationPanel.displayName = 'FeedbackModerationPanel';
+2 -1
View File
@@ -27,4 +27,5 @@ export { BackupDashboard } from './BackupDashboard';
export { BackupConfiguration } from './BackupConfiguration';
export { BackupHistory } from './BackupHistory';
export { RestoreWizard } from './RestoreWizard';
export { FeedbackSettings } from './FeedbackSettings';
export { FeedbackSettings } from './FeedbackSettings';
export { FeedbackModerationPanel } from './FeedbackModerationPanel';
@@ -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 { feedbackService } from '../../services/feedback.service';
import { useWatermarkSettings } from '../../hooks/useWatermarkSettings';
interface GalleryViewProps {
@@ -48,6 +49,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [isSelectionMode, setIsSelectionMode] = useState(false);
const [selectedPhotos, setSelectedPhotos] = useState<Set<number>>(new Set());
const [feedbackEnabled, setFeedbackEnabled] = useState(false);
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const { watermarkEnabled } = useWatermarkSettings();
@@ -76,6 +78,25 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
});
// Fetch feedback settings
const { data: feedbackSettings } = useQuery({
queryKey: ['gallery-feedback-settings', event.id],
queryFn: async () => {
try {
// Use public endpoint to get feedback settings
const response = await api.get(`/gallery/${slug}/feedback-settings`);
return response.data;
} catch (error) {
// If endpoint doesn't exist or returns error, default to disabled
return { feedback_enabled: false };
}
},
onSuccess: (data) => {
setFeedbackEnabled(data?.feedback_enabled || false);
},
enabled: !!event.id,
});
// Apply branding settings
useEffect(() => {
if (settingsData) {
@@ -429,6 +450,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
photos={filteredPhotos}
slug={slug}
categoryId={selectedCategoryId}
feedbackEnabled={feedbackEnabled}
isSelectionMode={isSelectionMode}
selectedPhotos={selectedPhotos}
onSelectionChange={setSelectedPhotos}
@@ -15,9 +15,10 @@ interface PhotoGridProps {
photos: Photo[];
slug: string;
categoryId?: number | null;
feedbackEnabled?: boolean;
}
export const PhotoGrid: React.FC<PhotoGridProps> = ({ photos, slug, categoryId }) => {
export const PhotoGrid: React.FC<PhotoGridProps> = ({ photos, slug, categoryId, feedbackEnabled = false }) => {
const { t } = useTranslation();
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState<number | null>(null);
const [selectedPhotos, setSelectedPhotos] = useState<Set<number>>(new Set());
@@ -204,6 +205,7 @@ export const PhotoGrid: React.FC<PhotoGridProps> = ({ photos, slug, categoryId }
initialIndex={selectedPhotoIndex}
onClose={() => setSelectedPhotoIndex(null)}
slug={slug}
feedbackEnabled={feedbackEnabled}
/>
)}
</>
@@ -34,6 +34,7 @@ interface PhotoGridWithLayoutsProps {
eventLogo?: string | null;
eventDate?: string;
expiresAt?: string;
feedbackEnabled?: boolean;
}
export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
@@ -42,6 +43,7 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
categoryId,
isSelectionMode: parentSelectionMode,
selectedPhotos: parentSelectedPhotos,
feedbackEnabled,
onSelectionChange,
onToggleSelectionMode: parentToggleSelectionMode,
showSelectionControls = true,
@@ -259,6 +261,7 @@ export const PhotoGridWithLayouts: React.FC<PhotoGridWithLayoutsProps> = ({
initialIndex={selectedPhotoIndex}
onClose={() => setSelectedPhotoIndex(null)}
slug={slug}
feedbackEnabled={feedbackEnabled || false}
/>
)}
</>
@@ -10,6 +10,7 @@ interface PhotoLightboxProps {
initialIndex: number;
onClose: () => void;
slug: string;
feedbackEnabled?: boolean;
}
export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
@@ -17,6 +18,7 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
initialIndex,
onClose,
slug,
feedbackEnabled = false,
}) => {
const [currentIndex, setCurrentIndex] = useState(initialIndex);
const [zoom, setZoom] = useState(1);
@@ -227,13 +229,15 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
<Download className="w-5 h-5 text-white" />
</button>
<button
onClick={() => setShowFeedback(!showFeedback)}
className="p-2 bg-white/10 hover:bg-white/20 rounded-full transition-colors"
aria-label="Toggle feedback"
>
<MessageSquare className="w-5 h-5 text-white" />
</button>
{feedbackEnabled && (
<button
onClick={() => setShowFeedback(!showFeedback)}
className="p-2 bg-white/10 hover:bg-white/20 rounded-full transition-colors"
aria-label="Toggle feedback"
>
<MessageSquare className="w-5 h-5 text-white" />
</button>
)}
</div>
</div>
</div>
+5
View File
@@ -367,6 +367,11 @@
"noEventsDescription": "Erstellen Sie Ihre erste Veranstaltung, um zu beginnen.",
"eventsSelected": "{{count}} Veranstaltung ausgewählt",
"eventsSelected_plural": "{{count}} Veranstaltungen ausgewählt",
"viewDetails": "Details anzeigen",
"archiveEventAction": "Veranstaltung archivieren",
"downloadArchiveAction": "Archiv herunterladen",
"deleteEvent": "Veranstaltung löschen",
"deleteEventConfirm": "Sind Sie sicher, dass Sie diese Veranstaltung löschen möchten?",
"bulkArchive": "Archivieren",
"confirmBulkArchive": "Sind Sie sicher, dass Sie {{count}} Veranstaltung(en) archivieren möchten?",
"confirmBulkArchiveDescription": "Diese Aktion kann nicht rückgängig gemacht werden. Archivierte Veranstaltungen sind nicht mehr öffentlich zugänglich.",
+59 -10
View File
@@ -24,11 +24,12 @@ import { toast } from 'react-toastify';
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
import { Button, Input, Card, Loading } from '../../components/common';
import { EventCategoryManager, AdminPhotoGrid, AdminPhotoViewer, PhotoFilters, PasswordResetModal, ThemeCustomizerEnhanced, ThemeDisplay, HeroPhotoSelector, PhotoUploadModal } from '../../components/admin';
import { EventCategoryManager, AdminPhotoGrid, AdminPhotoViewer, PhotoFilters, PasswordResetModal, ThemeCustomizerEnhanced, ThemeDisplay, HeroPhotoSelector, PhotoUploadModal, FeedbackSettings, FeedbackModerationPanel } from '../../components/admin';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { eventsService } from '../../services/events.service';
import { archiveService } from '../../services/archive.service';
import { photosService, AdminPhoto } from '../../services/photos.service';
import { feedbackService, FeedbackSettings as FeedbackSettingsType } from '../../services/feedback.service';
import { ThemeConfig, GALLERY_THEME_PRESETS } from '../../types/theme.types';
export const EventDetailsPage: React.FC = () => {
@@ -55,6 +56,15 @@ export const EventDetailsPage: React.FC = () => {
hero_photo_id: null as number | null,
host_name: '',
});
const [feedbackSettings, setFeedbackSettings] = useState<FeedbackSettingsType>({
feedback_enabled: false,
allow_ratings: true,
allow_likes: true,
allow_comments: true,
allow_favorites: true,
require_moderation: true,
show_public_stats: false
});
const [copiedLink, setCopiedLink] = useState(false);
const [showPhotoUpload, setShowPhotoUpload] = useState(false);
const [activeTab, setActiveTab] = useState<'overview' | 'photos' | 'categories'>('overview');
@@ -78,6 +88,16 @@ export const EventDetailsPage: React.FC = () => {
enabled: !!id,
});
// Fetch feedback settings
const { data: eventFeedbackSettings } = useQuery({
queryKey: ['admin-event-feedback-settings', id],
queryFn: () => feedbackService.getEventFeedbackSettings(id!),
enabled: !!id,
onSuccess: (data) => {
setFeedbackSettings(data);
}
});
// Statistics are now fetched with the event details from the admin API
// Fetch photos (needed for both photos tab and hero photo selector)
@@ -198,7 +218,7 @@ export const EventDetailsPage: React.FC = () => {
setIsEditing(true);
};
const handleSaveEdit = () => {
const handleSaveEdit = async () => {
// Prepare color_theme - if we have a custom theme, serialize it
let themeToSave = editForm.color_theme;
if (currentTheme && currentPresetName === 'custom') {
@@ -240,7 +260,16 @@ export const EventDetailsPage: React.FC = () => {
console.log('Updating event with data:', updateData);
console.log('Theme length:', updateData.color_theme ? updateData.color_theme.length : 0);
// Update event details
updateMutation.mutate(updateData);
// Update feedback settings separately
try {
await feedbackService.updateEventFeedbackSettings(id!, feedbackSettings);
} catch (error) {
console.error('Failed to update feedback settings:', error);
}
};
const handleCopyLink = async () => {
@@ -320,14 +349,16 @@ export const EventDetailsPage: React.FC = () => {
{t('common.edit')}
</Button>
)}
<Button
variant="outline"
size="sm"
leftIcon={<MessageSquare className="w-4 h-4" />}
onClick={() => navigate(`/admin/events/${id}/feedback`)}
>
{t('feedback.manage', 'Manage Feedback')}
</Button>
{feedbackSettings?.feedback_enabled && (
<Button
variant="outline"
size="sm"
leftIcon={<MessageSquare className="w-4 h-4" />}
onClick={() => navigate(`/admin/events/${id}/feedback`)}
>
{t('feedback.manage', 'Manage Feedback')}
</Button>
)}
</>
)}
{event.share_link && (
@@ -518,6 +549,15 @@ export const EventDetailsPage: React.FC = () => {
</p>
</div>
)}
{/* Feedback Settings */}
<div className="mt-4 pt-4 border-t border-neutral-200">
<h3 className="text-sm font-semibold text-neutral-900 mb-3">{t('feedback.settings', 'Feedback Settings')}</h3>
<FeedbackSettings
settings={feedbackSettings}
onChange={setFeedbackSettings}
/>
</div>
</div>
) : (
<dl className="space-y-4">
@@ -788,6 +828,15 @@ export const EventDetailsPage: React.FC = () => {
</Card>
)}
{/* Feedback Moderation Panel */}
{!event.is_archived && feedbackSettings?.feedback_enabled && (
<FeedbackModerationPanel
eventId={parseInt(id!)}
compact={true}
maxItems={3}
/>
)}
{/* Archive Status */}
{event.is_archived ? (
<Card padding="md">
@@ -392,7 +392,7 @@ export const EventFeedbackPage: React.FC = () => {
<div className="flex items-center gap-3 mb-2">
<Star className="w-8 h-8 text-yellow-500" />
<div>
<p className="text-2xl font-bold">{analytics.summary.average_rating.toFixed(1)}</p>
<p className="text-2xl font-bold">{(analytics.summary.average_rating || 0).toFixed(1)}</p>
<p className="text-sm text-neutral-600">{t('feedback.avgRating', 'Average Rating')}</p>
</div>
</div>