# Frontend Component & Module Integration Guide

> Panduan penggunaan dan pembuatan Frontend Components berdasarkan model content modules.

---

## 1. Ringkasan Perubahan

### 1.1 Portal Display Dropdown
**Controller:** `ContentArticleController`, `ContentPhotoGalleryController`
- Portal Display sebelum: `Ref(PORTAL)` — dropdown dari table `ref`
- Portal Display sekarang: `FrontendSite::where('status', true)` — dropdown dari `frontend_sites`
- **Sebab:** Portal perlu berdasarkan site yang aktif dalam CMS, bukan ref statik

### 1.2 FrontendSiteRenderController — Fallback Chain
**File:** `app/Http/Controllers/FrontendSiteRenderController.php`
```
/                           → default site home
/{slug}                     → default site page OR site home OR content article
/{siteSlug}/{pageSlug}      → non-default site page
/{siteSlug}/{slug}          → check page → check ContentArticle → check ContentPhotoGallery → 404
```

> **Default site:** Pages accessible directly at `/{pageSlug}` tanpa site slug.
> **Other sites:** Guna format `/{siteSlug}/{pageSlug}`.
> **ContentArticle/Gallery detail:** Dimuat melalui page component guna `request()->query('id')`, bukan auto-render URL path.

### 1.3 PortalHandler — Module Pages
**File:** `app/Services/PortalHandler.php`

| Method | Fungsi |
|--------|--------|
| `handle($siteSlug, $pageSlug)` | Render page biasa dalam site layout |

> **Nota:** Semua module content (ContentArticle, ContentSlider, ContentVideo, dll) dipapar sebagai **component list** dalam page (via `mod_*` component). Tiada dedicated URL path untuk detail item — detail dimuat melalui page component guna `request()->query('id')`.
>
> Contoh URL: `http://127.0.0.1:8000/new-site/article?id=pelancaran-2026`
> - `/{siteSlug}/{pageSlug}` → render page biasa
> - Page component query `?id=...` untuk papar detail

### 1.4 Blade Storage — Per-Site Page Paths
**File:** `app/Services/BladeSyncService.php`
- Sebelum: `LAYOUTS/PAGE/{slug}.blade.php`
- Sekarang: `LAYOUTS/PAGE/{siteSlug}/{slug}.blade.php`
- **Sebab:** Dua site boleh ada page dengan slug sama (cth: "home")

Juga untuk entry scripts: `ENTRY/PAGE/{siteSlug}/{slug}.blade.php`

Perubahan berkaitan di `PortalHandler::handle()` — baca layout guna `LAYOUTS/PAGE/{$siteSlug}/{$page->slug}.blade.php`

### 1.5 Demo Seeders

| Seeder | Fungsi |
|--------|--------|
| `WebSiteBackupSeeder` | Create components + sites + pages + menus |
| `Content*Seeder` (13 seeders) | Seed content data (articles, galleries, videos, etc.) |

### 1.6 Image URL Handling
Semua komponen dan PortalHandler guna pattern:
```php
str_starts_with($url, 'http') ? $url : Storage::url($url)
```
Ini membolehkan guna **external URL** (picsum.photos) dan **local storage path** secara serentak.

---

## 2. Dua Jenis Site

### 2.1 Static Landing Page (`/my-site`)
- Guna **FrontendPage** sebagai content
- Home: hero + features
- About: about_content
- Contact: contact_form
- sesuai untuk laman statik

### 2.2 Module Content Site (`/module-site`)
- Guna **FrontendComponent** jenis PHP untuk query data terus dari DB
- Papar data dari ContentArticle, ContentPhotoGallery, ContentVideo, dll
- Sesuai untuk portal kandungan

---

## 3. Cara Penggunaan Components

### 3.1 Component Categories

