diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..95853f93 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:2050", + "webRoot": "${workspaceFolder}", + "sourceMapPathOverrides": { + "webpack:///./*": "${webRoot}/*" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..24347771 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "IMQS" + ] +} \ No newline at end of file diff --git a/app.ts b/app.ts new file mode 100644 index 00000000..6f7e6d90 --- /dev/null +++ b/app.ts @@ -0,0 +1,353 @@ +class InitializeApp { + IMQS: string = "http://localhost:2050"; + /** Current value of the first record being displayed */ + currentValueOfFirstRecord: number = 0; + /** Index of the first record currently displayed on the page */ + currentFirstRecordIndex: number = 0; + /** Current page number (changes dynamically) */ + currentPage: number = 1; + /** Total number of pages available (changes dynamically) */ + totalPages: number = 1; + /** Default number of records to display per page (changes on screen size) */ + recordsPerPage: number = 16; + /** Index of the record being searched for (null if not searching) */ + searchedIndex: number | null = null; + /** Actual value of the record being searched for (null if not searching) */ + searchedValue: number | null = null; + /** Checks if the button is enabled/disabled */ + isButtonDisabled: boolean = false; + + constructor() { + $(window).on( + "resize", + this.debounce(() => { + this.updateScreen(); + }, 250) + ); + this.fetchColumns(); + this.updateScreen(); + this.eventHandlers(); + } + + /** Fetch the total number of records from the server */ + totalRecords(): Promise { + return fetch(`${this.IMQS}/recordCount`) + .then(recordCountResponse => { + if (!recordCountResponse.ok) { + throw new Error("Error trying to get recordCount"); + } + return recordCountResponse.text(); + }) + .then(recordCountData => { + return parseInt(recordCountData); + }) + .catch(error => { + throw error; + }); + } + + /** Fetch column names and create them as table headings */ + fetchColumns(): Promise { + return fetch(`${this.IMQS}/columns`) + .then((columnsResponse) => { + if (!columnsResponse.ok) { + throw new Error("Error trying to fetch the columns"); + } + return columnsResponse.json(); + }) + .then((columns: string[]) => { + const tableHeaderRow = $("#tableHeaderRow"); + for (const columnName of columns) { + const th = $("").text(columnName); + tableHeaderRow.append(th); + } + return columns; + }) + .catch((error) => { + throw error; + }); + } + + /** Fetch records within a specified range */ + fetchRecords(fromRecord: number, toRecord: number): Promise { + if (fromRecord > toRecord) { + return Promise.reject( + new Error( + "Invalid arguments: fromRecord cannot be greater than toRecord" + ) + ); + } + return fetch(`${this.IMQS}/records?from=${fromRecord}&to=${toRecord}`).then( + (response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response.json(); + } + ); + } + + displayData(fromRecord: number, recordsDisplayed: number): void { + $("#loader").show(); + $("#tableWrapper").hide(); + const adjustedFromRecord = Math.max(fromRecord, 0); + let recordCount: number; + this.totalRecords() + .then(count => { + recordCount = count; + this.totalPages = Math.ceil(recordCount / this.recordsPerPage); + if (this.currentPage > this.totalPages) { + this.currentPage = this.totalPages; + this.currentFirstRecordIndex = + Math.max(0, (this.currentPage - 1) * this.recordsPerPage); + } + const maxToRecord = + Math.min(adjustedFromRecord + recordsDisplayed - 1, recordCount - 1); + // Check if the calculated range exceeds the total records + if (maxToRecord < adjustedFromRecord) { + throw new Error("Error trying to display the data"); + } + return this.fetchRecords(adjustedFromRecord, maxToRecord).then( + (data) => { + return { data, maxToRecord }; + } + ); + }) + .then(({ data, maxToRecord }) => { + let tableData = ""; + if (data && data.length > 0) { + this.currentValueOfFirstRecord = parseInt(data[0][0]); + this.currentFirstRecordIndex = adjustedFromRecord; + for (const record of data) { + tableData += ""; + for (const value of record) { + tableData += `${value}`; + } + tableData += ""; + } + } + // Hide the "Next Page" button if maxToRecord is the last record + if (maxToRecord >= recordCount - 1) { + $("#nextPageButton").hide(); + } else { + $("#nextPageButton").show(); + } + if (this.searchedIndex !== null) { + this.currentPage = Math.ceil( + (this.searchedIndex + 1) / this.recordsPerPage + ); + this.currentFirstRecordIndex = Math.max( + this.searchedIndex - this.recordsPerPage + 1, 0 + ); + this.searchedIndex = null; + } + $("#tableBody").html(tableData); + $("#loader").hide(); + $("#tableWrapper").show(); + }) + .catch(error => { + throw new Error("An error occurred while fetching and displaying data." + error); + }); + } + + /** Handle the search method */ + async searchMethod(searchValue: number): Promise { + const totalRecCount = await this.totalRecords() + .catch(error => { + throw new Error("No valid records found" + error); + }); + if (searchValue < 0 || searchValue >= totalRecCount) { + window.alert("Record not found on this database"); + return; + } + const lastRecordIndex = totalRecCount - 1; + const targetPage = Math.ceil((searchValue + 1) / this.recordsPerPage); + const fromRecord = (targetPage - 1) * this.recordsPerPage; + const toRecord = Math.min( + fromRecord + this.recordsPerPage, + lastRecordIndex + ); + this.currentPage = targetPage; + this.searchedValue = searchValue; + this.currentFirstRecordIndex = fromRecord; + this.displayData(fromRecord, this.recordsPerPage); + } + + /** Update the screen layout and data display */ + updateScreen(): void { + const newScreenHeight = window.innerHeight; + this.recordsPerPage = this.windowAdjustments(newScreenHeight); + this.totalRecords() + .then(totalRecCount => { + let fromRecord: number; + if (this.searchedValue !== null) { + const searchIndex = Math.min(this.searchedValue, totalRecCount - 1); + const targetPage = Math.ceil((searchIndex + 1) / this.recordsPerPage); + fromRecord = (targetPage - 1) * this.recordsPerPage; + } else { + const previousFirstRecordIndex = this.currentFirstRecordIndex; + this.currentPage = Math.ceil( + (previousFirstRecordIndex + 1) / this.recordsPerPage + ); + fromRecord = this.currentFirstRecordIndex; + } + if (this.currentPage * this.recordsPerPage > totalRecCount - 1) { + const lastPage = Math.ceil(totalRecCount / this.recordsPerPage); + this.currentPage = lastPage; + fromRecord = (lastPage - 1) * this.recordsPerPage; + } + this.displayData(fromRecord, this.recordsPerPage); + }) + .catch(error => { + throw new Error("Error updating screen:" + error); + }); + } + + /** Adjust the number of records displayed based on screen height */ + windowAdjustments(screenHeight: number): number { + const estimatedRowHeightFactor = 1; + const estimatedRowHeight = estimatedRowHeightFactor * 50; + const availableScreenHeight = screenHeight - 140; + this.recordsPerPage = Math.floor( + availableScreenHeight / estimatedRowHeight + ); + // This ensures that will at least be 1 record on display + return Math.max(this.recordsPerPage, 1); + } + + /** Create a debounce function to delay function execution */ + debounce(func: any, delay: number) { + let timeoutId: any; + return function (...args: any) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + func(...args); + }, delay); + }; + } + + /** Handles the events such as pagination buttons, input handling and search form */ + eventHandlers(): void { + /* Ensures that only positive numbers is accepted */ + $("#searchInput").on("input", (e) => { + const inputElement = e.target as HTMLInputElement; + const inputValue = inputElement.value; + const validInputRegex = /^[0-9]+$/; + if (!validInputRegex.test(inputValue)) { + const sanitizedInput = inputValue.replace(/[^0-9]/g, ""); + inputElement.value = sanitizedInput; + } + }); + + /* Handle form submission for searching */ + $("#searchForm").submit((e) => { + e.preventDefault(); + const searchInputValue = $("#searchInput").val(); + const searchValue = Number(searchInputValue); + this.searchedIndex = null; + this.searchMethod(searchValue) + .catch(error => { + window.alert("An error occurred during search. Please try again." + error); + }); + }); + + /* Handle previous page button click */ + $("#prevPageButton").on("click", () => { + if ($("#prevPageButton").hasClass("hidden")) { + return; + } + if (this.currentFirstRecordIndex <= 0) { + const errorMessage = "Already on the first page"; + window.alert(errorMessage); + return; + } + $("#prevPageButton").addClass("hidden"); + this.searchedIndex = null; + this.searchedValue = null; + const firstRecordOfCurrentPage = + (this.currentPage - 1) * this.recordsPerPage; + let fromRecord = firstRecordOfCurrentPage; + $("#nextPageButton").hide(); + $("#prevPageButton").hide(); + $("#tableWrapper").hide(); + $("#loader").show(); + this.totalRecords() + .then(() => { + $("#prevPageButton").removeClass("hidden"); + $("#loader").hide(); + $("#tableWrapper").show(); + $("#nextPageButton").show(); + $("#prevPageButton").show(); + if (this.currentPage < this.totalPages) { + $("#nextPageButton").show(); + } + }) + .then(() => { + if (this.currentValueOfFirstRecord <= this.recordsPerPage) { + this.currentPage = 1; + this.currentFirstRecordIndex = 0; + } else { + this.currentPage--; + this.currentFirstRecordIndex -= this.recordsPerPage; + } + fromRecord = this.currentFirstRecordIndex; + this.displayData(fromRecord, this.recordsPerPage); + }) + .catch(error => { + throw new Error("Error while trying go to the previous page" + error); + }); + }); + + /* Handle next page button click */ + $("#nextPageButton").on("click", () => { + if ($("#nextPageButton").hasClass("hidden")) { + return; + } + if (this.currentPage >= this.totalPages) { + const errorMessage = "Already on the last page"; + window.alert(errorMessage); + return; + } + $("#nextPageButton").addClass("hidden"); + this.searchedIndex = null; + this.searchedValue = null; + let fromRecord = this.currentFirstRecordIndex; + $("#nextPageButton").hide(); + $("#prevPageButton").hide(); + $("#tableWrapper").hide(); + $("#loader").show(); + this.totalRecords() + .then((totalRecCount: number) => { + $("#nextPageButton").removeClass("hidden"); + $("#loader").hide(); + $("#tableWrapper").show(); + $("#nextPageButton").show(); + $("#prevPageButton").show(); + if ( + typeof totalRecCount === "number" && + this.currentFirstRecordIndex >= totalRecCount + ) { + $("#nextPageButton").hide(); + } + }) + .then(() => { + if (this.currentPage < this.totalPages) { + this.currentPage++; + this.currentFirstRecordIndex += this.recordsPerPage; + } else { + this.currentPage = this.totalPages; + } + fromRecord = this.currentFirstRecordIndex; + this.displayData(fromRecord, this.recordsPerPage); + }) + .catch(() => { + throw new Error("Error while trying go to the next page"); + }); + }); + } +} + +window.onload = () => { + $("#loader").hide(); + new InitializeApp(); +}; diff --git a/index.html b/index.html index add5e736..b0e8f30c 100644 --- a/index.html +++ b/index.html @@ -2,12 +2,40 @@ JS Onboard Project + + - -

