From 2a9629ea226a0ac058b7c43aa8d983ff3cbd28c3 Mon Sep 17 00:00:00 2001 From: Laura Dolibois Date: Sun, 1 Jun 2025 11:34:41 +0200 Subject: [PATCH] implemented infinity scroll --- 01-frontend/src/pages/Home.tsx | 170 ++++++++++++++++++-------------- 01-frontend/src/pages/pages.css | 32 +++--- 2 files changed, 113 insertions(+), 89 deletions(-) diff --git a/01-frontend/src/pages/Home.tsx b/01-frontend/src/pages/Home.tsx index 7196eae..d75e3a0 100644 --- a/01-frontend/src/pages/Home.tsx +++ b/01-frontend/src/pages/Home.tsx @@ -1,11 +1,10 @@ -import { Box, Pagination } from "@mui/material"; -import { useEffect, useState } from "react"; +import { Box } from "@mui/material"; +import {useCallback, useEffect, useRef, useState} from "react"; import Item from "../components/Item"; import FilterItem from "../helper/homepage/FilterItem"; import ItemCard from "../helper/homepage/ItemCard"; import "./pages.css"; // Import der CSS-Datei import { useNavigate } from "react-router-dom" -import { useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; import PriceSlider from "../helper/homepage/PriceSlider"; @@ -254,6 +253,16 @@ export default function Home() { category: "Other", rating: 1.0, discount: 0, + }, + { + id: "212", + name: "Item 4", + description: "Description 3", + price: 50, + stock: 300, + category: "Other", + rating: 1.0, + discount: 0, } // Weitere Items hinzufügen ]; @@ -274,27 +283,87 @@ export default function Home() { })) ]; - const [currentPage, setCurrentPage] = useState(1); // Zustand für die aktuelle Seite - const [itemsPerPage, setItemsPerPage] = useState(9); // Dynamische Anzahl der Items pro Seite - const [visibleItems, setVisibleItems] = useState([]); // Zustand für die sichtbaren Items + const [itemsToShow, setItemsToShow] = useState(20); // Anzahl Items, die geladen sind const [selectedCategory, setSelectedCategory] = useState(null); const [selectedRating, setSelectedRating] = useState(null); - const discountedPrices = items.map(item => item.price * (1 - item.discount / 100)); + const discountedPrices = items.map( + (item) => item.price * (1 - item.discount / 100) + ); const minPrice = Math.min(...discountedPrices); const maxPrice = Math.max(...discountedPrices); - const [priceRange, setPriceRange] = useState<[number, number]>([minPrice, maxPrice]); - - const location = useLocation(); + const [priceRange, setPriceRange] = useState<[number, number]>([ + minPrice, + maxPrice, + ]); + // Filter aus URL übernehmen useEffect(() => { const params = new URLSearchParams(location.search); const category = params.get("category"); - if (category && categoriesFilter.some(filter => filter.value === category)) { + if (category && categoriesFilter.some((f) => f.value === category)) { setSelectedCategory(category); + } else { + setSelectedCategory(null); } }, [location.search]); + // Filterfunktion bleibt gleich + const filteredItems = items + .filter((item) => { + const discountedPrice = item.price * (1 - item.discount / 100); + return discountedPrice >= priceRange[0] && discountedPrice <= priceRange[1]; + }) + .filter((item) => { + if (!selectedCategory) return true; + return item.category === selectedCategory; + }) + .filter((item) => { + if (!selectedRating) return true; + return Math.round(item.rating) >= Number(selectedRating); + }); + + // Items, die aktuell angezeigt werden + const visibleItems = filteredItems.slice(0, itemsToShow); + + // Intersection Observer Ref + const loaderRef = useRef(null); + + // Callback für Observer + const handleObserver = useCallback( + (entries: IntersectionObserverEntry[]) => { + const target = entries[0]; + if (target.isIntersecting) { + // Wenn noch mehr Items da sind, lade 20 weitere + setItemsToShow((prev) => { + if (prev >= filteredItems.length) return prev; + return prev + 20; + }); + } + }, + [filteredItems.length] + ); + + // Observer Setup + useEffect(() => { + const option = { + root: null, + rootMargin: "20px", + threshold: 1.0, + }; + const observer = new IntersectionObserver(handleObserver, option); + if (loaderRef.current) observer.observe(loaderRef.current); + return () => { + if (loaderRef.current) observer.unobserve(loaderRef.current); + }; + }, [handleObserver]); + + // Reset itemsToShow bei Filteränderungen + useEffect(() => { + setItemsToShow(20); + }, [selectedCategory, selectedRating, priceRange]); + + // Kategorie-Änderung const handleCategoryChange = (category: string) => { if (category === "") { setSelectedCategory(null); @@ -305,65 +374,20 @@ export default function Home() { } }; - // Handler für die Pagination - const handlePageChange = (event: React.ChangeEvent, page: number) => { - setCurrentPage(page); - }; - - const filteredItems = items - .filter(item => { - const discountedPrice = item.price * (1 - item.discount / 100); - return discountedPrice >= priceRange[0] && discountedPrice <= priceRange[1]; - }) - .filter(item => { - if (!selectedCategory) return true; - return item.category === selectedCategory; - }) - .filter(item => { - if (!selectedRating) return true; - return Math.round(item.rating) >= Number(selectedRating); - }); - - const filteredCount = filteredItems.length; - -// Aktualisiert die sichtbaren Items bei Seitenwechsel oder Fenstergrößenänderung - useEffect(() => { - const updateItems = () => { - const newItemsPerPage = calculateItemsPerPage(); - setItemsPerPage(newItemsPerPage); - - const startIndex = (currentPage - 1) * newItemsPerPage; - const newVisibleItems = filteredItems.slice(startIndex, startIndex + newItemsPerPage); - - setVisibleItems(prev => (JSON.stringify(prev) !== JSON.stringify(newVisibleItems) ? newVisibleItems : prev)); - }; - - updateItems(); - - const handleResize = () => { - updateItems(); - }; - - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, [currentPage, priceRange, selectedCategory, selectedRating]); - - - // Dynamische Berechnung der Anzahl der Items pro Seite basierend auf der Fensterbreite - const calculateItemsPerPage = () => { - const cardHeight = 220; // Höhe einer Karte (inkl. Margin/Padding) - const paginationHeight = 60; // Höhe der Pagination - const availableHeight = window.innerHeight - paginationHeight - 60; // Verfügbare Höhe (inkl. Padding) - const itemsPerRow = Math.floor(window.innerWidth / 220); // Karten pro Zeile (220px Breite inkl. Margin) - const rows = Math.floor(availableHeight / cardHeight); // Maximale Anzahl an vollständigen Reihen - return itemsPerRow * rows; + // Rating-Änderung (bleibt gleich) + const handleRatingChange = (rating: string) => { + if (rating === "") { + setSelectedRating(null); + } else { + setSelectedRating(rating); + } }; return (
{ setPriceRange(range); - setCurrentPage(1); }} />
@@ -390,14 +413,11 @@ export default function Home() { ))} - + {/* Loader für Intersection Observer */} +
+ {/* Hier kann ein Lade-Spinner rein, wenn gewünscht */} + {itemsToShow < filteredItems.length ? "Laden..." : "Keine weiteren Items"} +
); diff --git a/01-frontend/src/pages/pages.css b/01-frontend/src/pages/pages.css index d36ac8a..fa35dfd 100644 --- a/01-frontend/src/pages/pages.css +++ b/01-frontend/src/pages/pages.css @@ -43,15 +43,14 @@ } .cardgrid { - display: grid; - gap: 2%; - width: 90%; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); /* Dynamische Spalten */ - padding: 16px; - margin: 0 auto; - max-height: calc(100vh - 100px); /* Dynamische Höhe, abhängig von der Pagination */ - box-sizing: border-box; /* Stellt sicher, dass Padding in die Breite einbezogen wird */ - + display: grid; + gap: 2%; + width: 90%; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + padding: 16px; + margin: 0 auto; + box-sizing: border-box; + padding-bottom: 50px; } .page-background { @@ -62,7 +61,7 @@ flex-direction: column; align-items: center; justify-content: space-between; - overflow: hidden; + overflow-y: auto; padding: 20px 0; /* Abstand oben und unten */ box-sizing: border-box; /* Stellt sicher, dass Padding in die Höhe einbezogen wird */ width: 100%; /* Damit der Hintergrund die gesamte Breite abdeckt */ @@ -126,12 +125,12 @@ .home-page-background { display: flex; - align-items: center; + align-items: stretch; width: 100%; - overflow: hidden; + overflow: auto; height: calc(100vh - 3rem); /* Damit der Hintergrund die gesamte Seite abdeckt */ min-height: 600px; - padding: 20px 0; /* Abstand oben und unten */ + /* padding: 20px 0 40px 0; /* Abstand oben und unten */ box-sizing: border-box; color: black; } @@ -144,6 +143,11 @@ margin-top: 20px; margin-bottom: 20px; place-self: start; +} - +.loader-container { + width: 100%; + display: flex; + justify-content: center; + margin-top: 60px; /* Abstand zu den Items */ } \ No newline at end of file