consolidate all repos to one for archive
This commit is contained in:
62
projektna_naloga/frontend/.gitignore
vendored
Normal file
62
projektna_naloga/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.DS_STORE
|
||||
node_modules
|
||||
scripts/flow/*/.flowconfig
|
||||
.flowconfig
|
||||
*~
|
||||
*.pyc
|
||||
.grunt
|
||||
_SpecRunner.html
|
||||
__benchmarks__
|
||||
build/
|
||||
remote-repo/
|
||||
coverage/
|
||||
.module-cache
|
||||
fixtures/dom/public/react-dom.js
|
||||
fixtures/dom/public/react.js
|
||||
test/the-files-to-test.generated.js
|
||||
*.log*
|
||||
chrome-user-data
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.idea
|
||||
*.iml
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
packages/react-devtools-core/dist
|
||||
packages/react-devtools-extensions/chrome/build
|
||||
packages/react-devtools-extensions/chrome/*.crx
|
||||
packages/react-devtools-extensions/chrome/*.pem
|
||||
packages/react-devtools-extensions/firefox/build
|
||||
packages/react-devtools-extensions/firefox/*.xpi
|
||||
packages/react-devtools-extensions/firefox/*.pem
|
||||
packages/react-devtools-extensions/shared/build
|
||||
packages/react-devtools-extensions/.tempUserDataDir
|
||||
packages/react-devtools-inline/dist
|
||||
packages/react-devtools-shell/dist
|
||||
packages/react-devtools-timeline/dist
|
3
projektna_naloga/frontend/README.md
Normal file
3
projektna_naloga/frontend/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Frontend of Highway analysis website
|
||||
|
||||
The website uses React framework to develope the frontend.
|
17630
projektna_naloga/frontend/package-lock.json
generated
Normal file
17630
projektna_naloga/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
projektna_naloga/frontend/package.json
Normal file
45
projektna_naloga/frontend/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"bootstrap": "^5.2.3",
|
||||
"chart.js": "^4.3.0",
|
||||
"leaflet": "^1.9.3",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.7.4",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-router-dom": "^6.11.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
BIN
projektna_naloga/frontend/public/favicon.ico
Normal file
BIN
projektna_naloga/frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
47
projektna_naloga/frontend/public/index.html
Normal file
47
projektna_naloga/frontend/public/index.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
|
||||
|
||||
<title>Highway Tracker</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
BIN
projektna_naloga/frontend/public/logo192.png
Normal file
BIN
projektna_naloga/frontend/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
projektna_naloga/frontend/public/logo512.png
Normal file
BIN
projektna_naloga/frontend/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
projektna_naloga/frontend/public/manifest.json
Normal file
25
projektna_naloga/frontend/public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
3
projektna_naloga/frontend/public/robots.txt
Normal file
3
projektna_naloga/frontend/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
51
projektna_naloga/frontend/src/App.js
Normal file
51
projektna_naloga/frontend/src/App.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { UserContext } from "./userContext";
|
||||
import { ReactDOM } from 'react';
|
||||
import Header from './components/Header';
|
||||
import MapComponent from './components/Map';
|
||||
import Login from './components/Login';
|
||||
import Register from './components/Register';
|
||||
import Logout from './components/Logout';
|
||||
import Profile from './components/Profile';
|
||||
import Saved from './components/Saved';
|
||||
import Detail from './components/Detail';
|
||||
|
||||
|
||||
function App() {
|
||||
const [user, setUser] = useState(localStorage.user ? JSON.parse(localStorage.user) : null);
|
||||
const updateUserData = (userInfo) => {
|
||||
localStorage.setItem("user", JSON.stringify(userInfo));
|
||||
setUser(userInfo);
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<UserContext.Provider value={{
|
||||
user: user,
|
||||
setUserContext: updateUserData
|
||||
}}>
|
||||
<div className="App" style={{ backgroundImage: 'url("https://cdn.wallpapersafari.com/93/52/fRFtzX.jpg")', backgroundRepeat: "no-repeat", backgroundSize: "cover", minHeight: "100vh" }}>
|
||||
<Header title="Highway Tracker"></Header>
|
||||
<Routes>
|
||||
<Route path="/" exact element={<MapComponent />}></Route>
|
||||
{/* <Route path="/login" exact element={<Login />}></Route>
|
||||
<Route path="/register" element={<Register />}></Route>
|
||||
<Route path="/save" element={<Saved />}></Route>
|
||||
<Route path="/profile" element={<Profile />}></Route>
|
||||
<Route path="/logout" element={<Logout />}></Route> */}
|
||||
<Route path="/details/:id" element={<Detail />}></Route>
|
||||
</Routes>
|
||||
</div>
|
||||
<footer className="footer mt-auto py-3 bg-dark fixed-bottom">
|
||||
<div className="container text-center">
|
||||
<span style={{ color: "grey" }}>Highway Tracker - All rights reserved</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</UserContext.Provider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
BIN
projektna_naloga/frontend/src/assets/icon.png
Normal file
BIN
projektna_naloga/frontend/src/assets/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
13
projektna_naloga/frontend/src/components/BarChart.js
Normal file
13
projektna_naloga/frontend/src/components/BarChart.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { Bar } from "react-chartjs-2";
|
||||
import {Chart as ChartJS} from "chart.js/auto"
|
||||
|
||||
function BarChart({chartData}) {
|
||||
|
||||
// options = {} can be added as a second parameter to Bar to customize the chart
|
||||
return (
|
||||
<Bar data={chartData}/>
|
||||
);
|
||||
}
|
||||
|
||||
export default BarChart;
|
38
projektna_naloga/frontend/src/components/Boxes.js
Normal file
38
projektna_naloga/frontend/src/components/Boxes.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Card, Container, Row, Col } from 'react-bootstrap';
|
||||
|
||||
const Boxes = () => {
|
||||
return (
|
||||
<div style={{ marginTop: "5px" }}>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col className="align-items-center">
|
||||
<Card className="text-center">
|
||||
<Card.Body className="bg-success">
|
||||
</Card.Body>
|
||||
<span><b>LOW</b></span>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col className="align-items-center">
|
||||
<Card className="text-center">
|
||||
<Card.Body className="bg-warning">
|
||||
</Card.Body>
|
||||
<span><b>MEDIUM</b></span>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col className="align-items-center">
|
||||
<Card className="text-center">
|
||||
<Card.Body className="bg-danger">
|
||||
</Card.Body>
|
||||
<span><b>HIGH</b></span>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Boxes;
|
189
projektna_naloga/frontend/src/components/Detail.js
Normal file
189
projektna_naloga/frontend/src/components/Detail.js
Normal file
@@ -0,0 +1,189 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from 'react-router-dom';
|
||||
import BarChart from "./BarChart.js";
|
||||
import { Card } from 'react-bootstrap';
|
||||
import { Spinner } from 'react-bootstrap';
|
||||
import { UserContext } from '../userContext';
|
||||
import { MapContainer, TileLayer, Circle } from 'react-leaflet';
|
||||
import Boxes from "./Boxes.js";
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
|
||||
function Detail(param) {
|
||||
const [carAmount, setCarAmount] = useState([]);
|
||||
const [carData, setCarData] = useState([]);
|
||||
const [lastHourCarAmount, setLastHourCarAmount] = useState(0);
|
||||
// Possible states for dataChange: "day", "month", "year"
|
||||
const [dataChange, setDataChange] = useState("day");
|
||||
const { id } = useParams();
|
||||
const [cardTitle, setCardTitle] = useState("Today");
|
||||
const [locationData, setLocationData] = useState({});
|
||||
const [coordinates, setCoordinates] = React.useState([46.05730669203195, 14.504340106047238]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const getCarAmount = async () => {
|
||||
var response = new Response();
|
||||
var data = [];
|
||||
var label = [];
|
||||
|
||||
// Getting the current date (year, month, day)
|
||||
const currentData = new Date();
|
||||
const currentYear = currentData.getFullYear();
|
||||
const currentMonth = currentData.getMonth() + 1;
|
||||
const currentDay = currentData.getDate();
|
||||
|
||||
|
||||
if (dataChange === "day") {
|
||||
// The date variables are then used for the fetch request
|
||||
response = await fetch(`/api/data/year/${currentYear}/month/${currentMonth}/day/${currentDay}/location/${id}`);
|
||||
data = await response.json();
|
||||
label = data.map((data) => data.hour + ":00")
|
||||
setLastHourCarAmount(data[data.length - 1].car_count);
|
||||
setCardTitle("Today");
|
||||
}
|
||||
else if (dataChange === "month") {
|
||||
response = await fetch(`/api/data/year/${currentYear}/month/${currentMonth}/location/${id}`);
|
||||
data = await response.json();
|
||||
label = data.map((data) => data.day + "");
|
||||
setCardTitle("Last 30 days");
|
||||
}
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
setCarAmount(data);
|
||||
setCarData({
|
||||
labels: label,
|
||||
datasets: [{
|
||||
label: "Car Amount",
|
||||
data: data.map((data) => data.car_count),
|
||||
backgroundColor: ["green", "red", "blue"],
|
||||
borderColor: ["black"],
|
||||
borderWidth: 2
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const getLocationData = async () => {
|
||||
const response = await fetch(`/api/location/${id}`);
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
setLocationData(data);
|
||||
setCoordinates([data.cord_N, data.cord_E]);
|
||||
}
|
||||
}
|
||||
|
||||
getCarAmount();
|
||||
getLocationData();
|
||||
}, [id, dataChange]);
|
||||
|
||||
const SaveLocation = async () => {
|
||||
const response = await fetch(`/api/users/addLocation/${locationData.location_id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
if (response.status === 400) {
|
||||
const data = await response.json();
|
||||
if (data.message === 'Location already added') {
|
||||
alert('Location already saved');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const StatusCheck = (amount) => {
|
||||
if (amount <= 100) {
|
||||
return 'green';
|
||||
} else if (amount <= 250) {
|
||||
return 'yellow';
|
||||
} else {
|
||||
return 'red';
|
||||
}
|
||||
};
|
||||
|
||||
if (Object.keys(carData).length === 0 || Object.keys(locationData).length === 0) {
|
||||
return (
|
||||
<div className="d-flex justify-content-center align-items-center" style={{ height: '100vh' }}>
|
||||
<Spinner animation="border" role="status" variant="success" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<h2 style={{ color: 'white' }} className="d-flex justify-content-center">Location Details</h2>
|
||||
|
||||
{/* The bar chart is displayed here */}
|
||||
<div className="row">
|
||||
<div className="col-lg">
|
||||
<Card style={{ width: '100%' }}>
|
||||
<Card.Body>
|
||||
<Card.Title>{cardTitle}</Card.Title>
|
||||
<div className="d-flex justify-content-center">
|
||||
<BarChart chartData={carData} />
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-center mt-4">
|
||||
<button className="btn btn-primary mx-2" onClick={() => setDataChange('day')}>
|
||||
Day
|
||||
</button>
|
||||
<button className="btn btn-primary mx-2" onClick={() => setDataChange('month')}>
|
||||
Month
|
||||
</button>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* The location details are displayed here */}
|
||||
<div className="col-lg">
|
||||
<Card style={{ width: '100%' }}>
|
||||
<Card.Body>
|
||||
<h4>Location: {locationData.name}</h4>
|
||||
<div style={{ display: "flex", justifyContent: "center" }}>
|
||||
<img src={locationData.img_urls[0]} className="img-fluid rounded" style={{ border: "5px solid black" }} alt="Camera view" />
|
||||
</div>
|
||||
|
||||
{/* Only show the button if the user is logged in */}
|
||||
<UserContext.Consumer>
|
||||
{context => (
|
||||
context.user ?
|
||||
<div style={{ display: "flex", justifyContent: "center", marginTop: "5px" }}>
|
||||
<button className="btn btn-primary mx-2" onClick={() => SaveLocation()}>Save location</button>
|
||||
</div>
|
||||
:
|
||||
<></>
|
||||
)}
|
||||
</UserContext.Consumer>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* The map is displayed here */}
|
||||
<div className="col-lg">
|
||||
<Card style={{ width: '100%' }}>
|
||||
<Card.Body>
|
||||
<h4>Last hour traffic status</h4>
|
||||
<MapContainer center={coordinates} zoom={16} scrollWheelZoom={false} zoomControl={false} style={{ height: "50vh" }}>
|
||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
||||
|
||||
<Circle
|
||||
key={locationData._id}
|
||||
center={[locationData.cord_N, locationData.cord_E]}
|
||||
pathOptions={{ color: StatusCheck(lastHourCarAmount), fillColor: StatusCheck(lastHourCarAmount), fillOpacity: 0.5 }}
|
||||
radius={100}
|
||||
>
|
||||
</Circle>
|
||||
|
||||
</MapContainer>
|
||||
<Boxes />
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default Detail;
|
62
projektna_naloga/frontend/src/components/Header.js
Normal file
62
projektna_naloga/frontend/src/components/Header.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useContext } from "react";
|
||||
import { UserContext } from "../userContext";
|
||||
import { Link } from "react-router-dom";
|
||||
import icon from "../assets/icon.png";
|
||||
|
||||
function Header(props) {
|
||||
return (
|
||||
<header className="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div className="container">
|
||||
<Link to="/" className="navbar-brand d-flex align-items-center">
|
||||
<img src={icon} width="30" height="30" className="mr-2" alt="" />
|
||||
<span>{props.title}</span>
|
||||
</Link>
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div className="collapse navbar-collapse" id="navbarNav">
|
||||
<ul className="navbar-nav ml-auto">
|
||||
<li className="nav-item">
|
||||
<Link to="/" className="nav-link">Map</Link>
|
||||
</li>
|
||||
{/* <UserContext.Consumer>
|
||||
{context => (
|
||||
context.user ?
|
||||
<>
|
||||
<li className="nav-item">
|
||||
<Link to="/save" className="nav-link">Saved</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link to="/profile" className="nav-link">Profile</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link to="/logout" className="nav-link">Logout</Link>
|
||||
</li>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<li className="nav-item">
|
||||
<Link to="/login" className="nav-link">Login</Link>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<Link to="/register" className="nav-link">Register</Link>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</UserContext.Consumer> */}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
74
projektna_naloga/frontend/src/components/Login.js
Normal file
74
projektna_naloga/frontend/src/components/Login.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useContext, useState } from 'react';
|
||||
import { UserContext } from '../userContext';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
function Login() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const userContext = useContext(UserContext);
|
||||
|
||||
async function Login(e) {
|
||||
e.preventDefault();
|
||||
const res = await fetch("/api/users/login", {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data._id !== undefined) {
|
||||
userContext.setUserContext(data);
|
||||
} else {
|
||||
setUsername("");
|
||||
setPassword("");
|
||||
setError("Invalid username or password");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="container mt-5" onSubmit={Login}>
|
||||
{userContext.user ? <Navigate replace to="/" /> : ""}
|
||||
<div className="mb-3">
|
||||
<label htmlFor="username" className="form-label" style={{ color: "white" }}>Username</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="password" className="form-label" style={{ color: "white" }}>Password</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
name="submit"
|
||||
value="Log in"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label>{error}</label>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
20
projektna_naloga/frontend/src/components/Logout.js
Normal file
20
projektna_naloga/frontend/src/components/Logout.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useEffect, useContext } from 'react';
|
||||
import { UserContext } from '../userContext';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
function Logout() {
|
||||
const userContext = useContext(UserContext);
|
||||
useEffect(function () {
|
||||
const logout = async function () {
|
||||
userContext.setUserContext(null);
|
||||
const res = await fetch("/api/users/logout");
|
||||
}
|
||||
logout();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Navigate replace to="/" />
|
||||
);
|
||||
}
|
||||
|
||||
export default Logout;
|
117
projektna_naloga/frontend/src/components/Map.js
Normal file
117
projektna_naloga/frontend/src/components/Map.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { MapContainer, TileLayer, Circle } from 'react-leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { Spinner } from 'react-bootstrap';
|
||||
import SideView from './SideView';
|
||||
|
||||
function MapComponent() {
|
||||
const [coordinates, setCoordinates] = React.useState([46.05730669203195, 14.504340106047238]);
|
||||
const [locations, setLocations] = React.useState([]);
|
||||
const [carAmount, setCarAmount] = React.useState([]);
|
||||
|
||||
const [isClicked, setIsClicked] = React.useState(false);
|
||||
const [clickedCircleData, setClickedCircleData] = React.useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const getLocations = async () => {
|
||||
const response = await fetch('/api/location');
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
setLocations(data);
|
||||
}
|
||||
}
|
||||
getLocations();
|
||||
|
||||
const getCarAmount = async () => {
|
||||
// Getting the current date (year, month, day)
|
||||
var currentDate = new Date();
|
||||
currentDate.setTime(currentDate.getTime() - 60 * 60 * 1000);
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const currentMonth = currentDate.getMonth() + 1;
|
||||
const currentDay = currentDate.getDate();
|
||||
const lastHour = currentDate.getHours();
|
||||
|
||||
|
||||
const response = await fetch(`/api/data/year/${currentYear}/month/${currentMonth}/day/${currentDay}/hour/${lastHour}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
setCarAmount(data);
|
||||
}
|
||||
}
|
||||
getCarAmount();
|
||||
|
||||
}, []);
|
||||
|
||||
const onClick = (e, location) => {
|
||||
setClickedCircleData(location);
|
||||
setIsClicked(true);
|
||||
};
|
||||
|
||||
const StatusCheck = (amount) => {
|
||||
if (amount <= 100) {
|
||||
return 'green';
|
||||
} else if (amount <= 250) {
|
||||
return 'yellow';
|
||||
} else {
|
||||
return 'red';
|
||||
}
|
||||
};
|
||||
|
||||
const locationMathching = (location) => {
|
||||
return StatusCheck(carAmount[location].car_count);
|
||||
};
|
||||
|
||||
if (carAmount.length === 0) {
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
<MapContainer center={coordinates} zoom={12} scrollWheelZoom={true} style={{ height: "90vh" }}>
|
||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
||||
</MapContainer>
|
||||
</div>
|
||||
<div className="col-sm d-flex align-items-center justify-content-center" style={{ marginBottom: "50px" }}>
|
||||
<h1 className="text-justify text-center" style={{ color: 'white' }}><i>Waiting for camera data...</i></h1>
|
||||
<div className="d-flex justify-content-center align-items-center" style={{ height: '100vh' }}>
|
||||
<Spinner animation="border" role="status" variant="light" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
<MapContainer center={coordinates} zoom={12} scrollWheelZoom={true} style={{ height: "90vh" }}>
|
||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
||||
{locations.filter(location => location.location_id !== 999).map(location => (
|
||||
<Circle
|
||||
key={location._id}
|
||||
center={[location.cord_N, location.cord_E]}
|
||||
pathOptions={{ color: locationMathching(location.location_id), fillColor: locationMathching(location.location_id), fillOpacity: 0.5 }}
|
||||
radius={200}
|
||||
eventHandlers={{ click: (e) => onClick(e, location) }}
|
||||
>
|
||||
</Circle>
|
||||
))}
|
||||
</MapContainer>
|
||||
</div>
|
||||
{isClicked ? (
|
||||
<SideView locationData={clickedCircleData} carAmountDat={carAmount} />
|
||||
) : (
|
||||
<div className="col-sm d-flex align-items-center justify-content-center" style={{ marginBottom: "50px" }}>
|
||||
<h1 className="text-justify text-center" style={{ color: 'white' }}><i>Select a camera to view details.</i></h1>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default MapComponent;
|
36
projektna_naloga/frontend/src/components/Profile.js
Normal file
36
projektna_naloga/frontend/src/components/Profile.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { UserContext } from '../userContext';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
function Profile() {
|
||||
const userContext = useContext(UserContext);
|
||||
const [profile, setProfile] = useState({});
|
||||
|
||||
useEffect(function () {
|
||||
const getProfile = async function () {
|
||||
const res = await fetch("/api/users/profile", { credentials: "include" });
|
||||
const data = await res.json();
|
||||
setProfile(data);
|
||||
}
|
||||
|
||||
getProfile();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!userContext.user ? <Navigate replace to="/login" /> : ""}
|
||||
<div className="container">
|
||||
<h1 style={{ color: "white" }}>User profile</h1>
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Username: {profile.username}</h5>
|
||||
<h5 className="card-text">Email: {profile.email}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default Profile;
|
54
projektna_naloga/frontend/src/components/Register.js
Normal file
54
projektna_naloga/frontend/src/components/Register.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
function Register() {
|
||||
const [username, setUsername] = useState([]);
|
||||
const [password, setPassword] = useState([]);
|
||||
const [email, setEmail] = useState([]);
|
||||
const [error, setError] = useState([]);
|
||||
|
||||
async function Register(e) {
|
||||
e.preventDefault();
|
||||
const res = await fetch("/api/users", {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data._id !== undefined) {
|
||||
window.location.href = "/";
|
||||
}
|
||||
else {
|
||||
setUsername("");
|
||||
setPassword("");
|
||||
setEmail("");
|
||||
setError("Registration failed");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="container" onSubmit={Register}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="form-label" style={{ color: "white" }}>Email</label>
|
||||
<input type="text" className="form-control" id="email" name="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="username" className="form-label" style={{ color: "white" }}>Username</label>
|
||||
<input type="text" className="form-control" id="username" name="username" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Username" />
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="password" className="form-label" style={{ color: "white" }}>Password</label>
|
||||
<input type="password" className="form-control" id="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary">Register</button>
|
||||
<label>{error}</label>
|
||||
</form>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default Register;
|
85
projektna_naloga/frontend/src/components/Saved.js
Normal file
85
projektna_naloga/frontend/src/components/Saved.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Card, Spinner } from 'react-bootstrap';
|
||||
|
||||
|
||||
function Saved() {
|
||||
const [savedLocations, setSavedLocations] = useState([]);
|
||||
const [locationData, setLocationData] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const getSavedLocations = async () => {
|
||||
const response = await fetch(`/api/users/savedLocation`);
|
||||
const data = await response.json();
|
||||
setSavedLocations(data);
|
||||
};
|
||||
|
||||
const getLocationData = async () => {
|
||||
const response = await fetch(`/api/location`);
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
setLocationData(data);
|
||||
}
|
||||
};
|
||||
|
||||
getSavedLocations();
|
||||
getLocationData();
|
||||
}, []);
|
||||
|
||||
|
||||
const merge = () => {
|
||||
if (savedLocations.length > 0 && locationData.length > 0) {
|
||||
var mergedLocations = [];
|
||||
for (var i = 0; i < savedLocations.length; i++) {
|
||||
mergedLocations.push(locationData[savedLocations[i]]);
|
||||
}
|
||||
return mergedLocations;
|
||||
}
|
||||
};
|
||||
|
||||
if (Object.keys(locationData).length === 0) {
|
||||
return (
|
||||
<div className="d-flex justify-content-center align-items-center" style={{ height: '100vh' }}>
|
||||
<Spinner animation="border" role="status" variant="success" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h2 style={{ color: 'white' }} className="d-flex justify-content-center">Saved Locations</h2>
|
||||
{savedLocations.length === 0 ? (
|
||||
<div className="d-flex justify-content-center">
|
||||
<Card style={{ width: '700px' }}>
|
||||
<Card.Body>
|
||||
<h4 className="text-center">No saved locations!</h4>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="d-flex justify-content-center">
|
||||
<ul>
|
||||
{merge().map((location) => (
|
||||
<li className="list-group-item" style={{ margin: "5px" }} key={location._id}>
|
||||
<Card style={{ width: '700px' }}>
|
||||
<Card.Body className="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<Card.Title>{location.name}</Card.Title>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-primary" type="button">
|
||||
<Link to={'/details/' + location.location_id} className="nav-link" style={{ textDecoration: 'none', color: 'inherit' }}>View details</Link>
|
||||
</button>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Saved;
|
51
projektna_naloga/frontend/src/components/SideView.js
Normal file
51
projektna_naloga/frontend/src/components/SideView.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Spinner } from 'react-bootstrap';
|
||||
|
||||
|
||||
function SideView({ locationData, carAmountDat }) {
|
||||
|
||||
if (Object.keys(carAmountDat).length === 0) {
|
||||
return (
|
||||
<div className="d-flex justify-content-center align-items-center" style={{ height: '100vh' }}>
|
||||
<Spinner animation="border" role="status" variant="success" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const GetLastHourCarAmount = () => {
|
||||
const clickedLocation = carAmountDat[locationData.location_id];
|
||||
if (clickedLocation) {
|
||||
return clickedLocation.car_count;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Use the data to display relevant information
|
||||
return (
|
||||
<div className="col-md d-flex justify-content-center align-items-center">
|
||||
{locationData && (
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<h1 className="card-title text-center">{locationData.name}</h1>
|
||||
<h5 className="card-subtitle mb-2 text-muted text-center"><i>Camera view:</i></h5>
|
||||
<div className="d-flex justify-content-center">
|
||||
<img src={locationData.img_urls[0]} className="img-fluid rounded" style={{ border: "5px solid black" }} alt="Camera view" />
|
||||
</div>
|
||||
<hr />
|
||||
<div className="text-center">
|
||||
<h5>Last hour car amount: {GetLastHourCarAmount()}</h5>
|
||||
<button className="btn btn-primary" type="button" style={{ marginBottom: "30px" }}>
|
||||
<Link to={'details/' + locationData.location_id} className="nav-link" style={{ textDecoration: 'none', color: 'inherit' }}>View more details</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default SideView;
|
13
projektna_naloga/frontend/src/index.css
Normal file
13
projektna_naloga/frontend/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
11
projektna_naloga/frontend/src/index.js
Normal file
11
projektna_naloga/frontend/src/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
6
projektna_naloga/frontend/src/userContext.js
Normal file
6
projektna_naloga/frontend/src/userContext.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const UserContext = createContext({
|
||||
user: null,
|
||||
setUserContext: () => { }
|
||||
});
|
Reference in New Issue
Block a user