# FrontendSite (CMS Builder) Module — Dokumentasi Penuh

## 1. Pengenalan

Module untuk membina website secara dynamic tanpa coding. Component-based architecture — admin pilih, susun, dan configure component untuk setiap page. Guna **DB sebagai source of truth**, **storage files sebagai performance layer**.

```
DB (Source of Truth)               Storage (Cache/Performance)
┌─────────────────────┐            ┌──────────────────────────────────┐
│ frontend_sites      │──sync──>   │ LAYOUTS/SITE/{slug}.blade.php   │
│ frontend_pages      │──sync──>   │ LAYOUTS/PAGE/{siteSlug}/{slug}.blade.php│
│ frontend_components │──sync──>   │ PHP/{code}.blade.php             │
│ frontend_page_comp  │            │ ENTRY/PAGE/{slug}.blade.php      │
│                     │            │ site-meta.json (cache)           │
└─────────────────────┘            └──────────────────────────────────┘
```

---

## 2. Database Schema

### `frontend_sites`

| Column | Type | Description |
|--------|------|-------------|
| id | bigint (PK) | |
| name | string | Nama site |
| slug | string (unique) | URL slug |
| header_fk | bigint (FK) | FK → `frontend_components.id` (category: Header) |
| footer_fk | bigint (FK) | FK → `frontend_components.id` (category: Footer) |
| layout | longText | Site layout Blade |
| status | boolean | 1 = active, 0 = inactive |
| is_default | boolean | Site utama bila access `/` |
| sort_order | integer | Urutan display |
| timestamps | | |

### `frontend_components`

| Column | Type | Description |
|--------|------|-------------|
| id | bigint (PK) | |
| name | string | Nama display |
| code | string (unique) | Identifier, jadi variable name dalam Blade (`$navbar`) |
| content | longText | HTML/Blade code |
| type | string | `PHP`, `HTML`, `JS`, `CSS` |
| category | string | `Header`, `Footer`, `Section`, `Content`, `Panel`, `Modal` |
| status | boolean | |
| timestamps | | |

### `frontend_pages`

| Column | Type | Description |
|--------|------|-------------|
| id | bigint (PK) | |
| site_fk | bigint (FK) | FK → `frontend_sites.id` |
| name | string | Nama page |
| slug | string | URL slug dalam site |
| layout | text (nullable) | Page layout Blade — override site layout kalau `use_custom_layout = true` |
| entry_script | text (nullable) | PHP init preprocessing (run first, set `$_shared`) |
| page_content | text (nullable) | Main page content (render utama, jadi `$__page__`) |
| use_custom_layout | boolean (default: false) | True = guna page layout sebagai override site layout |
| is_default | boolean | Page utama bila access `/{site}` |
| status | boolean | |
| sort_order | integer | Urutan display |
| timestamps | | |

**Unique:** `(site_fk, slug)`

### `frontend_page_components` (Pivot)

| Column | Type | Description |
|--------|------|-------------|
| id | bigint (PK) | |
| page_fk | bigint (FK) | FK → `frontend_pages.id` |
| component_fk | bigint (FK) | FK → `frontend_components.id` |
| sort | integer | Urutan component dalam page |
| params | json (nullable) | Override parameters |
| timestamps | | |

---

## 3. Architecture & Flow

### 3.1 Render Flow

```
Request: /my-site/about
           │
   FrontendSiteRenderController@resolve('my-site', 'about')
           │
           ▼
   PortalHandler::handle('my-site', 'about')
           │
   ┌───────┴───────┐
   ▼               ▼
 FrontendSite    FrontendPage
 (by slug)       (by slug + site_fk)
           │
           ▼
   FrontendSiteCacheService::get('my-site')
   → baca site-meta.json / refresh
           │
           ▼
   1. Entry Script (init) → set $_shared vars, output = $__entry__
   2. Render setiap component → jadi variable ($navbar, $hero, etc.)
   3. Render Page Content → jadi $__page__ (main content)
   4. Render header & footer components → $__header__, $__footer__
   5. Tentukan layout:
      ├── use_custom_layout = true  → guna page layout (override site layout)
      ├── use_custom_layout = false → guna site layout
      └── tiada langsung          → output $__page__ sahaja
           │
           ▼
   Return HTML → view('frontend.cms', ['html' => $html])
```

### 3.2 Storage Sync Flow

```
Admin CRUD (save/update/delete)
           │
           ▼
    BladeSyncService::syncAll()
            │
    ├── syncComponents()    → Storage::put("PHP/{code}.blade.php")
    ├── syncPageLayouts()   → Storage::put("LAYOUTS/PAGE/{siteSlug}/{slug}.blade.php")  ← per-site
    ├── syncSiteLayouts()   → Storage::put("LAYOUTS/SITE/{slug}.blade.php")
    ├── syncEntries()       → Storage::put("ENTRY/PAGE/{siteSlug}/{slug}.blade.php")  ← entry_script
    ├── syncPageContent()   → Storage::put("PAGE_CONTENT/{siteSlug}/{slug}.blade.php") ← page_content
    └── CacheService::rebuild() → Storage::put('site-meta.json')
```

### 3.3 Variable Availability

Semua variable ini auto-available dalam component, page content, layout, dan entry script:

| Variable | Source | Description |
|----------|--------|-------------|
| `$siteSlug` | URL slug | Slug site semasa |
| `$siteName` | DB `sites.name` | Nama site |
| `$sitePages` | Cache | List semua pages dalam site |
| `$menus`, `$menuTree` | Role mappings | Menu navigation |
| `$__entry__` | Entry script output | Rendered HTML dari entry script |
| `$_shared` | stdClass object | Shared object — set property dalam entry script, guna di component/pageContent/layout |
| `$__header__` | Header component | Rendered header (navbar) — hanya dalam site/page layout |
| `$__page__` | Page Content | Rendered page content utama — hanya dalam site/page layout |
| `$__footer__` | Footer component | Rendered footer — hanya dalam site/page layout |
| `$navbar`, `$hero`, etc | Component code | Setiap component yang ditick di page |

