Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] created AuthContext #20

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 11 additions & 43 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,21 @@
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import supabase from '../../../api/supabase/createClient';
import { useAuth } from '../../utils/AuthProvider';

export default function Login() {
const { signIn } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { push } = useRouter();

const handleSignUp = async () => {
const { data, error } = await supabase.auth.signUp({
email,
password,
});

if (error) {
// Check if the error is due to an already registered email
if (error.message.includes('User already registered')) {
throw new Error(
'This email is already registered. Please try signing in instead.',
);
} else {
throw new Error(
`An error occurred trying to sign up: ${error.message}`,
);
const handleSignIn = async () => {
try {
await signIn(email, password);
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
}

push('/');

return data;
};

const handleSignInWithEmail = async () => {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});

if (error) {
throw new Error(`An error occurred trying to sign in: ${error}`);
}

push('/');

return data;
};

return (
Expand All @@ -63,12 +33,10 @@ export default function Login() {
value={password}
placeholder="Password"
/>
<button type="button" onClick={handleSignUp}>
Sign up
</button>
<button type="button" onClick={handleSignInWithEmail}>
<button type="button" onClick={handleSignIn}>
Sign in
</button>
</button>{' '}
{/* Sign in button */}
</>
);
}
50 changes: 50 additions & 0 deletions app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client';

import { useState } from 'react';
import { useAuth } from '../../utils/AuthProvider';

export default function SignUp() {
const { signUp } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const handleSignUp = async () => {
// Define handleSignUp
try {
await signUp(email, password);
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
}
};

return (
<div>
<input
name="email"
onChange={e => setEmail(e.target.value)}
value={email}
placeholder="Email"
/>
<input
type="password"
name="password"
onChange={e => setPassword(e.target.value)}
value={password}
placeholder="Password"
/>
<input
type="password"
name="Confirm Password"
onChange={e => setPassword(e.target.value)}
value={password}
placeholder="Confirm Password"
/>
<button type="button" onClick={handleSignUp}>
Sign up
</button>{' '}
{/* Sign up button */}
</div>
);
}
34 changes: 34 additions & 0 deletions app/dashboard/page.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while this is a good test of using AuthProvider, we don't want a dashboard page yet, so let's delete this file.

You can leave the code in this file as a block comment in AuthProvider.tsx for example usage

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import { AuthProvider, useAuth } from '../utils/AuthProvider';

export default function DashboardPage() {
return (
<AuthProvider>
<Dashboard />
</AuthProvider>
);
}

function Dashboard() {
const { authUser, isLoggedIn, signOut } = useAuth();

if (!authUser) {
return <p>Loading...</p>;
}

return (
<>
<header>
<h1>Dashboard</h1>
</header>

<main>
<p>User is currently: {isLoggedIn ? 'Logged In' : 'Logged Out'}</p>
{/* Display user's email */}
{authUser && <p>User name: {authUser.email}</p>}{' '}
<button onClick={signOut}>Log Out</button> {/* Logout button */}
</main>
</>
);
}
5 changes: 4 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import StyledComponentsRegistry from '@/lib/registry';
import { AuthProvider } from './utils/AuthProvider';

// font definitions
const sans = Inter({
Expand All @@ -22,7 +23,9 @@ export default function RootLayout({
return (
<html lang="en">
<body className={sans.className}>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
<AuthProvider>
<StyledComponentsRegistry>{children}</StyledComponentsRegistry>
</AuthProvider>
</body>
</html>
);
Expand Down
174 changes: 174 additions & 0 deletions app/utils/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
'use client';

import {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from 'react';
import { useRouter } from 'next/navigation';
import supabase from '../../api/supabase/createClient';

interface AuthContextType {
authUser: AuthUser | null;
isLoggedIn: boolean;
signUp: (email: string, password: string) => Promise<void>;
signIn: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
loading: boolean;
}

interface AuthUser {
id: string;
email: string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to pack these together, we can directly have 2 states in AuthProvider (i.e. after line 40):

const [userId, setUserId] = useState<string | null>(null)
const [email, setEmail] = useState<string | null>(null)

}

// Create the AuthContext
const AuthContext = createContext<AuthContextType | null>(null);

// Custom hook to use AuthContext
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

// AuthProvider component that wraps around your app or specific pages
export function AuthProvider({ children }: { children: ReactNode }) {
const [authUser, setAuthUser] = useState<AuthUser | null>(null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const [isLoggedIn, setIsLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
const { push } = useRouter(); // Get Next.js router for navigation

// Sign Up function
const signUp = async (email: string, password: string) => {
try {
const { data, error } = await supabase.auth.signUp({ email, password });
if (error) throw new Error(error.message);

if (data.user) {
// Map the user data to AuthUser type, ensuring `id` and `email` are defined
const user: AuthUser = {
id: data.user.id ?? '', // Fallback to an empty string if `id` is undefined
email: data.user.email ?? '', // Fallback to an empty string if `email` is undefined
};

setAuthUser(user);
setIsLoggedIn(true);
push('/dashboard'); // Redirect to dashboard after sign-up
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the redirecting should be moved to /signup. The functions in AuthProvider should only handle the supabase calls and updating the AuthContext. All additional logic/processing should happen within the respective page

also, since /dashboard is a test page that will be deleted, we should nav to '/' instead (i.e. the home page)

}
} catch (error) {
if (error instanceof Error) {
console.error('Sign-up Error:', error.message);
}
}
};

// Sign In function
const signIn = async (email: string, password: string) => {
try {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw new Error(error.message);

// Map the user data to AuthUser type
const user: AuthUser = {
id: data.user.id ?? '', // Fallback to an empty string if `id` is undefined
email: data.user.email ?? '', // Fallback to an empty string if `email` is undefined
};

setAuthUser(user);
setIsLoggedIn(true);
push('/dashboard'); // Redirect to dashboard after sign-in
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the redirecting should be moved to /login. The functions in AuthProvider should only handle the supabase calls and updating the AuthContext. All additional logic/processing should happen within the respective page

} catch (error) {
if (error instanceof Error) {
console.error('Sign-in Error:', error.message);
}
}
};

// Sign Out function
const signOut = async () => {
try {
const { error } = await supabase.auth.signOut();
if (error) throw new Error(error.message);
setAuthUser(null);
setIsLoggedIn(false);
push('/login'); // Redirect to login after sign-out
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flagging that this routing shouldn't be here

also to clarify, we're not using signOut anywhere yet right?

} catch (error) {
if (error instanceof Error) {
console.error('Sign-out Error:', error.message);
}
}
};

// Fetch the currently logged-in user on mount and redirect to dashboard if signed in
useEffect(() => {
const getUser = async () => {
setLoading(true);
const { data } = await supabase.auth.getUser();
if (data.user && data.user.id && data.user.email) {
// Map the user data to AuthUser type
const user: AuthUser = {
id: data.user.id,
email: data.user.email,
};

setAuthUser(user);
setIsLoggedIn(true);
} else {
setAuthUser(null);
setIsLoggedIn(false);
}
setLoading(false);
};

getUser();

// Listen for auth state changes
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => {
if (session?.user) {
const user: AuthUser = {
id: session.user.id ?? '', // Fallback to empty string if `id` is undefined
email: session.user.email ?? '', // Fallback to empty string if `email` is undefined
};

setAuthUser(user);
setIsLoggedIn(true);
} else {
setAuthUser(null);
setIsLoggedIn(false);

if (!loading) {
if (window.location.pathname !== '/signup') {
push('/login');
}
}
}
});

return () => subscription?.unsubscribe();
}, [push]);

Check warning on line 158 in app/utils/AuthProvider.tsx

View workflow job for this annotation

GitHub Actions / Run ESLint, Prettier, and TypeScript compiler

React Hook useEffect has a missing dependency: 'loading'. Either include it or remove the dependency array

Check warning on line 158 in app/utils/AuthProvider.tsx

View workflow job for this annotation

GitHub Actions / Run ESLint, Prettier, and TypeScript compiler

React Hook useEffect has a missing dependency: 'loading'. Either include it or remove the dependency array

const value: AuthContextType = {
authUser,
isLoggedIn,
signUp,
signIn,
signOut,
loading,
};

return (
<AuthContext.Provider value={value}>
{!loading ? children : <div>Loading...</div>}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove this conditional check. we can do this !loading check on individual pages, but we don't want even the home page to display "Loading...". So these lines should look like

<AuthContext.Provider value={value}>
      {children}
</AuthContext.Provider>

</AuthContext.Provider>
);
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"eslint-config-prettier": "^9.1.0",
"husky": "^9.1.5",
"prettier": "^3.3.3",
"typescript": "^5",
"typescript": "5.4",
"yarnhook": "^0.6.2"
}
}
Loading
Loading