Дилема з сайтом. Приклад скрипту для IndexedDBsteemCreated with Sketch.

in Ukraine on Steemyesterday

Кілька днів тому я отримав повідомлення від хостинг-провайдера про закінчення терміну реєстрації доменного імені linkly.com.ua. Колись давно я розповідав, що підтримую роботу сайту, суть якого в збереженні та організації особистих веб-посилань. Це щось на кшталт закладок, які можна зберігати в браузері, тільки зберігання відбувається на сервері, а відображаються вони через веб-інтерфейс. Причому до збережених посилань за бажанням можна додати зображення, і виходить щось як візуальний органайзер. Сайт створювався для мого особистого використання, але з часом я додав реєстрацію, щоб зареєструватися міг будь-хто.

З одного боку, це не комерційний проєкт, попри те що доступ і реєстрація відкриті для всіх. З іншого боку, якась внутрішня частина мене очікує активності на сайті, але за весь час зареєструвалися лише два користувачі, та після підтвердження електронної пошти вони не проявляли жодної активності. От я й стою перед дилемою — продовжувати реєстрацію доменного імені чи ні. І справа не в грошах. Реєстрація мені обійдеться на рік, умовно кажучи для порівняння, як пачка сигарет.

Мій внутрішній демон каже: ти старався, вкладав душу в код, «вилизував» візуальний дизайн. А мій внутрішній прагматик заявляє: «навіщо це взагалі потрібно, це все іграшки і немає в цьому ніякого сенсу». На тлі внутрішніх суперечностей я почав замислюватися над альтернативою, яка справді буде слугувати лише мені й не вимагатиме ресурсів.

Мою увагу привернули два способи зберігання даних, за яких база даних зберігається в самому браузері. Йдеться про взаємодію з базами даних localStorage та IndexedDB, тобто на вибір. Якщо розглядати localStorage — це більш простий спосіб зберігання даних у вигляді рядкових пар "ключ-значення". Як зазначено в описі цього типу сховищ даних, це більш простий спосіб зберігання інформації, і загальний обсяг бази не зможе перевищувати понад 5 МБ, також там є деякі обмеження. База даних IndexedDB використовує структуровані дані, тобто об’єкти та масиви. Можливий асинхронний доступ до бази (можливість працювати далі після команди й не дочікуватися відповіді), можна працювати з великими обсягами інформації, а також здійснювати пошук за даними. Можливе створення резервної копії та експорт даних в окремий файл із розширенням .JSON. Якщо чесно, мене зацікавила саме IndexedDB.

З базою даних взаємодіє звичайна веб-сторінка, побудована в розмітці HTML, а операції виконуються за допомогою JavaScript. Причому цей файл .html можна розмістити на будь-якому хостингу, який підтримує JavaScript, і він буде працювати. Його можна відкрити також просто з комп’ютера без завантаження на сервер, і він також буде працювати. Найголовніше — робити резервну копію БД перед тим, як відкрити цю сторінку на іншому комп’ютері, а потім імпортувати її, тому що база даних зберігається в браузері (я повторюю, щоб нагадати).

