implemented infinity scroll

This commit is contained in:
Laura Dolibois
2025-06-01 11:34:41 +02:00
parent a811493c97
commit 2a9629ea22
2 changed files with 113 additions and 89 deletions

View File

@@ -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<Item[]>([]); // Zustand für die sichtbaren Items
const [itemsToShow, setItemsToShow] = useState(20); // Anzahl Items, die geladen sind
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [selectedRating, setSelectedRating] = useState<string | null>(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<HTMLDivElement | null>(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<unknown>, 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 (
<div className="home-page-background">
<div className="filter-container">
<FilterItem
filterName={t('category')}
filterName={t("category")}
filterItems={categoriesFilter}
value={selectedCategory}
onChange={handleCategoryChange}
@@ -373,14 +397,13 @@ export default function Home() {
max={maxPrice}
onChange={(range) => {
setPriceRange(range);
setCurrentPage(1);
}}
/>
<FilterItem
filterName={t('rating')}
filterName={t("rating")}
filterItems={ratingFilter}
value={selectedRating}
onChange={setSelectedRating}
onChange={handleRatingChange}
/>
</div>
<div className="page-background">
@@ -390,14 +413,11 @@ export default function Home() {
))}
</Box>
<Pagination
count={Math.ceil(filteredCount / itemsPerPage)}
page={currentPage}
onChange={handlePageChange}
variant="outlined"
shape="rounded"
className="pagination"
/>
{/* Loader für Intersection Observer */}
<div ref={loaderRef} className="loader-container">
{/* Hier kann ein Lade-Spinner rein, wenn gewünscht */}
{itemsToShow < filteredItems.length ? "Laden..." : "Keine weiteren Items"}
</div>
</div>
</div>
);

View File

@@ -46,12 +46,11 @@
display: grid;
gap: 2%;
width: 90%;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); /* Dynamische Spalten */
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
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 */
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 */
}