Kamis, 09 April 2026

Web - Upload Coffee

 






PROMPT

Pengembangan Proyek OatBrew

Dokumen ini merangkum seluruh instruksi, fitur, dan arsitektur yang telah diimplementasikan dalam pengembangan sistem Landing Page dan Admin Panel OatBrew menggunakan Google Apps Script.

1. Arsitektur Sistem

Proyek ini menggunakan arsitektur Single Page Application (SPA) yang dijalankan sepenuhnya di atas ekosistem Google:

  • Frontend: HTML5, Tailwind CSS, dan Vanilla JavaScript.

  • Backend: Google Apps Script (.gs).

  • Database: Google Sheets (Metadata) dan Google Drive (Penyimpanan Gambar).

  • Komunikasi: google.script.run (Asynchronous server-side call).

2. Ringkasan Frontend (index.html)

Frontend dirancang dengan pendekatan modern, bersih, dan responsif menggunakan Tailwind CSS.

Fitur Utama:

  • Navigasi SPA: Berpindah antara tampilan Landing Page dan Admin Panel secara instan tanpa memuat ulang halaman.

  • Mode Landing Page:

    • Headline persuasif dengan gaya bahasa witty.

    • Visual produk yang menonjol.

    • Section manfaat dan testimoni pekerja kantoran.

  • Form Admin Panel:

    • Image Preview: Menampilkan pratinjau foto kopi segera setelah dipilih.

    • Base64 Converter: Mengonversi file gambar menjadi string Base64 agar dapat dikirim ke server Apps Script.

    • Feedback UI: Tombol dengan status loading dan pesan sukses setelah data tersimpan.

3. Ringkasan Backend (backend.gs)

Backend berfungsi sebagai jembatan antara interface pengguna dan layanan penyimpanan Google.

Fitur Utama:

  • doGet(): Melayani file HTML ke browser dan mengatur konfigurasi viewport serta judul halaman.

  • setupDatabase(): Fungsi otomatisasi untuk membuat sheet "MenuCoffee" dan mengatur header kolom secara rapi.

  • saveMenuData(data):

    • Menerima objek data dari frontend.

    • Mengonversi Base64 kembali menjadi blob gambar.

    • Menyimpan gambar ke Google Drive berdasarkan FOLDER_ID.

    • Mengatur izin akses gambar menjadi "Anyone with link" agar bisa ditampilkan kembali.

    • Mencatat nama produk, harga, deskripsi, dan URL gambar ke Google Sheets berdasarkan SS_URL.

4. Instruksi Integrasi Penting

Untuk memastikan sistem berjalan dengan sempurna, pastikan poin-poin berikut telah dikonfigurasi:

Komponen

Nilai yang Harus Diisi

Lokasi File

URL Spreadsheet

Link lengkap Google Sheets Anda

backend.gs (SS_URL)

ID Folder Drive

Kode unik folder penyimpanan foto

backend.gs (FOLDER_ID)

Deployment

Deploy sebagai "Web App" dengan akses "Anyone"

Apps Script Settings

5. Kesimpulan Teknis

Pengembangan ini berhasil menggabungkan kebutuhan pemasaran (Landing Page) dengan kebutuhan operasional (Inventory Management) dalam satu aplikasi ringan. Penggunaan google.script.run menghilangkan kendala CORS yang biasanya terjadi pada permintaan API tradisional ke Google Services, menjadikannya solusi internal yang sangat stabil dan hemat biaya.

Saran Pengembangan Selanjutnya:

  1. Menambahkan fitur Display Menu otomatis di Landing Page yang mengambil data langsung dari Spreadsheet.

  2. Menambahkan sistem login sederhana menggunakan google.script.run untuk mengamankan akses ke Admin Panel.

Code.gs

/**
 * KONFIGURASI UTAMA
 * Masukkan ID Folder Drive (untuk simpan foto) dan URL Spreadsheet Anda.
 */
const FOLDER_ID = "1gWEJ-d3PGhkngLIgqDYzLUSFEnCYcZRC";
const SS_URL = "https://docs.google.com/spreadsheets/d/11XMXDbfjOn2gUCS8x74hKnodAe7aEXBNlES5L81ozTQ/edit?usp=sharing";
const SHEET_NAME = "MenuCoffee";

/**
 * Melayani halaman web utama.
 */
function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle('OatBrew Admin & Landing Page')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

/**
 * Fungsi untuk inisialisasi awal Spreadsheet.
 */
