Sabtu, 10 Januari 2026

CRUD Canva

 








PROMPT Chat Gpt


Buatkan prompt aplikasi SPA menggunakan backen google apps script dan google spreadsheet . Front end menggunakan html dan tailwind. Frontend dipanggil langsung dengan fungsi doget untuk memanggil index

PROMPT Canva

Buatkan aplikasi web SPA (Single Page Application) lengkap menggunakan:

BACKEND:
- Google Apps Script (Web App)
- Google Spreadsheet sebagai database

FRONTEND:
- HTML + Tailwind CSS (via CDN)
- Vanilla JavaScript (tanpa framework)
- SPA (tanpa reload halaman)

ARSITEKTUR:
- Frontend dipanggil langsung dari Google Apps Script menggunakan:
  function doGet(e) {
    return HtmlService.createHtmlOutputFromFile('index')
      .setTitle('SPA CRUD App')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
      .addMetaTag('viewport', 'width=device-width, initial-scale=1.0');
  }

KETENTUAN BACKEND:
1. Gunakan satu file code.gs
2. Sediakan fungsi:
   - initializeSheet() → membuat sheet jika belum ada
   - apiRouter(e) → router API berbasis parameter `action`
3. Gunakan Google Spreadsheet sebagai database dengan kolom:
   - id (UUID)
   - created_at
   - title
   - description
4. CRUD API:
   - action=list     → GET semua data
   - action=create   → POST data baru
   - action=update   → POST update data
   - action=delete   → POST hapus data
5. Semua response menggunakan JSON:
   { status, message, data }

KETENTUAN FRONTEND:
1. File: index.html
2. Gunakan Tailwind CDN
3. Struktur UI:
   - Header / Navbar
   - Form tambah & edit data (modal)
   - Tabel data
   - Tombol Edit & Delete
4. SPA behavior:
   - Load data tanpa reload
   - Submit form via fetch()
   - Refresh tabel otomatis
5. API URL:
   - Gunakan google.script.run atau fetch ke doGet dengan parameter action



Code.gs

