diff --git a/frontend-assignment/.gitignore b/frontend-assignment/.gitignore
new file mode 100644
index 000000000..d317a61dd
--- /dev/null
+++ b/frontend-assignment/.gitignore
@@ -0,0 +1,25 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+/package-lock.json
\ No newline at end of file
diff --git a/frontend-assignment/package.json b/frontend-assignment/package.json
new file mode 100644
index 000000000..ce93508e4
--- /dev/null
+++ b/frontend-assignment/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "frontend-assignment",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@testing-library/jest-dom": "^5.17.0",
+ "@testing-library/react": "^13.4.0",
+ "@testing-library/user-event": "^13.5.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "5.0.1",
+ "web-vitals": "^2.1.4"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test",
+ "eject": "react-scripts eject"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ }
+}
diff --git a/frontend-assignment/public/index.html b/frontend-assignment/public/index.html
new file mode 100644
index 000000000..aa069f27c
--- /dev/null
+++ b/frontend-assignment/public/index.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ React App
+
+
+
+
+
+
+
diff --git a/frontend-assignment/public/manifest.json b/frontend-assignment/public/manifest.json
new file mode 100644
index 000000000..080d6c77a
--- /dev/null
+++ b/frontend-assignment/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/frontend-assignment/public/robots.txt b/frontend-assignment/public/robots.txt
new file mode 100644
index 000000000..e9e57dc4d
--- /dev/null
+++ b/frontend-assignment/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/frontend-assignment/src/App.css b/frontend-assignment/src/App.css
new file mode 100644
index 000000000..41e30ee9e
--- /dev/null
+++ b/frontend-assignment/src/App.css
@@ -0,0 +1,35 @@
+
+.App-logo {
+ height: 40vmin;
+ pointer-events: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .App-logo {
+ animation: App-logo-spin infinite 20s linear;
+ }
+}
+
+.App-header {
+ background-color: #282c34;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: calc(10px + 2vmin);
+ color: white;
+}
+
+.App-link {
+ color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/frontend-assignment/src/App.js b/frontend-assignment/src/App.js
new file mode 100644
index 000000000..d0768e7f3
--- /dev/null
+++ b/frontend-assignment/src/App.js
@@ -0,0 +1,12 @@
+import './App.css';
+import KickStarter from './components/KickStarter/KickStarter';
+
+function App() {
+ return (
+
+
+
+ );
+}
+
+export default App;
diff --git a/frontend-assignment/src/components/KickStarter/KickStarter.js b/frontend-assignment/src/components/KickStarter/KickStarter.js
new file mode 100644
index 000000000..b03b928d6
--- /dev/null
+++ b/frontend-assignment/src/components/KickStarter/KickStarter.js
@@ -0,0 +1,55 @@
+import React, { useState, useEffect } from 'react'
+
+import Pagination from '../Pagination/Pagination';
+
+import { getKickStarterData } from './kickStarter.service';
+
+import './kickStarter.css';
+
+const KickStarter = () => {
+ const [kickStarterData, setKickStarterData] = useState([]);
+ const [paginatedData, setPaginatedData] = useState([]);
+
+ useEffect(() => {
+ onInit();
+ }, []);
+
+ const onInit = async () => {
+ const data = await getKickStarterData();
+ setKickStarterData(data);
+ setPaginatedData(data.slice(0, 5));
+ }
+
+ const onPageChange = (page) => {
+ setPaginatedData(kickStarterData.slice((page - 1) * 5, page * 5));
+ }
+
+ return (
+
+
KickStarter Projects
+ {paginatedData.length > 0 &&
+
+
+
S.No
+
Percentage Funded
+
Amount Pledged
+
+
+ {
+ paginatedData.map((item) => (
+
+
{item["s.no"]}
+
{item["percentage.funded"]}
+
{item["amt.pledged"]}
+
+ ))
+ }
+
+
+ }
+ {paginatedData.length > 0 &&
}
+
+ )
+}
+
+export default KickStarter;
diff --git a/frontend-assignment/src/components/KickStarter/__tests__/kickStarter.tests.js b/frontend-assignment/src/components/KickStarter/__tests__/kickStarter.tests.js
new file mode 100644
index 000000000..567a0c194
--- /dev/null
+++ b/frontend-assignment/src/components/KickStarter/__tests__/kickStarter.tests.js
@@ -0,0 +1,25 @@
+import { render, screen } from '@testing-library/react';
+import KickStarter from '../KickStarter';
+
+describe('KickStarter', () => {
+ it('should render the KickStarter component', async () => {
+ render();
+ expect(await screen.findByRole('table')).toBeInTheDocument();
+ const headers = await screen.findAllByRole('columnheader');
+ expect(headers[0]).toHaveTextContent('S.No');
+ expect(headers[1]).toHaveTextContent('Percentage Funded');
+ expect(headers[2]).toHaveTextContent('Amount Pledged');
+ });
+
+ it('should render the KickStarter component with 5 rows', async () => {
+ render();
+ const rows = await screen.findAllByRole('row');
+ expect(rows).toHaveLength(6);
+ });
+
+ it('should render the Pagination component', async () => {
+ render();
+ const pagination = await screen.findByRole('navigation', { name: /pagination navigation/i });
+ expect(pagination).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend-assignment/src/components/KickStarter/kickStarter.css b/frontend-assignment/src/components/KickStarter/kickStarter.css
new file mode 100644
index 000000000..ab2ac2bcc
--- /dev/null
+++ b/frontend-assignment/src/components/KickStarter/kickStarter.css
@@ -0,0 +1,62 @@
+.kick-starter-header {
+ display: flex;
+ background-color: #f8f9fa;
+ border-bottom: 2px solid #dee2e6;
+}
+
+.kick-starter-row {
+ display: flex;
+ border-bottom: 1px solid #dee2e6;
+}
+
+.kick-starter-row:hover {
+ background-color: #f8f9fa;
+}
+
+.kick-starter-container {
+ width: 100%;
+ max-width: 800px;
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ background-color: #fff;
+ min-height: 320px;
+}
+
+.container {
+ margin: 24px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.kick-starter-header-item {
+ padding: 16px;
+ font-weight: 600;
+ color: #212529;
+ width: 33%;
+}
+
+.kick-starter-body {
+ width: 100%;
+}
+
+.kick-starter-row-item {
+ padding: 16px;
+ width: 33%;
+ color: #495057;
+}
+
+.loading-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ width: 100%;
+}
+
+.kick-starter-title {
+ font-size: 24px;
+ font-weight: 600;
+ color: #212529;
+}
\ No newline at end of file
diff --git a/frontend-assignment/src/components/KickStarter/kickStarter.service.js b/frontend-assignment/src/components/KickStarter/kickStarter.service.js
new file mode 100644
index 000000000..67ab646a3
--- /dev/null
+++ b/frontend-assignment/src/components/KickStarter/kickStarter.service.js
@@ -0,0 +1,9 @@
+export const getKickStarterData = async () => {
+ try {
+ const response = await fetch('https://raw.githubusercontent.com/saaslabsco/frontend-assignment/refs/heads/master/frontend-assignment.json');
+ const data = await response.json();
+ return data;
+ } catch (error) {
+ return [];
+ }
+}
\ No newline at end of file
diff --git a/frontend-assignment/src/components/Pagination/Pagination.js b/frontend-assignment/src/components/Pagination/Pagination.js
new file mode 100644
index 000000000..6ef327b52
--- /dev/null
+++ b/frontend-assignment/src/components/Pagination/Pagination.js
@@ -0,0 +1,54 @@
+import React from 'react';
+
+import usePagination from '../../hooks/Pagination';
+
+import './pagination.css';
+
+const MAX_PAGES = 5;
+
+const Pagination = (props) => {
+ const { dataLength, pageSize, onPageChange } = props;
+
+ const totalPages = Math.ceil(dataLength / pageSize);
+ const maxPages = Math.min(MAX_PAGES, totalPages);
+
+ const { currentPage, paginationArr, offset, handlePageChange } = usePagination(totalPages, maxPages, onPageChange);
+
+ const handlePreviousPage = () => {
+ handlePageChange(currentPage - 1);
+ }
+
+ const handleNextPage = () => {
+ handlePageChange(currentPage + 1);
+ }
+
+ return (
+
+ )
+}
+
+export default Pagination;
\ No newline at end of file
diff --git a/frontend-assignment/src/components/Pagination/__tests__/pagination.tests.js b/frontend-assignment/src/components/Pagination/__tests__/pagination.tests.js
new file mode 100644
index 000000000..19dad3d1d
--- /dev/null
+++ b/frontend-assignment/src/components/Pagination/__tests__/pagination.tests.js
@@ -0,0 +1,106 @@
+import { render, screen } from '@testing-library/react';
+import Pagination from '../Pagination';
+
+describe('Pagination', () => {
+ it('should render the Pagination component', async () => {
+ render();
+ expect(await screen.findByLabelText('pagination navigation')).toBeInTheDocument();
+ });
+
+ it('should render the Pagination component with page numbers and next and previous buttons', async () => {
+ render( {}} />);
+ let buttons = await screen.findAllByRole('button');
+ expect(buttons).toHaveLength(3);
+ expect(buttons[0]).toHaveTextContent('1');
+ expect(buttons[1]).toHaveTextContent('2');
+ expect(buttons[2]).toHaveTextContent('Next');
+
+ expect(buttons[0]).toHaveClass('selected-btn');
+ await buttons[1].click();
+ expect(buttons[1]).toHaveClass('selected-btn');
+ buttons = await screen.findAllByRole('button');
+ expect(buttons).toHaveLength(3);
+ expect(buttons[0]).toHaveTextContent('Previous');
+ expect(buttons[1]).toHaveTextContent('1');
+ expect(buttons[2]).toHaveTextContent('2');
+
+ await buttons[2].click();
+ expect(buttons[2]).toHaveClass('selected-btn');
+
+ await buttons[0].click();
+ buttons = await screen.findAllByRole('button');
+ expect(buttons).toHaveLength(3);
+ expect(buttons[0]).toHaveTextContent('1');
+ expect(buttons[1]).toHaveTextContent('2');
+ expect(buttons[2]).toHaveTextContent('Next');
+
+ expect(buttons[0]).toHaveClass('selected-btn');
+ });
+
+ it('should render the Pagination component with 5 number buttons', async () => {
+ render( {}} />);
+ let buttons = await screen.findAllByRole('button');
+
+ expect(buttons).toHaveLength(6);
+ expect(buttons[0]).toHaveTextContent('1');
+ expect(buttons[1]).toHaveTextContent('2');
+ expect(buttons[2]).toHaveTextContent('3');
+ expect(buttons[3]).toHaveTextContent('4');
+ expect(buttons[4]).toHaveTextContent('5');
+ expect(buttons[5]).toHaveTextContent('Next');
+
+ await buttons[1].click();
+ buttons = await screen.findAllByRole('button');
+ expect(buttons).toHaveLength(7);
+ expect(buttons[0]).toHaveTextContent('Previous');
+ expect(buttons[1]).toHaveTextContent('2');
+ expect(buttons[2]).toHaveTextContent('3');
+ expect(buttons[3]).toHaveTextContent('4');
+ expect(buttons[4]).toHaveTextContent('5');
+ expect(buttons[5]).toHaveTextContent('6');
+ expect(buttons[6]).toHaveTextContent('Next');
+ expect(buttons[1]).toHaveClass('selected-btn');
+
+ await buttons[6].click();
+ buttons = await screen.findAllByRole('button');
+ expect(buttons).toHaveLength(7);
+ expect(buttons[0]).toHaveTextContent('Previous');
+ expect(buttons[1]).toHaveTextContent('2');
+ expect(buttons[2]).toHaveTextContent('3');
+ expect(buttons[3]).toHaveTextContent('4');
+ expect(buttons[4]).toHaveTextContent('5');
+ expect(buttons[5]).toHaveTextContent('6');
+ expect(buttons[6]).toHaveTextContent('Next');
+ expect(buttons[2]).toHaveClass('selected-btn');
+
+ await buttons[5].click();
+ buttons = await screen.findAllByRole('button');
+ expect(buttons).toHaveLength(6);
+ expect(buttons[0]).toHaveTextContent('Previous');
+ expect(buttons[1]).toHaveTextContent('2');
+ expect(buttons[2]).toHaveTextContent('3');
+ expect(buttons[3]).toHaveTextContent('4');
+ expect(buttons[4]).toHaveTextContent('5');
+ expect(buttons[5]).toHaveTextContent('6');
+ expect(buttons[5]).toHaveClass('selected-btn');
+ });
+
+ it('should handle for single page', async () => {
+ render( {}} />);
+ const buttons = await screen.findAllByRole('button');
+ expect(buttons).toHaveLength(1);
+ expect(buttons[0]).toHaveTextContent('1');
+ });
+
+ it('should call onPageChange with correct page number', async () => {
+ const mockOnPageChange = jest.fn();
+ render();
+ const buttons = await screen.findAllByRole('button');
+
+ await buttons[1].click();
+ expect(mockOnPageChange).toHaveBeenCalledWith(2);
+
+ await buttons[0].click();
+ expect(mockOnPageChange).toHaveBeenCalledWith(1);
+ });
+});
diff --git a/frontend-assignment/src/components/Pagination/pagination.css b/frontend-assignment/src/components/Pagination/pagination.css
new file mode 100644
index 000000000..30ddcce80
--- /dev/null
+++ b/frontend-assignment/src/components/Pagination/pagination.css
@@ -0,0 +1,45 @@
+.btn {
+ cursor: pointer;
+ border: 1px solid #e2e2e2;
+ padding: 8px 16px;
+ border-radius: 6px;
+ background-color: #ffffff;
+ margin: 0 4px;
+ font-size: 14px;
+}
+
+.btn:hover {
+ background-color: #f5f5f5;
+ border-color: #d0d0d0;
+}
+
+.number-btn {
+ border-radius: 50%;
+ height: 36px;
+ width: 36px;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.selected-btn {
+ background-color: #4285f4;
+ color: #fff;
+ border-color: #4285f4;
+}
+
+.selected-btn:hover {
+ background-color: #3367d6;
+ border-color: #3367d6;
+}
+
+.first-page-btn,
+.last-page-btn {
+ font-weight: 500;
+}
+
+.pagination-container {
+ display: flex;
+ margin-top: 16px;
+}
\ No newline at end of file
diff --git a/frontend-assignment/src/hooks/Pagination.js b/frontend-assignment/src/hooks/Pagination.js
new file mode 100644
index 000000000..d2713aa24
--- /dev/null
+++ b/frontend-assignment/src/hooks/Pagination.js
@@ -0,0 +1,30 @@
+import { useState, useEffect } from 'react';
+
+const usePagination = (totalPages, maxPages, onPageChange) => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [paginationArr, setPaginationArr] = useState([]);
+ const [offset, setOffset] = useState(0);
+
+ useEffect(() => {
+ setPaginationArr(Array.from({ length: maxPages }, (_, index) => index + 1));
+ }, [totalPages, maxPages]);
+
+ const isLastPageVisible = (page) => {
+ return page + maxPages > totalPages;
+ }
+
+ const handlePageChange = (page) => {
+ setCurrentPage(page);
+ onPageChange(page);
+
+ if (isLastPageVisible(page)) {
+ setOffset(totalPages - maxPages);
+ } else {
+ setOffset(page === 1 ? 0 : page - 2);
+ }
+ }
+
+ return { currentPage, paginationArr, offset, handlePageChange };
+}
+
+export default usePagination;
\ No newline at end of file
diff --git a/frontend-assignment/src/index.css b/frontend-assignment/src/index.css
new file mode 100644
index 000000000..ec2585e8c
--- /dev/null
+++ b/frontend-assignment/src/index.css
@@ -0,0 +1,13 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
+}
diff --git a/frontend-assignment/src/index.js b/frontend-assignment/src/index.js
new file mode 100644
index 000000000..d563c0fb1
--- /dev/null
+++ b/frontend-assignment/src/index.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import App from './App';
+import reportWebVitals from './reportWebVitals';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+
+
+
+);
+
+// If you want to start measuring performance in your app, pass a function
+// to log results (for example: reportWebVitals(console.log))
+// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
+reportWebVitals();
diff --git a/frontend-assignment/src/reportWebVitals.js b/frontend-assignment/src/reportWebVitals.js
new file mode 100644
index 000000000..5253d3ad9
--- /dev/null
+++ b/frontend-assignment/src/reportWebVitals.js
@@ -0,0 +1,13 @@
+const reportWebVitals = onPerfEntry => {
+ if (onPerfEntry && onPerfEntry instanceof Function) {
+ import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+ getCLS(onPerfEntry);
+ getFID(onPerfEntry);
+ getFCP(onPerfEntry);
+ getLCP(onPerfEntry);
+ getTTFB(onPerfEntry);
+ });
+ }
+};
+
+export default reportWebVitals;
diff --git a/frontend-assignment/src/setupTests.js b/frontend-assignment/src/setupTests.js
new file mode 100644
index 000000000..8f2609b7b
--- /dev/null
+++ b/frontend-assignment/src/setupTests.js
@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';