diff --git a/package-lock.json b/package-lock.json index e5346eb..3b37ac3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "react-dom": "^18.1.0", "react-loader-spinner": "^5.4.5", "react-redux": "^8.1.3", + "react-router-dom": "^6.17.0", "react-scripts": "5.0.1", "redux-persist": "^6.0.0", "web-vitals": "^2.1.3" @@ -2631,6 +2632,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.10.0.tgz", + "integrity": "sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -11827,6 +11836,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.17.0.tgz", + "integrity": "sha512-YJR3OTJzi3zhqeJYADHANCGPUu9J+6fT5GLv82UWRGSxu6oJYCKVmxUcaBQuGm9udpWmPsvpme/CdHumqgsoaA==", + "dependencies": { + "@remix-run/router": "1.10.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.17.0.tgz", + "integrity": "sha512-qWHkkbXQX+6li0COUUPKAUkxjNNqPJuiBd27dVwQGDNsuFBdMbrS6UZ0CLYc4CsbdLYTckn4oB4tGDuPZpPhaQ==", + "dependencies": { + "@remix-run/router": "1.10.0", + "react-router": "6.17.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -16438,6 +16477,11 @@ "reselect": "^4.1.8" } }, + "@remix-run/router": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.10.0.tgz", + "integrity": "sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw==" + }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -23035,6 +23079,23 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" }, + "react-router": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.17.0.tgz", + "integrity": "sha512-YJR3OTJzi3zhqeJYADHANCGPUu9J+6fT5GLv82UWRGSxu6oJYCKVmxUcaBQuGm9udpWmPsvpme/CdHumqgsoaA==", + "requires": { + "@remix-run/router": "1.10.0" + } + }, + "react-router-dom": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.17.0.tgz", + "integrity": "sha512-qWHkkbXQX+6li0COUUPKAUkxjNNqPJuiBd27dVwQGDNsuFBdMbrS6UZ0CLYc4CsbdLYTckn4oB4tGDuPZpPhaQ==", + "requires": { + "@remix-run/router": "1.10.0", + "react-router": "6.17.0" + } + }, "react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/package.json b/package.json index cc834de..63443bf 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "react-dom": "^18.1.0", "react-loader-spinner": "^5.4.5", "react-redux": "^8.1.3", + "react-router-dom": "^6.17.0", "react-scripts": "5.0.1", "redux-persist": "^6.0.0", "web-vitals": "^2.1.3" diff --git a/src/components/App.jsx b/src/components/App.jsx index ac50725..a1c2426 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -1,31 +1,42 @@ -import React, { useEffect } from 'react'; -import { ContactForm } from './ContactForm/ContactForm'; -import { Filter } from './Filter/Filter'; -import { ContactsList } from './ContactsList/ContactList'; -import { useDispatch, useSelector } from 'react-redux'; -import { fetchContacts } from 'redux/operations'; -import { selectError, selectLoading } from 'redux/selectors'; -import { Loading } from './Loading'; -export function App() { - const isLoading = useSelector(selectLoading); - const error = useSelector(selectError); - const dispatch = useDispatch(); - - useEffect(() => { - dispatch(fetchContacts()); - }, [dispatch]); +import { Contacts } from 'pages/Contacts/Contacts'; +import { Layout } from 'pages/Layout/Layout'; +import { Login } from 'pages/Login/Login'; +import { Register } from 'pages/Register/Register'; +import { Suspense } from 'react'; +import { Route, Routes } from 'react-router-dom'; +import { AppBar } from './AppBar/AppBar'; +export function App() { return ( -
- {isLoading && } - {error && 'something went wrong'} -
-

Phonebook

- -

Contacts

