Jumat, 10 April 2026

MPA - E-Commerce MPA (Multi-Page Application)

 






Pengembangan aplikasi E-Commerce MPA (Multi-Page Application) berbasis Google Apps Script yang telah kita selesaikan:

1. Arsitektur Utama

  • Struktur MPA: Berhasil mengimplementasikan sistem routing server-side menggunakan fungsi doGet(e) dengan parameter ?page=. Hal ini memastikan setiap perpindahan halaman memicu reload penuh, sehingga menghindari state error pada iframe Google.

  • Modularitas: Menggunakan fungsi include() yang memungkinkan komponen seperti Navbar.html dipanggil secara dinamis di berbagai halaman (Index, Produk, Form, Dashboard) tanpa duplikasi kode.

2. Integrasi Backend & Database

  • Otomasi Database: Membuat fungsi setupDatabase untuk inisialisasi header pada Google Sheets secara otomatis (ID, Nama, Harga, Kategori, Foto_URL).

  • Manajemen File: Mengintegrasikan Google Drive API untuk penyimpanan foto produk. Sistem secara otomatis memberikan izin akses publik (view only) pada setiap file yang diunggah.

  • Sinkronisasi URL: Menggunakan variabel rootUrl yang di-inject dari server ke client untuk memastikan link navigasi selalu akurat sesuai dengan URL deployment terbaru.

3. Perbaikan UI/UX & Stabilitas

  • Solusi Error Navigasi: Penggunaan atribut target="_top" pada seluruh elemen <a> untuk mengatasi error "Maaf saat ini tidak bisa membuka file" yang sering muncul akibat pembatasan frame Google.

  • Optimasi Gambar: Mengalihkan format URL gambar dari link preview Drive biasa ke format https://lh3.googleusercontent.com/d/${fileId}. Ini adalah solusi paling stabil agar foto produk dapat dirender langsung oleh browser tanpa kendala izin akses atau login.

  • Modern Design: Implementasi Tailwind CSS untuk antarmuka yang profesional, responsif, dan dilengkapi dengan visual feedback seperti loading spinners dan hover effects.

4. Status Final File

Aplikasi sekarang terdiri dari:

  1. Code.gs: Sebagai otak aplikasi (Router, Database Logic, Drive Integration).

  2. Navbar.html: Komponen navigasi global.

  3. Index.html: Landing page utama.

  4. Produk.html: Katalog produk dinamis dengan sistem penanganan error gambar.

  5. Form.html: Panel admin untuk input produk dan unggah foto.


PROMPT

Prompt Master: Web App Google Apps Script (MPA & Tailwind)

Peran: Bertindaklah sebagai Senior Full-stack Developer spesialis Google Apps Script dan UI Designer modern.

Tujuan: Buatlah kode lengkap untuk aplikasi web bernama [Nama Aplikasi] dengan struktur Multi-Page Application (MPA).

Spesifikasi Teknologi:

  1. Backend: Google Apps Script (Code.gs) sebagai router menggunakan parameter ?page=.

  2. Database: Google Sheets sebagai database utama.

  3. UI Framework: Tailwind CSS (via CDN) dengan desain yang modern, profesional, dan minimalist menggunakan skema warna [Sebutkan Warna, misal: Dark Mode / Slate & Indigo].

  4. Fitur Navigasi: Gunakan fungsi include(filename, rootUrl) agar komponen seperti Navbar bisa dipanggil di tiap halaman. Pastikan semua link navigasi menggunakan target="_top" untuk menghindari error iframe Google.

Fitur Aplikasi yang Dibutuhkan:

  • Halaman Dashboard: Menampilkan ringkasan data dari Google Sheets.

  • Halaman Form: Untuk input data ke Sheets (termasuk fitur upload file ke Google Drive jika ada).

  • Halaman Tabel: Menampilkan data dari Sheets dengan fitur search/filter sederhana di sisi klien.

  • [Tambahkan Fitur Khusus Lainnya].

Instruksi Khusus untuk Stabilitas:

  • Sertakan fungsi setupDatabase() untuk membuat header otomatis di Google Sheets.

  • Pastikan URL gambar menggunakan format googleusercontent.com/d/[ID] agar bisa tampil secara publik.

  • Implementasikan penanganan error (try-catch) pada doGet untuk mengarahkan user ke halaman 404 jika file HTML tidak ditemukan.

  • Berikan komentar penjelasan pada bagian kode yang krusial.

Output: Berikan kode lengkap untuk Code.gs, Navbar.html, dan halaman-halaman utama lainnya secara terpisah.