Setiap component yang diassign ke page akan jadi variable dengan nama `$code` — contoh component dengan code `navbar` boleh diguna sebagai `$navbar` dalam page content atau layout.

**Auto-capture dari Entry Script:** Mulai v2, semua variable yang dideclare dengan `@php $var = value` dalam entry script **auto-carry** ke component, page content, dan layout. Tak perlu guna `$_shared` untuk variable ringkas:

```blade
{{-- Entry Script --}}
@php
    $page_title = 'Selamat Datang';
    $show_banner = true;
    $items = App\Models\Backend\ContentArticle::take(3)->get();
@endphp

{{-- Page Content — $page_title, $show_banner, $items auto-available --}}
<h1>{{ $page_title }}</h1>
@if($show_banner)
    <div class="banner">...</div>
@endif
@foreach($items as $item) ... @endforeach
```

**`$_shared`** — object khas yang guna pass-by-reference. Property yang diset dalam entry script akan nampak di component, page content, dan layout. Disarankan guna `$_shared` untuk data yang perlu diubah suai oleh multiple component:

```blade
{{-- Entry Script --}}
@php
    $_shared->page_title = 'Selamat Datang';
    $_shared->show_banner = true;
@endphp

{{-- Page Content --}}
<h1>{{ $_shared->page_title }}</h1>
@if($_shared->show_banner)
    <div class="banner">...</div>
@endif
```

---

## 4. Models

### `FrontendSite`
- **Table:** `frontend_sites`
- **Attributes:** `#[Table]`, `#[Fillable]` (Laravel 13)
- **Relations:** `header()`, `footer()`, `pages()`, `defaultPage()`
- **Casts:** `status` (boolean), `is_default` (boolean), `sort_order` (integer)

### `FrontendComponent`
- **Table:** `frontend_components`
- **Attributes:** `#[Table]`, `#[Fillable]` (Laravel 13)
- **Relations:** `pages()` (BelongsToMany through pivot)
- **Casts:** `status` (boolean)

### `FrontendPage`
- **Table:** `frontend_pages`
- **Attributes:** `#[Table]`, `#[Fillable]` (Laravel 13)
- **Relations:** `site()`, `components()` (BelongsToMany through pivot)
- **Casts:** `is_default` (boolean), `status` (boolean)

### `FrontendPageComponent` (Pivot)
- **Table:** `frontend_page_components`
- **Attributes:** `#[Table]`, `#[Fillable]` (Laravel 13)
- **Relations:** `page()`, `component()`
- **Casts:** `sort` (integer), `params` (array)

---

## 5. Controllers

### `FrontendSiteRenderController` (Frontend)

Controller untuk render website di frontend.

| Method | Route | Description |
|--------|-------|-------------|
| `default()` | `GET /` | Render default site (is_default = true) |
| `resolve()` | `GET /{site}` / `GET /{site}/{slug}` / `GET /{slug}` | Render site/page atau fallback ke content-article |
| `home()` | `GET /{site}` | Render home page site |
| `page()` | `GET /{site}/{slug}` | Render specific page |

**Logic `resolve()`:**
1. **Single slug** — check cache untuk page dalam default site (`/$pageSlug`)
2. Jika jumpa → render page default site (`base-url/about`)
3. Jika tak jumpa → check cache untuk site slug (`/$siteSlug`)
4. Jika site wujud → render CMS
5. Jika tak wujud → fallback ke `HomeController@contentArticle`

### `FrontendSiteController` (Admin Backend)

CRUD untuk sites.

| Method | Route | Permission | Description |
|--------|-------|------------|-------------|
| `index()` | `GET /admin/frontend-site/sites` | `frontend-site.view` | List sites (sorted: default → active → inactive) |
| `create()` | `GET /admin/frontend-site/sites/create` | `frontend-site.create` | Form create site |
| `store()` | `POST /admin/frontend-site/sites` | `frontend-site.create` | Save site + sync storage |
| `edit()` | `GET /admin/frontend-site/sites/{id}/edit` | `frontend-site.update` | Form edit site |
| `update()` | `PUT /admin/frontend-site/sites/{id}` | `frontend-site.update` | Update site + sync |
| `destroy()` | `DELETE /admin/frontend-site/sites/{id}` | `frontend-site.delete` | Delete site + sync |
| `toggleStatus()` | `POST /admin/frontend-site/sites/{id}/toggle-status` | `frontend-site.update` | Toggle active/inactive via AJAX |

### `FrontendComponentController` (Admin Backend)

CRUD untuk components.

| Method | Route | Permission | Description |
|--------|-------|------------|-------------|
| `index()` | `GET /admin/frontend-site/components` | `frontend-site.view` | List components |
| `create()` | `GET /admin/frontend-site/components/create` | `frontend-site.create` | Form create component |
| `store()` | `POST /admin/frontend-site/components` | `frontend-site.create` | Save component + sync |
| `edit()` | `GET /admin/frontend-site/components/{id}/edit` | `frontend-site.update` | Form edit component |
| `update()` | `PUT /admin/frontend-site/components/{id}` | `frontend-site.update` | Update component + sync |
| `destroy()` | `DELETE /admin/frontend-site/components/{id}` | `frontend-site.delete` | Delete component + sync |

### `FrontendPageController` (Admin Backend)

