diff --git a/.gitignore b/.gitignore
index 2f7896d..188ebd1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
target/
+.vscode/
+.idea/
\ No newline at end of file
diff --git a/site/hex-color/index.html b/site/hex-color/index.html
new file mode 100644
index 0000000..921b912
--- /dev/null
+++ b/site/hex-color/index.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+ Hex Color Converter - Tools
+
+
+
+
+
+
+
+
+
+
+
+
+ | Preview |
+ Hex |
+ Normalized (R, G, B, A) |
+ Action |
+
+
+
+
+
+
+
+
+
+
diff --git a/site/hex-color/script.js b/site/hex-color/script.js
new file mode 100644
index 0000000..07852ed
--- /dev/null
+++ b/site/hex-color/script.js
@@ -0,0 +1,116 @@
+async function addHexFromClipboard() {
+ try {
+ const text = await navigator.clipboard.readText();
+ const hex = text.trim();
+
+ // Parse hex color
+ const color = parseHexColor(hex);
+ if (!color) {
+ alert('Invalid hex color in clipboard. Expected format: #RRGGBB or #RRGGBBAA');
+ return;
+ }
+
+ // Add row to table
+ addColorRow(hex, color);
+
+ // Show table if hidden
+ document.getElementById('colorTable').classList.remove('hidden');
+ } catch (err) {
+ alert('Failed to read from clipboard: ' + err.message);
+ }
+}
+
+document.getElementById('addHexBtn').addEventListener('click', addHexFromClipboard);
+
+document.getElementById('showHiddenBtn').addEventListener('click', () => {
+ const rows = document.querySelectorAll('#colorTableBody tr.hidden');
+ rows.forEach(row => row.classList.remove('hidden'));
+ updateShowHiddenButton();
+});
+
+document.addEventListener('keydown', (e) => {
+ if (e.key === 'p' || e.key === 'P') {
+ addHexFromClipboard();
+ }
+});
+
+function parseHexColor(hex) {
+ // Remove leading hash if present
+ const cleanHex = hex.startsWith('#') ? hex.slice(1) : hex;
+
+ // Validate hex string (6 or 8 characters)
+ if (!/^[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(cleanHex)) {
+ return null;
+ }
+
+ // Parse components
+ const r = parseInt(cleanHex.slice(0, 2), 16) / 255;
+ const g = parseInt(cleanHex.slice(2, 4), 16) / 255;
+ const b = parseInt(cleanHex.slice(4, 6), 16) / 255;
+ const a = cleanHex.length === 8 ? parseInt(cleanHex.slice(6, 8), 16) / 255 : 1.0;
+
+ return { r, g, b, a, hex: '#' + cleanHex };
+}
+
+function addColorRow(originalHex, color) {
+ const tbody = document.getElementById('colorTableBody');
+ const row = tbody.insertRow();
+
+ // Preview cell
+ const previewCell = row.insertCell();
+ const preview = document.createElement('div');
+ preview.style.width = '40px';
+ preview.style.height = '40px';
+ preview.style.backgroundColor = `rgba(${color.r * 255}, ${color.g * 255}, ${color.b * 255}, ${color.a})`;
+ preview.style.border = '1px solid #ccc';
+ preview.style.borderRadius = '4px';
+ previewCell.appendChild(preview);
+
+ // Hex cell
+ const hexCell = row.insertCell();
+ hexCell.textContent = color.hex;
+
+ // Normalized components cell
+ const normalizedCell = row.insertCell();
+ const normalizedText = `${color.r.toFixed(1)}, ${color.g.toFixed(1)}, ${color.b.toFixed(1)}, ${color.a.toFixed(1)}`;
+ normalizedCell.textContent = normalizedText;
+
+ // Action cell with copy and hide buttons
+ const actionCell = row.insertCell();
+
+ const copyBtn = document.createElement('button');
+ copyBtn.textContent = 'Copy';
+ copyBtn.className = 'secondary';
+ copyBtn.addEventListener('click', () => {
+ navigator.clipboard.writeText(normalizedText).then(() => {
+ const originalText = copyBtn.textContent;
+ copyBtn.textContent = 'Copied!';
+ setTimeout(() => {
+ copyBtn.textContent = originalText;
+ }, 1500);
+ }).catch(err => {
+ alert('Failed to copy: ' + err.message);
+ });
+ });
+ actionCell.appendChild(copyBtn);
+
+ const hideBtn = document.createElement('button');
+ hideBtn.textContent = 'Hide';
+ hideBtn.className = 'secondary';
+ hideBtn.addEventListener('click', () => {
+ row.classList.add('hidden');
+ updateShowHiddenButton();
+ });
+ actionCell.appendChild(hideBtn);
+}
+
+function updateShowHiddenButton() {
+ const hiddenRows = document.querySelectorAll('#colorTableBody tr.hidden');
+ const showHiddenBtn = document.getElementById('showHiddenBtn');
+
+ if (hiddenRows.length > 0) {
+ showHiddenBtn.classList.remove('hidden');
+ } else {
+ showHiddenBtn.classList.add('hidden');
+ }
+}
diff --git a/site/hex-color/style.css b/site/hex-color/style.css
new file mode 100644
index 0000000..a5b0997
--- /dev/null
+++ b/site/hex-color/style.css
@@ -0,0 +1,20 @@
+.hidden {
+ display: none;
+}
+
+#colorTable {
+ margin-top: 2rem;
+}
+
+#colorTable button {
+ margin: 0;
+ margin-right: 0.5rem;
+}
+
+#colorTable td {
+ vertical-align: middle;
+}
+
+#showHiddenBtn {
+ margin-top: 1rem;
+}
diff --git a/site/index.html b/site/index.html
index 55afdd4..23d989a 100644
--- a/site/index.html
+++ b/site/index.html
@@ -30,6 +30,7 @@
Finska Scorecard
Mental Arithmatic Game
Neon Snake: vibe-coded by Google Gemini
+ Hex Color Converter
-
+
diff --git a/site/mental-arithmatic/scripts/countdown.js b/site/mental-arithmatic/scripts/countdown.js
new file mode 100644
index 0000000..dbdb79e
--- /dev/null
+++ b/site/mental-arithmatic/scripts/countdown.js
@@ -0,0 +1,35 @@
+import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
+
+export class CountdownController extends Controller {
+ static targets = ["countdown", "fakeinput"];
+
+ start() {
+ this.element.classList.remove('hidden');
+
+ this._countdown = 3;
+ this.countdownTarget.innerText = this._countdown;
+
+ this._tickInterval = window.setInterval(this._tick.bind(this), 1000);
+ window.setTimeout(() => {
+ this.fakeinputTarget.focus();
+ }, 1);
+ }
+
+ _tick() {
+ this._countdown -= 1;
+
+ if (this._countdown === 0) {
+ this.countdownTarget.innerText = "GO!";
+ } else if (this._countdown < 0) {
+ window.clearInterval(this._tickInterval);
+ this._startActualGame();
+ } else {
+ this.countdownTarget.innerText = this._countdown;
+ }
+ }
+
+ _startActualGame() {
+ this.element.classList.add('hidden');
+ window.dispatchEvent(new CustomEvent("startGame"));
+ }
+}
\ No newline at end of file
diff --git a/site/mental-arithmatic/scripts/game.js b/site/mental-arithmatic/scripts/game.js
index e583a19..fe495b9 100644
--- a/site/mental-arithmatic/scripts/game.js
+++ b/site/mental-arithmatic/scripts/game.js
@@ -70,16 +70,15 @@ export class GameController extends Controller {
this.problemTarget.appendChild(div);
}
- this.answerTarget.disabled = false;
this.answerTarget.value = "";
- this.answerTarget.focus();
+
+ window.setTimeout(() => {
+ this.answerTarget.focus();
+ }, 1);
}
_checkAnswer() {
let isRight = parseInt(this.answerTarget.value) === this._answer;
-
- this.answerTarget.disabled = true;
-
let delay = 500;
if (isRight) {
diff --git a/site/mental-arithmatic/scripts/main.js b/site/mental-arithmatic/scripts/main.js
index c15b905..05b427d 100644
--- a/site/mental-arithmatic/scripts/main.js
+++ b/site/mental-arithmatic/scripts/main.js
@@ -1,8 +1,10 @@
import { Application } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
import { WelcomeController } from "./welcome.js"
+import { CountdownController } from "./countdown.js"
import { GameController } from "./game.js"
window.Stimulus = Application.start();
window.Stimulus.register('welcome', WelcomeController);
+window.Stimulus.register('countdown', CountdownController);
window.Stimulus.register('game', GameController);
\ No newline at end of file
diff --git a/site/mental-arithmatic/scripts/welcome.js b/site/mental-arithmatic/scripts/welcome.js
index c4ce76f..9fc549e 100644
--- a/site/mental-arithmatic/scripts/welcome.js
+++ b/site/mental-arithmatic/scripts/welcome.js
@@ -1,14 +1,35 @@
import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
export class WelcomeController extends Controller {
+ static targets = ["simpleHighScores"];
+
+ connect() {
+ this._buildHighScores();
+ }
+
startGame(ev) {
ev.preventDefault();
this.element.classList.add('hidden');
- window.dispatchEvent(new CustomEvent("startGame"));
+ window.dispatchEvent(new CustomEvent("startCountdown"));
}
gameEnded(ev) {
this.element.classList.remove('hidden');
}
+
+ _buildHighScores() {
+ let scores = {
+ last: 12,
+ high: 10,
+ streak: 3
+ };
+
+ let newEls = [
+ `đ ${scores.last}`,
+ `đĨ ${scores.streak}`,
+ `âŠī¸ ${scores.high}`
+ ];
+ this.simpleHighScoresTarget.innerHTML = newEls.join('');
+ }
}
\ No newline at end of file
diff --git a/site/mental-arithmatic/style.css b/site/mental-arithmatic/style.css
index 58d4173..ce22cc0 100644
--- a/site/mental-arithmatic/style.css
+++ b/site/mental-arithmatic/style.css
@@ -3,14 +3,35 @@
}
body {
- min-height: 100dvh;
--pico-form-element-disabled-opacity: 1.0;
}
.game-card {
display: flex;
flex-direction: column;
- min-height: 80dvh;
+ min-height: 50vh;
+}
+
+.welcome-card .game-variant footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.welcome-card .game-variant .high-scores {
+ display: flex;
+ gap: 10px;
+}
+
+.countdown-timer {
+ font-size: 3em;
+ text-align: center;
+ width: 100%;
+}
+
+input.fake {
+ position: fixed;
+ top: -600px;
}
.game-card header,
@@ -29,6 +50,8 @@ body {
display: flex;
align-items: center;
+
+ padding-block-start: 40px;
}
.game-card .question {
@@ -46,13 +69,13 @@ body {
}
.game-card .answer.right {
- border-color: green;
- color: green;
+ border-color: #4EB31B;
+ color: #4EB31B;
}
.game-card .answer.wrong {
- border-color: red;
- color: red;
+ border-color: #EE402E;
+ color: #EE402E;
}
.game-card .answer .indicator {
diff --git a/site/mental-arithmatic/index.html b/site/mental-arithmatic/index.html
index 31e8a6c..16c4321 100644
--- a/site/mental-arithmatic/index.html
+++ b/site/mental-arithmatic/index.html
@@ -2,25 +2,44 @@