| Category | Untuk |
|----------|-------|
| `Header` | Navbar, navigation components |
| `Footer` | Footer, copyright components |
| `Section` | Hero, features, about sections |
| `Content` | Article lists, gallery, video |
| `Panel` | Sidebar, widget panels |
| `Modal` | Popup, dialog components |

> **Penting:** Category mesti match dengan pilihan dalam form. Seeder (`WebSiteBackupSeeder`) guna `Header` untuk navbar dan `Footer` untuk footer.

### 3.2 Komponen Static (HTML/CSS sahaja)
Component guna **Blade template** terus dalam field `content`:
```blade
<section style="padding:2rem;">
    <h1>{{ $siteName }}</h1>
    <p>Welcome to {{ $siteSlug }}</p>
</section>
```

**Variable yang tersedia:**
| Variable | Description |
|----------|-------------|
| `$siteSlug` | Slug site semasa |
| `$siteName` | Nama site |
| `$sitePages` | List semua pages dalam site |
| `$menus` | Flat list menu items |
| `$menuTree` | Nested menu tree (parent-child) |
| `$__entry__` | Rendered output dari entry script |
| `$__header__` | Rendered header HTML |
| `$__page__` | Rendered page HTML |
| `$__footer__` | Rendered footer HTML |
| `$_shared` | stdClass object — sharing data antara entry script, component, layout |

> **Entry script auto-capture:** Semua variable yang dideclare dengan `@php $var = value` dalam entry script **auto-carried** ke component dan page content. Tak perlu `$_shared` untuk variable ringkas.

### 3.2 Komponen PHP (Query DB)
Component guna **PHP code** dalam field `content` untuk query data:

```blade
@php
$items = \App\Models\Backend\ContentArticle::with('translations')
    ->where('article_status', 'ACTIVE')
    ->latest()
    ->take(3)
    ->get();
@endphp
@if($items->count())
<section>
    @foreach($items as $item)
        @php $t = $item->translations->firstWhere('article_translation_main', 1) ?? $item->translations->first(); @endphp
        <h3>{{ $t?->article_translation_title ?? 'Untitled' }}</h3>
    @endforeach
</section>
@endif
```

---

## 4. Cara Pembuatan Component Baru

### 4.1 Steps
1. **Tulis component** — pergi ke admin `/admin/frontend-site/components` → Add Component
   - `Code`: guna underscore, cth: `mod_videos`
   - `Name`: Nama display
   - `Category`: Header / Footer / Section / Content / Panel / Modal
   - `Type`: `PHP` jika guna query DB, `HTML` jika statik
   - `Content`: Template Blade (inline styles atau guna CSS classes dari site layout)

2. **Attach ke page** — edit page, pilih component dalam list

3. **Guna dalam editor** — ada tiga cara insert variable component:
   - **Checkbox** — tick component, tulis `{!! $code !!}` manual
   - **Drag & drop** — drag label komponen (≡ icon) terus ke Monaco editor
   - **Autocomplete** — taip `$` dalam editor, pilih dari dropdown suggestion

4. **Auto-tick** — checkbox auto tick bila drag/pilih suggestion. Auto untick bila `{!! $code !!}` dipadam dari editor

5. **Sync** — lepas create/edit, `BladeSyncService::syncAll()` akan simulate component content ke storage

### 4.2 Pattern Component untuk Setiap Module

#### ContentArticle — List + Detail (Query Param)

**Component listing** (letak dalam page content/layout):
```blade
@php
$items = \App\Models\Backend\ContentArticle::with('translations')
    ->where('article_status', 'ACTIVE')
    ->latest()
    ->take(3)
    ->get();
@endphp
@if($items->count())
<section class="cms-section">
    <h2>Latest News</h2>
    <div class="cms-grid">
        @foreach($items as $item)
            @php $t = $item->translations->firstWhere('article_translation_main',1) ?? $item->translations->first(); @endphp
            <div class="cms-card">
                @if($item->article_image)
                <img src="{{ str_starts_with($item->article_image, 'http') ? $item->article_image : Storage::url($item->article_image) }}">
                @endif
                <h3>{{ $t?->article_translation_title ?? 'Untitled' }}</h3>
                <p>{{ $item->article_category }}</p>
            </div>
        @endforeach
    </div>
</section>
@endif
```

