Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Online/Offline Status Display #18

Merged
merged 5 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CleanAspire.ClientApp/Layout/Appbar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" />
<MudText Typo="Typo.h6"> @L[AppSettings.AppName]</MudText>
<MudSpacer />


<MudTooltip Delay="1000" Text="https://github.com/neozhu/cleanaspire.git">
<MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Inherit" Href="https://github.com/neozhu/cleanaspire.git" Target="_blank" />
</MudTooltip>
Expand Down
61 changes: 48 additions & 13 deletions src/CleanAspire.ClientApp/Layout/UserMenu.razor
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@

@using CleanAspire.ClientApp.Services.Identity
@using CleanAspire.ClientApp.Services.Identity
@using Microsoft.AspNetCore.Components.Authorization
@inject IIdentityManagement IdentityManagement

@inject OnlineStatusService OnlineStatusService
@inject LayoutService LayoutService
<MudMenu Class="ml-1" PositionAtCursor="false" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.BottomLeft">
<ActivatorContent>
<MudStack Row="true" AlignItems="AlignItems.Center">
@if (string.IsNullOrEmpty(userModel?.AvatarUrl))
{
<MudAvatar Size="Size.Medium" Square="true">@userModel?.Username?.FirstOrDefault()</MudAvatar>
}
else
{
<MudAvatar Size="Size.Medium" Square="true">
<MudImage Src="@userModel.AvatarUrl"></MudImage>
</MudAvatar>
}
<MudTooltip Text="@statusTooltip" Delay="300">
<div class="avatar-container">
@if (_isOnline == false)
{
<div class="rotating-border @(LayoutService.IsDarkMode ? "dark-mode" : "light-mode")"></div>
}
else
{
<div class="online-border @(LayoutService.IsDarkMode ? "dark-mode" : "light-mode")"></div>
}
@if (string.IsNullOrEmpty(userModel?.AvatarUrl))
{
<MudAvatar Style="width:40px;height:40px" >@userModel?.Username?.FirstOrDefault()</MudAvatar>
}
else
{
<MudAvatar Style="width:40px;height:40px">
<MudImage Src="@userModel.AvatarUrl"></MudImage>
</MudAvatar>
}
</div>
</MudTooltip>
<MudStack Justify="Justify.Center" Spacing="0">
<MudText Typo="Typo.body2">@userModel?.Username</MudText>
<MudText Typo="Typo.body2" Class="mud-text-secondary">@userModel?.Email</MudText>
Expand Down Expand Up @@ -54,10 +66,33 @@
public AuthenticationStateProvider AuthenticationStateProvider { get; set; } = null!;

private ProfileResponse? userModel => UserProfileStore.Profile;
private string statusTooltip = "Loading...";
private bool _isOnline = false;
private async Task OnSignOut()
{
await IdentityManagement.LogoutAsync();
StateHasChanged();
}
protected override async Task OnInitializedAsync()
{
await OnlineStatusService.InitializeAsync();
bool isOnline = await OnlineStatusService.GetOnlineStatusAsync();
UpdateStatus(isOnline);
OnlineStatusService.OnlineStatusChanged += UpdateStatus;
await OnlineStatusService.RegisterOnlineStatusListenerAsync();
}
private void UpdateStatus(bool isOnline)
{
// Update the Base64 image and tooltip based on the network status
statusTooltip = isOnline ? "Online" : "Offline";
_isOnline = isOnline;
InvokeAsync(StateHasChanged);
}

public async ValueTask DisposeAsync()
{
// Unsubscribe from events and dispose of resources
OnlineStatusService.OnlineStatusChanged -= UpdateStatus;
await OnlineStatusService.DisposeAsync();
}
}
2 changes: 2 additions & 0 deletions src/CleanAspire.ClientApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.Kiota.Serialization.Multipart;
using CleanAspire.ClientApp.Services;
using Microsoft.Extensions.DependencyInjection;
using CleanAspire.ClientApp.Services.JsInterop;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

Expand All @@ -25,6 +26,7 @@
builder.Services.AddTransient<CookieHandler>();
builder.Services.AddTransient<WebpushrAuthHandler>();
builder.Services.AddSingleton<UserProfileStore>();
builder.Services.AddSingleton<OnlineStatusService>();

var clientAppSettings = builder.Configuration.GetSection(ClientAppSettings.KEY).Get<ClientAppSettings>();
builder.Services.AddSingleton(clientAppSettings!);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.JSInterop;

namespace CleanAspire.ClientApp.Services.JsInterop;

using Microsoft.JSInterop;
using System;
using System.Threading.Tasks;

public class OnlineStatusService : IAsyncDisposable
{
private readonly IJSRuntime _jsRuntime;
private IJSObjectReference? _jsModule;
private DotNetObjectReference<OnlineStatusService>? _dotNetRef;

public event Action<bool>? OnlineStatusChanged;

public OnlineStatusService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task InitializeAsync()
{
_jsModule = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "/js/onlinestatus.js");
_dotNetRef = DotNetObjectReference.Create(this);
}

