Read more »

Product Management System
ID Product Name Price Action

Để xây dựng một ứng dụng web (Web App) chạy trực tiếp trên nền tảng Blogspot (Blogger) mà vẫn có đầy đủ tính năng của một phần mềm quản lý (như lưu trữ dữ liệu, thêm, sửa, xóa - CRUD), chúng ta sẽ kết hợp 3 yếu tố:

  1. HTML/CSS/JS (Giao diện & Logic): Chạy hoàn toàn ở client-side (trình duyệt của người dùng).

  2. IndexedDB (Cơ sở dữ liệu): Lưu trữ dữ liệu lớn, có cấu trúc ngay tại trình duyệt, hoạt động ngoại tuyến (offline) và không phụ thuộc vào server.

  3. Blogspot: Đóng vai trò là host để chứa giao diện và mã nguồn.

Dưới đây là hướng dẫn kỹ thuật và ví dụ minh họa chi tiết về cách xây dựng một ứng dụng quản lý sản phẩm đơn giản, giao diện tiếng Anh, tối ưu hoàn toàn cho Blogspot.

1. Tư duy kiến trúc (Architecture)

Khi đưa một ứng dụng có cơ sở dữ liệu lên Blogspot, luồng xử lý dữ liệu sẽ như sau:

[Trình duyệt của khách hàng] 
       │
       ▼
 [Tải Trang Blogspot] ──► [Chạy HTML/JS] ──► [Đọc/Ghi Dữ Liệu vào IndexedDB (Local)]
  • Ưu điểm: Tốc độ tải cực nhanh, không tốn chi phí server, bảo mật dữ liệu tuyệt đối cho người dùng (vì dữ liệu lưu tại máy của họ).

  • Hạn chế: Dữ liệu lưu trên trình duyệt nào thì chỉ trình duyệt đó nhìn thấy (Local storage). Rất phù hợp làm công cụ quản lý cá nhân, POS offline, hoặc công cụ hỗ trợ quản trị.

2. Mã nguồn minh họa (Full Single-File Code)

