Skip to content

Commit

Permalink
Merge branch 'develop' into dependabot/npm_and_yarn/next-13.5.0
Browse files Browse the repository at this point in the history
  • Loading branch information
sahsisunny authored Nov 6, 2023
2 parents 54af080 + 15a1d23 commit 4e6995c
Show file tree
Hide file tree
Showing 18 changed files with 285 additions and 57 deletions.
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;
10 changes: 1 addition & 9 deletions src/components/Button/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { ButtonProps } from '@/types/button.types';
import React from 'react';

interface ButtonProps {
type?: 'button' | 'submit' | 'reset';
className: string;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
disabled?: boolean;
testId?: string;
}

const Button: React.FC<ButtonProps> = ({ type, className, onClick, children, disabled, testId }) => {
return (
<button data-testid={testId} type={type} className={className} onClick={onClick} disabled={disabled}>
Expand Down
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
7 changes: 1 addition & 6 deletions src/components/ProfileIcon/ProfileIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { ProfileIconProps } from '@/types/profileIcon.types';
import React from 'react';

interface ProfileIconProps {
firstName: string;
lastName: string;
size: number;
}

const ProfileIcon: React.FC<ProfileIconProps> = ({ firstName, lastName, size }) => {
const initials = (firstName[0] + lastName[0]).toUpperCase();
const randomColor = Math.floor(Math.random() * 16777215).toString(16);
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;
8 changes: 2 additions & 6 deletions src/pages/dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useState } from 'react';
import Button from '@/components/Button';
import InputBox from '@/components/InputBox';
import Layout from '@/components/Layout';
import { randomString } from '@/utils/constants';
import { useState } from 'react';
import AddIcon from '../../../public/assets/icons/add';
import CopyIcon from '../../../public/assets/icons/copy';
import ReloadIcon from '../../../public/assets/icons/reload';
Expand All @@ -10,12 +11,7 @@ const Dashboard = () => {
const [url, getUrl] = useState<string>('');
const [shortUrl, setUrl] = useState<string>('');

function generateRandomString() {
const randomString = Math.random().toString(36).substring(2, 7);
return randomString;
}
const handleUniqueUrl = () => {
const randomString = generateRandomString();
setUrl(`https://rds.li/${randomString}`);
};
return (
Expand Down
3 changes: 2 additions & 1 deletion src/pages/login/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Button from '@/components/Button';
import InputBox from '@/components/InputBox';
import Layout from '@/components/Layout';
import { alphanumicUnderscore } from '@/utils/constants';
import { ChangeEvent, SetStateAction, useState } from 'react';

const LoginPage = () => {
Expand All @@ -12,7 +13,7 @@ const LoginPage = () => {

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const inputValue = (event.target as HTMLInputElement).value;
const alphanumicUnderscore = /^[a-zA-Z0-9_]+$/;

if (event.target.name === 'username') {
if (alphanumicUnderscore.test(inputValue)) {
setUsernameBorder(' border-2 border-green-500');
Expand Down
8 changes: 8 additions & 0 deletions src/types/button.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface ButtonProps {
type?: 'button' | 'submit' | 'reset';
className: string;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
disabled?: boolean;
testId?: string;
}
5 changes: 5 additions & 0 deletions src/types/profileIcon.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface ProfileIconProps {
firstName: string;
lastName: string;
size: number;
}
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;
}
2 changes: 2 additions & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const alphanumicUnderscore = /^[a-zA-Z0-9_]+$/;
export const randomString = Math.random().toString(36).substring(2, 7);
Loading

0 comments on commit 4e6995c

Please sign in to comment.