CRUD untuk pages.

| Method | Route | Permission | Description |
|--------|-------|------------|-------------|
| `index()` | `GET /admin/frontend-site/pages` | `frontend-site.view` | List pages (sorted: default → active) |
| `create()` | `GET /admin/frontend-site/pages/create` | `frontend-site.create` | Form create page (with optional `site_id` pre-select) |
| `store()` | `POST /admin/frontend-site/pages` | `frontend-site.create` | Save/update page + assign components + sync. Support hidden `page_id` untuk update page yang dah create via modal Save. Validate Entry Script tak boleh ada `{!! $... !!}` syntax. |
| `edit()` | `GET /admin/frontend-site/pages/{id}/edit` | `frontend-site.update` | Form edit page |
| `update()` | `PUT /admin/frontend-site/pages/{id}` | `frontend-site.update` | Update page + reassign components + sync |
| `destroy()` | `DELETE /admin/frontend-site/pages/{id}` | `frontend-site.delete` | Delete page + detach components + sync |

---

## 6. Services

### `PortalHandler` — Core Render Engine

Service utama yang handle rendering CMS pages. **Hanya ada satu method utama** — semua content module dipapar sebagai component dalam page, bukan melalui dedicated handler.

**Method `handle(string $siteSlug, ?string $pageSlug = null): string`**

1. **Load meta** dari cache (`FrontendSiteCacheService::get()`)
2. **Load site & page** dari DB
3. **Sediakan variables** — `$siteSlug`, `$siteName`, `$sitePages`, `$_shared` (stdClass)
4. **Execute entry script** — render PHP preprocessing, auto-capture `@php $var = value`, output simpan sebagai `$__entry__`
5. **Render components** — loop page components, render guna `Blade::render()`
6. **Render Page Content** — render `PAGE_CONTENT/{siteSlug}/{slug}.blade.php` sebagai `$pageHtml`
7. **Render header & footer** — dari site FK ke component
8. **Tentukan layout**:
    - `use_custom_layout = true` → render page layout sebagai override site layout
    - `use_custom_layout = false` → render site layout
    - tiada langsung → output page content sahaja

> **ContentArticle / Gallery detail** tidak di-render oleh PortalHandler secara berasingan. Ia dimuat melalui page component guna `request()->query('id')` — lihat `docs/FRONTEND-COMPONENT-GUIDE.md` §4.2.

### `BladeSyncService` — Storage Sync

Sync data dari DB ke storage files.

**Method `syncAll()`:**
1. `syncComponents()` — write semua component content ke `PHP/{code}.blade.php`
2. `syncPageLayouts()` — write page layouts ke `LAYOUTS/PAGE/{siteSlug}/{slug}.blade.php`
3. `syncSiteLayouts()` — write site layouts ke `LAYOUTS/SITE/{slug}.blade.php`
4. `syncEntries()` — write entry scripts (`entry_script`) ke `ENTRY/PAGE/{siteSlug}/{slug}.blade.php`
5. `syncPageContent()` — write page content (`page_content`) ke `PAGE_CONTENT/{siteSlug}/{slug}.blade.php`
6. Rebuild cache via `FrontendSiteCacheService::rebuild()`

### `FrontendSiteCacheService` — Cache Management

Cache structure `site-meta.json` untuk performance.

**Structure `site-meta.json`:**
```json
{
    "my-site": {
        "id": 1,
        "name": "My Modern Site",
        "slug": "my-site",
        "is_default": true,
        "header_code": "navbar",
        "footer_code": "footer",
        "pages": {
            "home": {
                "id": 1,
                "name": "Home",
                "is_default": true,
                "components": ["hero", "features"]
            },
            "about": {
                "id": 2,
                "name": "About",
                "is_default": false,
                "components": ["about_content"]
            }
        }
    }
}
```

---

## 7. Routes

### Admin Routes (dalam `prefix('admin')->middleware('auth:admin')`)

**Prefix:** `/admin/frontend-site`
**Route name:** `frontend-site.*`

#### Sites (7 routes)
| Method | URI | Permission | Name |
|--------|-----|------------|------|
| GET | `/admin/frontend-site/sites` | `frontend-site.view` | `frontend-site.sites.index` |
| GET | `/admin/frontend-site/sites/create` | `frontend-site.create` | `frontend-site.sites.create` |
| POST | `/admin/frontend-site/sites` | `frontend-site.create` | `frontend-site.sites.store` |
| GET | `/admin/frontend-site/sites/{id}/edit` | `frontend-site.update` | `frontend-site.sites.edit` |
| PUT | `/admin/frontend-site/sites/{id}` | `frontend-site.update` | `frontend-site.sites.update` |
| DELETE | `/admin/frontend-site/sites/{id}` | `frontend-site.delete` | `frontend-site.sites.destroy` |
| POST | `/admin/frontend-site/sites/{id}/toggle-status` | `frontend-site.update` | `frontend-site.sites.toggle-status` |

#### Components (6 routes)
| Method | URI | Permission | Name |
|--------|-----|------------|------|
| GET | `/admin/frontend-site/components` | `frontend-site.view` | `frontend-site.components.index` |
| GET | `/admin/frontend-site/components/create` | `frontend-site.create` | `frontend-site.components.create` |
| POST | `/admin/frontend-site/components` | `frontend-site.create` | `frontend-site.components.store` |
| GET | `/admin/frontend-site/components/{id}/edit` | `frontend-site.update` | `frontend-site.components.edit` |
| PUT | `/admin/frontend-site/components/{id}` | `frontend-site.update` | `frontend-site.components.update` |
| DELETE | `/admin/frontend-site/components/{id}` | `frontend-site.delete` | `frontend-site.components.destroy` |