**Component detail page** (guna query param `?id=` untuk single item):
```blade
@php
$id = request()->query('id');
$items = \App\Models\Backend\ContentArticle::with('translations')
    ->where('article_status', 'ACTIVE')
    ->where('article_code', $id)
    ->latest()
    ->take(3)
    ->get();
@endphp
@if($items->count())
<section style="padding:4rem 2rem;font-family:sans-serif;">
    <div style="max-width:1100px;margin:0 auto;">
        @foreach($items as $item)
        @php $t = $item->translations->firstWhere('article_translation_main',1) ?? $item->translations->first(); @endphp
        <div style="background:white;border-radius:1rem;overflow:hidden;border:1px solid #e2e8f0;">
            @if($item->article_image)
            <img src="{{ str_starts_with($item->article_image, 'http') ? $item->article_image : Storage::url($item->article_image) }}" style="width:100%;height:180px;object-fit:cover;display:block;">
            @endif
            <div style="padding:1.25rem;">
                <h3 style="color:#0f172a;font-size:1rem;font-weight:600;margin:0 0 0.5rem;">{{ $t?->article_translation_title ?? 'Untitled' }}</h3>
                <p style="color:#64748b;font-size:0.85rem;line-height:1.5;margin:0;">{{ $item->article_category }}</p>
            </div>
        </div>
        @endforeach
    </div>
</section>
@endif
```

> **Contoh URL:** `http://127.0.0.1:8000/new-site/article?id=pelancaran-2026`
> - `article` = page slug dalam site
> - `?id=pelancaran-2026` = `article_code` ContentArticle

#### ContentPhotoGallery
```blade
@php
$items = \App\Models\Backend\ContentPhotoGallery::with('translations','photoLists')
    ->where('gallery_status', 'ACTIVE')
    ->latest()
    ->take(4)
    ->get();
@endphp
@if($items->count())
<section class="cms-section cms-section--alt">
    <h2>Galleries</h2>
    <div class="cms-grid">
        @foreach($items as $item)
            @php $t = $item->translations->firstWhere('gallery_translation_main',1) ?? $item->translations->first();
            $thumb = $item->gallery_thumbnail ? (str_starts_with($item->gallery_thumbnail, 'http') ? $item->gallery_thumbnail : Storage::url($item->gallery_thumbnail)) : null; @endphp
            <a href="?id={{ $item->gallery_code }}" class="cms-card">
                @if($thumb)
                <img src="{{ $thumb }}">
                @endif
                <h3>{{ $t?->gallery_translation_title ?? 'Gallery' }}</h3>
                <span>{{ $item->photoLists->count() }} photos</span>
            </a>
        @endforeach
    </div>
</section>
@endif
```