function setupDatabase() {
  try {
    const ss = SpreadsheetApp.openByUrl(SS_URL);
    let sheet = ss.getSheetByName(SHEET_NAME);
   
    if (!sheet) {
      sheet = ss.insertSheet(SHEET_NAME);
    }
   
    const headers = ["Timestamp", "Nama Produk", "Harga", "Deskripsi", "URL Foto"];
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
    sheet.getRange(1, 1, 1, headers.length).setFontWeight("bold").setBackground("#f3f3f3");
   
    return "Database berjaya disiapkan!";
  } catch (e) {
    return "Ralat: " + e.toString();
  }
}

/**
 * Fungsi Server-side untuk menyimpan data menu.
 * Dipanggil dari frontend menggunakan google.script.run.
 */
function saveMenuData(data) {
  try {
    // 1. Simpan Foto ke Google Drive
    const folder = DriveApp.getFolderById(FOLDER_ID);
    const contentType = data.mimeType;
    const bytes = Utilities.base64Decode(data.image);
    const blob = Utilities.newBlob(bytes, contentType, data.fileName);
    const file = folder.createFile(blob);
   
    // Atur izin lihat bagi sesiapa yang mempunyai pautan
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
    const fileUrl = file.getUrl();

    // 2. Simpan Data ke Spreadsheet
    const ss = SpreadsheetApp.openByUrl(SS_URL);
    const sheet = ss.getSheetByName(SHEET_NAME);
   
    sheet.appendRow([
      new Date(),
      data.productName,
      data.price,
      data.description,
      fileUrl
    ]);

    return { status: "success", message: "Menu berjaya disimpan ke Spreadsheet!" };

  } catch (error) {
    return { status: "error", message: error.toString() };
  }
}

