From d9ca9a23b47173230d771d0f4514bbd48a71e5d2 Mon Sep 17 00:00:00 2001 From: BohdanNAUKMA Date: Sat, 16 May 2026 23:35:32 +0300 Subject: [PATCH 1/3] feat: Update index.html, main.css --- index.html | 77 ++++++++-- main.css | 412 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 464 insertions(+), 25 deletions(-) diff --git a/index.html b/index.html index 5971bf7d..323a294f 100644 --- a/index.html +++ b/index.html @@ -1,16 +1,75 @@ - - + + - My Page - - + + Кошик покупок - - Apple - 4 - + +
+
+
+ + +
+ +
+
+ Помідори +
+ 2 + +
+
+ +
+ Печиво +
+ + 2 + + + +
+
+ +
+ +
+ + 1 + + + +
+
+
+
+ +
+
+

Залишилося

+
+ Печиво 2 + Сир 1 +
+ +
+ +

Куплено

+
+ Помідори 2 +
+
+
+
+ +
+ Buy List + Виконано: Мартинов Богдан +
+ \ No newline at end of file diff --git a/main.css b/main.css index 00bc872e..758ab52f 100644 --- a/main.css +++ b/main.css @@ -1,17 +1,397 @@ -.product-item { - background-color: gray; - display: inline-block; - height: 25px; - padding: 5px; - border-radius: 5px; -} - -.amount { - background-color: yellow; - border-radius: 10px; - - display: inline-block; - height: 20px; - width: 20px; - text-align: center; + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; +} + +body +{ + background-color: #e5e5e5; + min-height: 100vh; + padding: 40px 20px; +} + + +.app-container +{ + max-width: 1000px; + margin: 0 auto; + display: flex; + gap: 24px; + align-items: flex-start; +} + +.products-section +{ + flex: 2; + background: #ffffff; + border-radius: 8px; + padding: 16px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); +} + +.summary-section +{ + flex: 1; +} + +.summary-card +{ + background: #ffffff; + border-radius: 8px; + padding: 20px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); +} + + +.add-item-box +{ + display: flex; + gap: 12px; + border-bottom: 2px solid #f0f0f0; + padding-bottom: 16px; + margin-bottom: 8px; +} + +.input-field, .input-edit +{ + flex: 1; + padding: 10px 14px; + border: 1px solid #dcdcdc; + border-radius: 4px; + font-size: 16px; + outline: none; +} + +.input-edit +{ + border-color: #4285f4; + max-width: 200px; +} + +.input-field:focus, .input-edit:focus +{ + border-color: #4285f4; + box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2); +} + + +button +{ + cursor: pointer; + border: none; + outline: none; + font-size: 14px; + transition: background-color 0.2s ease, transform 0.1s ease; +} + +button:focus-visible +{ + outline: 3px solid #7b2cbf; + outline-offset: 2px; +} + +.btn-add +{ + background-color: #4285f4; + color: white; + padding: 10px 24px; + border-radius: 4px; + font-weight: bold; +} + +.btn-add:hover { background-color: #357ae8; } + + +.products-list +{ + display: flex; + flex-direction: column; +} + +.product-row +{ + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #f0f0f0; +} + +.product-row:last-child +{ + border-bottom: none; +} + +.product-name +{ + font-size: 18px; + color: #333; +} + + + + +.controls +{ + display: grid; + grid-template-columns: 28px 28px 28px 100px 28px; + gap: 8px; + align-items: center; + justify-items: center; +} + +.amount-badge +{ + background-color: #e0e0e0; + color: #333; + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 4px; + font-weight: bold; + font-size: 14px; +} + +.btn-plus, .btn-minus +{ + width: 28px; + height: 28px; + border-radius: 50%; + color: white; + font-weight: bold; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; +} + + +.btn-plus { background-color: #22ba46; } +.btn-plus:hover { background-color: #1e9e3b; } + + +.btn-minus { background-color: #db2828; } +.btn-minus:hover { background-color: #b82222; } + + +.btn-minus.disabled +{ + background-color: #ef9f9e; + cursor: not-allowed; +} + +.btn-status +{ + width: 100%; + background-color: #eeeeee; + color: #444; + padding: 6px 0; + border-radius: 4px; + border: 1px solid #dcdcdc; + text-align: center; +} +.btn-status:hover { background-color: #e0e0e0; } + +.btn-status.active +{ + background-color: #f8f9fa; + box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); +} + +.btn-delete +{ + background-color: #db2828; + color: white; + width: 28px; + height: 28px; + border-radius: 4px; + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; +} +.btn-delete:hover { background-color: #b82222; } + + +.summary-card h2 +{ + font-size: 20px; + margin-bottom: 14px; + color: #111; +} + +.summary-divider +{ + border: none; + border-top: 1px solid #f0f0f0; + margin: 20px 0; +} + +.summary-tags +{ + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.product-tag +{ + background-color: #eeeeee; + padding: 6px 12px; + border-radius: 20px; + font-size: 14px; + display: inline-flex; + align-items: center; + gap: 6px; + color: #444; +} + +.product-tag.bought +{ + text-decoration: line-through; + color: #888; +} + + +.tag-count { + background-color: #f2711d; + color: white; + width: 18px; + height: 18px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: bold; +} + + +[data-tooltip] +{ + position: relative; +} + +[data-tooltip]::after +{ + content: attr(data-tooltip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translate(-50%, 8px) scale(0.6); + opacity: 0; + pointer-events: none; + transition: transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.2s ease; + + background-color: #7b2cbf; + color: #ffffff; + padding: 6px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: normal; + white-space: nowrap; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); + margin-bottom: 8px; + z-index: 999; +} + +[data-tooltip]:hover::after, +[data-tooltip]:focus-visible::after +{ + opacity: 1; + transform: translate(-50%, 0) scale(1); +} + + + +.author-badge +{ + position: fixed; + bottom: 0; + left: 40px; + background-color: #5a189a; + color: #ffffff; + padding: 12px 24px; + border-radius: 12px 12px 0 0; + font-weight: bold; + font-size: 16px; + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.15); + z-index: 1000; + cursor: default; + + height: 45px; + overflow: hidden; + display: flex; + flex-direction: column; + gap: 12px; + transition: height 0.3s ease, background-color 0.3s ease; +} + +.badge-text-author +{ + font-weight: normal; + font-size: 13px; + opacity: 0.9; +} + +.author-badge:hover, .author-badge:focus-within +{ + height: 75px; + background-color: #3c096c; +} + + +@media (max-width: 650px) +{ + body + { + padding: 16px 10px; + } + + .app-container + { + flex-direction: column; + gap: 16px; + } + + .products-section, .summary-section + { + width: 100%; + flex: none; + } +} + + +@media print +{ + .author-badge + { + position: absolute; + bottom: 20px; + left: 20px; + background: #ffffff; + color: #5a189a; + border: 2px solid #5a189a; + height: auto; + padding: 8px 16px; + border-radius: 8px; + box-shadow: none; + } + + .badge-text-main + { + display: none; + } + + .badge-text-author + { + display: block; + font-size: 16px; + font-weight: bold; + } } \ No newline at end of file From a239c0a06c3d4161ead0175292ba75644d9ac60c Mon Sep 17 00:00:00 2001 From: BohdanNAUKMA Date: Sat, 13 Jun 2026 00:17:48 +0300 Subject: [PATCH 2/3] js --- index.html | 2 +- main.js | 222 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 main.js diff --git a/index.html b/index.html index 323a294f..3752d31f 100644 --- a/index.html +++ b/index.html @@ -70,6 +70,6 @@

Куплено

Buy List Виконано: Мартинов Богдан - + \ No newline at end of file diff --git a/main.js b/main.js new file mode 100644 index 00000000..cb9fd28f --- /dev/null +++ b/main.js @@ -0,0 +1,222 @@ +document.addEventListener('DOMContentLoaded', () => { + let products = [ + { id: 1, name: 'Помідори', amount: 2, isBought: true }, + { id: 2, name: 'Печиво', amount: 2, isBought: false }, + { id: 3, name: 'Сир', amount: 1, isBought: false } + ]; + + let nextId = 4; + + const productsListElement = document.querySelector('.products-list'); + const inputField = document.querySelector('.input-field'); + const btnAdd = document.querySelector('.btn-add'); + const summaryTagsContainers = document.querySelectorAll('.summary-tags'); + + function renderList() + { + productsListElement.innerHTML = ''; + + products.forEach(product => { + productsListElement.appendChild(createProductRow(product)); + }); + + updateSummary(); + } + + function createProductRow(product) + { + const row = document.createElement('div'); + row.className = `product-row ${product.isBought ? 'bought' : ''}`; + + + const nameSpan = document.createElement('span'); + nameSpan.className = 'product-name'; + nameSpan.textContent = product.name; + + const nameInput = document.createElement('input'); + nameInput.type = 'text'; + nameInput.className = 'input-edit'; + nameInput.setAttribute('aria-label', 'Редагувати назву товару'); + + if (!product.isBought) + { + nameSpan.style.cursor = 'pointer'; + + nameSpan.addEventListener('click', () => { + row.replaceChild(nameInput, nameSpan); + nameInput.value = product.name; + nameInput.focus(); + }); + + const saveEdit = () => { + const newName = nameInput.value.trim(); + if (newName) + { + product.name = newName; + } + nameSpan.textContent = product.name; + + if (nameInput.parentNode === row) + { + row.replaceChild(nameSpan, nameInput); + } + updateSummary(); + }; + + nameInput.addEventListener('blur', saveEdit); + nameInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') saveEdit(); + }); + } + + row.appendChild(nameSpan); + + const controls = document.createElement('div'); + controls.className = 'controls'; + + if (product.isBought) + { + const amountBadge = document.createElement('span'); + amountBadge.className = 'amount-badge'; + amountBadge.style.gridColumn = '2'; + amountBadge.textContent = product.amount; + + const btnStatus = document.createElement('button'); + btnStatus.type = 'button'; + btnStatus.className = 'btn-status'; + btnStatus.style.gridColumn = '4'; + btnStatus.setAttribute('data-tooltip', 'Повернути в список залишившихся'); + btnStatus.textContent = 'Не куплено'; + + btnStatus.addEventListener('click', () => { + product.isBought = false; + renderList(); + }); + + controls.appendChild(amountBadge); + controls.appendChild(btnStatus); + } + else + { + const btnMinus = document.createElement('button'); + btnMinus.type = 'button'; + btnMinus.className = `btn-minus ${product.amount === 1 ? 'disabled' : ''}`; + if (product.amount === 1) + { + btnMinus.disabled = true; + } + btnMinus.setAttribute('aria-label', 'Зменшити кількість'); + btnMinus.setAttribute('data-tooltip', 'Мінімум 1'); + btnMinus.textContent = '-'; + btnMinus.addEventListener('click', () => { + if (product.amount > 1) + { + product.amount--; + renderList(); + } + }); + + const amountBadge = document.createElement('span'); + amountBadge.className = 'amount-badge'; + amountBadge.textContent = product.amount; + + const btnPlus = document.createElement('button'); + btnPlus.type = 'button'; + btnPlus.className = 'btn-plus'; + btnPlus.setAttribute('aria-label', 'Збільшити кількість'); + btnPlus.setAttribute('data-tooltip', 'Збільшити'); + btnPlus.textContent = '+'; + btnPlus.addEventListener('click', () => { + product.amount++; + renderList(); + }); + + const btnStatus = document.createElement('button'); + btnStatus.type = 'button'; + btnStatus.className = 'btn-status active'; + btnStatus.setAttribute('data-tooltip', 'Позначити як куплене'); + btnStatus.textContent = 'Куплено'; + btnStatus.addEventListener('click', () => { + product.isBought = true; + renderList(); + }); + + const btnDelete = document.createElement('button'); + btnDelete.type = 'button'; + btnDelete.className = 'btn-delete'; + btnDelete.setAttribute('aria-label', 'Видалити товар'); + btnDelete.setAttribute('data-tooltip', 'Видалити зі списку'); + btnDelete.textContent = '×'; + btnDelete.addEventListener('click', () => { + products = products.filter(p => p.id !== product.id); + renderList(); + }); + + controls.appendChild(btnMinus); + controls.appendChild(amountBadge); + controls.appendChild(btnPlus); + controls.appendChild(btnStatus); + controls.appendChild(btnDelete); + } + + row.appendChild(controls); + return row; + } + + function updateSummary() { + const leftContainer = summaryTagsContainers[0]; + const rightContainer = summaryTagsContainers[1]; + + leftContainer.innerHTML = ''; + rightContainer.innerHTML = ''; + + products.forEach(product => { + const tag = document.createElement('span'); + tag.className = `product-tag ${product.isBought ? 'bought' : ''}`; + + tag.appendChild(document.createTextNode(product.name + ' ')); + + const countSpan = document.createElement('span'); + countSpan.className = 'tag-count'; + countSpan.textContent = product.amount; + + tag.appendChild(countSpan); + + if (product.isBought) + { + rightContainer.appendChild(tag); + } + else + { + leftContainer.appendChild(tag); + } + }); + } + + function handleAddProduct() + { + const name = inputField.value.trim(); + if (name) + { + products.push({ + id: nextId++, + name: name, + amount: 1, + isBought: false + }); + inputField.value = ''; + inputField.focus(); + renderList(); + } + } + + btnAdd.addEventListener('click', handleAddProduct); + inputField.addEventListener('keypress', (e) => { + if (e.key === 'Enter') + { + handleAddProduct(); + } + }); + + renderList(); +}); \ No newline at end of file From ebb8d5c91e9f64d0076a897afd2abc2d3d58b2e6 Mon Sep 17 00:00:00 2001 From: BohdanNAUKMA Date: Fri, 19 Jun 2026 09:12:07 +0300 Subject: [PATCH 3/3] bonus --- bonus/index.html | 58 ++++++ bonus/main.css | 352 +++++++++++++++++++++++++++++++++++ bonus/main.js | 474 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 884 insertions(+) create mode 100644 bonus/index.html create mode 100644 bonus/main.css create mode 100644 bonus/main.js diff --git a/bonus/index.html b/bonus/index.html new file mode 100644 index 00000000..e1f741fb --- /dev/null +++ b/bonus/index.html @@ -0,0 +1,58 @@ + + + + + + 💣 Сапер — Minesweeper на WebDataRocks + + + + + + + + + + + + + + +
+

💣 САПЕР

+
+
+ 🚩 Міни + 10 +
+ +
+ +
+ +
+ +
+ +
+ ⏱️ Час + 000 +
+
+
+ + + + +
+
+
+ + + + diff --git a/bonus/main.css b/bonus/main.css new file mode 100644 index 00000000..0a000278 --- /dev/null +++ b/bonus/main.css @@ -0,0 +1,352 @@ +:root +{ + --bg-page: #e8e4df; + --bg-card: #ffffff; + --bg-header: #f5f3f0; + --bg-closed: #c8c4be; + --bg-closed-hover: #bab6b0; + --bg-open: #e8e4df; + --border-color: #d0ccc6; + --border-dark: #a8a49e; + --text-primary: #2c2c2c; + --text-secondary: #6b6b6b; + --text-muted: #999; + --accent: #5a7a5a; + --danger: #b94a48; + --cell-size: 72px; + --radius: 6px; + --font-main: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; +} + +*, +*::before, +*::after +{ + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html +{ + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body +{ + font-family: var(--font-main); + background: var(--bg-page); + color: var(--text-primary); + display: flex; + flex-direction: column; + align-items: center; + min-height: 100vh; + padding: 32px 16px; + overflow-x: hidden; +} + + + + +#game-header +{ + width: 100%; + max-width: 860px; + text-align: center; + margin-bottom: 20px; +} + +#game-title +{ + font-size: 1.5rem; + font-weight: 700; + letter-spacing: 2px; + text-transform: uppercase; + color: var(--text-primary); + margin-bottom: 16px; +} + + + +#scoreboard +{ + display: flex; + justify-content: center; + align-items: stretch; + gap: 10px; + flex-wrap: wrap; +} + +.score-cell +{ + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 10px 18px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: 100px; +} + +.score-label +{ + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--text-secondary); + margin-bottom: 4px; +} + +.score-value +{ + font-size: 1.5rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: var(--text-primary); +} + + +#restart-btn, +#mode-toggle +{ + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: var(--radius); + padding: 8px 16px; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + font-family: var(--font-main); + color: var(--text-primary); + font-size: 0.85rem; + font-weight: 600; + transition: background-color 0.15s ease; +} + +#restart-btn:hover, +#mode-toggle:hover +{ + background: var(--bg-header); +} + +#restart-btn:active, +#mode-toggle:active +{ + background: var(--bg-page); +} + +#restart-btn #face +{ + font-size: 1.8rem; + line-height: 1; +} + +#mode-icon +{ + font-size: 1.2rem; +} + +/*Прапорець */ +#mode-toggle.flag-mode +{ + border-color: var(--border-dark); + background: var(--bg-header); +} + +#pivot-wrapper +{ + width: 100%; + max-width: 860px; + border-radius: var(--radius); + overflow: hidden; + border: 1px solid var(--border-color); + background: var(--bg-card); +} + +#wdr-container +{ + width: 100%; + position: relative; +} + + + +#wdr-container .wdr-credits, +#wdr-container .wdr-toolbar-wrapper, +#wdr-container .wdr-sheet-list +{ + display: none !important; +} + +#wdr-container .wdr-grid-layout +{ + border: none !important; + background: var(--bg-header) !important; +} + +#wdr-container td.wdr-cell, +#wdr-container th.wdr-cell, +#wdr-container td.wdr-header, +#wdr-container th.wdr-header +{ + width: var(--cell-size) !important; + min-width: var(--cell-size) !important; + max-width: var(--cell-size) !important; + height: var(--cell-size) !important; + min-height: var(--cell-size) !important; + max-height: var(--cell-size) !important; + text-align: center !important; + vertical-align: middle !important; + font-size: 1.3rem !important; + line-height: var(--cell-size) !important; + padding: 0 !important; + overflow: hidden !important; + font-family: var(--font-main) !important; +} + +#wdr-container .wdr-header +{ + background: var(--bg-header) !important; + color: var(--text-muted) !important; + font-size: 0.65rem !important; + font-weight: 600 !important; + letter-spacing: 0.5px !important; + border: 1px solid var(--border-color) !important; +} + +#wdr-container td.wdr-cell +{ + border: 1px solid var(--border-color) !important; + user-select: none; +} + +#wdr-container .wdr-grid-view +{ + overflow: hidden !important; +} + +#wdr-container .wdr-cell +{ + -webkit-user-select: none !important; + user-select: none !important; +} + +.cell-closed +{ + background: var(--bg-closed) !important; + border-top: 2px solid #ddd !important; + border-left: 2px solid #ddd !important; + border-bottom: 2px solid #999 !important; + border-right: 2px solid #999 !important; + cursor: pointer !important; +} + +.cell-closed:hover +{ + background: var(--bg-closed-hover) !important; +} + +/* Прапорець */ +.cell-flag +{ + background: var(--bg-closed) !important; + border-top: 2px solid #ddd !important; + border-left: 2px solid #ddd !important; + border-bottom: 2px solid #999 !important; + border-right: 2px solid #999 !important; + cursor: pointer !important; + font-size: 1.3rem !important; +} + +.cell-open-0 +{ + background: var(--bg-open) !important; + color: transparent !important; + cursor: default !important; +} + +.cell-open-number +{ + background: var(--bg-open) !important; + cursor: default !important; + font-weight: 800 !important; + font-size: 1.3rem !important; +} + +.cell-num-1 { color: #0000ff !important; } +.cell-num-2 { color: #008000 !important; } +.cell-num-3 { color: #ff0000 !important; } +.cell-num-4 { color: #000080 !important; } +.cell-num-5 { color: #800000 !important; } +.cell-num-6 { color: #008080 !important; } +.cell-num-7 { color: #000000 !important; } +.cell-num-8 { color: #808080 !important; } + + +.cell-mine +{ + background: var(--bg-open) !important; + cursor: default !important; + font-size: 1.3rem !important; +} + +.cell-mine-exploded +{ + background: #e8b4b4 !important; + cursor: default !important; + font-size: 1.4rem !important; +} + + + + +@media (max-width: 580px) +{ + :root + { + --cell-size: 42px; + } + + #game-title + { + font-size: 1.2rem; + letter-spacing: 1px; + } + + #scoreboard + { + gap: 6px; + } + + .score-cell + { + min-width: 80px; + padding: 8px 10px; + } + + .score-value + { + font-size: 1.2rem; + } + + #wdr-container td.wdr-cell, + #wdr-container th.wdr-cell, + #wdr-container td.wdr-header, + #wdr-container th.wdr-header { + font-size: 1rem !important; + } +} + +@media (max-width: 400px) +{ + :root + { + --cell-size: 34px; + } +} diff --git a/bonus/main.js b/bonus/main.js new file mode 100644 index 00000000..d532e19a --- /dev/null +++ b/bonus/main.js @@ -0,0 +1,474 @@ +const ROWS = 9; +const COLS = 9; +const MINES_TOTAL = 10; + +const SYM_CLOSED = "⬜"; +const SYM_FLAG = "🚩"; +const SYM_MINE = "💣"; + + +let gridData = []; +let gameOver = false; +let gameWon = false; +let flagMode = false; +let timerInterval = null; +let seconds = 0; +let timerStarted = false; +let explodedRow = -1; +let explodedCol = -1; + +const elMineCounter = document.getElementById("mine-counter"); +const elTimer = document.getElementById("timer"); +const elFace = document.getElementById("face"); +const elModeToggle = document.getElementById("mode-toggle"); +const elModeIcon = document.getElementById("mode-icon"); +const elModeText = document.getElementById("mode-text"); +const elRestartBtn = document.getElementById("restart-btn"); + + +let pivot = null; + + + +function generateGrid() +{ + const data = []; + + for (let r = 0; r < ROWS; r++) + { + for (let c = 0; c < COLS; c++) + { + data.push({ + Row: `Рядок ${r + 1}`, + Col: `Кол ${c + 1}`, + IsMine: false, + MinesAround: 0, + IsOpen: false, + IsFlagged: false, + Display: SYM_CLOSED, + _row: r, + _col: c + }); + } + } + + let minesPlaced = 0; + while (minesPlaced < MINES_TOTAL) + { + const idx = Math.floor(Math.random() * data.length); + if (!data[idx].IsMine) + { + data[idx].IsMine = true; + minesPlaced++; + } + } + + for (let r = 0; r < ROWS; r++) + { + for (let c = 0; c < COLS; c++) + { + const cell = getCellByRC(data, r, c); + if (cell.IsMine) continue; // для мін не рахуємо + let count = 0; + forEachNeighbor(r, c, (nr, nc) => { + const neighbor = getCellByRC(data, nr, nc); + if (neighbor && neighbor.IsMine) count++; + }); + cell.MinesAround = count; + } + } + + return data; +} + + +function getCellByRC(data, row, col) +{ + return data[row * COLS + col]; +} + + +function forEachNeighbor(row, col, callback) +{ + for (let dr = -1; dr <= 1; dr++) + { + for (let dc = -1; dc <= 1; dc++) + { + if (dr === 0 && dc === 0) continue; + const nr = row + dr; + const nc = col + dc; + if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS) + { + callback(nr, nc); + } + } + } +} + + + +function startGame() { + gameOver = false; + gameWon = false; + flagMode = false; + seconds = 0; + timerStarted = false; + explodedRow = -1; + explodedCol = -1; + clearInterval(timerInterval); + timerInterval = null; + + elFace.textContent = "🙂"; + elTimer.textContent = "000"; + elModeIcon.textContent = "⛏️"; + elModeText.textContent = "Копати"; + elModeToggle.classList.remove("flag-mode"); + document.body.classList.remove("game-won"); + + gridData = generateGrid(); + updateMineCounter(); + + if (pivot) + { + applyReport(); + } +} + + +function updateMineCounter() +{ + const flagsCount = gridData.filter(c => c.IsFlagged).length; + const remaining = MINES_TOTAL - flagsCount; + elMineCounter.textContent = String(remaining).padStart(2, "0"); +} + + + +function startTimer() +{ + if (timerStarted) return; + timerStarted = true; + timerInterval = setInterval(() => { + seconds++; + if (seconds > 999) seconds = 999; + elTimer.textContent = String(seconds).padStart(3, "0"); + }, 1000); +} + + +function stopTimer() +{ + clearInterval(timerInterval); + timerInterval = null; +} + + + +function buildReport() +{ + return { + dataSource: { + data: prepareDataForWDR() + }, + slice: { + rows: [{ uniqueName: "Row" }], + columns: [{ uniqueName: "Col" }], + measures: [{ + uniqueName: "Display", + aggregation: "max" + }] + }, + options: { + grid: { + type: "classic", + showGrandTotals: "off", + showTotals: "off" + }, + showAggregationLabels: false + } + }; +} + + +function prepareDataForWDR() +{ + return gridData.map(cell => ({ + Row: cell.Row, + Col: cell.Col, + Display: cell.Display + })); +} + + +function applyReport() +{ + pivot.setReport(buildReport()); +} + + + +function refreshPivot() +{ + if (!pivot) return; + pivot.updateData({ data: prepareDataForWDR() }); +} + + + +function onCustomizeCell(cell, data) +{ + if (!data || data.type !== "value") return; + + const rowName = (data.rows && data.rows.length > 0) ? (data.rows[0].caption || data.rows[0].member) : null; + const colName = (data.columns && data.columns.length > 0) ? (data.columns[0].caption || data.columns[0].member) : null; + + if (!rowName || !colName) return; + + const cellObj = gridData.find(c => c.Row === rowName && c.Col === colName); + if (!cellObj) return; + + const value = cellObj.Display; + cell.text = value; + + if (value === SYM_CLOSED) + { + cell.addClass("cell-closed"); + } + else if (value === SYM_FLAG) + { + cell.addClass("cell-flag"); + } + else if (value === SYM_MINE) + { + if (cellObj._row === explodedRow && cellObj._col === explodedCol) + { + cell.addClass("cell-mine-exploded"); + } + else + { + cell.addClass("cell-mine"); + } + } + else if (value === "·") + { + cell.addClass("cell-open-0"); + cell.text = ""; + } + else + { + const num = parseInt(value, 10); + if (!isNaN(num) && num >= 1 && num <= 8) { + cell.addClass("cell-open-number"); + cell.addClass("cell-num-" + num); + } + } +} + + +function onCellClick(cellData) +{ + if (gameOver || gameWon) return; + + if (!cellData || !cellData.rows || cellData.rows.length === 0 || + !cellData.columns || cellData.columns.length === 0) { + return; + } + + if (!cellData.measure) return; + + const rowName = cellData.rows[0].caption || cellData.rows[0].member; + + + const colObj = cellData.columns.find(c => c.hierarchyName === "Col" || (c.caption && c.caption.startsWith("Кол"))); + if (!colObj) return; + const colName = colObj.caption || colObj.member; + + const cell = gridData.find(c => c.Row === rowName && c.Col === colName); + if (!cell) return; + + startTimer(); + + if (flagMode) + { + handleFlagClick(cell); + } + else + { + handleDigClick(cell); + } +} + + +function handleDigClick(cell) +{ + + if (cell.IsOpen || cell.IsFlagged) return; + + if (cell.IsMine) + { + + handleGameOver(cell); + } + else if (cell.MinesAround === 0) + { + openCell(cell); + floodFill(cell._row, cell._col); + refreshPivot(); + checkWin(); + } + else + { + openCell(cell); + refreshPivot(); + checkWin(); + } +} + + +function handleFlagClick(cell) +{ + if (cell.IsOpen) return; + + + cell.IsFlagged = !cell.IsFlagged; + cell.Display = cell.IsFlagged ? SYM_FLAG : SYM_CLOSED; + + updateMineCounter(); + refreshPivot(); +} + + +function openCell(cell) +{ + if (cell.IsOpen || cell.IsFlagged) return; + cell.IsOpen = true; + + if (cell.MinesAround === 0) + { + cell.Display = "·"; + } + else + { + cell.Display = String(cell.MinesAround); + } +} + + +function floodFill(row, col) +{ + forEachNeighbor(row, col, (nr, nc) => { + const neighbor = getCellByRC(gridData, nr, nc); + if (neighbor.IsOpen || neighbor.IsFlagged || neighbor.IsMine) return; + + openCell(neighbor); + + if (neighbor.MinesAround === 0) + { + floodFill(nr, nc); + } + }); +} + + + +function handleGameOver(clickedMine) { + gameOver = true; + stopTimer(); + + + explodedRow = clickedMine._row; + explodedCol = clickedMine._col; + + + gridData.forEach(cell => { + if (cell.IsMine) { + cell.IsOpen = true; + cell.Display = SYM_MINE; + } + }); + + + elFace.textContent = "😵"; + + + + refreshPivot(); +} + + +function checkWin() +{ + const nonMineCells = gridData.filter(c => !c.IsMine); + const allOpened = nonMineCells.every(c => c.IsOpen); + + if (allOpened) + { + gameWon = true; + stopTimer(); + + elFace.textContent = "😎"; + + gridData.forEach(cell => { + if (cell.IsMine && !cell.IsFlagged) { + cell.IsFlagged = true; + cell.Display = SYM_FLAG; + } + }); + updateMineCounter(); + + document.body.classList.add("game-won"); + + refreshPivot(); + } +} + + + +elRestartBtn.addEventListener("click", () => { + startGame(); +}); + +elModeToggle.addEventListener("click", () => { + flagMode = !flagMode; + + if (flagMode) + { + elModeIcon.textContent = "🚩"; + elModeText.textContent = "Прапорець"; + elModeToggle.classList.add("flag-mode"); + } + else + { + elModeIcon.textContent = "⛏️"; + elModeText.textContent = "Копати"; + elModeToggle.classList.remove("flag-mode"); + } +}); + + + +let cellClickRegistered = false; + +function initPivot() +{ + gridData = generateGrid(); + updateMineCounter(); + + pivot = new WebDataRocks({ + container: "#wdr-container", + toolbar: false, + width: "100%", + height: "auto", + report: buildReport(), + customizeCell: onCustomizeCell, + reportcomplete: function () { + if (!cellClickRegistered) + { + cellClickRegistered = true; + pivot.on("cellclick", onCellClick); + } + } + }); +} + +document.addEventListener("DOMContentLoaded", initPivot);