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

Implement user authentication #56

Merged
merged 10 commits into from
Oct 28, 2023
24 changes: 18 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import React from 'react';
import React, { useContext, useEffect } from 'react';

import Home from './components/Home/Home';
import MainLayout from './components/Layout/MainLayout';
import { UserContext, type UserContextType } from './contexts/UserContext';

const App: React.FC = () => (
<MainLayout>
<Home />
</MainLayout>
);
const App: React.FC = () => {
const { user, getUser } = useContext(UserContext) as UserContextType;

useEffect(() => {
getUser();
}, []);

user !== null &&
console.log(`user is authenticated as ${user.primary_email}`);

return (
<MainLayout>
<Home />
</MainLayout>
);
};

export default App;
13 changes: 13 additions & 0 deletions src/assets/svg/closeIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 19 additions & 1 deletion src/components/Layout/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@ import { Button, Col, Row, Space, Typography } from 'antd';

import styles from './Navbar.module.css';
import MenuDrawer from '../MenuDrawer/MenuDrawer';
import LoginModal from '../../LoginModal';

const { Text } = Typography;

const Navbar: React.FC = () => {
const [openMenu, setOpenMenu] = useState(false);
const [isLoginModalVisible, setIsLoginModalVisible] = useState(false);

const handleLoginModalClose = (): void => {
setIsLoginModalVisible(false);
};

const handleLoginModalOpen = (): void => {
setIsLoginModalVisible(true);
};

return (
<>
Expand Down Expand Up @@ -89,11 +99,19 @@ const Navbar: React.FC = () => {
<InstagramOutlined className={styles.antIcon} />
</Button>
</a>
<Button className={styles.loginButton}>Join Us</Button>
<Button
className={styles.loginButton}
onClick={handleLoginModalOpen}
>
Login
</Button>
</Space>
</Col>
</Row>
<MenuDrawer openMenu={openMenu} setOpenMenu={setOpenMenu} />
{isLoginModalVisible ? (
<LoginModal onClose={handleLoginModalClose} />
) : null}
</>
);
};
Expand Down
159 changes: 159 additions & 0 deletions src/components/LoginModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import React, { useState, useContext } from 'react';
import axios, { type AxiosResponse } from 'axios';
import { API_URL } from '../../constants';
import { type Profile } from '../../types';
import { UserContext, type UserContextType } from '../../contexts/UserContext';
import closeIcon from '../../assets/svg/closeIcon.svg';

interface LoginModalProps {
onClose: () => void;
}

const LoginModal: React.FC<LoginModalProps> = ({ onClose }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { setUserContext } = useContext(UserContext) as UserContextType;

const handleLogin = (e: React.FormEvent): void => {
e.preventDefault();
try {
axios
.post(
`${API_URL}/auth/login`,
{
email,
password,
},
{
withCredentials: true,
}
)
.then((response: AxiosResponse<Profile>) => {
setUserContext(response.data);
onClose();
})
.catch((error) => {
if (error.response.status !== 401) {
console.error({
message: 'Something went wrong when fetching the user',
description: error.toString(),
});
} else {
setUserContext(null);
}
});
} catch (error) {
console.error('Login error:', error);
}
};

return (
<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 transition-opacity">
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen"></span>
&#8203;
<div
className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<button
className="absolute top-2 right-2 p-2 text-gray-700 hover:text-gray-900 focus:outline-none"
onClick={onClose}
>
<img className="w-6 h-6" src={closeIcon} alt="Modal Close Icon" />
</button>

<div className="bg-white p-6 space-y-8 rounded-lg shadow-xl">
<h2 className="text-2xl font-bold text-gray-900 text-center">
Log in to ScholarX
</h2>
<form className="mt-8 space-y-6" onSubmit={handleLogin}>
<div>
<label
htmlFor="email"
className="block mb-2 text-sm font-medium text-gray-900"
>
Your email
</label>
<input
type="email"
name="email"
id="email"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="[email protected]"
value={email}
onChange={(e) => {
setEmail(e.target.value);
}}
required
/>
</div>
<div>
<label
htmlFor="password"
className="block mb-2 text-sm font-medium text-gray-900"
>
Your password
</label>
<input
type="password"
name="password"
id="password"
placeholder="••••••••"
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
required
/>
</div>
<div className="flex items-start">
<div className="flex items-center h-5">
<input
id="remember"
aria-describedby="remember"
name="remember"
type="checkbox"
className="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300"
/>
</div>
<div className="ml-3 text-sm">
<label
htmlFor="remember"
className="font-medium text-gray-500"
>
Remember this device
</label>
</div>
<a
href="#"
className="ml-auto text-sm font-medium text-blue-600 hover:underline"
>
Lost Password?
</a>
</div>
<button
type="submit"
className="w-full px-5 py-3 text-base font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:ring-blue-300"
>
Login to your account
</button>
<div className="text-sm font-medium text-gray-900">
Not registered yet?{' '}
<a className="text-blue-600 hover:underline">Create account</a>
</div>
</form>
</div>
</div>
</div>
</div>
);
};

export default LoginModal;
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const API_URL = 'http://localhost:3000/api';
50 changes: 50 additions & 0 deletions src/contexts/UserContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { createContext, useState } from 'react';
import axios, { type AxiosResponse } from 'axios';
import type { Profile } from '../types';
import { API_URL } from './../constants';
Madhawa97 marked this conversation as resolved.
Show resolved Hide resolved

export interface UserContextType {
user: Profile | null;
setUserContext: (user: Profile | null) => void;
getUser: () => void;
}

export const UserContext = createContext<UserContextType | null>(null);

export const UserProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [user, setUser] = useState<Profile | null>(null);

const setUserContext = (user: Profile | null): void => {
setUser(user);
};

const getUser = (): void => {
axios
.get(`${API_URL}/me/profile`, { withCredentials: true })
.then((response: AxiosResponse<Profile>) => {
setUser(response.data);
})
.catch((error) => {
if (error.response.status !== 401) {
console.error({
message: 'Something went wrong when fetching the user',
description: error.toString(),
});
} else {
console.error({
message: 'User not authenticated',
description: error.toString(),
});
setUser(null);
}
});
};

return (
<UserContext.Provider value={{ user, setUserContext, getUser }}>
{children}
</UserContext.Provider>
);
};
6 changes: 5 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import ReactDOM from 'react-dom/client';

import App from './App';

import { UserProvider } from './contexts/UserContext';

import './index.css';

const root = ReactDOM.createRoot(document.getElementById('root') as Element);

root.render(
<React.StrictMode>
<App />
<UserProvider>
<App />
</UserProvider>
</React.StrictMode>
);