This commit is contained in:
Tim
2026-03-11 12:30:20 +01:00
parent e3004bbc72
commit 483cb4b043
46 changed files with 5456 additions and 4850 deletions

View File

@@ -14,7 +14,8 @@
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
html, body {
html,
body {
margin: 0;
padding: 0;
overflow: hidden;

View File

@@ -1,24 +1,23 @@
import {StyledEngineProvider} from '@mui/material/styles';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {BrowserRouter, Route, Routes} from 'react-router-dom';
import './App.css';
import {BasketProvider} from './helper/BasketProvider';
import NavBar from './helper/navbar/NavBar';
import Account from './pages/Account';
import Contact from './pages/Contact';
import Home from './pages/Home';
import NoPage from './pages/NoPage';
import Orders from './pages/Orders';
import Payment from './pages/Payment';
import Product from './pages/Product';
import {CustomThemeProvider} from './theme/ThemeContext';
import FSComponents from './pages/FSComponents.tsx';
import AdminPanel from './pages/AdminPanel';
import {AccountProvider} from './helper/AccountProvider.tsx';
import {CookiesProvider} from 'react-cookie';
import { StyledEngineProvider } from "@mui/material/styles";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import "./App.css";
import { BasketProvider } from "./helper/BasketProvider";
import NavBar from "./helper/navbar/NavBar";
import Account from "./pages/Account";
import Contact from "./pages/Contact";
import Home from "./pages/Home";
import NoPage from "./pages/NoPage";
import Orders from "./pages/Orders";
import Payment from "./pages/Payment";
import Product from "./pages/Product";
import { CustomThemeProvider } from "./theme/ThemeContext";
import FSComponents from "./pages/FSComponents.tsx";
import AdminPanel from "./pages/AdminPanel";
import { AccountProvider } from "./helper/AccountProvider.tsx";
import { CookiesProvider } from "react-cookie";
export default function App() {
const queryClient = new QueryClient();
return (
@@ -29,17 +28,17 @@ export default function App() {
<AccountProvider>
<BasketProvider>
<BrowserRouter>
<NavBar/>
<NavBar />
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="*" element={<NoPage/>}/>
<Route path="/product/:id" element={<Product/>}/>
<Route path="/checkout" element={<Payment/>}/>
<Route path="/components" element={<FSComponents/>}/>
<Route path="/contact" element={<Contact/>}/>
<Route path='/account' element={<Account/>}/>
<Route path='/orders' element={<Orders/>}/>
<Route path='/admin' element={<AdminPanel/>}/>
<Route path="/" element={<Home />} />
<Route path="*" element={<NoPage />} />
<Route path="/product/:id" element={<Product />} />
<Route path="/checkout" element={<Payment />} />
<Route path="/components" element={<FSComponents />} />
<Route path="/contact" element={<Contact />} />
<Route path="/account" element={<Account />} />
<Route path="/orders" element={<Orders />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</BrowserRouter>
</BasketProvider>
@@ -48,5 +47,5 @@ export default function App() {
</CustomThemeProvider>
</StyledEngineProvider>
</QueryClientProvider>
)
);
}

View File

@@ -16,15 +16,14 @@ type AccountType = {
export default AccountType;
export type CustomerType =
{
export type CustomerType = {
id: number;
name: string;
surname: string;
address: string;
country: string;
zip: string;
}
};
export type SubmitLogin = {
email: string;
@@ -40,7 +39,7 @@ export type AdminAccountOperation = {
email: string;
uuid: string;
accountId: number;
}
};
export type User = {
password: string;

View File

@@ -1,9 +1,9 @@
export enum OrderStatusEnum {
ORDERED = 'ORDERED',
IN_PROGRESS = 'IN_PROGRESS',
ISSUES = 'ISSUES',
DELIVERED = 'DELIVERED',
CANCELLED = 'CANCELLED',
ORDERED = "ORDERED",
IN_PROGRESS = "IN_PROGRESS",
ISSUES = "ISSUES",
DELIVERED = "DELIVERED",
CANCELLED = "CANCELLED",
}
type OrderType = {
@@ -17,7 +17,6 @@ type OrderType = {
export default OrderType;
export type ShippingDetails = {
firstName: string;
lastName: string;

View File

@@ -1,5 +1,5 @@
import i18next from "i18next";
import {initReactI18next} from "react-i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import HttpBackend from "i18next-http-backend";
@@ -15,5 +15,5 @@ i18next
},
backend: {
loadPath: "/locales/{{lng}}/translation.json",
}
},
});

View File

@@ -1,10 +1,16 @@
import {createContext, ReactNode, useContext, useEffect, useState} from "react";
import {AccountContextType, User} from "../components/Account";
import {useCookies} from 'react-cookie';
import {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from "react";
import { AccountContextType, User } from "../components/Account";
import { useCookies } from "react-cookie";
const AccountContext = createContext<AccountContextType | undefined>(undefined);
export const AccountProvider = ({children}: { children: ReactNode }) => {
export const AccountProvider = ({ children }: { children: ReactNode }) => {
const [cookies, setCookie] = useCookies(["account"]);
const initialAccount =
@@ -22,11 +28,11 @@ export const AccountProvider = ({children}: { children: ReactNode }) => {
};
useEffect(() => {
setCookie("account", user, {path: "/", maxAge: 3600 * 24 * 7});
setCookie("account", user, { path: "/", maxAge: 3600 * 24 * 7 });
}, [user, setCookie]);
return (
<AccountContext.Provider value={{user, login, logout}}>
<AccountContext.Provider value={{ user, login, logout }}>
{children}
</AccountContext.Provider>
);

View File

@@ -1,6 +1,6 @@
import React, {createContext, useContext, useEffect, useState} from 'react';
import Item from '../components/Item';
import {useCookies} from 'react-cookie';
import React, { createContext, useContext, useEffect, useState } from "react";
import Item from "../components/Item";
import { useCookies } from "react-cookie";
export interface BasketItem {
item: Item;
@@ -15,23 +15,27 @@ interface BasketContextType {
const BasketContext = createContext<BasketContextType | undefined>(undefined);
export const BasketProvider: React.FC<{ children: React.ReactNode }> = ({children}) => {
export const BasketProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [cookies, setCookie] = useCookies(["basket"]);
const [basket, setBasket] = useState<BasketItem[]>(cookies.basket || []);
const addToBasket = (item: Item, quantity: number) => {
setBasket((prevBasket) => {
const existingItem = prevBasket.find((basketItem) => basketItem.item.uuid === item.uuid);
const existingItem = prevBasket.find(
(basketItem) => basketItem.item.uuid === item.uuid,
);
if (existingItem) {
// Update quantity if item already exists
return prevBasket.map((basketItem) =>
basketItem.item.uuid === item.uuid
? {...basketItem, quantity: basketItem.quantity + quantity}
: basketItem
? { ...basketItem, quantity: basketItem.quantity + quantity }
: basketItem,
);
}
// Add new item to basket
return [...prevBasket, {item, quantity}];
return [...prevBasket, { item, quantity }];
});
};
@@ -40,11 +44,11 @@ export const BasketProvider: React.FC<{ children: React.ReactNode }> = ({childre
};
useEffect(() => {
setCookie("basket", basket, {path: "/", maxAge: 3600 * 24 * 7}); // 7 Tage
setCookie("basket", basket, { path: "/", maxAge: 3600 * 24 * 7 }); // 7 Tage
}, [basket, setCookie]);
return (
<BasketContext.Provider value={{basket, addToBasket, clearBasket}}>
<BasketContext.Provider value={{ basket, addToBasket, clearBasket }}>
{children}
</BasketContext.Provider>
);
@@ -54,7 +58,7 @@ export const BasketProvider: React.FC<{ children: React.ReactNode }> = ({childre
export const useBasket = () => {
const context = useContext(BasketContext);
if (!context) {
throw new Error('useBasket must be used within a BasketProvider');
throw new Error("useBasket must be used within a BasketProvider");
}
return context;
};

View File

@@ -1,18 +1,30 @@
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import {Box, Button, IconButton, Toolbar, useTheme} from "@mui/material";
import {DataGrid, GridColDef, GridRowId, GridRowSelectionModel} from "@mui/x-data-grid";
import {useMutation, useQuery} from "@tanstack/react-query";
import {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import AccountType, {AdminAccountOperation, CustomerType} from "../../components/Account";
import {useAccount} from "../AccountProvider";
import {deleteAccountAdmin, fetchAccounts, updateAccountAdmin} from "../query/Queries";
import { Box, Button, IconButton, Toolbar, useTheme } from "@mui/material";
import {
DataGrid,
GridColDef,
GridRowId,
GridRowSelectionModel,
} from "@mui/x-data-grid";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import AccountType, {
AdminAccountOperation,
CustomerType,
} from "../../components/Account";
import { useAccount } from "../AccountProvider";
import {
deleteAccountAdmin,
fetchAccounts,
updateAccountAdmin,
} from "../query/Queries";
import CustomerEditDialog from "./CustomerEditDialog";
export default function AccountsInfo() {
const theme = useTheme();
const {t} = useTranslation();
const { t } = useTranslation();
const [customerData, setCustomerData] = useState<CustomerType>({
id: 0,
@@ -20,7 +32,7 @@ export default function AccountsInfo() {
surname: "",
address: "",
zip: "",
country: ""
country: "",
});
async function handleCustomerEdit(account: AccountType) {
@@ -31,24 +43,28 @@ export default function AccountsInfo() {
const [rows, setRows] = useState<AccountType[]>([]);
const [selectedRows, setSelectedRows] = useState<Set<GridRowId>>(new Set());
const {user: loginData} = useAccount();
const { user: loginData } = useAccount();
const {data, refetch} = useQuery({
const { data, refetch } = useQuery({
queryKey: ["fetchAccounts", loginData],
queryFn: () => fetchAccounts(loginData ? loginData : {
queryFn: () =>
fetchAccounts(
loginData
? loginData
: {
email: "",
password: "",
session: "",
customerId: -1,
isAdmin: false
}),
isAdmin: false,
},
),
retry: 3,
retryDelay: 1000,
});
const deleteAccount = useMutation({
mutationFn: (user: AdminAccountOperation) =>
deleteAccountAdmin(user),
mutationFn: (user: AdminAccountOperation) => deleteAccountAdmin(user),
});
useEffect(() => {
@@ -63,60 +79,70 @@ export default function AccountsInfo() {
const handleDeleteSelected = async () => {
selectedRows.forEach(async (rowId) => {
let id = rows.find((row) => row.id === rowId)?.id
if(id === undefined) id = -1;
let id = rows.find((row) => row.id === rowId)?.id;
if (id === undefined) id = -1;
await deleteAccount.mutateAsync({
email: loginData?.email || '',
uuid: loginData?.session || '',
email: loginData?.email || "",
uuid: loginData?.session || "",
accountId: id,
});
})
});
setRows(rows.filter((row) => !selectedRows.has(row.id)));
};
const updateAdmin = useMutation({
mutationFn: (account: AccountType) =>
updateAccountAdmin(account, loginData ? loginData : {
updateAccountAdmin(
account,
loginData
? loginData
: {
email: "",
password: "",
session: "",
customerId: -1,
isAdmin: false
}),
isAdmin: false,
},
),
});
const columns: GridColDef<(typeof rows)[number]>[] = [
{field: 'id', headerName: 'ID', width: 60},
{ field: "id", headerName: "ID", width: 60 },
{
field: 'admin',
headerName: t('admin'),
field: "admin",
headerName: t("admin"),
type: "boolean",
width: 90,
editable: true
editable: true,
},
{
field: 'email',
headerName: t('email'),
field: "email",
headerName: t("email"),
width: 150,
editable: false,
},
{
field: 'langI18n',
headerName: t('language'),
field: "langI18n",
headerName: t("language"),
width: 150,
editable: false,
},
{ //edit billing information button
{
//edit billing information button
field: "customer",
headerName: t('address'),
headerName: t("address"),
width: 90,
sortable: false,
disableReorder: true,
disableColumnMenu: true,
renderCell: params => <IconButton onClick={() => handleCustomerEdit(params.row)}> <EditIcon/> </IconButton>,
}
renderCell: (params) => (
<IconButton onClick={() => handleCustomerEdit(params.row)}>
{" "}
<EditIcon />{" "}
</IconButton>
),
},
];
const [open, setOpen] = useState(false);
@@ -132,7 +158,7 @@ export default function AccountsInfo() {
className="page-table"
sx={{
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.secondary
color: theme.palette.text.secondary,
}}
>
<DataGrid
@@ -148,25 +174,27 @@ export default function AccountsInfo() {
<Button
variant="contained"
color="error"
startIcon={<DeleteIcon/>}
startIcon={<DeleteIcon />}
onClick={handleDeleteSelected}
disabled={selectedRows.size === 0}
sx={{
marginRight: 1
marginRight: 1,
}}
>
{t('deleteAccount')}
{t("deleteAccount")}
</Button>
</Toolbar>
)
),
}}
showToolbar
processRowUpdate={async (updatedRow) => {
const originalRow = rows.find(row => row.id === updatedRow.id);
const originalRow = rows.find((row) => row.id === updatedRow.id);
if (originalRow && originalRow.admin !== updatedRow.admin) {
await updateAdmin.mutateAsync(updatedRow);
}
setRows(rows.map(row => row.id === updatedRow.id ? updatedRow : row));
setRows(
rows.map((row) => (row.id === updatedRow.id ? updatedRow : row)),
);
return updatedRow;
}}
/>

View File

@@ -1,9 +1,16 @@
import {Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField} from "@mui/material";
import {useMutation} from "@tanstack/react-query";
import React, {useEffect} from "react";
import {useTranslation} from "react-i18next";
import {CustomerType} from "../../components/Account";
import {updateCustomer} from "../query/Queries"; // Importiere die Funktion für die Registrierung
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
} from "@mui/material";
import { useMutation } from "@tanstack/react-query";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { CustomerType } from "../../components/Account";
import { updateCustomer } from "../query/Queries"; // Importiere die Funktion für die Registrierung
type CustomerDialogProps = {
open: boolean;
@@ -18,10 +25,9 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({
onClose,
customerData,
setCustomerData,
onSubmit
}) => {
const {t} = useTranslation();
onSubmit,
}) => {
const { t } = useTranslation();
useEffect(() => {
if (open) {
@@ -33,8 +39,7 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({
}, [open]);
const changeCustomer = useMutation({
mutationFn: (customer: CustomerType) =>
updateCustomer(customer),
mutationFn: (customer: CustomerType) => updateCustomer(customer),
});
const handleSave = async () => {
@@ -49,11 +54,11 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({
await changeCustomer.mutateAsync(customer);
onSubmit(); // Rufe die onSubmit-Funktion auf, um den Dialog zu schließen und die Änderungen zu übernehmen
onClose(); // Schließe den Dialog
}
};
return (
<Dialog open={open} onClose={onClose} disableEnforceFocus>
<DialogTitle>{t('changeCustomer')}</DialogTitle>
<DialogTitle>{t("changeCustomer")}</DialogTitle>
<DialogContent>
<TextField
margin="dense"
@@ -61,7 +66,9 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({
type="text"
fullWidth
value={customerData.name}
onChange={e => setCustomerData(prev => ({...prev, name: e.target.value}))}
onChange={(e) =>
setCustomerData((prev) => ({ ...prev, name: e.target.value }))
}
/>
<TextField
margin="dense"
@@ -69,7 +76,9 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({
type="text"
fullWidth
value={customerData.surname}
onChange={e => setCustomerData(prev => ({...prev, surname: e.target.value}))}
onChange={(e) =>
setCustomerData((prev) => ({ ...prev, surname: e.target.value }))
}
/>
<TextField
margin="dense"
@@ -77,7 +86,9 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({
type="text"
fullWidth
value={customerData.address}
onChange={e => setCustomerData(prev => ({...prev, address: e.target.value}))}
onChange={(e) =>
setCustomerData((prev) => ({ ...prev, address: e.target.value }))
}
/>
<TextField
margin="dense"
@@ -85,7 +96,9 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({
type="text"
fullWidth
value={customerData.zip}
onChange={e => setCustomerData(prev => ({...prev, zip: e.target.value}))}
onChange={(e) =>
setCustomerData((prev) => ({ ...prev, zip: e.target.value }))
}
/>
<TextField
margin="dense"
@@ -93,7 +106,9 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({
type="text"
fullWidth
value={customerData.country}
onChange={e => setCustomerData(prev => ({...prev, country: e.target.value}))}
onChange={(e) =>
setCustomerData((prev) => ({ ...prev, country: e.target.value }))
}
/>
</DialogContent>
<DialogActions>

View File

@@ -1,4 +1,4 @@
import React, {useState} from 'react';
import React, { useState } from "react";
import {
Alert,
Box,
@@ -10,11 +10,11 @@ import {
DialogTitle,
IconButton,
Typography,
} from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CloseIcon from '@mui/icons-material/Close';
import {useTranslation} from 'react-i18next';
import {Item} from '../../components/Item.tsx';
} from "@mui/material";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import CloseIcon from "@mui/icons-material/Close";
import { useTranslation } from "react-i18next";
import { Item } from "../../components/Item.tsx";
interface ItemImageDialogProps {
open: boolean;
@@ -24,8 +24,14 @@ interface ItemImageDialogProps {
isFarmStationImage: boolean;
}
export default function ItemImageDialog({open, onClose, item, onSuccess, isFarmStationImage}: ItemImageDialogProps) {
const {t} = useTranslation();
export default function ItemImageDialog({
open,
onClose,
item,
onSuccess,
isFarmStationImage,
}: ItemImageDialogProps) {
const { t } = useTranslation();
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
@@ -36,14 +42,14 @@ export default function ItemImageDialog({open, onClose, item, onSuccess, isFarmS
const file = event.target.files?.[0];
if (file) {
// Validate file type
if (!file.type.startsWith('image/')) {
setError('Please select a valid image file');
if (!file.type.startsWith("image/")) {
setError("Please select a valid image file");
return;
}
// Validate file size (5MB limit)
if (file.size > 5 * 1024 * 1024) {
setError('File size must be less than 5MB');
setError("File size must be less than 5MB");
return;
}
@@ -65,7 +71,7 @@ export default function ItemImageDialog({open, onClose, item, onSuccess, isFarmS
reader.onload = () => {
const result = reader.result as string;
// Remove the data URL prefix (e.g., "data:image/jpeg;base64,")
const base64 = result.split(',')[1];
const base64 = result.split(",")[1];
resolve(base64);
};
reader.onerror = reject;
@@ -75,7 +81,7 @@ export default function ItemImageDialog({open, onClose, item, onSuccess, isFarmS
const handleUpload = async () => {
if (!selectedFile) {
setError('Please select an image file');
setError("Please select an image file");
return;
}
@@ -85,16 +91,23 @@ export default function ItemImageDialog({open, onClose, item, onSuccess, isFarmS
try {
const base64Image = await convertFileToBase64(selectedFile);
const response = await fetch((isFarmStationImage ? 'http://localhost:8085/farm' : 'http://localhost:8085/image') + '?uuid=' + item.uuid, {
method: 'PUT',
const response = await fetch(
(isFarmStationImage
? "http://localhost:8085/farm"
: "http://localhost:8085/image") +
"?uuid=" +
item.uuid,
{
method: "PUT",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: base64Image,
});
},
);
if (!response.ok) {
console.error('Failed to upload image:', await response.text());
console.error("Failed to upload image:", await response.text());
}
setSuccess(true);
@@ -104,9 +117,8 @@ export default function ItemImageDialog({open, onClose, item, onSuccess, isFarmS
setTimeout(() => {
handleClose();
}, 1500);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to upload image');
setError(err instanceof Error ? err.message : "Failed to upload image");
} finally {
setLoading(false);
}
@@ -122,42 +134,45 @@ export default function ItemImageDialog({open, onClose, item, onSuccess, isFarmS
};
return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<DialogTitle sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
{t('uploadImage')} - {item.name}
{t("uploadImage")} - {item.name}
<IconButton onClick={handleClose} size="small">
<CloseIcon/>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2, py: 1}}>
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, py: 1 }}>
{/* Item Info */}
<Box>
<Typography variant="body2" color="textSecondary">
{t('item')}: {item.name}
{t("item")}: {item.name}
</Typography>
<Typography variant="body2" color="textSecondary">
UUID: {item.uuid}
</Typography>
<Typography variant="body2" color="error">
{t('imageUploadNotice')}
{t("imageUploadNotice")}
</Typography>
{isFarmStationImage && <Typography variant="body2" color="error">
{t('imageUploadNoticeFs')}
</Typography>}
{isFarmStationImage && (
<Typography variant="body2" color="error">
{t("imageUploadNoticeFs")}
</Typography>
)}
</Box>
{/* File Upload */}
<Box>
<input
accept="image/*"
style={{display: 'none'}}
style={{ display: "none" }}
id="image-upload"
type="file"
onChange={handleFileSelect}
@@ -166,31 +181,32 @@ export default function ItemImageDialog({open, onClose, item, onSuccess, isFarmS
<Button
variant="outlined"
component="span"
startIcon={<CloudUploadIcon/>}
startIcon={<CloudUploadIcon />}
fullWidth
sx={{mb: 2}}
sx={{ mb: 2 }}
>
{selectedFile ? selectedFile.name : t('selectImage')}
{selectedFile ? selectedFile.name : t("selectImage")}
</Button>
</label>
</Box>
{/* Image Preview */}
{preview && (
<Box sx={{textAlign: 'center'}}>
<Box sx={{ textAlign: "center" }}>
<img
src={preview}
alt="Preview"
style={{
maxWidth: '100%',
maxHeight: '300px',
objectFit: 'contain',
border: '1px solid #ddd',
borderRadius: '4px',
maxWidth: "100%",
maxHeight: "300px",
objectFit: "contain",
border: "1px solid #ddd",
borderRadius: "4px",
}}
/>
<Typography variant="caption" display="block" sx={{mt: 1}}>
{selectedFile?.name} ({(selectedFile?.size || 0 / 1024).toFixed(1)} KB)
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
{selectedFile?.name} (
{(selectedFile?.size || 0 / 1024).toFixed(1)} KB)
</Typography>
</Box>
)}
@@ -204,24 +220,22 @@ export default function ItemImageDialog({open, onClose, item, onSuccess, isFarmS
{/* Success Message */}
{success && (
<Alert severity="success">
{t('imageUploadedSuccessfully')}
</Alert>
<Alert severity="success">{t("imageUploadedSuccessfully")}</Alert>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={loading}>
{t('cancel')}
{t("cancel")}
</Button>
<Button
onClick={handleUpload}
variant="contained"
disabled={!selectedFile || loading}
startIcon={loading ? <CircularProgress size={20}/> : undefined}
startIcon={loading ? <CircularProgress size={20} /> : undefined}
>
{loading ? t('uploading') : t('upload')}
{loading ? t("uploading") : t("upload")}
</Button>
</DialogActions>
</Dialog>

View File

@@ -3,14 +3,23 @@ import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import { Box, Button, IconButton, Toolbar, useTheme } from "@mui/material";
import { Gauge, gaugeClasses } from "@mui/x-charts";
import { DataGrid, GridColDef, GridRowId, GridRowSelectionModel } from "@mui/x-data-grid";
import {
DataGrid,
GridColDef,
GridRowId,
GridRowSelectionModel,
} from "@mui/x-data-grid";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Item from "../../components/Item";
import { mapValueToColor } from "../../util/ColorUtil.tsx";
import { useAccount } from "../AccountProvider.tsx";
import { deleteItemQuery, fetchItems, updateItemAdmin } from "../query/Queries.tsx";
import {
deleteItemQuery,
fetchItems,
updateItemAdmin,
} from "../query/Queries.tsx";
import ItemImageDialog from "./ItemImageDialog.tsx";
import NewItemDialog from "./NewItemDialog.tsx";
@@ -26,7 +35,6 @@ export default function ItemsInfo() {
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
const [isFarmStationImage, setIsFarmStationImage] = useState(false);
function handleImageEdit(item: Item) {
setIsFarmStationImage(false);
setSelectedItem(item);
@@ -41,7 +49,6 @@ export default function ItemsInfo() {
console.log("IconEdit", item);
}
function handleAddItem() {
setNewItemDialog(true);
}
@@ -66,127 +73,158 @@ export default function ItemsInfo() {
};
const deleteItem = useMutation({
mutationFn: (uuid: string) =>
deleteItemQuery(uuid),
mutationFn: (uuid: string) => deleteItemQuery(uuid),
});
const handleDeleteSelected = async () => {
await Promise.all(
Array.from(selectedRows).map(async (row) => {
await deleteItem.mutateAsync(rows.find(item => item.id === row)?.uuid || "");
})
await deleteItem.mutateAsync(
rows.find((item) => item.id === row)?.uuid || "",
);
}),
);
setRows(rows.filter((row) => !selectedRows.has(row.id)));
};
const updateItem = useMutation({
mutationFn: (item: Item) =>
updateItemAdmin(item),
mutationFn: (item: Item) => updateItemAdmin(item),
});
const handleRowUpdate = async (updatedRow: Item) => {
setRows(rows.map(row => row.id === updatedRow.id ? updatedRow : row));
setRows(rows.map((row) => (row.id === updatedRow.id ? updatedRow : row)));
await updateItem.mutateAsync(updatedRow);
return updatedRow;
}
};
const columns: GridColDef<(typeof rows)[number]>[] = [
{ field: 'id', headerName: 'ID', width: 60 },
{ field: "id", headerName: "ID", width: 60 },
{
field: 'uuid',
headerName: t('uuid'),
field: "uuid",
headerName: t("uuid"),
type: "string",
width: 120,
editable: false
editable: false,
},
{
field: 'name',
headerName: t('name'),
field: "name",
headerName: t("name"),
width: 200,
editable: true,
},
{
field: 'category',
headerName: t('category'),
field: "category",
headerName: t("category"),
width: 150,
editable: true,
valueFormatter: (val) => t(val),
},
{
field: 'description',
headerName: t('description'),
field: "description",
headerName: t("description"),
width: 150,
editable: true,
},
{
field: 'price100',
headerName: t('price100€'),
field: "price100",
headerName: t("price100€"),
width: 100,
editable: true,
type: 'number',
type: "number",
valueFormatter: (val) => (val / 100).toFixed(2),
},
{
field: 'discount100',
headerName: t('discount100'),
field: "discount100",
headerName: t("discount100"),
width: 120,
editable: true,
type: 'number'
type: "number",
},
{
field: 'stock',
headerName: t('stock'),
field: "stock",
headerName: t("stock"),
width: 100,
editable: true,
type: 'number',
renderCell: params => <Gauge value={Math.min(params.row.stock, params.row.stockExpected)} valueMin={0}
valueMax={params.row.stockExpected} startAngle={-90} endAngle={90} sx={{
type: "number",
renderCell: (params) => (
<Gauge
value={Math.min(params.row.stock, params.row.stockExpected)}
valueMin={0}
valueMax={params.row.stockExpected}
startAngle={-90}
endAngle={90}
sx={{
[`& .${gaugeClasses.valueArc}`]: {
fill: () => {
return mapValueToColor(0, params.row.stockExpected, params.row.stock)
return mapValueToColor(
0,
params.row.stockExpected,
params.row.stock,
);
},
},
}} text={() => `${params.row.stock}`} />
}}
text={() => `${params.row.stock}`}
/>
),
},
{
field: 'rating',
headerName: t('rating'),
field: "rating",
headerName: t("rating"),
width: 100,
editable: false, //the rating is averaged from ratings
type: 'number',
renderCell: params => <Gauge value={Math.min(params.row.rating, 10)} valueMin={0} valueMax={10}
startAngle={-90} endAngle={90} sx={{
type: "number",
renderCell: (params) => (
<Gauge
value={Math.min(params.row.rating, 10)}
valueMin={0}
valueMax={10}
startAngle={-90}
endAngle={90}
sx={{
[`& .${gaugeClasses.valueArc}`]: {
fill: () => {
return mapValueToColor(0, 10, params.row.rating)
return mapValueToColor(0, 10, params.row.rating);
},
},
}} text={() => `${params.row.rating.toFixed(2)}`} />
}}
text={() => `${params.row.rating.toFixed(2)}`}
/>
),
},
{
field: "actualPrice",
headerName: t('actualPrice'),
headerName: t("actualPrice"),
width: 90,
editable: false,
valueGetter: (_, row) => (row.price100 / 100 * ((100 - row.discount100) / 100)).toFixed(2)
valueGetter: (_, row) =>
((row.price100 / 100) * ((100 - row.discount100) / 100)).toFixed(2),
},
{
field: 'images',
headerName: t('images'),
field: "images",
headerName: t("images"),
width: 90,
editable: false,
renderCell: params => <IconButton onClick={() => handleImageEdit(params.row)}> <EditIcon /> </IconButton>,
renderCell: (params) => (
<IconButton onClick={() => handleImageEdit(params.row)}>
{" "}
<EditIcon />{" "}
</IconButton>
),
},
{
field: 'farmImage',
headerName: t('fsImage'),
field: "farmImage",
headerName: t("fsImage"),
width: 90,
editable: false,
renderCell: params => <IconButton onClick={() => handleFarmImageEdit(params.row)}> <EditIcon />
</IconButton>,
}
renderCell: (params) => (
<IconButton onClick={() => handleFarmImageEdit(params.row)}>
{" "}
<EditIcon />
</IconButton>
),
},
];
return (
@@ -194,7 +232,7 @@ export default function ItemsInfo() {
className="page-table"
sx={{
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.secondary
color: theme.palette.text.secondary,
}}
>
<DataGrid
@@ -214,24 +252,24 @@ export default function ItemsInfo() {
onClick={handleDeleteSelected}
disabled={selectedRows.size === 0}
sx={{
marginRight: 1
marginRight: 1,
}}
>
{t('deleteProduct')}
{t("deleteProduct")}
</Button>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon/>}
startIcon={<AddIcon />}
onClick={handleAddItem}
sx={{
marginRight: 1
marginRight: 1,
}}
>
{t('addProduct')}
{t("addProduct")}
</Button>
</Toolbar>
)
),
}}
showToolbar
processRowUpdate={handleRowUpdate}
@@ -243,7 +281,7 @@ export default function ItemsInfo() {
item={selectedItem}
onSuccess={() => {
// Refresh data or update UI
console.log('Image uploaded successfully');
console.log("Image uploaded successfully");
}}
isFarmStationImage={isFarmStationImage}
/>

View File

@@ -1,4 +1,4 @@
import CloseIcon from '@mui/icons-material/Close';
import CloseIcon from "@mui/icons-material/Close";
import {
Alert,
Box,
@@ -8,21 +8,21 @@ import {
DialogContent,
DialogTitle,
IconButton,
TextField
} from '@mui/material';
import {useMutation} from '@tanstack/react-query';
import {useState} from 'react';
import {useTranslation} from 'react-i18next';
import {Item} from '../../components/Item';
import {submitItem} from '../query/Queries';
TextField,
} from "@mui/material";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Item } from "../../components/Item";
import { submitItem } from "../query/Queries";
interface NewItemDialogProps {
open: boolean;
onClose: () => void;
}
export default function NewItemDialog({open, onClose}: NewItemDialogProps) {
const {t} = useTranslation();
export default function NewItemDialog({ open, onClose }: NewItemDialogProps) {
const { t } = useTranslation();
const [item, setItem] = useState<Item>({
id: 0,
uuid: "",
@@ -47,12 +47,11 @@ export default function NewItemDialog({open, onClose}: NewItemDialogProps) {
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setItem({...item, [e.target.name]: e.target.value});
setItem({ ...item, [e.target.name]: e.target.value });
};
const saveItem = useMutation({
mutationFn: (item: Item) =>
submitItem(item),
mutationFn: (item: Item) => submitItem(item),
});
const handleSave = async () => {
@@ -60,28 +59,29 @@ export default function NewItemDialog({open, onClose}: NewItemDialogProps) {
setError(null);
setSuccess(false);
await saveItem.mutateAsync(item)
await saveItem.mutateAsync(item);
onClose(); // Close the dialog after saving
setLoading(false);
};
return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="sm"
fullWidth
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<DialogTitle sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
{t('createNewItem')}
{t("createNewItem")}
<IconButton onClick={handleClose} size="small">
<CloseIcon/>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{display: 'flex', flexDirection: 'column', gap: 2, py: 1}}>
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, py: 1 }}>
{/* Name, Kategorie, Beschreibung, Preis, Rabatt, Bestand, Bestand erwartet */}
<TextField
label={t("name")}
@@ -110,7 +110,7 @@ export default function NewItemDialog({open, onClose}: NewItemDialogProps) {
value={item.price100}
onChange={handleChange}
fullWidth
type='number'
type="number"
/>
<TextField
label={t("discount100")}
@@ -118,7 +118,7 @@ export default function NewItemDialog({open, onClose}: NewItemDialogProps) {
value={item.discount100}
onChange={handleChange}
fullWidth
type='number'
type="number"
/>
<TextField
label={t("stockExpected")}
@@ -126,7 +126,7 @@ export default function NewItemDialog({open, onClose}: NewItemDialogProps) {
value={item.stockExpected}
onChange={handleChange}
fullWidth
type='number'
type="number"
/>
<TextField
label={t("stock")}
@@ -134,7 +134,7 @@ export default function NewItemDialog({open, onClose}: NewItemDialogProps) {
value={item.stock}
onChange={handleChange}
fullWidth
type='number'
type="number"
/>
{/* Error Message */}
@@ -146,19 +146,17 @@ export default function NewItemDialog({open, onClose}: NewItemDialogProps) {
{/* Success Message */}
{success && (
<Alert severity="success">
{t('itemCreatedSuccessfully')}
</Alert>
<Alert severity="success">{t("itemCreatedSuccessfully")}</Alert>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleSave} disabled={loading}>
{t('save')}
{t("save")}
</Button>
<Button onClick={handleClose} disabled={loading}>
{t('cancel')}
{t("cancel")}
</Button>
</DialogActions>
</Dialog>

View File

@@ -12,16 +12,16 @@ import {
Snackbar,
Stack,
Typography,
useTheme
useTheme,
} from "@mui/material";
import {useMutation, useQuery} from "@tanstack/react-query";
import React, {PropsWithChildren, useState} from "react";
import {DndProvider, useDrag, useDrop} from 'react-dnd';
import {HTML5Backend} from 'react-dnd-html5-backend';
import {useTranslation} from "react-i18next";
import OrderType, {OrderPatch, OrderStatusEnum} from "../../components/Order";
import {useAccount} from "../AccountProvider";
import {fetchOrdersAdmin, orderPatch} from "../query/Queries";
import { useMutation, useQuery } from "@tanstack/react-query";
import React, { PropsWithChildren, useState } from "react";
import { DndProvider, useDrag, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { useTranslation } from "react-i18next";
import OrderType, { OrderPatch, OrderStatusEnum } from "../../components/Order";
import { useAccount } from "../AccountProvider";
import { fetchOrdersAdmin, orderPatch } from "../query/Queries";
// The order in which the statuses are displayed
const statusOrder: OrderStatusEnum[] = [
@@ -29,36 +29,40 @@ const statusOrder: OrderStatusEnum[] = [
OrderStatusEnum.ISSUES,
OrderStatusEnum.ORDERED,
OrderStatusEnum.IN_PROGRESS,
OrderStatusEnum.DELIVERED
OrderStatusEnum.DELIVERED,
];
const OrderCard: React.FC<{ order: OrderType; onClick: () => void }> = ({
order,
onClick,
}) => {
const { t } = useTranslation();
const OrderCard: React.FC<{ order: OrderType; onClick: () => void }> = ({order, onClick}) => {
const {t} = useTranslation();
const [{isDragging}, drag] = useDrag(() => ({
type: 'order',
item: {id: order.id},
const [{ isDragging }, drag] = useDrag(() => ({
type: "order",
item: { id: order.id },
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
}));
return (
<div ref={drag} style={{opacity: isDragging ? 0.5 : 1, marginBottom: 8}}>
<Card elevation={4} sx={{
m: 1
}}>
<div ref={drag} style={{ opacity: isDragging ? 0.5 : 1, marginBottom: 8 }}>
<Card
elevation={4}
sx={{
m: 1,
}}
>
<CardContent onClick={onClick}>
<Typography gutterBottom variant="h5" component="div">
Order: {order.id}
</Typography>
<Typography>
{t('date') + ": " + new Date(order.time).toUTCString()}
{t("date") + ": " + new Date(order.time).toUTCString()}
</Typography>
<Typography>
{t('total') + ": " + (order.total / 100).toFixed(2) + " €"}
{t("total") + ": " + (order.total / 100).toFixed(2) + " €"}
</Typography>
</CardContent>
</Card>
@@ -66,16 +70,17 @@ const OrderCard: React.FC<{ order: OrderType; onClick: () => void }> = ({order,
);
};
const Column: React.FC<PropsWithChildren<{
const Column: React.FC<
PropsWithChildren<{
status: OrderStatusEnum;
onDrop: (id: number, status: OrderStatusEnum) => void
}>> = ({status, onDrop, children}) => {
const {t} = useTranslation();
onDrop: (id: number, status: OrderStatusEnum) => void;
}>
> = ({ status, onDrop, children }) => {
const { t } = useTranslation();
const theme = useTheme();
const [{isOver}, drop] = useDrop(() => ({
accept: 'order',
const [{ isOver }, drop] = useDrop(() => ({
accept: "order",
drop: (item: { id: number }) => onDrop(item.id, status),
collect: (monitor) => ({
isOver: !!monitor.isOver(),
@@ -83,58 +88,61 @@ const Column: React.FC<PropsWithChildren<{
}));
return (
<div ref={drop} style={{
<div
ref={drop}
style={{
flex: 1,
backgroundColor: isOver ? theme.palette.background.paper : 'transparent',
backgroundColor: isOver
? theme.palette.background.paper
: "transparent",
minWidth: 300,
maxWidth: 400
}}>
<Card sx={{
minHeight: '100%',
maxWidth: 400,
}}
>
<Card
sx={{
minHeight: "100%",
marginTop: 2,
marginLeft: 1,
height: '80vh',
display: 'flex',
flexDirection: 'column'
}} elevation={1}>
<CardContent sx={{
height: "80vh",
display: "flex",
flexDirection: "column",
}}
elevation={1}
>
<CardContent
sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
overflowY: 'auto',
height: '100%',
}}>
display: "flex",
flexDirection: "column",
overflowY: "auto",
height: "100%",
}}
>
<Typography variant="h6">{t(status)}</Typography>
<div style={{flex: 1, overflowY: 'auto'}}>
{children}
</div>
<div style={{ flex: 1, overflowY: "auto" }}>{children}</div>
</CardContent>
</Card>
</div>
);
};
const EditOrder: React.FC<{ open: boolean; order: OrderType | null; onClose: () => void }> = ({
open,
order,
onClose
}) => {
const {t} = useTranslation();
if (order === null)
return "";
const EditOrder: React.FC<{
open: boolean;
order: OrderType | null;
onClose: () => void;
}> = ({ open, order, onClose }) => {
const { t } = useTranslation();
if (order === null) return "";
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>
{`${t(order.status)} #${order?.id}`}
</DialogTitle>
<DialogTitle>{`${t(order.status)} #${order?.id}`}</DialogTitle>
<DialogContent dividers>
{order && (
<Stack spacing={2}>
<Typography
variant="subtitle1">{`${t('orderDate')}: ${new Date(order.time).toDateString()}`}</Typography>
<Divider/>
<Typography variant="subtitle2">{t('orderedItems')}:</Typography>
<Typography variant="subtitle1">{`${t("orderDate")}: ${new Date(order.time).toDateString()}`}</Typography>
<Divider />
<Typography variant="subtitle2">{t("orderedItems")}:</Typography>
<List dense>
{order.orderItems.map((item, idx) => (
<ListItemText
@@ -143,13 +151,13 @@ const EditOrder: React.FC<{ open: boolean; order: OrderType | null; onClose: ()
/>
))}
</List>
<Divider/>
<Typography variant="h6">{`${t('sum')}: ${(order.total / 100).toFixed(2)}`}</Typography>
<Divider />
<Typography variant="h6">{`${t("sum")}: ${(order.total / 100).toFixed(2)}`}</Typography>
</Stack>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{t('close')}</Button>
<Button onClick={onClose}>{t("close")}</Button>
</DialogActions>
</Dialog>
);
@@ -159,29 +167,32 @@ export default function OrdersInfo() {
const [editOrder, setEditOrder] = useState<OrderType | null>(null);
const [openSnackbar, setOpenSnackbar] = useState(false);
const {user: loginData} = useAccount();
const { user: loginData } = useAccount();
const {data, refetch, isLoading} = useQuery({
const { data, refetch, isLoading } = useQuery({
queryKey: ["fetchOrdersAdmin", loginData],
queryFn: () => fetchOrdersAdmin(loginData ? loginData : {
queryFn: () =>
fetchOrdersAdmin(
loginData
? loginData
: {
email: "",
password: "",
session: "",
customerId: -1,
isAdmin: false
}),
isAdmin: false,
},
),
retry: 3,
retryDelay: 1000,
});
const patchOrderMutation = useMutation({
mutationFn: (order: OrderPatch) =>
orderPatch({id: order.id, status: order.status}),
orderPatch({ id: order.id, status: order.status }),
});
const handleDrop = async (id: number, status: OrderStatusEnum) => {
const currentOrders = data ?? [];
const obj = currentOrders.find((o) => o.id === id);
if (!obj) {
@@ -189,7 +200,7 @@ export default function OrdersInfo() {
return;
}
try {
await patchOrderMutation.mutateAsync({id: obj.id, status: status});
await patchOrderMutation.mutateAsync({ id: obj.id, status: status });
await refetch();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
@@ -204,13 +215,13 @@ export default function OrdersInfo() {
return (
<DndProvider backend={HTML5Backend}>
<div style={{display: 'flex', gap: 10, minHeight: '90%'}}>
<div style={{ display: "flex", gap: 10, minHeight: "90%" }}>
{statusOrder.map((status) => (
<Column key={status} status={status} onDrop={handleDrop}>
{data
.filter((o) => o.status === status)
.map((o) => (
<OrderCard key={o.id} order={o} onClick={() => handleEdit(o)}/>
<OrderCard key={o.id} order={o} onClick={() => handleEdit(o)} />
))}
</Column>
))}

View File

@@ -1,160 +1,190 @@
import {Box, Typography, useTheme} from "@mui/material";
import {BarChart} from '@mui/x-charts/BarChart';
import {PieChart} from '@mui/x-charts/PieChart';
import {BarSeriesType} from '@mui/x-charts'
import {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {useQuery} from "@tanstack/react-query";
import {fetchOrderStatus, fetchStatisticsRevenue, fetchStatisticsVolume, fetchStockPercent} from "../query/Queries.tsx";
import {useAccount} from "../AccountProvider.tsx";
import {getColorFromPercent} from "../../util/ColorUtil.tsx";
import { Box, Typography, useTheme } from "@mui/material";
import { BarChart } from "@mui/x-charts/BarChart";
import { PieChart } from "@mui/x-charts/PieChart";
import { BarSeriesType } from "@mui/x-charts";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query";
import {
fetchOrderStatus,
fetchStatisticsRevenue,
fetchStatisticsVolume,
fetchStockPercent,
} from "../query/Queries.tsx";
import { useAccount } from "../AccountProvider.tsx";
import { getColorFromPercent } from "../../util/ColorUtil.tsx";
export default function StatisticsInfo() {
const theme = useTheme();
const {t} = useTranslation();
const { t } = useTranslation();
const [monthlyVolume, setMonthlyVolume] = useState<BarSeriesType[]>([]);
const [monthlyVolumeXaxis, setMonthlyVolumeXaxis] = useState([{data: []}]);
const [monthlyVolumeXaxis, setMonthlyVolumeXaxis] = useState([{ data: [] }]);
const [totalVolume, setTotalVolume] = useState([]);
const [monthlyRevenue, setMonthlyRevenue] = useState<BarSeriesType[]>([]);
const [monthlyRevenueXaxis, setMonthlyRevenueXaxis] = useState([{data: []}]);
const [monthlyRevenueXaxis, setMonthlyRevenueXaxis] = useState([
{ data: [] },
]);
const [totalRevenue, setTotalRevenue] = useState([]);
const [orderStatus, setOrderStatus] = useState([]);
const [stockPercent, setStockPercent] = useState([]);
const { user: loginData } = useAccount();
const {user: loginData} = useAccount();
const {data: dataVolume} = useQuery({
const { data: dataVolume } = useQuery({
queryKey: ["fetchStatisticsVolume", loginData],
queryFn: () => fetchStatisticsVolume(loginData ? loginData : {
queryFn: () =>
fetchStatisticsVolume(
loginData
? loginData
: {
email: "",
password: "",
session: "",
customerId: -1,
isAdmin: false
}),
isAdmin: false,
},
),
retry: 0,
retryDelay: 0,
});
const {data: dataRevenue} = useQuery({
const { data: dataRevenue } = useQuery({
queryKey: ["fetchStatisticsRevenue", loginData],
queryFn: () => fetchStatisticsRevenue(loginData ? loginData : {
queryFn: () =>
fetchStatisticsRevenue(
loginData
? loginData
: {
email: "",
password: "",
session: "",
customerId: -1,
isAdmin: false
}),
isAdmin: false,
},
),
retry: 0,
retryDelay: 0,
});
const {data: dataOrderStatus} = useQuery({
const { data: dataOrderStatus } = useQuery({
queryKey: ["fetchOrderStatus", loginData],
queryFn: () => fetchOrderStatus(loginData ? loginData : {
queryFn: () =>
fetchOrderStatus(
loginData
? loginData
: {
email: "",
password: "",
session: "",
customerId: -1,
isAdmin: false
}),
isAdmin: false,
},
),
retry: 0,
retryDelay: 0,
});
const {data: dataStockPercent} = useQuery({
const { data: dataStockPercent } = useQuery({
queryKey: ["fetchStockPercent", loginData],
queryFn: () => fetchStockPercent(loginData ? loginData : {
queryFn: () =>
fetchStockPercent(
loginData
? loginData
: {
email: "",
password: "",
session: "",
customerId: -1,
isAdmin: false
}),
isAdmin: false,
},
),
retry: 0,
retryDelay: 0,
});
useEffect(() => {
if (dataVolume) {
const cmm = []
const cmmx = monthlyVolumeXaxis
const tv = []
let i = 0
const cmm = [];
const cmmx = monthlyVolumeXaxis;
const tv = [];
let i = 0;
for (const cat in dataVolume.catMonthMap) {
for (const timestamp in dataVolume.catMonthMap[cat]) {
const date = new Date(parseInt(timestamp))
const formattedDate = date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, '0')
const date = new Date(parseInt(timestamp));
const formattedDate =
date.getFullYear() +
"-" +
String(date.getMonth() + 1).padStart(2, "0");
if (!cmmx[0].data.includes(formattedDate)) {
cmmx[0].data.push(formattedDate);
}
const datapoint = dataVolume.catMonthMap[cat][timestamp]
const datapoint = dataVolume.catMonthMap[cat][timestamp];
if (cmm.length == i) {
cmm.push({id: i, data: [], label: t(cat), type: "bar"})
cmm.push({ id: i, data: [], label: t(cat), type: "bar" });
}
cmm[i].data.push(datapoint)
cmm[i].data.push(datapoint);
if (tv.length == i) {
tv.push({id: i, value: 0, label: t(cat)})
tv.push({ id: i, value: 0, label: t(cat) });
}
tv[i].value += datapoint
tv[i].value += datapoint;
}
i++;
}
setMonthlyVolume(cmm)
setMonthlyVolumeXaxis(cmmx)
setTotalVolume(tv)
setMonthlyVolume(cmm);
setMonthlyVolumeXaxis(cmmx);
setTotalVolume(tv);
}
}, [dataVolume]);
useEffect(() => {
if (dataRevenue) {
const cmm = []
const cmmx = monthlyRevenueXaxis
const tv = []
let i = 0
const cmm = [];
const cmmx = monthlyRevenueXaxis;
const tv = [];
let i = 0;
for (const cat in dataRevenue.catMonthMap) {
for (const timestamp in dataRevenue.catMonthMap[cat]) {
const date = new Date(parseInt(timestamp))
const formattedDate = date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, '0')
const date = new Date(parseInt(timestamp));
const formattedDate =
date.getFullYear() +
"-" +
String(date.getMonth() + 1).padStart(2, "0");
if (!cmmx[0].data.includes(formattedDate)) {
cmmx[0].data.push(formattedDate);
}
const datapoint = dataRevenue.catMonthMap[cat][timestamp] / 100
const datapoint = dataRevenue.catMonthMap[cat][timestamp] / 100;
if (cmm.length == i) {
cmm.push({id: i, data: [], label: t(cat), type: "bar"})
cmm.push({ id: i, data: [], label: t(cat), type: "bar" });
}
cmm[i].data.push(datapoint)
cmm[i].data.push(datapoint);
if (tv.length == i) {
tv.push({id: i, value: 0, label: t(cat)})
tv.push({ id: i, value: 0, label: t(cat) });
}
tv[i].value += datapoint
tv[i].value += datapoint;
}
i++;
}
setMonthlyRevenue(cmm)
setMonthlyRevenueXaxis(cmmx)
setTotalRevenue(tv)
setMonthlyRevenue(cmm);
setMonthlyRevenueXaxis(cmmx);
setTotalRevenue(tv);
}
}, [dataRevenue, monthlyRevenueXaxis, t]);
useEffect(() => {
if (dataOrderStatus) {
const orderStatus = []
const orderStatus = [];
for (const status in dataOrderStatus) {
orderStatus.push({value: dataOrderStatus[status], label: t(status)})
orderStatus.push({ value: dataOrderStatus[status], label: t(status) });
}
setOrderStatus(orderStatus)
setOrderStatus(orderStatus);
}
}, [dataOrderStatus, t]);
@@ -164,41 +194,44 @@ export default function StatisticsInfo() {
}
if (dataStockPercent) {
const stockPercent = []
let i = 0
const stockPercent = [];
let i = 0;
for (let x = 0; x < 10; x++) {
stockPercent.push({
value: 0,
label: generateName(String(x * 10)),
color: getColorFromPercent(String(x * 10))
color: getColorFromPercent(String(x * 10)),
});
}
for (const cat in dataStockPercent) {
for (const percent in dataStockPercent[cat]) {
let index = stockPercent.findIndex((entry) => entry.label == generateName(percent))
const datapoint = dataStockPercent[cat][percent]
let index = stockPercent.findIndex(
(entry) => entry.label == generateName(percent),
);
const datapoint = dataStockPercent[cat][percent];
if (index === -1) {
index = stockPercent.push({
index =
stockPercent.push({
value: 0,
label: generateName(percent),
color: getColorFromPercent(percent)
}) - 1
color: getColorFromPercent(percent),
}) - 1;
}
stockPercent[index].value += datapoint
stockPercent[index].value += datapoint;
}
i++
i++;
}
setStockPercent(stockPercent)
setStockPercent(stockPercent);
}
}, [dataStockPercent])
}, [dataStockPercent]);
return (
<Box className="" sx={{color: theme.palette.text.primary}}>
<Box className="" sx={{ color: theme.palette.text.primary }}>
<Typography mt={4} variant="h4" align="center" gutterBottom>
{t("salesStatistics")}
</Typography>
<Box sx={{mb: 4}}>
<Box sx={{ mb: 4 }}>
<Typography variant="h6" align="center" gutterBottom>
{t("monthlySalesVolume")}
</Typography>
@@ -208,7 +241,7 @@ export default function StatisticsInfo() {
xAxis={monthlyVolumeXaxis}
/>
</Box>
<Box sx={{mb: 4}}>
<Box sx={{ mb: 4 }}>
<Typography variant="h6" align="center" gutterBottom>
{t("monthlySalesRevenue")}
</Typography>
@@ -219,67 +252,83 @@ export default function StatisticsInfo() {
/>
</Box>
<Box display={"flex"} mb={9}>
<Box className="vw20" sx={{m: 2}}>
<Box className="vw20" sx={{ m: 2 }}>
<Typography variant="h6" align="center" gutterBottom>
{t("itemVolumeDistribution")}
</Typography>
<PieChart
series={[{
series={[
{
data: totalVolume,
highlightScope: {fade: 'global', highlight: 'item'},
faded: {innerRadius: 30, additionalRadius: -30, color: 'gray'},
}]}
highlightScope: { fade: "global", highlight: "item" },
faded: {
innerRadius: 30,
additionalRadius: -30,
color: "gray",
},
},
]}
width={200}
height={200}
/>
</Box>
<Box className="vw20" sx={{m: 2}}>
<Box className="vw20" sx={{ m: 2 }}>
<Typography variant="h6" align="center" gutterBottom>
{t("itemRevenueDistribution")}
</Typography>
<PieChart
series={[{
series={[
{
data: totalRevenue,
highlightScope: {fade: 'global', highlight: 'item'},
valueFormatter: (v) => (v ? `${v.value}` : '-'),
}]}
highlightScope: { fade: "global", highlight: "item" },
valueFormatter: (v) => (v ? `${v.value}` : "-"),
},
]}
width={200}
height={200}
/>
</Box>
<Box className="vw20" sx={{m: 2}}>
<Box className="vw20" sx={{ m: 2 }}>
<Typography variant="h6" align="center" gutterBottom>
{t("stockFulfillment")}
</Typography>
<PieChart
series={[{
series={[
{
data: stockPercent,
innerRadius: 10,
outerRadius: 85,
cornerRadius: 5,
paddingAngle: 2,
valueFormatter: (v) => (v ? `${v.value}%` : '-'),
}]}
valueFormatter: (v) => (v ? `${v.value}%` : "-"),
},
]}
width={200}
height={200}
hideLegend
/>
</Box>
<Box className="vw20" sx={{m: 2}}>
<Box className="vw20" sx={{ m: 2 }}>
<Typography variant="h6" align="center" gutterBottom>
{t("orderStatus")}
</Typography>
<PieChart
series={[{
series={[
{
data: orderStatus,
innerRadius: 20,
outerRadius: 70,
cornerRadius: 5,
paddingAngle: 1,
highlightScope: {fade: 'global', highlight: 'item'},
faded: {innerRadius: 30, additionalRadius: -10, color: 'gray'},
}]}
highlightScope: { fade: "global", highlight: "item" },
faded: {
innerRadius: 30,
additionalRadius: -10,
color: "gray",
},
},
]}
height={200}
width={200}
/>

View File

@@ -1,4 +1,12 @@
import {FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, Rating, useTheme,} from "@mui/material";
import {
FormControl,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
Rating,
useTheme,
} from "@mui/material";
import React from "react";
type FilterItemOption = {
@@ -18,7 +26,7 @@ export default function FilterItem({
filterItems,
value,
onChange,
}: FilterItemProps) {
}: FilterItemProps) {
const theme = useTheme();
if (!value && filterItems.length > 0) {
@@ -32,7 +40,7 @@ export default function FilterItem({
};
return (
<div style={{marginBottom: "1.5rem"}}>
<div style={{ marginBottom: "1.5rem" }}>
<FormLabel
component="legend"
sx={{
@@ -50,7 +58,7 @@ export default function FilterItem({
<FormControlLabel
key={idx}
value={item.value}
control={<Radio/>}
control={<Radio />}
label={
/^[1-5]$/.test(item.value) ? (
<Rating

View File

@@ -1,17 +1,26 @@
import {AddShoppingCart} from "@mui/icons-material";
import {Box, Card, CardActionArea, CardContent, CardMedia, IconButton, Paper, Rating, Typography} from "@mui/material";
import {useState} from "react";
import {useTranslation} from 'react-i18next';
import {useNavigate} from "react-router-dom";
import { AddShoppingCart } from "@mui/icons-material";
import {
Box,
Card,
CardActionArea,
CardContent,
CardMedia,
IconButton,
Paper,
Rating,
Typography,
} from "@mui/material";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import ItemWithImage from "../../components/Item";
import {useBasket} from "../BasketProvider";
import { useBasket } from "../BasketProvider";
import "../helper.css";
export default function ItemCard({item}: { item: ItemWithImage }) {
const {t} = useTranslation();
const navigate = useNavigate()
const {addToBasket} = useBasket();
export default function ItemCard({ item }: { item: ItemWithImage }) {
const { t } = useTranslation();
const navigate = useNavigate();
const { addToBasket } = useBasket();
const handleAddToCart = () => {
addToBasket(item, 1);
@@ -19,18 +28,34 @@ export default function ItemCard({item}: { item: ItemWithImage }) {
};
const handleClick = () => {
navigate(`/product/${item.id}`, {state: {item}});
}
const [imageUrl, setImageUrl] = useState<string>(item.image || "/src/assets/default.jpg"); // Fallback-Bild
navigate(`/product/${item.id}`, { state: { item } });
};
const [imageUrl, setImageUrl] = useState<string>(
item.image || "/src/assets/default.jpg",
); // Fallback-Bild
if (imageUrl !== "/src/assets/default.jpg" && !imageUrl.startsWith("data:image/")) {
if (
imageUrl !== "/src/assets/default.jpg" &&
!imageUrl.startsWith("data:image/")
) {
setImageUrl("data:image/jpeg;base64," + imageUrl);
}
return (
<Paper elevation={4}>
<Card sx={{height: "100%", width: "100%"}}>
<CardActionArea onClick={handleClick} sx={{height: "100%"}} component="div">
<Card
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
width: "100%",
}}
>
<CardActionArea
onClick={handleClick}
sx={{ height: "100%", width: "100%" }}
component="div"
>
<CardMedia
component="img"
height="140"
@@ -45,47 +70,81 @@ export default function ItemCard({item}: { item: ItemWithImage }) {
maxHeight: "100%", // Begrenze die maximale Höhe auf den Container
}}
/>
<CardContent>
<CardContent
sx={{ display: "flex", flexDirection: "column", flexGrow: 1 }}
>
<Typography gutterBottom variant="h5" component="div">
{item.name}
</Typography>
<Rating name="half-rating" readOnly defaultValue={item.rating / 2} precision={0.5}/>
<Box sx={{display: "flex", justifyContent: "space-between", alignItems: "flex-end"}}>
<Typography variant="body2" sx={{color: 'text.secondary'}} className="item-description">
{(item.price100 / 100 * (1 - item.discount100 / 100)).toFixed(2)}
<Box
sx={{
mt: "auto",
display: "flex",
flexGrow: 1,
flexDirection: "column",
}}
>
<Rating
name="half-rating"
readOnly
defaultValue={item.rating / 2}
precision={0.5}
/>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "flex-end",
}}
>
<Typography
variant="body2"
sx={{ color: "text.secondary" }}
className="item-description"
>
{(
(item.price100 / 100) *
(1 - item.discount100 / 100)
).toFixed(2)}{" "}
</Typography>
{item.discount100 == 0 ? <></> :
<Typography variant="body2" sx={{color: 'red'}} className="item-description">
{(- item.discount100)}%
{item.discount100 == 0 ? (
<></>
) : (
<Typography
variant="body2"
sx={{ color: "red" }}
className="item-description"
>
{-item.discount100}%
</Typography>
}
)}
<IconButton
aria-label={t('addToCart')}
aria-label={t("addToCart")}
onClick={(event) => {
event.stopPropagation();
handleAddToCart();
}}
>
<AddShoppingCart/>
<AddShoppingCart />
</IconButton>
</Box>
{item.stock > 10 ? (
<Typography variant="body2">
{t('inStock')}
</Typography>
<Typography variant="body2">{t("inStock")}</Typography>
) : item.stock > 0 ? (
<Typography variant="body2" sx={{color: 'orange'}}>
{t('almostSoldOut')}
<Typography variant="body2" sx={{ color: "orange" }}>
{t("almostSoldOut")}
</Typography>
) : (
<Typography variant="body2" sx={{color: 'red'}}>
{t('outOfStock')}
<Typography variant="body2" sx={{ color: "red" }}>
{t("outOfStock")}
</Typography>
)}
</Box>
</CardContent>
</CardActionArea>
</Card>
</Paper>
)
);
}

View File

@@ -1,6 +1,6 @@
import {Box, Slider, Typography, useTheme} from "@mui/material";
import {SyntheticEvent, useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import { Box, Slider, Typography, useTheme } from "@mui/material";
import { SyntheticEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
type PriceSliderProps = {
min?: number;
@@ -8,8 +8,12 @@ type PriceSliderProps = {
onChange?: (range: [number, number]) => void;
};
export default function PriceSlider({min = 0, max = 10000, onChange}: PriceSliderProps) {
const {t} = useTranslation();
export default function PriceSlider({
min = 0,
max = 10000,
onChange,
}: PriceSliderProps) {
const { t } = useTranslation();
const theme = useTheme();
const [value, setValue] = useState<[number, number]>([min, max]);
@@ -25,7 +29,10 @@ export default function PriceSlider({min = 0, max = 10000, onChange}: PriceSlide
}
};
const handleCommitted = (_: Event | SyntheticEvent<Element, Event>, newValue: number | number[]) => {
const handleCommitted = (
_: Event | SyntheticEvent<Element, Event>,
newValue: number | number[],
) => {
if (Array.isArray(newValue)) {
onChange?.([newValue[0], newValue[1]]);
}
@@ -34,7 +41,7 @@ export default function PriceSlider({min = 0, max = 10000, onChange}: PriceSlide
const formatValueToEuro = (val: number) => `${(val / 100).toFixed(2)}`;
return (
<Box sx={{mb: 4}}>
<Box sx={{ mb: 4 }}>
<Typography
variant="h6"
sx={{
@@ -46,7 +53,7 @@ export default function PriceSlider({min = 0, max = 10000, onChange}: PriceSlide
{t("price")}
</Typography>
<Box sx={{px: 1}}>
<Box sx={{ px: 1 }}>
<Slider
value={value}
onChange={handleChange}
@@ -58,7 +65,7 @@ export default function PriceSlider({min = 0, max = 10000, onChange}: PriceSlide
step={1}
sx={{
color: "#0fd13f",
'& .MuiSlider-valueLabel': {
"& .MuiSlider-valueLabel": {
color: theme.palette.text.primary,
},
}}

View File

@@ -1,32 +1,59 @@
import {Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Link, TextField} from "@mui/material";
import {useQuery} from "@tanstack/react-query";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Link,
TextField,
} from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import i18next from "i18next";
import React, {useEffect, useState} from "react";
import AccountType, {User} from "../../components/Account";
import {useAccount} from "../AccountProvider";
import {fetchAccount, submitLogin, submitRegister} from "../query/Queries"; // Importiere die Funktion für die Registrierung
import {useTranslation} from "react-i18next";
import React, { useEffect, useState } from "react";
import AccountType, { User } from "../../components/Account";
import { useAccount } from "../AccountProvider";
import { fetchAccount, submitLogin, submitRegister } from "../query/Queries"; // Importiere die Funktion für die Registrierung
import { useTranslation } from "react-i18next";
type LoginDialogProps = {
open: boolean;
onClose: () => void;
onSubmit: () => void; // Funktion, die aufgerufen wird, wenn der Login erfolgreich ist
loginData: { email: string; password: string };
setLoginData: React.Dispatch<React.SetStateAction<{ email: string; password: string, customerId: number }>>;
setLoginData: React.Dispatch<
React.SetStateAction<{
email: string;
password: string;
customerId: number;
}>
>;
};
const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setLoginData, onSubmit}) => {
const {t} = useTranslation();
const {login} = useAccount();
const LoginDialog: React.FC<LoginDialogProps> = ({
open,
onClose,
loginData,
setLoginData,
onSubmit,
}) => {
const { t } = useTranslation();
const { login } = useAccount();
const [showRegister, setShowRegister] = useState(false);
const [registerData, setRegisterData] = useState<AccountType>({
email: "",
password: "",
id: 0,
customer: {id: 0, name: "", surname: "", address: "", country: "", zip: ""},
customer: {
id: 0,
name: "",
surname: "",
address: "",
country: "",
zip: "",
},
langI18n: i18next.language,
admin: false
admin: false,
});
const [showErrorRegister, setShowErrorRegister] = useState(false); // Neuer Zustand für die Anzeige der Fehlermeldung
const [showErrorLogin, setShowErrorLogin] = useState(false); // Neuer Zustand für die Anzeige der Login-Fehlermeldung
@@ -40,9 +67,12 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
}
}, [open]);
// useQuery für Login
const {refetch: refetchLogin, isLoading: isLoadingLogin, error: errorLogin} = useQuery({
const {
refetch: refetchLogin,
isLoading: isLoadingLogin,
error: errorLogin,
} = useQuery({
queryKey: ["submitLogin", loginData],
queryFn: () => submitLogin(loginData),
retry: 0,
@@ -50,7 +80,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
enabled: false,
});
const {refetch: refetchAccount} = useQuery({
const { refetch: refetchAccount } = useQuery({
queryKey: ["fetchAccount", loginData],
queryFn: () => fetchAccount(loginData),
retry: 0,
@@ -59,7 +89,11 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
});
// useQuery für Registrierung
const {refetch: refetchRegister, isLoading: isLoadingRegister, error: errorRegister} = useQuery({
const {
refetch: refetchRegister,
isLoading: isLoadingRegister,
error: errorRegister,
} = useQuery({
queryKey: ["submitRegister", registerData],
queryFn: () => submitRegister(registerData),
retry: 0,
@@ -86,7 +120,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
password: customerData.password,
customerId: customerData.customer.id, // Setze die customerId aus den Account-Daten
session: session, // Setze die Session aus der Login-Antwort
isAdmin: customerData.admin
isAdmin: customerData.admin,
};
login(user);
setShowRegister(false); // Zurück zum Login wechseln
@@ -114,14 +148,17 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
return (
<Dialog open={open} onClose={handleClose} disableEnforceFocus>
<form onSubmit={e => {
<form
onSubmit={(e) => {
e.preventDefault();
if (showRegister) {
handleLogin();
} else {
handleRegister();
}
}} noValidate>
}}
noValidate
>
<DialogTitle>{showRegister ? t("register") : t("login")}</DialogTitle>
<DialogContent>
<TextField
@@ -130,9 +167,9 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
type="email"
fullWidth
value={showRegister ? registerData.email : loginData.email}
onChange={e => {
setLoginData(prev => ({...prev, email: e.target.value}));
setRegisterData(prev => ({...prev, email: e.target.value}))
onChange={(e) => {
setLoginData((prev) => ({ ...prev, email: e.target.value }));
setRegisterData((prev) => ({ ...prev, email: e.target.value }));
}}
/>
<TextField
@@ -141,12 +178,15 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
type="password"
fullWidth
value={showRegister ? registerData.password : loginData.password}
onChange={e => {
setLoginData(prev => ({...prev, password: e.target.value}))
setRegisterData(prev => ({...prev, password: e.target.value}))
onChange={(e) => {
setLoginData((prev) => ({ ...prev, password: e.target.value }));
setRegisterData((prev) => ({
...prev,
password: e.target.value,
}));
}}
/>
{showRegister &&
{showRegister && (
<>
<TextField
margin="dense"
@@ -154,10 +194,12 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
type="text"
fullWidth
value={registerData.customer.name}
onChange={e => setRegisterData(prev => ({
onChange={(e) =>
setRegisterData((prev) => ({
...prev,
customer: {...prev.customer, name: e.target.value},
}))}
customer: { ...prev.customer, name: e.target.value },
}))
}
/>
<TextField
margin="dense"
@@ -165,10 +207,12 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
type="text"
fullWidth
value={registerData.customer.surname}
onChange={e => setRegisterData(prev => ({
onChange={(e) =>
setRegisterData((prev) => ({
...prev,
customer: {...prev.customer, surname: e.target.value},
}))}
customer: { ...prev.customer, surname: e.target.value },
}))
}
/>
<TextField
margin="dense"
@@ -176,10 +220,12 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
type="text"
fullWidth
value={registerData.customer.address}
onChange={e => setRegisterData(prev => ({
onChange={(e) =>
setRegisterData((prev) => ({
...prev,
customer: {...prev.customer, address: e.target.value},
}))}
customer: { ...prev.customer, address: e.target.value },
}))
}
/>
<TextField
margin="dense"
@@ -187,10 +233,12 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
type="text"
fullWidth
value={registerData.customer.country}
onChange={e => setRegisterData(prev => ({
onChange={(e) =>
setRegisterData((prev) => ({
...prev,
customer: {...prev.customer, country: e.target.value},
}))}
customer: { ...prev.customer, country: e.target.value },
}))
}
/>
<TextField
margin="dense"
@@ -198,13 +246,15 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
type="text"
fullWidth
value={registerData.customer.zip}
onChange={e => setRegisterData(prev => ({
onChange={(e) =>
setRegisterData((prev) => ({
...prev,
customer: {...prev.customer, zip: e.target.value},
}))}
customer: { ...prev.customer, zip: e.target.value },
}))
}
/>
</>
}
)}
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>{t("cancel")}</Button>
@@ -225,7 +275,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
<Box color="error.main">{t("registerFailed")}</Box>
)}
{showRegister ? (
<Box sx={{width: '100%', textAlign: 'center', pb: 2}}>
<Box sx={{ width: "100%", textAlign: "center", pb: 2 }}>
<Link
component="button"
variant="body2"
@@ -237,7 +287,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setL
</Link>
</Box>
) : (
<Box sx={{width: '100%', textAlign: 'center', pb: 2}}>
<Box sx={{ width: "100%", textAlign: "center", pb: 2 }}>
<Link
component="button"
variant="body2"

View File

@@ -61,9 +61,9 @@
/* Typography styles */
.navbar-typography {
font-family: 'monospace';
font-family: "monospace";
font-weight: 700;
letter-spacing: .3rem;
letter-spacing: 0.3rem;
color: inherit;
text-decoration: none;
}

View File

@@ -1,76 +1,79 @@
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
import {Autocomplete, Badge, TextField} from '@mui/material';
import AppBar from '@mui/material/AppBar';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Toolbar from '@mui/material/Toolbar';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import {useQuery} from '@tanstack/react-query';
import * as React from 'react';
import {useTranslation} from 'react-i18next';
import {useNavigate} from 'react-router-dom';
import Item from '../../components/Item';
import ThemeToggle from '../../theme/ThemeToggle';
import {useAccount} from '../AccountProvider';
import {fetchItemList} from '../query/Queries';
import LoginDialog from './LoginDialog';
import './NavBar.css';
import {useBasket} from '../BasketProvider';
import logo from '../../assets/logo/Blume-logo.png';
import MenuIcon from "@mui/icons-material/Menu";
import SearchIcon from "@mui/icons-material/Search";
import { Autocomplete, Badge, TextField } from "@mui/material";
import AppBar from "@mui/material/AppBar";
import Avatar from "@mui/material/Avatar";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Toolbar from "@mui/material/Toolbar";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { useQuery } from "@tanstack/react-query";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import Item from "../../components/Item";
import ThemeToggle from "../../theme/ThemeToggle";
import { useAccount } from "../AccountProvider";
import { fetchItemList } from "../query/Queries";
import LoginDialog from "./LoginDialog";
import "./NavBar.css";
import { useBasket } from "../BasketProvider";
import logo from "../../assets/logo/Blume-logo.png";
export default function NavBar() {
const {t} = useTranslation();
const { t } = useTranslation();
const navigate = useNavigate();
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(null);
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(null);
const [avatarName, setAvatarName] = React.useState<string>(''); // Für Avatar-Tooltip
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(
null,
);
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
null,
);
const [avatarName, setAvatarName] = React.useState<string>(""); // Für Avatar-Tooltip
const {user, logout} = useAccount();
const { user, logout } = useAccount();
const {basket} = useBasket();
const totalQuantity = basket?.reduce((sum, item) => sum + item.quantity, 0) ?? 0;
const { basket } = useBasket();
const totalQuantity =
basket?.reduce((sum, item) => sum + item.quantity, 0) ?? 0;
const [loginOpen, setLoginOpen] = React.useState(false);
const [loginData, setLoginData] = React.useState({password: '', email: '', customerId: 0});
const [loginData, setLoginData] = React.useState({
password: "",
email: "",
customerId: 0,
});
const [itemNames, setItemNames] = React.useState<string[]>([]); // Für Autocomplete
const pageKeys = ['components', 'checkout', 'contact', 'admin'];
const pageKeys = ["components", "checkout", "contact", "admin"];
const filteredPages = pageKeys
.filter(key => {
.filter((key) => {
if (key === "admin") {
return user?.isAdmin === true; // nur Admins sehen Admin-Seite
}
return true; // alle anderen Seiten immer anzeigen
})
.map(key => ({key, label: t(key)}));
.map((key) => ({ key, label: t(key) }));
const settings = user
? [
{
key: 'email',
label: `${t('loggedInAs')}: ${user.email}`,
disabled: true // wir nutzen dieses Flag gleich zur Erkennung
key: "email",
label: `${t("loggedInAs")}: ${user.email}`,
disabled: true, // wir nutzen dieses Flag gleich zur Erkennung
},
{key: 'account', label: t('account')},
{key: 'orders', label: t('orders')},
{key: 'logout', label: t('logout')}
{ key: "account", label: t("account") },
{ key: "orders", label: t("orders") },
{ key: "logout", label: t("logout") },
]
: [
{key: 'login', label: t('login')}
];
: [{ key: "login", label: t("login") }];
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElNav(event.currentTarget);
@@ -87,18 +90,18 @@ export default function NavBar() {
const handleCloseUserMenu = (link: string) => {
setAnchorElUser(null);
if (link === 'login') {
if (link === "login") {
setLoginOpen(true);
} else if (link === 'logout') {
} else if (link === "logout") {
logout();
if (
location.pathname.startsWith('/account') ||
location.pathname.startsWith('/orders')
location.pathname.startsWith("/account") ||
location.pathname.startsWith("/orders")
) {
navigate('/');
navigate("/");
}
} else {
navigate(`/${link.toLowerCase()}`)
navigate(`/${link.toLowerCase()}`);
}
};
@@ -106,9 +109,8 @@ export default function NavBar() {
setLoginOpen(false);
};
// useQuery, um die Item-Namen zu laden
const {data: items = []} = useQuery<Item[]>({
const { data: items = [] } = useQuery<Item[]>({
queryKey: ["fetchItemList"],
queryFn: fetchItemList,
});
@@ -124,12 +126,11 @@ export default function NavBar() {
setAvatarName(user.email.toUpperCase());
}
if (!user) {
setAvatarName('');
setAvatarName("");
}
}, [user]);
const handleSearch = (_: React.SyntheticEvent, value: string | null) => {
if (!value) {
// Wenn der Suchwert leer ist, navigiere zur Homepage ohne Suchparameter
navigate("/");
@@ -145,26 +146,31 @@ export default function NavBar() {
<Toolbar
disableGutters
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center', // <--- HIER hinzugefügt
display: "flex",
justifyContent: "space-between",
alignItems: "center", // <--- HIER hinzugefügt
px: 3,
minHeight: { xs: 56, sm: 64 }, // optional: Standardhöhe für bessere Zentrierung
}}
>
<Box sx={{display: "flex", alignItems: "center", minWidth: "0px"}}>
<Box sx={{ display: "flex", alignItems: "center", minWidth: "0px" }}>
<img
src={logo}
alt="Logo"
className="navbar-logo"
style={{ display: "block", height: 40, marginRight: 12, objectFit: "contain" }} // optional: Höhe anpassen
onClick={() => navigate('/')}
style={{
display: "block",
height: 40,
marginRight: 12,
objectFit: "contain",
}} // optional: Höhe anpassen
onClick={() => navigate("/")}
/>
<Typography
variant="h6"
noWrap
component="a"
onClick={() => navigate('/')}
onClick={() => navigate("/")}
sx={{
fontFamily: "monospace",
fontWeight: 700,
@@ -175,30 +181,31 @@ export default function NavBar() {
alignItems: "center", // <--- HIER hinzugefügt
height: "100%", // optional
":hover": {
color: "#fff1d8ff"
}
color: "#fff1d8ff",
},
}}
>
Digitaler Produktionsshop
</Typography>
</Box>
<Box sx={{
<Box
sx={{
flexGrow: 1,
display: "flex",
justifyContent: "center",
alignItems: "center", // <--- HIER hinzugefügt
px: 3,
zIndex: 100000
}}>
zIndex: 100000,
}}
>
<Autocomplete
sx={{
flexGrow: 1,
minWidth: "150px",
maxWidth: "600px",
display: "flex",
alignItems: "center" // <--- HIER hinzugefügt
alignItems: "center", // <--- HIER hinzugefügt
}}
freeSolo
options={itemNames}
@@ -210,24 +217,24 @@ export default function NavBar() {
InputProps={{
...params.InputProps,
startAdornment: (
<SearchIcon sx={{color: "white", mr: 1}}/>
<SearchIcon sx={{ color: "white", mr: 1 }} />
),
}}
sx={{
'& .MuiOutlinedInput-root': {
'& fieldset': {
borderColor: 'white',
borderWidth: '1px',
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: "white",
borderWidth: "1px",
},
'&:hover fieldset': {
borderColor: 'white',
"&:hover fieldset": {
borderColor: "white",
},
'&.Mui-focused fieldset': {
borderColor: 'white',
"&.Mui-focused fieldset": {
borderColor: "white",
},
},
input: {
color: 'white',
color: "white",
},
}}
/>
@@ -235,15 +242,22 @@ export default function NavBar() {
/>
</Box>
<Box sx={{display: "flex", alignItems: "center", gap: 2, marginLeft: 'auto'}}>
<Box sx={{display: {xs: "none", md: "flex"}, gap: 2}}>
{filteredPages.map(({key, label}) => {
if (key === 'checkout') {
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 2,
marginLeft: "auto",
}}
>
<Box sx={{ display: { xs: "none", md: "flex" }, gap: 2 }}>
{filteredPages.map(({ key, label }) => {
if (key === "checkout") {
return (
<Button
key={key}
onClick={() => handleCloseNavMenu(key)}
sx={{color: "white", fontWeight: 500}}
sx={{ color: "white", fontWeight: 500 }}
>
<Badge
badgeContent={totalQuantity}
@@ -261,7 +275,7 @@ export default function NavBar() {
<Button
key={key}
onClick={() => handleCloseNavMenu(key)}
sx={{color: "white", fontWeight: 500}}
sx={{ color: "white", fontWeight: 500 }}
>
{label}
</Button>
@@ -269,23 +283,23 @@ export default function NavBar() {
})}
</Box>
<Box sx={{display: {xs: "flex", md: "none"}}}>
<Box sx={{ display: { xs: "flex", md: "none" } }}>
<IconButton
size="large"
aria-label={t('menu')}
aria-label={t("menu")}
onClick={handleOpenNavMenu}
color="inherit"
>
<MenuIcon/>
<MenuIcon />
</IconButton>
<Menu
anchorEl={anchorElNav}
anchorOrigin={{vertical: "top", horizontal: "right"}}
transformOrigin={{vertical: "top", horizontal: "right"}}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
transformOrigin={{ vertical: "top", horizontal: "right" }}
open={Boolean(anchorElNav)}
onClose={() => setAnchorElNav(null)}
>
{filteredPages.map(({key, label}) => (
{filteredPages.map(({ key, label }) => (
<MenuItem key={key} onClick={() => handleCloseNavMenu(key)}>
<Typography>{label}</Typography>
</MenuItem>
@@ -293,20 +307,20 @@ export default function NavBar() {
</Menu>
</Box>
<ThemeToggle/>
<Tooltip title={t('openSettings')} placement='bottom-end'>
<IconButton onClick={handleOpenUserMenu} sx={{p: 0}}>
<Avatar alt={avatarName} src="/static/images/avatar/2.jpg"/>
<ThemeToggle />
<Tooltip title={t("openSettings")} placement="bottom-end">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar alt={avatarName} src="/static/images/avatar/2.jpg" />
</IconButton>
</Tooltip>
<Menu
sx={{mt: "15px"}}
sx={{ mt: "15px" }}
id="menu-appbar-user"
anchorEl={anchorElUser}
open={Boolean(anchorElUser)}
onClose={() => setAnchorElUser(null)}
>
{settings.map(({key, label, disabled}) => (
{settings.map(({ key, label, disabled }) => (
<MenuItem
key={key}
onClick={() => {
@@ -314,7 +328,7 @@ export default function NavBar() {
}}
disabled={disabled}
>
<Typography sx={{textAlign: "center"}}>{label}</Typography>
<Typography sx={{ textAlign: "center" }}>{label}</Typography>
</MenuItem>
))}
</Menu>
@@ -328,7 +342,7 @@ export default function NavBar() {
loginData={loginData}
setLoginData={setLoginData}
/>
<div className="navbar-offset"/>
<div className="navbar-offset" />
</>
);
}

View File

@@ -1,4 +1,4 @@
import {Close, LocalShipping, ShoppingCart} from "@mui/icons-material";
import { Close, LocalShipping, ShoppingCart } from "@mui/icons-material";
import {
Alert,
Box,
@@ -12,28 +12,29 @@ import {
SnackbarCloseReason,
Stack,
TextField,
Typography
Typography,
} from "@mui/material";
import React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Item from "../../components/Item";
import {useBasket} from "../BasketProvider";
import { useBasket } from "../BasketProvider";
export default function ProductInfo({item}: { item: Item }) {
const {t} = useTranslation();
export default function ProductInfo({ item }: { item: Item }) {
const { t } = useTranslation();
const [quantity, setQuantity] = useState<number>(1);
const [open, setOpen] = useState<boolean>(false);
const [imageDimensions, setImageDimensions] = useState({width: 0, height: 0});
const [imageDimensions, setImageDimensions] = useState({
width: 0,
height: 0,
});
const {addToBasket} = useBasket();
const { addToBasket } = useBasket();
const handleClose = (
_: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason,
) => {
if (reason === 'clickaway') {
if (reason === "clickaway") {
return;
}
@@ -44,16 +45,15 @@ export default function ProductInfo({item}: { item: Item }) {
<React.Fragment>
<IconButton
size="small"
aria-label={t('close')}
aria-label={t("close")}
color="inherit"
onClick={handleClose}
>
<Close fontSize="small"/>
<Close fontSize="small" />
</IconButton>
</React.Fragment>
);
const handleAddToCart = () => {
addToBasket(item, quantity);
setOpen(true);
@@ -63,8 +63,8 @@ export default function ProductInfo({item}: { item: Item }) {
const discountedPrice = item.price100 * (1 - item.discount100 / 100);
const handleImageLoad = (event: React.SyntheticEvent<HTMLImageElement>) => {
const {naturalWidth, naturalHeight} = event.currentTarget;
setImageDimensions({width: naturalWidth, height: naturalHeight});
const { naturalWidth, naturalHeight } = event.currentTarget;
setImageDimensions({ width: naturalWidth, height: naturalHeight });
};
const [imageUrl, setImageUrl] = useState<string>("/src/assets/default.jpg"); // Fallback-Bild
@@ -72,13 +72,15 @@ export default function ProductInfo({item}: { item: Item }) {
useEffect(() => {
const fetchImage = async () => {
try {
const response = await fetch(`http://localhost:8085/image?uuid=${item.uuid}`);
const response = await fetch(
`http://localhost:8085/image?uuid=${item.uuid}`,
);
let data = await response.text();
if (data.length == 0) {
console.error("Got emtpy picture for article ", item.uuid);
}
if (!data.startsWith("data:image/")) {
data = "data:image/jpeg;base64," + data
data = "data:image/jpeg;base64," + data;
}
setImageUrl(data);
} catch (error) {
@@ -93,7 +95,10 @@ export default function ProductInfo({item}: { item: Item }) {
<Grid container spacing={4}>
{/* Left Column - Image */}
<Card elevation={2} sx={{width: '100%', maxWidth: 400, display: 'inherit'}}>
<Card
elevation={2}
sx={{ width: "100%", maxWidth: 400, display: "inherit" }}
>
<Box
component="img"
src={imageUrl}
@@ -103,8 +108,10 @@ export default function ProductInfo({item}: { item: Item }) {
event.currentTarget.src = "/src/assets/default.jpg"; // Standardbild setzen
}}
sx={{
maxWidth: imageDimensions.width > imageDimensions.height ? "100%" : "auto",
maxHeight: imageDimensions.height >= imageDimensions.width ? 400 : "auto",
maxWidth:
imageDimensions.width > imageDimensions.height ? "100%" : "auto",
maxHeight:
imageDimensions.height >= imageDimensions.width ? 400 : "auto",
width: "auto",
height: "auto",
objectFit: "contain",
@@ -112,7 +119,6 @@ export default function ProductInfo({item}: { item: Item }) {
/>
</Card>
{/* Right Column - Product Details */}
<Stack spacing={3}>
@@ -121,10 +127,9 @@ export default function ProductInfo({item}: { item: Item }) {
</Typography>
<Box display="flex" alignItems="center" gap={1}>
<Rating value={item.rating / 2} precision={0.5} readOnly/>
<Rating value={item.rating / 2} precision={0.5} readOnly />
<Typography variant="body2" color="text.secondary">
{item.rating > 0 ? `(${item.rating / 2} / 5)` : t('noRatingsYet')}
{item.rating > 0 ? `(${item.rating / 2} / 5)` : t("noRatingsYet")}
</Typography>
</Box>
@@ -137,7 +142,7 @@ export default function ProductInfo({item}: { item: Item }) {
<Typography
variant="h6"
color="text.secondary"
sx={{textDecoration: 'line-through'}}
sx={{ textDecoration: "line-through" }}
>
{(item.price100 / 100).toFixed(2)}
</Typography>
@@ -152,58 +157,59 @@ export default function ProductInfo({item}: { item: Item }) {
)}
</Stack>
<Divider/>
<Divider />
<Box>
{item.stock > 10 ? (
<Alert severity="success" variant='outlined'>
{t('inStock')} ({item.stock} {t('available')})
<Alert severity="success" variant="outlined">
{t("inStock")} ({item.stock} {t("available")})
</Alert>
) : item.stock > 0 ? (
<Alert severity="warning"
variant='outlined'>{t('almostSoldOut')} ({item.stock} {t('available')})</Alert>
<Alert severity="warning" variant="outlined">
{t("almostSoldOut")} ({item.stock} {t("available")})
</Alert>
) : (
<Alert severity="error" variant='filled'>{t('outOfStock')}</Alert>
<Alert severity="error" variant="filled">
{t("outOfStock")}
</Alert>
)}
</Box>
<Stack direction="row" spacing={2} alignItems="center">
<TextField
type="number"
label={t('quantity')}
label={t("quantity")}
value={quantity}
onChange={(e) => setQuantity(Math.max(1, parseInt(e.target.value)))}
InputProps={{inputProps: {min: 1, max: item.stock}}}
sx={{width: 100}}
InputProps={{ inputProps: { min: 1, max: item.stock } }}
sx={{ width: 100 }}
/>
<Button
variant="contained"
size="large"
startIcon={<ShoppingCart/>}
startIcon={<ShoppingCart />}
onClick={handleAddToCart}
disabled={item.stock <= 0}
fullWidth
>
{t('addToCart')}
{t("addToCart")}
</Button>
</Stack>
<Box sx={{mt: 2}}>
<Box sx={{ mt: 2 }}>
<Typography variant="body2" color="text.secondary">
<LocalShipping sx={{mr: 1, verticalAlign: 'middle'}}/>
{t('freeShipping')}
<LocalShipping sx={{ mr: 1, verticalAlign: "middle" }} />
{t("freeShipping")}
</Typography>
</Box>
</Stack>
<Snackbar
open={open}
autoHideDuration={3000}
onClose={handleClose}
message={t('addedToCart')}
message={t("addedToCart")}
action={action}
/>
</Grid>
);
}

View File

@@ -1,13 +1,20 @@
import {Card, CardActionArea, CardContent, Paper, Rating, Typography, useTheme} from "@mui/material";
import {
Card,
CardActionArea,
CardContent,
Paper,
Rating,
Typography,
useTheme,
} from "@mui/material";
import RatingType from "../../components/Rating";
import {useTranslation} from 'react-i18next';
import { useTranslation } from "react-i18next";
export default function RatingCard(ratingType: RatingType) {
const {t} = useTranslation();
const { t } = useTranslation();
const theme = useTheme(); // Zugriff auf Light/Dark-Mode
const handleClick = () => {
};
const handleClick = () => {};
return (
<Paper
@@ -15,7 +22,7 @@ export default function RatingCard(ratingType: RatingType) {
sx={{
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
mb: 3
mb: 3,
}}
>
<Card
@@ -29,9 +36,10 @@ export default function RatingCard(ratingType: RatingType) {
gutterBottom
variant="h6"
component="div"
sx={{color: theme.palette.text.primary}}
sx={{ color: theme.palette.text.primary }}
>
{t('ratingFrom')} {new Date(ratingType.timestamp).toLocaleDateString('de-DE')}
{t("ratingFrom")}{" "}
{new Date(ratingType.timestamp).toLocaleDateString("de-DE")}
</Typography>
<Rating
@@ -43,7 +51,7 @@ export default function RatingCard(ratingType: RatingType) {
<Typography
variant="body2"
sx={{color: theme.palette.text.secondary, mt: 1}}
sx={{ color: theme.palette.text.secondary, mt: 1 }}
>
{ratingType.content}
</Typography>

View File

@@ -8,19 +8,19 @@ import {
SnackbarCloseReason,
TextField,
Typography,
useTheme
useTheme,
} from "@mui/material";
import {Close} from "@mui/icons-material";
import {useQuery} from "@tanstack/react-query";
import React, {useMemo, useState} from "react";
import {useTranslation} from 'react-i18next';
import { Close } from "@mui/icons-material";
import { useQuery } from "@tanstack/react-query";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import RatingType from "../../components/Rating";
import {fetchRatingList, submitRating} from "../query/Queries";
import { fetchRatingList, submitRating } from "../query/Queries";
import RatingCard from "./RatingCard";
import RatingSubmitType from "../../components/RatingSubmit";
export default function Ratings({itemId}: { itemId: string }) {
const {t} = useTranslation();
export default function Ratings({ itemId }: { itemId: string }) {
const { t } = useTranslation();
const theme = useTheme();
const [open, setOpen] = useState<boolean>(false);
@@ -33,7 +33,7 @@ export default function Ratings({itemId}: { itemId: string }) {
articleId: itemId,
};
const {refetch} = useQuery({
const { refetch } = useQuery({
queryKey: ["submitRating", ratingData],
queryFn: () => submitRating(ratingData),
retry: 3,
@@ -48,7 +48,7 @@ export default function Ratings({itemId}: { itemId: string }) {
const handleClose = (
_: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason
reason?: SnackbarCloseReason,
) => {
if (reason === "clickaway") return;
setOpen(false);
@@ -62,12 +62,12 @@ export default function Ratings({itemId}: { itemId: string }) {
color="inherit"
onClick={handleClose}
>
<Close fontSize="small"/>
<Close fontSize="small" />
</IconButton>
</React.Fragment>
);
const {data = []} = useQuery<RatingType[]>({
const { data = [] } = useQuery<RatingType[]>({
queryKey: ["fetchRatingList", itemId],
queryFn: () => fetchRatingList(itemId),
retry: 3,
@@ -79,7 +79,10 @@ export default function Ratings({itemId}: { itemId: string }) {
const getRatings = () => {
if (ratings.length === 0) {
return (
<Typography variant="body1" sx={{color: theme.palette.text.secondary}}>
<Typography
variant="body1"
sx={{ color: theme.palette.text.secondary }}
>
{t("noRatingsYet")}
</Typography>
);
@@ -92,10 +95,13 @@ export default function Ratings({itemId}: { itemId: string }) {
return (
<>
<Divider sx={{backgroundColor: theme.palette.divider, my: 3}}/>
<Divider sx={{ backgroundColor: theme.palette.divider, my: 3 }} />
<Box sx={{mb: 4}}>
<Typography variant="h5" sx={{color: theme.palette.text.primary, mb: 2}}>
<Box sx={{ mb: 4 }}>
<Typography
variant="h5"
sx={{ color: theme.palette.text.primary, mb: 2 }}
>
{t("rateThisProduct")}:
</Typography>
@@ -117,20 +123,20 @@ export default function Ratings({itemId}: { itemId: string }) {
mb: 2,
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.primary,
'& .MuiInputBase-input': {
"& .MuiInputBase-input": {
color: theme.palette.text.primary,
},
'& label': {
"& label": {
color: theme.palette.text.secondary,
},
'& .MuiOutlinedInput-root': {
'& fieldset': {
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: theme.palette.divider,
},
'&:hover fieldset': {
"&:hover fieldset": {
borderColor: theme.palette.text.primary,
},
'&.Mui-focused fieldset': {
"&.Mui-focused fieldset": {
borderColor: theme.palette.primary.main,
},
},
@@ -148,11 +154,9 @@ export default function Ratings({itemId}: { itemId: string }) {
</Button>
</Box>
<Divider sx={{backgroundColor: theme.palette.divider, my: 3}}/>
<Divider sx={{ backgroundColor: theme.palette.divider, my: 3 }} />
<Box>
{getRatings()}
</Box>
<Box>{getRatings()}</Box>
<Snackbar
open={open}

View File

@@ -1,110 +1,129 @@
// api/queries.js
import AccountType, {AdminAccountOperation, CustomerType, SubmitLogin, User} from "../../components/Account";
import OrderType, {OrderPatch} from "../../components/Order";
import AccountType, {
AdminAccountOperation,
CustomerType,
SubmitLogin,
User,
} from "../../components/Account";
import OrderType, { OrderPatch } from "../../components/Order";
import RatingSubmitType from "../../components/RatingSubmit";
import {Item, ItemWithFSImage} from "../../components/Item";
import { Item, ItemWithFSImage } from "../../components/Item";
export const fetchItemList = async () => {
const response = await fetch('http://localhost:8085/article/all');
const response = await fetch("http://localhost:8085/article/all");
if (!response.ok) {
throw new Error('Fehler beim Laden der Items');
throw new Error("Fehler beim Laden der Items");
}
const data = await response.json();
return data;
};
export const fetchItemListWithImage = async () => {
const response = await fetch('http://localhost:8085/article/all/image');
const response = await fetch("http://localhost:8085/article/all/image");
if (!response.ok) {
throw new Error('Fehler beim Laden der Items');
throw new Error("Fehler beim Laden der Items");
}
const data = await response.json();
return data;
};
export const submitRating = async (ratingData: RatingSubmitType) => {
const response = await fetch('http://localhost:8085/review?uuid=' + ratingData.articleId + '&rating=' + ratingData.rating * 2, {
method: 'POST',
const response = await fetch(
"http://localhost:8085/review?uuid=" +
ratingData.articleId +
"&rating=" +
ratingData.rating * 2,
{
method: "POST",
headers: {
'Content-Type': 'text/plain',
"Content-Type": "text/plain",
},
body: ratingData.content,
});
},
);
if (!response.ok) {
throw new Error('Fehler beim Senden der Bewertung');
throw new Error("Fehler beim Senden der Bewertung");
}
const data = await response.json();
return data;
}
};
export const fetchRatingList = async (itemId: string) => {
const response = await fetch('http://localhost:8085/review/all?uuid=' + itemId);
const response = await fetch(
"http://localhost:8085/review/all?uuid=" + itemId,
);
if (!response.ok) {
throw new Error('Fehler beim Laden der Items');
throw new Error("Fehler beim Laden der Items");
}
const data = await response.json();
return data;
};
export const submitOrder = async (data: OrderType) => {
const response = await fetch('http://localhost:8085/order', {
method: 'POST',
const response = await fetch("http://localhost:8085/order", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Fehler beim Senden der Bestellung');
throw new Error("Fehler beim Senden der Bestellung");
}
return await response.json();
}
};
export const submitAccount = async (data: AccountType) => {
const response = await fetch('http://localhost:8085/account', {
method: 'POST',
const response = await fetch("http://localhost:8085/account", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Fehler beim Senden des Accounts');
throw new Error("Fehler beim Senden des Accounts");
}
return await response.json();
}
};
export const submitCustomer = async (data: CustomerType) => {
const response = await fetch('http://localhost:8085/customer', {
method: 'POST',
const response = await fetch("http://localhost:8085/customer", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Fehler beim Senden des Accounts');
throw new Error("Fehler beim Senden des Accounts");
}
return await response.json();
}
};
export const submitLogin = async (loginData: SubmitLogin) => {
const response = await fetch("http://localhost:8085/session?email=" + loginData.email + "&password=" + loginData.password, {
const response = await fetch(
"http://localhost:8085/session?email=" +
loginData.email +
"&password=" +
loginData.password,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(loginData),
});
},
);
if (!response.ok) {
throw new Error("Login failed");
}
@@ -112,7 +131,12 @@ export const submitLogin = async (loginData: SubmitLogin) => {
};
export const fetchAccount = async (loginData: SubmitLogin) => {
const response = await fetch("http://localhost:8085/account?email=" + loginData.email + "&password=" + loginData.password);
const response = await fetch(
"http://localhost:8085/account?email=" +
loginData.email +
"&password=" +
loginData.password,
);
if (!response.ok) {
throw new Error("Login failed");
}
@@ -134,52 +158,74 @@ export const submitRegister = async (registerData: AccountType) => {
};
export const fetchCustomer = async (userId: number) => {
const response = await fetch('http://localhost:8085/customer?id=' + userId);
const response = await fetch("http://localhost:8085/customer?id=" + userId);
if (!response.ok) {
throw new Error('Fehler beim Laden des Customers');
throw new Error("Fehler beim Laden des Customers");
}
const data = await response.json();
return data;
};
export const deleteAccount = async (user: User) => {
const response = await fetch('http://localhost:8085/account?email=' + user.email + '&password=' + user.password, {
method: 'DELETE',
});
const response = await fetch(
"http://localhost:8085/account?email=" +
user.email +
"&password=" +
user.password,
{
method: "DELETE",
},
);
if (!response.ok) {
throw new Error('Fehler beim Löschen des Accounts');
throw new Error("Fehler beim Löschen des Accounts");
}
return await response.json();
};
export const deleteAccountAdmin = async (operation: AdminAccountOperation) => {
const response = await fetch('http://localhost:8085/account/admin?email=' + operation.email + '&uuid=' + operation.uuid + '&id=' + operation.accountId, {
method: 'DELETE',
});
const response = await fetch(
"http://localhost:8085/account/admin?email=" +
operation.email +
"&uuid=" +
operation.uuid +
"&id=" +
operation.accountId,
{
method: "DELETE",
},
);
if (!response.ok) {
throw new Error('Fehler beim Löschen des Accounts');
throw new Error("Fehler beim Löschen des Accounts");
}
return await response.json();
};
export const fetchOrders = async (customerId: number) => {
const response = await fetch('http://localhost:8085/order/all?customerId=' + customerId);
const response = await fetch(
"http://localhost:8085/order/all?customerId=" + customerId,
);
if (!response.ok) {
throw new Error('Fehler beim Laden des Customers');
throw new Error("Fehler beim Laden des Customers");
}
const data = await response.json();
return data;
};
export const fetchAccounts = async (loginData: User) => {
const response = await fetch("http://localhost:8085/account/all?email=" + loginData.email + "&session=" + loginData.session);
const response = await fetch(
"http://localhost:8085/account/all?email=" +
loginData.email +
"&session=" +
loginData.session,
);
if (!response.ok) {
throw new Error("Login failed");
}
return response.json();
};
export const fetchItems = async () => { //TODO: remove and use above
export const fetchItems = async () => {
//TODO: remove and use above
const response = await fetch("http://localhost:8085/article/all");
if (!response.ok) {
throw new Error("fetching items failed");
@@ -188,7 +234,12 @@ export const fetchItems = async () => { //TODO: remove and use above
};
export const fetchStatisticsVolume = async (loginData: User) => {
const response = await fetch("http://localhost:8085/statistics/volume?email=" + loginData.email + "&session=" + loginData.session);
const response = await fetch(
"http://localhost:8085/statistics/volume?email=" +
loginData.email +
"&session=" +
loginData.session,
);
if (!response.ok) {
throw new Error("fetching satistics Volume failed");
}
@@ -196,7 +247,12 @@ export const fetchStatisticsVolume = async (loginData: User) => {
};
export const fetchStatisticsRevenue = async (loginData: User) => {
const response = await fetch("http://localhost:8085/statistics/revenue?email=" + loginData.email + "&session=" + loginData.session);
const response = await fetch(
"http://localhost:8085/statistics/revenue?email=" +
loginData.email +
"&session=" +
loginData.session,
);
if (!response.ok) {
throw new Error("fetching satistics Revenue failed");
}
@@ -204,23 +260,28 @@ export const fetchStatisticsRevenue = async (loginData: User) => {
};
export const editAccount = async (customer: CustomerType) => {
const response = await fetch('http://localhost:8085/customer?id=' + customer.id, {
method: 'PUT',
const response = await fetch(
"http://localhost:8085/customer?id=" + customer.id,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(customer),
});
},
);
if (!response.ok) {
throw new Error('Fehler beim Löschen des Accounts');
throw new Error("Fehler beim Löschen des Accounts");
}
return await response.json();
};
export const orderPatch = async (order: OrderPatch) => {
const response = await fetch("http://localhost:8085/order?id=" + order.id + "&status=" + order.status, {
const response = await fetch(
"http://localhost:8085/order?id=" + order.id + "&status=" + order.status,
{
method: "PATCH",
}
},
);
if (!response.ok) {
throw new Error("Order patch failed");
@@ -229,7 +290,12 @@ export const orderPatch = async (order: OrderPatch) => {
};
export const fetchOrderStatus = async (loginData: User) => {
const response = await fetch("http://localhost:8085/statistics/orderstatus?email=" + loginData.email + "&session=" + loginData.session);
const response = await fetch(
"http://localhost:8085/statistics/orderstatus?email=" +
loginData.email +
"&session=" +
loginData.session,
);
if (!response.ok) {
throw new Error("fetching order status failed");
}
@@ -237,16 +303,25 @@ export const fetchOrderStatus = async (loginData: User) => {
};
export const fetchStockPercent = async (loginData: User) => {
const response = await fetch("http://localhost:8085/statistics/stockpercent?email=" + loginData.email + "&session=" + loginData.session);
const response = await fetch(
"http://localhost:8085/statistics/stockpercent?email=" +
loginData.email +
"&session=" +
loginData.session,
);
if (!response.ok) {
throw new Error("fetching stock% failed");
}
return response.json();
};
export const fetchOrdersAdmin = async (loginData: User) => {
const response = await fetch("http://localhost:8085/order/all/all?email=" + loginData.email + "&session=" + loginData.session);
const response = await fetch(
"http://localhost:8085/order/all/all?email=" +
loginData.email +
"&session=" +
loginData.session,
);
if (!response.ok) {
throw new Error("fetching admin orders failed");
}
@@ -254,25 +329,29 @@ export const fetchOrdersAdmin = async (loginData: User) => {
};
export const updateCustomer = async (customer: CustomerType) => {
const response = await fetch('http://localhost:8085/customer?id=' + customer.id, {
method: 'PUT',
const response = await fetch(
"http://localhost:8085/customer?id=" + customer.id,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(customer),
});
},
);
if (!response.ok) {
throw new Error('Fehler beim Ändern des Customers');
throw new Error("Fehler beim Ändern des Customers");
}
return await response.json();
}
};
export const fetchFarmingStationItemList = async (): Promise<ItemWithFSImage[]> => {
const response = await fetch('http://localhost:8085/farm/articles');
if (!response.ok)
throw new Error('Failed to fetch items');
export const fetchFarmingStationItemList = async (): Promise<
ItemWithFSImage[]
> => {
const response = await fetch("http://localhost:8085/farm/articles");
if (!response.ok) throw new Error("Failed to fetch items");
return response.json();
}
};
export const submitItem = async (item: Item) => {
const response = await fetch("http://localhost:8085/article", {
@@ -290,7 +369,7 @@ export const submitItem = async (item: Item) => {
export const deleteItemQuery = async (uuid: string) => {
const response = await fetch("http://localhost:8085/article?uuid=" + uuid, {
method: "DELETE"
method: "DELETE",
});
if (!response.ok) {
throw new Error("Login failed");
@@ -299,25 +378,38 @@ export const deleteItemQuery = async (uuid: string) => {
};
export const updateAccountAdmin = async (account: AccountType, user: User) => {
const response = await fetch('http://localhost:8085/account/admin?email=' + user.email + "&uuid=" + user.session + "&id=" + account.id + "&admin=" + account.admin, {
method: 'POST',
});
const response = await fetch(
"http://localhost:8085/account/admin?email=" +
user.email +
"&uuid=" +
user.session +
"&id=" +
account.id +
"&admin=" +
account.admin,
{
method: "POST",
},
);
if (!response.ok) {
throw new Error('Fehler beim Ändern des Customers');
throw new Error("Fehler beim Ändern des Customers");
}
return await response.json();
}
};
export const updateItemAdmin = async (item: Item) => {
const response = await fetch('http://localhost:8085/article?uuid=' + item.uuid, {
method: 'PUT',
const response = await fetch(
"http://localhost:8085/article?uuid=" + item.uuid,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(item),
});
},
);
if (!response.ok) {
throw new Error('Fehler beim Ändern des Items');
throw new Error("Fehler beim Ändern des Items");
}
return await response.json();
}
};

View File

@@ -1,15 +1,15 @@
import './components/i18n/i18n';
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import './index.css';
import App from './App.tsx';
import "./components/i18n/i18n";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
const rootElement = document.getElementById('root');
const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Root element not found");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App/>
</StrictMode>
<App />
</StrictMode>,
);

View File

@@ -11,19 +11,23 @@ import {
TextField,
Typography,
} from "@mui/material";
import {useQuery} from "@tanstack/react-query";
import {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {useNavigate} from "react-router-dom";
import {CustomerType, User} from "../components/Account";
import {useAccount} from "../helper/AccountProvider";
import {deleteAccount, editAccount, fetchCustomer} from "../helper/query/Queries";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { CustomerType, User } from "../components/Account";
import { useAccount } from "../helper/AccountProvider";
import {
deleteAccount,
editAccount,
fetchCustomer,
} from "../helper/query/Queries";
import "./pages.css";
export default function Account() {
const {t} = useTranslation();
const { t } = useTranslation();
const navigate = useNavigate();
const {user: userData, logout} = useAccount();
const { user: userData, logout } = useAccount();
const [user, setUser] = useState<CustomerType>({
name: "",
@@ -34,13 +38,15 @@ export default function Account() {
id: userData?.customerId || 0,
});
const [userDataState, setUserDataState] = useState<User>(userData || {
const [userDataState, setUserDataState] = useState<User>(
userData || {
password: "",
email: "",
customerId: 0,
session: "",
isAdmin: false,
});
},
);
useEffect(() => {
if (userData?.customerId) {
@@ -58,20 +64,20 @@ export default function Account() {
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
const [passwordInput, setPasswordInput] = useState("");
const {data} = useQuery<CustomerType>({
const { data } = useQuery<CustomerType>({
queryKey: ["fetchCustomer", userData?.customerId],
queryFn: () => fetchCustomer(userData?.customerId || 0),
retry: 1,
retryDelay: 1000,
});
const {refetch: deleteRefetch} = useQuery({
const { refetch: deleteRefetch } = useQuery({
queryKey: ["deleteAccount", userDataState],
queryFn: () => deleteAccount(userDataState!),
enabled: false,
});
const {refetch: editRefetch} = useQuery({
const { refetch: editRefetch } = useQuery({
queryKey: ["editAccount", form],
queryFn: () => editAccount(form),
enabled: false,
@@ -90,7 +96,7 @@ export default function Account() {
setEdit(false);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({...form, [e.target.name]: e.target.value});
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSave = async () => {
setUser(form);
@@ -116,7 +122,7 @@ export default function Account() {
return;
}
// Passwort in Form aktualisieren (hier z.B. als field "password", anpassen falls anders)
setUserDataState({...userDataState, password: passwordInput});
setUserDataState({ ...userDataState, password: passwordInput });
// Erst User-Daten mit Passwort aktualisieren
try {
@@ -136,13 +142,16 @@ export default function Account() {
return (
<Box
className="page-background page-background-center"
sx={{minHeight: "100vh", justifyContent: "flex-start", pt: 4}}
sx={{ minHeight: "100vh", justifyContent: "flex-start", pt: 4 }}
>
<Paper
elevation={3}
sx={{ p: 4, maxWidth: 500, width: "100%", mx: "auto" }}
>
<Paper elevation={3} sx={{p: 4, maxWidth: 500, width: "100%", mx: "auto"}}>
<Typography variant="h4" gutterBottom>
{t("myAccount")}
</Typography>
<Divider sx={{mb: 3}}/>
<Divider sx={{ mb: 3 }} />
<Stack spacing={2}>
<TextField
label={t("name")}
@@ -185,13 +194,17 @@ export default function Account() {
fullWidth
/>
</Stack>
<Box sx={{display: "flex", gap: 2, mt: 4}}>
<Box sx={{ display: "flex", gap: 2, mt: 4 }}>
{edit ? (
<>
<Button variant="contained" color="primary" onClick={handleSave}>
{t("save")}
</Button>
<Button variant="outlined" color="secondary" onClick={handleCancel}>
<Button
variant="outlined"
color="secondary"
onClick={handleCancel}
>
{t("cancel")}
</Button>
</>
@@ -204,7 +217,7 @@ export default function Account() {
variant="outlined"
color="error"
onClick={handleDeleteClick} // Neu: Passwort-Dialog öffnen
sx={{marginLeft: "auto"}}
sx={{ marginLeft: "auto" }}
>
{t("deleteAccount")}
</Button>

View File

@@ -1,14 +1,27 @@
import {AccountCircle, Category, QueryStats, ReceiptLong,} from "@mui/icons-material";
import {Box, List, ListItem, ListItemButton, ListItemIcon, ListItemText, useTheme,} from "@mui/material";
import {useState} from "react";
import {useTranslation} from "react-i18next";
import {
AccountCircle,
Category,
QueryStats,
ReceiptLong,
} from "@mui/icons-material";
import {
Box,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
useTheme,
} from "@mui/material";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import AccountsInfo from "../helper/adminpanel/AccountsInfo";
import ItemInfo from "../helper/adminpanel/ItemsInfo";
import OrdersInfo from "../helper/adminpanel/OrdersInfo";
import StatisticsInfo from "../helper/adminpanel/StatisticsInfo";
export default function AdminPanel() {
const {t} = useTranslation();
const { t } = useTranslation();
const theme = useTheme();
const [infoStatus, setInfoStatus] = useState<string>("statistics");
@@ -19,33 +32,34 @@ export default function AdminPanel() {
const renderContent = () => {
switch (infoStatus) {
case "statistics":
return <StatisticsInfo/>;
return <StatisticsInfo />;
case "orders":
return <OrdersInfo/>;
return <OrdersInfo />;
case "accounts":
return <AccountsInfo/>;
return <AccountsInfo />;
case "items":
return <ItemInfo/>;
return <ItemInfo />;
default:
return <StatisticsInfo/>;
return <StatisticsInfo />;
}
};
const menuItems = [
{key: "statistics", icon: <QueryStats/>, label: t("statistics")},
{key: "orders", icon: <ReceiptLong/>, label: t("orders")},
{key: "accounts", icon: <AccountCircle/>, label: t("accounts")},
{key: "items", icon: <Category/>, label: t("items")},
{ key: "statistics", icon: <QueryStats />, label: t("statistics") },
{ key: "orders", icon: <ReceiptLong />, label: t("orders") },
{ key: "accounts", icon: <AccountCircle />, label: t("accounts") },
{ key: "items", icon: <Category />, label: t("items") },
];
return (
<Box className="page-container" sx={{color: theme.palette.text.primary}}>
<Box className="page-container" sx={{ color: theme.palette.text.primary }}>
<div
className="home-page-background"
style={{
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.paper,
}}>
}}
>
{/* Sidebar */}
<Box className="sidebar">
<nav aria-label={t("mainAdminMenu")}>
@@ -63,9 +77,12 @@ export default function AdminPanel() {
"&:hover": {
bgcolor: theme.palette.action.hover,
},
}}>
<ListItemIcon sx={{color: "inherit"}}>{item.icon}</ListItemIcon>
<ListItemText primary={item.label}/>
}}
>
<ListItemIcon sx={{ color: "inherit" }}>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}

View File

@@ -1,90 +1,129 @@
import {Box, Divider, Typography} from "@mui/material";
import { Box, Divider, Typography } from "@mui/material";
import "./pages.css";
export default function Impressum() {
return (
<Box className="impressum-container">
<Typography variant="h4" sx={{color: 'text.primary', mb: 2}}>
<Typography variant="h4" sx={{ color: "text.primary", mb: 2 }}>
Impressum
</Typography>
<Typography variant="body1" sx={{color: 'text.primary', mb: 4}}>
Hochschule für Technik und Wirtschaft<br/>
des Saarlandes<br/>
Goebenstraße 40<br/>
66117 Saarbrücken<br/><br/>
Telefon: (0681) 58 67 - 0<br/>
Telefax: (0681) 58 67 - 122<br/>
E-Mail: info@htwsaar.de<br/><br/>
Aufsichtsbehörde:<br/>
<Typography variant="body1" sx={{ color: "text.primary", mb: 4 }}>
Hochschule für Technik und Wirtschaft
<br />
des Saarlandes
<br />
Goebenstraße 40
<br />
66117 Saarbrücken
<br />
<br />
Telefon: (0681) 58 67 - 0<br />
Telefax: (0681) 58 67 - 122
<br />
E-Mail: info@htwsaar.de
<br />
<br />
Aufsichtsbehörde:
<br />
Ministerium der Finanzen und für Wissenschaft des Saarlandes
</Typography>
<Divider className="contact-divider"/>
<Divider className="contact-divider" />
<Typography variant="h5" sx={{color: 'text.primary', mt: 4, mb: 2}}>
<Typography variant="h5" sx={{ color: "text.primary", mt: 4, mb: 2 }}>
Datenschutzerklärung
</Typography>
<Typography variant="body1" sx={{color: 'text.primary', mb: 2}}>
<Typography variant="body1" sx={{ color: "text.primary", mb: 2 }}>
Personenbezogene Daten (nachfolgend zumeist nur Daten genannt) ...
</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 variant="body1" sx={{ color: "text.primary", mb: 2 }}>
Gemäß Art. 4 Ziffer 1. der Verordnung (EU) 2016/679, also der
Datenschutz-Grundverordnung ...
</Typography>
<Typography variant="body1" sx={{color: 'text.primary', mb: 2}}>
Unsere Datenschutzerklärung ist wie folgt gegliedert:<br/>
I. Informationen über uns als Verantwortliche<br/>
II. Rechte der Nutzer und Betroffenen<br/>
<Typography variant="body1" sx={{ color: "text.primary", mb: 2 }}>
Unsere Datenschutzerklärung ist wie folgt gegliedert:
<br />
I. Informationen über uns als Verantwortliche
<br />
II. Rechte der Nutzer und Betroffenen
<br />
III. Informationen zur Datenverarbeitung
</Typography>
<Typography variant="h6" sx={{color: 'text.primary', mt: 4, mb: 1}}>
<Typography variant="h6" sx={{ color: "text.primary", mt: 4, mb: 1 }}>
I. Informationen über uns als Verantwortliche
</Typography>
<Typography variant="body1" sx={{color: 'text.primary', mb: 2}}>
<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 variant="h6" sx={{ color: "text.primary", mt: 4, mb: 1 }}>
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 variant="body1" sx={{ color: "text.primary", mb: 2 }}>
Mit Blick auf die nachfolgend noch näher beschriebene Datenverarbeitung
haben die Nutzer und Betroffenen ...
</Typography>
<Box component="ul" sx={{listStyle: 'none', p: 0, m: 0}}>
<li><Typography variant="body2" sx={{color: 'text.primary', mb: 0.5}}> Auskunft über die verarbeiteten
Daten (Art. 15 DSGVO)</Typography></li>
<li><Typography variant="body2" sx={{color: 'text.primary', mb: 0.5}}> Berichtigung unrichtiger Daten
(Art. 16 DSGVO)</Typography></li>
<li><Typography variant="body2" sx={{color: 'text.primary', mb: 0.5}}> Löschung der Daten (Art. 17
DSGVO)</Typography></li>
<li><Typography variant="body2" sx={{color: 'text.primary', mb: 0.5}}> Einschränkung der Verarbeitung
(Art. 18 DSGVO)</Typography></li>
<li><Typography variant="body2" sx={{color: 'text.primary', mb: 0.5}}> Datenübertragbarkeit (Art. 20
DSGVO)</Typography></li>
<Box component="ul" sx={{ listStyle: "none", p: 0, m: 0 }}>
<li>
<Typography variant="body2" sx={{ color: "text.primary", mb: 0.5 }}>
{" "}
Auskunft über die verarbeiteten Daten (Art. 15 DSGVO)
</Typography>
</li>
<li>
<Typography variant="body2" sx={{ color: "text.primary", mb: 0.5 }}>
{" "}
Berichtigung unrichtiger Daten (Art. 16 DSGVO)
</Typography>
</li>
<li>
<Typography variant="body2" sx={{ color: "text.primary", mb: 0.5 }}>
{" "}
Löschung der Daten (Art. 17 DSGVO)
</Typography>
</li>
<li>
<Typography variant="body2" sx={{ color: "text.primary", mb: 0.5 }}>
{" "}
Einschränkung der Verarbeitung (Art. 18 DSGVO)
</Typography>
</li>
<li>
<Typography variant="body2" sx={{ color: "text.primary", mb: 0.5 }}>
{" "}
Datenübertragbarkeit (Art. 20 DSGVO)
</Typography>
</li>
</Box>
<Typography variant="h6" sx={{color: 'text.primary', mt: 4, mb: 1}}>
<Typography variant="h6" sx={{ color: "text.primary", mt: 4, mb: 1 }}>
III. Informationen zur Datenverarbeitung
</Typography>
<Typography variant="body1" sx={{color: 'text.primary', mb: 2}}>
<Typography variant="body1" sx={{ color: "text.primary", mb: 2 }}>
Ihre bei Nutzung unseres Internetauftritts verarbeiteten Daten ...
</Typography>
{/* Du kannst einfach alle weiteren Absätze so fortsetzen copy & paste,
jeweils in: <Typography variant="body1" sx={{ color: 'text.primary' }}>…</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 variant="body2" sx={{ color: "text.primary", mt: 4 }}>
Mehr Infos unter:{" "}
<a
href="https://www.cloudflare.com/privacypolicy/"
target="_blank"
rel="noopener noreferrer"
>
CloudFlare Datenschutzerklärung
</a>
</Typography>
</Box>
);

View File

@@ -1,29 +1,44 @@
import {Box, Button, Typography, useTheme} from "@mui/material";
import {useTranslation} from "react-i18next";
import {useState} from "react";
import {useQuery} from "@tanstack/react-query";
import {useBasket} from "../helper/BasketProvider";
import { Box, Button, Typography, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useBasket } from "../helper/BasketProvider";
import ItemCard from "../helper/homepage/ItemCard";
import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart';
import AddShoppingCartIcon from "@mui/icons-material/AddShoppingCart";
import farmingStation from '../assets/fscomponents/fs_components_0.png';
import {ItemWithFSImage} from "../components/Item";
import {fetchFarmingStationItemList} from "../helper/query/Queries";
import farmingStation from "../assets/fscomponents/fs_components_0.png";
import { ItemWithFSImage } from "../components/Item";
import { fetchFarmingStationItemList } from "../helper/query/Queries";
"../components/Item";
("../components/Item");
export default function FSComponents() {
const {t} = useTranslation();
const { t } = useTranslation();
const theme = useTheme();
const {addToBasket} = useBasket();
const { addToBasket } = useBasket();
const [hoverIndex, setHoverIndex] = useState<number | null>(null);
// Sehr sehr dummer Weg das zu machen, aber wird später noch refactored
const wantedIds = ["60", "67", "68", "69", "70", "71", "72", "73", "74", "75"];
const wantedIds = [
"60",
"67",
"68",
"69",
"70",
"71",
"72",
"73",
"74",
"75",
];
// Daten mit react-query laden
const {data = [], isLoading, error} = useQuery<ItemWithFSImage[]>({
queryKey: ['fetchFarmingStationItemList'],
const {
data = [],
isLoading,
error,
} = useQuery<ItemWithFSImage[]>({
queryKey: ["fetchFarmingStationItemList"],
queryFn: fetchFarmingStationItemList,
retry: 3,
retryDelay: 1000,
@@ -31,37 +46,51 @@ export default function FSComponents() {
// Button-Funktion: alle gefilterten Items in den Warenkorb packen
const handleAddAllToCart = () => {
data.forEach(item => {
data.forEach((item) => {
addToBasket(item, 1);
});
};
if (isLoading) return <Typography>{t('loading')}</Typography>;
if (error) return <Typography color="error">{t('errorLoadingItems')}</Typography>;
if (isLoading) return <Typography>{t("loading")}</Typography>;
if (error)
return <Typography color="error">{t("errorLoadingItems")}</Typography>;
return (
<Box sx={{width: '100%', display: 'flex', maxWidth: 1600, mx: 'auto', pt: 2, height: '100vh'}}>
<Box
sx={{
width: "100%",
display: "flex",
maxWidth: 1600,
mx: "auto",
pt: 2,
height: "100vh",
}}
>
{/* Bild links */}
<Box sx={{
width: 'auto',
height: '90vh',
position: 'sticky',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
<Box
sx={{
width: "auto",
height: "90vh",
position: "sticky",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: 2,
overflow: 'hidden',
}}>
overflow: "hidden",
}}
>
<Box
component="img"
src={hoverIndex !== null ? data[hoverIndex].farmImage : farmingStation}
alt={t('componentsFarmingStation')}
src={
hoverIndex !== null ? data[hoverIndex].farmImage : farmingStation
}
alt={t("componentsFarmingStation")}
sx={{
width: '100%',
height: 'auto',
maxHeight: '80vh',
objectFit: 'contain',
width: "100%",
height: "auto",
maxHeight: "80vh",
objectFit: "contain",
borderRadius: 2,
border: `4px solid ${theme.palette.primary.main}`,
marginBottom: 2,
@@ -70,23 +99,30 @@ export default function FSComponents() {
<Button
variant="contained"
fullWidth
startIcon={<AddShoppingCartIcon/>}
startIcon={<AddShoppingCartIcon />}
onClick={handleAddAllToCart}
>
{t('addAllToCart')}
{t("addAllToCart")}
</Button>
</Box>
{/* Items rechts */}
<Box sx={{
width: '60%',
height: '90vh',
overflowY: 'auto',
<Box
sx={{
width: "60%",
height: "90vh",
overflowY: "auto",
padding: 2,
}}>
}}
>
<Box mb={2}>
<Typography variant="h4" align="center" gutterBottom sx={{color: 'text.primary'}}>
{t('componentsFarmingStation')}
<Typography
variant="h4"
align="center"
gutterBottom
sx={{ color: "text.primary" }}
>
{t("componentsFarmingStation")}
</Typography>
</Box>
<Box className="cardgrid">
@@ -96,7 +132,7 @@ export default function FSComponents() {
onMouseEnter={() => setHoverIndex(index)}
onMouseLeave={() => setHoverIndex(null)}
>
<ItemCard item={item}/>
<ItemCard item={item} />
</div>
))}
</Box>

View File

@@ -1,44 +1,46 @@
import {Alert, Box, useTheme} from "@mui/material";
import {useQuery} from "@tanstack/react-query";
import {useEffect, useMemo, useRef, useState} from "react";
import {useTranslation} from "react-i18next";
import {useLocation, useNavigate} from "react-router-dom";
import { Alert, Box, useTheme } from "@mui/material";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
import ItemWithImage from "../components/Item";
import FilterItem from "../helper/homepage/FilterItem";
import ItemCard from "../helper/homepage/ItemCard";
import PriceSlider from "../helper/homepage/PriceSlider";
import {fetchItemListWithImage} from '../helper/query/Queries';
import { fetchItemListWithImage } from "../helper/query/Queries";
import "./pages.css"; // Import der CSS-Datei
export default function Home() {
const {t} = useTranslation();
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const theme = useTheme();
const [searchQuery, setSearchQuery] = useState<string | null>(null);
const categoriesFilter = useMemo(() => [
{value: "", label: t("allCategories")},
{value: "Seeds", label: t("seeds")},
{value: "GardenSupplies", label: t("gardenSupplies")},
{value: "TechnicalComponents", label: t("technicalComponents")},
{value: "Other", label: t("other")}
], [t]);
const categoriesFilter = useMemo(
() => [
{ value: "", label: t("allCategories") },
{ value: "Seeds", label: t("seeds") },
{ value: "GardenSupplies", label: t("gardenSupplies") },
{ value: "TechnicalComponents", label: t("technicalComponents") },
{ value: "Other", label: t("other") },
],
[t],
);
const ratingFilter = [
{value: "", label: t('allRatings')},
...[5, 4, 3, 2, 1].map(value => ({
{ value: "", label: t("allRatings") },
...[5, 4, 3, 2, 1].map((value) => ({
value: value.toString(),
label: value.toString()
}))
label: value.toString(),
})),
];
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [selectedRating, setSelectedRating] = useState<string | null>(null);
const {data = [], isLoading} = useQuery<ItemWithImage[]>({
queryKey: ['fetchItemListWithImage'],
const { data = [], isLoading } = useQuery<ItemWithImage[]>({
queryKey: ["fetchItemListWithImage"],
queryFn: fetchItemListWithImage,
retry: 3, // Versucht es 3-mal erneut
retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms)
@@ -47,10 +49,12 @@ export default function Home() {
const items: ItemWithImage[] = useMemo(() => data || [], [data]);
const discountedPrices = items.map(
(item) => item.price100 * (1 - item.discount100 / 100)
(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 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,
@@ -72,7 +76,9 @@ export default function Home() {
return items
.filter((item) => {
const discountedPrice = item.price100 * (1 - item.discount100 / 100);
return discountedPrice >= priceRange[0] && discountedPrice <= priceRange[1];
return (
discountedPrice >= priceRange[0] && discountedPrice <= priceRange[1]
);
})
.filter((item) => {
if (!selectedCategory) return true;
@@ -81,16 +87,14 @@ export default function Home() {
.filter((item) => {
if (!selectedRating) return true;
const rating = item.rating;
return rating >= (Number(selectedRating) * 2);
return rating >= Number(selectedRating) * 2;
})
.filter((item) => {
if (!searchQuery) return true;
return (item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
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);
@@ -98,7 +102,6 @@ export default function Home() {
setSearchQuery(query);
}, [location.search]);
// Container Ref
const containerRef = useRef<HTMLDivElement>(null);
@@ -140,7 +143,7 @@ export default function Home() {
return (
<div
className="home-page-background"
style={{backgroundColor: theme.palette.homepage}}
style={{ backgroundColor: theme.palette.homepage }}
>
<div className="sidebar sidebar-filter">
<FilterItem
@@ -165,10 +168,11 @@ export default function Home() {
</div>
<div
className="home-page-background"
style={{backgroundColor: theme.palette.homepage}}
style={{ backgroundColor: theme.palette.homepage }}
>
{isLoading && t('loading')}
{!isLoading && <main
{isLoading && t("loading")}
{!isLoading && (
<main
className="page-background page-background-center"
ref={containerRef}
>
@@ -180,7 +184,7 @@ export default function Home() {
sx={{
bgcolor: theme.palette.error.main,
color: theme.palette.getContrastText(
theme.palette.error.main
theme.palette.error.main,
),
width: "100%",
justifyContent: "center",
@@ -189,12 +193,13 @@ export default function Home() {
{t("noItemsFound")}
</Alert>
) : (
filteredItems.map(item => (
<ItemCard key={item.id} item={item}/>
filteredItems.map((item) => (
<ItemCard key={item.id} item={item} />
))
)}
</Box>
</main>}
</main>
)}
</div>
</div>
);

View File

@@ -1,12 +1,11 @@
import {Box, Button, Typography, useTheme} from "@mui/material";
import {useNavigate} from "react-router-dom";
import { Box, Button, Typography, useTheme } from "@mui/material";
import { useNavigate } from "react-router-dom";
import "./pages.css";
import {useTranslation} from "react-i18next";
import { useTranslation } from "react-i18next";
export default function NoPage() {
const theme = useTheme();
const {t} = useTranslation();
const { t } = useTranslation();
const navigate = useNavigate();
const handleGoHome = () => {
@@ -16,16 +15,16 @@ export default function NoPage() {
return (
<Box
className="no-page-container"
sx={{color: theme.palette.text.primary}}
sx={{ color: theme.palette.text.primary }}
>
<Typography variant="h1" className="no-page-title">
404
</Typography>
<Typography variant="h5" className="no-page-subtitle">
{t('pageDoesNotExist')}
{t("pageDoesNotExist")}
</Typography>
<Typography variant="body1" className="no-page-description">
{t('wrongTurn')}
{t("wrongTurn")}
</Typography>
<Button
variant="contained"
@@ -34,7 +33,7 @@ export default function NoPage() {
className="no-page-button"
onClick={handleGoHome}
>
{t('backToHome')}
{t("backToHome")}
</Button>
</Box>
);

View File

@@ -13,24 +13,23 @@ import {
Stack,
Tab,
Tabs,
Typography
Typography,
} from "@mui/material";
import {useQuery} from "@tanstack/react-query";
import {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import OrderType, {OrderStatusEnum} from "../components/Order";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import OrderType, { OrderStatusEnum } from "../components/Order";
import "./pages.css";
import {useAccount} from "../helper/AccountProvider";
import {fetchOrders, orderPatch} from "../helper/query/Queries";
import { useAccount } from "../helper/AccountProvider";
import { fetchOrders, orderPatch } from "../helper/query/Queries";
export default function Orders() {
const { user } = useAccount();
const [orders, setOrders] = useState<OrderType[]>([]);
const {user} = useAccount();
const [orders, setOrders] = useState<OrderType[]>([])
const {data: accountOrders, refetch} = useQuery<OrderType[]>({
queryKey: ['fetchOrders', user?.customerId], // Hier sollte die tatsächliche Kunden-ID verwendet werden
queryFn: () => user ? fetchOrders(user.customerId) : Promise.resolve([]), // Simulierte API-Antwort
const { data: accountOrders, refetch } = useQuery<OrderType[]>({
queryKey: ["fetchOrders", user?.customerId], // Hier sollte die tatsächliche Kunden-ID verwendet werden
queryFn: () => (user ? fetchOrders(user.customerId) : Promise.resolve([])), // Simulierte API-Antwort
enabled: !!user, // Nur ausführen, wenn user existiert
retry: 3, // Versucht es 3-mal erneut
retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms)
@@ -41,20 +40,36 @@ export default function Orders() {
console.log("Orders fetched:", accountOrders);
}, [accountOrders]);
const {t} = useTranslation();
const { t } = useTranslation();
const [tab, setTab] = useState(0);
const [selectedOrder, setSelectedOrder] = useState<OrderType | null>(null);
const activeOrders = orders.filter(o => o.status === OrderStatusEnum.ISSUES || o.status === OrderStatusEnum.IN_PROGRESS || o.status === OrderStatusEnum.ORDERED);
const inactiveOrders = orders.filter(o => o.status === OrderStatusEnum.CANCELLED || o.status === OrderStatusEnum.DELIVERED);
const activeOrders = orders.filter(
(o) =>
o.status === OrderStatusEnum.ISSUES ||
o.status === OrderStatusEnum.IN_PROGRESS ||
o.status === OrderStatusEnum.ORDERED,
);
const inactiveOrders = orders.filter(
(o) =>
o.status === OrderStatusEnum.CANCELLED ||
o.status === OrderStatusEnum.DELIVERED,
);
const handleTabChange = (_: React.SyntheticEvent, newValue: number) => setTab(newValue);
const handleTabChange = (_: React.SyntheticEvent, newValue: number) =>
setTab(newValue);
const {refetch: cancleOrder} = useQuery({
queryKey: ["orderPatch", {id: selectedOrder?.id || -1, status: OrderStatusEnum.CANCELLED}],
queryFn: () => orderPatch({id: selectedOrder?.id || -1, status: OrderStatusEnum.CANCELLED}),
const { refetch: cancleOrder } = useQuery({
queryKey: [
"orderPatch",
{ id: selectedOrder?.id || -1, status: OrderStatusEnum.CANCELLED },
],
queryFn: () =>
orderPatch({
id: selectedOrder?.id || -1,
status: OrderStatusEnum.CANCELLED,
}),
retry: 0,
retryDelay: 1000,
enabled: false,
@@ -67,59 +82,79 @@ export default function Orders() {
};
return (
<Box className="page-background page-background-center" sx={{minHeight: "100vh", pt: 4}}>
<Paper elevation={3} sx={{p: 4, maxWidth: 700, width: "100%", mx: "auto"}}>
<Box
className="page-background page-background-center"
sx={{ minHeight: "100vh", pt: 4 }}
>
<Paper
elevation={3}
sx={{ p: 4, maxWidth: 700, width: "100%", mx: "auto" }}
>
<Typography variant="h4" gutterBottom>
{t('myOrders')}
{t("myOrders")}
</Typography>
<Tabs value={tab} onChange={handleTabChange} sx={{mb: 3}}>
<Tab label={t('active')}/>
<Tab label={t('previous')}/>
<Tabs value={tab} onChange={handleTabChange} sx={{ mb: 3 }}>
<Tab label={t("active")} />
<Tab label={t("previous")} />
</Tabs>
<Divider sx={{mb: 2}}/>
<Divider sx={{ mb: 2 }} />
{tab === 0 ? (
activeOrders.length > 0 ? (
<List>
{activeOrders.map(order => (
<ListItemButton key={order.id} onClick={() => setSelectedOrder(order)}>
{activeOrders.map((order) => (
<ListItemButton
key={order.id}
onClick={() => setSelectedOrder(order)}
>
<ListItemText
primary={`${t('order')} #${order.id}${order.status}${new Date(order.time).toUTCString()}`}
secondary={`${t('sum')}: ${(order.total / 100).toFixed(2)} € • ${order.orderItems.length} ${t('items')}`}
primary={`${t("order")} #${order.id}${order.status}${new Date(order.time).toUTCString()}`}
secondary={`${t("sum")}: ${(order.total / 100).toFixed(2)} € • ${order.orderItems.length} ${t("items")}`}
/>
</ListItemButton>
))}
</List>
) : (
<Typography color="text.secondary">{t('noActiveOrders')}</Typography>
<Typography color="text.secondary">
{t("noActiveOrders")}
</Typography>
)
) : (
inactiveOrders.length > 0 ? (
) : inactiveOrders.length > 0 ? (
<List>
{inactiveOrders.map(order => (
<ListItemButton key={order.id} onClick={() => setSelectedOrder(order)}>
{inactiveOrders.map((order) => (
<ListItemButton
key={order.id}
onClick={() => setSelectedOrder(order)}
>
<ListItemText
primary={`${t('order')} #${order.id}${order.status}${new Date(order.time).toUTCString()}`}
secondary={`${t('sum')}: ${(order.total / 100).toFixed(2)} € • ${order.orderItems.length} ${t('items')}`}
primary={`${t("order")} #${order.id}${order.status}${new Date(order.time).toUTCString()}`}
secondary={`${t("sum")}: ${(order.total / 100).toFixed(2)} € • ${order.orderItems.length} ${t("items")}`}
/>
</ListItemButton>
))}
</List>
) : (
<Typography color="text.secondary">{t('noPreviousOrders')}</Typography>
)
<Typography color="text.secondary">
{t("noPreviousOrders")}
</Typography>
)}
<Dialog open={!!selectedOrder} onClose={() => setSelectedOrder(null)} maxWidth="sm" fullWidth>
<Dialog
open={!!selectedOrder}
onClose={() => setSelectedOrder(null)}
maxWidth="sm"
fullWidth
>
<DialogTitle>
{`${selectedOrder?.status === OrderStatusEnum.IN_PROGRESS ? t('activeOrder') : t('previousOrder')} #${selectedOrder?.id}`}
{`${selectedOrder?.status === OrderStatusEnum.IN_PROGRESS ? t("activeOrder") : t("previousOrder")} #${selectedOrder?.id}`}
</DialogTitle>
<DialogContent dividers>
{selectedOrder && (
<Stack spacing={2}>
<Typography
variant="subtitle1">{`${t('orderDate')}: ${new Date(selectedOrder.time).toUTCString()}`}</Typography>
<Divider/>
<Typography variant="subtitle2">{t('orderedItems')}:</Typography>
<Typography variant="subtitle1">{`${t("orderDate")}: ${new Date(selectedOrder.time).toUTCString()}`}</Typography>
<Divider />
<Typography variant="subtitle2">
{t("orderedItems")}:
</Typography>
<List dense>
{selectedOrder.orderItems.map((item, idx) => (
<ListItemText
@@ -128,19 +163,20 @@ export default function Orders() {
/>
))}
</List>
<Divider/>
<Typography
variant="h6">{`${t('sum')}: ${(selectedOrder.total / 100).toFixed(2)}`}</Typography>
<Divider />
<Typography variant="h6">{`${t("sum")}: ${(selectedOrder.total / 100).toFixed(2)}`}</Typography>
</Stack>
)}
</DialogContent>
<DialogActions>
{(selectedOrder?.status === OrderStatusEnum.ISSUES || selectedOrder?.status === OrderStatusEnum.IN_PROGRESS || selectedOrder?.status === OrderStatusEnum.ORDERED) && (
{(selectedOrder?.status === OrderStatusEnum.ISSUES ||
selectedOrder?.status === OrderStatusEnum.IN_PROGRESS ||
selectedOrder?.status === OrderStatusEnum.ORDERED) && (
<Button color="error" onClick={() => handleCancelOrder()}>
{t('cancelOrder')}
{t("cancelOrder")}
</Button>
)}
<Button onClick={() => setSelectedOrder(null)}>{t('close')}</Button>
<Button onClick={() => setSelectedOrder(null)}>{t("close")}</Button>
</DialogActions>
</Dialog>
</Paper>

View File

@@ -13,28 +13,35 @@ import {
StepLabel,
Stepper,
TextField,
Typography
} from '@mui/material';
import {useMutation, useQuery} from '@tanstack/react-query';
import {TFunction} from 'i18next';
import React, {useEffect, useState} from 'react';
import {useTranslation} from "react-i18next";
import {useNavigate} from 'react-router-dom';
import {CustomerType} from '../components/Account';
import Item from '../components/Item';
import OrderType, {OrderStatusEnum} from '../components/Order';
import {useAccount} from '../helper/AccountProvider';
import {BasketItem, useBasket} from '../helper/BasketProvider';
import {fetchCustomer, submitCustomer, submitOrder} from '../helper/query/Queries';
Typography,
} from "@mui/material";
import { useMutation, useQuery } from "@tanstack/react-query";
import { TFunction } from "i18next";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { CustomerType } from "../components/Account";
import Item from "../components/Item";
import OrderType, { OrderStatusEnum } from "../components/Order";
import { useAccount } from "../helper/AccountProvider";
import { BasketItem, useBasket } from "../helper/BasketProvider";
import {
fetchCustomer,
submitCustomer,
submitOrder,
} from "../helper/query/Queries";
function getDiscountedPrice(item: Item): number {
return (item.price100 / 100 * (100 - item.discount100) / 100);
return ((item.price100 / 100) * (100 - item.discount100)) / 100;
}
function generateBasket(t: TFunction<"translation", undefined>, basket: BasketItem[]) {
function generateBasket(
t: TFunction<"translation", undefined>,
basket: BasketItem[],
) {
return basket.length === 0 ? (
<Typography color="error" sx={{my: 2}}>
{t('basketEmpty')}
<Typography color="error" sx={{ my: 2 }}>
{t("basketEmpty")}
</Typography>
) : (
<List>
@@ -46,56 +53,78 @@ function generateBasket(t: TFunction<"translation", undefined>, basket: BasketIt
/>
<div className="rightBound">
{`${(item.quantity * getDiscountedPrice(item.item)).toFixed(2)}`}<br/>
{item.item.discount100 > 0 ? <a className='rightBound red'>{-item.item.discount100}%</a> : ""}
{`${(item.quantity * getDiscountedPrice(item.item)).toFixed(2)}`}
<br />
{item.item.discount100 > 0 ? (
<a className="rightBound red">{-item.item.discount100}%</a>
) : (
""
)}
</div>
</ListItem>
))}
</List>
)
);
}
function generateTotal(t: TFunction<"translation", undefined>, basket: BasketItem[]) {
return basket.length === 0 ? "" :
<div className='rightBound'>
{t('total') + ": " + basket.map((item) => item.quantity * getDiscountedPrice(item.item))
.reduce((prev: number, cur: number) => prev + cur, 0).toFixed(2) + ``}
function generateTotal(
t: TFunction<"translation", undefined>,
basket: BasketItem[],
) {
return basket.length === 0 ? (
""
) : (
<div className="rightBound">
{t("total") +
": " +
basket
.map((item) => item.quantity * getDiscountedPrice(item.item))
.reduce((prev: number, cur: number) => prev + cur, 0)
.toFixed(2) +
``}
</div>
);
}
export default function Payment() {
const {t} = useTranslation();
const {basket, clearBasket} = useBasket();
const { t } = useTranslation();
const { basket, clearBasket } = useBasket();
const navigator = useNavigate();
const [activeStep, setActiveStep] = useState(0);
const [shippingDetails, setShippingDetails] = useState<CustomerType>({
id: 0, // This will be set by the backend or user data
name: '',
surname: '',
address: '',
zip: '',
country: 'Deutschland',
name: "",
surname: "",
address: "",
zip: "",
country: "Deutschland",
});
const [orderNumber, setOrderNumber] = useState<string | null>(null);
const steps = [t('reviewCart'), t('shippingDetails'), t('payment'), t('orderSummary')];
const {user} = useAccount();
const steps = [
t("reviewCart"),
t("shippingDetails"),
t("payment"),
t("orderSummary"),
];
const { user } = useAccount();
const submitOrderData: OrderType = {
id: 0, // This will be set by the backend
customerId: user ? user.customerId : 0, // Use user ID if logged in, otherwise 0
time: Date.now(),
status: OrderStatusEnum.ORDERED, // Initial status when order is placed
orderItems: basket.map(item => ({
orderItems: basket.map((item) => ({
id: item.item.id,
amount: item.quantity,
article: item.item.uuid, // Assuming UUID is the identifier for the item
})),
total: basket.reduce((total, item) => total + (item.quantity * getDiscountedPrice(item.item)), 0),
total: basket.reduce(
(total, item) => total + item.quantity * getDiscountedPrice(item.item),
0,
),
};
const {refetch: refetchCustomer} = useQuery({
const { refetch: refetchCustomer } = useQuery({
queryKey: ["submitCustomer", shippingDetails],
queryFn: () => submitCustomer(shippingDetails),
retry: 0,
@@ -104,15 +133,13 @@ export default function Payment() {
});
const showAlert = () => {
return <Alert>
//TODO:
</Alert>
return <Alert>//TODO:</Alert>;
};
const {refetch: customerData} = useQuery<CustomerType>({
queryKey: ['fetchCustomer', user?.customerId],
const { refetch: customerData } = useQuery<CustomerType>({
queryKey: ["fetchCustomer", user?.customerId],
queryFn: () => fetchCustomer(user?.customerId || 0), // Funktion zum Abrufen der Kundendaten
enabled: false
enabled: false,
});
useEffect(() => {
@@ -130,9 +157,8 @@ export default function Payment() {
fetchShippingDetails();
}, [user, customerData]);
// Verwende useMutation statt useQuery für submitOrder
const {mutateAsync: submitOrderMutation} = useMutation({
const { mutateAsync: submitOrderMutation } = useMutation({
mutationFn: (orderData: OrderType) => submitOrder(orderData),
});
@@ -175,7 +201,7 @@ export default function Payment() {
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const {name, value} = e.target;
const { name, value } = e.target;
setShippingDetails((prevDetails) => ({
...prevDetails,
[name]: value,
@@ -188,11 +214,11 @@ export default function Payment() {
// Hilfsfunktion prüfen, ob alle Pflichtfelder ausgefüllt sind
const isShippingDetailsValid = () => {
return (
shippingDetails.name.trim() !== '' &&
shippingDetails.surname.trim() !== '' &&
shippingDetails.address.trim() !== '' &&
shippingDetails.zip.trim() !== '' &&
shippingDetails.country.trim() !== ''
shippingDetails.name.trim() !== "" &&
shippingDetails.surname.trim() !== "" &&
shippingDetails.address.trim() !== "" &&
shippingDetails.zip.trim() !== "" &&
shippingDetails.country.trim() !== ""
);
};
@@ -202,13 +228,17 @@ export default function Payment() {
return (
<Box>
<Typography variant="h6" gutterBottom>
{t('reviewCart')}
{t("reviewCart")}
</Typography>
{generateBasket(t, basket)}
<Divider sx={{my: 2}}/>
<Button variant="outlined" color="error" onClick={handleClearBasket}
disabled={basket.length === 0}>
{t('clearCart')}
<Divider sx={{ my: 2 }} />
<Button
variant="outlined"
color="error"
onClick={handleClearBasket}
disabled={basket.length === 0}
>
{t("clearCart")}
</Button>
{generateTotal(t, basket)}
</Box>
@@ -217,13 +247,12 @@ export default function Payment() {
return (
<Box>
<Typography variant="h6" gutterBottom>
{t('shippingDetails')}
{t("shippingDetails")}
</Typography>
<Grid container spacing={2}>
<TextField
fullWidth
label={t('firstName')}
label={t("firstName")}
name="name"
value={shippingDetails.name}
onChange={handleInputChange}
@@ -232,7 +261,7 @@ export default function Payment() {
<TextField
fullWidth
label={t('lastName')}
label={t("lastName")}
name="surname"
value={shippingDetails.surname}
onChange={handleInputChange}
@@ -241,7 +270,7 @@ export default function Payment() {
<TextField
fullWidth
label={t('address')}
label={t("address")}
name="address"
value={shippingDetails.address}
onChange={handleInputChange}
@@ -250,7 +279,7 @@ export default function Payment() {
<TextField
fullWidth
label={t('postalCode')}
label={t("postalCode")}
name="zip"
value={shippingDetails.zip}
onChange={handleInputChange}
@@ -259,7 +288,7 @@ export default function Payment() {
<TextField
fullWidth
label={t('country')}
label={t("country")}
name="country"
value={shippingDetails.country}
onChange={handleInputChange}
@@ -272,38 +301,39 @@ export default function Payment() {
return (
<Box>
<Typography variant="h6" gutterBottom>
{t('payment')}
</Typography>
<Typography variant="body1">
{t('paymentNotAvailable')}
{t("payment")}
</Typography>
<Typography variant="body1">{t("paymentNotAvailable")}</Typography>
</Box>
);
case 3:
return (
<Box>
<Typography variant="h6" gutterBottom>
{t('orderSummary')}
{t("orderSummary")}
</Typography>
<Typography variant="body1" gutterBottom>
{t('thanksForOrder')}
{t("thanksForOrder")}
</Typography>
<Typography variant="body2" gutterBottom>
{t('yourOrderNumber')}: <strong>{orderNumber}</strong>
{t("yourOrderNumber")}: <strong>{orderNumber}</strong>
</Typography>
<Divider sx={{my: 2}}/>
<Typography variant="h6">{t('shippingDetails')}:</Typography>
<Divider sx={{ my: 2 }} />
<Typography variant="h6">{t("shippingDetails")}:</Typography>
<Typography variant="body2">
{shippingDetails.name} {shippingDetails.surname}<br/>
{shippingDetails.address}<br/>
{shippingDetails.zip} {shippingDetails.country}<br/>
{shippingDetails.name} {shippingDetails.surname}
<br />
{shippingDetails.address}
<br />
{shippingDetails.zip} {shippingDetails.country}
<br />
</Typography>
<Divider sx={{my: 2}}/>
<Typography variant="h6">{t('orderedItems')}:</Typography>
<Divider sx={{ my: 2 }} />
<Typography variant="h6">{t("orderedItems")}:</Typography>
{generateBasket(t, basket)}
<Divider sx={{my: 2}}/>
<Divider sx={{ my: 2 }} />
{generateTotal(t, basket)}
<br/>
<br />
</Box>
);
default:
@@ -317,13 +347,13 @@ export default function Payment() {
maxWidth="md"
sx={{
py: 4,
maxHeight: '90vh',
overflowY: 'auto'
maxHeight: "90vh",
overflowY: "auto",
}}
>
<Paper elevation={3} sx={{p: 4}}>
<Paper elevation={3} sx={{ p: 4 }}>
<Typography variant="h4" align="center" gutterBottom>
{t('completeYourOrder')}
{t("completeYourOrder")}
</Typography>
<Stepper activeStep={activeStep} alternativeLabel>
{steps.map((label) => (
@@ -332,14 +362,14 @@ export default function Payment() {
</Step>
))}
</Stepper>
<Box sx={{mt: 4}}>{renderStepContent(activeStep)}</Box>
<Box sx={{display: 'flex', justifyContent: 'space-between', mt: 4}}>
<Box sx={{ mt: 4 }}>{renderStepContent(activeStep)}</Box>
<Box sx={{ display: "flex", justifyContent: "space-between", mt: 4 }}>
<Button
disabled={activeStep === 0}
onClick={handleBack}
variant="outlined"
>
{t('back')}
{t("back")}
</Button>
{activeStep === steps.length - 1 ? (
<Button
@@ -347,10 +377,10 @@ export default function Payment() {
color="primary"
onClick={() => {
handleClearBasket();
navigator('/');
navigator("/");
}}
>
{t('finish')}
{t("finish")}
</Button>
) : (
<Button
@@ -362,7 +392,7 @@ export default function Payment() {
(activeStep === 1 && !isShippingDetailsValid())
}
>
{activeStep === steps.length - 2 ? t('placeOrder') : t('next')}
{activeStep === steps.length - 2 ? t("placeOrder") : t("next")}
</Button>
)}
</Box>

View File

@@ -1,13 +1,13 @@
import {Box, Button, Container, Divider, Typography} from '@mui/material';
import {useLocation, useNavigate} from 'react-router-dom';
import Item from '../components/Item';
import ProductInfo from '../helper/productpage/ProductInfo';
import Ratings from '../helper/productpage/Ratings';
import {useTranslation} from "react-i18next";
import {useTheme} from '@mui/material/styles';
import { Box, Button, Container, Divider, Typography } from "@mui/material";
import { useLocation, useNavigate } from "react-router-dom";
import Item from "../components/Item";
import ProductInfo from "../helper/productpage/ProductInfo";
import Ratings from "../helper/productpage/Ratings";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
export default function Product() {
const {t} = useTranslation();
const { t } = useTranslation();
const location = useLocation();
const item = location.state?.item as Item;
const navigate = useNavigate();
@@ -22,32 +22,28 @@ export default function Product() {
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
textAlign: 'center',
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
height: "100vh",
textAlign: "center",
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
gap: '2rem',
overflow: 'auto',
gap: "2rem",
overflow: "auto",
px: 2,
}}
>
<Typography variant="h1">
{t('productNotFound')}
</Typography>
<Typography variant="h5">
{t('productDoesNotExist')}
</Typography>
<Typography variant="h1">{t("productNotFound")}</Typography>
<Typography variant="h5">{t("productDoesNotExist")}</Typography>
<Button
variant="contained"
color="primary"
size="large"
onClick={handleGoHome}
>
{t('backToHome')}
{t("backToHome")}
</Button>
</Box>
);
@@ -58,35 +54,35 @@ export default function Product() {
sx={{
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
height: '100vh',
overflow: 'auto',
height: "100vh",
overflow: "auto",
pt: 4,
pb: 10,
}}
>
<Container maxWidth="lg">
<ProductInfo item={item}/>
<ProductInfo item={item} />
<Box sx={{my: 4}}>
<Divider sx={{backgroundColor: theme.palette.text.secondary}}/>
<Box sx={{ my: 4 }}>
<Divider sx={{ backgroundColor: theme.palette.text.secondary }} />
</Box>
<Typography
variant="body2"
sx={{color: theme.palette.text.secondary, mb: 1}}
sx={{ color: theme.palette.text.secondary, mb: 1 }}
>
{t('articleNumber')}: {item.uuid}
{t("articleNumber")}: {item.uuid}
</Typography>
<Typography
variant="body1"
sx={{color: theme.palette.text.primary, mb: 3}}
sx={{ color: theme.palette.text.primary, mb: 3 }}
>
{item.description}
</Typography>
<Ratings itemId={item.uuid}/>
<Ratings itemId={item.uuid} />
</Container>
</Box>
);
};
}

View File

@@ -150,7 +150,7 @@
}
.sidebar-filter {
margin: 30px
margin: 30px;
}
.no-results {
@@ -166,5 +166,5 @@
}
.red {
color: #F00;
color: #f00;
}

View File

@@ -1,10 +1,16 @@
'use client';
import React, {createContext, ReactNode, useContext, useEffect, useState} from 'react';
import {createTheme, ThemeProvider} from '@mui/material/styles';
import {CssBaseline, GlobalStyles} from '@mui/material';
import useMediaQuery from '@mui/material/useMediaQuery';
"use client";
import React, {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from "react";
import { createTheme, ThemeProvider } from "@mui/material/styles";
import { CssBaseline, GlobalStyles } from "@mui/material";
import useMediaQuery from "@mui/material/useMediaQuery";
type ThemeMode = 'light' | 'dark';
type ThemeMode = "light" | "dark";
interface ThemeContextType {
mode: ThemeMode;
@@ -12,9 +18,8 @@ interface ThemeContextType {
}
const ThemeContext = createContext<ThemeContextType>({
mode: 'light',
toggleMode: () => {
},
mode: "light",
toggleMode: () => {},
});
export const useThemeMode = () => useContext(ThemeContext);
@@ -23,47 +28,54 @@ interface CustomThemeProviderProps {
children: ReactNode;
}
export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({children}) => {
export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({
children,
}) => {
// 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
const [mode, setMode] = useState<ThemeMode>('light');
const [mode, setMode] = useState<ThemeMode>("light");
const [mounted, setMounted] = useState(false);
// Nach dem ersten Render ausführen (SSR-sicher)
useEffect(() => {
setMounted(true);
if (typeof window !== 'undefined') {
const savedMode = localStorage.getItem('themeMode') as ThemeMode;
if (savedMode === 'light' || savedMode === 'dark') {
if (typeof window !== "undefined") {
const savedMode = localStorage.getItem("themeMode") as ThemeMode;
if (savedMode === "light" || savedMode === "dark") {
setMode(savedMode);
} else {
setMode(prefersDarkMode ? 'dark' : 'light');
setMode(prefersDarkMode ? "dark" : "light");
}
}
}, [prefersDarkMode]);
// Mode in localStorage speichern
useEffect(() => {
if (mounted && typeof window !== 'undefined') {
localStorage.setItem('themeMode', mode);
if (mounted && typeof window !== "undefined") {
localStorage.setItem("themeMode", mode);
}
}, [mode, mounted]);
// Browser-only DOM-Manipulation
useEffect(() => {
if (mounted && typeof window !== 'undefined') {
const backgroundColor = mode === 'dark' ? '#121212' : '#fafafa';
if (mounted && typeof window !== "undefined") {
const backgroundColor = mode === "dark" ? "#121212" : "#fafafa";
// Warten bis DOM geladen ist
const updateBackground = () => {
document.documentElement.style.setProperty('--background-color', backgroundColor);
document.documentElement.style.setProperty(
"--background-color",
backgroundColor,
);
document.documentElement.style.backgroundColor = backgroundColor;
document.body.style.backgroundColor = backgroundColor;
const root = document.getElementById('root');
const root = document.getElementById("root");
if (root) {
root.style.backgroundColor = backgroundColor;
}
@@ -80,46 +92,52 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({childre
}, [mode, mounted]);
const toggleMode = () => {
setMode(prevMode => prevMode === 'light' ? 'dark' : 'light');
setMode((prevMode) => (prevMode === "light" ? "dark" : "light"));
};
// Theme basierend auf Mode erstellen
const theme = React.useMemo(() =>
const theme = React.useMemo(
() =>
createTheme({
palette: {
mode,
primary: {
main: '#0fd13f', // Grüne NavBar
light: mode === 'dark' ? '#4caf50' : '#42a5f5',
dark: mode === 'dark' ? '#388e3c' : '#1565c0',
contrastText: '#fff',
main: "#0fd13f", // Grüne NavBar
light: mode === "dark" ? "#4caf50" : "#42a5f5",
dark: mode === "dark" ? "#388e3c" : "#1565c0",
contrastText: "#fff",
},
secondary: {
main: mode === 'dark' ? '#bb86fc' : '#9c27b0',
light: mode === 'dark' ? '#d7aefb' : '#ba68c8',
dark: mode === 'dark' ? '#985eff' : '#7b1fa2',
contrastText: '#fff',
main: mode === "dark" ? "#bb86fc" : "#9c27b0",
light: mode === "dark" ? "#d7aefb" : "#ba68c8",
dark: mode === "dark" ? "#985eff" : "#7b1fa2",
contrastText: "#fff",
},
background: {
default: mode === 'dark' ? '#121212' : '#fafafa',
paper: mode === 'dark' ? '#1e1e1e' : '#ffffff',
default: mode === "dark" ? "#121212" : "#fafafa",
paper: mode === "dark" ? "#1e1e1e" : "#ffffff",
},
text: {
primary: mode === 'dark' ? '#ffffff' : '#000000',
secondary: mode === 'dark' ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.6)',
primary: mode === "dark" ? "#ffffff" : "#000000",
secondary:
mode === "dark"
? "rgba(255, 255, 255, 0.7)"
: "rgba(0, 0, 0, 0.6)",
},
homepage: mode === 'dark' ? '#1e1e1e' : '#f4f4f4',
homepage: mode === "dark" ? "#1e1e1e" : "#f4f4f4",
},
components: {
MuiCssBaseline: {
styleOverrides: {
html: {
backgroundColor: `${mode === 'dark' ? '#121212' : '#fafafa'} !important`,
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
backgroundColor: `${mode === "dark" ? "#121212" : "#fafafa"} !important`,
transition:
"background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)",
},
body: {
backgroundColor: `${mode === 'dark' ? '#121212' : '#fafafa'} !important`,
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
backgroundColor: `${mode === "dark" ? "#121212" : "#fafafa"} !important`,
transition:
"background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)",
margin: 0,
},
},
@@ -127,37 +145,38 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({childre
MuiAppBar: {
styleOverrides: {
colorPrimary: {
backgroundColor: mode === 'dark' ? '#388e3c' : '#0fd13f',
color: '#ffffff',
backgroundColor: mode === "dark" ? "#388e3c" : "#0fd13f",
color: "#ffffff",
},
},
},
},
}),
[mode]
[mode],
);
// Aggressive GlobalStyles mit CSS-Variablen
const globalStyles = mounted ? (
<GlobalStyles
styles={{
':root': {
'--background-color': mode === 'dark' ? '#121212' : '#fafafa',
'--text-color': mode === 'dark' ? '#ffffff' : '#000000',
":root": {
"--background-color": mode === "dark" ? "#121212" : "#fafafa",
"--text-color": mode === "dark" ? "#ffffff" : "#000000",
},
'*': {
boxSizing: 'border-box',
"*": {
boxSizing: "border-box",
},
'html, body, #root': {
"html, body, #root": {
backgroundColor: `var(--background-color) !important`,
color: `var(--text-color) !important`,
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1), color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
transition:
"background-color 300ms cubic-bezier(0.4, 0, 0.2, 1), color 300ms cubic-bezier(0.4, 0, 0.2, 1)",
margin: 0,
padding: 0,
minHeight: '100vh',
minHeight: "100vh",
},
'div, section, main, article': {
backgroundColor: 'transparent',
"div, section, main, article": {
backgroundColor: "transparent",
},
}}
/>
@@ -166,17 +185,17 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({childre
// SSR-Fallback während mounted = false
if (!mounted) {
return (
<ThemeProvider theme={createTheme({palette: {mode: 'light'}})}>
<CssBaseline/>
<ThemeProvider theme={createTheme({ palette: { mode: "light" } })}>
<CssBaseline />
{children}
</ThemeProvider>
);
}
return (
<ThemeContext.Provider value={{mode, toggleMode}}>
<ThemeContext.Provider value={{ mode, toggleMode }}>
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme/>
<CssBaseline enableColorScheme />
{globalStyles}
{children}
</ThemeProvider>

View File

@@ -1,28 +1,27 @@
// theme/ThemeToggle.tsx
import React from 'react';
import {IconButton, Tooltip} from '@mui/material';
import {Brightness4, Brightness7} from '@mui/icons-material';
import {useThemeMode} from './ThemeContext';
import {useTranslation} from "react-i18next";
import React from "react";
import { IconButton, Tooltip } from "@mui/material";
import { Brightness4, Brightness7 } from "@mui/icons-material";
import { useThemeMode } from "./ThemeContext";
import { useTranslation } from "react-i18next";
const ThemeToggle: React.FC = () => {
const {mode, toggleMode} = useThemeMode();
const {t} = useTranslation();
const { mode, toggleMode } = useThemeMode();
const { t } = useTranslation();
return (
<Tooltip title={mode === 'dark' ? t('lightMode') : t('darkMode')}>
<Tooltip title={mode === "dark" ? t("lightMode") : t("darkMode")}>
<IconButton
onClick={toggleMode}
color="inherit"
sx={{
transition: 'transform 0.2s',
'&:hover': {
transform: 'scale(1.1)',
transition: "transform 0.2s",
"&:hover": {
transform: "scale(1.1)",
},
}}
>
{mode === 'dark' ? <Brightness7/> : <Brightness4/>}
{mode === "dark" ? <Brightness7 /> : <Brightness4 />}
</IconButton>
</Tooltip>
);

View File

@@ -1,13 +1,13 @@
import '@mui/material/styles';
import "@mui/material/styles";
declare module '@mui/material/styles' {
declare module "@mui/material/styles" {
interface Palette {
tertiary: Palette['primary'];
tertiary: Palette["primary"];
homepage: string;
}
interface PaletteOptions {
tertiary?: PaletteOptions['primary'];
tertiary?: PaletteOptions["primary"];
homepage?: string;
}
}

View File

@@ -1,51 +1,51 @@
import {createTheme} from '@mui/material/styles';
import './theme-augmentation.d.ts'; // Falls vorhanden
import { createTheme } from "@mui/material/styles";
import "./theme-augmentation.d.ts"; // Falls vorhanden
export const darkmode = createTheme({
palette: {
primary: {
main: '#0fd13f', // Grüne Standardfarbe
light: '#42a5f5',
dark: '#15650',
contrastText: '#fff',
main: "#0fd13f", // Grüne Standardfarbe
light: "#42a5f5",
dark: "#15650",
contrastText: "#fff",
},
secondary: {
main: '#9c27b0',
light: '#ba68c8',
dark: '#7b1fa2',
contrastText: '#fff',
main: "#9c27b0",
light: "#ba68c8",
dark: "#7b1fa2",
contrastText: "#fff",
},
error: {
main: '#f44336',
light: '#e57373',
dark: '#d32f2f',
contrastText: '#fff',
main: "#f44336",
light: "#e57373",
dark: "#d32f2f",
contrastText: "#fff",
},
warning: {
main: '#ed6c02',
light: '#ff9800',
dark: '#e65100',
contrastText: '#fff',
main: "#ed6c02",
light: "#ff9800",
dark: "#e65100",
contrastText: "#fff",
},
info: {
main: '#0288d1',
light: '#03a9f4',
dark: '#01579b',
contrastText: '#fff',
main: "#0288d1",
light: "#03a9f4",
dark: "#01579b",
contrastText: "#fff",
},
success: {
main: '#2e7d32',
light: '#4caf50',
dark: '#1b5e20',
contrastText: '#fff',
main: "#2e7d32",
light: "#4caf50",
dark: "#1b5e20",
contrastText: "#fff",
},
background: {
default: '#fafafa',
paper: '#ffffff',
default: "#fafafa",
paper: "#ffffff",
},
text: {
primary: '#000000',
secondary: 'rgba(0, 0, 0, 0.6)',
primary: "#000000",
secondary: "rgba(0, 0, 0, 0.6)",
},
},
// Sanfte Übergänge
@@ -60,10 +60,10 @@ export const darkmode = createTheme({
leavingScreen: 195,
},
easing: {
easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
easeOut: 'cubic-bezier(0.0, 0, 0.2, 1)',
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
sharp: 'cubic-bezier(0.4, 0, 0.6, 1)',
easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)",
easeOut: "cubic-bezier(0.0, 0, 0.2, 1)",
easeIn: "cubic-bezier(0.4, 0, 1, 1)",
sharp: "cubic-bezier(0.4, 0, 0.6, 1)",
},
},
// Verbesserte Komponenten-Overrides
@@ -71,20 +71,20 @@ export const darkmode = createTheme({
MuiCssBaseline: {
styleOverrides: {
body: {
backgroundColor: '#fafafa',
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
backgroundColor: "#fafafa",
transition: "background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)",
},
html: {
backgroundColor: '#fafafa',
transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)',
backgroundColor: "#fafafa",
transition: "background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)",
},
},
},
MuiAppBar: {
styleOverrides: {
colorPrimary: {
backgroundColor: '#0fd13f', // Grüne NavBar
color: '#ffffff',
backgroundColor: "#0fd13f", // Grüne NavBar
color: "#ffffff",
},
},
},
@@ -92,13 +92,13 @@ export const darkmode = createTheme({
styleOverrides: {
root: {
borderRadius: 8,
textTransform: 'none', // Entfernt automatische Großschreibung
textTransform: "none", // Entfernt automatische Großschreibung
fontWeight: 600,
},
contained: {
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
'&:hover': {
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
"&:hover": {
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
},
},
},
@@ -107,10 +107,10 @@ export const darkmode = createTheme({
styleOverrides: {
root: {
borderRadius: 12,
boxShadow: '0 2px 12px rgba(0,0,0,0.08)',
transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
boxShadow: '0 4px 20px rgba(0,0,0,0.12)',
boxShadow: "0 2px 12px rgba(0,0,0,0.08)",
transition: "box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1)",
"&:hover": {
boxShadow: "0 4px 20px rgba(0,0,0,0.12)",
},
},
},
@@ -118,7 +118,7 @@ export const darkmode = createTheme({
MuiTextField: {
styleOverrides: {
root: {
'& .MuiOutlinedInput-root': {
"& .MuiOutlinedInput-root": {
borderRadius: 8,
},
},
@@ -137,10 +137,10 @@ export const darkmode = createTheme({
borderRadius: 8,
},
elevation1: {
boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
boxShadow: "0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)",
},
elevation2: {
boxShadow: '0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)',
boxShadow: "0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)",
},
},
},
@@ -150,45 +150,45 @@ export const darkmode = createTheme({
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
h1: {
fontWeight: 700,
fontSize: '2.5rem',
fontSize: "2.5rem",
lineHeight: 1.2,
},
h2: {
fontWeight: 600,
fontSize: '2rem',
fontSize: "2rem",
lineHeight: 1.3,
},
h3: {
fontWeight: 600,
fontSize: '1.75rem',
fontSize: "1.75rem",
lineHeight: 1.3,
},
h4: {
fontWeight: 600,
fontSize: '1.5rem',
fontSize: "1.5rem",
lineHeight: 1.4,
},
h5: {
fontWeight: 600,
fontSize: '1.25rem',
fontSize: "1.25rem",
lineHeight: 1.4,
},
h6: {
fontWeight: 600,
fontSize: '1rem',
fontSize: "1rem",
lineHeight: 1.4,
},
body1: {
fontSize: '1rem',
fontSize: "1rem",
lineHeight: 1.5,
},
body2: {
fontSize: '0.875rem',
fontSize: "0.875rem",
lineHeight: 1.4,
},
button: {
fontWeight: 600,
textTransform: 'none',
textTransform: "none",
},
},
// Benutzerdefinierte Breakpoints

View File

@@ -1,15 +1,17 @@
export function mapValueToColor(minVal: number, maxVal: number, actualVal: number): string {
export function mapValueToColor(
minVal: number,
maxVal: number,
actualVal: number,
): string {
const clamped = Math.min(Math.max(actualVal, minVal), maxVal);
// Calculate interpolation ratio (0-1)
const ratio = maxVal !== minVal
? (clamped - minVal) / (maxVal - minVal)
: 0;
return hsvDegToHex(120 * ratio);//120° is green, 0° is red
const ratio = maxVal !== minVal ? (clamped - minVal) / (maxVal - minVal) : 0;
return hsvDegToHex(120 * ratio); //120° is green, 0° is red
}
export function getColorFromPercent(percent: string): string {
let perc = Number(percent) / 100
let perc = Number(percent) / 100;
if (perc > 1) {
perc = 2.5;
}
@@ -20,7 +22,7 @@ export function hsvDegToHex(h: number): string {
h = Math.max(0, Math.min(360, h));
const c = 1;
const x = (1 - Math.abs(((h / 60) % 2) - 1));
const x = 1 - Math.abs(((h / 60) % 2) - 1);
let r, g, b;
if (h < 60) {
@@ -50,9 +52,15 @@ export function hsvDegToHex(h: number): string {
}
// scale to 0-255
const rHex = Math.round(r * 255).toString(16).padStart(2, '0');
const gHex = Math.round(g * 255).toString(16).padStart(2, '0');
const bHex = Math.round(b * 255).toString(16).padStart(2, '0');
const rHex = Math.round(r * 255)
.toString(16)
.padStart(2, "0");
const gHex = Math.round(g * 255)
.toString(16)
.padStart(2, "0");
const bHex = Math.round(b * 255)
.toString(16)
.padStart(2, "0");
return `#${rHex}${gHex}${bHex}`;
}