Added backend and refactored frond-end to support it.

* Removed unecesery home page
* Added PHP Api that provides auth and replaces the json-server for data storage
* Added support for alternate geocode-api
* Added registration  page
This commit is contained in:
2025-05-27 00:42:00 +02:00
parent 25c1b73e32
commit e88269224c
60 changed files with 4438 additions and 8994 deletions

View File

@@ -16,6 +16,7 @@ module.exports = {
'warn',
{ allowConstantExport: true },
],
"no-unused-vars": ["off"]
"no-unused-vars": ["off"],
'react/prop-types': 'off'
},
}

23
LICENSE Normal file
View File

@@ -0,0 +1,23 @@
MIT License
Copyright (c) 2025 brammp
Copyright (c) 2023 nameishappy
Copyright (c) 2023 Aleksandar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

54
Nginx-Example.conf Normal file
View File

@@ -0,0 +1,54 @@
server {
listen 80;
server_name devsrv.tld;
#beginConf
index index.php index.html;
root /<Path to WorldWise hosting folder>;
gzip on;
gzip_proxied any;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/x-javascript image/svg image/svg+xml application/xml image/x-icon;
gzip_comp_level 2;
gzip_disable "msie6";
gzip_buffers 16 8k;
location / {
root /<Path to WorldWise hosting folder>/dist;
try_files $uri $uri/ /index.html;
}
location /api/ {
try_files $uri $uri/ /api/index.php$is_args$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.4-fpm.sock;
# regex to split $uri to $fastcgi_script_name and $fastcgi_path
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# Check that the PHP script exists before passing it
try_files $fastcgi_script_name =404;
# Bypass the fact that try_files resets $fastcgi_path_info
# see: http://trac.nginx.org/nginx/ticket/321
set $path_info $fastcgi_path_info;
fastcgi_param PATH_INFO $path_info;
fastcgi_index index.php;
include fastcgi.conf;
# Get request file name
fastcgi_param SCRIPT_FILENAME $request_filename;
}
location ~ /\.ht {
deny all;
}
#endConf
}

View File

@@ -1,31 +1,64 @@
# travel-tracker
# WorldWise Travel Tracker
WorldWise is a travel tracker app where you can pin your visited cities on the map and also write a short note about it.
The Travel Tracker App is a web application built with React.js that allows users to track and manage their travel plans. Whether you're a frequent traveler or just planning your next vacation, this app provides a simple and intuitive way to organize your trips, keep track of important details, and stay on top of your travel itinerary.
The WorldWise Web-App is built with React.js that allows users to track travel plans.
Whether you're a casual or frequent traveler, this app provides a simple and intuitive way to organize your trips, keep track of important details.
Features
User Authentication: Users can sign up or log in to the app securely. Personal information and travel data are protected.
## Set-up
### One-time and development requirements:
* NodeJS (22 or newer).
* npm (10 or newer).
Add Trips: Easily create new trips by providing basic details such as destination, travel dates, and any notes you want to add.
### Hosting requirements:
* a Web server (for example Nginx or Apache).
* PHP (8.2 or higher with modules PDO,PDO-mysql,curl).
* Mysql (8.0 or higher) or MariaDB (10 or higher).
View Trips: See a list of all your upcoming trips and quickly access their details.
### Steps:
* Clone the repository to the system with npm/nodejs.
* Open the cloned directory.
* Run `npm install`.
* Run `npm run build`.
* Copy the `api` and `dist` folders to the hosting server.
* Create a MySQL database with a corresponding user and import `api/db.sql`.
* Copy example configurations in `api/config/*.php.example` to `api/config/*.php`.
* Update `api/config/db.php` with your database name and credentials.
Edit and Delete Trips: Users can update trip details or delete trips they no longer need.
The application is now available on the address configured by the web server,
an account can be created using the link on the login page.
Getting Started
Follow these steps to set up the Travel Tracker App locally on your machine:
Clone the repository:
Open your web browser and visit http://localhost:3000 to access the Travel Tracker App.
### Extra steps to disable account creation
**Please make sure an account exists before continuing.**
* Set the `AllowUserRegistration` variable in `api/config/auth.php` to `false`
Dependencies
The Travel Tracker App uses the following main dependencies:
React.js: A popular JavaScript library for building user interfaces.
### Extra steps for frond-end development:
* Uncomment disabled variables `api/config/headers.php`.
* Update the api url in `vite.config.js` to reflect your environment.
* Run `npm run dev`.
The application is now available on the displayed url
React Router: Used for handling routing within the app.
### Extra steps for use of self-hosted geocode api
**A requirement is that a komoot photon api is already set-up.**
* Update variables `geocodeType`, `geocodeApiurl` and if required `geocodeApikey` in `api/config/geocode.php`.
CSS Modules : styling has been done using CSS Modules
Thank you for using the Travel Tracker App! Happy travels! 🌍✈️
## Technologies
The WorldWise App uses the following technologies:
* React.js: A popular JavaScript library for building user interfaces.
* React Router: Used for handling routing within the app.
* php : Provides the auth and data api.
* MySQL : Used for data storage.
* Reverse Geocode : by default provided by Big Data Cloud but can be configured to use a self-hosted komoot photon api.
Thank you for using the WorldWise Travel Tracker App!
Happy travels! 🌍✈️

2
api/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
config/db.php
config/geocode.php

View File

@@ -0,0 +1,10 @@
<?php
require_once 'config/auth.php';
session_start();
if (isset($_SESSION['name'])) {
echo json_encode(['message' => 'Authorized', 'user' => $_SESSION['name'] , 'allowRegistration' => $AllowUserRegistration]);
} else {
http_response_code(401);
echo json_encode(['message' => 'Unauthorized', 'allowRegistration' => $AllowUserRegistration]);
}

