diff --git a/src/app/globals.css b/src/app/globals.css index ecff112..cadb647 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;700;800&display=swap"); @import "../styles/variables.css"; :root { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ecf5c4c..d26e75f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Manrope } from "next/font/google"; - +// import { Geist, Geist_Mono } from "next/font/google"; +import { UserProvider } from "@/lib/client/userContext"; import "./globals.css"; const manrope = Manrope({ @@ -10,8 +11,8 @@ const manrope = Manrope({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "TRCC Dashboard", + description: "TRCC volunteer management dashboard", }; export default function RootLayout({ @@ -25,7 +26,7 @@ export default function RootLayout({ className={`${manrope.variable} font-sans`} suppressHydrationWarning > - {children} + {children} ); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 5d308ca..f0a2823 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,125 +1,100 @@ -// temp ugly auth page for testing "use client"; -import { useState } from "react"; -import { AnimatedButton } from "@/components/ui/AnimatedButton"; -import { AnimatedInput } from "@/components/ui/AnimatedInput"; -import { InteractiveSurface } from "@/components/ui/InteractiveSurface"; -import { Reveal } from "@/components/ui/Reveal"; -import { Stagger } from "@/components/ui/Stagger"; -import { signInWithEmail, signUpWithEmail } from "@/lib/client/supabase/auth"; +import { JSX, useState } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { signInWithEmail } from "@/lib/client/supabase/auth"; +import styles from "@/styles/login.module.css"; -export default function LoginPage(): React.JSX.Element { - const [signInEmail, setSignInEmail] = useState(""); - const [signInPassword, setSignInPassword] = useState(""); - const [signUpEmail, setSignUpEmail] = useState(""); - const [signUpPassword, setSignUpPassword] = useState(""); - const [message, setMessage] = useState(null); - const [responseData, setResponseData] = useState(null); +export default function LoginPage(): JSX.Element { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); const [loading, setLoading] = useState(false); + const router = useRouter(); - const handleSignIn = async ( + const handleSubmit = async ( event: React.FormEvent ): Promise => { event.preventDefault(); + setError(null); setLoading(true); - setMessage(null); - setResponseData(null); - const { data, error } = await signInWithEmail(signInEmail, signInPassword); - setMessage(error ? error.message : "Signed in successfully."); - setResponseData(data); - setLoading(false); - }; - const handleSignUp = async ( - event: React.FormEvent - ): Promise => { - event.preventDefault(); - setLoading(true); - setMessage(null); - setResponseData(null); - const { data, error } = await signUpWithEmail(signUpEmail, signUpPassword); - setMessage( - error ? error.message : "Check your email to confirm your account." - ); - setResponseData(data); - setLoading(false); + try { + const { error: authError } = await signInWithEmail(email, password); + + if (authError) { + setError(authError.message); + return; + } + + router.push("/volunteers"); + } finally { + setLoading(false); + } }; return ( -
- - Sign In - - - - - setSignInEmail(event.target.value)} - required - /> - - - setSignInPassword(event.target.value)} - required - /> +
+
+

Log in

