Revert "Darkmode verbessert, Preisfilter geht wieder und Impressum wurde bearbeitet"

This reverts commit aab1fa182b.
This commit is contained in:
Tim
2025-06-15 17:34:29 +02:00
parent 86469f5882
commit a38612c200
12 changed files with 270 additions and 535 deletions

View File

@@ -15,7 +15,7 @@
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"@mui/x-charts": "^8.5.2",
"@mui/x-charts": "^8.5.1",
"@mui/x-data-grid": "^8.5.2",
"@tanstack/react-query": "^5.79.2",
"chart.js": "^4.4.9",
@@ -1491,15 +1491,15 @@
}
},
"node_modules/@mui/x-charts": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.5.2.tgz",
"integrity": "sha512-JLPTtd9m8CWMoIxwHFM9QpPDpfdsetfkCErJUvsyQnj/rC8sBMmQqk0c1olusA+OqTyVT3gGmiqXXFar/0cvkw==",
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.5.1.tgz",
"integrity": "sha512-6g0Gdyf2x/2UFZUWdifg7l8L1xl+YB8mz3NlsgK/Oa4Mf9EqPJUvXnzodxyNRT2UkX0l40p6yuOX7o+Mql20/w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@babel/runtime": "^7.27.4",
"@mui/utils": "^7.1.1",
"@mui/x-charts-vendor": "8.5.2",
"@mui/x-internals": "8.5.2",
"@mui/x-charts-vendor": "8.5.1",
"@mui/x-internals": "8.5.1",
"bezier-easing": "^2.1.0",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
@@ -1527,12 +1527,12 @@
}
},
"node_modules/@mui/x-charts-vendor": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.5.2.tgz",
"integrity": "sha512-93KFrEpo3Xhr0g2TQsbtPVqGAsbkKBN5J57ykrCM5GxFmq3kDGFU4k9+FpKiaIYYL8ijzgHGNh+jNVbP0pq3rQ==",
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.5.1.tgz",
"integrity": "sha512-da6QET4FBSzBYjhaaEIA+nrprc2revJMuwXPtDE14KAjEpIluchxsKTqn2XBg0j1NWm40FZX+fLh8m3w2crGIQ==",
"license": "MIT AND ISC",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@babel/runtime": "^7.27.4",
"@types/d3-color": "^3.1.3",
"@types/d3-delaunay": "^6.0.4",
"@types/d3-interpolate": "^3.0.4",
@@ -1611,14 +1611,13 @@
}
},
"node_modules/@mui/x-internals": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.5.2.tgz",
"integrity": "sha512-5YhB2AekK7G8d0YrAjg3WNf0uy3V73JD98WNxJhbIlCraQgl8QOQzr2zNO7MAf/X7mZQtjpjuAsiG3+gI2NVyg==",
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.5.1.tgz",
"integrity": "sha512-7rAWK7SB6FxEIXKgsHsJjIzeeKOLxFJ16gePgZVWlvyew+xDb4P0fgjwW3ThcJjgvkUm0UhGGfLh/JP8l514IA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"@mui/utils": "^7.1.1",
"reselect": "^5.1.1"
"@babel/runtime": "^7.27.4",
"@mui/utils": "^7.1.1"
},
"engines": {
"node": ">=14.0.0"

View File

@@ -19,7 +19,7 @@
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2",
"@mui/x-charts": "^8.5.2",
"@mui/x-charts": "^8.5.1",
"@mui/x-data-grid": "^8.5.2",
"@tanstack/react-query": "^5.79.2",
"chart.js": "^4.4.9",

View File

@@ -1,20 +1,18 @@
import { Box, Typography, useTheme } from "@mui/material";
import { BarChart } from '@mui/x-charts/BarChart';
import { PieChart } from '@mui/x-charts/PieChart';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
ArcElement,
BarElement,
CategoryScale,
Chart as ChartJS,
Legend,
LinearScale,
LineElement,
PointElement,
Title,
Tooltip,
} from "chart.js";
import { Bar, Pie, Line } from "react-chartjs-2";
import { BarChart } from '@mui/x-charts/BarChart';
import { RadarChart } from '@mui/x-charts/RadarChart';
import { PieChart } from '@mui/x-charts/PieChart';
import { useTranslation } from "react-i18next";
// Chart.js registrieren
@@ -34,88 +32,6 @@ export default function StatisticsInfo() {
const theme = useTheme();
const {t} = useTranslation();
const weeklySalesData = {
labels: ["Week 1", "Week 2", "Week 3", "Week 4"],
datasets: [
{
label: "Weekly Sales (€)",
data: [1200, 2100, 800, 1600],
backgroundColor: theme.palette.mode === "dark"
? "rgba(0, 230, 255, 0.5)"
: "rgba(75, 192, 192, 0.5)",
borderColor: theme.palette.mode === "dark"
? "rgba(0, 230, 255, 1)"
: "rgba(75, 192, 192, 1)",
borderWidth: 1,
},
],
};
const itemSalesData = {
labels: ["Tomatensamen", "Blumenerde", "Gießkanne", "Pflanzendünger"],
datasets: [
{
label: "Item Sales",
data: [400, 300, 200, 100],
backgroundColor: [
theme.palette.warning.main,
theme.palette.info.main,
theme.palette.success.main,
theme.palette.secondary.main,
],
hoverBackgroundColor: [
theme.palette.warning.light,
theme.palette.info.light,
theme.palette.success.light,
theme.palette.secondary.light,
],
},
],
};
const userSalesData = {
labels: ["John Doe", "Jane Smith", "Alice Johnson", "Bob Brown"],
datasets: [
{
label: "Sales by User",
data: [5, 8, 3, 6],
fill: false,
borderColor: theme.palette.primary.main,
backgroundColor: theme.palette.primary.light,
tension: 0.1,
},
],
};
const baseOptions = {
responsive: true,
plugins: {
legend: {
position: "top" as const,
labels: {
color: theme.palette.text.primary,
},
},
title: {
display: false,
},
},
scales: {
x: {
ticks: { color: theme.palette.text.primary },
grid: {
color: theme.palette.divider,
},
},
y: {
ticks: { color: theme.palette.text.primary },
grid: {
color: theme.palette.divider,
},
},
},
};
return (
<Box sx={{ color: theme.palette.text.primary }}>
<Typography variant="h4" align="center" gutterBottom>

View File

@@ -1,51 +0,0 @@
/* FilterItem.css */
/* Container rund um jedes Filter-Widget */
.filter-item {
margin-bottom: 1.5rem;
}
/* Überschrift (FormLabel) */
.filter-item__label {
font-weight: bold;
margin-bottom: 0.5rem;
color: var(--text-color);
}
/* Das Material-UI FormControl-Element */
.filter-item__group {
display: flex;
flex-direction: column;
}
/* Jeder Radio-Button mit Label */
.filter-item__option {
color: var(--text-color);
}
/* Dark-Mode Anpassungen */
body.dark .filter-item__label,
body.dark .filter-item__option {
color: #ffffff;
/* FilterItem.css (am Ende) */
/* Sterne-Icons (gefüllt) */
.filter-item__option .MuiRating-root .MuiRating-iconFilled {
color: #FFC107; /* Gold-Ton für alle Modi */
}
/* Optional: leere Sterne etwas abgedunkelt darstellen */
.filter-item__option .MuiRating-root .MuiRating-iconEmpty {
color: rgba(255, 255, 255, 0.3);
}
/* Dark-Mode Überschreibungen */
body.dark .filter-item__option .MuiRating-root .MuiRating-iconFilled {
color: #FFC107; /* bleibt Gold */
}
body.dark .filter-item__option .MuiRating-root .MuiRating-iconEmpty {
color: rgba(255, 255, 255, 0.3);
}
}

View File

@@ -1,4 +1,3 @@
import React from "react";
import {
FormControl,
FormControlLabel,
@@ -6,8 +5,9 @@ import {
Radio,
RadioGroup,
Rating,
useTheme,
} from "@mui/material";
import "./FilterItem.css";
import React from "react";
type FilterItemOption = {
value: string;
@@ -22,26 +22,37 @@ type FilterItemProps = {
};
export default function FilterItem({
filterName,
filterItems,
value,
onChange,
}: FilterItemProps) {
// Default-Wert, falls noch nichts ausgewählt
filterName,
filterItems,
value,
onChange,
}: FilterItemProps) {
const theme = useTheme();
if (!value && filterItems.length > 0) {
value = filterItems[0].value;
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(e.target.value);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (onChange) {
onChange(event.target.value);
}
};
return (
<div className="filter-item">
<FormLabel component="legend" className="filter-item__label">
<div style={{ marginBottom: "1.5rem" }}>
<FormLabel
component="legend"
sx={{
fontWeight: "bold",
color: theme.palette.text.primary,
mb: 1,
}}
>
{filterName}
</FormLabel>
<FormControl className="filter-item__group">
<FormControl>
<RadioGroup value={value} onChange={handleChange}>
{filterItems.map((item, idx) => (
<FormControlLabel
@@ -60,7 +71,9 @@ export default function FilterItem({
item.label
)
}
className="filter-item__option"
sx={{
color: theme.palette.text.primary,
}}
/>
))}
</RadioGroup>

View File

@@ -1,32 +0,0 @@
/* PriceSlider.css */
.price-slider-container {
margin-bottom: 2rem;
}
.price-slider-title {
font-weight: bold;
margin-bottom: 8px;
color: var(--text-color);
}
.price-slider-wrapper {
padding: 0 8px;
}
.price-slider-value {
margin-top: 8px;
font-size: 0.9rem;
text-align: center;
color: var(--text-color);
}
/* 🌙 Dark Mode Unterstützung */
body.dark .price-slider-title,
body.dark .price-slider-value {
color: #ffffff;
}
body.dark .MuiSlider-root {
color: #0fd13f;
}

View File

@@ -1,62 +1,37 @@
import { Slider, Typography, Box, useTheme } from "@mui/material";
import { useState, useEffect } from "react";
import { useState, useEffect, SyntheticEvent } from "react";
import { useTranslation } from "react-i18next";
type PriceSliderProps = {
/** in Cent übergeben */
min?: number;
/** in Cent übergeben */
max?: number;
/** in Euro zurückbekommen */
onChange?: (range: [number, number]) => void;
};
export default function PriceSlider({
min = 0,
max = 10000,
onChange,
}: PriceSliderProps) {
export default function PriceSlider({ min = 0, max = 10000, onChange }: PriceSliderProps) {
const { t } = useTranslation();
const theme = useTheme();
// Slider-Werte in Euro
const [value, setValue] = useState<[number, number]>([
min / 100,
max / 100,
]);
const [value, setValue] = useState<[number, number]>([min, max]);
// Wenn sich min/max ändern, neu setzen
useEffect(() => {
const euroMin = min / 100;
const euroMax = max / 100;
setValue([euroMin, euroMax]);
onChange?.([euroMin, euroMax]);
setValue([min, max]);
onChange?.([min, max]);
}, [min, max]);
const handleChange = (
_: Event | React.SyntheticEvent,
newValue: number | number[]
) => {
const handleChange = (_: Event, newValue: number | number[]) => {
if (Array.isArray(newValue)) {
setValue([newValue[0], newValue[1]]);
}
};
const handleCommitted = (
_: Event | React.SyntheticEvent,
newValue: number | number[]
) => {
const handleCommitted = (_: Event | SyntheticEvent<Element, Event>, newValue: number | number[]) => {
if (Array.isArray(newValue)) {
// Direkt in Euro zurückgeben
onChange?.([newValue[0], newValue[1]]);
}
};
const formatValueToEuro = (v: number) =>
new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(v);
const formatValueToEuro = (val: number) => `${(val/100).toFixed(2)}`;
return (
<Box sx={{ mb: 4 }}>
@@ -70,6 +45,7 @@ export default function PriceSlider({
>
{t("price")}
</Typography>
<Box sx={{ px: 1 }}>
<Slider
value={value}
@@ -77,21 +53,23 @@ export default function PriceSlider({
onChangeCommitted={handleCommitted}
valueLabelDisplay="auto"
valueLabelFormat={formatValueToEuro}
min={min / 100}
max={max / 100}
step={0.01}
min={min}
max={max}
step={1}
sx={{
color: "#0fd13f",
"& .MuiSlider-valueLabel": {
'& .MuiSlider-valueLabel': {
color: theme.palette.text.primary,
},
}}
/>
<Typography
variant="body2"
sx={{
mt: 1,
color: theme.palette.text.primary,
fontSize: "0.9rem",
textAlign: "center",
}}
>

View File

@@ -1,39 +1,18 @@
/* index.css (oder App.css), ganz oben importieren */
:root {
/* Light-Mode Defaults */
--background-color: #fafafa;
--text-color: #000000;
--divider-color: rgba(0, 0, 0, 0.6);
--primary-main: #0fd13f;
/* generelle System-Fonts & -Resets */
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body.dark {
/* Dark-Mode Overrides */
--background-color: #121212;
--text-color: #ffffff;
--divider-color: rgba(255, 255, 255, 0.7);
--primary-main: #0fd13f;
}
/* Basis-Styling, kann bleiben */
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
background-color: var(--background-color);
color: var(--text-color);
}
/* Link-Styles */
a {
font-weight: 500;
color: #646cff;
@@ -43,7 +22,19 @@ a:hover {
color: #535bf2;
}
/* Button-Styles */
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
@@ -63,8 +54,14 @@ button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
/* Optional: systemweite Light-Mode-Ergänzungen */
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}

View File

@@ -1,58 +0,0 @@
/* Impressum.css */
.impressum-container {
background-color: var(--background-color);
color: var(--text-color);
min-height: 100vh;
padding: 2rem; /* entspricht etwa theme.spacing(3) */
}
.impressum-title {
font-size: 2rem; /* entspricht variant h3 */
margin-bottom: 1rem;
}
.impressum-subtitle {
font-size: 1.25rem; /* entspricht variant h4 */
margin-top: 2rem;
margin-bottom: 1rem;
}
.impressum-heading {
font-size: 1.125rem; /* entspricht variant h5 */
margin-top: 1.5rem;
margin-bottom: 0.5rem;
font-weight: bold;
}
.impressum-paragraph {
margin-bottom: 1rem;
line-height: 1.6;
}
.impressum-divider {
margin: 2rem 0;
background-color: var(--divider-color);
height: 1px;
border: none;
}
.impressum-list {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.impressum-list li {
margin-bottom: 0.5rem;
}
.impressum-link {
color: var(--primary-main);
text-decoration: none;
}
.impressum-caption {
display: block;
margin-top: 2rem;
font-size: 0.75rem; /* entspricht variant caption */
}

View File

@@ -1,21 +1,14 @@
import { Box, Divider, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import "./Contact.css";
import "./pages.css";
export default function Impressum() {
const theme = useTheme();
// Body-Klasse für Darkmode setzen
// (Alternativ übernimmt das dein ThemeProvider global per document.body.classList)
document.body.classList.toggle("dark", theme.palette.mode === "dark");
return (
<Box className="impressum-container">
<Typography component="h1" className="impressum-title">
<Typography variant="h4" sx={{ color: 'text.primary', mb: 2 }}>
Impressum
</Typography>
<Typography component="p" className="impressum-paragraph">
<Typography variant="body1" sx={{ color: 'text.primary', mb: 4 }}>
Hochschule für Technik und Wirtschaft<br />
des Saarlandes<br />
Goebenstraße 40<br />
@@ -27,97 +20,64 @@ export default function Impressum() {
Ministerium der Finanzen und für Wissenschaft des Saarlandes
</Typography>
<Divider className="impressum-divider" />
<Divider className="contact-divider" />
<Typography component="h2" className="impressum-subtitle">
<Typography variant="h5" sx={{ color: 'text.primary', mt: 4, mb: 2 }}>
Datenschutzerklärung
</Typography>
<Typography component="p" className="impressum-paragraph">
Personenbezogene Daten (Daten) werden von uns nur verarbeitet, wenn es notwendig ist,
um einen funktionierenden und benutzerfreundlichen Internetauftritt bereitzustellen.
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
Personenbezogene Daten (nachfolgend zumeist nur Daten genannt) ...
</Typography>
<Typography component="h3" className="impressum-heading">
Gliederung:
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
Gemäß Art. 4 Ziffer 1. der Verordnung (EU) 2016/679, also der Datenschutz-Grundverordnung ...
</Typography>
<Typography component="p" className="impressum-paragraph">
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
Unsere Datenschutzerklärung ist wie folgt gegliedert:<br />
I. Informationen über uns als Verantwortliche<br />
II. Rechte der Nutzer und Betroffenen<br />
III. Informationen zur Datenverarbeitung
</Typography>
<Typography component="h3" className="impressum-heading">
I. Verantwortlicher Anbieter
</Typography>
<Typography component="p" className="impressum-paragraph">
Hochschule für Technik und Wirtschaft<br />
des Saarlandes<br />
Goebenstraße 40<br />
66117 Saarbrücken<br />
Telefon: (0681) 58 67 - 0<br />
E-Mail: info@htwsaar.de
<Typography variant="h6" sx={{ color: 'text.primary', mt: 4, mb: 1 }}>
I. Informationen über uns als Verantwortliche
</Typography>
<Typography component="h3" className="impressum-heading">
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
Verantwortlicher Anbieter dieses Internetauftritts ...
</Typography>
<Typography variant="h6" sx={{ color: 'text.primary', mt: 4, mb: 1 }}>
II. Rechte der Nutzer und Betroffenen
</Typography>
<ul className="impressum-list">
<li>Auskunft über verarbeitete Daten (Art. 15 DSGVO)</li>
<li>Berichtigung unrichtiger Daten (Art. 16 DSGVO)</li>
<li>Löschung oder Einschränkung (Art. 17, 18 DSGVO)</li>
<li>Datenübertragbarkeit (Art. 20 DSGVO)</li>
<li>Widerspruch gegen Verarbeitung (Art. 21 DSGVO)</li>
<li>Beschwerde bei der Aufsichtsbehörde (Art. 77 DSGVO)</li>
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
Mit Blick auf die nachfolgend noch näher beschriebene Datenverarbeitung haben die Nutzer und Betroffenen ...
</Typography>
<ul>
<li><Typography variant="body2" sx={{ color: 'text.primary' }}>Auskunft über die verarbeiteten Daten (Art. 15 DSGVO)</Typography></li>
<li><Typography variant="body2" sx={{ color: 'text.primary' }}>Berichtigung unrichtiger Daten (Art. 16 DSGVO)</Typography></li>
<li><Typography variant="body2" sx={{ color: 'text.primary' }}>Löschung der Daten (Art. 17 DSGVO)</Typography></li>
<li><Typography variant="body2" sx={{ color: 'text.primary' }}>Einschränkung der Verarbeitung (Art. 18 DSGVO)</Typography></li>
<li><Typography variant="body2" sx={{ color: 'text.primary' }}>Datenübertragbarkeit (Art. 20 DSGVO)</Typography></li>
</ul>
<Typography component="h3" className="impressum-heading">
<Typography variant="h6" sx={{ color: 'text.primary', mt: 4, mb: 1 }}>
III. Informationen zur Datenverarbeitung
</Typography>
<Typography component="p" className="impressum-paragraph">
Daten werden gelöscht oder gesperrt, sobald der Zweck entfällt oder keine gesetzliche
Aufbewahrungspflicht mehr besteht.
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
Ihre bei Nutzung unseres Internetauftritts verarbeiteten Daten ...
</Typography>
<Typography component="h4" className="impressum-heading">
Cookies
</Typography>
<Typography component="p" className="impressum-paragraph">
Unsere Website verwendet Cookies, um Funktionen wie Sprache, Warenkorb oder Benutzerkomfort zu ermöglichen.
</Typography>
{/* Du kannst einfach alle weiteren Absätze so fortsetzen copy & paste,
jeweils in: <Typography variant="body1" sx={{ color: 'text.primary' }}>…</Typography> */}
<Typography component="h4" className="impressum-heading">
Serverdaten
</Typography>
<Typography component="p" className="impressum-paragraph">
Ihr Browser übermittelt technische Daten (IP, Browsertyp etc.) an unseren Server, um die Website auszuliefern.
</Typography>
<Typography component="h4" className="impressum-heading">
Cloudflare
</Typography>
<Typography component="p" className="impressum-paragraph">
Unser CDN-Anbieter Cloudflare verarbeitet Zugriffsdaten zur Sicherheit und Optimierung. Mehr unter:&nbsp;
<a
href="https://www.cloudflare.com/privacypolicy/"
target="_blank"
rel="noopener noreferrer"
className="impressum-link"
>
cloudflare.com/privacypolicy
</a>
</Typography>
<Typography component="span" className="impressum-caption">
Quelle:&nbsp;
<a
href="https://www.ratgeberrecht.eu/datenschutz/datenschutzerklaerung-generator-dsgvo.html"
target="_blank"
rel="noopener noreferrer"
className="impressum-link"
>
Datenschutz Generator der Kanzlei Weiß &amp; Partner
</a>
<Typography variant="body2" sx={{ color: 'text.primary', mt: 4 }}>
Mehr Infos unter: <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener noreferrer">CloudFlare Datenschutzerklärung</a>
</Typography>
</Box>
);

View File

@@ -8,116 +8,138 @@ import FilterItem from "../helper/homepage/FilterItem";
import ItemCard from "../helper/homepage/ItemCard";
import PriceSlider from "../helper/homepage/PriceSlider";
import { fetchItemList } from '../helper/query/Queries';
import "./pages.css";
import "./pages.css"; // Import der CSS-Datei
export default function Home() {
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const theme = useTheme();
// URLSuchQuery
const [searchQuery, setSearchQuery] = useState<string | null>(null);
// Kategorie & Rating
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [selectedRating, setSelectedRating] = useState<string | null>(null);
// Items laden
const { data = [] } = useQuery<Item[]>({
queryKey: ['fetchItemList'],
queryFn: fetchItemList,
retry: 3,
retryDelay: 1000,
});
const items: Item[] = useMemo(() => data, [data]);
// DiscountPreis in Cent berechnen
const pricesInCent = items.map(i =>
Math.round(i.price100 * (1 - i.discount100 / 100))
);
const minCent = pricesInCent.length ? Math.min(...pricesInCent) : 0;
const maxCent = pricesInCent.length ? Math.max(...pricesInCent) : 0;
// **EuroBereich** für den Slider
const [priceRangeEuro, setPriceRangeEuro] = useState<[number, number]>([
minCent / 100,
maxCent / 100,
]);
// Kategorie aus URL
useEffect(() => {
const p = new URLSearchParams(location.search).get("category");
setSelectedCategory(
p && pagesCategoryOptions.some(f => f.value === p) ? p : null
);
}, [location.search]);
// SuchQuery aus URL
useEffect(() => {
setSearchQuery(new URLSearchParams(location.search).get("search"));
}, [location.search]);
// FilterFunktionen
const filteredItems = useMemo(() => {
return items
// 1) nach Preis: konvertiere Cent→Euro
.filter(i => {
const euro = (i.price100 * (1 - i.discount100 / 100)) / 100;
return euro >= priceRangeEuro[0] && euro <= priceRangeEuro[1];
})
// 2) Kategorie
.filter(i =>
!selectedCategory
? true
: i.category.toLowerCase() === selectedCategory.toLowerCase()
)
// 3) Rating (z.B. selectedRating=“4” → mind. 4 Sterne)
.filter(i =>
!selectedRating
? true
: Math.round(i.rating) >= Number(selectedRating)
)
// 4) Suche
.filter(i =>
!searchQuery
? true
: i.name.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [items, priceRangeEuro, selectedCategory, selectedRating, searchQuery]);
// ScrollReset, wenn Items schrumpfen
const containerRef = useRef<HTMLDivElement>(null);
const prevLen = useRef(items.length);
useEffect(() => {
if (items.length < prevLen.current) {
containerRef.current?.scrollTo(0, 0);
}
prevLen.current = items.length;
}, [items]);
// Handler
const handleCategoryChange = (cat: string) => {
setSelectedCategory(cat || null);
navigate(cat ? `/?category=${encodeURIComponent(cat)}` : "/");
};
const handleRatingChange = (rt: string) => {
setSelectedRating(rt || null);
};
// Definiere hier deine Kategoriendaten nochmal kurz:
const pagesCategoryOptions = useMemo(() => [
const categoriesFilter = useMemo(() => [
{ value: "", label: t("allCategories") },
{ value: "Seeds", label: t("seeds") },
{ value: "GardenSupplies", label: t("gardenSupplies") },
{ value: "TechnicalComponents", label: t("technicalComponents") },
{ value: "Other", label: t("other") },
{ value: "Other", label: t("other") }
], [t]);
const ratingFilter = useMemo(() => [
{ value: "", label: t("allRatings") },
...[5,4,3,2,1].map(v => ({ value: v.toString(), label: v.toString() }))
], [t]);
const ratingFilter = [
{ value: "", label: t('allRatings') },
...[5, 4, 3, 2, 1].map(value => ({
value: value.toString(),
label: value.toString()
}))
];
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [selectedRating, setSelectedRating] = useState<string | null>(null);
const { data = [] } = useQuery<Item[]>({
queryKey: ['fetchItemList'],
queryFn: fetchItemList,
retry: 3, // Versucht es 3-mal erneut
retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms)
});
const items:Item[] = useMemo(() => data || [], [data]);
const discountedPrices = items.map(
(item) => item.price100 * (1 - item.discount100 / 100)
);
const minPrice = discountedPrices.length > 0 ? Math.min(...discountedPrices) : 0;
const maxPrice = discountedPrices.length > 0 ? Math.max(...discountedPrices) : 1000;
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((f) => f.value === category)) {
setSelectedCategory(category);
} else {
setSelectedCategory(null);
}
}, [location.search, categoriesFilter]);
// Filterfunktion bleibt gleich
const filteredItems = useMemo(() => {return items
.filter((item) => {
const discountedPrice = item.price100 * (1 - item.discount100 / 100);
return discountedPrice >= priceRange[0] && discountedPrice <= priceRange[1];
})
.filter((item) => {
if (!selectedCategory) return true;
return item.category.toLowerCase() === selectedCategory.toLowerCase();
})
.filter((item) => {
if (!selectedRating) return true;
const rating = Math.trunc(item.rating);
return rating === (Number(selectedRating) * 2) -1 || rating === (Number(selectedRating) * 2);
})
.filter((item) => {
if (!searchQuery) return true;
return (item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
});
}, [items, priceRange, selectedCategory, selectedRating, searchQuery]);
// Lese die Suchanfrage aus der URL
useEffect(() => {
const params = new URLSearchParams(location.search);
const query = params.get("search");
setSearchQuery(query);
}, [location.search]);
// Items, die aktuell angezeigt werden
const visibleItems: Item[] = filteredItems;
// Container Ref
const containerRef = useRef<HTMLDivElement>(null);
const prevItemsLength = useRef(items.length);
useEffect(() => {
if (items.length >= prevItemsLength.current) {
prevItemsLength.current = items.length;
return;
}
setTimeout(() => {
containerRef.current?.scrollTo(0, 0);
}, 50);
prevItemsLength.current = items.length;
}, [items]);
// Kategorie-Änderung
const handleCategoryChange = (category: string) => {
if (category === "") {
setSelectedCategory(null);
navigate(`/`);
} else {
setSelectedCategory(category);
navigate(`/?category=${encodeURIComponent(category)}`);
}
};
// Rating-Änderung (bleibt gleich)
const handleRatingChange = (rating: string) => {
if (rating === "") {
setSelectedRating(null);
} else {
setSelectedRating(rating);
}
};
return (
<div
@@ -127,14 +149,16 @@ export default function Home() {
<div className="sidebar sidebar-filter">
<FilterItem
filterName={t("category")}
filterItems={pagesCategoryOptions}
filterItems={categoriesFilter}
value={selectedCategory}
onChange={handleCategoryChange}
/>
<PriceSlider
min={minCent}
max={maxCent}
onChange={setPriceRangeEuro}
min={minPrice}
max={maxPrice}
onChange={(range) => {
setPriceRange(range);
}}
/>
<FilterItem
filterName={t("rating")}
@@ -143,22 +167,12 @@ export default function Home() {
onChange={handleRatingChange}
/>
</div>
<div
className="page-background page-background-center"
ref={containerRef}
>
<div className="page-background page-background-center" ref={containerRef}>
<Box className="cardgrid">
{filteredItems.length === 0 ? (
<Alert
variant="filled"
severity="warning"
className="no-results"
>
{t("noItemsFound")}
</Alert>
{visibleItems.length === 0 ? (
<Alert variant="filled" severity="warning" className="no-results">{t('noItemsFound')}</Alert>
) : (
filteredItems.map(item => (
visibleItems.map((item) => (
<ItemCard key={item.id} item={item} />
))
)}

View File

@@ -23,14 +23,14 @@ interface CustomThemeProviderProps {
}
export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ children }) => {
// SSR-sichere System-Präferenz-Erkennung
// SSR-sichere System-Präferenz-Erkennung
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true });
// SSR-sichere Initialisierung
// SSR-sichere Initialisierung
const [mode, setMode] = useState<ThemeMode>('light');
const [mounted, setMounted] = useState(false);
// Nach dem ersten Render ausführen (SSR-sicher)
// Nach dem ersten Render ausführen (SSR-sicher)
useEffect(() => {
setMounted(true);
@@ -51,7 +51,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
}
}, [mode, mounted]);
// Browser-only DOM-Manipulation
// Browser-only DOM-Manipulation
useEffect(() => {
if (mounted && typeof window !== 'undefined') {
const backgroundColor = mode === 'dark' ? '#121212' : '#fafafa';
@@ -61,7 +61,6 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
document.documentElement.style.setProperty('--background-color', backgroundColor);
document.documentElement.style.backgroundColor = backgroundColor;
document.body.style.backgroundColor = backgroundColor;
document.body.classList.toggle('dark', mode === 'dark');
const root = document.getElementById('root');
if (root) {
@@ -89,7 +88,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
palette: {
mode,
primary: {
main: '#0fd13f', // Ihre grüne NavBar
main: '#0fd13f', // Grüne NavBar
light: mode === 'dark' ? '#4caf50' : '#42a5f5',
dark: mode === 'dark' ? '#388e3c' : '#1565c0',
contrastText: '#fff',
@@ -127,7 +126,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
MuiAppBar: {
styleOverrides: {
colorPrimary: {
backgroundColor: '#0fd13f',
backgroundColor: mode === 'dark' ? '#388e3c' : '#0fd13f',
color: '#ffffff',
},
},
@@ -137,7 +136,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
[mode]
);
// Aggressive GlobalStyles mit CSS-Variablen
// Aggressive GlobalStyles mit CSS-Variablen
const globalStyles = mounted ? (
<GlobalStyles
styles={{
@@ -163,7 +162,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
/>
) : null;
// SSR-Fallback während mounted = false
// SSR-Fallback während mounted = false
if (!mounted) {
return (
<ThemeProvider theme={createTheme({ palette: { mode: 'light' } })}>