import React, { useState, useEffect } from 'react'; import { ChevronLeft, ChevronRight, X, MoreHorizontal, Heart, MessageCircle, Play, Bookmark } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { Post } from '../types'; import { cn, formatDateSafe } from '../lib/utils'; import { MediaRenderer } from './MediaRenderer'; interface PostModalProps { post: Post; nextPost?: Post; prevPost?: Post; onClose: () => void; onNextPost?: () => void; onPrevPost?: () => void; hasNextPost?: boolean; hasPrevPost?: boolean; profilePic: string | null; } export const PostModal: React.FC = ({ post, nextPost, prevPost, onClose, onNextPost, onPrevPost, hasNextPost, hasPrevPost, profilePic }) => { const [currentIndex, setCurrentIndex] = useState(0); const [direction, setDirection] = useState(0); // Preloading Logic useEffect(() => { const controller = new AbortController(); const preloadMedia = async (url: string, type: 'image' | 'video') => { if (!url) return; try { if (type === 'image') { const img = new Image(); img.src = url; } else { const video = document.createElement('video'); video.src = url; video.preload = 'auto'; } } catch (e) {} }; // 1. Current post: Immediate preload of first two slides if (post.media[0]) preloadMedia(post.media[0].url, post.media[0].type); if (post.media[1]) preloadMedia(post.media[1].url, post.media[1].type); // 2. Next/Prev posts: Preload their first slides if (nextPost?.media[0]) preloadMedia(nextPost.media[0].url, nextPost.media[0].type); if (prevPost?.media[0]) preloadMedia(prevPost.media[0].url, prevPost.media[0].type); // 3. Current post: Delayed preload of the rest const timeout = setTimeout(() => { for (let i = 2; i < post.media.length; i++) { if (controller.signal.aborted) break; preloadMedia(post.media[i].url, post.media[i].type); } }, 1000); return () => { controller.abort(); clearTimeout(timeout); }; }, [post.id, post.media, nextPost?.id, prevPost?.id]); useEffect(() => setCurrentIndex(0), [post.id]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'ArrowRight') onNextPost?.(); else if (e.key === 'ArrowLeft') onPrevPost?.(); else if (e.key === '.') paginate(1); else if (e.key === ',') paginate(-1); else if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onNextPost, onPrevPost, currentIndex, post.media.length, onClose]); const paginate = (newDirection: number) => { const nextIndex = currentIndex + newDirection; if (nextIndex >= 0 && nextIndex < post.media.length) { setDirection(newDirection); setCurrentIndex(nextIndex); } }; const variants = { enter: (d: number) => ({ x: d > 0 ? '100%' : '-100%', opacity: 1, zIndex: 0 }), center: { zIndex: 1, x: 0, opacity: 1 }, exit: (d: number) => ({ zIndex: 0, x: d < 0 ? '100%' : '-100%', opacity: 1 }) }; const swipePower = (offset: number, velocity: number) => Math.abs(offset) * velocity; return (
{hasPrevPost && onPrevPost && } {hasNextPost && onNextPost && } { if (offset.y > 200 || velocity.y > 800) onClose(); }} className="bg-black flex flex-col md:flex-row w-full max-w-6xl h-auto md:rounded-sm overflow-hidden shadow-2xl relative text-black" onClick={e => e.stopPropagation()}>
{ const s = swipePower(offset.x, velocity.x); if (s < -15000) { if (currentIndex < post.media.length - 1) paginate(1); else if (hasNextPost && onNextPost && s < -40000) onNextPost(); } else if (s > 15000) { if (currentIndex > 0) paginate(-1); else if (hasPrevPost && onPrevPost && s > 40000) onPrevPost(); } }} className="col-start-1 row-start-1 w-full flex items-center justify-center cursor-grab active:cursor-grabbing relative text-black" >
{post.media.length > 1 && ( <> {currentIndex > 0 && } {currentIndex < post.media.length - 1 && }
{post.media.map((_, i) =>
)}
)}
{profilePic ? : {post.username[0]}}
{post.username}
{profilePic ? : {post.username[0]}}
{post.username}{post.caption}
{formatDateSafe(post.date, 'MMMM d, yyyy')}
Archived Post{post.id}
); };