basic admin panel stuff

This commit is contained in:
Lapis
2026-06-17 13:39:28 -04:00
parent c20d215ee0
commit 004f1921a6
44 changed files with 3432 additions and 315 deletions
@@ -0,0 +1,176 @@
@page "/rooms/create"
@rendermode InteractiveServer
<PageTitle>Create Room</PageTitle>
<h3>Create Room</h3>
<p class="text-muted">Create a new room and optionally a default subroom.</p>
<form>
<div class="row">
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header">Room Details</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Room ID (leave 0 for auto)</label>
<input type="number" class="form-control" @bind="roomId" />
</div>
<div class="mb-3">
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" @bind="name" required />
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea class="form-control" rows="3" @bind="description"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Creator Account ID</label>
<input type="number" class="form-control" @bind="creatorAccountId" />
</div>
<div class="mb-3">
<label class="form-label">Image Name</label>
<input type="text" class="form-control" @bind="imageName" />
</div>
<div class="mb-3">
<label class="form-label">Accessibility</label>
<input type="number" class="form-control" @bind="accessibility" />
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header">SubRoom</div>
<div class="card-body">
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" @bind="createSubRoom" />
<label class="form-check-label">Create a default subroom</label>
</div>
@if (createSubRoom)
{
<div class="mb-3">
<label class="form-label">SubRoom Name</label>
<input type="text" class="form-control" @bind="subRoomName" />
</div>
<div class="mb-3">
<label class="form-label">Max Players</label>
<input type="number" class="form-control" @bind="maxPlayers" />
</div>
}
</div>
</div>
<div class="card mb-3">
<div class="card-header">Platform Support</div>
<div class="card-body">
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="supportsVRLow" />
<label class="form-check-label">VR Low</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="supportsQuest2" />
<label class="form-check-label">Quest 2</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="supportsMobile" />
<label class="form-check-label">Mobile</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="supportsScreens" />
<label class="form-check-label">Screens</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="supportsWalkVR" />
<label class="form-check-label">Walk VR</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="supportsTeleportVR" />
<label class="form-check-label">Teleport VR</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="supportsJuniors" />
<label class="form-check-label">Supports Juniors</label>
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" @onclick="CreateRoom" disabled="@creating">
@(creating ? "Creating..." : "Create Room")
</button>
<a href="rooms/search" class="btn btn-outline-secondary ms-2">Cancel</a>
@if (!string.IsNullOrEmpty(message))
{
<div class="alert mt-3 @(message.Contains("Error") ? "alert-danger" : "alert-success")">@message</div>
}
@if (createdRoom != null)
{
<div class="alert alert-success mt-2">
Room created! <a href="rooms/edit/@createdRoom.RoomId">Edit Room #@createdRoom.RoomId</a>
</div>
}
</form>
@code {
[Inject] private HttpClient Http { get; set; } = default!;
private bool creating;
private string? message;
private Room? createdRoom;
private int roomId;
private string name = "";
private string description = "";
private int creatorAccountId;
private string imageName = "";
private int accessibility;
private bool createSubRoom = true;
private string subRoomName = "";
private int maxPlayers = 16;
private bool supportsVRLow = true;
private bool supportsQuest2 = true;
private bool supportsMobile = true;
private bool supportsScreens = true;
private bool supportsWalkVR = true;
private bool supportsTeleportVR = true;
private bool supportsJuniors = true;
private async Task CreateRoom()
{
creating = true;
message = null;
try
{
var payload = new
{
roomId = roomId > 0 ? roomId : 0,
name,
description,
creatorAccountId,
imageName,
accessibility,
createSubRoom,
subRoomName = createSubRoom ? (string.IsNullOrWhiteSpace(subRoomName) ? name : subRoomName) : "",
maxPlayers
};
var response = await Http.PostAsJsonAsync("/admin/api/rooms", payload);
response.EnsureSuccessStatusCode();
createdRoom = await response.Content.ReadFromJsonAsync<Room>();
message = $"Room '{createdRoom?.Name}' created successfully!";
}
catch (Exception ex)
{
message = $"Error: {ex.Message}";
}
finally
{
creating = false;
}
}
}
@@ -0,0 +1,358 @@
@page "/rooms/edit/{RoomId:int}"
@rendermode InteractiveServer
@inject ImageUrlService Img
<PageTitle>Edit Room @RoomId</PageTitle>
<h3>Edit Room #@RoomId</h3>
@if (loading)
{
<div class="spinner-border" role="status"></div>
}
else if (!string.IsNullOrEmpty(error))
{
<div class="alert alert-danger">@error</div>
}
else if (roomData == null)
{
<div class="alert alert-warning">Room not found.</div>
}
else
{
<div class="row">
<div class="col-md-12">
<form>
<div class="row">
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header">Room Info</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Room ID</label>
<input type="number" class="form-control" @bind="editRoomId" />
</div>
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" @bind="editName" />
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea class="form-control" rows="3" @bind="editDescription"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Image Name</label>
<input type="text" class="form-control" @bind="editImageName" />
@if (!string.IsNullOrEmpty(editImageName))
{
<img src="@Img.GetUrl(editImageName)" alt="preview" class="mt-2 img-thumbnail" style="max-width: 200px;" onerror="this.style.display='none'" />
}
</div>
<div class="mb-3">
<label class="form-label">Creator Account ID</label>
<input type="number" class="form-control" @bind="editCreatorAccountId" />
</div>
<div class="mb-3">
<label class="form-label">Tags (JSON)</label>
<textarea class="form-control font-monospace" rows="3" @bind="editTags"></textarea>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-header">Settings</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">State</label>
<input type="number" class="form-control" @bind="editState" />
</div>
<div class="mb-3">
<label class="form-label">Accessibility</label>
<input type="number" class="form-control" @bind="editAccessibility" />
</div>
<div class="mb-3">
<label class="form-label">Min Level</label>
<input type="number" class="form-control" @bind="editMinLevel" />
</div>
<div class="mb-3">
<label class="form-label">Warning Mask</label>
<input type="number" class="form-control" @bind="editWarningMask" />
</div>
<div class="mb-3">
<label class="form-label">Custom Warning</label>
<input type="text" class="form-control" @bind="editCustomWarning" />
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header">Flags</div>
<div class="card-body">
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="editIsDorm" />
<label class="form-check-label">Is Dorm</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="editIsRRO" />
<label class="form-check-label">Is RRO</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="editCloningAllowed" />
<label class="form-check-label">Cloning Allowed</label>
</div>
<hr />
<h6>Platform Support</h6>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="editSupportsVRLow" />
<label class="form-check-label">VR Low</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="editSupportsQuest2" />
<label class="form-check-label">Quest 2</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="editSupportsMobile" />
<label class="form-check-label">Mobile</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="editSupportsScreens" />
<label class="form-check-label">Screens</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="editSupportsWalkVR" />
<label class="form-check-label">Walk VR</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="editSupportsTeleportVR" />
<label class="form-check-label">Teleport VR</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" @bind="editSupportsJuniors" />
<label class="form-check-label">Supports Juniors</label>
</div>
</div>
</div>
</div>
</div>
@if (roomData.SubRooms?.Count > 0)
{
<div class="card mb-3">
<div class="card-header">SubRooms (@roomData.SubRooms.Count)</div>
<div class="card-body">
<table class="table table-sm">
<thead>
<tr><th>ID</th><th>SubRoomId</th><th>Name</th><th>Max Players</th><th>Is Sandbox</th><th>Accessibility</th></tr>
</thead>
<tbody>
@foreach (var sub in roomData.SubRooms)
{
<tr>
<td>@sub.Id</td>
<td>@sub.SubRoomId</td>
<td>@sub.Name</td>
<td>@sub.MaxPlayers</td>
<td>@sub.IsSandbox</td>
<td>@sub.Accessibility</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
@if (roomData.LoadScreens?.Count > 0)
{
<div class="card mb-3">
<div class="card-header">Load Screens (@roomData.LoadScreens.Count)</div>
<div class="card-body">
@foreach (var ls in roomData.LoadScreens)
{
<div>@ls.ImageUrl - @ls.Tooltip</div>
}
</div>
</div>
}
<div class="d-flex gap-2 mb-3">
<button type="submit" class="btn btn-success" @onclick="SaveChanges" disabled="@saving">
@(saving ? "Saving..." : "Save Changes")
</button>
<button type="button" class="btn btn-danger" @onclick="DeleteRoom" disabled="@deleting">
@(deleting ? "Deleting..." : "Delete Room")
</button>
<a href="rooms/search" class="btn btn-outline-secondary">Back to Search</a>
</div>
@if (!string.IsNullOrEmpty(saveMessage))
{
<div class="alert mt-2 @(saveMessage.Contains("Error") || saveMessage.Contains("Failed") ? "alert-danger" : "alert-success")">@saveMessage</div>
}
</form>
</div>
</div>
}
@code {
[Parameter] public int RoomId { get; set; }
private bool loading = true;
private bool saving;
private bool deleting;
private string? error;
private string? saveMessage;
private RoomDetails? roomData;
private int editRoomId;
private string editName = "";
private string editDescription = "";
private string editImageName = "";
private int editCreatorAccountId;
private string editTags = "[]";
private int editState;
private int editAccessibility;
private int editMinLevel;
private int editWarningMask;
private string? editCustomWarning;
private bool editIsDorm;
private bool editIsRRO;
private bool editCloningAllowed;
private bool editSupportsVRLow;
private bool editSupportsQuest2;
private bool editSupportsMobile;
private bool editSupportsScreens;
private bool editSupportsWalkVR;
private bool editSupportsTeleportVR;
private bool editSupportsJuniors;
protected override async Task OnInitializedAsync()
{
await LoadRoom();
}
private async Task LoadRoom()
{
loading = true;
error = null;
try
{
roomData = await Http.GetFromJsonAsync<RoomDetails>($"/admin/api/rooms/{RoomId}");
if (roomData?.Room != null)
{
var r = roomData.Room;
editRoomId = r.RoomId;
editName = r.Name;
editDescription = r.Description;
editImageName = r.ImageName ?? "";
editCreatorAccountId = r.CreatorAccountId;
editTags = r.Tags ?? "[]";
editState = r.State;
editAccessibility = r.Accessibility;
editMinLevel = r.MinLevel;
editWarningMask = r.WarningMask;
editCustomWarning = r.CustomWarning;
editIsDorm = r.IsDorm;
editIsRRO = r.IsRRO;
editCloningAllowed = r.CloningAllowed;
editSupportsVRLow = r.SupportsVRLow;
editSupportsQuest2 = r.SupportsQuest2;
editSupportsMobile = r.SupportsMobile;
editSupportsScreens = r.SupportsScreens;
editSupportsWalkVR = r.SupportsWalkVR;
editSupportsTeleportVR = r.SupportsTeleportVR;
editSupportsJuniors = r.SupportsJuniors;
}
}
catch (Exception ex)
{
error = $"Failed to load room: {ex.Message}";
}
finally
{
loading = false;
}
}
private async Task SaveChanges()
{
saving = true;
saveMessage = null;
try
{
var payload = new
{
name = editName,
description = editDescription,
imageName = editImageName,
state = editState,
accessibility = editAccessibility,
isDorm = editIsDorm,
isRRO = editIsRRO,
cloningAllowed = editCloningAllowed,
minLevel = editMinLevel,
warningMask = editWarningMask,
customWarning = editCustomWarning,
supportsVRLow = editSupportsVRLow,
supportsQuest2 = editSupportsQuest2,
supportsMobile = editSupportsMobile,
supportsScreens = editSupportsScreens,
supportsWalkVR = editSupportsWalkVR,
supportsTeleportVR = editSupportsTeleportVR,
supportsJuniors = editSupportsJuniors,
tags = editTags
};
var response = await Http.PutAsJsonAsync($"/admin/api/rooms/{RoomId}", payload);
response.EnsureSuccessStatusCode();
saveMessage = "Room updated successfully!";
}
catch (Exception ex)
{
saveMessage = $"Error: {ex.Message}";
}
finally
{
saving = false;
}
}
private async Task DeleteRoom()
{
if (!await ConfirmDelete()) return;
deleting = true;
saveMessage = null;
try
{
var response = await Http.DeleteAsync($"/admin/api/rooms/{RoomId}");
response.EnsureSuccessStatusCode();
saveMessage = "Room deleted successfully!";
}
catch (Exception ex)
{
saveMessage = $"Error: {ex.Message}";
}
finally
{
deleting = false;
}
}
private async Task<bool> ConfirmDelete()
{
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm", $"Are you sure you want to delete room #{RoomId} and all its subrooms, load screens, promotions, and roles? This cannot be undone.");
return confirmed;
}
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
[Inject] private HttpClient Http { get; set; } = default!;
private class RoomDetails
{
public Room? Room { get; set; }
public List<SubRoom>? SubRooms { get; set; }
public List<LoadScreen>? LoadScreens { get; set; }
}
}
@@ -0,0 +1,96 @@
@page "/rooms/search"
@rendermode InteractiveServer
@inject ImageUrlService Img
<PageTitle>Search Rooms</PageTitle>
<h3>Search Rooms</h3>
<p class="text-muted">Search for rooms by name, description, or room ID.</p>
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Search by name, description, or room ID..." @bind="searchQuery" @bind:event="oninput" />
<button class="btn btn-primary" @onclick="DoSearch" disabled="@(string.IsNullOrWhiteSpace(searchQuery) || searching)">
@(searching ? "Searching..." : "Search")
</button>
</div>
@if (searching)
{
<div class="spinner-border" role="status"></div>
}
else if (!string.IsNullOrEmpty(error))
{
<div class="alert alert-danger">@error</div>
}
else if (results != null && results.Count == 0)
{
<div class="alert alert-info">No rooms found matching "@searchQuery".</div>
}
else if (results != null)
{
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Room ID</th>
<th>Image</th>
<th>Name</th>
<th>Creator</th>
<th>State</th>
<th>Dorm</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var room in results)
{
<tr>
<td>@room.RoomId</td>
<td>
@if (!string.IsNullOrEmpty(room.ImageName))
{
<img src="@Img.GetUrl(room.ImageName)" alt="room" style="width: 60px; height: 40px; object-fit: cover; border-radius: 4px;" onerror="this.style.display='none'" />
}
</td>
<td>@room.Name</td>
<td>@room.CreatorAccountId</td>
<td>@room.State</td>
<td>@(room.IsDorm ? "Yes" : "No")</td>
<td>
<a href="rooms/edit/@room.RoomId" class="btn btn-sm btn-outline-primary">Edit</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
@code {
[Inject] private HttpClient Http { get; set; } = default!;
private string searchQuery = "";
private bool searching;
private string? error;
private List<Room>? results;
private async Task DoSearch()
{
searching = true;
error = null;
results = null;
try
{
results = await Http.GetFromJsonAsync<List<Room>>($"/admin/api/rooms/search?q={Uri.EscapeDataString(searchQuery)}");
}
catch (Exception ex)
{
error = $"Search failed: {ex.Message}";
}
finally
{
searching = false;
}
}
}