This commit is contained in:
2025-10-27 17:39:18 -04:00
commit 31f723bea4
1579 changed files with 642409 additions and 0 deletions

View File

@@ -0,0 +1,302 @@
@using Prefab.Web.Client.Services
@inherits LayoutComponentBase
<TelerikRootComponent>
<TelerikNotification @ref="GlobalNotification"
HorizontalPosition="@NotificationHorizontalPosition.Right"
VerticalPosition="@NotificationVerticalPosition.Top">
</TelerikNotification>
<!-- site -->
<div class="site">
<div class="site__container">
<!-- site__header -->
<header class="site__header">
<div class="header">
<div class="header__body">
<div class="search">
<form class="search__form">
<input class="search__input" type="search" placeholder="Search Query...">
<button class="search__button" type="submit">
<svg width="20px" height="20px">
<use xlink:href="images/sprite.svg#search-20"></use>
</svg>
</button>
<button class="search__button search-trigger" type="button">
<svg width="20px" height="20px">
<use xlink:href="images/sprite.svg#cross-20"></use>
</svg>
</button>
</form>
</div>
<button class="header__mobilemenu" type="button">
<svg width="22px" height="16px">
<use xlink:href="images/sprite.svg#menu"></use>
</svg>
</button>
<a href="/" class="header__logo">
<!-- logo -->
<svg class="logo" xmlns="http://www.w3.org/2000/svg" width="82px" height="24px">
@* <path d="M79.011,17.987 L79.011,15.978 C79.011,15.978 77.402,17.366 76.739,17.672 C76.074,17.979 75.110,17.987 74.043,17.987 C72.730,17.987 71.698,17.757 70.944,17.004 C70.191,16.252 70.001,15.282 70.001,13.892 C70.001,12.437 70.421,10.962 71.435,10.272 C72.450,9.581 73.878,9.020 75.924,8.947 L78.295,8.953 C78.295,7.563 77.586,6.768 76.168,6.768 C75.077,6.768 73.794,7.099 72.319,7.761 L71.085,5.235 C72.657,4.409 74.400,3.997 76.315,3.997 C78.149,3.997 79.555,4.397 80.532,5.198 C81.510,6 81.999,7.217 81.999,8.852 L81.999,17.987 L79.011,17.987 ZM78.295,11.636 L76.852,11.685 C75.769,11.718 74.963,11.914 74.434,12.273 C73.904,12.633 73.639,13.181 73.639,13.917 C73.639,14.971 74.242,15.297 75.448,15.297 C76.311,15.297 77.101,14.948 77.619,14.449 C78.135,13.951 78.295,13.590 78.295,12.764 L78.295,11.636 ZM57.289,24 C56.646,24 55,23.994 55,23.994 L55,20.995 C55,20.995 56.332,20.998 56.861,20.998 C59.189,20.998 59.962,17.898 59.962,17.898 L54,4 L58,4 L61.720,14.396 L65,4 L69,4 L62.989,19.741 C61.931,22.589 59.909,24 57.289,24 ZM49,0 L52,0 L52,18 L49,18 L49,0 ZM41.500,18 C40.163,18 38.953,17.368 38,16.358 L38,18 L35,18 L35,0 L38,0 L38,5.642 C38.953,4.632 40.163,4 41.500,4 C44.538,4 47,7.134 47,11 C47,14.866 44.538,18 41.500,18 ZM41,7 C39.343,7 38,8.791 38,11 C38,13.209 39.343,15 41,15 C42.657,15 44,13.209 44,11 C44,8.791 42.657,7 41,7 ZM25.157,14.338 C25.743,14.932 26.565,15.229 27.623,15.229 C28.444,15.229 29.222,15.144 29.954,14.973 C30.687,14.802 31.451,14.529 32,14.155 L32,17.036 C31.598,17.361 30.902,17.603 30.162,17.762 C29.421,17.921 28.518,18 27.452,18 C24,18 21,16 21,11 C21,8 22,4 27,4 C32,4 33,8 33,11 L33,12 L24.217,12 C24.257,13.162 24.571,13.744 25.157,14.338 ZM29.527,9 C29.510,8.081 29,7 27,7 C25,7 24.367,8.081 24.302,9 L29.527,9 ZM16,4.500 L10.977,18 L8,18 L3,4.500 L3,18 L0,18 L0,0 L5,0 L9.500,13.400 L14,0 L19,0 L19,18 L16,18 L16,4.500 Z"></path> *@
<text x="2" y="16" font-family="Arial, sans-serif" font-size="22" font-weight="600" fill="currentColor">
Prefab
</text>
<path class="logo__accent" d="M0,22 L52,22 L52,24 L0,24 L0,22"></path>
</svg>
<!-- logo / end -->
</a>
<NavMenu />
<div class="header__spring"></div>
<div class="header__indicator">
<button type="button" class="header__indicator-button indicator search-trigger">
<span class="indicator__area">
<svg class="indicator__icon" width="20px" height="20px">
<use xlink:href="images/sprite.svg#search-20"></use>
</svg>
</span>
</button>
</div>
<div class="header__indicator">
<a href="/wishlist" class="header__indicator-button indicator">
<span class="indicator__area">
<svg class="indicator__icon" width="20px" height="20px">
<use xlink:href="images/sprite.svg#heart-20"></use>
</svg>
</span>
</a>
</div>
<div class="header__indicator" data-dropdown-trigger="click">
<a href="/cart" class="header__indicator-button indicator">
<span class="indicator__area">
<span class="indicator__value" data-testid="mini-cart-count">@* @MiniCount() *@</span>
<svg class="indicator__icon" width="20px" height="20px">
<use xlink:href="images/sprite.svg#cart-20"></use>
</svg>
</span>
</a>
<div class="header__indicator-dropdown">
<div class="dropcart">
@* <div class="dropcart__products-list">
@if (miniCart?.Items?.Count > 0)
{
@foreach (var i in miniCart.Items)
{
<div class="dropcart__product">
<div class="dropcart__product-image">
<a href="/products">
<img src="images/logo.png" alt="@i.Sku">
</a>
</div>
<div class="dropcart__product-info">
<div class="dropcart__product-name"><a href="/products">@DisplayName(i.Sku)</a></div>
<div class="d-flex align-items-center gap-2">
<TelerikNumericTextBox Value="@i.Quantity" Min="1" Format="0" Width="80px"
ValueChanged="@(async (int v) => { await CartService.UpdateItemQuantityAsync(miniCartId, i.Id, v); miniCart = await CartService.GetCartAsync(miniCartId); StateHasChanged(); })" />
<span>@i.UnitPrice.ToString("C2")</span>
</div>
</div>
<button type="button" class="dropcart__product-remove button-remove" @onclick="() => RemoveMiniItem(i)">
<svg width="10px" height="10px">
<use xlink:href="images/sprite.svg#cross-10"></use>
</svg>
</button>
</div>
}
}
else
{
<div class="p-2 text-muted">Your cart is empty.</div>
}
</div>
<div class="dropcart__totals">
<table>
<tr>
<th>Subtotal</th>
<td>@MiniSubtotal().ToString("C2")</td>
</tr>
<tr>
<th>Shipping</th>
<td>Free Shipping</td>
</tr>
<tr>
<th>Tax</th>
<td>$0.00</td>
</tr>
<tr>
<th>Total</th>
<td>@MiniSubtotal().ToString("C2")</td>
</tr>
</table>
</div>
<div class="dropcart__buttons">
<a class="btn btn-secondary" href="/cart">View Cart</a>
<a class="btn btn-primary" href="/checkout">Checkout</a>
</div> *@
</div>
</div>
</div>
</div>
</div>
</header>
<!-- site__header / end -->
<!-- site__body -->
<div class="site__body">
<!-- page -->
<div class="page">
<!-- page__body -->
<div class="page__body">
<ErrorBoundary @ref="_errorBoundary">
<ChildContent>
@Body
</ChildContent>
</ErrorBoundary>
</div>
<!-- page__body / end -->
</div>
<!-- page / end -->
</div>
<!-- site__body / end -->
<!-- site__footer -->
<footer class="site__footer">
<div class="footer">
<div class="container container--max--xl">
<div class="row g-custom-30">
<div class="col">
<div class="footer__widgets">
<div class="row g-custom-30 justify-content-between">
<div class="col-12 col-lg-4 col-sm-6 footer__aboutus">
<div class="footer-aboutus">
<div class="footer-aboutus__title">
<h4 class="footer-aboutus__header decor-header">About Us</h4>
</div>
<div class="footer-aboutus__text">
We are a leading provider of pre-fabricated solutions for the electrical and construction industries. Our products are designed to save time and reduce costs while maintaining the highest quality standards.
</div>
<ul class="footer-aboutus__contacts">
<li class="footer-aboutus__contacts-item">
<a href="">6504 111th Ave, Kenosha, WI 53142</a>
</li>
<li class="footer-aboutus__contacts-item"><a href="">info@wemsoftware.com</a></li>
<li class="footer-aboutus__contacts-item"><a href="">+1 (810) 232 9797</a></li>
</ul>
</div>
</div>
<div class="col-12 col-lg-2 col-sm-6 col-md-3">
<div class="footer-links" data-collapse data-collapse-open-class="footer-links--open" data-collapse-item>
<div class="footer-links__title">
<h4 class="footer-links__header" data-collapse-trigger>
Information
<svg class="footer-links__header-arrow" width="9px" height="6px">
<use xlink:href="images/sprite.svg#arrow-down-9x6"></use>
</svg>
</h4>
</div>
<div class="footer-links__content" data-collapse-content>
<ul class="footer-links__list">
<li class="footer-links__item"><a href="" class="footer-links__link">About Us</a></li>
<li class="footer-links__item"><a href="" class="footer-links__link">Delivery Information</a></li>
<li class="footer-links__item"><a href="" class="footer-links__link">Privacy Policy</a></li>
<li class="footer-links__item"><a href="" class="footer-links__link">Brands</a></li>
<li class="footer-links__item"><a href="" class="footer-links__link">Contact Us</a></li>
<li class="footer-links__item"><a href="" class="footer-links__link">Returns</a></li>
<li class="footer-links__item"><a href="" class="footer-links__link">Site Map</a></li>
</ul>
</div>
</div>
</div>
<div class="col-12 col-lg-2 col-sm-6 col-md-3">
<div class="footer-links" data-collapse data-collapse-open-class="footer-links--open" data-collapse-item>
<div class="footer-links__title">
<h4 class="footer-links__header" data-collapse-trigger>
My Account
<svg class="footer-links__header-arrow" width="9px" height="6px">
<use xlink:href="images/sprite.svg#arrow-down-9x6"></use>
</svg>
</h4>
</div>
<div class="footer-links__content" data-collapse-content>
<ul class="footer-links__list">
<li class="footer-links__item"><a href="" class="footer-links__link">My Account</a></li>
<li class="footer-links__item"><a href="" class="footer-links__link">Order History</a></li>
<li class="footer-links__item"><a href="" class="footer-links__link">Wish List</a></li>
<li class="footer-links__item"><a href="" class="footer-links__link">Newsletter</a></li>
<li class="footer-links__item"><a href="" class="footer-links__link">Specials</a></li>
<li class="footer-links__item"><a href="" class="footer-links__link">Gift Certificates</a></li>
<li class="footer-links__item"><a href="" class="footer-links__link">Affiliate</a></li>
</ul>
</div>
</div>
</div>
<div class="col-12 col-lg-4 footer__followus">
<div class="footer-followus">
<div class="footer-followus__title">
<h4 class="footer-followus__header">Follow Us</h4>
</div>
<div class="footer-followus__text">
Sign up for our newsletter to get the latest news, updates and special offers delivered directly to your inbox.
</div>
<form class="footer-followus__subscribe-form">
<input type="email" class="form-control form-control--footer" placeholder="Email address">
<button type="submit" class="btn btn-primary">Subscribe</button>
</form>
<ul class="footer-followus__social-links">
<li class="footer-followus__social-link"><a href="#" target="_blank"><i class="fab fa-facebook-f"></i></a></li>
<li class="footer-followus__social-link"><a href="#" target="_blank"><i class="fab fa-twitter"></i></a></li>
<li class="footer-followus__social-link"><a href="#" target="_blank"><i class="fab fa-youtube"></i></a></li>
<li class="footer-followus__social-link"><a href="#" target="_blank"><i class="fab fa-instagram"></i></a></li>
<li class="footer-followus__social-link"><a href="#" target="_blank"><i class="fas fa-rss"></i></a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-custom-30 justify-content-between">
<div class="col-12 col-sm-auto">
<div class="copyright">
<!-- copyright -->
<span class="copyright__text">Prefab.Server © 2023-@DateTime.Now.Year. All Rights Reserved.</span>
<!-- copyright / end -->
</div>
</div>
<div class="footer__payments col-auto">
<img srcset="images/payments.png 1x, images/payments@2x.png 2x" src="images/payments.png" alt="">
</div>
</div>
</div>
</div>
</footer>
<!-- site__footer / end -->
</div>
</div>
<!-- site / end -->
</TelerikRootComponent>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@code {
private ErrorBoundary? _errorBoundary;
private TelerikNotification GlobalNotification { get; set; } = new();
[Inject]
public INotificationService NotificationService { get; set; } = null!;
protected override void OnParametersSet()
{
_errorBoundary?.Recover();
}
protected override void OnAfterRender(bool firstRender)
{
if (!firstRender)
return;
NotificationService.Attach(GlobalNotification);
}
}

