288 lines
12 KiB
Plaintext
288 lines
12 KiB
Plaintext
@using Commerce.Contracts.DTOs
|
|
@inject CategoryAPIConsumer CategoryService
|
|
|
|
<div class="relative w-full">
|
|
<button type="button"
|
|
class="select select-bordered w-full flex items-center justify-between h-auto py-3 bg-base-100 hover:bg-base-200 transition-colors"
|
|
@onclick="ToggleDropdown">
|
|
<span class="truncate">@(SelectedCategory?.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty) ??
|
|
Placeholder)</span>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-50 shrink-0" viewBox="0 0 20 20"
|
|
fill="currentColor">
|
|
<path fill-rule="evenodd"
|
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
|
clip-rule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
|
|
@if (_isOpen)
|
|
{
|
|
<!-- Backdrop to close dropdown -->
|
|
<div class="fixed inset-0 z-40 bg-transparent" @onclick="ToggleDropdown"></div>
|
|
|
|
<div
|
|
class="absolute z-50 mt-2 w-full lg:min-w-[400px] bg-base-100 shadow-2xl rounded-2xl border border-base-300 p-4 animate-in fade-in zoom-in duration-200 origin-top">
|
|
<!-- Tabs Navigation (Breadcrumbs style) -->
|
|
<div class="flex flex-wrap items-center gap-1 mb-4 bg-base-200/50 p-2 rounded-xl">
|
|
<button type="button"
|
|
class="btn btn-ghost btn-xs normal-case @(_activeLevel == 0 ? "text-primary font-bold bg-primary/10" : "text-base-content/60")"
|
|
@onclick="() => SetLevel(0)">
|
|
Root
|
|
</button>
|
|
|
|
@for (int i = 0; i < _path.Count; i++)
|
|
{
|
|
var index = i;
|
|
var step = _path[i];
|
|
<div class="flex items-center gap-1">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 text-base-content/30" viewBox="0 0 20 20"
|
|
fill="currentColor">
|
|
<path fill-rule="evenodd"
|
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
clip-rule="evenodd" />
|
|
</svg>
|
|
<button type="button"
|
|
class="btn btn-ghost btn-xs normal-case @(_activeLevel == index + 1 ? "text-primary font-bold bg-primary/10" : "text-base-content/60")"
|
|
@onclick="() => SetLevel(index + 1)">
|
|
@step.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Categories List -->
|
|
<div class="max-h-72 overflow-y-auto custom-scrollbar pr-1">
|
|
@if (_loading)
|
|
{
|
|
<div class="flex flex-col items-center justify-center p-8 gap-3">
|
|
<span class="loading loading-spinner loading-md text-primary"></span>
|
|
<span class="text-sm text-gray-500">Loading categories...</span>
|
|
</div>
|
|
}
|
|
else if (!string.IsNullOrEmpty(_error))
|
|
{
|
|
<div class="alert alert-error shadow-sm text-sm">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
|
|
viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span>@_error</span>
|
|
<button type="button" class="btn btn-xs btn-ghost" @onclick="LoadTree">Retry</button>
|
|
</div>
|
|
}
|
|
else if (_activeCategories == null || !_activeCategories.Any())
|
|
{
|
|
<div class="py-8 text-center text-gray-500 italic">No categories found in this level</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="grid grid-cols-1 gap-1">
|
|
@foreach (var cat in _activeCategories)
|
|
{
|
|
<div
|
|
class="group flex items-center justify-between p-2 rounded-xl hover:bg-base-200 transition-all border border-transparent @(cat.Id == Value ? "bg-primary/10 border-primary/20" : "")">
|
|
<div class="flex-1 flex items-center gap-3 cursor-pointer py-1" @onclick="() => SelectValue(cat)">
|
|
<div
|
|
class="w-1.5 h-6 rounded-full @(cat.Id == Value ? "bg-primary" : "bg-transparent") transition-colors">
|
|
</div>
|
|
<div class="flex flex-col">
|
|
<span
|
|
class="font-medium @(cat.Id == Value ? "text-primary font-bold" : "text-base-content")">@cat.Translations?.FirstOrDefault()?.Info?.Name
|
|
?? string.Empty)</span>
|
|
@if (cat.Id == Value)
|
|
{
|
|
<span class="text-[10px] text-primary uppercase font-bold tracking-wider">Selected</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@if (cat.ChildCategories?.Any() == true)
|
|
{
|
|
<button type="button"
|
|
class="btn btn-ghost btn-circle btn-sm hover:bg-primary/20 text-primary transition-colors"
|
|
@onclick="() => DrillDown(cat)" title="View subcategories">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd"
|
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
clip-rule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="mt-4 pt-4 border-t border-base-200 flex justify-between items-center text-xs text-gray-400">
|
|
<span>Total: @(_tree.Count) roots</span>
|
|
<div class="flex gap-2">
|
|
<button type="button" class="btn btn-ghost btn-xs text-error" @onclick="ClearSelection">Clear</button>
|
|
<button type="button" class="btn btn-primary btn-xs" @onclick="ToggleDropdown">Done</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<style>
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: #e5e7eb;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: #374151;
|
|
}
|
|
|
|
.scrollbar-hide::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.scrollbar-hide {
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
}
|
|
</style>
|
|
|
|
@code {
|
|
[Parameter] public Guid? Value { get; set; }
|
|
[Parameter] public EventCallback<Guid?> ValueChanged { get; set; }
|
|
[Parameter] public string Placeholder { get; set; } = "Select Category";
|
|
[Parameter] public List<Guid>? DisallowedIds { get; set; }
|
|
[Parameter] public bool ShowBundles { get; set; } = true;
|
|
|
|
private List<CategoryVM> _tree = new();
|
|
private List<CategoryVM> _path = new();
|
|
private int _activeLevel = 0;
|
|
private bool _isOpen = false;
|
|
private bool _loading = true;
|
|
private string? _error;
|
|
|
|
private List<CategoryVM> _activeCategories => GetCategoriesForLevel(_activeLevel);
|
|
|
|
private CategoryVM? SelectedCategory;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadTree();
|
|
}
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
await UpdateSelectedCategory();
|
|
}
|
|
|
|
private async Task LoadTree()
|
|
{
|
|
_loading = true;
|
|
_error = null;
|
|
try
|
|
{
|
|
_tree = await CategoryService.FetchCategoryTree();
|
|
await UpdateSelectedCategory();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_error = "Failed to load categories hierarchy.";
|
|
Console.WriteLine($"Error loading category tree: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
|
|
private async Task UpdateSelectedCategory()
|
|
{
|
|
if (Value.HasValue)
|
|
{
|
|
SelectedCategory = FindInCategoryTree(_tree, Value.Value);
|
|
}
|
|
else
|
|
{
|
|
SelectedCategory = null;
|
|
}
|
|
}
|
|
|
|
private CategoryVM? FindInCategoryTree(List<CategoryVM> categories, Guid id)
|
|
{
|
|
foreach (var cat in categories)
|
|
{
|
|
if (cat.Id == id) return cat;
|
|
if (cat.ChildCategories != null)
|
|
{
|
|
var found = FindInCategoryTree(cat.ChildCategories.ToList(), id);
|
|
if (found != null) return found;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private List<CategoryVM> GetCategoriesForLevel(int level)
|
|
{
|
|
var cats = level == 0 ? _tree : _path[level - 1].ChildCategories?.ToList();
|
|
|
|
if (cats != null)
|
|
{
|
|
if (DisallowedIds != null)
|
|
{
|
|
cats = cats.Where(x => !DisallowedIds.Contains(x.Id ?? Guid.Empty)).ToList();
|
|
}
|
|
|
|
if (!ShowBundles)
|
|
{
|
|
cats = cats.Where(x => !x.IsBundle).ToList();
|
|
}
|
|
}
|
|
|
|
return cats ?? new List<CategoryVM>();
|
|
}
|
|
|
|
private void ToggleDropdown() => _isOpen = !_isOpen;
|
|
|
|
private void SetLevel(int level)
|
|
{
|
|
_activeLevel = level;
|
|
if (level <= _path.Count)
|
|
{
|
|
_path = _path.Take(level).ToList();
|
|
}
|
|
}
|
|
|
|
private async Task SelectValue(CategoryVM category)
|
|
{
|
|
Value = category.Id;
|
|
await ValueChanged.InvokeAsync(Value);
|
|
SelectedCategory = category;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void DrillDown(CategoryVM category)
|
|
{
|
|
if (category.ChildCategories?.Any() == true)
|
|
{
|
|
_path.Add(category);
|
|
_activeLevel = _path.Count;
|
|
}
|
|
}
|
|
|
|
private async Task ClearSelection()
|
|
{
|
|
Value = null;
|
|
await ValueChanged.InvokeAsync(Value);
|
|
SelectedCategory = null;
|
|
_path.Clear();
|
|
_activeLevel = 0;
|
|
}
|
|
} |