Я не хочу розписувати, як працюють окремі частини коду JavaScript, мені простіше зробити примітки безпосередньо в коді. Я також не "вкладав душу" у візуальне оформлення, а зробив лише базовий акуратний дизайн. Якщо комусь ця тема буде цікава і хтось захоче удосконалити — будь ласка, код нижче:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Нотатки в IndexedDB</title>

    <style>
    * {
        margin: 0;
        padding: 0;
    }

    body {
        font-family: sans-serif;
        font-size: 16px;
    }

    h1 {
        padding: 20px 0;
    }

    h3 {
        padding: 0 0 10px 0;
    }

    #main {
        margin: 0 auto;
        padding: 5px;
        max-width: 800px;
        width: 100%;
    }

    textarea {
        max-width: 800px;
        width: 95%;
        min-height: 100px;
        resize: vertical;
        font-family: sans-serif;
        font-size: 16px;
        padding: 10px;
        border-radius: 6px;
        border: 1px solid #ccc;
    }

    .btn {
        display: inline-block;
        margin: 5px 5px 5px 0;
        padding: 10px 18px;
        background-color: #007BFF;
        color: white;
        border: none;
        border-radius: 6px;
        cursor: pointer;
        transition: background-color 0.2s ease;
        font-size: 16px;
    }

    .btn:hover {
        background-color: #0056b3;
    }

    .btn-small {
        padding: 2px 6px;
        font-size: 10px;
        background-color: transparent;
        color: #555;
        border-radius: 3px;
        opacity: 0.9;
        transition: all 0.2s;
        border: none;
        cursor: pointer;
    }

    .btn-small:hover {
        opacity: 1;
        background-color: #dcdcdc;
        color: #000;
    }

    #output {
        margin-top: 20px;
        padding: 10px;
        max-width: 800px;
        width: 95%;
        border: 1px solid #ccc;
        word-wrap: break-word;
        overflow-wrap: anywhere;
        background: #fafafa;
        border-radius: 6px;
    }

    #msg,
    #msg_edit {
        color: red;
        margin: 10px 0;
        height: 20px;
    }

    #modal {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.5);
        align-items: center;
        justify-content: center;
        padding: 20px;
        z-index: 1000;
    }

    #modalContent {
        background: #fff;
        padding: 20px;
        border-radius: 10px;
        max-width: 400px;
        width: 100%;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
        box-sizing: border-box;
    }

    #modal textarea {
        display: block;
        margin: 0 auto;
        width: 100%;
        max-width: 100%;
        height: 120px;
        font-size: 16px;
        padding: 10px;
        border-radius: 6px;
        border: 1px solid #ccc;
        resize: vertical;
        box-sizing: border-box;
    }

    ul {
        padding-left: 20px;
        margin: 0;
    }

    ul li {
        margin-bottom: 12px;
        list-style-type: disc;
    }

    ul li button {
        margin-left: 4px;
    }

    .tools {}

    @media (max-width: 600px) {

        h3 {
            padding: 8px 16px;
        }

        #main {
            padding: 4px;
            width: 98%;
        }

        #output {
            width: 90%;
        }

        #msg_edit {
            padding-left: 15px;
        }

        .btn {
            font-size: 16px;
            padding: 8px 12px;
        }

        .btn-small {
            font-size: 9px;
            padding: 1px 4px;
        }

        textarea {
            width: 90%;
        }

        h1 {
            padding: 10px 3px;
            font-size: 18px;
        }

        #modalContent {
            max-width: 95%;
            padding: 1px;
        }

        #modal textarea {
            width: 90%;
        }

        .tools {
            padding: 0 15px;
        }

    }
    </style>
</head>

<body>

<div id="main">

    <h1>Нотатки в IndexedDB</h1>
    <textarea id="noteInput" placeholder="Напиши нотатку..."></textarea><br>
    <div id="msg"></div>
    <button class="btn" onclick="saveNote()">💾 Зберегти нотатку</button>
    <button class="btn" onclick="exportNotes()">📤 Експортувати нотатки</button>
    <label class="btn" for="importFile">📥 Імпортувати нотатки</label>
    <input type="file" id="importFile" onchange="importNotes()" style="display:none">

    <div id="output"></div>

    <div id="modal">
        <div id="modalContent">
          <h3>Редагувати нотатку</h3>
          <textarea id="editInput"></textarea><br>
          <div id="msg_edit"></div>
          <div class="tools">
              <button class="btn" onclick="saveEdit()">💾 Зберегти</button>
              <button class="btn" onclick="closeModal()">✖ Закрити</button>
          </div>
        </div>
    </div>

</div>

