diff --git a/client/package.json b/client/package.json index c9f6fa5..748e19a 100644 --- a/client/package.json +++ b/client/package.json @@ -7,6 +7,7 @@ "@emotion/styled": "^11.9.3", "@fontsource/roboto": "^4.5.7", "@mui/icons-material": "^5.8.4", + "@mui/lab": "^5.0.0-alpha.89", "@mui/material": "^5.8.7", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", @@ -17,6 +18,7 @@ "@types/react-dom": "^18.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.33.1", "react-scripts": "5.0.1", "typescript": "^4.4.2", "web-vitals": "^2.1.0" diff --git a/client/src/App.tsx b/client/src/App.tsx index af33dba..deb4e0d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,6 +9,8 @@ import { AuthProvider } from "./providers/AuthProvider"; import "./index.css"; import { LoginDialog } from "./components/LoginDialog"; +import { UnautorizedBlock } from "./components/UnautorizedBlock"; +import { NewAccount } from "./pages/NewAccount/NewAccount"; function App() { return ( @@ -16,6 +18,10 @@ function App() { + } + unauthenticatedContent={} + /> diff --git a/client/src/components/LoginDialog.tsx b/client/src/components/LoginDialog.tsx index edc08d9..491de2d 100644 --- a/client/src/components/LoginDialog.tsx +++ b/client/src/components/LoginDialog.tsx @@ -7,11 +7,11 @@ import { TextField, } from "@mui/material"; import { useState } from "react"; -import { useAuth } from "../providers/AuthProvider"; +import { useAuth } from "../hooks/useAuth"; export const LoginDialog = () => { const { isLoginDialogOpen, setIsLoginDialogOpen, login } = useAuth(); - console.log(isLoginDialogOpen); + const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); diff --git a/client/src/components/Topbar.tsx b/client/src/components/Topbar.tsx index 29aea0c..197d91c 100644 --- a/client/src/components/Topbar.tsx +++ b/client/src/components/Topbar.tsx @@ -1,5 +1,5 @@ import { AppBar, Button, Toolbar, Typography } from "@mui/material"; -import { useAuth } from "../providers/AuthProvider"; +import { useAuth } from "../hooks/useAuth"; export const Topbar = () => { const { authenticated, logout, setIsLoginDialogOpen } = useAuth(); diff --git a/client/src/components/UnautorizedBlock.tsx b/client/src/components/UnautorizedBlock.tsx new file mode 100644 index 0000000..d7762df --- /dev/null +++ b/client/src/components/UnautorizedBlock.tsx @@ -0,0 +1,14 @@ +import { useAuth } from "../hooks/useAuth"; + +export type UnautorizedBlockProps = { + protectedContent: React.ReactNode; + unauthenticatedContent: React.ReactNode; +}; + +export const UnautorizedBlock = (props: UnautorizedBlockProps) => { + const { authenticated } = useAuth(); + + return ( + <>{authenticated ? props.protectedContent : props.unauthenticatedContent} + ); +}; diff --git a/client/src/hooks/useAuth.ts b/client/src/hooks/useAuth.ts new file mode 100644 index 0000000..a16fb5e --- /dev/null +++ b/client/src/hooks/useAuth.ts @@ -0,0 +1,12 @@ +import { useContext } from "react"; +import { AuthContext } from "../providers/AuthProvider"; + +export const useAuth = () => { + const context = useContext(AuthContext); + + if (context === null) { + throw new Error("You probably forgot to put ."); + } + + return context; +}; diff --git a/client/src/pages/NewAccount/NewAccount.tsx b/client/src/pages/NewAccount/NewAccount.tsx new file mode 100644 index 0000000..203a124 --- /dev/null +++ b/client/src/pages/NewAccount/NewAccount.tsx @@ -0,0 +1,84 @@ +import { LoadingButton } from "@mui/lab"; +import { Alert, Stack, TextField } from "@mui/material"; +import { useState } from "react"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { useAuth } from "../../hooks/useAuth"; +import { createApiClient } from "../../utils/apiFetch"; + +type NewAccountForm = { + email: string; + password: string; +}; + +export const NewAccount = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const apiClient = createApiClient(); + + const { login } = useAuth(); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm(); + + const onSubmit: SubmitHandler = (data) => { + setLoading(true); + + apiClient("users", { + method: "POST", + body: JSON.stringify(data), + }) + .then(async (res) => { + const result = await res.json(); + if (result.error) { + setError(result.error); + } else { + login(data.email, data.password); + } + }) + .finally(() => setLoading(false)); + }; + return ( +
+