**Component detail page** (guna query param `?id=` untuk single gallery):
```blade
@php
$id = request()->query('id');
$items = \App\Models\Backend\ContentPhotoGallery::with('translations','photoLists')
    ->where('gallery_status', 'ACTIVE')
    ->where('gallery_code', $id)
    ->get();
@endphp
@if($items->count())
@foreach($items as $item)
@php $t = $item->translations->firstWhere('gallery_translation_main',1) ?? $item->translations->first();
$thumb = $item->gallery_thumbnail ? (str_starts_with($item->gallery_thumbnail, 'http') ? $item->gallery_thumbnail : Storage::url($item->gallery_thumbnail)) : null; @endphp
<section style="padding:2rem;font-family:sans-serif;">
    <div style="max-width:800px;margin:0 auto;">
        @if($thumb)
        <img src="{{ $thumb }}" style="width:100%;max-width:400px;border-radius:12px;margin-bottom:1.5rem;">
        @endif
        <h1 style="font-size:1.5rem;font-weight:700;color:#0f172a;margin-bottom:0.5rem;">{{ $t?->gallery_translation_title ?? 'Gallery' }}</h1>
        <p style="color:#64748b;margin-bottom:2rem;">{{ $t?->gallery_translation_descr ?? '' }}</p>
        <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:1rem;">
            @foreach($item->photoLists as $photo)
            @php $url = $photo->photo_url ? (str_starts_with($photo->photo_url, 'http') ? $photo->photo_url : Storage::url($photo->photo_url)) : '';
            $cap = $photo->photo_descr ?? $photo->translations?->first()?->photo_descr ?? ''; @endphp
            @if($url)
            <div style="border-radius:0.75rem;overflow:hidden;border:1px solid #e2e8f0;">
                <img src="{{ $url }}" style="width:100%;height:180px;object-fit:cover;display:block;">
                @if($cap) <p style="padding:0.5rem;font-size:0.8rem;color:#64748b;">{{ $cap }}</p> @endif
            </div>
            @endif
            @endforeach
        </div>
    </div>
</section>
@endforeach
@endif
```

> **Contoh URL:** `http://127.0.0.1:8000/new-site/galleries?id=galeri-2026`
> - `galleries` = page slug
> - `?id=galeri-2026` = `gallery_code` ContentPhotoGallery

#### ContentVideo
```blade
@php
$items = \App\Models\Backend\ContentVideo::with('translations')
    ->where('video_status', 'ACTIVE')
    ->latest()
    ->take(3)
    ->get();
@endphp
@if($items->count())
<section class="cms-section">
    <h2>Videos</h2>
    <div class="cms-grid">
        @foreach($items as $item)
            @php $t = $item->translations->firstWhere('video_translation_main',1) ?? $item->translations->first();
            $img = $item->video_img ? (str_starts_with($item->video_img, 'http') ? $item->video_img : Storage::url($item->video_img)) : null; @endphp
            <div class="cms-card">
                @if($img)
                <img src="{{ $img }}" class="cms-card-img">
                @endif
                <h3>{{ $t?->video_translation_title ?? 'Untitled' }}</h3>
            </div>
        @endforeach
    </div>
</section>
@endif
```

**Component detail page** (guna query param `?id=` untuk single video):
```blade
@php
$id = request()->query('id');
$items = \App\Models\Backend\ContentVideo::with('translations')
    ->where('video_status', 'ACTIVE')
    ->where('video_code', $id)
    ->get();
@endphp
@if($items->count())
@foreach($items as $item)
@php $t = $item->translations->firstWhere('video_translation_main',1) ?? $item->translations->first();
$img = $item->video_img ? (str_starts_with($item->video_img, 'http') ? $item->video_img : Storage::url($item->video_img)) : null; @endphp
<section style="padding:2rem;font-family:sans-serif;">
    <div style="max-width:800px;margin:0 auto;">
        @if($item->video_url)
        <div style="position:relative;padding-bottom:56.25%;margin-bottom:1.5rem;background:#f1f5f9;border-radius:12px;overflow:hidden;">
            <iframe src="{{ $item->video_url }}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:none;" allowfullscreen></iframe>
        </div>
        @elseif($img)
        <img src="{{ $img }}" style="width:100%;max-width:600px;border-radius:12px;margin-bottom:1.5rem;">
        @endif
        <h1 style="font-size:1.5rem;font-weight:700;color:#0f172a;margin-bottom:0.5rem;">{{ $t?->video_translation_title ?? 'Video' }}</h1>
        <p style="color:#64748b;">{{ $t?->video_translation_descr ?? '' }}</p>
    </div>
</section>
@endforeach
@endif
```

