diff --git a/app/assets/oauth_providers/facebook-icon.png b/app/assets/oauth_providers/facebook-icon.png deleted file mode 100644 index 38b0370..0000000 Binary files a/app/assets/oauth_providers/facebook-icon.png and /dev/null differ diff --git a/app/components/SidePanel.tsx b/app/components/SidePanel.tsx index b0739ae..d4e62fb 100644 --- a/app/components/SidePanel.tsx +++ b/app/components/SidePanel.tsx @@ -1,18 +1,30 @@ +import { Link } from '@remix-run/react'; + import { useTheme } from '~/utils/Navbar/ThemeProvider'; +import type { ContextProps } from '../utils/types/ContextProps.type'; + + type Props = { sidePanelIsShown: boolean; setSidePanelIsShown: React.Dispatch>; + user: ContextProps["user"]; + supabase: ContextProps["supabase"] }; export default function SidePanel( - {sidePanelIsShown, setSidePanelIsShown} + {sidePanelIsShown, setSidePanelIsShown, user, supabase} : Props){ const [theme, setTheme] = useTheme(); const toggleTheme = () => { setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light")); }; + const logout = async () => { + const {error} = await supabase.auth.signOut(); + if (error) alert("Error while logging out."); + } + return
setSidePanelIsShown(false)}>CLOSE + + {user?
+

Logged in as {user.email}

+ +
: + + } +
} \ No newline at end of file diff --git a/app/components/SpinnerSVG.tsx b/app/components/SpinnerSVG.tsx new file mode 100644 index 0000000..89f4be4 --- /dev/null +++ b/app/components/SpinnerSVG.tsx @@ -0,0 +1,24 @@ + +type Props = { + size: number; +}; + +export default function SidePanel({size} : Props){ + return + + + + + + + + + + + + + + + + ; +} \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx index 2204da9..ac29365 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,15 +1,16 @@ import type { LinksFunction, LoaderFunction } from "@remix-run/node"; -import type { Database } from '../database.types' +import type { User } from "@supabase/supabase-js"; +import { Database } from "database.types"; import { json } from "@remix-run/node"; import { - Links, - Meta, - Outlet, - Scripts, - useLoaderData, - useRevalidator + Links, + Meta, + Outlet, + Scripts, + useLoaderData, + useRevalidator } from "@remix-run/react"; import { useState, useEffect } from "react"; import { createBrowserClient } from '@supabase/ssr' @@ -25,91 +26,101 @@ import Navbar from "./components/Navbar"; import SidePanel from "./components/SidePanel"; + + export const links: LinksFunction = () => [ - { rel: "stylesheet", href: stylesheet }, + { rel: "stylesheet", href: stylesheet }, ]; export type LoaderData = { - theme: ThemeType | null; - env: { [key: string]: string } + theme: ThemeType | null; + env: { [key: string]: string } }; export const loader: LoaderFunction = async ({ request }) => { - const themeSession = await getThemeSession(request); + const themeSession = await getThemeSession(request); - const data: LoaderData = { - theme: themeSession.getTheme(), - env: { - SUPABASE_URL: process.env.SUPABASE_URL!, - SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY!, - } - }; + const data: LoaderData = { + theme: themeSession.getTheme(), + env: { + SUPABASE_URL: process.env.SUPABASE_URL!, + SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY!, + } + }; - return json(data) + return json(data) } function App() { - const { env } = useLoaderData() - const [supabase] = useState(() => - createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY) - ) - - // recalls loaders when authentication state changes - const revalidator = useRevalidator() - useEffect(() => { - const { - data: { subscription } - } = supabase.auth.onAuthStateChange((event, session) => { - revalidator.revalidate() - }) - - return () => { - subscription.unsubscribe() - } - }, [supabase /*, revalidator*/ ]) - - - const [theme] = useTheme(); - const [sidePanelIsShown, setSidePanelIsShown] = useState(false); - - - // hide for specific routes - const location = useLocation(); - const routesToHideNavigation = ['/login', '/signup']; ///// add homepage - const shouldHideNavigation = routesToHideNavigation.includes(location.pathname); - - - - return ( - - - - - - - - - {shouldHideNavigation ? null : <> - - - } - - {/* space for navbar above main page content */} -
- -
- - - - ); + const { env } = useLoaderData() + const [supabase] = useState(() => + createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY) + ) + const [user, setUser] = useState(undefined); + + // recalls loaders when authentication state changes + const revalidator = useRevalidator(); + useEffect(() => { + const { + data: { subscription } + } = supabase.auth.onAuthStateChange((event, session) => { + revalidator.revalidate(); + if (event === "SIGNED_IN") { + setUser(session?.user); + } else if (event === "SIGNED_OUT"){ + setUser(undefined); + } + }) + + return () => { + subscription.unsubscribe() + } + }, [supabase /*, revalidator*/]) + + + const [theme] = useTheme(); + const [sidePanelIsShown, setSidePanelIsShown] = useState(false); + + + // hide for specific routes + const location = useLocation(); + const routesToHideNavigation = ['/login', '/signup']; ///// add homepage + const shouldHideNavigation = routesToHideNavigation.includes(location.pathname); + + + + return ( + + + + + + + + + {shouldHideNavigation ? null : <> + + + } + + {/* space for navbar above main page content */} +
+ +
+ + + + ); } export default function AppWithProviders() { - const data = useLoaderData(); - return ( - - - - ); + const data = useLoaderData(); + return ( + + + + ); } \ No newline at end of file diff --git a/app/routes/login.tsx b/app/routes/login.tsx index 6e89f6a..7a2297a 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -1,106 +1,25 @@ -import { Link, Form, useOutletContext, useActionData, redirect } from "@remix-run/react"; -import { json } from "@remix-run/node"; +import { Form, useOutletContext, useNavigate } from "@remix-run/react"; import { useEffect, useState } from "react"; import { createServerClient, parse, serialize } from '@supabase/ssr'; - +import SpinnerSVG from "~/components/SpinnerSVG" import googleIcon from "~/assets/oauth_providers/google-icon.png"; -import facebookIcon from "~/assets/oauth_providers/facebook-icon.png"; import githubIcon from "~/assets/oauth_providers/github-icon.png"; - -import type { Database } from '../../database.types' -import type { SupabaseClient, Provider } from '@supabase/supabase-js' -import type { ActionFunctionArgs } from "@remix-run/node" - - -export async function action({request}: ActionFunctionArgs) { - const formData = await request.formData(); - - const cookies = parse(request.headers.get('Cookie') ?? '') - const headers = new Headers() - const supabase = createServerClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, { - cookies: { - get(key) { - return cookies[key] - }, - set(key, value, options) { - headers.append('Set-Cookie', serialize(key, value, options)) - }, - remove(key, options) { - headers.append('Set-Cookie', serialize(key, '', options)) - }, - }, - }) - - // LOGIN - if (formData.get("form_type") === "LOGIN"){ - const {error} = await supabase.auth.signInWithPassword({ - email: String(formData.get("email_input")), - password: String(formData.get("password_input")) - }) - if (error) return json({ errorMessage: error.message }); - return redirect("/"); - } - - // REGISTER - else if (formData.get("form_type") === "REGISTER") { - const displayName = String(formData.get("reg_display_name_input")); - const email = String(formData.get("reg_email_input")); - const password = String(formData.get("reg_password_input")); - const passwordConfirm = String(formData.get("reg_confirm_password_input")); - - - let errorMessage: string | null = null; - const validations : [boolean, string][] = [ - [!email.includes("@") || !email.includes("."), "Invalid email address."], - [password.length < 6, "Password should be at least 6 characters."], - [!(/[A-Z]/.test(password)), "Password should contain at least 1 uppercase."], - [!(/[a-z]/.test(password)), "Password should contain at least 1 lowercase."], - [!(/\d/.test(password)), "Password should contain at least 1 number."], - [!(/[!@#$%^&*()_+={}\[\]:;<>,.?~\\|\-]/.test(password)), "Password should contain at least 1 symbol."], - [password !== passwordConfirm, "Confirm password doesn't match."] - ]; - validations.some(vali => { - if (vali[0]){ - errorMessage = vali[1]; - return true; - } else return false; - }); - - // const d = await supabase.auth.getUser() - // console.log(d) - // const s = await supabase.auth.getSession() - // console.log(s) - - if (errorMessage) return json({ errorMessage }); - const {error} = await supabase.auth.signUp({email, password}); - if (error) json({ errorMessage: "Error: Failed to register." }); - - return redirect("/"); - } - - return json({ errorMessage: "Unknown form submitted." }); - } +import type { Provider } from '@supabase/supabase-js' +import { ContextProps } from "~/utils/types/ContextProps.type"; export default function Login(){ - - const actionData = useActionData(); + const navigate = useNavigate(); + const [errorMessage, setErrorMessage] = useState(null); - const resetErrorMessage = () => setErrorMessage(null); - // update errorMessage on form submission - useEffect(()=>{ - if (actionData?.errorMessage){ - setErrorMessage(actionData.errorMessage); - } - }, [actionData]); - + const [isSubmitting, setIsSubmitting] = useState(false); // switch between login & signup const [isAtLogin, setIsAtLogin] = useState(true); - const { supabase } = useOutletContext<{ supabase: SupabaseClient }>() + const { supabase } = useOutletContext() const providerClicked = async (providerName: Provider) => { const { data, error } = await supabase.auth.signInWithOAuth({ @@ -123,6 +42,7 @@ export default function Login(){ hasSymbol: false }); + // revalidate new password useEffect(()=>{ setPassReqs({ hasSixChar: regPassword.length >= 6, @@ -136,7 +56,72 @@ export default function Login(){ const fillDemoAcc = ()=>{ setEmail("hynguyendev@gmail.com"); - setPassword("sup3rs3cur3"); + setPassword("123+Ab"); + }; + + const loginOnSubmit: React.FormEventHandler = async (event) => { + event.preventDefault(); + resetErrorMessage(); + setIsSubmitting(true); + + const {error, data} = await supabase.auth.signInWithPassword({ + email: email, + password: password + }) + if (error) { + setIsSubmitting(false); + setErrorMessage(error.message); + return; + } + + return navigate("/"); + }; + + const registerOnSubmit: React.FormEventHandler = async (event) => { + event.preventDefault(); + resetErrorMessage(); + setIsSubmitting(true); + + const form = event.currentTarget; + const formValues: {[key:string]: string} = { + displayName: form["reg_display_name_input"].value, + email: form["reg_email_input"].value, + password: form["reg_password_input"].value, + passwordConfirm: form["reg_confirm_password_input"].value + }; + + const validations: [boolean, string][] = [ + [passReqs.hasSixChar, "Password should be at least 6 characters"], + [passReqs.hasUppercase, "Password should contain at least 1 uppercase"], + [passReqs.hasLowercase, "Password should contain at least 1 lowercase"], + [passReqs.hasNumber, "Password should contain at least 1 number"], + [passReqs.hasSymbol, "Password should contain at least 1 symbol"], + [formValues.password === formValues.passwordConfirm, "Passwords don't match"] + ]; + + const validationsPassed = validations.every(vali => { + if (!vali[0]){ + setTimeout(()=>{ + setErrorMessage(vali[1]); + }, 1); + return false; + } else return true; + }); + + if (validationsPassed){ + const {error, data} = await supabase.auth.signUp({ + email: formValues.email, password: formValues.password + }); + if (error){ + setIsSubmitting(false); + setErrorMessage(error.message); + return; + } + return navigate("/"); + } else { + setIsSubmitting(false); + return; + } }; @@ -148,10 +133,6 @@ export default function Login(){ onClick={()=>providerClicked("google")}> google -
-
+

Login

@@ -183,7 +164,10 @@ export default function Login(){ type="password" autoComplete="current-password" className="text-input" value={password} onChange={(e) => setPassword(e.target.value)}/> - + {errorMessage}
@@ -192,7 +176,7 @@ export default function Login(){
-
+

Register

@@ -238,7 +222,10 @@ export default function Login(){ className="text-input" required /> {errorMessage} - +
diff --git a/app/utils/types/ContextProps.type.ts b/app/utils/types/ContextProps.type.ts new file mode 100644 index 0000000..82a43dc --- /dev/null +++ b/app/utils/types/ContextProps.type.ts @@ -0,0 +1,7 @@ +import { SupabaseClient, User } from "@supabase/supabase-js" +import { Database } from "database.types" + +export type ContextProps = { + supabase: SupabaseClient, + user: User | undefined +} \ No newline at end of file