New Account Page

+ {error && {error}} +
+ + + + + Create account + + + +
+ ); +}; diff --git a/client/src/providers/AuthProvider.tsx b/client/src/providers/AuthProvider.tsx index 75f206d..c3c8c8b 100644 --- a/client/src/providers/AuthProvider.tsx +++ b/client/src/providers/AuthProvider.tsx @@ -1,10 +1,5 @@ -import { - createContext, - useCallback, - useContext, - useMemo, - useState, -} from "react"; +import { createContext, useCallback, useMemo, useState } from "react"; +import { createApiClient } from "../utils/apiFetch"; type LoginCallback = (email: string, password: string) => void; type LogoutCallback = () => void; @@ -16,19 +11,10 @@ type AuthProviderValue = { logout: LogoutCallback; setIsLoginDialogOpen: React.Dispatch>; isLoginDialogOpen: boolean; + apiClient: typeof fetch; }; -const AuthContext = createContext(null); - -export const useAuth = () => { - const context = useContext(AuthContext); - - if (context === null) { - throw new Error("You probably forgot to put ."); - } - - return context; -}; +export const AuthContext = createContext(null); type AuthProviderProps = { children: React.ReactNode; @@ -36,16 +22,29 @@ type AuthProviderProps = { export const AuthProvider = ({ children, ...rest }: AuthProviderProps) => { const [loading] = useState(true); - const [token] = useState(null); + const [token, setToken] = useState(null); const [isLoginDialogOpen, setIsLoginDialogOpen] = useState(false); + const apiClient = createApiClient(token ?? ""); - const login = useCallback((_email, __password) => { - throw new Error("Not implemented yet"); - }, []); + const login = useCallback((email, password) => { + apiClient("users/sign_in", { + method: "POST", + body: JSON.stringify({ + email, + password, + }), + }).then(async (res) => { + setToken(await res.json()); + }); + }, [apiClient, setToken]); const logout = useCallback(() => { - throw new Error("Not implemented yet"); - }, []); + apiClient("users/sign_out", { + method: "DELETE", + }).then(() => { + setToken(null); + }); + }, [apiClient, setToken]); const authenticated = !!token; @@ -65,6 +64,7 @@ export const AuthProvider = ({ children, ...rest }: AuthProviderProps) => { value={{ ...providerValue, ...rest, + apiClient, isLoginDialogOpen, setIsLoginDialogOpen, }} diff --git a/client/src/utils/apiFetch.ts b/client/src/utils/apiFetch.ts new file mode 100644 index 0000000..24fa205 --- /dev/null +++ b/client/src/utils/apiFetch.ts @@ -0,0 +1,18 @@ +const host = "http://localhost:5000/"; + +export const createApiClient = + (token?: string): typeof fetch => + (input, init = {}): Promise => { + const { headers, ...rest } = init; + + const customInt: RequestInit = { + headers: { + "Content-Type": "application/json", + "X-Access-Token": token ?? "", + ...headers, + }, + ...rest, + }; + + return fetch(host + input, customInt); + }; diff --git a/client/yarn.lock b/client/yarn.lock index a72a42e..9b0dc44 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1169,6 +1169,39 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== +"@date-io/core@^2.14.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.14.0.tgz#03e9b9b9fc8e4d561c32dd324df0f3ccd967ef14" + integrity sha512-qFN64hiFjmlDHJhu+9xMkdfDG2jLsggNxKXglnekUpXSq8faiqZgtHm2lsHCUuaPDTV6wuXHcCl8J1GQ5wLmPw== + +"@date-io/date-fns@^2.11.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.14.0.tgz#92ab150f488f294c135c873350d154803cebdbea" + integrity sha512-4fJctdVyOd5cKIKGaWUM+s3MUXMuzkZaHuTY15PH70kU1YTMrCoauA7hgQVx9qj0ZEbGrH9VSPYJYnYro7nKiA== + dependencies: + "@date-io/core" "^2.14.0" + +"@date-io/dayjs@^2.11.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.14.0.tgz#8d4e93e1d473bb5f25210866204dc33384ca4c20" + integrity sha512-4fRvNWaOh7AjvOyJ4h6FYMS7VHLQnIEeAV5ahv6sKYWx+1g1UwYup8h7+gPuoF+sW2hTScxi7PVaba2Jk/U8Og== + dependencies: + "@date-io/core" "^2.14.0" + +"@date-io/luxon@^2.11.1": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.14.0.tgz#cd1641229e00a899625895de3a31e3aaaf66629f" + integrity sha512-KmpBKkQFJ/YwZgVd0T3h+br/O0uL9ZdE7mn903VPAG2ZZncEmaUfUdYKFT7v7GyIKJ4KzCp379CRthEbxevEVg== + dependencies: + "@date-io/core" "^2.14.0" + +"@date-io/moment@^2.11.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.14.0.tgz#8300abd6ae8c55d8edee90d118db3cef0b1d4f58" + integrity sha512-VsoLXs94GsZ49ecWuvFbsa081zEv2xxG7d+izJsqGa2L8RPZLlwk27ANh87+SNnOUpp+qy2AoCAf0mx4XXhioA== + dependencies: + "@date-io/core" "^2.14.0" + "@emotion/babel-plugin@^11.7.1": version "11.9.2" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.9.2.tgz#723b6d394c89fb2ef782229d92ba95a740576e95" @@ -1604,6 +1637,22 @@ dependencies: "@babel/runtime" "^7.17.2" +"@mui/lab@^5.0.0-alpha.89": + version "5.0.0-alpha.89" + resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.89.tgz#cc61560f16eebc3f44890835ee22cdc560ce4d48" + integrity sha512-u5bMi/V+Utwouo9awVzGasj/LudlRqPFyMo2L5/y60uFo0EaG17bt1jh/U7smQCdjd+7tvJ39HNMkEmIoGr7BQ== + dependencies: + "@babel/runtime" "^7.17.2" + "@mui/base" "5.0.0-alpha.88" + "@mui/system" "^5.8.7" + "@mui/utils" "^5.8.6" + "@mui/x-date-pickers" "5.0.0-alpha.1" + clsx "^1.2.0" + prop-types "^15.8.1" + react-is "^17.0.2" + react-transition-group "^4.4.2" + rifm "^0.12.1" + "@mui/material@^5.8.7": version "5.8.7" resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.8.7.tgz#28617c5b8a9e354e300f19fc38e1286ba1e15ad3" @@ -1659,7 +1708,7 @@ resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.4.tgz#4185c05d6df63ec673cda15feab80440abadc764" integrity sha512-uveM3byMbthO+6tXZ1n2zm0W3uJCQYtwt/v5zV5I77v2v18u0ITkb8xwhsDD2i3V2Kye7SaNR6FFJ6lMuY/WqQ== -"@mui/utils@^5.8.6": +"@mui/utils@^5.6.0", "@mui/utils@^5.8.6": version "5.8.6" resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.8.6.tgz#543de64a64bb9135316ecfd91d75a8740544d79f" integrity sha512-QM2Sd1xZo2jOt2Vz5Rmro+pi2FLJyiv4+OjxkUwXR3oUM65KSMAMLl/KNYU55s3W3DLRFP5MVwE4FhAbHseHAg== @@ -1670,6 +1719,21 @@ prop-types "^15.8.1" react-is "^17.0.2" +"@mui/x-date-pickers@5.0.0-alpha.1": + version "5.0.0-alpha.1" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-5.0.0-alpha.1.tgz#7450b5544b9ed655db41891c74e2c5f652fbedb7" + integrity sha512-dLPkRiIn2Gr0momblxiOnIwrxn4SijVix+8e08mwAGWhiWcmWep1O9XTRDpZsjB0kjHYCf+kZjlRX4dxnj2acg== + dependencies: + "@date-io/date-fns" "^2.11.0" + "@date-io/dayjs" "^2.11.0" + "@date-io/luxon" "^2.11.1" + "@date-io/moment" "^2.11.0" + "@mui/utils" "^5.6.0" + clsx "^1.1.1" + prop-types "^15.7.2" + react-transition-group "^4.4.2" + rifm "^0.12.1" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -3225,7 +3289,7 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" -clsx@^1.2.0: +clsx@^1.1.1, clsx@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== @@ -7346,7 +7410,7 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.6.2, prop-types@^15.8.1: +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -7479,6 +7543,11 @@ react-error-overlay@^6.0.11: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== +react-hook-form@^7.33.1: + version "7.33.1" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.33.1.tgz#8c4410e3420788d3b804d62cc4c142915c2e46d0" + integrity sha512-ydTfTxEJdvgjCZBj5DDXRc58oTEfnFupEwwTAQ9FSKzykEJkX+3CiAkGtAMiZG7IPWHuzgT6AOBfogiKhUvKgg== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -7781,6 +7850,11 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rifm@^0.12.1: + version "0.12.1" + resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.12.1.tgz#8fa77f45b7f1cda2a0068787ac821f0593967ac4" + integrity sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg== + rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"