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
+110 -46

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 { 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<any>(null);
const [visibleCount, setVisibleCount] = useState(5);
const loadMoreRef = useRef<HTMLDivElement>(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 (
<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">
@@ -123,8 +147,8 @@ export default function MediaUpload() {
</span>
</h4>
<div className="space-y-3">
{(mediaList as any[])?.slice(0, 5).map((media: any) => (
<div className="space-y-3 max-h-[500px] overflow-y-auto">
{(mediaList as any[])?.slice(0, visibleCount).map((media: any) => (
<div
key={media.id}
onClick={() => setSelectedFile(media)}
@@ -165,6 +189,13 @@ export default function MediaUpload() {
</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>
+76 -43
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 { 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<HTMLDivElement>(null);
const { data: pages = [] } = useQuery<SocialPage[]>({
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 (
<div className="flex h-screen overflow-hidden bg-background">
{sidebarOpen && (
@@ -451,48 +475,57 @@ export default function NewPost() {
<div className="text-sm font-medium text-muted-foreground mb-2">
Toutes les photos
</div>
<div className="grid grid-cols-3 gap-2">
{mediaList.map((media) => {
const isSelected = selectedMedia.includes(media.id);
return (
<button
key={media.id}
onClick={() => {
if (isSelected) {
// Remove from selection
setSelectedMedia(prev => prev.filter(id => id !== media.id));
} else if (selectedMedia.length < 10) {
// Add to selection (max 10)
setSelectedMedia(prev => [...prev, media.id]);
} else {
toast({
title: "Limite atteinte",
description: "Maximum 10 photos par publication",
variant: "destructive",
});
}
}}
className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all ${
isSelected
? 'border-primary ring-2 ring-primary opacity-50'
: 'border-transparent hover:border-muted-foreground'
}`}
data-testid={`button-select-media-${media.id}`}
>
<img
src={media.originalUrl}
alt={media.fileName}
className="w-full h-full object-cover"
/>
{isSelected && (
<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>
)}
</button>
);
})}
<div className="max-h-[500px] overflow-y-auto">
<div className="grid grid-cols-3 gap-2">
{mediaList.slice(0, visibleMediaCount).map((media) => {
const isSelected = selectedMedia.includes(media.id);
return (
<button
key={media.id}
onClick={() => {
if (isSelected) {
// Remove from selection
setSelectedMedia(prev => prev.filter(id => id !== media.id));
} else if (selectedMedia.length < 10) {
// Add to selection (max 10)
setSelectedMedia(prev => [...prev, media.id]);
} else {
toast({
title: "Limite atteinte",
description: "Maximum 10 photos par publication",
variant: "destructive",
});
}
}}
className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all ${
isSelected
? 'border-primary ring-2 ring-primary opacity-50'
: 'border-transparent hover:border-muted-foreground'
}`}
data-testid={`button-select-media-${media.id}`}
>
<img
src={media.originalUrl}
alt={media.fileName}
className="w-full h-full object-cover"
/>
{isSelected && (
<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>
)}
</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>
</>
)}