> **Contoh URL:** `http://127.0.0.1:8000/new-site/videos?id=tutorial-2026`
> - `videos` = page slug
> - `?id=tutorial-2026` = `video_code` ContentVideo

#### ContentDownload
```blade
@php
$items = \App\Models\Backend\ContentDownload::where('download_status', 'ACTIVE')->latest()->take(4)->get();
@endphp
@if($items->count())
<section class="cms-section cms-section--alt">
    <h2>Downloads</h2>
    @foreach($items as $item)
    <a href="{{ $item->download_file ? Storage::url($item->download_file) : '#' }}" class="cms-download-item">
        <span>{{ $item->download_title ?? 'Document' }}</span>
        <small>{{ $item->download_category ?? '' }}</small>
    </a>
    @endforeach
</section>
@endif
```

#### ContentApplication
```blade
@php
$items = \App\Models\Backend\ContentApplication::with('translations')
    ->where('application_status', 'ACTIVE')->latest()->take(3)->get();
@endphp
@if($items->count())
<section class="cms-section">
    <h2>Applications</h2>
    <div class="cms-grid">
        @foreach($items as $item)
        @php $t = $item->translations->firstWhere('application_translation_main',1) ?? $item->translations->first(); @endphp
        <div class="cms-card">
            <h3>{{ $t?->application_translation_title ?? 'Service' }}</h3>
        </div>
        @endforeach
    </div>
</section>
@endif
```

#### ContentImage
```blade
@php
$items = \App\Models\Backend\ContentImage::where('image_status', 'ACTIVE')->latest()->take(6)->get();
@endphp
@if($items->count())
<section class="cms-section cms-section--alt">
    <h2>Images</h2>
    <div class="cms-grid cms-grid--sm">
        @foreach($items as $item)
        @php $url = $item->image_file ? (str_starts_with($item->image_file, 'http') ? $item->image_file : Storage::url($item->image_file)) : null; @endphp
        @if($url)
        <img src="{{ $url }}" class="cms-thumb">
        @endif
        @endforeach
    </div>
</section>
@endif
```

#### ContentSlider
```blade
@php
$items = \App\Models\Backend\ContentSlider::with('translations')
    ->where('slider_status', 'ACTIVE')->latest()->take(3)->get();
@endphp
@if($items->count())
<section class="cms-section">
    <h2>Sliders</h2>
    <div class="cms-grid">
        @foreach($items as $item)
        @php $t = $item->translations->firstWhere('slider_translation_main',1) ?? $item->translations->first();
        $img = $t?->slider_translation_img ? (str_starts_with($t->slider_translation_img, 'http') ? $t->slider_translation_img : Storage::url($t->slider_translation_img)) : null; @endphp
        <div class="cms-slide" @if($img) style="background-image:url('{{ $img }}')" @endif>
            <h3>{{ $t?->slider_translation_title ?? 'Banner' }}</h3>
        </div>
        @endforeach
    </div>
</section>
@endif
```

#### ContentPhotoList
```blade
@php
$items = \App\Models\Backend\ContentPhotoList::whereNotNull('photo_url')
    ->where('photo_main', 1)->latest()->take(6)->get();
@endphp
@if($items->count())
<section class="cms-section cms-section--alt">
    <h2>Photos</h2>
    <div class="cms-grid cms-grid--sm">
        @foreach($items as $item)
        @php $url = $item->photo_url ? (str_starts_with($item->photo_url, 'http') ? $item->photo_url : Storage::url($item->photo_url)) : null; @endphp
        @if($url)
        <img src="{{ $url }}" class="cms-thumb">
        @endif
        @endforeach
    </div>
</section>
@endif
```

---

## 5. CSS Classes (Site Layout)

Tambah dalam `<style>` site layout untuk styling seragam:

