Reformat code
This commit is contained in:
@@ -1,63 +1,63 @@
|
||||
/* App.css - CSS-Variablen-basierte Version */
|
||||
:root {
|
||||
--background-color: #fafafa;
|
||||
--text-color: #000000;
|
||||
--navbar-height: 6vh;
|
||||
--page-height: 94vh;
|
||||
--background-color: #fafafa;
|
||||
--text-color: #000000;
|
||||
--navbar-height: 6vh;
|
||||
--page-height: 94vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: var(--background-color) !important;
|
||||
color: var(--text-color) !important;
|
||||
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background-color: var(--background-color) !important;
|
||||
color: var(--text-color) !important;
|
||||
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: var(--background-color) !important;
|
||||
color: var(--text-color) !important;
|
||||
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: var(--background-color) !important;
|
||||
color: var(--text-color) !important;
|
||||
transition: background-color 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
border-radius: 8px;
|
||||
padding: 2em;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
opacity: 0.7;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { StyledEngineProvider } from '@mui/material/styles';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
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 {BasketProvider} from './helper/BasketProvider';
|
||||
import NavBar from './helper/navbar/NavBar';
|
||||
import Account from './pages/Account';
|
||||
import Contact from './pages/Contact';
|
||||
@@ -11,42 +11,42 @@ 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 {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 {AccountProvider} from './helper/AccountProvider.tsx';
|
||||
import {CookiesProvider} from 'react-cookie';
|
||||
|
||||
export default function App() {
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<CustomThemeProvider>
|
||||
<CookiesProvider>
|
||||
<AccountProvider>
|
||||
<BasketProvider>
|
||||
<BrowserRouter>
|
||||
<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 />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</BasketProvider>
|
||||
</AccountProvider>
|
||||
</CookiesProvider>
|
||||
</CustomThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<CustomThemeProvider>
|
||||
<CookiesProvider>
|
||||
<AccountProvider>
|
||||
<BasketProvider>
|
||||
<BrowserRouter>
|
||||
<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/>}/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</BasketProvider>
|
||||
</AccountProvider>
|
||||
</CookiesProvider>
|
||||
</CustomThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@ type AccountType = {
|
||||
export default AccountType;
|
||||
|
||||
export type CustomerType =
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
surname: string;
|
||||
address: string;
|
||||
country: string;
|
||||
zip: string;
|
||||
}
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
surname: string;
|
||||
address: string;
|
||||
country: string;
|
||||
zip: string;
|
||||
}
|
||||
|
||||
export type SubmitLogin = {
|
||||
email: string;
|
||||
@@ -32,27 +32,27 @@ export type SubmitLogin = {
|
||||
};
|
||||
|
||||
export type SubmitLoginSession = {
|
||||
email: string;
|
||||
session: string;
|
||||
email: string;
|
||||
session: string;
|
||||
};
|
||||
|
||||
export type AdminAccountOperation = {
|
||||
email: string;
|
||||
session: string;
|
||||
accountId: number;
|
||||
email: string;
|
||||
session: string;
|
||||
accountId: number;
|
||||
}
|
||||
|
||||
export type User = {
|
||||
password: string;
|
||||
email: string;
|
||||
customerId: number;
|
||||
session: string;
|
||||
isAdmin: boolean;
|
||||
// weitere Felder nach Bedarf
|
||||
password: string;
|
||||
email: string;
|
||||
customerId: number;
|
||||
session: string;
|
||||
isAdmin: boolean;
|
||||
// weitere Felder nach Bedarf
|
||||
};
|
||||
|
||||
export type AccountContextType = {
|
||||
user: User | null;
|
||||
login: (userData: User) => void;
|
||||
logout: () => void;
|
||||
user: User | null;
|
||||
login: (userData: User) => void;
|
||||
logout: () => void;
|
||||
};
|
||||
@@ -4,7 +4,7 @@ export enum OrderStatusEnum {
|
||||
ISSUES = 'ISSUES',
|
||||
DELIVERED = 'DELIVERED',
|
||||
CANCELLED = 'CANCELLED',
|
||||
};
|
||||
}
|
||||
|
||||
type OrderType = {
|
||||
id: number;
|
||||
@@ -37,4 +37,4 @@ export type OrderItem = {
|
||||
export type OrderPatch = {
|
||||
id: number;
|
||||
status: OrderStatusEnum;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -1,16 +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 =
|
||||
typeof cookies.account === "object" && !Array.isArray(cookies.account)
|
||||
? cookies.account
|
||||
: null;
|
||||
typeof cookies.account === "object" && !Array.isArray(cookies.account)
|
||||
? cookies.account
|
||||
: null;
|
||||
const [user, setUser] = useState<User | null>(initialAccount);
|
||||
|
||||
const login = (userData: User) => {
|
||||
@@ -22,11 +22,11 @@ export const AccountProvider = ({ children }: { children: ReactNode }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCookie("account", user, { path: "/", maxAge: 3600 * 24 * 7 });
|
||||
}, [user, setCookie]);
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import React, {createContext, useContext, useEffect, useState} from 'react';
|
||||
import Item from '../components/Item';
|
||||
import { useCookies } from 'react-cookie';
|
||||
import {useCookies} from 'react-cookie';
|
||||
|
||||
export interface BasketItem {
|
||||
item: Item;
|
||||
@@ -15,7 +15,7 @@ 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 || []);
|
||||
|
||||
@@ -26,12 +26,12 @@ export const BasketProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||
// Update quantity if item already exists
|
||||
return prevBasket.map((basketItem) =>
|
||||
basketItem.item.uuid === item.uuid
|
||||
? { ...basketItem, quantity: basketItem.quantity + quantity }
|
||||
? {...basketItem, quantity: basketItem.quantity + quantity}
|
||||
: basketItem
|
||||
);
|
||||
}
|
||||
// Add new item to basket
|
||||
return [...prevBasket, { item, quantity }];
|
||||
return [...prevBasket, {item, quantity}];
|
||||
});
|
||||
};
|
||||
|
||||
@@ -40,11 +40,11 @@ export const BasketProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCookie("basket", basket, { path: "/", maxAge: 3600 * 24 * 7 }); // 7 Tage
|
||||
}, [basket, setCookie]);
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
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,
|
||||
@@ -31,11 +31,17 @@ 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 : { email: "", password: "", session: "", customerId: -1, isAdmin: false }),
|
||||
queryFn: () => fetchAccounts(loginData ? loginData : {
|
||||
email: "",
|
||||
password: "",
|
||||
session: "",
|
||||
customerId: -1,
|
||||
isAdmin: false
|
||||
}),
|
||||
retry: 3,
|
||||
retryDelay: 1000,
|
||||
});
|
||||
@@ -57,7 +63,11 @@ export default function AccountsInfo() {
|
||||
|
||||
const handleDeleteSelected = async () => {
|
||||
selectedRows.forEach(async (row) => {
|
||||
await deleteAccount.mutateAsync({ email: loginData?.email || '', session: loginData?.session || '', accountId: row.id as number });
|
||||
await deleteAccount.mutateAsync({
|
||||
email: loginData?.email || '',
|
||||
session: loginData?.session || '',
|
||||
accountId: row.id as number
|
||||
});
|
||||
})
|
||||
|
||||
setRows(rows.filter((row) => !selectedRows.has(row.id)));
|
||||
@@ -65,12 +75,18 @@ export default function AccountsInfo() {
|
||||
|
||||
const updateAdmin = useMutation({
|
||||
mutationFn: (account: AccountType) =>
|
||||
updateAccountAdmin(account, loginData ? loginData : { email: "", password: "", session: "", customerId: -1, isAdmin: false }),
|
||||
updateAccountAdmin(account, loginData ? loginData : {
|
||||
email: "",
|
||||
password: "",
|
||||
session: "",
|
||||
customerId: -1,
|
||||
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'),
|
||||
@@ -97,7 +113,7 @@ export default function AccountsInfo() {
|
||||
sortable: false,
|
||||
disableReorder: true,
|
||||
disableColumnMenu: true,
|
||||
renderCell: params => <IconButton onClick={() => handleCustomerEdit(params.row)}> <EditIcon /> </IconButton>,
|
||||
renderCell: params => <IconButton onClick={() => handleCustomerEdit(params.row)}> <EditIcon/> </IconButton>,
|
||||
}
|
||||
];
|
||||
|
||||
@@ -130,7 +146,7 @@ export default function AccountsInfo() {
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
startIcon={<DeleteIcon />}
|
||||
startIcon={<DeleteIcon/>}
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={selectedRows.size === 0}
|
||||
sx={{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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;
|
||||
@@ -13,9 +13,15 @@ type CustomerDialogProps = {
|
||||
setCustomerData: React.Dispatch<React.SetStateAction<CustomerType>>;
|
||||
};
|
||||
|
||||
const CustomerEditDialog: React.FC<CustomerDialogProps> = ({ open, onClose, customerData, setCustomerData, onSubmit }) => {
|
||||
const CustomerEditDialog: React.FC<CustomerDialogProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
customerData,
|
||||
setCustomerData,
|
||||
onSubmit
|
||||
}) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const {t} = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -55,7 +61,7 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({ open, onClose, cust
|
||||
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"
|
||||
@@ -63,7 +69,7 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({ open, onClose, cust
|
||||
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"
|
||||
@@ -71,7 +77,7 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({ open, onClose, cust
|
||||
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"
|
||||
@@ -79,7 +85,7 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({ open, onClose, cust
|
||||
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"
|
||||
@@ -87,7 +93,7 @@ const CustomerEditDialog: React.FC<CustomerDialogProps> = ({ open, onClose, cust
|
||||
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>
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, {useState} from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
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';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import {Item} from '../../components/Item.tsx';
|
||||
|
||||
interface ItemImageDialogProps {
|
||||
open: boolean;
|
||||
@@ -25,8 +24,8 @@ 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);
|
||||
@@ -86,7 +85,7 @@ export default function ItemImageDialog({ open, onClose, item, onSuccess, isFarm
|
||||
try {
|
||||
const base64Image = await convertFileToBase64(selectedFile);
|
||||
|
||||
const response = await fetch((isFarmStationImage ? 'http://localhost:8085/farm' : 'http://localhost:8085/image') + '?uuid=' + item.uuid , {
|
||||
const response = await fetch((isFarmStationImage ? 'http://localhost:8085/farm' : 'http://localhost:8085/image') + '?uuid=' + item.uuid, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -129,15 +128,15 @@ export default function ItemImageDialog({ open, onClose, item, onSuccess, isFarm
|
||||
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}
|
||||
<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">
|
||||
@@ -158,7 +157,7 @@ export default function ItemImageDialog({ open, onClose, item, onSuccess, isFarm
|
||||
<Box>
|
||||
<input
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
style={{display: 'none'}}
|
||||
id="image-upload"
|
||||
type="file"
|
||||
onChange={handleFileSelect}
|
||||
@@ -167,9 +166,9 @@ export default function ItemImageDialog({ open, onClose, item, onSuccess, isFarm
|
||||
<Button
|
||||
variant="outlined"
|
||||
component="span"
|
||||
startIcon={<CloudUploadIcon />}
|
||||
startIcon={<CloudUploadIcon/>}
|
||||
fullWidth
|
||||
sx={{ mb: 2 }}
|
||||
sx={{mb: 2}}
|
||||
>
|
||||
{selectedFile ? selectedFile.name : t('selectImage')}
|
||||
</Button>
|
||||
@@ -178,7 +177,7 @@ export default function ItemImageDialog({ open, onClose, item, onSuccess, isFarm
|
||||
|
||||
{/* Image Preview */}
|
||||
{preview && (
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Box sx={{textAlign: 'center'}}>
|
||||
<img
|
||||
src={preview}
|
||||
alt="Preview"
|
||||
@@ -190,7 +189,7 @@ export default function ItemImageDialog({ open, onClose, item, onSuccess, isFarm
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
||||
<Typography variant="caption" display="block" sx={{mt: 1}}>
|
||||
{selectedFile?.name} ({(selectedFile?.size || 0 / 1024).toFixed(1)} KB)
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -220,7 +219,7 @@ export default function ItemImageDialog({ open, onClose, item, onSuccess, isFarm
|
||||
onClick={handleUpload}
|
||||
variant="contained"
|
||||
disabled={!selectedFile || loading}
|
||||
startIcon={loading ? <CircularProgress size={20} /> : undefined}
|
||||
startIcon={loading ? <CircularProgress size={20}/> : undefined}
|
||||
>
|
||||
{loading ? t('uploading') : t('upload')}
|
||||
</Button>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
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 { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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 {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 } from "../query/Queries.tsx";
|
||||
import {mapValueToColor} from "../../util/ColorUtil.tsx";
|
||||
import {useAccount} from "../AccountProvider.tsx";
|
||||
import {deleteItemQuery, fetchItems} from "../query/Queries.tsx";
|
||||
import ItemImageDialog from "./ItemImageDialog.tsx";
|
||||
import NewItemDialog from "./NewItemDialog.tsx";
|
||||
|
||||
export default function ItemsInfo() {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const [rows, setRows] = useState<Item[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Set<GridRowId>>(new Set());
|
||||
@@ -41,14 +41,13 @@ export default function ItemsInfo() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
function handleAddItem() {
|
||||
setNewItemDialog(true);
|
||||
}
|
||||
|
||||
const { user: loginData } = useAccount();
|
||||
const {user: loginData} = useAccount();
|
||||
|
||||
const { data } = useQuery({
|
||||
const {data} = useQuery({
|
||||
queryKey: ["fetchItems", loginData],
|
||||
queryFn: () => fetchItems(),
|
||||
retry: 3,
|
||||
@@ -81,7 +80,7 @@ export default function ItemsInfo() {
|
||||
};
|
||||
|
||||
const columns: GridColDef<(typeof rows)[number]>[] = [
|
||||
{ field: 'id', headerName: 'ID', width: 60 },
|
||||
{field: 'id', headerName: 'ID', width: 60},
|
||||
{
|
||||
field: 'uuid',
|
||||
headerName: t('uuid'),
|
||||
@@ -129,11 +128,14 @@ export default function ItemsInfo() {
|
||||
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={{
|
||||
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) },
|
||||
fill: () => {
|
||||
return mapValueToColor(0, params.row.stockExpected, params.row.stock)
|
||||
},
|
||||
},
|
||||
}} text={() => `${params.row.stock}`} />
|
||||
}} text={() => `${params.row.stock}`}/>
|
||||
},
|
||||
{
|
||||
field: 'rating',
|
||||
@@ -141,11 +143,14 @@ export default function ItemsInfo() {
|
||||
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={{
|
||||
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) },
|
||||
fill: () => {
|
||||
return mapValueToColor(0, 10, params.row.rating)
|
||||
},
|
||||
},
|
||||
}} text={() => `${params.row.rating.toFixed(2)}`} />
|
||||
}} text={() => `${params.row.rating.toFixed(2)}`}/>
|
||||
},
|
||||
{
|
||||
field: "actualPrice",
|
||||
@@ -159,14 +164,15 @@ export default function ItemsInfo() {
|
||||
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'),
|
||||
width: 90,
|
||||
editable: false,
|
||||
renderCell: params => <IconButton onClick={() => handleFarmImageEdit(params.row)}> <EditIcon /> </IconButton>,
|
||||
renderCell: params => <IconButton onClick={() => handleFarmImageEdit(params.row)}> <EditIcon/>
|
||||
</IconButton>,
|
||||
}
|
||||
];
|
||||
|
||||
@@ -191,7 +197,7 @@ export default function ItemsInfo() {
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
startIcon={<DeleteIcon />}
|
||||
startIcon={<DeleteIcon/>}
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={selectedRows.size === 0}
|
||||
sx={{
|
||||
@@ -203,7 +209,7 @@ export default function ItemsInfo() {
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<DeleteIcon />}
|
||||
startIcon={<DeleteIcon/>}
|
||||
onClick={handleAddItem}
|
||||
sx={{
|
||||
marginRight: 1
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import {
|
||||
Alert,
|
||||
@@ -11,19 +10,19 @@ import {
|
||||
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';
|
||||
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: "",
|
||||
@@ -48,7 +47,7 @@ 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({
|
||||
@@ -74,15 +73,15 @@ export default function NewItemDialog({ open, onClose }: NewItemDialogProps) {
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<DialogTitle sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
||||
{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")}
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import {
|
||||
Button,
|
||||
Card, CardContent,
|
||||
Card,
|
||||
CardContent,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
List, ListItemText,
|
||||
List,
|
||||
ListItemText,
|
||||
Snackbar,
|
||||
Stack,
|
||||
Typography,
|
||||
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[] = [
|
||||
@@ -31,24 +33,24 @@ const statusOrder: OrderStatusEnum[] = [
|
||||
];
|
||||
|
||||
|
||||
const OrderCard: React.FC<{ order: OrderType; onClick: () => void }> = ({ order, onClick }) => {
|
||||
const OrderCard: React.FC<{ order: OrderType; onClick: () => void }> = ({order, onClick}) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const [{ isDragging }, drag] = useDrag(() => ({
|
||||
const [{isDragging}, drag] = useDrag(() => ({
|
||||
type: 'order',
|
||||
item: { id: order.id },
|
||||
item: {id: order.id},
|
||||
collect: (monitor) => ({
|
||||
isDragging: !!monitor.isDragging(),
|
||||
}),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div ref={drag} style={{ opacity: isDragging ? 0.5 : 1, marginBottom: 8 }}>
|
||||
<div ref={drag} style={{opacity: isDragging ? 0.5 : 1, marginBottom: 8}}>
|
||||
<Card elevation={4}>
|
||||
<CardContent onClick={onClick}>
|
||||
<Typography gutterBottom variant="h5" component="div">
|
||||
Order: {order.id}
|
||||
Order: {order.id}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{t('date') + ": " + new Date(order.time).toUTCString()}
|
||||
@@ -62,12 +64,15 @@ const OrderCard: React.FC<{ order: OrderType; onClick: () => void }> = ({ order,
|
||||
);
|
||||
};
|
||||
|
||||
const Column: React.FC<PropsWithChildren<{ status: OrderStatusEnum; onDrop: (id: number, status: OrderStatusEnum) => void }>> = ({ status, onDrop, children }) => {
|
||||
const Column: React.FC<PropsWithChildren<{
|
||||
status: OrderStatusEnum;
|
||||
onDrop: (id: number, status: OrderStatusEnum) => void
|
||||
}>> = ({status, onDrop, children}) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const {t} = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const [{ isOver }, drop] = useDrop(() => ({
|
||||
const [{isOver}, drop] = useDrop(() => ({
|
||||
accept: 'order',
|
||||
drop: (item: { id: number }) => onDrop(item.id, status),
|
||||
collect: (monitor) => ({
|
||||
@@ -76,11 +81,31 @@ const Column: React.FC<PropsWithChildren<{ status: OrderStatusEnum; onDrop: (id:
|
||||
}));
|
||||
|
||||
return (
|
||||
<div ref={drop} style={{ flex: 1, backgroundColor: isOver ? theme.palette.background.paper : 'transparent', padding: 1, minWidth: 300, maxWidth: 400 }}>
|
||||
<Card sx={{ minHeight: '100%', marginTop: 2, marginLeft: 1, height: '80vh', display: 'flex', flexDirection: 'column' }} elevation={4}>
|
||||
<CardContent sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflowY: 'auto', height: '100%', padding: 2 }}>
|
||||
<div ref={drop} style={{
|
||||
flex: 1,
|
||||
backgroundColor: isOver ? theme.palette.background.paper : 'transparent',
|
||||
padding: 1,
|
||||
minWidth: 300,
|
||||
maxWidth: 400
|
||||
}}>
|
||||
<Card sx={{
|
||||
minHeight: '100%',
|
||||
marginTop: 2,
|
||||
marginLeft: 1,
|
||||
height: '80vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}} elevation={4}>
|
||||
<CardContent sx={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflowY: 'auto',
|
||||
height: '100%',
|
||||
padding: 2
|
||||
}}>
|
||||
<Typography variant="h6">{t(status)}</Typography>
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
<div style={{flex: 1, overflowY: 'auto'}}>
|
||||
{children}
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -89,9 +114,13 @@ const Column: React.FC<PropsWithChildren<{ status: OrderStatusEnum; onDrop: (id:
|
||||
);
|
||||
};
|
||||
|
||||
const EditOrder: React.FC<{ open: boolean; order: OrderType | null; onClose: () => void }> = ({ open, order, onClose }) => {
|
||||
const EditOrder: React.FC<{ open: boolean; order: OrderType | null; onClose: () => void }> = ({
|
||||
open,
|
||||
order,
|
||||
onClose
|
||||
}) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const {t} = useTranslation();
|
||||
if (order === null)
|
||||
return "";
|
||||
return (
|
||||
@@ -102,8 +131,9 @@ const EditOrder: React.FC<{ open: boolean; order: OrderType | null; onClose: ()
|
||||
<DialogContent dividers>
|
||||
{order && (
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="subtitle1">{`${t('orderDate')}: ${new Date(order.time).toDateString()}`}</Typography>
|
||||
<Divider />
|
||||
<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) => (
|
||||
@@ -113,7 +143,7 @@ const EditOrder: React.FC<{ open: boolean; order: OrderType | null; onClose: ()
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
<Divider />
|
||||
<Divider/>
|
||||
<Typography variant="h6">{`${t('sum')}: ${(order.total / 100).toFixed(2)} €`}</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -129,18 +159,24 @@ 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 : { email: "", password: "", session: "", customerId: -1, isAdmin: false }),
|
||||
queryFn: () => fetchOrdersAdmin(loginData ? loginData : {
|
||||
email: "",
|
||||
password: "",
|
||||
session: "",
|
||||
customerId: -1,
|
||||
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}),
|
||||
});
|
||||
|
||||
|
||||
@@ -153,7 +189,7 @@ export default function OrdersInfo() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await patchOrderMutation.mutateAsync({ id: obj.id, status: status });
|
||||
await patchOrderMutation.mutateAsync({id: obj.id, status: status});
|
||||
refetch();
|
||||
} catch (error) {
|
||||
setOpenSnackbar(true);
|
||||
@@ -167,13 +203,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>
|
||||
))}
|
||||
|
||||
@@ -1,62 +1,86 @@
|
||||
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 { fetchStatisticsVolume, fetchStatisticsRevenue, fetchOrderStatus, 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 { data: dataVolume } = useQuery({
|
||||
const {user: loginData} = useAccount();
|
||||
|
||||
|
||||
const {data: dataVolume} = useQuery({
|
||||
queryKey: ["fetchStatisticsVolume", loginData],
|
||||
queryFn: () => fetchStatisticsVolume(loginData ? loginData : { email: "", password: "", session: "", customerId: -1, isAdmin: false }),
|
||||
queryFn: () => fetchStatisticsVolume(loginData ? loginData : {
|
||||
email: "",
|
||||
password: "",
|
||||
session: "",
|
||||
customerId: -1,
|
||||
isAdmin: false
|
||||
}),
|
||||
retry: 0,
|
||||
retryDelay: 0,
|
||||
});
|
||||
|
||||
const { data: dataRevenue } = useQuery({
|
||||
const {data: dataRevenue} = useQuery({
|
||||
queryKey: ["fetchStatisticsRevenue", loginData],
|
||||
queryFn: () => fetchStatisticsRevenue(loginData ? loginData : { email: "", password: "", session: "", customerId: -1, isAdmin: false }),
|
||||
queryFn: () => fetchStatisticsRevenue(loginData ? loginData : {
|
||||
email: "",
|
||||
password: "",
|
||||
session: "",
|
||||
customerId: -1,
|
||||
isAdmin: false
|
||||
}),
|
||||
retry: 0,
|
||||
retryDelay: 0,
|
||||
});
|
||||
|
||||
const { data: dataOrderStatus } = useQuery({
|
||||
const {data: dataOrderStatus} = useQuery({
|
||||
queryKey: ["fetchOrderStatus", loginData],
|
||||
queryFn: () => fetchOrderStatus(loginData ? loginData : { email: "", password: "", session: "", customerId: -1, isAdmin: false }),
|
||||
queryFn: () => fetchOrderStatus(loginData ? loginData : {
|
||||
email: "",
|
||||
password: "",
|
||||
session: "",
|
||||
customerId: -1,
|
||||
isAdmin: false
|
||||
}),
|
||||
retry: 0,
|
||||
retryDelay: 0,
|
||||
});
|
||||
|
||||
const { data: dataStockPercent } = useQuery({
|
||||
const {data: dataStockPercent} = useQuery({
|
||||
queryKey: ["fetchStockPercent", loginData],
|
||||
queryFn: () => fetchStockPercent(loginData ? loginData : { email: "", password: "", session: "", customerId: -1, isAdmin: false }),
|
||||
queryFn: () => fetchStockPercent(loginData ? loginData : {
|
||||
email: "",
|
||||
password: "",
|
||||
session: "",
|
||||
customerId: -1,
|
||||
isAdmin: false
|
||||
}),
|
||||
retry: 0,
|
||||
retryDelay: 0,
|
||||
});
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (dataVolume) {
|
||||
@@ -72,19 +96,19 @@ export default function StatisticsInfo() {
|
||||
cmmx[0].data.push(formattedDate);
|
||||
}
|
||||
const datapoint = dataVolume.catMonthMap[cat][timestamp]
|
||||
if(cmm.length == i) {
|
||||
cmm.push({ id: i, data: [], label: t(cat), type: "bar" })
|
||||
if (cmm.length == i) {
|
||||
cmm.push({id: i, data: [], label: t(cat), type: "bar"})
|
||||
}
|
||||
cmm[i].data.push(datapoint)
|
||||
|
||||
if(tv.length == i) {
|
||||
tv.push({ id: i, value: 0, label: t(cat)})
|
||||
if (tv.length == i) {
|
||||
tv.push({id: i, value: 0, label: t(cat)})
|
||||
}
|
||||
tv[i].value += datapoint
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
|
||||
setMonthlyVolume(cmm)
|
||||
setMonthlyVolumeXaxis(cmmx)
|
||||
setTotalVolume(tv)
|
||||
@@ -105,19 +129,19 @@ export default function StatisticsInfo() {
|
||||
cmmx[0].data.push(formattedDate);
|
||||
}
|
||||
const datapoint = dataRevenue.catMonthMap[cat][timestamp] / 100
|
||||
if(cmm.length == i) {
|
||||
cmm.push({ id: i, data: [], label: t(cat), type: "bar" })
|
||||
if (cmm.length == i) {
|
||||
cmm.push({id: i, data: [], label: t(cat), type: "bar"})
|
||||
}
|
||||
cmm[i].data.push(datapoint)
|
||||
|
||||
if(tv.length == i) {
|
||||
tv.push({ id: i, value: 0, label: t(cat)})
|
||||
if (tv.length == i) {
|
||||
tv.push({id: i, value: 0, label: t(cat)})
|
||||
}
|
||||
tv[i].value += datapoint
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
|
||||
setMonthlyRevenue(cmm)
|
||||
setMonthlyRevenueXaxis(cmmx)
|
||||
setTotalRevenue(tv)
|
||||
@@ -125,31 +149,40 @@ export default function StatisticsInfo() {
|
||||
}, [dataRevenue, monthlyRevenueXaxis, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if(dataOrderStatus) {
|
||||
if (dataOrderStatus) {
|
||||
const orderStatus = []
|
||||
for(const status in dataOrderStatus) {
|
||||
for (const status in dataOrderStatus) {
|
||||
orderStatus.push({value: dataOrderStatus[status], label: t(status)})
|
||||
}
|
||||
setOrderStatus(orderStatus)
|
||||
}
|
||||
}, [dataOrderStatus, t]) ;
|
||||
}, [dataOrderStatus, t]);
|
||||
|
||||
useEffect(() => {
|
||||
function generateName(percent: string) : string {
|
||||
function generateName(percent: string): string {
|
||||
return ">" + percent + "%";
|
||||
}
|
||||
if(dataStockPercent) {
|
||||
|
||||
if (dataStockPercent) {
|
||||
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))});
|
||||
for (let x = 0; x < 10; x++) {
|
||||
stockPercent.push({
|
||||
value: 0,
|
||||
label: generateName(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))
|
||||
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]
|
||||
if(index === -1) {
|
||||
index = stockPercent.push({value: 0, label: generateName(percent), color: getColorFromPercent(percent)}) -1
|
||||
if (index === -1) {
|
||||
index = stockPercent.push({
|
||||
value: 0,
|
||||
label: generateName(percent),
|
||||
color: getColorFromPercent(percent)
|
||||
}) - 1
|
||||
}
|
||||
stockPercent[index].value += datapoint
|
||||
}
|
||||
@@ -160,12 +193,12 @@ export default function StatisticsInfo() {
|
||||
}, [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>
|
||||
@@ -175,7 +208,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>
|
||||
@@ -186,36 +219,36 @@ 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={[{
|
||||
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={[{
|
||||
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>
|
||||
@@ -233,7 +266,7 @@ export default function StatisticsInfo() {
|
||||
hideLegend
|
||||
/>
|
||||
</Box>
|
||||
<Box className="vw20" sx={{ m: 2 }}>
|
||||
<Box className="vw20" sx={{m: 2}}>
|
||||
<Typography variant="h6" align="center" gutterBottom>
|
||||
{t("orderStatus")}
|
||||
</Typography>
|
||||
@@ -244,8 +277,8 @@ export default function StatisticsInfo() {
|
||||
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}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
.item-description{
|
||||
.item-description {
|
||||
display: flex;
|
||||
justify-content: space-between; /* Elemente an gegenüberliegenden Seiten platzieren */
|
||||
align-items: center; /* Vertikale Zentrierung */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item-card-button{
|
||||
.item-card-button {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.rating-card-body{
|
||||
.rating-card-body {
|
||||
display: grid;
|
||||
align-items: center; /* Vertikale Zentrierung */
|
||||
width: 100%;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.rating-button{
|
||||
.rating-button {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.rating-card-box{
|
||||
.rating-card-box {
|
||||
display: grid;
|
||||
align-items: center; /* Vertikale Zentrierung */
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rating-text-field{
|
||||
.rating-text-field {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
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 = {
|
||||
@@ -22,11 +14,11 @@ type FilterItemProps = {
|
||||
};
|
||||
|
||||
export default function FilterItem({
|
||||
filterName,
|
||||
filterItems,
|
||||
value,
|
||||
onChange,
|
||||
}: FilterItemProps) {
|
||||
filterName,
|
||||
filterItems,
|
||||
value,
|
||||
onChange,
|
||||
}: FilterItemProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!value && filterItems.length > 0) {
|
||||
@@ -40,7 +32,7 @@ export default function FilterItem({
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: "1.5rem" }}>
|
||||
<div style={{marginBottom: "1.5rem"}}>
|
||||
<FormLabel
|
||||
component="legend"
|
||||
sx={{
|
||||
@@ -58,7 +50,7 @@ export default function FilterItem({
|
||||
<FormControlLabel
|
||||
key={idx}
|
||||
value={item.value}
|
||||
control={<Radio />}
|
||||
control={<Radio/>}
|
||||
label={
|
||||
/^[1-5]$/.test(item.value) ? (
|
||||
<Rating
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
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 }) {
|
||||
export default function ItemCard({item}: { item: ItemWithImage }) {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const {t} = useTranslation();
|
||||
const navigate = useNavigate()
|
||||
const { addToBasket } = useBasket();
|
||||
const {addToBasket} = useBasket();
|
||||
|
||||
const handleAddToCart = () => {
|
||||
addToBasket(item, 1);
|
||||
@@ -19,18 +19,18 @@ export default function ItemCard({ item }: { item: ItemWithImage }) {
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
navigate(`/product/${item.id}`, { state: { item } });
|
||||
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/")) {
|
||||
setImageUrl("data:image/jpeg;base64," +imageUrl);
|
||||
|
||||
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={{height: "100%", width: "100%"}}>
|
||||
<CardActionArea onClick={handleClick} sx={{height: "100%"}} component="div">
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="140"
|
||||
@@ -49,9 +49,9 @@ export default function ItemCard({ item }: { item: ItemWithImage }) {
|
||||
<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">
|
||||
<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>
|
||||
<IconButton
|
||||
@@ -61,7 +61,7 @@ export default function ItemCard({ item }: { item: ItemWithImage }) {
|
||||
handleAddToCart();
|
||||
}}
|
||||
>
|
||||
<AddShoppingCart />
|
||||
<AddShoppingCart/>
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -70,11 +70,11 @@ export default function ItemCard({ item }: { item: ItemWithImage }) {
|
||||
{t('inStock')}
|
||||
</Typography>
|
||||
) : item.stock > 0 ? (
|
||||
<Typography variant="body2" sx={{ color: 'orange' }}>
|
||||
<Typography variant="body2" sx={{color: 'orange'}}>
|
||||
{t('almostSoldOut')}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ color: 'red' }}>
|
||||
<Typography variant="body2" sx={{color: 'red'}}>
|
||||
{t('outOfStock')}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Slider, Typography, Box, useTheme } from "@mui/material";
|
||||
import { useState, useEffect, SyntheticEvent } 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,8 @@ 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]);
|
||||
@@ -31,10 +31,10 @@ export default function PriceSlider({ min = 0, max = 10000, onChange }: PriceSli
|
||||
}
|
||||
};
|
||||
|
||||
const formatValueToEuro = (val: number) => `${(val/100).toFixed(2)} €`;
|
||||
const formatValueToEuro = (val: number) => `${(val / 100).toFixed(2)} €`;
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{mb: 4}}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
@@ -46,7 +46,7 @@ export default function PriceSlider({ min = 0, max = 10000, onChange }: PriceSli
|
||||
{t("price")}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ px: 1 }}>
|
||||
<Box sx={{px: 1}}>
|
||||
<Slider
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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;
|
||||
@@ -15,12 +15,19 @@ type LoginDialogProps = {
|
||||
setLoginData: React.Dispatch<React.SetStateAction<{ email: string; password: string, customerId: number }>>;
|
||||
};
|
||||
|
||||
const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, setLoginData, onSubmit }) => {
|
||||
const LoginDialog: React.FC<LoginDialogProps> = ({open, onClose, loginData, setLoginData, onSubmit}) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { login } = useAccount();
|
||||
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: "" }, langI18n: i18next.language, admin: false });
|
||||
const [registerData, setRegisterData] = useState<AccountType>({
|
||||
email: "",
|
||||
password: "",
|
||||
id: 0,
|
||||
customer: {id: 0, name: "", surname: "", address: "", country: "", zip: ""},
|
||||
langI18n: i18next.language,
|
||||
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
|
||||
|
||||
@@ -34,9 +41,8 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
}, [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,
|
||||
@@ -44,7 +50,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const { refetch: refetchAccount } = useQuery({
|
||||
const {refetch: refetchAccount} = useQuery({
|
||||
queryKey: ["fetchAccount", loginData],
|
||||
queryFn: () => fetchAccount(loginData),
|
||||
retry: 0,
|
||||
@@ -53,7 +59,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
});
|
||||
|
||||
// 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,
|
||||
@@ -110,11 +116,12 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
<Dialog open={open} onClose={handleClose} disableEnforceFocus>
|
||||
<form onSubmit={e => {
|
||||
e.preventDefault();
|
||||
if (showRegister){
|
||||
if (showRegister) {
|
||||
handleLogin();
|
||||
}else {
|
||||
} else {
|
||||
handleRegister();
|
||||
}}} noValidate>
|
||||
}
|
||||
}} noValidate>
|
||||
<DialogTitle>{showRegister ? t("register") : t("login")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
@@ -124,8 +131,8 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
fullWidth
|
||||
value={showRegister ? registerData.email : loginData.email}
|
||||
onChange={e => {
|
||||
setLoginData(prev => ({ ...prev, email: e.target.value }));
|
||||
setRegisterData(prev => ({ ...prev, email: e.target.value }))
|
||||
setLoginData(prev => ({...prev, email: e.target.value}));
|
||||
setRegisterData(prev => ({...prev, email: e.target.value}))
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
@@ -135,8 +142,8 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
fullWidth
|
||||
value={showRegister ? registerData.password : loginData.password}
|
||||
onChange={e => {
|
||||
setLoginData(prev => ({ ...prev, password: e.target.value }))
|
||||
setRegisterData(prev => ({ ...prev, password: e.target.value }))
|
||||
setLoginData(prev => ({...prev, password: e.target.value}))
|
||||
setRegisterData(prev => ({...prev, password: e.target.value}))
|
||||
}}
|
||||
/>
|
||||
{showRegister &&
|
||||
@@ -149,7 +156,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
value={registerData.customer.name}
|
||||
onChange={e => setRegisterData(prev => ({
|
||||
...prev,
|
||||
customer: { ...prev.customer, name: e.target.value },
|
||||
customer: {...prev.customer, name: e.target.value},
|
||||
}))}
|
||||
/>
|
||||
<TextField
|
||||
@@ -160,7 +167,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
value={registerData.customer.surname}
|
||||
onChange={e => setRegisterData(prev => ({
|
||||
...prev,
|
||||
customer: { ...prev.customer, surname: e.target.value },
|
||||
customer: {...prev.customer, surname: e.target.value},
|
||||
}))}
|
||||
/>
|
||||
<TextField
|
||||
@@ -171,7 +178,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
value={registerData.customer.address}
|
||||
onChange={e => setRegisterData(prev => ({
|
||||
...prev,
|
||||
customer: { ...prev.customer, address: e.target.value },
|
||||
customer: {...prev.customer, address: e.target.value},
|
||||
}))}
|
||||
/>
|
||||
<TextField
|
||||
@@ -182,7 +189,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
value={registerData.customer.country}
|
||||
onChange={e => setRegisterData(prev => ({
|
||||
...prev,
|
||||
customer: { ...prev.customer, country: e.target.value },
|
||||
customer: {...prev.customer, country: e.target.value},
|
||||
}))}
|
||||
/>
|
||||
<TextField
|
||||
@@ -193,7 +200,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
value={registerData.customer.zip}
|
||||
onChange={e => setRegisterData(prev => ({
|
||||
...prev,
|
||||
customer: { ...prev.customer, zip: e.target.value },
|
||||
customer: {...prev.customer, zip: e.target.value},
|
||||
}))}
|
||||
/>
|
||||
</>
|
||||
@@ -218,7 +225,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
<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"
|
||||
@@ -230,7 +237,7 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ open, onClose, loginData, set
|
||||
</Link>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ width: '100%', textAlign: 'center', pb: 2 }}>
|
||||
<Box sx={{width: '100%', textAlign: 'center', pb: 2}}>
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
|
||||
@@ -32,9 +32,11 @@
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.search:hover {
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.search-icon-wrapper {
|
||||
padding: 8px;
|
||||
height: 100%;
|
||||
@@ -44,6 +46,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { Autocomplete, Badge, TextField } from '@mui/material';
|
||||
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';
|
||||
@@ -11,36 +11,36 @@ 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 {useQuery} from '@tanstack/react-query';
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
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 {useAccount} from '../AccountProvider';
|
||||
import {fetchItemList} from '../query/Queries';
|
||||
import LoginDialog from './LoginDialog';
|
||||
import './NavBar.css';
|
||||
import { useBasket } from '../BasketProvider';
|
||||
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 { user, logout } = useAccount();
|
||||
const {user, logout} = useAccount();
|
||||
|
||||
const { basket } = useBasket();
|
||||
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
|
||||
@@ -54,8 +54,7 @@ export default function NavBar() {
|
||||
}
|
||||
return true; // alle anderen Seiten immer anzeigen
|
||||
})
|
||||
.map(key => ({ key, label: t(key) }));
|
||||
|
||||
.map(key => ({key, label: t(key)}));
|
||||
|
||||
|
||||
const settings = user
|
||||
@@ -65,12 +64,12 @@ export default function NavBar() {
|
||||
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>) => {
|
||||
@@ -109,7 +108,7 @@ export default function NavBar() {
|
||||
|
||||
|
||||
// useQuery, um die Item-Namen zu laden
|
||||
const { data: items = [] } = useQuery<Item[]>({
|
||||
const {data: items = []} = useQuery<Item[]>({
|
||||
queryKey: ["fetchItemList"],
|
||||
queryFn: fetchItemList,
|
||||
});
|
||||
@@ -151,7 +150,7 @@ export default function NavBar() {
|
||||
px: 3
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", minWidth: "0px" }}>
|
||||
<Box sx={{display: "flex", alignItems: "center", minWidth: "0px"}}>
|
||||
<img
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
@@ -175,9 +174,9 @@ export default function NavBar() {
|
||||
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flexGrow: 1, display: "flex", justifyContent: "center", px: 3, zIndex: 100000 }}>
|
||||
<Box sx={{flexGrow: 1, display: "flex", justifyContent: "center", px: 3, zIndex: 100000}}>
|
||||
<Autocomplete
|
||||
sx={{ flexGrow: 1, minWidth: "150px", maxWidth: "600px" }}
|
||||
sx={{flexGrow: 1, minWidth: "150px", maxWidth: "600px"}}
|
||||
freeSolo
|
||||
options={itemNames} // Item-Namen für Autocomplete
|
||||
onInputChange={handleSearch} // Suche auslösen
|
||||
@@ -188,7 +187,7 @@ export default function NavBar() {
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<SearchIcon sx={{ color: "white", mr: 1 }} />
|
||||
<SearchIcon sx={{color: "white", mr: 1}}/>
|
||||
),
|
||||
}}
|
||||
sx={{
|
||||
@@ -213,15 +212,15 @@ 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 }) => {
|
||||
<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}
|
||||
@@ -239,7 +238,7 @@ export default function NavBar() {
|
||||
<Button
|
||||
key={key}
|
||||
onClick={() => handleCloseNavMenu(key)}
|
||||
sx={{ color: "white", fontWeight: 500 }}
|
||||
sx={{color: "white", fontWeight: 500}}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
@@ -247,23 +246,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')}
|
||||
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>
|
||||
@@ -271,20 +270,20 @@ export default function NavBar() {
|
||||
</Menu>
|
||||
</Box>
|
||||
|
||||
<ThemeToggle />
|
||||
<ThemeToggle/>
|
||||
<Tooltip title={t('openSettings')} placement='bottom-end'>
|
||||
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
|
||||
<Avatar alt={avatarName} src="/static/images/avatar/2.jpg" />
|
||||
<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={() => {
|
||||
@@ -292,7 +291,7 @@ export default function NavBar() {
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Typography sx={{ textAlign: "center" }}>{label}</Typography>
|
||||
<Typography sx={{textAlign: "center"}}>{label}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
@@ -306,7 +305,7 @@ export default function NavBar() {
|
||||
loginData={loginData}
|
||||
setLoginData={setLoginData}
|
||||
/>
|
||||
<div className="navbar-offset" />
|
||||
<div className="navbar-offset"/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
import { Close, LocalShipping, ShoppingCart } from "@mui/icons-material";
|
||||
import { Alert, Box, Button, Card, Divider, Grid, IconButton, Rating, Snackbar, SnackbarCloseReason, Stack, TextField, Typography } from "@mui/material";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {Close, LocalShipping, ShoppingCart} from "@mui/icons-material";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Grid,
|
||||
IconButton,
|
||||
Rating,
|
||||
Snackbar,
|
||||
SnackbarCloseReason,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
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 }) {
|
||||
export default function ProductInfo({item}: { item: Item }) {
|
||||
|
||||
const { t } = useTranslation();
|
||||
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,
|
||||
@@ -34,7 +48,7 @@ export default function ProductInfo({ item }: { item: Item }) {
|
||||
color="inherit"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<Close fontSize="small" />
|
||||
<Close fontSize="small"/>
|
||||
</IconButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -49,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
|
||||
@@ -60,10 +74,10 @@ export default function ProductInfo({ item }: { item: Item }) {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:8085/image?uuid=${item.uuid}`);
|
||||
let data = await response.text();
|
||||
if(data.length == 0) {
|
||||
if (data.length == 0) {
|
||||
console.error("Got emtpy picture for article ", item.uuid);
|
||||
}
|
||||
if(!data.startsWith("data:image/")) {
|
||||
if (!data.startsWith("data:image/")) {
|
||||
data = "data:image/jpeg;base64," + data
|
||||
}
|
||||
setImageUrl(data);
|
||||
@@ -79,7 +93,7 @@ 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}
|
||||
@@ -107,7 +121,7 @@ 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')}
|
||||
|
||||
@@ -123,7 +137,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>
|
||||
@@ -138,7 +152,7 @@ export default function ProductInfo({ item }: { item: Item }) {
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
<Divider/>
|
||||
|
||||
<Box>
|
||||
{item.stock > 10 ? (
|
||||
@@ -146,7 +160,8 @@ export default function ProductInfo({ item }: { item: Item }) {
|
||||
{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>
|
||||
)}
|
||||
@@ -158,13 +173,13 @@ export default function ProductInfo({ item }: { item: Item }) {
|
||||
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
|
||||
@@ -173,9 +188,9 @@ export default function ProductInfo({ item }: { item: Item }) {
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Box sx={{mt: 2}}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<LocalShipping sx={{ mr: 1, verticalAlign: 'middle' }} />
|
||||
<LocalShipping sx={{mr: 1, verticalAlign: 'middle'}}/>
|
||||
{t('freeShipping')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
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 = () => {
|
||||
@@ -37,7 +29,7 @@ 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')}
|
||||
</Typography>
|
||||
@@ -51,7 +43,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>
|
||||
|
||||
@@ -10,17 +10,17 @@ import {
|
||||
Typography,
|
||||
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,
|
||||
@@ -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,7 @@ 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 +92,10 @@ 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>
|
||||
|
||||
@@ -148,7 +148,7 @@ 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()}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// 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');
|
||||
@@ -36,7 +36,7 @@ export const submitRating = async (ratingData: RatingSubmitType) => {
|
||||
throw new Error('Fehler beim Senden der Bewertung');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -219,8 +219,8 @@ export const editAccount = async (customer: CustomerType) => {
|
||||
|
||||
export const orderPatch = async (order: OrderPatch) => {
|
||||
const response = await fetch("http://localhost:8085/order?id=" + order.id + "&status=" + order.status, {
|
||||
method: "PATCH",
|
||||
}
|
||||
method: "PATCH",
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Order patch failed");
|
||||
|
||||
@@ -1,68 +1,73 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
border-color: #646cff;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import './components/i18n/i18n';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App.tsx';
|
||||
|
||||
@@ -10,6 +10,6 @@ if (!rootElement) throw new Error("Root element not found");
|
||||
const root = createRoot(rootElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<App/>
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -1,239 +1,239 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
Paper,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
Paper,
|
||||
Stack,
|
||||
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 navigate = useNavigate();
|
||||
const { user: userData, logout } = useAccount();
|
||||
const {t} = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const {user: userData, logout} = useAccount();
|
||||
|
||||
const [user, setUser] = useState<CustomerType>({
|
||||
name: "",
|
||||
surname: "",
|
||||
address: "",
|
||||
country: "",
|
||||
zip: "",
|
||||
id: userData?.customerId || 0,
|
||||
});
|
||||
const [user, setUser] = useState<CustomerType>({
|
||||
name: "",
|
||||
surname: "",
|
||||
address: "",
|
||||
country: "",
|
||||
zip: "",
|
||||
id: userData?.customerId || 0,
|
||||
});
|
||||
|
||||
const [userDataState, setUserDataState] = useState<User>(userData || {
|
||||
password: "",
|
||||
email: "",
|
||||
customerId: 0,
|
||||
session: "",
|
||||
isAdmin: false,
|
||||
});
|
||||
const [userDataState, setUserDataState] = useState<User>(userData || {
|
||||
password: "",
|
||||
email: "",
|
||||
customerId: 0,
|
||||
session: "",
|
||||
isAdmin: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (userData?.customerId) {
|
||||
setUser((prev) => ({
|
||||
...prev,
|
||||
id: userData.customerId,
|
||||
}));
|
||||
}
|
||||
}, [userData]);
|
||||
useEffect(() => {
|
||||
if (userData?.customerId) {
|
||||
setUser((prev) => ({
|
||||
...prev,
|
||||
id: userData.customerId,
|
||||
}));
|
||||
}
|
||||
}, [userData]);
|
||||
|
||||
const [edit, setEdit] = useState(false);
|
||||
const [form, setForm] = useState(user);
|
||||
const [edit, setEdit] = useState(false);
|
||||
const [form, setForm] = useState(user);
|
||||
|
||||
// Neu: Passwort-Dialog-Status und Passwort-Input
|
||||
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
||||
const [passwordInput, setPasswordInput] = useState("");
|
||||
// Neu: Passwort-Dialog-Status und Passwort-Input
|
||||
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
||||
const [passwordInput, setPasswordInput] = useState("");
|
||||
|
||||
const { data } = useQuery<CustomerType>({
|
||||
queryKey: ["fetchCustomer", userData?.customerId],
|
||||
queryFn: () => fetchCustomer(userData?.customerId || 0),
|
||||
retry: 1,
|
||||
retryDelay: 1000,
|
||||
});
|
||||
const {data} = useQuery<CustomerType>({
|
||||
queryKey: ["fetchCustomer", userData?.customerId],
|
||||
queryFn: () => fetchCustomer(userData?.customerId || 0),
|
||||
retry: 1,
|
||||
retryDelay: 1000,
|
||||
});
|
||||
|
||||
const { refetch: deleteRefetch } = useQuery({
|
||||
queryKey: ["deleteAccount", userDataState],
|
||||
queryFn: () => deleteAccount(userDataState!),
|
||||
enabled: false,
|
||||
});
|
||||
const {refetch: deleteRefetch} = useQuery({
|
||||
queryKey: ["deleteAccount", userDataState],
|
||||
queryFn: () => deleteAccount(userDataState!),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const { refetch: editRefetch } = useQuery({
|
||||
queryKey: ["editAccount", form],
|
||||
queryFn: () => editAccount(form),
|
||||
enabled: false,
|
||||
});
|
||||
const {refetch: editRefetch} = useQuery({
|
||||
queryKey: ["editAccount", form],
|
||||
queryFn: () => editAccount(form),
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setUser(data);
|
||||
setForm(data);
|
||||
}
|
||||
}, [data]);
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setUser(data);
|
||||
setForm(data);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const handleEdit = () => setEdit(true);
|
||||
const handleCancel = () => {
|
||||
setForm(user);
|
||||
setEdit(false);
|
||||
};
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setForm({ ...form, [e.target.name]: e.target.value });
|
||||
};
|
||||
const handleSave = async () => {
|
||||
setUser(form);
|
||||
setEdit(false);
|
||||
await editRefetch();
|
||||
};
|
||||
const handleEdit = () => setEdit(true);
|
||||
const handleCancel = () => {
|
||||
setForm(user);
|
||||
setEdit(false);
|
||||
};
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setForm({...form, [e.target.name]: e.target.value});
|
||||
};
|
||||
const handleSave = async () => {
|
||||
setUser(form);
|
||||
setEdit(false);
|
||||
await editRefetch();
|
||||
};
|
||||
|
||||
// Neu: Passwort-Dialog öffnen
|
||||
const handleDeleteClick = () => {
|
||||
setPasswordInput("");
|
||||
setPasswordDialogOpen(true);
|
||||
};
|
||||
// Neu: Passwort-Dialog öffnen
|
||||
const handleDeleteClick = () => {
|
||||
setPasswordInput("");
|
||||
setPasswordDialogOpen(true);
|
||||
};
|
||||
|
||||
// Neu: Passwort-Dialog schließen
|
||||
const handlePasswordDialogClose = () => {
|
||||
setPasswordDialogOpen(false);
|
||||
};
|
||||
// Neu: Passwort-Dialog schließen
|
||||
const handlePasswordDialogClose = () => {
|
||||
setPasswordDialogOpen(false);
|
||||
};
|
||||
|
||||
// Neu: Passwort-Eingabe bestätigen
|
||||
const handlePasswordConfirm = async () => {
|
||||
if (!passwordInput) {
|
||||
alert(t("pleaseEnterPassword"));
|
||||
return;
|
||||
}
|
||||
// Passwort in Form aktualisieren (hier z.B. als field "password", anpassen falls anders)
|
||||
setUserDataState({ ...userDataState, password: passwordInput });
|
||||
// Neu: Passwort-Eingabe bestätigen
|
||||
const handlePasswordConfirm = async () => {
|
||||
if (!passwordInput) {
|
||||
alert(t("pleaseEnterPassword"));
|
||||
return;
|
||||
}
|
||||
// Passwort in Form aktualisieren (hier z.B. als field "password", anpassen falls anders)
|
||||
setUserDataState({...userDataState, password: passwordInput});
|
||||
|
||||
// Erst User-Daten mit Passwort aktualisieren
|
||||
try {
|
||||
await editRefetch(); // Achtung: editRefetch verwendet immer noch alten form, daher call direkt mit updatedForm:
|
||||
// Danach Account löschen
|
||||
await deleteRefetch();
|
||||
logout();
|
||||
navigate("/");
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Löschen des Accounts:", error);
|
||||
alert(t("deleteAccountFailed"));
|
||||
} finally {
|
||||
setPasswordDialogOpen(false);
|
||||
}
|
||||
};
|
||||
// Erst User-Daten mit Passwort aktualisieren
|
||||
try {
|
||||
await editRefetch(); // Achtung: editRefetch verwendet immer noch alten form, daher call direkt mit updatedForm:
|
||||
// Danach Account löschen
|
||||
await deleteRefetch();
|
||||
logout();
|
||||
navigate("/");
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Löschen des Accounts:", error);
|
||||
alert(t("deleteAccountFailed"));
|
||||
} finally {
|
||||
setPasswordDialogOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="page-background page-background-center"
|
||||
sx={{ minHeight: "100vh", justifyContent: "flex-start", pt: 4 }}
|
||||
>
|
||||
<Paper elevation={3} sx={{ p: 4, maxWidth: 500, width: "100%", mx: "auto" }}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
{t("myAccount")}
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
label={t("name")}
|
||||
name="name"
|
||||
value={edit ? form.name : user.name}
|
||||
onChange={handleChange}
|
||||
disabled={!edit}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label={t("surname")}
|
||||
name="surname"
|
||||
value={edit ? form.surname : user.surname}
|
||||
onChange={handleChange}
|
||||
disabled={!edit}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label={t("address")}
|
||||
name="address"
|
||||
value={edit ? form.address : user.address}
|
||||
onChange={handleChange}
|
||||
disabled={!edit}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label={t("country")}
|
||||
name="country"
|
||||
value={edit ? form.country : user.country}
|
||||
onChange={handleChange}
|
||||
disabled={!edit}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label={t("zip")}
|
||||
name="zip"
|
||||
value={edit ? form.zip : user.zip}
|
||||
onChange={handleChange}
|
||||
disabled={!edit}
|
||||
fullWidth
|
||||
/>
|
||||
</Stack>
|
||||
<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}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="contained" color="primary" onClick={handleEdit}>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={handleDeleteClick} // Neu: Passwort-Dialog öffnen
|
||||
sx={{ marginLeft: "auto" }}
|
||||
>
|
||||
{t("deleteAccount")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
return (
|
||||
<Box
|
||||
className="page-background page-background-center"
|
||||
sx={{minHeight: "100vh", justifyContent: "flex-start", pt: 4}}
|
||||
>
|
||||
<Paper elevation={3} sx={{p: 4, maxWidth: 500, width: "100%", mx: "auto"}}>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
{t("myAccount")}
|
||||
</Typography>
|
||||
<Divider sx={{mb: 3}}/>
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
label={t("name")}
|
||||
name="name"
|
||||
value={edit ? form.name : user.name}
|
||||
onChange={handleChange}
|
||||
disabled={!edit}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label={t("surname")}
|
||||
name="surname"
|
||||
value={edit ? form.surname : user.surname}
|
||||
onChange={handleChange}
|
||||
disabled={!edit}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label={t("address")}
|
||||
name="address"
|
||||
value={edit ? form.address : user.address}
|
||||
onChange={handleChange}
|
||||
disabled={!edit}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label={t("country")}
|
||||
name="country"
|
||||
value={edit ? form.country : user.country}
|
||||
onChange={handleChange}
|
||||
disabled={!edit}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label={t("zip")}
|
||||
name="zip"
|
||||
value={edit ? form.zip : user.zip}
|
||||
onChange={handleChange}
|
||||
disabled={!edit}
|
||||
fullWidth
|
||||
/>
|
||||
</Stack>
|
||||
<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}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="contained" color="primary" onClick={handleEdit}>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={handleDeleteClick} // Neu: Passwort-Dialog öffnen
|
||||
sx={{marginLeft: "auto"}}
|
||||
>
|
||||
{t("deleteAccount")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Passwort-Dialog */}
|
||||
<Dialog open={passwordDialogOpen} onClose={handlePasswordDialogClose}>
|
||||
<DialogTitle>{t("confirmDeleteAccount")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>{t("enterPasswordToConfirmDeletion")}</Typography>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
label={t("password")}
|
||||
type="password"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
value={passwordInput}
|
||||
onChange={(e) => setPasswordInput(e.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handlePasswordDialogClose}>{t("cancel")}</Button>
|
||||
<Button color="error" onClick={handlePasswordConfirm}>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
{/* Passwort-Dialog */}
|
||||
<Dialog open={passwordDialogOpen} onClose={handlePasswordDialogClose}>
|
||||
<DialogTitle>{t("confirmDeleteAccount")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>{t("enterPasswordToConfirmDeletion")}</Typography>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
label={t("password")}
|
||||
type="password"
|
||||
fullWidth
|
||||
variant="standard"
|
||||
value={passwordInput}
|
||||
onChange={(e) => setPasswordInput(e.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handlePasswordDialogClose}>{t("cancel")}</Button>
|
||||
<Button color="error" onClick={handlePasswordConfirm}>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
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");
|
||||
|
||||
@@ -32,27 +19,27 @@ 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={{
|
||||
@@ -77,8 +64,8 @@ export default function AdminPanel() {
|
||||
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>
|
||||
))}
|
||||
|
||||
@@ -1,83 +1,90 @@
|
||||
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 }}>
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
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 farmingStation from '../assets/fscomponents/fs_components_0.png';
|
||||
import { ItemWithFSImage } from "../components/Item"; "../components/Item";
|
||||
import { fetchFarmingStationItemList } from "../helper/query/Queries";
|
||||
import {ItemWithFSImage} from "../components/Item";
|
||||
import {fetchFarmingStationItemList} from "../helper/query/Queries";
|
||||
|
||||
"../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"];
|
||||
|
||||
// Daten mit react-query laden
|
||||
const { data = [], isLoading, error } = useQuery<ItemWithFSImage[]>({
|
||||
const {data = [], isLoading, error} = useQuery<ItemWithFSImage[]>({
|
||||
queryKey: ['fetchFarmingStationItemList'],
|
||||
queryFn: fetchFarmingStationItemList,
|
||||
retry: 3,
|
||||
@@ -38,7 +40,7 @@ export default function FSComponents() {
|
||||
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',
|
||||
@@ -68,7 +70,7 @@ export default function FSComponents() {
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
startIcon={<AddShoppingCartIcon />}
|
||||
startIcon={<AddShoppingCartIcon/>}
|
||||
onClick={handleAddAllToCart}
|
||||
>
|
||||
{t('addAllToCart')}
|
||||
@@ -83,7 +85,7 @@ export default function FSComponents() {
|
||||
padding: 2,
|
||||
}}>
|
||||
<Box mb={2}>
|
||||
<Typography variant="h4" align="center" gutterBottom sx={{ color: 'text.primary' }}>
|
||||
<Typography variant="h4" align="center" gutterBottom sx={{color: 'text.primary'}}>
|
||||
{t('componentsFarmingStation')}
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -94,7 +96,7 @@ export default function FSComponents() {
|
||||
onMouseEnter={() => setHoverIndex(index)}
|
||||
onMouseLeave={() => setHoverIndex(null)}
|
||||
>
|
||||
<ItemCard item={item} />
|
||||
<ItemCard item={item}/>
|
||||
</div>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
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") }
|
||||
{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') },
|
||||
{value: "", label: t('allRatings')},
|
||||
...[5, 4, 3, 2, 1].map(value => ({
|
||||
value: value.toString(),
|
||||
label: value.toString()
|
||||
@@ -37,7 +37,7 @@ export default function Home() {
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [selectedRating, setSelectedRating] = useState<string | null>(null);
|
||||
|
||||
const { data = [], isLoading } = useQuery<ItemWithImage[]>({
|
||||
const {data = [], isLoading} = useQuery<ItemWithImage[]>({
|
||||
queryKey: ['fetchItemListWithImage'],
|
||||
queryFn: fetchItemListWithImage,
|
||||
retry: 3, // Versucht es 3-mal erneut
|
||||
@@ -68,25 +68,26 @@ export default function Home() {
|
||||
}, [location.search, categoriesFilter]);
|
||||
|
||||
// Filterfunktion bleibt gleich
|
||||
const filteredItems: ItemWithImage[] = useMemo(() => {return items
|
||||
.filter((item) => {
|
||||
const discountedPrice = item.price100 * (1 - item.discount100 / 100);
|
||||
return discountedPrice >= priceRange[0] && discountedPrice <= priceRange[1];
|
||||
})
|
||||
.filter((item) => {
|
||||
if (!selectedCategory) return true;
|
||||
return item.category.toLowerCase() === selectedCategory.toLowerCase();
|
||||
})
|
||||
.filter((item) => {
|
||||
if (!selectedRating) return true;
|
||||
const rating = item.rating;
|
||||
return rating >= (Number(selectedRating) * 2);
|
||||
})
|
||||
.filter((item) => {
|
||||
if (!searchQuery) return true;
|
||||
return (item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
});
|
||||
const filteredItems: ItemWithImage[] = useMemo(() => {
|
||||
return items
|
||||
.filter((item) => {
|
||||
const discountedPrice = item.price100 * (1 - item.discount100 / 100);
|
||||
return discountedPrice >= priceRange[0] && discountedPrice <= priceRange[1];
|
||||
})
|
||||
.filter((item) => {
|
||||
if (!selectedCategory) return true;
|
||||
return item.category.toLowerCase() === selectedCategory.toLowerCase();
|
||||
})
|
||||
.filter((item) => {
|
||||
if (!selectedRating) return true;
|
||||
const rating = item.rating;
|
||||
return rating >= (Number(selectedRating) * 2);
|
||||
})
|
||||
.filter((item) => {
|
||||
if (!searchQuery) return true;
|
||||
return (item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
});
|
||||
}, [items, priceRange, selectedCategory, selectedRating, searchQuery]);
|
||||
|
||||
|
||||
@@ -98,10 +99,6 @@ export default function Home() {
|
||||
}, [location.search]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Container Ref
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -143,7 +140,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
|
||||
@@ -168,7 +165,7 @@ 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
|
||||
@@ -193,7 +190,7 @@ export default function Home() {
|
||||
</Alert>
|
||||
) : (
|
||||
filteredItems.map(item => (
|
||||
<ItemCard key={item.id} item={item} />
|
||||
<ItemCard key={item.id} item={item}/>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {Box, Typography, Button, 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";
|
||||
|
||||
export default function NoPage() {
|
||||
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const {t} = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleGoHome = () => {
|
||||
@@ -16,7 +16,7 @@ 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
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Divider, List, ListItemButton, ListItemText, Paper, Stack, Tab, Tabs, 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 {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Stack,
|
||||
Tab,
|
||||
Tabs,
|
||||
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 "./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 { data: accountOrders, refetch } = useQuery<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
|
||||
enabled: !!user, // Nur ausführen, wenn user existiert
|
||||
@@ -25,7 +41,7 @@ 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);
|
||||
@@ -36,31 +52,31 @@ export default function Orders() {
|
||||
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}),
|
||||
retry: 0,
|
||||
retryDelay: 1000,
|
||||
enabled: false,
|
||||
});
|
||||
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,
|
||||
});
|
||||
|
||||
const handleCancelOrder = async() => {
|
||||
const handleCancelOrder = async () => {
|
||||
await cancleOrder();
|
||||
setSelectedOrder(null);
|
||||
refetch();
|
||||
};
|
||||
|
||||
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')}
|
||||
</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>
|
||||
@@ -68,7 +84,7 @@ export default function Orders() {
|
||||
<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')}`}
|
||||
secondary={`${t('sum')}: ${(order.total / 100).toFixed(2)} € • ${order.orderItems.length} ${t('items')}`}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
@@ -83,7 +99,7 @@ export default function Orders() {
|
||||
<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')}`}
|
||||
secondary={`${t('sum')}: ${(order.total / 100).toFixed(2)} € • ${order.orderItems.length} ${t('items')}`}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
@@ -100,8 +116,9 @@ export default function Orders() {
|
||||
<DialogContent dividers>
|
||||
{selectedOrder && (
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="subtitle1">{`${t('orderDate')}: ${new Date(selectedOrder.time).toUTCString()}`}</Typography>
|
||||
<Divider />
|
||||
<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) => (
|
||||
@@ -111,8 +128,9 @@ 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>
|
||||
|
||||
@@ -15,25 +15,25 @@ import {
|
||||
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 {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';
|
||||
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[]) {
|
||||
return basket.length === 0 ? (
|
||||
<Typography color="error" sx={{ my: 2 }}>
|
||||
<Typography color="error" sx={{my: 2}}>
|
||||
{t('basketEmpty')}
|
||||
</Typography>
|
||||
) : (
|
||||
@@ -44,7 +44,7 @@ function generateBasket(t: TFunction<"translation", undefined>, basket: BasketIt
|
||||
primary={`${item.quantity}x ${item.item.name}`}
|
||||
secondary={`Item ID: ${item.item.uuid}`}
|
||||
/>
|
||||
|
||||
|
||||
<div className="rightBound">
|
||||
{`${(item.quantity * getDiscountedPrice(item.item)).toFixed(2)} €`}<br/>
|
||||
{item.item.discount100 > 0 ? <a className='rightBound red'>{-item.item.discount100}%</a> : ""}
|
||||
@@ -59,15 +59,15 @@ function generateTotal(t: TFunction<"translation", undefined>, basket: BasketIte
|
||||
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) + ` €`}
|
||||
.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>({
|
||||
@@ -80,7 +80,7 @@ export default function Payment() {
|
||||
});
|
||||
const [orderNumber, setOrderNumber] = useState<string | null>(null);
|
||||
const steps = [t('reviewCart'), t('shippingDetails'), t('payment'), t('orderSummary')];
|
||||
const { user } = useAccount();
|
||||
const {user} = useAccount();
|
||||
|
||||
const submitOrderData: OrderType = {
|
||||
id: 0, // This will be set by the backend
|
||||
@@ -95,7 +95,7 @@ export default function Payment() {
|
||||
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,
|
||||
@@ -109,30 +109,30 @@ export default function Payment() {
|
||||
</Alert>
|
||||
};
|
||||
|
||||
const { refetch: customerData } = useQuery<CustomerType>({
|
||||
queryKey: ['fetchCustomer', user?.customerId],
|
||||
queryFn: () => fetchCustomer(user?.customerId || 0), // Funktion zum Abrufen der Kundendaten
|
||||
enabled: false
|
||||
});
|
||||
const {refetch: customerData} = useQuery<CustomerType>({
|
||||
queryKey: ['fetchCustomer', user?.customerId],
|
||||
queryFn: () => fetchCustomer(user?.customerId || 0), // Funktion zum Abrufen der Kundendaten
|
||||
enabled: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchShippingDetails = async () => {
|
||||
if (user) {
|
||||
try {
|
||||
const userShippingDetails = (await customerData()).data;
|
||||
setShippingDetails(userShippingDetails || shippingDetails);
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Laden der Kundendaten:", error);
|
||||
}
|
||||
useEffect(() => {
|
||||
const fetchShippingDetails = async () => {
|
||||
if (user) {
|
||||
try {
|
||||
const userShippingDetails = (await customerData()).data;
|
||||
setShippingDetails(userShippingDetails || shippingDetails);
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Laden der Kundendaten:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchShippingDetails();
|
||||
}, [user, customerData, shippingDetails]);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
fetchShippingDetails();
|
||||
}, [user, customerData, shippingDetails]);
|
||||
|
||||
|
||||
// Verwende useMutation statt useQuery für submitOrder
|
||||
const { mutateAsync: submitOrderMutation } = useMutation({
|
||||
const {mutateAsync: submitOrderMutation} = useMutation({
|
||||
mutationFn: (orderData: OrderType) => submitOrder(orderData),
|
||||
});
|
||||
|
||||
@@ -158,12 +158,12 @@ export default function Payment() {
|
||||
|
||||
try {
|
||||
await submitOrderMutation(orderData);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
next = false;
|
||||
}
|
||||
}
|
||||
if(next) {
|
||||
if (next) {
|
||||
setActiveStep((prevStep) => prevStep + 1);
|
||||
} else {
|
||||
showAlert();
|
||||
@@ -175,7 +175,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,
|
||||
@@ -205,11 +205,12 @@ export default function Payment() {
|
||||
{t('reviewCart')}
|
||||
</Typography>
|
||||
{generateBasket(t, basket)}
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Button variant="outlined" color="error" onClick={handleClearBasket} disabled={basket.length === 0}>
|
||||
<Divider sx={{my: 2}}/>
|
||||
<Button variant="outlined" color="error" onClick={handleClearBasket}
|
||||
disabled={basket.length === 0}>
|
||||
{t('clearCart')}
|
||||
</Button>
|
||||
{generateTotal(t,basket)}
|
||||
{generateTotal(t, basket)}
|
||||
</Box>
|
||||
);
|
||||
case 1:
|
||||
@@ -290,17 +291,17 @@ export default function Payment() {
|
||||
<Typography variant="body2" gutterBottom>
|
||||
{t('yourOrderNumber')}: <strong>{orderNumber}</strong>
|
||||
</Typography>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<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 }} />
|
||||
<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/>
|
||||
</Box>
|
||||
@@ -320,7 +321,7 @@ export default function Payment() {
|
||||
overflowY: 'auto'
|
||||
}}
|
||||
>
|
||||
<Paper elevation={3} sx={{ p: 4 }}>
|
||||
<Paper elevation={3} sx={{p: 4}}>
|
||||
<Typography variant="h4" align="center" gutterBottom>
|
||||
{t('completeYourOrder')}
|
||||
</Typography>
|
||||
@@ -331,8 +332,8 @@ 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}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Divider,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
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 {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();
|
||||
@@ -71,27 +65,27 @@ export default function Product() {
|
||||
}}
|
||||
>
|
||||
<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}
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
width: 100%;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.toppad {
|
||||
padding: 20px 0;
|
||||
}
|
||||
@@ -163,6 +164,7 @@
|
||||
.rightBound {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
import { CssBaseline, GlobalStyles } from '@mui/material';
|
||||
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';
|
||||
@@ -13,7 +13,8 @@ interface ThemeContextType {
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType>({
|
||||
mode: 'light',
|
||||
toggleMode: () => {},
|
||||
toggleMode: () => {
|
||||
},
|
||||
});
|
||||
|
||||
export const useThemeMode = () => useContext(ThemeContext);
|
||||
@@ -22,9 +23,9 @@ 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');
|
||||
@@ -165,17 +166,17 @@ export const CustomThemeProvider: React.FC<CustomThemeProviderProps> = ({ childr
|
||||
// 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>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// 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 {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')}>
|
||||
|
||||
<IconButton
|
||||
<IconButton
|
||||
onClick={toggleMode}
|
||||
color="inherit"
|
||||
sx={{
|
||||
@@ -22,7 +22,7 @@ const ThemeToggle: React.FC = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{mode === 'dark' ? <Brightness7 /> : <Brightness4 />}
|
||||
{mode === 'dark' ? <Brightness7/> : <Brightness4/>}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import {createTheme} from '@mui/material/styles';
|
||||
import './theme-augmentation.d.ts'; // Falls vorhanden
|
||||
|
||||
export const darkmode = createTheme({
|
||||
|
||||
@@ -10,7 +10,7 @@ export function mapValueToColor(minVal: number, maxVal: number, actualVal: numbe
|
||||
|
||||
export function getColorFromPercent(percent: string): string {
|
||||
let perc = Number(percent) / 100
|
||||
if(perc > 1){
|
||||
if (perc > 1) {
|
||||
perc = 2.5;
|
||||
}
|
||||
return hsvDegToHex(120 * perc);
|
||||
@@ -21,26 +21,38 @@ export function hsvDegToHex(h: number): string {
|
||||
|
||||
const c = 1;
|
||||
const x = (1 - Math.abs(((h / 60) % 2) - 1));
|
||||
|
||||
|
||||
let r, g, b;
|
||||
if (h < 60) {
|
||||
r = c; g = x; b = 0;
|
||||
r = c;
|
||||
g = x;
|
||||
b = 0;
|
||||
} else if (h < 120) {
|
||||
r = x; g = c; b = 0;
|
||||
r = x;
|
||||
g = c;
|
||||
b = 0;
|
||||
} else if (h < 180) {
|
||||
r = 0; g = c; b = x;
|
||||
r = 0;
|
||||
g = c;
|
||||
b = x;
|
||||
} else if (h < 240) {
|
||||
r = 0; g = x; b = c;
|
||||
r = 0;
|
||||
g = x;
|
||||
b = c;
|
||||
} else if (h < 300) {
|
||||
r = x; g = 0; b = c;
|
||||
r = x;
|
||||
g = 0;
|
||||
b = c;
|
||||
} else {
|
||||
r = c; g = 0; b = x;
|
||||
r = c;
|
||||
g = 0;
|
||||
b = x;
|
||||
}
|
||||
|
||||
|
||||
// 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');
|
||||
|
||||
|
||||
return `#${rHex}${gHex}${bHex}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user