diff --git a/testFrontend/SafeNSound.Demo/Pages/Admin.razor b/testFrontend/SafeNSound.Demo/Pages/Admin.razor
new file mode 100644
index 0000000..0dcb1a3
--- /dev/null
+++ b/testFrontend/SafeNSound.Demo/Pages/Admin.razor
@@ -0,0 +1,12 @@
+@page "/Admin"
+<h3>Admin</h3>
+<LinkButton OnClick="@(() => {
+ NavigationManager.NavigateTo("/Admin/Devices");
+ return Task.CompletedTask;
+ })">Manage devices
+</LinkButton>
+
+@code {
+
+
+}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.Demo/Pages/Auth.razor b/testFrontend/SafeNSound.Demo/Pages/Auth.razor
new file mode 100644
index 0000000..58fe6d6
--- /dev/null
+++ b/testFrontend/SafeNSound.Demo/Pages/Auth.razor
@@ -0,0 +1,239 @@
+@* @page "/Auth" *@
+@* *@
+@* <h1>Auth</h1> *@
+@* <u>User:</u><br/> *@
+@* <span>Username (L?, R): </span> *@
+@* <FancyTextBox @bind-Value="@AuthData.Username"/><br/> *@
+@* <span>Email (L? R): </span> *@
+@* <FancyTextBox @bind-Value="@AuthData.Email"/><br/> *@
+@* <span>Password (L, R): </span> *@
+@* <FancyTextBox @bind-Value="@AuthData.Password" IsPassword="true"/><br/> *@
+@* <span>Type (R): </span> *@
+@* <FancyTextBox @bind-Value="@AuthData.UserType"/><span> (one of user|monitor|admin)</span><br/> *@
+@* <LinkButton OnClick="@Randomise">Randomise</LinkButton> *@
+@* <LinkButton OnClick="@Register">Register</LinkButton> *@
+@* <LinkButton OnClick="@Login">Login</LinkButton> *@
+@* <LinkButton OnClick="@WhoAmI">Who Am I</LinkButton> *@
+@* <LinkButton OnClick="@Delete">Delete</LinkButton> *@
+@* <LinkButton OnClick="@MakeFullAdmin">Register superadmin</LinkButton> *@
+@* <br/><br/> *@
+@* *@
+@* <u>Monitor:</u><br/> *@
+@* <span>User ID: </span> *@
+@* <FancyTextBox @bind-Value="@TargetUserId"/><br/> *@
+@* <LinkButton OnClick="@GetAssignedUsers">Get</LinkButton> *@
+@* <LinkButton OnClick="@AddAssignedUser">Add</LinkButton> *@
+@* <LinkButton OnClick="@RemoveAssignedUser">Remove</LinkButton> *@
+@* <br/><br/> *@
+@* *@
+@* <u>Devices:</u><br/> *@
+@* @if (CurrentDevice is not null) { *@
+@* <span>Device ID: @CurrentDevice.Id</span><br/> *@
+@* <span>Log in date: @CurrentDevice.CreatedAt</span><br/> *@
+@* <span>Last seen: @CurrentDevice.LastSeen</span><br/> *@
+@* <span>Device name: </span> *@
+@* <FancyTextBox @bind-Value="@CurrentDevice.Name"/><br/> *@
+@* <LinkButton OnClick="@GetDevice">Get</LinkButton> *@
+@* <LinkButton OnClick="@UpdateDevice">Update</LinkButton> *@
+@* <LinkButton OnClick="@DeleteDevice">Delete</LinkButton> *@
+@* <LinkButton OnClick="@GetAllDevices">Get all</LinkButton> *@
+@* } *@
+@* *@
+@* @if (Exception != null) { *@
+@* <div class="alert alert-danger"> *@
+@* <strong>Error:</strong><br/> *@
+@* <pre> *@
+@* @Exception *@
+@* </pre> *@
+@* </div> *@
+@* } *@
+@* *@
+@* @if (Result != null) { *@
+@* <div class="alert alert-success"> *@
+@* <strong>Result:</strong><br/> *@
+@* <pre> *@
+@* @Result.ToJson(indent: true) *@
+@* </pre> *@
+@* </div> *@
+@* } *@
+@* *@
+@* @code { *@
+@* *@
+@* private RegisterDto AuthData { get; set; } = new() { *@
+@* Username = string.Empty, *@
+@* UserType = string.Empty, *@
+@* Email = String.Empty, *@
+@* Password = string.Empty *@
+@* }; *@
+@* *@
+@* private DeviceDto? CurrentDevice { get; set; } *@
+@* *@
+@* private string TargetUserId { get; set; } = string.Empty; *@
+@* *@
+@* private Exception? Exception { get; set; } *@
+@* private object? Result { get; set; } *@
+@* *@
+@* private async Task Randomise() { *@
+@* AuthData.Username = Guid.NewGuid().ToString(); *@
+@* AuthData.Email = Guid.NewGuid() + "@example.com"; *@
+@* AuthData.Password = Guid.NewGuid().ToString(); *@
+@* AuthData.UserType = Random.Shared.GetItems(["user", "monitor", "admin"], 1)[0]; *@
+@* StateHasChanged(); *@
+@* } *@
+@* *@
+@* private async Task Register() { *@
+@* Result = null; *@
+@* Exception = null; *@
+@* try { *@
+@* await Authentication.Register(new() { *@
+@* Username = AuthData.Username, *@
+@* Password = AuthData.Password, *@
+@* Email = AuthData.Email, *@
+@* UserType = AuthData.UserType *@
+@* }); *@
+@* } *@
+@* catch (Exception ex) { *@
+@* Exception = ex; *@
+@* } *@
+@* *@
+@* StateHasChanged(); *@
+@* } *@
+@* *@
+@* private async Task Login() { *@
+@* Result = null; *@
+@* Exception = null; *@
+@* try { *@
+@* AuthResult result; *@
+@* Result = result = await Authentication.Login(new() { *@
+@* Username = AuthData.Username, *@
+@* Password = AuthData.Password, *@
+@* Email = AuthData.Email *@
+@* }); *@
+@* App.Client = new SafeNSoundClient(Config, result.AccessToken); *@
+@* CurrentDevice = await App.Client.GetDevice( *@
+@* (await App.Client.WhoAmI()).DeviceId *@
+@* ); *@
+@* } *@
+@* catch (Exception ex) { *@
+@* Exception = ex; *@
+@* } *@
+@* *@
+@* StateHasChanged(); *@
+@* } *@
+@* *@
+@* private async Task Delete() { *@
+@* Result = null; *@
+@* Exception = null; *@
+@* try { *@
+@* await Authentication.Delete(new() { *@
+@* Username = AuthData.Username, *@
+@* Password = AuthData.Password, *@
+@* Email = AuthData.Email *@
+@* }); *@
+@* } *@
+@* catch (Exception ex) { *@
+@* Exception = ex; *@
+@* } *@
+@* *@
+@* StateHasChanged(); *@
+@* } *@
+@* *@
+@* private async Task WhoAmI() { *@
+@* Result = null; *@
+@* Exception = null; *@
+@* try { *@
+@* Result = await App.Client!.WhoAmI(); *@
+@* } *@
+@* catch (Exception ex) { *@
+@* Exception = ex; *@
+@* } *@
+@* *@
+@* StateHasChanged(); *@
+@* } *@
+@* *@
+@* private async Task GetAssignedUsers() { *@
+@* Result = null; *@
+@* Exception = null; *@
+@* try { *@
+@* Result = await App.Client!.GetAssignedUsers(); *@
+@* } *@
+@* catch (Exception ex) { *@
+@* Exception = ex; *@
+@* } *@
+@* *@
+@* StateHasChanged(); *@
+@* } *@
+@* *@
+@* private async Task AddAssignedUser() { *@
+@* Result = null; *@
+@* Exception = null; *@
+@* try { *@
+@* await App.Client!.AddAssignedUser(TargetUserId); *@
+@* await GetAssignedUsers(); *@
+@* } *@
+@* catch (Exception ex) { *@
+@* Exception = ex; *@
+@* } *@
+@* *@
+@* StateHasChanged(); *@
+@* } *@
+@* *@
+@* private async Task RemoveAssignedUser() { *@
+@* Result = null; *@
+@* Exception = null; *@
+@* try { *@
+@* await App.Client!.RemoveAssignedUser(TargetUserId); *@
+@* await GetAssignedUsers(); *@
+@* } *@
+@* catch (Exception ex) { *@
+@* Exception = ex; *@
+@* } *@
+@* *@
+@* StateHasChanged(); *@
+@* } *@
+@* *@
+@* private async Task MakeFullAdmin() { *@
+@* Result = null; *@
+@* Exception = null; *@
+@* try { *@
+@* AuthResult result; *@
+@* RegisterDto auth = new() { *@
+@* Username = Guid.NewGuid().ToString(), *@
+@* Password = Guid.NewGuid().ToString(), *@
+@* Email = Guid.NewGuid() + "@example.com", *@
+@* UserType = "admin" *@
+@* }; *@
+@* await Authentication.Register(auth); *@
+@* Result = result = await Authentication.Login(auth); *@
+@* App.Client = new SafeNSoundClient(Config, result.AccessToken); *@
+@* await App.Client.MonitorAllUsers(); *@
+@* } *@
+@* catch (Exception ex) { *@
+@* Exception = ex; *@
+@* } *@
+@* *@
+@* StateHasChanged(); *@
+@* } *@
+@* *@
+@* private async Task GetDevice() { *@
+@* Result = CurrentDevice = await App.Client!.GetDevice(CurrentDevice!.Id!); *@
+@* StateHasChanged(); *@
+@* } *@
+@* *@
+@* private async Task UpdateDevice() { *@
+@* await App.Client!.UpdateDevice(CurrentDevice!.Id!, new() { *@
+@* Name = CurrentDevice.Name *@
+@* }); *@
+@* await GetDevice(); *@
+@* } *@
+@* *@
+@* private async Task DeleteDevice() { *@
+@* await App.Client!.DeleteDevice(CurrentDevice!.Id!); *@
+@* } *@
+@* *@
+@* private async Task GetAllDevices() { *@
+@* Result = await App.Client!.GetDevices(); *@
+@* StateHasChanged(); *@
+@* } *@
+@* *@
+@* } *@
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.Demo/Pages/Devices.razor b/testFrontend/SafeNSound.Demo/Pages/Devices.razor
new file mode 100644
index 0000000..5f90132
--- /dev/null
+++ b/testFrontend/SafeNSound.Demo/Pages/Devices.razor
@@ -0,0 +1,81 @@
+@page "/{UserType}/Devices"
+
+@if (_isInitialized) {
+ <h3>Devices (@WhoAmI.UserId#@WhoAmI.DeviceId[..5])</h3>
+ <hr/>
+ <LinkButton OnClick="@CreateDevice">Create new device</LinkButton>
+ <LinkButton OnClick="@ReloadDevices">Reload Devices</LinkButton>
+ <LinkButton OnClick="@DeleteAccount">Delete account</LinkButton>
+ @foreach (var device in CurrentDevices) {
+ <div class="device-card">
+ <h4>ID: @device.Id
+ @if (device.Id == WhoAmI.DeviceId) {
+ <span> (current)</span>
+ }
+ </h4>
+ <span>Name: @device.Name</span><br/>
+ <span>First seen: @device.CreatedAt.ToLocalTime()</span><br/>
+ <span>Last seen: @device.LastSeen.ToLocalTime()</span><br/>
+ <LinkButton OnClick="@(() => DeleteDevice(device))">Log out</LinkButton>
+ </div>
+ }
+}
+
+@code {
+ private bool _isInitialized;
+
+ [Parameter]
+ public string UserType { get; set; } = string.Empty;
+
+ private SafeNSoundClient Client { get; set; } = null!;
+ private RegisterDto Auth { get; set; } = null!;
+ private WhoAmI WhoAmI { get; set; } = null!;
+ private List<DeviceDto> CurrentDevices { get; set; } = null!;
+
+ protected override async Task OnInitializedAsync() {
+ if (UserType is not "User" and not "Monitor" and not "Admin") {
+ NavigationManager.NavigateTo("/");
+ return;
+ }
+
+ Client = UserType switch {
+ "User" => App.UserClient,
+ "Monitor" => App.MonitorClient,
+ "Admin" => App.AdminClient,
+ _ => throw new InvalidOperationException("Invalid UserType")
+ };
+
+ Auth = UserType switch {
+ "User" => App.UserAuth,
+ "Monitor" => App.MonitorAuth,
+ "Admin" => App.AdminAuth,
+ _ => throw new InvalidOperationException("Invalid UserType")
+ };
+
+ WhoAmI = await Client.WhoAmI();
+
+ await ReloadDevices();
+
+ _isInitialized = true;
+ }
+
+ private async Task CreateDevice() {
+ await Authentication.Login(Auth);
+ await ReloadDevices();
+ }
+
+ private async Task DeleteDevice(DeviceDto device) {
+ await Client.DeleteDevice(device.Id!);
+ await ReloadDevices();
+ }
+
+ private async Task ReloadDevices() {
+ CurrentDevices = await Client.GetDevices();
+ StateHasChanged();
+ }
+
+ private async Task DeleteAccount() {
+ await Client.DeleteAccount(Auth);
+ }
+
+}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.Demo/Pages/Home.razor b/testFrontend/SafeNSound.Demo/Pages/Home.razor
new file mode 100644
index 0000000..ae35214
--- /dev/null
+++ b/testFrontend/SafeNSound.Demo/Pages/Home.razor
@@ -0,0 +1,50 @@
+@page "/"
+@using System.Text.Json.Nodes
+@implements IDisposable
+
+<PageTitle>Home</PageTitle>
+
+<h1>SafeNSound</h1>
+
+<p>Welcome to SafeNSound! This application is purely a demonstration of the basic concepts in SafeNSound!</p>
+<p>Here be dragons!</p>
+
+<pre>
+ @serverStatus?.ToJson(indent: true)
+</pre>
+
+@code {
+
+ private bool running = true;
+ private JsonObject? serverStatus { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ _ = PollServerStatus();
+ }
+
+ private async Task PollServerStatus() {
+ while (running) {
+ try {
+ serverStatus = await App.UserClient.HttpClient.GetFromJsonAsync<JsonObject>("/status");
+ }
+ catch (Exception ex) {
+ serverStatus = new JsonObject {
+ ["error"] = ex.Message
+ };
+ }
+ StateHasChanged();
+ await Task.Delay(1000);
+ }
+ }
+
+ private void ReleaseUnmanagedResources() {
+ running = false;
+ }
+
+ public void Dispose() {
+ ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ ~Home() => ReleaseUnmanagedResources();
+}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.Demo/Pages/Monitor.razor b/testFrontend/SafeNSound.Demo/Pages/Monitor.razor
new file mode 100644
index 0000000..d78d60a
--- /dev/null
+++ b/testFrontend/SafeNSound.Demo/Pages/Monitor.razor
@@ -0,0 +1,31 @@
+@page "/Monitor"
+@if (_isInitialized) {
+ <h3>User</h3>
+ <LinkButton OnClick="@(() => {
+ NavigationManager.NavigateTo("/User/Devices");
+ return Task.CompletedTask;
+ })">Manage devices
+ </LinkButton>
+
+ @foreach (var user in AssignedUsers) {
+ <p>Assigned user @user
+ @if (Alarms.ContainsKey(user)) {
+ <pre>@Alarms[user].ToJson(indent: false)</pre>
+ }
+ </p>
+ }
+}
+
+@code {
+
+ bool _isInitialized = false;
+ Dictionary<string, AlarmDto> Alarms { get; set; }
+ List<string> AssignedUsers { get; set; }
+
+ protected override async Task OnInitializedAsync() {
+ Alarms = await App.MonitorClient.GetAllAlarms();
+ AssignedUsers = await App.MonitorClient.GetAssignedUsers();
+ _isInitialized = true;
+ }
+
+}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.Demo/Pages/User.razor b/testFrontend/SafeNSound.Demo/Pages/User.razor
new file mode 100644
index 0000000..cdffbe3
--- /dev/null
+++ b/testFrontend/SafeNSound.Demo/Pages/User.razor
@@ -0,0 +1,83 @@
+@page "/User"
+@implements IDisposable
+@if (_isInitialized) {
+ <h3>User (@WhoAmI.UserId#@WhoAmI.DeviceId[..5])</h3>
+ <LinkButton OnClick="@(() => {
+ NavigationManager.NavigateTo("/User/Devices");
+ return Task.CompletedTask;
+ })">Manage devices
+ </LinkButton>
+ <hr/>
+
+ <span>
+ Raise alarm:
+ <LinkButton OnClick="@(() => RaiseAlarm("fall"))">Fall</LinkButton>
+ <LinkButton OnClick="@(() => RaiseAlarm("toilet"))">Toilet</LinkButton>
+ <LinkButton Color="#FF0000" OnClick="@(() => RaiseAlarm(null))">Clear</LinkButton>
+ </span>
+ <br/>
+}
+
+@if (Alarm != null) {
+ <ModalWindow Title="User alarm">
+ <div class="modal-body">
+ <span><strong>Alarm ID:</strong> @Alarm.Id</span><br/>
+ <span><strong>Reason:</strong> @Alarm.Reason</span><br/>
+ <span><strong>Raised at:</strong> @Alarm.CreatedAt.ToLocalTime()</span><br/>
+ </div>
+ <div class="modal-footer">
+ <LinkButton Color="#FF0000" OnClick="@(() => RaiseAlarm(null))">Clear Alarm</LinkButton>
+ </div>
+ </ModalWindow>
+}
+
+@code {
+
+ bool _isInitialized = false;
+ bool _running = true;
+
+ private WhoAmI WhoAmI { get; set; } = null!;
+ private AlarmDto? Alarm { get; set; } = null!;
+
+ protected override async Task OnInitializedAsync() {
+ WhoAmI = await App.UserClient.WhoAmI();
+ _isInitialized = true;
+ _ = PollAlarm();
+ NavigationManager.LocationChanged += (sender, args) => {
+ if (args.Location != NavigationManager.Uri) {
+ _running = false; // Stop polling when navigating away
+ }
+ };
+ }
+
+ private async Task PollAlarm() {
+ while (_running) {
+ var newAlarm = await App.UserClient.GetAlarm();
+ if (Alarm?.Id != newAlarm?.Id) {
+ Alarm = newAlarm;
+ StateHasChanged();
+ }
+
+ await Task.Delay(1000);
+ }
+ }
+
+ private async Task RaiseAlarm(string? reason) {
+ if (string.IsNullOrWhiteSpace(reason))
+ await App.UserClient.DeleteAlarm();
+ else
+ await App.UserClient.SetAlarm(new() { Reason = reason });
+ }
+
+ private void ReleaseUnmanagedResources() {
+ _running = false;
+ }
+
+ public void Dispose() {
+ ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ ~User() => ReleaseUnmanagedResources();
+
+}
\ No newline at end of file
|