# Tutorial Hari 10: Halaman Publik & Export

## 🎯 Tujuan
- Membuat halaman publik (index.php)
- Membuat halaman settings
- Membuat fitur ekspor data
- Menyelesaikan aplikasi WebGIS

---

## 📚 Teori: Single Page vs Multi Page

### Single Page Application (SPA)

```
index.html
    ↓
JavaScript mengubah konten
    ↓
Tidak ada refresh
```

### Multi Page Application (Kita pakai ini)

```
index.php (halaman publik)
login.php (halaman login)
dashboard.php (admin)
    ↓
Setiap halaman = file terpisah
```

| Aspek | SPA | Multi-Page |
|-------|-----|------------|
| Kecepatan | Lebih cepat setelah load | Perlu load setiap halaman |
| SEO | ⚠️ Perlu konfigurasi | ✅ Natural |
| Kompleksitas | Tinggi | Rendah |
| PHP Support | ❌ Butuh API | ✅ Native |

---

## 1. Buat index.php (Halaman Publik)

### Langkah 1: Buat file di folder utama

1. Klik kanan folder **pemetaan-desa** (folder utama)
2. New File → `index.php`

### Langkah 2: Tulis Kode

```php
<?php
/**
 * Halaman Publik (Home)
 * Bisa diakses tanpa login
 */

require_once __DIR__ . '/lib/auth.php';
require_once __DIR__ . '/lib/spatial.php';
require_once __DIR__ . '/lib/helpers.php';

// Ambil peta pertama
$stmt = db()->prepare("SELECT * FROM peta_desa ORDER BY id LIMIT 1");
$stmt->execute();
$peta = $stmt->fetch();

$petaNama = 'Peta Desa';
$geojsonData = '{}';
$fasilitasGeoJSON = ['type' => 'FeatureCollection', 'features' => []];
$jenisCount = [];
$totalFasilitas = 0;

if ($peta) {
    $petaId = $peta['id'];
    $petaNama = $peta['nama_peta'];
    $geojsonData = $peta['geojson_data'] ?? '{}';
    $fasilitasGeoJSON = getFasilitasGeoJSON($petaId);
    $jenisCount = getFasilitasCountByType($petaId);
    $totalFasilitas = array_sum($jenisCount);
}

$fasilitasTypes = [
    'pendidikan' => ['icon' => 'fa-school', 'color' => '#3b82f6', 'label' => 'Pendidikan'],
    'kesehatan' => ['icon' => 'fa-hospital', 'color' => '#ef4444', 'label' => 'Kesehatan'],
    'ibadah' => ['icon' => 'fa-place-of-worship', 'color' => '#22c55e', 'label' => 'Ibadah'],
    'pemerintahan' => ['icon' => 'fa-landmark', 'color' => '#f59e0b', 'label' => 'Pemerintahan'],
    'umkm' => ['icon' => 'fa-store', 'color' => '#ec4899', 'label' => 'UMKM'],
    'wisata' => ['icon' => 'fa-mountain-sun', 'color' => '#8b5cf6', 'label' => 'Wisata']
];
?>
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= sanitize($petaNama) ?> - WebGIS Pemetaan Desa</title>
    <meta name="description" content="Peta interaktif desa dengan informasi fasilitas umum">
    
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
    <link rel="stylesheet" href="/assets/css/style.css">
    
    <style>
        html { scroll-behavior: smooth; }
        
        .main-navbar {
            position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
            background: transparent;
            padding: 1rem 2rem;
            display: flex; justify-content: space-between; align-items: center;
            transition: all 0.3s;
        }
        .main-navbar.scrolled {
            background: rgba(15, 23, 42, 0.95);
            backdrop-filter: blur(10px);
            border-bottom: 1px solid rgba(255,255,255,0.1);
        }
        .navbar-brand { color: white; text-decoration: none; font-weight: 700; font-size: 1.25rem; display: flex; align-items: center; gap: 0.5rem; }
        .nav-link { color: rgba(255,255,255,0.8); text-decoration: none; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.875rem; }
        .nav-link:hover { color: white; background: rgba(255,255,255,0.1); }
        .btn-login { background: white; color: #1e293b; padding: 0.5rem 1rem; border-radius: 0.5rem; text-decoration: none; font-weight: 500; }
        
        .hero {
            background: linear-gradient(135deg, #0f172a 0%, #1e3a8a 40%, #2563eb 70%, #3b82f6 100%);
            color: white; padding: 8rem 1rem 6rem; text-align: center;
        }
        .hero h1 { font-size: clamp(2rem, 5vw, 3.5rem); font-weight: 800; margin: 0 0 1rem; }
        .hero p { font-size: 1.125rem; opacity: 0.9; max-width: 600px; margin: 0 auto 2rem; }
        .hero-stats { display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap; }
        .hero-stat { background: rgba(255,255,255,0.1); padding: 1.25rem 2rem; border-radius: 1rem; }
        .hero-stat h3 { font-size: 2rem; margin: 0; }
        .hero-stat p { margin: 0; font-size: 0.875rem; opacity: 0.8; }
        
        .section { padding: 4rem 1rem; max-width: 1200px; margin: 0 auto; }
        .section-title { text-align: center; margin-bottom: 3rem; }
        .section-title h2 { font-size: 2rem; margin: 0 0 0.5rem; color: #1e293b; }
        .section-title p { color: #64748b; }
        
        .facility-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1.5rem; }
        .facility-card {
            background: white; border-radius: 1rem; padding: 1.5rem; text-align: center;
            box-shadow: 0 4px 6px rgba(0,0,0,0.05); cursor: pointer; transition: all 0.3s;
            border: 2px solid transparent;
        }
        .facility-card:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
        .facility-card.active { border-color: #2563eb; background: #eff6ff; }
        .facility-icon { width: 56px; height: 56px; border-radius: 1rem; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: white; font-size: 1.5rem; }
        .facility-card h3 { font-size: 1.75rem; margin: 0; color: #1e293b; }
        .facility-card h4 { font-size: 1rem; margin: 0.5rem 0 0.25rem; color: #374151; }
        
        .map-section { background: white; border-radius: 1rem; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.05); margin-bottom: 2rem; }
        .map-header { padding: 1rem 1.5rem; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e2e8f0; }
        #map { height: 500px; }
        
        .footer { background: #0f172a; color: white; padding: 3rem 1rem; text-align: center; }
        .footer p { margin: 0; opacity: 0.7; }
    </style>
</head>
<body style="background: #f1f5f9;">
    <!-- NAVBAR -->
    <nav class="main-navbar" id="mainNavbar">
        <a href="#" class="navbar-brand"><i class="fas fa-map-marked-alt"></i> <?= sanitize($petaNama) ?></a>
        <div style="display: flex; gap: 0.5rem;">
            <a href="#fasilitas" class="nav-link">Fasilitas</a>
            <a href="#peta" class="nav-link">Peta</a>
            <a href="/pages/login.php" class="btn-login"><i class="fas fa-sign-in-alt"></i> Login Admin</a>
        </div>
    </nav>
    
    <!-- HERO -->
    <section class="hero">
        <h1><?= sanitize($petaNama) ?></h1>
        <p>Peta interaktif dengan informasi lengkap fasilitas umum desa.</p>
        <div class="hero-stats">
            <div class="hero-stat"><h3><?= $totalFasilitas ?></h3><p>Total Fasilitas</p></div>
            <div class="hero-stat"><h3><?= count($jenisCount) ?></h3><p>Kategori</p></div>
        </div>
    </section>
    
    <!-- FACILITIES -->
    <section class="section" id="fasilitas">
        <div class="section-title"><h2>Fasilitas Desa</h2><p>Klik kategori untuk filter</p></div>
        <div class="facility-grid">
            <?php foreach ($fasilitasTypes as $key => $type): ?>
                <div class="facility-card" data-type="<?= $key ?>" onclick="filterByType('<?= $key ?>')">
                    <div class="facility-icon" style="background: <?= $type['color'] ?>"><i class="fas <?= $type['icon'] ?>"></i></div>
                    <h3><?= $jenisCount[$key] ?? 0 ?></h3>
                    <h4><?= $type['label'] ?></h4>
                </div>
            <?php endforeach; ?>
        </div>
    </section>
    
    <!-- MAP -->
    <section class="section" id="peta">
        <div class="map-section">
            <div class="map-header">
                <h3><i class="fas fa-map text-primary"></i> Peta Interaktif</h3>
                <div>
                    <button onclick="fitMapBounds()" class="btn btn-secondary btn-sm"><i class="fas fa-expand"></i> Zoom</button>
                    <button onclick="resetFilter()" class="btn btn-primary btn-sm"><i class="fas fa-sync"></i> Reset</button>
                </div>
            </div>
            <div id="map"></div>
        </div>
    </section>
    
    <!-- FOOTER -->
    <footer class="footer">
        <p>&copy; <?= date('Y') ?> WebGIS Pemetaan Desa. Dibuat dengan ❤️</p>
    </footer>
    
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <script>
        // Navbar scroll effect
        window.addEventListener('scroll', () => {
            document.getElementById('mainNavbar').classList.toggle('scrolled', window.scrollY > 50);
        });
        
        let map, boundaryLayer, allMarkers = [], currentFilter = null;
        const fasilitasTypes = {
            pendidikan: { icon: 'fa-school', color: '#3b82f6' },
            kesehatan: { icon: 'fa-hospital', color: '#ef4444' },
            ibadah: { icon: 'fa-place-of-worship', color: '#22c55e' },
            pemerintahan: { icon: 'fa-landmark', color: '#f59e0b' },
            umkm: { icon: 'fa-store', color: '#ec4899' },
            wisata: { icon: 'fa-mountain-sun', color: '#8b5cf6' }
        };
        
        document.addEventListener('DOMContentLoaded', function() {
            map = L.map('map').setView([<?= $peta['center_lat'] ?? -6.2 ?>, <?= $peta['center_lng'] ?? 106.8 ?>], <?= $peta['zoom_level'] ?? 13 ?>);
            L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
            
            const boundaryData = <?= $geojsonData ?: '{}' ?>;
            if (boundaryData?.features?.length > 0) {
                boundaryLayer = L.geoJSON(boundaryData, { style: { color: '#2563eb', weight: 3, fillOpacity: 0.1 } }).addTo(map);
                map.fitBounds(boundaryLayer.getBounds());
            }
            
            const fasilitasData = <?= json_encode($fasilitasGeoJSON) ?>;
            fasilitasData?.features?.forEach(f => {
                const [lng, lat] = f.geometry.coordinates;
                const type = f.properties.jenis || 'wisata';
                const cfg = fasilitasTypes[type];
                const marker = L.marker([lat, lng], {
                    icon: L.divIcon({
                        className: 'marker',
                        html: `<div style="background:${cfg.color};width:36px;height:36px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);display:flex;align-items:center;justify-content:center;border:3px solid white;box-shadow:0 2px 5px rgba(0,0,0,0.3);"><i class="fas ${cfg.icon}" style="transform:rotate(45deg);color:white;font-size:14px;"></i></div>`,
                        iconSize: [36, 36], iconAnchor: [18, 36]
                    })
                }).addTo(map).bindPopup(`<strong>${f.properties.nama}</strong><br><small>${type}</small>`);
                allMarkers.push({ marker, type });
            });
        });
        
        function filterByType(type) {
            if (currentFilter === type) { resetFilter(); return; }
            currentFilter = type;
            allMarkers.forEach(m => m.type === type ? m.marker.addTo(map) : map.removeLayer(m.marker));
            document.querySelectorAll('.facility-card').forEach(c => c.classList.toggle('active', c.dataset.type === type));
        }
        
        function resetFilter() {
            currentFilter = null;
            allMarkers.forEach(m => m.marker.addTo(map));
            document.querySelectorAll('.facility-card').forEach(c => c.classList.remove('active'));
        }
        
        function fitMapBounds() { if (boundaryLayer) map.fitBounds(boundaryLayer.getBounds()); }
    </script>
</body>
</html>
```

