From 95ad7275d95d7e49a530fdba6abe027ebdcf28d1 Mon Sep 17 00:00:00 2001 From: thetiagogil Date: Thu, 23 Jan 2025 12:41:26 +0000 Subject: [PATCH] add supabase connection and auth context with login and logout functions --- package-lock.json | 164 ++++++++++++++++++++++++++- package.json | 4 +- src/api/mock-data.ts | 2 +- src/api/useUserApi.ts | 20 ++++ src/components/navigation/navbar.tsx | 8 +- src/contexts/auth.context.tsx | 58 ++++++++++ src/lib/constants.ts | 4 + src/lib/supabase.ts | 7 ++ src/main.tsx | 7 +- src/models/user.model.ts | 7 ++ src/pages/signup.page.tsx | 27 ++++- src/router/app.tsx | 6 +- src/theme.ts | 60 ++-------- src/utils/toast.ts | 5 + 14 files changed, 316 insertions(+), 63 deletions(-) create mode 100644 src/api/useUserApi.ts create mode 100644 src/contexts/auth.context.tsx create mode 100644 src/lib/constants.ts create mode 100644 src/lib/supabase.ts create mode 100644 src/models/user.model.ts create mode 100644 src/utils/toast.ts diff --git a/package-lock.json b/package-lock.json index bee4557..e1e37e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,11 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@mui/joy": "^5.0.0-beta.51", + "@supabase/supabase-js": "^2.48.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^7.1.3" + "react-router-dom": "^7.1.3", + "react-toastify": "^11.0.3" }, "devDependencies": { "@eslint/js": "^9.17.0", @@ -1738,6 +1740,80 @@ "win32" ] }, + "node_modules/@supabase/auth-js": { + "version": "2.67.3", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.67.3.tgz", + "integrity": "sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", + "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.18.0.tgz", + "integrity": "sha512-DqUEVF5ZFytrYLAuywnUR0srhZbzSoLZePMfGFamYMNdMttBLwAWXJNJ5kBDHn2gK2n87NwLsIshUbdyxPtpsw==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", + "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.18.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.48.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.48.0.tgz", + "integrity": "sha512-8ql5ra3NOIHLBYoFZpxYHRx05J/FmJAQW9EYax0lU4DsU1/5PC4yb2hxMcCIf0oimLcBgsogIaAqba+/9jaUVg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.67.3", + "@supabase/functions-js": "2.4.4", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.18.0", + "@supabase/realtime-js": "2.11.2", + "@supabase/storage-js": "2.7.1" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1803,12 +1879,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -1836,6 +1927,15 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz", @@ -3523,6 +3623,19 @@ "react-dom": ">=18" } }, + "node_modules/react-toastify": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.3.tgz", + "integrity": "sha512-cbPtHJPfc0sGqVwozBwaTrTu1ogB9+BLLjd4dDXd863qYLj7DGrQ2sg5RAChjFUB4yc3w8iXOtWcJqPK/6xqRQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -3756,6 +3869,12 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", @@ -3825,6 +3944,12 @@ "typescript": ">=4.8.4 <5.8.0" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", @@ -3938,6 +4063,22 @@ } } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3964,6 +4105,27 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index f7f9c4b..a0c6be8 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,11 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@mui/joy": "^5.0.0-beta.51", + "@supabase/supabase-js": "^2.48.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^7.1.3" + "react-router-dom": "^7.1.3", + "react-toastify": "^11.0.3" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/src/api/mock-data.ts b/src/api/mock-data.ts index a91b627..4644091 100644 --- a/src/api/mock-data.ts +++ b/src/api/mock-data.ts @@ -1,6 +1,6 @@ export const mockUser = { isAuth: true, - hasAvatar: true, + hasAvatar: false, isAvatarLoading: false }; diff --git a/src/api/useUserApi.ts b/src/api/useUserApi.ts new file mode 100644 index 0000000..c794980 --- /dev/null +++ b/src/api/useUserApi.ts @@ -0,0 +1,20 @@ +import { supabase } from "../lib/supabase"; +import { UserModel } from "../models/user.model"; +import { showToast } from "../utils/toast"; + +export const getUserByEmail = async (email: string) => { + try { + const { data, error } = await supabase.from("users").select().eq("email", email).single(); + if (error) { + if (error.details === "The result contains 0 rows") { + showToast("error", "Invalid credential."); + } else { + showToast("error", "Failed to get user."); + } + } + return data as UserModel; + } catch (error) { + console.error("Failed to get user:", error); + throw error; + } +}; diff --git a/src/components/navigation/navbar.tsx b/src/components/navigation/navbar.tsx index 2c5139c..b0e898b 100644 --- a/src/components/navigation/navbar.tsx +++ b/src/components/navigation/navbar.tsx @@ -1,15 +1,17 @@ import { Avatar, Dropdown, IconButton, Menu, MenuButton, MenuItem, Stack, Typography } from "@mui/joy"; -import { useState } from "react"; +import { useContext, useState } from "react"; import { ArrowDownOutlined } from "../../assets/icons/arrow-down"; import { ArrowUpOutlined } from "../../assets/icons/arrow-up"; import { NotificationsOutlined } from "../../assets/icons/notifications-icon"; import { SubvisualLogo } from "../../assets/icons/subvisual-logo"; +import { AuthContext } from "../../contexts/auth.context"; type NavbarProps = { hasSubvisualIcon?: boolean; }; export const Navbar = ({ hasSubvisualIcon }: NavbarProps) => { + const { handleLogout } = useContext(AuthContext); const [isOpen, setIsOpen] = useState(false); return ( @@ -34,9 +36,7 @@ export const Navbar = ({ hasSubvisualIcon }: NavbarProps) => { {isOpen ? : } - Test - Test - Test + Logout diff --git a/src/contexts/auth.context.tsx b/src/contexts/auth.context.tsx new file mode 100644 index 0000000..81d1165 --- /dev/null +++ b/src/contexts/auth.context.tsx @@ -0,0 +1,58 @@ +import { createContext, ReactNode, useEffect, useState } from "react"; +import { getUserByEmail } from "../api/useUserApi"; + +type AuthContextProps = { + userId: string | null; + isAuthenticated: boolean; + hasAvatar: boolean; + handleLogin: (email: string) => Promise; + handleLogout: () => Promise; +}; + +type AuthContextProvider = { + children: ReactNode; +}; + +export const AuthContext = createContext({} as AuthContextProps); + +export const AuthContextProvider = ({ children }: AuthContextProvider) => { + const [userId, setUserId] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [hasAvatar, setHasAvatar] = useState(false); + + useEffect(() => { + const userId = window.localStorage.getItem("userId"); + if (userId) { + setUserId(userId); + setIsAuthenticated(true); + } + }, []); + + const handleLogin = async (email: string) => { + try { + const user = await getUserByEmail(email); + if (user) { + setUserId(user.id); + setHasAvatar(user.hasAvatar); + window.localStorage.setItem("userId", user.id); + setIsAuthenticated(true); + } + } catch (error) { + console.error("Login failed:", error); + throw error; + } + }; + + const handleLogout = async () => { + setIsAuthenticated(false); + setUserId(null); + setHasAvatar(false); + window.localStorage.removeItem("userId"); + }; + + return ( + + {children} + + ); +}; diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000..6cd9f2f --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,4 @@ +export const ENV_VARS = { + SUPABASE_URL: import.meta.env.VITE_SUPABASE_URL, + SUPABASE_KEY: import.meta.env.VITE_SUPABASE_KEY +}; diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts new file mode 100644 index 0000000..c05efe3 --- /dev/null +++ b/src/lib/supabase.ts @@ -0,0 +1,7 @@ +import { createClient } from "@supabase/supabase-js"; +import { ENV_VARS } from "./constants"; + +const SUPABASE_URL = ENV_VARS.SUPABASE_URL; +const SUPABASE_KEY = ENV_VARS.SUPABASE_KEY; + +export const supabase = createClient(SUPABASE_URL, SUPABASE_KEY); diff --git a/src/main.tsx b/src/main.tsx index 5ea5879..5de97f1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,6 +2,8 @@ import { CssBaseline, CssVarsProvider } from "@mui/joy"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; +import { ToastContainer } from "react-toastify"; +import { AuthContextProvider } from "./contexts/auth.context"; import "./main.css"; import { App } from "./router/app"; import { theme } from "./theme"; @@ -11,7 +13,10 @@ createRoot(document.getElementById("root")!).render( - + + + + diff --git a/src/models/user.model.ts b/src/models/user.model.ts new file mode 100644 index 0000000..4b9b72c --- /dev/null +++ b/src/models/user.model.ts @@ -0,0 +1,7 @@ +export type UserModel = { + id: string; + name: string; + email: string; + hasAvatar: boolean; + strengths: number[]; +}; diff --git a/src/pages/signup.page.tsx b/src/pages/signup.page.tsx index e1b8884..d088036 100644 --- a/src/pages/signup.page.tsx +++ b/src/pages/signup.page.tsx @@ -1,6 +1,19 @@ import { Box, Button, Input, Link, Stack, Typography } from "@mui/joy"; +import { FormEvent, useContext, useState } from "react"; +import { AuthContext } from "../contexts/auth.context"; export const SignupPage = () => { + const { handleLogin } = useContext(AuthContext); + const [email, setEmail] = useState("tiago.gil@subvisual.academy"); + const [isLoadingSubmit, setIsLoadingSubmit] = useState(false); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setIsLoadingSubmit(true); + await handleLogin(email); + setIsLoadingSubmit(false); + }; + return ( { - - - + + setEmail?.(e.target.value)} + /> + By signing up you're agreeing to our{" "} - + Terms of service & Privacy Policy. diff --git a/src/router/app.tsx b/src/router/app.tsx index 7debfc2..3bbf345 100644 --- a/src/router/app.tsx +++ b/src/router/app.tsx @@ -1,5 +1,7 @@ +import { useContext } from "react"; import { Navigate, Route, Routes } from "react-router-dom"; import { mockUser } from "../api/mock-data"; +import { AuthContext } from "../contexts/auth.context"; import { AvatarCreatePage } from "../pages/avatar-create.page"; import { AvatarResultsPage } from "../pages/avatar-results.page"; import { LearnPage } from "../pages/learn.page"; @@ -8,9 +10,11 @@ import { SignupPage } from "../pages/signup.page"; import { TeamPage } from "../pages/team.page"; export const App = () => { + const { isAuthenticated } = useContext(AuthContext); + return ( - {!mockUser.isAuth ? ( + {!isAuthenticated ? ( <> } /> } /> diff --git a/src/theme.ts b/src/theme.ts index 3b82062..19fa32a 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -1,5 +1,4 @@ import { extendTheme } from "@mui/joy/styles"; -import { addHexTransparency } from "./utils/add-hex-transparency"; import { colors } from "./utils/colors"; declare module "@mui/joy/styles" { @@ -14,6 +13,9 @@ export const theme = extendTheme({ colorSchemes: { light: { palette: { + primary: { + 500: colors.subvisual.primary + }, subvisual: colors.subvisual, strengths: colors.strengths, neutral: colors.neutral @@ -71,55 +73,13 @@ export const theme = extendTheme({ }, JoyButton: { styleOverrides: { - root: ({ ownerState, theme }) => { - const sharedStyles = { - border: "2px solid", - borderRadius: 20, - transition: "0.3s" - }; - - const solidStyles = { - backgroundColor: theme.palette.subvisual.primary, - color: theme.palette.neutral.white, - borderColor: theme.palette.subvisual.primary, - "&:hover": { - backgroundColor: theme.palette.subvisual.primaryDark, - borderColor: theme.palette.subvisual.primaryDark - }, - "&:focus": { - borderColor: theme.palette.subvisual.pink - }, - "&:disabled": { - backgroundColor: theme.palette.neutral.light, - borderColor: theme.palette.neutral.light, - color: theme.palette.neutral.white - } - }; - - const outlinedStyles = { - backgroundColor: "transparent", - color: theme.palette.subvisual.primary, - borderColor: theme.palette.subvisual.primary, - "&:hover": { - color: theme.palette.subvisual.primaryDark, - borderColor: theme.palette.subvisual.primaryDark - }, - "&:focus": { - backgroundColor: addHexTransparency(theme.palette.subvisual.primaryDark, "10%"), - borderColor: theme.palette.subvisual.pink - }, - "&:disabled": { - color: theme.palette.neutral.light, - borderColor: theme.palette.neutral.light - } - }; - - return { - ...sharedStyles, - ...(ownerState.variant === "solid" ? solidStyles : {}), - ...(ownerState.variant === "outlined" ? outlinedStyles : {}) - }; - } + root: ({ theme }) => ({ + borderRadius: 20, + transition: "0.3s", + "&:focus": { + borderColor: theme.palette.subvisual.pink + } + }) } } } diff --git a/src/utils/toast.ts b/src/utils/toast.ts new file mode 100644 index 0000000..05fce92 --- /dev/null +++ b/src/utils/toast.ts @@ -0,0 +1,5 @@ +import { toast } from "react-toastify"; + +export const showToast = (type: "success" | "error" | "info", message: string) => { + toast[type](message, { closeOnClick: true, pauseOnHover: true, autoClose: 2000 }); +};