Selasa, 16 Desember 2025

Apps Script - Pelatihan 1

 





Buatkan aplikasi web  CRUD lengkap (Create/Read/Update/Delete) untuk data Pelatihan/Workshop yang disimpan di Google Sheet, dengan backend Apps Script (Code.gs) dan frontend (index.html) berbasis landing page kamu (SPA-style tanpa reload) + menu “Data Peserta” (tabel + edit + hapus).

Prinsipnya:

  • Create: submit form ➜ append ke Sheet (dengan ID unik)

  • Read: load list ➜ tampilkan tabel

  • Update: klik “Edit” ➜ form edit (modal) ➜ update row by ID

  • Delete: klik “Hapus” ➜ delete row by ID


Update dari landing page kamu: sekarang ada 3 route:
  • #landing

  • #form (Create)

  • #data (Read + Update + Delete)

Index.html

<!doctype html>
<html lang="id">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Pelatihan / Workshop — Form Publik</title>

  <style>
    /* =========================
       Base / Tokens
    ========================== */
    :root{
      --bg0:#071a3a;
      --bg1:#0b2a6b;
      --ink:#0b1220;
      --white:#ffffff;
      --card:rgba(255,255,255,.92);
      --shadow: 0 22px 55px rgba(0,0,0,.25);
      --shadow-soft: 0 10px 30px rgba(2, 10, 35, .18);
      --radius: 20px;
      --max: 1120px;
      --focus: 0 0 0 3px rgba(14,165,233,.25);
      --btn: linear-gradient(135deg, #38bdf8, #0ea5e9, #2563eb);
    }

    *{ box-sizing:border-box; }
    html,body{ height:100%; }
    body{
      margin:0;
      font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
      color: var(--white);
      background:
        radial-gradient(1200px 700px at 20% 10%, rgba(56,189,248,.45), transparent 60%),
        radial-gradient(900px 600px at 80% 30%, rgba(37,99,235,.45), transparent 62%),
        linear-gradient(135deg, var(--bg0), var(--bg1) 45%, #06214a);
      overflow-x:hidden;
    }

    a{ color:inherit; text-decoration:none; }
    button{ font:inherit; }
    code{ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }

    .container{
      width: min(var(--max), calc(100% - 48px));
      margin: 0 auto;
    }

    /* =========================
       Header (SPA Nav)
    ========================== */
    header{
      position: sticky;
      top: 0;
      z-index: 10;
      backdrop-filter: blur(12px);
      background: rgba(6, 19, 54, .55);
      border-bottom: 1px solid rgba(255,255,255,.10);
    }
    .topbar{
      display:flex;
      align-items:center;
      justify-content:space-between;
      padding: 14px 0;
      gap: 14px;
    }
    .brand{
      display:flex;
      align-items:center;
      gap: 10px;
      font-weight: 750;
      letter-spacing: .2px;
      white-space:nowrap;
    }
    .logo{
      width: 38px; height: 38px;
      border-radius: 12px;
      background: linear-gradient(135deg, rgba(56,189,248,.95), rgba(37,99,235,.95));
      box-shadow: 0 12px 25px rgba(14,165,233,.25);
      display:grid;
      place-items:center;
      border: 1px solid rgba(255,255,255,.18);
    }
    .logo svg{ width:20px; height:20px; }

    nav{
      display:flex;
      align-items:center;
      gap: 10px;
      flex-wrap: wrap;
      justify-content:flex-end;
    }
    .pill{
      padding: 10px 14px;
      border-radius: 999px;
      border: 1px solid rgba(255,255,255,.14);
      background: rgba(255,255,255,.06);
      cursor:pointer;
      transition: transform .12s ease, background .12s ease, border-color .12s ease;
      user-select:none;
    }
    .pill:hover{ transform: translateY(-1px); background: rgba(255,255,255,.09); }
    .pill[aria-current="page"]{
      background: rgba(56,189,248,.16);
      border-color: rgba(56,189,248,.38);
    }
    .cta-mini{
      padding: 10px 14px;
      border-radius: 999px;
      background: rgba(255,255,255,.92);
      color: #0b2a6b;
      border: 1px solid rgba(255,255,255,.35);
      cursor:pointer;
      transition: transform .12s ease;
      font-weight: 800;
    }
    .cta-mini:hover{ transform: translateY(-1px); }

    /* =========================
       Views (SPA)
    ========================== */
    main{ min-height: calc(100dvh - 70px); }
    .view{ display:none; }
    .view.is-active{ display:block; }

    /* =========================
       Hero Section
    ========================== */
    section.hero{ padding: 52px 0 32px; }
    .hero-grid{
      display:grid;
      grid-template-columns: 1.05fr .95fr;
      gap: 34px;
      align-items:center;
      padding: 22px 0 10px;
    }
    .kicker{
      display:inline-flex;
      align-items:center;
      gap: 10px;
      padding: 8px 12px;
      border-radius: 999px;
      border: 1px solid rgba(255,255,255,.18);
      background: rgba(255,255,255,.08);
      color: rgba(255,255,255,.92);
      font-size: 13px;
      letter-spacing:.2px;
    }
    .dot{
      width: 8px; height: 8px;
      border-radius: 999px;
      background: #38bdf8;
      box-shadow: 0 0 0 4px rgba(56,189,248,.18);
    }
    h1{
      margin: 16px 0 12px;
      font-size: clamp(32px, 4vw, 52px);
      line-height: 1.06;
      letter-spacing: -0.6px;
    }
    .lead{
      margin: 0 0 18px;
      font-size: 16px;
      line-height: 1.7;
      color: rgba(255,255,255,.86);
      max-width: 60ch;
    }

    .hero-actions{
      display:flex;
      gap: 12px;
      flex-wrap: wrap;
      margin-top: 10px;
    }
    .btn{
      border: 0;
      padding: 14px 16px;
      border-radius: 14px;
      cursor:pointer;
      font-weight: 900;
      letter-spacing: .2px;
      transition: transform .12s ease, box-shadow .12s ease, opacity .12s ease;
      display:inline-flex;
      align-items:center;
      gap: 10px;
      user-select:none;
    }
    .btn-primary{
      color: var(--white);
      background: var(--btn);
      box-shadow: 0 16px 28px rgba(14,165,233,.25);
      border: 1px solid rgba(255,255,255,.18);
    }
    .btn-primary:hover{ transform: translateY(-1px); box-shadow: 0 18px 32px rgba(14,165,233,.30); }
    .btn-ghost{
      color: rgba(255,255,255,.92);
      background: rgba(255,255,255,.08);
      border: 1px solid rgba(255,255,255,.18);
    }
    .btn-ghost:hover{ transform: translateY(-1px); }

    .meta{
      margin-top: 18px;
      display:flex;
      gap: 18px;
      flex-wrap: wrap;
      color: rgba(255,255,255,.82);
      font-size: 13px;
    }
    .meta b{ color:#fff; }

    /* Illustration card */
    .illus-card{
      border-radius: var(--radius);
      background: linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.06));
      border: 1px solid rgba(255,255,255,.16);
      box-shadow: var(--shadow-soft);
      overflow:hidden;
      position:relative;
      min-height: 380px;
    }
    .illus-top{
      padding: 16px 18px;
      display:flex;
      align-items:center;
      justify-content:space-between;
      border-bottom: 1px solid rgba(255,255,255,.12);
      background: rgba(0,0,0,.08);
    }
    .window-dots{ display:flex; gap:8px; align-items:center; }
    .window-dots span{
      width:10px; height:10px; border-radius:999px;
      background: rgba(255,255,255,.35);
    }
    .illus-body{
      padding: 18px;
      display:grid;
      place-items:center;
      height: calc(100% - 54px);
    }

    /* =========================
       Features
    ========================== */
    .features{
      margin-top: 18px;
      display:grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 14px;
      padding-bottom: 14px;
    }
    .feat{
      border-radius: 18px;
      padding: 14px 14px;
      border: 1px solid rgba(255,255,255,.14);
      background: rgba(255,255,255,.06);
      box-shadow: 0 12px 22px rgba(2,10,35,.12);
    }
    .feat h3{ margin: 8px 0 6px; font-size: 14px; letter-spacing:.1px; }
    .feat p{ margin:0; font-size: 13px; line-height:1.55; color: rgba(255,255,255,.82); }
    .ico{
      width: 34px; height: 34px;
      border-radius: 12px;
      display:grid;
      place-items:center;
      background: rgba(56,189,248,.18);
      border: 1px solid rgba(56,189,248,.28);
    }
    .ico svg{ width:18px; height:18px; }

    /* =========================
       Form View
    ========================== */
    section.form-view{ padding: 42px 0 64px; }
    .form-header{
      display:flex;
      align-items:flex-end;
      justify-content:space-between;
      gap: 16px;
      margin-bottom: 16px;
    }
    .form-header h2{
      margin:0;
      font-size: clamp(22px, 2.6vw, 30px);
      letter-spacing: -0.3px;
    }
    .form-header p{
      margin: 6px 0 0;
      color: rgba(255,255,255,.82);
      max-width: 70ch;
      line-height: 1.6;
      font-size: 14px;
    }

    .split{
      display:grid;
      grid-template-columns: .9fr 1.1fr;
      gap: 16px;
      align-items:start;
    }
    .panel{
      border-radius: var(--radius);
      background: rgba(255,255,255,.06);
      border: 1px solid rgba(255,255,255,.14);
      box-shadow: var(--shadow-soft);
      overflow:hidden;
    }
    .panel .head{
      padding: 14px 16px;
      border-bottom: 1px solid rgba(255,255,255,.12);
      background: rgba(0,0,0,.08);
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap: 10px;
    }
    .badge{
      font-size: 12px;
      padding: 6px 10px;
      border-radius: 999px;
      border: 1px solid rgba(56,189,248,.30);
      background: rgba(56,189,248,.12);
      color: rgba(255,255,255,.92);
      white-space:nowrap;
    }
    .panel .body{ padding: 16px; }

    /* Form card (dipisahkan secara visual) */
    form{
      background: var(--card);
      color: var(--ink);
      border-radius: var(--radius);
      padding: 18px;
      box-shadow: var(--shadow);
      border: 1px solid rgba(255,255,255,.35);
    }
    .grid{ display:grid; grid-template-columns: 1fr 1fr; gap: 12px; }
    .field{ display:flex; flex-direction:column; gap: 8px; margin-bottom: 12px; }
    label{ font-size: 13px; color: #111827; font-weight: 800; }
    input, select, textarea{
      border: 1px solid rgba(15, 23, 42, .18);
      border-radius: 12px;
      padding: 12px 12px;
      outline:none;
      font-size: 14px;
      background: rgba(255,255,255,.92);
      transition: box-shadow .12s ease, border-color .12s ease;
    }
    input:focus, select:focus, textarea:focus{ box-shadow: var(--focus); border-color: rgba(14,165,233,.65); }
    textarea{ resize: vertical; min-height: 92px; }

    .form-actions{
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap: 12px;
      flex-wrap: wrap;
      margin-top: 10px;
    }
    .btn-submit{
      background: linear-gradient(135deg, #0ea5e9, #2563eb);
      color: white;
      border: 0;
      padding: 12px 14px;
      border-radius: 12px;
      cursor:pointer;
      font-weight: 900;
      box-shadow: 0 14px 22px rgba(37,99,235,.22);
      transition: transform .12s ease, opacity .12s ease;
      display:inline-flex;
      align-items:center;
      gap: 10px;
    }
    .btn-submit:hover{ transform: translateY(-1px); }
    .btn-submit:disabled{ opacity:.65; cursor:not-allowed; transform:none; }

    .spinner{
      width: 14px; height: 14px;
      border-radius: 999px;
      border: 2px solid rgba(255,255,255,.55);
      border-top-color: rgba(255,255,255,1);
      animation: spin .8s linear infinite;
      display:none;
    }
    .btn-submit.is-loading .spinner{ display:inline-block; }
    @keyframes spin{ to{ transform: rotate(360deg); } }

    .hint{
      color: rgba(17,24,39,.70);
      font-size: 12px;
      line-height: 1.5;
      max-width: 62ch;
    }

    .status{
      margin-top: 12px;
      padding: 12px 12px;
      border-radius: 14px;
      border: 1px dashed rgba(255,255,255,.22);
      background: rgba(255,255,255,.06);
      color: rgba(255,255,255,.92);
      display:none;
      line-height: 1.55;
      word-break: break-word;
    }
    .status.is-show{ display:block; }
    .status.ok{ border-color: rgba(34,197,94,.35); background: rgba(34,197,94,.12); }
    .status.err{ border-color: rgba(239,68,68,.40); background: rgba(239,68,68,.12); }

    footer{ padding: 18px 0 30px; color: rgba(255,255,255,.75); font-size: 13px; }

    /* =========================
       Responsive
    ========================== */
    @media (max-width: 980px){
      .hero-grid{ grid-template-columns: 1fr; }
      .illus-card{ min-height: 340px; }
      .features{ grid-template-columns: 1fr; }
      .split{ grid-template-columns: 1fr; }
      .grid{ grid-template-columns: 1fr; }
    }
  </style>
</head>

<body>
  <header>
    <div class="container">
      <div class="topbar">
        <div class="brand" role="banner" aria-label="Brand Pelatihan">
          <div class="logo" aria-hidden="true">
            <svg viewBox="0 0 24 24" fill="none">
              <path d="M7 17V9.8c0-.5.3-1 .8-1.2l3.5-1.6c.4-.2.9-.2 1.3 0l3.5 1.6c.5.2.8.7.8 1.2V17" stroke="white" stroke-width="2" stroke-linecap="round"/>
              <path d="M6 17h12" stroke="white" stroke-width="2" stroke-linecap="round"/>
            </svg>
          </div>
          <span>Pelatihan / Workshop</span>
        </div>

        <nav aria-label="Navigasi">
          <button class="pill" data-route="landing" aria-current="page" type="button">Landing</button>
          <button class="pill" data-route="form" type="button">Form Input</button>
          <button class="cta-mini" data-route="form" type="button">Mulai Isi Data</button>
        </nav>
      </div>
    </div>
  </header>

  <main>
    <!-- =========================
         VIEW: Landing
    ========================== -->
    <div id="view-landing" class="view is-active" role="region" aria-label="Halaman Landing">
      <section class="hero">
        <div class="container">
          <div class="hero-grid">
            <div>
              <span class="kicker"><span class="dot" aria-hidden="true"></span> Form publik GAS • Simpan ke Google Sheet otomatis</span>

              <h1>Landing Page Pelatihan yang Modern, Cepat, dan Profesional</h1>
              <p class="lead">
                Full screen gradient biru modern, UI clean, navigasi tanpa reload (SPA-style sederhana),
                dan form input yang dipisahkan secara visual agar pengalaman pengguna lebih rapi.
              </p>

              <div class="hero-actions">
                <button class="btn btn-primary" data-route="form" type="button" aria-label="Mulai Isi Data">
                  Mulai Isi Data
                  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                    <path d="M5 12h12" stroke="white" stroke-width="2" stroke-linecap="round"/>
                    <path d="M13 6l6 6-6 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                  </svg>
                </button>
                <button class="btn btn-ghost" data-route="form" type="button">Lihat Form</button>
              </div>

              <div class="meta" aria-label="Keunggulan cepat">
                <span><b>Tanpa reload</b> antar halaman</span>
                <span><b>Submit cepat</b> via google.script.run</span>
                <span><b>Data aman</b> masuk Google Sheet</span>
              </div>
            </div>

            <div class="illus-card" aria-label="Ilustrasi pelatihan">
              <div class="illus-top">
                <div class="window-dots" aria-hidden="true">
                  <span></span><span></span><span></span>
                </div>
                <span style="font-size:13px;color:rgba(255,255,255,.85);">Sistem Informasi Pelatihan</span>
              </div>
              <div class="illus-body">
                <!-- Ilustrasi SVG (inline) -->
                <svg viewBox="0 0 720 420" width="100%" height="100%" role="img" aria-label="Ilustrasi sistem pelatihan">
                  <defs>
                    <linearGradient id="g1" x1="0" y1="0" x2="1" y2="1">
                      <stop offset="0" stop-color="#38bdf8" stop-opacity=".95"/>
                      <stop offset="1" stop-color="#2563eb" stop-opacity=".95"/>
                    </linearGradient>
                    <linearGradient id="g2" x1="0" y1="1" x2="1" y2="0">
                      <stop offset="0" stop-color="#0ea5e9" stop-opacity=".35"/>
                      <stop offset="1" stop-color="#93c5fd" stop-opacity=".15"/>
                    </linearGradient>
                    <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
                      <feDropShadow dx="0" dy="18" stdDeviation="18" flood-color="#000" flood-opacity=".25"/>
                    </filter>
                  </defs>

                  <path d="M90 320c110 110 300 120 390 40 120-105 65-240-80-265-150-25-250 65-310 225z" fill="url(#g2)"/>
                  <circle cx="590" cy="110" r="70" fill="url(#g2)"/>

                  <g filter="url(#shadow)">
                    <rect x="110" y="70" rx="22" ry="22" width="500" height="280" fill="rgba(255,255,255,.10)" stroke="rgba(255,255,255,.18)"/>
                    <rect x="140" y="105" rx="16" ry="16" width="200" height="210" fill="rgba(255,255,255,.08)" stroke="rgba(255,255,255,.16)"/>
                    <rect x="360" y="105" rx="16" ry="16" width="220" height="120" fill="rgba(255,255,255,.08)" stroke="rgba(255,255,255,.16)"/>
                    <rect x="360" y="245" rx="16" ry="16" width="220" height="70" fill="rgba(255,255,255,.08)" stroke="rgba(255,255,255,.16)"/>
                  </g>

                  <path d="M388 198 L420 166 L452 184 L484 140 L516 160 L548 132" fill="none" stroke="url(#g1)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
                  <circle cx="420" cy="166" r="6" fill="#38bdf8"/>
                  <circle cx="484" cy="140" r="6" fill="#2563eb"/>

                  <g>
                    <rect x="160" y="130" width="160" height="16" rx="8" fill="rgba(255,255,255,.22)"/>
                    <rect x="160" y="160" width="130" height="14" rx="7" fill="rgba(255,255,255,.18)"/>
                    <rect x="160" y="190" width="150" height="14" rx="7" fill="rgba(255,255,255,.18)"/>
                    <rect x="160" y="220" width="120" height="14" rx="7" fill="rgba(255,255,255,.18)"/>
                    <rect x="160" y="250" width="150" height="14" rx="7" fill="rgba(255,255,255,.18)"/>
                  </g>

                  <g>
                    <rect x="390" y="262" width="170" height="46" rx="14" fill="url(#g1)"/>
                    <text x="475" y="291" text-anchor="middle" font-family="ui-sans-serif, system-ui" font-size="14" font-weight="900" fill="white">
                      Mulai Isi Data
                    </text>
                  </g>
                </svg>
              </div>
            </div>
          </div>

          <div class="features" aria-label="Fitur utama">
            <div class="feat">
              <div class="ico" aria-hidden="true">
                <svg viewBox="0 0 24 24" fill="none">
                  <path d="M4 12h16" stroke="white" stroke-width="2" stroke-linecap="round"/>
                  <path d="M12 4v16" stroke="white" stroke-width="2" stroke-linecap="round"/>
                </svg>
              </div>
              <h3>Struktur rapi</h3>
              <p>HTML semantic, CSS terstruktur, dan layout berbasis card modern.</p>
            </div>

            <div class="feat">
              <div class="ico" aria-hidden="true">
                <svg viewBox="0 0 24 24" fill="none">
                  <path d="M7 12l3 3 7-7" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
                </svg>
              </div>
              <h3>SPA-style sederhana</h3>
              <p>Navigasi “Landing → Form” pakai hash routing, tanpa reload.</p>
            </div>

            <div class="feat">
              <div class="ico" aria-hidden="true">
                <svg viewBox="0 0 24 24" fill="none">
                  <path d="M12 2l7 4v7c0 5-3 9-7 9s-7-4-7-9V6l7-4z" stroke="white" stroke-width="2" stroke-linejoin="round"/>
                </svg>
              </div>
              <h3>Simpan ke Sheet</h3>
              <p>Submit langsung memanggil fungsi Apps Script untuk menyimpan data.</p>
            </div>
          </div>
        </div>
      </section>

      <footer>
        <div class="container">
          © <span id="year"></span> Pelatihan / Workshop — GAS Web App
        </div>
      </footer>
    </div>

    <!-- =========================
         VIEW: Form
    ========================== -->
    <div id="view-form" class="view" role="region" aria-label="Halaman Form Input">
      <section class="form-view">
        <div class="container">
          <div class="form-header">
            <div>
              <h2>Form Pendaftaran Pelatihan / Workshop</h2>
              <p>Isi data berikut. Setelah submit, data akan otomatis tersimpan pada Google Sheet.</p>
            </div>
            <button class="pill" data-route="landing" type="button">← Kembali</button>
          </div>

          <div class="split">
            <!-- Panel kiri -->
            <div class="panel">
              <div class="head">
                <strong>Informasi</strong>
                <span class="badge">Public Form</span>
              </div>
              <div class="body">
                <ul style="margin:0; padding-left:18px; color:rgba(255,255,255,.86); line-height:1.85;">
                  <li>Pastikan email aktif.</li>
                  <li>Field wajib: Nama, Email, Kelas.</li>
                  <li>Data masuk ke tab <b>DataPelatihan</b> di Sheet.</li>
                </ul>

                <div class="status" id="statusBox" aria-live="polite"></div>
              </div>
            </div>

            <!-- Form card -->
            <div>
              <form id="trainingForm" novalidate>
                <div class="grid">
                  <div class="field">
                    <label for="nama">Nama Lengkap</label>
                    <input id="nama" name="nama" type="text" placeholder="Contoh: Andi Pratama" required />
                  </div>
                  <div class="field">
                    <label for="email">Email</label>
                    <input id="email" name="email" type="email" placeholder="contoh@email.com" required />
                  </div>
                </div>

                <div class="grid">
                  <div class="field">
                    <label for="instansi">Instansi</label>
                    <input id="instansi" name="instansi" type="text" placeholder="Sekolah / Kampus / Perusahaan" />
                  </div>
                  <div class="field">
                    <label for="kelas">Pilihan Kelas</label>
                    <select id="kelas" name="kelas" required>
                      <option value="" selected disabled>Pilih kelas…</option>
                      <option>GAS Dasar</option>
                      <option>GAS + Google Sheets</option>
                      <option>Web App GAS (Form Publik)</option>
                    </select>
                  </div>
                </div>

                <div class="field">
                  <label for="catatan">Catatan (opsional)</label>
                  <textarea id="catatan" name="catatan" placeholder="Tulis kebutuhan / tujuan ikut pelatihan…"></textarea>
                </div>

                <div class="form-actions">
                  <div class="hint">
                    Submit memakai <code>google.script.run</code> (tanpa reload, tanpa CORS).
                    Jika error, cek permission Web App & Spreadsheet ID di <code>Code.gs</code>.
                  </div>

                  <button class="btn-submit" id="btnSubmit" type="submit">
                    <span class="spinner" aria-hidden="true"></span>
                    <span id="btnText">Kirim Data</span>
                  </button>
                </div>
              </form>
            </div>
          </div>
        </div>
      </section>

      <footer>
        <div class="container">
          Deploy sebagai Web App: Execute as <b>Me</b>, Access <b>Anyone</b> (untuk form publik).
        </div>
      </footer>
    </div>
  </main>

  <script>
    /**
     * =========================
     * SPA-style routing sederhana (hash-based)
     * =========================
     */
    const routes = ["landing", "form"];

    function setActiveRoute(route){
      if (!routes.includes(route)) route = "landing";

      document.querySelectorAll(".view").forEach(v => v.classList.remove("is-active"));
      document.getElementById("view-" + route).classList.add("is-active");

      document.querySelectorAll("[data-route]").forEach(btn => {
        if (!btn.classList.contains("pill")) return;
        const isCurrent = btn.getAttribute("data-route") === route;
        btn.setAttribute("aria-current", isCurrent ? "page" : "false");
      });

      if (location.hash !== "#" + route) history.replaceState(null, "", "#" + route);
      window.scrollTo({ top: 0, behavior: "smooth" });
    }

    document.addEventListener("click", (e) => {
      const t = e.target.closest("[data-route]");
      if (!t) return;
      e.preventDefault();
      setActiveRoute(t.getAttribute("data-route"));
    });

    function syncFromHash(){
      const hash = (location.hash || "#landing").replace("#", "");
      setActiveRoute(hash);
    }
    window.addEventListener("hashchange", syncFromHash);

    document.getElementById("year").textContent = new Date().getFullYear();
    syncFromHash();

    /**
     * =========================
     * Form submit -> Apps Script backend (saveTrainingData)
     * =========================
     */
    const form = document.getElementById("trainingForm");
    const statusBox = document.getElementById("statusBox");
    const btnSubmit = document.getElementById("btnSubmit");
    const btnText = document.getElementById("btnText");

    function showStatus(message, type = "ok") {
      statusBox.classList.remove("ok", "err", "is-show");
      statusBox.classList.add("is-show", type === "ok" ? "ok" : "err");
      statusBox.textContent = message;
    }

    function setLoading(isLoading){
      btnSubmit.disabled = isLoading;
      btnSubmit.classList.toggle("is-loading", isLoading);
      btnText.textContent = isLoading ? "Mengirim..." : "Kirim Data";
    }

    form.addEventListener("submit", (e) => {
      e.preventDefault();

      if (!form.checkValidity()) {
        showStatus("Mohon lengkapi field wajib (Nama, Email, dan Kelas).", "err");
        return;
      }

      const payload = Object.fromEntries(new FormData(form).entries());

      setLoading(true);
      showStatus("⏳ Mengirim data ke server...", "ok");

      google.script.run
        .withSuccessHandler((res) => {
          showStatus("✅ " + (res?.message || "Berhasil disimpan ke Google Sheet."), "ok");
          form.reset();
          setLoading(false);
        })
        .withFailureHandler((err) => {
          const msg = (err && err.message) ? err.message : String(err);
          showStatus("❌ Gagal: " + msg, "err");
          setLoading(false);
        })
        .saveTrainingData(payload);
    });
  </script>
</body>
</html>


Code.gs

/** =========================
 * CONFIG
 * ========================= */
const SPREADSHEET_ID = "1XAPm4bQ8jGgKd5t5VExtDskdx0E_CfXo5H1_MYyo6_Q"; // contoh: "1AbC...xyz"
const SHEET_NAME = "DataPelatihan";

/** =========================
 * Web App Entry (Front-end)
 * ========================= */
function doGet(e) {
  return HtmlService
    .createHtmlOutputFromFile("index")
    .setTitle("Form Pelatihan / Workshop")
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

/** =========================
 * Save data (dipanggil dari index.html via google.script.run)
 * ========================= */
function saveTrainingData(payload) {
  // payload: { nama, email, instansi, kelas, catatan }
  if (!payload) throw new Error("Payload kosong.");

  // Validasi wajib
  const nama = (payload.nama || "").trim();
  const email = (payload.email || "").trim();
  const kelas = (payload.kelas || "").trim();

  if (!nama) throw new Error("Nama wajib diisi.");
  if (!email) throw new Error("Email wajib diisi.");
  if (!kelas) throw new Error("Kelas wajib diisi.");

  const ss = getSpreadsheet_();
  const sheet = getOrCreateSheet_(ss, SHEET_NAME);

  // Pastikan header ada
  ensureHeader_(sheet);

  // Append row
  const now = new Date();
  sheet.appendRow([
    now,                 // Timestamp
    nama,
    email,
    (payload.instansi || "").trim(),
    kelas,
    (payload.catatan || "").trim(),
    Session.getActiveUser().getEmail() || "", // kosong untuk user publik (normal)
  ]);

  return {
    ok: true,
    message: "Berhasil disimpan ke Google Sheet.",
    timestamp: now.toISOString()
  };
}

/** =========================
 * Helpers
 * ========================= */
function getSpreadsheet_() {
  if (SPREADSHEET_ID && SPREADSHEET_ID !== "PASTE_SPREADSHEET_ID_DI_SINI") {
    return SpreadsheetApp.openById(SPREADSHEET_ID);
  }
  // Jika project Apps Script kamu "bound" ke spreadsheet, ini bisa dipakai:
  return SpreadsheetApp.getActiveSpreadsheet();
}

function getOrCreateSheet_(ss, name) {
  let sh = ss.getSheetByName(name);
  if (!sh) sh = ss.insertSheet(name);
  return sh;
}

function ensureHeader_(sheet) {
  const firstRow = sheet.getRange(1, 1, 1, 6).getValues()[0];
  const isEmpty = firstRow.join("").trim() === "";
  if (isEmpty) {
    sheet.getRange(1, 1, 1, 7).setValues([[
      "Timestamp",
      "Nama",
      "Email",
      "Instansi",
      "Kelas",
      "Catatan",
      "SubmittedBy"
    ]]);
    sheet.setFrozenRows(1);
    sheet.autoResizeColumns(1, 7);
  }
}







Tidak ada komentar:

Posting Komentar

Web dengan Firebase

  https://www.youtube.com/watch?v=_KgCFwWTORI&list=PLJTyZKho7eUhpYMoKmYq9aeU20dRiWVes https://www.youtube.com/watch?v=2CPE5yKzMqE