Aplikasi Form Pendaftaran Siswa dengan Astro 5 + Google Sheets
Berikut aplikasi lengkap untuk form pendaftaran siswa yang terhubung langsung ke Google Sheets. Aplikasi ini 100% gratis dengan Vercel Hobby Plan
Aplikasi PPDB ini direncanakan untuk skala 200-500 calon siswa. Karena kebutuhan teknisnya sangat kecil, sehingga sangat memungkinkan untuk menjalankan seluruh sistem ini dengan biaya Rp. 0 (selamanya) menggunakan free tier dari berbagai layanan.
Berikut ini adalah beberapa skenario terbaik untuk mencapai tujuan GRATIS SELAMANYA namun dengan performa skala profesional:
1. Strategi Stack “Zero Cost”
| Komponen | Layanan | Alasan |
|---|---|---|
| Framework | Next.js atau Astro | Keduanya gratis di-host di Vercel. Astro lebih ringan jika formnya sederhana. |
| Hosting | Vercel Hobby Plan | Gratis untuk penggunaan non-komersial/skala kecil. |
| Penyimpanan | Google Sheets API atau Supabase | Google Sheets paling cocok agar admin sekolah bisa langsung melihat data tanpa dashboard tambahan. |
| Validasi | Zod | Memastikan data yang masuk tidak berantakan. |
2. Opsi Penyimpanan Terbaik untuk Sekolah
Untuk sekolah swasta, saya sangat menyarankan menggunakan Google Sheets sebagai database daripada database SQL murni. Mengapa?
- Aksesibilitas: Staf TU atau Guru bisa langsung membuka data pendaftar di Google Drive tanpa perlu akses ke database teknis.
- Tanpa Biaya: Tidak ada batasan baris (hingga jutaan sel), jauh melampaui kebutuhan 200 siswa.
- Mudah Diolah: Bisa langsung dicetak atau dibuatkan grafik laporan pendaftaran secara otomatis. Cara kerjanya:
- Buat Form di Astro.
- Gunakan Server Action untuk mengirim data.
- Gunakan library
google-auth-librarydangoogle-apisuntuk memasukkan data ke baris baru di Sheet.
3. Simulasi Keamanan & Limit (Vercel Hobby)
Dengan 200-500 pendaftar, kamu tidak akan menyentuh limit Vercel.
Berikut rinciannya:
- Serverless Execution: Vercel memberikan 100 GB-hours/bulan. Mengirim 200 form hanya akan memakan kurang dari 0,1% dari kuota ini.
- Bandwidth: Kamu dapat 100GB/bulan. Untuk form registrasi teks, ini sangat melimpah.
- Spam Protection: Karena ini gratis, kamu wajib memasang Honeypot field (input tersembunyi yang jika diisi oleh bot, form akan menolak) untuk menghindari spam yang menghabiskan kuota.
4. Estimasi Biaya Tahunan
| Item | Biaya | Keterangan |
|---|---|---|
| Hosting & Serverless | Rp 0 | Vercel Hobby Plan. |
| Database | Rp 0 | Google Sheets atau Supabase (Free Tier). |
| SSL (HTTPS) | Rp 0 | Sudah otomatis disediakan Vercel. |
| Domain | Rp 0 | Menggunakan nama-sekolah.vercel.app. |
| Total | Rp 0 | Sepenuhnya Gratis. |
Catatan: Jika ingin menggunakan domain profesional seperti .sch.id, biayanya hanya sekitar Rp 50.000 - Rp 75.000 per tahun. Ini adalah satu-satunya biaya opsional yang mungkin kamu keluarkan.
Eksekusi Skenario Nol Rupiah
Setelah mempertimbangkan pilihan di atas. Maka kita akan mencoba yang paling mungkin dan mudah untuk diaplikasikan secara mandiri sebagai aplikasi portal pendaftaran on-line: Astro 5:+ Google Sheet
Berikut aplikasi lengkap dengan framework Astro 5 untuk form pendaftaran siswa yang terhubung langsung ke Google Sheets. Aplikasi ini 100% gratis dengan Vercel Hobby Plan.
1. Struktur Proyek
form-ppdb/├── astro.config.mjs├── tailwind.config.mjs├── package.json├── .env.example├── public/├── src/│ ├── components/│ │ └── Form.astro│ ├── layouts/│ │ └── Layout.astro│ ├── pages/│ │ └── index.astro│ └── actions/│ └── submitRegistration.js└── README.md2. Setup Proyek
Buat proyek Astro baru:
npm create astro@latest form-ppdb -- --template minimalcd form-ppdbnpm install3. Install Dependensi
npm install tailwindcss @astrojs/tailwind zodnpx tailwindcss init4. Konfigurasi Astro
Buat file baru astro.config.mjs atau timpa isinya jika sudah ada:
import { defineConfig } from 'astro/config';import tailwind from '@astrojs/tailwind';
export default defineConfig({ integrations: [tailwind()], output: 'server', adapter: vercel()});Untuk menggunakan Vercel adapter, jalankan perintah di terminal:
npm install @astrojs/vercel5. Setup Google Sheets API
5.1 Buat Project di Google Cloud Console
Ikuti langkah berikut:
- Buka Google Cloud Console
- Buat project baru atau pilih yang sudah ada
- Aktifkan Google Sheets API dan Google Drive API
- Di “Credentials”, buat Service Account
- Download file JSON credentials
- Copy email Service Account (format:
xxx@xxx.iam.gserviceaccount.com)
5.2 Setup Google Sheet
- Buat Google Sheet baru di sheets.google.com
- Share sheet dengan email Service Account, beri akses Editor
- Copy ID Sheet dari URL:
https://docs.google.com/spreadsheets/d/ID_SHEET_DISINI/edit
6. File Konfigurasi Environment
Buat file .env.example di root project:
GOOGLE_SERVICE_ACCOUNT_EMAIL=your-service-account@project.iam.gserviceaccount.comGOOGLE_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY\n-----END PRIVATE KEY-----\nGOOGLE_SHEET_ID=your_google_sheet_id_hereCatatan:
- Untuk GOOGLE_PRIVATE_KEY, ganti
\ndengan baris baru yang sebenarnya di.env - Atau gunakan
base64encoding
7. Server Action untuk Google Sheets
Buat file src/actions/submitRegistration.js:
import { google } from 'googleapis';import { z } from 'zod';
// Schema validasi dengan Zodconst registrationSchema = z.object({ nama: z.string().min(3, "Nama minimal 3 karakter"), nisn: z.string().length(10, "NISN harus 10 digit"), tempat_lahir: z.string().min(2, "Tempat lahir wajib diisi"), tanggal_lahir: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Format tanggal: YYYY-MM-DD"), jenis_kelamin: z.enum(['L', 'P']), agama: z.string().min(1, "Agama wajib diisi"), alamat: z.string().min(10, "Alamat minimal 10 karakter"), nama_ayah: z.string().min(3, "Nama ayah minimal 3 karakter"), pekerjaan_ayah: z.string().min(2, "Pekerjaan ayah wajib diisi"), nama_ibu: z.string().min(3, "Nama ibu minimal 3 karakter"), pekerjaan_ibu: z.string().min(2, "Pekerjaan ibu wajib diisi"), no_hp: z.string().regex(/^08[0-9]{9,11}$/, "Format HP: 08xxxxxxxxxx"), email: z.string().email("Email tidak valid"), asal_sekolah: z.string().min(2, "Asal sekolah wajib diisi"), // Honeypot field untuk spam protection website: z.string().max(0, "Spam detected").optional()});
// Inisialisasi Google Sheets APIconst auth = new google.auth.GoogleAuth({ credentials: { client_email: import.meta.env.GOOGLE_SERVICE_ACCOUNT_EMAIL, private_key: import.meta.env.GOOGLE_PRIVATE_KEY.replace(/\\n/g, '\n'), }, scopes: ['https://www.googleapis.com/auth/spreadsheets'],});
const sheets = google.sheets({ version: 'v4', auth });
export async function POST({ request }) { try { const formData = await request.formData(); const data = Object.fromEntries(formData);
// Validasi data const validatedData = registrationSchema.parse(data);
// Cek honeypot field if (data.website && data.website.length > 0) { return new Response(JSON.stringify({ success: false, message: "Spam detected" }), { status: 400 }); }
// Cek duplikasi NISN const checkResponse = await sheets.spreadsheets.values.get({ spreadsheetId: import.meta.env.GOOGLE_SHEET_ID, range: 'Sheet1!B:B', // Kolom B untuk NISN });
const existingNISNs = checkResponse.data.values?.flat() || []; if (existingNISNs.includes(validatedData.nisn)) { return new Response(JSON.stringify({ success: false, message: "NISN sudah terdaftar" }), { status: 400 }); }
// Format data untuk Google Sheets const timestamp = new Date().toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' });
const rowData = [ timestamp, validatedData.nisn, validatedData.nama, validatedData.tempat_lahir, validatedData.tanggal_lahir, validatedData.jenis_kelamin === 'L' ? 'Laki-laki' : 'Perempuan', validatedData.agama, validatedData.alamat, validatedData.nama_ayah, validatedData.pekerjaan_ayah, validatedData.nama_ibu, validatedData.pekerjaan_ibu, validatedData.no_hp, validatedData.email, validatedData.asal_sekolah ];
// Append ke Google Sheets await sheets.spreadsheets.values.append({ spreadsheetId: import.meta.env.GOOGLE_SHEET_ID, range: 'Sheet1!A:O', valueInputOption: 'USER_ENTERED', insertDataOption: 'INSERT_ROWS', requestBody: { values: [rowData] } });
return new Response(JSON.stringify({ success: true, message: "Pendaftaran berhasil! Data telah tersimpan." }), { status: 200, headers: { 'Content-Type': 'application/json' } });
} catch (error) { console.error('Error:', error);
if (error instanceof z.ZodError) { return new Response(JSON.stringify({ success: false, message: "Validasi gagal", errors: error.errors.map(err => ({ field: err.path[0], message: err.message })) }), { status: 400 }); }
return new Response(JSON.stringify({ success: false, message: "Terjadi kesalahan server. Silakan coba lagi." }), { status: 500 }); }}8. Komponen Form
Buat file src/components/Form.astro:
---import { useState } from 'react';const clientSide = Astro.clientAddress();---
<div id="form-container"> <form id="registrationForm" class="space-y-6" onSubmit={async (e) => { e.preventDefault(); const form = e.target; const formData = new FormData(form); const submitBtn = form.querySelector('button[type="submit"]'); const messageDiv = document.getElementById('formMessage');
// Disable button submitBtn.disabled = true; submitBtn.innerHTML = 'Mengirim...'; messageDiv.innerHTML = ''; messageDiv.className = '';
try { const response = await fetch('/actions/submitRegistration', { method: 'POST', body: formData });
const result = await response.json();
if (result.success) { messageDiv.innerHTML = ` <div class="p-4 rounded-md bg-green-50 border border-green-200"> <div class="flex"> <div class="flex-shrink-0"> <svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /> </svg> </div> <div class="ml-3"> <p class="text-sm font-medium text-green-800">${result.message}</p> </div> </div> </div> `; form.reset(); } else { messageDiv.innerHTML = ` <div class="p-4 rounded-md bg-red-50 border border-red-200"> <div class="flex"> <div class="flex-shrink-0"> <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /> </svg> </div> <div class="ml-3"> <p class="text-sm font-medium text-red-800">${result.message}</p> ${result.errors ? ` <ul class="mt-2 text-sm text-red-700 list-disc list-inside"> ${result.errors.map(err => `<li>${err.field}: ${err.message}</li>`).join('')} </ul> ` : ''} </div> </div> </div> `; } } catch (error) { messageDiv.innerHTML = ` <div class="p-4 rounded-md bg-red-50 border border-red-200"> <div class="flex"> <div class="ml-3"> <p class="text-sm font-medium text-red-800">Koneksi internet bermasalah. Silakan coba lagi.</p> </div> </div> </div> `; } finally { submitBtn.disabled = false; submitBtn.innerHTML = 'Daftar Sekarang'; } }} > <!-- Honeypot Field (Hidden from users) --> <div class="hidden" aria-hidden="true"> <label for="website">Website</label> <input type="text" id="website" name="website" tabindex="-1" autocomplete="off" /> </div>
<!-- Data Pribadi --> <div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <h3 class="text-lg font-semibold text-gray-900 mb-4">A. Data Pribadi</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label for="nama" class="block text-sm font-medium text-gray-700 mb-1">Nama Lengkap *</label> <input type="text" id="nama" name="nama" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div>
<div> <label for="nisn" class="block text-sm font-medium text-gray-700 mb-1">NISN (10 digit) *</label> <input type="text" id="nisn" name="nisn" required maxlength="10" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="Contoh: 1234567890"> </div>
<div> <label for="tempat_lahir" class="block text-sm font-medium text-gray-700 mb-1">Tempat Lahir *</label> <input type="text" id="tempat_lahir" name="tempat_lahir" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div>
<div> <label for="tanggal_lahir" class="block text-sm font-medium text-gray-700 mb-1">Tanggal Lahir *</label> <input type="date" id="tanggal_lahir" name="tanggal_lahir" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div>
<div> <label class="block text-sm font-medium text-gray-700 mb-1">Jenis Kelamin *</label> <div class="flex space-x-4"> <label class="inline-flex items-center"> <input type="radio" name="jenis_kelamin" value="L" required class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"> <span class="ml-2 text-sm text-gray-700">Laki-laki</span> </label> <label class="inline-flex items-center"> <input type="radio" name="jenis_kelamin" value="P" required class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"> <span class="ml-2 text-sm text-gray-700">Perempuan</span> </label> </div> </div>
<div> <label for="agama" class="block text-sm font-medium text-gray-700 mb-1">Agama *</label> <select id="agama" name="agama" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> <option value="">Pilih Agama</option> <option value="Islam">Islam</option> <option value="Kristen">Kristen</option> <option value="Katolik">Katolik</option> <option value="Hindu">Hindu</option> <option value="Buddha">Buddha</option> <option value="Konghucu">Konghucu</option> </select> </div> </div>
<div class="mt-4"> <label for="alamat" class="block text-sm font-medium text-gray-700 mb-1">Alamat Lengkap *</label> <textarea id="alamat" name="alamat" rows="3" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="Jl. Contoh No. 123, RT/RW, Kelurahan, Kecamatan, Kota"></textarea> </div> </div>
<!-- Data Orang Tua --> <div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <h3 class="text-lg font-semibold text-gray-900 mb-4">B. Data Orang Tua</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label for="nama_ayah" class="block text-sm font-medium text-gray-700 mb-1">Nama Ayah *</label> <input type="text" id="nama_ayah" name="nama_ayah" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div>
<div> <label for="pekerjaan_ayah" class="block text-sm font-medium text-gray-700 mb-1">Pekerjaan Ayah *</label> <input type="text" id="pekerjaan_ayah" name="pekerjaan_ayah" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div>
<div> <label for="nama_ibu" class="block text-sm font-medium text-gray-700 mb-1">Nama Ibu *</label> <input type="text" id="nama_ibu" name="nama_ibu" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div>
<div> <label for="pekerjaan_ibu" class="block text-sm font-medium text-gray-700 mb-1">Pekerjaan Ibu *</label> <input type="text" id="pekerjaan_ibu" name="pekerjaan_ibu" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> </div> </div> </div>
<!-- Kontak dan Asal Sekolah --> <div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <h3 class="text-lg font-semibold text-gray-900 mb-4">C. Kontak & Asal Sekolah</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div> <label for="no_hp" class="block text-sm font-medium text-gray-700 mb-1">No. HP (Orang Tua) *</label> <input type="tel" id="no_hp" name="no_hp" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="08xxxxxxxxxx"> </div>
<div> <label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email *</label> <input type="email" id="email" name="email" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="contoh@email.com"> </div>
<div class="md:col-span-2"> <label for="asal_sekolah" class="block text-sm font-medium text-gray-700 mb-1">Asal Sekolah (SD/MI) *</label> <input type="text" id="asal_sekolah" name="asal_sekolah" required class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="Nama sekolah asal"> </div> </div> </div>
<!-- Submit Button --> <div class="flex justify-center"> <button type="submit" class="px-8 py-3 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200"> Daftar Sekarang </button> </div>
<!-- Message Area --> <div id="formMessage" class="mt-4"></div>
<p class="text-sm text-gray-500 text-center mt-4"> * Wajib diisi<br> Data akan langsung tersimpan di sistem sekolah </p> </form></div>
<script>// Client-side validationdocument.getElementById('nisn').addEventListener('input', function(e) { this.value = this.value.replace(/[^0-9]/g, '');});
document.getElementById('no_hp').addEventListener('input', function(e) { this.value = this.value.replace(/[^0-9]/g, '');});</script>9. Halaman Utama
Buata file baru src/pages/index.astro untuk halaman depan:
---import Layout from '../layouts/Layout.astro';import Form from '../components/Form.astro';---
<Layout title="Form Pendaftaran Siswa Baru"> <div class="min-h-screen bg-gradient-to-b from-blue-50 to-white py-8"> <div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8"> <!-- Header --> <div class="text-center mb-10"> <h1 class="text-3xl md:text-4xl font-bold text-gray-900 mb-3"> FORMULIR PENDAFTARAN SISWA BARU </h1> <p class="text-lg text-gray-600 mb-2"> Tahun Ajaran 2024/2025 </p> <div class="inline-flex items-center justify-center px-4 py-2 bg-blue-100 text-blue-800 rounded-full text-sm font-medium"> <svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/> </svg> Gratis 100% - Tidak Ada Biaya Pendaftaran </div> </div>
<!-- Instructions --> <div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8"> <div class="flex items-start"> <svg class="h-6 w-6 text-blue-600 mt-0.5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/> </svg> <div> <h3 class="text-lg font-semibold text-blue-900 mb-2">Petunjuk Pengisian</h3> <ul class="text-blue-800 space-y-1"> <li>• Semua field bertanda * wajib diisi</li> <li>• Pastikan data yang diisi sesuai dengan dokumen asli</li> <li>• NISN harus 10 digit angka (dapat dicek di data Kemendikbud)</li> <li>• Data akan langsung tersimpan di sistem sekolah setelah submit</li> <li>• Tidak perlu print form, data sudah digital</li> </ul> </div> </div> </div>
<!-- Form --> <div class="bg-white rounded-xl shadow-lg p-6 md:p-8"> <Form /> </div>
<!-- Footer Note --> <div class="mt-8 text-center text-gray-500 text-sm"> <p>© 2024 [Nama Sekolah]. Semua hak dilindungi.</p> <p class="mt-1">Sistem ini berjalan di Vercel Hobby Plan - 100% Gratis</p> </div> </div> </div></Layout>10. Layout
Buat layout utama src/layouts/Layout.astro seperti ini:
---const { title } = Astro.props;---
<!DOCTYPE html><html lang="id"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{title}</title> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="description" content="Form pendaftaran siswa baru secara online"></head><body> <slot /></body></html>11. Tailwind Config
Konfigurasi agar memakai styling modern dari Tailwind CSS dengan membuat file tailwind.config.mjs:
/** @type {import('tailwindcss').Config} */export default { content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], theme: { extend: {}, }, plugins: [],}12. Package.json
Buat file package.json untuk project.
{ "name": "form-ppdb", "type": "module", "version": "1.0.0", "scripts": { "dev": "astro dev", "build": "astro build", "preview": "astro preview", "astro": "astro" }, "dependencies": { "@astrojs/tailwind": "^5.0.4", "@astrojs/vercel": "^5.1.2", "astro": "^5.0.0", "googleapis": "^128.0.0", "tailwindcss": "^3.4.0", "zod": "^3.22.4" }}13. Deploy ke Vercel
Kita bisa men-deploy project dengan cara membuat repo di Github dan impor repo project dari Vercel. Langkah-langkahnya:
1. Push ke GitHub:
git initgit add .git commit -m "Initial commit"git branch -M maingit remote add origin https://github.com/username/repo.gitgit push -u origin main2. Deploy ke Vercel:
- Login ke vercel.com
- Import project dari GitHub
- Tambahkan environment variables:
- GOOGLE_SERVICE_ACCOUNT_EMAIL
- GOOGLE_PRIVATE_KEY
- GOOGLE_SHEET_ID
- Klik Deploy
14. Setup Google Sheets Headers
Buka Google Sheet dan buat header di baris pertama:
Timestamp | NISN | Nama Lengkap | Tempat Lahir | Tanggal Lahir | Jenis Kelamin | Agama | Alamat | Nama Ayah | Pekerjaan Ayah | Nama Ibu | Pekerjaan Ibu | No HP | Email | Asal SekolahFitur Keamanan:
- Honeypot Field: Deteksi bot otomatis
- Zod Validation: Validasi server-side yang kuat
- Duplicate Check: Cek NISN ganda
- Rate Limiting: Otomatis dari Vercel
- HTTPS: SSL gratis dari Vercel
Skala & Performa:
- 200 siswa: Hanya ~200 baris di Google Sheets
- Vercel Hobby: 100GB bandwidth/bulan cukup untuk 100,000+ submit
- Google Sheets: Gratis hingga 5 juta sel
- Zero Cost: Semuanya gratis selamanya
Aplikasi ini siap digunakan! Untuk 500 siswa bahkan lebih, sistem ini akan bekerja dengan sempurna tanpa biaya.
Dan yang paling penting adalah: admin atau TU sekolah bisa langsung melihat data dari formulir pendaftaran.