View File

@@ -0,0 +1,20 @@
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

@@ -0,0 +1,409 @@
@rendermode InteractiveAuto
@using System.Linq
@using Prefab.Web.Client.Models.Shared
@using Prefab.Web.Client.Services
@implements IDisposable
<nav class="header__nav main-nav">
<ul class="main-nav__list">
<li class="main-nav__item ">
<NavLink class="main-nav__link" href="/" Match="NavLinkMatch.All">
Home
</NavLink>
</li>
<li class="@ProductsMenuItemClass"
@onmouseenter="HandleMouseEnter"
@onmouseleave="HandleMouseLeave">
<span class="main-nav__trigger"
@onclick="OnProductsMenuLinkClick"
@onclick:preventDefault>
<NavLink id="productsMenuLink"
class="main-nav__link"
href="/catalog"
role="button"
aria-haspopup="true"
aria-expanded="@(_productsMenuOpen.ToString().ToLowerInvariant())"
aria-controls="productsMenuPanel">
<span>Products</span>
<svg class="main-nav__link-arrow" width="9px" height="6px">
<use xlink:href="images/sprite.svg#arrow-down-9x6"></use>
</svg>
</NavLink>
</span>
<div id="productsMenuPanel"
class="main-nav__sub-megamenu"
role="region"
aria-hidden="@((!_productsMenuOpen).ToString().ToLowerInvariant())"
tabindex="-1"
@ref="_panel">
@if (!_menuLoaded)
{
<div class="megamenu" role="menu" aria-live="polite">
<div class="row g-custom-30">
@for (var i = 0; i < 4; i++)
{
<div class="col-3">
<TelerikSkeleton Width="100%" Height="20px" Class="mb-2" />
<ul class="megamenu__links megamenu__links--sub">
@for (var j = 0; j < 4; j++)
{
<li class="mb-2">
<TelerikSkeleton Height="16px" Width="80%" />
</li>
}
</ul>
</div>
}
</div>
</div>
}
else if (_menu is null || _menu.Columns.Count == 0)
{
<div class="mega-menu__empty" role="status">@(_errorMessage ?? "Navigation unavailable.")</div>
}
else
{
<div class="megamenu" role="menu" aria-live="polite">
<div class="row g-custom-30">
@foreach (var column in _menu.Columns.Take(4))
{
<div class="col-3">
<ul class="megamenu__links megamenu__links--root">
<li>
<NavLink href="@column.Root.Url">@column.Root.Name</NavLink>
@if (column.Items is { Count: > 0 })
{
<ul class="megamenu__links megamenu__links--sub">
@foreach (var item in column.Items)
{
<li><NavLink href="@item.Url">@item.Name</NavLink></li>
}
<li class="megamenu__see-all">
<NavLink href="@column.SeeAllUrl">See all @column.Root.Name</NavLink>
</li>
</ul>
}
</li>
</ul>
</div>
}
</div>
</div>
}
</div>
</li>
<li class="@ProductsFlyoutItemClass"
@onmouseenter="HandleFlyoutMouseEnter"
@onmouseleave="HandleFlyoutMouseLeave">
<span class="main-nav__trigger"
@onclick="OnProductsFlyoutLinkClick"
@onclick:preventDefault>
<NavLink class="main-nav__link"
href="/catalog"
role="button"
aria-haspopup="true"
aria-expanded="@(_productsFlyoutOpen.ToString().ToLowerInvariant())"
aria-controls="productsFlyoutMenu">
<span>Products</span>
<svg class="main-nav__link-arrow" width="9px" height="6px">
<use xlink:href="images/sprite.svg#arrow-down-9x6"></use>
</svg>
</NavLink>
</span>
<div id="productsFlyoutMenu"
class="main-nav__sub-menu"
role="region"
aria-live="polite">
@if (!_menuLoaded)
{
<div class="p-3" role="status">
@for (var i = 0; i < 4; i++)
{
<div class="mb-2">
<TelerikSkeleton Width="180px" Height="16px" />
</div>
}
</div>
}
else if (_menu is null || _menu.Columns.Count == 0)
{
<div class="menu__empty p-3" role="status">@(_errorMessage ?? "Navigation unavailable.")</div>
}
else
{
<ul class="menu">
@foreach (var column in GetFlyoutColumns())
{
var hasChildren = column.Items is { Count: > 0 };
var hasSeeAll = !string.IsNullOrWhiteSpace(column.SeeAllUrl);
<li class="menu__item">
<NavLink class="menu__link"
href="@column.Root.Url">
@column.Root.Name
@if (hasChildren)
{
<svg class="menu__link-arrow" width="6px" height="9px">
<use xlink:href="images/sprite.svg#arrow-right-6x9"></use>
</svg>
}
</NavLink>
@if (hasChildren)
{
<div class="menu__sub">
<ul class="menu">
@if (column.Items is { Count: > 0 })
{
@foreach (var item in column.Items)
{
<li class="menu__item">
<NavLink class="menu__link" href="@item.Url">@item.Name</NavLink>
</li>
}
}
@if (hasSeeAll)
{
<li class="menu__item">
<NavLink class="menu__link menu__link--see-all" href="@column.SeeAllUrl">
See all @column.Root.Name
</NavLink>
</li>
}
</ul>
</div>
}
</li>
}
</ul>
}
</div>
</li>
<li class="main-nav__item">
<NavLink class="main-nav__link" href="Contact">
Contact Us
</NavLink>
</li>
</ul>
</nav>
@code {
private const string ProductsMenuCacheKey = "nav-menu/products";
[Inject]
private NavigationManager NavigationManager { get; set; } = default!;
[Inject]
private INavMenuService NavMenuService { get; set; } = default!;
[Inject]
private IServiceProvider ServiceProvider { get; set; } = default!;
private bool _productsMenuOpen;
private bool _productsFlyoutOpen;
private ElementReference _panel;
private NavMenuModel? _menu;
private bool _menuLoaded;
private string? _errorMessage;
private IDisposable? _persistRegistration;
private CancellationTokenSource? _loadCancellationSource;
private string ProductsMenuItemClass =>
$"main-nav__item main-nav__item--with--menu {(_productsMenuOpen ? "main-nav__item--open" : string.Empty)}";
private string ProductsFlyoutItemClass =>
$"main-nav__item main-nav__item--with--menu {(_productsFlyoutOpen ? "main-nav__item--open" : string.Empty)}";
private PersistentComponentState? AppState => ServiceProvider.GetService<PersistentComponentState>();
protected override void OnInitialized()
{
NavigationManager.LocationChanged += OnLocationChanged;
var appState = AppState;
if (appState is not null)
{
_persistRegistration = appState.RegisterOnPersisting(PersistAsync);
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender || _menuLoaded)
{
return;
}
if (AppState is not null && AppState.TryTakeFromJson(ProductsMenuCacheKey, out NavMenuModel? cached) && cached is not null)
{
_menu = cached;
}
else
{
await LoadAsync(forceReload: true);
}
_menuLoaded = true;
StateHasChanged();
}
private async Task ToggleProductsMenuAsync()
{
if (_productsMenuOpen)
{
await CloseAsync();
return;
}
await OpenAsync(focusPanel: true);
}
private Task OnProductsMenuLinkClick(MouseEventArgs _) => ToggleProductsMenuAsync();
private Task HandleMouseEnter() => OpenAsync(focusPanel: false);
private Task HandleMouseLeave() => CloseAsync();
private async Task HandleFlyoutMouseEnter()
{
await LoadAsync(forceReload: false);
_productsFlyoutOpen = true;
StateHasChanged();
}
private Task HandleFlyoutMouseLeave()
{
if (!_productsFlyoutOpen)
{
return Task.CompletedTask;
}
_productsFlyoutOpen = false;
StateHasChanged();
return Task.CompletedTask;
}
private Task OnProductsFlyoutLinkClick(MouseEventArgs _)
{
if (_productsFlyoutOpen)
{
_productsFlyoutOpen = false;
StateHasChanged();
return Task.CompletedTask;
}
return HandleFlyoutMouseEnter();
}
private async Task OpenAsync(bool focusPanel)
{
if (_productsMenuOpen)
{
return;
}
await LoadAsync(forceReload: false);
_productsMenuOpen = true;
StateHasChanged();
if (focusPanel)
{
await Task.Yield();
await _panel.FocusAsync();
}
}
private Task CloseAsync()
{
if (!_productsMenuOpen)
{
return Task.CompletedTask;
}
_productsMenuOpen = false;
StateHasChanged();
return Task.CompletedTask;
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
_ = InvokeAsync(async () =>
{
await CloseAsync();
_productsFlyoutOpen = false;
await LoadAsync(forceReload: true);
StateHasChanged();
});
}
private async Task LoadAsync(bool forceReload)
{
if (!forceReload && _menu is not null)
{
return;
}
_loadCancellationSource?.Cancel();
_loadCancellationSource?.Dispose();
_loadCancellationSource = new CancellationTokenSource();
var token = _loadCancellationSource.Token;
try
{
var result = await NavMenuService.Get(2, token);
if (result.IsSuccess && result.Value is { } model)
{
_menu = model;
_errorMessage = null;
}
else
{
_menu = null;
_errorMessage = result.Problem?.Detail ?? "Navigation unavailable.";
}
}
catch (OperationCanceledException)
{
// Swallow expected cancellation when a new request supersedes the current one.
}
catch (Exception exception)
{
_menu = null;
_errorMessage = exception.Message;
}
}
private Task PersistAsync()
{
var appState = AppState;
if (_menu is not null && appState is not null)
{
appState.PersistAsJson(ProductsMenuCacheKey, _menu);
}
return Task.CompletedTask;
}
public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
_persistRegistration?.Dispose();
_loadCancellationSource?.Cancel();
_loadCancellationSource?.Dispose();
}
private IEnumerable<NavMenuColumnModel> GetFlyoutColumns()
{
if (_menu is null || _menu.Columns.Count == 0)
{
return Array.Empty<NavMenuColumnModel>();
}
return _menu.Columns;
}
}