32
api/auth/login.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
require_once 'config/db.php';
$data = json_decode(file_get_contents("php://input"));
if (isset($data->username) && isset($data->password)) {
$username = $data->username;
$password = $data->password;
$query = "SELECT * FROM users WHERE username = :username";
$stmt = $pdo->prepare($query);
$stmt->bindParam(':username', $username);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['password'])) {
// Start a session and set session variables
session_start();
//$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['name'] = $user['name'];
echo json_encode(['message' => 'Login successful', 'username' => $user]);
} else {
http_response_code(401);
echo json_encode(['message' => 'Invalid credentials']);
}
} else {
http_response_code(400);
echo json_encode(['message' => 'Missing username or password']);
}
?>

7
api/auth/logout.php Normal file
View File

@@ -0,0 +1,7 @@
<?php
session_start();
session_unset();
session_destroy();
echo json_encode(['success' => true]);
?>

34
api/auth/register.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
require_once 'config/auth.php';
if (!$AllowUserRegistration){
echo json_encode(['message' => 'Endpoint not found']);
http_response_code(404);
die;
}
require_once 'config/db.php';
$data = json_decode(file_get_contents("php://input"));
if (isset($data->username) && isset($data->password) && isset($data->name)) {
$username = $data->username;
$password = password_hash($data->password, PASSWORD_BCRYPT);
$name = $data->name;
$query = "INSERT INTO users (username, password, name) VALUES (:username, :password, :name)";
$stmt = $pdo->prepare($query);
$stmt->bindParam(':username', $username);
$stmt->bindParam(':password', $password);
$stmt->bindParam(':name', $name);
if ($stmt->execute()) {
echo json_encode(['message' => 'User registered successfully']);
} else {
echo json_encode(['message' => 'User registration failed']);
}
} else {
http_response_code(400);
echo json_encode(['message' => 'Missing name, username or password']);
}
?>

40
api/calls/create_item.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
session_start();
if (!isset($_SESSION['name'])) {
http_response_code(401);
echo json_encode(['message' => 'Unauthorized']);
die;
}
require_once 'config/db.php';
$data = json_decode(file_get_contents("php://input"));
if (isset($data->cityName) && isset($data->country) && isset($data->flag) && isset($data->date) && isset($data->notes) && isset($data->lat) && isset($data->lng) ) {
$date = new DateTime($data->date);
$data->date = $date->format('Y-m-d');
$query = "INSERT INTO items (cityName, country, flag, date, notes, lat, lng) VALUES (:cityName, :country, :flag, :date, :notes, :lat, :lng)";
$stmt = $pdo->prepare($query);
$stmt->bindParam(':cityName', $data->cityName);
$stmt->bindParam(':country', $data->country);
$stmt->bindParam(':flag', $data->flag);
$stmt->bindParam(':date', $data->date);
$stmt->bindParam(':notes', $data->notes);
$stmt->bindParam(':lat', $data->lat);
$stmt->bindParam(':lng', $data->lng);
if ($stmt->execute()) {
$data->id = $pdo->lastInsertId();
echo json_encode(['message' => 'Item created successfully','id' => $data->id ,'id' => $data->id, 'cityName' => $data->cityName, 'country' => $data->country, 'flag' => $data->flag, 'date' => $data->date, 'notes' => $data->notes, 'lat' => $data->lat, 'lng' => $data->lng ]);
} else {
http_response_code(500);
echo json_encode(['message' => 'Failed to create item']);
}
} else {
http_response_code(400);
echo json_encode(['message' => 'Invalid input']);
}

22
api/calls/delete_item.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
session_start();
if (!isset($_SESSION['name'])) {
http_response_code(401);
echo json_encode(['message' => 'Unauthorized']);
die;
}
require_once 'config/db.php';
$id = isset($_GET['id']) ? $_GET['id'] : die('Item ID not provided');
$query = "DELETE FROM items WHERE id = :id";
$stmt = $pdo->prepare($query);
$stmt->bindParam(':id', $id);
if ($stmt->execute()) {
echo json_encode(['message' => 'Item deleted successfully']);
} else {
http_response_code(400);
echo json_encode(['message' => 'Failed to delete item']);
}

53
api/calls/geocode.php Normal file
View File

@@ -0,0 +1,53 @@
<?php
session_start();
if (!isset($_SESSION['name'])) {
http_response_code(401);
echo json_encode(['message' => 'Unauthorized']);
die;
}
if (!isset($_GET["latitude"]) || !isset($_GET["longitude"]) || empty($_GET["latitude"]) || empty($_GET["longitude"]) ) {
http_response_code(400);
echo json_encode(['message' => 'Invalid input']);
die;
}
require_once 'config/geocode.php';
$ch = curl_init();
switch ($geocodeType) {
case 'bigdatacloud':
$requrl = $geocodeApiurl . "?latitude=". $_GET["latitude"] ."&longitude=". $_GET["longitude"];
break;
case 'photon':
$requrl = $geocodeApiurl . "?lon=". $_GET["longitude"] ."&lat=". $_GET["latitude"];
break;
default:
http_response_code(401);
echo json_encode(['message' => 'Invalid request']);
die;
}
curl_setopt_array($ch, array(
CURLOPT_URL => $requrl,
CURLOPT_RETURNTRANSFER => true,
));
if ($geocodeApikey != false) {
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-Key: '. $geocodeApikey]);
}
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
switch ($geocodeType) {
case 'bigdatacloud':
echo json_encode(['city' => $response['city'], 'locality' => $response['locality'], 'countryName' => $response['countryName'],'countryCode' => $response['countryCode']]);
break;
case 'photon':
echo json_encode(['city' => $response['features'][0]['properties']['city'],'countryName' => $response['features'][0]['properties']['country'],'countryCode' => $response['features'][0]['properties']['countrycode']]);
break;
}