<script>
    let db; // Змінна для бази даних
    let currentEditId = null; // Зберігає ID нотатки, яка зараз редагується

    // Відкриття (або створення) бази даних з ім'ям 'DataBase123' і версією 1
    const request = indexedDB.open('DataBase123', 1);

    // Викликається, якщо потрібно створити або оновити структуру бази (наприклад, при першому запуску)
    request.onupgradeneeded = function(event) {
        db = event.target.result;
        // Створюємо сховище (object store) для нотаток, якщо воно ще не існує
        if (!db.objectStoreNames.contains('notes')) {
            db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
        }
    };

    // Викликається при успішному відкритті бази
    request.onsuccess = function(event) {
        db = event.target.result;
        loadNotes(); // Завантажуємо нотатки на сторінку
    };

    // Обробка помилок при відкритті бази
    request.onerror = function(event) {
        console.error('Помилка при відкритті бази', event);
    };

    // Збереження нової нотатки
    function saveNote() {
        const text = document.getElementById('noteInput').value.trim(); // Отримуємо текст
        if (!text) return showMessage('Не можна зберегти порожню нотатку.');

        const txn = db.transaction('notes', 'readwrite'); // Відкриваємо транзакцію
        const store = txn.objectStore('notes'); // Отримуємо сховище
        store.add({ text }); // Додаємо нову нотатку

        // Після завершення транзакції — очищаємо поле вводу та оновлюємо список
        txn.oncomplete = function() {
            document.getElementById('noteInput').value = '';
            loadNotes();
        };
    }

    // Завантаження всіх нотаток із бази
    function loadNotes() {
        const txn = db.transaction('notes', 'readonly');
        const store = txn.objectStore('notes');
        const request = store.getAll(); // Отримуємо всі нотатки

        request.onsuccess = function() {
            const notes = request.result;
            let html = '';
            if (notes.length > 0) {
                html += '<ul>';
                notes.forEach(note => {
                    html += `<li>
                               ${formatNoteText(note.text)} 
                               &nbsp;
                               <button class="btn btn-small" style="background:transparent" onclick="openModal(${note.id})">✏️</button>
                               <button class="btn btn-small" style="background:transparent" onclick="deleteNote(${note.id})">❌</button>
                             </li>`;
                });
                html += '</ul>';
            }
            document.getElementById('output').innerHTML = html;
        };
    }

    // Експорт нотаток у JSON-файл
    function exportNotes() {
        const txn = db.transaction('notes', 'readonly');
        const store = txn.objectStore('notes');
        const request = store.getAll();

        request.onsuccess = function() {
            const data = JSON.stringify(request.result, null, 2); // Форматуємо JSON
            const blob = new Blob([data], { type: 'application/json' }); // Створюємо файл
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = 'notes_backup.json'; // Ім’я файлу
            link.click(); // Симулюємо клік для завантаження
        };
    }

    // Імпорт нотаток з файлу
    function importNotes() {
        const fileInput = document.getElementById('importFile');
        const file = fileInput.files[0];
        if (!file) return;

        const reader = new FileReader();
        reader.onload = function(event) {
            try {
                const importedNotes = JSON.parse(event.target.result); // Парсимо JSON

                // Спочатку очищаємо базу
                const clearTxn = db.transaction('notes', 'readwrite');
                const store = clearTxn.objectStore('notes');
                const clearRequest = store.clear();

                clearRequest.onsuccess = function() {
                    // Після очищення — додаємо нові нотатки
                    const txn = db.transaction('notes', 'readwrite');
                    const store = txn.objectStore('notes');

                    importedNotes.forEach(note => {
                        delete note.id; // Видаляємо id, щоб автоінкремент спрацював
                        store.add(note);
                    });

                    txn.oncomplete = loadNotes;
                };

                clearRequest.onerror = function() {
                    showMessage('Помилка при очищенні бази');
                };

            } catch (e) {
                showMessage('Помилка при імпорті: ' + e.message);
            }
        };
        reader.readAsText(file);
    }

    // Відкриття модального вікна для редагування нотатки
    function openModal(id) {
        currentEditId = id;
        const txn = db.transaction('notes', 'readonly');
        const store = txn.objectStore('notes');
        const request = store.get(id);

        request.onsuccess = function() {
            const note = request.result;
            document.getElementById('editInput').value = note.text;
            document.getElementById('modal').style.display = 'flex'; // Показуємо модальне вікно
            document.getElementById('msg_edit').textContent = '';
        };
    }

    // Закриття модального вікна
    function closeModal() {
        document.getElementById('modal').style.display = 'none';
        currentEditId = null;
    }

    // Збереження зміненої нотатки
    function saveEdit() {
        const newText = document.getElementById('editInput').value.trim();
        if (!newText) return showEditMessage('Текст не може бути порожнім.');

        const txn = db.transaction('notes', 'readwrite');
        const store = txn.objectStore('notes');
        store.put({ id: currentEditId, text: newText }); // Оновлюємо запис

        txn.oncomplete = function() {
            closeModal();
            loadNotes();
        };
    }

    // Видалення нотатки
    function deleteNote(id) {
        if (!confirm('Видалити цю нотатку?')) return;

        const txn = db.transaction('notes', 'readwrite');
        const store = txn.objectStore('notes');
        store.delete(id); // Видаляємо за ID

        txn.oncomplete = loadNotes;
    }

    // Виведення повідомлення внизу (для загального інтерфейсу)
    function showMessage(msg) {
        const msgDiv = document.getElementById('msg');
        msgDiv.textContent = msg;
        setTimeout(() => msgDiv.textContent = '', 3000);
    }

    // Повідомлення при редагуванні
    function showEditMessage(msg) {
        const msgDiv = document.getElementById('msg_edit');
        msgDiv.textContent = msg;
        setTimeout(() => msgDiv.textContent = '', 3000);
    }

    // Екранування HTML-символів, щоб уникнути XSS
    function escapeHtml(text) {
        return text.replace(/&/g, '&amp;')
                   .replace(/</g, '&lt;')
                   .replace(/>/g, '&gt;')
                   .replace(/"/g, '&quot;')
                   .replace(/'/g, '&#039;');
    }

    // Форматування тексту нотатки: посилання + переноси рядків
    function formatNoteText(text) {
        const escaped = escapeHtml(text); // Спершу екрануємо
        const linked = escaped.replace(
            /(https?:\/\/[^\s]+)/g,
            '<a href="$1" target="_blank">$1</a>' // Додаємо посилання
        );
        return linked.replace(/\n/g, '<br>'); // Переноси рядків
    }
</script>

</body>
</html>

   
Сподіваюся, комусь знадобиться :)

Sort:  
Team Europe appreciates your content!
chriddi, moecki and/or the-gorilla
 4 hours ago 

Thanks a lot 🙏