Daily Work Note - IndexedDB

Daily Work Note

Saved Work Notes

Date Task Title Details Action

Dưới đây là bài hướng dẫn chi tiết, dễ hiểu dành cho học sinh, sinh viên để xây dựng một ứng dụng Daily Work Note (Ghi chú công việc hàng ngày).

Ứng dụng này sử dụng HTML Form để nhập liệu, hiển thị dạng Bảng (Table), và lưu trữ dữ liệu bền vững ngay trên trình duyệt bằng IndexedDB (không bị mất khi F5 hoặc tắt máy). Mã nguồn được tối ưu hóa gọn gàng trong 1 file để bạn dễ dàng nhúng thẳng vào Blogspot.

1. Tìm hiểu nhanh về các thành phần

  • HTML Form: Nơi người dùng nhập tiêu đề công việc, nội dung và chọn ngày tháng.

  • HTML Table: Nơi hiển thị danh sách các ghi chú đã lưu dưới dạng các hàng và cột trực quan.

  • IndexedDB: Một cơ sở dữ liệu (Database) thực sự tích hợp sẵn trong trình duyệt web. Nó mạnh hơn localStorage vì cho phép lưu trữ cấu trúc dữ liệu lớn, tìm kiếm và quản lý theo chỉ mục (Index).

2. Mã nguồn toàn diện (HTML + CSS + JavaScript)

Bạn chỉ cần sao chép toàn bộ đoạn code dưới đây. Code đã được viết theo chuẩn gọn - nhẹ - không dùng thư viện ngoài để tải nhanh nhất trên Blogspot.

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Daily Work Note - IndexedDB</title>
    <style>
        :root {
            --primary-color: #2563eb;
            --danger-color: #dc2626;
            --bg-color: #f8fafc;
            --text-color: #1e293b;
        }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background-color: var(--bg-color);
            color: var(--text-color);
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 900px;
            margin: 0 auto;
            background: #ffffff;
            padding: 25px;
            border-radius: 8px;
            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
        }
        h2 {
            margin-top: 0;
            color: var(--primary-color);
            border-bottom: 2px solid #e2e8f0;
            padding-bottom: 10px;
        }
        /* Style for Form */
        .note-form {
            display: grid;
            gap: 15px;
            margin-bottom: 30px;
            background: #f1f5f9;
            padding: 15px;
            border-radius: 6px;
        }
        .form-group {
            display: flex;
            flex-direction: column;
            gap: 5px;
        }
        label {
            font-weight: 600;
            font-size: 0.9rem;
        }
        input, textarea {
            padding: 8px 12px;
            border: 1px solid #cbd5e1;
            border-radius: 4px;
            font-size: 1rem;
            outline: none;
        }
        input:focus, textarea:focus {
            border-color: var(--primary-color);
        }
        button {
            cursor: pointer;
            padding: 10px 15px;
            border: none;
            border-radius: 4px;
            font-weight: bold;
            transition: opacity 0.2s;
        }
        button:hover {
            opacity: 0.9;
        }
        .btn-submit {
            background-color: var(--primary-color);
            color: white;
            align-self: flex-start;
        }
        /* Style for Data Table */
        .table-container {
            overflow-x: auto;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 10px;
            text-align: left;
        }
        th, td {
            padding: 12px;
            border-bottom: 1px solid #e2e8f0;
        }
        th {
            background-color: #e2e8f0;
            font-weight: 600;
        }
        tr:hover {
            background-color: #f8fafc;
        }
        .btn-delete {
            background-color: var(--danger-color);
            color: white;
            padding: 5px 10px;
            font-size: 0.85rem;
        }
        .no-data {
            text-align: center;
            color: #64748b;
            padding: 20px;
        }
    </style>
</head>
<body>

<div class="container">
    <h2>Daily Work Note</h2>
    
    <form id="workNoteForm" class="note-form">
        <div class="form-group">
            <label for="noteDate">Date:</label>
            <input type="date" id="noteDate" required>
        </div>
        <div class="form-group">
            <label for="noteTitle">Task Title:</label>
            <input type="text" id="noteTitle" placeholder="What are you doing today?..." required>
        </div>
        <div class="form-group">
            <label for="noteContent">Details/Content:</label>
            <textarea id="noteContent" rows="3" placeholder="Enter task descriptions, targets..."></textarea>
        </div>
        <button type="submit" class="btn-submit">Save Note</button>
    </form>

    <h3>Saved Work Notes</h3>
    <div class="table-container">
        <table id="notesTable">
            <thead>
                <tr>
                    <th>Date</th>
                    <th>Task Title</th>
                    <th>Details</th>
                    <th style="width: 80px;">Action</th>
                </tr>
            </thead>
            <tbody id="notesTableBody">
                </tbody>
        </table>
        <div id="noDataMessage" class="no-data" style="display: none;">No notes recorded yet.</div>
    </div>
</div>