---

## 2. Buat settings.php

Buat file `pages/settings.php`:

```php
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/helpers.php';

requireAuth();
$user = getCurrentUser();

$message = '';
$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $action = $_POST['action'] ?? '';
    
    if ($action === 'update_profile') {
        $username = trim($_POST['username'] ?? '');
        if ($username && $username !== $user['username']) {
            $stmt = db()->prepare("UPDATE users SET username = ? WHERE id = ?");
            $stmt->execute([$username, $user['id']]);
            $message = 'Username berhasil diupdate!';
            $user = getCurrentUser();
        }
    }
    
    if ($action === 'update_password') {
        $current = $_POST['current_password'] ?? '';
        $new = $_POST['new_password'] ?? '';
        
        $stmt = db()->prepare("SELECT password_hash FROM users WHERE id = ?");
        $stmt->execute([$user['id']]);
        $userData = $stmt->fetch();
        
        if (password_verify($current, $userData['password_hash'])) {
            $hash = password_hash($new, PASSWORD_BCRYPT);
            $stmt = db()->prepare("UPDATE users SET password_hash = ? WHERE id = ?");
            $stmt->execute([$hash, $user['id']]);
            $message = 'Password berhasil diubah!';
        } else {
            $error = 'Password saat ini salah!';
        }
    }
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pengaturan - WebGIS</title>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
    <link rel="stylesheet" href="/assets/css/style.css">
</head>
<body style="background: #f1f5f9; font-family: 'Inter', sans-serif;">
    <nav class="navbar">
        <div class="container nav-container">
            <a href="/pages/dashboard.php" class="navbar-brand"><i class="fas fa-arrow-left"></i> Kembali</a>
            <span>Pengaturan Akun</span>
            <span></span>
        </div>
    </nav>
    
    <main style="max-width: 600px; margin: 2rem auto; padding: 0 1rem;">
        <?php if ($message): ?><div class="alert alert-success"><?= $message ?></div><?php endif; ?>
        <?php if ($error): ?><div class="alert alert-danger"><?= $error ?></div><?php endif; ?>
        
        <div class="card" style="margin-bottom: 1.5rem;">
            <div class="card-header"><i class="fas fa-user"></i> Profil</div>
            <div class="card-body">
                <form method="POST">
                    <input type="hidden" name="action" value="update_profile">
                    <div class="form-group">
                        <label class="form-label">Username</label>
                        <input type="text" name="username" class="form-control" value="<?= sanitize($user['username']) ?>">
                    </div>
                    <div class="form-group">
                        <label class="form-label">Email</label>
                        <input type="email" class="form-control" value="<?= sanitize($user['email']) ?>" disabled>
                        <small style="color: #64748b;">Email tidak bisa diubah</small>
                    </div>
                    <button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> Simpan</button>
                </form>
            </div>
        </div>
        
        <div class="card">
            <div class="card-header"><i class="fas fa-lock"></i> Ubah Password</div>
            <div class="card-body">
                <form method="POST">
                    <input type="hidden" name="action" value="update_password">
                    <div class="form-group">
                        <label class="form-label">Password Saat Ini</label>
                        <input type="password" name="current_password" class="form-control" required>
                    </div>
                    <div class="form-group">
                        <label class="form-label">Password Baru</label>
                        <input type="password" name="new_password" class="form-control" required minlength="6">
                    </div>
                    <button type="submit" class="btn btn-primary"><i class="fas fa-key"></i> Ubah Password</button>
                </form>
            </div>
        </div>
    </main>
</body>
</html>
```

