finished project
This commit is contained in:
21
.eslintrc.cjs
Normal file
21
.eslintrc.cjs
Normal file
@@ -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"]
|
||||
},
|
||||
}
|
||||
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends":"react-app"
|
||||
}
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -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?
|
||||
100
data/cities.json
Normal file
100
data/cities.json
Normal file
@@ -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
index.html
Normal file
13
index.html
Normal file
@@ -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>
|
||||
13731
package-lock.json
generated
Normal file
13731
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
package.json
Normal file
33
package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
BIN
public/bg.jpg
Normal file
BIN
public/bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 336 KiB |
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
public/img-1.jpg
Normal file
BIN
public/img-1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
BIN
public/img-2.jpg
Normal file
BIN
public/img-2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -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
src/App.jsx
Normal file
50
src/App.jsx
Normal file
@@ -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;
|
||||
20
src/components/AppNav.jsx
Normal file
20
src/components/AppNav.jsx
Normal file
@@ -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
|
||||
28
src/components/AppNav.module.css
Normal file
28
src/components/AppNav.module.css
Normal file
@@ -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);
|
||||
}
|
||||
21
src/components/BackButton.jsx
Normal file
21
src/components/BackButton.jsx
Normal file
@@ -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;
|
||||
12
src/components/Button.jsx
Normal file
12
src/components/Button.jsx
Normal file
@@ -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
|
||||
35
src/components/Button.module.css
Normal file
35
src/components/Button.module.css
Normal file
@@ -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);
|
||||
}
|
||||
86
src/components/City.jsx
Normal file
86
src/components/City.jsx
Normal file
@@ -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;
|
||||
47
src/components/City.module.css
Normal file
47
src/components/City.module.css
Normal file
@@ -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);
|
||||
}
|
||||
38
src/components/CityItem.jsx
Normal file
38
src/components/CityItem.jsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
54
src/components/CityItem.module.css
Normal file
54
src/components/CityItem.module.css
Normal file
@@ -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);
|
||||
}
|
||||
27
src/components/CityList.jsx
Normal file
27
src/components/CityList.jsx
Normal file
@@ -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;
|
||||
15
src/components/CityList.module.css
Normal file
15
src/components/CityList.module.css
Normal file
@@ -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;
|
||||
}
|
||||
12
src/components/CountryItem.jsx
Normal file
12
src/components/CountryItem.jsx
Normal file
@@ -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;
|
||||
19
src/components/CountryItem.module.css
Normal file
19
src/components/CountryItem.module.css
Normal file
@@ -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;
|
||||
}
|
||||
35
src/components/CountryList.jsx
Normal file
35
src/components/CountryList.jsx
Normal file
@@ -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;
|
||||
12
src/components/CountryList.module.css
Normal file
12
src/components/CountryList.module.css
Normal file
@@ -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;
|
||||
}
|
||||
132
src/components/Form.jsx
Normal file
132
src/components/Form.jsx
Normal file
@@ -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;
|
||||
44
src/components/Form.module.css
Normal file
44
src/components/Form.module.css
Normal file
@@ -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;
|
||||
}
|
||||
12
src/components/Logo.jsx
Normal file
12
src/components/Logo.jsx
Normal file
@@ -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;
|
||||
3
src/components/Logo.module.css
Normal file
3
src/components/Logo.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.logo {
|
||||
height: 5.2rem;
|
||||
}
|
||||
80
src/components/Map.jsx
Normal file
80
src/components/Map.jsx
Normal file
@@ -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;
|
||||
38
src/components/Map.module.css
Normal file
38
src/components/Map.module.css
Normal file
@@ -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);
|
||||
}
|
||||
11
src/components/Message.jsx
Normal file
11
src/components/Message.jsx
Normal file
@@ -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;
|
||||
7
src/components/Message.module.css
Normal file
7
src/components/Message.module.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.message {
|
||||
text-align: center;
|
||||
font-size: 1.8rem;
|
||||
width: 80%;
|
||||
margin: 2rem auto;
|
||||
font-weight: 600;
|
||||
}
|
||||
24
src/components/PageNav.jsx
Normal file
24
src/components/PageNav.jsx
Normal file
@@ -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
|
||||
34
src/components/PageNav.module.css
Normal file
34
src/components/PageNav.module.css
Normal file
@@ -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;
|
||||
}
|
||||
25
src/components/Sidebar.jsx
Normal file
25
src/components/Sidebar.jsx
Normal file
@@ -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
|
||||
19
src/components/Sidebar.module.css
Normal file
19
src/components/Sidebar.module.css
Normal file
@@ -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);
|
||||
}
|
||||
11
src/components/Spinner.jsx
Normal file
11
src/components/Spinner.jsx
Normal file
@@ -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;
|
||||
21
src/components/Spinner.module.css
Normal file
21
src/components/Spinner.module.css
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
12
src/components/SpinnerFullPage.jsx
Normal file
12
src/components/SpinnerFullPage.jsx
Normal file
@@ -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;
|
||||
5
src/components/SpinnerFullPage.module.css
Normal file
5
src/components/SpinnerFullPage.module.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.spinnerFullpage {
|
||||
margin: 2.5rem;
|
||||
height: calc(100vh - 5rem);
|
||||
background-color: var(--color-dark--1);
|
||||
}
|
||||
42
src/components/User.jsx
Normal file
42
src/components/User.jsx
Normal file
@@ -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 `/`
|
||||
*/
|
||||
34
src/components/User.module.css
Normal file
34
src/components/User.module.css
Normal file
@@ -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;
|
||||
}
|
||||
145
src/contexts/CitiesContext.jsx
Normal file
145
src/contexts/CitiesContext.jsx
Normal file
@@ -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 };
|
||||
57
src/contexts/FakeAuthcontext.jsx
Normal file
57
src/contexts/FakeAuthcontext.jsx
Normal file
@@ -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 };
|
||||
29
src/hooks/useGeolocation.js
Normal file
29
src/hooks/useGeolocation.js
Normal file
@@ -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 };
|
||||
}
|
||||
8
src/hooks/useUrlPosition.js
Normal file
8
src/hooks/useUrlPosition.js
Normal file
@@ -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];
|
||||
}
|
||||
78
src/index.css
Normal file
78
src/index.css
Normal file
@@ -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`"
|
||||
}, */
|
||||
|
||||
11
src/main.jsx
Normal file
11
src/main.jsx
Normal file
@@ -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>,
|
||||
)
|
||||
22
src/pages/AppLayout.jsx
Normal file
22
src/pages/AppLayout.jsx
Normal file
@@ -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
|
||||
7
src/pages/AppLayout.module.css
Normal file
7
src/pages/AppLayout.module.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.app {
|
||||
height: 100vh;
|
||||
padding: 2.4rem;
|
||||
overscroll-behavior-y: none;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
24
src/pages/Homepage.jsx
Normal file
24
src/pages/Homepage.jsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
34
src/pages/Homepage.module.css
Normal file
34
src/pages/Homepage.module.css
Normal file
@@ -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;
|
||||
}
|
||||
58
src/pages/Login.jsx
Normal file
58
src/pages/Login.jsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
26
src/pages/Login.module.css
Normal file
26
src/pages/Login.module.css
Normal file
@@ -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;
|
||||
}
|
||||
7
src/pages/PageNotFound.jsx
Normal file
7
src/pages/PageNotFound.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function PageNotFound() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Page not found 😢</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
src/pages/Pricing.jsx
Normal file
26
src/pages/Pricing.jsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
30
src/pages/Product.jsx
Normal file
30
src/pages/Product.jsx
Normal file
@@ -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>
|
||||
);
|
||||
}
|
||||
34
src/pages/Product.module.css
Normal file
34
src/pages/Product.module.css
Normal file
@@ -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;
|
||||
}
|
||||
17
src/pages/ProtectedRoute.jsx
Normal file
17
src/pages/ProtectedRoute.jsx
Normal file
@@ -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;
|
||||
7
vite.config.js
Normal file
7
vite.config.js
Normal file
@@ -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