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>
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
import { api } from '../config/api';
|
||||
|
||||
export 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 interface PhotoFeedback {
|
||||
id: number;
|
||||
photo_id: number;
|
||||
event_id: number;
|
||||
feedback_type: 'rating' | 'like' | 'comment' | 'favorite';
|
||||
rating?: number;
|
||||
comment_text?: string;
|
||||
guest_name?: string;
|
||||
guest_email?: string;
|
||||
is_approved: boolean;
|
||||
is_hidden: boolean;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
filename?: string;
|
||||
path?: string;
|
||||
is_mine?: boolean;
|
||||
}
|
||||
|
||||
export interface FeedbackSummary {
|
||||
average_rating: number;
|
||||
total_ratings: number;
|
||||
like_count: number;
|
||||
favorite_count: number;
|
||||
comment_count: number;
|
||||
}
|
||||
|
||||
export interface MyFeedback {
|
||||
rating?: number;
|
||||
liked: boolean;
|
||||
favorited: boolean;
|
||||
}
|
||||
|
||||
export interface FeedbackResponse {
|
||||
feedback: PhotoFeedback[];
|
||||
summary: FeedbackSummary;
|
||||
my_feedback: MyFeedback;
|
||||
}
|
||||
|
||||
export interface FeedbackAnalytics {
|
||||
summary: {
|
||||
total_feedback: number;
|
||||
total_ratings: number;
|
||||
average_rating: number;
|
||||
total_likes: number;
|
||||
total_comments: number;
|
||||
total_favorites: number;
|
||||
pending_moderation: number;
|
||||
};
|
||||
topRated: Array<{
|
||||
id: number;
|
||||
filename: string;
|
||||
average_rating: number;
|
||||
feedback_count: number;
|
||||
like_count: number;
|
||||
}>;
|
||||
mostLiked: Array<{
|
||||
id: number;
|
||||
filename: string;
|
||||
like_count: number;
|
||||
average_rating: number;
|
||||
}>;
|
||||
recentComments: Array<{
|
||||
comment_text: string;
|
||||
guest_name: string;
|
||||
created_at: string;
|
||||
filename: string;
|
||||
}>;
|
||||
timeline: Array<{
|
||||
date: string;
|
||||
count: number;
|
||||
feedback_type: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
class FeedbackService {
|
||||
// Admin endpoints
|
||||
async getEventFeedbackSettings(eventId: string): Promise<FeedbackSettings> {
|
||||
const response = await api.get(`/admin/feedback/events/${eventId}/feedback-settings`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateEventFeedbackSettings(eventId: string, settings: FeedbackSettings): Promise<FeedbackSettings> {
|
||||
const response = await api.put(`/admin/feedback/events/${eventId}/feedback-settings`, settings);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getEventFeedback(eventId: string, params?: {
|
||||
type?: string;
|
||||
status?: string;
|
||||
photoId?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const response = await api.get(`/admin/feedback/events/${eventId}/feedback`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async moderateFeedback(feedbackId: string, action: 'approve' | 'hide' | 'reject') {
|
||||
const response = await api.put(`/admin/feedback/feedback/${feedbackId}/${action}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteFeedback(feedbackId: string) {
|
||||
const response = await api.delete(`/admin/feedback/feedback/${feedbackId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getEventFeedbackAnalytics(eventId: string): Promise<FeedbackAnalytics> {
|
||||
const response = await api.get(`/admin/feedback/events/${eventId}/feedback-analytics`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async exportEventFeedback(eventId: string, format: 'json' | 'csv' = 'json') {
|
||||
const response = await api.get(`/admin/feedback/events/${eventId}/feedback/export`, {
|
||||
params: { format },
|
||||
responseType: format === 'csv' ? 'blob' : 'json'
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getPendingModeration() {
|
||||
const response = await api.get('/admin/feedback/feedback/pending-moderation');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Word filter management
|
||||
async getWordFilters() {
|
||||
const response = await api.get('/admin/feedback/feedback/word-filters');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async addWordFilter(word: string, severity: 'low' | 'moderate' | 'high' = 'moderate') {
|
||||
const response = await api.post('/admin/feedback/feedback/word-filters', { word, severity });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateWordFilter(id: string, updates: { word?: string; severity?: string; is_active?: boolean }) {
|
||||
const response = await api.put(`/admin/feedback/feedback/word-filters/${id}`, updates);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteWordFilter(id: string) {
|
||||
const response = await api.delete(`/admin/feedback/feedback/word-filters/${id}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Guest endpoints
|
||||
async getGalleryFeedbackSettings(slug: string): Promise<Partial<FeedbackSettings>> {
|
||||
const response = await api.get(`/gallery/${slug}/feedback-settings`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getPhotoFeedback(slug: string, photoId: string): Promise<FeedbackResponse> {
|
||||
const response = await api.get(`/gallery/${slug}/photos/${photoId}/feedback`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async submitFeedback(slug: string, photoId: string, feedback: {
|
||||
feedback_type: 'rating' | 'like' | 'comment' | 'favorite';
|
||||
rating?: number;
|
||||
comment_text?: string;
|
||||
guest_name?: string;
|
||||
guest_email?: string;
|
||||
}) {
|
||||
const response = await api.post(`/gallery/${slug}/photos/${photoId}/feedback`, feedback);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getGalleryFeedbackSummary(slug: string) {
|
||||
const response = await api.get(`/gallery/${slug}/feedback-summary`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getMyFeedback(slug: string) {
|
||||
const response = await api.get(`/gallery/${slug}/my-feedback`);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const feedbackService = new FeedbackService();
|
||||
@@ -7,4 +7,5 @@ export { archiveService } from './archive.service';
|
||||
export { emailService } from './email.service';
|
||||
export { settingsService } from './settings.service';
|
||||
export { cmsService } from './cms.service';
|
||||
export { notificationsService } from './notifications.service';
|
||||
export { notificationsService } from './notifications.service';
|
||||
export { feedbackService } from './feedback.service';
|
||||
Reference in New Issue
Block a user