Add interactive progress bar with slide preview to reports
Implement a slider component for report navigation, enabling direct seeking and hover-based slide previews, replacing previous/next buttons. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9a264234-c5d7-4dcc-adf3-a954b149b30d Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3df548ff-50ae-432f-9be4-25d34eccc983/9a264234-c5d7-4dcc-adf3-a954b149b30d/VGhYqEL
This commit is contained in:
@ -5,6 +5,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useState, useEffect, useRef, useMemo } from "react";
|
import { useState, useEffect, useRef, useMemo } from "react";
|
||||||
@ -35,6 +36,8 @@ function MohamedSalahSlides() {
|
|||||||
const [currentSlide, setCurrentSlide] = useState(1);
|
const [currentSlide, setCurrentSlide] = useState(1);
|
||||||
const [numPages, setNumPages] = useState(0);
|
const [numPages, setNumPages] = useState(0);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [hoverSlide, setHoverSlide] = useState<number | null>(null);
|
||||||
|
const [hoverPosition, setHoverPosition] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
|
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
|
||||||
setNumPages(numPages);
|
setNumPages(numPages);
|
||||||
@ -47,6 +50,28 @@ function MohamedSalahSlides() {
|
|||||||
cMapPacked: true,
|
cMapPacked: true,
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
|
const handleSliderChange = (value: number[]) => {
|
||||||
|
if (numPages > 0) {
|
||||||
|
setCurrentSlide(value[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSliderHover = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (numPages === 0) return;
|
||||||
|
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const percentage = x / rect.width;
|
||||||
|
const slideNumber = Math.max(1, Math.min(numPages, Math.round(percentage * numPages)));
|
||||||
|
|
||||||
|
setHoverSlide(slideNumber);
|
||||||
|
setHoverPosition({ x: e.clientX, y: rect.top - 60 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSliderLeave = () => {
|
||||||
|
setHoverSlide(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="w-full relative overflow-hidden" style={{ paddingTop: '56.25%' }} data-testid="slide-container-16-9">
|
<div className="w-full relative overflow-hidden" style={{ paddingTop: '56.25%' }} data-testid="slide-container-16-9">
|
||||||
@ -77,26 +102,80 @@ function MohamedSalahSlides() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-4">
|
||||||
<Button
|
{/* Progress Bar */}
|
||||||
variant="outline"
|
<div className="relative px-2" data-testid="slide-progress-container">
|
||||||
onClick={() => setCurrentSlide(Math.max(1, currentSlide - 1))}
|
<div
|
||||||
disabled={numPages === 0 || currentSlide === 1}
|
className="relative py-2"
|
||||||
data-testid="button-previous-slide"
|
onMouseMove={handleSliderHover}
|
||||||
>
|
onMouseLeave={handleSliderLeave}
|
||||||
Previous
|
data-testid="slider-hover-area"
|
||||||
</Button>
|
>
|
||||||
<span className="text-sm text-gray-600" data-testid="text-slide-counter">
|
<Slider
|
||||||
Slide {currentSlide} of {numPages}
|
value={[currentSlide]}
|
||||||
</span>
|
onValueChange={handleSliderChange}
|
||||||
<Button
|
min={1}
|
||||||
variant="outline"
|
max={numPages || 1}
|
||||||
onClick={() => setCurrentSlide(Math.min(numPages || 1, currentSlide + 1))}
|
step={1}
|
||||||
disabled={numPages === 0 || currentSlide === numPages}
|
disabled={numPages === 0}
|
||||||
data-testid="button-next-slide"
|
className="cursor-pointer"
|
||||||
>
|
data-testid="slider-progress"
|
||||||
Next
|
/>
|
||||||
</Button>
|
{/* Slide markers */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-full pointer-events-none">
|
||||||
|
<div className="relative h-full px-2">
|
||||||
|
{numPages > 0 && Array.from({ length: numPages }).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="absolute top-1/2 -translate-y-1/2 w-1 h-1 bg-gray-300 rounded-full"
|
||||||
|
style={{
|
||||||
|
left: `${(i / (numPages - 1)) * 100}%`,
|
||||||
|
transform: 'translate(-50%, -50%)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hover tooltip */}
|
||||||
|
{hoverSlide !== null && (
|
||||||
|
<div
|
||||||
|
className="fixed z-50 bg-gray-900 text-white px-3 py-2 rounded-lg text-sm font-medium shadow-lg pointer-events-none"
|
||||||
|
style={{
|
||||||
|
left: `${hoverPosition.x}px`,
|
||||||
|
top: `${hoverPosition.y}px`,
|
||||||
|
transform: 'translateX(-50%)'
|
||||||
|
}}
|
||||||
|
data-testid="tooltip-slide-preview"
|
||||||
|
>
|
||||||
|
Slide {hoverSlide}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Control buttons */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setCurrentSlide(Math.max(1, currentSlide - 1))}
|
||||||
|
disabled={numPages === 0 || currentSlide === 1}
|
||||||
|
data-testid="button-previous-slide"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-gray-600" data-testid="text-slide-counter">
|
||||||
|
Slide {currentSlide} of {numPages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setCurrentSlide(Math.min(numPages || 1, currentSlide + 1))}
|
||||||
|
disabled={numPages === 0 || currentSlide === numPages}
|
||||||
|
data-testid="button-next-slide"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user