Tips Tambahan untuk Kamu:

  • Jika untuk MarketPlace: Tambahkan kalimat: "Implementasikan sistem kartu produk (product card) yang responsif dengan efek hover dan modal untuk detail produk."

  • Jika untuk Sistem Inventaris: Tambahkan kalimat: "Buat sistem validasi stok di sisi server sebelum data disimpan ke Google Sheets."

  • Jika untuk Portofolio: Tambahkan kalimat: "Fokuskan pada micro-interactions dan penggunaan tipografi yang elegan menggunakan Google Fonts."

Dengan prompt yang detail seperti ini, AI akan memberikan struktur kode yang bersih dan meminimalisir error "Maaf tidak bisa membuka file" sejak awal pengerjaan.


Code.gs

/**
 * KONFIGURASI DATABASE & STORAGE
 * Ganti ID di bawah ini dengan ID milik Anda
 */
const SPREADSHEET_ID = '1FMoksYgCv7mLZYyCfp8dEL3GNlplQnYATcBzFcXIs9k';
const DRIVE_FOLDER_ID = '1yCwQ8ASjtJRx2p6P_U1GhX1I3kfbGXQv';

/**
 * 1. FUNGSI SETUP DATABASE
 * Jalankan fungsi ini SEKALI di editor Apps Script (Klik 'Run')
 * Untuk menginisialisasi header tabel di Google Sheets secara otomatis.
 */
function setupDatabase() {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  let sheet = ss.getSheetByName('Produk');
 
  if (!sheet) {
    sheet = ss.insertSheet('Produk');
  }
 
  const headers = ['ID', 'Nama', 'Deskripsi', 'Harga', 'Kategori', 'Foto_ID', 'Foto_URL'];
  const currentHeader = sheet.getRange(1, 1, 1, headers.length).getValues()[0];
 
  // Jika header masih kosong, buat baru
  if (currentHeader[0] === "") {
    sheet.getRange(1, 1, 1, headers.length)
         .setValues([headers])
         .setFontWeight("bold")
         .setBackground("#4F46E5") // Warna Indigo 600
         .setFontColor("#FFFFFF")
         .setHorizontalAlignment("center");
   
    sheet.setFrozenRows(1); // Kunci baris pertama
    return "Database 'Produk' Berhasil Disiapkan!";
  }
  return "Database sudah tersedia, tidak ada perubahan dilakukan.";
}

/**
 * 2. ROUTER UTAMA (MPA)
 * Menangani navigasi antar halaman melalui parameter ?page=
 */
function doGet(e) {
  const page = e.parameter.page || 'Index';
  const rootUrl = ScriptApp.getService().getUrl();
 
  try {
    const template = HtmlService.createTemplateFromFile(page);
    // Sangat penting: Inject rootUrl agar link navigasi bekerja
    template.rootUrl = rootUrl;
   
    return template.evaluate()
      .setTitle('LuxeStore MPA - Apps Script')
      .addMetaTag('viewport', 'width=device-width, initial-scale=1')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
     
  } catch (err) {
    // Penanganan error jika file .html tidak ditemukan
    return HtmlService.createHtmlOutput(`
      <div style="font-family: sans-serif; text-align: center; padding: 50px;">
        <h2 style="color: #ef4444;">Error 404: Halaman Tidak Ditemukan</h2>
        <p>File <b>${page}.html</b> gagal dimuat atau belum dibuat.</p>
        <a href="${rootUrl}?page=Index" target="_top" style="color: #4f46e5;">Kembali ke Beranda</a>
      </div>
    `);
  }
}

/**
 * 3. FUNGSI INCLUDE (SERVER-SIDE)
 * Memungkinkan pemanggilan file HTML lain (seperti Navbar) ke dalam template utama.
 */
function include(filename, rootUrl) {
  try {
    const template = HtmlService.createTemplateFromFile(filename);
    template.rootUrl = rootUrl;
    return template.evaluate().getContent();
  } catch (err) {
    return ``;
  }
}

/**
 * 4. FUNGSI PROCESS FORM (WRITE DATA)
 * Mengupload foto ke Drive dan menyimpan metadata ke Google Sheets.
 */
function processForm(formData) {
  try {
    const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
    const sheet = ss.getSheetByName('Produk');
    const folder = DriveApp.getFolderById(DRIVE_FOLDER_ID);
   
    // Upload File ke Google Drive
    const blob = formData.fotoProduk;
    const file = folder.createFile(blob);
   
    // WAJIB: Atur izin file agar bisa dilihat siapa saja (Public View)
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
   
    const fileId = file.getId();
   
    /**
     * FORMAT URL TERBAIK UNTUK DISPLAY:
     * Menggunakan format 'lh3.googleusercontent.com' agar gambar
     * bisa tampil tanpa hambatan login/CORS di browser.
     */
    const fileUrl = `https://lh3.googleusercontent.com/d/${fileId}`;
   
    // Masukkan data ke baris terakhir sheet
    sheet.appendRow([
      Utilities.getUuid(), // Generate ID Unik
      formData.nama,
      formData.deskripsi || "-",
      formData.harga,
      formData.kategori,
      fileId,
      fileUrl
    ]);
   
    return "Sukses! Produk berhasil diterbitkan.";
  } catch (e) {
    return "Error Server: " + e.toString();
  }
}