#### Pages (6 routes)
| Method | URI | Permission | Name |
|--------|-----|------------|------|
| GET | `/admin/frontend-site/pages` | `frontend-site.view` | `frontend-site.pages.index` |
| GET | `/admin/frontend-site/pages/create` | `frontend-site.create` | `frontend-site.pages.create` |
| POST | `/admin/frontend-site/pages` | `frontend-site.create` | `frontend-site.pages.store` |
| GET | `/admin/frontend-site/pages/{id}/edit` | `frontend-site.update` | `frontend-site.pages.edit` |
| PUT | `/admin/frontend-site/pages/{id}` | `frontend-site.update` | `frontend-site.pages.update` |
| DELETE | `/admin/frontend-site/pages/{id}` | `frontend-site.delete` | `frontend-site.pages.destroy` |
| POST | `/admin/frontend-site/pages/reorder` | `frontend-site.update` | `frontend-site.pages.reorder` |
| GET | `/admin/frontend-site/pages/layout-builder` | `frontend-site.create` | `frontend-site.pages.layout-builder` |

### Frontend Render Routes

| Method | URI | Controller | Name | Notes |
|--------|-----|------------|------|-------|
| GET | `/` | `FrontendSiteRenderController@default` | `home` | Default site home page |
| GET | `/{slug}` | `FrontendSiteRenderController@resolve` | `frontend-site.home` | Default site page **atau** site home **atau** content article |
| GET | `/{site}/{slug}` | `FrontendSiteRenderController@resolve` | `frontend-site.page` | Non-default site page (e.g. `/site-2/page-2`) |

> **Default site:** Page slug terus di root URL: `base-url/page-slug`. Tak perlu suplai site slug.
> **Other sites:** Guna format `base-url/site-slug/page-slug`.

---

## 8. Views Structure

```
resources/views/backend/module/frontendSite/
├── index.blade.php                    ← Dashboard
├── site/
│   ├── index.blade.php                ← List sites
│   ├── create.blade.php               ← Create site form
│   └── edit.blade.php                 ← Edit site form
├── component/
│   ├── index.blade.php                ← List components
│   ├── create.blade.php               ← Create component form
│   └── edit.blade.php                 ← Edit component form
└── page/
    ├── index.blade.php                ← List pages
    ├── create.blade.php               ← Create page form
    └── edit.blade.php                 ← Edit page form
```

### Frontend Layout

```
resources/views/frontend/cms.blade.php  ← {!! $html !!}
```

---

## 9. Page Editor Features (Monaco)

Halaman create/edit page menggunakan **Monaco Editor** (VS Code engine) untuk Entry Script, Page Content, dan Layout.

### 9.1 Pre-editing Validation (Name & Slug)

Sebelum modal editor boleh dibuka atau disave:

- Editor trigger boxes **disable** (greyed out) kalau Name atau Slug kosong.
- Butang Save dalam modal **disable** sehingga Name & Slug diisi.
- Kalau user cuba klik trigger atau drop component tanpa isi Name/Slug:
  - Field Name & Slug dapat border merah + shake animation.
  - Toast merah: *"Please fill in Name and Slug before editing code."*
- Error highlight hilang automatik bila user mula taip.

### 9.2 AJAX Save Behavior

- AJAX save hanya hantar **field yang sedang diedit** (Entry Script / Page Content / Layout).
- AJAX modal save **tidak update/create** `name` & `slug`; hanya update kod.
- Create mode guna hidden field `page_id` untuk koordinasi antara modal Save dan main Save:
  - First modal Save create page dan return `page_id`.
  - Main Save kemudian update page yang sama, elak error *"The slug has already been taken"*.

### 9.3 Entry Script Restriction

- Entry Script **dilarang** mengandungi `{!! $... !!}` syntax.
- Output component (`{!! $code !!}`) mesti letak dalam **Page Content** atau **Layout**.
- Entry Script hanya untuk `@php` logic / preprocessing.

### 9.4 Component Selection & Variable Insertion

Admin page (`page/create.blade.php` & `page/edit.blade.php`) menyediakan tiga cara untuk insert component variables:

| Method | Cara | Kelebihan |
|--------|------|-----------|
| **Checkbox** | Tick component → tulis `@{!! $code !!}` manual dalam editor | Biasalah |
| **Drag & drop** | Drag label komponen (≡) terus ke editor | Insert auto, checkbox auto tick |
| **Autocomplete** | Taip `$` dalam editor → pilih dari suggestion dropdown | Cepat, tak perlu tinggalkan editor |

### 9.5 Auto-sync Checkbox

- **Drag component** → checkbox auto tick
- **Pilih suggestion autocomplete** → checkbox auto tick
- **Padam `{!! $code !!}` dari editor** → checkbox auto untick (lepas 400ms)
- Sync berlaku merentas semua editor (Entry, Page Content, Layout)

### 9.6 Drop Handler

Drag & drop guna Monaco API `executeEdits()` — bukan native browser drop. Jadi `$` dalam variable tak akan jadi snippet placeholder.

---

### `_page_header` Partial

`resources/views/backend/layouts/partials/_page_header.blade.php`

Menyokong parameter tambahan untuk route:

| Variable | Type | Example | Description |
|----------|------|---------|-------------|
| `$pageTitle` | string | `'Create Page'` | Title halaman |
| `$indexRoute` | string | `'frontend-site.pages.index'` | Route name untuk breadcrumb |
| `$indexLabel` | string | `'Pages'` | Label breadcrumb |
| `$indexRouteParams` | array | `['site_id' => 5]` | Optional query params untuk route |

