From bd15b66aeed06091c82a978a4357a0684854feae Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sun, 7 Jun 2026 13:48:12 -0400 Subject: [PATCH] feature: Home Page & Footer Mobile Friendly --- .../AudioPlayerBar/AudioPlayerBar.razor | 2 +- .../AudioPlayerBar/AudioPlayerBar.razor.cs | 72 +++++++++++++++++-- .../DeepDrftPublic.Client.csproj | 6 -- .../Layout/DeepDrftFooter.razor | 2 +- .../Layout/DeepDrftFooter.razor.css | 13 ++-- DeepDrftPublic.Client/Layout/MainLayout.razor | 15 +++- .../Layout/MainLayout.razor.css | 9 ++- DeepDrftPublic/Interop/layout/spacer.ts | 46 ++++++++++++ DeepDrftPublic/Program.cs | 2 + 9 files changed, 147 insertions(+), 20 deletions(-) create mode 100644 DeepDrftPublic/Interop/layout/spacer.ts diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor index f570315..dae7314 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor @@ -9,7 +9,7 @@ } else { -
+
diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs index d2f8366..44aa136 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs @@ -1,6 +1,7 @@ using DeepDrftModels.DTOs; using DeepDrftPublic.Client.Services; using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; using MudBlazor; namespace DeepDrftPublic.Client.Controls.AudioPlayerBar; @@ -9,12 +10,25 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable { [CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; } [Parameter] public bool Fixed { get; set; } = false; - + + [Parameter] public EventCallback OnMinimized { get; set; } + + [Inject] private IJSRuntime JsRuntime { get; set; } = default!; + private bool _isMinimized = true; private bool _isSeeking = false; private double _seekPosition = 0; private IStreamingPlayerService? _subscribedService; + // Spacer-height bridge: the expanded dock is position:fixed, so MainLayout's + // spacer reserves its space. We mirror this element's live height into a CSS + // var via a ResizeObserver (see Interop/layout/spacer.ts) rather than a static + // value, because the player reflows across breakpoints and grows with the + // error banner. + private ElementReference _playerRoot; + private IJSObjectReference? _spacerModule; + private bool _spacerObserved; + private bool IsLoaded => PlayerService?.IsLoaded ?? false; private bool IsLoading => PlayerService?.IsLoading ?? false; @@ -66,6 +80,41 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged); + protected override async Task OnAfterRenderAsync(bool firstRender) + { + // Only the docked, expanded shape needs a spacer: the Fixed embed is + // already in normal flow, and the minimized FAB floats clear of content. + // Toggle the observer on the minimized/expanded transition only — the + // ResizeObserver itself handles every size change in between. + var shouldObserve = !_isMinimized && !Fixed; + if (shouldObserve == _spacerObserved) return; + + var module = await GetSpacerModuleAsync(); + if (module is null) return; + + if (shouldObserve) + await module.InvokeVoidAsync("observe", _playerRoot); + else + await module.InvokeVoidAsync("unobserve"); + + _spacerObserved = shouldObserve; + } + + private async Task GetSpacerModuleAsync() + { + try + { + return _spacerModule ??= await JsRuntime.InvokeAsync( + "import", "./js/layout/spacer.js"); + } + catch (JSException) + { + // Module failed to load — the spacer falls back to 0px (no overlap + // guard, but the player still works). Nothing actionable here. + return null; + } + } + private async Task Expand() { if (_isMinimized) @@ -127,9 +176,10 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable PlayerService?.ClearError(); } - private void ToggleMinimized() + private async Task ToggleMinimized() { _isMinimized = !_isMinimized; + if (OnMinimized.HasDelegate) await OnMinimized.InvokeAsync(_isMinimized); StateHasChanged(); } @@ -147,13 +197,27 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable } } - public ValueTask DisposeAsync() + public async ValueTask DisposeAsync() { if (_subscribedService != null) { _subscribedService.StateChanged -= OnPlayerStateChanged; _subscribedService = null; } - return ValueTask.CompletedTask; + + if (_spacerModule is not null) + { + try + { + // Clear the var so a torn-down player can't strand a spacer height. + await _spacerModule.InvokeVoidAsync("unobserve"); + await _spacerModule.DisposeAsync(); + } + catch (JSException) + { + // Runtime already gone (navigation/teardown) — nothing to clean up. + } + _spacerModule = null; + } } } \ No newline at end of file diff --git a/DeepDrftPublic.Client/DeepDrftPublic.Client.csproj b/DeepDrftPublic.Client/DeepDrftPublic.Client.csproj index 715e8e0..cf4daaf 100644 --- a/DeepDrftPublic.Client/DeepDrftPublic.Client.csproj +++ b/DeepDrftPublic.Client/DeepDrftPublic.Client.csproj @@ -21,10 +21,4 @@ - - - true - - - diff --git a/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor b/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor index 88653d8..ec9fd51 100644 --- a/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor +++ b/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor @@ -4,5 +4,5 @@
  • About
  • Contact
  • - + \ No newline at end of file diff --git a/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor.css b/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor.css index 668e192..7cfb9fe 100644 --- a/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor.css +++ b/DeepDrftPublic.Client/Layout/DeepDrftFooter.razor.css @@ -47,15 +47,20 @@ color: var(--deepdrft-muted); } -@media (max-width: 599px) { +@media (max-width: 440px) { .deepdrft-footer { padding: 1.5rem; - flex-wrap: wrap; + /*flex-wrap: wrap;*/ gap: 1rem; } + + .deepdrft-footer-links { + flex-direction: column; + gap: 0.25rem; + } .deepdrft-footer-copy { - width: 100%; - text-align: right; + /*width: 100%;*/ + justify-self: right; } } diff --git a/DeepDrftPublic.Client/Layout/MainLayout.razor b/DeepDrftPublic.Client/Layout/MainLayout.razor index c24c59b..cca6ea2 100644 --- a/DeepDrftPublic.Client/Layout/MainLayout.razor +++ b/DeepDrftPublic.Client/Layout/MainLayout.razor @@ -21,10 +21,12 @@
    - + - @* Spacer to prevent content overlap *@ -
    + @* Spacer to prevent content overlap. Height tracks the fixed player + dock's live size via the --player-height var the player publishes + (see AudioPlayerBar / Interop/layout/spacer.ts). *@ +
    @@ -37,6 +39,7 @@
    @code { + private string _audioPlayerClass = "minimized"; private const string DarkModeKey = "darkMode"; private bool _isDarkMode = false; private PersistingComponentStateSubscription _persistingSubscription; @@ -76,6 +79,12 @@ { _persistingSubscription.Dispose(); } + + private void ToggleAudioPlayerMinimized(bool isMinimized) + { + _audioPlayerClass = isMinimized ? "minimized" : "expanded"; + } + } diff --git a/DeepDrftPublic.Client/Layout/MainLayout.razor.css b/DeepDrftPublic.Client/Layout/MainLayout.razor.css index 574c16e..1273952 100644 --- a/DeepDrftPublic.Client/Layout/MainLayout.razor.css +++ b/DeepDrftPublic.Client/Layout/MainLayout.razor.css @@ -1,10 +1,17 @@ /* Spacer to prevent content overlap */ .player-spacer { - height: 100px; width: 100%; flex-shrink: 0; } +.player-spacer.expanded { + height: var(--player-height, 60px); +} + +.player-spacer.minimized { + height: 60px; +} + #blazor-error-ui { color-scheme: light only; background: lightyellow; diff --git a/DeepDrftPublic/Interop/layout/spacer.ts b/DeepDrftPublic/Interop/layout/spacer.ts new file mode 100644 index 0000000..72173be --- /dev/null +++ b/DeepDrftPublic/Interop/layout/spacer.ts @@ -0,0 +1,46 @@ +/** + * Player-height spacer observer. + * + * The audio player docks `position: fixed` to the viewport bottom, so it sits + * outside normal flow and would overlap page content. A spacer div in the layout + * reserves the equivalent space — but the player's height is not constant: it + * reflows across the four breakpoints and grows when an error banner appears. A + * static height can't track that, so we mirror the player's live border-box + * height into the `--player-height` custom property on :root and let the spacer + * read it. One observer at a time, re-pointed on each `observe` call; the var + * resets to 0 on `unobserve` (player minimized / disposed) so the spacer + * collapses. + */ + +const HEIGHT_VAR = '--player-height'; +let observer: ResizeObserver | null = null; + +function writeHeight(px: number): void { + // Round up so sub-pixel heights never leave a hairline of overlap. + document.documentElement.style.setProperty(HEIGHT_VAR, `${Math.ceil(px)}px`); +} + +export function observe(element: Element): void { + unobserve(); + if (!element) return; + + observer = new ResizeObserver(entries => { + const entry = entries[0]; + if (!entry) return; + // Prefer the border-box measurement; fall back to contentRect on the + // (older) engines that don't populate borderBoxSize. + const box = entry.borderBoxSize?.[0]; + writeHeight(box ? box.blockSize : entry.contentRect.height); + }); + observer.observe(element); + + // Seed synchronously so the spacer is correct on this frame, before the + // first ResizeObserver callback fires. + writeHeight(element.getBoundingClientRect().height); +} + +export function unobserve(): void { + observer?.disconnect(); + observer = null; + writeHeight(0); +} diff --git a/DeepDrftPublic/Program.cs b/DeepDrftPublic/Program.cs index d96d438..dff19a3 100644 --- a/DeepDrftPublic/Program.cs +++ b/DeepDrftPublic/Program.cs @@ -116,5 +116,7 @@ app.MapRazorComponents() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(DeepDrftPublic.Client._Imports).Assembly); +app.UseStatusCodePagesWithReExecute("/404", createScopeForStatusCodePages: true); + app.Run();