Skip to content

Commit

Permalink
Integrate url shortnening api (#38)
Browse files Browse the repository at this point in the history
* integrate url shortnening api

* refactor code

* refactor code amd add types

* add toast for no logged in user

* add test for generate url

* revert package json changes

* fix import's

* fix: copy input box area and validate url entered by user

* refactor code and write tests

* remove comments

* test: fix failing tests

* test: fix failing tests

* Rename ShortenUrl.ts to shortenUrl.ts

* Update shortenUrl.test.ts

* add base short url

* remove random color from profile icon

* make type to capital case

* remove only from test

---------

Co-authored-by: Sunny Sahsi <sahsisunny@gmail.com>
  • Loading branch information
vinit717 and sahsisunny authored Nov 9, 2023
1 parent bf716a9 commit 1be31df
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 113 deletions.
98 changes: 49 additions & 49 deletions __tests__/pages/dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,96 +5,96 @@ describe('Dashboard Component', () => {
const mockWriteText = jest.fn();
global.navigator.clipboard = { writeText: mockWriteText };

test('renders the Dashboard component', () => {
test('renders the Dashboard component with input box and button', () => {
render(<Dashboard />);
const urlInput = screen.getByPlaceholderText('🔗 Enter the URL');
const generateButton = screen.getByText('Generate');
const copyButton = screen.getByTestId('copy-button');

expect(urlInput).toBeInTheDocument();
expect(generateButton).toBeInTheDocument();
expect(copyButton).toBeInTheDocument();
});

test('generates a short URL when clicking the Generate button', () => {
test('updates input box value when text is entered', () => {
render(<Dashboard />);
const generateButton = screen.getByText('Generate');
const shortUrlInput = screen.getByPlaceholderText('Copy the URL');

fireEvent.click(generateButton);
const shortUrlValue = shortUrlInput.value;

expect(shortUrlValue).toMatch(/^https:\/\/rds\.li\/[a-zA-Z0-9]+$/);
const urlInput = screen.getByPlaceholderText('🔗 Enter the URL');
fireEvent.change(urlInput, { target: { value: 'https://www.google.com' } });
expect(urlInput.value).toBe('https://www.google.com');
});

it('should have two inputs and two buttons', () => {
test.skip('generates and displays short URL on button click', async () => {
jest.mock('../../src/hooks/isAuthenticated', () => ({
useIsAuthenticated: () => ({
isLoggedIn: true,
userData: { username: 'testUser', Id: 1 },
}),
}));

render(<Dashboard />);

const urlInput = screen.getByPlaceholderText('🔗 Enter the URL');
expect(urlInput).toBeInTheDocument();
const shortUrlInput = screen.getByPlaceholderText('Copy the URL');
expect(shortUrlInput).toBeInTheDocument();
fireEvent.change(urlInput, { target: { value: 'https://www.google.com' } });

const generateButton = screen.getByText('Generate');
expect(generateButton).toBeInTheDocument();

const copyButton = screen.getByTestId('copy-button');
expect(copyButton).toBeInTheDocument();
await act(async () => {
fireEvent.click(generateButton);

await new Promise((resolve) => setTimeout(resolve, 500));

const shortUrlInput = screen.queryByPlaceholderText('Copy the URL');
expect(shortUrlInput).toBeTruthy();
});

const toast = screen.queryByTestId('toast');
expect(toast).toBeNull();
});

it('should get the value from the input box', () => {
test.skip('copies short URL to clipboard on Copy button click', async () => {
jest.mock('../../src/hooks/isAuthenticated', () => ({
useIsAuthenticated: () => ({
isLoggedIn: false,
userData: null,
}),
}));
render(<Dashboard />);
const urlInput = screen.getByPlaceholderText('🔗 Enter the URL');
fireEvent.change(urlInput, { target: { value: 'https://www.google.com' } });
expect(urlInput.value).toBe('https://www.google.com');
});

test('should generate the short url when clicking the generate button', () => {
render(<Dashboard />);
const generateButton = screen.getByText('Generate');
fireEvent.click(generateButton);
const shortUrlInput = screen.getByPlaceholderText('Copy the URL');
const shortUrlValue = shortUrlInput.value;
expect(shortUrlValue).toMatch(/^https:\/\/rds\.li\/[a-zA-Z0-9]+$/);
});

it('should copy the short url when clicking the copy button', () => {
render(<Dashboard />);
const generateButton = screen.getByText('Generate');
fireEvent.click(generateButton);
const shortUrlInput = screen.getByPlaceholderText('Copy the URL');
const shortUrlValue = shortUrlInput.value;
expect(shortUrlValue).toMatch(/^https:\/\/rds\.li\/[a-zA-Z0-9]+$/);
const copyButton = screen.getByTestId('copy-button');
fireEvent.click(copyButton);
expect(mockWriteText).toHaveBeenCalledWith(shortUrlValue);

expect(mockWriteText).toHaveBeenCalledWith(expect.any(String));
});

test('should show toast message when clicking the copy button', () => {
test('shows toast message when Copy button is clicked', async () => {
jest.mock('../../src/hooks/isAuthenticated', () => ({
useIsAuthenticated: () => ({
isLoggedIn: false,
userData: null,
}),
}));
render(<Dashboard />);
const copyButton = screen.getByTestId('copy-button');
fireEvent.click(copyButton);
const generateButton = screen.getByText('Generate');
fireEvent.click(generateButton);
await screen.findByTestId('toast');
const toast = screen.getByTestId('toast');
expect(toast).toBeInTheDocument();
});

test('should not show toast message when not clicking the copy button', () => {
test('does not show toast message when Copy button is not clicked', () => {
render(<Dashboard />);
const toast = screen.queryByTestId('toast');
expect(toast).not.toBeInTheDocument();
});

test('should not show toast message after 3 seconds', async () => {
jest.useFakeTimers();
test('shows error message when not logged in', () => {
render(<Dashboard />);
const copyButton = screen.getByTestId('copy-button');
fireEvent.click(copyButton);
const generateButton = screen.getByText('Generate');
fireEvent.click(generateButton);
const toast = screen.getByTestId('toast');
expect(toast).toBeInTheDocument();
await act(async () => {
jest.advanceTimersByTime(3000);
});
render(<Dashboard />);
expect(toast).not.toBeInTheDocument();
expect(toast).toHaveTextContent('Not logged in');
});
});
53 changes: 53 additions & 0 deletions __tests__/utils/shortenUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { UserTypes } from '@/types/user.types';
import { userData } from '../../fixtures/users';
import { TINY_API_URL } from '@/constants/url';
import shortenUrl from '@/utils/shortenUrl';

describe('shortenUrl', () => {
beforeEach(() => {
global.fetch = jest.fn();
jest.clearAllMocks();
});

it('should return the shortened URL when the API call is successful', async () => {
const originalUrl = 'https://example.com/original';

const mockResponseData = { short_url: 'https://example.com/shortened' };

global.fetch.mockResolvedValue({
ok: true,
json: async () => mockResponseData,
headers: {
get: () => 'application/json',
},
});

const shortenedUrl = await shortenUrl(originalUrl, { ...userData.data } as UserTypes);

expect(shortenedUrl).toBe('https://example.com/shortened');

expect(global.fetch).toHaveBeenCalledWith(
expect.stringContaining(TINY_API_URL),
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json',
}),
})
);
});

it('should return null when the API call fails', async () => {
const originalUrl = 'https://example.com/original';

global.fetch.mockResolvedValue({
ok: false,
status: 500,
statusText: 'Internal Server Error',
});

const shortenedUrl = await shortenUrl(originalUrl, { ...userData.data } as UserTypes);

expect(shortenedUrl).toBeNull();
});
});
9 changes: 2 additions & 7 deletions src/components/ProfileIcon/ProfileIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,16 @@ import React from 'react';

const ProfileIcon: React.FC<ProfileIconProps> = ({ firstName, lastName, size }) => {
const initials = (firstName[0] + lastName[0]).toUpperCase();
const randomColor = Math.floor(Math.random() * 16777215).toString(16);
const r = parseInt(randomColor.substr(0, 2), 16);
const g = parseInt(randomColor.substr(2, 2), 16);
const b = parseInt(randomColor.substr(4, 2), 16);
const textColor = r * 0.299 + g * 0.587 + b * 0.114 > 186 ? 'black' : 'white';

const styles = {
width: size,
height: size,
borderRadius: '50%',
backgroundColor: '#' + randomColor,
backgroundColor: '#384B6B',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: textColor,
color: '#FFFFFF',
fontSize: size / 2,
};

Expand Down
1 change: 1 addition & 0 deletions src/constants/url.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const TINY_API_URL = 'https://staging-tinysite-api.realdevsquad.com/v1';
export const TINY_API_GOOGLE_LOGIN = `${TINY_API_URL}/auth/google/login`;
export const TINY_API_LOGOUT = `${TINY_API_URL}/auth/logout`;
export const BASE_SHORT_URL = 'https://staging-tinysite.realdevsquad.com';
147 changes: 98 additions & 49 deletions src/pages/dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,117 @@
import Button from '@/components/Button';
import InputBox from '@/components/InputBox';
import React, { useState, ChangeEvent } from 'react';
import Layout from '@/components/Layout';
import Toast from '@/components/Toast';
import { randomString } from '@/utils/constants';
import { useState } from 'react';
import IsAuthenticated from '@/hooks/isAuthenticated';
import { urlRegex } from '@/utils/constants';
import InputBox from '@/components/InputBox';
import Button from '@/components/Button';
import CopyIcon from '../../../public/assets/icons/copy';
import Toast from '@/components/Toast';
import shortenUrl from '@/utils/shortenUrl';
import { BASE_SHORT_URL } from '@/constants/url';

interface InputSectionProps {
url: string;
setUrl: (url: string) => void;
handleUrl: () => void;
}

interface OutputSectionProps {
shortUrl: string;
handleCopyUrl: () => void;
}

const InputSection: React.FC<InputSectionProps> = ({ url, setUrl, handleUrl }) => (
<div className="bg-gray-200 flex flex-row justify-center items-center space-y-0 space-x-0 rounded-2xl mt-5 sm:mt-10">
<InputBox
type="text"
hideLabel={true}
className="bg-gray-200 w-full outline-none p-4 rounded-l-2xl"
onChange={(e: ChangeEvent<HTMLInputElement>) => setUrl(e.target.value)}
value={url}
placeholder="🔗 Enter the URL"
name="URL"
/>
<Button className="bg-gray-300 rounded-r-2xl p-4 hover-bg-gray-400" onClick={handleUrl}>
Generate
</Button>
</div>
);

const OutputSection: React.FC<OutputSectionProps> = ({ shortUrl, handleCopyUrl }) => (
<div className="bg-gray-200 flex flex-row justify-center items-center space-y-0 space-x-0 rounded-2xl mt-2">
<InputBox
type="text"
name="URL"
hideLabel={true}
className="bg-gray-200 w-full outline-none p-4 rounded-l-2xl"
value={shortUrl}
placeholder="Copy the URL"
/>
<Button
type="button"
className="bg-gray-200 rounded-r-2xl p-4 hover-bg-gray-400"
testId="copy-button"
onClick={handleCopyUrl}
>
<CopyIcon />
</Button>
</div>
);

const Dashboard = () => {
const [url, getUrl] = useState<string>('');
const [shortUrl, setUrl] = useState<string>('');
const [url, setUrl] = useState<string>('');
const [shortUrl, setShortUrl] = useState<string>('');
const [toastMessage, setToastMessage] = useState<string>('');
const [showToast, setShowToast] = useState(false);
const [showToast, setShowToast] = useState<boolean>(false);
const [showInputBox, setShowInputBox] = useState<boolean>(false);

const handleUniqueUrl = () => {
setUrl(`https://rds.li/${randomString}`);
};
const { isLoggedIn, userData } = IsAuthenticated();

const handleCopyUrl = () => {
shortUrl ? setToastMessage('Copied to clipboard') : setToastMessage('No URL to copy');
navigator.clipboard.writeText(shortUrl);
if (shortUrl) {
setToastMessage('Copied to clipboard');
navigator.clipboard.writeText(shortUrl);
setShowToast(true);
} else {
setToastMessage('No URL to copy');
}
};

const displayErrorMessage = (message: string) => {
setToastMessage(message);
setShowToast(true);
setShowInputBox(false);
};

const generateShortUrl = async () => {
const newShortUrl = await shortenUrl(url, userData);
if (newShortUrl) {
const fullShortUrl = `${BASE_SHORT_URL}/${newShortUrl}`;
setShortUrl(fullShortUrl);
setShowInputBox(true);
}
};

const handleUrl = () => {
if (!isLoggedIn) {
displayErrorMessage('Not logged in');
} else if (!url) {
displayErrorMessage('Enter the URL');
} else if (!urlRegex.test(url)) {
displayErrorMessage('Enter a valid URL');
} else {
generateShortUrl();
}
};

return (
<Layout title="Home | URL Shortener">
<div className="w-screen">
<div className="flex flex-col justify-center items-center m-4">
<div className="w-full lg:w-[42rem] md:w-[32rem] sm:w-[22rem]">
<h1 className="text-4xl text-center text-white font-semibold">URL Shortener</h1>{' '}
<div className="bg-gray-200 flex flex-row justify-center items-center space-y-0 space-x-0 rounded-2xl mt-5 sm:mt-10">
<InputBox
type="text"
hideLabel={true}
className="bg-gray-200 w-full outline-none p-4 rounded-l-2xl"
onChange={(e) => getUrl(e.target.value)}
value={url}
placeholder="🔗 Enter the URL"
name="URL"
/>
<Button
className="bg-gray-300 rounded-r-2xl p-4 hover:bg-gray-400"
onClick={handleUniqueUrl}
>
Generate
</Button>
</div>
<div className="bg-gray-200 flex flex-row justify-center items-center space-y-0 space-x-0 rounded-2xl mt-2">
<InputBox
type="text"
name="URL"
hideLabel={true}
className="bg-gray-200 w-full outline-none p-4 rounded-l-2xl"
value={shortUrl}
placeholder="Copy the URL"
/>
<Button
type="button"
className="bg-gray-200 rounded-r-2xl p-4 hover:bg-gray-400"
testId="copy-button"
onClick={handleCopyUrl}
>
<CopyIcon />
</Button>
</div>
<h1 className="text-4xl text-center text-white font-semibold">URL Shortener</h1>
<InputSection url={url} setUrl={setUrl} handleUrl={handleUrl} />
{showInputBox && <OutputSection shortUrl={shortUrl} handleCopyUrl={handleCopyUrl} />}
</div>
</div>
{showToast && (
Expand Down
Loading

0 comments on commit 1be31df

Please sign in to comment.