<script>
    // --- 1. INITIALIZE INDEXEDDB ---
    const dbName = "DailyWorkNoteDB";
    const dbVersion = 1;
    const storeName = "notes";
    let db;

    const request = indexedDB.open(dbName, dbVersion);

    // Triggered if the database is new or version changes
    request.onupgradeneeded = function(event) {
        const database = event.target.result;
        // Create an objectStore (table) with an auto-incrementing key path named 'id'
        if (!database.objectStoreNames.contains(storeName)) {
            database.createObjectStore(storeName, { keyPath: "id", autoIncrement: true });
        }
    };

    request.onsuccess = function(event) {
        db = event.target.result;
        console.log("Database initialized successfully.");
        displayNotes(); // Load data onto the table upon opening
    };

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

    // Set default date picker to today's date
    document.getElementById('noteDate').valueAsDate = new Date();


    // --- 2. INSERT DATA (CREATE) ---
    const form = document.getElementById('workNoteForm');
    form.addEventListener('submit', function(e) {
        e.preventDefault();

        const date = document.getElementById('noteDate').value;
        const title = document.getElementById('noteTitle').value;
        const content = document.getElementById('noteContent').value;

        const newNote = {
            date: date,
            title: title,
            content: content,
            createdAt: new Date().getTime()
        };

        const transaction = db.transaction([storeName], "readwrite");
        const store = transaction.objectStore(storeName);
        const addRequest = store.add(newNote);

        addRequest.onsuccess = function() {
            form.reset();
            document.getElementById('noteDate').valueAsDate = new Date(); // Re-set date
            displayNotes(); // Refresh data table
        };

        transaction.oncomplete = function() {
            console.log("Transaction completed: Note added.");
        };
    });


    // --- 3. FETCH & DISPLAY DATA (READ) ---
    function displayNotes() {
        const tableBody = document.getElementById('notesTableBody');
        const noDataMessage = document.getElementById('noDataMessage');
        tableBody.innerHTML = ""; // Clear existing rows

        const transaction = db.transaction([storeName], "readonly");
        const store = transaction.objectStore(storeName);
        
        // Open a cursor to iterate through records (sorted by ID)
        const cursorRequest = store.openCursor(null, "prev"); // "prev" to see newest first

        let hasData = false;

        cursorRequest.onsuccess = function(event) {
            const cursor = event.target.result;
            if (cursor) {
                hasData = true;
                const note = cursor.value;

                const row = document.createElement('tr');
                row.innerHTML = `
                    <td>${formatDate(note.date)}</td>
                    <td><strong>${escapeHTML(note.title)}</strong></td>
                    <td>${escapeHTML(note.content).replace(/\n/g, '<br>')}</td>
                    <td><button class="btn-delete" onclick="deleteNote(${note.id})">Delete</button></td>
                `;
                tableBody.appendChild(row);
                cursor.continue(); // Move to the next record
            } else {
                // Executed when cursor finished iterating
                if (!hasData) {
                    noDataMessage.style.display = "block";
                } else {
                    noDataMessage.style.display = "none";
                }
            }
        };
    }


    // --- 4. DELETE DATA (DELETE) ---
    window.deleteNote = function(id) {
        if (confirm("Are you sure you want to delete this note?")) {
            const transaction = db.transaction([storeName], "readwrite");
            const store = transaction.objectStore(storeName);
            const deleteRequest = store.delete(id);

            deleteRequest.onsuccess = function() {
                console.log(`Note with ID ${id} deleted.`);
                displayNotes(); // Refresh table UI
            };
        }
    };


    // --- HELPER FUNCTIONS ---
    function formatDate(dateString) {
        if (!dateString) return "";
        const parts = dateString.split("-");
        if (parts.length === 3) {
            return `${parts[2]}/${parts[1]}/${parts[0]}`; // Convert YYYY-MM-DD to DD/MM/YYYY
        }
        return dateString;
    }

    function escapeHTML(str) {
        return str.replace(/[&<>'"]/g, 
            tag => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[tag] || tag)
        );
    }
</script>

</body>
</html>

3. Các bước nhúng vào Blogspot an toàn

Để mã nguồn này chạy hoàn hảo trên Blogger/Blogspot mà không bị xung đột cú pháp, bạn thực hiện theo các bước sau:

Bước 1: Tạo bài viết hoặc trang mới

  • Đăng nhập vào trang quản trị Blogger.

  • Chọn Bài đăng mới (New Post) hoặc Trang (Pages) -> Trang mới.

Bước 2: Chuyển sang chế độ xem HTML

  • Tại thanh công cụ soạn thảo, nhấp vào biểu tượng Cây bút/Thước kẻ ở góc trên cùng bên trái.

  • Chọn Chế độ xem HTML (HTML view). Xóa mọi đoạn code mặc định đang có sẵn đi.

Bước 3: Dán mã nguồn và xử lý ký tự (Quan trọng)

Blogger đôi khi tự động định dạng lại mã JavaScript khi lưu, dẫn đến lỗi cú pháp (đặc biệt là các ký tự logic như && hoặc <). Đoạn code trên đã được tối ưu hóa bằng hàm tránh xung đột, nhưng để an toàn nhất:

  • Dán toàn bộ mã nguồn ở mục (2) vào ô soạn thảo HTML.

  • Nhấp Xuất bản (Publish).

4. Kiến thức cốt lõi cần nhớ cho học sinh, sinh viên

Khi học đoạn code trên, bạn hãy chú ý vào luồng xử lý dữ liệu (CRUD cơ bản):

  1. Mở kết nối: Dùng indexedDB.open(). Nếu DB chưa có, hàm onupgradeneeded sẽ chạy để tạo một cấu trúc bảng (objectStore) có khóa tự tăng (autoIncrement: true).

  2. Transaction (Giao dịch): Mọi thao tác ghi (readwrite) hoặc đọc (readonly) trong IndexedDB đều bắt buộc phải nằm trong một Transaction để đảm bảo tính toàn vẹn dữ liệu (nếu lỗi, nó tự động rollback để không làm hỏng database).

  3. Cursor (Con trỏ): Vì IndexedDB không dùng câu lệnh SELECT *, chúng ta dùng openCursor() để duyệt qua từng hàng dữ liệu từ trên xuống dưới (hoặc ngược lại với "prev") rồi render ra HTML Table bằng JS DOM (appendChild).