**Usage:**
```php
@php
    $pageTitle = 'Edit Page';
    $indexRoute = 'frontend-site.pages.index';
    $indexLabel = 'Pages';
    $indexRouteParams = ['site_id' => $page->site_fk];
@endphp
@include('backend.layouts.partials._page_header')
```

### Cancel Button

Page create/edit views redirect back to filtered list:
```blade
<a href="{{ route('frontend-site.pages.index', ['site_id' => $page->site_fk]) }}">Cancel</a>
```

---

## 10. Toggle System

### Style 1 — Inline Toggle (Index Tables)

Guna Alpine.js `x-data` dengan `$refs` untuk sync hidden checkbox:

```blade
<div x-data="{ on: {{ $page->status ? 'true' : 'false' }} }" class="flex items-center gap-2">
    <button type="button" @click="on = !on; fetch('{{ route('...toggle-status', $page->id) }}', { method: 'POST', headers: { 'X-CSRF-TOKEN': '...' } })"
        :class="on ? 'bg-brand-500' : 'bg-gray-300 dark:bg-gray-600'"
        class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200">
        <span :class="on ? 'translate-x-full' : ''"
            class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200"></span>
    </button>
    <span class="text-xs font-medium" :class="on ? 'text-success-600' : 'text-gray-400'" x-text="on ? 'Active' : 'Inactive'"></span>
</div>
```

### Style 2 — Form Toggle (Create/Edit Forms)

Guna Alpine.js `$refs.checkbox` untuk sync hidden input:

```blade
<div x-data="{ toggle: {{ old('status', true) ? 'true' : 'false' }} }" class="flex items-center gap-3">
    <span class="text-sm font-medium">Status</span>
    <button type="button" @click="toggle = !toggle; $refs.checkbox.checked = toggle"
        :class="toggle ? 'bg-brand-500' : 'bg-gray-300 dark:bg-gray-600'"
        class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200">
        <span :class="toggle ? 'translate-x-full' : ''"
            class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200"></span>
    </button>
    <span x-text="toggle ? 'Active' : 'Inactive'"></span>
    <input type="checkbox" name="status" value="1" x-ref="checkbox" {{ old('status', true) ? 'checked' : '' }} class="hidden">
</div>
```

### Default Site/Page Toggle (Index Tables)

Guna radio button dengan AJAX. Setiap site/page ada radio button. Klik → set sebagai default, yang lain auto unset.

```blade
<input type="radio" name="default_site" value="{{ $site->id }}"
    {{ $site->is_default ? 'checked' : '' }}
    onchange="if(this.checked) fetch('{{ route('frontend-site.sites.toggle-default', $site->id) }}', { method: 'POST', headers: { 'X-CSRF-TOKEN': '...' } }).then(r => { if(r.ok) window.location.reload(); })"
    class="h-4 w-4 border-gray-300 text-brand-500 cursor-pointer">
```

### Controller Methods

```php
// Toggle status (Active/Inactive)
public function toggleStatus($id)
{
    $model = Model::findOrFail($id);
    $model->update(['status' => !$model->status]);
    app(BladeSyncService::class)->syncAll();
    return response()->json(['status' => $model->fresh()->status]);
}

// Toggle default (set as default, unset others)
public function toggleDefault($id)
{
    $item = Model::findOrFail($id);
    Model::where('is_default', true)->update(['is_default' => false]);
    $item->update(['is_default' => true]);
    app(BladeSyncService::class)->syncAll();
    flash()->success("'{$item->name}' is now the default");
    return response()->json(['is_default' => true]);
}
```

### Routes
```php
Route::post('/sites/{site}/toggle-status', 'toggleStatus')->name('sites.toggle-status');
Route::post('/sites/{site}/toggle-default', 'toggleDefault')->name('sites.toggle-default');
Route::post('/pages/{page}/toggle-status', 'toggleStatus')->name('pages.toggle-status');
Route::post('/pages/{page}/toggle-default', 'toggleDefault')->name('pages.toggle-default');
```

### Auto-unset Default Logic

Bila set satu page sebagai default, page lain dalam **site yang sama** auto di-unset:

```php
// Store
if ($data['is_default']) {
    FrontendPage::where('site_fk', $data['site_fk'])->where('is_default', true)->update(['is_default' => false]);
}

// Update  
if ($data['is_default']) {
    FrontendPage::where('site_fk', $data['site_fk'])->where('id', '!=', $id)->where('is_default', true)->update(['is_default' => false]);
}
```

---

## 11. Storage Structure (Auto-Generated)

```
storage/app/private/
├── site-meta.json                         ← Cache semua metadata (json)
├── PHP/
│   ├── navbar.blade.php                   ← Component content (code → file)
│   ├── hero_banner.blade.php
│   └── footer.blade.php
├── LAYOUTS/SITE/
│   ├── my-site.blade.php                  ← Site layout (full HTML)
│   └── module-site.blade.php
├── LAYOUTS/PAGE/
│   ├── my-site/                           ← Per-site page layout directory
│   │   ├── home.blade.php
│   │   ├── about.blade.php
│   │   └── contact.blade.php
│   └── module-site/
│       └── home.blade.php
├── ENTRY/PAGE/
│   ├── my-site/
│   │   └── home.blade.php                 ← Entry scripts (entry_script)
│   └── module-site/
│       └── home.blade.php
└── PAGE_CONTENT/
    └── my-site/
        └── home.blade.php                 ← Page Content (page_content)
```

---

## 12. Example Usage

### Admin Creates:

