import { useState, useEffect, useRef, useCallback } from "react" import { Pause, Play } from "lucide-react" import { Button } from "@/app/_components/ui/button" import { cn } from "@/app/_lib/utils" import { Slider } from "@/app/_components/ui/slider" import { getMonthName } from "@/app/_utils/common" interface CrimeTimelapseProps { startYear: number endYear: number onChange?: (year: number, month: number, progress: number) => void onPlayingChange?: (isPlaying: boolean) => void className?: string autoPlay?: boolean autoPlaySpeed?: number // Time to progress through one month in ms enablePerformanceMode?: boolean // Flag untuk mode performa } export function CrimeTimelapse({ startYear = 2020, endYear = 2024, onChange, onPlayingChange, className, autoPlay = true, autoPlaySpeed = 1000, // Speed of month progress enablePerformanceMode = true, // Default aktifkan mode performa tinggi }: CrimeTimelapseProps) { const [currentYear, setCurrentYear] = useState(startYear) const [currentMonth, setCurrentMonth] = useState(1) // Start at January (1) const [progress, setProgress] = useState(0) // Progress within the current month const [isPlaying, setIsPlaying] = useState(autoPlay) const [isDragging, setIsDragging] = useState(false) const animationRef = useRef(null) const lastUpdateTimeRef = useRef(0) const frameSkipCountRef = useRef(0) // Untuk pelompatan frame // Jumlah frame yang akan dilewati saat performance mode aktif const frameSkipThreshold = enablePerformanceMode ? 3 : 0 // Hitung total bulan dari awal hingga akhir tahun (memoisasi) const totalMonths = useRef(((endYear - startYear) * 12) + 12) // +12 untuk memasukkan semua bulan tahun akhir // Menggunakan useCallback untuk fungsi yang sering dipanggil const calculateOverallProgress = useCallback((): number => { const yearDiff = currentYear - startYear const monthProgress = (yearDiff * 12) + (currentMonth - 1) return ((monthProgress + progress) / (totalMonths.current - 1)) * 100 }, [currentYear, currentMonth, progress, startYear, totalMonths]) const calculateTimeFromProgress = useCallback((overallProgress: number): { year: number; month: number; progress: number } => { const totalProgress = (overallProgress * (totalMonths.current - 1)) / 100 const monthsFromStart = Math.floor(totalProgress) const year = startYear + Math.floor(monthsFromStart / 12) const month = (monthsFromStart % 12) + 1 // 1-12 for months const monthProgress = totalProgress - Math.floor(totalProgress) return { year: Math.min(year, endYear), month: Math.min(month, 12), progress: monthProgress } }, [startYear, endYear, totalMonths]) // Calculate the current position for the active marker const calculateMarkerPosition = useCallback((): string => { return `${calculateOverallProgress()}%` }, [calculateOverallProgress]) // Optimasi animasi dengan throttling berbasis requestAnimationFrame const animate = useCallback((timestamp: number) => { if (!lastUpdateTimeRef.current) { lastUpdateTimeRef.current = timestamp } if (!isDragging) { // Performance optimization: skip frames in performance mode frameSkipCountRef.current++ if (frameSkipCountRef.current > frameSkipThreshold || !enablePerformanceMode) { frameSkipCountRef.current = 0 const elapsed = timestamp - lastUpdateTimeRef.current const progressIncrement = elapsed / autoPlaySpeed let newProgress = progress + progressIncrement let newMonth = currentMonth let newYear = currentYear if (newProgress >= 1) { newProgress = 0 newMonth = currentMonth + 1 if (newMonth > 12) { newMonth = 1 newYear = currentYear + 1 if (newYear > endYear) { newYear = startYear newMonth = 1 } } setCurrentMonth(newMonth) setCurrentYear(newYear) } setProgress(newProgress) // Notify parent component only after calculating all changes if (onChange) { onChange(newYear, newMonth, newProgress) } lastUpdateTimeRef.current = timestamp } } if (isPlaying) { animationRef.current = requestAnimationFrame(animate) } }, [isPlaying, isDragging, progress, currentMonth, currentYear, onChange, autoPlaySpeed, startYear, endYear, enablePerformanceMode, frameSkipThreshold]) useEffect(() => { if (isPlaying) { lastUpdateTimeRef.current = 0 frameSkipCountRef.current = 0 animationRef.current = requestAnimationFrame(animate) } else if (animationRef.current) { cancelAnimationFrame(animationRef.current) } return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current) } } }, [isPlaying, animate]) // Memoized handler for play/pause const handlePlayPause = useCallback(() => { setIsPlaying(prevState => { const newPlayingState = !prevState if (onPlayingChange) { onPlayingChange(newPlayingState) } return newPlayingState }) }, [onPlayingChange]) // Memoized handler for slider change const handleSliderChange = useCallback((value: number[]) => { const overallProgress = value[0] const { year, month, progress } = calculateTimeFromProgress(overallProgress) setCurrentYear(year) setCurrentMonth(month) setProgress(progress) if (onChange) { onChange(year, month, progress) } }, [calculateTimeFromProgress, onChange]) const handleSliderDragStart = useCallback(() => { setIsDragging(true) if (onPlayingChange) { onPlayingChange(true) // Treat dragging as a form of "playing" for performance optimization } }, [onPlayingChange]) const handleSliderDragEnd = useCallback(() => { setIsDragging(false) if (onPlayingChange) { onPlayingChange(isPlaying) // Restore to actual playing state } }, [isPlaying, onPlayingChange]) // Komputasi tahun marker dilakukan sekali saja dan di-cache const yearMarkers = useRef([]) useEffect(() => { const markers = [] for (let year = startYear; year <= endYear; year++) { markers.push(year) } yearMarkers.current = markers }, [startYear, endYear]) // Gunakan React.memo untuk komponen child jika diperlukan // Contoh: const YearMarker = React.memo(({ year, isActive }) => { ... }) return (
{/* Current month/year marker that moves with the slider */}
{isPlaying}{getMonthName(currentMonth)} {currentYear}
{/* Wrap button and slider in their container */}
{/* Play/Pause button */} {/* Slider */}
{/* Year markers */}
{yearMarkers.current.map((year, index) => (
{year}
))}
) }