diff --git a/client/src/components/reading/AnnotationSystem.tsx b/client/src/components/reading/AnnotationSystem.tsx new file mode 100644 index 0000000..4af3854 --- /dev/null +++ b/client/src/components/reading/AnnotationSystem.tsx @@ -0,0 +1,380 @@ +import { useState, useEffect, useRef } from 'react'; +import { Annotation } from '@/lib/types'; +import { Button } from '@/components/ui/button'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { useToast } from '@/hooks/use-toast'; +import { apiRequest, queryClient } from '@/lib/queryClient'; +import { MessageSquare, X, ThumbsUp, MessageCircle, Edit, Trash } from 'lucide-react'; + +interface AnnotationSystemProps { + workId: number; + selectedLineNumber: number | null; + onClose: () => void; + translationId?: number; +} + +export function AnnotationSystem({ + workId, + selectedLineNumber, + onClose, + translationId +}: AnnotationSystemProps) { + const { toast } = useToast(); + const [annotations, setAnnotations] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [newAnnotation, setNewAnnotation] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [editingAnnotationId, setEditingAnnotationId] = useState(null); + const [editText, setEditText] = useState(''); + + const annotationRef = useRef(null); + + // Mock user data - in a real app this would come from auth + const currentUser = { + id: 1, + name: 'Anonymous', + avatar: null + }; + + // Fetch annotations for the selected line + useEffect(() => { + if (!selectedLineNumber) return; + + setIsLoading(true); + + // Simulate API call to get annotations for the selected line + setTimeout(() => { + // These would be fetched from the API in a real app + const mockAnnotations: Annotation[] = [ + { + id: 1, + workId, + translationId, + lineNumber: selectedLineNumber, + userId: 2, + userName: 'Literary Scholar', + userAvatar: null, + content: 'This line demonstrates the poet\'s use of alliteration, creating a rhythmic pattern that emphasizes the emotional tone.', + createdAt: new Date(Date.now() - 1000000), + likes: 5, + liked: false + }, + { + id: 2, + workId, + translationId, + lineNumber: selectedLineNumber, + userId: 3, + userName: 'Translator', + userAvatar: null, + content: 'The original meaning in Russian contains a wordplay that is difficult to capture in English. A more literal translation might read as...', + createdAt: new Date(Date.now() - 5000000), + likes: 12, + liked: true + } + ]; + + setAnnotations(mockAnnotations); + setIsLoading(false); + }, 600); + }, [workId, selectedLineNumber, translationId]); + + // Submit new annotation + const handleSubmitAnnotation = async () => { + if (!newAnnotation.trim() || !selectedLineNumber) return; + + setIsSubmitting(true); + + try { + // In a real app, this would be an API call + // Mock API response + const newAnnotationObj: Annotation = { + id: Date.now(), + workId, + translationId, + lineNumber: selectedLineNumber, + userId: currentUser.id, + userName: currentUser.name, + userAvatar: currentUser.avatar, + content: newAnnotation, + createdAt: new Date(), + likes: 0, + liked: false + }; + + // Optimistically update UI + setAnnotations(prev => [newAnnotationObj, ...prev]); + setNewAnnotation(''); + + toast({ + description: "Annotation added successfully", + }); + + // In a real app, this would invalidate the query cache + // queryClient.invalidateQueries({ queryKey: [`/api/works/${workId}/annotations/${selectedLineNumber}`] }); + } catch (error) { + toast({ + title: "Error", + description: "Failed to add annotation", + variant: "destructive", + }); + } finally { + setIsSubmitting(false); + } + }; + + // Like an annotation + const handleLikeAnnotation = async (annotationId: number) => { + try { + // Optimistically update UI + setAnnotations(prev => + prev.map(anno => + anno.id === annotationId + ? { ...anno, liked: !anno.liked, likes: anno.liked ? anno.likes - 1 : anno.likes + 1 } + : anno + ) + ); + + // In a real app, this would be an API call + // await apiRequest('POST', `/api/annotations/${annotationId}/like`, { userId: currentUser.id }); + } catch (error) { + // Revert optimistic update if there's an error + setAnnotations(prev => [...prev]); + toast({ + title: "Error", + description: "Failed to update like", + variant: "destructive", + }); + } + }; + + // Delete annotation + const handleDeleteAnnotation = async (annotationId: number) => { + try { + // Optimistically update UI + const filteredAnnotations = annotations.filter(anno => anno.id !== annotationId); + setAnnotations(filteredAnnotations); + + // In a real app, this would be an API call + // await apiRequest('DELETE', `/api/annotations/${annotationId}`); + + toast({ + description: "Annotation deleted", + }); + } catch (error) { + // Revert optimistic update if there's an error + toast({ + title: "Error", + description: "Failed to delete annotation", + variant: "destructive", + }); + } + }; + + // Start editing an annotation + const handleStartEdit = (annotation: Annotation) => { + setEditingAnnotationId(annotation.id); + setEditText(annotation.content); + }; + + // Save edited annotation + const handleSaveEdit = async (annotationId: number) => { + if (!editText.trim()) return; + + try { + // Optimistically update UI + setAnnotations(prev => + prev.map(anno => + anno.id === annotationId + ? { ...anno, content: editText } + : anno + ) + ); + + // Reset edit state + setEditingAnnotationId(null); + setEditText(''); + + // In a real app, this would be an API call + // await apiRequest('PATCH', `/api/annotations/${annotationId}`, { content: editText }); + + toast({ + description: "Annotation updated", + }); + } catch (error) { + toast({ + title: "Error", + description: "Failed to update annotation", + variant: "destructive", + }); + } + }; + + // Cancel editing + const handleCancelEdit = () => { + setEditingAnnotationId(null); + setEditText(''); + }; + + // If no line is selected, don't render anything + if (!selectedLineNumber) return null; + + return ( +
+
+
+ +

+ Line {selectedLineNumber} Annotations +

+
+ +
+ +
+ {/* New annotation form */} +
+