finished project
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
settings: { react: { version: '18.2' } },
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
"no-unused-vars": ["off"]
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends":"react-app"
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<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>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+13731
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "worldwise",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"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 "
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"json-server": "^0.17.3",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-datepicker": "^4.16.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-leaflet": "^4.2.1",
|
||||||
|
"react-router-dom": "^6.14.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.14",
|
||||||
|
"@types/react-dom": "^18.2.6",
|
||||||
|
"@vitejs/plugin-react": "^4.0.1",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.1",
|
||||||
|
"vite": "^4.4.0",
|
||||||
|
"vite-plugin-eslint": "^1.8.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 336 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
+50
@@ -0,0 +1,50 @@
|
|||||||
|
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 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 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="app"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<AppLayout />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Route index element={<Navigate replace to="cities" />} />
|
||||||
|
<Route path="cities" element={<CityList />} />
|
||||||
|
<Route path="cities/:id" element={<City />} />
|
||||||
|
<Route path="countries" element={<CountryList />} />
|
||||||
|
<Route path="form" element={<Form />} />
|
||||||
|
</Route>
|
||||||
|
<Route path="*" element={<PageNotFound />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</CitiesProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styles from './AppNav.module.css'
|
||||||
|
import { Link, NavLink } from 'react-router-dom'
|
||||||
|
|
||||||
|
const AppNav = () => {
|
||||||
|
return (
|
||||||
|
<nav className={styles.nav}>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<NavLink to="cities" >cities</NavLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavLink to="countries" >Countries</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppNav
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
.nav {
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--color-dark--2);
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a:link,
|
||||||
|
.nav a:visited {
|
||||||
|
display: block;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0.5rem 2rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS Modules feature */
|
||||||
|
.nav a:global(.active) {
|
||||||
|
background-color: var(--color-dark--0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Button from "./Button";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
|
const BackButton = () => {
|
||||||
|
const navigate=useNavigate();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="back"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(-1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
← Back
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BackButton;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styles from './Button.module.css'
|
||||||
|
|
||||||
|
const Button = ({children,onClick,type}) => {
|
||||||
|
return (
|
||||||
|
<button onClick={onClick} className={`${styles.btn} ${styles[type]}`}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
.btn {
|
||||||
|
color: inherit;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.8rem 1.6rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
font-weight: 700;
|
||||||
|
background-color: var(--color-brand--2);
|
||||||
|
color: var(--color-dark--1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back {
|
||||||
|
font-weight: 600;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position {
|
||||||
|
font-weight: 700;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
bottom: 4rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: var(--color-brand--2);
|
||||||
|
color: var(--color-dark--1);
|
||||||
|
box-shadow: 0 0.4rem 1.2rem rgba(36, 42, 46, 0.16);
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import styles from "./City.module.css";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useCities } from "../contexts/CitiesContext";
|
||||||
|
import ButtonBack from "./Button";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
|
import BackButton from "./BackButton";
|
||||||
|
|
||||||
|
const formatDate = (date) =>
|
||||||
|
new Intl.DateTimeFormat("en", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
weekday: "long",
|
||||||
|
}).format(new Date(date));
|
||||||
|
|
||||||
|
function City() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const { currentCity, getCity ,isLoading} = useCities();
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
function () {
|
||||||
|
getCity(id);
|
||||||
|
console.log(id);
|
||||||
|
console.log(currentCity)
|
||||||
|
},
|
||||||
|
[id]
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
const {cityName, emoji, date, notes}=currentCity; // Declare variables in the outer scope
|
||||||
|
|
||||||
|
|
||||||
|
const [search, setSearchParams] = useSearchParams();
|
||||||
|
const lat = search.get("lat");
|
||||||
|
const lng = search.get("lng");
|
||||||
|
|
||||||
|
|
||||||
|
if(isLoading){
|
||||||
|
return <Spinner/>
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.city}>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<h6>City name</h6>
|
||||||
|
<h3>
|
||||||
|
{/* <span>{emoji}</span> */}
|
||||||
|
{cityName}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.row}>
|
||||||
|
<h6>You went to {cityName} on</h6>
|
||||||
|
<p>{formatDate(date || null)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{notes && (
|
||||||
|
<div className={styles.row}>
|
||||||
|
<h6>Your notes</h6>
|
||||||
|
<p>{notes}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.row}>
|
||||||
|
<h6>Learn more</h6>
|
||||||
|
<a
|
||||||
|
href={`https://en.wikipedia.org/wiki/${cityName}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Check out {cityName} on Wikipedia →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<BackButton/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default City;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
.city {
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
max-height: 70%;
|
||||||
|
background-color: var(--color-dark--2);
|
||||||
|
border-radius: 7px;
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.city h6 {
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--color-light--1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.city h3 {
|
||||||
|
font-size: 1.9rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.city h3 span {
|
||||||
|
font-size: 3.2rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.city p {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.city a:link,
|
||||||
|
.city a:visited {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: var(--color-brand--1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./CityItem.module.css";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useCities } from "../contexts/CitiesContext";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
|
|
||||||
|
const formatDate = (date) =>
|
||||||
|
new Intl.DateTimeFormat("en", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
weekday: "long",
|
||||||
|
}).format(new Date(date));
|
||||||
|
|
||||||
|
export default function CityItem({ city }) {
|
||||||
|
const {currentCity,isLoading,deleteCity}=useCities();
|
||||||
|
const { cityName, emoji, date,position,id } = city;
|
||||||
|
|
||||||
|
function handleClick(e){
|
||||||
|
e.preventDefault();
|
||||||
|
deleteCity(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
<h3 className={styles.name}>{cityName}</h3>
|
||||||
|
<time className={styles.date}>{formatDate(date)}</time>
|
||||||
|
<button className={styles.deleteBtn} onClick={handleClick}>×</button>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
.cityItem,
|
||||||
|
.cityItem:link,
|
||||||
|
.cityItem:visited {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.6rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
background-color: var(--color-dark--2);
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-left: 5px solid var(--color-brand--2);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cityItem--active {
|
||||||
|
border: 2px solid var(--color-brand--2);
|
||||||
|
border-left: 5px solid var(--color-brand--2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji {
|
||||||
|
font-size: 2.6rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteBtn {
|
||||||
|
height: 2rem;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background-color: var(--color-dark--1);
|
||||||
|
color: var(--color-light--2);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 400;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteBtn:hover {
|
||||||
|
background-color: var(--color-brand--1);
|
||||||
|
color: var(--color-dark--1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./CityList.module.css";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
|
import CityItem from "./CityItem";
|
||||||
|
import Message from "./Message";
|
||||||
|
import { useCities } from "../contexts/CitiesContext";
|
||||||
|
|
||||||
|
|
||||||
|
const CityList = () => {
|
||||||
|
const {cities,isLoading}=useCities();
|
||||||
|
if (isLoading) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
if(!cities.length){
|
||||||
|
return <Message message={"Add your first city by clicking on the map"}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={styles.cityList}>
|
||||||
|
{cities.map((city) => (
|
||||||
|
<CityItem city={city} key={city.id} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CityList;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
.cityList {
|
||||||
|
width: 100%;
|
||||||
|
height: 65vh;
|
||||||
|
list-style: none;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cityList::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import styles from "./CountryItem.module.css";
|
||||||
|
|
||||||
|
function CountryItem({ country }) {
|
||||||
|
return (
|
||||||
|
<li className={styles.countryItem}>
|
||||||
|
{/* <span>{country.emoji}</span> */}
|
||||||
|
<span>{country}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CountryItem;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.countryItem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
|
||||||
|
font-size: 1.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
background-color: var(--color-dark--2);
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
border-left: 5px solid var(--color-brand--1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.countryItem span:first-child {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./CountryList.module.css";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
|
import CountryItem from "./CountryItem";
|
||||||
|
import Message from "./Message";
|
||||||
|
import { useCities } from "../contexts/CitiesContext";
|
||||||
|
|
||||||
|
const CountryList = () => {
|
||||||
|
const {cities,isLoading}=useCities();
|
||||||
|
if (isLoading) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
if(!cities.length){
|
||||||
|
return <Message message={"Add your first city by clicking on the map"}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const countries = cities.reduce((countries, city) => {
|
||||||
|
if (!countries.includes(city.country)) {
|
||||||
|
countries.push(city.country);
|
||||||
|
}
|
||||||
|
return countries;
|
||||||
|
}, []);;
|
||||||
|
|
||||||
|
|
||||||
|
console.log(cities);
|
||||||
|
return (
|
||||||
|
<ul className={styles.countryList}>
|
||||||
|
{countries.map((country) => (
|
||||||
|
<CountryItem country={country} key={country} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CountryList;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.countryList {
|
||||||
|
width: 100%;
|
||||||
|
height: 65vh;
|
||||||
|
list-style: none;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
align-content: start;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
// "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";
|
||||||
|
import styles from "./Form.module.css";
|
||||||
|
import Button from "./Button";
|
||||||
|
import BackButton from "./BackButton";
|
||||||
|
import { useUrlPosition } from "../hooks/useUrlPosition";
|
||||||
|
import Message from "./Message";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
|
import { useCities } from "../contexts/CitiesContext";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export function convertToEmoji(countryCode) {
|
||||||
|
const codePoints = countryCode
|
||||||
|
.toUpperCase()
|
||||||
|
.split("")
|
||||||
|
.map((char) => 127397 + char.charCodeAt());
|
||||||
|
return String.fromCodePoint(...codePoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Form() {
|
||||||
|
const [cityName, setCityName] = useState("");
|
||||||
|
const {createCity,isLoading}=useCities();
|
||||||
|
const [country, setCountry] = useState("");
|
||||||
|
const [date, setDate] = useState(new Date());
|
||||||
|
const [notes, setNotes] = useState("");
|
||||||
|
const [isGeocodingLoading, setIsGeocodingLoading] = useState(false);
|
||||||
|
const [lat, lng] = useUrlPosition();
|
||||||
|
const [emoji, setEmoji] = useState("");
|
||||||
|
const [geocodingError, setGeocodingError] = useState(null);
|
||||||
|
const navigate=useNavigate();
|
||||||
|
useEffect(
|
||||||
|
function () {
|
||||||
|
if (!lat || !lng) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
async function fetchData() {
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (res.countryCode === "") {
|
||||||
|
throw new Error(
|
||||||
|
"That doesn't seems to be a country. Click somewhere else"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setCityName(res.city || res.locality || "");
|
||||||
|
setCountry(res.countryName || "");
|
||||||
|
setEmoji(convertToEmoji(res.countryCode));
|
||||||
|
} catch (err) {
|
||||||
|
setGeocodingError(err.message);
|
||||||
|
} finally {
|
||||||
|
setIsGeocodingLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchData();
|
||||||
|
},
|
||||||
|
[lat, lng]
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if(!cityName || !date) return;
|
||||||
|
const newCity = {
|
||||||
|
cityName,
|
||||||
|
country,
|
||||||
|
emoji,
|
||||||
|
date,
|
||||||
|
notes,
|
||||||
|
position: { lat, lng }
|
||||||
|
};
|
||||||
|
await createCity(newCity);
|
||||||
|
navigate("/app/cities")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lat || !lng) {
|
||||||
|
return <Message message="Click on the map to select a location" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGeocodingLoading) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
if (geocodingError) {
|
||||||
|
return <Message message={geocodingError} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<form className={`${styles.form} ${isLoading?styles.loading : ""} `} onSubmit={handleSubmit}>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<label htmlFor="cityName">City name</label>
|
||||||
|
<input
|
||||||
|
id="cityName"
|
||||||
|
onChange={(e) => setCityName(e.target.value)}
|
||||||
|
value={cityName}
|
||||||
|
/>
|
||||||
|
<span className={styles.flag}>{emoji}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.row}>
|
||||||
|
<label htmlFor="date">When did you go to {cityName}?</label>
|
||||||
|
<DatePicker
|
||||||
|
id="date"
|
||||||
|
selected={date}
|
||||||
|
onChange={(date) => setDate(date)}
|
||||||
|
dateFormat="dd/MM/yyyy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.row}>
|
||||||
|
<label htmlFor="notes">Notes about your trip to {cityName}</label>
|
||||||
|
<textarea
|
||||||
|
id="notes"
|
||||||
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
|
value={notes}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Button type="primary">Add</Button>
|
||||||
|
<BackButton />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
.form {
|
||||||
|
background-color: var(--color-dark--2);
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 2rem 3rem;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
top: 2.7rem;
|
||||||
|
font-size: 2.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form.loading {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form.loading button {
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: var(--color-light--1);
|
||||||
|
border: 1px solid var(--color-light--1);
|
||||||
|
color: var(--color-dark--0);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.react-datepicker) {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import styles from "./Logo.module.css";
|
||||||
|
|
||||||
|
function Logo() {
|
||||||
|
return (
|
||||||
|
<Link to="/">
|
||||||
|
<img src="/logo.png" alt="WorldWise logo" className={styles.logo} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Logo;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.logo {
|
||||||
|
height: 5.2rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { React, useEffect, useState } from "react";
|
||||||
|
import styles from "./Map.module.css";
|
||||||
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
import { MapContainer, Marker, Popup, TileLayer, useMap, useMapEvent } from "react-leaflet";
|
||||||
|
import { useCities } from "../contexts/CitiesContext";
|
||||||
|
import {useGeolocation} from "../hooks/useGeolocation";
|
||||||
|
import Button from "./Button";
|
||||||
|
import { useUrlPosition } from "../hooks/useUrlPosition";
|
||||||
|
|
||||||
|
const Map = () => {
|
||||||
|
|
||||||
|
const { cities } = useCities();
|
||||||
|
|
||||||
|
const [mapPosition, setMapPosition] = useState([20.5937, 78.9629]);
|
||||||
|
const [mapLat,mapLng]=useUrlPosition();
|
||||||
|
const {isLoading:isLoadingGeolocation,position:geoLocationPosition,getPosition}=useGeolocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mapLat && mapLng) {
|
||||||
|
setMapPosition([mapLat, mapLng]);
|
||||||
|
}
|
||||||
|
}, [mapLat, mapLng]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (geoLocationPosition) {
|
||||||
|
setMapPosition([geoLocationPosition.lat, geoLocationPosition.lng]);
|
||||||
|
}
|
||||||
|
},[geoLocationPosition])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.mapContainer}>
|
||||||
|
{<Button type="position" onClick={getPosition} disabled={isLoadingGeolocation}>
|
||||||
|
{ !isLoadingGeolocation?"Use your position":"Loading..."}
|
||||||
|
</Button>}
|
||||||
|
<div className={styles.map}>
|
||||||
|
<MapContainer
|
||||||
|
// center={[mapLat,mapLng]}
|
||||||
|
center={mapPosition}
|
||||||
|
zoom={6}
|
||||||
|
scrollWheelZoom={true}
|
||||||
|
className={styles.map}
|
||||||
|
>
|
||||||
|
<TileLayer
|
||||||
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
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}>
|
||||||
|
<Popup>
|
||||||
|
{city.cityName}
|
||||||
|
<br /> {city.country}
|
||||||
|
</Popup>
|
||||||
|
</Marker>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<DetectClick/>
|
||||||
|
<ChangeCenter position={mapPosition} />
|
||||||
|
</MapContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function ChangeCenter({position}){
|
||||||
|
const map=useMap();
|
||||||
|
map.setView(position);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetectClick(){
|
||||||
|
const navigate=useNavigate();
|
||||||
|
useMapEvent(
|
||||||
|
{click:(e)=>navigate(`form?lat=${e.latlng.lat}&lng=${e.latlng.lng}`)}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default Map;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
.mapContainer {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-dark--2);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map {
|
||||||
|
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);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding-right: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.leaflet-popup .leaflet-popup-content) {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.leaflet-popup .leaflet-popup-content span:first-child) {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.leaflet-popup .leaflet-popup-tip) {
|
||||||
|
background-color: var(--color-dark--1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.leaflet-popup-content-wrapper) {
|
||||||
|
border-left: 5px solid var(--color-brand--2);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import styles from "./Message.module.css";
|
||||||
|
|
||||||
|
function Message({ message }) {
|
||||||
|
return (
|
||||||
|
<p className={styles.message}>
|
||||||
|
<span role="img">👋</span> {message}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Message;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.message {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
width: 80%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav ul {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a:link,
|
||||||
|
.nav a:visited {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-light--2);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS Modules feature */
|
||||||
|
.nav a:global(.active) {
|
||||||
|
color: var(--color-brand--2);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.ctaLink:link,
|
||||||
|
a.ctaLink:visited {
|
||||||
|
background-color: var(--color-brand--2);
|
||||||
|
color: var(--color-dark--0);
|
||||||
|
padding: 0.8rem 2rem;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import AppNav from "./AppNav"
|
||||||
|
import Logo from './Logo'
|
||||||
|
import styles from "./Sidebar.module.css"
|
||||||
|
import { Outlet } from 'react-router-dom'
|
||||||
|
|
||||||
|
|
||||||
|
const Sidebar = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.sidebar}>
|
||||||
|
<Logo/>
|
||||||
|
<AppNav/>
|
||||||
|
<Outlet/>
|
||||||
|
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
<p className={styles.copyright}>
|
||||||
|
© Copyright {new Date().getFullYear()} by WorldWise corp
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sidebar
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.sidebar {
|
||||||
|
flex-basis: 56rem;
|
||||||
|
background-color: var(--color-dark--1);
|
||||||
|
padding: 3rem 5rem 3.5rem 5rem;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import styles from "./Spinner.module.css";
|
||||||
|
|
||||||
|
function Spinner() {
|
||||||
|
return (
|
||||||
|
<div className={styles.spinnerContainer}>
|
||||||
|
<div className={styles.spinner}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Spinner;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.spinnerContainer {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(#0000 10%, var(--color-light--2));
|
||||||
|
-webkit-mask: radial-gradient(farthest-side, #0000 calc(100% - 8px), #000 0);
|
||||||
|
animation: rotate 1.5s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
to {
|
||||||
|
transform: rotate(1turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import Spinner from "./Spinner";
|
||||||
|
import styles from "./SpinnerFullPage.module.css";
|
||||||
|
|
||||||
|
function SpinnerFullPage() {
|
||||||
|
return (
|
||||||
|
<div className={styles.spinnerFullpage}>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SpinnerFullPage;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.spinnerFullpage {
|
||||||
|
margin: 2.5rem;
|
||||||
|
height: calc(100vh - 5rem);
|
||||||
|
background-color: var(--color-dark--1);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useAuth } from "../contexts/FakeAuthcontext";
|
||||||
|
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();
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
logout();
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.user}>
|
||||||
|
<img src={user.avatar} alt={user.name} />
|
||||||
|
<span>Welcome, {user.name}</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 `/`
|
||||||
|
*/
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
.user {
|
||||||
|
position: absolute;
|
||||||
|
top: 4.2rem;
|
||||||
|
right: 4.2rem;
|
||||||
|
background-color: var(--color-dark--1);
|
||||||
|
padding: 1rem 1.4rem;
|
||||||
|
border-radius: 7px;
|
||||||
|
z-index: 999;
|
||||||
|
box-shadow: 0 0.8rem 2.4rem rgba(36, 42, 46, 0.5);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user img {
|
||||||
|
border-radius: 100px;
|
||||||
|
height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user button {
|
||||||
|
background-color: var(--color-dark--2);
|
||||||
|
border-radius: 7px;
|
||||||
|
border: none;
|
||||||
|
padding: 0.6rem 1.2rem;
|
||||||
|
color: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
createContext,
|
||||||
|
useState,
|
||||||
|
useContext,
|
||||||
|
useReducer,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
const CitiesContext = createContext();
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
cities: [],
|
||||||
|
isLoading: false,
|
||||||
|
currentCity: {},
|
||||||
|
error: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
function reducer(state, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case "loading":
|
||||||
|
return { ...state, isLoading: true };
|
||||||
|
case "cities/loaded":
|
||||||
|
return { ...state, isLoading: false,cities: action.payload };
|
||||||
|
case "city/loaded":
|
||||||
|
return { ...state, currentCity: action.payload, isLoading: false };
|
||||||
|
case "cities/created":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cities: [...state.cities, action.payload],
|
||||||
|
isLoading: false,
|
||||||
|
currentCity:action.payload
|
||||||
|
};
|
||||||
|
case "cities/deleted":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isLoading: false,
|
||||||
|
cities: state.cities.filter((city) => city.id !== action.payload),
|
||||||
|
currentCity:{}
|
||||||
|
};
|
||||||
|
case "rejected":
|
||||||
|
return { ...state, isLoading: false, error: action.payload };
|
||||||
|
default:
|
||||||
|
throw new Error(`Action not supported`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(function () {
|
||||||
|
async function fetchData() {
|
||||||
|
dispatch({ type: "loading" });
|
||||||
|
try {
|
||||||
|
const data = await fetch("http://localhost:8000/cities");
|
||||||
|
const res = await data.json();
|
||||||
|
|
||||||
|
dispatch({ type:"cities/loaded", payload: res });
|
||||||
|
} catch {
|
||||||
|
dispatch({
|
||||||
|
type: "rejected",
|
||||||
|
payload: "There was an error loading the cities",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function getCity(id) {
|
||||||
|
if(Number(id)===currentCity.id) return;
|
||||||
|
dispatch({ type: "loading" });
|
||||||
|
try {
|
||||||
|
const data = await fetch(`http://localhost:8000/cities/${id}`);
|
||||||
|
const res = await data.json();
|
||||||
|
|
||||||
|
dispatch({ type: "city/loaded", payload: res });
|
||||||
|
} catch {
|
||||||
|
dispatch({
|
||||||
|
type: "rejected",
|
||||||
|
payload: "There was an error loading the city",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function createCity(newCity) {
|
||||||
|
dispatch({ type: "loading" });
|
||||||
|
try {
|
||||||
|
const data = await fetch(`http://localhost:8000/cities`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(newCity),
|
||||||
|
headers: {
|
||||||
|
"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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function deleteCity(id) {
|
||||||
|
dispatch({ type: "loading" });
|
||||||
|
try {
|
||||||
|
const data = await fetch(`http://localhost:8000/cities/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
dispatch({ type: "cities/deleted", payload: id });
|
||||||
|
} catch {
|
||||||
|
dispatch({
|
||||||
|
type: "rejected",
|
||||||
|
payload: "There was an error deleting the city",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CitiesContext.Provider
|
||||||
|
value={{
|
||||||
|
cities,
|
||||||
|
isLoading,
|
||||||
|
currentCity,
|
||||||
|
getCity,
|
||||||
|
createCity,
|
||||||
|
deleteCity,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CitiesContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useCities() {
|
||||||
|
const context = useContext(CitiesContext);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { CitiesProvider, useCities };
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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 };
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function useGeolocation(defaultPosition=null) {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [position, setPosition] = useState(defaultPosition);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
function getPosition() {
|
||||||
|
if (!navigator.geolocation)
|
||||||
|
return setError("Your browser does not support geolocation");
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(pos) => {
|
||||||
|
setPosition({
|
||||||
|
lat: pos.coords.latitude,
|
||||||
|
lng: pos.coords.longitude,
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
setError(error.message);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isLoading, position, error, getPosition };
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
export function useUrlPosition() {
|
||||||
|
const [search] = useSearchParams();
|
||||||
|
const lat = search.get("lat");
|
||||||
|
const lng = search.get("lng");
|
||||||
|
return [lat,lng];
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/* 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;
|
||||||
|
|
||||||
|
--color-dark--0: #242a2e;
|
||||||
|
--color-dark--1: #2d3439;
|
||||||
|
--color-dark--2: #42484d;
|
||||||
|
--color-light--1: #aaa;
|
||||||
|
--color-light--2: #ececec;
|
||||||
|
--color-light--3: #d6dee0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 62.5%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Manrope", sans-serif;
|
||||||
|
color: var(--color-light--2);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.8rem 1.2rem;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: var(--color-light--3);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta:link,
|
||||||
|
.cta:visited {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: var(--color-brand--2);
|
||||||
|
color: var(--color-dark--1);
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
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`"
|
||||||
|
}, */
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.jsx'
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const AppLayout = () => {
|
||||||
|
const {isAuthenticated}=useAuth();
|
||||||
|
return (
|
||||||
|
<div className={styles.app}>
|
||||||
|
<Sidebar/>
|
||||||
|
<Map/>
|
||||||
|
{isAuthenticated && <User/>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppLayout
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.app {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 2.4rem;
|
||||||
|
overscroll-behavior-y: none;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import PageNav from "../components/PageNav";
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import styles from "./Login.module.css";
|
||||||
|
import {useAuth} from "../contexts/FakeAuthcontext";
|
||||||
|
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();
|
||||||
|
|
||||||
|
function handleSubmit(e){
|
||||||
|
e.preventDefault();
|
||||||
|
if(email && password) login(email,password);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
if(isAuthenticated){
|
||||||
|
console.log("isAuthenticated",isAuthenticated)
|
||||||
|
navigate("/app");
|
||||||
|
}
|
||||||
|
},[isAuthenticated])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className={styles.login}>
|
||||||
|
<PageNav/>
|
||||||
|
<form className={styles.form} onSubmit={handleSubmit}>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<label htmlFor="email">Email address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
value={email}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
<Button type='primary'>Login</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default function PageNotFound() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Page not found 😢</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useAuth } from "../contexts/FakeAuthcontext";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
|
function ProtectedRoute({children}){
|
||||||
|
const navigate=useNavigate();
|
||||||
|
const {isAuthenticated}=useAuth();
|
||||||
|
|
||||||
|
useEffect(function(){
|
||||||
|
if(!isAuthenticated) navigate("/");
|
||||||
|
},[isAuthenticated,navigate])
|
||||||
|
|
||||||
|
return isAuthenticated? children:null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProtectedRoute;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
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()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user