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:

FiturDeskripsi
📋 Daftar TamuMenampilkan semua pesan buku tamu
Tambah PesanForm untuk menambah pesan baru (publik)
✏️ Edit PesanMengubah pesan (hanya admin)
🗑️ Hapus PesanMenghapus pesan (hanya admin)
🔐 Login AdminSistem login untuk admin
🔍 PencarianCari 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

  1. Pastikan folder project ada di ~/Herd/buku-tamu/
  2. Buka http://buku-tamu.test/setup.php untuk setup database
  3. Buka http://buku-tamu.test/ — halaman utama
  4. 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!

BabYang Dipelajari
HTMLStruktur halaman, tag, form, semantic HTML
CSSStyling, layout (Flexbox, Grid), responsive design
JavaScriptVariabel, DOM, event handling, validasi form
PHPSintaks, form handling, include/require
DatabaseMySQL, PDO, CRUD, prepared statement
SecurityXSS prevention, SQL Injection prevention, password hashing
SessionLogin/logout, session management

Langkah Selanjutnya

Setelah menguasai dasar-dasar ini, kamu bisa melanjutkan ke:

  1. Framework PHP — Laravel (framework PHP paling populer)
  2. Frontend Framework — React, Vue, atau Tailwind CSS
  3. Version Control — Git dan GitHub
  4. 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!