View File

@@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,31 @@
<script type="module" src="@Assets["Layout/ReconnectModal.razor.js"]"></script>
<dialog id="components-reconnect-modal" data-nosnippet>
<div class="components-reconnect-container">
<div class="components-rejoining-animation" aria-hidden="true">
<div></div>
<div></div>
</div>
<p class="components-reconnect-first-attempt-visible">
Rejoining the server...
</p>
<p class="components-reconnect-repeated-attempt-visible">
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
</p>
<p class="components-reconnect-failed-visible">
Failed to rejoin.<br />Please retry or reload the page.
</p>
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
Retry
</button>
<p class="components-pause-visible">
The session has been paused by the server.
</p>
<button id="components-resume-button" class="components-pause-visible">
Resume
</button>
<p class="components-resume-failed-visible">
Failed to resume the session.<br />Please reload the page.
</p>
</div>
</dialog>

View File

@@ -0,0 +1,157 @@
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
display: none;
}
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: block;
}
#components-reconnect-modal {
background-color: white;
width: 20rem;
margin: 20vh auto;
padding: 2rem;
border: 0;
border-radius: 0.5rem;
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
&[open]
{
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
animation-fill-mode: both;
}
}
#components-reconnect-modal::backdrop {
background-color: rgba(0, 0, 0, 0.4);
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
opacity: 1;
}
@keyframes components-reconnect-modal-slideUp {
0% {
transform: translateY(30px) scale(0.95);
}
100% {
transform: translateY(0);
}
}
@keyframes components-reconnect-modal-fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes components-reconnect-modal-fadeOutOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.components-reconnect-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#components-reconnect-modal p {
margin: 0;
text-align: center;
}
#components-reconnect-modal button {
border: 0;
background-color: #6b9ed2;
color: white;
padding: 4px 24px;
border-radius: 4px;
}
#components-reconnect-modal button:hover {
background-color: #3b6ea2;
}
#components-reconnect-modal button:active {
background-color: #6b9ed2;
}
.components-rejoining-animation {
position: relative;
width: 80px;
height: 80px;
}
.components-rejoining-animation div {
position: absolute;
border: 3px solid #0087ff;
opacity: 1;
border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.components-rejoining-animation div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes components-rejoining-animation {
0% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 80px;
height: 80px;
opacity: 0;
}
}

View File

@@ -0,0 +1,63 @@
// Set up event handlers
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
retryButton.addEventListener("click", retry);
const resumeButton = document.getElementById("components-resume-button");
resumeButton.addEventListener("click", resume);
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
// Reconnect will asynchronously return:
// - true to mean success
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
// - exception to mean we didn't reach the server (this can be sync or async)
const successful = await Blazor.reconnect();
if (!successful) {
// We have been able to reach the server, but the circuit is no longer available.
// We'll reload the page so the user can continue using the app as quickly as possible.
const resumeSuccessful = await Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
} catch (err) {
// We got an exception, server is currently unavailable
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
const successful = await Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
} catch {
location.reload();
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}