```css
.cms-section { padding: 4rem 2rem; font-family: sans-serif; }
.cms-section--alt { background: #f8fafc; }
.cms-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; max-width: 1100px; margin: 0 auto; }
.cms-grid--sm { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1rem; }
.cms-card { background: white; border-radius: 1rem; overflow: hidden; border: 1px solid #e2e8f0; transition: 0.3s; text-decoration: none; color: inherit; display: block; }
.cms-card:hover { box-shadow: 0 10px 40px rgba(0,0,0,0.08); }
.cms-card-img { width: 100%; height: 180px; object-fit: cover; display: block; }
.cms-thumb { width: 100%; height: 120px; object-fit: cover; border-radius: 0.75rem; border: 1px solid #e2e8f0; cursor: pointer; transition: 0.3s; }
.cms-thumb:hover { transform: scale(1.05); }
.cms-download-item { display: flex; align-items: center; justify-content: space-between; padding: 1rem 1.5rem; background: white; border-radius: 0.75rem; border: 1px solid #e2e8f0; text-decoration: none; transition: 0.2s; }
.cms-download-item:hover { border-color: #3b82f6; }
.cms-slide { border-radius: 1rem; overflow: hidden; position: relative; height: 200px; background: linear-gradient(135deg, #0f172a, #1e293b); background-size: cover; background-position: center; display: flex; align-items: flex-end; padding: 2rem; }
.cms-slide h3 { color: white; font-size: 1.25rem; font-weight: 700; }
```

---

## 6. Content Seeders — Data Demo

**Folder:** `database/seeders/Portal/`

Seeders delete semua data sedia ada dan seed items untuk setiap module.

### Seeders:

| Seeder | Module | Items |
|--------|--------|-------|
| ContentNewsSeeder | ContentArticle | 5 (NEWS) |
| ContentArticleAboutUsSeeder | ContentArticle | 5 (ABOUT_US) |
| ContentArticleFaqSeeder | ContentArticle | 5 (FAQ) |
| ContentCalendarSeeder | ContentCalendar | 5 |
| ContentDirectorySeeder | ContentDirectory | 5 |
| ContentDownloadSeeder | ContentDownload | 5 |
| ContentGallerySeeder | ContentPhotoGallery | 5 (4 photos each) |
| ContentImageSeeder | ContentImage | 5 |
| ContentVideoSeeder | ContentVideo | 5 |
| ContentSliderSeeder | ContentSlider | 5 |
| ContentApplicationSeeder | ContentApplication | 5 |
| ContentTranslationSeeder | ContentTranslation | 5 |

### Categories (proper case):
- **Articles:** News, Event, Article, Blog
- **Videos:** Tutorial
- **Downloads:** Manual, Form, Report, Policy
- **Applications:** Registration, Request, Feedback, Complaint, Enquiry
- **Images:** Logo, Banner, Staff, Infographic, Certificate
- **Galleries:** Event

### Language:
Semua translation guna `'language' => 'ms'` (Bahasa Melayu).

### Run:
```bash
php artisan db:seed
```

---

## 7. Image URL Pattern

Untuk semua field gambar dalam module, guna pattern ni bila display:

```php
str_starts_with($value, 'http') ? $value : Storage::url($value)
```

Ini kerana gambar boleh jadi:
- **External URL:** `https://picsum.photos/seed/...` (dari seeder demo)
- **Local storage:** `content-article/image.jpg` (dari upload admin)

---

## 8. File Rujukan

| File | Fungsi |
|------|--------|
| `app/Services/PortalHandler.php` | Render content dalam site layout |
| `app/Services/BladeSyncService.php` | Sync component/page/site ke storage |
| `app/Services/FrontendSiteCacheService.php` | Cache site meta (site-meta.json) |
| `app/Http/Controllers/FrontendSiteRenderController.php` | Route resolver untuk frontend site |
| `database/seeders/FrontendSiteDemoSeeder.php` | Seeder untuk components + sites |
| `database/seeders/ContentModuleSeeder.php` | Seeder untuk module data (5 items each) |
