From 483cb4b04319b7423e70f8eb20f4442d625069d0 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 11 Mar 2026 12:30:20 +0100 Subject: [PATCH] prettier --- 01-frontend/src/App.css | 69 +- 01-frontend/src/App.tsx | 95 ++- 01-frontend/src/components/Account.tsx | 77 +- 01-frontend/src/components/Item.tsx | 68 +- 01-frontend/src/components/Order.tsx | 47 +- 01-frontend/src/components/Rating.tsx | 8 +- 01-frontend/src/components/RatingSubmit.tsx | 8 +- 01-frontend/src/components/i18n/i18n.ts | 28 +- 01-frontend/src/helper/AccountProvider.tsx | 66 +- 01-frontend/src/helper/BasketProvider.tsx | 90 +-- .../src/helper/adminpanel/AccountsInfo.tsx | 358 +++++---- .../helper/adminpanel/CustomerEditDialog.tsx | 209 ++--- .../src/helper/adminpanel/ItemImageDialog.tsx | 430 ++++++----- .../src/helper/adminpanel/ItemsInfo.tsx | 504 ++++++------ .../src/helper/adminpanel/NewItemDialog.tsx | 298 ++++---- .../src/helper/adminpanel/OrdersInfo.tsx | 419 +++++----- .../src/helper/adminpanel/StatisticsInfo.tsx | 581 +++++++------- 01-frontend/src/helper/helper.css | 34 +- .../src/helper/homepage/FilterItem.tsx | 130 ++-- 01-frontend/src/helper/homepage/ItemCard.css | 16 +- 01-frontend/src/helper/homepage/ItemCard.tsx | 223 ++++-- .../src/helper/homepage/PriceSlider.tsx | 143 ++-- 01-frontend/src/helper/navbar/LoginDialog.tsx | 530 +++++++------ 01-frontend/src/helper/navbar/NavBar.css | 82 +- 01-frontend/src/helper/navbar/NavBar.tsx | 638 ++++++++-------- .../src/helper/productpage/ProductInfo.tsx | 382 +++++----- .../src/helper/productpage/RatingCard.tsx | 100 +-- .../src/helper/productpage/Ratings.tsx | 286 +++---- 01-frontend/src/helper/query/Queries.tsx | 562 ++++++++------ 01-frontend/src/index.css | 84 +- 01-frontend/src/main.tsx | 20 +- 01-frontend/src/pages/Account.tsx | 447 +++++------ 01-frontend/src/pages/AdminPanel.tsx | 155 ++-- 01-frontend/src/pages/Contact.tsx | 181 +++-- 01-frontend/src/pages/FSComponents.tsx | 222 +++--- 01-frontend/src/pages/Home.tsx | 355 ++++----- 01-frontend/src/pages/NoPage.tsx | 71 +- 01-frontend/src/pages/Orders.tsx | 300 ++++---- 01-frontend/src/pages/Payment.tsx | 716 +++++++++--------- 01-frontend/src/pages/Product.tsx | 166 ++-- 01-frontend/src/pages/pages.css | 198 ++--- 01-frontend/src/theme/ThemeContext.tsx | 347 +++++---- 01-frontend/src/theme/ThemeToggle.tsx | 47 +- 01-frontend/src/theme/theme-augmentation.d.ts | 20 +- 01-frontend/src/theme/theme.ts | 396 +++++----- 01-frontend/src/util/ColorUtil.tsx | 100 +-- 46 files changed, 5456 insertions(+), 4850 deletions(-) diff --git a/01-frontend/src/App.css b/01-frontend/src/App.css index dc24bd7..b958ee6 100644 --- a/01-frontend/src/App.css +++ b/01-frontend/src/App.css @@ -1,63 +1,64 @@ /* App.css - CSS-Variablen-basierte Version */ :root { - --background-color: #fafafa; - --text-color: #000000; - --navbar-height: 5.1vh; - --page-height: 94vh; + --background-color: #fafafa; + --text-color: #000000; + --navbar-height: 5.1vh; + --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); +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); } .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; } diff --git a/01-frontend/src/App.tsx b/01-frontend/src/App.tsx index 8f7bdcc..5975282 100644 --- a/01-frontend/src/App.tsx +++ b/01-frontend/src/App.tsx @@ -1,52 +1,51 @@ -import {StyledEngineProvider} from '@mui/material/styles'; -import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; -import {BrowserRouter, Route, Routes} from 'react-router-dom'; -import './App.css'; -import {BasketProvider} from './helper/BasketProvider'; -import NavBar from './helper/navbar/NavBar'; -import Account from './pages/Account'; -import Contact from './pages/Contact'; -import Home from './pages/Home'; -import NoPage from './pages/NoPage'; -import Orders from './pages/Orders'; -import Payment from './pages/Payment'; -import Product from './pages/Product'; -import {CustomThemeProvider} from './theme/ThemeContext'; -import FSComponents from './pages/FSComponents.tsx'; -import AdminPanel from './pages/AdminPanel'; -import {AccountProvider} from './helper/AccountProvider.tsx'; -import {CookiesProvider} from 'react-cookie'; +import { StyledEngineProvider } from "@mui/material/styles"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import "./App.css"; +import { BasketProvider } from "./helper/BasketProvider"; +import NavBar from "./helper/navbar/NavBar"; +import Account from "./pages/Account"; +import Contact from "./pages/Contact"; +import Home from "./pages/Home"; +import NoPage from "./pages/NoPage"; +import Orders from "./pages/Orders"; +import Payment from "./pages/Payment"; +import Product from "./pages/Product"; +import { CustomThemeProvider } from "./theme/ThemeContext"; +import FSComponents from "./pages/FSComponents.tsx"; +import AdminPanel from "./pages/AdminPanel"; +import { AccountProvider } from "./helper/AccountProvider.tsx"; +import { CookiesProvider } from "react-cookie"; export default function App() { + const queryClient = new QueryClient(); - const queryClient = new QueryClient(); - - return ( - - - - - - - - - - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - }/> - - - - - - - - - ) + return ( + + + + + + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + + + + + ); } diff --git a/01-frontend/src/components/Account.tsx b/01-frontend/src/components/Account.tsx index dc81756..eab425e 100644 --- a/01-frontend/src/components/Account.tsx +++ b/01-frontend/src/components/Account.tsx @@ -1,58 +1,57 @@ type AccountType = { + id: number; + customer: { id: number; - customer: { - id: number; - name: string; - surname: string; - address: string; - country: string; - zip: string; - }; - password: string; - langI18n: string; - admin: boolean; - email: string; + name: string; + surname: string; + address: string; + country: string; + zip: string; + }; + password: string; + langI18n: string; + admin: boolean; + email: string; }; export default AccountType; -export type CustomerType = - { - id: number; - name: string; - surname: string; - address: string; - country: string; - zip: string; - } +export type CustomerType = { + id: number; + name: string; + surname: string; + address: string; + country: string; + zip: string; +}; export type SubmitLogin = { - email: string; - password: string; + email: string; + password: string; }; export type SubmitLoginSession = { - email: string; - session: string; + email: string; + session: string; }; export type AdminAccountOperation = { - email: string; - uuid: string; - accountId: number; -} + email: string; + uuid: 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; -}; \ No newline at end of file + user: User | null; + login: (userData: User) => void; + logout: () => void; +}; diff --git a/01-frontend/src/components/Item.tsx b/01-frontend/src/components/Item.tsx index 0b968cc..cc774ce 100644 --- a/01-frontend/src/components/Item.tsx +++ b/01-frontend/src/components/Item.tsx @@ -1,43 +1,43 @@ export type Item = { - id: number; - uuid: string; - name: string; - description: string; - price100: number; - stock: number; - stockExpected: number; - category: string; - rating: number; - discount100: number; + id: number; + uuid: string; + name: string; + description: string; + price100: number; + stock: number; + stockExpected: number; + category: string; + rating: number; + discount100: number; }; type ItemWithImage = { - id: number; - uuid: string; - name: string; - description: string; - price100: number; - stock: number; - stockExpected: number; - category: string; - rating: number; - discount100: number; - image: string; + id: number; + uuid: string; + name: string; + description: string; + price100: number; + stock: number; + stockExpected: number; + category: string; + rating: number; + discount100: number; + image: string; }; export type ItemWithFSImage = { - id: number; - uuid: string; - name: string; - description: string; - price100: number; - stock: number; - stockExpected: number; - category: string; - rating: number; - discount100: number; - image: string; - farmImage: string; + id: number; + uuid: string; + name: string; + description: string; + price100: number; + stock: number; + stockExpected: number; + category: string; + rating: number; + discount100: number; + image: string; + farmImage: string; }; -export default ItemWithImage; \ No newline at end of file +export default ItemWithImage; diff --git a/01-frontend/src/components/Order.tsx b/01-frontend/src/components/Order.tsx index d1a0e61..2f512cf 100644 --- a/01-frontend/src/components/Order.tsx +++ b/01-frontend/src/components/Order.tsx @@ -1,40 +1,39 @@ export enum OrderStatusEnum { - ORDERED = 'ORDERED', - IN_PROGRESS = 'IN_PROGRESS', - ISSUES = 'ISSUES', - DELIVERED = 'DELIVERED', - CANCELLED = 'CANCELLED', + ORDERED = "ORDERED", + IN_PROGRESS = "IN_PROGRESS", + ISSUES = "ISSUES", + DELIVERED = "DELIVERED", + CANCELLED = "CANCELLED", } type OrderType = { - id: number; - customerId: number; - time: number; - status: OrderStatusEnum; - orderItems: { id: number; amount: number; article: string }[]; - total: number; + id: number; + customerId: number; + time: number; + status: OrderStatusEnum; + orderItems: { id: number; amount: number; article: string }[]; + total: number; }; export default OrderType; - export type ShippingDetails = { - firstName: string; - lastName: string; - telefon: string; - address: string; - postalCode: string; - city: string; - country: string; + firstName: string; + lastName: string; + telefon: string; + address: string; + postalCode: string; + city: string; + country: string; }; export type OrderItem = { - id: string; - amount: number; - article: string; // UUID of the item + id: string; + amount: number; + article: string; // UUID of the item }; export type OrderPatch = { - id: number; - status: OrderStatusEnum; + id: number; + status: OrderStatusEnum; }; diff --git a/01-frontend/src/components/Rating.tsx b/01-frontend/src/components/Rating.tsx index c767c00..27b5ddd 100644 --- a/01-frontend/src/components/Rating.tsx +++ b/01-frontend/src/components/Rating.tsx @@ -1,7 +1,7 @@ type RatingType = { - rating: number; - content: string; - timestamp: number; + rating: number; + content: string; + timestamp: number; }; -export default RatingType; \ No newline at end of file +export default RatingType; diff --git a/01-frontend/src/components/RatingSubmit.tsx b/01-frontend/src/components/RatingSubmit.tsx index 9cd06da..12430f3 100644 --- a/01-frontend/src/components/RatingSubmit.tsx +++ b/01-frontend/src/components/RatingSubmit.tsx @@ -1,7 +1,7 @@ type RatingSubmitType = { - articleId: string; - rating: number; - content: string; + articleId: string; + rating: number; + content: string; }; -export default RatingSubmitType; \ No newline at end of file +export default RatingSubmitType; diff --git a/01-frontend/src/components/i18n/i18n.ts b/01-frontend/src/components/i18n/i18n.ts index d7fc833..423ffb8 100644 --- a/01-frontend/src/components/i18n/i18n.ts +++ b/01-frontend/src/components/i18n/i18n.ts @@ -1,19 +1,19 @@ 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"; i18next - .use(HttpBackend) - .use(LanguageDetector) - .use(initReactI18next) - .init({ - fallbackLng: "en", - debug: false, - interpolation: { - escapeValue: false, - }, - backend: { - loadPath: "/locales/{{lng}}/translation.json", - } - }); + .use(HttpBackend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: "en", + debug: false, + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: "/locales/{{lng}}/translation.json", + }, + }); diff --git a/01-frontend/src/helper/AccountProvider.tsx b/01-frontend/src/helper/AccountProvider.tsx index 47e58d5..7cb767f 100644 --- a/01-frontend/src/helper/AccountProvider.tsx +++ b/01-frontend/src/helper/AccountProvider.tsx @@ -1,42 +1,48 @@ -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(undefined); -export const AccountProvider = ({children}: { children: ReactNode }) => { - const [cookies, setCookie] = useCookies(["account"]); +export const AccountProvider = ({ children }: { children: ReactNode }) => { + const [cookies, setCookie] = useCookies(["account"]); - const initialAccount = - typeof cookies.account === "object" && !Array.isArray(cookies.account) - ? cookies.account - : null; - const [user, setUser] = useState(initialAccount); + const initialAccount = + typeof cookies.account === "object" && !Array.isArray(cookies.account) + ? cookies.account + : null; + const [user, setUser] = useState(initialAccount); - const login = (userData: User) => { - setUser(userData); - }; + const login = (userData: User) => { + setUser(userData); + }; - const logout = () => { - setUser(null); - }; + const logout = () => { + setUser(null); + }; - useEffect(() => { - setCookie("account", user, {path: "/", maxAge: 3600 * 24 * 7}); - }, [user, setCookie]); + useEffect(() => { + setCookie("account", user, { path: "/", maxAge: 3600 * 24 * 7 }); + }, [user, setCookie]); - return ( - - {children} - - ); + return ( + + {children} + + ); }; // eslint-disable-next-line react-refresh/only-export-components export const useAccount = () => { - const context = useContext(AccountContext); - if (!context) { - throw new Error("useAccount must be used within an AccountProvider"); - } - return context; -}; \ No newline at end of file + const context = useContext(AccountContext); + if (!context) { + throw new Error("useAccount must be used within an AccountProvider"); + } + return context; +}; diff --git a/01-frontend/src/helper/BasketProvider.tsx b/01-frontend/src/helper/BasketProvider.tsx index 9487188..d004626 100644 --- a/01-frontend/src/helper/BasketProvider.tsx +++ b/01-frontend/src/helper/BasketProvider.tsx @@ -1,60 +1,64 @@ -import React, {createContext, useContext, useEffect, useState} from 'react'; -import Item from '../components/Item'; -import {useCookies} from 'react-cookie'; +import React, { createContext, useContext, useEffect, useState } from "react"; +import Item from "../components/Item"; +import { useCookies } from "react-cookie"; export interface BasketItem { - item: Item; - quantity: number; + item: Item; + quantity: number; } interface BasketContextType { - basket: BasketItem[]; - addToBasket: (item: Item, quantity: number) => void; - clearBasket: () => void; + basket: BasketItem[]; + addToBasket: (item: Item, quantity: number) => void; + clearBasket: () => void; } const BasketContext = createContext(undefined); -export const BasketProvider: React.FC<{ children: React.ReactNode }> = ({children}) => { - const [cookies, setCookie] = useCookies(["basket"]); - const [basket, setBasket] = useState(cookies.basket || []); +export const BasketProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [cookies, setCookie] = useCookies(["basket"]); + const [basket, setBasket] = useState(cookies.basket || []); - const addToBasket = (item: Item, quantity: number) => { - setBasket((prevBasket) => { - const existingItem = prevBasket.find((basketItem) => basketItem.item.uuid === item.uuid); - if (existingItem) { - // Update quantity if item already exists - return prevBasket.map((basketItem) => - basketItem.item.uuid === item.uuid - ? {...basketItem, quantity: basketItem.quantity + quantity} - : basketItem - ); - } - // Add new item to basket - return [...prevBasket, {item, quantity}]; - }); - }; + const addToBasket = (item: Item, quantity: number) => { + setBasket((prevBasket) => { + const existingItem = prevBasket.find( + (basketItem) => basketItem.item.uuid === item.uuid, + ); + if (existingItem) { + // Update quantity if item already exists + return prevBasket.map((basketItem) => + basketItem.item.uuid === item.uuid + ? { ...basketItem, quantity: basketItem.quantity + quantity } + : basketItem, + ); + } + // Add new item to basket + return [...prevBasket, { item, quantity }]; + }); + }; - const clearBasket = () => { - setBasket([]); - }; + const clearBasket = () => { + setBasket([]); + }; - useEffect(() => { - setCookie("basket", basket, {path: "/", maxAge: 3600 * 24 * 7}); // 7 Tage - }, [basket, setCookie]); + useEffect(() => { + setCookie("basket", basket, { path: "/", maxAge: 3600 * 24 * 7 }); // 7 Tage + }, [basket, setCookie]); - return ( - - {children} - - ); + return ( + + {children} + + ); }; // eslint-disable-next-line react-refresh/only-export-components export const useBasket = () => { - const context = useContext(BasketContext); - if (!context) { - throw new Error('useBasket must be used within a BasketProvider'); - } - return context; -}; \ No newline at end of file + const context = useContext(BasketContext); + if (!context) { + throw new Error("useBasket must be used within a BasketProvider"); + } + return context; +}; diff --git a/01-frontend/src/helper/adminpanel/AccountsInfo.tsx b/01-frontend/src/helper/adminpanel/AccountsInfo.tsx index 19a201b..8143e64 100644 --- a/01-frontend/src/helper/adminpanel/AccountsInfo.tsx +++ b/01-frontend/src/helper/adminpanel/AccountsInfo.tsx @@ -1,183 +1,211 @@ 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 theme = useTheme(); + const { t } = useTranslation(); - const [customerData, setCustomerData] = useState({ - id: 0, - name: "", - surname: "", - address: "", - zip: "", - country: "" - }); + const [customerData, setCustomerData] = useState({ + id: 0, + name: "", + surname: "", + address: "", + zip: "", + country: "", + }); - async function handleCustomerEdit(account: AccountType) { - setCustomerData(account.customer); - setOpen(true); + async function handleCustomerEdit(account: AccountType) { + setCustomerData(account.customer); + setOpen(true); + } + + const [rows, setRows] = useState([]); + const [selectedRows, setSelectedRows] = useState>(new Set()); + + const { user: loginData } = useAccount(); + + const { data, refetch } = useQuery({ + queryKey: ["fetchAccounts", loginData], + queryFn: () => + fetchAccounts( + loginData + ? loginData + : { + email: "", + password: "", + session: "", + customerId: -1, + isAdmin: false, + }, + ), + retry: 3, + retryDelay: 1000, + }); + + const deleteAccount = useMutation({ + mutationFn: (user: AdminAccountOperation) => deleteAccountAdmin(user), + }); + + useEffect(() => { + if (data) { + setRows(data); } + }, [data]); - const [rows, setRows] = useState([]); - const [selectedRows, setSelectedRows] = useState>(new Set()); + const handleSelectionChange = (newSelection: GridRowSelectionModel) => { + setSelectedRows(newSelection.ids); + }; - const {user: loginData} = useAccount(); - - const {data, refetch} = useQuery({ - queryKey: ["fetchAccounts", loginData], - queryFn: () => fetchAccounts(loginData ? loginData : { - email: "", - password: "", - session: "", - customerId: -1, - isAdmin: false - }), - retry: 3, - retryDelay: 1000, + const handleDeleteSelected = async () => { + selectedRows.forEach(async (rowId) => { + let id = rows.find((row) => row.id === rowId)?.id; + if (id === undefined) id = -1; + await deleteAccount.mutateAsync({ + email: loginData?.email || "", + uuid: loginData?.session || "", + accountId: id, + }); }); - const deleteAccount = useMutation({ - mutationFn: (user: AdminAccountOperation) => - deleteAccountAdmin(user), - }); + setRows(rows.filter((row) => !selectedRows.has(row.id))); + }; - useEffect(() => { - if (data) { - setRows(data); - } - }, [data]); + const updateAdmin = useMutation({ + mutationFn: (account: AccountType) => + updateAccountAdmin( + account, + loginData + ? loginData + : { + email: "", + password: "", + session: "", + customerId: -1, + isAdmin: false, + }, + ), + }); - const handleSelectionChange = (newSelection: GridRowSelectionModel) => { - setSelectedRows(newSelection.ids); - }; + const columns: GridColDef<(typeof rows)[number]>[] = [ + { field: "id", headerName: "ID", width: 60 }, + { + field: "admin", + headerName: t("admin"), + type: "boolean", + width: 90, + editable: true, + }, + { + field: "email", + headerName: t("email"), + width: 150, + editable: false, + }, + { + field: "langI18n", + headerName: t("language"), + width: 150, + editable: false, + }, + { + //edit billing information button + field: "customer", + headerName: t("address"), + width: 90, + sortable: false, + disableReorder: true, + disableColumnMenu: true, + renderCell: (params) => ( + handleCustomerEdit(params.row)}> + {" "} + {" "} + + ), + }, + ]; - const handleDeleteSelected = async () => { - selectedRows.forEach(async (rowId) => { - let id = rows.find((row) => row.id === rowId)?.id - if(id === undefined) id = -1; - await deleteAccount.mutateAsync({ - email: loginData?.email || '', - uuid: loginData?.session || '', - accountId: id, - }); - }) + const [open, setOpen] = useState(false); - setRows(rows.filter((row) => !selectedRows.has(row.id))); - }; + const handleCustomerEditSubmit = async () => { + setOpen(false); + await refetch(); + }; - const updateAdmin = useMutation({ - mutationFn: (account: AccountType) => - updateAccountAdmin(account, loginData ? loginData : { - email: "", - password: "", - session: "", - customerId: -1, - isAdmin: false - }), - - }); - - const columns: GridColDef<(typeof rows)[number]>[] = [ - {field: 'id', headerName: 'ID', width: 60}, - { - field: 'admin', - headerName: t('admin'), - type: "boolean", - width: 90, - editable: true - }, - { - field: 'email', - headerName: t('email'), - width: 150, - editable: false, - }, - { - field: 'langI18n', - headerName: t('language'), - width: 150, - editable: false, - }, - { //edit billing information button - field: "customer", - headerName: t('address'), - width: 90, - sortable: false, - disableReorder: true, - disableColumnMenu: true, - renderCell: params => handleCustomerEdit(params.row)}> , - } - ]; - - const [open, setOpen] = useState(false); - - const handleCustomerEditSubmit = async () => { - setOpen(false); - await refetch(); - }; - - return ( - <> - - ( - - - - ) - }} - showToolbar - processRowUpdate={async (updatedRow) => { - const originalRow = rows.find(row => row.id === updatedRow.id); - if (originalRow && originalRow.admin !== updatedRow.admin) { - await updateAdmin.mutateAsync(updatedRow); - } - setRows(rows.map(row => row.id === updatedRow.id ? updatedRow : row)); - return updatedRow; - }} - /> - - setOpen(false)} - onSubmit={handleCustomerEditSubmit} - customerData={customerData} - setCustomerData={setCustomerData} - /> - - ); + return ( + <> + + ( + + + + ), + }} + showToolbar + processRowUpdate={async (updatedRow) => { + const originalRow = rows.find((row) => row.id === updatedRow.id); + if (originalRow && originalRow.admin !== updatedRow.admin) { + await updateAdmin.mutateAsync(updatedRow); + } + setRows( + rows.map((row) => (row.id === updatedRow.id ? updatedRow : row)), + ); + return updatedRow; + }} + /> + + setOpen(false)} + onSubmit={handleCustomerEditSubmit} + customerData={customerData} + setCustomerData={setCustomerData} + /> + + ); } diff --git a/01-frontend/src/helper/adminpanel/CustomerEditDialog.tsx b/01-frontend/src/helper/adminpanel/CustomerEditDialog.tsx index 56acf33..966cc47 100644 --- a/01-frontend/src/helper/adminpanel/CustomerEditDialog.tsx +++ b/01-frontend/src/helper/adminpanel/CustomerEditDialog.tsx @@ -1,108 +1,123 @@ -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; - onClose: () => void; - onSubmit: () => void; // Funktion, die aufgerufen wird, wenn der Login erfolgreich ist - customerData: CustomerType; - setCustomerData: React.Dispatch>; + open: boolean; + onClose: () => void; + onSubmit: () => void; // Funktion, die aufgerufen wird, wenn der Login erfolgreich ist + customerData: CustomerType; + setCustomerData: React.Dispatch>; }; const CustomerEditDialog: React.FC = ({ - open, - onClose, - customerData, - setCustomerData, - onSubmit - }) => { + open, + onClose, + customerData, + setCustomerData, + onSubmit, +}) => { + const { t } = useTranslation(); - const {t} = useTranslation(); - - useEffect(() => { - if (open) { - const active = document.activeElement as HTMLElement | null; - if (active && typeof active.blur === "function") { - active.blur(); - } - } - }, [open]); - - const changeCustomer = useMutation({ - mutationFn: (customer: CustomerType) => - updateCustomer(customer), - }); - - const handleSave = async () => { - const customer: CustomerType = { - id: customerData.id, - name: customerData.name, - surname: customerData.surname, - address: customerData.address, - zip: customerData.zip, - country: customerData.country, - }; - await changeCustomer.mutateAsync(customer); - onSubmit(); // Rufe die onSubmit-Funktion auf, um den Dialog zu schließen und die Änderungen zu übernehmen - onClose(); // Schließe den Dialog + useEffect(() => { + if (open) { + const active = document.activeElement as HTMLElement | null; + if (active && typeof active.blur === "function") { + active.blur(); + } } + }, [open]); - return ( - - {t('changeCustomer')} - - setCustomerData(prev => ({...prev, name: e.target.value}))} - /> - setCustomerData(prev => ({...prev, surname: e.target.value}))} - /> - setCustomerData(prev => ({...prev, address: e.target.value}))} - /> - setCustomerData(prev => ({...prev, zip: e.target.value}))} - /> - setCustomerData(prev => ({...prev, country: e.target.value}))} - /> - - - - - - ); + const changeCustomer = useMutation({ + mutationFn: (customer: CustomerType) => updateCustomer(customer), + }); + + const handleSave = async () => { + const customer: CustomerType = { + id: customerData.id, + name: customerData.name, + surname: customerData.surname, + address: customerData.address, + zip: customerData.zip, + country: customerData.country, + }; + await changeCustomer.mutateAsync(customer); + onSubmit(); // Rufe die onSubmit-Funktion auf, um den Dialog zu schließen und die Änderungen zu übernehmen + onClose(); // Schließe den Dialog + }; + + return ( + + {t("changeCustomer")} + + + setCustomerData((prev) => ({ ...prev, name: e.target.value })) + } + /> + + setCustomerData((prev) => ({ ...prev, surname: e.target.value })) + } + /> + + setCustomerData((prev) => ({ ...prev, address: e.target.value })) + } + /> + + setCustomerData((prev) => ({ ...prev, zip: e.target.value })) + } + /> + + setCustomerData((prev) => ({ ...prev, country: e.target.value })) + } + /> + + + + + + ); }; export default CustomerEditDialog; diff --git a/01-frontend/src/helper/adminpanel/ItemImageDialog.tsx b/01-frontend/src/helper/adminpanel/ItemImageDialog.tsx index 766a06f..f8cfd00 100644 --- a/01-frontend/src/helper/adminpanel/ItemImageDialog.tsx +++ b/01-frontend/src/helper/adminpanel/ItemImageDialog.tsx @@ -1,229 +1,243 @@ -import React, {useState} from 'react'; +import React, { useState } from "react"; import { - 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'; + 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"; interface ItemImageDialogProps { - open: boolean; - onClose: () => void; - item: Item; - onSuccess?: () => void; - isFarmStationImage: boolean; + open: boolean; + onClose: () => void; + item: Item; + onSuccess?: () => void; + isFarmStationImage: boolean; } -export default function ItemImageDialog({open, onClose, item, onSuccess, isFarmStationImage}: ItemImageDialogProps) { - const {t} = useTranslation(); - const [selectedFile, setSelectedFile] = useState(null); - const [preview, setPreview] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(false); +export default function ItemImageDialog({ + open, + onClose, + item, + onSuccess, + isFarmStationImage, +}: ItemImageDialogProps) { + const { t } = useTranslation(); + const [selectedFile, setSelectedFile] = useState(null); + const [preview, setPreview] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); - const handleFileSelect = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - // Validate file type - if (!file.type.startsWith('image/')) { - setError('Please select a valid image file'); - return; - } + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + // Validate file type + if (!file.type.startsWith("image/")) { + setError("Please select a valid image file"); + return; + } - // Validate file size (5MB limit) - if (file.size > 5 * 1024 * 1024) { - setError('File size must be less than 5MB'); - return; - } + // Validate file size (5MB limit) + if (file.size > 5 * 1024 * 1024) { + setError("File size must be less than 5MB"); + return; + } - setSelectedFile(file); - setError(null); + setSelectedFile(file); + setError(null); - // Create preview - const reader = new FileReader(); - reader.onload = (e) => { - setPreview(e.target?.result as string); - }; - reader.readAsDataURL(file); - } - }; + // Create preview + const reader = new FileReader(); + reader.onload = (e) => { + setPreview(e.target?.result as string); + }; + reader.readAsDataURL(file); + } + }; - const convertFileToBase64 = (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - const result = reader.result as string; - // Remove the data URL prefix (e.g., "data:image/jpeg;base64,") - const base64 = result.split(',')[1]; - resolve(base64); - }; - reader.onerror = reject; - reader.readAsDataURL(file); - }); - }; + const convertFileToBase64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const result = reader.result as string; + // Remove the data URL prefix (e.g., "data:image/jpeg;base64,") + const base64 = result.split(",")[1]; + resolve(base64); + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); + }; - const handleUpload = async () => { - if (!selectedFile) { - setError('Please select an image file'); - return; - } + const handleUpload = async () => { + if (!selectedFile) { + setError("Please select an image file"); + return; + } - setLoading(true); - setError(null); + setLoading(true); + setError(null); - try { - const base64Image = await convertFileToBase64(selectedFile); + try { + const base64Image = await convertFileToBase64(selectedFile); - const response = await fetch((isFarmStationImage ? 'http://localhost:8085/farm' : 'http://localhost:8085/image') + '?uuid=' + item.uuid, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: base64Image, - }); + const response = await fetch( + (isFarmStationImage + ? "http://localhost:8085/farm" + : "http://localhost:8085/image") + + "?uuid=" + + item.uuid, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: base64Image, + }, + ); - if (!response.ok) { - console.error('Failed to upload image:', await response.text()); - } + if (!response.ok) { + console.error("Failed to upload image:", await response.text()); + } - setSuccess(true); - onSuccess?.(); + setSuccess(true); + onSuccess?.(); - // Auto-close dialog after 1.5 seconds - setTimeout(() => { - handleClose(); - }, 1500); + // Auto-close dialog after 1.5 seconds + setTimeout(() => { + handleClose(); + }, 1500); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to upload image"); + } finally { + setLoading(false); + } + }; - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to upload image'); - } finally { - setLoading(false); - } - }; + const handleClose = () => { + setSelectedFile(null); + setPreview(null); + setError(null); + setSuccess(false); + setLoading(false); + onClose(); + }; - const handleClose = () => { - setSelectedFile(null); - setPreview(null); - setError(null); - setSuccess(false); - setLoading(false); - onClose(); - }; + return ( + + + {t("uploadImage")} - {item.name} + + + + - return ( - + + {/* Item Info */} + + + {t("item")}: {item.name} + + + UUID: {item.uuid} + + + {t("imageUploadNotice")} + + {isFarmStationImage && ( + + {t("imageUploadNoticeFs")} + + )} + + + {/* File Upload */} + + + + + + {/* Image Preview */} + {preview && ( + + Preview + + {selectedFile?.name} ( + {(selectedFile?.size || 0 / 1024).toFixed(1)} KB) + + + )} + + {/* Error Message */} + {error && ( + setError(null)}> + {error} + + )} + + {/* Success Message */} + {success && ( + {t("imageUploadedSuccessfully")} + )} + + + + + + - - - - {/* Image Preview */} - {preview && ( - - Preview - - {selectedFile?.name} ({(selectedFile?.size || 0 / 1024).toFixed(1)} KB) - - - )} - - {/* Error Message */} - {error && ( - setError(null)}> - {error} - - )} - - {/* Success Message */} - {success && ( - - {t('imageUploadedSuccessfully')} - - )} - - - - - - - - - ); -} \ No newline at end of file + {loading ? t("uploading") : t("upload")} + + + + ); +} diff --git a/01-frontend/src/helper/adminpanel/ItemsInfo.tsx b/01-frontend/src/helper/adminpanel/ItemsInfo.tsx index a7a5585..3f1f2f2 100644 --- a/01-frontend/src/helper/adminpanel/ItemsInfo.tsx +++ b/01-frontend/src/helper/adminpanel/ItemsInfo.tsx @@ -3,256 +3,294 @@ import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; import { Box, Button, IconButton, Toolbar, useTheme } from "@mui/material"; import { Gauge, gaugeClasses } from "@mui/x-charts"; -import { DataGrid, GridColDef, GridRowId, GridRowSelectionModel } from "@mui/x-data-grid"; +import { + DataGrid, + GridColDef, + GridRowId, + GridRowSelectionModel, +} from "@mui/x-data-grid"; import { useMutation, useQuery } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Item from "../../components/Item"; import { mapValueToColor } from "../../util/ColorUtil.tsx"; import { useAccount } from "../AccountProvider.tsx"; -import { deleteItemQuery, fetchItems, updateItemAdmin } from "../query/Queries.tsx"; +import { + deleteItemQuery, + fetchItems, + updateItemAdmin, +} from "../query/Queries.tsx"; import ItemImageDialog from "./ItemImageDialog.tsx"; import NewItemDialog from "./NewItemDialog.tsx"; export default function ItemsInfo() { - const theme = useTheme(); - const { t } = useTranslation(); + const theme = useTheme(); + const { t } = useTranslation(); - const [rows, setRows] = useState([]); - const [selectedRows, setSelectedRows] = useState>(new Set()); + const [rows, setRows] = useState([]); + const [selectedRows, setSelectedRows] = useState>(new Set()); - const [editImageDialog, setEditImageDialog] = useState(false); - const [newItemDialog, setNewItemDialog] = useState(false); - const [selectedItem, setSelectedItem] = useState(null); - const [isFarmStationImage, setIsFarmStationImage] = useState(false); + const [editImageDialog, setEditImageDialog] = useState(false); + const [newItemDialog, setNewItemDialog] = useState(false); + const [selectedItem, setSelectedItem] = useState(null); + const [isFarmStationImage, setIsFarmStationImage] = useState(false); + function handleImageEdit(item: Item) { + setIsFarmStationImage(false); + setSelectedItem(item); + setEditImageDialog(true); + console.log("IconEdit", item); + } - function handleImageEdit(item: Item) { - setIsFarmStationImage(false); - setSelectedItem(item); - setEditImageDialog(true); - console.log("IconEdit", item); + function handleFarmImageEdit(item: Item) { + setIsFarmStationImage(true); + setSelectedItem(item); + setEditImageDialog(true); + console.log("IconEdit", item); + } + + function handleAddItem() { + setNewItemDialog(true); + } + + const { user: loginData } = useAccount(); + + const { data } = useQuery({ + queryKey: ["fetchItems", loginData], + queryFn: () => fetchItems(), + retry: 3, + retryDelay: 1000, + }); + + useEffect(() => { + if (data) { + setRows(data); } + }, [data]); - function handleFarmImageEdit(item: Item) { - setIsFarmStationImage(true); - setSelectedItem(item); - setEditImageDialog(true); - console.log("IconEdit", item); - } + const handleSelectionChange = (newSelection: GridRowSelectionModel) => { + setSelectedRows(newSelection.ids); + }; + const deleteItem = useMutation({ + mutationFn: (uuid: string) => deleteItemQuery(uuid), + }); - function handleAddItem() { - setNewItemDialog(true); - } - - const { user: loginData } = useAccount(); - - const { data } = useQuery({ - queryKey: ["fetchItems", loginData], - queryFn: () => fetchItems(), - retry: 3, - retryDelay: 1000, - }); - - useEffect(() => { - if (data) { - setRows(data); - } - }, [data]); - - const handleSelectionChange = (newSelection: GridRowSelectionModel) => { - setSelectedRows(newSelection.ids); - }; - - const deleteItem = useMutation({ - mutationFn: (uuid: string) => - deleteItemQuery(uuid), - }); - - const handleDeleteSelected = async () => { - await Promise.all( - Array.from(selectedRows).map(async (row) => { - await deleteItem.mutateAsync(rows.find(item => item.id === row)?.uuid || ""); - }) + const handleDeleteSelected = async () => { + await Promise.all( + Array.from(selectedRows).map(async (row) => { + await deleteItem.mutateAsync( + rows.find((item) => item.id === row)?.uuid || "", ); - - setRows(rows.filter((row) => !selectedRows.has(row.id))); - }; - - const updateItem = useMutation({ - mutationFn: (item: Item) => - updateItemAdmin(item), - - }); - - const handleRowUpdate = async (updatedRow: Item) => { - setRows(rows.map(row => row.id === updatedRow.id ? updatedRow : row)); - await updateItem.mutateAsync(updatedRow); - return updatedRow; - } - - const columns: GridColDef<(typeof rows)[number]>[] = [ - { field: 'id', headerName: 'ID', width: 60 }, - { - field: 'uuid', - headerName: t('uuid'), - type: "string", - width: 120, - editable: false - }, - { - field: 'name', - headerName: t('name'), - width: 200, - editable: true, - }, - { - field: 'category', - headerName: t('category'), - width: 150, - editable: true, - valueFormatter: (val) => t(val), - }, - { - field: 'description', - headerName: t('description'), - width: 150, - editable: true, - }, - { - field: 'price100', - headerName: t('price100€'), - width: 100, - editable: true, - type: 'number', - valueFormatter: (val) => (val / 100).toFixed(2), - }, - { - field: 'discount100', - headerName: t('discount100'), - width: 120, - editable: true, - type: 'number' - }, - { - field: 'stock', - headerName: t('stock'), - width: 100, - editable: true, - type: 'number', - renderCell: params => { - return mapValueToColor(0, params.row.stockExpected, params.row.stock) - }, - }, - }} text={() => `${params.row.stock}`} /> - }, - { - field: 'rating', - headerName: t('rating'), - width: 100, - editable: false, //the rating is averaged from ratings - type: 'number', - renderCell: params => { - return mapValueToColor(0, 10, params.row.rating) - }, - }, - }} text={() => `${params.row.rating.toFixed(2)}`} /> - }, - { - field: "actualPrice", - headerName: t('actualPrice'), - width: 90, - editable: false, - valueGetter: (_, row) => (row.price100 / 100 * ((100 - row.discount100) / 100)).toFixed(2) - }, - { - field: 'images', - headerName: t('images'), - width: 90, - editable: false, - renderCell: params => handleImageEdit(params.row)}> , - }, - { - field: 'farmImage', - headerName: t('fsImage'), - width: 90, - editable: false, - renderCell: params => handleFarmImageEdit(params.row)}> - , - } - ]; - - return ( - - ( - - - - - ) - }} - showToolbar - processRowUpdate={handleRowUpdate} - /> - {selectedItem && ( - setEditImageDialog(false)} - item={selectedItem} - onSuccess={() => { - // Refresh data or update UI - console.log('Image uploaded successfully'); - }} - isFarmStationImage={isFarmStationImage} - /> - )} - - setNewItemDialog(false)} - /> - + }), ); -} \ No newline at end of file + + setRows(rows.filter((row) => !selectedRows.has(row.id))); + }; + + const updateItem = useMutation({ + mutationFn: (item: Item) => updateItemAdmin(item), + }); + + const handleRowUpdate = async (updatedRow: Item) => { + setRows(rows.map((row) => (row.id === updatedRow.id ? updatedRow : row))); + await updateItem.mutateAsync(updatedRow); + return updatedRow; + }; + + const columns: GridColDef<(typeof rows)[number]>[] = [ + { field: "id", headerName: "ID", width: 60 }, + { + field: "uuid", + headerName: t("uuid"), + type: "string", + width: 120, + editable: false, + }, + { + field: "name", + headerName: t("name"), + width: 200, + editable: true, + }, + { + field: "category", + headerName: t("category"), + width: 150, + editable: true, + valueFormatter: (val) => t(val), + }, + { + field: "description", + headerName: t("description"), + width: 150, + editable: true, + }, + { + field: "price100", + headerName: t("price100€"), + width: 100, + editable: true, + type: "number", + valueFormatter: (val) => (val / 100).toFixed(2), + }, + { + field: "discount100", + headerName: t("discount100"), + width: 120, + editable: true, + type: "number", + }, + { + field: "stock", + headerName: t("stock"), + width: 100, + editable: true, + type: "number", + renderCell: (params) => ( + { + return mapValueToColor( + 0, + params.row.stockExpected, + params.row.stock, + ); + }, + }, + }} + text={() => `${params.row.stock}`} + /> + ), + }, + { + field: "rating", + headerName: t("rating"), + width: 100, + editable: false, //the rating is averaged from ratings + type: "number", + renderCell: (params) => ( + { + return mapValueToColor(0, 10, params.row.rating); + }, + }, + }} + text={() => `${params.row.rating.toFixed(2)}`} + /> + ), + }, + { + field: "actualPrice", + headerName: t("actualPrice"), + width: 90, + editable: false, + valueGetter: (_, row) => + ((row.price100 / 100) * ((100 - row.discount100) / 100)).toFixed(2), + }, + { + field: "images", + headerName: t("images"), + width: 90, + editable: false, + renderCell: (params) => ( + handleImageEdit(params.row)}> + {" "} + {" "} + + ), + }, + { + field: "farmImage", + headerName: t("fsImage"), + width: 90, + editable: false, + renderCell: (params) => ( + handleFarmImageEdit(params.row)}> + {" "} + + + ), + }, + ]; + + return ( + + ( + + + + + ), + }} + showToolbar + processRowUpdate={handleRowUpdate} + /> + {selectedItem && ( + setEditImageDialog(false)} + item={selectedItem} + onSuccess={() => { + // Refresh data or update UI + console.log("Image uploaded successfully"); + }} + isFarmStationImage={isFarmStationImage} + /> + )} + + setNewItemDialog(false)} + /> + + ); +} diff --git a/01-frontend/src/helper/adminpanel/NewItemDialog.tsx b/01-frontend/src/helper/adminpanel/NewItemDialog.tsx index 224721e..b1d5941 100644 --- a/01-frontend/src/helper/adminpanel/NewItemDialog.tsx +++ b/01-frontend/src/helper/adminpanel/NewItemDialog.tsx @@ -1,166 +1,164 @@ -import CloseIcon from '@mui/icons-material/Close'; +import CloseIcon from "@mui/icons-material/Close"; import { - Alert, - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - IconButton, - TextField -} from '@mui/material'; -import {useMutation} from '@tanstack/react-query'; -import {useState} from 'react'; -import {useTranslation} from 'react-i18next'; -import {Item} from '../../components/Item'; -import {submitItem} from '../query/Queries'; + Alert, + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + TextField, +} from "@mui/material"; +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Item } from "../../components/Item"; +import { submitItem } from "../query/Queries"; interface NewItemDialogProps { - open: boolean; - onClose: () => void; + open: boolean; + onClose: () => void; } -export default function NewItemDialog({open, onClose}: NewItemDialogProps) { - const {t} = useTranslation(); - const [item, setItem] = useState({ - id: 0, - uuid: "", - name: "", - description: "", - price100: 0, - stock: 0, - stockExpected: 1, - category: "", - rating: -1, - discount100: 0, - }); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(false); +export default function NewItemDialog({ open, onClose }: NewItemDialogProps) { + const { t } = useTranslation(); + const [item, setItem] = useState({ + id: 0, + uuid: "", + name: "", + description: "", + price100: 0, + stock: 0, + stockExpected: 1, + category: "", + rating: -1, + discount100: 0, + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); - const handleClose = () => { - setError(null); - setSuccess(false); - setLoading(false); - onClose(); - }; + const handleClose = () => { + setError(null); + setSuccess(false); + setLoading(false); + onClose(); + }; - const handleChange = (e: React.ChangeEvent) => { - setItem({...item, [e.target.name]: e.target.value}); - }; + const handleChange = (e: React.ChangeEvent) => { + setItem({ ...item, [e.target.name]: e.target.value }); + }; - const saveItem = useMutation({ - mutationFn: (item: Item) => - submitItem(item), - }); + const saveItem = useMutation({ + mutationFn: (item: Item) => submitItem(item), + }); - const handleSave = async () => { - setLoading(true); - setError(null); - setSuccess(false); + const handleSave = async () => { + setLoading(true); + setError(null); + setSuccess(false); - await saveItem.mutateAsync(item) + await saveItem.mutateAsync(item); - onClose(); // Close the dialog after saving - setLoading(false); - }; + onClose(); // Close the dialog after saving + setLoading(false); + }; - return ( - + + {t("createNewItem")} + + + + + + + + {/* Name, Kategorie, Beschreibung, Preis, Rabatt, Bestand, Bestand erwartet */} + - - {t('createNewItem')} - - - - + /> + + + + + + - - - {/* Name, Kategorie, Beschreibung, Preis, Rabatt, Bestand, Bestand erwartet */} - - - - - - - + {/* Error Message */} + {error && ( + setError(null)}> + {error} + + )} - {/* Error Message */} - {error && ( - setError(null)}> - {error} - - )} + {/* Success Message */} + {success && ( + {t("itemCreatedSuccessfully")} + )} + + - {/* Success Message */} - {success && ( - - {t('itemCreatedSuccessfully')} - - )} - - - - - - - - - ); -} \ No newline at end of file + + + + + + ); +} diff --git a/01-frontend/src/helper/adminpanel/OrdersInfo.tsx b/01-frontend/src/helper/adminpanel/OrdersInfo.tsx index a6932b6..a66cf1e 100644 --- a/01-frontend/src/helper/adminpanel/OrdersInfo.tsx +++ b/01-frontend/src/helper/adminpanel/OrdersInfo.tsx @@ -1,231 +1,242 @@ import { - Button, - Card, - CardContent, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Divider, - List, - ListItemText, - Snackbar, - Stack, - Typography, - useTheme + Button, + Card, + CardContent, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + 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[] = [ - OrderStatusEnum.CANCELLED, - OrderStatusEnum.ISSUES, - OrderStatusEnum.ORDERED, - OrderStatusEnum.IN_PROGRESS, - OrderStatusEnum.DELIVERED + OrderStatusEnum.CANCELLED, + OrderStatusEnum.ISSUES, + OrderStatusEnum.ORDERED, + OrderStatusEnum.IN_PROGRESS, + OrderStatusEnum.DELIVERED, ]; +const OrderCard: React.FC<{ order: OrderType; onClick: () => void }> = ({ + order, + onClick, +}) => { + const { t } = useTranslation(); -const OrderCard: React.FC<{ order: OrderType; onClick: () => void }> = ({order, onClick}) => { + const [{ isDragging }, drag] = useDrag(() => ({ + type: "order", + item: { id: order.id }, + collect: (monitor) => ({ + isDragging: !!monitor.isDragging(), + }), + })); - const {t} = useTranslation(); - - const [{isDragging}, drag] = useDrag(() => ({ - type: 'order', - item: {id: order.id}, - collect: (monitor) => ({ - isDragging: !!monitor.isDragging(), - }), - })); - - return ( -
- - - - Order: {order.id} - - - {t('date') + ": " + new Date(order.time).toUTCString()} - - - {t('total') + ": " + (order.total / 100).toFixed(2) + " €"} - - - -
- ); + return ( +
+ + + + Order: {order.id} + + + {t("date") + ": " + new Date(order.time).toUTCString()} + + + {t("total") + ": " + (order.total / 100).toFixed(2) + " €"} + + + +
+ ); }; -const Column: React.FC void -}>> = ({status, onDrop, children}) => { + onDrop: (id: number, status: OrderStatusEnum) => void; + }> +> = ({ status, onDrop, children }) => { + const { t } = useTranslation(); + const theme = useTheme(); - const {t} = useTranslation(); - const theme = useTheme(); + const [{ isOver }, drop] = useDrop(() => ({ + accept: "order", + drop: (item: { id: number }) => onDrop(item.id, status), + collect: (monitor) => ({ + isOver: !!monitor.isOver(), + }), + })); - const [{isOver}, drop] = useDrop(() => ({ - accept: 'order', - drop: (item: { id: number }) => onDrop(item.id, status), - collect: (monitor) => ({ - isOver: !!monitor.isOver(), - }), - })); - - return ( -
+ + - - - {t(status)} -
- {children} -
-
-
-
- ); + display: "flex", + flexDirection: "column", + overflowY: "auto", + height: "100%", + }} + > + {t(status)} +
{children}
+ + + + ); }; -const EditOrder: React.FC<{ open: boolean; order: OrderType | null; onClose: () => void }> = ({ - open, - order, - onClose - }) => { - - const {t} = useTranslation(); - if (order === null) - return ""; - return ( - - - {`${t(order.status)} #${order?.id}`} - - - {order && ( - - {`${t('orderDate')}: ${new Date(order.time).toDateString()}`} - - {t('orderedItems')}: - - {order.orderItems.map((item, idx) => ( - - ))} - - - {`${t('sum')}: ${(order.total / 100).toFixed(2)} €`} - - )} - - - - - - ); +const EditOrder: React.FC<{ + open: boolean; + order: OrderType | null; + onClose: () => void; +}> = ({ open, order, onClose }) => { + const { t } = useTranslation(); + if (order === null) return ""; + return ( + + {`${t(order.status)} #${order?.id}`} + + {order && ( + + {`${t("orderDate")}: ${new Date(order.time).toDateString()}`} + + {t("orderedItems")}: + + {order.orderItems.map((item, idx) => ( + + ))} + + + {`${t("sum")}: ${(order.total / 100).toFixed(2)} €`} + + )} + + + + + + ); }; export default function OrdersInfo() { - const [editOrder, setEditOrder] = useState(null); - const [openSnackbar, setOpenSnackbar] = useState(false); + const [editOrder, setEditOrder] = useState(null); + const [openSnackbar, setOpenSnackbar] = useState(false); - const {user: loginData} = useAccount(); + const { user: loginData } = useAccount(); - const {data, refetch, isLoading} = useQuery({ - queryKey: ["fetchOrdersAdmin", loginData], - queryFn: () => fetchOrdersAdmin(loginData ? loginData : { - email: "", - password: "", - session: "", - customerId: -1, - isAdmin: false - }), - retry: 3, - retryDelay: 1000, - }); + const { data, refetch, isLoading } = useQuery({ + queryKey: ["fetchOrdersAdmin", loginData], + 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}), - }); + const patchOrderMutation = useMutation({ + mutationFn: (order: OrderPatch) => + orderPatch({ id: order.id, status: order.status }), + }); - - const handleDrop = async (id: number, status: OrderStatusEnum) => { - - const currentOrders = data ?? []; - const obj = currentOrders.find((o) => o.id === id); - if (!obj) { - setOpenSnackbar(true); - return; - } - try { - await patchOrderMutation.mutateAsync({id: obj.id, status: status}); - await refetch(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - setOpenSnackbar(true); - } - }; - const handleEdit = (order: OrderType) => setEditOrder(order); - - if (isLoading || !data) { - return
Lade Bestellungen...
; + const handleDrop = async (id: number, status: OrderStatusEnum) => { + const currentOrders = data ?? []; + const obj = currentOrders.find((o) => o.id === id); + if (!obj) { + setOpenSnackbar(true); + return; } + try { + await patchOrderMutation.mutateAsync({ id: obj.id, status: status }); + await refetch(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + setOpenSnackbar(true); + } + }; + const handleEdit = (order: OrderType) => setEditOrder(order); - return ( - -
- {statusOrder.map((status) => ( - - {data - .filter((o) => o.status === status) - .map((o) => ( - handleEdit(o)}/> - ))} - - ))} -
- setEditOrder(null)} - /> - setOpenSnackbar(false)} - message="Failed changing Orderstatus" - /> -
- ); + if (isLoading || !data) { + return
Lade Bestellungen...
; + } + + return ( + +
+ {statusOrder.map((status) => ( + + {data + .filter((o) => o.status === status) + .map((o) => ( + handleEdit(o)} /> + ))} + + ))} +
+ setEditOrder(null)} + /> + setOpenSnackbar(false)} + message="Failed changing Orderstatus" + /> +
+ ); } diff --git a/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx b/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx index f8e3a84..36b87ad 100644 --- a/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx +++ b/01-frontend/src/helper/adminpanel/StatisticsInfo.tsx @@ -1,290 +1,339 @@ -import {Box, Typography, useTheme} from "@mui/material"; -import {BarChart} from '@mui/x-charts/BarChart'; -import {PieChart} from '@mui/x-charts/PieChart'; -import {BarSeriesType} from '@mui/x-charts' -import {useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; -import {useQuery} from "@tanstack/react-query"; -import {fetchOrderStatus, fetchStatisticsRevenue, fetchStatisticsVolume, fetchStockPercent} from "../query/Queries.tsx"; -import {useAccount} from "../AccountProvider.tsx"; -import {getColorFromPercent} from "../../util/ColorUtil.tsx"; +import { Box, Typography, useTheme } from "@mui/material"; +import { BarChart } from "@mui/x-charts/BarChart"; +import { PieChart } from "@mui/x-charts/PieChart"; +import { BarSeriesType } from "@mui/x-charts"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useQuery } from "@tanstack/react-query"; +import { + fetchOrderStatus, + fetchStatisticsRevenue, + fetchStatisticsVolume, + fetchStockPercent, +} from "../query/Queries.tsx"; +import { useAccount } from "../AccountProvider.tsx"; +import { getColorFromPercent } from "../../util/ColorUtil.tsx"; export default function StatisticsInfo() { - const theme = useTheme(); - const {t} = useTranslation(); + const theme = useTheme(); + const { t } = useTranslation(); - const [monthlyVolume, setMonthlyVolume] = useState([]); - const [monthlyVolumeXaxis, setMonthlyVolumeXaxis] = useState([{data: []}]); - const [totalVolume, setTotalVolume] = useState([]); + const [monthlyVolume, setMonthlyVolume] = useState([]); + const [monthlyVolumeXaxis, setMonthlyVolumeXaxis] = useState([{ data: [] }]); + const [totalVolume, setTotalVolume] = useState([]); - const [monthlyRevenue, setMonthlyRevenue] = useState([]); - const [monthlyRevenueXaxis, setMonthlyRevenueXaxis] = useState([{data: []}]); - const [totalRevenue, setTotalRevenue] = useState([]); + const [monthlyRevenue, setMonthlyRevenue] = useState([]); + const [monthlyRevenueXaxis, setMonthlyRevenueXaxis] = useState([ + { data: [] }, + ]); + const [totalRevenue, setTotalRevenue] = useState([]); - const [orderStatus, setOrderStatus] = useState([]); + const [orderStatus, setOrderStatus] = useState([]); - const [stockPercent, setStockPercent] = useState([]); + const [stockPercent, setStockPercent] = useState([]); + const { user: loginData } = useAccount(); - const {user: loginData} = useAccount(); + const { data: dataVolume } = useQuery({ + queryKey: ["fetchStatisticsVolume", loginData], + queryFn: () => + fetchStatisticsVolume( + loginData + ? loginData + : { + email: "", + password: "", + session: "", + customerId: -1, + isAdmin: false, + }, + ), + retry: 0, + retryDelay: 0, + }); + const { data: dataRevenue } = useQuery({ + queryKey: ["fetchStatisticsRevenue", loginData], + queryFn: () => + fetchStatisticsRevenue( + loginData + ? loginData + : { + email: "", + password: "", + session: "", + customerId: -1, + isAdmin: false, + }, + ), + retry: 0, + retryDelay: 0, + }); - const {data: dataVolume} = useQuery({ - queryKey: ["fetchStatisticsVolume", loginData], - queryFn: () => fetchStatisticsVolume(loginData ? loginData : { - email: "", - password: "", - session: "", - customerId: -1, - isAdmin: false - }), - retry: 0, - retryDelay: 0, - }); + const { data: dataOrderStatus } = useQuery({ + queryKey: ["fetchOrderStatus", loginData], + queryFn: () => + fetchOrderStatus( + loginData + ? loginData + : { + email: "", + password: "", + session: "", + customerId: -1, + isAdmin: false, + }, + ), + retry: 0, + retryDelay: 0, + }); - const {data: dataRevenue} = useQuery({ - queryKey: ["fetchStatisticsRevenue", loginData], - queryFn: () => fetchStatisticsRevenue(loginData ? loginData : { - email: "", - password: "", - session: "", - customerId: -1, - isAdmin: false - }), - retry: 0, - retryDelay: 0, - }); + const { data: dataStockPercent } = useQuery({ + queryKey: ["fetchStockPercent", loginData], + queryFn: () => + fetchStockPercent( + loginData + ? loginData + : { + email: "", + password: "", + session: "", + customerId: -1, + isAdmin: false, + }, + ), + retry: 0, + retryDelay: 0, + }); - const {data: dataOrderStatus} = useQuery({ - queryKey: ["fetchOrderStatus", loginData], - queryFn: () => fetchOrderStatus(loginData ? loginData : { - email: "", - password: "", - session: "", - customerId: -1, - isAdmin: false - }), - retry: 0, - retryDelay: 0, - }); + useEffect(() => { + if (dataVolume) { + const cmm = []; + const cmmx = monthlyVolumeXaxis; + const tv = []; + let i = 0; + for (const cat in dataVolume.catMonthMap) { + for (const timestamp in dataVolume.catMonthMap[cat]) { + const date = new Date(parseInt(timestamp)); + const formattedDate = + date.getFullYear() + + "-" + + String(date.getMonth() + 1).padStart(2, "0"); + if (!cmmx[0].data.includes(formattedDate)) { + 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" }); + } + cmm[i].data.push(datapoint); - const {data: dataStockPercent} = useQuery({ - queryKey: ["fetchStockPercent", loginData], - queryFn: () => fetchStockPercent(loginData ? loginData : { - email: "", - password: "", - session: "", - customerId: -1, - isAdmin: false - }), - retry: 0, - retryDelay: 0, - }); - - - useEffect(() => { - if (dataVolume) { - const cmm = [] - const cmmx = monthlyVolumeXaxis - const tv = [] - let i = 0 - for (const cat in dataVolume.catMonthMap) { - for (const timestamp in dataVolume.catMonthMap[cat]) { - const date = new Date(parseInt(timestamp)) - const formattedDate = date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, '0') - if (!cmmx[0].data.includes(formattedDate)) { - 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"}) - } - cmm[i].data.push(datapoint) - - if (tv.length == i) { - tv.push({id: i, value: 0, label: t(cat)}) - } - tv[i].value += datapoint - } - i++; - } - - setMonthlyVolume(cmm) - setMonthlyVolumeXaxis(cmmx) - setTotalVolume(tv) + if (tv.length == i) { + tv.push({ id: i, value: 0, label: t(cat) }); + } + tv[i].value += datapoint; } - }, [dataVolume]); + i++; + } - useEffect(() => { - if (dataRevenue) { - const cmm = [] - const cmmx = monthlyRevenueXaxis - const tv = [] - let i = 0 - for (const cat in dataRevenue.catMonthMap) { - for (const timestamp in dataRevenue.catMonthMap[cat]) { - const date = new Date(parseInt(timestamp)) - const formattedDate = date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, '0') - if (!cmmx[0].data.includes(formattedDate)) { - 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"}) - } - cmm[i].data.push(datapoint) + setMonthlyVolume(cmm); + setMonthlyVolumeXaxis(cmmx); + setTotalVolume(tv); + } + }, [dataVolume]); - if (tv.length == i) { - tv.push({id: i, value: 0, label: t(cat)}) - } - tv[i].value += datapoint - } - i++; - } + useEffect(() => { + if (dataRevenue) { + const cmm = []; + const cmmx = monthlyRevenueXaxis; + const tv = []; + let i = 0; + for (const cat in dataRevenue.catMonthMap) { + for (const timestamp in dataRevenue.catMonthMap[cat]) { + const date = new Date(parseInt(timestamp)); + const formattedDate = + date.getFullYear() + + "-" + + String(date.getMonth() + 1).padStart(2, "0"); + if (!cmmx[0].data.includes(formattedDate)) { + 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" }); + } + cmm[i].data.push(datapoint); - setMonthlyRevenue(cmm) - setMonthlyRevenueXaxis(cmmx) - setTotalRevenue(tv) + if (tv.length == i) { + tv.push({ id: i, value: 0, label: t(cat) }); + } + tv[i].value += datapoint; } - }, [dataRevenue, monthlyRevenueXaxis, t]); + i++; + } - useEffect(() => { - if (dataOrderStatus) { - const orderStatus = [] - for (const status in dataOrderStatus) { - orderStatus.push({value: dataOrderStatus[status], label: t(status)}) - } - setOrderStatus(orderStatus) + setMonthlyRevenue(cmm); + setMonthlyRevenueXaxis(cmmx); + setTotalRevenue(tv); + } + }, [dataRevenue, monthlyRevenueXaxis, t]); + + useEffect(() => { + if (dataOrderStatus) { + const orderStatus = []; + for (const status in dataOrderStatus) { + orderStatus.push({ value: dataOrderStatus[status], label: t(status) }); + } + setOrderStatus(orderStatus); + } + }, [dataOrderStatus, t]); + + useEffect(() => { + function generateName(percent: string): string { + return ">" + percent + "%"; + } + + 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 (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; + } + stockPercent[index].value += datapoint; } - }, [dataOrderStatus, t]); + i++; + } + setStockPercent(stockPercent); + } + }, [dataStockPercent]); - useEffect(() => { - function generateName(percent: string): string { - return ">" + percent + "%"; - } + return ( + + + {t("salesStatistics")} + - 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 (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 - } - stockPercent[index].value += datapoint - } - i++ - } - setStockPercent(stockPercent) - } - }, [dataStockPercent]) - - return ( - - - {t("salesStatistics")} - - - - - {t("monthlySalesVolume")} - - - - - - {t("monthlySalesRevenue")} - - - - - - - {t("itemVolumeDistribution")} - - - - - - - {t("itemRevenueDistribution")} - - (v ? `${v.value}€` : '-'), - }]} - width={200} - height={200} - /> - - - - {t("stockFulfillment")} - - (v ? `${v.value}%` : '-'), - }]} - width={200} - height={200} - hideLegend - /> - - - - {t("orderStatus")} - - - - + + + {t("monthlySalesVolume")} + + + + + + {t("monthlySalesRevenue")} + + + + + + + {t("itemVolumeDistribution")} + + - ); + + + + {t("itemRevenueDistribution")} + + (v ? `${v.value}€` : "-"), + }, + ]} + width={200} + height={200} + /> + + + + {t("stockFulfillment")} + + (v ? `${v.value}%` : "-"), + }, + ]} + width={200} + height={200} + hideLegend + /> + + + + {t("orderStatus")} + + + + + + ); } diff --git a/01-frontend/src/helper/helper.css b/01-frontend/src/helper/helper.css index 783fed2..1bca847 100644 --- a/01-frontend/src/helper/helper.css +++ b/01-frontend/src/helper/helper.css @@ -1,36 +1,36 @@ .item-description { - display: flex; - justify-content: space-between; /* Elemente an gegenüberliegenden Seiten platzieren */ - align-items: center; /* Vertikale Zentrierung */ - width: 100%; + display: flex; + justify-content: space-between; /* Elemente an gegenüberliegenden Seiten platzieren */ + align-items: center; /* Vertikale Zentrierung */ + width: 100%; } .item-card-button { - color: green; + color: green; } .rating-card-body { - display: grid; - align-items: center; /* Vertikale Zentrierung */ - width: 100%; - gap: 30px; + display: grid; + align-items: center; /* Vertikale Zentrierung */ + width: 100%; + gap: 30px; } .rating-button { - max-width: 200px; + max-width: 200px; } .rating-card-box { - display: grid; - align-items: center; /* Vertikale Zentrierung */ - gap: 10px; - width: 100%; + display: grid; + align-items: center; /* Vertikale Zentrierung */ + gap: 10px; + width: 100%; } .rating-text-field { - background-color: whitesmoke; + background-color: whitesmoke; } .vw20 { - width: 20vw; -} \ No newline at end of file + width: 20vw; +} diff --git a/01-frontend/src/helper/homepage/FilterItem.tsx b/01-frontend/src/helper/homepage/FilterItem.tsx index b4238a9..2d10574 100644 --- a/01-frontend/src/helper/homepage/FilterItem.tsx +++ b/01-frontend/src/helper/homepage/FilterItem.tsx @@ -1,75 +1,83 @@ -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 = { - value: string; - label: string; + value: string; + label: string; }; type FilterItemProps = { - filterName: string; - filterItems: FilterItemOption[]; - value?: string | null; - onChange?: (value: string) => void; + filterName: string; + filterItems: FilterItemOption[]; + value?: string | null; + onChange?: (value: string) => void; }; export default function FilterItem({ - filterName, - filterItems, - value, - onChange, - }: FilterItemProps) { - const theme = useTheme(); + filterName, + filterItems, + value, + onChange, +}: FilterItemProps) { + const theme = useTheme(); - if (!value && filterItems.length > 0) { - value = filterItems[0].value; + if (!value && filterItems.length > 0) { + value = filterItems[0].value; + } + + const handleChange = (event: React.ChangeEvent) => { + if (onChange) { + onChange(event.target.value); } + }; - const handleChange = (event: React.ChangeEvent) => { - if (onChange) { - onChange(event.target.value); - } - }; + return ( +
+ + {filterName} + - return ( -
- - {filterName} - - - - - {filterItems.map((item, idx) => ( - } - label={ - /^[1-5]$/.test(item.value) ? ( - - ) : ( - item.label - ) - } - sx={{ - color: theme.palette.text.primary, - }} - /> - ))} - - -
- ); + + + {filterItems.map((item, idx) => ( + } + label={ + /^[1-5]$/.test(item.value) ? ( + + ) : ( + item.label + ) + } + sx={{ + color: theme.palette.text.primary, + }} + /> + ))} + + +
+ ); } diff --git a/01-frontend/src/helper/homepage/ItemCard.css b/01-frontend/src/helper/homepage/ItemCard.css index f06c739..4234a96 100644 --- a/01-frontend/src/helper/homepage/ItemCard.css +++ b/01-frontend/src/helper/homepage/ItemCard.css @@ -2,24 +2,24 @@ /* Container um jedes Filter-Widget */ .filter-item { - margin-bottom: 1.5rem; + margin-bottom: 1.5rem; } /* Überschrift (FormLabel) */ .filter-item__label { - font-weight: bold; - margin-bottom: 0.5rem; - /* nutze die CSS-Variable, die GlobalStyles füllen */ - color: var(--text-color); + font-weight: bold; + margin-bottom: 0.5rem; + /* nutze die CSS-Variable, die GlobalStyles füllen */ + color: var(--text-color); } /* Das Material-UI FormControl-Element */ .filter-item__group { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } /* Jeder Radio-Button mit Label */ .filter-item__option { - color: var(--text-color); + color: var(--text-color); } diff --git a/01-frontend/src/helper/homepage/ItemCard.tsx b/01-frontend/src/helper/homepage/ItemCard.tsx index a9458fb..a78fcbe 100644 --- a/01-frontend/src/helper/homepage/ItemCard.tsx +++ b/01-frontend/src/helper/homepage/ItemCard.tsx @@ -1,91 +1,150 @@ -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 navigate = useNavigate(); + const { addToBasket } = useBasket(); - const {t} = useTranslation(); - const navigate = useNavigate() - const {addToBasket} = useBasket(); + const handleAddToCart = () => { + addToBasket(item, 1); + console.log(`Added ${1} of ${item.name} to basket`); + }; - const handleAddToCart = () => { - addToBasket(item, 1); - console.log(`Added ${1} of ${item.name} to basket`); - }; + const handleClick = () => { + navigate(`/product/${item.id}`, { state: { item } }); + }; + const [imageUrl, setImageUrl] = useState( + item.image || "/src/assets/default.jpg", + ); // Fallback-Bild - const handleClick = () => { - navigate(`/product/${item.id}`, {state: {item}}); - } - const [imageUrl, setImageUrl] = useState(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 ( + + + + { + event.currentTarget.src = "/src/assets/default.jpg"; // Standardbild setzen + }} + sx={{ + objectFit: "contain", // Bild wird skaliert, um vollständig sichtbar zu sein + maxWidth: "100%", // Begrenze die maximale Breite auf den Container + maxHeight: "100%", // Begrenze die maximale Höhe auf den Container + }} + /> + + + {item.name} + + + + + + {( + (item.price100 / 100) * + (1 - item.discount100 / 100) + ).toFixed(2)}{" "} + € + + {item.discount100 == 0 ? ( + <> + ) : ( + + {-item.discount100}% + + )} + { + event.stopPropagation(); + handleAddToCart(); + }} + > + + + - return ( - - - - { - event.currentTarget.src = "/src/assets/default.jpg"; // Standardbild setzen - }} - sx={{ - objectFit: "contain", // Bild wird skaliert, um vollständig sichtbar zu sein - maxWidth: "100%", // Begrenze die maximale Breite auf den Container - maxHeight: "100%", // Begrenze die maximale Höhe auf den Container - }} - /> - - - {item.name} - - - - - {(item.price100 / 100 * (1 - item.discount100 / 100)).toFixed(2)} € - - {item.discount100 == 0 ? <> : - - {(- item.discount100)}% - - } - { - event.stopPropagation(); - handleAddToCart(); - }} - > - - - - - {item.stock > 10 ? ( - - {t('inStock')} - - ) : item.stock > 0 ? ( - - {t('almostSoldOut')} - - ) : ( - - {t('outOfStock')} - - )} - - - - - ) + {item.stock > 10 ? ( + {t("inStock")} + ) : item.stock > 0 ? ( + + {t("almostSoldOut")} + + ) : ( + + {t("outOfStock")} + + )} + + + + + + ); } diff --git a/01-frontend/src/helper/homepage/PriceSlider.tsx b/01-frontend/src/helper/homepage/PriceSlider.tsx index fc5d314..8635cf2 100644 --- a/01-frontend/src/helper/homepage/PriceSlider.tsx +++ b/01-frontend/src/helper/homepage/PriceSlider.tsx @@ -1,81 +1,88 @@ -import {Box, Slider, Typography, useTheme} from "@mui/material"; -import {SyntheticEvent, useEffect, useState} from "react"; -import {useTranslation} from "react-i18next"; +import { Box, Slider, Typography, useTheme } from "@mui/material"; +import { SyntheticEvent, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; type PriceSliderProps = { - min?: number; - max?: number; - onChange?: (range: [number, number]) => void; + min?: number; + max?: number; + onChange?: (range: [number, number]) => void; }; -export default function PriceSlider({min = 0, max = 10000, onChange}: PriceSliderProps) { - const {t} = useTranslation(); - const theme = useTheme(); +export default function PriceSlider({ + min = 0, + max = 10000, + onChange, +}: PriceSliderProps) { + const { t } = useTranslation(); + const theme = useTheme(); - const [value, setValue] = useState<[number, number]>([min, max]); + const [value, setValue] = useState<[number, number]>([min, max]); - useEffect(() => { - setValue([min, max]); - onChange?.([min, max]); - }, [min, max]); + useEffect(() => { + setValue([min, max]); + onChange?.([min, max]); + }, [min, max]); - const handleChange = (_: Event, newValue: number | number[]) => { - if (Array.isArray(newValue)) { - setValue([newValue[0], newValue[1]]); - } - }; + const handleChange = (_: Event, newValue: number | number[]) => { + if (Array.isArray(newValue)) { + setValue([newValue[0], newValue[1]]); + } + }; - const handleCommitted = (_: Event | SyntheticEvent, newValue: number | number[]) => { - if (Array.isArray(newValue)) { - onChange?.([newValue[0], newValue[1]]); - } - }; + const handleCommitted = ( + _: Event | SyntheticEvent, + newValue: number | number[], + ) => { + if (Array.isArray(newValue)) { + onChange?.([newValue[0], newValue[1]]); + } + }; - const formatValueToEuro = (val: number) => `${(val / 100).toFixed(2)} €`; + const formatValueToEuro = (val: number) => `${(val / 100).toFixed(2)} €`; - return ( - - - {t("price")} - + return ( + + + {t("price")} + - - + + - - {formatValueToEuro(value[0])} – {formatValueToEuro(value[1])} - - - - ); + + {formatValueToEuro(value[0])} – {formatValueToEuro(value[1])} + + + + ); } diff --git a/01-frontend/src/helper/navbar/LoginDialog.tsx b/01-frontend/src/helper/navbar/LoginDialog.tsx index 6a21443..26e2b5f 100644 --- a/01-frontend/src/helper/navbar/LoginDialog.tsx +++ b/01-frontend/src/helper/navbar/LoginDialog.tsx @@ -1,257 +1,307 @@ -import {Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Link, TextField} from "@mui/material"; -import {useQuery} from "@tanstack/react-query"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Link, + TextField, +} from "@mui/material"; +import { useQuery } from "@tanstack/react-query"; import i18next from "i18next"; -import React, {useEffect, useState} from "react"; -import AccountType, {User} from "../../components/Account"; -import {useAccount} from "../AccountProvider"; -import {fetchAccount, submitLogin, submitRegister} from "../query/Queries"; // Importiere die Funktion für die Registrierung -import {useTranslation} from "react-i18next"; +import React, { useEffect, useState } from "react"; +import AccountType, { User } from "../../components/Account"; +import { useAccount } from "../AccountProvider"; +import { fetchAccount, submitLogin, submitRegister } from "../query/Queries"; // Importiere die Funktion für die Registrierung +import { useTranslation } from "react-i18next"; type LoginDialogProps = { - open: boolean; - onClose: () => void; - onSubmit: () => void; // Funktion, die aufgerufen wird, wenn der Login erfolgreich ist - loginData: { email: string; password: string }; - setLoginData: React.Dispatch>; + open: boolean; + onClose: () => void; + onSubmit: () => void; // Funktion, die aufgerufen wird, wenn der Login erfolgreich ist + loginData: { email: string; password: string }; + setLoginData: React.Dispatch< + React.SetStateAction<{ + email: string; + password: string; + customerId: number; + }> + >; }; -const LoginDialog: React.FC = ({open, onClose, loginData, setLoginData, onSubmit}) => { +const LoginDialog: React.FC = ({ + open, + onClose, + loginData, + setLoginData, + onSubmit, +}) => { + const { t } = useTranslation(); + const { login } = useAccount(); + const [showRegister, setShowRegister] = useState(false); + const [registerData, setRegisterData] = useState({ + 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 - const {t} = useTranslation(); - const {login} = useAccount(); - const [showRegister, setShowRegister] = useState(false); - const [registerData, setRegisterData] = useState({ - 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 + useEffect(() => { + if (open) { + const active = document.activeElement as HTMLElement | null; + if (active && typeof active.blur === "function") { + active.blur(); + } + } + }, [open]); - useEffect(() => { - if (open) { - const active = document.activeElement as HTMLElement | null; - if (active && typeof active.blur === "function") { - active.blur(); - } - } - }, [open]); + // useQuery für Login + const { + refetch: refetchLogin, + isLoading: isLoadingLogin, + error: errorLogin, + } = useQuery({ + queryKey: ["submitLogin", loginData], + queryFn: () => submitLogin(loginData), + retry: 0, + retryDelay: 1000, + enabled: false, + }); + const { refetch: refetchAccount } = useQuery({ + queryKey: ["fetchAccount", loginData], + queryFn: () => fetchAccount(loginData), + retry: 0, + retryDelay: 1000, + enabled: false, + }); - // useQuery für Login - const {refetch: refetchLogin, isLoading: isLoadingLogin, error: errorLogin} = useQuery({ - queryKey: ["submitLogin", loginData], - queryFn: () => submitLogin(loginData), - retry: 0, - retryDelay: 1000, - enabled: false, - }); + // useQuery für Registrierung + const { + refetch: refetchRegister, + isLoading: isLoadingRegister, + error: errorRegister, + } = useQuery({ + queryKey: ["submitRegister", registerData], + queryFn: () => submitRegister(registerData), + retry: 0, + retryDelay: 1000, + enabled: false, + }); - const {refetch: refetchAccount} = useQuery({ - queryKey: ["fetchAccount", loginData], - queryFn: () => fetchAccount(loginData), - retry: 0, - retryDelay: 1000, - enabled: false, - }); + const handleClose = () => { + setShowErrorLogin(false); // Fehlermeldung zurücksetzen + setShowErrorRegister(false); // Fehlermeldung zurücksetzen + onClose(); + }; - // useQuery für Registrierung - const {refetch: refetchRegister, isLoading: isLoadingRegister, error: errorRegister} = useQuery({ - queryKey: ["submitRegister", registerData], - queryFn: () => submitRegister(registerData), - retry: 0, - retryDelay: 1000, - enabled: false, - }); + const handleLogin = async () => { + try { + setShowErrorLogin(false); // Fehlermeldung zurücksetzen + setShowErrorRegister(false); // Fehlermeldung zurücksetzen + const response = await refetchLogin(); // Anfrage auslösen + if (response.status === "success") { + const session = response.data.uuid; // Session-Daten aus der Antwort extrahieren + const customerData = (await refetchAccount()).data; + const user: User = { + email: customerData.email, + password: customerData.password, + customerId: customerData.customer.id, // Setze die customerId aus den Account-Daten + session: session, // Setze die Session aus der Login-Antwort + isAdmin: customerData.admin, + }; + login(user); + setShowRegister(false); // Zurück zum Login wechseln + onSubmit(); // Dialog schließen + } else { + setShowErrorLogin(true); // Fehlermeldung anzeigen + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + setShowErrorLogin(true); // Fehlermeldung anzeigen + } + }; - const handleClose = () => { - setShowErrorLogin(false); // Fehlermeldung zurücksetzen - setShowErrorRegister(false); // Fehlermeldung zurücksetzen - onClose(); - }; + const handleRegister = async () => { + try { + setShowErrorLogin(false); // Fehlermeldung zurücksetzen + setShowErrorRegister(false); // Fehlermeldung zurücksetzen + await refetchRegister(); // Beispiel für den Refetch-Aufruf + // Erfolgslogik hier + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + setShowErrorRegister(true); // Fehlermeldung anzeigen + } + }; - const handleLogin = async () => { - try { - setShowErrorLogin(false); // Fehlermeldung zurücksetzen - setShowErrorRegister(false); // Fehlermeldung zurücksetzen - const response = await refetchLogin(); // Anfrage auslösen - if (response.status === "success") { - const session = response.data.uuid; // Session-Daten aus der Antwort extrahieren - const customerData = (await refetchAccount()).data; - const user: User = { - email: customerData.email, - password: customerData.password, - customerId: customerData.customer.id, // Setze die customerId aus den Account-Daten - session: session, // Setze die Session aus der Login-Antwort - isAdmin: customerData.admin - }; - login(user); - setShowRegister(false); // Zurück zum Login wechseln - onSubmit(); // Dialog schließen - } else { - setShowErrorLogin(true); // Fehlermeldung anzeigen - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - setShowErrorLogin(true); // Fehlermeldung anzeigen - } - }; - - const handleRegister = async () => { - try { - setShowErrorLogin(false); // Fehlermeldung zurücksetzen - setShowErrorRegister(false); // Fehlermeldung zurücksetzen - await refetchRegister(); // Beispiel für den Refetch-Aufruf - // Erfolgslogik hier - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - setShowErrorRegister(true); // Fehlermeldung anzeigen - } - }; - - return ( - -
{ - e.preventDefault(); - if (showRegister) { - handleLogin(); - } else { - handleRegister(); + return ( + + { + e.preventDefault(); + if (showRegister) { + handleLogin(); + } else { + handleRegister(); + } + }} + noValidate + > + {showRegister ? t("register") : t("login")} + + { + setLoginData((prev) => ({ ...prev, email: e.target.value })); + setRegisterData((prev) => ({ ...prev, email: e.target.value })); + }} + /> + { + setLoginData((prev) => ({ ...prev, password: e.target.value })); + setRegisterData((prev) => ({ + ...prev, + password: e.target.value, + })); + }} + /> + {showRegister && ( + <> + + setRegisterData((prev) => ({ + ...prev, + customer: { ...prev.customer, name: e.target.value }, + })) } - }} noValidate> - {showRegister ? t("register") : t("login")} - - { - setLoginData(prev => ({...prev, email: e.target.value})); - setRegisterData(prev => ({...prev, email: e.target.value})) - }} - /> - { - setLoginData(prev => ({...prev, password: e.target.value})) - setRegisterData(prev => ({...prev, password: e.target.value})) - }} - /> - {showRegister && - <> - setRegisterData(prev => ({ - ...prev, - customer: {...prev.customer, name: e.target.value}, - }))} - /> - setRegisterData(prev => ({ - ...prev, - customer: {...prev.customer, surname: e.target.value}, - }))} - /> - setRegisterData(prev => ({ - ...prev, - customer: {...prev.customer, address: e.target.value}, - }))} - /> - setRegisterData(prev => ({ - ...prev, - customer: {...prev.customer, country: e.target.value}, - }))} - /> - setRegisterData(prev => ({ - ...prev, - customer: {...prev.customer, zip: e.target.value}, - }))} - /> - - } - - - - {showRegister ? ( - - ) : ( - - )} - - {showErrorLogin && errorLogin && ( - {t("loginFailed")} - )} - {showErrorRegister && errorRegister !== null && ( - {t("registerFailed")} - )} - {showRegister ? ( - - setShowRegister(false)} - color="primary" - underline="hover" - > - {t("backToLogin")} - - - ) : ( - - setShowRegister(true)} - color="primary" - underline="hover" - > - {t("noAccountRegister")} - - - )} - -
- ); + /> + + setRegisterData((prev) => ({ + ...prev, + customer: { ...prev.customer, surname: e.target.value }, + })) + } + /> + + setRegisterData((prev) => ({ + ...prev, + customer: { ...prev.customer, address: e.target.value }, + })) + } + /> + + setRegisterData((prev) => ({ + ...prev, + customer: { ...prev.customer, country: e.target.value }, + })) + } + /> + + setRegisterData((prev) => ({ + ...prev, + customer: { ...prev.customer, zip: e.target.value }, + })) + } + /> + + )} + + + + {showRegister ? ( + + ) : ( + + )} + + {showErrorLogin && errorLogin && ( + {t("loginFailed")} + )} + {showErrorRegister && errorRegister !== null && ( + {t("registerFailed")} + )} + {showRegister ? ( + + setShowRegister(false)} + color="primary" + underline="hover" + > + {t("backToLogin")} + + + ) : ( + + setShowRegister(true)} + color="primary" + underline="hover" + > + {t("noAccountRegister")} + + + )} + +
+ ); }; export default LoginDialog; diff --git a/01-frontend/src/helper/navbar/NavBar.css b/01-frontend/src/helper/navbar/NavBar.css index 8a87617..21083a6 100644 --- a/01-frontend/src/helper/navbar/NavBar.css +++ b/01-frontend/src/helper/navbar/NavBar.css @@ -1,80 +1,80 @@ /* Navbar styles */ .navbar { - /*color in tsx*/ - box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); - position: fixed; - top: 0; - width: 100%; - overflow-x: hidden; - z-index: 10000; - height: var(--navbar-height); - min-height: 64px; + /*color in tsx*/ + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); + position: fixed; + top: 0; + width: 100%; + overflow-x: hidden; + z-index: 10000; + height: var(--navbar-height); + min-height: 64px; } /* Logo styles */ .navbar-logo { - text-decoration: none; - margin-right: 1rem; - height: 3rem; + text-decoration: none; + margin-right: 1rem; + height: 3rem; } /* Menu styles */ .navbar-menu { - display: flex; - align-items: center; - margin-left: auto; + display: flex; + align-items: center; + margin-left: auto; } /* Search styles */ .search { - position: relative; - border-radius: 4px; - background-color: rgba(255, 255, 255, 0.15); + position: relative; + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.15); } .search:hover { - background-color: rgba(255, 255, 255, 0.25); + background-color: rgba(255, 255, 255, 0.25); } .search-icon-wrapper { - padding: 8px; - height: 100%; - position: absolute; - pointer-events: none; - display: flex; - align-items: center; - justify-content: center; + padding: 8px; + height: 100%; + position: absolute; + pointer-events: none; + display: flex; + align-items: center; + justify-content: center; } .search-input { - color: inherit; - width: 100%; - padding: 8px 8px 8px 40px; - font-size: 1rem; + color: inherit; + width: 100%; + padding: 8px 8px 8px 40px; + font-size: 1rem; } /* User avatar styles */ .navbar-user { - margin-left: 16px; + margin-left: 16px; } /* Typography styles */ .navbar-typography { - font-family: 'monospace'; - font-weight: 700; - letter-spacing: .3rem; - color: inherit; - text-decoration: none; + font-family: "monospace"; + font-weight: 700; + letter-spacing: 0.3rem; + color: inherit; + text-decoration: none; } /* Button styles */ .navbar-button { - margin: 2; - color: white; - display: block; + margin: 2; + color: white; + display: block; } .navbar-offset { - height: var(--navbar-height); -} \ No newline at end of file + height: var(--navbar-height); +} diff --git a/01-frontend/src/helper/navbar/NavBar.tsx b/01-frontend/src/helper/navbar/NavBar.tsx index 5427996..a9732c3 100644 --- a/01-frontend/src/helper/navbar/NavBar.tsx +++ b/01-frontend/src/helper/navbar/NavBar.tsx @@ -1,334 +1,348 @@ -import MenuIcon from '@mui/icons-material/Menu'; -import SearchIcon from '@mui/icons-material/Search'; -import {Autocomplete, Badge, TextField} from '@mui/material'; -import AppBar from '@mui/material/AppBar'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import IconButton from '@mui/material/IconButton'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; -import Toolbar from '@mui/material/Toolbar'; -import Tooltip from '@mui/material/Tooltip'; -import Typography from '@mui/material/Typography'; -import {useQuery} from '@tanstack/react-query'; -import * as React from 'react'; -import {useTranslation} from 'react-i18next'; -import {useNavigate} from 'react-router-dom'; -import Item from '../../components/Item'; -import ThemeToggle from '../../theme/ThemeToggle'; -import {useAccount} from '../AccountProvider'; -import {fetchItemList} from '../query/Queries'; -import LoginDialog from './LoginDialog'; -import './NavBar.css'; -import {useBasket} from '../BasketProvider'; -import logo from '../../assets/logo/Blume-logo.png'; - +import MenuIcon from "@mui/icons-material/Menu"; +import SearchIcon from "@mui/icons-material/Search"; +import { Autocomplete, Badge, TextField } from "@mui/material"; +import AppBar from "@mui/material/AppBar"; +import Avatar from "@mui/material/Avatar"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import IconButton from "@mui/material/IconButton"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import Toolbar from "@mui/material/Toolbar"; +import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; +import { useQuery } from "@tanstack/react-query"; +import * as React from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import Item from "../../components/Item"; +import ThemeToggle from "../../theme/ThemeToggle"; +import { useAccount } from "../AccountProvider"; +import { fetchItemList } from "../query/Queries"; +import LoginDialog from "./LoginDialog"; +import "./NavBar.css"; +import { useBasket } from "../BasketProvider"; +import logo from "../../assets/logo/Blume-logo.png"; export default function NavBar() { - const {t} = useTranslation(); - const navigate = useNavigate(); - const [anchorElNav, setAnchorElNav] = React.useState(null); - const [anchorElUser, setAnchorElUser] = React.useState(null); - const [avatarName, setAvatarName] = React.useState(''); // Für Avatar-Tooltip + const { t } = useTranslation(); + const navigate = useNavigate(); + const [anchorElNav, setAnchorElNav] = React.useState( + null, + ); + const [anchorElUser, setAnchorElUser] = React.useState( + null, + ); + const [avatarName, setAvatarName] = React.useState(""); // 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 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 [loginOpen, setLoginOpen] = React.useState(false); - const [loginData, setLoginData] = React.useState({password: '', email: '', customerId: 0}); + const [itemNames, setItemNames] = React.useState([]); // Für Autocomplete + const pageKeys = ["components", "checkout", "contact", "admin"]; - const [itemNames, setItemNames] = React.useState([]); // Für Autocomplete + const filteredPages = pageKeys + .filter((key) => { + if (key === "admin") { + return user?.isAdmin === true; // nur Admins sehen Admin-Seite + } + return true; // alle anderen Seiten immer anzeigen + }) + .map((key) => ({ key, label: t(key) })); - const pageKeys = ['components', 'checkout', 'contact', 'admin']; + const settings = user + ? [ + { + key: "email", + label: `${t("loggedInAs")}: ${user.email}`, + disabled: true, // wir nutzen dieses Flag gleich zur Erkennung + }, + { key: "account", label: t("account") }, + { key: "orders", label: t("orders") }, + { key: "logout", label: t("logout") }, + ] + : [{ key: "login", label: t("login") }]; - const filteredPages = pageKeys - .filter(key => { - if (key === "admin") { - return user?.isAdmin === true; // nur Admins sehen Admin-Seite - } - return true; // alle anderen Seiten immer anzeigen - }) - .map(key => ({key, label: t(key)})); + const handleOpenNavMenu = (event: React.MouseEvent) => { + setAnchorElNav(event.currentTarget); + }; + const handleOpenUserMenu = (event: React.MouseEvent) => { + setAnchorElUser(event.currentTarget); + }; - const settings = user - ? [ - { - key: 'email', - label: `${t('loggedInAs')}: ${user.email}`, - disabled: true // wir nutzen dieses Flag gleich zur Erkennung - }, - {key: 'account', label: t('account')}, - {key: 'orders', label: t('orders')}, - {key: 'logout', label: t('logout')} - ] - : [ - {key: 'login', label: t('login')} - ]; + const handleCloseNavMenu = (link: string) => { + setAnchorElNav(null); + navigate(`/${link.toLowerCase()}`); + }; - const handleOpenNavMenu = (event: React.MouseEvent) => { - setAnchorElNav(event.currentTarget); - }; + const handleCloseUserMenu = (link: string) => { + setAnchorElUser(null); + if (link === "login") { + setLoginOpen(true); + } else if (link === "logout") { + logout(); + if ( + location.pathname.startsWith("/account") || + location.pathname.startsWith("/orders") + ) { + navigate("/"); + } + } else { + navigate(`/${link.toLowerCase()}`); + } + }; - const handleOpenUserMenu = (event: React.MouseEvent) => { - setAnchorElUser(event.currentTarget); - }; + const handleLoginSubmit = () => { + setLoginOpen(false); + }; - const handleCloseNavMenu = (link: string) => { - setAnchorElNav(null); - navigate(`/${link.toLowerCase()}`); - }; + // useQuery, um die Item-Namen zu laden + const { data: items = [] } = useQuery({ + queryKey: ["fetchItemList"], + queryFn: fetchItemList, + }); - const handleCloseUserMenu = (link: string) => { - setAnchorElUser(null); - if (link === 'login') { - setLoginOpen(true); - } else if (link === 'logout') { - logout(); - if ( - location.pathname.startsWith('/account') || - location.pathname.startsWith('/orders') - ) { - navigate('/'); - } - } else { - navigate(`/${link.toLowerCase()}`) - } - }; + React.useEffect(() => { + // Extrahiere die Namen der Items für Autocomplete + setItemNames(items.map((item) => item.name)); + }, [items]); - const handleLoginSubmit = () => { - setLoginOpen(false); - }; + React.useEffect(() => { + // Setze den Avatar-Namen, wenn der Benutzer angemeldet ist + if (user !== undefined && user !== null) { + setAvatarName(user.email.toUpperCase()); + } + if (!user) { + setAvatarName(""); + } + }, [user]); + const handleSearch = (_: React.SyntheticEvent, value: string | null) => { + if (!value) { + // Wenn der Suchwert leer ist, navigiere zur Homepage ohne Suchparameter + navigate("/"); + } else { + // Navigiere zur Homepage mit dem Suchparameter + navigate(`/?search=${encodeURIComponent(value)}`); + } + }; - // useQuery, um die Item-Namen zu laden - const {data: items = []} = useQuery({ - queryKey: ["fetchItemList"], - queryFn: fetchItemList, - }); - - React.useEffect(() => { - // Extrahiere die Namen der Items für Autocomplete - setItemNames(items.map((item) => item.name)); - }, [items]); - - React.useEffect(() => { - // Setze den Avatar-Namen, wenn der Benutzer angemeldet ist - if (user !== undefined && user !== null) { - setAvatarName(user.email.toUpperCase()); - } - if (!user) { - setAvatarName(''); - } - }, [user]); - - const handleSearch = (_: React.SyntheticEvent, value: string | null) => { - - if (!value) { - // Wenn der Suchwert leer ist, navigiere zur Homepage ohne Suchparameter - navigate("/"); - } else { - // Navigiere zur Homepage mit dem Suchparameter - navigate(`/?search=${encodeURIComponent(value)}`); - } - }; - - return ( - <> - - - - Logo navigate('/')} - /> - navigate('/')} - sx={{ - fontFamily: "monospace", - fontWeight: 700, - letterSpacing: ".3rem", - color: "white", - textDecoration: "none", - display: "flex", - alignItems: "center", // <--- HIER hinzugefügt - height: "100%", // optional - ":hover": { - color: "#fff1d8ff" - } - }} - > - Digitaler Produktionsshop - - - - - - ( - - ), - }} - sx={{ - '& .MuiOutlinedInput-root': { - '& fieldset': { - borderColor: 'white', - borderWidth: '1px', - }, - '&:hover fieldset': { - borderColor: 'white', - }, - '&.Mui-focused fieldset': { - borderColor: 'white', - }, - }, - input: { - color: 'white', - }, - }} - /> - )} - /> - - - - - {filteredPages.map(({key, label}) => { - if (key === 'checkout') { - return ( - - ); - } - - return ( - - ); - })} - - - - - - - setAnchorElNav(null)} - > - {filteredPages.map(({key, label}) => ( - handleCloseNavMenu(key)}> - {label} - - ))} - - - - - - - - - - setAnchorElUser(null)} - > - {settings.map(({key, label, disabled}) => ( - { - if (!disabled) handleCloseUserMenu(key); - }} - disabled={disabled} - > - {label} - - ))} - - - - - setLoginOpen(false)} - onSubmit={handleLoginSubmit} - loginData={loginData} - setLoginData={setLoginData} + return ( + <> + + + + Logo navigate("/")} /> -
- - ); + navigate("/")} + sx={{ + fontFamily: "monospace", + fontWeight: 700, + letterSpacing: ".3rem", + color: "white", + textDecoration: "none", + display: "flex", + alignItems: "center", // <--- HIER hinzugefügt + height: "100%", // optional + ":hover": { + color: "#fff1d8ff", + }, + }} + > + Digitaler Produktionsshop + + + + + ( + + ), + }} + sx={{ + "& .MuiOutlinedInput-root": { + "& fieldset": { + borderColor: "white", + borderWidth: "1px", + }, + "&:hover fieldset": { + borderColor: "white", + }, + "&.Mui-focused fieldset": { + borderColor: "white", + }, + }, + input: { + color: "white", + }, + }} + /> + )} + /> + + + + + {filteredPages.map(({ key, label }) => { + if (key === "checkout") { + return ( + + ); + } + + return ( + + ); + })} + + + + + + + setAnchorElNav(null)} + > + {filteredPages.map(({ key, label }) => ( + handleCloseNavMenu(key)}> + {label} + + ))} + + + + + + + + + + setAnchorElUser(null)} + > + {settings.map(({ key, label, disabled }) => ( + { + if (!disabled) handleCloseUserMenu(key); + }} + disabled={disabled} + > + {label} + + ))} + + + + + setLoginOpen(false)} + onSubmit={handleLoginSubmit} + loginData={loginData} + setLoginData={setLoginData} + /> +
+ + ); } diff --git a/01-frontend/src/helper/productpage/ProductInfo.tsx b/01-frontend/src/helper/productpage/ProductInfo.tsx index f4fb8fc..b5d1c29 100644 --- a/01-frontend/src/helper/productpage/ProductInfo.tsx +++ b/01-frontend/src/helper/productpage/ProductInfo.tsx @@ -1,209 +1,215 @@ -import {Close, LocalShipping, ShoppingCart} from "@mui/icons-material"; +import { Close, LocalShipping, ShoppingCart } from "@mui/icons-material"; import { - Alert, - Box, - Button, - Card, - Divider, - Grid, - IconButton, - Rating, - Snackbar, - SnackbarCloseReason, - Stack, - TextField, - Typography + 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 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 [quantity, setQuantity] = useState(1); + const [open, setOpen] = useState(false); + const [imageDimensions, setImageDimensions] = useState({ + width: 0, + height: 0, + }); - const {t} = useTranslation(); - const [quantity, setQuantity] = useState(1); - const [open, setOpen] = useState(false); - const [imageDimensions, setImageDimensions] = useState({width: 0, height: 0}); + const { addToBasket } = useBasket(); + const handleClose = ( + _: React.SyntheticEvent | Event, + reason?: SnackbarCloseReason, + ) => { + if (reason === "clickaway") { + return; + } - const {addToBasket} = useBasket(); + setOpen(false); + }; - const handleClose = ( - _: React.SyntheticEvent | Event, - reason?: SnackbarCloseReason, - ) => { - if (reason === 'clickaway') { - return; + const action = ( + + + + + + ); + + const handleAddToCart = () => { + addToBasket(item, quantity); + setOpen(true); + console.log(`Added {quantity} of €{item.name} to basket`); + }; + + const discountedPrice = item.price100 * (1 - item.discount100 / 100); + + const handleImageLoad = (event: React.SyntheticEvent) => { + const { naturalWidth, naturalHeight } = event.currentTarget; + setImageDimensions({ width: naturalWidth, height: naturalHeight }); + }; + + const [imageUrl, setImageUrl] = useState("/src/assets/default.jpg"); // Fallback-Bild + + useEffect(() => { + const fetchImage = async () => { + try { + const response = await fetch( + `http://localhost:8085/image?uuid=${item.uuid}`, + ); + let data = await response.text(); + if (data.length == 0) { + console.error("Got emtpy picture for article ", item.uuid); } - - setOpen(false); + if (!data.startsWith("data:image/")) { + data = "data:image/jpeg;base64," + data; + } + setImageUrl(data); + } catch (error) { + console.error("Fehler beim Laden des Bildes:", error); + } }; - const action = ( - - - - - - ); + fetchImage(); + }, [item.uuid]); + return ( + + {/* Left Column - Image */} - const handleAddToCart = () => { - addToBasket(item, quantity); - setOpen(true); - console.log(`Added {quantity} of €{item.name} to basket`); - }; + + { + event.currentTarget.src = "/src/assets/default.jpg"; // Standardbild setzen + }} + sx={{ + maxWidth: + imageDimensions.width > imageDimensions.height ? "100%" : "auto", + maxHeight: + imageDimensions.height >= imageDimensions.width ? 400 : "auto", + width: "auto", + height: "auto", + objectFit: "contain", + }} + /> + - const discountedPrice = item.price100 * (1 - item.discount100 / 100); + {/* Right Column - Product Details */} - const handleImageLoad = (event: React.SyntheticEvent) => { - const {naturalWidth, naturalHeight} = event.currentTarget; - setImageDimensions({width: naturalWidth, height: naturalHeight}); - }; + + + {item.name} + - const [imageUrl, setImageUrl] = useState("/src/assets/default.jpg"); // Fallback-Bild + + + + {item.rating > 0 ? `(${item.rating / 2} / 5)` : t("noRatingsYet")} + + - useEffect(() => { - const fetchImage = async () => { - try { - const response = await fetch(`http://localhost:8085/image?uuid=${item.uuid}`); - let data = await response.text(); - if (data.length == 0) { - console.error("Got emtpy picture for article ", item.uuid); - } - if (!data.startsWith("data:image/")) { - data = "data:image/jpeg;base64," + data - } - setImageUrl(data); - } catch (error) { - console.error("Fehler beim Laden des Bildes:", error); - } - }; + + {item.discount100 > 0 ? ( + <> + + {(discountedPrice / 100).toFixed(2)} € + + + {(item.price100 / 100).toFixed(2)} € + + + -{item.discount100} % + + + ) : ( + + {(item.price100 / 100).toFixed(2)} € + + )} + - fetchImage(); - }, [item.uuid]); + - return ( - - {/* Left Column - Image */} + + {item.stock > 10 ? ( + + {t("inStock")} ({item.stock} {t("available")}) + + ) : item.stock > 0 ? ( + + {t("almostSoldOut")} ({item.stock} {t("available")}) + + ) : ( + + {t("outOfStock")} + + )} + - - { - event.currentTarget.src = "/src/assets/default.jpg"; // Standardbild setzen - }} - sx={{ - maxWidth: imageDimensions.width > imageDimensions.height ? "100%" : "auto", - maxHeight: imageDimensions.height >= imageDimensions.width ? 400 : "auto", - width: "auto", - height: "auto", - objectFit: "contain", - }} - /> - + + setQuantity(Math.max(1, parseInt(e.target.value)))} + InputProps={{ inputProps: { min: 1, max: item.stock } }} + sx={{ width: 100 }} + /> + + - - {/* Right Column - Product Details */} - - - - {item.name} - - - - - - {item.rating > 0 ? `(${item.rating / 2} / 5)` : t('noRatingsYet')} - - - - - - {item.discount100 > 0 ? ( - <> - - {(discountedPrice / 100).toFixed(2)} € - - - {(item.price100 / 100).toFixed(2)} € - - - -{item.discount100} % - - - ) : ( - - {(item.price100 / 100).toFixed(2)} € - - )} - - - - - - {item.stock > 10 ? ( - - {t('inStock')} ({item.stock} {t('available')}) - - ) : item.stock > 0 ? ( - {t('almostSoldOut')} ({item.stock} {t('available')}) - ) : ( - {t('outOfStock')} - )} - - - - setQuantity(Math.max(1, parseInt(e.target.value)))} - InputProps={{inputProps: {min: 1, max: item.stock}}} - sx={{width: 100}} - /> - - - - - - - {t('freeShipping')} - - - - - - - - ); -} \ No newline at end of file + + + + {t("freeShipping")} + + + + + + ); +} diff --git a/01-frontend/src/helper/productpage/RatingCard.tsx b/01-frontend/src/helper/productpage/RatingCard.tsx index 1de778a..5dcdca4 100644 --- a/01-frontend/src/helper/productpage/RatingCard.tsx +++ b/01-frontend/src/helper/productpage/RatingCard.tsx @@ -1,55 +1,63 @@ -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 theme = useTheme(); // Zugriff auf Light/Dark-Mode + const { t } = useTranslation(); + const theme = useTheme(); // Zugriff auf Light/Dark-Mode - const handleClick = () => { - }; + const handleClick = () => {}; - return ( - - + + + + - - - - {t('ratingFrom')} {new Date(ratingType.timestamp).toLocaleDateString('de-DE')} - + {t("ratingFrom")}{" "} + {new Date(ratingType.timestamp).toLocaleDateString("de-DE")} + - + - - {ratingType.content} - - - - - - ); + + {ratingType.content} + + + + + + ); } diff --git a/01-frontend/src/helper/productpage/Ratings.tsx b/01-frontend/src/helper/productpage/Ratings.tsx index fd36b93..b3141aa 100644 --- a/01-frontend/src/helper/productpage/Ratings.tsx +++ b/01-frontend/src/helper/productpage/Ratings.tsx @@ -1,166 +1,170 @@ import { - Box, - Button, - Divider, - IconButton, - Rating, - Snackbar, - SnackbarCloseReason, - TextField, - Typography, - useTheme + Box, + Button, + Divider, + IconButton, + Rating, + Snackbar, + SnackbarCloseReason, + TextField, + 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(); - const theme = useTheme(); +export default function Ratings({ itemId }: { itemId: string }) { + const { t } = useTranslation(); + const theme = useTheme(); - const [open, setOpen] = useState(false); - const [ratingText, setRatingText] = useState(""); - const [ratingValue, setRatingValue] = useState(2.5); + const [open, setOpen] = useState(false); + const [ratingText, setRatingText] = useState(""); + const [ratingValue, setRatingValue] = useState(2.5); - const ratingData: RatingSubmitType = { - rating: ratingValue || 0, - content: ratingText || "", - articleId: itemId, - }; + const ratingData: RatingSubmitType = { + rating: ratingValue || 0, + content: ratingText || "", + articleId: itemId, + }; - const {refetch} = useQuery({ - queryKey: ["submitRating", ratingData], - queryFn: () => submitRating(ratingData), - retry: 3, - retryDelay: 1000, - enabled: false, - }); + const { refetch } = useQuery({ + queryKey: ["submitRating", ratingData], + queryFn: () => submitRating(ratingData), + retry: 3, + retryDelay: 1000, + enabled: false, + }); - const handleRatingSubmit = () => { - setOpen(true); - void refetch(); // bewusst ausgelöst, kein await notwendig - }; + const handleRatingSubmit = () => { + setOpen(true); + void refetch(); // bewusst ausgelöst, kein await notwendig + }; - const handleClose = ( - _: React.SyntheticEvent | Event, - reason?: SnackbarCloseReason - ) => { - if (reason === "clickaway") return; - setOpen(false); - }; + const handleClose = ( + _: React.SyntheticEvent | Event, + reason?: SnackbarCloseReason, + ) => { + if (reason === "clickaway") return; + setOpen(false); + }; - const action = ( - - - - - - ); + const action = ( + + + + + + ); - const {data = []} = useQuery({ - queryKey: ["fetchRatingList", itemId], - queryFn: () => fetchRatingList(itemId), - retry: 3, - retryDelay: 1000, - }); + const { data = [] } = useQuery({ + queryKey: ["fetchRatingList", itemId], + queryFn: () => fetchRatingList(itemId), + retry: 3, + retryDelay: 1000, + }); - const ratings: RatingType[] = useMemo(() => data || [], [data]); + const ratings: RatingType[] = useMemo(() => data || [], [data]); - const getRatings = () => { - if (ratings.length === 0) { - return ( - - {t("noRatingsYet")} - - ); - } + const getRatings = () => { + if (ratings.length === 0) { + return ( + + {t("noRatingsYet")} + + ); + } - return ratings.map((ratingType: RatingType) => ( - - )); - }; + return ratings.map((ratingType: RatingType) => ( + + )); + }; - return ( - <> - + return ( + <> + - - - {t("rateThisProduct")}: - + + + {t("rateThisProduct")}: + - setRatingValue(value)} - precision={0.5} - /> + setRatingValue(value)} + precision={0.5} + /> - setRatingText(e.target.value)} - /> + setRatingText(e.target.value)} + /> - - + + - + - - {getRatings()} - + {getRatings()} - - - ); + + + ); } diff --git a/01-frontend/src/helper/query/Queries.tsx b/01-frontend/src/helper/query/Queries.tsx index 7915dd2..9f08f27 100644 --- a/01-frontend/src/helper/query/Queries.tsx +++ b/01-frontend/src/helper/query/Queries.tsx @@ -1,323 +1,415 @@ // 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'); - if (!response.ok) { - throw new Error('Fehler beim Laden der Items'); - } - const data = await response.json(); - return data; + const response = await fetch("http://localhost:8085/article/all"); + if (!response.ok) { + throw new Error("Fehler beim Laden der Items"); + } + const data = await response.json(); + return data; }; export const fetchItemListWithImage = async () => { - const response = await fetch('http://localhost:8085/article/all/image'); - if (!response.ok) { - throw new Error('Fehler beim Laden der Items'); - } - const data = await response.json(); - return data; + const response = await fetch("http://localhost:8085/article/all/image"); + if (!response.ok) { + throw new Error("Fehler beim Laden der Items"); + } + const data = await response.json(); + return data; }; export const submitRating = async (ratingData: RatingSubmitType) => { - const response = await fetch('http://localhost:8085/review?uuid=' + ratingData.articleId + '&rating=' + ratingData.rating * 2, { - method: 'POST', - headers: { - 'Content-Type': 'text/plain', - }, - body: ratingData.content, - }); + const response = await fetch( + "http://localhost:8085/review?uuid=" + + ratingData.articleId + + "&rating=" + + ratingData.rating * 2, + { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: ratingData.content, + }, + ); - if (!response.ok) { - throw new Error('Fehler beim Senden der Bewertung'); - } + if (!response.ok) { + throw new Error("Fehler beim Senden der Bewertung"); + } - const data = await response.json(); - return data; -} + const data = await response.json(); + return data; +}; export const fetchRatingList = async (itemId: string) => { - const response = await fetch('http://localhost:8085/review/all?uuid=' + itemId); - if (!response.ok) { - throw new Error('Fehler beim Laden der Items'); - } - const data = await response.json(); - return data; + const response = await fetch( + "http://localhost:8085/review/all?uuid=" + itemId, + ); + if (!response.ok) { + throw new Error("Fehler beim Laden der Items"); + } + const data = await response.json(); + return data; }; export const submitOrder = async (data: OrderType) => { - const response = await fetch('http://localhost:8085/order', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); + const response = await fetch("http://localhost:8085/order", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); - if (!response.ok) { - throw new Error('Fehler beim Senden der Bestellung'); - } + if (!response.ok) { + throw new Error("Fehler beim Senden der Bestellung"); + } - return await response.json(); -} + return await response.json(); +}; export const submitAccount = async (data: AccountType) => { - const response = await fetch('http://localhost:8085/account', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); + const response = await fetch("http://localhost:8085/account", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); - if (!response.ok) { - throw new Error('Fehler beim Senden des Accounts'); - } + if (!response.ok) { + throw new Error("Fehler beim Senden des Accounts"); + } - return await response.json(); -} + return await response.json(); +}; export const submitCustomer = async (data: CustomerType) => { - const response = await fetch('http://localhost:8085/customer', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); + const response = await fetch("http://localhost:8085/customer", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); - if (!response.ok) { - throw new Error('Fehler beim Senden des Accounts'); - } + if (!response.ok) { + throw new Error("Fehler beim Senden des Accounts"); + } - return await response.json(); -} + return await response.json(); +}; export const submitLogin = async (loginData: SubmitLogin) => { - const response = await fetch("http://localhost:8085/session?email=" + loginData.email + "&password=" + loginData.password, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(loginData), - }); - if (!response.ok) { - throw new Error("Login failed"); - } - return response.json(); + const response = await fetch( + "http://localhost:8085/session?email=" + + loginData.email + + "&password=" + + loginData.password, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(loginData), + }, + ); + if (!response.ok) { + throw new Error("Login failed"); + } + return response.json(); }; export const fetchAccount = async (loginData: SubmitLogin) => { - const response = await fetch("http://localhost:8085/account?email=" + loginData.email + "&password=" + loginData.password); - if (!response.ok) { - throw new Error("Login failed"); - } - return response.json(); + const response = await fetch( + "http://localhost:8085/account?email=" + + loginData.email + + "&password=" + + loginData.password, + ); + if (!response.ok) { + throw new Error("Login failed"); + } + return response.json(); }; export const submitRegister = async (registerData: AccountType) => { - const response = await fetch("http://localhost:8085/account", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(registerData), - }); - if (!response.ok) { - throw new Error("Login failed"); - } - return response.json(); + const response = await fetch("http://localhost:8085/account", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(registerData), + }); + if (!response.ok) { + throw new Error("Login failed"); + } + return response.json(); }; export const fetchCustomer = async (userId: number) => { - const response = await fetch('http://localhost:8085/customer?id=' + userId); - if (!response.ok) { - throw new Error('Fehler beim Laden des Customers'); - } - const data = await response.json(); - return data; + const response = await fetch("http://localhost:8085/customer?id=" + userId); + if (!response.ok) { + throw new Error("Fehler beim Laden des Customers"); + } + const data = await response.json(); + return data; }; export const deleteAccount = async (user: User) => { - const response = await fetch('http://localhost:8085/account?email=' + user.email + '&password=' + user.password, { - method: 'DELETE', - }); - if (!response.ok) { - throw new Error('Fehler beim Löschen des Accounts'); - } - return await response.json(); + const response = await fetch( + "http://localhost:8085/account?email=" + + user.email + + "&password=" + + user.password, + { + method: "DELETE", + }, + ); + if (!response.ok) { + throw new Error("Fehler beim Löschen des Accounts"); + } + return await response.json(); }; export const deleteAccountAdmin = async (operation: AdminAccountOperation) => { - const response = await fetch('http://localhost:8085/account/admin?email=' + operation.email + '&uuid=' + operation.uuid + '&id=' + operation.accountId, { - method: 'DELETE', - }); - if (!response.ok) { - throw new Error('Fehler beim Löschen des Accounts'); - } - return await response.json(); + const response = await fetch( + "http://localhost:8085/account/admin?email=" + + operation.email + + "&uuid=" + + operation.uuid + + "&id=" + + operation.accountId, + { + method: "DELETE", + }, + ); + if (!response.ok) { + throw new Error("Fehler beim Löschen des Accounts"); + } + return await response.json(); }; export const fetchOrders = async (customerId: number) => { - const response = await fetch('http://localhost:8085/order/all?customerId=' + customerId); - if (!response.ok) { - throw new Error('Fehler beim Laden des Customers'); - } - const data = await response.json(); - return data; + const response = await fetch( + "http://localhost:8085/order/all?customerId=" + customerId, + ); + if (!response.ok) { + throw new Error("Fehler beim Laden des Customers"); + } + const data = await response.json(); + return data; }; export const fetchAccounts = async (loginData: User) => { - const response = await fetch("http://localhost:8085/account/all?email=" + loginData.email + "&session=" + loginData.session); - if (!response.ok) { - throw new Error("Login failed"); - } - return response.json(); + const response = await fetch( + "http://localhost:8085/account/all?email=" + + loginData.email + + "&session=" + + loginData.session, + ); + if (!response.ok) { + throw new Error("Login failed"); + } + return response.json(); }; -export const fetchItems = async () => { //TODO: remove and use above - const response = await fetch("http://localhost:8085/article/all"); - if (!response.ok) { - throw new Error("fetching items failed"); - } - return response.json(); +export const fetchItems = async () => { + //TODO: remove and use above + const response = await fetch("http://localhost:8085/article/all"); + if (!response.ok) { + throw new Error("fetching items failed"); + } + return response.json(); }; export const fetchStatisticsVolume = async (loginData: User) => { - const response = await fetch("http://localhost:8085/statistics/volume?email=" + loginData.email + "&session=" + loginData.session); - if (!response.ok) { - throw new Error("fetching satistics Volume failed"); - } - return response.json(); + const response = await fetch( + "http://localhost:8085/statistics/volume?email=" + + loginData.email + + "&session=" + + loginData.session, + ); + if (!response.ok) { + throw new Error("fetching satistics Volume failed"); + } + return response.json(); }; export const fetchStatisticsRevenue = async (loginData: User) => { - const response = await fetch("http://localhost:8085/statistics/revenue?email=" + loginData.email + "&session=" + loginData.session); - if (!response.ok) { - throw new Error("fetching satistics Revenue failed"); - } - return response.json(); + const response = await fetch( + "http://localhost:8085/statistics/revenue?email=" + + loginData.email + + "&session=" + + loginData.session, + ); + if (!response.ok) { + throw new Error("fetching satistics Revenue failed"); + } + return response.json(); }; export const editAccount = async (customer: CustomerType) => { - const response = await fetch('http://localhost:8085/customer?id=' + customer.id, { - method: 'PUT', - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(customer), - }); - if (!response.ok) { - throw new Error('Fehler beim Löschen des Accounts'); - } - return await response.json(); + const response = await fetch( + "http://localhost:8085/customer?id=" + customer.id, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(customer), + }, + ); + if (!response.ok) { + throw new Error("Fehler beim Löschen des Accounts"); + } + return await response.json(); }; export const orderPatch = async (order: OrderPatch) => { - const response = await fetch("http://localhost:8085/order?id=" + order.id + "&status=" + order.status, { - method: "PATCH", - } - ); - if (!response.ok) { - throw new Error("Order patch failed"); - } - return response.json(); + const response = await fetch( + "http://localhost:8085/order?id=" + order.id + "&status=" + order.status, + { + method: "PATCH", + }, + ); + if (!response.ok) { + throw new Error("Order patch failed"); + } + return response.json(); }; export const fetchOrderStatus = async (loginData: User) => { - const response = await fetch("http://localhost:8085/statistics/orderstatus?email=" + loginData.email + "&session=" + loginData.session); - if (!response.ok) { - throw new Error("fetching order status failed"); - } - return response.json(); + const response = await fetch( + "http://localhost:8085/statistics/orderstatus?email=" + + loginData.email + + "&session=" + + loginData.session, + ); + if (!response.ok) { + throw new Error("fetching order status failed"); + } + return response.json(); }; export const fetchStockPercent = async (loginData: User) => { - const response = await fetch("http://localhost:8085/statistics/stockpercent?email=" + loginData.email + "&session=" + loginData.session); - if (!response.ok) { - throw new Error("fetching stock% failed"); - } - return response.json(); + const response = await fetch( + "http://localhost:8085/statistics/stockpercent?email=" + + loginData.email + + "&session=" + + loginData.session, + ); + if (!response.ok) { + throw new Error("fetching stock% failed"); + } + return response.json(); }; - export const fetchOrdersAdmin = async (loginData: User) => { - const response = await fetch("http://localhost:8085/order/all/all?email=" + loginData.email + "&session=" + loginData.session); - if (!response.ok) { - throw new Error("fetching admin orders failed"); - } - return response.json(); + const response = await fetch( + "http://localhost:8085/order/all/all?email=" + + loginData.email + + "&session=" + + loginData.session, + ); + if (!response.ok) { + throw new Error("fetching admin orders failed"); + } + return response.json(); }; export const updateCustomer = async (customer: CustomerType) => { - const response = await fetch('http://localhost:8085/customer?id=' + customer.id, { - method: 'PUT', - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(customer), - }); - if (!response.ok) { - throw new Error('Fehler beim Ändern des Customers'); - } - return await response.json(); -} + const response = await fetch( + "http://localhost:8085/customer?id=" + customer.id, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(customer), + }, + ); + if (!response.ok) { + throw new Error("Fehler beim Ändern des Customers"); + } + return await response.json(); +}; -export const fetchFarmingStationItemList = async (): Promise => { - const response = await fetch('http://localhost:8085/farm/articles'); - if (!response.ok) - throw new Error('Failed to fetch items'); - return response.json(); -} +export const fetchFarmingStationItemList = async (): Promise< + ItemWithFSImage[] +> => { + const response = await fetch("http://localhost:8085/farm/articles"); + if (!response.ok) throw new Error("Failed to fetch items"); + return response.json(); +}; export const submitItem = async (item: Item) => { - const response = await fetch("http://localhost:8085/article", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(item), - }); - if (!response.ok) { - throw new Error("Login failed"); - } - return response.json(); + const response = await fetch("http://localhost:8085/article", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(item), + }); + if (!response.ok) { + throw new Error("Login failed"); + } + return response.json(); }; export const deleteItemQuery = async (uuid: string) => { - const response = await fetch("http://localhost:8085/article?uuid=" + uuid, { - method: "DELETE" - }); - if (!response.ok) { - throw new Error("Login failed"); - } - return response.json(); + const response = await fetch("http://localhost:8085/article?uuid=" + uuid, { + method: "DELETE", + }); + if (!response.ok) { + throw new Error("Login failed"); + } + return response.json(); }; export const updateAccountAdmin = async (account: AccountType, user: User) => { - const response = await fetch('http://localhost:8085/account/admin?email=' + user.email + "&uuid=" + user.session + "&id=" + account.id + "&admin=" + account.admin, { - method: 'POST', - }); - if (!response.ok) { - throw new Error('Fehler beim Ändern des Customers'); - } - return await response.json(); -} + const response = await fetch( + "http://localhost:8085/account/admin?email=" + + user.email + + "&uuid=" + + user.session + + "&id=" + + account.id + + "&admin=" + + account.admin, + { + method: "POST", + }, + ); + if (!response.ok) { + throw new Error("Fehler beim Ändern des Customers"); + } + return await response.json(); +}; export const updateItemAdmin = async (item: Item) => { - const response = await fetch('http://localhost:8085/article?uuid=' + item.uuid, { - method: 'PUT', - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(item), - }); - if (!response.ok) { - throw new Error('Fehler beim Ändern des Items'); - } - return await response.json(); -} + const response = await fetch( + "http://localhost:8085/article?uuid=" + item.uuid, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(item), + }, + ); + if (!response.ok) { + throw new Error("Fehler beim Ändern des Items"); + } + return await response.json(); +}; diff --git a/01-frontend/src/index.css b/01-frontend/src/index.css index ed24c19..dd53f3c 100644 --- a/01-frontend/src/index.css +++ b/01-frontend/src/index.css @@ -1,73 +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; - } + :root { + color: #213547; + background-color: #ffffff; + } - a:hover { - color: #747bff; - } + a:hover { + color: #747bff; + } - button { - background-color: #f9f9f9; - } + button { + background-color: #f9f9f9; + } } diff --git a/01-frontend/src/main.tsx b/01-frontend/src/main.tsx index 56b8897..7c48602 100644 --- a/01-frontend/src/main.tsx +++ b/01-frontend/src/main.tsx @@ -1,15 +1,15 @@ -import './components/i18n/i18n'; -import {StrictMode} from 'react'; -import {createRoot} from 'react-dom/client'; -import './index.css'; -import App from './App.tsx'; +import "./components/i18n/i18n"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App.tsx"; -const rootElement = document.getElementById('root'); +const rootElement = document.getElementById("root"); if (!rootElement) throw new Error("Root element not found"); const root = createRoot(rootElement); root.render( - - - -); \ No newline at end of file + + + , +); diff --git a/01-frontend/src/pages/Account.tsx b/01-frontend/src/pages/Account.tsx index 35a2242..7a864f2 100644 --- a/01-frontend/src/pages/Account.tsx +++ b/01-frontend/src/pages/Account.tsx @@ -1,239 +1,252 @@ import { - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Divider, - Paper, - Stack, - TextField, - Typography, + 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({ - name: "", - surname: "", - address: "", - country: "", - zip: "", - id: userData?.customerId || 0, - }); + const [user, setUser] = useState({ + name: "", + surname: "", + address: "", + country: "", + zip: "", + id: userData?.customerId || 0, + }); - const [userDataState, setUserDataState] = useState(userData || { - password: "", - email: "", - customerId: 0, - session: "", - isAdmin: false, - }); + const [userDataState, setUserDataState] = useState( + 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({ - queryKey: ["fetchCustomer", userData?.customerId], - queryFn: () => fetchCustomer(userData?.customerId || 0), - retry: 1, - retryDelay: 1000, - }); + const { data } = useQuery({ + 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) => { - 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) => { + 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 ( - - - - {t("myAccount")} - - - - - - - - - - - {edit ? ( - <> - - - - ) : ( - - )} - - - - - {/* Passwort-Dialog */} - - {t("confirmDeleteAccount")} - - {t("enterPasswordToConfirmDeletion")} - setPasswordInput(e.target.value)} - /> - - - - - - + return ( + + + + {t("myAccount")} + + + + + + + + + + + {edit ? ( + <> + + + + ) : ( + + )} + - ); + + + {/* Passwort-Dialog */} + + {t("confirmDeleteAccount")} + + {t("enterPasswordToConfirmDeletion")} + setPasswordInput(e.target.value)} + /> + + + + + + + + ); } diff --git a/01-frontend/src/pages/AdminPanel.tsx b/01-frontend/src/pages/AdminPanel.tsx index 4c03d4c..a3f7155 100644 --- a/01-frontend/src/pages/AdminPanel.tsx +++ b/01-frontend/src/pages/AdminPanel.tsx @@ -1,81 +1,98 @@ -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 theme = useTheme(); - const [infoStatus, setInfoStatus] = useState("statistics"); + const { t } = useTranslation(); + const theme = useTheme(); + const [infoStatus, setInfoStatus] = useState("statistics"); - const handleInfoStatus = (path: string) => { - setInfoStatus(path); - }; + const handleInfoStatus = (path: string) => { + setInfoStatus(path); + }; - const renderContent = () => { - switch (infoStatus) { - case "statistics": - return ; - case "orders": - return ; - case "accounts": - return ; - case "items": - return ; - default: - return ; - } - }; + const renderContent = () => { + switch (infoStatus) { + case "statistics": + return ; + case "orders": + return ; + case "accounts": + return ; + case "items": + return ; + default: + return ; + } + }; - const menuItems = [ - {key: "statistics", icon: , label: t("statistics")}, - {key: "orders", icon: , label: t("orders")}, - {key: "accounts", icon: , label: t("accounts")}, - {key: "items", icon: , label: t("items")}, - ]; + const menuItems = [ + { key: "statistics", icon: , label: t("statistics") }, + { key: "orders", icon: , label: t("orders") }, + { key: "accounts", icon: , label: t("accounts") }, + { key: "items", icon: , label: t("items") }, + ]; - return ( - -
- {/* Sidebar */} - - - - - {/* Content */} - {renderContent()} -
+ return ( + +
+ {/* Sidebar */} + + - ); + + {/* Content */} + {renderContent()} +
+
+ ); } diff --git a/01-frontend/src/pages/Contact.tsx b/01-frontend/src/pages/Contact.tsx index b5bc7c6..39aa27a 100644 --- a/01-frontend/src/pages/Contact.tsx +++ b/01-frontend/src/pages/Contact.tsx @@ -1,91 +1,130 @@ -import {Box, Divider, Typography} from "@mui/material"; +import { Box, Divider, Typography } from "@mui/material"; import "./pages.css"; export default function Impressum() { - return ( - - - Impressum - + return ( + + + Impressum + - - Hochschule für Technik und Wirtschaft
- des Saarlandes
- Goebenstraße 40
- 66117 Saarbrücken

- Telefon: (0681) 58 67 - 0
- Telefax: (0681) 58 67 - 122
- E-Mail: info@htwsaar.de

- Aufsichtsbehörde:
- Ministerium der Finanzen und für Wissenschaft des Saarlandes -
+ + Hochschule für Technik und Wirtschaft +
+ des Saarlandes +
+ Goebenstraße 40 +
+ 66117 Saarbrücken +
+
+ Telefon: (0681) 58 67 - 0
+ Telefax: (0681) 58 67 - 122 +
+ E-Mail: info@htwsaar.de +
+
+ Aufsichtsbehörde: +
+ Ministerium der Finanzen und für Wissenschaft des Saarlandes +
- + - - Datenschutzerklärung - + + Datenschutzerklärung + - - Personenbezogene Daten (nachfolgend zumeist nur „Daten“ genannt) ... - + + Personenbezogene Daten (nachfolgend zumeist nur „Daten“ genannt) ... + - - Gemäß Art. 4 Ziffer 1. der Verordnung (EU) 2016/679, also der Datenschutz-Grundverordnung ... - + + Gemäß Art. 4 Ziffer 1. der Verordnung (EU) 2016/679, also der + Datenschutz-Grundverordnung ... + - - Unsere Datenschutzerklärung ist wie folgt gegliedert:
- I. Informationen über uns als Verantwortliche
- II. Rechte der Nutzer und Betroffenen
- III. Informationen zur Datenverarbeitung -
+ + Unsere Datenschutzerklärung ist wie folgt gegliedert: +
+ I. Informationen über uns als Verantwortliche +
+ II. Rechte der Nutzer und Betroffenen +
+ III. Informationen zur Datenverarbeitung +
- - I. Informationen über uns als Verantwortliche - + + I. Informationen über uns als Verantwortliche + - - Verantwortlicher Anbieter dieses Internetauftritts ... - + + Verantwortlicher Anbieter dieses Internetauftritts ... + - - II. Rechte der Nutzer und Betroffenen - + + II. Rechte der Nutzer und Betroffenen + - - Mit Blick auf die nachfolgend noch näher beschriebene Datenverarbeitung haben die Nutzer und Betroffenen - ... - + + Mit Blick auf die nachfolgend noch näher beschriebene Datenverarbeitung + haben die Nutzer und Betroffenen ... + - -
  • • Auskunft über die verarbeiteten - Daten (Art. 15 DSGVO)
  • -
  • • Berichtigung unrichtiger Daten - (Art. 16 DSGVO)
  • -
  • • Löschung der Daten (Art. 17 - DSGVO)
  • -
  • • Einschränkung der Verarbeitung - (Art. 18 DSGVO)
  • -
  • • Datenübertragbarkeit (Art. 20 - DSGVO)
  • -
    + +
  • + + {" "} + • Auskunft über die verarbeiteten Daten (Art. 15 DSGVO) + +
  • +
  • + + {" "} + • Berichtigung unrichtiger Daten (Art. 16 DSGVO) + +
  • +
  • + + {" "} + • Löschung der Daten (Art. 17 DSGVO) + +
  • +
  • + + {" "} + • Einschränkung der Verarbeitung (Art. 18 DSGVO) + +
  • +
  • + + {" "} + • Datenübertragbarkeit (Art. 20 DSGVO) + +
  • +
    - - III. Informationen zur Datenverarbeitung - + + III. Informationen zur Datenverarbeitung + - - Ihre bei Nutzung unseres Internetauftritts verarbeiteten Daten ... - + + Ihre bei Nutzung unseres Internetauftritts verarbeiteten Daten ... + - {/* Du kannst einfach alle weiteren Absätze so fortsetzen – copy & paste, + {/* Du kannst einfach alle weiteren Absätze so fortsetzen – copy & paste, jeweils in: */} - - Mehr Infos unter: CloudFlare Datenschutzerklärung - -
    - ); + + Mehr Infos unter:{" "} + + CloudFlare Datenschutzerklärung + + +
    + ); } diff --git a/01-frontend/src/pages/FSComponents.tsx b/01-frontend/src/pages/FSComponents.tsx index 95ed906..d512493 100644 --- a/01-frontend/src/pages/FSComponents.tsx +++ b/01-frontend/src/pages/FSComponents.tsx @@ -1,106 +1,142 @@ -import {Box, Button, Typography, useTheme} from "@mui/material"; -import {useTranslation} from "react-i18next"; -import {useState} from "react"; -import {useQuery} from "@tanstack/react-query"; -import {useBasket} from "../helper/BasketProvider"; +import { Box, Button, Typography, useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { useBasket } from "../helper/BasketProvider"; import ItemCard from "../helper/homepage/ItemCard"; -import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart'; +import AddShoppingCartIcon from "@mui/icons-material/AddShoppingCart"; -import farmingStation from '../assets/fscomponents/fs_components_0.png'; -import {ItemWithFSImage} from "../components/Item"; -import {fetchFarmingStationItemList} from "../helper/query/Queries"; +import farmingStation from "../assets/fscomponents/fs_components_0.png"; +import { ItemWithFSImage } from "../components/Item"; +import { fetchFarmingStationItemList } from "../helper/query/Queries"; -"../components/Item"; +("../components/Item"); export default function FSComponents() { - const {t} = useTranslation(); - const theme = useTheme(); - const {addToBasket} = useBasket(); - const [hoverIndex, setHoverIndex] = useState(null); + const { t } = useTranslation(); + const theme = useTheme(); + const { addToBasket } = useBasket(); + const [hoverIndex, setHoverIndex] = useState(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"]; + // 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({ - queryKey: ['fetchFarmingStationItemList'], - queryFn: fetchFarmingStationItemList, - retry: 3, - retryDelay: 1000, + // Daten mit react-query laden + const { + data = [], + isLoading, + error, + } = useQuery({ + queryKey: ["fetchFarmingStationItemList"], + queryFn: fetchFarmingStationItemList, + retry: 3, + retryDelay: 1000, + }); + + // Button-Funktion: alle gefilterten Items in den Warenkorb packen + const handleAddAllToCart = () => { + data.forEach((item) => { + addToBasket(item, 1); }); + }; - // Button-Funktion: alle gefilterten Items in den Warenkorb packen - const handleAddAllToCart = () => { - data.forEach(item => { - addToBasket(item, 1); - }); - }; + if (isLoading) return {t("loading")}; + if (error) + return {t("errorLoadingItems")}; - if (isLoading) return {t('loading')}; - if (error) return {t('errorLoadingItems')}; + return ( + + {/* Bild links */} + + + + - return ( - - {/* Bild links */} - - - - - - {/* Items rechts */} - - - - {t('componentsFarmingStation')} - - - - {data.map((item, index) => ( -
    setHoverIndex(index)} - onMouseLeave={() => setHoverIndex(null)} - > - -
    - ))} -
    -
    + {/* Items rechts */} + + + + {t("componentsFarmingStation")} + - ); + + {data.map((item, index) => ( +
    setHoverIndex(index)} + onMouseLeave={() => setHoverIndex(null)} + > + +
    + ))} +
    +
    +
    + ); } diff --git a/01-frontend/src/pages/Home.tsx b/01-frontend/src/pages/Home.tsx index a4b56f4..7ba45f4 100644 --- a/01-frontend/src/pages/Home.tsx +++ b/01-frontend/src/pages/Home.tsx @@ -1,201 +1,206 @@ -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 navigate = useNavigate(); + const location = useLocation(); + const theme = useTheme(); + const [searchQuery, setSearchQuery] = useState(null); - const {t} = useTranslation(); - const navigate = useNavigate(); - const location = useLocation(); - const theme = useTheme(); - const [searchQuery, setSearchQuery] = useState(null); + const categoriesFilter = useMemo( + () => [ + { value: "", label: t("allCategories") }, + { value: "Seeds", label: t("seeds") }, + { value: "GardenSupplies", label: t("gardenSupplies") }, + { value: "TechnicalComponents", label: t("technicalComponents") }, + { value: "Other", label: t("other") }, + ], + [t], + ); - const categoriesFilter = useMemo(() => [ - {value: "", label: t("allCategories")}, - {value: "Seeds", label: t("seeds")}, - {value: "GardenSupplies", label: t("gardenSupplies")}, - {value: "TechnicalComponents", label: t("technicalComponents")}, - {value: "Other", label: t("other")} - ], [t]); + const ratingFilter = [ + { value: "", label: t("allRatings") }, + ...[5, 4, 3, 2, 1].map((value) => ({ + value: value.toString(), + label: value.toString(), + })), + ]; - const ratingFilter = [ - {value: "", label: t('allRatings')}, - ...[5, 4, 3, 2, 1].map(value => ({ - value: value.toString(), - label: value.toString() - })) - ]; + const [selectedCategory, setSelectedCategory] = useState(null); + const [selectedRating, setSelectedRating] = useState(null); - const [selectedCategory, setSelectedCategory] = useState(null); - const [selectedRating, setSelectedRating] = useState(null); + const { data = [], isLoading } = useQuery({ + queryKey: ["fetchItemListWithImage"], + queryFn: fetchItemListWithImage, + retry: 3, // Versucht es 3-mal erneut + retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms) + }); - const {data = [], isLoading} = useQuery({ - queryKey: ['fetchItemListWithImage'], - queryFn: fetchItemListWithImage, - retry: 3, // Versucht es 3-mal erneut - retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms) - }); + const items: ItemWithImage[] = useMemo(() => data || [], [data]); - const items: ItemWithImage[] = useMemo(() => data || [], [data]); + const discountedPrices = items.map( + (item) => item.price100 * (1 - item.discount100 / 100), + ); + const minPrice = + discountedPrices.length > 0 ? Math.min(...discountedPrices) : 0; + const maxPrice = + discountedPrices.length > 0 ? Math.max(...discountedPrices) : 1000; + const [priceRange, setPriceRange] = useState<[number, number]>([ + minPrice, + maxPrice, + ]); - const discountedPrices = items.map( - (item) => item.price100 * (1 - item.discount100 / 100) - ); - const minPrice = discountedPrices.length > 0 ? Math.min(...discountedPrices) : 0; - const maxPrice = discountedPrices.length > 0 ? Math.max(...discountedPrices) : 1000; - const [priceRange, setPriceRange] = useState<[number, number]>([ - minPrice, - maxPrice, - ]); + // Filter aus URL übernehmen + useEffect(() => { + const params = new URLSearchParams(location.search); + const category = params.get("category"); + if (category && categoriesFilter.some((f) => f.value === category)) { + setSelectedCategory(category); + } else { + setSelectedCategory(null); + } + }, [location.search, categoriesFilter]); - // Filter aus URL übernehmen - useEffect(() => { - const params = new URLSearchParams(location.search); - const category = params.get("category"); - if (category && categoriesFilter.some((f) => f.value === category)) { - setSelectedCategory(category); - } else { - setSelectedCategory(null); - } - }, [location.search, categoriesFilter]); + // Filterfunktion bleibt gleich + const filteredItems: 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]); - // 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()) - ); - }); - }, [items, priceRange, selectedCategory, selectedRating, searchQuery]); + // Lese die Suchanfrage aus der URL + useEffect(() => { + const params = new URLSearchParams(location.search); + const query = params.get("search"); + setSearchQuery(query); + }, [location.search]); + // Container Ref + const containerRef = useRef(null); - // Lese die Suchanfrage aus der URL - useEffect(() => { - const params = new URLSearchParams(location.search); - const query = params.get("search"); - setSearchQuery(query); - }, [location.search]); + const prevItemsLength = useRef(items.length); + useEffect(() => { + if (items.length >= prevItemsLength.current) { + prevItemsLength.current = items.length; + return; + } - // Container Ref - const containerRef = useRef(null); + setTimeout(() => { + containerRef.current?.scrollTo(0, 0); + }, 50); - const prevItemsLength = useRef(items.length); + prevItemsLength.current = items.length; + }, [items]); - useEffect(() => { - if (items.length >= prevItemsLength.current) { - prevItemsLength.current = items.length; - return; - } + // Kategorie-Änderung + const handleCategoryChange = (category: string) => { + if (category === "") { + setSelectedCategory(null); + navigate(`/`); + } else { + setSelectedCategory(category); + navigate(`/?category=${encodeURIComponent(category)}`); + } + }; - setTimeout(() => { - containerRef.current?.scrollTo(0, 0); - }, 50); + // Rating-Änderung (bleibt gleich) + const handleRatingChange = (rating: string) => { + if (rating === "") { + setSelectedRating(null); + } else { + setSelectedRating(rating); + } + }; - prevItemsLength.current = items.length; - }, [items]); - - // Kategorie-Änderung - const handleCategoryChange = (category: string) => { - if (category === "") { - setSelectedCategory(null); - navigate(`/`); - } else { - setSelectedCategory(category); - navigate(`/?category=${encodeURIComponent(category)}`); - } - }; - - // Rating-Änderung (bleibt gleich) - const handleRatingChange = (rating: string) => { - if (rating === "") { - setSelectedRating(null); - } else { - setSelectedRating(rating); - } - }; - - return ( -
    -
    - - { - setPriceRange(range); - }} - /> - -
    -
    - {isLoading && t('loading')} - {!isLoading &&
    +
    + + { + setPriceRange(range); + }} + /> + +
    +
    + {isLoading && t("loading")} + {!isLoading && ( +
    + + {filteredItems.length === 0 ? ( + - - {filteredItems.length === 0 ? ( - - {t("noItemsFound")} - - ) : ( - filteredItems.map(item => ( - - )) - )} - -
    } -
    -
    - ); + {t("noItemsFound")} + + ) : ( + filteredItems.map((item) => ( + + )) + )} + + + )} +
    +
    + ); } diff --git a/01-frontend/src/pages/NoPage.tsx b/01-frontend/src/pages/NoPage.tsx index 5d97f67..45cdd7f 100644 --- a/01-frontend/src/pages/NoPage.tsx +++ b/01-frontend/src/pages/NoPage.tsx @@ -1,41 +1,40 @@ -import {Box, Button, Typography, useTheme} from "@mui/material"; -import {useNavigate} from "react-router-dom"; +import { Box, Button, Typography, useTheme } from "@mui/material"; +import { useNavigate } from "react-router-dom"; import "./pages.css"; -import {useTranslation} from "react-i18next"; +import { useTranslation } from "react-i18next"; export default function NoPage() { + const theme = useTheme(); + const { t } = useTranslation(); + const navigate = useNavigate(); - const theme = useTheme(); - const {t} = useTranslation(); - const navigate = useNavigate(); + const handleGoHome = () => { + navigate("/"); + }; - const handleGoHome = () => { - navigate("/"); - }; - - return ( - - - 404 - - - {t('pageDoesNotExist')} - - - {t('wrongTurn')} - - - - ); -} \ No newline at end of file + return ( + + + 404 + + + {t("pageDoesNotExist")} + + + {t("wrongTurn")} + + + + ); +} diff --git a/01-frontend/src/pages/Orders.tsx b/01-frontend/src/pages/Orders.tsx index 3c118ae..6b952f5 100644 --- a/01-frontend/src/pages/Orders.tsx +++ b/01-frontend/src/pages/Orders.tsx @@ -1,149 +1,185 @@ import { - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Divider, - List, - ListItemButton, - ListItemText, - Paper, - Stack, - Tab, - Tabs, - Typography + 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 { 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([]); - const {user} = useAccount(); - const [orders, setOrders] = useState([]) + const { data: accountOrders, refetch } = useQuery({ + queryKey: ["fetchOrders", user?.customerId], // Hier sollte die tatsächliche Kunden-ID verwendet werden + queryFn: () => (user ? fetchOrders(user.customerId) : Promise.resolve([])), // Simulierte API-Antwort + enabled: !!user, // Nur ausführen, wenn user existiert + retry: 3, // Versucht es 3-mal erneut + retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms) + }); - const {data: accountOrders, refetch} = useQuery({ - queryKey: ['fetchOrders', user?.customerId], // Hier sollte die tatsächliche Kunden-ID verwendet werden - queryFn: () => user ? fetchOrders(user.customerId) : Promise.resolve([]), // Simulierte API-Antwort - enabled: !!user, // Nur ausführen, wenn user existiert - retry: 3, // Versucht es 3-mal erneut - retryDelay: 1000, // Wartezeit zwischen den Versuchen (in ms) - }); + useEffect(() => { + setOrders(accountOrders ?? []); + console.log("Orders fetched:", accountOrders); + }, [accountOrders]); - useEffect(() => { - setOrders(accountOrders ?? []); - console.log("Orders fetched:", accountOrders); - }, [accountOrders]); + const { t } = useTranslation(); - const {t} = useTranslation(); + const [tab, setTab] = useState(0); + const [selectedOrder, setSelectedOrder] = useState(null); - const [tab, setTab] = useState(0); - const [selectedOrder, setSelectedOrder] = useState(null); + const activeOrders = orders.filter( + (o) => + o.status === OrderStatusEnum.ISSUES || + o.status === OrderStatusEnum.IN_PROGRESS || + o.status === OrderStatusEnum.ORDERED, + ); + const inactiveOrders = orders.filter( + (o) => + o.status === OrderStatusEnum.CANCELLED || + o.status === OrderStatusEnum.DELIVERED, + ); - const activeOrders = orders.filter(o => o.status === OrderStatusEnum.ISSUES || o.status === OrderStatusEnum.IN_PROGRESS || o.status === OrderStatusEnum.ORDERED); - const inactiveOrders = orders.filter(o => o.status === OrderStatusEnum.CANCELLED || o.status === OrderStatusEnum.DELIVERED); + const handleTabChange = (_: React.SyntheticEvent, newValue: number) => + setTab(newValue); - const handleTabChange = (_: React.SyntheticEvent, newValue: number) => setTab(newValue); + const { refetch: cancleOrder } = useQuery({ + queryKey: [ + "orderPatch", + { id: selectedOrder?.id || -1, status: OrderStatusEnum.CANCELLED }, + ], + queryFn: () => + orderPatch({ + id: selectedOrder?.id || -1, + status: OrderStatusEnum.CANCELLED, + }), + retry: 0, + retryDelay: 1000, + enabled: false, + }); + const handleCancelOrder = async () => { + await cancleOrder(); + setSelectedOrder(null); + refetch(); + }; - 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, - }); + return ( + + + + {t("myOrders")} + + + + + + + {tab === 0 ? ( + activeOrders.length > 0 ? ( + + {activeOrders.map((order) => ( + setSelectedOrder(order)} + > + + + ))} + + ) : ( + + {t("noActiveOrders")} + + ) + ) : inactiveOrders.length > 0 ? ( + + {inactiveOrders.map((order) => ( + setSelectedOrder(order)} + > + + + ))} + + ) : ( + + {t("noPreviousOrders")} + + )} - const handleCancelOrder = async () => { - await cancleOrder(); - setSelectedOrder(null); - refetch(); - }; - - return ( - - - - {t('myOrders')} + setSelectedOrder(null)} + maxWidth="sm" + fullWidth + > + + {`${selectedOrder?.status === OrderStatusEnum.IN_PROGRESS ? t("activeOrder") : t("previousOrder")} #${selectedOrder?.id}`} + + + {selectedOrder && ( + + {`${t("orderDate")}: ${new Date(selectedOrder.time).toUTCString()}`} + + + {t("orderedItems")}: - - - - - - {tab === 0 ? ( - activeOrders.length > 0 ? ( - - {activeOrders.map(order => ( - setSelectedOrder(order)}> - - - ))} - - ) : ( - {t('noActiveOrders')} - ) - ) : ( - inactiveOrders.length > 0 ? ( - - {inactiveOrders.map(order => ( - setSelectedOrder(order)}> - - - ))} - - ) : ( - {t('noPreviousOrders')} - ) - )} - - setSelectedOrder(null)} maxWidth="sm" fullWidth> - - {`${selectedOrder?.status === OrderStatusEnum.IN_PROGRESS ? t('activeOrder') : t('previousOrder')} #${selectedOrder?.id}`} - - - {selectedOrder && ( - - {`${t('orderDate')}: ${new Date(selectedOrder.time).toUTCString()}`} - - {t('orderedItems')}: - - {selectedOrder.orderItems.map((item, idx) => ( - - ))} - - - {`${t('sum')}: ${(selectedOrder.total / 100).toFixed(2)} €`} - - )} - - - {(selectedOrder?.status === OrderStatusEnum.ISSUES || selectedOrder?.status === OrderStatusEnum.IN_PROGRESS || selectedOrder?.status === OrderStatusEnum.ORDERED) && ( - - )} - - - - - - ); + + {selectedOrder.orderItems.map((item, idx) => ( + + ))} + + + {`${t("sum")}: ${(selectedOrder.total / 100).toFixed(2)} €`} + + )} + + + {(selectedOrder?.status === OrderStatusEnum.ISSUES || + selectedOrder?.status === OrderStatusEnum.IN_PROGRESS || + selectedOrder?.status === OrderStatusEnum.ORDERED) && ( + + )} + + + + + + ); } diff --git a/01-frontend/src/pages/Payment.tsx b/01-frontend/src/pages/Payment.tsx index e010eb0..b747e03 100644 --- a/01-frontend/src/pages/Payment.tsx +++ b/01-frontend/src/pages/Payment.tsx @@ -1,373 +1,403 @@ import { - Alert, - Box, - Button, - Container, - Divider, - Grid, - List, - ListItem, - ListItemText, - Paper, - Step, - StepLabel, - Stepper, - TextField, - Typography -} from '@mui/material'; -import {useMutation, useQuery} from '@tanstack/react-query'; -import {TFunction} from 'i18next'; -import React, {useEffect, useState} from 'react'; -import {useTranslation} from "react-i18next"; -import {useNavigate} from 'react-router-dom'; -import {CustomerType} from '../components/Account'; -import Item from '../components/Item'; -import OrderType, {OrderStatusEnum} from '../components/Order'; -import {useAccount} from '../helper/AccountProvider'; -import {BasketItem, useBasket} from '../helper/BasketProvider'; -import {fetchCustomer, submitCustomer, submitOrder} from '../helper/query/Queries'; + Alert, + Box, + Button, + Container, + Divider, + Grid, + List, + ListItem, + ListItemText, + Paper, + Step, + StepLabel, + Stepper, + TextField, + Typography, +} from "@mui/material"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { TFunction } from "i18next"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { CustomerType } from "../components/Account"; +import Item from "../components/Item"; +import OrderType, { OrderStatusEnum } from "../components/Order"; +import { useAccount } from "../helper/AccountProvider"; +import { BasketItem, useBasket } from "../helper/BasketProvider"; +import { + fetchCustomer, + submitCustomer, + submitOrder, +} from "../helper/query/Queries"; 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 ? ( - - {t('basketEmpty')} - - ) : ( - - {basket.map((item) => ( - - +function generateBasket( + t: TFunction<"translation", undefined>, + basket: BasketItem[], +) { + return basket.length === 0 ? ( + + {t("basketEmpty")} + + ) : ( + + {basket.map((item) => ( + + -
    - {`${(item.quantity * getDiscountedPrice(item.item)).toFixed(2)} €`}
    - {item.item.discount100 > 0 ? {-item.item.discount100}% : ""} -
    -
    - ))} -
    - ) +
    + {`${(item.quantity * getDiscountedPrice(item.item)).toFixed(2)} €`} +
    + {item.item.discount100 > 0 ? ( + {-item.item.discount100}% + ) : ( + "" + )} +
    +
    + ))} +
    + ); } -function generateTotal(t: TFunction<"translation", undefined>, basket: BasketItem[]) { - return basket.length === 0 ? "" : -
    - {t('total') + ": " + basket.map((item) => item.quantity * getDiscountedPrice(item.item)) - .reduce((prev: number, cur: number) => prev + cur, 0).toFixed(2) + ` €`} -
    +function generateTotal( + t: TFunction<"translation", undefined>, + basket: BasketItem[], +) { + return basket.length === 0 ? ( + "" + ) : ( +
    + {t("total") + + ": " + + basket + .map((item) => item.quantity * getDiscountedPrice(item.item)) + .reduce((prev: number, cur: number) => prev + cur, 0) + .toFixed(2) + + ` €`} +
    + ); } export default function Payment() { + const { t } = useTranslation(); + const { basket, clearBasket } = useBasket(); + const navigator = useNavigate(); + const [activeStep, setActiveStep] = useState(0); + const [shippingDetails, setShippingDetails] = useState({ + id: 0, // This will be set by the backend or user data + name: "", + surname: "", + address: "", + zip: "", + country: "Deutschland", + }); + const [orderNumber, setOrderNumber] = useState(null); + const steps = [ + t("reviewCart"), + t("shippingDetails"), + t("payment"), + t("orderSummary"), + ]; + const { user } = useAccount(); + const submitOrderData: OrderType = { + id: 0, // This will be set by the backend + customerId: user ? user.customerId : 0, // Use user ID if logged in, otherwise 0 + time: Date.now(), + status: OrderStatusEnum.ORDERED, // Initial status when order is placed + orderItems: basket.map((item) => ({ + id: item.item.id, + amount: item.quantity, + article: item.item.uuid, // Assuming UUID is the identifier for the item + })), + total: basket.reduce( + (total, item) => total + item.quantity * getDiscountedPrice(item.item), + 0, + ), + }; - const {t} = useTranslation(); - const {basket, clearBasket} = useBasket(); - const navigator = useNavigate(); - const [activeStep, setActiveStep] = useState(0); - const [shippingDetails, setShippingDetails] = useState({ - id: 0, // This will be set by the backend or user data - name: '', - surname: '', - address: '', - zip: '', - country: 'Deutschland', - }); - const [orderNumber, setOrderNumber] = useState(null); - const steps = [t('reviewCart'), t('shippingDetails'), t('payment'), t('orderSummary')]; - const {user} = useAccount(); + const { refetch: refetchCustomer } = useQuery({ + queryKey: ["submitCustomer", shippingDetails], + queryFn: () => submitCustomer(shippingDetails), + retry: 0, + retryDelay: 1000, + enabled: false, + }); - const submitOrderData: OrderType = { - id: 0, // This will be set by the backend - customerId: user ? user.customerId : 0, // Use user ID if logged in, otherwise 0 - time: Date.now(), - status: OrderStatusEnum.ORDERED, // Initial status when order is placed - orderItems: basket.map(item => ({ - id: item.item.id, - amount: item.quantity, - article: item.item.uuid, // Assuming UUID is the identifier for the item - })), - total: basket.reduce((total, item) => total + (item.quantity * getDiscountedPrice(item.item)), 0), - }; + const showAlert = () => { + return //TODO:; + }; - const {refetch: refetchCustomer} = useQuery({ - queryKey: ["submitCustomer", shippingDetails], - queryFn: () => submitCustomer(shippingDetails), - retry: 0, - retryDelay: 1000, - enabled: false, - }); + const { refetch: customerData } = useQuery({ + queryKey: ["fetchCustomer", user?.customerId], + queryFn: () => fetchCustomer(user?.customerId || 0), // Funktion zum Abrufen der Kundendaten + enabled: false, + }); - const showAlert = () => { - return - //TODO: - - }; - - const {refetch: customerData} = useQuery({ - 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); - } - } - }; - - fetchShippingDetails(); - }, [user, customerData]); - - - // Verwende useMutation statt useQuery für submitOrder - const {mutateAsync: submitOrderMutation} = useMutation({ - mutationFn: (orderData: OrderType) => submitOrder(orderData), - }); - - const handleNext = async () => { - let next: boolean = true; - - if (activeStep === steps.length - 2) { - // Simulate order placement and generate order number - const generatedOrderNumber = `ORD-${Math.floor(Math.random() * 1000000)}`; - setOrderNumber(generatedOrderNumber); - - let customerId = user ? user.customerId : 0; - if (!customerId) { - const customerResponse = await refetchCustomer(); - customerId = customerResponse.data.id; - } - - // Erzeuge die Orderdaten mit der richtigen customerId - const orderData: OrderType = { - ...submitOrderData, - customerId, - }; - - try { - await submitOrderMutation(orderData); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { - next = false; - } - } - if (next) { - setActiveStep((prevStep) => prevStep + 1); - } else { - showAlert(); + 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); } + } }; - const handleBack = () => { - setActiveStep((prevStep) => prevStep - 1); - }; + fetchShippingDetails(); + }, [user, customerData]); - const handleInputChange = (e: React.ChangeEvent) => { - const {name, value} = e.target; - setShippingDetails((prevDetails) => ({ - ...prevDetails, - [name]: value, - })); - }; - const handleClearBasket = () => { - clearBasket(); - }; + // Verwende useMutation statt useQuery für submitOrder + const { mutateAsync: submitOrderMutation } = useMutation({ + mutationFn: (orderData: OrderType) => submitOrder(orderData), + }); - // Hilfsfunktion prüfen, ob alle Pflichtfelder ausgefüllt sind - const isShippingDetailsValid = () => { - return ( - shippingDetails.name.trim() !== '' && - shippingDetails.surname.trim() !== '' && - shippingDetails.address.trim() !== '' && - shippingDetails.zip.trim() !== '' && - shippingDetails.country.trim() !== '' - ); - }; + const handleNext = async () => { + let next: boolean = true; - const renderStepContent = (step: number) => { - switch (step) { - case 0: - return ( - - - {t('reviewCart')} - - {generateBasket(t, basket)} - - - {generateTotal(t, basket)} - - ); - case 1: - return ( - - - {t('shippingDetails')} - - + if (activeStep === steps.length - 2) { + // Simulate order placement and generate order number + const generatedOrderNumber = `ORD-${Math.floor(Math.random() * 1000000)}`; + setOrderNumber(generatedOrderNumber); - + let customerId = user ? user.customerId : 0; + if (!customerId) { + const customerResponse = await refetchCustomer(); + customerId = customerResponse.data.id; + } - + // Erzeuge die Orderdaten mit der richtigen customerId + const orderData: OrderType = { + ...submitOrderData, + customerId, + }; - + try { + await submitOrderMutation(orderData); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + next = false; + } + } + if (next) { + setActiveStep((prevStep) => prevStep + 1); + } else { + showAlert(); + } + }; - + const handleBack = () => { + setActiveStep((prevStep) => prevStep - 1); + }; - - - - ); - case 2: - return ( - - - {t('payment')} - - - {t('paymentNotAvailable')} - - - ); - case 3: - return ( - - - {t('orderSummary')} - - - {t('thanksForOrder')} - - - {t('yourOrderNumber')}: {orderNumber} - - - {t('shippingDetails')}: - - {shippingDetails.name} {shippingDetails.surname}
    - {shippingDetails.address}
    - {shippingDetails.zip} {shippingDetails.country}
    -
    - - {t('orderedItems')}: - {generateBasket(t, basket)} - - {generateTotal(t, basket)} -
    -
    - ); - default: - return
    Unknown step
    ; - } - }; + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setShippingDetails((prevDetails) => ({ + ...prevDetails, + [name]: value, + })); + }; + const handleClearBasket = () => { + clearBasket(); + }; + // Hilfsfunktion prüfen, ob alle Pflichtfelder ausgefüllt sind + const isShippingDetailsValid = () => { return ( -
    - - - - {t('completeYourOrder')} - - - {steps.map((label) => ( - - {label} - - ))} - - {renderStepContent(activeStep)} - - - {activeStep === steps.length - 1 ? ( - - ) : ( - - )} - - - -
    + shippingDetails.name.trim() !== "" && + shippingDetails.surname.trim() !== "" && + shippingDetails.address.trim() !== "" && + shippingDetails.zip.trim() !== "" && + shippingDetails.country.trim() !== "" ); -} \ No newline at end of file + }; + + const renderStepContent = (step: number) => { + switch (step) { + case 0: + return ( + + + {t("reviewCart")} + + {generateBasket(t, basket)} + + + {generateTotal(t, basket)} + + ); + case 1: + return ( + + + {t("shippingDetails")} + + + + + + + + + + + + + + ); + case 2: + return ( + + + {t("payment")} + + {t("paymentNotAvailable")} + + ); + case 3: + return ( + + + {t("orderSummary")} + + + {t("thanksForOrder")} + + + {t("yourOrderNumber")}: {orderNumber} + + + {t("shippingDetails")}: + + {shippingDetails.name} {shippingDetails.surname} +
    + {shippingDetails.address} +
    + {shippingDetails.zip} {shippingDetails.country} +
    +
    + + {t("orderedItems")}: + {generateBasket(t, basket)} + + {generateTotal(t, basket)} +
    +
    + ); + default: + return
    Unknown step
    ; + } + }; + + return ( +
    + + + + {t("completeYourOrder")} + + + {steps.map((label) => ( + + {label} + + ))} + + {renderStepContent(activeStep)} + + + {activeStep === steps.length - 1 ? ( + + ) : ( + + )} + + + +
    + ); +} diff --git a/01-frontend/src/pages/Product.tsx b/01-frontend/src/pages/Product.tsx index 064c0d9..2cef099 100644 --- a/01-frontend/src/pages/Product.tsx +++ b/01-frontend/src/pages/Product.tsx @@ -1,92 +1,88 @@ -import {Box, Button, Container, Divider, Typography} from '@mui/material'; -import {useLocation, useNavigate} from 'react-router-dom'; -import Item from '../components/Item'; -import ProductInfo from '../helper/productpage/ProductInfo'; -import Ratings from '../helper/productpage/Ratings'; -import {useTranslation} from "react-i18next"; -import {useTheme} from '@mui/material/styles'; +import { Box, Button, Container, Divider, Typography } from "@mui/material"; +import { useLocation, useNavigate } from "react-router-dom"; +import Item from "../components/Item"; +import ProductInfo from "../helper/productpage/ProductInfo"; +import Ratings from "../helper/productpage/Ratings"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; export default function Product() { - const {t} = useTranslation(); - const location = useLocation(); - const item = location.state?.item as Item; - const navigate = useNavigate(); - const theme = useTheme(); // Zugriff auf das aktive Theme + const { t } = useTranslation(); + const location = useLocation(); + const item = location.state?.item as Item; + const navigate = useNavigate(); + const theme = useTheme(); // Zugriff auf das aktive Theme - const handleGoHome = () => { - navigate("/"); - }; - - // Wenn kein Produkt vorhanden ist - if (!item) { - return ( - - - {t('productNotFound')} - - - {t('productDoesNotExist')} - - - - ); - } + const handleGoHome = () => { + navigate("/"); + }; + // Wenn kein Produkt vorhanden ist + if (!item) { return ( - + {t("productNotFound")} + {t("productDoesNotExist")} + + ); -}; + } + + return ( + + + + + + + + + + {t("articleNumber")}: {item.uuid} + + + + {item.description} + + + + + + ); +} diff --git a/01-frontend/src/pages/pages.css b/01-frontend/src/pages/pages.css index b5270f4..faa68e1 100644 --- a/01-frontend/src/pages/pages.css +++ b/01-frontend/src/pages/pages.css @@ -1,170 +1,170 @@ .no-page-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100vh; - text-align: center; - gap: 2%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + text-align: center; + gap: 2%; } .no-page-title { - font-size: 8rem; - font-weight: bold; - margin-bottom: 16px; + font-size: 8rem; + font-weight: bold; + margin-bottom: 16px; } .no-page-subtitle { - font-size: 1.5rem; - margin-bottom: 8px; + font-size: 1.5rem; + margin-bottom: 8px; } .no-page-description { - font-size: 1rem; - margin-bottom: 24px; + font-size: 1rem; + margin-bottom: 24px; } .no-page-button { - font-size: 1rem; - padding: 12px 24px; - background-color: #0fd13f; - color: white; - border-radius: 8px; - transition: background-color 0.3s ease; + font-size: 1rem; + padding: 12px 24px; + background-color: #0fd13f; + color: white; + border-radius: 8px; + transition: background-color 0.3s ease; } .no-page-button:hover { - background-color: #0cc634; + background-color: #0cc634; } .cardgrid { - display: grid; - gap: 24px; - width: 90%; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - padding: 16px 16px 64px 16px; - margin: 0 auto; - box-sizing: border-box; + display: grid; + gap: 24px; + width: 90%; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + padding: 16px 16px 64px 16px; + margin: 0 auto; + box-sizing: border-box; } .page-background { - background-color: var(--background-color); - min-height: var(--page-height); - display: flex; - flex-direction: column; - overflow: auto; - box-sizing: border-box; - width: 100%; - color: var(--text-color); + background-color: var(--background-color); + min-height: var(--page-height); + display: flex; + flex-direction: column; + overflow: auto; + box-sizing: border-box; + width: 100%; + color: var(--text-color); } .toppad { - padding: 20px 0; + padding: 20px 0; } .page-background-center { - align-items: center; - justify-content: space-between; + align-items: center; + justify-content: space-between; } .page-background.page-background--no-space-between { - justify-content: flex-start !important; + justify-content: flex-start !important; } .page-table { - display: flex; - align-items: center; - justify-content: center; - text-align: center; - height: var(--page-height); + display: flex; + align-items: center; + justify-content: center; + text-align: center; + height: var(--page-height); } .impressum-container { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: flex-start; - padding: 20px; - text-align: left; - background-color: var(--background-color); - color: var(--text-color); - height: var(--page-height); - margin: 0; - width: 100%; - box-sizing: border-box; - overflow: auto; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + padding: 20px; + text-align: left; + background-color: var(--background-color); + color: var(--text-color); + height: var(--page-height); + margin: 0; + width: 100%; + box-sizing: border-box; + overflow: auto; } .impressum-title { - font-size: 2.5rem; - font-weight: bold; - margin-bottom: 16px; - color: var(--text-color); + font-size: 2.5rem; + font-weight: bold; + margin-bottom: 16px; + color: var(--text-color); } .impressum-content { - font-size: 1rem; - line-height: 1.6; - color: var(--text-color); + font-size: 1rem; + line-height: 1.6; + color: var(--text-color); } .contact-divider { - background-color: var(--text-color); - height: 2px; + background-color: var(--text-color); + height: 2px; } .contact-divider-box { - width: 100%; - margin: 25px 0; + width: 100%; + margin: 25px 0; } .product-page-background { - background-color: var(--background-color); - height: 100%; - min-height: 600px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - padding: 20px 0; - box-sizing: border-box; - color: var(--text-color); + background-color: var(--background-color); + height: 100%; + min-height: 600px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 20px 0; + box-sizing: border-box; + color: var(--text-color); } .home-page-background { - display: flex; - align-items: stretch; - width: 100%; - scroll-behavior: smooth; - height: 100vh; - box-sizing: border-box; - color: var(--text-color); + display: flex; + align-items: stretch; + width: 100%; + scroll-behavior: smooth; + height: 100vh; + box-sizing: border-box; + color: var(--text-color); } .sidebar { - width: fit-content; - display: grid; - place-self: start; - white-space: nowrap; - margin-top: 2vh; + width: fit-content; + display: grid; + place-self: start; + white-space: nowrap; + margin-top: 2vh; } .sidebar-filter { - margin: 30px + margin: 30px; } .no-results { - text-align: center; - font-size: 1rem; - min-width: 600px; - background-color: var(--background-color); - color: var(--text-color); + text-align: center; + font-size: 1rem; + min-width: 600px; + background-color: var(--background-color); + color: var(--text-color); } .rightBound { - float: right; + float: right; } .red { - color: #F00; + color: #f00; } diff --git a/01-frontend/src/theme/ThemeContext.tsx b/01-frontend/src/theme/ThemeContext.tsx index 91e1293..de7077e 100644 --- a/01-frontend/src/theme/ThemeContext.tsx +++ b/01-frontend/src/theme/ThemeContext.tsx @@ -1,185 +1,204 @@ -'use client'; -import React, {createContext, ReactNode, useContext, useEffect, useState} from 'react'; -import {createTheme, ThemeProvider} from '@mui/material/styles'; -import {CssBaseline, GlobalStyles} from '@mui/material'; -import useMediaQuery from '@mui/material/useMediaQuery'; +"use client"; +import React, { + createContext, + ReactNode, + useContext, + useEffect, + useState, +} from "react"; +import { createTheme, ThemeProvider } from "@mui/material/styles"; +import { CssBaseline, GlobalStyles } from "@mui/material"; +import useMediaQuery from "@mui/material/useMediaQuery"; -type ThemeMode = 'light' | 'dark'; +type ThemeMode = "light" | "dark"; interface ThemeContextType { - mode: ThemeMode; - toggleMode: () => void; + mode: ThemeMode; + toggleMode: () => void; } const ThemeContext = createContext({ - mode: 'light', - toggleMode: () => { - }, + mode: "light", + toggleMode: () => {}, }); export const useThemeMode = () => useContext(ThemeContext); interface CustomThemeProviderProps { - children: ReactNode; + children: ReactNode; } -export const CustomThemeProvider: React.FC = ({children}) => { - // SSR-sichere System-Präferenz-Erkennung - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)', {noSsr: true}); +export const CustomThemeProvider: React.FC = ({ + children, +}) => { + // SSR-sichere System-Präferenz-Erkennung + const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)", { + noSsr: true, + }); - // SSR-sichere Initialisierung - const [mode, setMode] = useState('light'); - const [mounted, setMounted] = useState(false); + // SSR-sichere Initialisierung + const [mode, setMode] = useState("light"); + const [mounted, setMounted] = useState(false); - // Nach dem ersten Render ausführen (SSR-sicher) - useEffect(() => { - setMounted(true); + // Nach dem ersten Render ausführen (SSR-sicher) + useEffect(() => { + setMounted(true); - if (typeof window !== 'undefined') { - const savedMode = localStorage.getItem('themeMode') as ThemeMode; - if (savedMode === 'light' || savedMode === 'dark') { - setMode(savedMode); - } else { - setMode(prefersDarkMode ? 'dark' : 'light'); - } - } - }, [prefersDarkMode]); - - // Mode in localStorage speichern - useEffect(() => { - if (mounted && typeof window !== 'undefined') { - localStorage.setItem('themeMode', mode); - } - }, [mode, mounted]); - - // Browser-only DOM-Manipulation - useEffect(() => { - if (mounted && typeof window !== 'undefined') { - const backgroundColor = mode === 'dark' ? '#121212' : '#fafafa'; - - // Warten bis DOM geladen ist - const updateBackground = () => { - document.documentElement.style.setProperty('--background-color', backgroundColor); - document.documentElement.style.backgroundColor = backgroundColor; - document.body.style.backgroundColor = backgroundColor; - - const root = document.getElementById('root'); - if (root) { - root.style.backgroundColor = backgroundColor; - } - }; - - // Sofort ausführen - updateBackground(); - - // Nach einem kurzen Delay nochmal (für hartnäckige Cases) - const timeoutId = setTimeout(updateBackground, 100); - - return () => clearTimeout(timeoutId); - } - }, [mode, mounted]); - - const toggleMode = () => { - setMode(prevMode => prevMode === 'light' ? 'dark' : 'light'); - }; - - // Theme basierend auf Mode erstellen - const theme = React.useMemo(() => - createTheme({ - palette: { - mode, - primary: { - main: '#0fd13f', // Grüne NavBar - light: mode === 'dark' ? '#4caf50' : '#42a5f5', - dark: mode === 'dark' ? '#388e3c' : '#1565c0', - contrastText: '#fff', - }, - secondary: { - main: mode === 'dark' ? '#bb86fc' : '#9c27b0', - light: mode === 'dark' ? '#d7aefb' : '#ba68c8', - dark: mode === 'dark' ? '#985eff' : '#7b1fa2', - contrastText: '#fff', - }, - background: { - default: mode === 'dark' ? '#121212' : '#fafafa', - paper: mode === 'dark' ? '#1e1e1e' : '#ffffff', - }, - text: { - primary: mode === 'dark' ? '#ffffff' : '#000000', - secondary: mode === 'dark' ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.6)', - }, - homepage: mode === 'dark' ? '#1e1e1e' : '#f4f4f4', - }, - components: { - MuiCssBaseline: { - styleOverrides: { - html: { - backgroundColor: `${mode === 'dark' ? '#121212' : '#fafafa'} !important`, - transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)', - }, - body: { - backgroundColor: `${mode === 'dark' ? '#121212' : '#fafafa'} !important`, - transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)', - margin: 0, - }, - }, - }, - MuiAppBar: { - styleOverrides: { - colorPrimary: { - backgroundColor: mode === 'dark' ? '#388e3c' : '#0fd13f', - color: '#ffffff', - }, - }, - }, - }, - }), - [mode] - ); - - // Aggressive GlobalStyles mit CSS-Variablen - const globalStyles = mounted ? ( - - ) : null; - - // SSR-Fallback während mounted = false - if (!mounted) { - return ( - - - {children} - - ); + if (typeof window !== "undefined") { + const savedMode = localStorage.getItem("themeMode") as ThemeMode; + if (savedMode === "light" || savedMode === "dark") { + setMode(savedMode); + } else { + setMode(prefersDarkMode ? "dark" : "light"); + } } + }, [prefersDarkMode]); + // Mode in localStorage speichern + useEffect(() => { + if (mounted && typeof window !== "undefined") { + localStorage.setItem("themeMode", mode); + } + }, [mode, mounted]); + + // Browser-only DOM-Manipulation + useEffect(() => { + if (mounted && typeof window !== "undefined") { + const backgroundColor = mode === "dark" ? "#121212" : "#fafafa"; + + // Warten bis DOM geladen ist + const updateBackground = () => { + document.documentElement.style.setProperty( + "--background-color", + backgroundColor, + ); + document.documentElement.style.backgroundColor = backgroundColor; + document.body.style.backgroundColor = backgroundColor; + + const root = document.getElementById("root"); + if (root) { + root.style.backgroundColor = backgroundColor; + } + }; + + // Sofort ausführen + updateBackground(); + + // Nach einem kurzen Delay nochmal (für hartnäckige Cases) + const timeoutId = setTimeout(updateBackground, 100); + + return () => clearTimeout(timeoutId); + } + }, [mode, mounted]); + + const toggleMode = () => { + setMode((prevMode) => (prevMode === "light" ? "dark" : "light")); + }; + + // Theme basierend auf Mode erstellen + const theme = React.useMemo( + () => + createTheme({ + palette: { + mode, + primary: { + main: "#0fd13f", // Grüne NavBar + light: mode === "dark" ? "#4caf50" : "#42a5f5", + dark: mode === "dark" ? "#388e3c" : "#1565c0", + contrastText: "#fff", + }, + secondary: { + main: mode === "dark" ? "#bb86fc" : "#9c27b0", + light: mode === "dark" ? "#d7aefb" : "#ba68c8", + dark: mode === "dark" ? "#985eff" : "#7b1fa2", + contrastText: "#fff", + }, + background: { + default: mode === "dark" ? "#121212" : "#fafafa", + paper: mode === "dark" ? "#1e1e1e" : "#ffffff", + }, + text: { + primary: mode === "dark" ? "#ffffff" : "#000000", + secondary: + mode === "dark" + ? "rgba(255, 255, 255, 0.7)" + : "rgba(0, 0, 0, 0.6)", + }, + homepage: mode === "dark" ? "#1e1e1e" : "#f4f4f4", + }, + components: { + MuiCssBaseline: { + styleOverrides: { + html: { + backgroundColor: `${mode === "dark" ? "#121212" : "#fafafa"} !important`, + transition: + "background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + body: { + backgroundColor: `${mode === "dark" ? "#121212" : "#fafafa"} !important`, + transition: + "background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)", + margin: 0, + }, + }, + }, + MuiAppBar: { + styleOverrides: { + colorPrimary: { + backgroundColor: mode === "dark" ? "#388e3c" : "#0fd13f", + color: "#ffffff", + }, + }, + }, + }, + }), + [mode], + ); + + // Aggressive GlobalStyles mit CSS-Variablen + const globalStyles = mounted ? ( + + ) : null; + + // SSR-Fallback während mounted = false + if (!mounted) { return ( - - - - {globalStyles} - {children} - - + + + {children} + ); + } + + return ( + + + + {globalStyles} + {children} + + + ); }; diff --git a/01-frontend/src/theme/ThemeToggle.tsx b/01-frontend/src/theme/ThemeToggle.tsx index ba1b71e..b28360b 100644 --- a/01-frontend/src/theme/ThemeToggle.tsx +++ b/01-frontend/src/theme/ThemeToggle.tsx @@ -1,31 +1,30 @@ // theme/ThemeToggle.tsx -import React from 'react'; -import {IconButton, Tooltip} from '@mui/material'; -import {Brightness4, Brightness7} from '@mui/icons-material'; -import {useThemeMode} from './ThemeContext'; -import {useTranslation} from "react-i18next"; +import React from "react"; +import { IconButton, Tooltip } from "@mui/material"; +import { Brightness4, Brightness7 } from "@mui/icons-material"; +import { useThemeMode } from "./ThemeContext"; +import { useTranslation } from "react-i18next"; const ThemeToggle: React.FC = () => { - const {mode, toggleMode} = useThemeMode(); - const {t} = useTranslation(); + const { mode, toggleMode } = useThemeMode(); + const { t } = useTranslation(); - return ( - - - - {mode === 'dark' ? : } - - - ); + return ( + + + {mode === "dark" ? : } + + + ); }; export default ThemeToggle; diff --git a/01-frontend/src/theme/theme-augmentation.d.ts b/01-frontend/src/theme/theme-augmentation.d.ts index 54f2f10..646201b 100644 --- a/01-frontend/src/theme/theme-augmentation.d.ts +++ b/01-frontend/src/theme/theme-augmentation.d.ts @@ -1,13 +1,13 @@ -import '@mui/material/styles'; +import "@mui/material/styles"; -declare module '@mui/material/styles' { - interface Palette { - tertiary: Palette['primary']; - homepage: string; - } +declare module "@mui/material/styles" { + interface Palette { + tertiary: Palette["primary"]; + homepage: string; + } - interface PaletteOptions { - tertiary?: PaletteOptions['primary']; - homepage?: string; - } + interface PaletteOptions { + tertiary?: PaletteOptions["primary"]; + homepage?: string; + } } diff --git a/01-frontend/src/theme/theme.ts b/01-frontend/src/theme/theme.ts index 94e571c..88b01cb 100644 --- a/01-frontend/src/theme/theme.ts +++ b/01-frontend/src/theme/theme.ts @@ -1,206 +1,206 @@ -import {createTheme} from '@mui/material/styles'; -import './theme-augmentation.d.ts'; // Falls vorhanden +import { createTheme } from "@mui/material/styles"; +import "./theme-augmentation.d.ts"; // Falls vorhanden export const darkmode = createTheme({ - palette: { - primary: { - main: '#0fd13f', // Grüne Standardfarbe - light: '#42a5f5', - dark: '#15650', - contrastText: '#fff', - }, - secondary: { - main: '#9c27b0', - light: '#ba68c8', - dark: '#7b1fa2', - contrastText: '#fff', - }, - error: { - main: '#f44336', - light: '#e57373', - dark: '#d32f2f', - contrastText: '#fff', - }, - warning: { - main: '#ed6c02', - light: '#ff9800', - dark: '#e65100', - contrastText: '#fff', - }, - info: { - main: '#0288d1', - light: '#03a9f4', - dark: '#01579b', - contrastText: '#fff', - }, - success: { - main: '#2e7d32', - light: '#4caf50', - dark: '#1b5e20', - contrastText: '#fff', - }, - background: { - default: '#fafafa', - paper: '#ffffff', - }, - text: { - primary: '#000000', - secondary: 'rgba(0, 0, 0, 0.6)', - }, + palette: { + primary: { + main: "#0fd13f", // Grüne Standardfarbe + light: "#42a5f5", + dark: "#15650", + contrastText: "#fff", }, - // Sanfte Übergänge - transitions: { - duration: { - shortest: 150, - shorter: 200, - short: 250, - standard: 300, - complex: 375, - enteringScreen: 225, - leavingScreen: 195, - }, - easing: { - easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)', - easeOut: 'cubic-bezier(0.0, 0, 0.2, 1)', - easeIn: 'cubic-bezier(0.4, 0, 1, 1)', - sharp: 'cubic-bezier(0.4, 0, 0.6, 1)', - }, + secondary: { + main: "#9c27b0", + light: "#ba68c8", + dark: "#7b1fa2", + contrastText: "#fff", }, - // Verbesserte Komponenten-Overrides - components: { - MuiCssBaseline: { - styleOverrides: { - body: { - backgroundColor: '#fafafa', - transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)', - }, - html: { - backgroundColor: '#fafafa', - transition: 'background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)', - }, - }, - }, - MuiAppBar: { - styleOverrides: { - colorPrimary: { - backgroundColor: '#0fd13f', // Grüne NavBar - color: '#ffffff', - }, - }, - }, - MuiButton: { - styleOverrides: { - root: { - borderRadius: 8, - textTransform: 'none', // Entfernt automatische Großschreibung - fontWeight: 600, - }, - contained: { - boxShadow: '0 2px 8px rgba(0,0,0,0.1)', - '&:hover': { - boxShadow: '0 4px 12px rgba(0,0,0,0.15)', - }, - }, - }, - }, - MuiCard: { - styleOverrides: { - root: { - borderRadius: 12, - boxShadow: '0 2px 12px rgba(0,0,0,0.08)', - transition: 'box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1)', - '&:hover': { - boxShadow: '0 4px 20px rgba(0,0,0,0.12)', - }, - }, - }, - }, - MuiTextField: { - styleOverrides: { - root: { - '& .MuiOutlinedInput-root': { - borderRadius: 8, - }, - }, - }, - }, - MuiChip: { - styleOverrides: { - root: { - borderRadius: 16, - }, - }, - }, - MuiPaper: { - styleOverrides: { - root: { - borderRadius: 8, - }, - elevation1: { - boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)', - }, - elevation2: { - boxShadow: '0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)', - }, - }, - }, + error: { + main: "#f44336", + light: "#e57373", + dark: "#d32f2f", + contrastText: "#fff", }, - // Benutzerdefinierte Typografie - typography: { - fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', - h1: { - fontWeight: 700, - fontSize: '2.5rem', - lineHeight: 1.2, - }, - h2: { - fontWeight: 600, - fontSize: '2rem', - lineHeight: 1.3, - }, - h3: { - fontWeight: 600, - fontSize: '1.75rem', - lineHeight: 1.3, - }, - h4: { - fontWeight: 600, - fontSize: '1.5rem', - lineHeight: 1.4, - }, - h5: { - fontWeight: 600, - fontSize: '1.25rem', - lineHeight: 1.4, - }, - h6: { - fontWeight: 600, - fontSize: '1rem', - lineHeight: 1.4, - }, - body1: { - fontSize: '1rem', - lineHeight: 1.5, - }, - body2: { - fontSize: '0.875rem', - lineHeight: 1.4, - }, - button: { - fontWeight: 600, - textTransform: 'none', - }, + warning: { + main: "#ed6c02", + light: "#ff9800", + dark: "#e65100", + contrastText: "#fff", }, - // Benutzerdefinierte Breakpoints - breakpoints: { - values: { - xs: 0, - sm: 600, - md: 960, - lg: 1280, - xl: 1920, - }, + info: { + main: "#0288d1", + light: "#03a9f4", + dark: "#01579b", + contrastText: "#fff", }, - // Angepasste Spacing-Funktion - spacing: 8, // 8px als Basis-Einheit + success: { + main: "#2e7d32", + light: "#4caf50", + dark: "#1b5e20", + contrastText: "#fff", + }, + background: { + default: "#fafafa", + paper: "#ffffff", + }, + text: { + primary: "#000000", + secondary: "rgba(0, 0, 0, 0.6)", + }, + }, + // Sanfte Übergänge + transitions: { + duration: { + shortest: 150, + shorter: 200, + short: 250, + standard: 300, + complex: 375, + enteringScreen: 225, + leavingScreen: 195, + }, + easing: { + easeInOut: "cubic-bezier(0.4, 0, 0.2, 1)", + easeOut: "cubic-bezier(0.0, 0, 0.2, 1)", + easeIn: "cubic-bezier(0.4, 0, 1, 1)", + sharp: "cubic-bezier(0.4, 0, 0.6, 1)", + }, + }, + // Verbesserte Komponenten-Overrides + components: { + MuiCssBaseline: { + styleOverrides: { + body: { + backgroundColor: "#fafafa", + transition: "background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + html: { + backgroundColor: "#fafafa", + transition: "background-color 300ms cubic-bezier(0.4, 0, 0.2, 1)", + }, + }, + }, + MuiAppBar: { + styleOverrides: { + colorPrimary: { + backgroundColor: "#0fd13f", // Grüne NavBar + color: "#ffffff", + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + borderRadius: 8, + textTransform: "none", // Entfernt automatische Großschreibung + fontWeight: 600, + }, + contained: { + boxShadow: "0 2px 8px rgba(0,0,0,0.1)", + "&:hover": { + boxShadow: "0 4px 12px rgba(0,0,0,0.15)", + }, + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + borderRadius: 12, + boxShadow: "0 2px 12px rgba(0,0,0,0.08)", + transition: "box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1)", + "&:hover": { + boxShadow: "0 4px 20px rgba(0,0,0,0.12)", + }, + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + "& .MuiOutlinedInput-root": { + borderRadius: 8, + }, + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + borderRadius: 16, + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + borderRadius: 8, + }, + elevation1: { + boxShadow: "0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)", + }, + elevation2: { + boxShadow: "0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)", + }, + }, + }, + }, + // Benutzerdefinierte Typografie + typography: { + fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + h1: { + fontWeight: 700, + fontSize: "2.5rem", + lineHeight: 1.2, + }, + h2: { + fontWeight: 600, + fontSize: "2rem", + lineHeight: 1.3, + }, + h3: { + fontWeight: 600, + fontSize: "1.75rem", + lineHeight: 1.3, + }, + h4: { + fontWeight: 600, + fontSize: "1.5rem", + lineHeight: 1.4, + }, + h5: { + fontWeight: 600, + fontSize: "1.25rem", + lineHeight: 1.4, + }, + h6: { + fontWeight: 600, + fontSize: "1rem", + lineHeight: 1.4, + }, + body1: { + fontSize: "1rem", + lineHeight: 1.5, + }, + body2: { + fontSize: "0.875rem", + lineHeight: 1.4, + }, + button: { + fontWeight: 600, + textTransform: "none", + }, + }, + // Benutzerdefinierte Breakpoints + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 960, + lg: 1280, + xl: 1920, + }, + }, + // Angepasste Spacing-Funktion + spacing: 8, // 8px als Basis-Einheit }); diff --git a/01-frontend/src/util/ColorUtil.tsx b/01-frontend/src/util/ColorUtil.tsx index 836c170..f2dc8e2 100644 --- a/01-frontend/src/util/ColorUtil.tsx +++ b/01-frontend/src/util/ColorUtil.tsx @@ -1,58 +1,66 @@ -export function mapValueToColor(minVal: number, maxVal: number, actualVal: number): string { - const clamped = Math.min(Math.max(actualVal, minVal), maxVal); +export function mapValueToColor( + minVal: number, + maxVal: number, + actualVal: number, +): string { + const clamped = Math.min(Math.max(actualVal, minVal), maxVal); - // Calculate interpolation ratio (0-1) - const ratio = maxVal !== minVal - ? (clamped - minVal) / (maxVal - minVal) - : 0; - return hsvDegToHex(120 * ratio);//120° is green, 0° is red + // Calculate interpolation ratio (0-1) + const ratio = maxVal !== minVal ? (clamped - minVal) / (maxVal - minVal) : 0; + return hsvDegToHex(120 * ratio); //120° is green, 0° is red } export function getColorFromPercent(percent: string): string { - let perc = Number(percent) / 100 - if (perc > 1) { - perc = 2.5; - } - return hsvDegToHex(120 * perc); + let perc = Number(percent) / 100; + if (perc > 1) { + perc = 2.5; + } + return hsvDegToHex(120 * perc); } export function hsvDegToHex(h: number): string { - h = Math.max(0, Math.min(360, h)); + h = Math.max(0, Math.min(360, h)); - const c = 1; - const x = (1 - Math.abs(((h / 60) % 2) - 1)); + 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; - } else if (h < 120) { - r = x; - g = c; - b = 0; - } else if (h < 180) { - r = 0; - g = c; - b = x; - } else if (h < 240) { - r = 0; - g = x; - b = c; - } else if (h < 300) { - r = x; - g = 0; - b = c; - } else { - r = c; - g = 0; - b = x; - } + let r, g, b; + if (h < 60) { + r = c; + g = x; + b = 0; + } else if (h < 120) { + r = x; + g = c; + b = 0; + } else if (h < 180) { + r = 0; + g = c; + b = x; + } else if (h < 240) { + r = 0; + g = x; + b = c; + } else if (h < 300) { + r = x; + g = 0; + b = c; + } else { + 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'); + // 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}`; + return `#${rHex}${gHex}${bHex}`; }