/**
 * 5. FUNGSI GET PRODUCTS (READ DATA)
 * Mengambil data dari Google Sheets untuk ditampilkan di Produk.html
 */
function getProducts() {
  try {
    const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('Produk');
    const data = sheet.getDataRange().getValues();
   
    if (data.length <= 1) return []; // Jika hanya ada header, kembalikan array kosong
   
    const headers = data.shift(); // Ambil header dan hapus dari array data utama
   
    return data.map(row => {
      let obj = {};
      headers.forEach((header, i) => {
        obj[header] = row[i];
      });
      return obj;
    });
  } catch (e) {
    console.error("Gagal mengambil data: " + e.toString());
    return [];
  }
}

Index.htm

<!DOCTYPE html>
<html>
  <head><base target="_top"></head>
  <body class="bg-slate-50">
    <?!= include('Navbar', rootUrl); ?>
   
    <header class="py-20 px-6 text-center">
      <h1 class="text-6xl font-black text-slate-900 mb-6">Belanja Pintar di <span class="text-indigo-600">Apps Script</span></h1>
      <p class="text-xl text-slate-600 max-w-2xl mx-auto mb-10">Platform e-commerce ringan, cepat, dan terintegrasi penuh dengan Google Workspace Anda.</p>
      <div class="flex justify-center gap-4">
        <a href="<?= rootUrl ?>?page=Produk" target="_top" class="bg-slate-900 text-white px-8 py-4 rounded-2xl font-bold text-lg hover:scale-105 transition">Mulai Belanja</a>
        <a href="<?= rootUrl ?>?page=About" target="_top" class="bg-white border border-slate-200 text-slate-800 px-8 py-4 rounded-2xl font-bold text-lg hover:bg-slate-50 transition">Tentang Kami</a>
      </div>
    </header>
  </body>
</html>

Form.html

<!DOCTYPE html>
<html>
  <head><base target="_top"></head>
  <body class="bg-slate-100">
    <?!= include('Navbar', rootUrl); ?>

    <div class="max-w-xl mx-auto my-12 bg-white rounded-3xl shadow-2xl overflow-hidden border border-white">
      <div class="bg-indigo-600 p-8 text-white">
        <h2 class="text-2xl font-bold">Panel Input Produk</h2>
        <p class="opacity-80">Data akan langsung tersimpan di Google Sheets.</p>
      </div>
     
      <form id="f" onsubmit="s(this)" class="p-8 space-y-6">
        <div>
          <label class="block text-sm font-bold text-slate-700 mb-2">Nama Barang</label>
          <input type="text" name="nama" required class="w-full p-4 bg-slate-50 border-0 rounded-2xl focus:ring-2 focus:ring-indigo-500 outline-none">
        </div>
        <div class="grid grid-cols-2 gap-4">
          <div>
            <label class="block text-sm font-bold text-slate-700 mb-2">Harga</label>
            <input type="number" name="harga" required class="w-full p-4 bg-slate-50 border-0 rounded-2xl focus:ring-2 focus:ring-indigo-500 outline-none">
          </div>
          <div>
            <label class="block text-sm font-bold text-slate-700 mb-2">Kategori</label>
            <select name="kategori" class="w-full p-4 bg-slate-50 border-0 rounded-2xl focus:ring-2 focus:ring-indigo-500 outline-none">
              <option>Elektronik</option><option>Fashion</option><option>Home</option>
            </select>
          </div>
        </div>
        <div>
          <label class="block text-sm font-bold text-slate-700 mb-2">Upload Foto</label>
          <input type="file" name="fotoProduk" accept="image/*" required class="w-full text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:bg-indigo-50 file:text-indigo-700">
        </div>
        <button id="b" class="w-full bg-indigo-600 text-white font-black py-5 rounded-2xl hover:bg-indigo-700 shadow-lg shadow-indigo-100 transition">UPLOAD PRODUK</button>
      </form>
    </div>

    <script>
      function s(form) {
        event.preventDefault();
        const b = document.getElementById('b');
        b.disabled = true; b.innerText = 'MEMPROSES...';
        google.script.run.withSuccessHandler(r => {
          alert(r);
          window.open("<?= rootUrl ?>?page=Produk", "_top");
        }).processForm(form);
      }
    </script>
  </body>