- - -
-
+ <> + + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + ); } diff --git a/src/components/AppBar/AppBar.jsx b/src/components/AppBar/AppBar.jsx new file mode 100644 index 0000000..79febe8 --- /dev/null +++ b/src/components/AppBar/AppBar.jsx @@ -0,0 +1,9 @@ +import { Navigation } from "components/Navigation/Navigation" + +export const AppBar = () =>{ + return( +
+ +
+ ) +} \ No newline at end of file diff --git a/src/components/Navigation/Navigation.jsx b/src/components/Navigation/Navigation.jsx new file mode 100644 index 0000000..7ffbf16 --- /dev/null +++ b/src/components/Navigation/Navigation.jsx @@ -0,0 +1,26 @@ +import { Link } from 'react-router-dom'; +import css from './Navigation.module.css'; + +export function Navigation() { + return ( + + ); +} diff --git a/src/components/Navigation/Navigation.module.css b/src/components/Navigation/Navigation.module.css new file mode 100644 index 0000000..75ee3cc --- /dev/null +++ b/src/components/Navigation/Navigation.module.css @@ -0,0 +1,20 @@ +.list{ + display: flex; + gap: 20px; + list-style: none; + padding: 0; +} +.link{ + font-size: 20px; + text-decoration: none; + color: pink; + transition: color 200ms ease-in-out; +} +.link:hover, +.link:focus{ + color: rgb(241, 131, 149); +} +.nav{ + display: flex; + justify-content: center; +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 92e1faa..4cad879 100644 --- a/src/index.js +++ b/src/index.js @@ -5,12 +5,15 @@ import { Provider } from 'react-redux'; import { persistor, store } from 'redux/store'; import './index.css'; import { PersistGate } from 'redux-persist/integration/react'; +import { BrowserRouter } from 'react-router-dom'; ReactDOM.createRoot(document.getElementById('root')).render( + + diff --git a/src/pages/Contacts/Contacts.jsx b/src/pages/Contacts/Contacts.jsx new file mode 100644 index 0000000..df6dd81 --- /dev/null +++ b/src/pages/Contacts/Contacts.jsx @@ -0,0 +1,5 @@ +export const Contacts = () =>{ + return( +
+ ) +} \ No newline at end of file diff --git a/src/pages/Contacts/Contacts.module.css b/src/pages/Contacts/Contacts.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/Layout/Layout.jsx b/src/pages/Layout/Layout.jsx new file mode 100644 index 0000000..e6df3ba --- /dev/null +++ b/src/pages/Layout/Layout.jsx @@ -0,0 +1,35 @@ +import React, { Suspense, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchContacts } from 'redux/operations'; +import { selectError, selectLoading } from 'redux/selectors'; +import { ContactForm } from 'components/ContactForm/ContactForm'; +import { Filter } from 'components/Filter/Filter'; +import { ContactsList } from 'components/ContactsList/ContactList'; +import { Loading } from 'components/Loading'; +import { Outlet } from 'react-router-dom'; + +export const Layout = () => { + const isLoading = useSelector(selectLoading); + const error = useSelector(selectError); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchContacts()); + }, [dispatch]); + return ( +
+ {isLoading && } + {error && 'something went wrong'} +
+

Phonebook

+ +

Contacts

+ + +
+ Loading page...
}> + + + + ); +}; diff --git a/src/pages/Layout/Layout.module.css b/src/pages/Layout/Layout.module.css new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/Login/Login.jsx b/src/pages/Login/Login.jsx new file mode 100644 index 0000000..19ff54b --- /dev/null +++ b/src/pages/Login/Login.jsx @@ -0,0 +1,32 @@ +import css from './Login.module.css'; + +export const Login = () => { + return ( +
+
+

Login

+
    +
  • + +
  • +
  • + +
  • +
+ +
+
+ ); +}; diff --git a/src/pages/Login/Login.module.css b/src/pages/Login/Login.module.css new file mode 100644 index 0000000..2987fae --- /dev/null +++ b/src/pages/Login/Login.module.css @@ -0,0 +1,53 @@ +.form { + display: flex; + text-align: center; + flex-direction: column; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); + width: 300px; + height: 320px; + background-color: white; + border-radius: 8px; +} +.title { + color: rgb(53, 189, 235); + margin: 44px 0; +} +.list { + display: flex; + flex-direction: column; + gap: 20px; + list-style: none; + padding: 0; + margin: 0; + margin-bottom: 44px; +} +.input { + border-radius: 3px; + border: 1px rgb(130, 209, 235) solid; + width: 74%; + padding: 5px; + transition: border 200ms ease-in-out; +} +.input:hover, +.input:focus { + border: 1px rgb(53, 189, 235) solid; +} +.button { + align-self: center; + width: 50%; + border-radius: 16px; + padding: 6px 2px; + border: 1px rgb(53, 189, 235) solid; + background-color: rgb(130, 209, 235); + color: white; + cursor: pointer; + transition: background-color 200ms ease-in-out; +} +.button:hover, +.button:focus { + background-color: rgb(53, 189, 235); +} diff --git a/src/pages/Register/Register.jsx b/src/pages/Register/Register.jsx new file mode 100644 index 0000000..1b49143 --- /dev/null +++ b/src/pages/Register/Register.jsx @@ -0,0 +1,48 @@ +import css from './Register.module.css'; +export const Register = () => { + const handleSubmit = e =>{ + e.preventDefault() + const newUser = { + name: e.target.elements.name.value, + email: e.target.elements.email.value, + password: e.target.elements.password.value + } + console.log(newUser) + } + return ( +
+
+

Registration

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ +
+
+ ); +}; diff --git a/src/pages/Register/Register.module.css b/src/pages/Register/Register.module.css new file mode 100644 index 0000000..07b054e --- /dev/null +++ b/src/pages/Register/Register.module.css @@ -0,0 +1,53 @@ +.form { + display: flex; + text-align: center; + flex-direction: column; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); + width: 300px; + height: 320px; + background-color: white; + border-radius: 8px; +} +.title { + color: green; + margin: 32px 0; +} +.list { + display: flex; + flex-direction: column; + gap: 20px; + list-style: none; + padding: 0; + margin: 0; + margin-bottom: 32px; +} +.input { + border-radius: 3px; + border: 1px rgb(2, 170, 2) solid; + width: 74%; + padding: 5px; + transition: border 200ms ease-in-out; +} +.input:hover, +.input:focus{ + border: 1px green solid; +} +.button { + align-self: center; + width: 50%; + border-radius: 16px; + padding: 6px 2px; + border: 1px green solid; + background-color: rgb(2, 170, 2); + color: white; + cursor: pointer; + transition: background-color 200ms ease-in-out; +} +.button:hover, +.button:focus { + background-color: green; +} diff --git a/src/redux/auth/operations.js b/src/redux/auth/operations.js new file mode 100644 index 0000000..2f71e38 --- /dev/null +++ b/src/redux/auth/operations.js @@ -0,0 +1,66 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import axios from 'axios'; + +axios.defaults.baseURL = 'https://connections-api.herokuapp.com/'; + +const setAuthHeader = token => { + axios.defaults.headers.common.Authorization = `Bearer ${token}`; +}; + +const clearAuthHeader = () => { + axios.defaults.headers.common.Authorization = ''; +}; + +export const register = createAsyncThunk( + 'auth/register', + async (credentials, thunkAPI) => { + try { + const response = await axios.post('users/signup', credentials); + setAuthHeader(response.data.token); + return response.data; + } catch (e) { + thunkAPI.rejectWithValue(e.message); + } + } +); + +export const logIn = createAsyncThunk( + 'auth/login', + async (credentials, thunkAPI) => { + try { + const response = await axios.post('users/login', credentials); + setAuthHeader(response.data.token); + return response.data; + } catch (e) { + thunkAPI.rejectWithValue(e.message); + } + } +); + +export const logOut = createAsyncThunk('auth/logout', async (_, thunkAPI) => { + try { + await axios.post('users/logout'); + clearAuthHeader(); + } catch (e) { + thunkAPI.rejectWithValue(e.message); + } +}); + +export const refreshUser = createAsyncThunk( + 'auth/refresh', + async (_, thunkAPI) => { + const state = thunkAPI.getState(); + const persistedToken = state.auth.token; + + if (persistedToken === null) { + return thunkAPI.rejectWithValue('oops unable'); + } + try { + setAuthHeader(persistedToken); + const response = await axios.get('users/current'); + return response.data; + } catch (e) { + thunkAPI.rejectWithValue(e.message); + } + } +); diff --git a/src/redux/auth/selectors.js b/src/redux/auth/selectors.js new file mode 100644 index 0000000..e69de29 diff --git a/src/redux/auth/slice.js b/src/redux/auth/slice.js new file mode 100644 index 0000000..e69de29