wpfat23-5 2 ani în urmă
părinte
comite
17d458862a

Fișier diff suprimat deoarece este prea mare
+ 574 - 201
package-lock.json


+ 10 - 0
package.json

@@ -6,8 +6,18 @@
     "@testing-library/jest-dom": "^5.16.5",
     "@testing-library/react": "^13.4.0",
     "@testing-library/user-event": "^13.5.0",
+    "i18next": "^23.4.4",
+    "jwt-decode": "^3.1.2",
+    "moment": "^2.29.4",
+    "primeflex": "^3.3.0",
+    "primeicons": "^6.0.1",
+    "primereact": "^9.4.0",
     "react": "^18.2.0",
+    "react-calendar": "^4.3.0",
     "react-dom": "^18.2.0",
+    "react-google-recaptcha": "^3.1.0",
+    "react-i18next": "^13.1.2",
+    "react-moment": "^1.1.3",
     "react-router-dom": "^6.11.2",
     "react-scripts": "5.0.1",
     "web-vitals": "^2.1.4"

BIN
public/images/profile.png


BIN
public/images/rentabike.png


BIN
public/images/road-bike.png


+ 1 - 0
public/index.html

@@ -25,6 +25,7 @@
       Learn how to configure a non-root public URL by running `npm run build`.
     -->
     <title>React App</title>
+    <script src="https://www.recaptcha.net/recaptcha/api.js" async defer></script>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>

+ 4 - 0
src/MainRouter.js

@@ -2,6 +2,8 @@ import {BrowserRouter, Route, Routes} from "react-router-dom";
 import LoginPage from "./pages/LoginPage";
 import HomePage from "./pages/HomePage";
 import React, {useEffect, useState} from "react";
+import CreditsPage from "./pages/CreditsPage";
+import SettingsPage from "./pages/SettingsPage";
 
 export default function (props) {
 
@@ -12,6 +14,8 @@ export default function (props) {
         <Routes>
             <Route path="/" element={<LoginPage />}/>
             <Route path="/home" element={<HomePage />}/>
+            <Route path="/credits" element={<CreditsPage />} />
+            <Route path="/settings" element={<SettingsPage />} />
             {/*<Route path="blogs" element={<Blogs />} />*/}
             {/*<Route path="contact" element={<Contact />} />*/}
             {/*<Route path="*" element={<NoPage />} />*/}

+ 24 - 0
src/Utils.js

@@ -0,0 +1,24 @@
+import {SPRING_SERVER} from "./config";
+
+export default function authorizedFetch(url, body = undefined) {
+    if (!body) {
+        return fetch(url, {
+            headers: {
+                "Authorization": "Bearer " + localStorage.getItem("access_token"),
+            }
+        })
+            .then(response => response.json())
+            .catch(error => console.error(error));
+    } else {
+        return fetch(url, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+                "Authorization": "Bearer " + localStorage.getItem("access_token"),
+            },
+            body: JSON.stringify(body)
+        })
+            .then(response => response.json())
+            .catch(error => console.error(error));
+    }
+}

+ 26 - 0
src/components/Address.js

@@ -0,0 +1,26 @@
+import {InputText} from "primereact/inputtext";
+import {useEffect, useState} from "react";
+import {Button} from "primereact/button";
+import authorizedFetch from "../Utils";
+import {SPRING_SERVER} from "../config";
+import {useTranslation} from "react-i18next";
+
+export default function (props) {
+    const [address, setAddress] = useState("")
+    const { t } = useTranslation(); // Initialize the hook
+
+    useEffect(() => {
+        authorizedFetch(SPRING_SERVER + "/api/getAddress/" + props.userData.login).then(data => setAddress(data))
+    },[])
+
+    function setAddressClick() {
+        authorizedFetch(SPRING_SERVER + "/api/setAddress/" + props.userData.login + "/" + address)
+    }
+
+    return (<div className="shadow-4 flex flex-column p-2 justify-content-between h-full">
+        <div>{t('address')}:</div>
+        <InputText className="mt-2" value={address} placeholder="Address"
+                   onChange={(e) => setAddress(e.target.value)}/>
+        <Button onClick={setAddressClick} className="mt-2">{t('setAddress')}</Button>
+    </div>)
+}

+ 54 - 0
src/components/BikesTable.js

