From fa28bfb5cc9e8f0a00e63fb237a99f48afee9269 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sun, 7 Jun 2026 16:38:37 -0400 Subject: [PATCH] feat: add Share popover to track detail page --- .../Controls/SharePopover.razor | 61 +++++++++++++ .../Controls/SharePopover.razor.cs | 91 +++++++++++++++++++ DeepDrftPublic.Client/Pages/TrackDetail.razor | 7 +- .../wwwroot/styles/deepdrft-styles.css | 17 ++++ 4 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 DeepDrftPublic.Client/Controls/SharePopover.razor create mode 100644 DeepDrftPublic.Client/Controls/SharePopover.razor.cs diff --git a/DeepDrftPublic.Client/Controls/SharePopover.razor b/DeepDrftPublic.Client/Controls/SharePopover.razor new file mode 100644 index 0000000..ffcb536 --- /dev/null +++ b/DeepDrftPublic.Client/Controls/SharePopover.razor @@ -0,0 +1,61 @@ +@namespace DeepDrftPublic.Client.Controls + + + + + + + + + + + + + Copy link + + @if (_linkCopied) + { + Copied! + } + + + + + + + @if (_embed) + { + + + + + @if (_embedCopied) + { + Copied! + } + + + } + + + diff --git a/DeepDrftPublic.Client/Controls/SharePopover.razor.cs b/DeepDrftPublic.Client/Controls/SharePopover.razor.cs new file mode 100644 index 0000000..7017f3c --- /dev/null +++ b/DeepDrftPublic.Client/Controls/SharePopover.razor.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace DeepDrftPublic.Client.Controls; + +/// +/// Share affordance for the track detail page: a popover offering a canonical-link copy +/// and an optional iframe embed snippet. Clipboard writes go through navigator.clipboard; +/// each copy shows a transient "Copied!" confirmation that resets after a short delay. +/// +public partial class SharePopover : ComponentBase, IDisposable +{ + [Parameter] public string? EntryKey { get; set; } + + [Inject] public required NavigationManager Navigation { get; set; } + [Inject] public required IJSRuntime JS { get; set; } + + private bool _open; + private bool _embed; + private bool _linkCopied; + private bool _embedCopied; + + private readonly CancellationTokenSource _cts = new(); + + private bool Embed + { + get => _embed; + set + { + _embed = value; + if (!value) _embedCopied = false; + } + } + + private string TrackUrl => $"{Navigation.BaseUri}track/{EntryKey}"; + + private string EmbedSnippet => + $""""""; + + private void Toggle() => _open = !_open; + + private void Close() => _open = false; + + private async Task CopyLink() + { + if (await CopyToClipboard(TrackUrl)) + { + _linkCopied = true; + await ResetAfterDelay(() => _linkCopied = false); + } + } + + private async Task CopyEmbed() + { + if (await CopyToClipboard(EmbedSnippet)) + { + _embedCopied = true; + await ResetAfterDelay(() => _embedCopied = false); + } + } + + private async Task CopyToClipboard(string text) + { + try + { + await JS.InvokeVoidAsync("navigator.clipboard.writeText", text); + return true; + } + catch (Exception) + { + return false; + } + } + + private async Task ResetAfterDelay(Action reset) + { + try + { + await Task.Delay(1500, _cts.Token); + } + catch (TaskCanceledException) + { + return; + } + + reset(); + StateHasChanged(); + } + + public void Dispose() => _cts.Cancel(); +} diff --git a/DeepDrftPublic.Client/Pages/TrackDetail.razor b/DeepDrftPublic.Client/Pages/TrackDetail.razor index c997b6f..73884e8 100644 --- a/DeepDrftPublic.Client/Pages/TrackDetail.razor +++ b/DeepDrftPublic.Client/Pages/TrackDetail.razor @@ -57,9 +57,10 @@ else if (ViewModel.Track is not null) @track.Artist -
+ + -
+
@@ -67,8 +68,6 @@ else if (ViewModel.Track is not null)
- - @if (hasMeta) { diff --git a/DeepDrftPublic/wwwroot/styles/deepdrft-styles.css b/DeepDrftPublic/wwwroot/styles/deepdrft-styles.css index aeb5efe..ce30251 100644 --- a/DeepDrftPublic/wwwroot/styles/deepdrft-styles.css +++ b/DeepDrftPublic/wwwroot/styles/deepdrft-styles.css @@ -339,6 +339,23 @@ h2, h3, h4, h5, h6, font-family: var(--deepdrft-font-mono) !important; } +.deepdrft-share-popover-body { + padding: 0.75rem 1rem; + min-width: 280px; + max-width: 360px; +} + +/* Monospace snippet so the iframe markup stays legible inside the readonly field. */ +.deepdrft-share-embed-field { + flex: 1 1 auto; +} + +.deepdrft-share-embed-field .mud-input-slot { + font-family: var(--deepdrft-font-mono) !important; + font-size: 0.75rem; + word-break: break-all; +} + /* display:contents so the anchor wraps the card's cover and title without introducing its own box — the container's positioning context and the content column's flex layout are both preserved. */