Index.html

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OatBrew - Segarnya Instan</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700;800&display=swap" rel="stylesheet">
    <style>
        body { font-family: 'Plus+Jakarta+Sans', sans-serif; }
        .coffee-gradient { background: linear-gradient(135deg, #4b3832 0%, #2b1d1a 100%); }
        .oat-bg { background-color: #f7f3f0; }
        .view-content { transition: opacity 0.3s ease-in-out; }
        .hidden-view { display: none; opacity: 0; }
    </style>
</head>
<body class="oat-bg text-stone-800">

    <!-- Navigation -->
    <nav class="fixed w-full z-50 bg-white/80 backdrop-blur-md border-b border-stone-200">
        <div class="max-w-7xl mx-auto px-6 py-4 flex justify-between items-center">
            <div class="text-2xl font-800 tracking-tighter text-stone-900 cursor-pointer" onclick="switchView('landing')">
                OAT<span class="text-amber-700">BREW.</span>
            </div>
            <div class="hidden md:flex space-x-8 font-semibold text-sm uppercase tracking-widest text-stone-600">
                <a href="javascript:void(0)" onclick="switchView('landing')" class="hover:text-amber-700 transition">Laman Utama</a>
                <a href="javascript:void(0)" onclick="switchView('admin')" class="hover:text-amber-700 transition">Upload Menu</a>
            </div>
            <button onclick="switchView('admin')" class="bg-amber-800 text-white px-6 py-2 rounded-full text-sm font-bold hover:bg-amber-900 transition">
                ADMIN PANEL
            </button>
        </div>
    </nav>

    <!-- LANDING VIEW -->
    <main id="landing-view" class="view-content pt-32 pb-20 px-6">
        <div class="max-w-7xl mx-auto grid md:grid-cols-2 gap-12 items-center">
            <div class="space-y-6 text-center md:text-left">
                <h1 class="text-5xl md:text-7xl font-800 text-stone-900">Segarnya Instan, <span class="text-amber-700">'Dosa'-nya Nol.</span></h1>
                <p class="text-lg text-stone-600">Booster kafein berkualiti untuk pekerja sibuk yang mementingkan kesihatan.</p>
                <button onclick="switchView('admin')" class="bg-stone-900 text-white px-8 py-4 rounded-2xl font-bold">Urus Menu Sekarang</button>
            </div>
            <div class="relative flex justify-center">
                <img src="https://images.unsplash.com/photo-1559496417-e7f25cb247f3?q=80&w=600" class="rounded-3xl shadow-2xl w-full max-w-sm" alt="Kopi">
            </div>
        </div>
    </main>

    <!-- ADMIN VIEW (Form Upload) -->
    <main id="admin-view" class="view-content hidden-view pt-32 pb-20 px-6">
        <div class="max-w-2xl mx-auto bg-white rounded-[2rem] shadow-xl p-8 border border-stone-100">
            <h2 class="text-2xl font-800 text-stone-900 mb-8">Tambah Menu Kopi Baru</h2>
            <form id="menuForm" class="space-y-6">
                <div class="space-y-2">
                    <label class="block text-xs font-bold text-stone-500 uppercase">Foto Produk</label>
                    <div class="border-2 border-dashed border-stone-200 rounded-xl p-6 text-center bg-stone-50 relative">
                        <input type="file" id="coffeePhoto" accept="image/*" class="absolute inset-0 opacity-0 cursor-pointer" onchange="handlePreview(this)" required>
                        <div id="placeholder">
                            <p class="text-stone-400">Klik untuk pilih gambar</p>
                        </div>
                        <img id="preview" class="hidden h-32 mx-auto rounded-lg">
                    </div>
                </div>

                <div class="grid md:grid-cols-2 gap-4">
                    <input type="text" id="productName" placeholder="Nama Produk" class="w-full p-4 bg-stone-50 border border-stone-200 rounded-xl" required>
                    <input type="number" id="productPrice" placeholder="Harga (RM/IDR)" class="w-full p-4 bg-stone-50 border border-stone-200 rounded-xl" required>
                </div>

                <textarea id="productDesc" placeholder="Deskripsi ringkas..." class="w-full p-4 bg-stone-50 border border-stone-200 rounded-xl" rows="3" required></textarea>

                <button type="submit" id="submitBtn" class="w-full bg-amber-800 text-white py-4 rounded-xl font-bold hover:bg-amber-900 transition flex justify-center items-center gap-2">
                    <span id="btnText">SIMPAN MENU</span>
                    <div id="loader" class="hidden animate-spin h-5 w-5 border-2 border-white border-t-transparent rounded-full"></div>
                </button>
            </form>
        </div>
    </main>

    <script>
        function switchView(view) {
            document.getElementById('landing-view').classList.toggle('hidden-view', view !== 'landing');
            document.getElementById('admin-view').classList.toggle('hidden-view', view !== 'admin');
        }

        function handlePreview(input) {
            if (input.files && input.files[0]) {
                const reader = new FileReader();
                reader.onload = (e) => {
                    document.getElementById('preview').src = e.target.result;
                    document.getElementById('preview').classList.remove('hidden');
                    document.getElementById('placeholder').classList.add('hidden');
                };
                reader.readAsDataURL(input.files[0]);
            }
        }

        document.getElementById('menuForm').onsubmit = function(e) {
            e.preventDefault();
            const btn = document.getElementById('submitBtn');
            const loader = document.getElementById('loader');
            const btnText = document.getElementById('btnText');

            btn.disabled = true;
            loader.classList.remove('hidden');
            btnText.innerText = "SEDANG DISIMPAN...";

            const file = document.getElementById('coffeePhoto').files[0];
            const reader = new FileReader();
           
            reader.readAsDataURL(file);
            reader.onload = function() {
                const payload = {
                    image: reader.result.split(',')[1],
                    fileName: file.name,
                    mimeType: file.type,
                    productName: document.getElementById('productName').value,
                    price: document.getElementById('productPrice').value,
                    description: document.getElementById('productDesc').value
                };

                // Memanggil fungsi server di backend.gs
                google.script.run
                    .withSuccessHandler((res) => {
                        alert(res.message);
                        document.getElementById('menuForm').reset();
                        document.getElementById('preview').classList.add('hidden');
                        document.getElementById('placeholder').classList.remove('hidden');
                        resetButton();
                    })
                    .withFailureHandler((err) => {
                        alert("Ralat: " + err);
                        resetButton();
                    })
                    .saveMenuData(payload);
            };
        };

        function resetButton() {
            const btn = document.getElementById('submitBtn');
            const loader = document.getElementById('loader');
            const btnText = document.getElementById('btnText');
            btn.disabled = false;
            loader.classList.add('hidden');
            btnText.innerText = "SIMPAN MENU";
        }
    </script>
</body>
</html>


Tidak ada komentar:

Posting Komentar

Web - Upload Coffee

  https://sites.google.com/view/coffee80/upload https://sites.google.com/view/coffee80/halaman-muka https://docs.google.com/spreadsheets/d/1...