diff --git a/.gitignore b/.gitignore index 0872c36c..1d1dcfd9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /node_modules app.js app.js.map +dataManager.js +dataManager.js.map diff --git a/app.ts b/app.ts new file mode 100644 index 00000000..509d150c --- /dev/null +++ b/app.ts @@ -0,0 +1,220 @@ +class RecordManager { + firstNumber: number; + lastNumber: number; + recordCount: number; + data: dataManager; + + constructor() { + this.data = new dataManager(); + this.firstNumber = 0; + this.lastNumber = 0; + this.recordCount = 0; + } + + initialize() { + let promiseArray: Promise[] = []; + promiseArray.push(this.createTableHeader()); + let recordCountPromise = this.data.fetchRecordCount() + .then(count => { + this.recordCount = count - 1; + this.updateAndDisplayRecords(); + this.recordEventHandlers(); + this.handleResize(); + }) + .catch(err => { + throw new Error('Error fetching and displaying the table records, reload the page' + err); + }); + promiseArray.push(recordCountPromise); + return Promise.all(promiseArray); + } + + /** Initializes the table head */ + createTableHeader(): Promise { + return this.data.fetchColumns() + .then(columns => { + for (const col of columns) { + $(".head").append(`${col}`); + } + }) + .catch(err => { + alert('Error creating table heading' + err); + }); + } + + /** calculates the number of rows that can fit the screen */ + getNumberOfCalculatingRows(): number { + const screenHeight = window.innerHeight; + const availableHeight = screenHeight - 110; + const rowHeight = 35; + if (availableHeight <= 0) { + return 0; + } else { + let maxRows = Math.floor(availableHeight / rowHeight); + return maxRows; + } + } + + /** fetching records that fit the screen */ + updateAndDisplayRecords(): Promise { + $('#loader').show(); + this.calculateFirstAndLastNumbers(); + this.updateArrowVisibility(); + return this.fetchAndDisplayRecords() + .then(() => { + $('#loader').hide(); + }) + .catch(err => { + alert('Error to fetch and display records, reload the page' + err); + }); + } + + /** Calculates the firstNumber and lastNumber */ + calculateFirstAndLastNumbers() { + let rowsPerPage = this.getNumberOfCalculatingRows(); + if (this.firstNumber < 0) { + this.firstNumber = 0; + } + this.lastNumber = this.firstNumber + (rowsPerPage - 1); + if (this.lastNumber >= this.recordCount) { + this.firstNumber = this.recordCount - (rowsPerPage - 1); + this.lastNumber = this.recordCount; + } + } + + updateArrowVisibility() { + if (this.firstNumber === 0) { + $('.arrow-left').hide(); + } else { + $('.arrow-left').show(); + } + if (this.lastNumber >= this.recordCount) { + $('.arrow-right').hide(); + } else { + $('.arrow-right').show(); + } + } + + fetchAndDisplayRecords(): Promise { + return this.data.fetchRecords(this.firstNumber, this.lastNumber) + .then(records => { + const inputValue = ($('#searchInput').val()); + $("#tableBody").empty(); + for (const record of records) { + // creates row for each record + $("tbody").append(``); + const lastRow = $(".row:last"); + for (const value of record) { + // assign each record to their column in a specified row + lastRow.append(`${value}`); + if (value === inputValue) { + // highlights the searched row + lastRow.css('background-color', '#DDC0B4'); + } + } + $("tbody").append(lastRow); + } + $('#page').empty().append(`Showing record: ${this.firstNumber} - ${this.lastNumber}`); + $('#loader').hide(); + }) + .catch(err => { + alert('Error while displaying records, reload the page' + err); + }); + } + + /** recalculates the record range that includes inputValue fromm user */ + searchRecordsAndResize() { + let inputValue = ($('#searchInput').val()); + if (inputValue >= 0 && inputValue <= this.recordCount) { + let calculatedRows = this.getNumberOfCalculatingRows(); + // divides the calculated max rows in half + const halfRange = Math.floor(calculatedRows / 2); + this.firstNumber = Math.max(0, inputValue - halfRange); + this.lastNumber = Math.min(this.recordCount, this.firstNumber + (calculatedRows - 1)); + } else { + alert(`Input value must be between 0 and ${this.recordCount}`); + } + } + + /** Navigates to the next set of records */ + navigateToNextPage() { + $('#searchInput').val(''); + if (0 <= this.lastNumber && this.lastNumber <= this.recordCount) { + // calculates the first number of the page + this.firstNumber = this.lastNumber + 1; + const calculatedRows = this.getNumberOfCalculatingRows(); + // calculates the last number of the page + this.lastNumber = this.firstNumber + (calculatedRows - 1); + } + return this.updateAndDisplayRecords() + .catch(err => { + alert('Error to fetch and display records, reload the page' + err); + }); + } + + navigateToPreviousPage() { + $('#searchInput').val(''); + if (0 <= this.firstNumber && this.firstNumber <= this.recordCount) { + const calculatedRows = this.getNumberOfCalculatingRows(); + this.lastNumber = this.firstNumber - 1; + this.firstNumber = this.lastNumber - (calculatedRows - 1); + } + return this.updateAndDisplayRecords() + .catch(err => { + alert('Error to fetch and display records, reload the page' + err); + }); + } + + /** calls to re-display records when screen is adjusted */ + handleResize() { + let resizeTimeout: number; + $(window).on('resize', () => { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(() => { + $('#loader').show(); + if ($('#searchInput').val() !== '') { + this.searchRecordsAndResize(); + } + this.updateAndDisplayRecords() + .then(() => { + $('#loader').hide(); + }) + .catch(err => { + alert('Error occurred while resizing, reload the page' + err); + $('#loader').hide(); + }); + }, 250); + }); + } + + recordEventHandlers() { + $('#btnSearch').on('click', (event) => { + event.preventDefault(); + this.searchRecordsAndResize(); + return this.updateAndDisplayRecords() + .catch(err => { + alert('Error to fetch and display records, reload the page' + err); + }); + }); + + $('.arrow-right').on('click', () => { + this.navigateToNextPage(); + }); + + $('.arrow-left').on('click', () => { + this.navigateToPreviousPage(); + }); + + $('#searchInput').on('input', () => { + const inputValue = $('#searchInput').val(); + if (inputValue !== undefined) { + let correctValue = inputValue.replace(/[^0-9]/g, ''); + $('#searchInput').val(correctValue); + } + }); + } +} + +window.onload = () => { + const record = new RecordManager(); + record.initialize(); +} diff --git a/dataManager.ts b/dataManager.ts new file mode 100644 index 00000000..2802233a --- /dev/null +++ b/dataManager.ts @@ -0,0 +1,52 @@ +class dataManager { + backend: string = "http://localhost:2050"; + + /** fetches the number of records from backend */ + fetchRecordCount(): Promise { + return fetch(`${this.backend}/recordCount`) + .then(res => { + if (!res.ok) { + throw 'Failed to fetch record count'; + } + return res.text(); + }) + .then(totalRecords => { + const value = parseInt(totalRecords, 10); + if (isNaN(value)) { + throw new Error('Invalid response format'); + } + return value; + }) + .catch(err => { + throw 'Error fetching the record count: ' + err; + }); + } + + /** fetches columns from backend */ + fetchColumns(): Promise { + return fetch(`${this.backend}/columns`) + .then(res => { + if (!res.ok) { + throw 'Failed to fetch columns'; + } + return res.json(); + }) + .catch(err => { + throw 'Error fetching columns' + err; + }); + } + + /** fetches records from backend */ + fetchRecords(from: number, to: number): Promise { + return fetch(`${this.backend}/records?from=${from}&to=${to}`) + .then(res => { + if (!res.ok) { + throw "Sorry, there's a problem with the network"; + } + return res.json(); + }) + .catch(err => { + throw 'Error fetching records from server ' + err; + }); + } +} diff --git a/index.html b/index.html index add5e736..4c609647 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,38 @@ + JS Onboard Project + + + -

Hello

+
+
+
+

Javascript Project

+
+
+ + +
+
+
+ +
+ +
+ + + + + + + +
- diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..af2563bc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,27 @@ +{ + "name": "onboard-javascript", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/jquery": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.16.tgz", + "integrity": "sha512-bsI7y4ZgeMkmpG9OM710RRzDFp+w4P1RGiIt30C1mSBT+ExCleeh4HObwgArnDFELmRrOpXgSYN9VF1hj+f1lw==", + "requires": { + "@types/sizzle": "*" + } + }, + "@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==" + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..a1a024bf --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "onboard-javascript", + "version": "1.0.0", + "description": "This is a JavaScript project for all new developers to complete before venturing into our web frontend codebase.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc -p .", + "start": "npm run build -- -w" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ThokozaniNqwili/onboard-javascript.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/ThokozaniNqwili/onboard-javascript/issues" + }, + "homepage": "https://github.com/ThokozaniNqwili/onboard-javascript#readme", + "devDependencies": { + "typescript": "^3.8.3" + }, + "dependencies": { + "@types/jquery": "^3.5.16" + } +} diff --git a/style.css b/style.css new file mode 100644 index 00000000..e2104ade --- /dev/null +++ b/style.css @@ -0,0 +1,224 @@ +body { + overflow: hidden; + background-color: #f7e7ce; + padding: 0; + margin: 0; +} + +tbody .row td { + padding: 8px; +} + +table { + width: 100%; + table-layout: fixed; +} + +table, +th, +td { + border: 1px solid #4B3832; + border-collapse: collapse; + text-align: center; + color: black; + white-space: normal; +} + +#recordsTable thead .head { + position: sticky; + background-color: #97694F; + border: 2px solid black; + top: 0; +} + +td:hover { + color: #97694F; + background-color: #DDC0B4; +} + +.heading { + text-align: center; + text-decoration: underline; + color: #4B3832; + padding: 0px 16px; +} + +.showRecords { + top: 0; + right: 0; + padding: 22px; + position: absolute; +} + +.showRecords #page { + display: inline-block; + font-size: 20px; +} + +.arrow-right, +.arrow-left { + border: none; + background-color: transparent; +} + +.arrow-right, +.arrow-left { + display: inline-block; + width: 25px; + height: 25px; + border-top: 5px solid #4B3832; + border-left: 5px solid #4B3832; +} + +.arrow-right:hover, +.arrow-left:hover { + color: black; +} + +.arrow-right { + transform: rotate(135deg); + float: right; +} + +.arrow-left { + transform: rotate(-45deg); + float: left; +} + +.search-container { + left: 0; + position: absolute; + top: 0; + padding: 16px; +} + +.search-container input[type=number] { + padding: 6px; + font-size: 16px; + border: 2px solid #4B3832; + background-color: #f7e7ce; + +} + +.btnSearch { + font-size: 16px; + border: 2px solid #4B3832; + padding: 6px; + background-color: #97694F; + color: #f7e7ce; +} + +#loader { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #f7e7ce; + display: flex; + justify-content: center; + align-items: center; + z-index: 1; +} + +.loader-spinner { + border: 20px solid #97694F; + border-top: 20px solid #4B3832; + border-radius: 100%; + width: 100px; + height: 100px; + animation: spin 2s linear infinite; + background-color: #f7e7ce; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.modal { + display: none; + position: fixed; + transform: translateX(50%); + z-index: 1; + top: 0; + width: 50%; + background-color: #f7e7ce; + border: 1px solid #4B3832; +} + +.modal-content { + background-color: #4B3832; + margin: 16px; + padding: 32px; + border: 1px solid #DDC0B4; + color: #f7e7ce; + font-size: 24px; +} + +.close { + float: right; + font-size: 16px; + font-weight: bold; + cursor: pointer; +} + +@media only screen and (max-width: 1330px) { + .heading { + scale: 0.9; + } + + .showRecords { + scale: 0.9; + } + + .search-container { + scale: 0.9; + } + + td { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + td:hover { + overflow: visible; + white-space: normal; + text-overflow: unset; + } +} + +@media only screen and (max-width: 900px) { + .heading { + scale: 0.8; + } + + .showRecords { + scale: 0.8; + } + + .search-container { + scale: 0.8; + } +} + +@media only screen and (max-width: 770px) { + .heading { + scale: 0.6; + margin-top: 40px; + } + + .showRecords { + scale: 0.7; + } + + .search-container { + scale: 0.7; + } +}