Skip to content

Commit

Permalink
Merge pull request #18 from neozhu/feature/onofflinestate
Browse files Browse the repository at this point in the history
Add Online/Offline Status Display
  • Loading branch information
neozhu authored Dec 10, 2024
2 parents bd170d6 + a2152d7 commit e343b0e
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 15 deletions.
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

0 comments on commit e343b0e

Please sign in to comment.