Files
ABSDEV/Modules/Content/Http/Controllers/PageCompositionController.php
T
Mark Gregory bfac7f555a Visual header, body, and footer composition screen.
* Drag-and-drop section ordering with automatic saving.
* Add existing reusable documents to regions.
* Remove placements without deleting documents.
* Create independent page-specific copies.
* Shared-document usage warnings.
* Document creation, duplication, deletion protection and filtering.
* Draft and published document states.
* Document and placement scheduling.
* Authenticated homepage preview.
* Draft sections displayed in preview with a clear draft marker.
* Content Composer added to the administration sidebar.
* Public rendering now respects:
    * Publication status
    * Future publication dates
    * Expiry dates
    * Placement start and end dates
    * Enabled state
* Original Saasland homepage remains as the fallback when no composition exists.
2026-07-27 08:28:52 +01:00

158 lines
6.3 KiB
PHP

<?php
namespace Modules\Content\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
use Modules\Content\Models\ContentDocument;
use Modules\Content\Models\PageContent;
final class PageCompositionController extends Controller
{
private const REGIONS = ['header', 'body', 'footer'];
public function edit(string $pageKey): View
{
$placements = PageContent::query()
->with('document.type')
->where('page_key', $pageKey)
->orderByRaw("CASE region WHEN 'header' THEN 1 WHEN 'body' THEN 2 ELSE 3 END")
->orderBy('sort_order')
->get()
->groupBy('region');
$documents = ContentDocument::query()
->with('type')
->withCount('placements')
->orderBy('title')
->get();
return view('content::admin.composition', compact('pageKey', 'placements', 'documents'));
}
public function preview(string $pageKey): View
{
abort_unless($pageKey === 'home', 404, 'This phase currently provides the Saasland homepage preview.');
return view('stylist-theme-saasland::home', ['pageKey' => $pageKey]);
}
public function store(Request $request, string $pageKey): RedirectResponse
{
$validated = $request->validate([
'content_document_id' => ['required', 'integer', Rule::exists('content_documents', 'id')->whereNull('deleted_at')],
'region' => ['required', Rule::in(self::REGIONS)],
]);
$document = ContentDocument::query()->with('type')->findOrFail($validated['content_document_id']);
abort_unless(in_array($validated['region'], $document->type?->allowed_regions ?? [], true), 422, 'This content type is not allowed in the selected region.');
$nextOrder = ((int) PageContent::query()->where('page_key', $pageKey)->where('region', $validated['region'])->max('sort_order')) + 10;
PageContent::query()->create([
'page_key' => $pageKey,
'content_document_id' => $document->id,
'region' => $validated['region'],
'sort_order' => $nextOrder,
'is_enabled' => true,
]);
return back()->with('status', 'Document added to the '.$validated['region'].' region.');
}
public function update(Request $request, PageContent $placement): RedirectResponse
{
$validated = $request->validate([
'is_enabled' => ['nullable', 'boolean'],
'starts_at' => ['nullable', 'date'],
'ends_at' => ['nullable', 'date', 'after:starts_at'],
]);
$placement->update([
'is_enabled' => $request->boolean('is_enabled'),
'starts_at' => $validated['starts_at'] ?? null,
'ends_at' => $validated['ends_at'] ?? null,
]);
return back()->with('status', 'Page placement updated.');
}
public function destroy(PageContent $placement): RedirectResponse
{
$placement->delete();
$this->normaliseRegion($placement->page_key, $placement->region);
return back()->with('status', 'Document removed from this page. The reusable document was not deleted.');
}
public function duplicate(Request $request, PageContent $placement): RedirectResponse
{
$source = $placement->document()->firstOrFail();
$copy = DB::transaction(function () use ($request, $placement, $source): ContentDocument {
$copy = $source->replicate(['slug', 'status', 'published_at', 'created_by', 'updated_by']);
$copy->title = $source->title.' copy';
$copy->slug = $source->slug.'-copy-'.strtolower(str()->random(6));
$copy->status = 'draft';
$copy->published_at = null;
$copy->created_by = $request->user()?->id;
$copy->updated_by = $request->user()?->id;
$copy->save();
PageContent::query()->create([
'page_id' => $placement->page_id,
'page_key' => $placement->page_key,
'content_document_id' => $copy->id,
'region' => $placement->region,
'sort_order' => $placement->sort_order + 5,
'is_enabled' => false,
'visibility_rules' => $placement->visibility_rules,
'presentation' => $placement->presentation,
]);
return $copy;
});
$this->normaliseRegion($placement->page_key, $placement->region);
return redirect()->route('content.documents.edit', $copy)->with('status', 'Page-specific draft copy created and added after the original.');
}
public function reorder(Request $request, string $pageKey): JsonResponse|RedirectResponse
{
$validated = $request->validate([
'region' => ['required', Rule::in(self::REGIONS)],
'placements' => ['required', 'array'],
'placements.*' => ['required', 'integer'],
]);
$ids = array_values(array_unique(array_map('intval', $validated['placements'])));
$owned = PageContent::query()->where('page_key', $pageKey)->where('region', $validated['region'])->whereIn('id', $ids)->pluck('id')->all();
abort_unless(count($owned) === count($ids), 422, 'One or more placements do not belong to this page region.');
DB::transaction(function () use ($ids): void {
PageContent::query()->whereIn('id', $ids)->increment('sort_order', 100000);
foreach ($ids as $index => $id) {
PageContent::query()->whereKey($id)->update(['sort_order' => ($index + 1) * 10]);
}
});
return $request->expectsJson()
? response()->json(['message' => 'Order saved.'])
: back()->with('status', 'Page order updated.');
}
private function normaliseRegion(?string $pageKey, string $region): void
{
$ids = PageContent::query()->where('page_key', $pageKey)->where('region', $region)->orderBy('sort_order')->pluck('id');
DB::transaction(function () use ($ids): void {
foreach ($ids as $index => $id) {
PageContent::query()->whereKey($id)->update(['sort_order' => ($index + 1) * 10]);
}
});
}
}