Skip to content

Commit

Permalink
feat: add page for redirect to original url (#40)
Browse files Browse the repository at this point in the history
* feat: add functionality for redirect ot original url

* refactor: redirect page

* refactor: redirect page and add visiticon for shorten url

* fix: typo in redirect page

* test: add test for changes

* refactor and fix test cases

* refactor : change anchor tag to Link tag

* test: fix failing test

* Update index.tsx
  • Loading branch information
sahsisunny authored Nov 11, 2023
1 parent 1be31df commit 9d33245
Show file tree
Hide file tree
Showing 12 changed files with 317 additions and 27 deletions.
10 changes: 6 additions & 4 deletions __tests__/components/Navbar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React from 'react';
import Navbar from '@/components/Navbar/';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';

import { fireEvent, render, screen } from '@testing-library/react';

import Navbar from '@/components/Navbar/';
import React from 'react';

describe('Navbar', () => {
it('should render', () => {
const { container } = render(<Navbar />);
expect(container).toHaveTextContent('URL Shortener');
expect(container.querySelector('a')).toHaveAttribute('href', '#');
expect(container.querySelector('a')).toHaveAttribute('href', '/');
});

it('should have dropdown menu', () => {
Expand Down
49 changes: 49 additions & 0 deletions __tests__/pages/[redirect].test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';

import Redirect from '../../src/pages/[redirect]/index';
import { useRouter } from 'next/router';

jest.mock('next/router', () => ({
useRouter: jest.fn(),
}));

describe('Redirect Component', () => {
const mockRouterPush = jest.fn();
const mockRouterReplace = jest.fn();
const mockRouter = {
push: mockRouterPush,
replace: mockRouterReplace,
query: { redirect: 'ffdsfds' },
};

beforeEach(() => {
(useRouter as jest.Mock).mockReturnValue(mockRouter);
});

test('redirects to original URL on Go button click', async () => {
render(<Redirect />);
const goButton = screen.getByText('Go');
await act(async () => {
fireEvent.click(goButton);
});
expect(mockRouterPush).toHaveBeenCalled();
});

test('show tooltip on Go button click', async () => {
render(<Redirect />);
const goButton = screen.getByText('Go');
await act(async () => {
fireEvent.click(goButton);
});
expect(mockRouterPush).toHaveBeenCalled();
const tooltip = screen.getByText('The skip feature is exclusively available to Premium users.');
expect(tooltip).toBeInTheDocument();
});

test('redirects when timer reaches zero', async () => {
jest.useFakeTimers();
render(<Redirect />);
act(() => jest.advanceTimersByTime(5000));
waitFor(() => expect(mockRouterPush).toHaveBeenCalled());
});
});
39 changes: 39 additions & 0 deletions __tests__/utils/fetchOriginalUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fetchMock from 'jest-fetch-mock';
import fetchOriginalUrl from '../../src/utils/fetchOriginalUrl';
import { act } from 'react-dom/test-utils';

fetchMock.enableMocks();

describe('fetchOriginalUrl', () => {
const shortUrlCode = '442d39ac';
const originalUrl = 'https://github.com/Real-Dev-Squad/tiny-site-frontend/pull/40';

it('fetches and displays the original URL if the response is successful', async () => {
const responseData = { url: { OriginalUrl: originalUrl } };

fetchMock.mockResponse(JSON.stringify(responseData), { status: 200 });

await act(async () => {
const result = await fetchOriginalUrl(shortUrlCode);
expect(result).toEqual(originalUrl);
});
});

it('returns null if the response is not successful', async () => {
fetchMock.mockResponse('', { status: 404 });

await act(async () => {
const result = await fetchOriginalUrl(shortUrlCode);
expect(result).toBeNull();
});
});

it('handles errors gracefully', async () => {
fetchMock.mockReject(new Error('Network error'));

await act(async () => {
const result = await fetchOriginalUrl(shortUrlCode);
expect(result).toBeNull();
});
});
});
7 changes: 7 additions & 0 deletions public/assets/icons/redirect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const RedirectIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" height="5em" viewBox="0 0 512 512" fill="#ffff">
<path d="M227.7 11.7c15.6-15.6 40.9-15.6 56.6 0l216 216c15.6 15.6 15.6 40.9 0 56.6l-216 216c-15.6 15.6-40.9 15.6-56.6 0l-216-216c-15.6-15.6-15.6-40.9 0-56.6l216-216zm87.6 137c-4.6-4.6-11.5-5.9-17.4-3.5s-9.9 8.3-9.9 14.8v56H224c-35.3 0-64 28.7-64 64v48c0 13.3 10.7 24 24 24s24-10.7 24-24V280c0-8.8 7.2-16 16-16h64v56c0 6.5 3.9 12.3 9.9 14.8s12.9 1.1 17.4-3.5l80-80c6.2-6.2 6.2-16.4 0-22.6l-80-80z" />
</svg>
);

export default RedirectIcon;
28 changes: 28 additions & 0 deletions public/assets/icons/share.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const ShareIcon = () => (
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="2em"
height="2em"
viewBox="0 0 513.000000 522.000000"
fill="#ffff"
>
<title>Visit</title>
<g transform="translate(0.000000,522.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none">
<path
d="M2710 4616 l0 -606 -22 -4 c-13 -2 -77 -9 -143 -16 -501 -49 -995
-249 -1430 -579 -142 -107 -421 -390 -531 -536 -348 -465 -535 -968 -574
-1544 -7 -90 -10 -382 -8 -691 l3 -534 184 424 c201 466 247 561 356 725 358
542 894 941 1530 1138 160 50 400 95 582 110 l53 4 2 -605 3 -604 365 298
c201 165 740 605 1198 978 458 374 832 683 832 687 0 3 -57 53 -127 110 -345
280 -2184 1777 -2225 1812 l-48 39 0 -606z m1124 -696 c443 -360 805 -657 805
-660 0 -3 -365 -303 -812 -667 l-812 -663 -3 440 -2 440 -138 0 c-848 -1
-1640 -340 -2232 -956 -98 -102 -240 -274 -302 -364 l-17 -25 6 40 c25 189 96
446 179 642 294 702 918 1255 1649 1462 238 67 391 90 663 98 l192 6 0 434 c0
271 4 433 10 431 5 -1 371 -298 814 -658z"
/>
</g>
</svg>
);

export default ShareIcon;
5 changes: 3 additions & 2 deletions src/components/Footer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import Link from 'next/link';
import React from 'react';

const Footer: React.FC = () => {
return (
<div className="bg-gray-200 flex justify-center items-center h-[6vh]">
<p className="text-gray-600 text-sm text-center">
The contents of this website are deployed from this{' '}
<a
<Link
href="https://github.com/Real-Dev-Squad/tiny-site-frontend"
target="_blank"
rel="noopener noreferrer"
className="text-blue-500"
>
open sourced repo
</a>
</Link>
</p>
</div>
);
Expand Down
24 changes: 13 additions & 11 deletions src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useEffect, useState } from 'react';
import { TINY_API_GOOGLE_LOGIN, TINY_API_LOGOUT } from '@/constants/url';

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 GoogleIcon from '../../../public/assets/icons/google';
import IsAuthenticated from '@/hooks/isAuthenticated';
import { TINY_API_GOOGLE_LOGIN, TINY_API_LOGOUT } from '@/constants/url';
import Link from 'next/link';
import ProfileIcon from '../ProfileIcon/ProfileIcon';

const Navbar: React.FC = () => {
const [menuOpen, setMenuOpen] = useState(false);
Expand All @@ -30,9 +32,9 @@ const Navbar: React.FC = () => {
return (
<nav className="bg-gray-800 p-4">
<div className="flex items-center justify-between">
<a href="#" className="text-white text-2xl font-bold">
<Link href="/" className="text-white text-2xl font-bold">
URL Shortener
</a>
</Link>

<ul className={'lg:flex space-x-4'}>
<li className="relative group">
Expand All @@ -45,27 +47,27 @@ const Navbar: React.FC = () => {
</div>
</Button>
) : (
<a
<Link
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>Sign In</span>
</a>
</Link>
)}
</li>
<ul className={`${menuOpen ? 'block' : 'hidden'} absolute top-[8vh] right-0 bg-gray-800 p-2 z-10`}>
<li>
<a href="#" className="text-white hover:bg-gray-700 block px-4 py-2">
<Link href="#" className="text-white hover:bg-gray-700 block px-4 py-2">
Dashboard
</a>
</Link>
</li>
<li>
<a href={TINY_API_LOGOUT} className="text-white hover:bg-gray-700 block px-4 py-2">
<Link href={TINY_API_LOGOUT} className="text-white hover:bg-gray-700 block px-4 py-2">
Sign Out
</a>
</Link>
</li>
</ul>
</ul>
Expand Down
4 changes: 3 additions & 1 deletion src/constants/url.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
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';
export const TINY_SITE = 'https://staging-tinysite.realdevsquad.com';
export const TINY_API_URL_DETAIL = `${TINY_API_URL}/urls`;
export const TINY_API_REDIRECT = `${TINY_API_URL}/tinyurl`;
113 changes: 113 additions & 0 deletions src/pages/[redirect]/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useEffect, useState } from 'react';

import Head from 'next/head';
import Link from 'next/link';
import RedirectIcon from '../../../public/assets/icons/redirect';
import { TINY_SITE } from '@/constants/url';
import fetchOriginalUrl from '@/utils/fetchOriginalUrl';
import { useRouter } from 'next/router';

interface LoaderTimerProps {
timer: number;
goButtonClickHandler: () => void;
}

const LoaderTimer = ({ timer, goButtonClickHandler }: LoaderTimerProps) => {
if (timer < 1) {
return (
<div className="mt-4 flex flex-col items-center space-y-2">
<RedirectIcon />
<p className="text-1xl">Redirecting...</p>
</div>
);
} else {
return (
<>
<div className="loader border-t-4 rounded-full border-gray-500 animate-spin aspect-square w-20 flex justify-center items-center text-yellow-700 text-4xl font-bold mt-4">
{timer}
</div>
<button onClick={goButtonClickHandler} className="mt-4 p-2 bg-blue-500 text-white rounded-md">
Go
</button>
</>
);
}
};

const RedirectFooter = () => (
<div className="absolute bottom-0 right-0 p-2 text-gray-500 w-screen flex justify-center items-center">
<Link
className="text-sm text-gray-400 font-bold cursor-pointer hover:underline"
href={TINY_SITE}
target="_blank"
rel="noopener noreferrer"
>
By <span className="font-bold">Real Dev Squad</span>
</Link>
</div>
);

const Redirect = () => {
const router = useRouter();
const { redirect: shortUrlCode } = router.query as { redirect: string };
const [originalUrl, setOriginalUrl] = useState('');
const [timer, setTimer] = useState(5);
const [isPremiumUser] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);

useEffect(() => {
if (shortUrlCode) {
const fetchOriginalUrlAsync = async () => {
try {
const url = await fetchOriginalUrl(shortUrlCode);
if (url) {
setOriginalUrl(url);
} else {
router.push('/');
}
} catch (error) {
console.error('Error fetching original URL:', error);
}
};
fetchOriginalUrlAsync();
}

if (timer > 0) {
const countdown = setTimeout(() => setTimer(timer - 1), 1000);
return () => clearTimeout(countdown);
} else if (timer === 0) {
router.push(originalUrl);
}
}, [shortUrlCode, timer, originalUrl]);

const handleGoButtonClick = () => {
if (isPremiumUser) {
router.push(originalUrl);
} else {
setShowTooltip(true);
}
};

return (
<>
<Head>
<title>Redirecting...</title>
<meta name="robots" content="noindex" />
</Head>
<div className="w-screen min-h-screen flex flex-col items-center justify-center bg-gray-900 text-white p-4">
<p className="text-lg">You are being redirected to:</p>
<p className="text-blue-500 text-xl font-bold w-1/2 text-center truncate xl:w-1/2">{originalUrl}</p>
<LoaderTimer timer={timer} goButtonClickHandler={handleGoButtonClick} />

{showTooltip && !isPremiumUser && (
<div className="mt-2 p-2 bg-yellow-100 text-yellow-800 rounded-md">
The skip feature is exclusively available to Premium users.
</div>
)}
<RedirectFooter />
</div>
</>
);
};

export default Redirect;
Loading

0 comments on commit 9d33245

Please sign in to comment.