First commit
This commit is contained in:
commit
7cf3558ba7
12933 changed files with 1180047 additions and 0 deletions
|
|
@ -0,0 +1,27 @@
|
|||
<div>
|
||||
<form wire:submit.prevent="save">
|
||||
{{-- Post Title --}}
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-gray-700">Title</label>
|
||||
<input type="text" id="title" wire:model.defer="post.title" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
|
||||
</div>
|
||||
|
||||
{{-- Post Content --}}
|
||||
<div class="mt-4">
|
||||
<label for="content" class="block text-sm font-medium text-gray-700">Content</label>
|
||||
<textarea id="content" wire:model.defer="post.content" rows="10" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm"></textarea>
|
||||
</div>
|
||||
|
||||
{{-- Tags --}}
|
||||
<div class="mt-4">
|
||||
<label for="tags" class="block text-sm font-medium text-gray-700">Tags (comma-separated)</label>
|
||||
<input type="text" id="tags" wire:model.defer="tags" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm" placeholder="laravel, php, cms">
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-500 active:bg-blue-700">
|
||||
Save Post
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,366 @@
|
|||
<div class="space-y-6">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900">Blog Manager</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Create and manage blog posts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<flux:button wire:click="createPost" variant="primary">
|
||||
New Post
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Stats Cards --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
@foreach($this->getStats() as $stat)
|
||||
<flux:card>
|
||||
<div class="text-center p-4">
|
||||
<div class="text-2xl font-bold text-gray-900">{{ $stat['value'] }}</div>
|
||||
<div class="text-sm text-gray-500">{{ $stat['label'] }}</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Filters --}}
|
||||
<flux:card>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div class="flex-1 min-w-64">
|
||||
<flux:input wire:model.live="search"
|
||||
placeholder="Search posts..."
|
||||
class="w-full" />
|
||||
</div>
|
||||
|
||||
<flux:select wire:model.live="statusFilter" placeholder="All Status">
|
||||
<flux:option value="">All Status</flux:option>
|
||||
<flux:option value="published">Published</flux:option>
|
||||
<flux:option value="draft">Draft</flux:option>
|
||||
<flux:option value="featured">Featured</flux:option>
|
||||
</flux:select>
|
||||
|
||||
<flux:select wire:model.live="categoryFilter" placeholder="All Categories">
|
||||
<flux:option value="">All Categories</flux:option>
|
||||
@foreach($this->getCategories() as $category)
|
||||
<flux:option value="{{ $category }}">{{ $category }}</flux:option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
|
||||
<flux:select wire:model.live="sortBy">
|
||||
<flux:option value="created_at">Created Date</flux:option>
|
||||
<flux:option value="published_at">Published Date</flux:option>
|
||||
<flux:option value="title">Title</flux:option>
|
||||
</flux:select>
|
||||
|
||||
<flux:select wire:model.live="sortDirection">
|
||||
<flux:option value="desc">Newest First</flux:option>
|
||||
<flux:option value="asc">Oldest First</flux:option>
|
||||
</flux:select>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Posts Table --}}
|
||||
<flux:card>
|
||||
<flux:table>
|
||||
<flux:columns>
|
||||
<flux:column>Title</flux:column>
|
||||
<flux:column>Status</flux:column>
|
||||
<flux:column>Category</flux:column>
|
||||
<flux:column>Author</flux:column>
|
||||
<flux:column>Published</flux:column>
|
||||
<flux:column>Actions</flux:column>
|
||||
</flux:columns>
|
||||
|
||||
<flux:rows>
|
||||
@forelse($posts as $post)
|
||||
<flux:row wire:key="post-{{ $post->id }}">
|
||||
<flux:cell>
|
||||
<div>
|
||||
<div class="font-medium text-gray-900">
|
||||
{{ $post->title }}
|
||||
</div>
|
||||
@if($post->excerpt)
|
||||
<div class="text-sm text-gray-500 mt-1">
|
||||
{{ str_limit($post->excerpt, 60) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:cell>
|
||||
|
||||
<flux:cell>
|
||||
<div class="flex items-center space-x-2">
|
||||
@if($post->is_published)
|
||||
<flux:badge color="green" size="sm">Published</flux:badge>
|
||||
@else
|
||||
<flux:badge color="gray" size="sm">Draft</flux:badge>
|
||||
@endif
|
||||
|
||||
@if($post->is_featured)
|
||||
<flux:badge color="blue" size="sm">Featured</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
</flux:cell>
|
||||
|
||||
<flux:cell>
|
||||
{{ $post->category ?? '—' }}
|
||||
</flux:cell>
|
||||
|
||||
<flux:cell>
|
||||
{{ $post->author?->name ?? '—' }}
|
||||
</flux:cell>
|
||||
|
||||
<flux:cell>
|
||||
{{ $post->published_at?->format('M j, Y') ?? '—' }}
|
||||
</flux:cell>
|
||||
|
||||
<flux:cell>
|
||||
<div class="flex items-center space-x-2">
|
||||
@if($post->is_published)
|
||||
<flux:button href="{{ $post->getUrl() }}"
|
||||
target="_blank"
|
||||
size="sm"
|
||||
variant="ghost">
|
||||
View
|
||||
</flux:button>
|
||||
@endif
|
||||
|
||||
<flux:button wire:click="editPost({{ $post->id }})"
|
||||
size="sm"
|
||||
variant="ghost">
|
||||
Edit
|
||||
</flux:button>
|
||||
|
||||
<flux:button wire:click="duplicatePost({{ $post->id }})"
|
||||
size="sm"
|
||||
variant="ghost">
|
||||
Duplicate
|
||||
</flux:button>
|
||||
|
||||
<flux:button wire:click="deletePost({{ $post->id }})"
|
||||
size="sm"
|
||||
variant="danger">
|
||||
Delete
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:cell>
|
||||
</flux:row>
|
||||
@empty
|
||||
<flux:row>
|
||||
<flux:cell colspan="6">
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<flux:icon.document-text class="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
||||
<p>No blog posts found.</p>
|
||||
<p class="text-sm">Create your first blog post to get started.</p>
|
||||
</div>
|
||||
</flux:cell>
|
||||
</flux:row>
|
||||
@endforelse
|
||||
</flux:rows>
|
||||
</flux:table>
|
||||
|
||||
{{-- Pagination --}}
|
||||
@if($posts->hasPages())
|
||||
<div class="px-6 py-4 border-t border-gray-200">
|
||||
{{ $posts->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</flux:card>
|
||||
|
||||
{{-- Post Form Modal --}}
|
||||
<flux:modal name="post-form" class="md:w-6xl max-h-screen overflow-y-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<flux:heading size="lg">
|
||||
{{ $editingPost ? 'Edit Post' : 'Create Post' }}
|
||||
</flux:heading>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
@if($editingPost)
|
||||
<flux:button wire:click="saveAsDraft" variant="ghost">
|
||||
Save as Draft
|
||||
</flux:button>
|
||||
<flux:button wire:click="publishPost" variant="primary">
|
||||
{{ $editingPost->is_published ? 'Update' : 'Publish' }}
|
||||
</flux:button>
|
||||
@else
|
||||
<flux:button wire:click="saveDraft" variant="ghost">
|
||||
Save Draft
|
||||
</flux:button>
|
||||
<flux:button wire:click="saveAndPublish" variant="primary">
|
||||
Publish
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Language Tabs --}}
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
@foreach($availableLocales as $locale)
|
||||
<button wire:click="setActiveLocale('{{ $locale }}')"
|
||||
class="py-2 px-1 border-b-2 font-medium text-sm {{ $activeLocale === $locale ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
||||
{{ strtoupper($locale) }}
|
||||
</button>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{{-- Main Content --}}
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
{{-- Title & Slug --}}
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>Title ({{ strtoupper($activeLocale) }})</flux:label>
|
||||
<flux:input wire:model.live="postForm.title.{{ $activeLocale }}"
|
||||
placeholder="Enter post title" />
|
||||
<flux:error name="postForm.title.{{ $activeLocale }}" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Slug ({{ strtoupper($activeLocale) }})</flux:label>
|
||||
<flux:input wire:model.live="postForm.slug.{{ $activeLocale }}"
|
||||
placeholder="post-url-slug" />
|
||||
<flux:error name="postForm.slug.{{ $activeLocale }}" />
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
{{-- Excerpt --}}
|
||||
<flux:field>
|
||||
<flux:label>Excerpt ({{ strtoupper($activeLocale) }})</flux:label>
|
||||
<flux:textarea wire:model.live="postForm.excerpt.{{ $activeLocale }}"
|
||||
rows="3"
|
||||
placeholder="Brief description of the post" />
|
||||
<flux:description>Optional excerpt for post listings and SEO</flux:description>
|
||||
<flux:error name="postForm.excerpt.{{ $activeLocale }}" />
|
||||
</flux:field>
|
||||
|
||||
{{-- Content --}}
|
||||
<flux:field>
|
||||
<flux:label>Content ({{ strtoupper($activeLocale) }})</flux:label>
|
||||
<div wire:ignore>
|
||||
<textarea wire:model.live="postForm.content.{{ $activeLocale }}"
|
||||
class="wysiwyg-editor"
|
||||
rows="15"
|
||||
placeholder="Write your post content here..."></textarea>
|
||||
</div>
|
||||
<flux:error name="postForm.content.{{ $activeLocale }}" />
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
{{-- Sidebar --}}
|
||||
<div class="space-y-6">
|
||||
{{-- Publishing Options --}}
|
||||
<flux:card>
|
||||
<flux:card.header>
|
||||
<flux:heading size="sm">Publishing</flux:heading>
|
||||
</flux:card.header>
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>Published Date</flux:label>
|
||||
<flux:input wire:model.live="postForm.published_at"
|
||||
type="datetime-local" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model.live="postForm.is_featured">
|
||||
Featured Post
|
||||
</flux:checkbox>
|
||||
<flux:description>Featured posts appear prominently on the site</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Categories & Tags --}}
|
||||
<flux:card>
|
||||
<flux:card.header>
|
||||
<flux:heading size="sm">Organization</flux:heading>
|
||||
</flux:card.header>
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>Category</flux:label>
|
||||
<flux:input wire:model.live="postForm.category"
|
||||
placeholder="e.g., Technology, News" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Tags</flux:label>
|
||||
<flux:input wire:model.live="tagsInput"
|
||||
placeholder="Enter tags separated by commas" />
|
||||
<flux:description>Separate multiple tags with commas</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- SEO Settings --}}
|
||||
<flux:card>
|
||||
<flux:card.header>
|
||||
<flux:heading size="sm">SEO Settings</flux:heading>
|
||||
</flux:card.header>
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>Meta Title ({{ strtoupper($activeLocale) }})</flux:label>
|
||||
<flux:input wire:model.live="postForm.meta_title.{{ $activeLocale }}"
|
||||
placeholder="SEO title (defaults to post title)" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Meta Description ({{ strtoupper($activeLocale) }})</flux:label>
|
||||
<flux:textarea wire:model.live="postForm.meta_description.{{ $activeLocale }}"
|
||||
rows="3"
|
||||
placeholder="SEO description (defaults to excerpt)" />
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Featured Image --}}
|
||||
<flux:card>
|
||||
<flux:card.header>
|
||||
<flux:heading size="sm">Featured Image</flux:heading>
|
||||
</flux:card.header>
|
||||
|
||||
<div class="space-y-4">
|
||||
@if($featuredImage)
|
||||
<div class="relative">
|
||||
<img src="{{ $featuredImage->getUrl('thumb') }}"
|
||||
alt="Featured image"
|
||||
class="w-full rounded">
|
||||
<button wire:click="removeFeaturedImage"
|
||||
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
@else
|
||||
<flux:button wire:click="selectFeaturedImage" size="sm" class="w-full">
|
||||
Select Featured Image
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-6 border-t border-gray-200">
|
||||
<flux:button wire:click="$dispatch('close-modal')" variant="ghost">
|
||||
Cancel
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize WYSIWYG editor for content
|
||||
// You can integrate your preferred editor here (CKEditor, TinyMCE, Quill, etc.)
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
<div class="space-y-6">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900">
|
||||
{{ $componentData ? 'Edit Component' : 'Add Component' }}
|
||||
</h1>
|
||||
@if($componentType)
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Component Type: {{ class_basename($componentType) }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<flux:button wire:click="$dispatch('close-modal')" variant="ghost">
|
||||
Cancel
|
||||
</flux:button>
|
||||
<flux:button wire:click="save" variant="primary">
|
||||
{{ $componentData ? 'Update' : 'Add' }} Component
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Component Settings --}}
|
||||
@if($componentType && $fields)
|
||||
<flux:card>
|
||||
<flux:card.header>
|
||||
<flux:heading size="lg">Component Settings</flux:heading>
|
||||
</flux:card.header>
|
||||
|
||||
<div class="space-y-6">
|
||||
{{-- Language Tabs for Translatable Fields --}}
|
||||
@if($hasTranslatableFields)
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
@foreach($availableLocales as $locale)
|
||||
<button wire:click="setActiveLocale('{{ $locale }}')"
|
||||
class="py-2 px-1 border-b-2 font-medium text-sm {{ $activeLocale === $locale ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
||||
{{ strtoupper($locale) }}
|
||||
</button>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Dynamic Fields --}}
|
||||
@foreach($fields as $field)
|
||||
<div class="space-y-2">
|
||||
@if($field->isTranslatable() && $hasTranslatableFields)
|
||||
{{-- Translatable Field --}}
|
||||
<flux:field>
|
||||
<flux:label>{{ $field->getLabel() }}</flux:label>
|
||||
@if($field->getHelpText())
|
||||
<flux:description>{{ $field->getHelpText() }}</flux:description>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$fieldKey = $field->getKey();
|
||||
$wireModel = "content.{$fieldKey}.{$activeLocale}";
|
||||
@endphp
|
||||
|
||||
@switch($field->getType())
|
||||
@case('text')
|
||||
<flux:input wire:model.live="{{ $wireModel }}"
|
||||
placeholder="{{ $field->getPlaceholder() }}" />
|
||||
@break
|
||||
|
||||
@case('textarea')
|
||||
<flux:textarea wire:model.live="{{ $wireModel }}"
|
||||
placeholder="{{ $field->getPlaceholder() }}"
|
||||
rows="4" />
|
||||
@break
|
||||
|
||||
@case('wysiwyg')
|
||||
<div wire:ignore>
|
||||
<textarea wire:model.live="{{ $wireModel }}"
|
||||
class="wysiwyg-editor"
|
||||
rows="10"></textarea>
|
||||
</div>
|
||||
@break
|
||||
|
||||
@case('select')
|
||||
<flux:select wire:model.live="{{ $wireModel }}">
|
||||
@if($field->getEmptyOption())
|
||||
<flux:option value="">{{ $field->getEmptyOption() }}</flux:option>
|
||||
@endif
|
||||
@foreach($field->getOptions() as $value => $label)
|
||||
<flux:option value="{{ $value }}">{{ $label }}</flux:option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
@break
|
||||
@endswitch
|
||||
|
||||
<flux:error name="{{ $wireModel }}" />
|
||||
</flux:field>
|
||||
@else
|
||||
{{-- Non-translatable Field --}}
|
||||
<flux:field>
|
||||
<flux:label>{{ $field->getLabel() }}</flux:label>
|
||||
@if($field->getHelpText())
|
||||
<flux:description>{{ $field->getHelpText() }}</flux:description>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$fieldKey = $field->getKey();
|
||||
$wireModel = "content.{$fieldKey}";
|
||||
@endphp
|
||||
|
||||
@switch($field->getType())
|
||||
@case('text')
|
||||
<flux:input wire:model.live="{{ $wireModel }}"
|
||||
placeholder="{{ $field->getPlaceholder() }}" />
|
||||
@break
|
||||
|
||||
@case('number')
|
||||
<flux:input wire:model.live="{{ $wireModel }}"
|
||||
type="number"
|
||||
step="{{ $field->getStep() }}"
|
||||
@if($field->getMin() !== null) min="{{ $field->getMin() }}" @endif
|
||||
@if($field->getMax() !== null) max="{{ $field->getMax() }}" @endif
|
||||
placeholder="{{ $field->getPlaceholder() }}" />
|
||||
@break
|
||||
|
||||
@case('boolean')
|
||||
@if($field->getDisplayType() === 'toggle')
|
||||
<flux:switch wire:model.live="{{ $wireModel }}">
|
||||
{{ $field->getTrueLabel() }}
|
||||
</flux:switch>
|
||||
@else
|
||||
<flux:checkbox wire:model.live="{{ $wireModel }}">
|
||||
{{ $field->getTrueLabel() }}
|
||||
</flux:checkbox>
|
||||
@endif
|
||||
@break
|
||||
|
||||
@case('select')
|
||||
@if($field->isMultiple())
|
||||
<flux:select wire:model.live="{{ $wireModel }}" multiple>
|
||||
@foreach($field->getOptions() as $value => $label)
|
||||
<flux:option value="{{ $value }}">{{ $label }}</flux:option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
@else
|
||||
<flux:select wire:model.live="{{ $wireModel }}">
|
||||
@if($field->getEmptyOption())
|
||||
<flux:option value="">{{ $field->getEmptyOption() }}</flux:option>
|
||||
@endif
|
||||
@foreach($field->getOptions() as $value => $label)
|
||||
<flux:option value="{{ $value }}">{{ $label }}</flux:option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
@endif
|
||||
@break
|
||||
|
||||
@case('media')
|
||||
<div class="space-y-3">
|
||||
<flux:button wire:click="openMediaPicker('{{ $fieldKey }}')" size="sm">
|
||||
Select {{ $field->acceptsImages() ? 'Image' : 'File' }}
|
||||
</flux:button>
|
||||
|
||||
@if(!empty($content[$fieldKey]))
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
@if($field->isMultiple())
|
||||
@foreach($content[$fieldKey] as $index => $mediaId)
|
||||
<div class="relative">
|
||||
<img src="{{ $this->getMediaUrl($mediaId) }}"
|
||||
alt="Selected media"
|
||||
class="w-full h-24 object-cover rounded">
|
||||
<button wire:click="removeMedia('{{ $fieldKey }}', {{ $index }})"
|
||||
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
<div class="relative">
|
||||
<img src="{{ $this->getMediaUrl($content[$fieldKey]) }}"
|
||||
alt="Selected media"
|
||||
class="w-full h-24 object-cover rounded">
|
||||
<button wire:click="removeMedia('{{ $fieldKey }}')"
|
||||
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@break
|
||||
@endswitch
|
||||
|
||||
<flux:error name="{{ $wireModel }}" />
|
||||
</flux:field>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Component Visibility Settings --}}
|
||||
<flux:card>
|
||||
<flux:card.header>
|
||||
<flux:heading size="lg">Visibility Settings</flux:heading>
|
||||
</flux:card.header>
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model.live="settings.is_active">
|
||||
Component is active
|
||||
</flux:checkbox>
|
||||
<flux:description>Inactive components will not be displayed on the frontend</flux:description>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>CSS Classes</flux:label>
|
||||
<flux:input wire:model.live="settings.css_classes"
|
||||
placeholder="Additional CSS classes" />
|
||||
<flux:description>Custom CSS classes to apply to this component</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
</flux:card>
|
||||
@else
|
||||
<div class="text-center py-8">
|
||||
<flux:icon.exclamation-triangle class="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<p class="text-gray-500">No component type selected or fields defined.</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Media Picker Modal --}}
|
||||
<flux:modal name="media-picker" class="md:w-4xl">
|
||||
<div class="space-y-6">
|
||||
<flux:heading size="lg">Select Media</flux:heading>
|
||||
|
||||
<div class="grid grid-cols-4 gap-4 max-h-96 overflow-y-auto">
|
||||
{{-- Media items would be loaded here --}}
|
||||
<div class="text-center py-8 col-span-4 text-gray-500">
|
||||
Media picker implementation needed
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Initialize WYSIWYG editors
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize your preferred WYSIWYG editor here
|
||||
// Example: CKEditor, TinyMCE, Quill, etc.
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
<div class="space-y-6">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900">Media Manager</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Manage your media files and assets
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<flux:button wire:click="$dispatch('open-upload-modal')" variant="primary">
|
||||
Upload Media
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Filters & Search --}}
|
||||
<flux:card>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-1">
|
||||
<flux:input wire:model.live="search"
|
||||
placeholder="Search media files..."
|
||||
class="w-full" />
|
||||
</div>
|
||||
|
||||
<flux:select wire:model.live="filterType" placeholder="All Types">
|
||||
<flux:option value="">All Types</flux:option>
|
||||
<flux:option value="image">Images</flux:option>
|
||||
<flux:option value="document">Documents</flux:option>
|
||||
<flux:option value="video">Videos</flux:option>
|
||||
<flux:option value="audio">Audio</flux:option>
|
||||
</flux:select>
|
||||
|
||||
<flux:select wire:model.live="sortBy">
|
||||
<flux:option value="created_at">Upload Date</flux:option>
|
||||
<flux:option value="name">Name</flux:option>
|
||||
<flux:option value="size">Size</flux:option>
|
||||
</flux:select>
|
||||
|
||||
<flux:select wire:model.live="sortDirection">
|
||||
<flux:option value="desc">Newest First</flux:option>
|
||||
<flux:option value="asc">Oldest First</flux:option>
|
||||
</flux:select>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Media Grid --}}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
@forelse($mediaItems as $media)
|
||||
<div wire:key="media-{{ $media->id }}"
|
||||
class="group relative bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-md transition-shadow">
|
||||
|
||||
{{-- Media Preview --}}
|
||||
<div class="aspect-square bg-gray-100 flex items-center justify-center">
|
||||
@if($media->hasGeneratedConversion('thumb'))
|
||||
<img src="{{ $media->getUrl('thumb') }}"
|
||||
alt="{{ $media->name }}"
|
||||
class="w-full h-full object-cover">
|
||||
@elseif(str_starts_with($media->mime_type, 'image/'))
|
||||
<img src="{{ $media->getUrl() }}"
|
||||
alt="{{ $media->name }}"
|
||||
class="w-full h-full object-cover">
|
||||
@else
|
||||
<div class="text-center p-4">
|
||||
@switch(explode('/', $media->mime_type)[0])
|
||||
@case('video')
|
||||
<flux:icon.film class="w-8 h-8 mx-auto text-gray-400 mb-2" />
|
||||
@break
|
||||
@case('audio')
|
||||
<flux:icon.musical-note class="w-8 h-8 mx-auto text-gray-400 mb-2" />
|
||||
@break
|
||||
@default
|
||||
<flux:icon.document class="w-8 h-8 mx-auto text-gray-400 mb-2" />
|
||||
@endswitch
|
||||
<span class="text-xs text-gray-500 font-medium">
|
||||
{{ strtoupper(pathinfo($media->name, PATHINFO_EXTENSION)) }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Media Info --}}
|
||||
<div class="p-3">
|
||||
<h3 class="text-sm font-medium text-gray-900 truncate"
|
||||
title="{{ $media->name }}">
|
||||
{{ $media->name }}
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ $this->formatFileSize($media->size) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{-- Actions Overlay --}}
|
||||
<div class="absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center space-x-2">
|
||||
<flux:button wire:click="viewMedia({{ $media->id }})"
|
||||
size="sm"
|
||||
variant="white">
|
||||
View
|
||||
</flux:button>
|
||||
<flux:button wire:click="editMedia({{ $media->id }})"
|
||||
size="sm"
|
||||
variant="white">
|
||||
Edit
|
||||
</flux:button>
|
||||
<flux:button wire:click="deleteMedia({{ $media->id }})"
|
||||
size="sm"
|
||||
variant="danger">
|
||||
Delete
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
{{-- Selection Checkbox --}}
|
||||
@if($selectionMode)
|
||||
<div class="absolute top-2 left-2">
|
||||
<flux:checkbox wire:model.live="selectedMedia"
|
||||
value="{{ $media->id }}"
|
||||
class="bg-white" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-span-full text-center py-12">
|
||||
<flux:icon.photo class="w-12 h-12 mx-auto text-gray-300 mb-4" />
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">No media files</h3>
|
||||
<p class="text-gray-500 mb-4">Upload your first media file to get started.</p>
|
||||
<flux:button wire:click="$dispatch('open-upload-modal')" variant="primary">
|
||||
Upload Media
|
||||
</flux:button>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
@if($mediaItems->hasPages())
|
||||
<div class="flex justify-center">
|
||||
{{ $mediaItems->links() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Upload Modal --}}
|
||||
<flux:modal name="upload-modal" class="md:w-2xl">
|
||||
<div class="space-y-6">
|
||||
<flux:heading size="lg">Upload Media</flux:heading>
|
||||
|
||||
<div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center"
|
||||
x-data="{
|
||||
isDragging: false,
|
||||
handleDrop(e) {
|
||||
this.isDragging = false;
|
||||
// Handle file drop
|
||||
}
|
||||
}"
|
||||
x-on:dragover.prevent="isDragging = true"
|
||||
x-on:dragleave.prevent="isDragging = false"
|
||||
x-on:drop.prevent="handleDrop"
|
||||
:class="{ 'border-blue-400 bg-blue-50': isDragging }">
|
||||
|
||||
<flux:icon.cloud-arrow-up class="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Upload files</h3>
|
||||
<p class="text-gray-500 mb-4">Drag and drop files here or click to browse</p>
|
||||
|
||||
<flux:button wire:click="triggerFileInput" variant="primary">
|
||||
Choose Files
|
||||
</flux:button>
|
||||
|
||||
<input type="file"
|
||||
wire:model="uploadFiles"
|
||||
multiple
|
||||
accept="image/*,video/*,audio/*,.pdf,.doc,.docx"
|
||||
class="hidden"
|
||||
id="file-input">
|
||||
</div>
|
||||
|
||||
{{-- Upload Progress --}}
|
||||
@if($uploading)
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium">Uploading...</span>
|
||||
<span class="text-sm text-gray-500">{{ $uploadProgress }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style="width: {{ $uploadProgress }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
{{-- Media Detail Modal --}}
|
||||
<flux:modal name="media-detail" class="md:w-4xl">
|
||||
@if($viewingMedia)
|
||||
<div class="space-y-6">
|
||||
<flux:heading size="lg">{{ $viewingMedia->name }}</flux:heading>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{{-- Media Preview --}}
|
||||
<div class="space-y-4">
|
||||
@if(str_starts_with($viewingMedia->mime_type, 'image/'))
|
||||
<img src="{{ $viewingMedia->getUrl() }}"
|
||||
alt="{{ $viewingMedia->name }}"
|
||||
class="w-full rounded-lg">
|
||||
@else
|
||||
<div class="bg-gray-100 rounded-lg p-8 text-center">
|
||||
<flux:icon.document class="w-16 h-16 mx-auto text-gray-400 mb-4" />
|
||||
<p class="text-gray-600">{{ $viewingMedia->name }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<flux:button href="{{ $viewingMedia->getUrl() }}"
|
||||
target="_blank"
|
||||
variant="outline"
|
||||
class="w-full">
|
||||
Download Original
|
||||
</flux:button>
|
||||
</div>
|
||||
|
||||
{{-- Media Details --}}
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>File Name</flux:label>
|
||||
<flux:input wire:model.live="editingMedia.name" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Alt Text</flux:label>
|
||||
<flux:input wire:model.live="editingMedia.alt_text"
|
||||
placeholder="Description for accessibility" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Caption</flux:label>
|
||||
<flux:textarea wire:model.live="editingMedia.caption"
|
||||
rows="3"
|
||||
placeholder="Optional caption" />
|
||||
</flux:field>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">File Type:</span>
|
||||
<span class="text-gray-600">{{ $viewingMedia->mime_type }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">File Size:</span>
|
||||
<span class="text-gray-600">{{ $this->formatFileSize($viewingMedia->size) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">Uploaded:</span>
|
||||
<span class="text-gray-600">{{ $viewingMedia->created_at->format('M j, Y') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-700">Collection:</span>
|
||||
<span class="text-gray-600">{{ $viewingMedia->collection_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3 pt-4">
|
||||
<flux:button wire:click="updateMedia" variant="primary">
|
||||
Update
|
||||
</flux:button>
|
||||
<flux:button wire:click="$dispatch('close-modal')" variant="ghost">
|
||||
Cancel
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</flux:modal>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// File input trigger
|
||||
window.addEventListener('triggerFileInput', function() {
|
||||
document.getElementById('file-input').click();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
<div class="space-y-6">
|
||||
{{-- Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900">Navigation Manager</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Manage site navigation and menu structures
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
<flux:button wire:click="createNavigation" variant="primary">
|
||||
Create Navigation
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Navigation Selector --}}
|
||||
<flux:card>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-1">
|
||||
<flux:select wire:model.live="selectedNavigationId" placeholder="Select Navigation">
|
||||
@foreach($navigations as $nav)
|
||||
<flux:option value="{{ $nav->id }}">{{ $nav->display_name }}</flux:option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
</div>
|
||||
|
||||
@if($selectedNavigationId)
|
||||
<flux:button wire:click="editNavigation({{ $selectedNavigationId }})" size="sm">
|
||||
Edit Navigation
|
||||
</flux:button>
|
||||
<flux:button wire:click="deleteNavigation({{ $selectedNavigationId }})"
|
||||
variant="danger" size="sm">
|
||||
Delete
|
||||
</flux:button>
|
||||
@endif
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Navigation Items --}}
|
||||
@if($selectedNavigationId && $navigationItems)
|
||||
<flux:card>
|
||||
<flux:card.header>
|
||||
<div class="flex items-center justify-between">
|
||||
<flux:heading size="lg">Navigation Items</flux:heading>
|
||||
<flux:button wire:click="addNavigationItem" size="sm">
|
||||
Add Item
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card.header>
|
||||
|
||||
<div class="space-y-2" wire:sortable="updateItemOrder">
|
||||
@forelse($navigationItems as $item)
|
||||
<div wire:sortable.item="{{ $item->id }}"
|
||||
wire:key="nav-item-{{ $item->id }}"
|
||||
class="nested-sortable-item">
|
||||
@include('flux-cms-components::partials.navigation-item', ['item' => $item, 'level' => 0])
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<flux:icon.bars-3 class="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
||||
<p>No navigation items yet.</p>
|
||||
<p class="text-sm">Click "Add Item" to create your first navigation item.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</flux:card>
|
||||
@endif
|
||||
|
||||
{{-- Navigation Form Modal --}}
|
||||
<flux:modal name="navigation-form" class="md:w-2xl">
|
||||
<div class="space-y-6">
|
||||
<flux:heading size="lg">
|
||||
{{ $editingNavigation ? 'Edit Navigation' : 'Create Navigation' }}
|
||||
</flux:heading>
|
||||
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>Name</flux:label>
|
||||
<flux:input wire:model.live="navigationForm.name"
|
||||
placeholder="main-menu" />
|
||||
<flux:description>Unique identifier for this navigation (lowercase, no spaces)</flux:description>
|
||||
<flux:error name="navigationForm.name" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Display Name</flux:label>
|
||||
<flux:input wire:model.live="navigationForm.display_name"
|
||||
placeholder="Main Menu" />
|
||||
<flux:description>Human-readable name for this navigation</flux:description>
|
||||
<flux:error name="navigationForm.display_name" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model.live="navigationForm.is_active">
|
||||
Active
|
||||
</flux:checkbox>
|
||||
<flux:description>Inactive navigations will not be displayed</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<flux:button wire:click="$dispatch('close-modal')" variant="ghost">
|
||||
Cancel
|
||||
</flux:button>
|
||||
<flux:button wire:click="saveNavigation" variant="primary">
|
||||
{{ $editingNavigation ? 'Update' : 'Create' }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
|
||||
{{-- Navigation Item Form Modal --}}
|
||||
<flux:modal name="navigation-item-form" class="md:w-2xl">
|
||||
<div class="space-y-6">
|
||||
<flux:heading size="lg">
|
||||
{{ $editingItem ? 'Edit Navigation Item' : 'Add Navigation Item' }}
|
||||
</flux:heading>
|
||||
|
||||
{{-- Language Tabs --}}
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
@foreach($availableLocales as $locale)
|
||||
<button wire:click="setActiveLocale('{{ $locale }}')"
|
||||
class="py-2 px-1 border-b-2 font-medium text-sm {{ $activeLocale === $locale ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
||||
{{ strtoupper($locale) }}
|
||||
</button>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
{{-- Title (Translatable) --}}
|
||||
<flux:field>
|
||||
<flux:label>Title ({{ strtoupper($activeLocale) }})</flux:label>
|
||||
<flux:input wire:model.live="itemForm.title.{{ $activeLocale }}"
|
||||
placeholder="Navigation item title" />
|
||||
<flux:error name="itemForm.title.{{ $activeLocale }}" />
|
||||
</flux:field>
|
||||
|
||||
{{-- Link Type --}}
|
||||
<flux:field>
|
||||
<flux:label>Link Type</flux:label>
|
||||
<flux:select wire:model.live="itemForm.link_type">
|
||||
<flux:option value="page">Page</flux:option>
|
||||
<flux:option value="url">Custom URL</flux:option>
|
||||
<flux:option value="none">No Link</flux:option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
|
||||
{{-- Page Selection --}}
|
||||
@if($itemForm['link_type'] === 'page')
|
||||
<flux:field>
|
||||
<flux:label>Select Page</flux:label>
|
||||
<flux:select wire:model.live="itemForm.page_id" placeholder="Choose a page">
|
||||
@foreach($availablePages as $page)
|
||||
<flux:option value="{{ $page->id }}">{{ $page->title }}</flux:option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:error name="itemForm.page_id" />
|
||||
</flux:field>
|
||||
@endif
|
||||
|
||||
{{-- Custom URL --}}
|
||||
@if($itemForm['link_type'] === 'url')
|
||||
<flux:field>
|
||||
<flux:label>URL</flux:label>
|
||||
<flux:input wire:model.live="itemForm.url"
|
||||
placeholder="https://example.com" />
|
||||
<flux:error name="itemForm.url" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Target</flux:label>
|
||||
<flux:select wire:model.live="itemForm.target">
|
||||
<flux:option value="_self">Same Window</flux:option>
|
||||
<flux:option value="_blank">New Window</flux:option>
|
||||
</flux:select>
|
||||
</flux:field>
|
||||
@endif
|
||||
|
||||
{{-- Parent Item --}}
|
||||
<flux:field>
|
||||
<flux:label>Parent Item</flux:label>
|
||||
<flux:select wire:model.live="itemForm.parent_id" placeholder="No parent (top level)">
|
||||
@foreach($this->getParentOptions() as $option)
|
||||
<flux:option value="{{ $option['id'] }}">
|
||||
{{ str_repeat('— ', $option['level']) }}{{ $option['title'] }}
|
||||
</flux:option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:description>Choose a parent to create a sub-menu item</flux:description>
|
||||
</flux:field>
|
||||
|
||||
{{-- Settings --}}
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model.live="itemForm.is_active">
|
||||
Active
|
||||
</flux:checkbox>
|
||||
<flux:description>Inactive items will not be displayed</flux:description>
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<flux:button wire:click="$dispatch('close-modal')" variant="ghost">
|
||||
Cancel
|
||||
</flux:button>
|
||||
<flux:button wire:click="saveNavigationItem" variant="primary">
|
||||
{{ $editingItem ? 'Update' : 'Add' }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize nested sortable
|
||||
const initializeNestedSortable = () => {
|
||||
// Implementation for nested drag & drop would go here
|
||||
// You might want to use a library like SortableJS with nested support
|
||||
};
|
||||
|
||||
initializeNestedSortable();
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
<div class="space-y-6">
|
||||
{{-- Page Header --}}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900">
|
||||
{{ $editingPage ? 'Edit Page' : 'Create Page' }}
|
||||
</h1>
|
||||
@if($editingPage)
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Last updated {{ $editingPage->updated_at->diffForHumans() }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-3">
|
||||
@if($editingPage && $editingPage->is_published)
|
||||
<flux:badge color="green" size="sm">Published</flux:badge>
|
||||
@elseif($editingPage)
|
||||
<flux:badge color="yellow" size="sm">Draft</flux:badge>
|
||||
@endif
|
||||
|
||||
<flux:button wire:click="save" variant="primary">
|
||||
{{ $editingPage ? 'Update' : 'Create' }} Page
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Page Settings --}}
|
||||
<flux:card>
|
||||
<flux:card.header>
|
||||
<flux:heading size="lg">Page Settings</flux:heading>
|
||||
</flux:card.header>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{{-- Basic Information --}}
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>Title</flux:label>
|
||||
<flux:input wire:model.live="pageData.title" placeholder="Enter page title" />
|
||||
<flux:error name="pageData.title" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Slug</flux:label>
|
||||
<flux:input wire:model.live="pageData.slug" placeholder="page-url-slug" />
|
||||
<flux:error name="pageData.slug" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Template</flux:label>
|
||||
<flux:select wire:model.live="pageData.template" placeholder="Select template">
|
||||
@foreach($availableTemplates as $template)
|
||||
<flux:option value="{{ $template }}">{{ $template }}</flux:option>
|
||||
@endforeach
|
||||
</flux:select>
|
||||
<flux:error name="pageData.template" />
|
||||
</flux:field>
|
||||
</div>
|
||||
|
||||
{{-- SEO & Publishing --}}
|
||||
<div class="space-y-4">
|
||||
<flux:field>
|
||||
<flux:label>Meta Title</flux:label>
|
||||
<flux:input wire:model.live="pageData.meta_title" placeholder="SEO title" />
|
||||
<flux:error name="pageData.meta_title" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Meta Description</flux:label>
|
||||
<flux:textarea wire:model.live="pageData.meta_description" placeholder="SEO description" rows="3" />
|
||||
<flux:error name="pageData.meta_description" />
|
||||
</flux:field>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model.live="pageData.is_published">
|
||||
Published
|
||||
</flux:checkbox>
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:checkbox wire:model.live="pageData.show_in_navigation">
|
||||
Show in Navigation
|
||||
</flux:checkbox>
|
||||
</flux:field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Page Components --}}
|
||||
<flux:card>
|
||||
<flux:card.header>
|
||||
<div class="flex items-center justify-between">
|
||||
<flux:heading size="lg">Page Components</flux:heading>
|
||||
<flux:button wire:click="$dispatch('open-component-modal')" size="sm">
|
||||
Add Component
|
||||
</flux:button>
|
||||
</div>
|
||||
</flux:card.header>
|
||||
|
||||
<div class="space-y-4" wire:sortable="updateComponentOrder">
|
||||
@forelse($components as $index => $component)
|
||||
<div wire:sortable.item="{{ $component['id'] }}"
|
||||
wire:key="component-{{ $component['id'] }}"
|
||||
class="p-4 border border-gray-200 rounded-lg bg-gray-50">
|
||||
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<flux:icon.bars-3 class="w-5 h-5 text-gray-400 cursor-move" wire:sortable.handle />
|
||||
<span class="font-medium text-gray-900">
|
||||
{{ $component['component_type'] }}
|
||||
</span>
|
||||
@if(!$component['is_active'])
|
||||
<flux:badge color="gray" size="sm">Inactive</flux:badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<flux:button wire:click="editComponent({{ $index }})" size="sm" variant="ghost">
|
||||
Edit
|
||||
</flux:button>
|
||||
<flux:button wire:click="toggleComponent({{ $index }})" size="sm" variant="ghost">
|
||||
{{ $component['is_active'] ? 'Disable' : 'Enable' }}
|
||||
</flux:button>
|
||||
<flux:button wire:click="removeComponent({{ $index }})" size="sm" variant="danger">
|
||||
Remove
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Component Preview --}}
|
||||
<div class="text-sm text-gray-600">
|
||||
@if(!empty($component['content']['title']))
|
||||
<strong>Title:</strong> {{ $component['content']['title'] }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<flux:icon.puzzle-piece class="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
||||
<p>No components added yet.</p>
|
||||
<p class="text-sm">Click "Add Component" to get started.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</flux:card>
|
||||
|
||||
{{-- Component Selection Modal --}}
|
||||
<flux:modal name="component-modal" class="md:w-2xl">
|
||||
<div class="space-y-6">
|
||||
<flux:heading size="lg">Add Component</flux:heading>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@foreach($availableComponents as $componentClass => $componentInfo)
|
||||
<div wire:click="addComponent('{{ $componentClass }}')"
|
||||
class="p-4 border border-gray-200 rounded-lg cursor-pointer hover:border-blue-300 hover:bg-blue-50 transition-colors">
|
||||
<h3 class="font-medium text-gray-900 mb-2">
|
||||
{{ $componentInfo['name'] }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ $componentInfo['description'] ?? 'No description available' }}
|
||||
</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</flux:modal>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
<div class="flux-cms-blog-list">
|
||||
{{-- Header --}}
|
||||
@if($showHeader)
|
||||
<div class="blog-header mb-8">
|
||||
@if($title)
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-4">{{ $title }}</h1>
|
||||
@endif
|
||||
|
||||
@if($description)
|
||||
<p class="text-lg text-gray-600">{{ $description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Filters --}}
|
||||
@if($showFilters)
|
||||
<div class="blog-filters mb-8 space-y-4">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{{-- Search --}}
|
||||
<div class="flex-1 min-w-64">
|
||||
<input type="text"
|
||||
wire:model.live="search"
|
||||
placeholder="Search posts..."
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
{{-- Category Filter --}}
|
||||
@if($categories->isNotEmpty())
|
||||
<select wire:model.live="selectedCategory"
|
||||
class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">All Categories</option>
|
||||
@foreach($categories as $category)
|
||||
<option value="{{ $category }}">{{ $category }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@endif
|
||||
|
||||
{{-- Tag Filter --}}
|
||||
@if($tags->isNotEmpty())
|
||||
<select wire:model.live="selectedTag"
|
||||
class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">All Tags</option>
|
||||
@foreach($tags as $tag)
|
||||
<option value="{{ $tag }}">{{ $tag }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Active Filters --}}
|
||||
@if($search || $selectedCategory || $selectedTag)
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-600">Active filters:</span>
|
||||
|
||||
@if($search)
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
Search: "{{ $search }}"
|
||||
<button wire:click="$set('search', '')" class="ml-1 text-blue-600 hover:text-blue-800">×</button>
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if($selectedCategory)
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Category: {{ $selectedCategory }}
|
||||
<button wire:click="$set('selectedCategory', '')" class="ml-1 text-green-600 hover:text-green-800">×</button>
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if($selectedTag)
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
||||
Tag: {{ $selectedTag }}
|
||||
<button wire:click="$set('selectedTag', '')" class="ml-1 text-purple-600 hover:text-purple-800">×</button>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Posts Grid --}}
|
||||
<div class="blog-posts">
|
||||
@if($posts->isNotEmpty())
|
||||
<div class="grid gap-8 {{ $this->getGridClasses() }}">
|
||||
@foreach($posts as $post)
|
||||
<article class="blog-post-card bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
|
||||
wire:key="post-{{ $post->id }}">
|
||||
|
||||
{{-- Featured Image --}}
|
||||
@if($showFeaturedImages && $post->getFeaturedImage())
|
||||
<div class="aspect-w-16 aspect-h-9">
|
||||
<img src="{{ $post->getFeaturedImageUrl('hero') }}"
|
||||
alt="{{ $post->getTranslation('title', app()->getLocale()) }}"
|
||||
class="w-full h-48 object-cover">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="p-6">
|
||||
{{-- Meta Information --}}
|
||||
@if($showMeta)
|
||||
<div class="flex items-center space-x-4 text-sm text-gray-500 mb-3">
|
||||
@if($showDate)
|
||||
<time datetime="{{ $post->published_at->toISOString() }}">
|
||||
{{ $post->published_at->format('M j, Y') }}
|
||||
</time>
|
||||
@endif
|
||||
|
||||
@if($showAuthor && $post->author)
|
||||
<span>By {{ $post->author->name }}</span>
|
||||
@endif
|
||||
|
||||
@if($showCategory && $post->category)
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
{{ $post->category }}
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if($showReadingTime)
|
||||
<span>{{ $post->reading_time }} min read</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Title --}}
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-3">
|
||||
<a href="{{ $post->getUrl() }}"
|
||||
class="hover:text-blue-600 transition-colors">
|
||||
{{ $post->getTranslation('title', app()->getLocale()) }}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
{{-- Excerpt --}}
|
||||
@if($showExcerpts && $post->excerpt)
|
||||
<p class="text-gray-600 mb-4 line-clamp-3">
|
||||
{{ $post->getTranslation('excerpt', app()->getLocale()) }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
{{-- Tags --}}
|
||||
@if($showTags && $post->tags && count($post->tags) > 0)
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
@foreach($post->tags as $tag)
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{{ $tag }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Read More Link --}}
|
||||
@if($showReadMore)
|
||||
<a href="{{ $post->getUrl() }}"
|
||||
class="inline-flex items-center text-blue-600 hover:text-blue-800 font-medium">
|
||||
{{ $readMoreText }}
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
{{-- Featured Badge --}}
|
||||
@if($post->is_featured)
|
||||
<div class="absolute top-4 right-4">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
Featured
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
@if($posts->hasPages() && $showPagination)
|
||||
<div class="mt-8 flex justify-center">
|
||||
{{ $posts->links() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@else
|
||||
{{-- Empty State --}}
|
||||
<div class="text-center py-12">
|
||||
<div class="max-w-md mx-auto">
|
||||
<svg class="w-16 h-16 mx-auto text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"></path>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">No posts found</h3>
|
||||
<p class="text-gray-500">
|
||||
@if($search || $selectedCategory || $selectedTag)
|
||||
Try adjusting your filters to find what you're looking for.
|
||||
@else
|
||||
There are no published blog posts yet.
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Styles --}}
|
||||
@pushOnce('styles')
|
||||
<style>
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aspect-w-16 {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* 16:9 */
|
||||
}
|
||||
|
||||
.aspect-w-16 img {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
@endPushOnce
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
<article class="flux-cms-blog-post max-w-4xl mx-auto">
|
||||
{{-- Post Header --}}
|
||||
<header class="blog-post-header mb-8">
|
||||
{{-- Featured Image --}}
|
||||
@if($post->getFeaturedImage())
|
||||
<div class="aspect-w-16 aspect-h-9 mb-8">
|
||||
<img src="{{ $post->getFeaturedImageUrl('hero') }}"
|
||||
alt="{{ $post->getTranslation('title', app()->getLocale()) }}"
|
||||
class="w-full h-64 md:h-96 object-cover rounded-lg">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Category & Featured Badge --}}
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
@if($post->category)
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
||||
{{ $post->category }}
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if($post->is_featured)
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800">
|
||||
Featured
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Share Buttons --}}
|
||||
@if($showShareButtons)
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-500">Share:</span>
|
||||
<a href="https://twitter.com/intent/tweet?url={{ urlencode(request()->url()) }}&text={{ urlencode($post->getTranslation('title', app()->getLocale())) }}"
|
||||
target="_blank"
|
||||
class="text-gray-400 hover:text-blue-500 transition-colors">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.facebook.com/sharer/sharer.php?u={{ urlencode(request()->url()) }}"
|
||||
target="_blank"
|
||||
class="text-gray-400 hover:text-blue-600 transition-colors">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{ urlencode(request()->url()) }}"
|
||||
target="_blank"
|
||||
class="text-gray-400 hover:text-blue-700 transition-colors">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Title --}}
|
||||
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 leading-tight mb-6">
|
||||
{{ $post->getTranslation('title', app()->getLocale()) }}
|
||||
</h1>
|
||||
|
||||
{{-- Meta Information --}}
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-600 mb-6">
|
||||
@if($showAuthor && $post->author)
|
||||
<div class="flex items-center space-x-2">
|
||||
@if($post->author->profile_photo_url ?? false)
|
||||
<img src="{{ $post->author->profile_photo_url }}"
|
||||
alt="{{ $post->author->name }}"
|
||||
class="w-8 h-8 rounded-full">
|
||||
@endif
|
||||
<span>By <strong>{{ $post->author->name }}</strong></span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($showDate)
|
||||
<time datetime="{{ $post->published_at->toISOString() }}"
|
||||
class="flex items-center space-x-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<span>{{ $post->published_at->format('F j, Y') }}</span>
|
||||
</time>
|
||||
@endif
|
||||
|
||||
@if($showReadingTime)
|
||||
<div class="flex items-center space-x-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>{{ $post->reading_time }} min read</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($showUpdatedDate && $post->updated_at->gt($post->published_at))
|
||||
<div class="flex items-center space-x-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
<span>Updated {{ $post->updated_at->format('F j, Y') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Excerpt --}}
|
||||
@if($showExcerpt && $post->excerpt)
|
||||
<div class="text-lg text-gray-600 leading-relaxed border-l-4 border-blue-500 pl-6 italic">
|
||||
{{ $post->getTranslation('excerpt', app()->getLocale()) }}
|
||||
</div>
|
||||
@endif
|
||||
</header>
|
||||
|
||||
{{-- Post Content --}}
|
||||
<div class="blog-post-content prose prose-lg max-w-none">
|
||||
{!! $post->getTranslation('content', app()->getLocale()) !!}
|
||||
</div>
|
||||
|
||||
{{-- Tags --}}
|
||||
@if($showTags && $post->tags && count($post->tags) > 0)
|
||||
<div class="mt-8 pt-8 border-t border-gray-200">
|
||||
<h3 class="text-sm font-medium text-gray-900 mb-3">Tags</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach($post->tags as $tag)
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800 hover:bg-gray-200 transition-colors">
|
||||
#{{ $tag }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Author Bio --}}
|
||||
@if($showAuthorBio && $post->author)
|
||||
<div class="mt-12 pt-8 border-t border-gray-200">
|
||||
<div class="flex items-start space-x-4">
|
||||
@if($post->author->profile_photo_url ?? false)
|
||||
<img src="{{ $post->author->profile_photo_url }}"
|
||||
alt="{{ $post->author->name }}"
|
||||
class="w-16 h-16 rounded-full">
|
||||
@endif
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">{{ $post->author->name }}</h3>
|
||||
@if($post->author->bio ?? false)
|
||||
<p class="text-gray-600 mt-1">{{ $post->author->bio }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Navigation to Other Posts --}}
|
||||
@if($showNavigation && ($previousPost || $nextPost))
|
||||
<nav class="mt-12 pt-8 border-t border-gray-200">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
@if($previousPost)
|
||||
<a href="{{ $previousPost->getUrl() }}" class="group">
|
||||
<div class="flex items-center space-x-3 p-4 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors">
|
||||
<svg class="w-5 h-5 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="text-sm text-gray-500">Previous</div>
|
||||
<div class="font-medium text-gray-900 group-hover:text-blue-600">
|
||||
{{ str_limit($previousPost->getTranslation('title', app()->getLocale()), 50) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($nextPost)
|
||||
<a href="{{ $nextPost->getUrl() }}" class="group">
|
||||
<div class="flex items-center justify-end space-x-3 p-4 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors">
|
||||
<div class="text-right">
|
||||
<div class="text-sm text-gray-500">Next</div>
|
||||
<div class="font-medium text-gray-900 group-hover:text-blue-600">
|
||||
{{ str_limit($nextPost->getTranslation('title', app()->getLocale()), 50) }}
|
||||
</div>
|
||||
</div>
|
||||
<svg class="w-5 h-5 text-gray-400 group-hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</nav>
|
||||
@endif
|
||||
|
||||
{{-- SEO & Structured Data --}}
|
||||
@pushOnce('meta')
|
||||
<title>{{ $post->getSeoTitle() }}</title>
|
||||
<meta name="description" content="{{ $post->getSeoDescription() }}">
|
||||
<meta name="author" content="{{ $post->author?->name }}">
|
||||
|
||||
{{-- Open Graph --}}
|
||||
<meta property="og:title" content="{{ $post->getSeoTitle() }}">
|
||||
<meta property="og:description" content="{{ $post->getSeoDescription() }}">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="{{ $post->getUrl() }}">
|
||||
@if($post->getFeaturedImage())
|
||||
<meta property="og:image" content="{{ $post->getFeaturedImageUrl('hero') }}">
|
||||
@endif
|
||||
<meta property="article:published_time" content="{{ $post->published_at->toISOString() }}">
|
||||
<meta property="article:modified_time" content="{{ $post->updated_at->toISOString() }}">
|
||||
@if($post->author)
|
||||
<meta property="article:author" content="{{ $post->author->name }}">
|
||||
@endif
|
||||
|
||||
{{-- Twitter Card --}}
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ $post->getSeoTitle() }}">
|
||||
<meta name="twitter:description" content="{{ $post->getSeoDescription() }}">
|
||||
@if($post->getFeaturedImage())
|
||||
<meta name="twitter:image" content="{{ $post->getFeaturedImageUrl('hero') }}">
|
||||
@endif
|
||||
@endPushOnce
|
||||
|
||||
@pushOnce('structured-data')
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
"headline": "{{ $post->getTranslation('title', app()->getLocale()) }}",
|
||||
"description": "{{ $post->getSeoDescription() }}",
|
||||
"image": "{{ $post->getFeaturedImageUrl('hero') }}",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "{{ $post->author?->name }}"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "{{ config('app.name') }}"
|
||||
},
|
||||
"datePublished": "{{ $post->published_at->toISOString() }}",
|
||||
"dateModified": "{{ $post->updated_at->toISOString() }}",
|
||||
"mainEntityOfPage": {
|
||||
"@type": "WebPage",
|
||||
"@id": "{{ $post->getUrl() }}"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endPushOnce
|
||||
</article>
|
||||
|
||||
{{-- Styles for content --}}
|
||||
@pushOnce('styles')
|
||||
<style>
|
||||
.aspect-w-16 {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* 16:9 */
|
||||
}
|
||||
|
||||
.aspect-w-16 img {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.prose {
|
||||
color: #374151;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
font-size: 1.875rem;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-top: 1.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.prose a {
|
||||
color: #2563eb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.prose a:hover {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
font-style: italic;
|
||||
border-left: 4px solid #e5e7eb;
|
||||
padding-left: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.prose ul, .prose ol {
|
||||
margin: 1.25rem 0;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.prose li {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.prose img {
|
||||
margin: 2rem 0;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.prose pre {
|
||||
background-color: #f3f4f6;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
background-color: #f3f4f6;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.125rem 0.25rem;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
</style>
|
||||
@endPushOnce
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
<nav class="flux-cms-navigation" data-navigation="{{ $navigation->name }}">
|
||||
@if($navigationItems->isNotEmpty())
|
||||
<ul class="navigation-items {{ $cssClasses }}">
|
||||
@foreach($navigationItems as $item)
|
||||
<li class="navigation-item {{ $item->hasChildren() ? 'has-children' : '' }} {{ $this->isActive($item) ? 'active' : '' }}"
|
||||
data-item-id="{{ $item->id }}">
|
||||
|
||||
{{-- Item Link --}}
|
||||
@if($item->page_id || $item->url)
|
||||
<a href="{{ $item->getEffectiveUrl() }}"
|
||||
@if($item->target) target="{{ $item->target }}" @endif
|
||||
class="navigation-link {{ $this->isActive($item) ? 'active' : '' }}">
|
||||
{{ $item->getTranslation('title', app()->getLocale()) }}
|
||||
</a>
|
||||
@else
|
||||
<span class="navigation-text">
|
||||
{{ $item->getTranslation('title', app()->getLocale()) }}
|
||||
</span>
|
||||
@endif
|
||||
|
||||
{{-- Sub-navigation --}}
|
||||
@if($item->hasChildren() && $showSubmenus)
|
||||
<ul class="sub-navigation">
|
||||
@foreach($item->children as $child)
|
||||
@if($child->is_active)
|
||||
<li class="sub-navigation-item {{ $this->isActive($child) ? 'active' : '' }}"
|
||||
data-item-id="{{ $child->id }}">
|
||||
|
||||
@if($child->page_id || $child->url)
|
||||
<a href="{{ $child->getEffectiveUrl() }}"
|
||||
@if($child->target) target="{{ $child->target }}" @endif
|
||||
class="sub-navigation-link {{ $this->isActive($child) ? 'active' : '' }}">
|
||||
{{ $child->getTranslation('title', app()->getLocale()) }}
|
||||
</a>
|
||||
@else
|
||||
<span class="sub-navigation-text">
|
||||
{{ $child->getTranslation('title', app()->getLocale()) }}
|
||||
</span>
|
||||
@endif
|
||||
|
||||
{{-- Third level navigation if needed --}}
|
||||
@if($child->hasChildren() && $maxDepth > 2)
|
||||
<ul class="sub-sub-navigation">
|
||||
@foreach($child->children as $grandchild)
|
||||
@if($grandchild->is_active)
|
||||
<li class="sub-sub-navigation-item {{ $this->isActive($grandchild) ? 'active' : '' }}"
|
||||
data-item-id="{{ $grandchild->id }}">
|
||||
|
||||
@if($grandchild->page_id || $grandchild->url)
|
||||
<a href="{{ $grandchild->getEffectiveUrl() }}"
|
||||
@if($grandchild->target) target="{{ $grandchild->target }}" @endif
|
||||
class="sub-sub-navigation-link {{ $this->isActive($grandchild) ? 'active' : '' }}">
|
||||
{{ $grandchild->getTranslation('title', app()->getLocale()) }}
|
||||
</a>
|
||||
@else
|
||||
<span class="sub-sub-navigation-text">
|
||||
{{ $grandchild->getTranslation('title', app()->getLocale()) }}
|
||||
</span>
|
||||
@endif
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@else
|
||||
{{-- Empty navigation state --}}
|
||||
@if(config('app.debug'))
|
||||
<div class="navigation-empty text-gray-500 text-sm">
|
||||
Navigation "{{ $navigation->name }}" has no active items.
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</nav>
|
||||
|
||||
{{-- Default Styles (can be overridden in your theme) --}}
|
||||
@pushOnce('styles')
|
||||
<style>
|
||||
.flux-cms-navigation .navigation-items {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.flux-cms-navigation .navigation-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flux-cms-navigation .navigation-link,
|
||||
.flux-cms-navigation .navigation-text {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.flux-cms-navigation .navigation-link:hover {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.flux-cms-navigation .navigation-link.active {
|
||||
font-weight: 600;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.flux-cms-navigation .sub-navigation,
|
||||
.flux-cms-navigation .sub-sub-navigation {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.flux-cms-navigation .has-children > .navigation-link::after {
|
||||
content: '▼';
|
||||
font-size: 0.75em;
|
||||
margin-left: 0.5rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Mobile-first responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.flux-cms-navigation .sub-navigation {
|
||||
margin-left: 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endPushOnce
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<div class="flux-cms-page" data-page-id="{{ $page->id }}">
|
||||
{{-- SEO Meta Tags (if not handled by layout) --}}
|
||||
@pushOnce('meta')
|
||||
<title>{{ $page->getSeoTitle() }}</title>
|
||||
<meta name="description" content="{{ $page->getSeoDescription() }}">
|
||||
|
||||
@if($page->meta_keywords)
|
||||
<meta name="keywords" content="{{ $page->meta_keywords }}">
|
||||
@endif
|
||||
|
||||
{{-- Open Graph --}}
|
||||
<meta property="og:title" content="{{ $page->getSeoTitle() }}">
|
||||
<meta property="og:description" content="{{ $page->getSeoDescription() }}">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="{{ $page->getUrl() }}">
|
||||
|
||||
{{-- Twitter Card --}}
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ $page->getSeoTitle() }}">
|
||||
<meta name="twitter:description" content="{{ $page->getSeoDescription() }}">
|
||||
@endPushOnce
|
||||
|
||||
{{-- Page Content --}}
|
||||
<div class="space-y-8">
|
||||
@forelse($components as $component)
|
||||
@if($component->is_active)
|
||||
<div class="flux-cms-component"
|
||||
data-component-id="{{ $component->id }}"
|
||||
data-component-type="{{ $component->component_type }}"
|
||||
@if($component->settings['css_classes'] ?? false)
|
||||
class="{{ $component->settings['css_classes'] }}"
|
||||
@endif>
|
||||
|
||||
{{-- Render the actual Livewire component --}}
|
||||
@livewire($component->component_type, [
|
||||
'content' => $component->content,
|
||||
'settings' => $component->settings,
|
||||
'componentId' => $component->id
|
||||
], key('component-'.$component->id))
|
||||
</div>
|
||||
@endif
|
||||
@empty
|
||||
{{-- Empty state for pages with no components --}}
|
||||
<div class="text-center py-16">
|
||||
<div class="max-w-md mx-auto">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-4">
|
||||
{{ $page->title }}
|
||||
</h1>
|
||||
@if($page->content)
|
||||
<div class="prose prose-lg text-gray-600">
|
||||
{!! $page->content !!}
|
||||
</div>
|
||||
@else
|
||||
<p class="text-gray-600">
|
||||
This page doesn't have any content components yet.
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{-- JSON-LD Structured Data --}}
|
||||
@pushOnce('structured-data')
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "{{ $page->getSeoTitle() }}",
|
||||
"description": "{{ $page->getSeoDescription() }}",
|
||||
"url": "{{ $page->getUrl() }}",
|
||||
"dateModified": "{{ $page->updated_at->toISOString() }}",
|
||||
"inLanguage": "{{ app()->getLocale() }}"
|
||||
}
|
||||
</script>
|
||||
@endPushOnce
|
||||
</div>
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<div class="flex items-center justify-between p-3 border border-gray-200 rounded-lg bg-white {{ $level > 0 ? 'ml-6' : '' }}"
|
||||
style="margin-left: {{ $level * 1.5 }}rem;">
|
||||
|
||||
<div class="flex items-center space-x-3 flex-1">
|
||||
{{-- Drag Handle --}}
|
||||
<div wire:sortable.handle class="cursor-move text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{{-- Item Content --}}
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="font-medium text-gray-900">
|
||||
{{ $item->getTranslation('title', app()->getLocale()) }}
|
||||
</span>
|
||||
|
||||
{{-- Status Badges --}}
|
||||
@if(!$item->is_active)
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
Inactive
|
||||
</span>
|
||||
@endif
|
||||
|
||||
@if($item->hasChildren())
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
{{ $item->children->count() }} children
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-500 mt-1">
|
||||
@if($item->page_id && $item->page)
|
||||
<span class="inline-flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
Page: {{ $item->page->title }}
|
||||
</span>
|
||||
@elseif($item->url)
|
||||
<span class="inline-flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.102m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
|
||||
</svg>
|
||||
URL: {{ $item->url }}
|
||||
@if($item->target === '_blank')
|
||||
<svg class="w-3 h-3 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
||||
</svg>
|
||||
@endif
|
||||
</span>
|
||||
@else
|
||||
<span class="text-gray-400">No link</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Actions --}}
|
||||
<div class="flex items-center space-x-2">
|
||||
<button wire:click="editNavigationItem({{ $item->id }})"
|
||||
class="text-gray-400 hover:text-blue-600 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button wire:click="addChildItem({{ $item->id }})"
|
||||
class="text-gray-400 hover:text-green-600 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button wire:click="deleteNavigationItem({{ $item->id }})"
|
||||
class="text-gray-400 hover:text-red-600 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Render children recursively --}}
|
||||
@if($item->children->isNotEmpty() && $level < 3)
|
||||
<div class="mt-2 space-y-2">
|
||||
@foreach($item->children as $child)
|
||||
@include('flux-cms-components::partials.navigation-item', ['item' => $child, 'level' => $level + 1])
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue