Deploy to Production (witstore.wende.it) / deploy (push) Successful in 7s
- E-Mail-Templates: DB-gespeicherte HTML-Templates mit {{placeholder}}-System,
Admin-UI zum Bearbeiten von Betreff und Body, alle 4 Mailables aktualisiert
- Seiten-Layouts: 4 Layouts für CMS-Seiten (Standard, Sidebar, Volle Breite, Landing),
Layout-Auswahl im Admin-Formular
- Shop-Themes: 5 Farbthemen via CSS-Variablen, Picker in Einstellungen
- Deployment: EmailTemplateSeeder wird automatisch beim Deploy ausgeführt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
261 lines
12 KiB
PHP
261 lines
12 KiB
PHP
@extends('layouts.admin')
|
||
@section('title', isset($page) ? 'Seite bearbeiten' : 'Neue Seite')
|
||
|
||
@push('head')
|
||
{{-- Quill WYSIWYG --}}
|
||
<link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
|
||
<style>
|
||
.ql-container { font-size: 14px; min-height: 320px; border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; }
|
||
.ql-toolbar { border-top-left-radius: 6px; border-top-right-radius: 6px; background: #f9fafb; }
|
||
.ql-editor { min-height: 320px; line-height: 1.7; }
|
||
.ql-editor h1 { font-size: 1.8em; font-weight: 700; margin-bottom: .5em; }
|
||
.ql-editor h2 { font-size: 1.4em; font-weight: 600; margin-bottom: .4em; }
|
||
.ql-editor h3 { font-size: 1.15em; font-weight: 600; margin-bottom: .3em; }
|
||
.ql-editor a { color: #2563eb; text-decoration: underline; }
|
||
.ql-editor blockquote { border-left: 4px solid #d1d5db; padding-left: 1em; color: #6b7280; font-style: italic; }
|
||
.ql-editor pre { background: #1e293b; color: #e2e8f0; padding: 1em; border-radius: 6px; font-family: monospace; overflow-x: auto; }
|
||
.ql-editor ul, .ql-editor ol { padding-left: 1.5em; }
|
||
.ql-editor img { max-width: 100%; border-radius: 4px; }
|
||
</style>
|
||
@endpush
|
||
|
||
@section('content')
|
||
<div class="max-w-4xl">
|
||
<form id="page-form"
|
||
method="POST"
|
||
action="{{ isset($page) ? route('admin.pages.update', $page) : route('admin.pages.store') }}"
|
||
enctype="multipart/form-data"
|
||
class="space-y-4">
|
||
@csrf @if(isset($page)) @method('PUT') @endif
|
||
|
||
{{-- Metadaten --}}
|
||
<div class="bg-white rounded shadow p-6 space-y-4">
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Titel *</label>
|
||
<input type="text" name="title" id="title-input"
|
||
value="{{ old('title', $page->title ?? '') }}"
|
||
class="w-full border rounded px-3 py-2 text-sm" required>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">
|
||
Slug (URL)
|
||
<span class="text-xs text-gray-400 font-normal">– automatisch aus Titel</span>
|
||
</label>
|
||
<div class="flex items-center border rounded overflow-hidden">
|
||
<span class="px-2 py-2 bg-gray-50 text-gray-400 text-xs border-r select-none">/seite/</span>
|
||
<input type="text" name="slug" id="slug-input"
|
||
value="{{ old('slug', $page->slug ?? '') }}"
|
||
class="flex-1 px-3 py-2 text-sm font-mono focus:outline-none"
|
||
placeholder="wird-automatisch-generiert">
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Typ</label>
|
||
<select name="type" class="w-full border rounded px-3 py-2 text-sm">
|
||
<option value="static" {{ old('type', $page->type ?? 'static') === 'static' ? 'selected' : '' }}>Statische Seite (Impressum, AGB, …)</option>
|
||
<option value="blog" {{ old('type', $page->type ?? '') === 'blog' ? 'selected' : '' }}>Blog-Beitrag</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Titelbild</label>
|
||
@if(isset($page) && $page->image)
|
||
<img src="{{ asset('storage/'.$page->image) }}" class="h-10 mb-1 rounded">
|
||
@endif
|
||
<input type="file" name="image" accept="image/*" class="text-sm">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- WYSIWYG Editor --}}
|
||
<div class="bg-white rounded shadow overflow-hidden">
|
||
<div class="px-5 py-3 border-b flex items-center justify-between">
|
||
<span class="text-sm font-semibold text-gray-700">Inhalt</span>
|
||
<button type="button" id="toggle-source"
|
||
class="text-xs px-3 py-1 border rounded hover:bg-gray-50 text-gray-500">
|
||
HTML-Quellcode
|
||
</button>
|
||
</div>
|
||
|
||
{{-- Quill Editor --}}
|
||
<div id="quill-editor" class="border-0">
|
||
{!! old('content', $page->content ?? '') !!}
|
||
</div>
|
||
|
||
{{-- Roher HTML-Quellcode (Toggle) --}}
|
||
<textarea id="source-editor" name="_content_source" rows="16"
|
||
class="hidden w-full px-4 py-3 text-sm font-mono border-0 focus:outline-none resize-none"
|
||
style="min-height: 320px">{{ old('content', $page->content ?? '') }}</textarea>
|
||
|
||
{{-- Verstecktes Feld das beim Submit gefüllt wird --}}
|
||
<input type="hidden" name="content" id="content-hidden"
|
||
value="{{ old('content', $page->content ?? '') }}">
|
||
</div>
|
||
|
||
{{-- SEO & Optionen --}}
|
||
<div class="bg-white rounded shadow p-6 space-y-4">
|
||
<h3 class="text-sm font-semibold text-gray-700">SEO & Einstellungen</h3>
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Meta-Titel <span class="text-xs text-gray-400 font-normal">(Browser-Tab)</span></label>
|
||
<input type="text" name="meta_title"
|
||
value="{{ old('meta_title', $page->meta_title ?? '') }}"
|
||
class="w-full border rounded px-3 py-2 text-sm">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Meta-Beschreibung <span class="text-xs text-gray-400 font-normal">(Suchergebnisse)</span></label>
|
||
<input type="text" name="meta_description"
|
||
value="{{ old('meta_description', $page->meta_description ?? '') }}"
|
||
class="w-full border rounded px-3 py-2 text-sm"
|
||
maxlength="160">
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Veröffentlichungsdatum</label>
|
||
<input type="date" name="published_at"
|
||
value="{{ old('published_at', isset($page->published_at) ? $page->published_at->format('Y-m-d') : '') }}"
|
||
class="w-full border rounded px-3 py-2 text-sm">
|
||
</div>
|
||
<div class="flex items-end pb-2">
|
||
<label class="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" name="published" value="1"
|
||
{{ old('published', $page->published ?? true) ? 'checked' : '' }}
|
||
class="w-4 h-4 accent-blue-600">
|
||
<span>Veröffentlicht</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<hr class="border-gray-100">
|
||
<h3 class="text-sm font-semibold text-gray-700">Navigation</h3>
|
||
<div class="grid grid-cols-3 gap-4">
|
||
<label class="flex items-start gap-2 text-sm cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
|
||
<input type="checkbox" name="show_in_footer" value="1"
|
||
{{ old('show_in_footer', $page->show_in_footer ?? true) ? 'checked' : '' }}
|
||
class="mt-0.5 accent-blue-600">
|
||
<div>
|
||
<span class="font-medium">Footer</span>
|
||
<p class="text-xs text-gray-400 mt-0.5">Link in der Fußzeile des Shops</p>
|
||
</div>
|
||
</label>
|
||
<label class="flex items-start gap-2 text-sm cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
|
||
<input type="checkbox" name="show_in_nav" value="1"
|
||
{{ old('show_in_nav', $page->show_in_nav ?? false) ? 'checked' : '' }}
|
||
class="mt-0.5 accent-blue-600">
|
||
<div>
|
||
<span class="font-medium">Hauptmenü</span>
|
||
<p class="text-xs text-gray-400 mt-0.5">Link in der Top-Navigation</p>
|
||
</div>
|
||
</label>
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Reihenfolge im Menü</label>
|
||
<input type="number" name="nav_sort" value="{{ old('nav_sort', $page->nav_sort ?? 0) }}"
|
||
class="w-full border rounded px-3 py-2 text-sm" min="0">
|
||
<p class="text-xs text-gray-400 mt-1">Niedrigere Zahl = weiter links</p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium mb-1">Seiten-Layout</label>
|
||
<select name="layout" class="w-full border rounded px-3 py-2 text-sm">
|
||
@foreach(['default'=>'Standard (zentriert)','sidebar'=>'Mit Sidebar','full'=>'Volle Breite','landing'=>'Landing Page (ohne Header/Footer)'] as $val => $label)
|
||
<option value="{{ $val }}" {{ old('layout', $page->layout ?? 'default') === $val ? 'selected' : '' }}>{{ $label }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@if($errors->any())
|
||
<div class="bg-red-50 border border-red-200 rounded px-4 py-3 text-sm text-red-700">
|
||
@foreach($errors->all() as $e)<p>• {{ $e }}</p>@endforeach
|
||
</div>
|
||
@endif
|
||
|
||
<div class="flex gap-3 pb-8">
|
||
<button type="submit" class="bg-blue-600 text-white px-6 py-2.5 rounded-lg hover:bg-blue-700 text-sm font-medium">
|
||
Speichern
|
||
</button>
|
||
<a href="{{ route('admin.pages.index') }}"
|
||
class="px-6 py-2.5 rounded-lg border text-sm hover:bg-gray-50">
|
||
Abbrechen
|
||
</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<script src="https://cdn.quilljs.com/1.3.7/quill.js"></script>
|
||
<script>
|
||
// ── Quill initialisieren ────────────────────────────────────────────────────
|
||
const quill = new Quill('#quill-editor', {
|
||
theme: 'snow',
|
||
modules: {
|
||
toolbar: [
|
||
[{ header: [1, 2, 3, false] }],
|
||
['bold', 'italic', 'underline', 'strike'],
|
||
[{ color: [] }, { background: [] }],
|
||
[{ align: [] }],
|
||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||
[{ indent: '-1' }, { indent: '+1' }],
|
||
['blockquote', 'code-block'],
|
||
['link', 'image'],
|
||
['clean']
|
||
]
|
||
},
|
||
placeholder: 'Seiteninhalt hier eingeben …',
|
||
});
|
||
|
||
// ── Slug automatisch aus Titel generieren ───────────────────────────────────
|
||
const titleInput = document.getElementById('title-input');
|
||
const slugInput = document.getElementById('slug-input');
|
||
let slugManuallyEdited = slugInput.value.length > 0;
|
||
|
||
titleInput.addEventListener('input', () => {
|
||
if (slugManuallyEdited) return;
|
||
slugInput.value = titleInput.value
|
||
.toLowerCase()
|
||
.replace(/ä/g,'ae').replace(/ö/g,'oe').replace(/ü/g,'ue').replace(/ß/g,'ss')
|
||
.replace(/[^a-z0-9]+/g, '-')
|
||
.replace(/^-+|-+$/g, '');
|
||
});
|
||
slugInput.addEventListener('input', () => { slugManuallyEdited = true; });
|
||
|
||
// ── HTML-Quellcode-Toggle ───────────────────────────────────────────────────
|
||
const toggleBtn = document.getElementById('toggle-source');
|
||
const sourceEditor = document.getElementById('source-editor');
|
||
const quillWrapper = document.getElementById('quill-editor');
|
||
let sourceMode = false;
|
||
|
||
toggleBtn.addEventListener('click', () => {
|
||
sourceMode = !sourceMode;
|
||
if (sourceMode) {
|
||
// Quill → HTML
|
||
sourceEditor.value = quill.root.innerHTML === '<p><br></p>' ? '' : quill.root.innerHTML;
|
||
quillWrapper.style.display = 'none';
|
||
sourceEditor.classList.remove('hidden');
|
||
toggleBtn.textContent = 'WYSIWYG';
|
||
toggleBtn.classList.add('bg-yellow-50', 'text-yellow-700', 'border-yellow-300');
|
||
} else {
|
||
// HTML → Quill
|
||
quill.root.innerHTML = sourceEditor.value;
|
||
sourceEditor.classList.add('hidden');
|
||
quillWrapper.style.display = '';
|
||
toggleBtn.textContent = 'HTML-Quellcode';
|
||
toggleBtn.classList.remove('bg-yellow-50', 'text-yellow-700', 'border-yellow-300');
|
||
}
|
||
});
|
||
|
||
// ── Beim Absenden: Quill-HTML in hidden field schreiben ─────────────────────
|
||
document.getElementById('page-form').addEventListener('submit', () => {
|
||
const html = sourceMode
|
||
? sourceEditor.value
|
||
: (quill.root.innerHTML === '<p><br></p>' ? '' : quill.root.innerHTML);
|
||
document.getElementById('content-hidden').value = html;
|
||
});
|
||
</script>
|
||
|
||
@push('styles')
|
||
<style>
|
||
/* Quill-Bilder klickbar im Editor */
|
||
.ql-editor img { cursor: pointer; }
|
||
</style>
|
||
@endpush
|
||
@endsection
|