Bạn có thể nhúng toàn bộ đoạn mã này vào một trang Page hoặc bài viết Post của Blogspot (chuyển sang chế độ xem HTML trước khi dán).

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Product Management System</title>
    <style>
        :root {
            --primary-color: #2c3e50;
            --accent-color: #16a085;
            --bg-color: #f5f6fa;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: var(--bg-color);
            color: var(--primary-color);
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 900px;
            margin: 0 auto;
            background: #fff;
            padding: 25px;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
        }
        h2 { margin-top: 0; border-bottom: 2px solid var(--primary-color); padding-bottom: 10px; }
        .form-group {
            display: grid;
            grid-template-columns: 1fr 1fr 120px;
            gap: 10px;
            margin-bottom: 20px;
        }
        input, button {
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 14px;
        }
        button {
            background-color: var(--accent-color);
            color: white;
            border: none;
            cursor: pointer;
            font-weight: bold;
            transition: background 0.2s;
        }
        button:hover { background-color: #148f77; }
        button.delete-btn { background-color: #e74c3c; }
        button.delete-btn:hover { background-color: #c0392b; }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
        }
        th, td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }
        th { background-color: var(--primary-color); color: white; }
        tr:hover { background-color: #f1f2f6; }
    </style>
</head>
<body>

<div class="container">
    <h2>Product Management (IndexedDB + Blogspot)</h2>
    
    <div class="form-group">
        <input type="text" id="prodName" placeholder="Product Name..." required>
        <input type="number" id="prodPrice" placeholder="Price..." required>
        <button onclick="saveProduct()">Save Product</button>
    </div>

    <table>
        <thead>
            <tr>
                <th>ID</th>
                <th>Product Name</th>
                <th>Price</th>
                <th style="text-align: center;">Action</th>
            </tr>
        </thead>
        <tbody id="productTableBody">
            </tbody>
    </table>
</div>

<script>
    // --- KEY TECHNICAL: INDEXEDDB SETUP ---
    const DB_NAME = 'ShopDatabase';
    const DB_VERSION = 1;
    const STORE_NAME = 'products';
    let db;

    // Initialize Database
    const request = indexedDB.open(DB_NAME, DB_VERSION);

    request.onerror = function(event) {
        console.error("Database error: " + event.target.errorCode);
    };

    request.onsuccess = function(event) {
        db = event.target.result;
        renderProducts(); // Load data on success
    };

    request.onupgradeneeded = function(event) {
        let dbInstance = event.target.result;
        // Create an objectStore with auto-incrementing key
        if (!dbInstance.objectStoreNames.contains(STORE_NAME)) {
            dbInstance.createObjectStore(STORE_NAME, { keyPath: "id", autoIncrement: true });
        }
    };

    // --- CRUD OPERATIONS ---

    // Create / Update
    function saveProduct() {
        const nameInput = document.getElementById('prodName');
        const priceInput = document.getElementById('prodPrice');

        if (!nameInput.value || !priceInput.value) {
            alert("Please fill in all fields");
            return;
        }

        const transaction = db.transaction([STORE_NAME], "readwrite");
        const store = transaction.objectStore(STORE_NAME);
        
        const product = {
            name: nameInput.value,
            price: parseFloat(priceInput.value),
            createdAt: new Date().toISOString()
        };

        const addRequest = store.add(product);

        addRequest.onsuccess = function() {
            // Clear input fields
            nameInput.value = '';
            priceInput.value = '';
            renderProducts(); // Refresh UI
        };

        transaction.onerror = function(event) {
            console.error("Transaction error: ", event.target.error);
        };
    }

    // Read & Render to UI
    function renderProducts() {
        const tbody = document.getElementById('productTableBody');
        tbody.innerHTML = ''; // Clear old data

        const transaction = db.transaction([STORE_NAME], "readonly");
        const store = transaction.objectStore(STORE_NAME);
        const cursorRequest = store.openCursor();

        cursorRequest.onsuccess = function(event) {
            const cursor = event.target.result;
            if (cursor) {
                const tr = document.createElement('tr');
                tr.innerHTML = `
                    <td>#${cursor.value.id}</td>
                    <td><b>${cursor.value.name}</b></td>
                    <td>${cursor.value.price.toLocaleString()} VND</td>
                    <td style="text-align: center;">
                        <button class="delete-btn" onclick="deleteProduct(${cursor.value.id})">Delete</button>
                    </td>
                `;
                tbody.appendChild(tr);
                cursor.continue(); // Move to next item
            }
        };
    }

    // Delete
    function deleteProduct(id) {
        if (!confirm("Are you sure you want to delete this product?")) return;

        const transaction = db.transaction([STORE_NAME], "readwrite");
        const store = transaction.objectStore(STORE_NAME);
        const deleteRequest = store.delete(id);

        deleteRequest.onsuccess = function() {
            renderProducts(); // Refresh UI
        };
    }
</script>

</body>
</html>

3. Các lưu ý kỹ thuật khi triển khai trên Blogspot

Để ứng dụng này chạy mượt mà và không bị xung đột với theme cấu trúc XML của Blogger, bạn cần tuân thủ các nguyên tắc sau:

  • Tránh xung đột dấu ký tự trong JS: Blogger sử dụng XML. Khi viết JavaScript trực tiếp trong cấu trúc mẫu (Theme), các ký tự như && hoặc < có thể gây lỗi biên dịch. Giải pháp tốt nhất là đặt mã Script trong bài viết/trang (Post/Page) ở chế độ HTML View, hoặc bọc mã JS trong thẻ <![CDATA[ ... ]]> nếu can thiệp vào Theme hệ thống.

  • Phạm vi dữ liệu (Origin): IndexedDB hoạt động dựa trên cơ chế Same-Origin Policy. Dữ liệu lưu tại tên miền yourblog.blogspot.com sẽ không thể truy cập từ tên miền tùy chỉnh (custom domain) nếu bạn đổi sau này. Do đó, hãy xác định tên miền cố định cho web app trước khi lưu trữ nhiều dữ liệu.

  • Bảo mật dữ liệu: Vì dữ liệu nằm hoàn toàn ở máy người dùng (Client-side), nếu họ xóa lịch sử duyệt web (Clear Cache/Site Data), dữ liệu trong IndexedDB cũng sẽ bị xóa. Đối với các ứng dụng dạng phần mềm quản lý, bạn nên viết thêm một hàm Export/Import JSON để người dùng có thể sao lưu dữ liệu ra file máy tính khi cần thiết.