feature: Home Page & Footer Mobile Friendly
Deploy DeepDrftAPI / Build, Publish & Bundle (push) Successful in 1m56s
Deploy DeepDrftManager / Build & Publish (push) Successful in 1m3s
Deploy DeepDrftPublic / Build & Publish (push) Successful in 3m22s
Deploy DeepDrftAPI / Deploy (push) Successful in 1m33s
Deploy DeepDrftManager / Deploy (push) Successful in 1m27s
Deploy DeepDrftPublic / Deploy (push) Successful in 1m29s

This commit is contained in:
daniel-c-harvey
2026-06-07 13:48:12 -04:00
parent 4072197313
commit bd15b66aee
9 changed files with 147 additions and 20 deletions
@@ -9,7 +9,7 @@
}
else
{
<div class="@PlayerModeClass d-flex flex-column">
<div class="@PlayerModeClass d-flex flex-column" @ref="_playerRoot">
<MudContainer MaxWidth="MaxWidth.Large" Class="player-inner-container">
<MudPaper Elevation="8" Class="player-surface pa-3">
@@ -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<bool> 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<IJSObjectReference?> GetSpacerModuleAsync()
{
try
{
return _spacerModule ??= await JsRuntime.InvokeAsync<IJSObjectReference>(
"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;
}
}
}
@@ -21,10 +21,4 @@
<ProjectReference Include="..\DeepDrftShared.Client\DeepDrftShared.Client.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="Layout\NavMenu.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
</ItemGroup>
</Project>
@@ -4,5 +4,5 @@
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
<div class="deepdrft-footer-copy">© 2026 Deep DRFT — Charleston, SC</div>
<div class="deepdrft-footer-copy">© 2026 Deep DRFT</div>
</footer>
@@ -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;
}
}
+12 -3
View File
@@ -21,10 +21,12 @@
</MudContainer>
</MudMainContent>
<DeepDrftFooter />
<AudioPlayerBar />
<AudioPlayerBar OnMinimized="ToggleAudioPlayerMinimized" />
@* Spacer to prevent content overlap *@
<div class="player-spacer"></div>
@* 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). *@
<div class="player-spacer @_audioPlayerClass"></div>
</AudioPlayerProvider>
</MudLayout>
</div>
@@ -37,6 +39,7 @@
</div>
@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";
}
}
@@ -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;
+46
View File
@@ -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);
}
+2
View File
@@ -116,5 +116,7 @@ app.MapRazorComponents<App>()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(DeepDrftPublic.Client._Imports).Assembly);
app.UseStatusCodePagesWithReExecute("/404", createScopeForStatusCodePages: true);
app.Run();