learnings from laptop
This commit is contained in:
parent
1972815bf0
commit
6f3b6c7d37
|
|
@ -0,0 +1,17 @@
|
|||
# Keyboard Learning Game
|
||||
|
||||
Full-screen blue background. Whatever key you press shows up **BIG + BOLD** for kids to learn letters, numbers, and symbols.
|
||||
|
||||
## Run
|
||||
|
||||
### Option 1: Open directly
|
||||
|
||||
Open `index.html` in your browser.
|
||||
|
||||
### Option 2: Run a small server (recommended)
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
Then open `http://localhost:8080`.
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Keyboard Learning Game</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b57ff;
|
||||
--fg: #ffffff;
|
||||
--muted: rgba(255, 255, 255, 0.85);
|
||||
--shadow: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji",
|
||||
"Segoe UI Emoji";
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
width: min(1100px, 92vw);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sound {
|
||||
position: fixed;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
padding: 10px 14px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
color: var(--fg);
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.02em;
|
||||
box-shadow: 0 10px 0 rgba(0, 0, 0, 0.12);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sound:focus-visible {
|
||||
outline: 4px solid rgba(255, 255, 255, 0.55);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
.big {
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.03em;
|
||||
font-size: clamp(72px, 18vw, 220px);
|
||||
line-height: 1;
|
||||
text-shadow: 0 12px 0 var(--shadow);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 18px;
|
||||
font-size: clamp(16px, 2.5vw, 26px);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.kbd {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 10px 0 rgba(0, 0, 0, 0.12);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pop 120ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
from {
|
||||
transform: scale(0.94);
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
filter: brightness(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button id="soundBtn" class="sound" type="button" aria-pressed="true">Sound: ON</button>
|
||||
<main class="wrap" aria-live="polite" aria-atomic="true">
|
||||
<h1 id="big" class="big">Press a key</h1>
|
||||
<div class="hint">
|
||||
Type any letter, number, or symbol on your keyboard (try <span class="kbd">A</span>, <span class="kbd">1</span>,
|
||||
<span class="kbd">!</span>, <span class="kbd">@</span>).
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const big = document.getElementById("big");
|
||||
const soundBtn = document.getElementById("soundBtn");
|
||||
let soundOn = true;
|
||||
|
||||
function setSound(on) {
|
||||
soundOn = on;
|
||||
soundBtn.textContent = `Sound: ${soundOn ? "ON" : "OFF"}`;
|
||||
soundBtn.setAttribute("aria-pressed", String(soundOn));
|
||||
}
|
||||
|
||||
setSound(true);
|
||||
|
||||
soundBtn.addEventListener("click", () => {
|
||||
setSound(!soundOn);
|
||||
});
|
||||
|
||||
function prettyKey(e) {
|
||||
const k = e.key;
|
||||
|
||||
if (k === " ") return "SPACE";
|
||||
if (k === "Enter") return "ENTER";
|
||||
if (k === "Backspace") return "BACKSPACE";
|
||||
if (k === "Tab") return "TAB";
|
||||
if (k === "Escape") return "ESC";
|
||||
if (k === "Shift" || k === "Control" || k === "Alt" || k === "Meta") return k.toUpperCase();
|
||||
if (k.startsWith("Arrow")) return k.replace("Arrow", "ARROW ");
|
||||
|
||||
// Printable characters come through as length 1 (letters, numbers, symbols).
|
||||
if (k.length === 1) return k.toUpperCase();
|
||||
|
||||
// Fallback for other keys like "Home", "End", "PageUp", etc.
|
||||
return k.toUpperCase();
|
||||
}
|
||||
|
||||
const speakMap = {
|
||||
" ": "space",
|
||||
Enter: "enter",
|
||||
Backspace: "backspace",
|
||||
Tab: "tab",
|
||||
Escape: "escape",
|
||||
"!": "exclamation mark",
|
||||
"@": "at sign",
|
||||
"#": "hash",
|
||||
"$": "dollar sign",
|
||||
"%": "percent",
|
||||
"^": "caret",
|
||||
"&": "ampersand",
|
||||
"*": "asterisk",
|
||||
"(": "left parenthesis",
|
||||
")": "right parenthesis",
|
||||
"-": "hyphen",
|
||||
_: "underscore",
|
||||
"+": "plus",
|
||||
"=": "equals",
|
||||
"[": "left bracket",
|
||||
"]": "right bracket",
|
||||
"{": "left brace",
|
||||
"}": "right brace",
|
||||
"\\": "backslash",
|
||||
"/": "slash",
|
||||
"|": "pipe",
|
||||
":": "colon",
|
||||
";": "semicolon",
|
||||
"'": "apostrophe",
|
||||
'"': "quote",
|
||||
",": "comma",
|
||||
".": "period",
|
||||
"<": "less than",
|
||||
">": "greater than",
|
||||
"?": "question mark",
|
||||
"`": "backtick",
|
||||
"~": "tilde"
|
||||
};
|
||||
|
||||
function speakForKey(e) {
|
||||
if (!soundOn) return;
|
||||
if (!("speechSynthesis" in window)) return;
|
||||
|
||||
const raw = e.key;
|
||||
let phrase = speakMap[raw];
|
||||
|
||||
if (!phrase) {
|
||||
if (raw.startsWith("Arrow")) phrase = raw.replace("Arrow", "arrow ").toLowerCase();
|
||||
else if (raw.length === 1) phrase = raw.toUpperCase(); // letters/numbers
|
||||
else phrase = raw.toLowerCase();
|
||||
}
|
||||
|
||||
try {
|
||||
window.speechSynthesis.cancel();
|
||||
const u = new SpeechSynthesisUtterance(phrase);
|
||||
u.rate = 0.9;
|
||||
u.pitch = 1.0;
|
||||
u.volume = 1.0;
|
||||
window.speechSynthesis.speak(u);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function show(text) {
|
||||
big.textContent = text;
|
||||
big.classList.remove("pulse");
|
||||
// Force reflow so animation restarts reliably
|
||||
void big.offsetWidth;
|
||||
big.classList.add("pulse");
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
// Prevent browser shortcuts from stealing focus in some cases
|
||||
// (Still allows normal typing, and this is a full-screen toy app.)
|
||||
e.preventDefault();
|
||||
show(prettyKey(e));
|
||||
speakForKey(e);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "keyboard-learning-game",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "A simple keyboard learning game for kids.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import http from "node:http";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const port = process.env.PORT ? Number(process.env.PORT) : 8080;
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const url = new URL(
|
||||
req.url ?? "/",
|
||||
`http://${req.headers.host ?? "localhost"}`,
|
||||
);
|
||||
const pathname = url.pathname === "/" ? "/index.html" : url.pathname;
|
||||
|
||||
// Only serve files from this folder; keep it intentionally simple.
|
||||
const safePath = path.normalize(pathname).replace(/^(\.\.(\/|\\|$))+/, "");
|
||||
const filePath = path.join(__dirname, safePath);
|
||||
|
||||
const data = await readFile(filePath);
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
|
||||
const contentType =
|
||||
ext === ".html"
|
||||
? "text/html; charset=utf-8"
|
||||
: ext === ".js"
|
||||
? "text/javascript; charset=utf-8"
|
||||
: ext === ".css"
|
||||
? "text/css; charset=utf-8"
|
||||
: "application/octet-stream";
|
||||
|
||||
res.writeHead(200, { "Content-Type": contentType });
|
||||
res.end(data);
|
||||
} catch {
|
||||
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
||||
res.end("Not found");
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, () => {
|
||||
// Intentionally minimal output.
|
||||
console.log(`Keyboard game running on http://localhost:${port}`);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user