From aab1fa182b0d4ad50ebafcc21cbc7868f2716a66 Mon Sep 17 00:00:00 2001 From: mathusan Date: Sun, 15 Jun 2025 17:25:23 +0200 Subject: [PATCH] Darkmode verbessert, Preisfilter geht wieder und Impressum wurde bearbeitet --- 01-frontend/package-lock.json | 33 +-- 01-frontend/package.json | 2 +- .../src/helper/adminpanel/StatisticsInfo.tsx | 100 +++++++- .../src/helper/homepage/FilterItem.css | 51 ++++ .../src/helper/homepage/FilterItem.tsx | 41 +-- .../src/helper/homepage/PriceSlider.css | 32 +++ .../src/helper/homepage/PriceSlider.tsx | 52 ++-- 01-frontend/src/index.css | 59 +++-- 01-frontend/src/pages/Contact.css | 58 +++++ 01-frontend/src/pages/Contact.tsx | 116 ++++++--- 01-frontend/src/pages/Home.tsx | 242 +++++++++--------- 01-frontend/src/theme/ThemeContext.tsx | 17 +- 12 files changed, 534 insertions(+), 269 deletions(-) create mode 100644 01-frontend/src/helper/homepage/FilterItem.css create mode 100644 01-frontend/src/helper/homepage/PriceSlider.css create mode 100644 01-frontend/src/pages/Contact.css diff --git a/01-frontend/package-lock.json b/01-frontend/package-lock.json index 1b740ee..66e64d6 100644 --- a/01-frontend/package-lock.json +++ b/01-frontend/package-lock.json @@ -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.1", + "@mui/x-charts": "^8.5.2", "@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.1", - "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.5.1.tgz", - "integrity": "sha512-6g0Gdyf2x/2UFZUWdifg7l8L1xl+YB8mz3NlsgK/Oa4Mf9EqPJUvXnzodxyNRT2UkX0l40p6yuOX7o+Mql20/w==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.5.2.tgz", + "integrity": "sha512-JLPTtd9m8CWMoIxwHFM9QpPDpfdsetfkCErJUvsyQnj/rC8sBMmQqk0c1olusA+OqTyVT3gGmiqXXFar/0cvkw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.4", + "@babel/runtime": "^7.27.6", "@mui/utils": "^7.1.1", - "@mui/x-charts-vendor": "8.5.1", - "@mui/x-internals": "8.5.1", + "@mui/x-charts-vendor": "8.5.2", + "@mui/x-internals": "8.5.2", "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.1", - "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.5.1.tgz", - "integrity": "sha512-da6QET4FBSzBYjhaaEIA+nrprc2revJMuwXPtDE14KAjEpIluchxsKTqn2XBg0j1NWm40FZX+fLh8m3w2crGIQ==", + "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==", "license": "MIT AND ISC", "dependencies": { - "@babel/runtime": "^7.27.4", + "@babel/runtime": "^7.27.6", "@types/d3-color": "^3.1.3", "@types/d3-delaunay": "^6.0.4", "@types/d3-interpolate": "^3.0.4", @@ -1611,13 +1611,14 @@ } }, "node_modules/@mui/x-internals": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.5.1.tgz", - "integrity": "sha512-7rAWK7SB6FxEIXKgsHsJjIzeeKOLxFJ16gePgZVWlvyew+xDb4P0fgjwW3ThcJjgvkUm0UhGGfLh/JP8l514IA==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.5.2.tgz", + "integrity": "sha512-5YhB2AekK7G8d0YrAjg3WNf0uy3V73JD98WNxJhbIlCraQgl8QOQzr2zNO7MAf/X7mZQtjpjuAsiG3+gI2NVyg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.4", - "@mui/utils": "^7.1.1" + "@babel/runtime": "^7.27.6", + "@mui/utils": "^7.1.1", + "reselect": "^5.1.1" }, "engines": { "node": ">=14.0.0" diff --git a/01-frontend/package.json b/01-frontend/package.json index 15bb1d7..ce64a4f 100644 --- a/01-frontend/package.json +++ b/01-frontend/package.json @@ -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.1", + "@mui/x-charts": "^8.5.2", "@mui/x-data-grid": "^8.5.2", "@tanstack/react-query": "^5.79.2", "chart.js": "^4.4.9", diff --git a/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx b/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx index 4ad3510..c3ca48d 100644 --- a/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx +++ b/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx @@ -1,18 +1,20 @@ import { Box, Typography, useTheme } from "@mui/material"; -import { BarChart } from '@mui/x-charts/BarChart'; -import { PieChart } from '@mui/x-charts/PieChart'; import { - ArcElement, - BarElement, - CategoryScale, Chart as ChartJS, - Legend, + CategoryScale, LinearScale, - LineElement, - PointElement, + BarElement, Title, Tooltip, + Legend, + ArcElement, + LineElement, + PointElement, } 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 @@ -32,6 +34,88 @@ 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 ( diff --git a/01-frontend/src/helper/homepage/FilterItem.css b/01-frontend/src/helper/homepage/FilterItem.css new file mode 100644 index 0000000..d5c90af --- /dev/null +++ b/01-frontend/src/helper/homepage/FilterItem.css @@ -0,0 +1,51 @@ +/* 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); + } + +} diff --git a/01-frontend/src/helper/homepage/FilterItem.tsx b/01-frontend/src/helper/homepage/FilterItem.tsx index 234cb70..f838581 100644 --- a/01-frontend/src/helper/homepage/FilterItem.tsx +++ b/01-frontend/src/helper/homepage/FilterItem.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { FormControl, FormControlLabel, @@ -5,9 +6,8 @@ import { Radio, RadioGroup, Rating, - useTheme, } from "@mui/material"; -import React from "react"; +import "./FilterItem.css"; type FilterItemOption = { value: string; @@ -22,37 +22,26 @@ type FilterItemProps = { }; export default function FilterItem({ - filterName, - filterItems, - value, - onChange, -}: FilterItemProps) { - const theme = useTheme(); - + filterName, + filterItems, + value, + onChange, + }: FilterItemProps) { + // Default-Wert, falls noch nichts ausgewählt if (!value && filterItems.length > 0) { value = filterItems[0].value; } - const handleChange = (event: React.ChangeEvent) => { - if (onChange) { - onChange(event.target.value); - } + const handleChange = (e: React.ChangeEvent) => { + onChange?.(e.target.value); }; return ( -
- +
+ {filterName} - - + {filterItems.map((item, idx) => ( ))} diff --git a/01-frontend/src/helper/homepage/PriceSlider.css b/01-frontend/src/helper/homepage/PriceSlider.css new file mode 100644 index 0000000..ddebe06 --- /dev/null +++ b/01-frontend/src/helper/homepage/PriceSlider.css @@ -0,0 +1,32 @@ +/* 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; +} diff --git a/01-frontend/src/helper/homepage/PriceSlider.tsx b/01-frontend/src/helper/homepage/PriceSlider.tsx index 780dd40..48947f6 100644 --- a/01-frontend/src/helper/homepage/PriceSlider.tsx +++ b/01-frontend/src/helper/homepage/PriceSlider.tsx @@ -1,37 +1,62 @@ import { Slider, Typography, Box, useTheme } from "@mui/material"; -import { useState, useEffect, SyntheticEvent } from "react"; +import { useState, useEffect } 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(); - const [value, setValue] = useState<[number, number]>([min, max]); + // Slider-Werte in Euro + const [value, setValue] = useState<[number, number]>([ + min / 100, + max / 100, + ]); + // Wenn sich min/max ändern, neu setzen useEffect(() => { - setValue([min, max]); - onChange?.([min, max]); + const euroMin = min / 100; + const euroMax = max / 100; + setValue([euroMin, euroMax]); + onChange?.([euroMin, euroMax]); }, [min, max]); - const handleChange = (_: Event, newValue: number | number[]) => { + const handleChange = ( + _: Event | React.SyntheticEvent, + newValue: number | number[] + ) => { if (Array.isArray(newValue)) { setValue([newValue[0], newValue[1]]); } }; - const handleCommitted = (_: Event | SyntheticEvent, newValue: number | number[]) => { + const handleCommitted = ( + _: Event | React.SyntheticEvent, + newValue: number | number[] + ) => { if (Array.isArray(newValue)) { + // Direkt in Euro zurückgeben onChange?.([newValue[0], newValue[1]]); } }; - const formatValueToEuro = (val: number) => `${(val/100).toFixed(2)} €`; + const formatValueToEuro = (v: number) => + new Intl.NumberFormat("de-DE", { + style: "currency", + currency: "EUR", + }).format(v); return ( @@ -45,7 +70,6 @@ export default function PriceSlider({ min = 0, max = 10000, onChange }: PriceSli > {t("price")} - - diff --git a/01-frontend/src/index.css b/01-frontend/src/index.css index 08a3ac9..ad5c67a 100644 --- a/01-frontend/src/index.css +++ b/01-frontend/src/index.css @@ -1,18 +1,39 @@ +/* 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; @@ -22,19 +43,7 @@ a:hover { color: #535bf2; } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - +/* Button-Styles */ button { border-radius: 8px; border: 1px solid transparent; @@ -54,14 +63,8 @@ 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; } diff --git a/01-frontend/src/pages/Contact.css b/01-frontend/src/pages/Contact.css new file mode 100644 index 0000000..4b300fb --- /dev/null +++ b/01-frontend/src/pages/Contact.css @@ -0,0 +1,58 @@ +/* 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 */ +} diff --git a/01-frontend/src/pages/Contact.tsx b/01-frontend/src/pages/Contact.tsx index d6df019..a2ec664 100644 --- a/01-frontend/src/pages/Contact.tsx +++ b/01-frontend/src/pages/Contact.tsx @@ -1,14 +1,21 @@ import { Box, Divider, Typography } from "@mui/material"; -import "./pages.css"; +import { useTheme } from "@mui/material/styles"; +import "./Contact.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 ( - + Impressum - + Hochschule für Technik und Wirtschaft
des Saarlandes
Goebenstraße 40
@@ -20,64 +27,97 @@ export default function Impressum() { Ministerium der Finanzen und für Wissenschaft des Saarlandes
- + - + Datenschutzerklärung - - Personenbezogene Daten (nachfolgend zumeist nur „Daten“ genannt) ... + + Personenbezogene Daten („Daten“) werden von uns nur verarbeitet, wenn es notwendig ist, + um einen funktionierenden und benutzerfreundlichen Internetauftritt bereitzustellen. - - Gemäß Art. 4 Ziffer 1. der Verordnung (EU) 2016/679, also der Datenschutz-Grundverordnung ... + + Gliederung: - - - Unsere Datenschutzerklärung ist wie folgt gegliedert:
+ I. Informationen über uns als Verantwortliche
II. Rechte der Nutzer und Betroffenen
III. Informationen zur Datenverarbeitung
- - I. Informationen über uns als Verantwortliche + + I. Verantwortlicher Anbieter + + + Hochschule für Technik und Wirtschaft
+ des Saarlandes
+ Goebenstraße 40
+ 66117 Saarbrücken
+ Telefon: (0681) 58 67 - 0
+ E-Mail: info@htwsaar.de
- - Verantwortlicher Anbieter dieses Internetauftritts ... - - - + II. Rechte der Nutzer und Betroffenen - - - Mit Blick auf die nachfolgend noch näher beschriebene Datenverarbeitung haben die Nutzer und Betroffenen ... - - -
    -
  • Auskunft über die verarbeiteten Daten (Art. 15 DSGVO)
  • -
  • Berichtigung unrichtiger Daten (Art. 16 DSGVO)
  • -
  • Löschung der Daten (Art. 17 DSGVO)
  • -
  • Einschränkung der Verarbeitung (Art. 18 DSGVO)
  • -
  • Datenübertragbarkeit (Art. 20 DSGVO)
  • +
      +
    • Auskunft über verarbeitete Daten (Art. 15 DSGVO)
    • +
    • Berichtigung unrichtiger Daten (Art. 16 DSGVO)
    • +
    • Löschung oder Einschränkung (Art. 17, 18 DSGVO)
    • +
    • Datenübertragbarkeit (Art. 20 DSGVO)
    • +
    • Widerspruch gegen Verarbeitung (Art. 21 DSGVO)
    • +
    • Beschwerde bei der Aufsichtsbehörde (Art. 77 DSGVO)
    - + III. Informationen zur Datenverarbeitung - - - Ihre bei Nutzung unseres Internetauftritts verarbeiteten Daten ... + + Daten werden gelöscht oder gesperrt, sobald der Zweck entfällt oder keine gesetzliche + Aufbewahrungspflicht mehr besteht. - {/* Du kannst einfach alle weiteren Absätze so fortsetzen – copy & paste, - jeweils in: */} + + Cookies + + + Unsere Website verwendet Cookies, um Funktionen wie Sprache, Warenkorb oder Benutzerkomfort zu ermöglichen. + - - Mehr Infos unter: CloudFlare Datenschutzerklärung + + Serverdaten + + + Ihr Browser übermittelt technische Daten (IP, Browsertyp etc.) an unseren Server, um die Website auszuliefern. + + + + Cloudflare + + + Unser CDN-Anbieter Cloudflare verarbeitet Zugriffsdaten zur Sicherheit und Optimierung. Mehr unter:  + + cloudflare.com/privacypolicy + + + + + Quelle:  + + Datenschutz Generator der Kanzlei Weiß & Partner + ); diff --git a/01-frontend/src/pages/Home.tsx b/01-frontend/src/pages/Home.tsx index 10b02a8..ea2e593 100644 --- a/01-frontend/src/pages/Home.tsx +++ b/01-frontend/src/pages/Home.tsx @@ -8,138 +8,116 @@ 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 der CSS-Datei +import "./pages.css"; export default function Home() { - const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); const theme = useTheme(); + + // URL‐Such‐Query const [searchQuery, setSearchQuery] = useState(null); - const categoriesFilter = useMemo(() => [ + // Kategorie & Rating + const [selectedCategory, setSelectedCategory] = useState(null); + const [selectedRating, setSelectedRating] = useState(null); + + // Items laden + const { data = [] } = useQuery({ + queryKey: ['fetchItemList'], + queryFn: fetchItemList, + retry: 3, + retryDelay: 1000, + }); + const items: Item[] = useMemo(() => data, [data]); + + // Discount‐Preis 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; + + // **Euro‐Bereich** 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]); + + // Such‐Query aus URL + useEffect(() => { + setSearchQuery(new URLSearchParams(location.search).get("search")); + }, [location.search]); + + // Filter‐Funktionen + 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]); + + // Scroll‐Reset, wenn Items schrumpfen + const containerRef = useRef(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(() => [ { 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 = [ - { value: "", label: t('allRatings') }, - ...[5, 4, 3, 2, 1].map(value => ({ - value: value.toString(), - label: value.toString() - })) - ]; - - const [selectedCategory, setSelectedCategory] = useState(null); - const [selectedRating, setSelectedRating] = useState(null); - - const { data = [] } = useQuery({ - 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(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); - } - }; - + const ratingFilter = useMemo(() => [ + { value: "", label: t("allRatings") }, + ...[5,4,3,2,1].map(v => ({ value: v.toString(), label: v.toString() })) + ], [t]); return (
    { - setPriceRange(range); - }} + min={minCent} + max={maxCent} + onChange={setPriceRangeEuro} />
    -
    + +
    - {visibleItems.length === 0 ? ( - {t('noItemsFound')} + {filteredItems.length === 0 ? ( + + {t("noItemsFound")} + ) : ( - visibleItems.map((item) => ( + filteredItems.map(item => ( )) )} diff --git a/01-frontend/src/theme/ThemeContext.tsx b/01-frontend/src/theme/ThemeContext.tsx index e999896..4ddac01 100644 --- a/01-frontend/src/theme/ThemeContext.tsx +++ b/01-frontend/src/theme/ThemeContext.tsx @@ -23,14 +23,14 @@ interface CustomThemeProviderProps { } export const CustomThemeProvider: React.FC = ({ 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('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 = ({ childr } }, [mode, mounted]); - // Browser-only DOM-Manipulation + // ✅ Browser-only DOM-Manipulation useEffect(() => { if (mounted && typeof window !== 'undefined') { const backgroundColor = mode === 'dark' ? '#121212' : '#fafafa'; @@ -61,6 +61,7 @@ export const CustomThemeProvider: React.FC = ({ 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) { @@ -88,7 +89,7 @@ export const CustomThemeProvider: React.FC = ({ childr palette: { mode, primary: { - main: '#0fd13f', // Grüne NavBar + main: '#0fd13f', // Ihre grüne NavBar light: mode === 'dark' ? '#4caf50' : '#42a5f5', dark: mode === 'dark' ? '#388e3c' : '#1565c0', contrastText: '#fff', @@ -126,7 +127,7 @@ export const CustomThemeProvider: React.FC = ({ childr MuiAppBar: { styleOverrides: { colorPrimary: { - backgroundColor: mode === 'dark' ? '#388e3c' : '#0fd13f', + backgroundColor: '#0fd13f', color: '#ffffff', }, }, @@ -136,7 +137,7 @@ export const CustomThemeProvider: React.FC = ({ childr [mode] ); - // Aggressive GlobalStyles mit CSS-Variablen + // ✅ Aggressive GlobalStyles mit CSS-Variablen const globalStyles = mounted ? ( = ({ childr /> ) : null; - // SSR-Fallback während mounted = false + // ✅ SSR-Fallback während mounted = false if (!mounted) { return (