@@ -0,0 +1,54 @@
+import {Image} from 'primereact/image';
+import {Badge} from "primereact/badge";
+import Moment from "react-moment";
+import {Button} from "primereact/button";
+import "../css/scrollbar.css"
+import {useState} from "react";
+import {useTranslation} from "react-i18next";
+
+export default function (props) {
+    const [selectedBikeId, setSelectedBikeId] = useState(null);
+    const { t } = useTranslation(); // Initialize the hook
+
+    let bike_items = props.bikes.map((bike) => {
+            const isSelected = bike.id === selectedBikeId;
+            const handleSelect = (bike) => {
+                setSelectedBikeId(bike.id);
+                props.callback(bike);
+            };
+
+            if (bike.user) return (
+                    <div key={bike.id} className={`flex flex-column p-2 m-1 shadow-5 w-3 flex-grow-1 `}
+                         style={{border: "solid #E5E5E5 1px "}}>
+                        <div><Badge value={bike.id} severity="info"></Badge></div>
+                        <Image className="align-self-center" src={"images/road-bike.png"} alt="Image" width="50px"/>
+
+                        <div>{bike.user.login}</div>
+                        <Moment format="YYYY/MM/DD">{bike.rentedFrom}</Moment>
+                        <Moment format="YYYY/MM/DD">{bike.rentedTo}</Moment>
+                    </div>
+
+                )
+                else return (
+                    <div key={bike.id} className={`flex  flex-column p-2 m-1 shadow-5 w-3 flex-grow-1 ${ isSelected ? "bg-green-200" : "" } `} style={{border: "solid #E5E5E5 1px"}}>
+                        <div className="flex flex-row justify-content-between"><Badge value={bike.id}
+                                                                                      severity="info"></Badge><Badge
+                            value={bike.price + "$"}/></div>
+                        <Image className="align-self-center" src={"images/road-bike.png"} alt="Image" width="50px"/>
+
+                        <Badge value={t('available')} severity="success"></Badge>
+                        <Button className="mt-2" onClick={() => handleSelect(bike)}>
+                            {t('select')}
+                        </Button>
+                    </div>
+                )
+            }
+        )
+    ;
+
+    return (<div className="flex flex-row flex-wrap w-6 flex-grow-0 shadow-6 m-2"
+                 style={{height: "80%", overflow: "auto", border: "solid #E5E5E5 1px", scrollbarWidth: "none"}}>
+            {bike_items}
+        </div>
+    )
+}

+ 26 - 0
src/components/ChangePassword.js

@@ -0,0 +1,26 @@
+import {useEffect, useState} from "react";
+import {InputText} from "primereact/inputtext";
+import {Button} from "primereact/button";
+import authorizedFetch from "../Utils";
+import {SPRING_SERVER} from "../config";
+import {useTranslation} from "react-i18next";
+
+export default function (props) {
+    const [newPassword, setNewPassword] = useState("")
+    const [repeatPassword, setRepeatPassword] = useState("")
+    const { t } = useTranslation(); // Initialize the hook
+
+
+    function changePasswordClick() {
+        if (newPassword === repeatPassword)
+            authorizedFetch(SPRING_SERVER + "/api/setPassword/" + props.userData.login + "/" + newPassword)
+    }
+
+    return (
+        <div className="shadow-4 flex flex-column p-2">
+            <InputText className="" value={newPassword} placeholder={t("chgPass")} onChange={(e) => setNewPassword(e.target.value)}/>
+            <InputText className="mt-2" value={repeatPassword} placeholder={t("repPass")} onChange={(e) => setRepeatPassword(e.target.value)}/>
+            <Button onClick={changePasswordClick} className="mt-2">{t("chgPass")}</Button>
+        </div>
+    )
+}

+ 30 - 0
src/components/CreateNewUser.js

@@ -0,0 +1,30 @@
+import {useState} from "react";
+import {InputText} from "primereact/inputtext";
+import {Button} from "primereact/button";
+import {Dropdown} from "primereact/dropdown";
+import authorizedFetch from "../Utils";
+import {SPRING_SERVER} from "../config";
+import {useTranslation} from "react-i18next";
+
+export default function (props) {
+    const [login, setLogin] = useState("")
+    const [password, setPassword] = useState("")
+    const [email, setEmail] = useState("")
+    const { t } = useTranslation(); // Initialize the hook
+
+
+    function registerUser() {
+        authorizedFetch(SPRING_SERVER + "/register", {login: login, password: password, email: email})
+    }
+
+    return (
+        <div className="shadow-4 flex flex-column p-2 h-full justify-content-around">
+            <InputText className="" value={login} placeholder={t("login")} onChange={(e) => setLogin(e.target.value)}/>
+            <InputText className="mt-2" value={password} placeholder={t("password")}
+                       onChange={(e) => setPassword(e.target.value)}/>
+            <InputText className="mt-2" value={email} placeholder="email"
+                       onChange={(e) => setEmail(e.target.value)}/>
+            <Button onClick={registerUser} className="mt-2">{t("addUser")}</Button>
+        </div>
+    )
+}

+ 27 - 0
src/components/NewBike.js

@@ -0,0 +1,27 @@
+import {useState} from "react";
+import {Slider} from "primereact/slider";
+import {InputText} from "primereact/inputtext";
+import {Button} from "primereact/button";
+import authorizedFetch from "../Utils";
+import {SPRING_SERVER} from "../config";
+import {useTranslation} from "react-i18next";
+
+export default function (props) {
+    const [value, setValue] = useState(0);
+    const { t } = useTranslation(); // Initialize the hook
+
+    function createNewBike() {
+        authorizedFetch(SPRING_SERVER + "/api/createNewBike/" + value)
+    }
+
+    return(
+        <div className="shadow-4 flex flex-column p-2">
+
+            <div >{t("addNewBike")}</div>
+            <div className="font-italic">{t("price")} $</div>
+            <InputText value={value} onChange={(e) => setValue(e.target.value)} />
+            <Slider value={value} onChange={(e) => setValue(e.value)} />
+            <Button onClick={createNewBike} className="mt-3">{t("add")} </Button>
+        </div>
+    )
+}

