Darkmode verbessert, Preisfilter geht wieder und Impressum wurde bearbeitet
This commit is contained in:
33
01-frontend/package-lock.json
generated
33
01-frontend/package-lock.json
generated
@@ -15,7 +15,7 @@
|
|||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@mui/icons-material": "^7.0.2",
|
"@mui/icons-material": "^7.0.2",
|
||||||
"@mui/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",
|
"@mui/x-data-grid": "^8.5.2",
|
||||||
"@tanstack/react-query": "^5.79.2",
|
"@tanstack/react-query": "^5.79.2",
|
||||||
"chart.js": "^4.4.9",
|
"chart.js": "^4.4.9",
|
||||||
@@ -1491,15 +1491,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/x-charts": {
|
"node_modules/@mui/x-charts": {
|
||||||
"version": "8.5.1",
|
"version": "8.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.5.2.tgz",
|
||||||
"integrity": "sha512-6g0Gdyf2x/2UFZUWdifg7l8L1xl+YB8mz3NlsgK/Oa4Mf9EqPJUvXnzodxyNRT2UkX0l40p6yuOX7o+Mql20/w==",
|
"integrity": "sha512-JLPTtd9m8CWMoIxwHFM9QpPDpfdsetfkCErJUvsyQnj/rC8sBMmQqk0c1olusA+OqTyVT3gGmiqXXFar/0cvkw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.4",
|
"@babel/runtime": "^7.27.6",
|
||||||
"@mui/utils": "^7.1.1",
|
"@mui/utils": "^7.1.1",
|
||||||
"@mui/x-charts-vendor": "8.5.1",
|
"@mui/x-charts-vendor": "8.5.2",
|
||||||
"@mui/x-internals": "8.5.1",
|
"@mui/x-internals": "8.5.2",
|
||||||
"bezier-easing": "^2.1.0",
|
"bezier-easing": "^2.1.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
@@ -1527,12 +1527,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/x-charts-vendor": {
|
"node_modules/@mui/x-charts-vendor": {
|
||||||
"version": "8.5.1",
|
"version": "8.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.5.2.tgz",
|
||||||
"integrity": "sha512-da6QET4FBSzBYjhaaEIA+nrprc2revJMuwXPtDE14KAjEpIluchxsKTqn2XBg0j1NWm40FZX+fLh8m3w2crGIQ==",
|
"integrity": "sha512-93KFrEpo3Xhr0g2TQsbtPVqGAsbkKBN5J57ykrCM5GxFmq3kDGFU4k9+FpKiaIYYL8ijzgHGNh+jNVbP0pq3rQ==",
|
||||||
"license": "MIT AND ISC",
|
"license": "MIT AND ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.4",
|
"@babel/runtime": "^7.27.6",
|
||||||
"@types/d3-color": "^3.1.3",
|
"@types/d3-color": "^3.1.3",
|
||||||
"@types/d3-delaunay": "^6.0.4",
|
"@types/d3-delaunay": "^6.0.4",
|
||||||
"@types/d3-interpolate": "^3.0.4",
|
"@types/d3-interpolate": "^3.0.4",
|
||||||
@@ -1611,13 +1611,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/x-internals": {
|
"node_modules/@mui/x-internals": {
|
||||||
"version": "8.5.1",
|
"version": "8.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.5.2.tgz",
|
||||||
"integrity": "sha512-7rAWK7SB6FxEIXKgsHsJjIzeeKOLxFJ16gePgZVWlvyew+xDb4P0fgjwW3ThcJjgvkUm0UhGGfLh/JP8l514IA==",
|
"integrity": "sha512-5YhB2AekK7G8d0YrAjg3WNf0uy3V73JD98WNxJhbIlCraQgl8QOQzr2zNO7MAf/X7mZQtjpjuAsiG3+gI2NVyg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.4",
|
"@babel/runtime": "^7.27.6",
|
||||||
"@mui/utils": "^7.1.1"
|
"@mui/utils": "^7.1.1",
|
||||||
|
"reselect": "^5.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@mui/icons-material": "^7.0.2",
|
"@mui/icons-material": "^7.0.2",
|
||||||
"@mui/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",
|
"@mui/x-data-grid": "^8.5.2",
|
||||||
"@tanstack/react-query": "^5.79.2",
|
"@tanstack/react-query": "^5.79.2",
|
||||||
"chart.js": "^4.4.9",
|
"chart.js": "^4.4.9",
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { Box, Typography, useTheme } from "@mui/material";
|
import { Box, Typography, useTheme } from "@mui/material";
|
||||||
import { BarChart } from '@mui/x-charts/BarChart';
|
|
||||||
import { PieChart } from '@mui/x-charts/PieChart';
|
|
||||||
import {
|
import {
|
||||||
ArcElement,
|
|
||||||
BarElement,
|
|
||||||
CategoryScale,
|
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
Legend,
|
CategoryScale,
|
||||||
LinearScale,
|
LinearScale,
|
||||||
LineElement,
|
BarElement,
|
||||||
PointElement,
|
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
} from "chart.js";
|
} 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";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
// Chart.js registrieren
|
// Chart.js registrieren
|
||||||
@@ -32,6 +34,88 @@ export default function StatisticsInfo() {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const {t} = useTranslation();
|
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 (
|
return (
|
||||||
<Box sx={{ color: theme.palette.text.primary }}>
|
<Box sx={{ color: theme.palette.text.primary }}>
|
||||||
<Typography variant="h4" align="center" gutterBottom>
|
<Typography variant="h4" align="center" gutterBottom>
|
||||||
|
|||||||
51
01-frontend/src/helper/homepage/FilterItem.css
Normal file
51
01-frontend/src/helper/homepage/FilterItem.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
@@ -5,9 +6,8 @@ import {
|
|||||||
Radio,
|
Radio,
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
Rating,
|
Rating,
|
||||||
useTheme,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React from "react";
|
import "./FilterItem.css";
|
||||||
|
|
||||||
type FilterItemOption = {
|
type FilterItemOption = {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -26,33 +26,22 @@ export default function FilterItem({
|
|||||||
filterItems,
|
filterItems,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: FilterItemProps) {
|
}: FilterItemProps) {
|
||||||
const theme = useTheme();
|
// Default-Wert, falls noch nichts ausgewählt
|
||||||
|
|
||||||
if (!value && filterItems.length > 0) {
|
if (!value && filterItems.length > 0) {
|
||||||
value = filterItems[0].value;
|
value = filterItems[0].value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (onChange) {
|
onChange?.(e.target.value);
|
||||||
onChange(event.target.value);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: "1.5rem" }}>
|
<div className="filter-item">
|
||||||
<FormLabel
|
<FormLabel component="legend" className="filter-item__label">
|
||||||
component="legend"
|
|
||||||
sx={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
mb: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{filterName}
|
{filterName}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
<FormControl className="filter-item__group">
|
||||||
<FormControl>
|
|
||||||
<RadioGroup value={value} onChange={handleChange}>
|
<RadioGroup value={value} onChange={handleChange}>
|
||||||
{filterItems.map((item, idx) => (
|
{filterItems.map((item, idx) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
@@ -71,9 +60,7 @@ export default function FilterItem({
|
|||||||
item.label
|
item.label
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sx={{
|
className="filter-item__option"
|
||||||
color: theme.palette.text.primary,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|||||||
32
01-frontend/src/helper/homepage/PriceSlider.css
Normal file
32
01-frontend/src/helper/homepage/PriceSlider.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,37 +1,62 @@
|
|||||||
import { Slider, Typography, Box, useTheme } from "@mui/material";
|
import { Slider, Typography, Box, useTheme } from "@mui/material";
|
||||||
import { useState, useEffect, SyntheticEvent } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type PriceSliderProps = {
|
type PriceSliderProps = {
|
||||||
|
/** in Cent übergeben */
|
||||||
min?: number;
|
min?: number;
|
||||||
|
/** in Cent übergeben */
|
||||||
max?: number;
|
max?: number;
|
||||||
|
/** in Euro zurückbekommen */
|
||||||
onChange?: (range: [number, number]) => void;
|
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 { t } = useTranslation();
|
||||||
const theme = useTheme();
|
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(() => {
|
useEffect(() => {
|
||||||
setValue([min, max]);
|
const euroMin = min / 100;
|
||||||
onChange?.([min, max]);
|
const euroMax = max / 100;
|
||||||
|
setValue([euroMin, euroMax]);
|
||||||
|
onChange?.([euroMin, euroMax]);
|
||||||
}, [min, max]);
|
}, [min, max]);
|
||||||
|
|
||||||
const handleChange = (_: Event, newValue: number | number[]) => {
|
const handleChange = (
|
||||||
|
_: Event | React.SyntheticEvent,
|
||||||
|
newValue: number | number[]
|
||||||
|
) => {
|
||||||
if (Array.isArray(newValue)) {
|
if (Array.isArray(newValue)) {
|
||||||
setValue([newValue[0], newValue[1]]);
|
setValue([newValue[0], newValue[1]]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCommitted = (_: Event | SyntheticEvent<Element, Event>, newValue: number | number[]) => {
|
const handleCommitted = (
|
||||||
|
_: Event | React.SyntheticEvent,
|
||||||
|
newValue: number | number[]
|
||||||
|
) => {
|
||||||
if (Array.isArray(newValue)) {
|
if (Array.isArray(newValue)) {
|
||||||
|
// Direkt in Euro zurückgeben
|
||||||
onChange?.([newValue[0], newValue[1]]);
|
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 (
|
return (
|
||||||
<Box sx={{ mb: 4 }}>
|
<Box sx={{ mb: 4 }}>
|
||||||
@@ -45,7 +70,6 @@ export default function PriceSlider({ min = 0, max = 10000, onChange }: PriceSli
|
|||||||
>
|
>
|
||||||
{t("price")}
|
{t("price")}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{ px: 1 }}>
|
<Box sx={{ px: 1 }}>
|
||||||
<Slider
|
<Slider
|
||||||
value={value}
|
value={value}
|
||||||
@@ -53,23 +77,21 @@ export default function PriceSlider({ min = 0, max = 10000, onChange }: PriceSli
|
|||||||
onChangeCommitted={handleCommitted}
|
onChangeCommitted={handleCommitted}
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
valueLabelFormat={formatValueToEuro}
|
valueLabelFormat={formatValueToEuro}
|
||||||
min={min}
|
min={min / 100}
|
||||||
max={max}
|
max={max / 100}
|
||||||
step={1}
|
step={0.01}
|
||||||
sx={{
|
sx={{
|
||||||
color: "#0fd13f",
|
color: "#0fd13f",
|
||||||
'& .MuiSlider-valueLabel': {
|
"& .MuiSlider-valueLabel": {
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
mt: 1,
|
mt: 1,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
fontSize: "0.9rem",
|
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,18 +1,39 @@
|
|||||||
|
/* index.css (oder App.css), ganz oben importieren */
|
||||||
|
|
||||||
:root {
|
: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;
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
color-scheme: light dark;
|
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 {
|
a {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #646cff;
|
color: #646cff;
|
||||||
@@ -22,19 +43,7 @@ a:hover {
|
|||||||
color: #535bf2;
|
color: #535bf2;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
/* Button-Styles */
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
@@ -54,14 +63,8 @@ button:focus-visible {
|
|||||||
outline: 4px auto -webkit-focus-ring-color;
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Optional: systemweite Light-Mode-Ergänzungen */
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
button {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|||||||
58
01-frontend/src/pages/Contact.css
Normal file
58
01-frontend/src/pages/Contact.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
@@ -1,14 +1,21 @@
|
|||||||
import { Box, Divider, Typography } from "@mui/material";
|
import { Box, Divider, Typography } from "@mui/material";
|
||||||
import "./pages.css";
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import "./Contact.css";
|
||||||
|
|
||||||
export default function Impressum() {
|
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 (
|
return (
|
||||||
<Box className="impressum-container">
|
<Box className="impressum-container">
|
||||||
<Typography variant="h4" sx={{ color: 'text.primary', mb: 2 }}>
|
<Typography component="h1" className="impressum-title">
|
||||||
Impressum
|
Impressum
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{ color: 'text.primary', mb: 4 }}>
|
<Typography component="p" className="impressum-paragraph">
|
||||||
Hochschule für Technik und Wirtschaft<br />
|
Hochschule für Technik und Wirtschaft<br />
|
||||||
des Saarlandes<br />
|
des Saarlandes<br />
|
||||||
Goebenstraße 40<br />
|
Goebenstraße 40<br />
|
||||||
@@ -20,64 +27,97 @@ export default function Impressum() {
|
|||||||
Ministerium der Finanzen und für Wissenschaft des Saarlandes
|
Ministerium der Finanzen und für Wissenschaft des Saarlandes
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Divider className="contact-divider" />
|
<Divider className="impressum-divider" />
|
||||||
|
|
||||||
<Typography variant="h5" sx={{ color: 'text.primary', mt: 4, mb: 2 }}>
|
<Typography component="h2" className="impressum-subtitle">
|
||||||
Datenschutzerklärung
|
Datenschutzerklärung
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
|
<Typography component="p" className="impressum-paragraph">
|
||||||
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.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
|
<Typography component="h3" className="impressum-heading">
|
||||||
Gemäß Art. 4 Ziffer 1. der Verordnung (EU) 2016/679, also der Datenschutz-Grundverordnung ...
|
Gliederung:
|
||||||
</Typography>
|
</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 />
|
I. Informationen über uns als Verantwortliche<br />
|
||||||
II. Rechte der Nutzer und Betroffenen<br />
|
II. Rechte der Nutzer und Betroffenen<br />
|
||||||
III. Informationen zur Datenverarbeitung
|
III. Informationen zur Datenverarbeitung
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="h6" sx={{ color: 'text.primary', mt: 4, mb: 1 }}>
|
<Typography component="h3" className="impressum-heading">
|
||||||
I. Informationen über uns als Verantwortliche
|
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>
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
|
<Typography component="h3" className="impressum-heading">
|
||||||
Verantwortlicher Anbieter dieses Internetauftritts ...
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography variant="h6" sx={{ color: 'text.primary', mt: 4, mb: 1 }}>
|
|
||||||
II. Rechte der Nutzer und Betroffenen
|
II. Rechte der Nutzer und Betroffenen
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<ul className="impressum-list">
|
||||||
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
|
<li>Auskunft über verarbeitete Daten (Art. 15 DSGVO)</li>
|
||||||
Mit Blick auf die nachfolgend noch näher beschriebene Datenverarbeitung haben die Nutzer und Betroffenen ...
|
<li>Berichtigung unrichtiger Daten (Art. 16 DSGVO)</li>
|
||||||
</Typography>
|
<li>Löschung oder Einschränkung (Art. 17, 18 DSGVO)</li>
|
||||||
|
<li>Datenübertragbarkeit (Art. 20 DSGVO)</li>
|
||||||
<ul>
|
<li>Widerspruch gegen Verarbeitung (Art. 21 DSGVO)</li>
|
||||||
<li><Typography variant="body2" sx={{ color: 'text.primary' }}>Auskunft über die verarbeiteten Daten (Art. 15 DSGVO)</Typography></li>
|
<li>Beschwerde bei der Aufsichtsbehörde (Art. 77 DSGVO)</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>
|
</ul>
|
||||||
|
|
||||||
<Typography variant="h6" sx={{ color: 'text.primary', mt: 4, mb: 1 }}>
|
<Typography component="h3" className="impressum-heading">
|
||||||
III. Informationen zur Datenverarbeitung
|
III. Informationen zur Datenverarbeitung
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Typography component="p" className="impressum-paragraph">
|
||||||
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
|
Daten werden gelöscht oder gesperrt, sobald der Zweck entfällt oder keine gesetzliche
|
||||||
Ihre bei Nutzung unseres Internetauftritts verarbeiteten Daten ...
|
Aufbewahrungspflicht mehr besteht.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Du kannst einfach alle weiteren Absätze so fortsetzen – copy & paste,
|
<Typography component="h4" className="impressum-heading">
|
||||||
jeweils in: <Typography variant="body1" sx={{ color: 'text.primary' }}>…</Typography> */}
|
Cookies
|
||||||
|
</Typography>
|
||||||
|
<Typography component="p" className="impressum-paragraph">
|
||||||
|
Unsere Website verwendet Cookies, um Funktionen wie Sprache, Warenkorb oder Benutzerkomfort zu ermöglichen.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<Typography variant="body2" sx={{ color: 'text.primary', mt: 4 }}>
|
<Typography component="h4" className="impressum-heading">
|
||||||
Mehr Infos unter: <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener noreferrer">CloudFlare Datenschutzerklärung</a>
|
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:
|
||||||
|
<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:
|
||||||
|
<a
|
||||||
|
href="https://www.ratgeberrecht.eu/datenschutz/datenschutzerklaerung-generator-dsgvo.html"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="impressum-link"
|
||||||
|
>
|
||||||
|
Datenschutz Generator der Kanzlei Weiß & Partner
|
||||||
|
</a>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,138 +8,116 @@ import FilterItem from "../helper/homepage/FilterItem";
|
|||||||
import ItemCard from "../helper/homepage/ItemCard";
|
import ItemCard from "../helper/homepage/ItemCard";
|
||||||
import PriceSlider from "../helper/homepage/PriceSlider";
|
import PriceSlider from "../helper/homepage/PriceSlider";
|
||||||
import { fetchItemList } from '../helper/query/Queries';
|
import { fetchItemList } from '../helper/query/Queries';
|
||||||
import "./pages.css"; // Import der CSS-Datei
|
import "./pages.css";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
// URL‐Such‐Query
|
||||||
const [searchQuery, setSearchQuery] = useState<string | null>(null);
|
const [searchQuery, setSearchQuery] = useState<string | null>(null);
|
||||||
|
|
||||||
const categoriesFilter = useMemo(() => [
|
// 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]);
|
||||||
|
|
||||||
|
// 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<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(() => [
|
||||||
{ value: "", label: t("allCategories") },
|
{ value: "", label: t("allCategories") },
|
||||||
{ value: "Seeds", label: t("seeds") },
|
{ value: "Seeds", label: t("seeds") },
|
||||||
{ value: "GardenSupplies", label: t("gardenSupplies") },
|
{ value: "GardenSupplies", label: t("gardenSupplies") },
|
||||||
{ value: "TechnicalComponents", label: t("technicalComponents") },
|
{ value: "TechnicalComponents", label: t("technicalComponents") },
|
||||||
{ value: "Other", label: t("other") }
|
{ value: "Other", label: t("other") },
|
||||||
], [t]);
|
], [t]);
|
||||||
|
|
||||||
const ratingFilter = [
|
const ratingFilter = useMemo(() => [
|
||||||
{ value: "", label: t('allRatings') },
|
{ value: "", label: t("allRatings") },
|
||||||
...[5, 4, 3, 2, 1].map(value => ({
|
...[5,4,3,2,1].map(v => ({ value: v.toString(), label: v.toString() }))
|
||||||
value: value.toString(),
|
], [t]);
|
||||||
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -149,16 +127,14 @@ export default function Home() {
|
|||||||
<div className="sidebar sidebar-filter">
|
<div className="sidebar sidebar-filter">
|
||||||
<FilterItem
|
<FilterItem
|
||||||
filterName={t("category")}
|
filterName={t("category")}
|
||||||
filterItems={categoriesFilter}
|
filterItems={pagesCategoryOptions}
|
||||||
value={selectedCategory}
|
value={selectedCategory}
|
||||||
onChange={handleCategoryChange}
|
onChange={handleCategoryChange}
|
||||||
/>
|
/>
|
||||||
<PriceSlider
|
<PriceSlider
|
||||||
min={minPrice}
|
min={minCent}
|
||||||
max={maxPrice}
|
max={maxCent}
|
||||||
onChange={(range) => {
|
onChange={setPriceRangeEuro}
|
||||||
setPriceRange(range);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<FilterItem
|
<FilterItem
|
||||||
filterName={t("rating")}
|
filterName={t("rating")}
|
||||||
@@ -167,12 +143,22 @@ export default function Home() {
|
|||||||
onChange={handleRatingChange}
|
onChange={handleRatingChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-background page-background-center" ref={containerRef}>
|
|
||||||
|
<div
|
||||||
|
className="page-background page-background-center"
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
<Box className="cardgrid">
|
<Box className="cardgrid">
|
||||||
{visibleItems.length === 0 ? (
|
{filteredItems.length === 0 ? (
|
||||||
<Alert variant="filled" severity="warning" className="no-results">{t('noItemsFound')}</Alert>
|
<Alert
|
||||||
|
variant="filled"
|
||||||
|
severity="warning"
|
||||||
|
className="no-results"
|
||||||
|
>
|
||||||
|
{t("noItemsFound")}
|
||||||
|
</Alert>
|
||||||
) : (
|
) : (
|
||||||
visibleItems.map((item) => (
|
filteredItems.map(item => (
|
||||||
<ItemCard key={item.id} item={item} />
|
<ItemCard key={item.id} item={item} />
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ interface CustomThemeProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ children }) => {
|
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 });
|
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', { noSsr: true });
|
||||||
|
|
||||||
// SSR-sichere Initialisierung
|
// ✅ SSR-sichere Initialisierung
|
||||||
const [mode, setMode] = useState<ThemeMode>('light');
|
const [mode, setMode] = useState<ThemeMode>('light');
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
// Nach dem ersten Render ausführen (SSR-sicher)
|
// ✅ Nach dem ersten Render ausführen (SSR-sicher)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
|
|||||||
}
|
}
|
||||||
}, [mode, mounted]);
|
}, [mode, mounted]);
|
||||||
|
|
||||||
// Browser-only DOM-Manipulation
|
// ✅ Browser-only DOM-Manipulation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mounted && typeof window !== 'undefined') {
|
if (mounted && typeof window !== 'undefined') {
|
||||||
const backgroundColor = mode === 'dark' ? '#121212' : '#fafafa';
|
const backgroundColor = mode === 'dark' ? '#121212' : '#fafafa';
|
||||||
@@ -61,6 +61,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
|
|||||||
document.documentElement.style.setProperty('--background-color', backgroundColor);
|
document.documentElement.style.setProperty('--background-color', backgroundColor);
|
||||||
document.documentElement.style.backgroundColor = backgroundColor;
|
document.documentElement.style.backgroundColor = backgroundColor;
|
||||||
document.body.style.backgroundColor = backgroundColor;
|
document.body.style.backgroundColor = backgroundColor;
|
||||||
|
document.body.classList.toggle('dark', mode === 'dark');
|
||||||
|
|
||||||
const root = document.getElementById('root');
|
const root = document.getElementById('root');
|
||||||
if (root) {
|
if (root) {
|
||||||
@@ -88,7 +89,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
|
|||||||
palette: {
|
palette: {
|
||||||
mode,
|
mode,
|
||||||
primary: {
|
primary: {
|
||||||
main: '#0fd13f', // Grüne NavBar
|
main: '#0fd13f', // Ihre grüne NavBar
|
||||||
light: mode === 'dark' ? '#4caf50' : '#42a5f5',
|
light: mode === 'dark' ? '#4caf50' : '#42a5f5',
|
||||||
dark: mode === 'dark' ? '#388e3c' : '#1565c0',
|
dark: mode === 'dark' ? '#388e3c' : '#1565c0',
|
||||||
contrastText: '#fff',
|
contrastText: '#fff',
|
||||||
@@ -126,7 +127,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
|
|||||||
MuiAppBar: {
|
MuiAppBar: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
colorPrimary: {
|
colorPrimary: {
|
||||||
backgroundColor: mode === 'dark' ? '#388e3c' : '#0fd13f',
|
backgroundColor: '#0fd13f',
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -136,7 +137,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
|
|||||||
[mode]
|
[mode]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Aggressive GlobalStyles mit CSS-Variablen
|
// ✅ Aggressive GlobalStyles mit CSS-Variablen
|
||||||
const globalStyles = mounted ? (
|
const globalStyles = mounted ? (
|
||||||
<GlobalStyles
|
<GlobalStyles
|
||||||
styles={{
|
styles={{
|
||||||
@@ -162,7 +163,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
|
|||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
// SSR-Fallback während mounted = false
|
// ✅ SSR-Fallback während mounted = false
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={createTheme({ palette: { mode: 'light' } })}>
|
<ThemeProvider theme={createTheme({ palette: { mode: 'light' } })}>
|
||||||
|
|||||||
Reference in New Issue
Block a user