Initial commit
This commit is contained in:
27
resources/views/livewire/categories/color-picker.blade.php
Normal file
27
resources/views/livewire/categories/color-picker.blade.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<div x-data="{ previewName: @entangle('previewName') }"
|
||||
x-on:updatePreviewName.window="previewName = $event.detail || '{{ __('Category name') }}'">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ $label }}
|
||||
</label>
|
||||
|
||||
<div class="relative">
|
||||
<select
|
||||
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 pr-10 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
wire:model.live="selectedColor">
|
||||
@foreach ($this->availableColors as $color)
|
||||
<option value="{{ $color['value'] }}">{{ $color['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<!-- Color swatch preview -->
|
||||
<div class="pointer-events-none absolute inset-y-0 right-10 flex items-center pr-2">
|
||||
<span class="inline-block h-6 w-6 rounded border border-gray-300 bg-{{ $selectedColor }}-400"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Large color preview -->
|
||||
<div class="mt-2">
|
||||
<div class="text-sm text-gray-600 mb-1">{{ __('Preview') }}:</div>
|
||||
<span class="bg-{{ $selectedColor }}-400 inline-flex items-center rounded-md px-3 py-1.5 text-sm font-normal text-black" x-text="previewName"></span>
|
||||
</div>
|
||||
</div>
|
||||
3
resources/views/livewire/categories/create.blade.php
Normal file
3
resources/views/livewire/categories/create.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- Care about people's approval and you will be their prisoner. --}}
|
||||
</div>
|
||||
796
resources/views/livewire/categories/manage.blade.php
Normal file
796
resources/views/livewire/categories/manage.blade.php
Normal file
@@ -0,0 +1,796 @@
|
||||
<div class="mt-12">
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<x-jetstream.button wire:click="openCreateCategoryModal" class="bg-theme-brand hover:bg-opacity-80">
|
||||
{{ __('New Category') }}
|
||||
</x-jetstream.button>
|
||||
</div>
|
||||
|
||||
<!-- Search box -->
|
||||
<div class="mb-4 flex items-center">
|
||||
<!-- Input and Reset Button Container -->
|
||||
<div class="relative w-2/3">
|
||||
<input class="w-full rounded-md border border-theme-primary px-3 py-1 pr-10 text-theme-primary shadow-sm focus:border-theme-accent focus:outline-none focus:ring-1 focus:ring-theme-accent sm:text-sm"
|
||||
placeholder="{{ __('Search keywords') . '...' }}" type="text"
|
||||
wire:keydown.enter="handleSearchEnter" wire:model="search">
|
||||
|
||||
<!-- Reset Button -->
|
||||
@if ($search)
|
||||
<button class="absolute inset-y-0 right-0 flex items-center pr-3 text-theme-secondary hover:text-theme-primary focus:outline-none"
|
||||
wire:click.prevent="resetSearch">
|
||||
<x-icon mini name="backspace" solid />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Search Button -->
|
||||
<x-jetstream.secondary-button class="ml-3 w-32 justify-center" wire:click.prevent="searchCategories">
|
||||
{{ __('Search') }}
|
||||
</x-jetstream.secondary-button>
|
||||
</div>
|
||||
|
||||
<!-- Filter dropdowns -->
|
||||
<div class="mb-4 flex flex-wrap items-center gap-3">
|
||||
<!-- Parent Filter Dropdown -->
|
||||
<div>
|
||||
<x-select :clearable="true" :searchable="true" class="!w-40" style="width: 24rem !important; min-width: 24rem !important;"
|
||||
placeholder="{{ __('Parent') }}" wire:model.live="parentFilter">
|
||||
@foreach ($this->parents as $parent)
|
||||
<x-select.option label="{{ $parent['name'] }}" value="{{ $parent['id'] }}" />
|
||||
@endforeach
|
||||
</x-select>
|
||||
</div>
|
||||
|
||||
<!-- Language Filter Dropdown -->
|
||||
<div>
|
||||
<x-select :clearable="true" :searchable="true" class="!w-40" style="width: 24rem !important; min-width: 24rem !important;"
|
||||
placeholder="{{ __('Language') }}" wire:model.live="languageFilter">
|
||||
@foreach ($this->languages as $lang)
|
||||
<x-select.option label="{{ $lang['name'] }}" value="{{ $lang['id'] }}" />
|
||||
@endforeach
|
||||
</x-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bulk action buttons -->
|
||||
<div class="mb-6 flex items-center justify-end space-x-4">
|
||||
<x-jetstream.danger-button
|
||||
:disabled="$this->bulkDisabled"
|
||||
title="{{ __('Delete') . ' ' . __('selection') }}"
|
||||
wire:click.prevent="openBulkDeleteTranslationsModal()">
|
||||
<x-icon class="mr-3 h-5 w-5" name="trash" />
|
||||
{{ __('Delete') }} {{ __('selection') }}
|
||||
</x-jetstream.danger-button>
|
||||
|
||||
@if (!$this->bulkDisabled)
|
||||
<x-jetstream.secondary-button wire:click="resetBulkSelection">
|
||||
{{ __('Clear Selection') }}
|
||||
</x-jetstream.secondary-button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="bg-white shadow-sm rounded-lg" x-data="{
|
||||
initScrollSync() {
|
||||
if (this.$refs.topScroll && this.$refs.bottomScroll && this.$refs.table && this.$refs.topScrollContent) {
|
||||
const topScroll = this.$refs.topScroll;
|
||||
const bottomScroll = this.$refs.bottomScroll;
|
||||
const table = this.$refs.table;
|
||||
|
||||
// Set the width of the dummy div to match table width
|
||||
this.$refs.topScrollContent.style.width = table.scrollWidth + 'px';
|
||||
}
|
||||
}
|
||||
}" x-init="setTimeout(() => initScrollSync(), 100)">
|
||||
|
||||
<!-- Top scrollbar -->
|
||||
<div class="overflow-x-auto border-b border-gray-200" x-ref="topScroll" @scroll="$refs.bottomScroll.scrollLeft = $event.target.scrollLeft">
|
||||
<div x-ref="topScrollContent" style="height: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Table with bottom scrollbar -->
|
||||
<div class="overflow-x-auto" x-ref="bottomScroll" @scroll="$refs.topScroll.scrollLeft = $event.target.scrollLeft">
|
||||
<table class="min-w-full divide-y divide-gray-200" x-ref="table">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
{{-- Checkbox column --}}
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
|
||||
|
||||
{{-- Sortable Id column --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('id')">
|
||||
<div class="flex items-center">
|
||||
{{ __('Id') }}
|
||||
@if ($sortField === 'id')
|
||||
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
|
||||
{{-- Sortable Name column --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('name')">
|
||||
<div class="flex items-center">
|
||||
{{ __('Name') }}
|
||||
@if ($sortField === 'name')
|
||||
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
|
||||
{{-- Sortable Language column --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('locale')">
|
||||
<div class="flex items-center">
|
||||
{{ __('Lang.') }}
|
||||
@if ($sortField === 'locale')
|
||||
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
|
||||
{{-- Sortable Tags column --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('tags_count')">
|
||||
<div class="flex items-center">
|
||||
{{ __('Tags') }}
|
||||
@if ($sortField === 'tags_count')
|
||||
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
|
||||
{{-- Sortable Parent column --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('parent')">
|
||||
<div class="flex items-center">
|
||||
{{ __('Parent') }}
|
||||
@if ($sortField === 'parent')
|
||||
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
|
||||
{{-- Sortable Updated column --}}
|
||||
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
wire:click="sortBy('updated_at')">
|
||||
<div class="flex items-center">
|
||||
{{ __('Updated') }}
|
||||
@if ($sortField === 'updated_at')
|
||||
<x-icon :name="$sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'" class="ml-1 h-4 w-4" micro />
|
||||
@endif
|
||||
</div>
|
||||
</th>
|
||||
|
||||
{{-- Non-sortable Action column --}}
|
||||
<th class="px-3 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider w-32">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="bg-white">
|
||||
@forelse ($categories as $category)
|
||||
@php
|
||||
// When sorting by translation fields, we have joined data instead of eager loaded translations
|
||||
$sortingByTranslation = in_array($sortField, ['name', 'locale', 'updated_at']);
|
||||
|
||||
if ($sortingByTranslation && isset($category->joined_translation_id)) {
|
||||
// Use joined translation data
|
||||
$translations = [
|
||||
(object)[
|
||||
'id' => $category->joined_translation_id,
|
||||
'locale' => $category->joined_locale,
|
||||
'name' => $category->joined_name,
|
||||
'updated_at' => $category->joined_updated_at ? \Carbon\Carbon::parse($category->joined_updated_at) : null,
|
||||
]
|
||||
];
|
||||
} else {
|
||||
// Use regular eager loaded translations
|
||||
$translations = $category->translations;
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if (count($translations) === 0)
|
||||
{{-- Skip categories without translations --}}
|
||||
@else
|
||||
@foreach ($translations as $loop_index => $translation)
|
||||
<tr class="@if($loop_index === 0) border-t border-gray-200 @endif">
|
||||
<td class="px-3 py-4 whitespace-nowrap">
|
||||
<input
|
||||
type="checkbox"
|
||||
value="{{ $category->id }}"
|
||||
wire:model.live="bulkSelected"
|
||||
class="rounded border-gray-300 text-theme-brand focus:border-theme-accent focus:ring-1 focus:ring-theme-accent">
|
||||
</td>
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $category->id }}
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm">
|
||||
@php
|
||||
// Use parent's color if it has a parent, otherwise use category's own color
|
||||
$nameColor = $category->parent_id && $category->parent
|
||||
? ($category->parent->color ?? 'gray')
|
||||
: ($category->color ?? 'gray');
|
||||
@endphp
|
||||
<span class="bg-{{ $nameColor }}-400 inline-flex items-center rounded-md px-2 py-1 text-sm font-normal text-black">
|
||||
{{ $translation->name }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $translation->locale }}
|
||||
</td>
|
||||
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $category->tags_count }}
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm">
|
||||
@if ($category->parent && $category->parent->translation)
|
||||
@php
|
||||
$parentColor = $category->parent->color ?? 'gray';
|
||||
@endphp
|
||||
<span class="bg-{{ $parentColor }}-400 inline-flex items-center rounded-md px-2 py-1 text-sm font-normal text-black">
|
||||
{{ $category->parent->translation->name }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-3 py-4">
|
||||
@if ($translation->updated_at)
|
||||
<div class="text-sm text-gray-500 leading-tight">
|
||||
<div>{{ $translation->updated_at->translatedFormat('M j') }}</div>
|
||||
<div>{{ $translation->updated_at->translatedFormat('Y') }}</div>
|
||||
<div class="text-xs">{{ $translation->updated_at->translatedFormat('H:i') }}</div>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-gray-400">-</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- Action Buttons --}}
|
||||
<td class="px-3 py-4 whitespace-nowrap text-center text-sm font-medium">
|
||||
<div class="flex items-center justify-center space-x-1">
|
||||
<x-jetstream.secondary-button
|
||||
title="{{ __('Edit') }}"
|
||||
wire:click="openEditCategoryModal({{ $translation->id }})">
|
||||
<x-icon class="h-5 w-5" name="pencil-square" />
|
||||
</x-jetstream.secondary-button>
|
||||
|
||||
<x-jetstream.danger-button
|
||||
title="{{ __('Delete') }}"
|
||||
wire:click="openDeleteCategoryModal({{ $translation->id }})">
|
||||
<x-icon class="h-5 w-5" name="trash" />
|
||||
</x-jetstream.danger-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@endif
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="px-6 py-12 text-center text-gray-500">
|
||||
{{ __('No results found') }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if($categories->hasPages())
|
||||
<div class="bg-white shadow-sm rounded-lg mt-4">
|
||||
<div class="py-3 border-t border-gray-200 flex items-center justify-between">
|
||||
<!-- Left Side: perPage Dropdown -->
|
||||
<div class="flex items-center">
|
||||
<select class="w-20 rounded-md border border-theme-primary bg-theme-background px-3 py-2 text-theme-primary shadow-sm focus:border-theme-accent focus:outline-none focus:ring-1 focus:ring-theme-accent sm:text-sm"
|
||||
wire:model.live="perPage">
|
||||
<option value="10"> 10 </option>
|
||||
<option value="50"> 50 </option>
|
||||
<option value="100"> 100 </option>
|
||||
</select>
|
||||
<span class="ml-2 text-sm text-theme-secondary">{{ __('per page') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Right Side: Paginator -->
|
||||
{{ $categories->links('livewire.long-paginator') }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Delete single translation modal -->
|
||||
@if ($modalDeleteTranslation)
|
||||
<x-jetstream.dialog-modal wire:key="modalDeleteTranslation" wire:model.live="modalDeleteTranslation" maxWidth="2xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Are you sure?') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ __('Do you want to permanently delete this category and all its translations?') }}
|
||||
|
||||
<table class="mt-6 table min-w-full border-t-white">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Id') }}</th>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Category ID') }}</th>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Language') }}</th>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Name') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@foreach ($selectedTranslations as $translation)
|
||||
<tr class="border-theme-background" wire:key="delete-translation-{{ $translation->id }}">
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->id }}
|
||||
</td>
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->category_id }}
|
||||
</td>
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->locale }}
|
||||
</td>
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->name }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if ($affectedTagsCount > 0)
|
||||
<div class="mt-6 rounded-md border border-gray-200 p-4">
|
||||
<div class="flex">
|
||||
<div class="w-1/3 font-medium">{{ __('Affected Tags') }}:</div>
|
||||
<div class="flex-1 text-red-600 font-semibold">{{ $affectedTagsCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($needsCategoryReassignment)
|
||||
<div class="mt-6 rounded-md border border-gray-300 bg-gray-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<x-icon class="h-5 w-5 text-gray-400" name="exclamation-triangle" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-800">
|
||||
<strong>{{ __('Important') }}:</strong>
|
||||
{{ __('This category has') }} {{ $affectedTagsCount }} {{ __('associated tags') }}.
|
||||
{{ __('Please select a category to reassign these tags to') }}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<x-select
|
||||
:clearable="false"
|
||||
:searchable="true"
|
||||
label="{{ __('Reassign tags to category') }}"
|
||||
placeholder="{{ __('Select a category') }}"
|
||||
:options="$availableCategories"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
wire:model="targetCategoryId" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-6 rounded-md border-l-4 border-red-400 bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<x-icon class="h-5 w-5 text-red-400" name="exclamation-triangle" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-red-700">
|
||||
<strong>{{ __('Warning') }}:</strong>
|
||||
{{ __('This can not be undone!') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 w-1/3">
|
||||
<x-input label="{!! __('messages.confirm_input') !!}" placeholder="{{ __('Confirmation keyword') }}"
|
||||
autocomplete="off"
|
||||
wire:model="confirmString" />
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalDeleteTranslation', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.danger-button class="ml-3" wire:click.prevent="deleteCategory"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Delete') }}
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
<!-- Bulk delete modal -->
|
||||
@if ($modalBulkDeleteTranslations)
|
||||
<x-jetstream.dialog-modal wire:key="modalBulkDeleteTranslations" wire:model.live="modalBulkDeleteTranslations" maxWidth="2xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Are you sure?') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
{{ __('Do you want to permanently delete these translations?') }}
|
||||
|
||||
<table class="mt-6 table min-w-full border-t-white">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Id') }}</th>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Category ID') }}</th>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Language') }}</th>
|
||||
<th class="px-6 py-3 text-left text-sm leading-4 tracking-wider">{{ __('Name') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@foreach ($selectedTranslations as $translation)
|
||||
<tr class="border-theme-background" wire:key="bulk-delete-translation-{{ $translation->id }}">
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->id }}
|
||||
</td>
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->category_id }}
|
||||
</td>
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->locale }}
|
||||
</td>
|
||||
<td class="whitespace-no-wrap border-theme-background px-6 text-sm leading-5">
|
||||
{{ $translation->name }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if ($affectedTagsCount > 0)
|
||||
<div class="mt-6 rounded-md border border-gray-200 p-4">
|
||||
<div class="flex">
|
||||
<div class="w-1/3 font-medium">{{ __('Total Affected Tags') }}:</div>
|
||||
<div class="flex-1 text-red-600 font-semibold">{{ $affectedTagsCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (count($categoriesWithTags) > 0)
|
||||
<div class="mt-6 rounded-md border border-gray-300 bg-gray-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<x-icon class="h-5 w-5 text-gray-400" name="exclamation-triangle" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-800">
|
||||
<strong>{{ __('Important') }}:</strong>
|
||||
{{ __('The following categories have associated tags. Please select a target category for each to reassign their tags') }}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
@foreach ($categoriesWithTags as $categoryData)
|
||||
<div class="rounded-md border border-gray-300 p-4">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div class="font-medium text-gray-700">
|
||||
{{ $categoryData['name'] }}
|
||||
</div>
|
||||
<div class="text-sm text-red-600">
|
||||
{{ $categoryData['tags_count'] }} {{ __('tags') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-select
|
||||
:clearable="false"
|
||||
:searchable="true"
|
||||
label="{{ __('Reassign tags to category') }}"
|
||||
placeholder="{{ __('Select a category') }}"
|
||||
:options="$availableCategories"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
wire:model="categoryReassignments.{{ $categoryData['category_id'] }}" />
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-6 rounded-md border-l-4 border-red-400 bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<x-icon class="h-5 w-5 text-red-400" name="exclamation-triangle" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-red-700">
|
||||
<strong>{{ __('Warning') }}:</strong>
|
||||
{{ __('This can not be undone!') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 w-1/3">
|
||||
<x-input label="{!! __('messages.confirm_input') !!}" placeholder="{{ __('Confirmation keyword') }}"
|
||||
autocomplete="off"
|
||||
wire:model="confirmString" />
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalBulkDeleteTranslations', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.danger-button class="ml-3" wire:click.prevent="deleteSelected"
|
||||
wire:loading.attr="disabled">
|
||||
{{ __('Delete') }}
|
||||
</x-jetstream.danger-button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
<!-- Edit category modal -->
|
||||
@if ($modalEditCategory)
|
||||
<x-jetstream.dialog-modal wire:key="modalEditCategory" wire:model.live="modalEditCategory" maxWidth="2xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Edit Category') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="space-y-4">
|
||||
<!-- Translation info -->
|
||||
<div class="rounded-md border border-gray-200 bg-gray-50 p-4">
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="font-medium">{{ __('Category ID') }}:</span>
|
||||
<span class="ml-2">{{ $editCategoryId }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">{{ __('Language') }}:</span>
|
||||
<span class="ml-2">{{ $editLocale }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Name field (translation-specific) -->
|
||||
<div>
|
||||
<x-input
|
||||
label="{{ __('Name') }}"
|
||||
placeholder="{{ __('Category name') }}"
|
||||
wire:model="editName" />
|
||||
</div>
|
||||
|
||||
<!-- Parent field (category-level - affects all translations) -->
|
||||
<div>
|
||||
<x-select
|
||||
:clearable="true"
|
||||
:searchable="true"
|
||||
label="{{ __('Parent Category') }} ({{ __('affects all translations') }})"
|
||||
placeholder="{{ __('Select parent category') }}"
|
||||
:options="$this->parents"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
wire:model.live="editParentId" />
|
||||
</div>
|
||||
|
||||
<!-- Color field (category-level - affects all translations) -->
|
||||
@if (!$editParentId || $editParentId === 'none')
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ __('Color') }} ({{ __('affects all translations') }})
|
||||
</label>
|
||||
|
||||
<div class="relative">
|
||||
<select
|
||||
class="w-full rounded-md border border-gray-300 bg-white px-3 py-2 pr-10 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
wire:model.live="editColor">
|
||||
@foreach ($availableColors as $color)
|
||||
<option value="{{ $color['value'] }}">{{ $color['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<!-- Color swatch preview -->
|
||||
<div class="pointer-events-none absolute inset-y-0 right-10 flex items-center pr-2">
|
||||
<span class="inline-block h-6 w-6 rounded border border-gray-300 bg-{{ $editColor }}-400"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-md border border-gray-200 bg-theme-surface p-4">
|
||||
<p class="text-sm text-theme-primary">
|
||||
{{ __('Color will be inherited from parent category') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Large color preview (always visible) -->
|
||||
<div class="mt-2">
|
||||
<div class="text-sm text-gray-600 mb-1">{{ __('Preview') }}:</div>
|
||||
<span class="bg-{{ $this->editPreviewColor }}-400 inline-flex items-center rounded-md px-3 py-1.5 text-sm font-normal text-black">
|
||||
{{ $editName ?: __('Category name') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Affected tags warning -->
|
||||
@if ($editAffectedTagsCount > 0)
|
||||
<div class="rounded-md border border-red-200 bg-red-50 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
<x-icon class="h-5 w-5 text-red-500" name="information-circle" />
|
||||
<span class="font-medium text-red-700">{{ __('Affected Tags') }}:</span>
|
||||
</div>
|
||||
<div class="text-red-600 font-semibold">{{ $editAffectedTagsCount }}</div>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-red-600">
|
||||
{{ __('Changes to parent or color will affect how tags in this category are displayed') }}.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Warning box -->
|
||||
<div class="rounded-md border border-gray-300 bg-gray-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<x-icon class="h-5 w-5 text-gray-400" name="exclamation-triangle" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-gray-800">
|
||||
<strong>{{ __('Important') }}:</strong>
|
||||
{{ __('Name changes only affect this translation') }}.
|
||||
{{ __('Parent and color changes affect all translations of this category') }}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation input -->
|
||||
<div class="w-1/2">
|
||||
<x-input
|
||||
label="{!! __('messages.confirm_input') !!}"
|
||||
placeholder="{{ __('Confirmation keyword') }}"
|
||||
autocomplete="off"
|
||||
wire:model="confirmString" />
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalEditCategory', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button class="ml-3" wire:click.prevent="updateCategory" wire:loading.attr="disabled">
|
||||
{{ __('Update') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
<!-- Create category modal -->
|
||||
@if ($modalCreateCategory)
|
||||
<x-jetstream.dialog-modal wire:key="modalCreateCategory" wire:model.live="modalCreateCategory" maxWidth="3xl">
|
||||
<x-slot name="title">
|
||||
{{ __('Create New Category') }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="space-y-6">
|
||||
<!-- Info box -->
|
||||
<div class="rounded-md border border-gray-200 bg-theme-surface p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<x-icon class="h-5 w-5 text-gray-500" name="information-circle" />
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-theme-primary">
|
||||
{{ __('Please provide category names in all supported languages') }}.
|
||||
{{ __('All fields are required') }}.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Translation fields -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-medium text-gray-700">{{ __('Category Names') }}</h3>
|
||||
|
||||
@php
|
||||
$appLocale = app()->getLocale();
|
||||
@endphp
|
||||
|
||||
@foreach ($this->supportedLocales as $index => $locale)
|
||||
@if($locale === $appLocale)
|
||||
<div x-data="{}"
|
||||
x-on:input.debounce.300ms="$dispatch('updatePreviewName', $event.target.value)"
|
||||
x-on:change="$dispatch('updatePreviewName', $event.target.value)">
|
||||
<x-input
|
||||
label="{{ __('messages.' . $locale) }}"
|
||||
placeholder="{{ __('Category name in') }} {{ __('messages.' . $locale) }}"
|
||||
wire:model.defer="createTranslations.{{ $locale }}" />
|
||||
</div>
|
||||
@else
|
||||
<div>
|
||||
<x-input
|
||||
label="{{ __('messages.' . $locale) }}"
|
||||
placeholder="{{ __('Category name in') }} {{ __('messages.' . $locale) }}"
|
||||
wire:model.defer="createTranslations.{{ $locale }}" />
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- Parent category selection -->
|
||||
<div>
|
||||
<x-select
|
||||
:clearable="true"
|
||||
:searchable="true"
|
||||
label="{{ __('Parent Category') }} ({{ __('optional') }})"
|
||||
placeholder="{{ __('Select parent category') }}"
|
||||
:options="$this->parents"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
wire:model.live="createParentId" />
|
||||
</div>
|
||||
|
||||
<!-- Color picker - only shown if no parent selected -->
|
||||
@if (!$createParentId || $createParentId === 'none')
|
||||
<div>
|
||||
@livewire('categories.color-picker', [
|
||||
'color' => $createColor,
|
||||
'label' => __('Color'),
|
||||
'required' => true
|
||||
])
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-md border border-gray-200 bg-theme-surface p-4">
|
||||
<p class="text-sm text-theme-primary">
|
||||
{{ __('Color will be inherited from parent category') }}
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Large color preview (always visible) -->
|
||||
<div class="mt-2">
|
||||
<div class="text-sm text-gray-600 mb-1">{{ __('Preview') }}:</div>
|
||||
@php $appLocale = app()->getLocale(); @endphp
|
||||
<span class="bg-{{ $this->createPreviewColor }}-400 inline-flex items-center rounded-md px-3 py-1.5 text-sm font-normal text-black">
|
||||
{{ $createTranslations[$appLocale] ?? __('Category name') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation input -->
|
||||
<div>
|
||||
<x-input
|
||||
label="{!! __('messages.confirm_input') !!}"
|
||||
placeholder="{{ __('Confirmation keyword') }}"
|
||||
autocomplete="off"
|
||||
wire:model="confirmString" />
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="footer">
|
||||
<x-jetstream.secondary-button wire:click="$set('modalCreateCategory', false)" wire:loading.attr="disabled">
|
||||
{{ __('Cancel') }}
|
||||
</x-jetstream.secondary-button>
|
||||
<x-jetstream.button class="ml-3 bg-theme-brand" wire:click.prevent="storeCategory" wire:loading.attr="disabled">
|
||||
{{ __('Create Category') }}
|
||||
</x-jetstream.button>
|
||||
</x-slot>
|
||||
</x-jetstream.dialog-modal>
|
||||
@endif
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('scroll-to-top', event => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
</div>
|
||||
Reference in New Issue
Block a user