// ============================================
  // KONFIGURASI
  // ============================================
  const SPREADSHEET_ID = '1cvg-wICvpKjTdujoTNED0FSwHHV-oUss2GpODSXhMUc'; // Kosongkan untuk auto-create atau isi ID spreadsheet
  const SHEET_NAME = 'Data';
 
  // ============================================
  // MAIN FUNCTIONS
  // ============================================
  function doGet(e) {
    // Jika ada parameter action, proses sebagai API
    if (e && e.parameter && e.parameter.action) {
      return handleApi(e);
    }
   
    // Jika tidak ada parameter, tampilkan HTML
    return HtmlService.createHtmlOutputFromFile('index')
      .setTitle('SPA CRUD App')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
      .addMetaTag('viewport', 'width=device-width, initial-scale=1.0');
  }
 
  function doPost(e) {
    return handleApi(e);
  }
 
  // ============================================
  // API ROUTER
  // ============================================
  function apiRouter(e) {
    const action = e.action || (e.parameter ? e.parameter.action : null);
   
    switch(action) {
      case 'list':
        return listData();
      case 'create':
        return createData(e);
      case 'update':
        return updateData(e);
      case 'delete':
        return deleteData(e);
      default:
        return { status: 'error', message: 'Action tidak valid' };
    }
  }
 
  function handleApi(e) {
    try {
      const params = e.parameter || {};
      const postData = e.postData ? JSON.parse(e.postData.contents) : {};
      const data = { ...params, ...postData };
     
      const result = apiRouter(data);
     
      return ContentService.createTextOutput(JSON.stringify(result))
        .setMimeType(ContentService.MimeType.JSON);
    } catch (error) {
      return ContentService.createTextOutput(JSON.stringify({
        status: 'error',
        message: error.message
      })).setMimeType(ContentService.MimeType.JSON);
    }
  }
 
  // ============================================
  // SHEET FUNCTIONS
  // ============================================
  function getSheet() {
    let ss;
   
    if (SPREADSHEET_ID) {
      ss = SpreadsheetApp.openById(SPREADSHEET_ID);
    } else {
      ss = SpreadsheetApp.getActiveSpreadsheet();
      if (!ss) {
        ss = SpreadsheetApp.create('SPA CRUD Database');
      }
    }
   
    let sheet = ss.getSheetByName(SHEET_NAME);
   
    if (!sheet) {
      sheet = ss.insertSheet(SHEET_NAME);
      // Set headers
      sheet.getRange('A1:D1').setValues([['id', 'title', 'description', 'created_at']]);
      sheet.getRange('A1:D1').setFontWeight('bold');
      sheet.setFrozenRows(1);
    }
   
    return sheet;
  }
 
  function initializeSheet() {
    const sheet = getSheet();
    return { status: 'success', message: 'Sheet initialized' };
  }
 
  // ============================================
  // CRUD OPERATIONS
  // ============================================
  function generateUUID() {
    return Utilities.getUuid();
  }
 
  function listData() {
    const sheet = getSheet();
    const lastRow = sheet.getLastRow();
   
    if (lastRow <= 1) {
      return { status: 'success', data: [] };
    }
   
    const dataRange = sheet.getRange(2, 1, lastRow - 1, 4);
    const values = dataRange.getValues();
   
    const data = values.map(row => ({
      id: row[0],
      title: row[1],
      description: row[2],
      created_at: row[3]
    })).filter(item => item.id); // Filter empty rows
   
    return { status: 'success', data: data };
  }
 
  function createData(params) {
    const sheet = getSheet();
    const id = generateUUID();
    const createdAt = new Date().toISOString();
   
    const title = params.title || '';
    const description = params.description || '';
   
    if (!title) {
      return { status: 'error', message: 'Title tidak boleh kosong' };
    }
   
    sheet.appendRow([id, title, description, createdAt]);
   
    return {
      status: 'success',
      message: 'Data berhasil ditambahkan',
      data: { id, title, description, created_at: createdAt }
    };
  }
 
  function updateData(params) {
    const sheet = getSheet();
    const id = params.id;
   
    if (!id) {
      return { status: 'error', message: 'ID tidak ditemukan' };
    }
   
    const dataRange = sheet.getDataRange();
    const values = dataRange.getValues();
   
    for (let i = 1; i < values.length; i++) {
      if (values[i][0] === id) {
        if (params.title !== undefined) {
          sheet.getRange(i + 1, 2).setValue(params.title);
        }
        if (params.description !== undefined) {
          sheet.getRange(i + 1, 3).setValue(params.description);
        }
       
        return {
          status: 'success',
          message: 'Data berhasil diupdate',
          data: {
            id: id,
            title: params.title || values[i][1],
            description: params.description || values[i][2],
            created_at: values[i][3]
          }
        };
      }
    }
   
    return { status: 'error', message: 'Data tidak ditemukan' };
  }
 
  function deleteData(params) {
    const sheet = getSheet();
    const id = params.id;
   
    if (!id) {
      return { status: 'error', message: 'ID tidak ditemukan' };
    }
   
    const dataRange = sheet.getDataRange();
    const values = dataRange.getValues();
   
    for (let i = 1; i < values.length; i++) {
      if (values[i][0] === id) {
        sheet.deleteRow(i + 1);
        return { status: 'success', message: 'Data berhasil dihapus' };
      }
    }
   
    return { status: 'error', message: 'Data tidak ditemukan' };
  }
 

Index.html

