Membuat Website dari 0 Sampai Online dengan PHP MySQL: Form OSIS Lengkap + Login Admin

membuat website dari nol sampai benar-benar bisa dibuka dari HP teman kalian adalah momen paling nagih. di panduan ini saya tidak bikin landing page statis lagi, tapi website dinamis PHP + MySQL yang datanya disimpan beneran: form pendataan anggota OSIS / organisasi masyarakat / karang taruna lengkap, lalu admin bisa login untuk edit, hapus, approve, dan kasih nomor anggota otomatis OSIS-2026-0001.
banyak pemula bingung: input type="text" sama type="number" bedanya apa di database? checkbox yang bisa pilih banyak itu kok jadi array? select dropdown vs select multiple vs datalist itu bedanya di mana? di artikel ini saya bedah semua tipe input yang ada di HTML + tipe datanya di PHP + tipe kolom MySQL-nya, langsung pakai studi kasus nyata yang bisa kalian publish.
Daftar Isi
- 1. Konsep Dasar: Website Dinamis PHP + MySQL
- 2. Persiapan Environment Laragon + VS Code
- 3. Desain Database: Tabel Admin dan Anggota
- 4. Daftar Lengkap Tipe Input HTML dan Tipe Datanya
- 5. Membuat Form Pendaftaran Anggota OSIS Komplit
- 6. Proses Simpan ke MySQL: Semua Tipe Data Masuk Database
- 7. Login Admin + Dashboard Approve Edit Delete
- 8. Deploy dari Localhost Sampai Online di Hosting
- 9. Error Paling Sering dan Solusinya
- 10. Best Practice Industri
- 11. Kesimpulan
1. Konsep Dasar: Website Dinamis PHP + MySQL
kalau di artikel sebelumnya kita hanya punya index.html + style.css yang isinya statis, sekarang alurnya bertambah satu lapisan:
flowchart LR
A["Browser Pengunjung<br/>Form HTML 24 Input"] -- "isi form<br/>POST + FILES" --> B["Server<br/>Apache + PHP<br/>proses_daftar.php"]
B -- "simpan / baca<br/>mysqli prepare" --> C[("Database MySQL<br/>anggota + users")]
C -- "tampilkan HTML + data" --> A
style A fill:#eef4ff,stroke:#000,stroke-width:3px
style B fill:#f0eefc,stroke:#000,stroke-width:3px
style C fill:#e6f7fb,stroke:#000,stroke-width:3px
- Client (Browser): menampilkan form HTML. Setiap
<input>punyatypedannameyang menentukan data apa yang dikirim. - Server (PHP): menangkap
$_POST/$_FILES, validasi, lalu simpan ke MySQL lewatmysqli/PDO. - Database (MySQL): menyimpan permanen. Tiap kolom punya tipe data (
VARCHAR,INT,DATE,JSON) yang harus cocok dengan tipe input HTML. - Hosting: komputer server di internet yang menjalankan Apache + PHP + MySQL 24 jam. Domain mengarah ke hosting itu.
Kenapa harus PHP + MySQL untuk OSIS? Karena datanya relational: satu anggota punya banyak hobi, satu admin mengelola banyak anggota, butuh login, approve, dan nomor anggota. Ini tidak bisa pakai HTML statis.
2. Persiapan Environment Laragon + VS Code
kalian butuh server lokal, bukan Live Server lagi.
- Instal Laragon (rekomendasi 2026) dari
laragon.org- centang Apache, PHP 8.3, MySQL 8.4. Alternatif: XAMPP. - Instal VS Code + ekstensi
PHP Intelephense. - Buka Laragon → Klik Start All → buka
http://localhostuntuk cek. - Buat folder project di
C:\laragon\www\osis-app→ buka di VS Code. - Buat database: klik Database di Laragon → HeidiSQL terbuka → buat database
organisasi_osischarsetutf8mb4.
Struktur folder akhir (tanpa CSS eksternal — cukup index.php saja):
osis-app/
├── config.php
├── index.php // form pendaftaran publik (sudah include <style> minimal)
├── proses_daftar.php
├── login.php // pakai <style> yang sama, copy dari index.php
├── dashboard.php // tabel native + 5 baris CSS opsional
├── edit.php
├── hapus.php
├── approve.php
├── logout.php
└── uploads/ // foto anggota ( Dibuat otomatis via mkdir )
assets/style.csstidak wajib. Semua UI di artikel ini pakai<style>18 baris di dalam<head>— hapus pun tetap jalan karena pakai elemen HTML bawaan.
3. Desain Database: Tabel Admin dan Anggota
buka tab SQL di HeidiSQL/phpMyAdmin, jalankan ini:
CREATE DATABASE IF NOT EXISTS organisasi_osis CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE organisasi_osis;
-- Tabel admin untuk login
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL, -- simpan hash, bukan plain text
nama_lengkap VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- Admin default: username admin / password admin123
INSERT INTO users (username, password, nama_lengkap) VALUES
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin OSIS');
-- Tabel anggota: menampung SEMUA tipe input
CREATE TABLE anggota (
id INT AUTO_INCREMENT PRIMARY KEY,
nomor_anggota VARCHAR(20) UNIQUE, -- OSIS-2026-0001, diisi saat approve
nama_lengkap VARCHAR(100) NOT NULL,
nis VARCHAR(20) NOT NULL,
kelas VARCHAR(50), -- dari datalist
jenis_kelamin ENUM('L','P') NOT NULL, -- dari radio
agama VARCHAR(20), -- dari select dropdown
tanggal_lahir DATE, -- dari input date
waktu_wawancara TIME, -- dari input time
jadwal_wawancara DATETIME, -- dari datetime-local
bulan_angkatan VARCHAR(7), -- dari month: 2026-09
minggu_orientasi VARCHAR(8), -- dari week: 2026-W38
email VARCHAR(100),
no_hp VARCHAR(20), -- tel, simpan sebagai VARCHAR biar 08 tidak hilang
instagram VARCHAR(100), -- url
alamat TEXT, -- textarea
warna_kartu VARCHAR(7), -- color: #2563eb
tingkat_keaktifan TINYINT, -- range 1-10
hobi JSON, -- checkbox array: ["Futsal","Coding"]
ekskul JSON, -- select multiple array: ["Paskibra","PMR"]
jabatan_diminati VARCHAR(50), -- select
motivasi TEXT,
referensi VARCHAR(100), -- search
foto VARCHAR(255), -- file path
status ENUM('pending','approved','rejected') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
Penjelasan Detail Per Baris SQL
bayangkan database itu lemari besar, tabel itu laci, kolom itu sekat di dalam laci.
| Baris | Kode | Artinya untuk Pemula |
|---|---|---|
| 1 | CREATE DATABASE IF NOT EXISTS organisasi_osis ... utf8mb4 | Bikin lemari baru namanya organisasi_osis. IF NOT EXISTS = kalau lemarinya sudah ada, jangan bikin lagi biar tidak error. utf8mb4 = biar bisa simpan emoji & huruf Indonesia lengkap. |
| 2 | USE organisasi_osis; | Buka lemarinya. Semua perintah setelah ini dikerjakan di dalam lemari itu. |
| 3 | CREATE TABLE users (...) | Bikin laci pertama namanya users untuk simpan data admin yang bisa login. |
| 4 | id INT AUTO_INCREMENT PRIMARY KEY | id = nomor urut laci. INT = angka. AUTO_INCREMENT = nomornya nambah sendiri 1,2,3 otomatis. PRIMARY KEY = KTP-nya laci, tidak boleh kembar. |
| 5 | username VARCHAR(50) UNIQUE NOT NULL | username = nama login. VARCHAR(50) = teks pendek max 50 huruf. UNIQUE = tidak boleh ada dua admin dengan username sama. NOT NULL = wajib diisi, tidak boleh kosong. |
| 6 | password VARCHAR(255) NOT NULL | Tempat simpan password yang sudah diacak (hash). 255 sengaja panjang karena hash itu panjang. Jangan pernah simpan password asli! |
| 7 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | Stempel waktu otomatis: kapan baris ini dibuat. DEFAULT CURRENT_TIMESTAMP = kalau tidak diisi, MySQL isi dengan jam sekarang otomatis. |
| 8 | INSERT INTO users VALUES ('admin', '$2y$10$...', 'Admin OSIS') | Masukkan 1 admin pertama. Password admin123 sudah diacak jadi $2y$10$... (hasil password_hash). Ibarat kunci gembok yang sudah dikunci. |
| 9 | CREATE TABLE anggota (...) | Bikin laci kedua namanya anggota — ini brankas utama semua pendaftar OSIS. |
| 10 | nomor_anggota VARCHAR(20) UNIQUE | Nomor anggota seperti OSIS-2026-0001. UNIQUE biar tidak ada yang kembar. Kosong dulu saat daftar, baru diisi saat admin klik Approve. |
| 11 | jenis_kelamin ENUM('L','P') | ENUM = pilihan yang sudah dikunci cuma boleh L atau P. Kalau isi X akan ditolak. Ibarat soal pilihan ganda yang cuma ada 2 jawaban. |
| 12 | tanggal_lahir DATE | Tipe kalender. Format wajib 2026-09-24. Cocok untuk <input type="date">. |
| 13 | hobi JSON dan ekskul JSON | JSON = laci yang bisa simpan banyak barang sekaligus: ["Futsal","Coding"]. Cocok untuk checkbox[] dan select multiple yang pilihnya bisa banyak. MySQL 8+ paham JSON. |
| 14 | no_hp VARCHAR(20) bukan INT | Nomor HP 0812... kalau pakai INT angka 0 depannya hilang jadi 812.... Makanya pakai VARCHAR biar 0-nya tetap. |
| 15 | status ENUM('pending','approved','rejected') DEFAULT 'pending' | Status pendaftaran. Default pending = menunggu verifikasi. Nanti admin ubah jadi approved. Ibarat stempel: belum diperiksa → lolos. |
4. Daftar Lengkap Tipe Input HTML dan Tipe Datanya
ini bagian paling penting yang kalian minta. semua input di HTML itu string di awal, tapi di PHP dan MySQL kita bedakan cara simpan dan validasinya.
| # | Tipe Input HTML | Contoh Kode | Data di PHP $_POST | Tipe Kolom MySQL | Catatan Pemula |
|---|---|---|---|---|---|
| 1 | text | <input type="text" name="nama"> | string "Budi" | VARCHAR(100) | Nama, NIS, kelas |
| 2 | email | <input type="email" name="email"> | string "[email protected]" | VARCHAR(100) | Browser validasi @ otomatis |
| 3 | password | <input type="password" name="pass"> | string | VARCHAR(255) hash | Wajib password_hash() |
| 4 | number | <input type="number" name="umur"> | string "17" → (int) | INT / TINYINT | Selalu cast ke int |
| 5 | tel | <input type="tel" name="no_hp"> | string "081234" | VARCHAR(20) | Jangan INT, angka 0 depan hilang |
| 6 | url | <input type="url" name="instagram"> | string "https://..." | VARCHAR(100) | Validasi filter_var() |
| 7 | search | <input type="search" name="referensi"> | string | VARCHAR(100) | Sama dengan text, ada icon X |
| 8 | textarea | <textarea name="alamat"> | string panjang | TEXT | Untuk alamat/motivasi |
| 9 | radio | <input type="radio" name="jk" value="L"> | string "L" | ENUM('L','P') | 1 pilihan, name sama |
| 10 | checkbox tunggal | <input type="checkbox" name="setuju" value="1"> | isset → "1" | TINYINT(1) | Centang/tidak |
| 11 | checkbox array | <input type="checkbox" name="hobi[]" value="Futsal"> | array ["Futsal","Coding"] | JSON | WAJIB [] di name |
| 12 | select dropdown | <select name="agama"><option>Islam | string "Islam" | VARCHAR(20) | 1 pilihan |
| 13 | select multiple | <select name="ekskul[]" multiple> | array | JSON | WAJIB [] + multiple |
| 14 | datalist | <input list="kelasList" name="kelas"><datalist id="kelasList"> | string | VARCHAR(50) | Kombinasi text + dropdown saran |
| 15 | date | <input type="date" name="tgl_lahir"> | string "2008-09-24" | DATE | Format YYYY-MM-DD |
| 16 | time | <input type="time" name="waktu"> | string "14:30" | TIME | Jam:menit |
| 17 | datetime-local | <input type="datetime-local" name="jadwal"> | string "2026-09-24T14:30" | DATETIME | Ganti T jadi spasi saat simpan |
| 18 | month | <input type="month" name="bulan"> | string "2026-09" | VARCHAR(7) | Bulan angkatan |
| 19 | week | <input type="week" name="minggu"> | string "2026-W38" | VARCHAR(8) | Minggu orientasi |
| 20 | color | <input type="color" name="warna"> | string "#2563eb" | VARCHAR(7) | Picker warna kartu |
| 21 | range | <input type="range" min="1" max="10" name="aktif"> | string "8" | TINYINT | Slider keaktifan |
| 22 | file | <input type="file" name="foto"> | $_FILES | VARCHAR(255) path | Simpan path, file di uploads/ |
| 23 | hidden | <input type="hidden" name="id" value="5"> | string | INT | Untuk edit, jangan percaya user |
| 24 | hidden CSRF | <input type="hidden" name="csrf"> | string | - | Keamanan anti tembak form |
5. Membuat Form Pendaftaran Anggota OSIS Komplit
buat file index.php - ini halaman publik yang bisa diisi siapa saja. saya sudah masukkan semua 24 tipe input di atas dalam satu form studi kasus OSIS.
<!-- index.php — tanpa CSS berat, cuma 18 baris style biar jelas pas php -S -->
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Daftar Anggota OSIS - SMP Negeri 1</title>
<style>
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;max-width:720px;margin:24px auto;padding:0 16px;line-height:1.5;color:#111}
h1{margin:0 0 4px;font-size:22px} p.muted{color:#555;margin:0 0 16px}
fieldset{border:1px solid #ccc;padding:12px 16px;margin:16px 0} legend{font-weight:700;padding:0 6px}
label{display:block;margin:10px 0} input,select,textarea{width:100%;padding:8px;margin-top:4px;box-sizing:border-box;border:1px solid #999}
input[type=radio],input[type=checkbox],input[type=color],input[type=range]{width:auto}
.row{display:grid;grid-template-columns:1fr 1fr;gap:12px} @media(max-width:600px){.row{grid-template-columns:1fr}}
button{padding:10px 16px;border:1px solid #000;background:#fff;cursor:pointer} button[type=submit]{background:#111;color:#fff}
small{color:#666} .req{color:#c00}
</style>
</head>
<body>
<h1>Form Pendaftaran Anggota OSIS 2026/2027</h1>
<p class="muted">Isi jujur. Setelah kirim, admin verifikasi & beri nomor anggota <code>OSIS-2026-0001</code>.</p>
<form action="proses_daftar.php" method="POST" enctype="multipart/form-data">
<fieldset>
<legend>Data Pribadi</legend>
<label>Nama Lengkap <span class="req">*</span> <input type="text" name="nama_lengkap" required placeholder="Budi Santoso"></label>
<label>NIS <span class="req">*</span> <input type="text" name="nis" required pattern="[0-9]{4,20}" placeholder="12345"></label>
<label>Kelas <span class="req">*</span>
<input list="kelasList" name="kelas" required placeholder="XII IPA 1">
<datalist id="kelasList"><option value="X IPA 1"><option value="XI IPA 1"><option value="XII IPA 1"><option value="XII IPS 1"></datalist>
</label>
<fieldset>
<legend>Jenis Kelamin <span class="req">*</span></legend>
<label><input type="radio" name="jenis_kelamin" value="L" required> Laki-laki</label>
<label><input type="radio" name="jenis_kelamin" value="P"> Perempuan</label>
</fieldset>
<label>Agama <span class="req">*</span>
<select name="agama" required><option value="">-- Pilih --</option><option>Islam</option><option>Kristen</option><option>Katolik</option><option>Hindu</option><option>Buddha</option><option>Konghucu</option></select>
</label>
</fieldset>
<fieldset>
<legend>Waktu</legend>
<div class="row">
<label>Tanggal Lahir <span class="req">*</span> <input type="date" name="tanggal_lahir" required></label>
<label>Waktu Wawancara <input type="time" name="waktu_wawancara"></label>
</div>
<label>Jadwal Lengkap <input type="datetime-local" name="jadwal_wawancara"></label>
<div class="row">
<label>Bulan Angkatan <input type="month" name="bulan_angkatan" value="2026-09"></label>
<label>Minggu Orientasi <input type="week" name="minggu_orientasi"></label>
</div>
</fieldset>
<fieldset>
<legend>Kontak & Sosial</legend>
<label>Email <span class="req">*</span> <input type="email" name="email" required placeholder="[email protected]"></label>
<label>No HP/WA <span class="req">*</span> <input type="tel" name="no_hp" required placeholder="08123456789"></label>
<label>Instagram <input type="url" name="instagram" placeholder="https://instagram.com/username"></label>
<label>Dari mana tahu OSIS? <input type="search" name="referensi" placeholder="Instagram / Guru"></label>
</fieldset>
<fieldset>
<legend>Preferensi</legend>
<div class="row">
<label>Umur <input type="number" name="umur" min="12" max="25" placeholder="17"></label>
<label>Warna Kartu <input type="color" name="warna_kartu" value="#2563eb"></label>
</div>
<label>Tingkat Keaktifan (1-10) <input type="range" name="tingkat_keaktifan" min="1" max="10" value="5" oninput="this.nextElementSibling.value=this.value"> <output>5</output></label>
</fieldset>
<fieldset>
<legend>Hobi <span class="req">*</span> — boleh banyak (array)</legend>
<label><input type="checkbox" name="hobi[]" value="Futsal"> Futsal</label>
<label><input type="checkbox" name="hobi[]" value="Coding"> Coding</label>
<label><input type="checkbox" name="hobi[]" value="Desain"> Desain</label>
<label><input type="checkbox" name="hobi[]" value="Musik"> Musik</label>
<label><input type="checkbox" name="hobi[]" value="Basket"> Basket</label>
</fieldset>
<label>Ekstrakurikuler (Ctrl+Klik untuk banyak — array)
<select name="ekskul[]" multiple size="4"><option>Paskibra</option><option>PMR</option><option>Pramuka</option><option>Rohis</option></select>
</label>
<label>Jabatan Diminati
<select name="jabatan_diminati"><option value="">-- Pilih --</option><option>Ketua</option><option>Sekretaris</option><option>Bendahara</option><option>Humas</option><option>Anggota</option></select>
</label>
<label>Alamat Lengkap <span class="req">*</span> <textarea name="alamat" rows="3" required placeholder="Jl. Mawar No 10..."></textarea></label>
<label>Motivasi <span class="req">*</span> <textarea name="motivasi" rows="4" required></textarea></label>
<label>Foto 3x4 (JPG/PNG max 2MB) <input type="file" name="foto" accept=".jpg,.jpeg,.png"></label>
<input type="hidden" name="csrf" value="osis2026">
<p><button type="submit">Kirim Pendaftaran</button> <button type="reset">Reset</button> <small><span class="req">*</span> wajib</small></p>
</form>
<p><small>Tanpa <code>assets/style.css</code> — semua style di <style> atas. Hapus <style> pun form tetap jalan karena pakai HTML bawaan browser.</small></p>
</body>
</html>
Penjelasan Detail Per Baris HTML
| Baris | Kode | Artinya untuk Pemula |
|---|---|---|
| 1 | <form action="proses_daftar.php" method="POST" enctype="multipart/form-data"> | Amplop pengiriman. action = alamat tujuan (file PHP yang menerima). method="POST" = kirimnya diam-diam (tidak terlihat di URL, aman untuk data banyak). enctype="multipart/form-data" = wajib kalau ada upload file, kalau lupa fotonya tidak akan kekirim. Ibarat amplop khusus yang bisa bawa foto. |
| 2 | <input type="text" name="nama_lengkap" required> | Kotak isian teks biasa. name="nama_lengkap" = label di amplop, nanti PHP baca $_POST['nama_lengkap']. required = tidak boleh kosong, browser akan marah kalau dikosongkan. |
| 3 | pattern="[0-9]{4,20}" di NIS | Polisi format: cuma boleh angka 0-9, panjang 4-20 digit. Kalau isi huruf, browser tolak sebelum dikirim. |
| 4 | <input list="kelasList" name="kelas"> + <datalist> | Kombinasi text + saran dropdown. User boleh ketik bebas XII IPA 1 atau pilih saran. Beda dengan <select> yang harus pilih dari daftar. Ibarat Google: ketik, muncul saran. |
| 5 | <input type="radio" name="jenis_kelamin" value="L"> | Pilihan cuma 1. Semua radio yang name-nya sama (jenis_kelamin) jadi satu grup. Kalau pilih L, yang P otomatis lepas. Ibarat pilih ketua kelas: cuma bisa 1. |
| 6 | <select name="agama"> | Dropdown 1 pilihan. Klik panah, muncul daftar. Beda dengan radio yang langsung terlihat semua. Cocok kalau pilihannya 6+. |
| 7 | <input type="date"> | Kalender mini. User klik, muncul kalender. Hasilnya selalu 2026-09-24 (tahun-bulan-tanggal) biar MySQL langsung paham. |
| 8 | <input type="checkbox" name="hobi[]" value="Futsal"> | Centang banyak. [] adalah kunci: tanpa [] cuma 1 hobi terakhir yang kekirim, dengan [] semua yang dicentang jadi array ["Futsal","Coding"]. Ibarat keranjang: boleh ambil banyak. |
| 9 | <select name="ekskul[]" multiple> | Dropdown tapi boleh pilih banyak dengan Ctrl+Klik. Juga wajib [] + multiple. Array juga: ["Paskibra","PMR"]. |
| 10 | <input type="file" name="foto" accept=".jpg,.png"> | Tombol pilih file dari HP/laptop. accept = saring cuma boleh JPG/PNG. Nanti di PHP bacanya bukan $_POST tapi $_FILES. |
| 11 | <textarea name="alamat"> | Kotak teks besar untuk alamat panjang. Beda dengan input text yang cuma 1 baris. |
| 12 | <input type="hidden" name="csrf"> | Input siluman: tidak terlihat user, tapi ikut kekirim. Untuk keamanan & bawa data diam-diam. Jangan taruh nomor_anggota di sini — itu dibuat admin, bukan user! |
| 13 | <input type="color">, range, month, week | Input cantik: color = palet warna, range = slider geser, month = bulan-tahun, week = minggu ke-berapa. Semua hasilnya tetap string di PHP, tinggal simpan ke VARCHAR. |
6. Proses Simpan ke MySQL: Semua Tipe Data Masuk Database
buat 2 file: config.php untuk koneksi dan proses_daftar.php untuk menangkap form.
<?php
// config.php - koneksi aman
$host = "localhost";
$user = "root";
$pass = "";
$db = "organisasi_osis";
$conn = new mysqli($host, $user, $pass, $db);
if ($conn->connect_error) die("Koneksi gagal: " . $conn->connect_error);
$conn->set_charset("utf8mb4");
?>
Penjelasan Detail Per Baris config.php
| Baris | Kode | Artinya |
|---|---|---|
| 1 | $host = "localhost"; | Alamat rumah database. localhost = database ada di laptop sendiri. Nanti di hosting ganti jadi sql123.infinityfree.com. |
| 2 | $user = "root"; | Username database. Di Laragon default-nya root tanpa password — seperti akun default. |
| 3 | $pass = ""; | Password database. Kosong di lokal, wajib isi di hosting. |
| 4 | $db = "organisasi_osis"; | Nama lemari yang mau dibuka. Harus sudah dibikin di HeidiSQL tadi. |
| 5 | new mysqli($host, $user, $pass, $db) | Buka koneksi database pakai 4 data itu (host, user, pass, nama database). Hasilnya simpan di $conn (connection = sambungan). |
| 6 | if ($conn->connect_error) die(...) | Kalau data koneksi salah, hentikan dan tampilkan “Koneksi gagal”. Biar tidak lanjut dengan sambungan rusak. |
| 7 | set_charset("utf8mb4") | Atur bahasa database biar paham huruf Indonesia, emoji, dll. Kalau lupa, nama Siti ❤️ bisa jadi ???. |
<?php
// proses_daftar.php
require "config.php";
// 1. Ambil data STRING: wajib trim + htmlspecialchars biar aman XSS
$nama = htmlspecialchars(trim($_POST['nama_lengkap']));
$nis = trim($_POST['nis']);
$kelas = trim($_POST['kelas']);
$jk = $_POST['jenis_kelamin']; // L/P
$agama = $_POST['agama'];
$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
$no_hp = trim($_POST['no_hp']);
$instagram = trim($_POST['instagram']);
$alamat = htmlspecialchars(trim($_POST['alamat']));
$motivasi = htmlspecialchars(trim($_POST['motivasi']));
$referensi = trim($_POST['referensi']);
$jabatan = $_POST['jabatan_diminati'];
$warna = $_POST['warna_kartu']; // #2563eb
$aktif = (int) $_POST['tingkat_keaktifan'];
// 2. WAKTU: validasi format
$tgl_lahir = $_POST['tanggal_lahir']; // 2026-09-24 -> langsung masuk DATE
$waktu = $_POST['waktu_wawancara'] ?: null; // 14:30 -> TIME
$jadwal = $_POST['jadwal_wawancara']; // 2026-09-24T14:30 -> ganti T jadi spasi
$jadwal = $jadwal ? str_replace("T", " ", $jadwal) . ":00" : null; // jadi DATETIME
$bulan = $_POST['bulan_angkatan']; // 2026-09
$minggu = $_POST['minggu_orientasi']; // 2026-W38
// 3. ARRAY: checkbox dan select multiple -> JSON
$hobi = isset($_POST['hobi']) ? json_encode($_POST['hobi'], JSON_UNESCAPED_UNICODE) : json_encode([]);
$ekskul = isset($_POST['ekskul']) ? json_encode($_POST['ekskul'], JSON_UNESCAPED_UNICODE) : json_encode([]);
// 4. FILE: upload foto
$foto_path = null;
if (isset($_FILES['foto']) && $_FILES['foto']['error'] == 0) {
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png'];
if (!in_array($ext, $allowed)) die("Format foto harus JPG/PNG");
if ($_FILES['foto']['size'] > 2*1024*1024) die("Foto max 2MB");
$nama_file = "foto_" . time() . "_" . uniqid() . "." . $ext;
$tujuan = "uploads/" . $nama_file;
if (!is_dir("uploads")) mkdir("uploads", 0755, true);
move_uploaded_file($_FILES['foto']['tmp_name'], $tujuan);
$foto_path = $tujuan;
}
// 5. Simpan pakai PREPARED STATEMENT (anti SQL Injection)
$stmt = $conn->prepare("INSERT INTO anggota
(nama_lengkap, nis, kelas, jenis_kelamin, agama, tanggal_lahir, waktu_wawancara, jadwal_wawancara, bulan_angkatan, minggu_orientasi, email, no_hp, instagram, alamat, warna_kartu, tingkat_keaktifan, hobi, ekskul, jabatan_diminati, motivasi, referensi, foto, status)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, 'pending')");
$stmt->bind_param("sssssssssssssssissssss",
$nama, $nis, $kelas, $jk, $agama, $tgl_lahir, $waktu, $jadwal, $bulan, $minggu, $email, $no_hp, $instagram, $alamat, $warna, $aktif, $hobi, $ekskul, $jabatan, $motivasi, $referensi, $foto_path
);
if ($stmt->execute()) {
echo "Pendaftaran berhasil! Menunggu verifikasi admin. <a href='index.php'>Kembali</a>";
} else {
echo "Gagal: " . $stmt->error;
}
?>
Penjelasan Detail Per Baris proses_daftar.php
| Blok | Baris Kode | Artinya untuk Pemula |
|---|---|---|
| Koneksi Database | require "config.php"; | Panggil file koneksi database. Tanpa file ini, PHP tidak tahu harus simpan ke database mana. |
| STRING | trim($_POST['nama_lengkap']) | $_POST = amplop yang dikirim form. trim() = gunting spasi kosong di depan/belakang " Budi " jadi "Budi" biar rapi. |
htmlspecialchars(...) | Ubah karakter jahat <script> jadi teks biasa <script> biar tidak bisa bobol website. Ibarat sensor: coret-coretan jahat jadi tidak berbahaya. | |
filter_var($_POST['email'], FILTER_VALIDATE_EMAIL) | Polisi email: cek apakah ada @ dan .com. Kalau budi@ tanpa belakang, ditolak jadi false. | |
(int) $_POST['tingkat_keaktifan'] | Paksa jadi angka. "5" (string) jadi 5 (integer). Biar MySQL kolom TINYINT tidak bingung. Ibarat ubah tulisan “lima” jadi angka 5. | |
| WAKTU | $waktu = $_POST['waktu_wawancara'] ?: null; | ?: null = kalau user kosongkan jam wawancara, simpan null (kosong) bukan error. Ibarat “kalau tidak isi, ya kosong saja, jangan marah”. |
str_replace("T", " ", $jadwal) | datetime-local kirim 2026-09-24T14:30 pakai huruf T di tengah. MySQL maunya spasi 2026-09-24 14:30:00. Jadi kita ganti T jadi spasi, tambah :00 detik. Ibarat terjemahkan bahasa browser ke bahasa MySQL. | |
| ARRAY | isset($_POST['hobi']) ? json_encode(...) : json_encode([]) | Cek dulu: apakah user centang hobi? isset = apakah ada? Kalau ada, json_encode ubah ["Futsal","Coding"] jadi teks '"[\"Futsal\",\"Coding\"]"' biar muat 1 kolom. Kalau tidak centang sama sekali, simpan [] (keranjang kosong) biar tidak error Undefined array key. |
JSON_UNESCAPED_UNICODE | Biar huruf Indonesia é tidak jadi kode aneh \u00e9. Tetap cantik. | |
| FILE | $_FILES['foto']['error'] == 0 | $_FILES = amplop khusus foto (beda dengan $_POST). error==0 = tidak ada error, foto berhasil di-upload sementara. Kalau 4 = tidak pilih foto. |
pathinfo(..., PATHINFO_EXTENSION) | Ambil ekstensi file: foto.jpg → jpg. Ibarat lihat buntut nama file. | |
strtolower(...) | Ubah JPG jadi jpg biar pengecekan tidak salah gara-gara huruf besar. | |
if (!in_array($ext, $allowed)) die(...) | Satpam: kalau bukan jpg/jpeg/png, tolak! Ibarat “selain 3 kartu itu, tidak boleh masuk”. | |
if ($_FILES['foto']['size'] > 2*1024*1024) | Cek ukuran: 2*1024*1024 = 2 MB. Kalau foto 5 MB, tolak biar hosting tidak penuh. | |
time() . "_" . uniqid() | Bikin nama file unik: foto_1727170000_abc123.jpg. Kenapa? Kalau dua orang upload foto.jpg, tidak tabrakan. Ibarat kasih cap waktu + kode unik. | |
mkdir("uploads", 0755, true) | Kalau folder uploads belum ada, bikin dulu. 0755 = izin baca-tulis. | |
move_uploaded_file(tmp_name, tujuan) | Pindahkan foto dari tempat sementara (tmp) ke folder uploads/ permanen. Ibarat pindahkan foto dari meja resepsionis ke lemari arsip. | |
| SIMPAN | $conn->prepare("INSERT INTO anggota (...) VALUES (?,?,?,...,'pending')") | Siapkan lubang ? sejumlah kolom. ? = tempat kosong yang nanti diisi data. pending hardcode karena pendaftar baru selalu pending. Ibarat siapkan 23 lubang untuk 23 data. Kenapa ?? Biar aman dari hacker SQL Injection! |
bind_param("sssssssssssssssissssss", $nama, ...) | Isi tiap ? dengan data beneran. Huruf s = string, i = integer. Urutannya harus pas! 22 s + 1 i (untuk tingkat_keaktifan yang integer) + 6 s. Kalau salah urut, data nyasar. | |
$stmt->execute() | Eksekusi! Masukkan data ke laci anggota. Kalau berhasil, true. | |
echo "Pendaftaran berhasil!" | Kasih tahu user: “Oke, data kamu masuk, tunggu admin approve ya!” |
7. Login Admin + Dashboard Approve Edit Delete
ini yang bikin website kalian jadi sistem beneran, bukan form doang.
7.1 Login Admin (login.php)
<?php
session_start();
require "config.php";
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$u = $_POST['username'];
$p = $_POST['password'];
$stmt = $conn->prepare("SELECT id, password, nama_lengkap FROM users WHERE username=?");
$stmt->bind_param("s", $u);
$stmt->execute();
$res = $stmt->get_result()->fetch_assoc();
if ($res && password_verify($p, $res['password'])) {
$_SESSION['admin'] = $res['id'];
$_SESSION['nama'] = $res['nama_lengkap'];
header("Location: dashboard.php"); exit;
} else {
$err = "Username atau password salah!";
}
}
?>
<!DOCTYPE html><html lang="id"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Login Admin OSIS</title>
<style>body{font-family:system-ui,sans-serif;max-width:480px;margin:40px auto;padding:0 16px} input,button{width:100%;padding:10px;margin:8px 0;box-sizing:border-box;border:1px solid #999} button{background:#111;color:#fff;cursor:pointer}</style>
</head><body>
<h2>Login Admin OSIS</h2>
<?php if(isset($err)) echo "<p style='color:red'>$err</p>"; ?>
<form method="POST">
<input type="text" name="username" placeholder="admin" required>
<input type="password" name="password" placeholder="admin123" required>
<button type="submit">Login</button>
</form>
<p><small>Demo: admin / admin123</small></p>
</body></html>
Penjelasan Detail Per Baris login.php
| Baris | Kode | Artinya |
|---|---|---|
| 1 | session_start(); | Nyalakan sesi. session = gelang tanda “sudah login”. Wajib di baris paling atas sebelum HTML apapun. Ibarat nyalakan lampu pos satpam. |
| 2 | if ($_SERVER['REQUEST_METHOD'] == 'POST') | Cek: apakah user baru saja klik tombol Login? POST = habis submit form. Kalau baru buka halaman (GET), jangan cek password dulu. |
| 3 | $stmt = $conn->prepare("SELECT ... WHERE username=?") | Cari admin dengan username yang diketik. Pakai ? biar aman. Ibarat “cari laci users yang label usernamenya sama dengan yang diketik”. |
| 4 | $res = $stmt->get_result()->fetch_assoc(); | Ambil hasilnya. fetch_assoc = ambil 1 baris jadi array: ['id'=>1, 'password'=>'$2y$10$...', 'nama_lengkap'=>'Admin OSIS']. Kalau username tidak ada, $res = null. |
| 5 | password_verify($p, $res['password']) | Cocokkan password ketikan vs hash di DB. password_verify("admin123", "$2y$10$...") = true. Jangan pernah bandingkan pakai ==! Hash tidak bisa dibaca balik. |
| 6 | $_SESSION['admin'] = $res['id']; | Kalau cocok, kasih gelang: simpan id admin ke sesi. Mulai sekarang, tiap buka dashboard.php, satpam cek isset($_SESSION['admin']) untuk tahu sudah login. |
| 7 | header("Location: dashboard.php"); exit; | Tendang user ke dashboard. exit wajib biar kode di bawah tidak jalan lagi. Ibarat “silakan masuk, pintu lain tutup”. |
| 8 | else { $err = ... } | Kalau password salah, simpan pesan error untuk ditampilkan di HTML. |
7.2 Dashboard (dashboard.php)
<?php
session_start();
if (!isset($_SESSION['admin'])) { header("Location: login.php"); exit; }
require "config.php";
$data = $conn->query("SELECT * FROM anggota ORDER BY created_at DESC");
?>
<!DOCTYPE html><html lang="id"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Dashboard OSIS</title>
<style>body{font-family:system-ui,sans-serif;max-width:960px;margin:24px auto;padding:0 16px} table{width:100%;border-collapse:collapse;margin-top:12px} th,td{border:1px solid #999;padding:8px;text-align:left} th{background:#f2f2f2} a{color:#111}</style>
</head><body>
<h2>Halo, <?= htmlspecialchars($_SESSION['nama']) ?> - Dashboard Anggota</h2>
<a href="logout.php">Logout</a>
<table>
<tr><th>No Anggota</th><th>Nama</th><th>Kelas</th><th>Hobi</th><th>Status</th><th>Aksi</th></tr>
<?php while($r = $data->fetch_assoc()): ?>
<tr>
<td><?= $r['nomor_anggota'] ?: '-' ?></td>
<td><?= htmlspecialchars($r['nama_lengkap']) ?></td>
<td><?= htmlspecialchars($r['kelas']) ?></td>
<td><?= implode(", ", json_decode($r['hobi'], true) ?: []) ?></td>
<td><?= $r['status'] ?></td>
<td>
<?php if($r['status']=='pending'): ?>
<a href="approve.php?id=<?= $r['id'] ?>" onclick="return confirm('Approve & beri nomor anggota?')">Approve</a> |
<?php endif; ?>
<a href="edit.php?id=<?= $r['id'] ?>">Edit</a> |
<a href="hapus.php?id=<?= $r['id'] ?>" onclick="return confirm('Hapus permanen?')">Delete</a>
</td>
</tr>
<?php endwhile; ?>
</table>
</body></html>
Penjelasan Detail Per Baris dashboard.php
| Baris | Kode | Artinya |
|---|---|---|
| 1 | if (!isset($_SESSION['admin'])) header("Location: login.php") | Satpam: kalau tidak punya gelang login, usir ke halaman login! Ini yang jaga dashboard tidak bisa dibuka orang luar. |
| 2 | $conn->query("SELECT * FROM anggota ORDER BY created_at DESC") | Ambil semua anggota, yang terbaru di atas (DESC = descending). Ibarat buka laci anggota dan tumpahkan semua isinya ke meja, yang baru daftar di paling atas. |
| 3 | while($r = $data->fetch_assoc()) | Loop: ambil baris satu per satu. while = selama masih ada data, buat baris <tr> baru. Ibarat “ambil kartu anggota satu per satu dari tumpukan, tempel ke papan”. |
| 4 | $r['nomor_anggota'] ?: '-' | ?: = kalau nomor anggota kosong (belum approve), tampilkan -. Kalau sudah ada OSIS-2026-0001, tampilkan itu. |
| 5 | implode(", ", json_decode($r['hobi'], true)) | Hobi di DB bentuk JSON '["Futsal","Coding"]'. json_decode ubah jadi array PHP ["Futsal","Coding"], lalu implode gabung jadi teks "Futsal, Coding" biar cantik di tabel. Kebalikan dari json_encode saat simpan! |
| 6 | if($r['status']=='pending') | Tombol Approve cuma muncul kalau status masih pending. Kalau sudah approved, tombolnya hilang. Ibarat “kalau sudah lolos, tidak perlu stempel lagi”. |
| 7 | onclick="return confirm(...)" | Pop-up konfirmasi browser: “Yakin mau approve/hapus?” Kalau klik Cancel, tidak jadi. Biar tidak kepencet tidak sengaja. |
7.3 Approve + Generate Nomor Anggota (approve.php)
<?php
session_start();
if (!isset($_SESSION['admin'])) die("Akses ditolak");
require "config.php";
$id = (int) $_GET['id'];
// Generate nomor: OSIS-2026-0001
$tahun = date("Y");
$q = $conn->query("SELECT COUNT(*) as total FROM anggota WHERE status='approved'");
$total = $q->fetch_assoc()['total'] + 1;
$nomor = sprintf("OSIS-%s-%04d", $tahun, $total); // 4 digit padding
$stmt = $conn->prepare("UPDATE anggota SET status='approved', nomor_anggota=? WHERE id=? AND status='pending'");
$stmt->bind_param("si", $nomor, $id);
$stmt->execute();
header("Location: dashboard.php");
?>
Penjelasan Detail Per Baris approve.php
| Baris | Kode | Artinya |
|---|---|---|
| 1 | $id = (int) $_GET['id']; | Ambil id dari URL approve.php?id=5. (int) paksa jadi angka biar kalau hacker kirim id=abc' OR 1=1 jadi 0 dan gagal. Ibarat saring: cuma angka boleh lewat. |
| 2 | date("Y") | Ambil tahun sekarang: 2026. Biar nomor anggota ada tahunnya. |
| 3 | SELECT COUNT(*) as total FROM anggota WHERE status='approved' | Hitung berapa anggota yang sudah approved. Misal sudah 5 orang, total=5. |
| 4 | $total + 1 | Tambah 1 untuk anggota baru: 5+1=6. Jadi nomor urut ke-6. |
| 5 | sprintf("OSIS-%s-%04d", $tahun, $total) | Bikin nomor cantik. %s = tahun, %04d = angka 4 digit dengan nol depan. 6 jadi 0006, 42 jadi 0042. Hasil: OSIS-2026-0006. Ibarat mesin cap nomor otomatis. |
| 6 | UPDATE anggota SET status='approved', nomor_anggota=? WHERE id=? AND status='pending' | Update 1 baris: ubah status jadi approved + isi nomor. AND status='pending' = pengaman: kalau sudah approved, tidak bisa di-approve lagi (biar nomor tidak ganda). |
| 7 | header("Location: dashboard.php") | Balik ke dashboard. User lihat nomornya sudah muncul! |
7.4 Edit Anggota (edit.php) — Full Code (Tanpa CSS Berat)
File ini paling ditakuti pemula karena harus tampilkan data lama + simpan yang baru. Kuncinya:
value="..."untuktext,selecteduntukselect,checkeduntukradio/checkbox.
<?php
// edit.php — edit data anggota (admin only)
session_start();
if (!isset($_SESSION['admin'])) { header("Location: login.php"); exit; }
require "config.php";
$id = (int) ($_GET['id'] ?? 0);
$stmt = $conn->prepare("SELECT * FROM anggota WHERE id=?");
$stmt->bind_param("i", $id);
$stmt->execute();
$r = $stmt->get_result()->fetch_assoc();
if (!$r) die("Data tidak ditemukan");
$hobi_arr = json_decode($r['hobi'], true) ?: [];
$ekskul_arr = json_decode($r['ekskul'], true) ?: [];
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// 1. Ambil ulang semua field (sama seperti proses_daftar.php)
$nama = htmlspecialchars(trim($_POST['nama_lengkap']));
$nis = trim($_POST['nis']);
$kelas = trim($_POST['kelas']);
$jk = $_POST['jenis_kelamin'];
$agama = $_POST['agama'];
$tgl_lahir = $_POST['tanggal_lahir'];
$waktu = $_POST['waktu_wawancara'] ?: null;
$jadwal = $_POST['jadwal_wawancara'];
$jadwal = $jadwal ? str_replace("T"," ",$jadwal).":00" : null;
$bulan = $_POST['bulan_angkatan'];
$minggu = $_POST['minggu_orientasi'];
$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
$no_hp = trim($_POST['no_hp']);
$instagram = trim($_POST['instagram']);
$alamat = htmlspecialchars(trim($_POST['alamat']));
$motivasi = htmlspecialchars(trim($_POST['motivasi']));
$referensi = trim($_POST['referensi']);
$jabatan = $_POST['jabatan_diminati'];
$warna = $_POST['warna_kartu'];
$aktif = (int) $_POST['tingkat_keaktifan'];
$hobi = isset($_POST['hobi']) ? json_encode($_POST['hobi'], JSON_UNESCAPED_UNICODE) : json_encode([]);
$ekskul = isset($_POST['ekskul']) ? json_encode($_POST['ekskul'], JSON_UNESCAPED_UNICODE) : json_encode([]);
// 2. Foto: kalau upload baru, ganti; kalau tidak, pakai foto lama
$foto_path = $r['foto'];
if (isset($_FILES['foto']) && $_FILES['foto']['error']==0) {
$ext = strtolower(pathinfo($_FILES['foto']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, ['jpg','jpeg','png'])) die("Foto harus JPG/PNG");
if ($_FILES['foto']['size'] > 2*1024*1024) die("Foto max 2MB");
$nama_file = "foto_".time()."_".uniqid().".".$ext;
$tujuan = "uploads/".$nama_file;
if (!is_dir("uploads")) mkdir("uploads",0755,true);
move_uploaded_file($_FILES['foto']['tmp_name'], $tujuan);
if ($r['foto'] && file_exists($r['foto'])) unlink($r['foto']); // hapus foto lama
$foto_path = $tujuan;
}
// 3. Update (nomor_anggota & status tidak diubah di sini — hanya admin via approve)
$stmt2 = $conn->prepare("UPDATE anggota SET nama_lengkap=?, nis=?, kelas=?, jenis_kelamin=?, agama=?, tanggal_lahir=?, waktu_wawancara=?, jadwal_wawancara=?, bulan_angkatan=?, minggu_orientasi=?, email=?, no_hp=?, instagram=?, alamat=?, warna_kartu=?, tingkat_keaktifan=?, hobi=?, ekskul=?, jabatan_diminati=?, motivasi=?, referensi=?, foto=? WHERE id=?");
$stmt2->bind_param("sssssssssssssssissssssi", $nama,$nis,$kelas,$jk,$agama,$tgl_lahir,$waktu,$jadwal,$bulan,$minggu,$email,$no_hp,$instagram,$alamat,$warna,$aktif,$hobi,$ekskul,$jabatan,$motivasi,$referensi,$foto_path,$id);
if ($stmt2->execute()) { header("Location: dashboard.php"); exit; }
else echo "Gagal update: ".$stmt2->error;
}
?>
<!DOCTYPE html><html lang="id"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Edit Anggota — <?= htmlspecialchars($r['nama_lengkap']) ?></title>
<style>body{font-family:system-ui,sans-serif;max-width:720px;margin:24px auto;padding:0 16px} label{display:block;margin:10px 0} input,select,textarea{width:100%;padding:8px;margin-top:4px;box-sizing:border-box;border:1px solid #999} input[type=radio],input[type=checkbox],input[type=color],input[type=range]{width:auto} fieldset{border:1px solid #ccc;padding:12px;margin:12px 0} button{padding:10px 16px;border:1px solid #000;background:#111;color:#fff;cursor:pointer}</style>
</head><body>
<h2>Edit Anggota: <?= htmlspecialchars($r['nama_lengkap']) ?> <small>(<?= $r['nomor_anggota'] ?: 'Belum ada nomor' ?>)</small></h2>
<form method="POST" enctype="multipart/form-data">
<label>Nama Lengkap <input type="text" name="nama_lengkap" value="<?= htmlspecialchars($r['nama_lengkap']) ?>" required></label>
<label>NIS <input type="text" name="nis" value="<?= htmlspecialchars($r['nis']) ?>" required></label>
<label>Kelas <input list="kelasList" name="kelas" value="<?= htmlspecialchars($r['kelas']) ?>" required><datalist id="kelasList"><option value="X IPA 1"><option value="XII IPA 1"><option value="XII IPS 1"></datalist></label>
<fieldset><legend>Jenis Kelamin</legend>
<label><input type="radio" name="jenis_kelamin" value="L" <?= $r['jenis_kelamin']=='L'?'checked':'' ?> required> Laki-laki</label>
<label><input type="radio" name="jenis_kelamin" value="P" <?= $r['jenis_kelamin']=='P'?'checked':'' ?>> Perempuan</label>
</fieldset>
<label>Agama <select name="agama" required>
<?php foreach(['Islam','Kristen','Katolik','Hindu','Buddha','Konghucu'] as $opt): ?>
<option <?= $r['agama']==$opt?'selected':'' ?>><?= $opt ?></option>
<?php endforeach; ?>
</select></label>
<label>Tanggal Lahir <input type="date" name="tanggal_lahir" value="<?= $r['tanggal_lahir'] ?>" required></label>
<label>Waktu Wawancara <input type="time" name="waktu_wawancara" value="<?= $r['waktu_wawancara'] ?>"></label>
<label>Jadwal Lengkap <input type="datetime-local" name="jadwal_wawancara" value="<?= $r['jadwal_wawancara'] ? str_replace(' ','T',substr($r['jadwal_wawancara'],0,16)) : '' ?>"></label>
<label>Bulan Angkatan <input type="month" name="bulan_angkatan" value="<?= $r['bulan_angkatan'] ?>"></label>
<label>Minggu Orientasi <input type="week" name="minggu_orientasi" value="<?= $r['minggu_orientasi'] ?>"></label>
<label>Email <input type="email" name="email" value="<?= htmlspecialchars($r['email']) ?>" required></label>
<label>No HP <input type="tel" name="no_hp" value="<?= htmlspecialchars($r['no_hp']) ?>" required></label>
<label>Instagram <input type="url" name="instagram" value="<?= htmlspecialchars($r['instagram']) ?>"></label>
<fieldset><legend>Hobi (boleh banyak)</legend>
<?php foreach(['Futsal','Coding','Desain','Musik','Basket'] as $h): ?>
<label><input type="checkbox" name="hobi[]" value="<?= $h ?>" <?= in_array($h,$hobi_arr)?'checked':'' ?>> <?= $h ?></label>
<?php endforeach; ?>
</fieldset>
<label>Ekskul (Ctrl+Klik) <select name="ekskul[]" multiple size="4">
<?php foreach(['Paskibra','PMR','Pramuka','Rohis'] as $e): ?>
<option value="<?= $e ?>" <?= in_array($e,$ekskul_arr)?'selected':'' ?>><?= $e ?></option>
<?php endforeach; ?>
</select></label>
<label>Alamat <textarea name="alamat" rows="3" required><?= htmlspecialchars($r['alamat']) ?></textarea></label>
<label>Motivasi <textarea name="motivasi" rows="4" required><?= htmlspecialchars($r['motivasi']) ?></textarea></label>
<label>Foto (kosongkan jika tidak ganti) <?php if($r['foto']): ?><br><img src="<?= htmlspecialchars($r['foto']) ?>" width="80" style="border:1px solid #000"><?php endif; ?> <input type="file" name="foto" accept=".jpg,.jpeg,.png"></label>
<p><button type="submit">Simpan Perubahan</button> <a href="dashboard.php">Batal</a></p>
</form>
</body></html>
Penjelasan Detail Per Baris edit.php
| Baris | Kode | Artinya untuk Pemula |
|---|---|---|
| 1 | session_start(); if (!isset($_SESSION['admin'])) | Satpam: cuma admin boleh edit. Kalau belum login, usir ke login.php. |
| 2 | $id = (int) $_GET['id'] | Ambil id dari URL edit.php?id=5 dan paksa jadi angka. |
| 3 | SELECT * FROM anggota WHERE id=? + fetch_assoc() | Ambil 1 baris data lama. $r jadi array berisi semua kolom anggota itu. |
| 4 | json_decode($r['hobi'], true) ?: [] | Ubah JSON '["Futsal","Coding"]' jadi array PHP. ?: [] biar kalau null jadi array kosong. |
| 5 | value="<?= htmlspecialchars($r['nama_lengkap']) ?>" | Tampilkan data lama di input. htmlspecialchars biar " tidak rusak HTML. |
| 6 | checked via $r['jenis_kelamin']=='L'?'checked':'' | Kalau data lama L, maka radio L dikasih checked biar nyala. |
| 7 | in_array($h, $hobi_arr)?'checked':'' | Untuk checkbox array: kalau Futsal ada di array hobi lama, centang. |
| 8 | selected via in_array($e,$ekskul_arr) | Sama untuk select multiple: kalau Paskibra pernah dipilih, kasih selected. |
| 9 | str_replace(' ','T', substr(...,0,16)) | DATETIME di DB 2026-09-24 14:30:00 harus diubah jadi 2026-09-24T14:30 untuk datetime-local. |
| 10 | if ($_FILES['foto']['error']==0) { ... unlink($r['foto']) } | Kalau user upload foto baru, simpan baru + hapus foto lama. Kalau tidak upload, pakai $r['foto'] lama. |
| 11 | UPDATE anggota SET ... WHERE id=? | Simpan perubahan. nomor_anggota tidak ikut — biar tidak berantakan, nomor hanya via approve.php. |
7.5 Hapus Anggota (hapus.php) — Full Code
<?php
// hapus.php — hapus permanen + hapus file foto
session_start();
if (!isset($_SESSION['admin'])) die("Akses ditolak");
require "config.php";
$id = (int) ($_GET['id'] ?? 0);
// Ambil foto dulu biar bisa dihapus dari folder
$stmt = $conn->prepare("SELECT foto FROM anggota WHERE id=?");
$stmt->bind_param("i", $id);
$stmt->execute();
$r = $stmt->get_result()->fetch_assoc();
if ($r && $r['foto'] && file_exists($r['foto'])) {
unlink($r['foto']); // hapus file fisik
}
$stmt2 = $conn->prepare("DELETE FROM anggota WHERE id=?");
$stmt2->bind_param("i", $id);
$stmt2->execute();
header("Location: dashboard.php");
exit;
?>
Penjelasan Detail Per Baris hapus.php
| Baris | Kode | Artinya |
|---|---|---|
| 1 | session_start(); if (!isset($_SESSION['admin'])) | Cuma admin boleh hapus. |
| 2 | SELECT foto FROM anggota WHERE id=? | Ambil path foto dulu sebelum baris dihapus, supaya file-nya bisa dihapus juga. |
| 3 | file_exists($r['foto']) && unlink(...) | Kalau file ada di uploads/, hapus. unlink = hapus file fisik, bukan cuma di database. |
| 4 | DELETE FROM anggota WHERE id=? | Hapus baris di database. |
| 5 | header("Location: dashboard.php") | Balik ke dashboard. |
7.6 Logout (logout.php) — Full Code
<?php
// logout.php
session_start();
session_unset(); // kosongkan semua $_SESSION
session_destroy(); // hancurkan sesi di server
header("Location: login.php");
exit;
?>
Penjelasan Detail Per Baris logout.php
| Baris | Kode | Artinya |
|---|---|---|
| 1 | session_start(); | Buka sesi dulu baru bisa dihancurkan. |
| 2 | session_unset(); | Kosongkan semua data di $_SESSION (nama, id, dll). |
| 3 | session_destroy(); | Hancurkan file sesi di server. Gelang login resmi dilepas. |
| 4 | header("Location: login.php") | Tendang balik ke halaman login. |
8. Deploy dari Localhost Sampai Online di Hosting
Vercel/GitHub Pages tidak bisa untuk PHP. Kalian butuh hosting dengan PHP + MySQL.
Opsi gratis untuk belajar: InfinityFree (infinityfree.net) — support PHP 8.3 + MySQL + SSL gratis. Atau berbayar murah: Hostinger, Niagahoster, RumahWeb.
Langkah publish:
- Export Database Lokal: di HeidiSQL/phpMyAdmin Laragon → pilih
organisasi_osis→ Export → SQL. - Daftar Hosting: buat akun InfinityFree → Create Account → catat
FTP Host,Username,Password, dan bukaMySQL Databasesbuat database baru (misalif0_123_osis). - Upload File: buka File Manager di hosting (atau pakai FileZilla FTP) → masuk folder
htdocs→ upload semua fileosis-app(zip lalu extract biar cepat). Pastikanuploads/permission755. - Import Database Hosting: di hosting buka phpMyAdmin → pilih database baru → Import → pilih file
.sqltadi. - Ubah
config.phpdi hosting:
$host = "sqlxxx.infinityfree.com"; // lihat di dashboard hosting
$user = "if0_12345678";
$pass = "password_hosting";
$db = "if0_12345678_osis";
- Aktifkan SSL: di hosting menu SSL → Request Free SSL → aktifkan Force HTTPS.
- Cek
https://osis-sekolahmu.infinityfreeapp.comdi HP pakai data seluler.
Kalau mau custom domain osis-sekolahku.my.id, tinggal arahkan nameserver hosting di tempat beli domain.
9. Error Paling Sering dan Solusinya
| Pesan Error | Penyebab | Solusi 1 Menit |
|---|---|---|
Access denied for user 'root'@'localhost' | Akses ditolak. Password database di config.php tidak cocok dengan MySQL. | Cek Laragon: MySQL user root password kosong. Di hosting, ganti dengan user/pass dari dashboard hosting. |
Headers already sent | Kalian echo dulu baru header(). Ibarat sudah kirim surat, baru mau ganti alamat tujuan — tidak bisa. | Taruh session_start() paling atas, tanpa spasi/echo sebelumnya. |
File too large / foto tidak terupload | Foto 5 MB tapi batas upload server cuma 2 MB. | Di Laragon: Menu → PHP → php.ini → ubah upload_max_filesize=8M dan post_max_size=8M, restart Laragon. |
Undefined array key "hobi" | Tidak pilih checkbox satupun, tapi PHP maksa baca $_POST['hobi']. Ibarat buka keranjang yang tidak ada. | Pakai isset($_POST['hobi']) ? ... : [] |
Duplicate entry 'OSIS-2026-0001' | Nomor anggota kembar karena 2 orang di-approve barengan. | Kolom nomor_anggota sudah UNIQUE, cek logic COUNT+1 di approve.php — lebih aman pakai MAX(id) atau transaksi. |
500 Internal Server Error di hosting | config.php masih pakai localhost. | Ganti host/user/pass DB ke yang dari dashboard hosting, bukan localhost lagi. |
Foto tidak tampil di hosting | Folder uploads/ tidak ada di hosting. | Buat manual folder uploads/ di htdocs dan chmod 755. Foto lama di laptop tidak ikut ke-upload, harus upload ulang. |
10. Best Practice Industri
- Selalu pakai
.envuntuk kredensial kalau sudah naik level (jangan hardcode password diconfig.phpyang ke-push GitHub). Untuk pemula, cukupconfig.phptapi masukkan ke.gitignore. - Validasi 2 lapis: HTML
required+ PHPfilter_var+ MySQLENUM. Jangan percaya input user. Ibarat periksa KTP 3 kali: di gerbang, di resepsionis, di ruang arsip. - Password wajib hash:
password_hash("admin123", PASSWORD_DEFAULT)saat buat admin, bukan plain text. - Gunakan
JSONuntuk array, bukanVARCHARkoma. Lebih aman untuk data seperti"Futsal, Coding"yang ada komanya. - Beri
LIMITdi dashboard kalau anggota sudah 1000+:SELECT ... LIMIT 50+ pagination (materiN8&cara-membuat-pagination-php-mysql-anti-lemot-1-juta-data). - Backup rutin: export
.sqltiap minggu, simpan di Google Drive.
11. Kesimpulan
kita baru saja membangun website dari nol sampai online beneran: dari memahami 24 tipe input HTML dan tipe datanya di MySQL, merakit form OSIS yang menampung string, array checkbox, dropdown, date sampai file, menyimpan dengan prepared statement, sampai bikin login admin yang bisa approve, edit, delete, dan generate nomor anggota otomatis.
alur ini persis yang dipakai di dunia industri untuk PPDB, absensi, hingga kasir: form → database → admin panel → hosting. kalau kalian sudah paham OSIS, ganti label jadi Keanggotaan Karang Taruna atau Koperasi Masyarakat pun tinggal ubah SQL dan label formnya saja.
Pertanyaan yang Sering Diajukan
Apa saja tipe input HTML yang wajib ada di form OSIS ini?
Semua: text, email, tel, url, number, password, textarea, radio, checkbox array, select dropdown, select multiple, datalist, date, time, datetime-local, month, week, color, range, file, hidden, dan search. Masing-masing sudah dipetakan ke tipe data PHP dan kolom MySQL (VARCHAR, TEXT, INT, DATE, JSON) di artikel ini.
Bagaimana cara simpan checkbox dan select multiple yang datanya array?
Gunakan atribut name="hobi[]" dan name="ekskul[]" agar PHP menangkapnya sebagai array $_POST. Lalu simpan ke MySQL sebagai JSON dengan json_encode() ke kolom JSON/TEXT, dan saat tampil kembalikan dengan json_decode(). Jangan simpan dengan implode koma jika butuh query spesifik.
Bagaimana nomor anggota OSIS dibuat otomatis?
Nomor anggota tidak diinput manual. Saat admin klik Approve, sistem generate format OSIS-2026-0001 dengan mengambil id terakhir + padding 4 digit. Nomor disimpan di kolom nomor_anggota VARCHAR(20) UNIQUE agar tidak duplikat.
Kenapa website PHP tidak bisa deploy ke Vercel atau GitHub Pages?
Vercel dan GitHub Pages hanya untuk website statis (HTML/CSS/JS). Website PHP + MySQL butuh server dengan Apache/Nginx + PHP + MySQL seperti InfinityFree, Hostinger, Niagahoster, atau VPS. Di artikel ini langkah upload via File Manager/FTP dan import database dijelaskan lengkap.
Apakah admin bisa edit, hapus, dan approve anggota?
Bisa. Setelah login dengan session + password_hash, admin masuk dashboard untuk melihat semua pendaftar status pending, lalu bisa Edit (ubah data), Delete (hapus permanen), dan Approve (ubah status jadi approved + kunci nomor anggota).
Apa beda domain dan hosting untuk website PHP ini?
Hosting adalah server tempat file PHP dan database MySQL disimpan agar online 24 jam. Domain adalah alamat nama (misal osis-sekolahku.my.id) yang mengarah ke hosting tersebut. Untuk PHP, kalian butuh keduanya + SSL gratis agar bisa diakses via HTTPS.

