diff --git a/README.md b/README.md index 6057f0a..5532d20 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,33 @@ -# Безопасность веб-приложений. Лабораторка №1 +# 🧮 Безопасность веб-приложений. Калькулятор. -## Схема сдачи +![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white) +![CSS3](https://img.shields.io/badge/css3-%231572B6.svg?style=for-the-badge&logo=css3&logoColor=white) +![JavaScript](https://img.shields.io/badge/javascript-%23F7DF1E.svg?style=for-the-badge&logo=javascript&logoColor=black) -1. Получить задание -2. Сделать форк данного репозитория -3. Выполнить задание согласно полученному варианту -4. Сделать PR (pull request) в данный репозиторий -6. Исправить замечания после code review -7. Получить approve -8. Прийти на занятие и защитить работу +Одностраничный калькулятор с адаптивной версткой. -## Основное задание +## 🌟 Особенности -Сделать одностраничное автономное браузерное приложение "Калькулятор". -На странице должно быть два поля ввода для чисел и выбор операции между ними. +* **История:** Поле результатов сохраняет историю вычислений. Последнее действие всегда ярче остальных. +* **Полная адаптивность:** Приложение корректно отображается как на широких мониторах, так и на узких экранах. +* **Валидация данных:** Подсветка ошибок и текстовые уведомления, если введено не число или допущена ошибка (например, деление на ноль или некорректный ввод). -![image](https://user-images.githubusercontent.com/573438/187915503-1ced3877-b5ea-45f8-8452-ef6b57749fd6.png) +## 🛠 Технологии -По нажатию на кнопку должен вычисляться результат и выводиться в текстовое поле. -Приложение должно контролировать формат вводимых значений и по необходимости сообщать об ошибке. +* **HTML5** — структура и семантика. +* **CSS3** — верстка. +* **JavaScript (ES6+)** — логика вычислений -## Дополнительные задания +## 📂 Структура проекта -1. Компоновка страницы должна адаптироваться под размер экрана. - -![image](https://user-images.githubusercontent.com/573438/187915546-846c3d9d-fdef-4f38-8f2e-b2480708e14f.png) - -2. Поле результатов должно содержать несколько предыдущие операции бледным шрифтом. - -3. При ошибке в одном из полей ввода оно должно подсвечиваться и даваться сообщение о том, что не так. - -4. Стилизовать элементы в точности, как на поясняющем рисунке. +1. `index.html` — основная разметка. +2. `style.css` — оформление и адаптивность. +3. `script.js` — математическая логика и валидация. +## 🚀 Как запустить +Приложение не требует сервера или установки зависимостей: +1. Сохраните все три файла в одну директорию. +2. Откройте `index.html` через любой современный браузер. +--- \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..10c9ecc --- /dev/null +++ b/index.html @@ -0,0 +1,41 @@ + + + + + + Калькулятор + + + + +
+
+
+ +
Введите число
+
+ + + +
+ +
Введите число
+
+
+ +
+ +
+ +
+
+
+ + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 0000000..6d046e0 --- /dev/null +++ b/script.js @@ -0,0 +1,104 @@ +document.addEventListener('DOMContentLoaded', () => { + const num1Input = document.getElementById('num1'); + const num2Input = document.getElementById('num2'); + const operatorSelect = document.getElementById('operator'); + const calcBtn = document.getElementById('calc-btn'); + const resultBox = document.getElementById('result-box'); + + const errorNum1 = document.getElementById('error-num1'); + const errorNum2 = document.getElementById('error-num2'); + + let history = []; + + calcBtn.addEventListener('click', () => { + resetErrors(); + + const val1 = num1Input.value.trim(); + const val2 = num2Input.value.trim(); + const operator = operatorSelect.value; + + let isValid = true; + + if (!isValidNumber(val1)) { + showError(num1Input, errorNum1, "Пожалуйста, введите корректное число"); + isValid = false; + } + + if (!isValidNumber(val2)) { + showError(num2Input, errorNum2, "Пожалуйста, введите корректное число"); + isValid = false; + } + + if (!isValid) return; + + const n1 = parseFloat(val1); + const n2 = parseFloat(val2); + let resultStr = ''; + + try { + const result = calculate(n1, n2, operator); + resultStr = `${n1} ${operator} ${n2} = ${result}`; + } catch (e) { + resultStr = e.message; + } + + history.push(resultStr); + + if (history.length > 4) { + history.shift(); + } + + renderResults(); + }); + + function isValidNumber(value) { + if (value === "") return false; + const numRegex = /^-?\d+([.,]\d+)?$/; + return numRegex.test(value); + } + + function calculate(n1, n2, operator) { + switch (operator) { + case '+': return n1 + n2; + case '-': return n1 - n2; + case '*': return n1 * n2; + case '/': + if (n2 === 0) throw new Error("Деление на ноль!"); + return Math.round((n1 / n2) * 100000) / 100000; + default: return 0; + } + } + + function showError(inputEl, errorEl, message) { + inputEl.classList.add('input-error'); + errorEl.textContent = message; + errorEl.style.display = 'block'; + } + + function resetErrors() { + num1Input.classList.remove('input-error'); + num2Input.classList.remove('input-error'); + errorNum1.style.display = 'none'; + errorNum2.style.display = 'none'; + } + + function renderResults() { + resultBox.innerHTML = ''; + + history.forEach((item, index) => { + const div = document.createElement('div'); + div.textContent = item; + + if (index === history.length - 1) { + div.classList.add('current-item'); + } else { + div.classList.add('history-item'); + } + + resultBox.appendChild(div); + }); + } + + num1Input.addEventListener('input', resetErrors); + num2Input.addEventListener('input', resetErrors); +}); \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..64e3cb6 --- /dev/null +++ b/style.css @@ -0,0 +1,114 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: sans-serif; +} + +body { + display: flex; + justify-content: center; + align-items: flex-start; + padding: 20px; + background-color: #e0e0e0; + min-height: 100vh; +} + +.calculator { + background-color: #bdfebf; + padding: 25px; + border-radius: 15px; + width: 100%; + max-width: 700px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.inputs-container { + display: flex; + flex-direction: row; + gap: 15px; + align-items: flex-start; + width: 100%; +} + +.input-group { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; +} + +input, select, button, .result-container { + background-color: transparent; + border: 1px dashed red; + border-radius: 10px; + color: red; + font-size: 20px; + padding: 12px; + outline: none; + width: 100%; +} + +select { + flex: 0 0 80px; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='red' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 10px center; + background-size: 16px; + padding-right: 30px; + text-align: center; +} + +.button-container { + display: flex; + justify-content: center; +} + +button { + width: auto; + padding: 10px 40px; + cursor: pointer; +} + +.result-container { + min-height: 100px; + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; +} + +.history-item { color: #c48b8b; } +.current-item { color: red; font-weight: bold; } + +.error-msg { + color: red; + font-size: 13px; + margin-top: 5px; + display: none; +} + +.input-error { + border: 2px solid red; + background-color: rgba(255, 0, 0, 0.05); +} + +@media (max-width: 650px) { + .inputs-container { + flex-direction: column; + align-items: center; + } + + .input-group, select { + width: 100%; + flex: none; + } + + select { + max-width: 120px; + } +} \ No newline at end of file