#Project Akhir: Aplikasi Buku Tamu
Selamat! 🎉 Kamu sudah mempelajari semua komponen web development. Sekarang saatnya menggabungkan semuanya menjadi satu aplikasi web lengkap — Buku Tamu dengan fitur CRUD dan autentikasi.
#Spesifikasi Aplikasi
Aplikasi ini memiliki fitur:
| Fitur | Deskripsi |
|---|---|
| 📋 Daftar Tamu | Menampilkan semua pesan buku tamu |
| ➕ Tambah Pesan | Form untuk menambah pesan baru (publik) |
| ✏️ Edit Pesan | Mengubah pesan (hanya admin) |
| 🗑️ Hapus Pesan | Menghapus pesan (hanya admin) |
| 🔐 Login Admin | Sistem login untuk admin |
| 🔍 Pencarian | Cari pesan berdasarkan nama/isi |
#Struktur Lengkap Project
~/Herd/buku-tamu/
├── config/
│ ├── database.php ← Koneksi database
│ └── auth.php ← Helper autentikasi
├── templates/
│ ├── header.php ← Header & navigasi
│ └── footer.php ← Footer
├── index.php ← Halaman utama (publik)
├── tambah.php ← Form tambah pesan (publik)
├── admin/
│ ├── index.php ← Dashboard admin
│ ├── edit.php ← Edit pesan
│ └── hapus.php ← Hapus pesan
├── login.php ← Halaman login
├── logout.php ← Proses logout
├── setup.php ← Setup database & user awal
└── style.css ← Global stylesheet#Langkah 1: Setup Database
Buat file setup.php — jalankan sekali untuk membuat tabel dan user admin:
<?php
// setup.php — Jalankan sekali untuk setup database
$host = 'localhost';
$username = 'root';
$password = '';
try {
// Koneksi tanpa database dulu
$pdo = new PDO("mysql:host=$host;charset=utf8mb4", $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
// Buat database
$pdo->exec("CREATE DATABASE IF NOT EXISTS buku_tamu_app");
$pdo->exec("USE buku_tamu_app");
// Buat tabel pesan
$pdo->exec("CREATE TABLE IF NOT EXISTS pesan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
pesan TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
// Buat tabel users
$pdo->exec("CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
nama_lengkap VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
// Cek apakah admin sudah ada
$stmt = $pdo->query("SELECT COUNT(*) FROM users WHERE username = 'admin'");
if ($stmt->fetchColumn() == 0) {
// Buat user admin
$hash = password_hash('admin123', PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (username, password, nama_lengkap) VALUES (?, ?, ?)");
$stmt->execute(['admin', $hash, 'Administrator']);
}
// Tambah data contoh
$stmt = $pdo->query("SELECT COUNT(*) FROM pesan");
if ($stmt->fetchColumn() == 0) {
$contoh = [
['Ahmad Fauzi', 'ahmad@mail.com', 'Website-nya bagus dan informatif! Terima kasih.'],
['Siti Nurhaliza', 'siti@mail.com', 'Bermanfaat sekali materinya, mudah dipahami.'],
['Budi Santoso', 'budi@mail.com', 'Kapan ada tutorial Laravel? Ditunggu ya!'],
['Dewi Lestari', 'dewi@mail.com', 'Penjelasannya detail, cocok untuk pemula.'],
];
$stmt = $pdo->prepare("INSERT INTO pesan (nama, email, pesan) VALUES (?, ?, ?)");
foreach ($contoh as $data) {
$stmt->execute($data);
}
}
echo "<!DOCTYPE html><html><head><style>
body { font-family: sans-serif; display: flex; justify-content: center;
align-items: center; height: 100vh; background: #f0f2f5; }
.card { background: white; padding: 40px; border-radius: 16px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1); text-align: center; }
h1 { color: #28a745; } a { color: #667eea; }
</style></head><body><div class='card'>
<h1>✅ Setup Berhasil!</h1>
<p>Database <strong>buku_tamu_app</strong> sudah siap.</p>
<p>User admin: <strong>admin</strong> / <strong>admin123</strong></p>
<br><a href='index.php'>Buka Aplikasi →</a>
</div></body></html>";
} catch (PDOException $e) {
echo "❌ Error: " . $e->getMessage();
}Buka http://buku-tamu.test/setup.php di browser untuk menjalankan setup.
#Langkah 2: File Konfigurasi
#config/database.php
<?php
$host = 'localhost';
$dbname = 'buku_tamu_app';
$username = 'root';
$password = '';
try {
$pdo = new PDO(
"mysql:host=$host;dbname=$dbname;charset=utf8mb4",
$username,
$password,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
die("❌ Koneksi gagal: " . $e->getMessage());
}#config/auth.php
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
function cekLogin() {
if (!isset($_SESSION['user_id'])) {
header("Location: /login.php");
exit;
}
}
function isLogin() {
return isset($_SESSION['user_id']);
}
function namaUser() {
return $_SESSION['nama_lengkap'] ?? 'Guest';
}#Langkah 3: Templates
#templates/header.php
<?php require_once __DIR__ . '/../config/auth.php'; ?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($title ?? 'Buku Tamu') ?></title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<nav>
<a href="/" class="logo">📖 Buku Tamu</a>
<ul class="menu">
<li><a href="/">Beranda</a></li>
<li><a href="/tambah.php">Tulis Pesan</a></li>
<?php if (isLogin()): ?>
<li><a href="/admin/">Dashboard</a></li>
<li class="user-info">
👤 <?= htmlspecialchars(namaUser()) ?>
<a href="/logout.php" class="btn-logout">Logout</a>
</li>
<?php else: ?>
<li><a href="/login.php">Admin Login</a></li>
<?php endif; ?>
</ul>
</nav>
<main class="container">#templates/footer.php
</main>
<footer>
<p>📖 Aplikasi Buku Tamu — Dibuat dengan PHP & MySQL</p>
<p>Project akhir Belajar Web Development</p>
</footer>
</body>
</html>#Langkah 4: Halaman Publik
#index.php — Halaman Utama
<?php
require 'config/database.php';
$title = 'Buku Tamu — Beranda';
// Pencarian
$cari = trim($_GET['cari'] ?? '');
if ($cari !== '') {
$stmt = $pdo->prepare(
"SELECT * FROM pesan WHERE nama LIKE ? OR pesan LIKE ? ORDER BY created_at DESC"
);
$keyword = "%$cari%";
$stmt->execute([$keyword, $keyword]);
} else {
$stmt = $pdo->query("SELECT * FROM pesan ORDER BY created_at DESC");
}
$daftarPesan = $stmt->fetchAll();
require 'templates/header.php';
?>
<div class="hero-section">
<h1>📖 Buku Tamu</h1>
<p>Tulis pesan dan kenangan kamu di sini!</p>
</div>
<!-- Form Pencarian -->
<form method="GET" class="search-form">
<input type="text" name="cari" value="<?= htmlspecialchars($cari) ?>"
placeholder="Cari pesan...">
<button type="submit">🔍 Cari</button>
<?php if ($cari): ?>
<a href="/" class="btn btn-secondary">✕ Reset</a>
<?php endif; ?>
</form>
<?php if ($cari): ?>
<p class="search-info">
Hasil pencarian "<strong><?= htmlspecialchars($cari) ?></strong>"
— ditemukan <?= count($daftarPesan) ?> pesan
</p>
<?php endif; ?>
<!-- Daftar Pesan -->
<div class="pesan-list">
<?php if (empty($daftarPesan)): ?>
<div class="empty-state">
<p>😊 Belum ada pesan.</p>
<a href="/tambah.php" class="btn btn-primary">Tulis Pesan Pertama →</a>
</div>
<?php else: ?>
<?php foreach ($daftarPesan as $pesan): ?>
<div class="pesan-card">
<div class="pesan-header">
<div class="pesan-avatar">
<?= strtoupper(substr($pesan['nama'], 0, 1)) ?>
</div>
<div>
<strong><?= htmlspecialchars($pesan['nama']) ?></strong>
<small><?= date('d M Y, H:i', strtotime($pesan['created_at'])) ?></small>
</div>
</div>
<p class="pesan-isi"><?= nl2br(htmlspecialchars($pesan['pesan'])) ?></p>
<?php if (isLogin()): ?>
<div class="pesan-actions">
<a href="/admin/edit.php?id=<?= $pesan['id'] ?>" class="btn btn-sm btn-warning">✏️ Edit</a>
<a href="/admin/hapus.php?id=<?= $pesan['id'] ?>" class="btn btn-sm btn-danger"
onclick="return confirm('Hapus pesan dari <?= htmlspecialchars($pesan['nama']) ?>?')">🗑️</a>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<div style="text-align: center; margin-top: 20px;">
<a href="/tambah.php" class="btn btn-primary">✍️ Tulis Pesan Baru</a>
</div>
<?php require 'templates/footer.php'; ?>#tambah.php — Tulis Pesan (Publik)
<?php
require 'config/database.php';
$title = 'Tulis Pesan Baru';
$nama = '';
$email = '';
$pesan = '';
$errors = [];
$sukses = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nama = htmlspecialchars(trim($_POST['nama'] ?? ''));
$email = filter_var(trim($_POST['email'] ?? ''), FILTER_SANITIZE_EMAIL);
$pesan = htmlspecialchars(trim($_POST['pesan'] ?? ''));
if (empty($nama)) $errors[] = "Nama wajib diisi";
if (strlen($nama) < 2) $errors[] = "Nama minimal 2 karakter";
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = "Email tidak valid";
if (empty($pesan)) $errors[] = "Pesan wajib diisi";
if (strlen($pesan) < 10) $errors[] = "Pesan minimal 10 karakter";
if (empty($errors)) {
$stmt = $pdo->prepare("INSERT INTO pesan (nama, email, pesan) VALUES (?, ?, ?)");
$stmt->execute([$nama, $email, $pesan]);
$sukses = true;
$nama = '';
$email = '';
$pesan = '';
}
}
require 'templates/header.php';
?>
<h1>✍️ Tulis Pesan</h1>
<?php if ($sukses): ?>
<div class="alert alert-success">
✅ Pesan kamu berhasil disimpan! Terima kasih.
<br><a href="/">← Lihat semua pesan</a>
</div>
<?php endif; ?>
<?php if (!empty($errors)): ?>
<div class="alert alert-error">
<?php foreach ($errors as $error): ?>
<p>⚠️ <?= $error ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!$sukses): ?>
<div class="form-card">
<form method="POST">
<div class="form-group">
<label for="nama">Nama</label>
<input type="text" id="nama" name="nama" value="<?= $nama ?>"
placeholder="Nama lengkap kamu" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" value="<?= $email ?>"
placeholder="contoh@email.com" required>
</div>
<div class="form-group">
<label for="pesan">Pesan</label>
<textarea id="pesan" name="pesan" rows="5"
placeholder="Tulis pesan, kesan, atau saran kamu di sini (minimal 10 karakter)..."
required><?= $pesan ?></textarea>
</div>
<button type="submit" class="btn-submit">📨 Kirim Pesan</button>
</form>
</div>
<?php endif; ?>
<?php require 'templates/footer.php'; ?>#Langkah 5: Halaman Admin
#admin/index.php — Dashboard Admin
<?php
require __DIR__ . '/../config/database.php';
require __DIR__ . '/../config/auth.php';
cekLogin();
$title = 'Dashboard Admin';
// Statistik
$totalPesan = $pdo->query("SELECT COUNT(*) FROM pesan")->fetchColumn();
$pesanHariIni = $pdo->query(
"SELECT COUNT(*) FROM pesan WHERE DATE(created_at) = CURDATE()"
)->fetchColumn();
// 5 pesan terbaru
$stmt = $pdo->query("SELECT * FROM pesan ORDER BY created_at DESC LIMIT 5");
$pesanTerbaru = $stmt->fetchAll();
require __DIR__ . '/../templates/header.php';
?>
<h1>📊 Dashboard Admin</h1>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number"><?= $totalPesan ?></div>
<div class="stat-label">Total Pesan</div>
</div>
<div class="stat-card">
<div class="stat-number"><?= $pesanHariIni ?></div>
<div class="stat-label">Pesan Hari Ini</div>
</div>
</div>
<h2 style="margin-top: 30px;">📝 Pesan Terbaru</h2>
<table>
<thead>
<tr>
<th>Nama</th>
<th>Pesan</th>
<th>Waktu</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($pesanTerbaru as $p): ?>
<tr>
<td>
<strong><?= htmlspecialchars($p['nama']) ?></strong><br>
<small style="color:#999"><?= htmlspecialchars($p['email']) ?></small>
</td>
<td><?= htmlspecialchars(mb_strimwidth($p['pesan'], 0, 50, '...')) ?></td>
<td><small><?= date('d M Y H:i', strtotime($p['created_at'])) ?></small></td>
<td>
<div class="actions">
<a href="edit.php?id=<?= $p['id'] ?>" class="btn btn-sm btn-warning">✏️</a>
<a href="hapus.php?id=<?= $p['id'] ?>" class="btn btn-sm btn-danger"
onclick="return confirm('Hapus?')">🗑️</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p style="margin-top: 15px;">
<a href="/">Lihat semua pesan di halaman publik →</a>
</p>
<?php require __DIR__ . '/../templates/footer.php'; ?>#admin/edit.php — Edit Pesan
<?php
require __DIR__ . '/../config/database.php';
require __DIR__ . '/../config/auth.php';
cekLogin();
$title = 'Edit Pesan';
$errors = [];
$id = (int) ($_GET['id'] ?? 0);
$stmt = $pdo->prepare("SELECT * FROM pesan WHERE id = ?");
$stmt->execute([$id]);
$data = $stmt->fetch();
if (!$data) {
header("Location: /admin/");
exit;
}
$nama = $data['nama'];
$email = $data['email'];
$pesan = $data['pesan'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nama = htmlspecialchars(trim($_POST['nama'] ?? ''));
$email = filter_var(trim($_POST['email'] ?? ''), FILTER_SANITIZE_EMAIL);
$pesan = htmlspecialchars(trim($_POST['pesan'] ?? ''));
if (empty($nama)) $errors[] = "Nama wajib diisi";
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = "Email tidak valid";
if (empty($pesan)) $errors[] = "Pesan wajib diisi";
if (empty($errors)) {
$stmt = $pdo->prepare("UPDATE pesan SET nama = ?, email = ?, pesan = ? WHERE id = ?");
$stmt->execute([$nama, $email, $pesan, $id]);
header("Location: /admin/?sukses=edit");
exit;
}
}
require __DIR__ . '/../templates/header.php';
?>
<h1>✏️ Edit Pesan #<?= $id ?></h1>
<?php if (!empty($errors)): ?>
<div class="alert alert-error">
<?php foreach ($errors as $error): ?>
<p>⚠️ <?= $error ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="form-card">
<form method="POST">
<div class="form-group">
<label for="nama">Nama</label>
<input type="text" id="nama" name="nama" value="<?= $nama ?>" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" value="<?= $email ?>" required>
</div>
<div class="form-group">
<label for="pesan">Pesan</label>
<textarea id="pesan" name="pesan" rows="5" required><?= $pesan ?></textarea>
</div>
<button type="submit" class="btn-submit">💾 Simpan Perubahan</button>
</form>
<p style="margin-top: 15px;">
<a href="/admin/">← Kembali ke Dashboard</a>
</p>
</div>
<?php require __DIR__ . '/../templates/footer.php'; ?>#admin/hapus.php — Hapus Pesan
<?php
require __DIR__ . '/../config/database.php';
require __DIR__ . '/../config/auth.php';
cekLogin();
$id = (int) ($_GET['id'] ?? 0);
if ($id > 0) {
$stmt = $pdo->prepare("DELETE FROM pesan WHERE id = ?");
$stmt->execute([$id]);
}
header("Location: /admin/?sukses=hapus");
exit;#Langkah 6: Stylesheet Lengkap
Update style.css dengan style tambahan:
/* ===== Tambahan style untuk project akhir ===== */
/* Hero Section */
.hero-section {
text-align: center;
padding: 40px 20px;
background: linear-gradient(135deg, #667eea22, #764ba222);
border-radius: 12px;
margin-bottom: 25px;
}
.hero-section h1 { color: #667eea; font-size: 2em; }
.hero-section p { color: #666; margin-top: 8px; }
/* Search */
.search-form {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.search-form input {
flex: 1;
padding: 10px 15px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 15px;
}
.search-form input:focus { outline: none; border-color: #667eea; }
.search-form button { padding: 10px 20px; }
.search-info { color: #666; margin-bottom: 15px; }
/* Pesan Cards */
.pesan-list { display: flex; flex-direction: column; gap: 15px; }
.pesan-card {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}
.pesan-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.pesan-avatar {
width: 44px; height: 44px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
display: flex; align-items: center; justify-content: center;
font-weight: bold; font-size: 18px;
}
.pesan-header strong { display: block; color: #333; }
.pesan-header small { color: #999; }
.pesan-isi { color: #555; line-height: 1.7; }
.pesan-actions { margin-top: 12px; display: flex; gap: 8px; }
/* Stats */
.stats-grid { display: flex; gap: 20px; }
.stat-card {
flex: 1;
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
text-align: center;
}
.stat-number { font-size: 36px; font-weight: bold; color: #667eea; }
.stat-label { color: #999; margin-top: 5px; }
/* Empty State */
.empty-state { text-align: center; padding: 40px; color: #999; }
/* Btn Secondary */
.btn-secondary { background: #6c757d; color: white; }
.btn-secondary:hover { background: #5a6268; }
/* Btn Small */
.btn-sm { padding: 4px 10px; font-size: 13px; }
/* Btn Logout */
.btn-logout {
color: rgba(255,255,255,0.8);
margin-left: 8px;
}
.btn-logout:hover { color: white; }
/* User Info */
.user-info {
display: flex;
align-items: center;
gap: 5px;
}
/* Responsive */
@media (max-width: 768px) {
nav { flex-direction: column; gap: 10px; }
.menu { flex-wrap: wrap; justify-content: center; gap: 10px; }
.stats-grid { flex-direction: column; }
.search-form { flex-direction: column; }
}#Menjalankan Aplikasi
- Pastikan folder project ada di
~/Herd/buku-tamu/ - Buka
http://buku-tamu.test/setup.phpuntuk setup database - Buka
http://buku-tamu.test/— halaman utama - Login admin:
http://buku-tamu.test/login.php(admin / admin123)
#Apa yang Sudah Kamu Pelajari?
Selamat! 🎉🎉🎉 Kamu sudah menyelesaikan perjalanan belajar web development dari nol!
| Bab | Yang Dipelajari |
|---|---|
| HTML | Struktur halaman, tag, form, semantic HTML |
| CSS | Styling, layout (Flexbox, Grid), responsive design |
| JavaScript | Variabel, DOM, event handling, validasi form |
| PHP | Sintaks, form handling, include/require |
| Database | MySQL, PDO, CRUD, prepared statement |
| Security | XSS prevention, SQL Injection prevention, password hashing |
| Session | Login/logout, session management |
#Langkah Selanjutnya
Setelah menguasai dasar-dasar ini, kamu bisa melanjutkan ke:
- Framework PHP — Laravel (framework PHP paling populer)
- Frontend Framework — React, Vue, atau Tailwind CSS
- Version Control — Git dan GitHub
- Deployment — Cara deploy website ke internet
Terus Berlatih!
Cara terbaik untuk belajar programming adalah membuat project. Coba buat:
- 📝 Blog sederhana
- 🛒 Toko online mini
- 📋 Aplikasi manajemen tugas
- 📊 Dashboard statistik
Setiap project akan mengajarkan hal baru dan memperkuat pemahaman kamu!