+ 14 - 0
src/components/Roles.js

@@ -0,0 +1,14 @@
+import {Badge} from "primereact/badge";
+import {useTranslation} from "react-i18next";
+
+export default function (props) {
+    const { t } = useTranslation(); // Initialize the hook
+
+    let elements = props?.userData?.roles.map((role) => {
+        return (<Badge key={Math.random()} severity="info" className={"m-1"} value={role.roleName}></Badge>)
+    })
+
+    return (<div className="flex flex-row">
+        {elements}
+    </div>)
+}

+ 108 - 0
src/components/SelectedBike.js

@@ -0,0 +1,108 @@
+import {Badge} from "primereact/badge";
+import Moment from "react-moment";
+import {Button} from "primereact/button";
+import {SPRING_SERVER} from "../config";
+import {useEffect, useState} from "react";
+import authorizedFetch from "../Utils";
+import {useTranslation} from "react-i18next";
+
+export default function (props) {
+    const [pdfGeneratorVisible, setPdfGeneratorVisible] = useState(false);
+    const [payVisible, setPayVisible] = useState(false);
+    const { t } = useTranslation(); // Initialize the hook
+
+    useEffect(() => {
+        console.log("rerender")
+        setPayVisible(false)
+        setPdfGeneratorVisible(false)
+    }, [props.selectedBike.id]);
+    function rentMethod() {
+        const url = SPRING_SERVER + '/api/rentABike/' + props.user + "/" + props.selectedBike.id;
+        fetch(url, {
+            method: "POST", headers: {
+                'Accept': 'application/json',
+                'Content-Type': 'application/json',
+                "Authorization": "Bearer " + localStorage.getItem("access_token"),
+            }, body: JSON.stringify({
+                "rentedFrom": props.selectedDate[0],
+                "rentedTo": props.selectedDate[1],
+                "price": props.selectedBike.price
+            })
+        }).then(() => {
+            props.callback()
+        }).then(() => {
+            setPayVisible(true)
+        })
+    }
+
+    function generateInvoice() {
+        const pdfEndpoint = SPRING_SERVER + "/api/download-pdf/" + props.selectedBike.id; // Replace with your PDF endpoint URL
+
+        fetch(pdfEndpoint, {
+            method: 'GET', headers: {
+                "Authorization": "Bearer " + localStorage.getItem("access_token"),
+            }
+        })
+            .then(response => {
+                if (!response.ok) {
+                    throw new Error('Network response was not ok');
+                }
+                return response.blob();
+            })
+            .then(blob => {
+                // Create a temporary URL for the blob
+                const url = URL.createObjectURL(blob);
+
+                // Create an anchor element to trigger the download
+                const a = document.createElement('a');
+                a.href = url;
+                a.download = 'document.pdf'; // Change the filename if needed
+
+                // Programmatically trigger the download
+                document.body.appendChild(a);
+                a.click();
+
+                // Clean up: remove the anchor and revoke the URL
+                document.body.removeChild(a);
+                URL.revokeObjectURL(url);
+            }).then(() => {
+            setPdfGeneratorVisible(false)
+        })
+            .catch(error => {
+                console.error('Fetch error:', error);
+            });
+
+    }
+
+    function payForABike() {
+        authorizedFetch(SPRING_SERVER + "/api/payForABike/" + props.selectedBike.id)
+            .then((boolean) => {
+                if (boolean) setPdfGeneratorVisible(true)
+                else {
+                    setPayVisible(false)
+                    //show notification that it's too late for payment
+                }
+            })
+    }
+
+    return (
+
+
+    <div className="p-2 surface-200 shadow-8 w-12 flex flex-column" style={{border: "1px solid #cfcfcf"}}>
+        <div className="flex flex-row"><Badge value={props.selectedBike.id} severity="info"/></div>
+        <div>{t("days")}: {Math.ceil((props.selectedDate[1] - props.selectedDate[0]) / (1000 * 3600 * 24))}</div>
+        <div>{t("price")}: {Math.ceil((props.selectedDate[1] - props.selectedDate[0]) / (1000 * 3600 * 24)) * props.selectedBike.price}$</div>
+        <div className={"flex flex-row"}><Button onClick={() => {
+            rentMethod()
+        }} className="mt-2 flex-grow-1">{t('rent')}</Button>{payVisible ? <Button severity={"danger"} onClick={() => {
+            payForABike()
+        }} className="mt-2 ml-2">{t('pay')}</Button> : <div></div>}</div>
+        {pdfGeneratorVisible ? <Button severity={"warning"} onClick={() => {
+            generateInvoice()
+        }} className="mt-2">{t('invoice')}</Button> : <div></div>}
+
+    </div>
+
+
+    )
+}

+ 57 - 0
src/components/UsersList.js

