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:
@@ -16,6 +16,7 @@ module.exports = {
|
|||||||
'warn',
|
'warn',
|
||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
],
|
],
|
||||||
"no-unused-vars": ["off"]
|
"no-unused-vars": ["off"],
|
||||||
|
'react/prop-types': 'off'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
23
LICENSE
Normal file
23
LICENSE
Normal 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
54
Nginx-Example.conf
Normal 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
|
||||||
|
}
|
||||||
67
README.md
67
README.md
@@ -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.
|
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
|
## Set-up
|
||||||
User Authentication: Users can sign up or log in to the app securely. Personal information and travel data are protected.
|
### 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:
|
### Extra steps to disable account creation
|
||||||
Open your web browser and visit http://localhost:3000 to access the Travel Tracker App.
|
**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
2
api/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
config/db.php
|
||||||
|
config/geocode.php
|
||||||
10
api/auth/check_session.php
Normal file
10
api/auth/check_session.php
Normal 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
32
api/auth/login.php
Normal 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
7
api/auth/logout.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
session_unset();
|
||||||
|
session_destroy();
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
?>
|
||||||
34
api/auth/register.php
Normal file
34
api/auth/register.php
Normal 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
40
api/calls/create_item.php
Normal 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
22
api/calls/delete_item.php
Normal 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
53
api/calls/geocode.php
Normal 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
24
api/calls/get_item.php
Normal 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
17
api/calls/get_items.php
Normal 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
3
api/config/auth.php
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$AllowUserRegistration = true;
|
||||||
13
api/config/db.php.example
Normal file
13
api/config/db.php.example
Normal 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();
|
||||||
|
}
|
||||||
5
api/config/geocode.php.example
Normal file
5
api/config/geocode.php.example
Normal 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
8
api/config/headers.php
Normal 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
45
api/db.sql
Normal 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
67
api/index.php
Normal 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']);
|
||||||
|
}
|
||||||
100
data/cities.json
100
data/cities.json
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/icon.png" />
|
<link rel="icon" type="image/png" href="/icon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>WorldWise : Track ypur journey...</title>
|
<title>WorldWise : Track your journey...</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
11975
package-lock.json
generated
11975
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,11 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview",
|
"lint-fix": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0 --fix",
|
||||||
"server": "json-server --watch data/cities.json --port 8000 "
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.9.0",
|
||||||
"json-server": "^0.17.3",
|
"json-server": "^0.17.3",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
BIN
public/bg.jpg
BIN
public/bg.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 2.0 MiB |
BIN
public/img-1.jpg
BIN
public/img-1.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 116 KiB |
BIN
public/img-2.jpg
BIN
public/img-2.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 287 KiB |
17
src/App.jsx
17
src/App.jsx
@@ -1,35 +1,32 @@
|
|||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
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 PageNotFound from "./pages/PageNotFound";
|
||||||
import Login from "./pages/Login";
|
import Login from "./pages/Login";
|
||||||
|
import Register from "./pages/Register";
|
||||||
import AppLayout from "./pages/AppLayout";
|
import AppLayout from "./pages/AppLayout";
|
||||||
import CityList from "./components/CityList";
|
import CityList from "./components/CityList";
|
||||||
import CountryList from "./components/CountryList";
|
import CountryList from "./components/CountryList";
|
||||||
import City from "./components/City";
|
import City from "./components/City";
|
||||||
import Form from "./components/Form";
|
import Form from "./components/Form";
|
||||||
import { CitiesProvider } from "./contexts/CitiesContext";
|
import { CitiesProvider } from "./contexts/CitiesContext";
|
||||||
import { AuthProvider } from "./contexts/FakeAuthcontext";
|
import { AuthProvider } from "./contexts/AuthContext";
|
||||||
import ProtectedRoute from "./pages/ProtectedRoute";
|
import ProtectedRoute from "./pages/ProtectedRoute";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<CitiesProvider>
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Homepage />} />
|
<Route path="/" element={<Login />} />
|
||||||
<Route path="Product" element={<Product />} />
|
<Route path="/register" element={<Register />} />
|
||||||
<Route path="Pricing" element={<Pricing />} />
|
|
||||||
<Route path="login" element={<Login />} />
|
|
||||||
<Route
|
<Route
|
||||||
path="app"
|
path="app"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
|
<CitiesProvider>
|
||||||
<AppLayout />
|
<AppLayout />
|
||||||
|
</CitiesProvider>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -42,7 +39,7 @@ const App = () => {
|
|||||||
<Route path="*" element={<PageNotFound />} />
|
<Route path="*" element={<PageNotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</CitiesProvider>
|
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,15 +22,12 @@ function City() {
|
|||||||
useEffect(
|
useEffect(
|
||||||
function () {
|
function () {
|
||||||
getCity(id);
|
getCity(id);
|
||||||
console.log(id);
|
|
||||||
console.log(currentCity)
|
|
||||||
},
|
},
|
||||||
[id]
|
[id]
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const {cityName, emoji, date, notes}=currentCity; // Declare variables in the outer scope
|
const {cityName, flag, date, notes}=currentCity;
|
||||||
|
|
||||||
|
|
||||||
const [search, setSearchParams] = useSearchParams();
|
const [search, setSearchParams] = useSearchParams();
|
||||||
const lat = search.get("lat");
|
const lat = search.get("lat");
|
||||||
@@ -47,7 +44,7 @@ function City() {
|
|||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
<h6>City name</h6>
|
<h6>City name</h6>
|
||||||
<h3>
|
<h3>
|
||||||
{/* <span>{emoji}</span> */}
|
{/* <span>{flag}</span> */}
|
||||||
{cityName}
|
{cityName}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const formatDate = (date) =>
|
|||||||
|
|
||||||
export default function CityItem({ city }) {
|
export default function CityItem({ city }) {
|
||||||
const {currentCity,isLoading,deleteCity}=useCities();
|
const {currentCity,isLoading,deleteCity}=useCities();
|
||||||
const { cityName, emoji, date,position,id } = city;
|
const { cityName, flag, date,position,id } = city;
|
||||||
|
|
||||||
function handleClick(e){
|
function handleClick(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -25,8 +25,8 @@ export default function CityItem({ city }) {
|
|||||||
<div >
|
<div >
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<Link className={`${styles.cityItem} ${(id===currentCity.id)?styles["cityItem--active"]:''}`} to={`${city.id}?lat=${position.lat}&lng=${position.lng}`}>
|
<Link className={`${styles.cityItem} ${(id===currentCity.id)?styles["cityItem--active"]:''}`} to={`${city.id}?lat=${city.lat}&lng=${city.lng}`}>
|
||||||
<span className={styles.emoji}>{emoji}</span>
|
<span className={styles.flag}>{flag}</span>
|
||||||
<h3 className={styles.name}>{cityName}</h3>
|
<h3 className={styles.name}>{cityName}</h3>
|
||||||
<time className={styles.date}>{formatDate(date)}</time>
|
<time className={styles.date}>{formatDate(date)}</time>
|
||||||
<button className={styles.deleteBtn} onClick={handleClick}>×</button>
|
<button className={styles.deleteBtn} onClick={handleClick}>×</button>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
border-left: 5px solid var(--color-brand--2);
|
border-left: 5px solid var(--color-brand--2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji {
|
.flag {
|
||||||
font-size: 2.6rem;
|
font-size: 2.6rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import styles from "./CountryItem.module.css";
|
|||||||
function CountryItem({ country }) {
|
function CountryItem({ country }) {
|
||||||
return (
|
return (
|
||||||
<li className={styles.countryItem}>
|
<li className={styles.countryItem}>
|
||||||
{/* <span>{country.emoji}</span> */}
|
|
||||||
<span>{country}</span>
|
<span>{country}</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,14 +15,12 @@ const CountryList = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const countries = cities.reduce((countries, city) => {
|
const countries = cities.reduce((countries, city) => {
|
||||||
if (!countries.includes(city.country)) {
|
if (!countries.includes(city.flag+" "+city.country)) {
|
||||||
countries.push(city.country);
|
countries.push(city.flag+" "+city.country);
|
||||||
}
|
}
|
||||||
return countries;
|
return countries;
|
||||||
}, []);;
|
}, []);;
|
||||||
|
|
||||||
|
|
||||||
console.log(cities);
|
|
||||||
return (
|
return (
|
||||||
<ul className={styles.countryList}>
|
<ul className={styles.countryList}>
|
||||||
{countries.map((country) => (
|
{countries.map((country) => (
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// "https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=0&longitude=0"
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
@@ -11,8 +9,9 @@ import Message from "./Message";
|
|||||||
import Spinner from "./Spinner";
|
import Spinner from "./Spinner";
|
||||||
import { useCities } from "../contexts/CitiesContext";
|
import { useCities } from "../contexts/CitiesContext";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
export function convertToEmoji(countryCode) {
|
export function convertToFlag(countryCode) {
|
||||||
const codePoints = countryCode
|
const codePoints = countryCode
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.split("")
|
.split("")
|
||||||
@@ -21,6 +20,7 @@ export function convertToEmoji(countryCode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Form() {
|
function Form() {
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
const [cityName, setCityName] = useState("");
|
const [cityName, setCityName] = useState("");
|
||||||
const {createCity,isLoading}=useCities();
|
const {createCity,isLoading}=useCities();
|
||||||
const [country, setCountry] = useState("");
|
const [country, setCountry] = useState("");
|
||||||
@@ -28,7 +28,7 @@ function Form() {
|
|||||||
const [notes, setNotes] = useState("");
|
const [notes, setNotes] = useState("");
|
||||||
const [isGeocodingLoading, setIsGeocodingLoading] = useState(false);
|
const [isGeocodingLoading, setIsGeocodingLoading] = useState(false);
|
||||||
const [lat, lng] = useUrlPosition();
|
const [lat, lng] = useUrlPosition();
|
||||||
const [emoji, setEmoji] = useState("");
|
const [flag, setFlag] = useState("");
|
||||||
const [geocodingError, setGeocodingError] = useState(null);
|
const [geocodingError, setGeocodingError] = useState(null);
|
||||||
const navigate=useNavigate();
|
const navigate=useNavigate();
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -40,10 +40,8 @@ function Form() {
|
|||||||
setGeocodingError(null);
|
setGeocodingError(null);
|
||||||
try {
|
try {
|
||||||
setIsGeocodingLoading(true);
|
setIsGeocodingLoading(true);
|
||||||
const data = await fetch(
|
const response = await axios.get(`${API_URL}/geocode?latitude=${lat}&longitude=${lng}`);
|
||||||
`https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lng}`
|
const res = await response.data;
|
||||||
);
|
|
||||||
const res = await data.json();
|
|
||||||
|
|
||||||
if (res.countryCode === "") {
|
if (res.countryCode === "") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -52,7 +50,7 @@ function Form() {
|
|||||||
}
|
}
|
||||||
setCityName(res.city || res.locality || "");
|
setCityName(res.city || res.locality || "");
|
||||||
setCountry(res.countryName || "");
|
setCountry(res.countryName || "");
|
||||||
setEmoji(convertToEmoji(res.countryCode));
|
setFlag(convertToFlag(res.countryCode));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setGeocodingError(err.message);
|
setGeocodingError(err.message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -70,10 +68,11 @@ function Form() {
|
|||||||
const newCity = {
|
const newCity = {
|
||||||
cityName,
|
cityName,
|
||||||
country,
|
country,
|
||||||
emoji,
|
flag,
|
||||||
date,
|
date,
|
||||||
notes,
|
notes,
|
||||||
position: { lat, lng }
|
lat,
|
||||||
|
lng
|
||||||
};
|
};
|
||||||
await createCity(newCity);
|
await createCity(newCity);
|
||||||
navigate("/app/cities")
|
navigate("/app/cities")
|
||||||
@@ -99,7 +98,7 @@ function Form() {
|
|||||||
onChange={(e) => setCityName(e.target.value)}
|
onChange={(e) => setCityName(e.target.value)}
|
||||||
value={cityName}
|
value={cityName}
|
||||||
/>
|
/>
|
||||||
<span className={styles.flag}>{emoji}</span>
|
<span className={styles.flag}>{flag}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.row}>
|
<div className={styles.row}>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const Map = () => {
|
|||||||
|
|
||||||
const { cities } = useCities();
|
const { cities } = useCities();
|
||||||
|
|
||||||
const [mapPosition, setMapPosition] = useState([20.5937, 78.9629]);
|
const [mapPosition, setMapPosition] = useState([52.1599, 5.6399]);
|
||||||
const [mapLat,mapLng]=useUrlPosition();
|
const [mapLat,mapLng]=useUrlPosition();
|
||||||
const {isLoading:isLoadingGeolocation,position:geoLocationPosition,getPosition}=useGeolocation();
|
const {isLoading:isLoadingGeolocation,position:geoLocationPosition,getPosition}=useGeolocation();
|
||||||
|
|
||||||
@@ -34,7 +34,6 @@ const Map = () => {
|
|||||||
</Button>}
|
</Button>}
|
||||||
<div className={styles.map}>
|
<div className={styles.map}>
|
||||||
<MapContainer
|
<MapContainer
|
||||||
// center={[mapLat,mapLng]}
|
|
||||||
center={mapPosition}
|
center={mapPosition}
|
||||||
zoom={6}
|
zoom={6}
|
||||||
scrollWheelZoom={true}
|
scrollWheelZoom={true}
|
||||||
@@ -45,9 +44,8 @@ const Map = () => {
|
|||||||
url="https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
{cities.map((city) => {
|
{cities.map((city) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker position={[city.position.lat, city.position.lng]} key={city.id}>
|
<Marker position={[city.lat, city.lng]} key={city.id}>
|
||||||
<Popup>
|
<Popup>
|
||||||
{city.cityName}
|
{city.cityName}
|
||||||
<br /> {city.country}
|
<br /> {city.country}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
height: 100%;
|
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) {
|
:global(.leaflet-popup .leaflet-popup-content-wrapper) {
|
||||||
background-color: var(--color-dark--1);
|
background-color: var(--color-dark--1);
|
||||||
color: var(--color-light--2);
|
color: var(--color-light--2);
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -11,13 +11,6 @@ const Sidebar = () => {
|
|||||||
<Logo/>
|
<Logo/>
|
||||||
<AppNav/>
|
<AppNav/>
|
||||||
<Outlet/>
|
<Outlet/>
|
||||||
|
|
||||||
<footer className={styles.footer}>
|
|
||||||
<p className={styles.copyright}>
|
|
||||||
© Copyright {new Date().getFullYear()} by WorldWise corp
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,5 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: calc(100vh - 4.8rem);
|
height: calc(100vh);
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyright {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: var(--color-light--1);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../contexts/FakeAuthcontext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import styles from "./User.module.css";
|
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() {
|
function User() {
|
||||||
const navigate=useNavigate();
|
const navigate=useNavigate();
|
||||||
const {user,logout} = useAuth();
|
const {user, logout} = useAuth();
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
logout();
|
logout();
|
||||||
@@ -22,21 +14,10 @@ function User() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.user}>
|
<div className={styles.user}>
|
||||||
<img src={user.avatar} alt={user.name} />
|
<span>Welcome, {user}</span>
|
||||||
<span>Welcome, {user.name}</span>
|
|
||||||
<button onClick={handleClick}>Logout</button>
|
<button onClick={handleClick}>Logout</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default User;
|
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 `/`
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
|
|
||||||
.user img {
|
.user img {
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
height: 4rem;
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user button {
|
.user button {
|
||||||
|
|||||||
83
src/contexts/AuthContext.jsx
Normal file
83
src/contexts/AuthContext.jsx
Normal 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);
|
||||||
@@ -6,7 +6,10 @@ import {
|
|||||||
useReducer,
|
useReducer,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
const CitiesContext = createContext();
|
const CitiesContext = createContext();
|
||||||
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
cities: [],
|
cities: [],
|
||||||
@@ -46,9 +49,6 @@ function reducer(state, action) {
|
|||||||
|
|
||||||
|
|
||||||
function CitiesProvider({ children }) {
|
function CitiesProvider({ children }) {
|
||||||
// const [cities, setCities] = useState([]);
|
|
||||||
// const [isLoading, setIsLoading] = useState(false);
|
|
||||||
// const [currentCity,setCurrentCity]=useState({});
|
|
||||||
const [state, dispatch] = useReducer(reducer,initialState);
|
const [state, dispatch] = useReducer(reducer,initialState);
|
||||||
const { cities, isLoading, currentCity } = state;
|
const { cities, isLoading, currentCity } = state;
|
||||||
|
|
||||||
@@ -57,8 +57,8 @@ function CitiesProvider({ children }) {
|
|||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
dispatch({ type: "loading" });
|
dispatch({ type: "loading" });
|
||||||
try {
|
try {
|
||||||
const data = await fetch("http://localhost:8000/cities");
|
const response = await axios.get(`${API_URL}/get_items`);
|
||||||
const res = await data.json();
|
const res = await response.data;
|
||||||
|
|
||||||
dispatch({ type:"cities/loaded", payload: res });
|
dispatch({ type:"cities/loaded", payload: res });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -75,8 +75,8 @@ function CitiesProvider({ children }) {
|
|||||||
if(Number(id)===currentCity.id) return;
|
if(Number(id)===currentCity.id) return;
|
||||||
dispatch({ type: "loading" });
|
dispatch({ type: "loading" });
|
||||||
try {
|
try {
|
||||||
const data = await fetch(`http://localhost:8000/cities/${id}`);
|
const response = await axios.get(`${API_URL}/get_item?id=${id}`);
|
||||||
const res = await data.json();
|
const res = await response.data.json();
|
||||||
|
|
||||||
dispatch({ type: "city/loaded", payload: res });
|
dispatch({ type: "city/loaded", payload: res });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -86,29 +86,32 @@ function CitiesProvider({ children }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function createCity(newCity) {
|
|
||||||
|
async function createCity(newCity) {
|
||||||
dispatch({ type: "loading" });
|
dispatch({ type: "loading" });
|
||||||
try {
|
try {
|
||||||
const data = await fetch(`http://localhost:8000/cities`, {
|
const response = await axios.post(
|
||||||
method: "POST",
|
`${API_URL}/create_item`,
|
||||||
body: JSON.stringify(newCity),
|
newCity,
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
const res = await data.json();
|
);
|
||||||
dispatch({ type: "cities/created", payload: res });
|
dispatch({ type: "cities/created", payload: response.data });
|
||||||
} catch {
|
} catch (error) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "rejected",
|
type: "rejected",
|
||||||
payload: "There was an error loading the city",
|
payload: error.response?.data?.message || 'There was an error loading the city',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function deleteCity(id) {
|
|
||||||
|
async function deleteCity(id) {
|
||||||
dispatch({ type: "loading" });
|
dispatch({ type: "loading" });
|
||||||
try {
|
try {
|
||||||
const data = await fetch(`http://localhost:8000/cities/${id}`, {
|
const response = await axios.delete(`${API_URL}/delete_item?id=${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
dispatch({ type: "cities/deleted", payload: id });
|
dispatch({ type: "cities/deleted", payload: id });
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
@@ -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://unpkg.com/leaflet@1.7.1/dist/leaflet.css";
|
||||||
@import "https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap";
|
@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 {
|
:root {
|
||||||
--color-brand--1: #ffb545;
|
--color-brand--1: #ffb545;
|
||||||
--color-brand--2: #00c46a;
|
--color-brand--2: #00c46a;
|
||||||
@@ -67,12 +65,3 @@ input:focus {
|
|||||||
padding: 1rem 3rem;
|
padding: 1rem 3rem;
|
||||||
border-radius: 5px;
|
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`"
|
|
||||||
}, */
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import Sidebar from '../components/Sidebar'
|
|||||||
import styles from './AppLayout.module.css'
|
import styles from './AppLayout.module.css'
|
||||||
import Map from '../components/Map'
|
import Map from '../components/Map'
|
||||||
import User from '../components/User'
|
import User from '../components/User'
|
||||||
import { useAuth } from '../contexts/FakeAuthcontext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const AppLayout = () => {
|
const AppLayout = () => {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
.app {
|
.app {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
padding: 2.4rem;
|
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,41 +1,62 @@
|
|||||||
import PageNav from "../components/PageNav";
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import styles from "./Login.module.css";
|
import Logo from '../components/Logo'
|
||||||
import {useAuth} from "../contexts/FakeAuthcontext";
|
import styles from "./Page.module.css";
|
||||||
|
import {useAuth} from "../contexts/AuthContext";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Button from "../components/Button";
|
import Button from "../components/Button";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
// PRE-FILL FOR DEV PURPOSES
|
const [username, setUsername] = useState("");
|
||||||
const [email, setEmail] = useState("jack@example.com");
|
const [password, setPassword] = useState("");
|
||||||
const [password, setPassword] = useState("qwerty");
|
const [showError, setShowError] = useState(false);
|
||||||
const {login,isAuthenticated}=useAuth();
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const navigate=useNavigate();
|
|
||||||
|
|
||||||
function handleSubmit(e){
|
const { login, isAuthenticated, isRegistratonEnabled } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
async function handleSubmit(e) {
|
||||||
e.preventDefault();
|
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(()=>{
|
useEffect(() => {
|
||||||
if(isAuthenticated){
|
if (isAuthenticated) {
|
||||||
console.log("isAuthenticated",isAuthenticated)
|
|
||||||
navigate("/app");
|
navigate("/app");
|
||||||
}
|
}
|
||||||
},[isAuthenticated])
|
}, [isAuthenticated, navigate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.login}>
|
<main className={styles.page}>
|
||||||
<PageNav/>
|
|
||||||
<form className={styles.form} onSubmit={handleSubmit}>
|
<form className={styles.form} onSubmit={handleSubmit}>
|
||||||
<div className={styles.row}>
|
<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
|
<input
|
||||||
type="email"
|
id="username"
|
||||||
id="email"
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
value={username}
|
||||||
value={email}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,10 +70,15 @@ useEffect(()=>{
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className={styles.btnrow}>
|
||||||
<Button type='primary'>Login</Button>
|
<Button type="primary">Login</Button>
|
||||||
|
|
||||||
|
{isRegistratonEnabled && (
|
||||||
|
<a className={styles.secondbtn} href="/register">Create account</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
57
src/pages/Page.module.css
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
|
import styles from "./Page.module.css";
|
||||||
|
import Logo from '../components/Logo'
|
||||||
export default function PageNotFound() {
|
export default function PageNotFound() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<main className={styles.page}>
|
||||||
<h1>Page not found 😢</h1>
|
<div className={styles.form} >
|
||||||
|
<div className={styles.row} >
|
||||||
|
<Logo className={styles.logo}/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.row} >
|
||||||
|
<h1 className={styles.PageNotFound}>Page not found</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useAuth } from "../contexts/FakeAuthcontext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, Navigate } from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
function ProtectedRoute({children}){
|
|
||||||
|
const ProtectedRoute = ({ children }) => {
|
||||||
|
const { user } = useAuth();
|
||||||
const navigate=useNavigate();
|
const navigate=useNavigate();
|
||||||
const {isAuthenticated}=useAuth();
|
const {isAuthenticated}=useAuth();
|
||||||
|
|
||||||
useEffect(function(){
|
if (!user) {
|
||||||
if(!isAuthenticated) navigate("/");
|
return <Navigate to="/" />;
|
||||||
},[isAuthenticated,navigate])
|
}
|
||||||
|
return children;
|
||||||
return isAuthenticated? children:null;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default ProtectedRoute;
|
export default ProtectedRoute;
|
||||||
111
src/pages/Register.jsx
Normal file
111
src/pages/Register.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
// import eslint from "vite-plugin-eslint"
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
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'),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user