```
Site: "My Modern Site" (slug: my-site)
├── Header → component: "Navbar" (code: navbar)
├── Footer → component: "Footer" (code: footer)
├── Layout → {!! $__header__ !!} {!! $__page__ !!} {!! $__footer__ !!}
│
├── Page: "Home" (slug: home, default: true)
│   ├── Entry Script: @php $_shared->title = 'Welcome' @endphp
│   ├── Page Content: <h1>{{ $_shared->title }}</h1> {!! $hero !!} {!! $features !!}
│   ├── Components: Hero (code: hero), Features (code: features)
│   └── Layout: (optional — override site layout)
│
├── Page: "About" (slug: about)
│   ├── Page Content: {!! $about_content !!}
│   └── Components: About Content (code: about_content)
│
└── Page: "Contact" (slug: contact)
    ├── Page Content: {!! $contact_form !!}
    └── Components: Contact Form (code: contact_form)
```

### Frontend Renders:

```
/                 → Default site's default page
/default-site/about    → Default site "About" page (via /about also works)
/my-site          → "my-site" home page
/my-site/about    → "my-site" About page
/my-site/article?id=pelancaran-2026  → Article detail via component query param
/module-site      → Module home (8 module components: articles, galleries, etc.)
```

### Module Site Renders:

```
/module-site      → Home page (8 module components rendering DB content)
  ├── module_sliders       → ContentSlider::where('slider_status', 'ACTIVE')
  ├── module_articles      → ContentArticle::with('translations')...ACTIVE
  ├── module_galleries     → ContentPhotoGallery::with('translations')...ACTIVE
  ├── module_videos        → ContentVideo::where('video_status', 'ACTIVE')
  ├── module_downloads     → ContentDownload::where('download_main','1')...ACTIVE
  ├── module_applications  → ContentApplication::with('translations')...ACTIVE
  ├── module_images        → ContentImage::where('image_main','1')...ACTIVE
  └── module_footer        → Static footer HTML
```

Setiap module component guna pattern `str_starts_with($val, 'http') ? $val : Storage::url($val)` untuk menyokong kedua-dua URL external (picsum.photos) dan local storage. Gambar external tidak di-serve melalui `Storage::url()`.

**ContentArticle / Gallery detail** tidak ada dedicated URL. Guna page component dengan query param:
```
/{siteSlug}/{pageSlug}?id={article_code}
```
Contoh: `/module-site/article?id=pelancaran-2026` — page component query `request()->query('id')` untuk filter single item.

---

## 13. Component Variable System

Component yang diassign ke page akan **auto menjadi variable** dalam layout berdasarkan `code`:

| Component Code | Variable Dalam Layout |
|----------------|---------------------|
| `navbar` | `{!! $navbar !!}` |
| `hero` | `{!! $hero !!}` |
| `footer` | `{!! $footer !!}` |
| `features` | `{!! $features !!}` |
| `about_content` | `{!! $about_content !!}` |
| `contact_form` | `{!! $contact_form !!}` |

### Dalam Site Layout:
```html
{!! $__header__ !!}   ← Header component (dari FK header_fk)
{!! $__page__ !!}     ← Page content (rendered page)
{!! $__footer__ !!}   ← Footer component (dari FK footer_fk)
```

### Dalam Component Code:
```blade
<nav>
    <a href="/{{ $siteSlug }}">{{ $siteName }}</a>
    @foreach($sitePages as $page)
        <a href="{{ $page['url'] }}">{{ $page['name'] }}</a>
    @endforeach
</nav>
```

---

## 14. Sample Code

### 12.1 Component — Navbar (`code: navbar`)

Disimpan dalam DB `frontend_components.content`, di-sync ke `storage/app/private/PHP/navbar.blade.php`.

```blade
<nav style="background:#0f172a;padding:0 2rem;display:flex;align-items:center;justify-content:space-between;height:70px;font-family:sans-serif;position:sticky;top:0;z-index:50;">
    <a href="/{{ $siteSlug }}" style="color:white;font-size:1.25rem;font-weight:700;text-decoration:none;">{{ $siteName }}</a>
    <div style="display:flex;gap:0.5rem;align-items:center;">
        @foreach($sitePages as $page)
        <a href="{{ $page['url'] }}" style="color:#cbd5e1;text-decoration:none;font-size:0.9rem;padding:0.5rem 1rem;border-radius:0.5rem;transition:0.2s;">
            {{ $page['name'] }}
        </a>
        @endforeach
    </div>
</nav>
```

**Available variables:** `$siteSlug`, `$siteName`, `$sitePages` (dari PortalHandler), plus semua component code lain.

### 12.2 Component — Hero Banner (`code: hero`)

```blade
<section style="background:linear-gradient(135deg,#0f172a 0%,#1e293b 50%,#0f172a 100%);padding:6rem 2rem;text-align:center;font-family:sans-serif;">
    <div style="max-width:720px;margin:0 auto;">
        <span style="display:inline-block;background:rgba(59,130,246,0.15);color:#60a5fa;padding:0.35rem 1rem;border-radius:999px;font-size:0.8rem;font-weight:600;margin-bottom:1.5rem;">
            &#10003; Modern CMS Solution
        </span>
        <h1 style="color:white;font-size:3rem;font-weight:700;margin:0 0 1rem;">Build Beautiful Websites<br>Without Coding</h1>
        <p style="color:#94a3b8;font-size:1.15rem;max-width:560px;margin:0 auto 2rem;">
            Create stunning pages with drag-and-drop components.
        </p>
        <a href="/{{ $siteSlug }}/contact" style="display:inline-block;background:#3b82f6;color:white;padding:0.85rem 2rem;border-radius:0.5rem;text-decoration:none;font-weight:600;">
            Get Started
        </a>
    </div>
</section>
```

### 12.3 Site Layout (di `frontend_sites.layout`)

Disimpan dalam DB, di-sync ke `storage/app/private/LAYOUTS/SITE/{slug}.blade.php`.