@@ -0,0 +1,57 @@
+import {useEffect, useState} from "react";
+import {InputText} from "primereact/inputtext";
+import {Button} from "primereact/button";
+import {Dropdown} from "primereact/dropdown";
+import authorizedFetch from "../Utils";
+import {SPRING_SERVER} from "../config";
+import {Badge} from "primereact/badge";
+import {SelectButton} from "primereact/selectbutton";
+import {useTranslation} from "react-i18next";
+
+export default function (props) {
+    const [users, setUsers] = useState()
+    const { t } = useTranslation(); // Initialize the hook
+
+    function arrayDifference(arr1, arr2) {
+        if (arr1.length === 0) {
+            return arr2.slice();
+        }
+        if (arr2.length === 0) {
+            return arr1.slice();
+        }
+        let result = arr1.filter(item => !arr2.includes(item));
+        if (result.length === 0) return arr2.filter(item => !arr1.includes(item))
+        else return result
+    }
+
+    useEffect(() => {
+        authorizedFetch(SPRING_SERVER + "/api/getUserData")
+            .then((data) => {
+                setUsers(data)
+            })
+    }, []);
+
+    return (<div className="overflow-auto flex flex-column shadow-4 surface-200 col-12 h-10rem">
+        {users?.map((user) => {
+            return <div key={user.id}
+                        className="flex flex-row  border-300 border-bottom-1 justify-content-between align-items-center">
+                <Badge className="mr-2" value={user.login}></Badge>
+                <SelectButton value={user.roles.filter(item => item.roleName !== "User").map(item => item.roleName)}
+                              onChange={(e) => {
+                                  console.log(user.roles.filter(item => item.roleName !== "User").map(item => item.roleName))
+                                  console.log(e.value)
+                                  console.log(arrayDifference(e.value, user.roles.filter(item => item.roleName !== "User").map(item => item.roleName)))
+                                  authorizedFetch(SPRING_SERVER + "/api/toggleRole/" + user.login + "/" + arrayDifference(e.value, user.roles.filter(item => item.roleName !== "User").map(item => item.roleName)))
+                                      .then(() => {
+                                          authorizedFetch(SPRING_SERVER + "/api/getUserData")
+                                              .then((data) => {
+                                                  setUsers(data)
+                                              })
+                                      })
+                              }} options={["Manager", "Admin"]}
+                              multiple/>
+
+            </div>
+        })}
+    </div>)
+}

+ 143 - 0
src/css/Calendar.css

@@ -0,0 +1,143 @@
+.react-calendar {
+    width: 450px;
+    max-width: 100%;
+    background: white;
+    border: 1px solid #e0e0e0;
+    font-family: Arial, Helvetica, sans-serif;
+    line-height: 1.125em;
+}
+
+.react-calendar--doubleView {
+    width: 700px;
+}
+
+.react-calendar--doubleView .react-calendar__viewContainer {
+    display: flex;
+    margin: -0.5em;
+}
+
+.react-calendar--doubleView .react-calendar__viewContainer > * {
+    width: 50%;
+    margin: 0.5em;
+}
+
+.react-calendar,
+.react-calendar *,
+.react-calendar *:before,
+.react-calendar *:after {
+    -moz-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+}
+
+.react-calendar button {
+    margin: 0;
+    border: 0;
+    outline: none;
+}
+
+.react-calendar button:enabled:hover {
+    cursor: pointer;
+}
+
+.react-calendar__navigation {
+    display: flex;
+    height: 44px;
+    margin-bottom: 1em;
+}
+
+.react-calendar__navigation button {
+    min-width: 44px;
+    background: none;
+}
+
+.react-calendar__navigation button:disabled {
+    background-color: #f0f0f0;
+}
+
+.react-calendar__navigation button:enabled:hover,
+.react-calendar__navigation button:enabled:focus {
+    background-color: #e6e6e6;
+}
+
+.react-calendar__month-view__weekdays {
+    text-align: center;
+    text-transform: uppercase;
+    font-weight: bold;
+    font-size: 0.75em;
+}
+
+.react-calendar__month-view__weekdays__weekday {
+    padding: 0.5em;
+}
+
+.react-calendar__month-view__weekNumbers .react-calendar__tile {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 0.75em;
+    font-weight: bold;
+}
+
+.react-calendar__month-view__days__day--weekend {
+    color: #d10000;
+}
+
+.react-calendar__month-view__days__day--neighboringMonth {
+    color: #757575;
+}
+
+.react-calendar__year-view .react-calendar__tile,
+.react-calendar__decade-view .react-calendar__tile,
+.react-calendar__century-view .react-calendar__tile {
+    padding: 2em 0.5em;
+}
+
+.react-calendar__tile {
+    max-width: 100%;
+    padding: 10px 6.6667px;
+    background: none;
+    text-align: center;
+    line-height: 16px;
+}
+
+.react-calendar__tile:disabled {
+    background-color: #f0f0f0;
+}
+
+.react-calendar__tile:enabled:hover,
+.react-calendar__tile:enabled:focus {
+    background-color: #e6e6e6;
+}
+
+.react-calendar__tile--now {
+    background: rgba(123, 255, 118, 0.51);
+}
+
+.react-calendar__tile--now:enabled:hover,
+.react-calendar__tile--now:enabled:focus {
+    background: rgba(175, 255, 169, 0.48);
+}
+
+.react-calendar__tile--hasActive {
+    background: #76baff;
+}
+
+.react-calendar__tile--hasActive:enabled:hover,
+.react-calendar__tile--hasActive:enabled:focus {
+    background: #a9d4ff;
+}
+
+.react-calendar__tile--active {
+    background: #006edc;
+    color: white;
+}
+
+.react-calendar__tile--active:enabled:hover,
+.react-calendar__tile--active:enabled:focus {
+    background: #1087ff;
+}
+
+.react-calendar--selectRange .react-calendar__tile--hover {
+    background-color: #e6e6e6;
+}

