mirror of
https://github.com/tiennm99/loto.git
synced 2026-08-30 22:24:41 +00:00
feat: convert to Next.js with TypeScript and Tailwind CSS
Replace vanilla HTML/JS/CSS with Next.js App Router, TypeScript, and Tailwind CSS. Responsive design with dark mode support. All original game logic preserved: grid generation, click-to-cross, localStorage persistence.
This commit is contained in:
@@ -1 +1,4 @@
|
||||
.vscode/
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
|
||||
+13
-1
@@ -1,3 +1,15 @@
|
||||
# Lô tô
|
||||
|
||||
Bàn số của trò chơi "Lô tô"
|
||||
Bàn số của trò chơi "Lô tô" — Next.js app.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,44 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #fafaf9;
|
||||
--foreground: #1c1917;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0c0a09;
|
||||
--foreground: #e7e5e4;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans), Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
/* Crossed cell diagonal line */
|
||||
.cell-crossed {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cell-crossed::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 4px;
|
||||
background-image: linear-gradient(
|
||||
to bottom right,
|
||||
transparent calc(50% - 1.5px),
|
||||
currentColor,
|
||||
transparent calc(50% + 1.5px)
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Lô tô",
|
||||
description: "Bàn số của trò chơi Lô tô",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="vi" className={`${geistSans.variable} h-full antialiased`}>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/** Number ranges for each column (0-8) in the lô tô grid */
|
||||
const NUM_IN_COL: number[][] = [
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
|
||||
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
|
||||
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39],
|
||||
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49],
|
||||
[50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
|
||||
[60, 61, 62, 63, 64, 65, 66, 67, 68, 69],
|
||||
[70, 71, 72, 73, 74, 75, 76, 77, 78, 79],
|
||||
[80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90],
|
||||
];
|
||||
|
||||
const NUM_ROWS = 9;
|
||||
const NUM_COLS = 9;
|
||||
const NUM_PER_ROW = 5;
|
||||
|
||||
/** Weighted random selection of a column index */
|
||||
function randomANumberInRow(weights: number[]): number {
|
||||
const tempWeight = [...weights];
|
||||
for (let i = 1; i < tempWeight.length; i++) {
|
||||
tempWeight[i] += tempWeight[i - 1];
|
||||
}
|
||||
const rand = Math.floor(Math.random() * tempWeight[tempWeight.length - 1]);
|
||||
for (let i = 0; i < tempWeight.length; i++) {
|
||||
if (rand < tempWeight[i]) return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Select NUM_PER_ROW columns for a row using weighted random */
|
||||
function randomARow(baseWeight: number[]): number[] {
|
||||
const tempWeight = [...baseWeight];
|
||||
const selectedCols: number[] = [];
|
||||
for (let i = 0; i < NUM_PER_ROW; i++) {
|
||||
const col = randomANumberInRow(tempWeight);
|
||||
selectedCols.push(col);
|
||||
tempWeight[col] = 0;
|
||||
baseWeight[col]--;
|
||||
}
|
||||
return selectedCols;
|
||||
}
|
||||
|
||||
/** Pick random numbers from a column's range */
|
||||
function randomNumbersInCol(num: number, col: number): number[] {
|
||||
const arr = [...NUM_IN_COL[col]];
|
||||
arr.sort(() => 0.5 - Math.random());
|
||||
return arr.slice(0, num);
|
||||
}
|
||||
|
||||
/** Generate a 9x9 lô tô grid. Returns cell values (0 = empty, >0 = number). */
|
||||
export function generateGrid(): number[][] {
|
||||
const cell = Array.from({ length: NUM_ROWS }, () =>
|
||||
new Array(NUM_COLS).fill(0)
|
||||
);
|
||||
const countNumPerCol = new Array(NUM_COLS).fill(0);
|
||||
const baseWeight = new Array(NUM_COLS).fill(6);
|
||||
|
||||
for (let i = 0; i < NUM_ROWS; i++) {
|
||||
const newRow = randomARow(baseWeight);
|
||||
newRow.forEach((col) => {
|
||||
countNumPerCol[col]++;
|
||||
cell[i][col] = -1;
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < NUM_COLS; i++) {
|
||||
const selectedNum = randomNumbersInCol(countNumPerCol[i], i);
|
||||
for (let j = 0; j < NUM_ROWS; j++) {
|
||||
if (cell[j][i] === -1) {
|
||||
cell[j][i] = selectedNum.shift() ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
const STORAGE_KEY_GRID = "loto_grid";
|
||||
const STORAGE_KEY_CROSSED = "loto_crossed";
|
||||
|
||||
export function saveGrid(grid: number[][]): void {
|
||||
localStorage.setItem(STORAGE_KEY_GRID, JSON.stringify(grid));
|
||||
}
|
||||
|
||||
export function loadGrid(): number[][] | null {
|
||||
const data = localStorage.getItem(STORAGE_KEY_GRID);
|
||||
if (!data) return null;
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveCrossedState(crossed: boolean[][]): void {
|
||||
localStorage.setItem(STORAGE_KEY_CROSSED, JSON.stringify(crossed));
|
||||
}
|
||||
|
||||
export function loadCrossedState(): boolean[][] | null {
|
||||
const data = localStorage.getItem(STORAGE_KEY_CROSSED);
|
||||
if (!data) return null;
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
generateGrid,
|
||||
loadCrossedState,
|
||||
loadGrid,
|
||||
saveCrossedState,
|
||||
saveGrid,
|
||||
} from "./loto-game-logic";
|
||||
|
||||
export default function Home() {
|
||||
const [grid, setGrid] = useState<number[][] | null>(null);
|
||||
const [crossed, setCrossed] = useState<boolean[][]>([]);
|
||||
const [showInstructions, setShowInstructions] = useState(false);
|
||||
|
||||
// Load saved state on mount
|
||||
useEffect(() => {
|
||||
const savedGrid = loadGrid();
|
||||
if (savedGrid) {
|
||||
setGrid(savedGrid);
|
||||
const savedCrossed = loadCrossedState();
|
||||
setCrossed(
|
||||
savedCrossed ??
|
||||
savedGrid.map((row) => row.map(() => false))
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Persist crossed state on change
|
||||
useEffect(() => {
|
||||
if (crossed.length > 0) {
|
||||
saveCrossedState(crossed);
|
||||
}
|
||||
}, [crossed]);
|
||||
|
||||
const handleGenerate = useCallback(() => {
|
||||
if (grid && !confirm("Bạn có muốn tạo lại bảng không?")) return;
|
||||
const newGrid = generateGrid();
|
||||
setGrid(newGrid);
|
||||
const newCrossed = newGrid.map((row) => row.map(() => false));
|
||||
setCrossed(newCrossed);
|
||||
saveGrid(newGrid);
|
||||
saveCrossedState(newCrossed);
|
||||
}, [grid]);
|
||||
|
||||
const handleCellClick = useCallback(
|
||||
(row: number, col: number) => {
|
||||
setCrossed((prev) => {
|
||||
const next = prev.map((r) => [...r]);
|
||||
next[row][col] = !next[row][col];
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center px-4 py-6 sm:py-10">
|
||||
<div className="w-full max-w-2xl">
|
||||
{/* Header */}
|
||||
<header className="text-center mb-6">
|
||||
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight mb-2">
|
||||
Lô tô
|
||||
</h1>
|
||||
<p className="text-sm sm:text-base text-stone-500 dark:text-stone-400 mb-3">
|
||||
Tạo bảng chơi lô tô, lấy cảm hứng từ những buổi họp lớp thiếu
|
||||
giấy chơi lô tô của TN1 (2014-2017)
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowInstructions((v) => !v)}
|
||||
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
{showInstructions ? "Ẩn hướng dẫn" : "Hướng dẫn"}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* Instructions */}
|
||||
{showInstructions && (
|
||||
<div className="mb-6 rounded-lg border border-stone-200 dark:border-stone-700 bg-stone-50 dark:bg-stone-900 p-4 text-sm">
|
||||
<h2 className="font-semibold mb-2">Hướng dẫn</h2>
|
||||
<ul className="list-disc list-inside space-y-1 text-stone-600 dark:text-stone-400">
|
||||
<li>
|
||||
Nhấn <strong>Tạo bảng mới</strong> để tạo bảng mới
|
||||
</li>
|
||||
<li>Nhấn vào ô số để đánh dấu khi số được xổ</li>
|
||||
<li>Nhấn lại ô đã đánh dấu để bỏ đánh dấu</li>
|
||||
<li>Bảng và trạng thái được lưu tự động</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generate button */}
|
||||
<div className="flex justify-center mb-6">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
className="px-6 py-2.5 rounded-lg bg-blue-600 text-white font-medium
|
||||
hover:bg-blue-700 active:bg-blue-800
|
||||
transition-colors shadow-sm"
|
||||
>
|
||||
Tạo bảng mới
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
{grid && (
|
||||
<div className="overflow-x-auto rounded-xl border border-stone-200 dark:border-stone-700 shadow-sm">
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
{grid.map((row, i) => (
|
||||
<tr key={i}>
|
||||
{row.map((num, j) => {
|
||||
const hasNumber = num > 0;
|
||||
const isCrossed =
|
||||
hasNumber && crossed[i]?.[j];
|
||||
|
||||
return (
|
||||
<td
|
||||
key={j}
|
||||
onClick={
|
||||
hasNumber
|
||||
? () => handleCellClick(i, j)
|
||||
: undefined
|
||||
}
|
||||
className={`
|
||||
relative text-center border border-stone-200 dark:border-stone-700
|
||||
h-10 sm:h-12 text-sm sm:text-lg font-medium
|
||||
transition-colors select-none
|
||||
${
|
||||
hasNumber
|
||||
? isCrossed
|
||||
? "cell-crossed bg-amber-100 dark:bg-amber-900/40 text-amber-800 dark:text-amber-300 cursor-pointer"
|
||||
: "bg-amber-50 dark:bg-amber-950/30 text-stone-800 dark:text-stone-200 cursor-pointer hover:bg-amber-100 dark:hover:bg-amber-900/30"
|
||||
: "bg-stone-50 dark:bg-stone-900/50"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{hasNumber ? num : ""}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{!grid && (
|
||||
<div className="text-center text-stone-400 dark:text-stone-500 py-16">
|
||||
Nhấn “Tạo bảng mới” để bắt đầu chơi
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-8 text-center text-xs text-stone-400 dark:text-stone-500">
|
||||
made by miti99
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -1,44 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<title>Lô tô (made by miti99)</title>
|
||||
</head>
|
||||
|
||||
<body onload="start()">
|
||||
<h1>Lô tô</h1>
|
||||
Tạo bảng chơi lô tô, lấy cảm hứng từ những buổi họp lớp thiếu giấy chơi lô tô của TN1 (2014-2017)
|
||||
<a href="javascript:toggle('instructions');">Hướng dẫn</a>
|
||||
|
||||
<noscript>
|
||||
<div class="error">
|
||||
<strong>Lỗi:</strong>
|
||||
<p>
|
||||
<em> JavaScript không được bật</em>
|
||||
</p>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div id="instructions">
|
||||
<h2>Hướng dẫn</h2>
|
||||
<ul>
|
||||
<noscript>
|
||||
<li>Bật JavaScript</li>
|
||||
</noscript>
|
||||
<li>Nhấn <strong>Tạo</strong> để tạo bảng mới</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form>
|
||||
<input type="button" value="Tạo" onclick="generate();" />
|
||||
</form>
|
||||
|
||||
<h2>Bảng</h2>
|
||||
<div id="grid"></div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
function randomNumbers(num, from, to) {
|
||||
let arr = Array.from({
|
||||
length: to - from
|
||||
}, (_, i) => from + i);
|
||||
arr.sort(() => 0.5 - Math.random());
|
||||
return arr.slice(0, num);
|
||||
}
|
||||
|
||||
let arr = randomNumbers(90, 1, 91);
|
||||
let i = 0;
|
||||
|
||||
const readline = require('readline');
|
||||
readline.emitKeypressEvents(process.stdin);
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.on('keypress', (str, key) => {
|
||||
if (key.ctrl && key.name === 'c') {
|
||||
console.log(JSON.stringify);
|
||||
process.exit();
|
||||
} else {
|
||||
console.log(`Next number: ${arr[i++]}\n`);
|
||||
}
|
||||
});
|
||||
console.log('Press any key...');
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
+6572
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "nextjs-temp",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.2.2",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.2",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
-192
@@ -1,192 +0,0 @@
|
||||
let numInCol = [
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
|
||||
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
|
||||
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39],
|
||||
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49],
|
||||
[50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
|
||||
[60, 61, 62, 63, 64, 65, 66, 67, 68, 69],
|
||||
[70, 71, 72, 73, 74, 75, 76, 77, 78, 79],
|
||||
[80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90]
|
||||
];
|
||||
|
||||
let numRows = 9;
|
||||
let numCols = 9;
|
||||
let numPerRow = 5;
|
||||
let maxNumPerCol = 6;
|
||||
let lotoKey = "loto_";
|
||||
|
||||
function randomANumberInRow(weights) {
|
||||
let tempWeight = [...weights];
|
||||
for (let i = 1; i < tempWeight.length; i++) {
|
||||
tempWeight[i] += tempWeight[i - 1];
|
||||
}
|
||||
let rand = Math.floor(Math.random() * tempWeight[tempWeight.length - 1]);
|
||||
for (let i = 0; i < tempWeight.length; i++) {
|
||||
if (rand < tempWeight[i]) return i;
|
||||
}
|
||||
}
|
||||
|
||||
function randomARow(baseWeight) {
|
||||
console.log("Before", JSON.stringify(baseWeight));
|
||||
let tempWeight = [...baseWeight];
|
||||
let selectedNum = [];
|
||||
for (let i = 0; i < numPerRow; i++) {
|
||||
let num = randomANumberInRow(tempWeight);
|
||||
selectedNum.push(num);
|
||||
tempWeight[num] = 0;
|
||||
baseWeight[num]--;
|
||||
}
|
||||
console.log("After", JSON.stringify(baseWeight));
|
||||
return selectedNum;
|
||||
}
|
||||
|
||||
function randomNumbersInCol(num, col) {
|
||||
if (col < 0 || col >= numInCol.length) return;
|
||||
let arr = numInCol[col];
|
||||
arr.sort(() => 0.5 - Math.random());
|
||||
return arr.slice(0, num);
|
||||
}
|
||||
|
||||
// function randomSelectCols() {
|
||||
// let arr = Array.from({
|
||||
// length: 9
|
||||
// }, (_, i) => i);
|
||||
// arr.sort(() => 0.5 - Math.random());
|
||||
// return arr;
|
||||
// }
|
||||
|
||||
function generate() {
|
||||
let node = document.getElementById("grid");
|
||||
if (node.innerHTML && !confirm("Bạn có muốn tạo lại bảng không?"))
|
||||
return;
|
||||
|
||||
let cell = new Array(numRows).fill(0).map(() => new Array(numCols).fill(0));
|
||||
let countNumPerCol = new Array(numCols).fill(0);
|
||||
// //Random cac cot co so trong tung dong
|
||||
// for (let i = 0; i < numRows; i++) {
|
||||
// let selectedCol = randomSelectCols(numPerRow, 0, 9);
|
||||
// let count = 0;
|
||||
// for (let j = 0; j < selectedCol.length; j++) {
|
||||
// let col = selectedCol[j];
|
||||
// if (countNumPerCol[col] == maxNumPerCol) continue;
|
||||
// countNumPerCol[col] += 1;
|
||||
// cell[i][col] = -1;
|
||||
// count++;
|
||||
// if (count == numPerRow) break;
|
||||
// }
|
||||
// }
|
||||
let baseWeight = new Array(numCols).fill(6);
|
||||
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
let newRow = randomARow(baseWeight);
|
||||
newRow.forEach(col => {
|
||||
countNumPerCol[col]++;
|
||||
cell[i][col] = -1;
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < numCols; i++) {
|
||||
let selectedNum = randomNumbersInCol(countNumPerCol[i], i);
|
||||
for (let j = 0; j < numRows; j++) {
|
||||
if (cell[j][i] == -1) {
|
||||
cell[j][i] = selectedNum.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let html = '<table border="0">';
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
html += "<tr>";
|
||||
for (let j = 0; j < numCols; j++) {
|
||||
let num = cell[i][j];
|
||||
let isEnabled = num > 0;
|
||||
html += `<td id="${i}_${j}" class="${isEnabled ? 'hightlight' : ''}" onClick="cellClicked('${i}_${j}')" style="text-align: center; pointer-events: ${isEnabled ? 'auto' : 'none'};">${isEnabled ? num : ""}</td>`;
|
||||
}
|
||||
html += "</tr >";
|
||||
}
|
||||
html += "</table>";
|
||||
node.innerHTML = html;
|
||||
save(lotoKey, html);
|
||||
saveGameState();
|
||||
}
|
||||
|
||||
function supports_html5_storage() {
|
||||
try {
|
||||
return "localStorage" in window && window["localStorage"] !== null;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function cellClicked(id) {
|
||||
let elem = document.getElementById(id);
|
||||
let isChecked = elem.classList.contains("crossed");
|
||||
if (isChecked)
|
||||
elem.classList.remove("crossed");
|
||||
else
|
||||
elem.classList.add("crossed");
|
||||
saveGameState();
|
||||
}
|
||||
|
||||
function saveGameState() {
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
for (let j = 0; j < numCols; j++) {
|
||||
let id = i + "_" + j;
|
||||
let elem = document.getElementById(id);
|
||||
if (elem.innerHTML == "") continue;
|
||||
let isChecked = elem.classList.contains("crossed");
|
||||
save(lotoKey + id, !isChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadGameState() {
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
for (let j = 0; j < numCols; j++) {
|
||||
let id = i + "_" + j;
|
||||
let elem = document.getElementById(id);
|
||||
if (elem.innerHTML == "") continue;
|
||||
let isChecked = load(lotoKey + id) == "true";
|
||||
if (isChecked)
|
||||
elem.classList.remove("crossed");
|
||||
else
|
||||
elem.classList.add("crossed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(id) {
|
||||
let elem = document.getElementById(id);
|
||||
if (elem.style.display == "block")
|
||||
elem.style.display = "none";
|
||||
else
|
||||
elem.style.display = "block";
|
||||
}
|
||||
|
||||
save = function (key, value) {};
|
||||
load = function (key) {
|
||||
return null;
|
||||
};
|
||||
|
||||
function start() {
|
||||
if (supports_html5_storage()) {
|
||||
save = function (key, value) {
|
||||
localStorage.setItem(key, value);
|
||||
};
|
||||
load = function (key) {
|
||||
return localStorage.getItem(key);
|
||||
};
|
||||
} else {
|
||||
alert("Trình duyệt của bạn không hỗ trợ localStorage!");
|
||||
return;
|
||||
}
|
||||
|
||||
let grid = load(lotoKey);
|
||||
if (grid) {
|
||||
console.log("Load bảng đã được tạo sẵn");
|
||||
let node = document.getElementById("grid");
|
||||
node.innerHTML = grid;
|
||||
loadGameState();
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
h2 {
|
||||
background: blueviolet;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
border: 1px solid blue;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 20px;
|
||||
border-top: 1px solid lightgray;
|
||||
border-left: 1px solid lightgray;
|
||||
border-bottom: 1px solid darkgray;
|
||||
border-right: 1px solid darkgray;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: orange;
|
||||
color: red;
|
||||
padding: 5px;
|
||||
margin: 20px 50px;
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
.hightlight {
|
||||
background: yellow;
|
||||
}
|
||||
|
||||
a {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
td label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
color: blue;
|
||||
}
|
||||
|
||||
#instructions {
|
||||
display: none;
|
||||
border: 1px solid blue;
|
||||
margin: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
table td.crossed {
|
||||
background-image: linear-gradient(to bottom right, transparent calc(50% - 2px), black, transparent calc(50% + 2px));
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user