diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..3a45fdd
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+ Lô tô (made by miti99)
+
+
+
+ Lô tô
+ 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)
+ Hướng dẫn
+
+
+
+
+
Hướng dẫn
+
+
+ - Nhấn Tạo để tạo bảng mới
+
+
+
+
+
+ Bảng
+
+
+
+
+
diff --git a/web/script.js b/web/script.js
new file mode 100644
index 0000000..3e6eea6
--- /dev/null
+++ b/web/script.js
@@ -0,0 +1,133 @@
+let numRows = 9;
+let numCols = 9;
+let numPerRow = 5;
+let lotoKey = "loto_";
+
+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).sort();
+}
+
+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 = randomNumbers(numPerRow, 0, 9);
+ selectedCol.forEach(col => {
+ countNumPerCol[col] += 1;
+ cell[i][col] = -1;
+ });
+ }
+
+ for (let i = 0; i < numCols; i++) {
+ let selectedNum = randomNumbers(countNumPerCol[i], 10 * i + 1, 10 * (i + 1) + 1);
+ for (let j = 0; j < numRows; j++) {
+ if (cell[j][i] == -1) {
+ cell[j][i] = selectedNum.shift();
+ }
+ }
+ }
+
+ let html = '';
+ for (let i = 0; i < numRows; i++) {
+ html += "";
+ for (let j = 0; j < numCols; j++) {
+ let num = cell[i][j];
+ let isEnabled = num > 0;
+ html += `| ${isEnabled ? num : ""} | `;
+ }
+ html += "
";
+ }
+ html += "
";
+ 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();
+ }
+}
diff --git a/web/style.css b/web/style.css
new file mode 100755
index 0000000..a69b985
--- /dev/null
+++ b/web/style.css
@@ -0,0 +1,61 @@
+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));
+}