diff --git a/apps/server/AliasVault.Client/Main/Components/Credentials/CredentialCard.razor b/apps/server/AliasVault.Client/Main/Components/Credentials/CredentialCard.razor index 9f709f1f5..639b5a1da 100644 --- a/apps/server/AliasVault.Client/Main/Components/Credentials/CredentialCard.razor +++ b/apps/server/AliasVault.Client/Main/Components/Credentials/CredentialCard.razor @@ -74,6 +74,6 @@ private void ShowDetails() { // Redirect to view page instead for now. - NavigationManager.NavigateTo($"/credentials/{Obj.Id}"); + NavigationManager.NavigateTo($"/items/{Obj.Id}"); } } diff --git a/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor b/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor index e507d0083..06466a4fa 100644 --- a/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor +++ b/apps/server/AliasVault.Client/Main/Components/Email/EmailPreview.razor @@ -44,7 +44,7 @@

@Localizer["DateLabel"] @Email.DateSystem

@if (!string.IsNullOrEmpty(CredentialName) && CredentialId != Guid.Empty) { -

@Localizer["CredentialLabel"] +

@Localizer["ItemLabel"] + + @* Option 2: Delete folder and contents - only show if folder has items *@ + @if (ItemCount > 0) + { + + } + + @* Cancel button *@ + + + + + +} + +@code { + [Inject] + private IStringLocalizerFactory LocalizerFactory { get; set; } = default!; + + private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Folders.DeleteFolderModal", "AliasVault.Client"); + + ///

+ /// Gets or sets whether the modal is open. + /// + [Parameter] + public bool IsOpen { get; set; } + + /// + /// Gets or sets the folder name to display. + /// + [Parameter] + public string FolderName { get; set; } = string.Empty; + + /// + /// Gets or sets the number of items in the folder. + /// + [Parameter] + public int ItemCount { get; set; } + + /// + /// Gets or sets the close callback. + /// + [Parameter] + public EventCallback OnClose { get; set; } + + /// + /// Gets or sets the callback for deleting folder only (keeping items). + /// + [Parameter] + public EventCallback OnDeleteFolderOnly { get; set; } + + /// + /// Gets or sets the callback for deleting folder and its contents. + /// + [Parameter] + public EventCallback OnDeleteFolderAndContents { get; set; } + + private bool IsDeleting { get; set; } + + private async Task HandleClose() + { + await OnClose.InvokeAsync(); + } + + private async Task HandleDeleteFolderOnly() + { + IsDeleting = true; + StateHasChanged(); + + try + { + await OnDeleteFolderOnly.InvokeAsync(); + await OnClose.InvokeAsync(); + } + finally + { + IsDeleting = false; + StateHasChanged(); + } + } + + private async Task HandleDeleteFolderAndContents() + { + IsDeleting = true; + StateHasChanged(); + + try + { + await OnDeleteFolderAndContents.InvokeAsync(); + await OnClose.InvokeAsync(); + } + finally + { + IsDeleting = false; + StateHasChanged(); + } + } +} diff --git a/apps/server/AliasVault.Client/Main/Components/Folders/FolderModal.razor b/apps/server/AliasVault.Client/Main/Components/Folders/FolderModal.razor new file mode 100644 index 000000000..8214f6002 --- /dev/null +++ b/apps/server/AliasVault.Client/Main/Components/Folders/FolderModal.razor @@ -0,0 +1,168 @@ +@using Microsoft.Extensions.Localization + +@* FolderModal component - modal for creating or editing a folder *@ +@if (IsOpen) +{ +
+
+ @* Background overlay *@ +
+ + @* Modal panel *@ +
+
+
+
+ + + +
+
+

+ @(Mode == "create" ? Localizer["CreateFolderTitle"] : Localizer["EditFolderTitle"]) +

+
+ + + @if (!string.IsNullOrEmpty(ErrorMessage)) + { +

@ErrorMessage

+ } +
+
+
+
+
+ + +
+
+
+
+} + +@code { + [Inject] + private IStringLocalizerFactory LocalizerFactory { get; set; } = default!; + + private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Folders.FolderModal", "AliasVault.Client"); + + /// + /// Gets or sets whether the modal is open. + /// + [Parameter] + public bool IsOpen { get; set; } + + /// + /// Gets or sets the mode (create or edit). + /// + [Parameter] + public string Mode { get; set; } = "create"; + + /// + /// Gets or sets the initial folder name (for edit mode). + /// + [Parameter] + public string InitialName { get; set; } = string.Empty; + + /// + /// Gets or sets the close callback. + /// + [Parameter] + public EventCallback OnClose { get; set; } + + /// + /// Gets or sets the save callback. + /// + [Parameter] + public EventCallback OnSave { get; set; } + + private string FolderName { get; set; } = string.Empty; + private string ErrorMessage { get; set; } = string.Empty; + private bool IsSaving { get; set; } + + /// + protected override void OnParametersSet() + { + if (IsOpen) + { + FolderName = InitialName; + ErrorMessage = string.Empty; + IsSaving = false; + } + } + + private async Task HandleSave() + { + var trimmedName = FolderName?.Trim() ?? string.Empty; + + if (string.IsNullOrEmpty(trimmedName)) + { + ErrorMessage = Localizer["FolderNameRequired"]; + return; + } + + IsSaving = true; + ErrorMessage = string.Empty; + StateHasChanged(); + + try + { + await OnSave.InvokeAsync(trimmedName); + await OnClose.InvokeAsync(); + } + catch (Exception ex) + { + ErrorMessage = ex.Message; + } + finally + { + IsSaving = false; + StateHasChanged(); + } + } + + private async Task HandleClose() + { + await OnClose.InvokeAsync(); + } + + private async Task HandleKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Enter") + { + await HandleSave(); + } + else if (e.Key == "Escape") + { + await HandleClose(); + } + } +} diff --git a/apps/server/AliasVault.Client/Main/Components/Folders/FolderPill.razor b/apps/server/AliasVault.Client/Main/Components/Folders/FolderPill.razor new file mode 100644 index 000000000..9696ab496 --- /dev/null +++ b/apps/server/AliasVault.Client/Main/Components/Folders/FolderPill.razor @@ -0,0 +1,24 @@ +@using AliasVault.Client.Main.Models + +@* FolderPill component - displays a folder as a compact clickable pill *@ + + +@code { + /// + /// Gets or sets the folder to display. + /// + [Parameter] + public required FolderWithCount Folder { get; set; } + + /// + /// Gets or sets the click callback. + /// + [Parameter] + public EventCallback OnClick { get; set; } +} diff --git a/apps/server/AliasVault.Client/Main/Components/Items/ItemCard.razor b/apps/server/AliasVault.Client/Main/Components/Items/ItemCard.razor new file mode 100644 index 000000000..c65aeaf25 --- /dev/null +++ b/apps/server/AliasVault.Client/Main/Components/Items/ItemCard.razor @@ -0,0 +1,121 @@ +@using AliasVault.Client.Main.Models +@inject NavigationManager NavigationManager + +@* ItemCard component - displays an item in a card format with appropriate icon and information *@ +
+
+
+ +
+
+
@GetServiceName()
+ @if (Obj.HasTotp) + { + + + + + } + @if (Obj.HasPasskey) + { + + + + } + @if (Obj.HasAttachment) + { + + + + } +
+
@GetDisplayText()
+ @if (ShowFolderPath && !string.IsNullOrEmpty(Obj.FolderName)) + { +
+ + + + @Obj.FolderName +
+ } +
+
+ +@code { + /// + /// Gets or sets the item list entry object to show. + /// + [Parameter] + public required ItemListEntry Obj { get; set; } + + /// + /// Gets or sets whether to show the folder path (used when searching). + /// + [Parameter] + public bool ShowFolderPath { get; set; } + + /// + /// Gets the display text for the item, showing username by default, + /// falling back to email. For credit cards, shows cardholder name or masked number. + /// + private string GetDisplayText() + { + if (Obj.ItemType == ItemTypes.CreditCard) + { + // For credit cards, show masked card number if available + if (!string.IsNullOrEmpty(Obj.CardNumber) && Obj.CardNumber.Length >= 4) + { + return "•••• " + Obj.CardNumber[^4..]; + } + + return string.Empty; + } + + if (Obj.ItemType == ItemTypes.Note) + { + // For notes, no secondary text needed + return string.Empty; + } + + // For Login/Alias, prioritize username then email + if (!string.IsNullOrEmpty(Obj.Username)) + { + return Obj.Username; + } + + if (!string.IsNullOrEmpty(Obj.Email)) + { + return Obj.Email; + } + + return string.Empty; + } + + /// + /// Get the service name (item name) for the item. + /// + private string GetServiceName() + { + if (!string.IsNullOrEmpty(Obj.Service)) + { + return Obj.Service; + } + + return "Untitled"; + } + + /// + /// Navigate to the details page of the item. + /// + private void ShowDetails() + { + NavigationManager.NavigateTo($"/items/{Obj.Id}"); + } +} diff --git a/apps/server/AliasVault.Client/Main/Components/Items/ItemIcon.razor b/apps/server/AliasVault.Client/Main/Components/Items/ItemIcon.razor new file mode 100644 index 000000000..6f0cca183 --- /dev/null +++ b/apps/server/AliasVault.Client/Main/Components/Items/ItemIcon.razor @@ -0,0 +1,148 @@ +@using AliasVault.Client.Main.Models +@using AliasVault.Client.Main.Utilities + +@* ItemIcon component - displays contextually appropriate icons based on item type *@ +@* For Login/Alias: Uses the Logo field if available, falls back to key placeholder *@ +@* For CreditCard: Shows card brand icons (Visa, MC, Amex, Discover) based on card number *@ +@* For Note: Shows a document/note icon *@ + +@if (ItemType == ItemTypes.Note) +{ + @* Note icon - document style *@ + + + + + + + +} +else if (ItemType == ItemTypes.CreditCard) +{ + @* Credit card icon - detect brand and show appropriate icon *@ + @switch (CardBrandDetector.Detect(CardNumber)) + { + case CardBrandDetector.CardBrand.Visa: + @* Visa card icon *@ + + + + + break; + case CardBrandDetector.CardBrand.Mastercard: + @* Mastercard icon *@ + + + + + + + break; + case CardBrandDetector.CardBrand.Amex: + @* Amex card icon *@ + + + AMEX + + break; + case CardBrandDetector.CardBrand.Discover: + @* Discover card icon *@ + + + + + + + + break; + default: + @* Generic credit card icon *@ + + + + + + + break; + } +} +else if (Logo != null && Logo.Length > 0) +{ + @* Login/Alias with logo *@ + @AltText +} +else +{ + @* Default placeholder - key icon for Login/Alias without logo *@ + + @* Key bow (circular head) - positioned top-left *@ + + @* Key hole in bow *@ + + @* Key shaft - diagonal *@ + + @* Key teeth - perpendicular to shaft *@ + + + +} + +@code { + /// + /// Gets or sets the item type (Login, Alias, CreditCard, Note). + /// + [Parameter] + public string ItemType { get; set; } = ItemTypes.Login; + + /// + /// Gets or sets the logo bytes for Login/Alias items. + /// + [Parameter] + public byte[]? Logo { get; set; } + + /// + /// Gets or sets the card number for CreditCard items (used for brand detection). + /// + [Parameter] + public string? CardNumber { get; set; } + + /// + /// Gets or sets the alt text for the image. + /// + [Parameter] + public string AltText { get; set; } = "Item"; + + /// + /// Gets or sets the size class for the icon (Tailwind CSS classes). + /// + [Parameter] + public string SizeClass { get; set; } = "w-10 h-10"; + + /// + /// Gets or sets whether to show placeholder on image error. + /// + private bool ShowPlaceholder { get; set; } + + /// + /// Converts logo bytes to data URL. + /// + private string GetLogoSrc() + { + if (Logo == null || Logo.Length == 0) + { + return string.Empty; + } + + var base64 = Convert.ToBase64String(Logo); + return $"data:image/png;base64,{base64}"; + } + + /// + /// Handle image load error by showing placeholder. + /// + private void OnImageError() + { + ShowPlaceholder = true; + StateHasChanged(); + } +} diff --git a/apps/server/AliasVault.Client/Main/Components/Credentials/CredentialsTable.razor b/apps/server/AliasVault.Client/Main/Components/Items/ItemsTable.razor similarity index 98% rename from apps/server/AliasVault.Client/Main/Components/Credentials/CredentialsTable.razor rename to apps/server/AliasVault.Client/Main/Components/Items/ItemsTable.razor index 67259b811..142db6d49 100644 --- a/apps/server/AliasVault.Client/Main/Components/Credentials/CredentialsTable.razor +++ b/apps/server/AliasVault.Client/Main/Components/Items/ItemsTable.razor @@ -153,6 +153,6 @@ /// The ID of the credential to navigate to. private void NavigateToCredential(Guid credentialId) { - NavigationManager.NavigateTo($"/credentials/{credentialId}"); + NavigationManager.NavigateTo($"/items/{credentialId}"); } } diff --git a/apps/server/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor b/apps/server/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor index 3cff5445d..681086841 100644 --- a/apps/server/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor +++ b/apps/server/AliasVault.Client/Main/Components/Widgets/CreateNewIdentityWidget.razor @@ -193,14 +193,14 @@ // Error saving. IsCreating = false; GlobalLoadingSpinner.Hide(); - GlobalNotificationService.AddErrorMessage(Localizer["CreateCredentialErrorMessage"], true); + GlobalNotificationService.AddErrorMessage(Localizer["CreateItemErrorMessage"], true); return; } // No error, add success message. - GlobalNotificationService.AddSuccessMessage(Localizer["CredentialCreatedSuccessMessage"]); + GlobalNotificationService.AddSuccessMessage(Localizer["ItemCreatedSuccessMessage"]); - NavigationManager.NavigateTo("/credentials/" + id); + NavigationManager.NavigateTo("/items/" + id); IsCreating = false; GlobalLoadingSpinner.Hide(); @@ -217,7 +217,7 @@ QuickCreateStateService.ServiceName = Model.ServiceName; QuickCreateStateService.ServiceUrl = Model.ServiceUrl; - NavigationManager.NavigateTo("/credentials/create"); + NavigationManager.NavigateTo("/items/create"); ClosePopup(); } diff --git a/apps/server/AliasVault.Client/Main/Components/Widgets/SearchWidget.razor b/apps/server/AliasVault.Client/Main/Components/Widgets/SearchWidget.razor index 089f52d14..c394b5bc4 100644 --- a/apps/server/AliasVault.Client/Main/Components/Widgets/SearchWidget.razor +++ b/apps/server/AliasVault.Client/Main/Components/Widgets/SearchWidget.razor @@ -278,7 +278,7 @@ private async Task SelectResult(Item item) { await JsInteropService.BlurElementById("searchWidget"); - NavigationManager.NavigateTo($"/credentials/{item.Id}"); + NavigationManager.NavigateTo($"/items/{item.Id}"); } private void ResetSearchField(object? sender, LocationChangedEventArgs e) diff --git a/apps/server/AliasVault.Client/Main/Layout/TopMenu.razor b/apps/server/AliasVault.Client/Main/Layout/TopMenu.razor index bef1d17c0..41e929304 100644 --- a/apps/server/AliasVault.Client/Main/Layout/TopMenu.razor +++ b/apps/server/AliasVault.Client/Main/Layout/TopMenu.razor @@ -17,8 +17,8 @@