+ 14 - 0
src/css/scrollbar.css

@@ -0,0 +1,14 @@
+*::-webkit-scrollbar {
+    width: 10px;
+}
+
+*::-webkit-scrollbar-track {
+    background: #b2b2b2;
+    border-left: 5px solid #c7c7c7;
+    border-right: 5px solid #c7c7c7;
+}
+
+*::-webkit-scrollbar-thumb {
+    background-color: #2ea400;
+    border-radius: 10px;
+}

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
src/css/theme.css


+ 27 - 0
src/index.js

@@ -4,8 +4,35 @@ import './index.css';
 import App from './App';
 import reportWebVitals from './reportWebVitals';
 import MainRouter from "./MainRouter";
+import i18n from 'i18next';
+import { initReactI18next } from 'react-i18next';
+import enTranslation from './locales/en.json';
+import plTranslation from './locales/pl.json';
+
+import "/node_modules/primeflex/primeflex.css";
+import 'primeicons/primeicons.css';
+
+//theme
+import "primereact/resources/themes/saga-green/theme.css";
+//core
+import "primereact/resources/primereact.min.css";
 
 const root = ReactDOM.createRoot(document.getElementById('root'));
+i18n.use(initReactI18next).init({
+    resources: {
+        en: {
+            translation: enTranslation,
+        },
+        pl: {
+            translation: plTranslation,
+        },
+    },
+    lng: 'en', // Default language
+    interpolation: {
+        escapeValue: false, // React already escapes variables
+    },
+});
+
 root.render(
   <React.StrictMode>
     <MainRouter/>

+ 31 - 0
src/locales/en.json

@@ -0,0 +1,31 @@
+{
+  "settings": "Settings",
+  "available": "Available",
+  "login": "Login",
+  "username": "Username",
+  "register": "Register",
+  "password": "Password",
+  "logout": "Logout",
+  "select": "Select",
+  "user": "User",
+  "manager": "Manager",
+  "admin": "Admin",
+  "newPass": "New Password",
+  "repPass": "Repeat Password",
+  "chgPass": "Change Password",
+  "address": "Address",
+  "setAddress": "Set Address",
+  "addNewBike": "Add new bike",
+  "price": "Price",
+  "add": "Add",
+  "addUser": "Add user",
+  "days": "Days",
+  "rent": "Rent",
+  "pay": "Pay for a bike",
+  "invoice": "Generate Invoice",
+  "lpanel": "Login Panel",
+  "rpanel": "Register Panel",
+  "ldesc": "Login using your credentials",
+  "rdesc": "Register your new account",
+  "log-in" : "Login"
+}

+ 31 - 0
src/locales/pl.json

@@ -0,0 +1,31 @@
+{
+  "settings": "Ustawienia",
+  "available": "Dostępny",
+  "login": "Login",
+  "username": "Użytkownik",
+  "register": "Zarejestruj",
+  "password": "Hasło",
+  "logout": "Wyloguj",
+  "select": "Wybierz",
+  "user": "Użytkownik",
+  "manager": "Menadżer",
+  "admin": "Administrator",
+  "newPass": "Nowe hasło",
+  "repPass": "Powtórz hasło",
+  "chgPass": "Zmień hasło",
+  "address": "Adres",
+  "setAddress": "Ustaw adres",
+  "addNewBike": "Dodaj nowy rower",
+  "price": "Cena",
+  "add": "Dodaj",
+  "addUser": "Dodaj użytkownika",
+  "days": "Dni",
+  "rent": "Wynajmij",
+  "pay": "Zapłać za rower",
+  "invoice": "Wygeneruj fakturę",
+  "lpanel": "Login Panel",
+  "rpanel": "Register Panel",
+  "ldesc": "Login using your credentials",
+  "rdesc": "Register your new account",
+  "log-in" : "Zaloguj"
+}

+ 5 - 0
src/pages/CreditsPage.js

@@ -0,0 +1,5 @@
+export default function () {
+    return(<div>
+        <a href="https://www.flaticon.com/" title="icons">This website has been designed using images from Flaticon.com</a>
+    </div>)
+}

+ 121 - 2
src/pages/HomePage.js

@@ -1,10 +1,129 @@
+import Calendar from 'react-calendar';
+import {useEffect, useState} from "react";
+import BikesTable from "../components/BikesTable";
+import {Image} from 'primereact/image';
+import SelectedBike from "../components/SelectedBike";
+import '../css/Calendar.css';
+import {Navigate, useNavigate} from "react-router-dom";
+import jwt_decode from "jwt-decode";
+import {Button} from "primereact/button";
+import {SPRING_SERVER} from "../config";
+import Roles from "../components/Roles";
+import {useTranslation} from "react-i18next";
+import i18n from "i18next";
 
 export default function () {
+    const [date, setDate] = useState();
+    const [selectedBike, setSelectedBike] = useState();
+    const [selectedDate, setSelectedDate] = useState();
+    const [userData, setUserData] = useState();
+    const [username, setUsername] = useState();
+    const [selectedBikeSeed, setSelectedBikeSeed] = useState();
+    const [bikesData, setBikesData] = useState();
+    const navigate = useNavigate();
+    const {t} = useTranslation();
 
+    if (!localStorage.getItem("access_token") || !localStorage.getItem("expires_at")) {
+        localStorage.removeItem("access_token")
+        localStorage.removeItem("expires_at")
+        return <Navigate to="/" replace/>
+    } else {
+        if (parseInt(localStorage.getItem("expires_at")) < Date.now()) {
+            localStorage.removeItem("access_token")
+            localStorage.removeItem("expires_at")
+
+            return <Navigate to="/" replace/>
+        }
+    }
+    const decoded = jwt_decode(localStorage.getItem("access_token"));
+
+    useEffect(() => {
+        const url = SPRING_SERVER + '/api/getAllBikes';
+        fetch(url, {
+            headers: {
+                "Authorization": "Bearer " + localStorage.getItem("access_token"),
+            }
+        })
+            .then(response => response.json())
+            .then(data => {
+                console.log(data)
+                setBikesData(data)
+            })
+            .catch(error => console.error(error));
+    }, [selectedBikeSeed])
+
+    useEffect(() => {
+        const url = SPRING_SERVER + '/api/getUserData/' + decoded.sub;
+        fetch(url, {
+            headers: {
+                "Authorization": "Bearer " + localStorage.getItem("access_token"),
+            }
+        })
+            .then(response => response.json())
+            .then(data => {
+                setUserData(data)
+                console.log(data)
+            })
+            .catch(error => console.error(error));
+    }, [])
+
+
+    useEffect(() => {
+        console.log("Bearer " + localStorage.getItem("access_token"))
+        setUsername(decoded.sub)
+    }, []);
+
+    const onButtonClickLogOut = () => {
+        localStorage.removeItem("access_token")
+        return navigate("/")
+    }
+    const changeLanguage = (lng) => {
+        i18n.changeLanguage(lng);
+    };
     return (
         < >
-            <div>
-                hello
+            <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh'}}
+                 className="surface-200 flex flex-column">
+                <div className="mb-1 flex flex-row justify-content-between align-items-center surface-0 shadow-3"
+                     style={{width: "90%"}}>
+                    <div><Image src="/images/rentabike.png" width="50%"/></div>
+                    <div className="flex flex-row justify-content-center align-items-center">
+                        <div className={"mr-2"}>
+
+                                <Button severity={"warning"} onClick={() => changeLanguage('en')}>EN</Button></div>
+                            <div><Button severity={"warning"} onClick={() => changeLanguage('pl')}>PL</Button></div>
+                            <Roles userData={userData}></Roles>
+                        <div onClick={() => {
+                            return navigate("/settings")
+                        }}
+                             className="p-1 flex flex-row align-items-center justify-content-center surface-300 border-round-xl mr-4"
+                             style={{border: "solid #d1d1d1 1px", height: "50%", "cursor": "pointer"}}>
+                            <Image
+                                src="/images/profile.png" width="50px"></Image>
+                            <div className="text-2xl font-bold p-1">{username}</div>
+                        </div>
+                        <Button className="mr-3" onClick={onButtonClickLogOut}>{t('logout')}</Button>
+                    </div>
+                </div>
+                <div
+                    className="surface-0 shadow-5 flex flex-row justify-content-around align-items-center overflow-hidden "
+                    style={{height: "80%", width: "90%"}}>
+                    {bikesData ? <BikesTable
+                        bikes={bikesData} callback={(selected) => {
+                        setSelectedBike(selected)
+                    }}></BikesTable> : <></>}
+                    <div className="flex flex-column justify-content-around align-items-center flex-wrap "
+                         style={{height: "100%"}}>
+                        <Calendar className="mb-2 shadow-6" id="minmax" value={date}
+                                  onChange={(e) => setSelectedDate(e)}
+                                  selectRange={true}
+                                  inline minDate={new Date()}/>
+                        {(selectedBike && selectedDate) ?
+                            <SelectedBike callback={() => {
+                                setSelectedBikeSeed(Math.random())
+                            }} user={username} selectedBike={selectedBike} selectedDate={selectedDate}/> : <></>}
+                    </div>
+                </div>
             </div>
         </>
     )

+ 132 - 5
src/pages/LoginPage.js

@@ -1,8 +1,135 @@
+import {Button} from "primereact/button";
+import {InputText} from "primereact/inputtext";
+import {Navigate, useNavigate} from "react-router-dom";
+import {useEffect, useRef, useState} from "react";
+import {Toast} from "primereact/toast";
+import {SPRING_SERVER} from "../config";
+import jwt_decode from "jwt-decode";
+import authorizedFetch from "../Utils";
+import {useTranslation} from "react-i18next";
+import {ReCAPTCHA} from "react-google-recaptcha";
+
 export default function () {
+    const navigate = useNavigate();
+    const [login, setLogin] = useState("");
+    const [password, setPassword] = useState("");
+    const [login2, setLogin2] = useState("");
+    const [password2, setPassword2] = useState("");
+    const [email, setEmail] = useState("");
+    const [recaptchaValue, setRecaptchaValue] = useState('');
+
+    const handleRecaptchaChange = (value) => {
+        setRecaptchaValue(value);
+    };
+    useEffect(() => {
+        window.handleRecaptchaChange = handleRecaptchaChange;
+    })
+
+    const my_toast = useRef(null)
+    const { t } = useTranslation(); // Initialize the hook
+
+    if (localStorage.getItem("access_token")) {
+        return <Navigate to="/home" replace/>
+    }
+
+    function onLoginClick() {
+        fetch(SPRING_SERVER + '/login', {
+            method: 'POST', headers: {
+                'Content-Type': 'application/json'
+            }, body: JSON.stringify({
+                'login': login, 'password': password,'captcha': recaptchaValue,
+
+            })
+        })
+            .then(response => {
+                if (!response.ok) {
+                    my_toast.current.show({severity: 'error', summary: 'Error Message', detail: 'Validation failed'});
+                    throw new Error(response.status)
+                } else {
+                    return response.json()
+                }
+            })
+            .then((data) => {
+                var decoded = jwt_decode(data);
+                console.log(decoded)
+                localStorage.setItem("access_token", data)
+                localStorage.setItem("expires_at", (parseInt(decoded.exp) * 1000).toString())
+            })
+            .then(() => {
+                if (localStorage.getItem("access_token")) return navigate("/home")
+            })
+            .catch((e) => console.log(e))
+
+
+    }
+
+    function onRegisterClick() {
+        fetch(SPRING_SERVER + '/register', {
+            method: 'POST', headers: {
+                'Content-Type': 'application/json'
+            }, body: JSON.stringify({
+                'login': login2, 'password': password2, 'email': email
+            })
+        }).then(response => {
+            if (!response.ok)
+                my_toast.current.show({severity: 'error', summary: 'Error Message', detail: 'Validation failed'});
+            else
+                my_toast.current.show({severity: 'success', summary: 'Success', detail: 'Account created'});
+
+        })
+    }
+
+    return (<div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh'}}
+                 className="bg-blue-500 flex flex-row">
+
+
+
+            <Toast ref={my_toast}></Toast>
+
+            <div className="surface-card p-4 shadow-2 border-round flex flex-column w-3 mx-2">
+                <div className="text-3xl font-medium text-900 mb-2">{t('lpanel')}</div>
+                <div className="font-medium text-500 mb-3">{t('ldesc')}</div>
+
+                <span className="p-input-icon-left py-1 ">
+                        <i className="pi pi-user"/>
+                        <InputText placeholder={t('username')} onChange={(e) => setLogin(e.target.value)}
+                                   className="min-w-full max-w-full"/>
+                </span>
+                <span className="p-input-icon-left py-1">
+                        <i className="pi pi-key"/>
+                        <InputText type="password" placeholder={t('password')} onChange={(e) => setPassword(e.target.value)}
+                                   className="min-w-full max-w-full"/>
+                </span>
+                <div className="g-recaptcha" data-sitekey="SITE_KEY" data-callback="handleRecaptchaChange"></div>
+
+                <div className="flex justify-content-end pt-2">
+                    <Button onClick={onLoginClick}>{t('login')}</Button>
+                </div>
+
+            </div>
+            <div className="surface-card p-4 shadow-2 border-round flex flex-column w-3 mx-2">
+                <div className="text-3xl font-medium text-900 mb-2">{t('rpanel')}</div>
+                <div className="font-medium text-500 mb-3">{t('rdesc')}</div>
+
+                <span className="p-input-icon-left py-1 ">
+                        <i className="pi pi-user"/>
+                        <InputText placeholder={t('username')} onChange={(e) => setLogin2(e.target.value)}
+                                   className="min-w-full max-w-full"/>
+                </span>
+                <span className="p-input-icon-left py-1 ">
+                        <i className="pi pi-envelope"/>
+                        <InputText placeholder="Email" onChange={(e) => setEmail(e.target.value)}
+                                   className="min-w-full max-w-full"/>
+                </span>
+                <span className="p-input-icon-left py-1">
+                        <i className="pi pi-key"/>
+                        <InputText type="password" placeholder={t('password')} onChange={(e) => setPassword2(e.target.value)}
+                                   className="min-w-full max-w-full"/>
+                </span>
+                <div className="flex justify-content-end pt-2">
+                    <Button onClick={onRegisterClick}>{t('register')}</Button>
+                </div>
 
-    return (
-        <div>
-            hello
-        </div>
-    )
+            </div>
+        </div>)
 }

+ 133 - 0
src/pages/SettingsPage.js

@@ -0,0 +1,133 @@
+import Calendar from 'react-calendar';
+import {useEffect, useState} from "react";
+import BikesTable from "../components/BikesTable";
+import {Image} from 'primereact/image';
+import SelectedBike from "../components/SelectedBike";
+import '../css/Calendar.css';
+import {Navigate, useNavigate} from "react-router-dom";
+import jwt_decode from "jwt-decode";
+import {Button} from "primereact/button";
+import {SPRING_SERVER} from "../config";
+import Roles from "../components/Roles";
+import NewBike from "../components/NewBike";
+import ChangePassword from "../components/ChangePassword";
+import CreateNewUser from "../components/CreateNewUser";
+import UsersList from "../components/UsersList";
+import Address from "../components/Address";
+import {useTranslation} from "react-i18next";
+import i18n from "i18next";
+
+
+export default function () {
+    const [userData, setUserData] = useState();
+    const [username, setUsername] = useState();
+    const navigate = useNavigate();
+    const { t } = useTranslation(); // Initialize the hook
+
+
+    if (!localStorage.getItem("access_token") || !localStorage.getItem("expires_at")) {
+        localStorage.removeItem("access_token")
+        localStorage.removeItem("expires_at")
+        return <Navigate to="/" replace/>
+    } else {
+        if (parseInt(localStorage.getItem("expires_at")) < Date.now()) {
+            localStorage.removeItem("access_token")
+            localStorage.removeItem("expires_at")
+
+            return <Navigate to="/" replace/>
+        }
+    }
+    const decoded = jwt_decode(localStorage.getItem("access_token"));
+
+
+    useEffect(() => {
+        const url = SPRING_SERVER + '/api/getUserData/' + decoded.sub;
+        fetch(url, {
+            headers: {
+                "Authorization": "Bearer " + localStorage.getItem("access_token"),
+            }
+        })
+            .then(response => response.json())
+            .then(data => {
+                setUserData(data)
+            })
+            .catch(error => console.error(error));
+    }, [])
+
+
+    useEffect(() => {
+        console.log("Bearer " + localStorage.getItem("access_token"))
+        setUsername(decoded.sub)
+    }, []);
+
+    const onButtonClickLogOut = () => {
+        localStorage.removeItem("access_token")
+        return navigate("/")
+    }
+
+    function checkUserRole(rolesArray, roleName) {
+        return rolesArray?.some(role => role.roleName === roleName);
+    }
+    const changeLanguage = (lng) => {
+        i18n.changeLanguage(lng);
+    };
+    return (
+        < >
+            <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh'}}
+                 className="surface-200 flex flex-column">
+                <div className="mb-1 flex flex-row justify-content-between align-items-center surface-0 shadow-3"
+                     style={{width: "90%"}}>
+                    <div style={{"cursor": "pointer"}} onClick={() => {
+                        return navigate("/")
+                    }}><Image src="/images/rentabike.png" width="50%"/></div>
+                    <div className="flex flex-row justify-content-center align-items-center">
+
+                            <div className={"mr-1"}><Button severity={"warning"} onClick={() => changeLanguage('en')}>EN</Button></div>
+                            <div><Button severity={"warning"} onClick={() => changeLanguage('pl')}>PL</Button></div>
+
+                        <Roles userData={userData}></Roles>
+                        <div onClick={() => {
+                            console.log("a")
+                        }}
+                             className="p-1 flex flex-row align-items-center justify-content-center surface-300 border-round-xl mr-4"
+                             style={{border: "solid #d1d1d1 1px", height: "50%", "cursor": "pointer"}}>
+                            <Image
+                                src="/images/profile.png" width="50px"></Image>
+                            <div className="text-2xl font-bold p-1">{username}</div>
+                        </div>
+                        <Button className="mr-3" onClick={onButtonClickLogOut}>{t("logout")}</Button>
+                    </div>
+                </div>
+                <div
+                    className="surface-0 shadow-5 flex flex-column overflow-auto"
+                    style={{height: "80%", width: "90%"}}>
+                    {checkUserRole(userData?.roles, "User") ?
+                        <div className="flex flex-column m-2 surface-100 p-2">
+                            <div className="bg-blue-100 align-self-start border-round mb-2 p-1">{t('user')}</div>
+
+                            <div className="flex flex-row">
+                                <div className="mr-2"><ChangePassword userData={userData}></ChangePassword></div>
+                                <div><Address userData={userData}></Address></div>
+                            </div>
+                        </div> : <></>}
+                    {checkUserRole(userData?.roles, "Manager") ?
+                        <div className="flex flex-column m-2 surface-100 p-2">
+                            <div className="bg-blue-100 align-self-start border-round mb-2 p-1">{t('manager')}</div>
+                            <div className="flex flex-row align-content-center align-items-center">
+                                <div><NewBike></NewBike></div>
+                            </div>
+                        </div> : <></>}
+                    {checkUserRole(userData?.roles, "Admin") ?
+                        <div className="flex flex-column m-2 surface-100 p-2">
+                            <div className="bg-blue-100 align-self-start border-round mb-2 p-1">{t('admin')}</div>
+
+                            <div className="flex flex-row ">
+                                <div><CreateNewUser></CreateNewUser></div>
+                                <div className="w-full"><UsersList></UsersList></div>
+                            </div>
+                        </div> : <></>}
+                </div>
+            </div>
+        </>
+    )
+}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff