Improve media loading with infinite scrolling on upload and new post pages

Integrates IntersectionObserver for infinite scrolling in MediaUpload and NewPost components, enhancing performance by loading media items in batches.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/lEm79pj
This commit is contained in:
michaelschal committed 2025-10-09 12:15:32 +00:00
1 parent ca3acd2df4
commit 5bfff22bab
2 files changed
+109 -45

No files matched your search

+34 -3
View File
@@ -1,4 +1,4 @@
import { useState, useCallback } from "react"; import { useState, useCallback, useRef, useEffect } from "react";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Dialog, DialogContent } from "@/components/ui/dialog"; import { Dialog, DialogContent } from "@/components/ui/dialog";
@@ -11,6 +11,8 @@ import { SiFacebook, SiInstagram } from "react-icons/si";
export default function MediaUpload() { export default function MediaUpload() {
const { toast } = useToast(); const { toast } = useToast();
const [selectedFile, setSelectedFile] = useState<any>(null); const [selectedFile, setSelectedFile] = useState<any>(null);
const [visibleCount, setVisibleCount] = useState(5);
const loadMoreRef = useRef<HTMLDivElement>(null);
const { data: mediaList } = useQuery({ const { data: mediaList } = useQuery({
queryKey: ["/api/media"], 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 ( return (
<div className="bg-card rounded-2xl border border-border/50 overflow-hidden shadow-lg"> <div className="bg-card rounded-2xl border border-border/50 overflow-hidden shadow-lg">
<div className="border-b border-border/50 p-6 bg-gradient-to-r from-primary/5 to-secondary/5"> <div className="border-b border-border/50 p-6 bg-gradient-to-r from-primary/5 to-secondary/5">
@@ -123,8 +147,8 @@ export default function MediaUpload() {
</span> </span>
</h4> </h4>
<div className="space-y-3"> <div className="space-y-3 max-h-[500px] overflow-y-auto">
{(mediaList as any[])?.slice(0, 5).map((media: any) => ( {(mediaList as any[])?.slice(0, visibleCount).map((media: any) => (
<div <div
key={media.id} key={media.id}
onClick={() => setSelectedFile(media)} onClick={() => setSelectedFile(media)}
@@ -165,6 +189,13 @@ export default function MediaUpload() {
</div> </div>
</div> </div>
))} ))}
{/* Élément sentinelle pour le scroll infini */}
{mediaList && visibleCount < (mediaList as any[]).length && (
<div ref={loadMoreRef} className="flex justify-center py-4">
<Loader2 className="w-5 h-5 text-primary animate-spin" />
</div>
)}
</div> </div>
</div> </div>
</div> </div>
+75 -42
View File
@@ -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 { useQuery, useMutation } from "@tanstack/react-query";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import { Send, Sparkles, Image as ImageIcon, Calendar, Upload, Camera, GripVertical, Loader2 } from "lucide-react"; 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 [postText, setPostText] = useState('');
const [postType, setPostType] = useState<'feed' | 'story' | 'both'>('feed'); const [postType, setPostType] = useState<'feed' | 'story' | 'both'>('feed');
const [previewModalOpen, setPreviewModalOpen] = useState(false); const [previewModalOpen, setPreviewModalOpen] = useState(false);
const [visibleMediaCount, setVisibleMediaCount] = useState(12);
const loadMoreMediaRef = useRef<HTMLDivElement>(null);
const { data: pages = [] } = useQuery<SocialPage[]>({ const { data: pages = [] } = useQuery<SocialPage[]>({
queryKey: ['/api/pages'], 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 ( return (
<div className="flex h-screen overflow-hidden bg-background"> <div className="flex h-screen overflow-hidden bg-background">
{sidebarOpen && ( {sidebarOpen && (
@@ -451,48 +475,57 @@ export default function NewPost() {
<div className="text-sm font-medium text-muted-foreground mb-2"> <div className="text-sm font-medium text-muted-foreground mb-2">
Toutes les photos Toutes les photos
</div> </div>
<div className="grid grid-cols-3 gap-2"> <div className="max-h-[500px] overflow-y-auto">
{mediaList.map((media) => { <div className="grid grid-cols-3 gap-2">
const isSelected = selectedMedia.includes(media.id); {mediaList.slice(0, visibleMediaCount).map((media) => {
const isSelected = selectedMedia.includes(media.id);
return ( return (
<button <button
key={media.id} key={media.id}
onClick={() => { onClick={() => {
if (isSelected) { if (isSelected) {
// Remove from selection // Remove from selection
setSelectedMedia(prev => prev.filter(id => id !== media.id)); setSelectedMedia(prev => prev.filter(id => id !== media.id));
} else if (selectedMedia.length < 10) { } else if (selectedMedia.length < 10) {
// Add to selection (max 10) // Add to selection (max 10)
setSelectedMedia(prev => [...prev, media.id]); setSelectedMedia(prev => [...prev, media.id]);
} else { } else {
toast({ toast({
title: "Limite atteinte", title: "Limite atteinte",
description: "Maximum 10 photos par publication", description: "Maximum 10 photos par publication",
variant: "destructive", variant: "destructive",
}); });
} }
}} }}
className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all ${ className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all ${
isSelected isSelected
? 'border-primary ring-2 ring-primary opacity-50' ? 'border-primary ring-2 ring-primary opacity-50'
: 'border-transparent hover:border-muted-foreground' : 'border-transparent hover:border-muted-foreground'
}`} }`}
data-testid={`button-select-media-${media.id}`} data-testid={`button-select-media-${media.id}`}
> >
<img <img
src={media.originalUrl} src={media.originalUrl}
alt={media.fileName} alt={media.fileName}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
{isSelected && ( {isSelected && (
<div className="absolute inset-0 bg-primary/10 flex items-center justify-center"> <div className="absolute inset-0 bg-primary/10 flex items-center justify-center">
<div className="text-xs font-semibold text-primary">Sélectionnée</div> <div className="text-xs font-semibold text-primary">Sélectionnée</div>
</div> </div>
)} )}
</button> </button>
); );
})} })}
</div>
{/* Élément sentinelle pour le scroll infini */}
{mediaList && visibleMediaCount < mediaList.length && (
<div ref={loadMoreMediaRef} className="flex justify-center py-4 col-span-3">
<Loader2 className="w-5 h-5 text-primary animate-spin" />
</div>
)}
</div> </div>
</> </>
)} )}