using LiteCharms.Features.S3.Abstractions; using static LiteCharms.Features.S3.Constants; namespace ShopAdmin.Components; public partial class CreateProduct([FromKeyedServices(BookshopBucketName)] IS3Service s3Service) { private bool isCalendarOpen = false; private DateTime calendarViewingMonth = DateTime.Today; private List calendarDays = new(); private readonly CancellationTokenSource cancellationTokenSource = new(); private CancellationToken cancellationToken; protected string? ActivePreviewUrl { get; set; } protected CreateProductModel ProductModel { get; set; } = new(); private const long MaxAllowedFileSize = 1024 * 1024 * 5; private readonly Func GetFileKeyFromUrl = url => url.Split('/').Last(); protected override void OnInitialized() { base.OnInitialized(); cancellationToken = cancellationTokenSource.Token; if (ProductModel.Thumbnails.Count == 0) ProductModel.Thumbnails = [.. Enumerable.Repeat(string.Empty, 5)]; } public Task HandleValidSubmit() => Task.CompletedTask; public bool HasAssetAt(int index) => (ProductModel?.Thumbnails) != null && index < ProductModel.Thumbnails.Count && !string.IsNullOrWhiteSpace(ProductModel.Thumbnails[index]); private async Task HandleMainImageUpload(InputFileChangeEventArgs e) { try { var file = e.File; if (file == null) return; using var stream = new MemoryStream(); await file.OpenReadStream(MaxAllowedFileSize).CopyToAsync(stream, cancellationToken); stream.Seek(0, SeekOrigin.Begin); var result = await s3Service.UploadFileAsync(file.Name, stream, MimeTypes.GetMimeType(file.Name), cancellationToken); if (result.IsSuccess) { ProductModel.ImageUrl = result.Value; StateHasChanged(); } } catch (Exception ex) { Console.WriteLine($"Main Image Upload Exception: {ex.Message}"); } } public void SetPreviewActive(string? url) { if (string.IsNullOrWhiteSpace(url)) return; ActivePreviewUrl = url; StateHasChanged(); } public void ClosePreviewDrawer() { ActivePreviewUrl = null; StateHasChanged(); } private async Task HandleThumbnailUpload(InputFileChangeEventArgs e, int index) { try { var file = e.File; if (file == null) return; using var stream = new MemoryStream(); await file.OpenReadStream(MaxAllowedFileSize, cancellationToken).CopyToAsync(stream, cancellationToken); stream.Seek(0, SeekOrigin.Begin); var result = await s3Service.UploadFileAsync(file.Name, stream, MimeTypes.GetMimeType(file.Name), cancellationToken); if (result.IsSuccess && index < ProductModel.Thumbnails.Count) { ProductModel.Thumbnails[index] = result.Value; StateHasChanged(); } } catch (Exception ex) { Console.WriteLine($"Thumbnail Slot {index} Upload Exception: {ex.Message}"); } } public async Task ClearMainImage() { if (string.IsNullOrEmpty(ProductModel.ImageUrl)) return; var targetUrl = ProductModel.ImageUrl; if (ActivePreviewUrl == targetUrl) ActivePreviewUrl = null; ProductModel.ImageUrl = null; StateHasChanged(); var result = await s3Service.DeleteFileAsync(GetFileKeyFromUrl(targetUrl)); if (!result.IsSuccess) Console.WriteLine($"[S3 Orphan Cleanup Failure]: {result.Errors[0].Message}"); } public async Task RemoveThumbnailAt(int index) { if (index < 0 || index >= ProductModel.Thumbnails.Count) return; var targetUrl = ProductModel.Thumbnails[index]; if (string.IsNullOrEmpty(targetUrl)) return; if (ActivePreviewUrl == targetUrl) ActivePreviewUrl = null; ProductModel.Thumbnails[index] = string.Empty; StateHasChanged(); var result = await s3Service.DeleteFileAsync(GetFileKeyFromUrl(targetUrl)); if (result.IsFailed) Console.WriteLine($"[S3 Thumbnail Cleanup Failure]: {result.Errors[0].Message}"); } private void ToggleCalendar() { if (!isCalendarOpen) { // Default viewport context to currently selected value or fallback to today calendarViewingMonth = ProductModel.PublishDate; RebuildCalendarMatrix(); } isCalendarOpen = !isCalendarOpen; } private void RebuildCalendarMatrix() { calendarDays.Clear(); var firstDayOfMonth = new DateTime(calendarViewingMonth.Year, calendarViewingMonth.Month, 1); var totalDaysInMonth = DateTime.DaysInMonth(calendarViewingMonth.Year, calendarViewingMonth.Month); // Offset leading days to align day positions correctly with day of week headers int leadingOffsets = (int)firstDayOfMonth.DayOfWeek; for (int i = 0; i < leadingOffsets; i++) { calendarDays.Add(null); } // Populate active dates for (int day = 1; day <= totalDaysInMonth; day++) { calendarDays.Add(new DateTime(calendarViewingMonth.Year, calendarViewingMonth.Month, day)); } } private void NavigateToPreviousMonth() { calendarViewingMonth = calendarViewingMonth.AddMonths(-1); RebuildCalendarMatrix(); } private void NavigateToNextMonth() { calendarViewingMonth = calendarViewingMonth.AddMonths(1); RebuildCalendarMatrix(); } private void SelectCalendarDate(DateTime date) { ProductModel.PublishDate = date; isCalendarOpen = false; // Collapse popup smoothly on successful selection StateHasChanged(); } } public class CreateProductModel { [MaxLength(128)] [Required(ErrorMessage = "Product name is required.")] public string? Name { get; set; } [MaxLength(512)] [Required(ErrorMessage = "Summary is required.")] public string? Summary { get; set; } [MaxLength(2048)] [Required(ErrorMessage = "Description is required.")] public string? Description { get; set; } [Range(0.01, double.MaxValue, ErrorMessage = "Price must be greater than zero.")] public decimal Price { get; set; } [MaxLength(128)] [Required(ErrorMessage = "Author metadata is required.")] public string? Author { get; set; } [Required(ErrorMessage = "Publication Date is required.")] public DateTime PublishDate { get; set; } = DateTime.Today; [MaxLength(255)] [Required(ErrorMessage = "Copyright Information field is required.")] public string? CopyrightInfo { get; set; } [MaxLength(128)] [Required(ErrorMessage = "ISBN code is required.")] [RegularExpression(@"^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$", ErrorMessage = "Please enter a valid ISBN-10 or ISBN-13 string.")] public string? Isbn { get; set; } [Required(ErrorMessage = "Primary image is required.")] public string? ImageUrl { get; set; } public List Thumbnails { get; set; } = []; }