tercul-frontend/client/src/components/reading/EnhancedLineNumberedText.tsx
Damir Mukimov 4ab2f3162a
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.
2025-11-30 15:33:52 +01:00

246 lines
6.8 KiB
TypeScript

import { Bookmark, Copy, MessageSquare } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useToast } from "@/hooks/use-toast";
interface EnhancedLineNumberedTextProps {
content: string;
fontSizeClass?: string;
onAnnotate: (lineNumber: number) => void;
highlightedLine?: number;
workId: string;
}
export function EnhancedLineNumberedText({
content,
fontSizeClass = "text-size-md",
onAnnotate,
highlightedLine,
workId,
}: EnhancedLineNumberedTextProps) {
const { toast } = useToast();
const [hoveredLine, setHoveredLine] = useState<number | null>(null);
const [bookmarkedLines, setBookmarkedLines] = useState<Set<number>>(
new Set(),
);
const [lineAnnotationCounts, _setLineAnnotationCounts] = useState<
Record<number, number>
>({
// Mock annotation counts - in a real app this would come from an API
2: 3,
5: 1,
8: 7,
});
// Split content into lines
const lines = content.split("\n");
const handleLineHover = (lineNumber: number) => {
setHoveredLine(lineNumber);
};
const handleLineLeave = () => {
setHoveredLine(null);
};
const handleCopyLine = (_lineNumber: number, lineText: string) => {
navigator.clipboard.writeText(lineText);
toast({
description: "Line copied to clipboard",
duration: 2000,
});
};
const handleCopyLineLink = (lineNumber: number) => {
const url = new URL(window.location.href);
url.hash = `line-${lineNumber}`;
navigator.clipboard.writeText(url.toString());
toast({
description: "Link to line copied to clipboard",
duration: 2000,
});
};
const handleToggleBookmark = async (lineNumber: number) => {
try {
const isBookmarked = bookmarkedLines.has(lineNumber);
// Optimistically update UI
setBookmarkedLines((prev) => {
const newBookmarks = new Set(prev);
if (isBookmarked) {
newBookmarks.delete(lineNumber);
} else {
newBookmarks.add(lineNumber);
}
return newBookmarks;
});
// In a real app, this would make an API call
// await apiRequest('POST', '/api/reading-bookmarks', {
// userId: 1, // Mock user ID
// workId,
// lineNumber,
// isBookmarked: !isBookmarked
// });
toast({
description: isBookmarked ? "Bookmark removed" : "Line bookmarked",
duration: 2000,
});
} catch (_error) {
// Revert on error
toast({
title: "Error",
description: "Could not update bookmark",
variant: "destructive",
});
}
};
return (
<div className={`reading-text ${fontSizeClass}`}>
{lines.map((line, index) => {
const lineNumber = index + 1;
const isHighlighted = lineNumber === highlightedLine;
const _isHovered = lineNumber === hoveredLine;
const isBookmarked = bookmarkedLines.has(lineNumber);
const annotationCount = lineAnnotationCounts[lineNumber] || 0;
// For blank lines, render a smaller empty line
if (!line.trim()) {
return (
<div
key={`line-${lineNumber}`}
id={`line-${lineNumber}`}
className="text-line-empty h-4"
/>
);
}
return (
<div
key={`line-${lineNumber}`}
id={`line-${lineNumber}`}
className={`text-line group ${
isHighlighted
? "bg-navy/10 dark:bg-cream/10"
: "hover:bg-navy/5 dark:hover:bg-cream/5"
} py-1.5 rounded flex relative transition-colors`}
onMouseEnter={() => handleLineHover(lineNumber)}
onMouseLeave={handleLineLeave}
>
{/* Line number indicator with bookmark feature */}
<div
className="line-number-container w-12 flex-shrink-0 flex justify-center items-center relative"
onClick={() => handleToggleBookmark(lineNumber)}
>
{isBookmarked ? (
<Bookmark
className="h-4 w-4 text-russet cursor-pointer"
fill="currentColor"
/>
) : (
<span className="line-number text-navy/40 dark:text-cream/40 text-sm select-none">
{lineNumber}
</span>
)}
</div>
{/* Line content */}
<div className="line-content flex-1 relative">
<p>{line}</p>
{/* Annotation indicator - if the line has annotations */}
{annotationCount > 0 && (
<div
className="annotation-indicator absolute -right-6 top-1/2 transform -translate-y-1/2 cursor-pointer"
onClick={() => onAnnotate(lineNumber)}
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
<MessageSquare className="h-4 w-4 text-russet" />
<span className="text-xs text-russet ml-0.5">
{annotationCount}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">Click to view annotations</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
{/* Action buttons that appear on hover */}
<div
className={`absolute right-0 top-1/2 transform -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity bg-cream/80 dark:bg-dark-surface/80 backdrop-blur-sm px-1 rounded ${isHighlighted ? "opacity-100" : ""}`}
>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={(e) => {
e.stopPropagation();
handleCopyLine(lineNumber, line);
}}
>
<Copy className="h-4 w-4 text-navy/70 dark:text-cream/70" />
<span className="sr-only">Copy line</span>
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={(e) => {
e.stopPropagation();
handleCopyLineLink(lineNumber);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-navy/70 dark:text-cream/70"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
<span className="sr-only">Copy link to line</span>
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={(e) => {
e.stopPropagation();
onAnnotate(lineNumber);
}}
>
<MessageSquare className="h-4 w-4 text-navy/70 dark:text-cream/70" />
<span className="sr-only">Annotate line</span>
</Button>
</div>
</div>
</div>
);
})}
</div>
);
}