221 lines
7.2 KiB
C#
221 lines
7.2 KiB
C#
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<DateTime?> calendarDays = new();
|
|
private string currentCalendarMonthYearText => calendarViewingMonth.ToString("MMMM yyyy");
|
|
|
|
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<string, string> 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
|
|
{
|
|
[Required(ErrorMessage = "Product name is required.")]
|
|
public string? Name { get; set; }
|
|
|
|
[Required(ErrorMessage = "Summary is required.")]
|
|
public string? Summary { get; set; }
|
|
|
|
[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; }
|
|
|
|
[Required(ErrorMessage = "Author metadata is required.")]
|
|
public string? Author { get; set; }
|
|
|
|
[Required(ErrorMessage = "Publication Date is required.")]
|
|
public DateTime PublishDate { get; set; } = DateTime.Today;
|
|
|
|
[Required(ErrorMessage = "Copyright Information field is required.")]
|
|
public string? CopyrightInfo { get; set; }
|
|
|
|
[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<string> Thumbnails { get; set; } = [];
|
|
} |