- - Sign In - - - +
+
+ + setEmail(event.target.value)} + /> +
- - Sign Up - - - - - setSignUpEmail(event.target.value)} - required - /> +
+ + setPassword(event.target.value)} + /> +
- - setSignUpPassword(event.target.value)} - required - /> +
+ + Forgot password? + +
- - Sign Up - -
-
+ +
- {message ? ( - - - {message} - - - ) : null} - {responseData ? ( - - - {JSON.stringify(responseData, null, 2)} - - - ) : null} + {error ? ( +

+ {error} +

+ ) : null} +
); } diff --git a/src/lib/client/userContext.tsx b/src/lib/client/userContext.tsx new file mode 100644 index 0000000..3c5b825 --- /dev/null +++ b/src/lib/client/userContext.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { + createContext, + useContext, + useEffect, + useState, + type JSX, + type ReactNode, +} from "react"; +import type { AuthChangeEvent, Session, User } from "@supabase/supabase-js"; +import { createClient } from "@/lib/client/supabase/client"; + +interface UserContextValue { + user: User | null; + loading: boolean; +} + +const UserContext = createContext({ + user: null, + loading: true, +}); + +export function UserProvider({ + children, +}: Readonly<{ children: ReactNode }>): JSX.Element { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const supabase = createClient(); + + // Fetch the initial session + supabase.auth + .getUser() + .then(({ data }: { data: { user: User | null } }) => { + setUser(data.user); + }) + .catch((error: unknown) => { + console.error("Error fetching user:", error); + }) + .finally(() => { + setLoading(false); + }); + + // Subscribe to auth state changes + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange( + (_event: AuthChangeEvent, session: Session | null) => { + setUser(session?.user ?? null); + } + ); + + return (): void => { + subscription.unsubscribe(); + }; + }, []); + + return {children}; +} + +export function useUser(): UserContextValue { + return useContext(UserContext); +} diff --git a/src/styles/login.module.css b/src/styles/login.module.css new file mode 100644 index 0000000..2dad4ca --- /dev/null +++ b/src/styles/login.module.css @@ -0,0 +1,118 @@ +.container { + /* Login page specific typography */ + --login-line-height: 1.366; + --login-font-title: var(--font-weight-extrabold) var(--font-size-h1) / + var(--login-line-height) var(--font-family-primary); + --login-font-h3: var(--font-weight-bold) 20px / var(--login-line-height) + var(--font-family-primary); + --login-font-body: var(--font-weight-regular) var(--font-size-body) / + var(--login-line-height) var(--font-family-primary); + --login-font-caption: var(--font-weight-regular) 14px / + var(--login-line-height) var(--font-family-primary); + + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 20px; + background-color: var(--color-white, #ffffff); +} + +.content { + display: flex; + flex-direction: column; + gap: 32px; + width: 594px; + max-width: 100%; +} + +.title { + font: var(--login-font-title); + color: var(--color-neutral-900); + text-align: center; +} + +.formCard { + background-color: var(--color-white, #ffffff); + border: 1px solid #d9d9d9; + border-radius: 8px; + box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.1); + padding: 24px 24px 48px; + display: flex; + flex-direction: column; + gap: 24px; +} + +.inputGroup { + display: flex; + flex-direction: column; + gap: 14px; +} + +.label { + font: var(--login-font-body); + color: var(--color-neutral-800); +} + +.input { + font: var(--login-font-body); + padding: 12px 14px; + background-color: var(--color-white, #ffffff); + border: 1px solid #757575; + border-radius: 4px; + color: #1b1b1b; +} + +.input::placeholder { + color: #757575; +} + +.input:focus { + outline: none; + border-color: var(--color-teal-600); + box-shadow: 0 0 0 1px var(--color-teal-600); +} + +.forgotPasswordContainer { + display: flex; + justify-content: flex-end; +} + +.forgotPassword { + font: var(--login-font-caption); + color: var(--color-purple-600); + text-decoration: none; +} + +.forgotPassword:hover { + text-decoration: underline; +} + +.submitButton { + background-color: #1b1b1b; + border: 4px solid #1b1b1b; + border-radius: 8px; + padding: 12px 24px; + color: #ffffff; + font: var(--login-font-h3); + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; +} + +.submitButton:hover:not(:disabled) { + opacity: 0.9; +} + +.submitButton:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.error { + color: #d93025; + margin-top: 10px; + text-align: center; + font: var(--login-font-body); +} diff --git a/src/styles/variables.css b/src/styles/variables.css index abbaa5c..66c413f 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -1,16 +1,61 @@ -@theme { - --color-tag-yellow: #fdecc8; - --color-tag-green: #dbeddb; - --color-tag-blue: #d3e5ef; - --color-tag-pink: #f5e0e9; - --color-tag-purple: #e8deee; - --color-tag-gray: #e3e2e0; - --color-tag-red: #ffe2dd; - --color-tag-orange: #fadec9; - --color-tag-brown: #eee0da; +:root { + /* Purple Palette */ + --color-purple-900: #311d38; + --color-purple-800: #492a55; + --color-purple-700: #663d75; + --color-purple-600: #78468c; + --color-purple-500: #9d68b3; + --color-purple-400: #b487c4; + --color-purple-300: #c9a9d5; + --color-purple-200: #decce6; + --color-purple-100: #faf8fb; - --color-primary-purple: #e9ddee; - --color-secondary-purple: #decce6; - --color-accent-purple: #78468c; - --color-dark-accent-purple: #663d75; + /* Teal Palette */ + --color-teal-900: #13353b; + --color-teal-800: #1f5861; + --color-teal-700: #2c7c88; + --color-teal-600: #379fae; + --color-teal-500: #58bbca; + --color-teal-400: #77c8d4; + --color-teal-300: #9ed8e1; + --color-teal-200: #c5e8ed; + --color-teal-100: #f0f5f7; + + /* Neutral Palette */ + --color-neutral-900: #1c2230; + --color-neutral-800: #272e40; + --color-neutral-700: #43516f; + --color-neutral-600: #56698f; + --color-neutral-500: #7082a8; + --color-neutral-400: #8f9dbc; + --color-neutral-300: #afbacf; + --color-neutral-200: #d9dee8; + --color-neutral-100: #f9f9fb; + + /* Borders / Strokes */ + --color-border: #d4e3ee; + + /* Typography */ + --font-family-primary: "Manrope", sans-serif; + --font-family-secondary: "Jost", sans-serif; + + /* Font Sizes */ + --font-size-h1: 48px; + --font-size-h2: 24px; + --font-size-body: 18px; + --font-size-sm: 16px; + --font-size-xs: 14px; + + /* Font Weights */ + --font-weight-regular: 400; + --font-weight-bold: 700; + --font-weight-extrabold: 800; + + /* Pre-composed Typography defaults */ + --font-heading-1: var(--font-weight-extrabold) var(--font-size-h1) + var(--font-family-primary); + --font-heading-2: var(--font-weight-bold) var(--font-size-h2) + var(--font-family-primary); + --font-body: var(--font-weight-regular) var(--font-size-body) + var(--font-family-primary); }