```blade
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{!! $siteName !!}</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { width: 100%; min-height: 100vh; background: #fff; font-family: sans-serif; }
    </style>
</head>
<body>
    {!! $__header__ !!}
    {!! $__page__ !!}
    {!! $__footer__ !!}
</body>
</html>
```

**Available variables:** `$siteSlug`, `$siteName`, `$sitePages`, `$__header__`, `$__page__`, `$__footer__`, plus semua component code.

### 12.4 Page Layout (Override Site Layout) — di `frontend_pages.layout`

Disimpan dalam DB, di-sync ke `storage/app/private/LAYOUTS/PAGE/{siteSlug}/{slug}.blade.php`.

Hanya diguna jika `use_custom_layout = true` (toggle ON). Layout ni **menggantikan** site layout sepenuhnya. Boleh guna `$__header__`, `$__page__`, `$__footer__`:

```blade
<!DOCTYPE html>
<html>
<head><title>{{ $_shared->page_title ?? $siteName }}</title></head>
<body>
    {!! $__header__ !!}
    {!! $__page__ !!}
    {!! $__footer__ !!}
</body>
</html>
```

**Available variables:** Semua component variable, `$__header__`, `$__page__`, `$__footer__`, `$_shared`, `$__entry__`

### 12.5 Entry Script (PHP Init Preprocessing)

Disimpan dalam `frontend_pages.entry_script`, di-sync ke `storage/app/private/ENTRY/PAGE/{slug}.blade.php`.

Entry script berfungsi sebagai **init** — run paling awal. Ada dua cara untuk kongsi data ke component/page content/layout:

**Cara 1 — Auto-capture (v2):** `@php $var = value` terus auto-carried forward:

```blade
@php
    $items = App\Models\Backend\ContentArticle::take(5)->get();
    $page_title = 'Latest Articles';
@endphp
```

**Cara 2 — `$_shared` (object, guna pass-by-reference):**

```blade
@php
    use App\Models\Backend\Visit;
    $_shared->total_visits = Visit::count();
    $_shared->today_visits = Visit::whereDate('created_at', today())->count();
    $_shared->page_title = 'Visitor Statistics';
@endphp
```

Output entry script boleh diguna sebagai `{{ $__entry__ }}` atau `{!! $__entry__ !!}` dalam component, page content, atau layout.

**Nota:** Entry script output jadi fallback page content jika page_content kosong.

### 12.6 Page Content (Render Utama)

Disimpan dalam `frontend_pages.page_content`, di-sync ke `storage/app/private/PAGE_CONTENT/{siteSlug}/{slug}.blade.php`.

Ini adalah content utama page. Boleh guna semua component variables (`$hero`, `$features`), auto-captured variables dari entry script, dan shared variables (`$_shared`):

```blade
<h1>{{ $_shared->page_title }}</h1>
<p>Total visits: {{ $_shared->total_visits }}</p>
<p>Today: {{ $_shared->today_visits }}</p>

{!! $hero !!}
{!! $features !!}
```

### 12.7 Frontend Wrapper View

`resources/views/frontend/cms.blade.php` — satu-satunya file yang perlu ada:

```blade
{!! $html !!}
```

Ianya hanya me-render output HTML dari `PortalHandler`.

### 12.8 PortalHandler — Core Render Logic

```php
// Pseudocode ringkas
public function handle(string $siteSlug, ?string $pageSlug = null): string
{
    // 1. Load dari cache
    $meta = $this->cache->get($siteSlug);

    // 2. Load site & page dari DB
    $site = FrontendSite::where('slug', $siteSlug)->firstOrFail();
    $page = $pageSlug
        ? FrontendPage::where('site_fk', $site->id)->where('slug', $pageSlug)->firstOrFail()
        : $site->defaultPage()->firstOrFail();

    // 3. Sediakan variables + shared object
    $shared = new \stdClass();
    $vars = ['siteSlug' => $siteSlug, 'siteName' => $meta['name'], 'sitePages' => $pageList, '_shared' => $shared];

    // 4. Entry script — init preprocessing (run first)
    //    Auto-capture @php $var = value → merge ke $vars
    $entryBlade = Storage::get("ENTRY/PAGE/{$siteSlug}/{$page->slug}.blade.php");
    if ($entryBlade) {
        $compiled = Blade::compileString($entryBlade);
        $__tmp = tempnam(sys_get_temp_dir(), 'entry_') . '.php';
        file_put_contents($__tmp, $compiled);
        $__result = (function() use ($__tmp, $vars) {
            $__orig = array_keys($vars);
            extract($vars); ob_start(); include $__tmp; $output = ob_get_clean();
            $all = get_defined_vars();
            foreach ($all as $k => $v) {
                if (!in_array($k, $__orig, true) && !in_array($k, ['__orig','vars','output','all','k','v','__tmp'], true))
                    $vars[$k] = $v;
            }
            return ['output' => $output, 'vars' => $vars];
        })();
        $vars = $__result['vars'];
        $vars['__entry__'] = $__result['output'];
        unlink($__tmp);
    }

    // 5. Render setiap component → jadi variable ($hero, $features, etc)
    foreach ($page->components as $comp) {
        $blade = Storage::get("PHP/{$comp->code}.blade.php");
        $vars[$comp->code] = $blade ? Blade::render($blade, $vars) : '';
    }

    // 6. Render Page Content sebagai content utama
    $pageContentBlade = Storage::get("PAGE_CONTENT/{$siteSlug}/{$page->slug}.blade.php");
    $pageHtml = $pageContentBlade
        ? Blade::render($pageContentBlade, $vars)
        : $vars['__entry__'];

    // 7. Render header & footer
    $header = Blade::render(Storage::get("PHP/{$site->header->code}.blade.php"), $vars);
    $footer = Blade::render(Storage::get("PHP/{$site->footer->code}.blade.php"), $vars);

    $layoutVars = array_merge($vars, [
        '__header__' => $header, '__page__' => $pageHtml, '__footer__' => $footer
    ]);

    // 8. Tentukan layout
    if ($page->use_custom_layout) {
        $pageLayout = Storage::get("LAYOUTS/PAGE/{$siteSlug}/{$page->slug}.blade.php");
        return $pageLayout ? Blade::render($pageLayout, $layoutVars) : Blade::render($siteLayout, $layoutVars);
    }

    $siteLayout = Storage::get("LAYOUTS/SITE/{$site->slug}.blade.php");
    return $siteLayout ? Blade::render($siteLayout, $layoutVars) : $pageHtml;
}
```