<!doctype html>
<html lang="id" class="h-full">
 <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SPA CRUD Registration App</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="/_sdk/element_sdk.js"></script>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet">
  <style>
    body {
      box-sizing: border-box;
    }
    * {
      font-family: 'Plus Jakarta Sans', sans-serif;
    }
    .fade-in {
      animation: fadeIn 0.3s ease-out;
    }
    @keyframes fadeIn {
      from { opacity: 0; transform: translateY(-10px); }
      to { opacity: 1; transform: translateY(0); }
    }
    .slide-up {
      animation: slideUp 0.3s ease-out;
    }
    @keyframes slideUp {
      from { opacity: 0; transform: translateY(20px); }
      to { opacity: 1; transform: translateY(0); }
    }
    .toast {
      animation: toastIn 0.3s ease-out, toastOut 0.3s ease-in 2.7s;
    }
    @keyframes toastIn {
      from { opacity: 0; transform: translateX(100%); }
      to { opacity: 1; transform: translateX(0); }
    }
    @keyframes toastOut {
      from { opacity: 1; transform: translateX(0); }
      to { opacity: 0; transform: translateX(100%); }
    }
  </style>
  <style>@view-transition { navigation: auto; }</style>
  <script src="/_sdk/data_sdk.js" type="text/javascript"></script>
 </head>
 <body class="h-full bg-slate-50">
  <div id="app" class="h-full w-full overflow-auto"><!-- Header -->
   <header class="bg-gradient-to-r from-indigo-600 to-purple-600 shadow-lg">
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
     <div class="flex items-center justify-between">
      <div class="flex items-center gap-3">
       <div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
        <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
        </svg>
       </div>
       <h1 id="app-title" class="text-xl sm:text-2xl font-bold text-white">SPA CRUD Registration</h1>
      </div><button onclick="openModal()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-xl font-medium transition-all duration-200 flex items-center gap-2 backdrop-blur-sm">
       <svg class="w-5 h-5" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
       </svg><span class="hidden sm:inline">Tambah Data</span> </button>
     </div>
    </div>
   </header><!-- Main Content -->
   <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"><!-- Stats Cards -->
    <div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
     <div class="bg-white rounded-2xl p-5 shadow-sm border border-slate-100">
      <div class="flex items-center gap-4">
       <div class="w-12 h-12 bg-indigo-100 rounded-xl flex items-center justify-center">
        <svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
        </svg>
       </div>
       <div>
        <p class="text-slate-500 text-sm">Total Data</p>
        <p id="total-count" class="text-2xl font-bold text-slate-800">0</p>
       </div>
      </div>
     </div>
     <div class="bg-white rounded-2xl p-5 shadow-sm border border-slate-100">
      <div class="flex items-center gap-4">
       <div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center">
        <svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
        </svg>
       </div>
       <div>
        <p class="text-slate-500 text-sm">Status</p>
        <p id="status-text" class="text-lg font-semibold text-green-600">Online</p>
       </div>
      </div>
     </div>
     <div class="bg-white rounded-2xl p-5 shadow-sm border border-slate-100">
      <div class="flex items-center gap-4">
       <div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center">
        <svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
        </svg>
       </div>
       <div>
        <p class="text-slate-500 text-sm">Last Update</p>
        <p id="last-update" class="text-lg font-semibold text-slate-800">-</p>
       </div>
      </div>
     </div>
    </div><!-- Search & Filter -->
    <div class="bg-white rounded-2xl p-4 shadow-sm border border-slate-100 mb-6">
     <div class="flex flex-col sm:flex-row gap-4">
      <div class="flex-1 relative">
       <svg class="w-5 h-5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
       </svg><input type="text" id="search-input" placeholder="Cari data..." class="w-full pl-10 pr-4 py-2.5 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all">
      </div><button onclick="loadData()" class="bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-2.5 rounded-xl font-medium transition-all flex items-center justify-center gap-2">
       <svg class="w-5 h-5" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
       </svg> Refresh </button>
     </div>
    </div><!-- Data Table -->
    <div class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
     <div class="overflow-x-auto">
      <table class="w-full">
       <thead class="bg-slate-50 border-b border-slate-100">
        <tr>
         <th class="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No</th>
         <th class="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Title</th>
         <th class="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider hidden sm:table-cell">Description</th>
         <th class="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider hidden md:table-cell">Created</th>
         <th class="px-6 py-4 text-center text-xs font-semibold text-slate-500 uppercase tracking-wider">Actions</th>
        </tr>
       </thead>
       <tbody id="data-table" class="divide-y divide-slate-100"><!-- Data rows will be inserted here -->
       </tbody>
      </table>
     </div><!-- Empty State -->
     <div id="empty-state" class="hidden py-16 text-center">
      <div class="w-20 h-20 bg-slate-100 rounded-full flex items-center justify-center mx-auto mb-4">
       <svg class="w-10 h-10 text-slate-400" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
       </svg>
      </div>
      <h3 class="text-lg font-semibold text-slate-700 mb-2">Belum ada data</h3>
      <p class="text-slate-500 mb-4">Mulai dengan menambahkan data baru</p><button onclick="openModal()" class="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-2.5 rounded-xl font-medium transition-all"> Tambah Data Pertama </button>
     </div><!-- Loading State -->
     <div id="loading-state" class="py-16 text-center">
      <div class="w-12 h-12 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin mx-auto mb-4"></div>
      <p class="text-slate-500">Memuat data...</p>
     </div>
    </div><!-- Instructions Panel -->
 
   </main><!-- Modal -->
   <div id="modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden items-center justify-center z-50 p-4">
    <div class="bg-white rounded-2xl w-full max-w-lg shadow-2xl slide-up">
     <div class="p-6 border-b border-slate-100">
      <div class="flex items-center justify-between">
       <h2 id="modal-title" class="text-xl font-bold text-slate-800">Tambah Data Baru</h2><button onclick="closeModal()" class="w-8 h-8 bg-slate-100 hover:bg-slate-200 rounded-lg flex items-center justify-center transition-all">
        <svg class="w-5 h-5 text-slate-500" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
        </svg></button>
      </div>
     </div>
     <form id="data-form" onsubmit="handleSubmit(event)" class="p-6 space-y-5"><input type="hidden" id="edit-id">
      <div><label for="title" class="block text-sm font-medium text-slate-700 mb-2">Title <span class="text-red-500">*</span></label> <input type="text" id="title" name="title" required class="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all" placeholder="Masukkan title...">
      </div>
      <div><label for="description" class="block text-sm font-medium text-slate-700 mb-2">Description</label> <textarea id="description" name="description" rows="4" class="w-full px-4 py-3 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all resize-none" placeholder="Masukkan description..."></textarea>
      </div>
      <div class="flex gap-3 pt-2"><button type="button" onclick="closeModal()" class="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-3 rounded-xl font-medium transition-all"> Batal </button> <button type="submit" id="submit-btn" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-3 rounded-xl font-medium transition-all flex items-center justify-center gap-2"> <span id="submit-text">Simpan</span>
        <div id="submit-loader" class="hidden w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div></button>
      </div>
     </form>
    </div>
   </div><!-- Delete Confirmation Modal -->
   <div id="delete-modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden items-center justify-center z-50 p-4">
    <div class="bg-white rounded-2xl w-full max-w-sm shadow-2xl slide-up">
     <div class="p-6 text-center">
      <div class="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
       <svg class="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewbox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
       </svg>
      </div>
      <h3 class="text-xl font-bold text-slate-800 mb-2">Hapus Data?</h3>
      <p class="text-slate-500 mb-6">Data yang dihapus tidak dapat dikembalikan.</p><input type="hidden" id="delete-id">
      <div class="flex gap-3"><button onclick="closeDeleteModal()" class="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-3 rounded-xl font-medium transition-all"> Batal </button> <button onclick="confirmDelete()" id="delete-btn" class="flex-1 bg-red-600 hover:bg-red-700 text-white px-4 py-3 rounded-xl font-medium transition-all flex items-center justify-center gap-2"> <span id="delete-text">Hapus</span>
        <div id="delete-loader" class="hidden w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div></button>
      </div>
     </div>
    </div>
   </div><!-- Toast Container -->
   <div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2"></div>
  </div>
  <script>
    // ============================================
    // CONFIGURATION - Ubah URL sesuai deployment
    // ============================================
    const CONFIG = {
      // Untuk Google Apps Script, gunakan format:
      // 'https://script.google.com/macros/s/YOUR_SCRIPT_ID/exec'
      API_URL: '', // Kosongkan jika menggunakan google.script.run
      USE_GOOGLE_SCRIPT_RUN: true // Set true jika di-deploy di Apps Script
    };

    // ============================================
    // STATE MANAGEMENT
    // ============================================
    let dataStore = [];
    let isEditing = false;

    // ============================================
    // ELEMENT SDK CONFIGURATION
    // ============================================
    const defaultConfig = {
      app_title: 'SPA CRUD Registration',
      primary_color: '#4f46e5',
      secondary_color: '#7c3aed',
      background_color: '#f8fafc',
      text_color: '#1e293b',
      accent_color: '#10b981'
    };

    // ============================================
    // UTILITY FUNCTIONS
    // ============================================
    function generateUUID() {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        const r = Math.random() * 16 | 0;
        const v = c === 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
      });
    }

    function formatDate(dateString) {
      if (!dateString) return '-';
      const date = new Date(dateString);
      return date.toLocaleDateString('id-ID', {
        day: '2-digit',
        month: 'short',
        year: 'numeric',
        hour: '2-digit',
        minute: '2-digit'
      });
    }

    function showToast(message, type = 'success') {
      const container = document.getElementById('toast-container');
      const toast = document.createElement('div');
     
      const bgColor = type === 'success' ? 'bg-green-600' : type === 'error' ? 'bg-red-600' : 'bg-indigo-600';
      const icon = type === 'success'
        ? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>'
        : type === 'error'
        ? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>'
        : '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>';

      toast.className = `toast ${bgColor} text-white px-4 py-3 rounded-xl shadow-lg flex items-center gap-3 min-w-72`;
      toast.innerHTML = `
        <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">${icon}</svg>
        <span class="font-medium">${message}</span>
      `;
     
      container.appendChild(toast);
     
      setTimeout(() => {
        toast.remove();
      }, 3000);
    }

    // ============================================
    // API FUNCTIONS
    // ============================================
    async function apiCall(action, data = {}) {
      // Simulasi untuk demo - ganti dengan implementasi sebenarnya
      if (CONFIG.USE_GOOGLE_SCRIPT_RUN && typeof google !== 'undefined' && google.script) {
        return new Promise((resolve, reject) => {
          google.script.run
            .withSuccessHandler(resolve)
            .withFailureHandler(reject)
            .apiRouter({ action, ...data });
        });
      } else {
        // Demo mode - simulasi API
        return simulateAPI(action, data);
      }
    }

    // Simulasi API untuk demo
    function simulateAPI(action, data) {
      return new Promise((resolve) => {
        setTimeout(() => {
          switch(action) {
            case 'list':
              resolve({ status: 'success', data: dataStore });
              break;
            case 'create':
              const newItem = {
                id: generateUUID(),
                title: data.title,
                description: data.description,
                created_at: new Date().toISOString()
              };
              dataStore.push(newItem);
              resolve({ status: 'success', message: 'Data berhasil ditambahkan', data: newItem });
              break;
            case 'update':
              const updateIndex = dataStore.findIndex(item => item.id === data.id);
              if (updateIndex !== -1) {
                dataStore[updateIndex] = { ...dataStore[updateIndex], ...data };
                resolve({ status: 'success', message: 'Data berhasil diupdate', data: dataStore[updateIndex] });
              } else {
                resolve({ status: 'error', message: 'Data tidak ditemukan' });
              }
              break;
            case 'delete':
              const deleteIndex = dataStore.findIndex(item => item.id === data.id);
              if (deleteIndex !== -1) {
                dataStore.splice(deleteIndex, 1);
                resolve({ status: 'success', message: 'Data berhasil dihapus' });
              } else {
                resolve({ status: 'error', message: 'Data tidak ditemukan' });
              }
              break;
            default:
              resolve({ status: 'error', message: 'Action tidak valid' });
          }
        }, 500);
      });
    }

    // ============================================
    // DATA OPERATIONS
    // ============================================
    async function loadData() {
      const loadingState = document.getElementById('loading-state');
      const emptyState = document.getElementById('empty-state');
      const dataTable = document.getElementById('data-table');

      loadingState.classList.remove('hidden');
      emptyState.classList.add('hidden');
      dataTable.innerHTML = '';

      try {
        const response = await apiCall('list');
       
        loadingState.classList.add('hidden');

        if (response.status === 'success') {
          dataStore = response.data || [];
          renderTable(dataStore);
          updateStats();
        } else {
          showToast(response.message || 'Gagal memuat data', 'error');
        }
      } catch (error) {
        loadingState.classList.add('hidden');
        showToast('Terjadi kesalahan saat memuat data', 'error');
        console.error(error);
      }
    }

    function renderTable(data) {
      const dataTable = document.getElementById('data-table');
      const emptyState = document.getElementById('empty-state');
      const searchTerm = document.getElementById('search-input').value.toLowerCase();

      const filteredData = data.filter(item =>
        item.title.toLowerCase().includes(searchTerm) ||
        (item.description && item.description.toLowerCase().includes(searchTerm))
      );

      if (filteredData.length === 0) {
        dataTable.innerHTML = '';
        emptyState.classList.remove('hidden');
        return;
      }

      emptyState.classList.add('hidden');

      dataTable.innerHTML = filteredData.map((item, index) => `
        <tr class="hover:bg-slate-50 transition-colors fade-in" data-id="${item.id}">
          <td class="px-6 py-4 text-sm text-slate-500">${index + 1}</td>
          <td class="px-6 py-4">
            <div class="font-medium text-slate-800">${escapeHtml(item.title)}</div>
            <div class="text-sm text-slate-500 sm:hidden mt-1 line-clamp-1">${escapeHtml(item.description || '-')}</div>
          </td>
          <td class="px-6 py-4 text-sm text-slate-600 hidden sm:table-cell">
            <div class="max-w-xs truncate">${escapeHtml(item.description || '-')}</div>
          </td>
          <td class="px-6 py-4 text-sm text-slate-500 hidden md:table-cell">${formatDate(item.created_at)}</td>
          <td class="px-6 py-4">
            <div class="flex items-center justify-center gap-2">
              <button onclick="editData('${item.id}')" class="w-8 h-8 bg-indigo-100 hover:bg-indigo-200 text-indigo-600 rounded-lg flex items-center justify-center transition-all" title="Edit">
                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
                </svg>
              </button>
              <button onclick="deleteData('${item.id}')" class="w-8 h-8 bg-red-100 hover:bg-red-200 text-red-600 rounded-lg flex items-center justify-center transition-all" title="Hapus">
                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
                </svg>
              </button>
            </div>
          </td>
        </tr>
      `).join('');
    }

    function escapeHtml(text) {
      const div = document.createElement('div');
      div.textContent = text;
      return div.innerHTML;
    }

    function updateStats() {
      document.getElementById('total-count').textContent = dataStore.length;
      document.getElementById('last-update').textContent = new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
    }

    // ============================================
    // MODAL FUNCTIONS
    // ============================================
    function openModal(editMode = false) {
      const modal = document.getElementById('modal');
      const modalTitle = document.getElementById('modal-title');
      const submitText = document.getElementById('submit-text');
     
      isEditing = editMode;
      modalTitle.textContent = editMode ? 'Edit Data' : 'Tambah Data Baru';
      submitText.textContent = editMode ? 'Update' : 'Simpan';
     
      modal.classList.remove('hidden');
      modal.classList.add('flex');
      document.body.style.overflow = 'hidden';
    }

    function closeModal() {
      const modal = document.getElementById('modal');
      const form = document.getElementById('data-form');
     
      modal.classList.add('hidden');
      modal.classList.remove('flex');
      document.body.style.overflow = '';
      form.reset();
      document.getElementById('edit-id').value = '';
      isEditing = false;
    }

    function editData(id) {
      const item = dataStore.find(d => d.id === id);
      if (!item) return;

      document.getElementById('edit-id').value = item.id;
      document.getElementById('title').value = item.title;
      document.getElementById('description').value = item.description || '';
     
      openModal(true);
    }

    // ============================================
    // DELETE FUNCTIONS
    // ============================================
    function deleteData(id) {
      document.getElementById('delete-id').value = id;
      const deleteModal = document.getElementById('delete-modal');
      deleteModal.classList.remove('hidden');
      deleteModal.classList.add('flex');
      document.body.style.overflow = 'hidden';
    }

    function closeDeleteModal() {
      const deleteModal = document.getElementById('delete-modal');
      deleteModal.classList.add('hidden');
      deleteModal.classList.remove('flex');
      document.body.style.overflow = '';
      document.getElementById('delete-id').value = '';
    }

    async function confirmDelete() {
      const id = document.getElementById('delete-id').value;
      const deleteBtn = document.getElementById('delete-btn');
      const deleteText = document.getElementById('delete-text');
      const deleteLoader = document.getElementById('delete-loader');

      deleteBtn.disabled = true;
      deleteText.textContent = 'Menghapus...';
      deleteLoader.classList.remove('hidden');

      try {
        const response = await apiCall('delete', { id });
       
        if (response.status === 'success') {
          showToast('Data berhasil dihapus', 'success');
          closeDeleteModal();
          loadData();
        } else {
          showToast(response.message || 'Gagal menghapus data', 'error');
        }
      } catch (error) {
        showToast('Terjadi kesalahan', 'error');
        console.error(error);
      } finally {
        deleteBtn.disabled = false;
        deleteText.textContent = 'Hapus';
        deleteLoader.classList.add('hidden');
      }
    }

    // ============================================
    // FORM HANDLING
    // ============================================
    async function handleSubmit(event) {
      event.preventDefault();
     
      const submitBtn = document.getElementById('submit-btn');
      const submitText = document.getElementById('submit-text');
      const submitLoader = document.getElementById('submit-loader');
     
      const title = document.getElementById('title').value.trim();
      const description = document.getElementById('description').value.trim();
      const editId = document.getElementById('edit-id').value;

      if (!title) {
        showToast('Title tidak boleh kosong', 'error');
        return;
      }

      submitBtn.disabled = true;
      submitText.textContent = isEditing ? 'Updating...' : 'Menyimpan...';
      submitLoader.classList.remove('hidden');

      try {
        let response;
        if (isEditing && editId) {
          response = await apiCall('update', { id: editId, title, description });
        } else {
          response = await apiCall('create', { title, description });
        }

        if (response.status === 'success') {
          showToast(response.message || (isEditing ? 'Data berhasil diupdate' : 'Data berhasil ditambahkan'), 'success');
          closeModal();
          loadData();
        } else {
          showToast(response.message || 'Operasi gagal', 'error');
        }
      } catch (error) {
        showToast('Terjadi kesalahan', 'error');
        console.error(error);
      } finally {
        submitBtn.disabled = false;
        submitText.textContent = isEditing ? 'Update' : 'Simpan';
        submitLoader.classList.add('hidden');
      }
    }

    // ============================================
    // SEARCH FUNCTIONALITY
    // ============================================
    document.getElementById('search-input').addEventListener('input', function() {
      renderTable(dataStore);
    });

    // ============================================
    // ELEMENT SDK INITIALIZATION
    // ============================================
    async function initApp() {
      if (window.elementSdk) {
        await window.elementSdk.init({
          defaultConfig,
          onConfigChange: async (config) => {
            const appTitle = document.getElementById('app-title');
            if (appTitle) {
              appTitle.textContent = config.app_title || defaultConfig.app_title;
            }
          },
          mapToCapabilities: (config) => ({
            recolorables: [
              {
                get: () => config.primary_color || defaultConfig.primary_color,
                set: (value) => window.elementSdk.setConfig({ primary_color: value })
              }
            ],
            borderables: [],
            fontEditable: undefined,
            fontSizeable: undefined
          }),
          mapToEditPanelValues: (config) => new Map([
            ['app_title', config.app_title || defaultConfig.app_title]
          ])
        });
      }
     
      loadData();
    }

    // Initialize app
    initApp();
  </script>
   <script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'9bc0059183dfd210',t:'MTc2ODA4ODQ1MS4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>

Tidak ada komentar:

Posting Komentar

Materi Pelatihan GAS

  https://drive.google.com/file/d/1vqQ-Qb35jqFP7L-34wwgTHl81PSesy1w/view?usp=drive_link https://docs.google.com/document/d/14_XzcaU-yGf1-qKz...