An unhandled error has occurred.
diff --git a/CannedNet.Admin/Components/Layout/NavMenu.razor b/CannedNet.Admin/Components/Layout/NavMenu.razor
index c9f9e94..359e2fd 100644
--- a/CannedNet.Admin/Components/Layout/NavMenu.razor
+++ b/CannedNet.Admin/Components/Layout/NavMenu.razor
@@ -8,23 +8,69 @@
-
diff --git a/CannedNet.Admin/Components/Layout/NavMenu.razor.css b/CannedNet.Admin/Components/Layout/NavMenu.razor.css
index a2aeace..c0d9481 100644
--- a/CannedNet.Admin/Components/Layout/NavMenu.razor.css
+++ b/CannedNet.Admin/Components/Layout/NavMenu.razor.css
@@ -16,7 +16,7 @@
}
.top-row {
- min-height: 3.5rem;
+ height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
@@ -34,18 +34,6 @@
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;
@@ -59,27 +47,24 @@
padding-bottom: 1rem;
}
- .nav-item ::deep .nav-link {
+ .nav-item ::deep a {
color: #d7d7d7;
- background: none;
- border: none;
border-radius: 4px;
- height: 3rem;
+ height: 2.5rem;
display: flex;
align-items: center;
- line-height: 3rem;
- width: 100%;
+ line-height: 2.5rem;
}
-.nav-item ::deep a.active {
- background-color: rgba(255,255,255,0.37);
- color: white;
-}
+ .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-item ::deep a:hover {
+ background-color: rgba(255,255,255,0.1);
+ color: white;
+ }
.nav-scrollable {
display: none;
@@ -89,16 +74,19 @@
display: block;
}
+.nav-section-label {
+ font-size: 0.7rem;
+ letter-spacing: 0.05em;
+ opacity: 0.7;
+}
+
@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;
}
diff --git a/CannedNet.Admin/Components/Layout/RedirectToLogin.razor b/CannedNet.Admin/Components/Layout/RedirectToLogin.razor
new file mode 100644
index 0000000..f4b9f0a
--- /dev/null
+++ b/CannedNet.Admin/Components/Layout/RedirectToLogin.razor
@@ -0,0 +1,13 @@
+@using CannedNet.Admin.Services
+@inject AdminAuthService AuthService
+@inject NavigationManager Navigation
+
+@code {
+ protected override void OnInitialized()
+ {
+ if (!AuthService.HasAnyUser())
+ Navigation.NavigateTo("/setup", forceLoad: true);
+ else
+ Navigation.NavigateTo("/login", forceLoad: true);
+ }
+}
diff --git a/CannedNet.Admin/Components/Pages/ChangePassword.razor b/CannedNet.Admin/Components/Pages/ChangePassword.razor
new file mode 100644
index 0000000..c784994
--- /dev/null
+++ b/CannedNet.Admin/Components/Pages/ChangePassword.razor
@@ -0,0 +1,106 @@
+@page "/change-password"
+@attribute [Authorize]
+@rendermode InteractiveServer
+@using System.Security.Claims
+@using CannedNet.Admin.Services
+@inject AdminAuthService AuthService
+@inject NavigationManager Navigation
+@inject IJSRuntime JSRuntime
+
+
Change Password
+
+
Change Password
+
+
+
+@code {
+ [CascadingParameter] private Task
? AuthState { get; set; }
+
+ private string currentPassword = "";
+ private string newPassword = "";
+ private string confirmNewPassword = "";
+ private bool saving;
+ private string? message;
+
+ private async Task DoChangePassword()
+ {
+ saving = true;
+ message = null;
+
+ try
+ {
+ if (string.IsNullOrWhiteSpace(currentPassword))
+ {
+ message = "Current password is required.";
+ return;
+ }
+ if (string.IsNullOrWhiteSpace(newPassword) || newPassword.Length < 6)
+ {
+ message = "New password must be at least 6 characters.";
+ return;
+ }
+ if (newPassword != confirmNewPassword)
+ {
+ message = "New passwords do not match.";
+ return;
+ }
+
+ var state = await AuthState!;
+ var userIdClaim = state.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
+ if (userIdClaim == null || !int.TryParse(userIdClaim, out var userId))
+ {
+ message = "Error: Could not identify user.";
+ return;
+ }
+
+ if (!AuthService.ChangePassword(userId, currentPassword, newPassword))
+ {
+ message = "Error: Current password is incorrect.";
+ return;
+ }
+
+ message = "Password changed successfully!";
+ currentPassword = "";
+ newPassword = "";
+ confirmNewPassword = "";
+ }
+ catch (Exception ex)
+ {
+ message = $"Error: {ex.Message}";
+ }
+ finally
+ {
+ saving = false;
+ }
+ }
+}
diff --git a/CannedNet.Admin/Components/Pages/Config/CommunityBoard.razor b/CannedNet.Admin/Components/Pages/Config/CommunityBoard.razor
new file mode 100644
index 0000000..a72fce1
--- /dev/null
+++ b/CannedNet.Admin/Components/Pages/Config/CommunityBoard.razor
@@ -0,0 +1,227 @@
+@page "/config/communityboard"
+@rendermode InteractiveServer
+
+Community Board
+
+Community Board
+Edit the community board content including featured players, announcements, Instagram images, and videos.
+
+@if (loading)
+{
+
+}
+else if (!string.IsNullOrEmpty(error))
+{
+ @error
+}
+else
+{
+
+}
+
+@code {
+ [Inject] private HttpClient Http { get; set; } = default!;
+
+ private bool loading = true;
+ private bool saving;
+ private string? error;
+ private string? saveMessage;
+
+ private string announcementMessage = "";
+ private string announcementUrl = "";
+ private int featuredPlayerId;
+ private string? featuredPlayerTitle;
+ private string? featuredPlayerUrl;
+ private List videos = new();
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ var jsonContent = await Http.GetStringAsync("/admin/api/config/communityboard");
+
+ using var doc = JsonDocument.Parse(jsonContent);
+ var root = doc.RootElement;
+
+ if (root.TryGetProperty("currentAnnouncement", out var ann))
+ {
+ announcementMessage = ann.TryGetProperty("message", out var m) ? m.GetString() ?? "" : "";
+ announcementUrl = ann.TryGetProperty("moreInfoUrl", out var u) ? u.GetString() ?? "" : "";
+ }
+
+ if (root.TryGetProperty("featuredPlayer", out var fp))
+ {
+ featuredPlayerId = fp.TryGetProperty("id", out var id) ? id.GetInt32() : 0;
+ featuredPlayerTitle = fp.TryGetProperty("titleOverride", out var t) ? t.GetString() : null;
+ featuredPlayerUrl = fp.TryGetProperty("urlOverride", out var url) ? url.GetString() : null;
+ }
+
+ if (root.TryGetProperty("videos", out var vids))
+ {
+ videos = new List();
+ foreach (var v in vids.EnumerateArray())
+ {
+ videos.Add(new VideoEntry
+ {
+ Title = v.TryGetProperty("title", out var title) ? title.GetString() ?? "" : "",
+ Description = v.TryGetProperty("description", out var desc) ? desc.GetString() ?? "" : "",
+ SourceUrl = v.TryGetProperty("sourceUrl", out var src) ? src.GetString() ?? "" : "",
+ BlobName = v.TryGetProperty("blobName", out var blob) ? blob.GetString() ?? "" : "",
+ ThumbnailBlobName = v.TryGetProperty("thumbnailBlobName", out var thumb) ? thumb.GetString() ?? "" : ""
+ });
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ error = $"Failed to load: {ex.Message}";
+ }
+ finally
+ {
+ loading = false;
+ }
+ }
+
+ private async Task SaveFromForm()
+ {
+ saving = true;
+ saveMessage = null;
+ try
+ {
+ var payload = new
+ {
+ featuredPlayer = new
+ {
+ id = featuredPlayerId,
+ titleOverride = featuredPlayerTitle,
+ urlOverride = featuredPlayerUrl
+ },
+ featuredRoomGroup = new
+ {
+ featuredRoomGroupId = 2,
+ name = "Featured Rooms",
+ rooms = new List