Hello

+ +
+
+
+
+
+
+ + + + + + + + + +
+
+
- - - + \ No newline at end of file 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..8edad3a8 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "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/MeezaanD/onboard-javascript.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/MeezaanD/onboard-javascript/issues" + }, + "homepage": "https://github.com/MeezaanD/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..a8408ec1 --- /dev/null +++ b/style.css @@ -0,0 +1,201 @@ +/* Colors */ +:root { + --primary-color: white; + --secondary-color: #292929; + --tertiary-color: black; + --shadow-color: #e3e3e3; + --selected-color: #0d6efd; +} + +* { + padding: 0; + margin: 0; + box-sizing: border-box; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; +} + +body { + overflow: hidden; + background: var(--shadow-color); + background-attachment: fixed; + height: 100vh; +} + +/* BUTTON STARTS */ +button { + cursor: pointer; + padding: 15px; + height: auto; + min-width: fit-content; + margin-top: 20px; + color: var(--primary-color); + background-color: var(--secondary-color); + border-radius: 0; + box-shadow: inset 20px 20px 60px #232323, inset -20px -20px 60px #2f2f2f; +} + +/* LIST STYLING STARTS */ +.navigation { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 20px; + gap: 5px; + width: 100%; +} + +/* FORM STARTS */ +form { + display: flex; + justify-content: center; + gap: 5px; +} + +.form-group { + display: flex; +} + +input { + margin-top: 20px; + padding: 10px; + height: auto; + width: auto; + color: var(--tertiary-color); + background-color: transparent !important; + border-radius: 0; + box-shadow: inset 20px 20px 60px #c1c1c1, inset -20px -20px 60px #ffffff; +} + +input::placeholder { + color: var(--tertiary-color); +} + +/* LOADER STARTS */ +#loader { + margin: 5rem; +} + +.loader { + margin: auto; + width: 50px; + height: 50px; + position: relative; + display: flex; + justify-content: center; +} + +.loader:before, +.loader:after { + content: ""; + position: absolute; + top: -10px; + left: -10px; + width: 100%; + height: 100%; + border-radius: 100%; + border: 10px solid transparent; + border-top-color: var(--secondary-color); +} + +.loader:before { + z-index: 100; + animation: spin 1s infinite; +} + +.loader:after { + border: 10px solid #ccc; +} + +@keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } + + 100% { + -webkit-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +/* TABLE STARTS */ +.table-container { + width: 100%; + border: 1px solid var(--tertiary-color); + position: fixed; +} + +table { + flex-grow: 1; + width: 100%; + border-collapse: collapse; + table-layout: fixed; + padding: 1rem; + font-size: normal; +} + +body, +thead, +tbody { + background: var(--shadow-color); + box-shadow: inset 20px 20px 60px #c1c1c1, inset -20px -20px 60px #ffffff; +} + +table, +th { + color: var(--tertiary-color); + font-weight: 700; + background: var(--shadow-color); + box-shadow: inset 20px 20px 60px #c1c1c1, inset -20px -20px 60px #ffffff; +} + +th { + border-right: 2px solid var(--tertiary-color); + border-bottom: 2px solid var(--tertiary-color); + padding: 10px; +} + +th:hover { + color: var(--primary-color); + background: var(--secondary-color); + box-shadow: none !important; + border: none !important; +} + +td { + border-right: 1px solid var(--secondary-color); + font-weight: 300; + padding: 14px; + white-space: normal; +} + +tr:hover { + cursor: pointer; + color: var(--primary-color) !important; + background: var(--selected-color); + box-shadow: none !important; + border: 1px solid var(--secondary-color); +} + +td:nth-child(1) { + font-weight: 500; +} + +@media screen and (max-width: 768px) { + td { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + td:hover { + overflow: visible; + white-space: normal; + text-overflow: unset; + } +}