diff --git a/client/src/components/media-upload.tsx b/client/src/components/media-upload.tsx index ea14abb..7ce35bb 100644 --- a/client/src/components/media-upload.tsx +++ b/client/src/components/media-upload.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from "react"; +import { useState, useCallback, useRef, useEffect } from "react"; import { useDropzone } from "react-dropzone"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent } from "@/components/ui/dialog"; @@ -11,6 +11,8 @@ import { SiFacebook, SiInstagram } from "react-icons/si"; export default function MediaUpload() { const { toast } = useToast(); const [selectedFile, setSelectedFile] = useState(null); + const [visibleCount, setVisibleCount] = useState(5); + const loadMoreRef = useRef(null); const { data: mediaList } = useQuery({ queryKey: ["/api/media"], @@ -69,6 +71,28 @@ export default function MediaUpload() { }, }); + // Scroll infini - IntersectionObserver + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && mediaList && visibleCount < (mediaList as any[]).length) { + setVisibleCount(prev => Math.min(prev + 5, (mediaList as any[]).length)); + } + }, + { threshold: 0.1 } + ); + + if (loadMoreRef.current) { + observer.observe(loadMoreRef.current); + } + + return () => { + if (loadMoreRef.current) { + observer.unobserve(loadMoreRef.current); + } + }; + }, [mediaList, visibleCount]); + return (
@@ -123,8 +147,8 @@ export default function MediaUpload() { -
- {(mediaList as any[])?.slice(0, 5).map((media: any) => ( +
+ {(mediaList as any[])?.slice(0, visibleCount).map((media: any) => (
setSelectedFile(media)} @@ -165,6 +189,13 @@ export default function MediaUpload() {
))} + + {/* Élément sentinelle pour le scroll infini */} + {mediaList && visibleCount < (mediaList as any[]).length && ( +
+ +
+ )}
diff --git a/client/src/pages/new-post.tsx b/client/src/pages/new-post.tsx index 3bd1486..9baacbc 100644 --- a/client/src/pages/new-post.tsx +++ b/client/src/pages/new-post.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useRef } from "react"; +import { useState, useCallback, useRef, useEffect } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { useLocation } from "wouter"; import { Send, Sparkles, Image as ImageIcon, Calendar, Upload, Camera, GripVertical, Loader2 } from "lucide-react"; @@ -112,6 +112,8 @@ export default function NewPost() { const [postText, setPostText] = useState(''); const [postType, setPostType] = useState<'feed' | 'story' | 'both'>('feed'); const [previewModalOpen, setPreviewModalOpen] = useState(false); + const [visibleMediaCount, setVisibleMediaCount] = useState(12); + const loadMoreMediaRef = useRef(null); const { data: pages = [] } = useQuery({ queryKey: ['/api/pages'], @@ -310,6 +312,28 @@ export default function NewPost() { } }; + // Scroll infini - IntersectionObserver pour la grille de médias + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && mediaList && visibleMediaCount < mediaList.length) { + setVisibleMediaCount(prev => Math.min(prev + 12, mediaList.length)); + } + }, + { threshold: 0.1 } + ); + + if (loadMoreMediaRef.current) { + observer.observe(loadMoreMediaRef.current); + } + + return () => { + if (loadMoreMediaRef.current) { + observer.unobserve(loadMoreMediaRef.current); + } + }; + }, [mediaList, visibleMediaCount]); + return (
{sidebarOpen && ( @@ -451,48 +475,57 @@ export default function NewPost() {
Toutes les photos
-
- {mediaList.map((media) => { - const isSelected = selectedMedia.includes(media.id); - - return ( - - ); - })} +
+
+ {mediaList.slice(0, visibleMediaCount).map((media) => { + const isSelected = selectedMedia.includes(media.id); + + return ( + + ); + })} +
+ + {/* Élément sentinelle pour le scroll infini */} + {mediaList && visibleMediaCount < mediaList.length && ( +
+ +
+ )}
)} diff --git a/replit.md b/replit.md index f37ed4c..ad54cda 100644 --- a/replit.md +++ b/replit.md @@ -43,6 +43,12 @@ Authentication uses **Passport.js** with local strategy and `bcrypt` for passwor The UI is built with **Radix UI** and **shadcn/ui**, leveraging **Tailwind CSS** for styling. The default theme is dark mode. Key design decisions include a component-based architecture for reusability, custom path aliases, and a responsive, mobile-first approach. The platform supports multi-photo carousel posts with drag-and-drop reordering, and a **PreviewModal** offers realistic rendering of posts across Facebook and Instagram formats before publishing. The calendar view is responsive, with a grid layout for desktop and an expandable list view for mobile, allowing editing and deletion of scheduled posts directly from the calendar. +**Infinite Scroll**: Both the media library and new post pages implement infinite scroll using IntersectionObserver for performance optimization: +- **Media Library**: Loads 5 media items initially, then 5 more when scrolling to bottom (max height: 500px) +- **New Post Page**: Loads 12 media items initially in a 3-column grid, then 12 more when scrolling (max height: 500px) +- Visual loading indicator ("Chargement..." with spinner) appears when more content is available +- Proper cleanup to prevent memory leaks + ## External Dependencies ### AI Content Generation