24
api/calls/get_item.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
session_start();
if (!isset($_SESSION['name'])) {
http_response_code(401);
echo json_encode(['message' => 'Unauthorized']);
die;
}
require_once 'config/db.php';
$id = isset($_GET['id']) ? $_GET['id'] : die('Item ID not provided');
$query = "SELECT * FROM items WHERE id = :id";
$stmt = $pdo->prepare($query);
$stmt->bindParam(':id', $id);
$stmt->execute();
$item = $stmt->fetch(PDO::FETCH_ASSOC);
if ($item) {
echo json_encode($item);
} else {
http_response_code(404);
echo json_encode(['message' => 'Item not found']);
}

17
api/calls/get_items.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
session_start();
if (!isset($_SESSION['name'])) {
http_response_code(401);
echo json_encode(['message' => 'Unauthorized']);
die;
}
require_once 'config/db.php';
$query = "SELECT * FROM items";
$stmt = $pdo->prepare($query);
$stmt->execute();
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($items);

3
api/config/auth.php Normal file
View File

@@ -0,0 +1,3 @@
<?php
$AllowUserRegistration = true;

13
api/config/db.php.example Normal file
View File

@@ -0,0 +1,13 @@
<?php
$host = 'localhost';
$dbname = 'ttapi';
$username = 'ttapi';
$password = '';
try {
$pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
echo "Connection failed: " . $e->getMessage();
}

View File

@@ -0,0 +1,5 @@
<?php
$geocodeType = 'bigdatacloud'; //bigdatacloud or photon
$geocodeApikey = false; //false to disable or enter api key between quotes to enable auth for the api call (key is send as 'X-Api-Key' header )
$geocodeApiurl = 'https://api.bigdatacloud.net/data/reverse-geocode-client'; //Set url of the geoip Api

8
api/config/headers.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
//Use for Dev environment
/*
header("Access-Control-Allow-Origin: http://localhost:5173");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS,DELETE');
*/

45
api/db.sql Normal file
View File

