Files
picpeak/frontend/src/components/admin/FeedbackSettings.tsx
T
paul dc1419c051 feat: implement gallery feedback system with version tracking for backups
Gallery Feedback Features:
- Add feedback system allowing ratings, likes, comments, and favorites on photos
- Implement admin controls for enabling/disabling feedback per event
- Add content moderation with word filters and spam detection
- Implement rate limiting to prevent abuse (10 requests/15min per type)
- Create comprehensive admin interface for feedback management
- Add analytics dashboard for feedback insights
- Export feedback data when archiving events

Frontend Components:
- PhotoRating: 5-star rating system with optimistic updates
- PhotoLikes: Like/unlike with animation
- PhotoComments: Threaded comments with moderation
- PhotoFavorites: Bookmark functionality
- FeedbackSettings: Admin configuration panel
- EventFeedbackPage: Complete management interface

Backend Implementation:
- Database migration 033: 4 new tables for feedback system
- RESTful API with proper authorization
- Guest identification via SHA256(IP+UserAgent)
- Automatic backup integration
- Email notification support

Backup Version Tracking:
- Migration 034: Add version columns to backup tables
- Track app version, Node.js version, and DB schema version
- Create restore_history table for tracking restore attempts
- Add version compatibility checking for safe restores
- Configurable version matching requirements

Security & Performance:
- Input validation and sanitization
- Rate limiting per feedback type
- Content moderation system
- Optimistic UI updates
- Efficient database queries with proper indexes

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-24 16:57:08 +02:00

274 lines
12 KiB
TypeScript