### 12.9 Controller CRUD — Example (FrontendSiteController)

```php
// Store method
public function store(FrontendSiteRequest $request)
{
    $data = $request->validated();
    $data['status'] = $request->boolean('status');
    $data['is_default'] = $request->boolean('is_default');

    FrontendSite::create($data);

    app(BladeSyncService::class)->syncAll(); // Auto sync ke storage

    flash()->success('Site created successfully!');
    return redirect()->route('frontend-site.sites.index');
}
```

### 12.10 Toggle Status — AJAX

`FrontendSiteController@toggleStatus` dipanggil via fetch dari index table:

```javascript
// Dalam site/index.blade.php — Alpine.js
fetch('{{ route('frontend-site.sites.toggle-status', $site->id) }}', {
    method: 'POST',
    headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}', 'Content-Type': 'application/json' }
})
```

```php
// Controller
public function toggleStatus($id)
{
    $site = FrontendSite::findOrFail($id);
    $site->update(['status' => !$site->status]);
    app(BladeSyncService::class)->syncAll();
    return response()->json(['status' => $site->fresh()->status]);
}
```

---

## 15. Permissions

| Permission | Description |
|------------|-------------|
| `frontend-site.view` | View sites, components, pages list |
| `frontend-site.create` | Create sites, components, pages |
| `frontend-site.update` | Edit sites, components, pages |
| `frontend-site.delete` | Delete sites, components, pages |

---

## 16. Seeder — `FrontendSiteDemoSeeder`

Mencipta 2 demo sites dengan component berbeza:

| Site | Slug | Type | Pages | Description |
|------|------|------|-------|-------------|
| My Modern Site | `my-site` | Static landing | 3 (Home, About, Contact) | Fixed HTML components (navbar, hero, features, footer) |
| Module Content Site | `module-site` | Module content | 1 (Home) | 8 PHP components querying DB (articles, galleries, videos, etc.) |

**Static site components:** `navbar`, `hero_banner`, `features_grid`, `about_content`, `contact_form`, `footer`

**Module site components:** `module_articles`, `module_galleries`, `module_videos`, `module_downloads`, `module_applications`, `module_images`, `module_sliders`, `module_footer`

### Prasyarat
```bash
# Mula-mula seed data content module (5 items setiap module)
php artisan db:seed --class=ContentModuleSeeder

# Kemudian seed sites + components + sync storage
php artisan db:seed --class=FrontendSiteDemoSeeder
```

> **Nota:** `ContentModuleSeeder` mesti dijalankan SEBELUM `FrontendSiteDemoSeeder` kerana module site components query data dari content module tables.

Buka:
- `http://127.0.0.1:8000/my-site` — Static landing page
- `http://127.0.0.1:8000/module-site` — Module content site

---

## 17. File Index

```
app/
├── Http/
│   ├── Controllers/
│   │   ├── FrontendSiteRenderController.php     ← Frontend render
│   │   └── Backend/
│   │       ├── FrontendSiteController.php       ← Site CRUD
│   │       ├── FrontendPageController.php       ← Page CRUD
│   │       └── FrontendComponentController.php   ← Component CRUD
│   └── Requests/Backend/
│       └── FrontendSiteRequest.php              ← Site validation
├── Models/Backend/
│   ├── FrontendSite.php
│   ├── FrontendComponent.php
│   ├── FrontendPage.php
│   └── FrontendPageComponent.php
└── Services/
    ├── PortalHandler.php                        ← Render engine
    ├── BladeSyncService.php                     ← Storage sync
    └── FrontendSiteCacheService.php             ← Cache

database/
├── migrations/
│   ├── xxxx_create_frontend_components_table.php
│   ├── xxxx_create_frontend_sites_table.php
│   ├── xxxx_create_frontend_pages_table.php
│   └── xxxx_create_frontend_page_components_table.php
└── seeders/
    ├── BackendMenuSeeder.php                    ← Menu seeder
    ├── AdminRolePermissionSeeder.php            ← Permission seeder
    └── FrontendSiteDemoSeeder.php               ← Demo data seeder

storage/app/private/
├── site-meta.json
├── PHP/*.blade.php                          ← Component content (navbar, hero, etc)
├── LAYOUTS/SITE/*.blade.php                ← Site layout
├── LAYOUTS/PAGE/{site}/*.blade.php         ← Page layout (override site layout)
├── ENTRY/PAGE/{site}/*.blade.php           ← Entry scripts (entry_script)
└── PAGE_CONTENT/{site}/*.blade.php         ← Page Content (page_content)

resources/views/
├── frontend/cms.blade.php
└── backend/module/frontendSite/
    ├── index.blade.php
    ├── site/{index,create,edit}.blade.php
    ├── component/{index,create,edit}.blade.php
    └── page/{index,create,edit}.blade.php
```