@@ -0,0 +1,45 @@
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
CREATE TABLE `items` (
`id` int NOT NULL,
`cityName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`country` varchar(255) NOT NULL,
`flag` varchar(2) NOT NULL,
`date` date NOT NULL,
`notes` text NOT NULL,
`lat` varchar(100) NOT NULL,
`lng` varchar(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE `users` (
`id` int NOT NULL,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
ALTER TABLE `items`
ADD PRIMARY KEY (`id`);
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `username` (`username`);
ALTER TABLE `items`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
ALTER TABLE `users`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

67
api/index.php Normal file
View File

@@ -0,0 +1,67 @@
<?php
require_once 'config/headers.php';
header('Content-Type: application/json');
$request_method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$path_parts = explode('/', $path);
if ($path_parts[1] == 'api') {
switch ($path_parts[2]) {
case 'geocode':
if ($request_method == 'GET') {
include 'calls/geocode.php';
}
break;
case 'get_items':
if ($request_method == 'GET') {
include 'calls/get_items.php';
}
break;
case 'get_item':
if ($request_method == 'GET') {
include 'calls/get_item.php';
}
break;
case 'create_item':
if ($request_method == 'POST') {
include 'calls/create_item.php';
}
break;
case 'update_item':
if ($request_method == 'PUT') {
include 'calls/update_item.php';
}
break;
case 'delete_item':
if ($request_method == 'DELETE') {
include 'calls/delete_item.php';
}
break;
case 'check':
if ($request_method == 'GET') {
include 'auth/check_session.php';
}
break;
case 'login':
if ($request_method == 'POST') {
include 'auth/login.php';
}
break;
case 'logout':
if ($request_method == 'POST') {
include 'auth/logout.php';
}
break;
case 'register':
if ($request_method == 'POST') {
include 'auth/register.php';
}
break;
default:
http_response_code(404);
echo json_encode(['message' => 'Endpoint not found']);
}
} else {
http_response_code(400);
echo json_encode(['message' => 'Invalid request']);
}

View File

@@ -1,100 +0,0 @@
{
"cities": [
{
"cityName": "Lisbon",
"country": "Portugal",
"emoji": "🇵🇹",
"date": "2027-10-31T15:59:59.138Z",
"notes": "My favorite city so far!",
"position": {
"lat": 38.727881642324164,
"lng": -9.140900099907554
},
"id": 73930385
},
{
"cityName": "Muneville-le-Bingard",
"country": "France",
"emoji": "🇫🇷",
"date": "2023-07-24T12:43:14.903Z",
"notes": "",
"position": {
"lat": "49.1549385508535",
"lng": "-1.450192918262432"
},
"id": 98443210
},
{
"cityName": "Tachilek",
"country": "Myanmar",
"emoji": "🇲🇲",
"date": "2023-07-25T05:36:52.300Z",
"notes": "",
"position": {
"lat": "20.47168563901091",
"lng": "99.70512653573469"
},
"id": 98443211
},
{
"cityName": "Amborompotsy",
"country": "Madagascar",
"emoji": "🇲🇬",
"date": "2023-07-25T05:44:10.656Z",
"notes": "",
"position": {
"lat": "-20.868672459730085",
"lng": "46.142629530132155"
},
"id": 98443215
},
{
"cityName": "Gibeon",
"country": "Namibia",
"emoji": "🇳🇦",
"date": "2023-07-25T05:44:17.206Z",
"notes": "",
"position": {
"lat": "-24.279471155061238",
"lng": "16.96304591111141"
},
"id": 98443216
},
{
"cityName": "Luni Junction",
"country": "India",
"emoji": "🇮🇳",
"date": "2023-08-03T05:20:38.419Z",
"notes": "such a nice place would love to go there again",
"position": {
"lat": "26.15565523194215",
"lng": "73.1250312372971"
},
"id": 98443218
},
{
"cityName": "Khan Sahib",
"country": "India",
"emoji": "🇮🇳",
"date": "2023-08-03T05:21:37.486Z",
"notes": "",
"position": {
"lat": "33.90726362193601",
"lng": "74.70705467324112"
},
"id": 98443219
},
{
"cityName": "Yavatmal",
"country": "India",
"emoji": "🇮🇳",
"date": "2023-08-03T15:45:19.272Z",
"notes": "nice",
"position": {
"lat": "20.307043551027597",
"lng": "78.04687500000001"
},
"id": 98443220
}
]
}

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WorldWise : Track ypur journey...</title>
<title>WorldWise : Track your journey...</title>
</head>
<body>
<div id="root"></div>

11975
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,10 +7,11 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"server": "json-server --watch data/cities.json --port 8000 "
"lint-fix": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0 --fix",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.9.0",
"json-server": "^0.17.3",
"leaflet": "^1.9.4",
"react": "^18.2.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

View File

@@ -1,35 +1,32 @@
import React, { useContext } from "react";
import { useState } from "react";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import Product from "./pages/Product";
import Homepage from "./pages/Homepage";
import Pricing from "./pages/Pricing";
import PageNotFound from "./pages/PageNotFound";
import Login from "./pages/Login";
import Register from "./pages/Register";
import AppLayout from "./pages/AppLayout";
import CityList from "./components/CityList";
import CountryList from "./components/CountryList";
import City from "./components/City";
import Form from "./components/Form";
import { CitiesProvider } from "./contexts/CitiesContext";
import { AuthProvider } from "./contexts/FakeAuthcontext";
import { AuthProvider } from "./contexts/AuthContext";
import ProtectedRoute from "./pages/ProtectedRoute";
const App = () => {
return (
<AuthProvider>
<CitiesProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Homepage />} />
<Route path="Product" element={<Product />} />
<Route path="Pricing" element={<Pricing />} />
<Route path="login" element={<Login />} />
<Route path="/" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route
path="app"
element={
<ProtectedRoute>
<AppLayout />
<CitiesProvider>
<AppLayout />
</CitiesProvider>
</ProtectedRoute>
}
>
@@ -42,7 +39,7 @@ const App = () => {
<Route path="*" element={<PageNotFound />} />
</Routes>
</BrowserRouter>
</CitiesProvider>
</AuthProvider>
);
};

View File

@@ -22,15 +22,12 @@ function City() {
useEffect(
function () {
getCity(id);
console.log(id);
console.log(currentCity)
},
[id]
);
const {cityName, emoji, date, notes}=currentCity; // Declare variables in the outer scope
const {cityName, flag, date, notes}=currentCity;
const [search, setSearchParams] = useSearchParams();
const lat = search.get("lat");
@@ -47,7 +44,7 @@ function City() {
<div className={styles.row}>
<h6>City name</h6>
<h3>
{/* <span>{emoji}</span> */}
{/* <span>{flag}</span> */}
{cityName}
</h3>
</div>

View File

@@ -14,7 +14,7 @@ const formatDate = (date) =>
export default function CityItem({ city }) {
const {currentCity,isLoading,deleteCity}=useCities();
const { cityName, emoji, date,position,id } = city;
const { cityName, flag, date,position,id } = city;
function handleClick(e){
e.preventDefault();
@@ -25,8 +25,8 @@ export default function CityItem({ city }) {
<div >
<ul>
<li>
<Link className={`${styles.cityItem} ${(id===currentCity.id)?styles["cityItem--active"]:''}`} to={`${city.id}?lat=${position.lat}&lng=${position.lng}`}>
<span className={styles.emoji}>{emoji}</span>
<Link className={`${styles.cityItem} ${(id===currentCity.id)?styles["cityItem--active"]:''}`} to={`${city.id}?lat=${city.lat}&lng=${city.lng}`}>
<span className={styles.flag}>{flag}</span>
<h3 className={styles.name}>{cityName}</h3>
<time className={styles.date}>{formatDate(date)}</time>
<button className={styles.deleteBtn} onClick={handleClick}>&times;</button>

View File

@@ -20,7 +20,7 @@
border-left: 5px solid var(--color-brand--2);
}
.emoji {
.flag {
font-size: 2.6rem;
line-height: 1;
}

View File

@@ -1,9 +1,8 @@
import styles from "./CountryItem.module.css";
function CountryItem({ country }) {
function CountryItem({ country }) {
return (
<li className={styles.countryItem}>
{/* <span>{country.emoji}</span> */}
<span>{country}</span>
</li>
);

View File

@@ -15,14 +15,12 @@ const CountryList = () => {
}
const countries = cities.reduce((countries, city) => {
if (!countries.includes(city.country)) {
countries.push(city.country);
if (!countries.includes(city.flag+" "+city.country)) {
countries.push(city.flag+" "+city.country);
}
return countries;
}, []);;
console.log(cities);
return (
<ul className={styles.countryList}>
{countries.map((country) => (

View File

@@ -1,5 +1,3 @@
// "https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=0&longitude=0"
import { useEffect, useState } from "react";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
@@ -11,8 +9,9 @@ import Message from "./Message";
import Spinner from "./Spinner";
import { useCities } from "../contexts/CitiesContext";
import { useNavigate } from "react-router-dom";
import axios from 'axios';
export function convertToEmoji(countryCode) {
export function convertToFlag(countryCode) {
const codePoints = countryCode
.toUpperCase()
.split("")
@@ -21,6 +20,7 @@ export function convertToEmoji(countryCode) {
}
function Form() {
const API_URL = import.meta.env.VITE_API_URL;
const [cityName, setCityName] = useState("");
const {createCity,isLoading}=useCities();
const [country, setCountry] = useState("");
@@ -28,7 +28,7 @@ function Form() {
const [notes, setNotes] = useState("");
const [isGeocodingLoading, setIsGeocodingLoading] = useState(false);
const [lat, lng] = useUrlPosition();
const [emoji, setEmoji] = useState("");
const [flag, setFlag] = useState("");
const [geocodingError, setGeocodingError] = useState(null);
const navigate=useNavigate();
useEffect(
@@ -40,10 +40,8 @@ function Form() {
setGeocodingError(null);
try {
setIsGeocodingLoading(true);
const data = await fetch(
`https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lng}`
);
const res = await data.json();
const response = await axios.get(`${API_URL}/geocode?latitude=${lat}&longitude=${lng}`);
const res = await response.data;
if (res.countryCode === "") {
throw new Error(
@@ -52,7 +50,7 @@ function Form() {
}
setCityName(res.city || res.locality || "");
setCountry(res.countryName || "");
setEmoji(convertToEmoji(res.countryCode));
setFlag(convertToFlag(res.countryCode));
} catch (err) {
setGeocodingError(err.message);
} finally {
@@ -70,10 +68,11 @@ function Form() {
const newCity = {
cityName,
country,
emoji,
flag,
date,
notes,
position: { lat, lng }
lat,
lng
};
await createCity(newCity);
navigate("/app/cities")
@@ -99,7 +98,7 @@ function Form() {
onChange={(e) => setCityName(e.target.value)}
value={cityName}
/>
<span className={styles.flag}>{emoji}</span>
<span className={styles.flag}>{flag}</span>
</div>
<div className={styles.row}>

View File

@@ -11,7 +11,7 @@ const Map = () => {
const { cities } = useCities();
const [mapPosition, setMapPosition] = useState([20.5937, 78.9629]);
const [mapPosition, setMapPosition] = useState([52.1599, 5.6399]);
const [mapLat,mapLng]=useUrlPosition();
const {isLoading:isLoadingGeolocation,position:geoLocationPosition,getPosition}=useGeolocation();
@@ -34,7 +34,6 @@ const Map = () => {
</Button>}
<div className={styles.map}>
<MapContainer
// center={[mapLat,mapLng]}
center={mapPosition}
zoom={6}
scrollWheelZoom={true}
@@ -45,9 +44,8 @@ const Map = () => {
url="https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png"
/>
{cities.map((city) => {
return (
<Marker position={[city.position.lat, city.position.lng]} key={city.id}>
<Marker position={[city.lat, city.lng]} key={city.id}>
<Popup>
{city.cityName}
<br /> {city.country}

View File

@@ -9,7 +9,6 @@
height: 100%;
}
/* Here we want to style classes that are coming from leaflet. So we want CSS Modules to give us the ACTUAL classnames, not to add some random ID to them, because then they won't match the classnames defined inside the map. The solution is to define these classes as GLOBAL */
:global(.leaflet-popup .leaflet-popup-content-wrapper) {
background-color: var(--color-dark--1);
color: var(--color-light--2);

View File

@@ -1,24 +0,0 @@
import React from 'react'
import Logo from './Logo'
import styles from "./PageNav.module.css"
import { NavLink } from 'react-router-dom'
const PageNav = () => {
return (
<nav className={styles.nav}>
<Logo/>
<ul>
<li>
<NavLink to="/pricing" >Pricing</NavLink>
</li>
<li>
<NavLink to="/product" >Product</NavLink>
</li>
<li>
<NavLink to="/login" className={styles.ctaLink} >Login</NavLink>
</li>
</ul>
</nav>
)
}
export default PageNav

View File

@@ -11,13 +11,6 @@ const Sidebar = () => {
<Logo/>
<AppNav/>
<Outlet/>
<footer className={styles.footer}>
<p className={styles.copyright}>
&copy; Copyright {new Date().getFullYear()} by WorldWise corp
</p>
</footer>
</div>
)
}

View File

@@ -6,14 +6,5 @@
display: flex;
flex-direction: column;
align-items: center;
height: calc(100vh - 4.8rem);
}
.footer {
margin-top: auto;
}
.copyright {
font-size: 1.2rem;
color: var(--color-light--1);
height: calc(100vh);
}

View File

@@ -1,19 +1,11 @@
import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/FakeAuthcontext";
import { useAuth } from "../contexts/AuthContext";
import styles from "./User.module.css";
const FAKE_USER = {
name: "Jack",
email: "jack@example.com",
password: "qwerty",
avatar: "https://i.pravatar.cc/100?u=zz",
};
function User() {
const navigate=useNavigate();
const {user,logout} = useAuth();
const {user, logout} = useAuth();
function handleClick() {
logout();
@@ -22,21 +14,10 @@ function User() {
return (
<div className={styles.user}>
<img src={user.avatar} alt={user.name} />
<span>Welcome, {user.name}</span>
<span>Welcome, {user}</span>
<button onClick={handleClick}>Logout</button>
</div>
);
}
export default User;
/*
CHALLENGE
1) Add `AuthProvider` to `App.jsx`
2) In the `Login.jsx` page, call `login()` from context
3) Inside an effect, check whether `isAuthenticated === true`. If so, programatically navigate to `/app`
4) In `User.js`, read and display logged in user from context (`user` object). Then include this component in `AppLayout.js`
5) Handle logout button by calling `logout()` and navigating back to `/`
*/

View File

@@ -17,7 +17,9 @@
.user img {
border-radius: 100px;
height: 4rem;
height: 40px;
width: 40px;
object-fit: contain;
}
.user button {

View File

@@ -0,0 +1,83 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import axios from 'axios';
const AuthContext = createContext();
const API_URL = import.meta.env.VITE_API_URL;
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
const [isRegistratonEnabled, setIsRetratonEnabled] = useState(null);
// Automatically include credentials (cookies) in requests
axios.defaults.withCredentials = true;
useEffect(() => {
const checkAuth = async () => {
try {
const res = await axios.get(`${API_URL}/check`);
setUser(res.data.user);
setIsRetratonEnabled(res.data.allowRegistration);
} catch (error) {
if (typeof error.response.data.allowRegistration != "undefined") {
setIsRetratonEnabled(error.response.data.allowRegistration);
}else {
setIsRetratonEnabled(false);
}
setIsAuthenticated(false);
setUser(null);
} finally {
setLoading(false);
}
};
checkAuth();
}, []);
const login = async (username, password) => {
try {
await axios.post(`${API_URL}/login`, {
username,
password,
});
const res = await axios.get(`${API_URL}/check`);
setUser(res.data.user);
setIsAuthenticated(true);
} catch (error) {
throw new Error('Login failed');
}
};
const logout = async () => {
try {
await axios.post(`${API_URL}/logout`);
} catch (error) {
console.log("error")
}
setUser(null);
setIsAuthenticated(false);
};
const register = async (name, username, password) => {
try {
await axios.post(`${API_URL}/register`, {
name,
username,
password,
});
} catch (error) {
throw new Error('Registration failed');
}
};
return (
<AuthContext.Provider value={{ user, isAuthenticated, isRegistratonEnabled, loading, login, logout, register }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);

View File

@@ -6,7 +6,10 @@ import {
useReducer,
} from "react";
import axios from 'axios';
const CitiesContext = createContext();
const API_URL = import.meta.env.VITE_API_URL;
const initialState = {
cities: [],
@@ -46,9 +49,6 @@ function reducer(state, action) {
function CitiesProvider({ children }) {
// const [cities, setCities] = useState([]);
// const [isLoading, setIsLoading] = useState(false);
// const [currentCity,setCurrentCity]=useState({});
const [state, dispatch] = useReducer(reducer,initialState);
const { cities, isLoading, currentCity } = state;
@@ -57,8 +57,8 @@ function CitiesProvider({ children }) {
async function fetchData() {
dispatch({ type: "loading" });
try {
const data = await fetch("http://localhost:8000/cities");
const res = await data.json();
const response = await axios.get(`${API_URL}/get_items`);
const res = await response.data;
dispatch({ type:"cities/loaded", payload: res });
} catch {
@@ -75,8 +75,8 @@ function CitiesProvider({ children }) {
if(Number(id)===currentCity.id) return;
dispatch({ type: "loading" });
try {
const data = await fetch(`http://localhost:8000/cities/${id}`);
const res = await data.json();
const response = await axios.get(`${API_URL}/get_item?id=${id}`);
const res = await response.data.json();
dispatch({ type: "city/loaded", payload: res });
} catch {
@@ -86,29 +86,32 @@ function CitiesProvider({ children }) {
});
}
}
async function createCity(newCity) {
dispatch({ type: "loading" });
try {
const data = await fetch(`http://localhost:8000/cities`, {
method: "POST",
body: JSON.stringify(newCity),
async function createCity(newCity) {
dispatch({ type: "loading" });
try {
const response = await axios.post(
`${API_URL}/create_item`,
newCity,
{
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const res = await data.json();
dispatch({ type: "cities/created", payload: res });
} catch {
dispatch({
type: "rejected",
payload: "There was an error loading the city",
});
}
}
);
dispatch({ type: "cities/created", payload: response.data });
} catch (error) {
dispatch({
type: "rejected",
payload: error.response?.data?.message || 'There was an error loading the city',
});
}
async function deleteCity(id) {
}
async function deleteCity(id) {
dispatch({ type: "loading" });
try {
const data = await fetch(`http://localhost:8000/cities/${id}`, {
const response = await axios.delete(`${API_URL}/delete_item?id=${id}`, {
method: "DELETE",
});
dispatch({ type: "cities/deleted", payload: id });

View File

@@ -1,57 +0,0 @@
import { createContext, useContext, useReducer } from "react";
const AuthContext = createContext();
const initialState = {
user: null,
isAuthenticated: false,
};
function reducer(state, action) {
switch (action.type) {
case "login":
return { ...state, user: action.payload, isAuthenticated: true };
case "logout":
return { ...state, user: null, isAuthenticated: false };
default:
throw new Error("Unknown action");
}
}
const FAKE_USER = {
name: "Jack",
email: "jack@example.com",
password: "qwerty",
avatar: "https://i.pravatar.cc/100?u=zz",
};
function AuthProvider({ children }) {
const [{ user, isAuthenticated }, dispatch] = useReducer(
reducer,
initialState
);
function login(email, password) {
if (email === FAKE_USER.email && password === FAKE_USER.password)
dispatch({ type: "login", payload: FAKE_USER });
}
function logout() {
dispatch({ type: "logout" });
}
return (
<AuthContext.Provider value={{ user, isAuthenticated, login, logout }}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
const context = useContext(AuthContext);
if (context === undefined)
throw new Error("AuthContext was used outside AuthProvider");
return context;
}
export { AuthProvider, useAuth };

View File

@@ -1,8 +1,6 @@
/* Taken from getting started guide at: https://leafletjs.com/examples/quick-start/ */
@import "https://unpkg.com/leaflet@1.7.1/dist/leaflet.css";
@import "https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap";
/* These CSS variables are global, so they are available in all CSS modules */
:root {
--color-brand--1: #ffb545;
--color-brand--2: #00c46a;
@@ -67,12 +65,3 @@ input:focus {
padding: 1rem 3rem;
border-radius: 5px;
}
/*
"importCSSModule": {
"prefix": "csm",
"scope": "javascript,typescript,javascriptreact",
"body": ["import styles from './${TM_FILENAME_BASE}.module.css'"],
"description": "Import CSS Module as `styles`"
}, */

View File

@@ -3,9 +3,7 @@ import Sidebar from '../components/Sidebar'
import styles from './AppLayout.module.css'
import Map from '../components/Map'
import User from '../components/User'
import { useAuth } from '../contexts/FakeAuthcontext'
import { useAuth } from '../contexts/AuthContext'
const AppLayout = () => {

View File

@@ -1,6 +1,5 @@
.app {
height: 100vh;
padding: 2.4rem;
overscroll-behavior-y: none;
display: flex;
position: relative;

View File

@@ -1,24 +0,0 @@
import { Link } from "react-router-dom";
import styles from "./Homepage.module.css";
import PageNav from "../components/PageNav";
export default function Homepage() {
return (
<main className={styles.homepage}>
<PageNav/>
<section>
<h1>
You travel the world.
<br />
WorldWise keeps track of your adventures.
</h1>
<h2>
A world map that tracks your footsteps into every city you can think
of. Never forget your wonderful experiences, and show your friends how
you have wandered the world.
</h2>
<Link to='/login' className="cta">Start Tracking Now</Link>
</section>
</main>
);
}

View File

@@ -1,34 +0,0 @@
.homepage {
height: calc(100vh - 5rem);
margin: 2.5rem;
background-image: linear-gradient(
rgba(36, 42, 46, 0.8),
rgba(36, 42, 46, 0.8)
),
url("../bg.jpg");
background-size: cover;
background-position: center;
padding: 2.5rem 5rem;
}
.homepage section {
display: flex;
flex-direction: column;
height: 85%;
align-items: center;
justify-content: center;
gap: 2.5rem;
text-align: center;
}
.homepage h1 {
font-size: 4.5rem;
line-height: 1.3;
}
.homepage h2 {
width: 90%;
font-size: 1.9rem;
color: var(--color-light--1);
margin-bottom: 2.5rem;
}

View File

@@ -1,41 +1,62 @@
import PageNav from "../components/PageNav";
import React, { useEffect, useState } from 'react';
import styles from "./Login.module.css";
import {useAuth} from "../contexts/FakeAuthcontext";
import Logo from '../components/Logo'
import styles from "./Page.module.css";
import {useAuth} from "../contexts/AuthContext";
import { useNavigate } from "react-router-dom";
import Button from "../components/Button";
export default function Login() {
// PRE-FILL FOR DEV PURPOSES
const [email, setEmail] = useState("jack@example.com");
const [password, setPassword] = useState("qwerty");
const {login,isAuthenticated}=useAuth();
const navigate=useNavigate();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [showError, setShowError] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
function handleSubmit(e){
const { login, isAuthenticated, isRegistratonEnabled } = useAuth();
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
if(email && password) login(email,password);
if (!username || !password) {
setErrorMessage("Username and password are required.");
setShowError(true);
return;
}
try {
setShowError(false);
await login(username, password);
} catch (err) {
setErrorMessage("Invalid username or password.");
setShowError(true);
}
}
useEffect(()=>{
if(isAuthenticated){
console.log("isAuthenticated",isAuthenticated)
navigate("/app");
}
},[isAuthenticated])
useEffect(() => {
if (isAuthenticated) {
navigate("/app");
}
}, [isAuthenticated, navigate]);
return (
<main className={styles.login}>
<PageNav/>
<main className={styles.page}>
<form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.row}>
<label htmlFor="email">Email address</label>
<Logo className={styles.logo} />
</div>
{showError && (
<div className={styles.row}>
<div className={styles.error}>
<h1>{errorMessage}</h1>
</div>
</div>
)}
<div className={styles.row}>
<label htmlFor="username">Username</label>
<input
type="email"
id="email"
onChange={(e) => setEmail(e.target.value)}
value={email}
id="username"
onChange={(e) => setUsername(e.target.value)}
value={username}
/>
</div>
@@ -49,10 +70,15 @@ useEffect(()=>{
/>
</div>
<div>
<Button type='primary'>Login</Button>
<div className={styles.btnrow}>
<Button type="primary">Login</Button>
{isRegistratonEnabled && (
<a className={styles.secondbtn} href="/register">Create account</a>
)}
</div>
</form>
</main>
);
}

View File

@@ -1,26 +0,0 @@
.login {
margin: 2.5rem;
padding: 2.5rem 5rem;
background-color: var(--color-dark--1);
min-height: calc(100vh - 5rem);
}
.form {
background-color: var(--color-dark--2);
border-radius: 7px;
padding: 2rem 3rem;
width: 100%;
display: flex;
flex-direction: column;
gap: 2rem;
/* Different from other form */
width: 48rem;
margin: 8rem auto;
}
.row {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

57
src/pages/Page.module.css Normal file
View File

@@ -0,0 +1,57 @@
body {
min-height: calc(100vh);
background-image: linear-gradient(
rgba(36, 42, 46, 0.8),
rgba(36, 42, 46, 0.8)
),
url("../bg.jpg");
background-size: cover;
background-position: center;
}
.form {
background-color: var(--color-dark--2);
border-radius: 7px;
padding: 2rem 3rem;
width: 100%;
display: flex;
flex-direction: column;
gap: 2rem;
width: 48rem;
margin: 8rem auto;
}
.row {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.PageNotFound {
text-align: center;
font-size: x-large;
}
.error{
border: 1px solid;
border-color: red;
background-color: #bf616a;
}
.secondbtn{
margin-left: auto;
color: white;
font-weight: 700;
font-size: 1.5rem;
text-decoration: none;
}
.secondbtn:hover{
text-decoration: underline;
}
.btnrow{
display: flex;
}

View File

@@ -1,7 +1,16 @@
import styles from "./Page.module.css";
import Logo from '../components/Logo'
export default function PageNotFound() {
return (
<div>
<h1>Page not found 😢</h1>
<main className={styles.page}>
<div className={styles.form} >
<div className={styles.row} >
<Logo className={styles.logo}/>
</div>
<div className={styles.row} >
<h1 className={styles.PageNotFound}>Page not found</h1>
</div>
</div>
</main>
);
}

View File

@@ -1,26 +0,0 @@
// Uses the same styles as Product
import PageNav from "../components/PageNav";
import styles from "./Product.module.css";
export default function Product() {
return (
<main className={styles.product}>
<PageNav/>
<section>
<div>
<h2>
Simple pricing.
<br />
Just $9/month.
</h2>
<p>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Vitae vel
labore mollitia iusto. Recusandae quos provident, laboriosam fugit
voluptatem iste.
</p>
</div>
<img src="img-2.jpg" alt="overview of a large city with skyscrapers" />
</section>
</main>
);
}

View File

@@ -1,30 +0,0 @@
import PageNav from "../components/PageNav";
import styles from "./Product.module.css";
export default function Product() {
return (
<main className={styles.product}>
<PageNav/>
<section>
<img
src="img-1.jpg"
alt="person with dog overlooking mountain with sunset"
/>
<div>
<h2>About WorldWide.</h2>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Illo est
dicta illum vero culpa cum quaerat architecto sapiente eius non
soluta, molestiae nihil laborum, placeat debitis, laboriosam at fuga
perspiciatis?
</p>
<p>
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Corporis
doloribus libero sunt expedita ratione iusto, magni, id sapiente
sequi officiis et.
</p>
</div>
</section>
</main>
);
}

View File

@@ -1,34 +0,0 @@
.product {
margin: 2.5rem;
padding: 2.5rem 5rem;
background-color: var(--color-dark--1);
min-height: calc(100vh - 5rem);
}
.product section {
width: clamp(80rem, 80%, 90rem);
margin: 6rem auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 7rem;
align-items: center;
}
.product img {
width: 100%;
}
.product h2 {
font-size: 4rem;
line-height: 1.2;
margin-bottom: 3rem;
}
.product p {
font-size: 1.6rem;
margin-bottom: 2rem;
}
.product section a {
margin-top: 2rem;
}

View File

@@ -1,17 +1,18 @@
import { useEffect } from "react";
import { useAuth } from "../contexts/FakeAuthcontext";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import { useNavigate, Navigate } from "react-router-dom";
function ProtectedRoute({children}){
const ProtectedRoute = ({ children }) => {
const { user } = useAuth();
const navigate=useNavigate();
const {isAuthenticated}=useAuth();
useEffect(function(){
if(!isAuthenticated) navigate("/");
},[isAuthenticated,navigate])
return isAuthenticated? children:null;
}
if (!user) {
return <Navigate to="/" />;
}
return children;
};
export default ProtectedRoute;

111
src/pages/Register.jsx Normal file
View File

@@ -0,0 +1,111 @@
import React, { useEffect, useState } from 'react';
import Logo from '../components/Logo'
import styles from "./Page.module.css";
import {useAuth} from "../contexts/AuthContext";
import { useNavigate } from "react-router-dom";
import Button from "../components/Button";
export default function Register() {
const [name, setName] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState("");
const [showError, setShowError] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const { login, register, isAuthenticated, isRegistratonEnabled } = useAuth();
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
if (!username || !name || !password || !passwordConfirmation) {
setErrorMessage("Some required fields are empty.");
setShowError(true);
return;
} else if (password !== passwordConfirmation){
setErrorMessage("Passwords don't match");
setShowError(true);
return;
}
try {
setShowError(false);
await register(name, username, password);
await login(username, password);
} catch (err) {
setErrorMessage("Registration failed");
setShowError(true);
}
}
useEffect(() => {
if (isAuthenticated) {
navigate("/app");
}
if (isRegistratonEnabled == false) {
navigate("/");
}
}, [isRegistratonEnabled, isAuthenticated, navigate]);
return (
<main className={styles.page}>
<form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.row}>
<Logo className={styles.logo} />
</div>
{showError && (
<div className={styles.row}>
<div className={styles.error}>
<h1>{errorMessage}</h1>
</div>
</div>
)}
<div className={styles.row}>
<label htmlFor="name">Display name</label>
<input
id="name"
onChange={(e) => setName(e.target.value)}
value={name}
/>
</div>
<div className={styles.row}>
<label htmlFor="username">Username</label>
<input
id="username"
onChange={(e) => setUsername(e.target.value)}
value={username}
/>
</div>
<div className={styles.row}>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
onChange={(e) => setPassword(e.target.value)}
value={password}
/>
</div>
<div className={styles.row}>
<label htmlFor="passwordConfirmation">Password confirmation</label>
<input
type="password"
id="passwordConfirmation"
onChange={(e) => setPasswordConfirmation(e.target.value)}
value={passwordConfirmation}
/>
</div>
<div className={styles.btnrow}>
<Button type="primary">Register & login</Button>
<a className={styles.secondbtn} href="/">Back</a>
</div>
</form>
</main>
);
}

View File

@@ -1,7 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// import eslint from "vite-plugin-eslint"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
define: {
//Prod//
'import.meta.env.VITE_API_URL': JSON.stringify('/api'),
//Dev//
// 'import.meta.env.VITE_API_URL': JSON.stringify('http://devsrv.tld/api'),
}
})