learnings from laptop

This commit is contained in:
Abhishek Test Tiwari 2026-03-17 13:44:54 +05:30
parent 1972815bf0
commit 6f3b6c7d37
4 changed files with 296 additions and 0 deletions

17
README.md Normal file
View File

@ -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`.

224
index.html Normal file
View File

@ -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>

10
package.json Normal file
View File

@ -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"
}
}

45
server.js Normal file
View File

@ -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}`);
});