mirror of
https://github.com/CannedNet/CannedNet.git
synced 2026-10-27 07:58:59 -07:00
basic admin panel stuff
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
@page "/config/communityboard"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Community Board</PageTitle>
|
||||
|
||||
<h3>Community Board</h3>
|
||||
<p class="text-muted">Edit the community board content including featured players, announcements, Instagram images, and videos.</p>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<div class="spinner-border" role="status"></div>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
<div class="alert alert-danger">@error</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Current Announcement</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Message</label>
|
||||
<input type="text" class="form-control" @bind="announcementMessage" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">More Info URL</label>
|
||||
<input type="text" class="form-control" @bind="announcementUrl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Featured Player</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Player ID</label>
|
||||
<input type="number" class="form-control" @bind="featuredPlayerId" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Title Override</label>
|
||||
<input type="text" class="form-control" @bind="featuredPlayerTitle" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">URL Override</label>
|
||||
<input type="text" class="form-control" @bind="featuredPlayerUrl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">Videos</div>
|
||||
<div class="card-body">
|
||||
@if (videos.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No videos.</p>
|
||||
}
|
||||
@for (int i = 0; i < videos.Count; i++)
|
||||
{
|
||||
var idx = i;
|
||||
<div class="border rounded p-3 mb-2">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" @bind="videos[idx].Title" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" rows="2" @bind="videos[idx].Description"></textarea>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Source URL</label>
|
||||
<input type="text" class="form-control" @bind="videos[idx].SourceUrl" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Blob Name</label>
|
||||
<input type="text" class="form-control" @bind="videos[idx].BlobName" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Thumbnail Blob</label>
|
||||
<input type="text" class="form-control" @bind="videos[idx].ThumbnailBlobName" />
|
||||
</div>
|
||||
<button type="button" class="btn btn-danger btn-sm" @onclick="() => videos.RemoveAt(idx)">Remove</button>
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" @onclick="() => videos.Add(new VideoEntry())">Add Video</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<button type="submit" class="btn btn-success" @onclick="SaveFromForm" disabled="@saving">
|
||||
@(saving ? "Saving..." : "Save Changes")
|
||||
</button>
|
||||
@if (!string.IsNullOrEmpty(saveMessage))
|
||||
{
|
||||
<span class="ms-2 @(saveMessage.Contains("Error") ? "text-danger" : "text-success")">@saveMessage</span>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@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<VideoEntry> 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<VideoEntry>();
|
||||
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<object>()
|
||||
},
|
||||
currentAnnouncement = new
|
||||
{
|
||||
message = announcementMessage,
|
||||
moreInfoUrl = announcementUrl
|
||||
},
|
||||
instagramImages = new List<object>(),
|
||||
videos = videos.Select(v => new
|
||||
{
|
||||
blobName = v.BlobName,
|
||||
title = v.Title,
|
||||
description = v.Description,
|
||||
thumbnailBlobName = v.ThumbnailBlobName,
|
||||
sourceUrl = v.SourceUrl
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var content = new StringContent(
|
||||
System.Text.Json.JsonSerializer.Serialize(payload, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }),
|
||||
System.Text.Encoding.UTF8, "application/json");
|
||||
var response = await Http.PutAsync("/admin/api/config/communityboard", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
saveMessage = "Saved successfully!";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
saveMessage = $"Error: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private class VideoEntry
|
||||
{
|
||||
public string Title { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public string SourceUrl { get; set; } = "";
|
||||
public string BlobName { get; set; } = "";
|
||||
public string ThumbnailBlobName { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
@page "/config/gameconfig"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Game Config</PageTitle>
|
||||
|
||||
<h3>Game Config</h3>
|
||||
<p class="text-muted">Edit the gameconfigs.json file that controls various game settings.</p>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<div class="spinner-border" role="status"></div>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
<div class="alert alert-danger">@error</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @GetActiveClass("edit")" href="#" @onclick='() => SetTab("edit")' @onclick:preventDefault="true">Raw Editor</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @GetActiveClass("browse")" href="#" @onclick='() => SetTab("browse")' @onclick:preventDefault="true">Browse</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@if (activeTab == "edit")
|
||||
{
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<button type="submit" class="btn btn-success" @onclick="Save" disabled="@saving">
|
||||
@(saving ? "Saving..." : "Save Changes")
|
||||
</button>
|
||||
@if (!string.IsNullOrEmpty(saveMessage))
|
||||
{
|
||||
<span class="ms-2 @(saveMessage.Contains("Error") ? "text-danger" : "text-success")">@saveMessage</span>
|
||||
}
|
||||
</div>
|
||||
<textarea class="form-control font-monospace" rows="30" @bind="jsonContent"></textarea>
|
||||
</form>
|
||||
}
|
||||
else if (activeTab == "browse")
|
||||
{
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control" placeholder="Filter keys..." @bind="filterText" @bind:event="oninput" />
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr><th>Key</th><th>Value</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var entry in configEntries.Where(e => string.IsNullOrEmpty(filterText) || e.Key.Contains(filterText, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
<tr>
|
||||
<td class="font-monospace small">@entry.Key</td>
|
||||
<td class="small" style="max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="@entry.Value">@entry.Value</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-muted small">@configEntries.Count(e => string.IsNullOrEmpty(filterText) || e.Key.Contains(filterText, StringComparison.OrdinalIgnoreCase)) entries</div>
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
[Inject] private HttpClient Http { get; set; } = default!;
|
||||
|
||||
private bool loading = true;
|
||||
private bool saving;
|
||||
private string? error;
|
||||
private string? saveMessage;
|
||||
private string jsonContent = "";
|
||||
private string activeTab = "browse";
|
||||
private string filterText = "";
|
||||
private List<ConfigEntry> configEntries = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
jsonContent = await Http.GetStringAsync("/admin/api/config/gameconfigs");
|
||||
configEntries = JsonSerializer.Deserialize<List<ConfigEntry>>(jsonContent) ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = $"Failed to load: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetActiveClass(string tab) => activeTab == tab ? "active" : "";
|
||||
|
||||
private void SetTab(string tab)
|
||||
{
|
||||
activeTab = tab;
|
||||
}
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
saving = true;
|
||||
saveMessage = null;
|
||||
try
|
||||
{
|
||||
var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
|
||||
var response = await Http.PutAsync("/admin/api/config/gameconfigs", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
saveMessage = "Saved successfully!";
|
||||
configEntries = JsonSerializer.Deserialize<List<ConfigEntry>>(jsonContent) ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
saveMessage = $"Error: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfigEntry
|
||||
{
|
||||
public string Key { get; set; } = "";
|
||||
public string Value { get; set; } = "";
|
||||
public string? EndTime { get; set; }
|
||||
public string? StartTime { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
@page "/config/nameserver"
|
||||
@rendermode InteractiveServer
|
||||
@using CannedNet.Admin.Models
|
||||
@using System.Text.Json
|
||||
|
||||
<PageTitle>NS Config</PageTitle>
|
||||
|
||||
<h3>Nameserver (NS) Config</h3>
|
||||
<p class="text-muted">Manage the nameserver endpoints.json file that controls where the game client connects.</p>
|
||||
|
||||
<div class="alert alert-info">
|
||||
This is the same editor as the <a href="setup/nameserver">Setup > Nameserver</a> page.
|
||||
All endpoint URLs are derived from a single base URL.
|
||||
</div>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<div class="spinner-border" role="status"></div>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
<div class="alert alert-danger">@error</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<button type="submit" class="btn btn-success" @onclick="Save" disabled="@saving">
|
||||
@(saving ? "Saving..." : "Save Changes")
|
||||
</button>
|
||||
@if (!string.IsNullOrEmpty(saveMessage))
|
||||
{
|
||||
<span class="@(saveError ? "text-danger" : "text-success") ms-2">@saveMessage</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="baseUrl">Base URL:</label>
|
||||
<input id="baseUrl" class="form-control font-monospace" @bind="baseUrl" placeholder="https://cannednet.lapis.codes" />
|
||||
<div class="form-text">All endpoint URLs will use this as the base.</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service</th>
|
||||
<th>Full URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var (name, path) in EndpointDefinitions.Endpoints)
|
||||
{
|
||||
<tr>
|
||||
<td><code>@name</code></td>
|
||||
<td><code>@(baseUrl.TrimEnd('/') + path)</code></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Inject] private HttpClient Http { get; set; } = default!;
|
||||
|
||||
private bool loading = true;
|
||||
private bool saving;
|
||||
private bool saveError;
|
||||
private string? error;
|
||||
private string? saveMessage;
|
||||
private string baseUrl = "";
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await Http.GetStringAsync("/admin/api/endpoints");
|
||||
baseUrl = EndpointDefinitions.ExtractBaseUrl(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = $"Failed to load: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Save()
|
||||
{
|
||||
saving = true;
|
||||
saveMessage = null;
|
||||
saveError = false;
|
||||
try
|
||||
{
|
||||
var json = EndpointDefinitions.BuildEndpointsJson(baseUrl);
|
||||
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
|
||||
var response = await Http.PutAsync("/admin/api/endpoints", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
saveMessage = "Saved successfully!";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
saveMessage = $"Error: {ex.Message}";
|
||||
saveError = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user