---

## 3. Buat ekspor.php

Buat file `pages/ekspor.php`:

```php
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/spatial.php';
require_once __DIR__ . '/../lib/helpers.php';

requireAuth();
$userId = getCurrentUserId();

$petaId = $_GET['id'] ?? null;
$peta = $petaId ? getPetaById($petaId, $userId) : null;

if (!$peta) {
    header('Location: /pages/dashboard.php');
    exit;
}

// Export GeoJSON
if (isset($_GET['export']) && $_GET['export'] === 'geojson') {
    header('Content-Type: application/json');
    header('Content-Disposition: attachment; filename="fasilitas.geojson"');
    echo json_encode(getFasilitasGeoJSON($petaId), JSON_PRETTY_PRINT);
    exit;
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ekspor Data - WebGIS</title>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
    <link rel="stylesheet" href="/assets/css/style.css">
</head>
<body style="background: #f1f5f9; font-family: 'Inter', sans-serif;">
    <nav class="navbar">
        <div class="container nav-container">
            <a href="/pages/dashboard.php" class="navbar-brand"><i class="fas fa-arrow-left"></i> Kembali</a>
            <span>Ekspor Data</span>
            <span></span>
        </div>
    </nav>
    
    <main style="max-width: 600px; margin: 2rem auto; padding: 0 1rem;">
        <div class="card">
            <div class="card-header"><i class="fas fa-download"></i> Pilih Format Ekspor</div>
            <div class="card-body">
                <a href="?id=<?= $petaId ?>&export=geojson" class="btn btn-primary" style="margin-bottom: 1rem; display: block; text-align: center;">
                    <i class="fas fa-file-code"></i> Download GeoJSON
                </a>
                <p style="color: #64748b; font-size: 0.875rem;">
                    GeoJSON adalah format standar untuk data geografis. Bisa dibuka di QGIS, ArcGIS, atau library peta lainnya.
                </p>
            </div>
        </div>
    </main>
</body>
</html>
```

