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

Integration: With backend for sign and signout #34

Merged
merged 6 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
44 changes: 34 additions & 10 deletions __tests__/components/Navbar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import Navbar from '@/components/Navbar/';
import { render } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';

describe('Navbar', () => {
Expand All @@ -12,16 +12,40 @@ describe('Navbar', () => {

it('should have dropdown menu', () => {
const { container } = render(<Navbar />);
expect(container.querySelector('ul')).toHaveTextContent('Profile');
expect(container.querySelector('ul')).toHaveTextContent('Dashboard');
expect(container.querySelector('ul')).toHaveTextContent('Settings');
expect(container.querySelector('ul')).toBeInTheDocument();
expect(container.querySelector('ul')).toContainHTML('Profile');
expect(container.querySelector('ul')).toContainHTML('Dashboard');
expect(container.querySelector('ul')).toContainHTML('Settings');
expect(container.querySelector('ul')).toContainHTML('Sign Out');
});

it('should toggle menu', () => {
const { container } = render(<Navbar />);
const button = container.querySelector('button');
expect(button).toHaveTextContent('Sunny');
button?.click();
expect(container.querySelector('ul')).toHaveClass('lg:flex space-x-4');
it('should have google login button', () => {
render(<Navbar />);
const googleLoginButton = screen.getByTestId('google-login');
expect(googleLoginButton).toBeInTheDocument();
expect(googleLoginButton).toHaveTextContent('Sign In');
expect(googleLoginButton).toHaveAttribute('href', 'https://api-tinysite.onrender.com/v1/auth/google/login');
});

it('should display "Sign In" when not logged in', () => {
render(<Navbar />);
const signInButton = screen.getByText('Sign In');
expect(signInButton).toBeInTheDocument();
});

it('should handle "Sign Out" button click', () => {
render(<Navbar />);
const signOutButton = screen.getByText('Sign Out');
expect(signOutButton).toBeInTheDocument();

const originalIsLoggedIn = screen.getByText('Sign In');
fireEvent.click(originalIsLoggedIn);

expect(signOutButton).toBeInTheDocument();

fireEvent.click(signOutButton);

const signInButton = screen.getByText('Sign In');
expect(signInButton).toBeInTheDocument();
});
});
28 changes: 28 additions & 0 deletions __tests__/hooks/isAuthenticated.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { renderHook } from '@testing-library/react-hooks';
import fetchMock from 'jest-fetch-mock';
import IsAuthenticated from '@/hooks/isAuthenticated';
import { userData } from '../../fixtures/users';

beforeAll(() => {
fetchMock.enableMocks();
});

afterEach(() => {
fetchMock.resetMocks();
});

it('should return isLoggedIn as true and userData if the request is successful', async () => {
const userDataMock = userData;
fetchMock.mockResponseOnce(JSON.stringify(userDataMock), { status: 200 });
const { result, waitForNextUpdate } = renderHook(() => IsAuthenticated());
await waitForNextUpdate();
expect(result.current.isLoggedIn).toBe(true);
expect(result.current.userData).toEqual(userDataMock.data);
});

it('should return isLoggedIn as false if the request is unsuccessful', async () => {
fetchMock.mockResponseOnce(JSON.stringify({}), { status: 401 });
const { result } = renderHook(() => IsAuthenticated());
expect(result.current.isLoggedIn).toBe(false);
expect(result.current.userData).toBeNull();
});
13 changes: 13 additions & 0 deletions fixtures/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const userData = {
data: {
Id: 1,
Username: 'Sunny Sahsi',
Email: 'sunnysahsi@gmail.com',
Password: '',
IsVerified: false,
IsOnboarding: true,
CreatedAt: '2023-11-04T10:02:26.582699Z',
UpdatedAt: '2023-11-04T10:02:26.582699Z',
},
message: 'user fetched successfully',
};
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"check:all": "yarn format:check && yarn lint:check",
"fix:all": "yarn format:fix && yarn lint:fix",
"prepare": "husky install",
"test": "jest --coverage",
"test": "jest --coverage -u",
"lint": "eslint .",
"lint-fix": "eslint . --fix"
},
Expand All @@ -30,6 +30,7 @@
"devDependencies": {
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/eslint": "^8",
"@types/jest": "^29.5.3",
"@types/node": "20.2.5",
Expand All @@ -48,6 +49,7 @@
"husky": "^8.0.3",
"jest": "^29.6.2",
"jest-environment-jsdom": "^29.6.2",
"jest-fetch-mock": "^3.0.3",
"prettier": "^2.8.8",
"ts-jest": "^29.1.1"
},
Expand Down
13 changes: 13 additions & 0 deletions public/assets/icons/downArrow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const DownArrow = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 inline-block ml-1 transform group-hover:rotate-180 transition-transform"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
);

export default DownArrow;
23 changes: 23 additions & 0 deletions public/assets/icons/google.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const GoogleIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="28px" height="28px">
<title>Google</title>
<path
fill="#fbc02d"
d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12 s5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24s8.955,20,20,20 s20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"
/>
<path
fill="#e53935"
d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039 l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"
/>
<path
fill="#4caf50"
d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36 c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"
/>
<path
fill="#1565c0"
d="M43.611,20.083L43.595,20L42,20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571 c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"
/>
</svg>
);

export default GoogleIcon;
66 changes: 42 additions & 24 deletions src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import Button from '@/components/Button';
import ProfileIcon from '../ProfileIcon/ProfileIcon';
import GoogleIcon from '../../../public/assets/icons/google';
import DownArrowIcon from '../../../public/assets/icons/downArrow';
import IsAuthenticated from '@/hooks/isAuthenticated';
import { TINY_API_GOOGLE_LOGIN, TINY_API_LOGOUT } from '@/constants/url';

const Navbar: React.FC = () => {
const [menuOpen, setMenuOpen] = useState(false);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const { isLoggedIn: isAuth, userData } = IsAuthenticated();

useEffect(() => {
setIsLoggedIn(isAuth);
if (userData) {
const username = userData.Username;
const [first, last] = username.split(' ');
setFirstName(first);
setLastName(last);
}
}, [isAuth, userData]);

const toggleDropdown = () => {
setMenuOpen(!menuOpen);
};

const firstName = 'Sunny';
const lastName = 'Sahsi';

return (
<nav className="bg-gray-800 p-4">
<div className="flex items-center justify-between">
Expand All @@ -21,27 +36,25 @@ const Navbar: React.FC = () => {

<ul className={'lg:flex space-x-4'}>
<li className="relative group">
<Button type="button" onClick={toggleDropdown} className="text-white focus:outline-none">
<div className="flex items-center space-x-2">
<ProfileIcon firstName={firstName} lastName={lastName} size={50} />
{isLoggedIn ? (
<Button type="button" onClick={toggleDropdown} className="text-white focus:outline-none">
<div className="flex items-center space-x-2">
<ProfileIcon firstName={firstName} lastName={lastName} size={50} />
<span> {firstName}</span>
<DownArrowIcon />
</div>
</Button>
) : (
<a
className="flex items-center space-x-2 bg-white text-black px-4 py-2 rounded-md hover:bg-gray-100 cursor-pointer"
href={TINY_API_GOOGLE_LOGIN}
data-testid="google-login"
>
<GoogleIcon />

<span> {firstName}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 inline-block ml-1 transform group-hover:rotate-180 transition-transform"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</Button>
<span>Sign In</span>
</a>
)}
</li>
<ul className={`${menuOpen ? 'block' : 'hidden'} absolute top-[8vh] right-0 bg-gray-800 p-2 z-10`}>
<li>
Expand All @@ -59,6 +72,11 @@ const Navbar: React.FC = () => {
Settings
</a>
</li>
<li>
<a href={TINY_API_LOGOUT} className="text-white hover:bg-gray-700 block px-4 py-2">
Sign Out
</a>
</li>
</ul>
</ul>
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/constants/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const TINY_API_URL = 'https://api-tinysite.onrender.com/v1';
export const TINY_API_GOOGLE_LOGIN = `${TINY_API_URL}/auth/google/login`;
export const TINY_API_LOGOUT = `${TINY_API_URL}/auth/logout`;
35 changes: 35 additions & 0 deletions src/hooks/isAuthenticated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';
import { TINY_API_URL } from '@/constants/url';
import { UserTypes } from '@/types/user.types';

const IsAuthenticated = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [userData, setUserData] = useState<UserTypes | null>(null);

useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(`${TINY_API_URL}/users/self`, {
method: 'GET',
credentials: 'include',
});
if (response.status === 200) {
const userData = await response.json();
setUserData(userData.data);
setIsLoggedIn(true);
} else {
setIsLoggedIn(false);
}
} catch (err) {
setIsLoggedIn(false);
console.error(err);
}
};

fetchData();
}, []);

return { isLoggedIn, userData };
};

export default IsAuthenticated;
sahsisunny marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 10 additions & 0 deletions src/types/user.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface UserTypes {
id: number;
Username: string;
email: string;
password: string;
isVerified: boolean;
isOnboarding: boolean;
createdAt: string;
updatedAt: string;
}
Loading