mirror of
https://github.com/SamyRai/tercul-frontend.git
synced 2025-12-27 04:51:34 +00:00
* feat: Add missing shared schema types and fix TypeScript imports - Add AuthorWithStats and AnnotationWithUser schemas with relations - Add corresponding TypeScript type exports - Update tsconfig.json with @shared/* path mapping - Fix @shared/schema import issues across components - Resolve major TypeScript compilation errors Next: Fix remaining type mismatches and component prop issues * fix: resolve major TypeScript errors and type mismatches - Add AuthorWithStats and AnnotationWithUser schemas with proper relations - Fix AnnotationSystem component type issues (string IDs, nested user objects) - Update component props to match schema expectations - Fix function parameter types for annotation operations - Resolve null/undefined type assignments - Add missing required properties (type, isOfficial) to annotations Remaining issues: Test ES module configuration and some component prop type mismatches * fix: resolve remaining TypeScript errors and improve type safety - Fix tag-manager component to work with string IDs from schema - Update author-stats component to use schema-based AuthorWithStats type - Add missing utility functions (formatNumber, formatRating) to author utils - Fix WorkCard test to use correct schema types with string IDs - Resolve type mismatches in component props and form handling - Update interface definitions to match schema requirements Linting: ✅ 90%+ resolved, remaining minor issues Testing: ⚠️ ES module configuration needs refinement * fix: complete TypeScript fixes and testing refinements - Fix remaining AnnotationSystem component type issues - Update FilterSidebar to use string tag IDs - Resolve all major TypeScript compilation errors - Testing infrastructure fully functional with Jest + ES modules - Linting errors reduced to minor unused variable warnings All critical type safety and testing issues resolved! * Fix annotation types and author utils * Fix TypeScript and testing infrastructure issues - Fix AnnotationSystem component types (string IDs, user objects, liked/likes properties) - Add formatNumber and formatRating utilities for author components - Update FilterSidebar to use correct tag ID types (string vs number) - Fix EnhancedReadingView translation and work ID type mismatches - Resolve Playwright dependency issues in testing setup - Update Jest configuration for ES module compatibility - Fix import paths and type conflicts across components All unit tests now pass and major TypeScript compilation errors resolved. * Fix Vite build configuration for CI - Set root to 'client' directory to find index.html - Configure path aliases (@/* and @shared/*) for proper module resolution - Set build output directory to '../dist' to place files in frontend root Resolves CI build failure: 'Could not resolve entry module index.html' * Fix Docker build for Yarn v4 - Replace deprecated 'yarn install --immutable --production' with 'yarn workspaces focus --production' - This resolves the YN0050 error in CI Docker builds Yarn v4 deprecated the --production flag on install command.
433 lines
12 KiB
TypeScript
433 lines
12 KiB
TypeScript
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import {
|
|
Edit,
|
|
MessageCircle,
|
|
MessageSquare,
|
|
ThumbsUp,
|
|
Trash,
|
|
X,
|
|
} from "lucide-react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import type { AnnotationWithUser } from "../../../../shared/schema";
|
|
|
|
interface AnnotationSystemProps {
|
|
workId: string;
|
|
selectedLineNumber: number | null;
|
|
onClose: () => void;
|
|
translationId?: string;
|
|
}
|
|
|
|
export function AnnotationSystem({
|
|
workId,
|
|
selectedLineNumber,
|
|
onClose,
|
|
translationId,
|
|
}: AnnotationSystemProps) {
|
|
const { toast } = useToast();
|
|
const [annotations, setAnnotations] = useState<AnnotationWithUser[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [newAnnotation, setNewAnnotation] = useState("");
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [editingAnnotationId, setEditingAnnotationId] = useState<string | null>(
|
|
null,
|
|
);
|
|
const [editText, setEditText] = useState("");
|
|
|
|
const annotationRef = useRef<HTMLDivElement>(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: AnnotationWithUser[] = [
|
|
{
|
|
id: "1" as const,
|
|
workId: workId,
|
|
translationId: translationId,
|
|
lineNumber: selectedLineNumber,
|
|
userId: "2" as const,
|
|
user: {
|
|
name: "Literary Scholar",
|
|
avatar: undefined,
|
|
},
|
|
likes: 5,
|
|
liked: false,
|
|
content:
|
|
"This line demonstrates the poet's use of alliteration, creating a rhythmic pattern that emphasizes the emotional tone.",
|
|
type: "analysis" as const,
|
|
isOfficial: false,
|
|
createdAt: new Date(Date.now() - 1000000).toISOString(),
|
|
},
|
|
{
|
|
id: "2" as const,
|
|
workId: workId,
|
|
translationId: translationId,
|
|
lineNumber: selectedLineNumber,
|
|
userId: "3" as const,
|
|
user: {
|
|
name: "Translator",
|
|
avatar: undefined,
|
|
},
|
|
likes: 3,
|
|
liked: false,
|
|
content:
|
|
"The original meaning in Russian contains a wordplay that is difficult to capture in English. A more literal translation might read as...",
|
|
type: "translation" as const,
|
|
isOfficial: false,
|
|
createdAt: new Date(Date.now() - 5000000).toISOString(),
|
|
},
|
|
];
|
|
|
|
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: AnnotationWithUser = {
|
|
id: Date.now().toString(),
|
|
workId,
|
|
translationId,
|
|
lineNumber: selectedLineNumber,
|
|
userId: currentUser.id.toString(),
|
|
user: {
|
|
name: currentUser.name,
|
|
avatar: currentUser.avatar || undefined,
|
|
},
|
|
content: newAnnotation,
|
|
type: "comment",
|
|
isOfficial: false,
|
|
createdAt: new Date().toISOString(),
|
|
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: string) => {
|
|
try {
|
|
// Optimistically update UI
|
|
setAnnotations((prev) =>
|
|
prev.map((anno) =>
|
|
anno.id === annotationId
|
|
? {
|
|
...anno,
|
|
liked: !anno.liked,
|
|
likes: anno.liked ? (anno.likes || 0) - 1 : (anno.likes || 0) + 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: string) => {
|
|
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: AnnotationWithUser) => {
|
|
setEditingAnnotationId(annotation.id);
|
|
setEditText(annotation.content);
|
|
};
|
|
|
|
// Save edited annotation
|
|
const handleSaveEdit = async (annotationId: string) => {
|
|
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 (
|
|
<div
|
|
ref={annotationRef}
|
|
className="annotation-panel bg-cream dark:bg-dark-surface border-l border-sage/20 dark:border-sage/10 w-80 lg:w-96 fixed top-0 right-0 bottom-0 z-50 overflow-y-auto transition-transform shadow-xl"
|
|
>
|
|
<div className="sticky top-0 z-10 bg-cream dark:bg-dark-surface border-b border-sage/20 dark:border-sage/10 px-4 py-3 flex justify-between items-center">
|
|
<div className="flex items-center gap-2">
|
|
<MessageSquare className="h-5 w-5 text-russet" />
|
|
<h3 className="font-medium text-navy dark:text-cream">
|
|
Line {selectedLineNumber} Annotations
|
|
</h3>
|
|
</div>
|
|
<Button variant="ghost" size="icon" onClick={onClose}>
|
|
<X className="h-5 w-5" />
|
|
<span className="sr-only">Close</span>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="p-4">
|
|
{/* New annotation form */}
|
|
<div className="mb-6">
|
|
<Textarea
|
|
placeholder="Add your annotation to this line..."
|
|
className="min-h-24 resize-y"
|
|
value={newAnnotation}
|
|
onChange={(e) => setNewAnnotation(e.target.value)}
|
|
/>
|
|
<div className="flex justify-end mt-2">
|
|
<Button
|
|
onClick={handleSubmitAnnotation}
|
|
disabled={isSubmitting || !newAnnotation.trim()}
|
|
className="bg-russet hover:bg-russet/90 text-white"
|
|
>
|
|
{isSubmitting ? "Submitting..." : "Add Annotation"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Annotations list */}
|
|
<div className="space-y-4">
|
|
{isLoading ? (
|
|
<div className="text-center py-8">
|
|
<div className="loader"></div>
|
|
<p className="text-navy/70 dark:text-cream/70 mt-2">
|
|
Loading annotations...
|
|
</p>
|
|
</div>
|
|
) : annotations.length === 0 ? (
|
|
<div className="text-center py-8">
|
|
<MessageCircle className="h-12 w-12 mx-auto text-navy/30 dark:text-cream/30" />
|
|
<p className="text-navy/70 dark:text-cream/70 mt-2">
|
|
No annotations yet
|
|
</p>
|
|
<p className="text-sm text-navy/50 dark:text-cream/50">
|
|
Be the first to annotate this line
|
|
</p>
|
|
</div>
|
|
) : (
|
|
annotations.map((annotation) => (
|
|
<Card
|
|
key={annotation.id}
|
|
className="border-sage/20 dark:border-sage/10"
|
|
>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex justify-between items-start">
|
|
<div className="flex items-center gap-2">
|
|
<Avatar className="h-8 w-8">
|
|
<AvatarImage
|
|
src={annotation.user.avatar || ""}
|
|
alt={annotation.user.name}
|
|
/>
|
|
<AvatarFallback className="text-xs bg-navy/10 dark:bg-navy/20 text-navy dark:text-cream">
|
|
{annotation.user.name.charAt(0).toUpperCase()}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<CardTitle className="text-sm font-medium text-navy dark:text-cream">
|
|
{annotation.user.name}
|
|
</CardTitle>
|
|
<p className="text-xs text-navy/60 dark:text-cream/60">
|
|
{new Date(annotation.createdAt).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Edit/Delete buttons for user's own annotations */}
|
|
{annotation.userId === currentUser.id.toString() && (
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => handleStartEdit(annotation)}
|
|
>
|
|
<Edit className="h-4 w-4" />
|
|
<span className="sr-only">Edit</span>
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
|
onClick={() => handleDeleteAnnotation(annotation.id)}
|
|
>
|
|
<Trash className="h-4 w-4" />
|
|
<span className="sr-only">Delete</span>
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="pt-2">
|
|
{editingAnnotationId === annotation.id ? (
|
|
<div>
|
|
<Textarea
|
|
value={editText}
|
|
onChange={(e) => setEditText(e.target.value)}
|
|
className="min-h-24 resize-y mb-2"
|
|
/>
|
|
<div className="flex justify-end gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleCancelEdit}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
onClick={() => handleSaveEdit(annotation.id)}
|
|
disabled={!editText.trim()}
|
|
>
|
|
Save
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-navy/90 dark:text-cream/90 whitespace-pre-wrap">
|
|
{annotation.content}
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
|
|
<CardFooter className="pt-2 flex justify-between items-center">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className={`flex items-center gap-1 text-xs ${
|
|
annotation.liked
|
|
? "text-russet"
|
|
: "text-navy/70 dark:text-cream/70"
|
|
}`}
|
|
onClick={() => handleLikeAnnotation(annotation.id)}
|
|
>
|
|
<ThumbsUp
|
|
className={`h-4 w-4 ${annotation.liked ? "fill-russet" : ""}`}
|
|
/>
|
|
<span>{annotation.likes}</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="text-xs text-navy/70 dark:text-cream/70"
|
|
>
|
|
<MessageCircle className="h-4 w-4 mr-1" />
|
|
<span>Reply</span>
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|