* Autosave after 1.2 seconds of inactivity * Saved, saving, unsaved and error indicators * Last-saved time * Dirty-state tracking * Warning before leaving with unsaved changes * Server-rendered Saasland theme preview * Preview refresh while editing * Active renderer view name * Graceful message when a content type has no theme renderer * Existing manual save remains available * Existing undo, redo, responsive controls and inline editing retained
130 lines
5.5 KiB
PHP
130 lines
5.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\Content\Http\Controllers;
|
|
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Routing\Controller;
|
|
use Illuminate\Support\Facades\View as ViewFacade;
|
|
use Illuminate\View\View;
|
|
use Modules\Content\Http\Requests\SaveContentDocumentRequest;
|
|
use Modules\Content\Models\ContentDocument;
|
|
use Modules\Content\Models\ContentType;
|
|
|
|
final class ContentDocumentController extends Controller
|
|
{
|
|
public function index(Request $request): View
|
|
{
|
|
$documents = ContentDocument::query()->with(['type','placements'])
|
|
->when($request->filled('search'), fn ($q) => $q->where(fn ($q) => $q->where('title','like','%'.$request->string('search').'%')->orWhere('slug','like','%'.$request->string('search').'%')))
|
|
->when($request->filled('type'), fn ($q) => $q->whereHas('type', fn ($q) => $q->where('key',$request->string('type'))))
|
|
->when($request->filled('status'), fn ($q) => $q->where('status',$request->string('status')))
|
|
->latest()->paginate(25)->withQueryString();
|
|
return view('content::admin.index', ['documents'=>$documents, 'types'=>ContentType::orderBy('name')->get()]);
|
|
}
|
|
|
|
public function create(Request $request): View
|
|
{
|
|
$types = ContentType::orderBy('name')->get();
|
|
$selectedType = $types->firstWhere('id', (int) $request->integer('type')) ?? $types->first();
|
|
return view('content::admin.form', ['document'=>new ContentDocument(['status'=>'draft','content_type_id'=>$selectedType?->id]), 'types'=>$types, 'selectedType'=>$selectedType]);
|
|
}
|
|
|
|
public function store(SaveContentDocumentRequest $request): RedirectResponse
|
|
{
|
|
$document = ContentDocument::create($request->payload());
|
|
return redirect()->route('content.documents.edit',$document)->with('status','Content document created.');
|
|
}
|
|
|
|
public function edit(ContentDocument $document): View
|
|
{
|
|
$document->load('placements');
|
|
return view('content::admin.form', ['document'=>$document, 'types'=>ContentType::orderBy('name')->get(), 'selectedType'=>$document->type()->first()]);
|
|
}
|
|
|
|
public function update(SaveContentDocumentRequest $request, ContentDocument $document): RedirectResponse
|
|
{
|
|
$document->update($request->payload());
|
|
return back()->with('status','Content document saved.');
|
|
}
|
|
|
|
public function autosave(Request $request, ContentDocument $document): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'content_type_id' => ['required', 'integer', 'exists:content_types,id'],
|
|
'title' => ['required', 'string', 'max:255'],
|
|
'slug' => ['nullable', 'string', 'max:255'],
|
|
'status' => ['required', 'in:draft,published,archived'],
|
|
'data_json' => ['required', 'json'],
|
|
'published_at' => ['nullable', 'date'],
|
|
'expires_at' => ['nullable', 'date', 'after:published_at'],
|
|
]);
|
|
|
|
$document->update([
|
|
'content_type_id' => $validated['content_type_id'],
|
|
'title' => $validated['title'],
|
|
'slug' => filled($validated['slug'] ?? null) ? $validated['slug'] : str($validated['title'])->slug(),
|
|
'status' => $validated['status'],
|
|
'data' => json_decode($validated['data_json'], true, flags: JSON_THROW_ON_ERROR),
|
|
'published_at' => $validated['published_at'] ?? null,
|
|
'expires_at' => $validated['expires_at'] ?? null,
|
|
]);
|
|
|
|
return response()->json([
|
|
'saved' => true,
|
|
'saved_at' => now()->toIso8601String(),
|
|
'updated_at' => $document->fresh()->updated_at?->toIso8601String(),
|
|
]);
|
|
}
|
|
|
|
public function preview(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'content_type_id' => ['required', 'integer', 'exists:content_types,id'],
|
|
'data_json' => ['required', 'json'],
|
|
]);
|
|
|
|
$type = ContentType::query()->findOrFail($validated['content_type_id']);
|
|
$view = 'stylist-theme-saasland::documents.'.$type->key;
|
|
|
|
if (! ViewFacade::exists($view)) {
|
|
return response()->json([
|
|
'html' => '<div style="padding:2rem;font-family:system-ui;color:#71717a">No Saasland renderer exists for this content type yet.</div>',
|
|
'view' => $view,
|
|
]);
|
|
}
|
|
|
|
$data = json_decode($validated['data_json'], true, flags: JSON_THROW_ON_ERROR);
|
|
|
|
return response()->json([
|
|
'html' => ViewFacade::make($view, array_merge($data, [
|
|
'data' => $data,
|
|
'document' => new ContentDocument(['data' => $data]),
|
|
'placement' => null,
|
|
'preview' => true,
|
|
]))->render(),
|
|
'view' => $view,
|
|
]);
|
|
}
|
|
|
|
public function duplicate(ContentDocument $document): RedirectResponse
|
|
{
|
|
$copy = $document->replicate(['slug']);
|
|
$copy->title = $document->title.' copy';
|
|
$copy->slug = str($copy->title)->slug().'-'.str()->lower(str()->random(6));
|
|
$copy->status = 'draft';
|
|
$copy->save();
|
|
return redirect()->route('content.documents.edit',$copy)->with('status','Independent copy created.');
|
|
}
|
|
|
|
public function destroy(ContentDocument $document): RedirectResponse
|
|
{
|
|
abort_if($document->placements()->exists(), 422, 'A document used by a page cannot be deleted.');
|
|
$document->delete();
|
|
return redirect()->route('content.index')->with('status','Content document deleted.');
|
|
}
|
|
}
|