Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c9113ca
added radio to settings page
YoyoCaleb Jul 7, 2026
a7e3308
UserPreferences created
YoyoCaleb Jul 16, 2026
53c8bef
Added userpreference to application user
YoyoCaleb Jul 16, 2026
c832a54
added display to enum, moved radio
YoyoCaleb Jul 20, 2026
2a779c8
GetPreferencesAsync created
YoyoCaleb Jul 23, 2026
edce306
implemented in index.cs
YoyoCaleb Jul 23, 2026
0e71748
Added test to see if theme displays. dark set to test if default them…
YoyoCaleb Jul 24, 2026
c635604
Removed display names
YoyoCaleb Jul 24, 2026
03d558d
changed Preferences to make it create when application user is created
YoyoCaleb Jul 27, 2026
6fb274b
Created UpdateThemePreferenceAsync
YoyoCaleb Jul 28, 2026
d56cf62
made radio a form. Checks correct theme
YoyoCaleb Jul 29, 2026
7d27fcf
Added OnPost method
YoyoCaleb Jul 30, 2026
8a18351
removed localstorage and implemented preferredtheme
YoyoCaleb Aug 3, 2026
de1497b
added Preferences variable to layout
YoyoCaleb Aug 4, 2026
b8d4fa9
changed auto to match preference auto
YoyoCaleb Aug 4, 2026
e3d27b7
deleted old navbar theme methods
YoyoCaleb Aug 4, 2026
e24e334
removed dropdown
YoyoCaleb Aug 4, 2026
56ec29f
easier to read
YoyoCaleb Aug 7, 2026
99e7975
fixed button name
YoyoCaleb Aug 10, 2026
26b4550
fixed preference methods
YoyoCaleb Aug 10, 2026
f38afb3
checks to see if user is logged in before checking preference
YoyoCaleb Aug 10, 2026
0e9affc
added event listener to detect theme changes
YoyoCaleb Aug 12, 2026
777b676
removed form and save changes button
YoyoCaleb Aug 12, 2026
455dc30
Looks for "theme" in lowercase. values in radio are now lowercase
YoyoCaleb Aug 14, 2026
efaab76
brought form back
YoyoCaleb Aug 18, 2026
cb74eaf
js now submits the form when radtio is clicked
YoyoCaleb Aug 20, 2026
1cebd32
removed the old theme labl and current theme variable display
YoyoCaleb Aug 20, 2026
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
17 changes: 17 additions & 0 deletions src/AppServices.Core/EntityServices/Staff/StaffService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public interface IStaffService : IDisposable, IAsyncDisposable
Task<IReadOnlyList<AppRole>> GetAppRolesAsync(string id);
Task<IdentityResult> UpdateRolesAsync(string id, Dictionary<string, bool> roles);
Task<IdentityResult> UpdateAsync(string id, StaffUpdateDto resource);
Task<UserPreferences> GetPreferencesAsync();
Task UpdateThemePreferenceAsync(ThemePreference theme);

}

