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",
|
||||
"@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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<Box sx={{ color: theme.palette.text.primary }}>
|
||||
<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 {
|
||||
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;
|
||||
@@ -27,32 +27,21 @@ export default function FilterItem({
|
||||
value,
|
||||
onChange,
|
||||
}: FilterItemProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
// Default-Wert, falls noch nichts ausgewählt
|
||||
if (!value && filterItems.length > 0) {
|
||||
value = filterItems[0].value;
|
||||
}
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (onChange) {
|
||||
onChange(event.target.value);
|
||||
}
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange?.(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<FormLabel
|
||||
component="legend"
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
color: theme.palette.text.primary,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<div className="filter-item">
|
||||
<FormLabel component="legend" className="filter-item__label">
|
||||
{filterName}
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<FormControl className="filter-item__group">
|
||||
<RadioGroup value={value} onChange={handleChange}>
|
||||
{filterItems.map((item, idx) => (
|
||||
<FormControlLabel
|
||||
@@ -71,9 +60,7 @@ export default function FilterItem({
|
||||
item.label
|
||||
)
|
||||
}
|
||||
sx={{
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
className="filter-item__option"
|
||||
/>
|
||||
))}
|
||||
</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 { 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<Element, Event>, 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 (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
@@ -45,7 +70,6 @@ export default function PriceSlider({ min = 0, max = 10000, onChange }: PriceSli
|
||||
>
|
||||
{t("price")}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ px: 1 }}>
|
||||
<Slider
|
||||
value={value}
|
||||
@@ -53,23 +77,21 @@ export default function PriceSlider({ min = 0, max = 10000, onChange }: PriceSli
|
||||
onChangeCommitted={handleCommitted}
|
||||
valueLabelDisplay="auto"
|
||||
valueLabelFormat={formatValueToEuro}
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
||||
min={min / 100}
|
||||
max={max / 100}
|
||||
step={0.01}
|
||||
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",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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 "./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 (
|
||||
<Box className="impressum-container">
|
||||
<Typography variant="h4" sx={{ color: 'text.primary', mb: 2 }}>
|
||||
<Typography component="h1" className="impressum-title">
|
||||
Impressum
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" sx={{ color: 'text.primary', mb: 4 }}>
|
||||
<Typography component="p" className="impressum-paragraph">
|
||||
Hochschule für Technik und Wirtschaft<br />
|
||||
des Saarlandes<br />
|
||||
Goebenstraße 40<br />
|
||||
@@ -20,64 +27,97 @@ export default function Impressum() {
|
||||
Ministerium der Finanzen und für Wissenschaft des Saarlandes
|
||||
</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
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
|
||||
Personenbezogene Daten (nachfolgend zumeist nur „Daten“ genannt) ...
|
||||
<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>
|
||||
|
||||
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
|
||||
Gemäß Art. 4 Ziffer 1. der Verordnung (EU) 2016/679, also der Datenschutz-Grundverordnung ...
|
||||
<Typography component="h3" className="impressum-heading">
|
||||
Gliederung:
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
|
||||
Unsere Datenschutzerklärung ist wie folgt gegliedert:<br />
|
||||
<Typography component="p" className="impressum-paragraph">
|
||||
I. Informationen über uns als Verantwortliche<br />
|
||||
II. Rechte der Nutzer und Betroffenen<br />
|
||||
III. Informationen zur Datenverarbeitung
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" sx={{ color: 'text.primary', mt: 4, mb: 1 }}>
|
||||
I. Informationen über uns als Verantwortliche
|
||||
<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>
|
||||
|
||||
<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 }}>
|
||||
<Typography component="h3" className="impressum-heading">
|
||||
II. Rechte der Nutzer und Betroffenen
|
||||
</Typography>
|
||||
|
||||
<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 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>
|
||||
</ul>
|
||||
|
||||
<Typography variant="h6" sx={{ color: 'text.primary', mt: 4, mb: 1 }}>
|
||||
<Typography component="h3" className="impressum-heading">
|
||||
III. Informationen zur Datenverarbeitung
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" sx={{ color: 'text.primary', mb: 2 }}>
|
||||
Ihre bei Nutzung unseres Internetauftritts verarbeiteten Daten ...
|
||||
<Typography component="p" className="impressum-paragraph">
|
||||
Daten werden gelöscht oder gesperrt, sobald der Zweck entfällt oder keine gesetzliche
|
||||
Aufbewahrungspflicht mehr besteht.
|
||||
</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">
|
||||
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 }}>
|
||||
Mehr Infos unter: <a href="https://www.cloudflare.com/privacypolicy/" target="_blank" rel="noopener noreferrer">CloudFlare Datenschutzerklärung</a>
|
||||
<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:
|
||||
<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>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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<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: "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<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);
|
||||
}
|
||||
};
|
||||
|
||||
const ratingFilter = useMemo(() => [
|
||||
{ value: "", label: t("allRatings") },
|
||||
...[5,4,3,2,1].map(v => ({ value: v.toString(), label: v.toString() }))
|
||||
], [t]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -149,16 +127,14 @@ export default function Home() {
|
||||
<div className="sidebar sidebar-filter">
|
||||
<FilterItem
|
||||
filterName={t("category")}
|
||||
filterItems={categoriesFilter}
|
||||
filterItems={pagesCategoryOptions}
|
||||
value={selectedCategory}
|
||||
onChange={handleCategoryChange}
|
||||
/>
|
||||
<PriceSlider
|
||||
min={minPrice}
|
||||
max={maxPrice}
|
||||
onChange={(range) => {
|
||||
setPriceRange(range);
|
||||
}}
|
||||
min={minCent}
|
||||
max={maxCent}
|
||||
onChange={setPriceRangeEuro}
|
||||
/>
|
||||
<FilterItem
|
||||
filterName={t("rating")}
|
||||
@@ -167,12 +143,22 @@ 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">
|
||||
{visibleItems.length === 0 ? (
|
||||
<Alert variant="filled" severity="warning" className="no-results">{t('noItemsFound')}</Alert>
|
||||
{filteredItems.length === 0 ? (
|
||||
<Alert
|
||||
variant="filled"
|
||||
severity="warning"
|
||||
className="no-results"
|
||||
>
|
||||
{t("noItemsFound")}
|
||||
</Alert>
|
||||
) : (
|
||||
visibleItems.map((item) => (
|
||||
filteredItems.map(item => (
|
||||
<ItemCard key={item.id} item={item} />
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -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,6 +61,7 @@ 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) {
|
||||
@@ -88,7 +89,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ 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<CustomThemeProviderProps> = ({ childr
|
||||
MuiAppBar: {
|
||||
styleOverrides: {
|
||||
colorPrimary: {
|
||||
backgroundColor: mode === 'dark' ? '#388e3c' : '#0fd13f',
|
||||
backgroundColor: '#0fd13f',
|
||||
color: '#ffffff',
|
||||
},
|
||||
},
|
||||
@@ -136,7 +137,7 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
|
||||
[mode]
|
||||
);
|
||||
|
||||
// Aggressive GlobalStyles mit CSS-Variablen
|
||||
// ✅ Aggressive GlobalStyles mit CSS-Variablen
|
||||
const globalStyles = mounted ? (
|
||||
<GlobalStyles
|
||||
styles={{
|
||||
@@ -162,7 +163,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' } })}>
|
||||
|
||||
Reference in New Issue
Block a user