public async Task<bool> GetOnlineStatusAsync()
{
if (_jsModule == null)
{
throw new InvalidOperationException("JavaScript module is not initialized. Call InitializeAsync first.");
}
return await _jsModule.InvokeAsync<bool>("getOnlineStatus");
}
public async Task RegisterOnlineStatusListenerAsync()
{
if (_jsModule == null)
{
throw new InvalidOperationException("JavaScript module is not initialized. Call InitializeAsync first.");
}
await _jsModule.InvokeVoidAsync("addOnlineStatusListener", _dotNetRef);
}

[JSInvokable]
public void UpdateOnlineStatus(bool isOnline)
{
OnlineStatusChanged?.Invoke(isOnline);
}

public async ValueTask DisposeAsync()
{
if (_dotNetRef != null)
{
_dotNetRef.Dispose();
}
if (_jsModule != null)
{
await _jsModule.DisposeAsync();
}
}
}
2 changes: 1 addition & 1 deletion src/CleanAspire.ClientApp/Themes/Theme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public static MudTheme ApplicationTheme()
PaletteDark = new()
{
// **Primary Colors**
Primary = "#bd93ff", // Deep blue, used for highlights and key elements
Primary = "#8a2be2", // Deep blue, used for highlights and key elements
Secondary = "#B0BEC5", // Light gray, secondary text color

// **Background and Surface**
Expand Down
99 changes: 99 additions & 0 deletions src/CleanAspire.ClientApp/wwwroot/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,105 @@
padding: 0rem !important;
}

.network-status-indicator {
width: 48px;
height: 48px;
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto; /* Center horizontally if needed */
box-sizing: border-box;
}

.avatar-container {
position: relative;
width: 40px; /* Fixed container width, same as avatar */
height: 40px; /* Fixed container height, same as avatar */
}

/* Red rotating border (common style) */
.rotating-border {
position: absolute;
top: -4px;
left: -4px;
width: 48px;
height: 48px;
border: 3px solid transparent; /* Default transparent */
border-radius: 50%;
animation: spin 2s linear infinite; /* Rotating animation */
z-index: 1;
box-sizing: border-box;
}

/* Red border in dark mode */
.rotating-border.dark-mode {
border-top: 3px solid rgba(255, 0, 0, 0.8); /* Dark red border */
box-shadow: 0 0 8px rgba(255, 0, 0, 0.6), 0 0 12px rgba(255, 50, 50, 0.4); /* Enhanced shadow */
}

/* Red border in light mode */
.rotating-border.light-mode {
border-top: 3px solid rgba(255, 50, 50, 0.8); /* Light red border */
box-shadow: 0 0 6px rgba(255, 50, 50, 0.5), 0 0 10px rgba(255, 100, 100, 0.3); /* Soft shadow */
}

/* Dynamic green border (common style) */
.online-border {
position: absolute;
top: -4px;
left: -4px;
width: 48px;
height: 48px;
border-radius: 50%;
animation: green-pulse 2s infinite; /* Breathing animation */
z-index: 1;
box-sizing: border-box;
}

/* Green border in dark mode */
.online-border.dark-mode {
border: 3px solid rgba(0, 128, 0, 1); /* Dark green border */
box-shadow: 0 0 8px rgba(0, 128, 0, 0.6), 0 0 12px rgba(0, 255, 0, 0.4);
}

/* Green border in light mode */
.online-border.light-mode {
border: 3px solid rgba(0, 200, 0, 1); /* Light green border */
box-shadow: 0 0 6px rgba(0, 200, 0, 0.5), 0 0 10px rgba(0, 255, 0, 0.3);
}
/* Breathing animation */
@keyframes green-pulse {
0% {
box-shadow: 0 0 8px rgba(0, 128, 0, 0.6), 0 0 12px rgba(0, 255, 0, 0.4);
transform: scale(1);
}

50% {
box-shadow: 0 0 10px rgba(0, 128, 0, 0.8), 0 0 14px rgba(0, 255, 0, 0.5);
transform: scale(1.03); /* Reduce scaling amplitude */
}

100% {
box-shadow: 0 0 8px rgba(0, 128, 0, 0.6), 0 0 12px rgba(0, 255, 0, 0.4);
transform: scale(1);
}
}

MudAvatar {
position: relative;
z-index: 2; /* Ensure avatar is above the border */
}

@keyframes spin {
from {
transform: rotate(0deg);
}

to {
transform: rotate(360deg);
}
}


.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
Expand Down
47 changes: 47 additions & 0 deletions src/CleanAspire.ClientApp/wwwroot/green-point.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions src/CleanAspire.ClientApp/wwwroot/js/onlinestatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function getOnlineStatus() {
return navigator.onLine;
}
export function addOnlineStatusListener(dotNetObjectRef) {
window.addEventListener('online', () => {
dotNetObjectRef.invokeMethodAsync('UpdateOnlineStatus', true);
});
window.addEventListener('offline', () => {
dotNetObjectRef.invokeMethodAsync('UpdateOnlineStatus', false);
});
}
20 changes: 20 additions & 0 deletions src/CleanAspire.ClientApp/wwwroot/loading.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading