diff --git a/package.json b/package.json new file mode 100644 index 000000000..9722bc132 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "saaslabs", + "version": "0.1.0", + "private": true, + "dependencies": { + "cra-template": "1.2.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-scripts": "5.0.1" + }, + "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" + ] + }, + "devDependencies": { + "web-vitals": "^4.2.4" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 000000000..aa069f27c --- /dev/null +++ b/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/public/logo192.png b/public/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/public/logo512.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 000000000..080d6c77a --- /dev/null +++ b/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/public/robots.txt b/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/src/App.css b/src/App.css new file mode 100644 index 000000000..74b5e0534 --- /dev/null +++ b/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.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/src/App.js b/src/App.js new file mode 100644 index 000000000..ebfb9caab --- /dev/null +++ b/src/App.js @@ -0,0 +1,73 @@ + +import './App.css'; +import Table from './components/table/index.tsx'; +import {theadData} from './mockData/mockData.js'; +import {useEffect, useState} from 'react'; +import { getService } from './services/http.ts'; +import {returnDataWrtTableContent} from './utils/helpers.ts'; + + +function App() { + + // console.log(data); + const [tbodyData, settbodyData] = useState([]); + const [startIndex, setStartIndex] = useState(0); + const [endIndex, setendIndex] = useState(5); + const [loading, setLoading] = useState(false); + const [tableData, setTbaldata] = useState([]); + + const callAPI = async ()=>{ + setLoading(true); + const resp = await getService('https://raw.githubusercontent.com/saaslabsco/frontend-assignment/refs/heads/master/frontend-assignment.json'); + const {data, pageSize} = returnDataWrtTableContent(resp); + settbodyData([...data]); + setLoading(false); + setTbaldata([...resp]) + } + + useEffect(()=>{ + callAPI(); + },[]) + + const handlePrevPage =()=>{ + setStartIndex(startIndex - 5); + setendIndex(endIndex - 5); + } + + const handleNextPage =()=>{ + setStartIndex(endIndex); + setendIndex(endIndex + 5); + } + + useEffect(()=>{ + const data = returnDataWrtTableContent(tableData, startIndex, endIndex); + settbodyData([...data.data]); + console.log(startIndex, endIndex); + + },[startIndex, endIndex]) + + function Pagination (){ + + return ( +
+ + +
+ ) + } + + + return ( +
+ SaasLabs Assignment + + + + ); +} + +export default App; diff --git a/src/components/table/index.css b/src/components/table/index.css new file mode 100644 index 000000000..daf98d557 --- /dev/null +++ b/src/components/table/index.css @@ -0,0 +1,20 @@ +#tbl { + font-family: Arial, Helvetica, sans-serif; + border-collapse: collapse; + width: 100%; + } + + + #tbl td, #tbl th { + border: 1px solid #ddd; + padding: 8px; + } + + + #tbl th { + padding-top: 12px; + padding-bottom: 12px; + text-align: left; + background-color: #04AA6D; + color: white; + } \ No newline at end of file diff --git a/src/components/table/index.tsx b/src/components/table/index.tsx new file mode 100644 index 000000000..fa6e60842 --- /dev/null +++ b/src/components/table/index.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import TableRow from "./tableRow/index.tsx"; +import TableHeader from "./tableHeader/index.tsx"; +import './index.css' + +type tbodyType = { + "s.no": number; + "amt.pledged": number; + blurb: string; + by: string; + country: string; + currency: string; + "end.time": string; + location: string; + "percentage.funded": number; + "num.backers": string; + state: string; + title: string; + type: string; + url: string; +}; + +interface tableProps { + theadData: string[]; + tbodyData: tbodyType[]; + loading: boolean +} + +const Table = (props: tableProps) => { + const { theadData = [], tbodyData = [], loading } = props; + return ( +
+ + + {theadData.map((h) => { + return ; + })} + + + + { + loading ?
Loading ...
: tbodyData.map((item) => { + return ; + })} + + +
+ ); +}; + +export default Table; diff --git a/src/components/table/tableHeader/index.test.js b/src/components/table/tableHeader/index.test.js new file mode 100644 index 000000000..fa5efa53e --- /dev/null +++ b/src/components/table/tableHeader/index.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import TableHeader from './index.tsx'; + +test('render table header with correct title', () => { + render(); + const textEle = screen.getByText(/S.No./i); + expect(textEle).toBeInTheDocument(); +}); diff --git a/src/components/table/tableHeader/index.tsx b/src/components/table/tableHeader/index.tsx new file mode 100644 index 000000000..208e900e5 --- /dev/null +++ b/src/components/table/tableHeader/index.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +const TableHeader = ({ item }) => { + return ( + + {item} + + ); +}; + +export default TableHeader; \ No newline at end of file diff --git a/src/components/table/tableRow/index.test.js b/src/components/table/tableRow/index.test.js new file mode 100644 index 000000000..bf297e5f0 --- /dev/null +++ b/src/components/table/tableRow/index.test.js @@ -0,0 +1,9 @@ +import { render, screen } from '@testing-library/react'; +import TableRow from './index.tsx'; + +test('render table body with correct title', () => { + const mock = [1,2,3] + render(); + const textEle = screen.getByText(/1/i); + expect(textEle).toBeInTheDocument(); +}); diff --git a/src/components/table/tableRow/index.tsx b/src/components/table/tableRow/index.tsx new file mode 100644 index 000000000..a632bc664 --- /dev/null +++ b/src/components/table/tableRow/index.tsx @@ -0,0 +1,28 @@ + +import React from "react"; + +type dataObj = { + id: number, + items: number[] +} + +interface props{ + data: dataObj, + loading: boolean, +} + +const TableRow = (props: props) => { + const { data = [] } = props; + return ( + <> + + {data?.map((item) => { + return {item}; + })} + + + + ); +}; + +export default TableRow; \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 000000000..ec2585e8c --- /dev/null +++ b/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/src/index.js b/src/index.js new file mode 100644 index 000000000..d563c0fb1 --- /dev/null +++ b/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/src/logo.svg b/src/logo.svg new file mode 100644 index 000000000..9dfc1c058 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/mockData/mockData.js b/src/mockData/mockData.js new file mode 100644 index 000000000..be06fe8da --- /dev/null +++ b/src/mockData/mockData.js @@ -0,0 +1,5 @@ +const theadData = ["S.No.", "Percentage funded", "Amount pledged"]; + +export { + theadData, +} \ No newline at end of file diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js new file mode 100644 index 000000000..5253d3ad9 --- /dev/null +++ b/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/src/services/http.ts b/src/services/http.ts new file mode 100644 index 000000000..dfa69d89f --- /dev/null +++ b/src/services/http.ts @@ -0,0 +1,17 @@ + +const getService = async (baseURL: string) =>{ + try{ + const response = await fetch(baseURL, { + method: 'GET' + }) + if(response.status > 400){ + return []; + } + return await response.json(); + } + catch(err){ + console.log('in catch', err); + } +} + +export {getService} \ No newline at end of file diff --git a/src/setupTests.js b/src/setupTests.js new file mode 100644 index 000000000..8f2609b7b --- /dev/null +++ b/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'; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts new file mode 100644 index 000000000..c4f1d18b1 --- /dev/null +++ b/src/utils/helpers.ts @@ -0,0 +1,21 @@ + +type inputType = {}; +type outputType = { + data: inputType[], + pageSize: number, +} +function returnDataWrtTableContent(data: inputType[] = [], startIndex: number = 0, endIndex: number = 5): outputType{ + // just extract only keys which we need to have for showing data ie, sl no, ... + const tdataList = data?.map((item, index)=>{ + let obj: {id: number, items: string[]} = {}; + obj.id = item["s.no"]; + obj.items = [index+1, item["amt.pledged"], item["percentage.funded"]]; + return obj; + }) + return { + data: tdataList.slice(startIndex, endIndex) ?? [], + pageSize: data.length/5 + }; +} + +export {returnDataWrtTableContent} \ No newline at end of file