From b275603afbc0f8393ac7f2e9ff096da4e5d6f6c0 Mon Sep 17 00:00:00 2001 From: Yana Chernenko Date: Tue, 2 Jun 2026 21:10:29 +0200 Subject: [PATCH 1/2] add task solution --- .github/workflows/test.yml-template | 29 ++++ README.md | 32 ++-- package-lock.json | 181 +-------------------- package.json | 3 +- src/scripts/main.js | 241 +++++++++++++++++++++++++++- 5 files changed, 296 insertions(+), 190 deletions(-) create mode 100644 .github/workflows/test.yml-template diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template new file mode 100644 index 000000000..44ac4e963 --- /dev/null +++ b/.github/workflows/test.yml-template @@ -0,0 +1,29 @@ +name: Test + +on: + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm start & sleep 5 && npm test + - name: Upload tests report(cypress mochaawesome merged HTML report) + if: ${{ always() }} + uses: actions/upload-artifact@v2 + with: + name: report + path: reports diff --git a/README.md b/README.md index cca9c9f4a..f48053002 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ 1. Replace `` with your GitHub username in the link - - [DEMO LINK](https://.github.io/js_employees_table_DOM/) + - [DEMO LINK](https://yana-longstocking.github.io/js_employees_table_DOM/) 2. Follow [this instructions](https://mate-academy.github.io/layout_task-guideline/) - - Run `npm run test` command to test your code; - - Run `npm run test:only -- -n` to run fast test ignoring linter; - - Run `npm run test:only -- -l` to run fast test with additional info in console, ignoring linter. + - Run `npm run test` command to test your code; + - Run `npm run test:only -- -n` to run fast test ignoring linter; + - Run `npm run test:only -- -l` to run fast test with additional info in console, ignoring linter. ### Task: Employees table @@ -11,6 +11,7 @@ Dear mate, this is the final task of the JS Advanced course. Apply all the acquired skills and demonstrate what you are capable of! Let's get started. Briefly about the tasks: + 1. Implement table sorting by clicking on the title (in two directions). 2. When the user clicks on a row, it should become selected. 3. Write a script to add a form to the document. Form allows users to add new employees to the spreadsheet. @@ -25,31 +26,38 @@ Start table: ![Preview](./src/images/preview.png) ##### Implement table sorting by clicking on the title (in two directions) + - When users click on one of the table headers, the table should be sorted in ASC order; the second click sorts it in DESC order. - When users click on a new title, always sort in ASC order. ##### When the user clicks on a row, it should become selected. + - Use 'active' class for the table row to indicate it is selected. - Only one line can be selected at a time. ##### Write a script to add a form to the document. Form allows users to add new employees to the spreadsheet. + - The form should have class `new-employee-form` (to apply correct styles). - The form should have: - 4 inputs: `name`, `position`, `age`, and `salary` - 1 select: `office` - Submit button. - Put inputs inside labels: + ```html - + ``` + - Add qa attributes for each input field: + ``` - data-qa="name" - data-qa="position" - data-qa="office" - data-qa="age" - data-qa="salary" + data-qa="name" + data-qa="position" + data-qa="office" + data-qa="age" + data-qa="salary" ``` + - Select should have 6 options: `Tokyo`, `Singapore`, `London`, `New York`, `Edinburgh`, `San Francisco`. - Use texts for labels and buttons from the screenshot below. - Age and salary inputs should be of a numeric type. Don't forget to convert the string from the salary input to the correct value, like in the table. @@ -57,6 +65,7 @@ Start table: - All fields are required. ##### Show notification if form data is invalid. + - Click on `Save to table` should run validation for form inputs. If the data is valid, add a new employee to the table. - If the `Name` value has fewer than 4 letters, show error notification. - If the `Age` value is less than 18 or more than 90, show an error notification. @@ -64,7 +73,8 @@ Start table: - Notification titles and descriptions are up to you. - Add qa attribute for notification: `data-qa="notification"` and class `error`/`success` depending on the result. -##### Implement editing of table cells by double-clicking on them (optional). +##### Implement editing of table cells by double-clicking on them (optional). + - Double click on the cell of the table, which should remove the text, and append input with `cell-input` class. - The input value should be replaced by the input text. - Only one cell can be edited at a time. diff --git a/package-lock.json b/package-lock.json index f844d43fd..adcefabad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@mate-academy/eslint-config": "latest", "@mate-academy/jest-mochawesome-reporter": "^1.0.0", "@mate-academy/linthtml-config": "latest", - "@mate-academy/scripts": "^1.8.5", + "@mate-academy/scripts": "^2.1.3", "@mate-academy/stylelint-config": "latest", "@parcel/transformer-sass": "^2.12.0", "cypress": "^13.13.0", @@ -1467,10 +1467,11 @@ "dev": true }, "node_modules/@mate-academy/scripts": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.6.tgz", - "integrity": "sha512-b4om/whj4G9emyi84ORE3FRZzCRwRIesr8tJHXa8EvJdOaAPDpzcJ8A0sFfMsWH9NUOVmOwkBtOXDu5eZZ00Ig==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", @@ -5843,87 +5844,6 @@ "node": ">=12 || >=16" } }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-select/node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/css-select/node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/css-select/node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/css-select/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", @@ -5961,45 +5881,6 @@ "node": ">=4" } }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true, - "optional": true, - "peer": true - }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -12947,20 +12828,6 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, - "node_modules/srcset": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/srcset/-/srcset-5.0.1.tgz", - "integrity": "sha512-/P1UYbGfJVlxZag7aABNRrulEXAwCSDo7fklafOQrantuPTDmYgijJMks2zusPCVzgW9+4P69mq7w6pYuZpgxw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -13433,44 +13300,6 @@ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", "dev": true }, - "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index 8d3435b6e..33390a32c 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,12 @@ "postinstall": "npm run update", "test": "npm run lint && npm run test:only" }, - "dependencies": {}, "devDependencies": { "@linthtml/linthtml": "^0.9.6", "@mate-academy/eslint-config": "latest", "@mate-academy/jest-mochawesome-reporter": "^1.0.0", "@mate-academy/linthtml-config": "latest", - "@mate-academy/scripts": "^1.8.5", + "@mate-academy/scripts": "^2.1.3", "@mate-academy/stylelint-config": "latest", "@parcel/transformer-sass": "^2.12.0", "cypress": "^13.13.0", diff --git a/src/scripts/main.js b/src/scripts/main.js index a765fdb1d..f51a337da 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -1,3 +1,242 @@ 'use strict'; -// write code here +const table = document.querySelector('table'); +let ascending = true; + +table.onclick = function (e) { + if (e.target.tagName !== 'TH') { + return; + } + + const th = e.target; + + sortTable(th.cellIndex); + ascending = !ascending; +}; + +function compareTextValues(textA, textB, asc = true) { + if (asc) { + return textA.localeCompare(textB); + } + + return textB.localeCompare(textA); +} + +function compareNumericValues(numA, numB, asc = true) { + if (ascending) { + return numA - numB; + } + + return numB - numA; +} + +function parseCurrency(price) { + return parseFloat(price.replace('$', '').replace(',', '')); +} + +function compareCurrencyValues(priceA, priceB, asc = true) { + const amountA = parseCurrency(priceA); + const amountB = parseCurrency(priceB); + + if (ascending) { + return amountA - amountB; + } + + return amountB - amountA; +} + +function sortTable(columnIndex) { + const tbody = table.querySelector('tbody'); + const rows = Array.from(tbody.rows); + + rows.sort((a, b) => { + const cellA = a.cells[columnIndex].textContent.trim(); + const cellB = b.cells[columnIndex].textContent.trim(); + + if (cellA.includes('$')) { + return compareCurrencyValues(cellA, cellB, ascending); + } + + if (!isNaN(cellA)) { + return compareNumericValues(cellA, cellB, ascending); + } + + return compareTextValues(cellA, cellB, ascending); + }); + + rows.forEach((row) => { + tbody.appendChild(row); + }); +} + +const rowsElements = Array.from(document.querySelectorAll('tbody tr')); + +rowsElements.forEach((rowElement) => { + rowElement.addEventListener('click', () => { + rowsElements.forEach((r) => r.classList.remove('active')); + rowElement.classList.add('active'); + }); +}); + +document.body.append(addNewEmployeeForm()); + +function addNewEmployeeForm( + newEmployeeName = '', + newEmployeePosition = '', + newEmployeeOffice = 'Tokyo', + newEmployeeAge = '', + newEmployeeSalary = '', +) { + const newEmployeeForm = document.createElement('form'); + + newEmployeeForm.classList.add('new-employee-form'); + + newEmployeeForm.innerHTML = ` + + + + + + + `; + + newEmployeeForm.elements.office.value = newEmployeeOffice; + + newEmployeeForm.addEventListener('submit', (e) => { + e.preventDefault(); + validateForm(newEmployeeForm); + }); + + return newEmployeeForm; +} + +function showNotification(type, description) { + const existingNotification = document.querySelector( + '[data-qa="notification"]', + ); + + if (existingNotification) { + existingNotification.remove(); + } + + const notification = document.createElement('div'); + + notification.classList.add('notification', type); + notification.setAttribute('data-qa', 'notification'); + + let title = 'Error'; + + if (type === 'success') { + title = 'Success'; + } + + notification.innerHTML = `${title} ${description}`; + + document.body.append(notification); +} + +function formatSalary(salary) { + return `$${Number(salary).toLocaleString('en-US')}`; +} + +function addEmployeeToTable(employeeName, position, office, age, salary) { + const tbody = document.querySelector('tbody'); + const row = document.createElement('tr'); + + row.innerHTML = ` + ${employeeName} + ${position} + ${office} + ${age} + ${formatSalary(salary)} + `; + + tbody.appendChild(row); +} + +function validateForm(newEmployeeForm) { + const employeeName = newEmployeeForm.elements.name.value; + const position = newEmployeeForm.elements.position.value; + const office = newEmployeeForm.elements.office.value; + const age = newEmployeeForm.elements.age.value; + const salary = newEmployeeForm.elements.salary.value; + + if (!employeeName || !position || !office || !age || !salary) { + showNotification('error', 'All fields are required'); + + return; + } + + if (employeeName.length < 4) { + showNotification('error', 'Name must be at least 4 characters long'); + + return; + } + + if (Number(age) < 18 || Number(age) > 90) { + showNotification('error', 'Age must be between 18 and 90'); + + return; + } + + const ageValue = Number(age); + const salaryValue = Number(salary); + + addEmployeeToTable(employeeName, position, office, ageValue, salaryValue); + showNotification('success', 'Employee added successfully'); + newEmployeeForm.reset(); +} + +let currentlyEditingCell = null; + +document.querySelector('tbody').addEventListener('dblclick', (e) => { + const cell = e.target; + + if (cell.tagName !== 'TD') { + return; + } + + if (currentlyEditingCell) { + return; + } + + currentlyEditingCell = cell; + + const oldValue = cell.textContent; + + const input = document.createElement('input'); + + input.classList.add('cell-input'); + input.value = oldValue; + + cell.textContent = ''; + cell.appendChild(input); + + input.focus(); + + function save() { + let newValue = input.value.trim(); + + if (newValue === '') { + newValue = oldValue; + } + + cell.textContent = newValue; + currentlyEditingCell = null; + } + + input.addEventListener('blur', save); + + input.addEventListener('keydown', (evt) => { + if (evt.key === 'Enter') { + save(); + } + }); +}); From ab24ef6a54436b4d767d549b082d49cd9690a006 Mon Sep 17 00:00:00 2001 From: Yana Chernenko Date: Tue, 2 Jun 2026 22:01:49 +0200 Subject: [PATCH 2/2] add task solution fix --- src/scripts/main.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/scripts/main.js b/src/scripts/main.js index f51a337da..86309c9b1 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -2,16 +2,23 @@ const table = document.querySelector('table'); let ascending = true; +let lastSortedColumn = null; table.onclick = function (e) { if (e.target.tagName !== 'TH') { return; } - const th = e.target; + const columnIndex = e.target.cellIndex; - sortTable(th.cellIndex); - ascending = !ascending; + if (lastSortedColumn === columnIndex) { + ascending = !ascending; + } else { + ascending = true; + lastSortedColumn = columnIndex; + } + + sortTable(columnIndex); }; function compareTextValues(textA, textB, asc = true) { @@ -23,7 +30,7 @@ function compareTextValues(textA, textB, asc = true) { } function compareNumericValues(numA, numB, asc = true) { - if (ascending) { + if (asc) { return numA - numB; } @@ -38,7 +45,7 @@ function compareCurrencyValues(priceA, priceB, asc = true) { const amountA = parseCurrency(priceA); const amountB = parseCurrency(priceB); - if (ascending) { + if (asc) { return amountA - amountB; }