---

## 📁 Struktur Final Project

```
📁 pemetaan-desa/
├── 📁 assets/
│   ├── 📁 css/
│   │   └── 📄 style.css
│   └── 📁 js/
├── 📁 config/
│   └── 📄 database.php
├── 📁 database/
│   └── 📄 schema.sql
├── 📁 lib/
│   ├── 📄 auth.php
│   ├── 📄 helpers.php
│   └── 📄 spatial.php
├── 📁 pages/
│   ├── 📄 dashboard.php
│   ├── 📄 edit-peta.php
│   ├── 📄 ekspor.php
│   ├── 📄 login.php
│   └── 📄 settings.php
├── 📁 process/
│   └── 📄 logout.php
├── 📁 tutorial/
│   ├── 📄 hari1.md - hari10.md
└── 📄 index.php
```

---

## 🎉 Selamat! Tutorial Selesai!

### Fitur yang Dibuat:

| # | Fitur | File |
|---|-------|------|
| 1 | Setup Environment | Laragon, VS Code |
| 2 | Database | schema.sql |
| 3 | Konfigurasi | database.php, helpers.php |
| 4 | Autentikasi | auth.php, login.php |
| 5 | CRUD Peta & Fasilitas | spatial.php |
| 6 | CSS Global | style.css |
| 7 | Dashboard Admin | dashboard.php |
| 8 | Edit Peta (Leaflet) | edit-peta.php |
| 9 | Halaman Publik | index.php |
| 10 | Settings & Export | settings.php, ekspor.php |

### Cara Test Final:

1. Buka: `http://localhost/pemetaan-desa/`
2. Lihat halaman publik dengan peta
3. Login: `demo` / `password123`
4. Edit peta, tambah fasilitas
5. Export data

---

**← [Hari 9: Edit Peta](hari9.md)**

---

**Terima kasih sudah mengikuti tutorial 10 hari ini! 🙏**