</html>

Navbar.html

<nav class="bg-white border-b border-gray-100 px-6 py-4 sticky top-0 z-50 shadow-sm">
  <div class="max-w-7xl mx-auto flex justify-between items-center">
    <a href="<?= rootUrl ?>?page=Index" target="_top" class="text-2xl font-black text-indigo-600 tracking-tighter">
      GAS<span class="text-slate-800">SHOP</span>
    </a>
   
    <div class="hidden md:flex space-x-8 font-semibold text-slate-600">
      <a href="<?= rootUrl ?>?page=Index" target="_top" class="hover:text-indigo-600 transition">Beranda</a>
      <a href="<?= rootUrl ?>?page=Produk" target="_top" class="hover:text-indigo-600 transition">Belanja</a>
      <a href="<?= rootUrl ?>?page=Form" target="_top" class="hover:text-indigo-600 transition">Dashboard</a>
    </div>

    <div class="flex items-center gap-4">
      <a href="<?= rootUrl ?>?page=Form" target="_top" class="bg-indigo-600 text-white px-5 py-2.5 rounded-xl font-bold hover:bg-indigo-700 transition shadow-md shadow-indigo-100">
        Tambah Produk
      </a>
    </div>
  </div>
</nav>
<script src="https://cdn.tailwindcss.com"></script>

Produk.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
      .aspect-card { aspect-ratio: 4 / 3; }
    </style>
  </head>
  <body class="bg-gray-50">
    <?!= include('Navbar', rootUrl); ?>

    <div class="max-w-7xl mx-auto px-6 py-12">
      <div class="flex justify-between items-end mb-10">
        <div>
          <h2 class="text-4xl font-black text-slate-900">Katalog Produk</h2>
          <p class="text-slate-500 mt-2">Temukan produk terbaik untuk kebutuhan Anda.</p>
        </div>
      </div>
     
      <div id="productGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
        <div class="col-span-full flex flex-col items-center py-20">
          <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-600 mb-4"></div>
          <p class="text-slate-500 font-medium">Menghubungkan ke database...</p>
        </div>
      </div>
    </div>

    <script>
      window.onload = () => {
        google.script.run.withSuccessHandler(products => {
          const grid = document.getElementById('productGrid');
         
          if (!products || products.length === 0) {
            grid.innerHTML = `
              <div class="col-span-full text-center py-20 bg-white rounded-3xl border-2 border-dashed border-slate-200">
                <p class="text-slate-400">Belum ada produk yang tersedia.</p>
              </div>`;
            return;
          }

          grid.innerHTML = '';
         
          products.forEach(p => {
            grid.innerHTML += `
              <div class="bg-white rounded-3xl overflow-hidden shadow-sm hover:shadow-2xl transition-all duration-300 border border-slate-100 group">
                <div class="aspect-card overflow-hidden bg-slate-200 relative">
                  <img src="${p.Foto_URL}"
                       alt="${p.Nama}"
                       loading="lazy"
                       onerror="this.onerror=null; this.src='https://placehold.co/600x400?text=Gambar+Error';"
                       class="w-full h-full object-cover group-hover:scale-110 transition duration-700">
                 
                  <div class="absolute top-4 left-4">
                    <span class="bg-white/90 backdrop-blur-sm text-indigo-600 text-[10px] font-bold px-3 py-1 rounded-full uppercase tracking-widest shadow-sm">
                      ${p.Kategori}
                    </span>
                  </div>
                </div>
               
                <div class="p-6">
                  <h3 class="text-lg font-bold text-slate-900 leading-tight group-hover:text-indigo-600 transition">${p.Nama}</h3>
                  <p class="text-slate-500 text-xs mt-2 line-clamp-2">${p.Deskripsi}</p>
                 
                  <div class="flex justify-between items-center mt-6">
                    <div>
                      <p class="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">Harga</p>
                      <p class="text-xl font-black text-slate-900">Rp${Number(p.Harga).toLocaleString('id-ID')}</p>
                    </div>
                    <button class="bg-slate-900 text-white p-3 rounded-2xl hover:bg-indigo-600 transition-colors shadow-lg">
                      <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="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path>
                      </svg>
                    </button>
                  </div>
                </div>
              </div>`;
          });
        }).getProducts();
      };
    </script>
  </body>
</html>

Tidak ada komentar:

Posting Komentar

MPA - E-Commerce MPA (Multi-Page Application)

  https://script.google.com/macros/s/AKfycbzv3nr5h0mp_OpZRKzuulC5axkN2NFyBaST7sY7plT4QNcSrvETkfEEsZZrnd74v0Rs/exec?page=Index https://docs.g...