import React from 'react';
import { MessageSquare, Star, Heart, Bookmark, Shield, Eye } from 'lucide-react';
import { Card } from '../common';
import { useTranslation } from 'react-i18next';
interface FeedbackSettingsProps {
settings: FeedbackSettings;
onChange: (settings: FeedbackSettings) => void;
className?: string;
}
interface FeedbackSettings {
feedback_enabled: boolean;
allow_ratings: boolean;
allow_likes: boolean;
allow_comments: boolean;
allow_favorites: boolean;
require_name_email: boolean;
moderate_comments: boolean;
show_feedback_to_guests: boolean;
enable_rate_limiting: boolean;
rate_limit_window_minutes?: number;
rate_limit_max_requests?: number;
}
export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
settings,
onChange,
className = ''
}) => {
const { t } = useTranslation();
const handleToggle = (field: keyof FeedbackSettings) => {
onChange({
...settings,
[field]: !settings[field]
});
};
const handleNumberChange = (field: keyof FeedbackSettings, value: string) => {
const numValue = parseInt(value, 10);
if (!isNaN(numValue)) {
onChange({
...settings,
[field]: numValue
});
}
};
return (
<Card className={className}>
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-neutral-900 flex items-center gap-2">
<MessageSquare className="w-5 h-5" />
{t('feedback.settings.title', 'Guest Feedback Settings')}
</h2>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={settings.feedback_enabled}
onChange={() => handleToggle('feedback_enabled')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<span className="text-sm font-medium text-neutral-700">
{t('feedback.settings.enableFeedback', 'Enable feedback')}
</span>
</label>
</div>
{settings.feedback_enabled && (
<>
{/* Feedback Types */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-neutral-700">
{t('feedback.settings.feedbackTypes', 'Feedback Types')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="flex items-center gap-3 p-3 bg-neutral-50 rounded-lg cursor-pointer hover:bg-neutral-100">
<input
type="checkbox"
checked={settings.allow_ratings}
onChange={() => handleToggle('allow_ratings')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<Star className="w-5 h-5 text-neutral-600" />
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
{t('feedback.settings.ratings', 'Star Ratings')}
</div>
<div className="text-xs text-neutral-500">
{t('feedback.settings.ratingsDesc', 'Allow guests to rate photos (1-5 stars)')}
</div>
</div>
</label>
<label className="flex items-center gap-3 p-3 bg-neutral-50 rounded-lg cursor-pointer hover:bg-neutral-100">
<input
type="checkbox"
checked={settings.allow_likes}
onChange={() => handleToggle('allow_likes')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<Heart className="w-5 h-5 text-neutral-600" />
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
{t('feedback.settings.likes', 'Likes')}
</div>
<div className="text-xs text-neutral-500">
{t('feedback.settings.likesDesc', 'Simple like/unlike functionality')}
</div>
</div>
</label>
<label className="flex items-center gap-3 p-3 bg-neutral-50 rounded-lg cursor-pointer hover:bg-neutral-100">
<input
type="checkbox"
checked={settings.allow_comments}
onChange={() => handleToggle('allow_comments')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<MessageSquare className="w-5 h-5 text-neutral-600" />
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
{t('feedback.settings.comments', 'Comments')}
</div>
<div className="text-xs text-neutral-500">
{t('feedback.settings.commentsDesc', 'Text comments on photos')}
</div>
</div>
</label>
<label className="flex items-center gap-3 p-3 bg-neutral-50 rounded-lg cursor-pointer hover:bg-neutral-100">
<input
type="checkbox"
checked={settings.allow_favorites}
onChange={() => handleToggle('allow_favorites')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<Bookmark className="w-5 h-5 text-neutral-600" />
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
{t('feedback.settings.favorites', 'Favorites')}
</div>
<div className="text-xs text-neutral-500">
{t('feedback.settings.favoritesDesc', 'Mark photos as favorites')}
</div>
</div>
</label>
</div>
</div>
<div className="border-t pt-4" />
{/* Privacy & Moderation */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-neutral-700">
{t('feedback.settings.privacyModeration', 'Privacy & Moderation')}
</h3>
<div className="space-y-3">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={settings.require_name_email}
onChange={() => handleToggle('require_name_email')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
{t('feedback.settings.requireInfo', 'Require Name & Email')}
</div>
<div className="text-xs text-neutral-500">
{t('feedback.settings.requireInfoDesc', 'Guests must provide name and email to leave feedback')}
</div>
</div>
</label>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={settings.moderate_comments}
onChange={() => handleToggle('moderate_comments')}
disabled={!settings.allow_comments}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500 disabled:opacity-50"
/>
<Shield className="w-5 h-5 text-neutral-600" />
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
{t('feedback.settings.moderateComments', 'Moderate Comments')}
</div>
<div className="text-xs text-neutral-500">
{t('feedback.settings.moderateCommentsDesc', 'Comments require approval before being visible')}
</div>
</div>
</label>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={settings.show_feedback_to_guests}
onChange={() => handleToggle('show_feedback_to_guests')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<Eye className="w-5 h-5 text-neutral-600" />
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
{t('feedback.settings.showToGuests', 'Show Feedback to Guests')}
</div>
<div className="text-xs text-neutral-500">
{t('feedback.settings.showToGuestsDesc', 'Other guests can see ratings, likes, and approved comments')}
</div>
</div>
</label>
</div>
</div>
<div className="border-t pt-4" />
{/* Rate Limiting */}
<div className="space-y-4">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={settings.enable_rate_limiting}
onChange={() => handleToggle('enable_rate_limiting')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
{t('feedback.settings.enableRateLimiting', 'Enable Rate Limiting')}
</div>
<div className="text-xs text-neutral-500">
{t('feedback.settings.rateLimitingDesc', 'Prevent spam by limiting feedback frequency')}
</div>
</div>
</label>
{settings.enable_rate_limiting && (
<div className="grid grid-cols-2 gap-4 ml-7">
<div>
<label className="block text-xs font-medium text-neutral-600 mb-1">
{t('feedback.settings.timeWindow', 'Time Window (minutes)')}
</label>
<input
type="number"
min="1"
max="60"
value={settings.rate_limit_window_minutes || 15}
onChange={(e) => handleNumberChange('rate_limit_window_minutes', e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-neutral-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-neutral-600 mb-1">
{t('feedback.settings.maxRequests', 'Max Requests')}
</label>
<input
type="number"
min="1"
max="100"
value={settings.rate_limit_max_requests || 10}
onChange={(e) => handleNumberChange('rate_limit_max_requests', e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-neutral-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
/>
</div>
</div>
)}
</div>
</>
)}
</div>
</Card>
);
};