public sealed class StaffService(
Expand Down Expand Up @@ -175,7 +178,21 @@ public async Task<IdentityResult> UpdateAsync(string id, StaffUpdateDto resource
await cache.RemoveByTagAsync(CachedStaffLists).ConfigureAwait(false);
return await userManager.UpdateAsync(user).ConfigureAwait(false);
}
public async Task<UserPreferences> GetPreferencesAsync()
{
var user = await userService.GetCurrentUserAsync()
?? throw new CurrentUserNotFoundException();

return user.Preferences;
}
public async Task UpdateThemePreferenceAsync(ThemePreference theme)
{
var user = await userService.GetCurrentUserAsync()
?? throw new CurrentUserNotFoundException();

user.Preferences.Theme = theme;
await userManager.UpdateAsync(user).ConfigureAwait(false);
}
#region IDisposable, IAsyncDisposable

public void Dispose()
Expand Down
4 changes: 3 additions & 1 deletion src/Domain.Core/Entities/ApplicationUser.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using GaEpd.AppLibrary.Extensions;
using GaEpd.AppLibrary.Extensions;
using Microsoft.AspNetCore.Identity;
using System.Text;

Expand Down Expand Up @@ -37,6 +37,8 @@ public sealed class ApplicationUser : IdentityUser, IEntity<string>
public string SortableFullName => new[] { FamilyName, GivenName }.ConcatWithSeparator(", ");
public string FullName => new[] { GivenName, FamilyName }.ConcatWithSeparator();

public UserPreferences Preferences { get; set; } = new();

public string SortableNameWithInactive
{
get
Expand Down
16 changes: 16 additions & 0 deletions src/Domain.Core/Entities/UserPreferences.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace AirWeb.Domain.Core.Entities;

public class UserPreferences
{
public ThemePreference Theme { get; set; } = ThemePreference.Dark;
}
public enum ThemePreference
{
Auto = 0,
Light = 1,
Dark = 2,
}
4 changes: 2 additions & 2 deletions src/WebApp/Pages/Account/Edit.cshtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@page
@page
@using AirWeb.WebApp.Pages.Shared.EditorTemplates
@model EditModel
@{
Expand All @@ -24,7 +24,7 @@
@Html.EditorFor(m => m.UpdateStaff.OfficeId, EditorTemplate.Select, additionalViewData: new { Items = Model.OfficeSelectList })
</div>
</div>

<partial name="Shared/_SubmitCancelButtons" />
</form>

</div>
45 changes: 45 additions & 0 deletions src/WebApp/Pages/Account/Index.cshtml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@page
@using AirWeb.WebApp.Pages.Shared.DisplayTemplates
@using AirWeb.Domain.Core.Entities
@model AccountIndexModel
@{
ViewData["Title"] = "Account Profile";
Expand Down Expand Up @@ -27,6 +28,50 @@
<dt class="col-sm-3">Office</dt>
<dd class="col-sm-9">@Html.DisplayFor(m => m.OfficeName, DisplayTemplate.TextOrPlaceholder,
additionalViewData: new { Placeholder = DefaultText.PlaceholderNone })</dd>

<dt class="col-sm-3">Theme</dt>
<dd class="col-sm-9">
<form method="post" id="themeForm">

<div class="btn-group" role="group" aria-label="Theme selection">

Check warning on line 36 in src/WebApp/Pages/Account/Index.cshtml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use <address> or <details> or <fieldset> or <optgroup> instead of the group role to ensure accessibility across all devices.

See more on https://sonarcloud.io/project/issues?id=gaepdit_air-web&issues=AaAfoAdxW524RbarhK_O&open=AaAfoAdxW524RbarhK_O&pullRequest=684

<input class="btn-check"
type="radio"
name="theme"
id="themeLight"
value="light"
checked="@(Model.Preferences.Theme == ThemePreference.Light)" />

<label class="btn btn-outline-secondary" for="themeLight">
Light
</label>

<input class="btn-check"
type="radio"
name="theme"
id="themeDark"
value="dark"
checked="@(Model.Preferences.Theme == ThemePreference.Dark)" />

<label class="btn btn-outline-secondary" for="themeDark">
Dark
</label>

<input class="btn-check"
type="radio"
name="theme"
id="themeAuto"
value="auto"
checked="@(Model.Preferences.Theme == ThemePreference.Auto)" />

<label class="btn btn-outline-secondary" for="themeAuto">
Auto
</label>

</div>

</form>
</dd>
</dl>

<div class="mb-3">
Expand Down
11 changes: 10 additions & 1 deletion src/WebApp/Pages/Account/Index.cshtml.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
using AirWeb.AppServices.Core.AuthorizationServices;
using AirWeb.AppServices.Core.AuthorizationServices;
using AirWeb.AppServices.Core.EntityServices.Staff;
using AirWeb.AppServices.Core.EntityServices.Staff.Dto;
using AirWeb.Domain.Core.AppRoles;
using AirWeb.Domain.Core.Entities;
using AirWeb.WebApp.Models;
using DocumentFormat.OpenXml.Presentation;

namespace AirWeb.WebApp.Pages.Account;

[Authorize(Policy = nameof(Policies.ActiveUser))]
public class AccountIndexModel : PageModel
{
public StaffViewDto DisplayStaff { get; private set; } = null!;
public UserPreferences Preferences { get; private set; } = null!;
public string? OfficeName => DisplayStaff.Office?.Name;
public IReadOnlyList<AppRole> Roles { get; private set; } = null!;

public async Task<IActionResult> OnGetAsync([FromServices] IStaffService staffService)
{
DisplayStaff = await staffService.GetCurrentUserAsync();
Roles = await staffService.GetAppRolesAsync(DisplayStaff.Id);
Preferences = await staffService.GetPreferencesAsync();

if (DisplayStaff.Office is null)
TempData.AddDisplayMessage(DisplayMessage.AlertContext.Warning,
message: "Your Office must be set. Select “Edit Profile” and choose an Office from the list.");

return Page();
}
public async Task<IActionResult> OnPostAsync(ThemePreference theme, [FromServices] IStaffService staffservice)
{
await staffservice.UpdateThemePreferenceAsync(theme);
return RedirectToPage();
}
}
40 changes: 1 addition & 39 deletions src/WebApp/Pages/Shared/Components/MainMenu/Default.cshtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@model MainMenuViewComponent.MenuParams
@model MainMenuViewComponent.MenuParams

<header role="banner">
<nav id="main-nav" aria-label="Main site navigation"
Expand Down Expand Up @@ -130,44 +130,6 @@
<a class="nav-link text-body-secondary" asp-page="/Account/Login">Agency&nbsp;Login</a>
</li>
}
<!-- Dropdown selector for color mode -->
<li class="nav-item dropdown">
<button class="nav-link dropdown-toggle" id="bd-theme" type="button"
aria-haspopup="menu" aria-expanded="false" data-bs-toggle="dropdown" data-bs-display="static" title="Toggle theme">
<partial name="Shared/_AppIconsPartial" model="@AppIcon.AutoDarkMode" view-data="new ViewDataDictionary(ViewData)"
view-data-CssClass="@("me-1 theme-icon-active")" />
<span class="visually-hidden" id="bd-theme-text">Toggle theme</span>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme-text">
<li>
<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="light">
<partial name="Shared/_AppIconsPartial" model="@AppIcon.LightMode" view-data="new ViewDataDictionary(ViewData)"
view-data-CssClass="@("me-2 opacity-50")" />
Light
<partial name="Shared/_AppIconsPartial" model="@AppIcon.CheckMark" view-data="new ViewDataDictionary(ViewData)"
view-data-CssClass="@("ms-auto theme-checkmark d-none")" />
</button>
</li>
<li>
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark">
<partial name="Shared/_AppIconsPartial" model="@AppIcon.DarkMode" view-data="new ViewDataDictionary(ViewData)"
view-data-CssClass="@("me-2 opacity-50")" />
Dark
<partial name="Shared/_AppIconsPartial" model="@AppIcon.CheckMark" view-data="new ViewDataDictionary(ViewData)"
view-data-CssClass="@("ms-auto theme-checkmark d-none")" />
</button>
</li>
<li>
<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto">
<partial name="Shared/_AppIconsPartial" model="@AppIcon.AutoDarkMode" view-data="new ViewDataDictionary(ViewData)"
view-data-CssClass="@("me-2 opacity-50")" />
Auto
<partial name="Shared/_AppIconsPartial" model="@AppIcon.CheckMark" view-data="new ViewDataDictionary(ViewData)"
view-data-CssClass="@("ms-auto theme-checkmark d-none")" />
</button>
</li>
</ul>
</li>
</ul>
</div>
</div>
Expand Down
10 changes: 9 additions & 1 deletion src/WebApp/Pages/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
@inject AirWeb.AppServices.Core.EntityServices.Staff.IStaffService StaffService
@{
var displayMessages = TempData.GetDisplayMessages();
var themePreference = "auto";
if (User.Identity?.IsAuthenticated == true)
{
var preferences = await StaffService.GetPreferencesAsync();
themePreference = preferences.Theme.ToString().ToLower();
}
}
<!DOCTYPE html>
<html lang="en">
<html lang="en"
data-theme-preference="@themePreference">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
Expand Down
66 changes: 12 additions & 54 deletions src/WebApp/wwwroot/js/color-modes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*!
/*!
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
Expand All @@ -7,76 +7,34 @@
(() => {
'use strict'

const getStoredTheme = () => localStorage.getItem('theme')
const setStoredTheme = theme => localStorage.setItem('theme', theme)

const getPreferredTheme = () => {
const storedTheme = getStoredTheme()
if (storedTheme) {
return storedTheme
}

return globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
return document.documentElement.dataset.themePreference ?? 'auto';
}

const setTheme = theme => {
if (theme === 'auto') {
document.documentElement.dataset.bsTheme = globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
document.documentElement.dataset.bsTheme =
globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
} else {
document.documentElement.dataset.bsTheme = theme
}
}

setTheme(getPreferredTheme())

const showActiveTheme = (theme, focus = false) => {
const themeSwitcher = document.querySelector('#bd-theme')

if (!themeSwitcher) {
return
}

const themeSwitcherText = document.querySelector('#bd-theme-text')
const activeThemeIcon = document.querySelector('.theme-icon-active use')
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')

document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.classList.remove('active')
element.setAttribute('aria-pressed', 'false')
element.querySelector('.theme-checkmark').classList.add('d-none')
})

btnToActive.classList.add('active')
btnToActive.setAttribute('aria-pressed', 'true')
btnToActive.querySelector('.theme-checkmark').classList.remove('d-none')
activeThemeIcon.setAttribute('href', svgOfActiveBtn)
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
themeSwitcher.setAttribute('title', themeSwitcherLabel)

if (focus) {
themeSwitcher.focus()
}
}

globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = getStoredTheme()
if (storedTheme !== 'light' && storedTheme !== 'dark') {
setTheme(getPreferredTheme())
if (getPreferredTheme() === 'auto') {
setTheme('auto')
}
})

globalThis.addEventListener('DOMContentLoaded', () => {
showActiveTheme(getPreferredTheme())

document.querySelectorAll('[data-bs-theme-value]')
.forEach(toggle => {
toggle.addEventListener('click', () => {
const theme = toggle.dataset.bsThemeValue
setStoredTheme(theme)
setTheme(theme)
showActiveTheme(theme, true)
document.querySelectorAll('input[name="theme"]')
.forEach(radio => {
radio.addEventListener('change', () => {
const theme = radio.value;
setTheme(theme);
document.querySelector('#themeForm').submit();
})
})
})
Expand Down
Loading