Files
ECommerce.RCL/Components/Pages/Category/ManageCategory.razor
T

352 lines
17 KiB
Plaintext

@page "/manage-category/{id?}"
@layout AdminLayout
@using Generic.Media.Components
@using Commerce.Contracts.DTOs
@inject CategoryAPIConsumer categoryAPIConsumer
@inject ISnackbar Snackbar
<div class="max-w-7xl mx-auto mt-10 px-4 pb-32 min-h-screen">
<div class="flex items-center justify-between mb-8">
<h3 class="text-3xl font-extrabold text-base-content tracking-tight">
@(string.IsNullOrEmpty(Id) ? "Create Category" : "Edit Category")
</h3>
<!-- Global Language Switcher -->
<div class="flex gap-1 bg-base-200 p-1.5 rounded-2xl border border-base-300 shadow-sm">
@foreach (var lang in _languages)
{
<button type="button" @onclick="() => _activeLang = lang.Key"
class="btn btn-sm border-none transition-all duration-300 rounded-xl @(_activeLang == lang.Key ? "btn-primary shadow-md" : "btn-ghost opacity-60")">
@lang.Value
</button>
}
</div>
</div>
<EditForm EditContext="_editContext" OnSubmit="HandleSubmit">
<FluentValidationValidator />
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 items-start">
<!-- SIDEBAR: Configuration -->
<div class="lg:col-span-1 space-y-8">
<div class="card bg-base-100 shadow-xl border border-base-200 !overflow-visible">
<div class="p-6 space-y-6">
<div class="flex items-center gap-2 pb-2 border-b border-base-200">
<div class="p-2 bg-primary/10 rounded-lg text-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
</svg>
</div>
<h4 class="font-bold text-lg">Configuration</h4>
</div>
<!-- Parent Category -->
<div class="form-control">
<label class="label"><span class="label-text font-semibold">Parent Category</span></label>
<CategoryTreeSelect @bind-value="category.ParentCategoryId"
DisallowedIds="@(category.Id.HasValue ? new List<Guid> { category.Id.Value } : null)"
Placeholder="Root Category" />
<ValidationMessage For="@(() => category.ParentCategoryId)"
class="text-error text-xs mt-1" />
</div>
<!-- Bundle Toggle -->
<div class="form-control p-4 bg-base-200/50 rounded-2xl border border-base-300">
<label class="label cursor-pointer justify-between">
<span class="label-text font-bold">Bundle Category?</span>
<InputCheckbox @bind-value="category.IsBundle" class="toggle toggle-primary" />
</label>
<p class="text-[10px] text-base-content/50 mt-1 uppercase tracking-widest font-bold">Groups
multiple products</p>
</div>
</div>
</div>
</div>
<!-- MAIN: Localized Content -->
<div class="lg:col-span-2 space-y-8">
<div class="card bg-base-100 shadow-2xl border border-base-200 !overflow-visible">
<div class="p-8 space-y-10">
<!-- Category Name -->
<div class="form-control">
<div class="flex items-center justify-between mb-4">
<h4 class="text-xl font-black tracking-tight flex items-center gap-2">
Category Name
<div class="badge badge-primary badge-sm">@(_languages[_activeLang])</div>
</h4>
</div>
<MultiLangInput @bind="_names" ActiveCulture="@_activeLang" HideTabs="true"
Placeholder="Enter category name in the selected language..." />
<ValidationMessage For="@(() => _names)" class="text-error text-sm mt-1" />
</div>
<!-- Description -->
<div class="form-control">
<h4 class="text-xl font-black tracking-tight mb-4 flex items-center gap-2">
Description
<div class="badge badge-secondary badge-sm">@(_languages[_activeLang])</div>
</h4>
<MultiLangInput @bind="_descriptions" ActiveCulture="@_activeLang"
HideTabs="true" IsTextArea="true"
Placeholder="Summarize what this category contains..." />
<ValidationMessage For="@(() => _descriptions)" class="text-error text-sm mt-1" />
</div>
<!-- Localized Specifications -->
<div class="form-control pt-6 border-t border-base-200">
<div class="flex items-center justify-between mb-4">
<h4 class="text-xl font-black tracking-tight flex items-center gap-2">
Category Specifications
<div class="badge badge-accent badge-sm">@(_languages[_activeLang])</div>
</h4>
<button type="button" @onclick="AddTechnicalDetail"
class="btn btn-sm btn-primary rounded-xl gap-2 shadow-lg shadow-primary/20">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4" />
</svg>
Add Spec
</button>
</div>
<div class="space-y-4">
@if (!_technicalDetails.ContainsKey(_activeLang) ||
!_technicalDetails[_activeLang].Any())
{
<div
class="border-2 border-dashed border-base-200 rounded-3xl p-10 text-center opacity-30">
<p class="text-sm font-bold uppercase tracking-widest">No specifications for
@(_languages[_activeLang])</p>
</div>
}
else
{
@foreach (var item in _technicalDetails[_activeLang])
{
<div
class="grid grid-cols-1 md:grid-cols-2 gap-4 bg-base-200/30 p-4 rounded-2xl border border-base-300 relative group transition-all hover:bg-base-200/50">
<div class="form-control">
<label class="label p-0 mb-1">
<span
class="text-[9px] uppercase font-black text-primary/60">Attribute</span>
</label>
<InputText @bind-value="item.Key"
class="input input-bordered input-sm rounded-xl focus:input-primary"
Placeholder="e.g. Series" />
</div>
<div class="form-control">
<label class="label p-0 mb-1">
<span class="text-[9px] uppercase font-black text-accent/60">Value</span>
</label>
<div class="flex gap-2">
<div class="flex-1">
<InputText @bind-value="item.Value"
class="input input-bordered input-sm rounded-xl focus:input-primary w-full"
Placeholder="e.g. Pro" />
</div>
<button type="button" @onclick="() => RemoveTechnicalDetail(item)"
class="btn btn-ghost btn-circle btn-sm text-error self-end">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
</button>
</div>
</div>
</div>
}
}
</div>
</div>
</div>
</div>
<!-- Media Card -->
<div class="card bg-base-100 shadow-xl border border-base-200 !overflow-visible">
<div class="p-8">
<div class="flex items-center gap-2 mb-6">
<div class="p-2 bg-secondary/10 rounded-lg text-secondary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<h4 class="font-bold text-xl">Category Media</h4>
</div>
<Media @ref="mediaComponent" InitialUrls="@(category.Photos?.ToList() ?? new List<string>())"
SingleUploadMode="false" MaxItems="10" />
</div>
</div>
<!-- Action Bar -->
<div
class="flex items-center justify-end gap-4 p-8 bg-base-300/30 rounded-3xl border border-base-300 shadow-inner">
<button type="button" @onclick="ResetForm" class="btn btn-ghost px-8 rounded-2xl">Reset</button>
<button type="submit"
class="btn btn-primary btn-wide rounded-2xl shadow-xl shadow-primary/20 gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
</svg>
Save Category
</button>
</div>
</div>
</div>
</EditForm>
</div>
@code {
[Parameter]
public string? Id { get; set; }
CategoryVM category = new();
private EditContext? _editContext;
Media? mediaComponent;
Dictionary<string, string> _names = new();
Dictionary<string, string> _descriptions = new();
Dictionary<string, List<TechnicalDetailVM>> _technicalDetails = new();
private string _activeLang = "en";
private readonly Dictionary<string, string> _languages = new()
{
{ "en", "English" },
{ "ar", "العربية" },
{ "es", "Español" },
{ "de", "Deutsch" }
};
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrEmpty(Id))
{
category = await categoryAPIConsumer.GetCategory(Id);
}
// Map from Translations to local dictionaries
foreach(var t in category.Translations ?? new())
{
_names[t.Language] = t.Info.Name;
_descriptions[t.Language] = t.Info.Description;
_technicalDetails[t.Language] = t.Info.TechnicalDetails?.ToList() ?? new();
}
// Ensure supported cultures exist in dictionaries
foreach (var lang in _languages.Keys)
{
if (!_names.ContainsKey(lang)) _names[lang] = "";
if (!_descriptions.ContainsKey(lang)) _descriptions[lang] = "";
if (!_technicalDetails.ContainsKey(lang)) _technicalDetails[lang] = new();
}
category.Photos ??= Array.Empty<string>();
SyncTranslations();
_editContext = new EditContext(category);
}
void SyncTranslations()
{
category.Translations = _languages.Keys.Select(lang => new CategoryTranslationVM
{
Language = lang,
Info = new CategoryInfoVM
{
Name = _names.GetValueOrDefault(lang, ""),
Description = _descriptions.GetValueOrDefault(lang, ""),
TechnicalDetails = _technicalDetails.GetValueOrDefault(lang, new())
}
}).ToList();
}
async Task HandleSubmit()
{
SyncTranslations();
if (_editContext != null && _editContext.Validate())
{
await OnSubmit();
}
}
void AddTechnicalDetail()
{
if (!_technicalDetails.ContainsKey(_activeLang))
{
_technicalDetails[_activeLang] = new List<TechnicalDetailVM>();
}
_technicalDetails[_activeLang].Add(new TechnicalDetailVM());
}
void RemoveTechnicalDetail(TechnicalDetailVM item)
{
if (_technicalDetails.ContainsKey(_activeLang))
{
_technicalDetails[_activeLang].Remove(item);
}
}
async Task OnSubmit()
{
Console.WriteLine("OnSubmit started");
try
{
if (mediaComponent != null)
{
Console.WriteLine("Starting ProcessUploadsAsync");
var uploadResult = await mediaComponent.ProcessUploadsAsync();
Console.WriteLine("ProcessUploadsAsync finished successfully");
category.Photos = uploadResult.FinalUrls.ToArray();
}
SyncTranslations();
if (string.IsNullOrEmpty(Id))
{
Console.WriteLine("Creating category...");
await categoryAPIConsumer.CreateCategory(category);
ToastService.ShowToast("Category created successfully!", ToastLevel.Success);
ResetForm();
}
else
{
Console.WriteLine("Updating category...");
await categoryAPIConsumer.UpdateCategory(category);
ToastService.ShowToast("Category updated successfully!", ToastLevel.Success);
}
}
catch (Exception ex)
{
Console.WriteLine($"Caught exception in OnSubmit: {ex.Message}");
ToastService.ShowToast($"Error: {ex.Message}", ToastLevel.Error);
}
}
void ResetForm()
{
category = new();
_names.Clear();
_descriptions.Clear();
_technicalDetails.Clear();
foreach (var lang in _languages.Keys)
{
_names[lang] = "";
_descriptions[lang] = "";
_technicalDetails[lang] = new();
}
SyncTranslations();
_editContext = new EditContext(category);
if (mediaComponent != null)
{
mediaComponent.Clear();
}
}
}