feat: build blazor client UI and PWA assets

This commit is contained in:
Andre Beging
2026-01-29 10:17:26 +01:00
parent b6ce21b1b8
commit 2032dcd6ee
68 changed files with 60781 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.2" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ASTRAIN.Shared\ASTRAIN.Shared.csproj" />
</ItemGroup>
</Project>
+11
View File
@@ -0,0 +1,11 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<NotFound />
</LayoutView>
</NotFound>
</Router>
@@ -0,0 +1,35 @@
@inherits LayoutComponentBase
@inject NavigationManager Navigation
@inject UserContext UserContext
<div class="app-shell">
<header class="brand-header">
<span class="brand-white">AS</span><span class="brand-red">TRAIN</span>
</header>
<main class="main-content">
@Body
</main>
<nav class="bottom-nav">
<a class="nav-item" href="@GetLink("routines")">Routines</a>
<div class="nav-logo">
<img src="logo_square.png" alt="ASTRAIN" />
</div>
<a class="nav-item" href="@GetLink("exercises")">Exercises</a>
</nav>
</div>
@code {
private string GetLink(string page)
{
var uri = new Uri(Navigation.Uri);
var segments = uri.AbsolutePath.Trim('/').Split('/', StringSplitOptions.RemoveEmptyEntries);
var userId = segments.Length > 0 && segments[0].Length == 8 ? segments[0] : UserContext.UserId;
return string.IsNullOrWhiteSpace(userId) ? $"/{page}" : $"/{userId}/{page}";
}
}
@@ -0,0 +1,111 @@
.app-shell {
min-height: 100vh;
background: #0b0b0b;
color: #f5f5f5;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
padding: 1.5rem 1.25rem 6rem;
}
.brand-header {
text-align: center;
font-weight: 800;
font-size: 1.1rem;
padding: 1.25rem 0 0.25rem;
}
.brand-white {
color: #ffffff;
}
.brand-red {
color: #ff3b3b;
}
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 96px;
background: #141414;
border-top: 1px solid #2a2a2a;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 0 1.5rem;
gap: 1rem;
z-index: 10;
}
.nav-item {
color: #bdbdbd;
text-decoration: none;
font-weight: 700;
font-size: 0.9rem;
display: flex;
justify-content: center;
align-items: center;
height: 44px;
border: 1px solid transparent;
letter-spacing: 0.08em;
text-transform: uppercase;
width: 100%;
max-width: 160px;
margin: 0 auto;
justify-self: center;
text-align: center;
transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.nav-item.active {
color: #ffffff;
font-weight: 700;
border-color: #ff3b3b;
box-shadow: 0 0 0 2px rgba(255, 59, 59, 0.15);
background: rgba(255, 59, 59, 0.12);
}
.nav-item:hover {
color: #ffffff;
background: rgba(255, 255, 255, 0.06);
}
.nav-logo {
width: 80px;
height: 80px;
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
border-radius: 999px;
font-size: 0.7rem;
letter-spacing: 0.1em;
}
.nav-logo img {
width: 80px;
height: 80px;
border-radius: 10px;
object-fit: cover;
}
@media (min-width: 768px) {
.main-content {
padding: 2rem 2.5rem 7rem;
max-width: 960px;
margin: 0 auto;
}
.bottom-nav {
left: 50%;
transform: translateX(-50%);
max-width: 720px;
border-radius: 24px 24px 0 0;
}
}
+18
View File
@@ -0,0 +1,18 @@
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
+151
View File
@@ -0,0 +1,151 @@
@page "/exercises"
@page "/{UserId}/exercises"
@inject ApiClient Api
@inject NavigationManager Navigation
@inject UserContext UserContext
<PageTitle>Exercises</PageTitle>
<div class="page">
<header class="page-header">
<h1>Exercises</h1>
<p>Create and manage your exercise list.</p>
<button class="primary" @onclick="ToggleCreate">@(ShowCreateExercise ? "Close" : "+")</button>
</header>
@if (ShowCreateExercise)
{
<section class="card">
<h2>Add Exercise</h2>
<div class="form-row">
<input class="input" placeholder="Exercise name" @bind="NewExerciseName" @bind:event="oninput" />
<button class="primary" @onclick="CreateExerciseAsync" disabled="@string.IsNullOrWhiteSpace(NewExerciseName)">Add</button>
</div>
</section>
}
<section class="card">
<h2>Your Exercises</h2>
@if (IsLoading)
{
<p>Loading...</p>
}
else if (ExerciseList.Count == 0)
{
<p class="muted">No exercises yet. Add your first one above.</p>
}
else
{
<div class="list">
@foreach (var exercise in ExerciseList)
{
<div class="list-item">
@if (EditingId == exercise.Id)
{
<input class="input" @bind="EditingName" @bind:event="oninput" />
<button class="primary" @onclick="() => SaveEditAsync(exercise.Id)">Save</button>
<button class="ghost" @onclick="CancelEdit">Cancel</button>
}
else
{
<div class="item-title">@exercise.Name</div>
<button class="ghost" @onclick="() => StartEdit(exercise)">Edit</button>
}
</div>
}
</div>
}
</section>
</div>
@code {
[Parameter]
public string? UserId { get; set; }
private List<ExerciseDto> ExerciseList { get; set; } = new();
private bool IsLoading { get; set; } = true;
private bool ShowCreateExercise { get; set; }
private string NewExerciseName { get; set; } = string.Empty;
private int? EditingId { get; set; }
private string EditingName { get; set; } = string.Empty;
protected override async Task OnInitializedAsync()
{
var ensured = await Api.EnsureUserAsync(UserId);
if (string.IsNullOrWhiteSpace(ensured))
{
return;
}
UserContext.SetUserId(ensured);
if (UserId != ensured)
{
Navigation.NavigateTo($"/{ensured}/exercises", true);
return;
}
await LoadExercisesAsync();
}
private async Task LoadExercisesAsync()
{
IsLoading = true;
ExerciseList = await Api.GetExercisesAsync(UserContext.UserId);
IsLoading = false;
}
private async Task CreateExerciseAsync()
{
if (string.IsNullOrWhiteSpace(NewExerciseName))
{
return;
}
var result = await Api.CreateExerciseAsync(UserContext.UserId, new ExerciseUpsertRequest(NewExerciseName));
if (result is not null)
{
ExerciseList.Add(result);
ExerciseList = ExerciseList.OrderBy(e => e.Name).ToList();
NewExerciseName = string.Empty;
}
}
private void ToggleCreate()
{
ShowCreateExercise = !ShowCreateExercise;
}
private void StartEdit(ExerciseDto exercise)
{
EditingId = exercise.Id;
EditingName = exercise.Name;
}
private void CancelEdit()
{
EditingId = null;
EditingName = string.Empty;
}
private async Task SaveEditAsync(int exerciseId)
{
if (string.IsNullOrWhiteSpace(EditingName))
{
return;
}
var result = await Api.UpdateExerciseAsync(UserContext.UserId, exerciseId, new ExerciseUpsertRequest(EditingName));
if (result is not null)
{
var index = ExerciseList.FindIndex(e => e.Id == exerciseId);
if (index >= 0)
{
ExerciseList[index] = result;
ExerciseList = ExerciseList.OrderBy(e => e.Name).ToList();
}
}
CancelEdit();
}
}
+36
View File
@@ -0,0 +1,36 @@
@page "/"
@page "/{UserId}"
@inject ApiClient Api
@inject NavigationManager Navigation
@inject UserContext UserContext
<PageTitle>ASTRAIN</PageTitle>
<div class="page-center">
<div class="card">
<h1>ASTRAIN</h1>
<p>Preparing your training logbook...</p>
</div>
</div>
@code {
[Parameter]
public string? UserId { get; set; }
protected override async Task OnInitializedAsync()
{
var ensured = await Api.EnsureUserAsync(UserId);
if (string.IsNullOrWhiteSpace(ensured))
{
return;
}
UserContext.SetUserId(ensured);
var target = $"/{ensured}/routines";
if (!Navigation.Uri.EndsWith(target, StringComparison.OrdinalIgnoreCase))
{
Navigation.NavigateTo(target, true);
}
}
}
+9
View File
@@ -0,0 +1,9 @@
@page "/not-found"
@layout MainLayout
<div class="page-center">
<div class="card">
<h1>Page not found</h1>
<p class="muted">The page you requested doesn't exist yet.</p>
</div>
</div>
+296
View File
@@ -0,0 +1,296 @@
@page "/routines"
@page "/{UserId}/routines"
@inject ApiClient Api
@inject NavigationManager Navigation
@inject UserContext UserContext
<PageTitle>Routines</PageTitle>
<div class="page">
<header class="page-header">
<h1>Routines</h1>
<p>Build routines from your exercise list.</p>
<button class="primary" @onclick="ToggleCreate">@(ShowCreateRoutine ? "Close" : "+")</button>
</header>
@if (ActiveRun is null)
{
@if (ExerciseList.Count == 0)
{
<section class="card">
<h2>No exercises yet</h2>
<p class="muted">Add exercises first, then create routines.</p>
<button class="primary" @onclick="GoToExercises">Go to Exercises</button>
</section>
}
@if (ShowCreateRoutine)
{
<section class="card">
<h2>Create Routine</h2>
<input class="input" placeholder="Routine name" @bind="NewRoutineName" @bind:event="oninput" />
<div class="list">
@foreach (var exercise in ExerciseList)
{
<label class="checkbox-row">
<input type="checkbox" checked="@SelectedExerciseIds.Contains(exercise.Id)" @onchange="() => ToggleExercise(exercise.Id)" />
<span>@exercise.Name</span>
</label>
}
</div>
<button class="primary" @onclick="CreateRoutineAsync" disabled="@string.IsNullOrWhiteSpace(NewRoutineName)">Save Routine</button>
</section>
}
<section class="card">
<h2>Your Routines</h2>
@if (IsLoading)
{
<p>Loading...</p>
}
else if (RoutineList.Count == 0)
{
<p class="muted">No routines yet. Create one above.</p>
}
else
{
<div class="list">
@foreach (var routine in RoutineList)
{
<div class="list-item">
<div>
<div class="item-title">@routine.Name</div>
<div class="item-subtitle">@string.Join(" · ", routine.Exercises.Select(e => e.Name))</div>
</div>
<div class="actions">
<button class="ghost" @onclick="() => StartEdit(routine)">Edit</button>
<button class="primary" @onclick="() => StartRun(routine)">Start</button>
</div>
</div>
}
</div>
}
</section>
@if (EditingRoutine is not null)
{
<section class="card">
<h2>Edit Routine</h2>
<input class="input" @bind="EditingName" @bind:event="oninput" />
<div class="list">
@foreach (var exercise in ExerciseList)
{
<label class="checkbox-row">
<input type="checkbox" checked="@EditingExerciseIds.Contains(exercise.Id)" @onchange="() => ToggleEditExercise(exercise.Id)" />
<span>@exercise.Name</span>
</label>
}
</div>
<div class="actions">
<button class="primary" @onclick="SaveEditAsync">Save Changes</button>
<button class="ghost" @onclick="CancelEdit">Cancel</button>
</div>
</section>
}
}
else
{
<section class="card">
<h2>Routine Run: @ActiveRun.Name</h2>
<div class="list">
@foreach (var entry in RunEntries)
{
<div class="list-item @(entry.Completed ? "done" : string.Empty)">
<label class="checkbox-row">
<input type="checkbox" checked="@entry.Completed" @onchange="() => ToggleRunCompleted(entry.ExerciseId)" />
<span>@GetExerciseName(entry.ExerciseId)</span>
</label>
<div class="input-unit">
<input class="input input-sm" type="number" step="0.5" @bind="entry.Weight" @bind:event="oninput" />
<span class="unit">kg</span>
</div>
</div>
}
</div>
<div class="actions">
<button class="ghost" @onclick="AbortRun">Abort</button>
<button class="primary" @onclick="SaveRunAsync">Save Run</button>
</div>
</section>
}
</div>
@code {
[Parameter]
public string? UserId { get; set; }
private List<ExerciseDto> ExerciseList { get; set; } = new();
private List<RoutineDto> RoutineList { get; set; } = new();
private bool IsLoading { get; set; } = true;
private bool ShowCreateRoutine { get; set; }
private string NewRoutineName { get; set; } = string.Empty;
private HashSet<int> SelectedExerciseIds { get; set; } = new();
private RoutineDto? EditingRoutine { get; set; }
private string EditingName { get; set; } = string.Empty;
private HashSet<int> EditingExerciseIds { get; set; } = new();
private RoutineDto? ActiveRun { get; set; }
private List<RoutineRunEntryDto> RunEntries { get; set; } = new();
protected override async Task OnInitializedAsync()
{
var ensured = await Api.EnsureUserAsync(UserId);
if (string.IsNullOrWhiteSpace(ensured))
{
return;
}
UserContext.SetUserId(ensured);
if (UserId != ensured)
{
Navigation.NavigateTo($"/{ensured}/routines", true);
return;
}
await LoadDataAsync();
}
private async Task LoadDataAsync()
{
IsLoading = true;
ExerciseList = await Api.GetExercisesAsync(UserContext.UserId);
RoutineList = await Api.GetRoutinesAsync(UserContext.UserId);
IsLoading = false;
}
private void ToggleExercise(int exerciseId)
{
if (!SelectedExerciseIds.Add(exerciseId))
{
SelectedExerciseIds.Remove(exerciseId);
}
}
private async Task CreateRoutineAsync()
{
if (string.IsNullOrWhiteSpace(NewRoutineName))
{
return;
}
var request = new RoutineUpsertRequest(NewRoutineName, SelectedExerciseIds.ToList());
var created = await Api.CreateRoutineAsync(UserContext.UserId, request);
if (created is not null)
{
RoutineList.Add(created);
RoutineList = RoutineList.OrderBy(r => r.Name).ToList();
NewRoutineName = string.Empty;
SelectedExerciseIds.Clear();
}
}
private void ToggleCreate()
{
ShowCreateRoutine = !ShowCreateRoutine;
}
private void GoToExercises()
{
Navigation.NavigateTo($"/{UserContext.UserId}/exercises");
}
private void StartEdit(RoutineDto routine)
{
EditingRoutine = routine;
EditingName = routine.Name;
EditingExerciseIds = routine.Exercises.Select(e => e.ExerciseId).ToHashSet();
}
private void CancelEdit()
{
EditingRoutine = null;
EditingName = string.Empty;
EditingExerciseIds.Clear();
}
private void ToggleEditExercise(int exerciseId)
{
if (!EditingExerciseIds.Add(exerciseId))
{
EditingExerciseIds.Remove(exerciseId);
}
}
private async Task SaveEditAsync()
{
if (EditingRoutine is null)
{
return;
}
var request = new RoutineUpsertRequest(EditingName, EditingExerciseIds.ToList());
var updated = await Api.UpdateRoutineAsync(UserContext.UserId, EditingRoutine.Id, request);
if (updated is not null)
{
var index = RoutineList.FindIndex(r => r.Id == EditingRoutine.Id);
if (index >= 0)
{
RoutineList[index] = updated;
RoutineList = RoutineList.OrderBy(r => r.Name).ToList();
}
}
CancelEdit();
}
private async Task StartRun(RoutineDto routine)
{
ActiveRun = routine;
var lastRun = await Api.GetLastRunAsync(UserContext.UserId, routine.Id);
RunEntries = routine.Exercises
.OrderBy(e => e.Order)
.Select(e =>
{
var last = lastRun.Entries.FirstOrDefault(x => x.ExerciseId == e.ExerciseId);
return new RoutineRunEntryDto(e.ExerciseId, last?.Weight ?? 0, false);
}).ToList();
}
private void ToggleRunCompleted(int exerciseId)
{
var entry = RunEntries.FirstOrDefault(e => e.ExerciseId == exerciseId);
if (entry is null)
{
return;
}
entry.Completed = !entry.Completed;
}
private string GetExerciseName(int exerciseId)
{
return ExerciseList.FirstOrDefault(e => e.Id == exerciseId)?.Name ?? "Exercise";
}
private void AbortRun()
{
ActiveRun = null;
RunEntries = new List<RoutineRunEntryDto>();
}
private async Task SaveRunAsync()
{
if (ActiveRun is null)
{
return;
}
var request = new RoutineRunRequest(RunEntries);
await Api.SaveRunAsync(UserContext.UserId, ActiveRun.Id, request);
ActiveRun = null;
RunEntries = new List<RoutineRunEntryDto>();
}
}
+57
View File
@@ -0,0 +1,57 @@
@page "/weather"
@inject HttpClient Http
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Fahrenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
+19
View File
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using ASTRAIN.Client;
using ASTRAIN.Client.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
var apiBaseAddress = builder.Configuration["ApiBaseAddress"];
var baseAddress = string.IsNullOrWhiteSpace(apiBaseAddress)
? builder.HostEnvironment.BaseAddress
: apiBaseAddress;
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(baseAddress) });
builder.Services.AddScoped<ApiClient>();
builder.Services.AddScoped<UserContext>();
await builder.Build().RunAsync();
@@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5014",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7252;http://localhost:5014",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
using System.Net.Http.Json;
using ASTRAIN.Shared;
namespace ASTRAIN.Client.Services;
public class ApiClient
{
private readonly HttpClient _http;
public ApiClient(HttpClient http)
{
_http = http;
}
public async Task<string> EnsureUserAsync(string? userId)
{
var response = await _http.GetFromJsonAsync<EnsureUserResponse>($"/api/users/ensure?userId={userId}");
return response?.UserId ?? string.Empty;
}
public async Task<List<ExerciseDto>> GetExercisesAsync(string userId)
{
return await _http.GetFromJsonAsync<List<ExerciseDto>>($"/api/users/{userId}/exercises") ?? new List<ExerciseDto>();
}
public async Task<ExerciseDto?> CreateExerciseAsync(string userId, ExerciseUpsertRequest request)
{
var response = await _http.PostAsJsonAsync($"/api/users/{userId}/exercises", request);
return await response.Content.ReadFromJsonAsync<ExerciseDto>();
}
public async Task<ExerciseDto?> UpdateExerciseAsync(string userId, int exerciseId, ExerciseUpsertRequest request)
{
var response = await _http.PutAsJsonAsync($"/api/users/{userId}/exercises/{exerciseId}", request);
return await response.Content.ReadFromJsonAsync<ExerciseDto>();
}
public async Task<List<RoutineDto>> GetRoutinesAsync(string userId)
{
return await _http.GetFromJsonAsync<List<RoutineDto>>($"/api/users/{userId}/routines") ?? new List<RoutineDto>();
}
public async Task<RoutineDto?> GetRoutineAsync(string userId, int routineId)
{
return await _http.GetFromJsonAsync<RoutineDto>($"/api/users/{userId}/routines/{routineId}");
}
public async Task<RoutineDto?> CreateRoutineAsync(string userId, RoutineUpsertRequest request)
{
var response = await _http.PostAsJsonAsync($"/api/users/{userId}/routines", request);
return await response.Content.ReadFromJsonAsync<RoutineDto>();
}
public async Task<RoutineDto?> UpdateRoutineAsync(string userId, int routineId, RoutineUpsertRequest request)
{
var response = await _http.PutAsJsonAsync($"/api/users/{userId}/routines/{routineId}", request);
return await response.Content.ReadFromJsonAsync<RoutineDto>();
}
public async Task<RoutineRunSummaryDto> GetLastRunAsync(string userId, int routineId)
{
return await _http.GetFromJsonAsync<RoutineRunSummaryDto>($"/api/users/{userId}/routines/{routineId}/last-run")
?? new RoutineRunSummaryDto(DateTime.MinValue, new List<RoutineRunEntryDto>());
}
public async Task SaveRunAsync(string userId, int routineId, RoutineRunRequest request)
{
await _http.PostAsJsonAsync($"/api/users/{userId}/routines/{routineId}/runs", request);
}
}
@@ -0,0 +1,11 @@
namespace ASTRAIN.Client.Services;
public class UserContext
{
public string UserId { get; private set; } = string.Empty;
public void SetUserId(string userId)
{
UserId = userId;
}
}
+13
View File
@@ -0,0 +1,13 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using ASTRAIN.Client
@using ASTRAIN.Client.Layout
@using ASTRAIN.Client.Services
@using ASTRAIN.Shared
@using ASTRAIN.Client.Pages
@@ -0,0 +1,3 @@
{
"ApiBaseAddress": "http://localhost:5055"
}
@@ -0,0 +1,3 @@
{
"ApiBaseAddress": ""
}
+228
View File
@@ -0,0 +1,228 @@
* {
box-sizing: border-box;
}
html, body {
font-family: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
background-color: #0f1117;
color: #f5f5f5;
margin: 0;
}
a {
color: inherit;
}
h1, h2, h3 {
margin: 0 0 0.5rem;
}
p {
margin: 0.25rem 0 0;
}
.page {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.page-center {
min-height: 70vh;
display: flex;
align-items: center;
justify-content: center;
}
.page-header p {
color: #9aa4b2;
}
.page-header {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.5rem;
}
.page-header .primary {
width: 52px;
height: 52px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
padding: 0;
}
.card {
background: #151515;
border: 1px solid #2a2a2a;
border-radius: 20px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-row {
display: flex;
gap: 0.75rem;
flex-direction: column;
}
.input {
width: 100%;
padding: 0.75rem 1rem;
background: #0e0e0e;
border: 1px solid #2a2a2a;
border-radius: 14px;
color: #f5f5f5;
font-size: 1rem;
}
.input:focus {
outline: none;
border-color: #ff3b3b;
box-shadow: 0 0 0 2px rgba(255, 59, 59, 0.2);
}
.input-sm {
max-width: 110px;
}
.primary {
background: #ff3b3b;
color: #ffffff;
border: none;
border-radius: 14px;
padding: 0.75rem 1.25rem;
font-weight: 600;
cursor: pointer;
}
.primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.ghost {
background: transparent;
color: #ffffff;
border: 1px solid #3a3a3a;
border-radius: 14px;
padding: 0.6rem 1rem;
cursor: pointer;
}
.list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 1rem;
background: #101010;
border-radius: 14px;
border: 1px solid #2a2a2a;
}
.list-item.done {
background: #10331a;
border-color: #1f6b38;
}
.item-title {
font-weight: 600;
}
.item-subtitle {
color: #bdbdbd;
font-size: 0.85rem;
}
.actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.checkbox-row {
display: flex;
gap: 0.5rem;
align-items: center;
color: #f0f0f0;
}
.muted {
color: #bdbdbd;
}
.input-unit {
position: relative;
display: flex;
align-items: center;
gap: 0.4rem;
}
.input-unit .unit {
padding: 0.4rem 0.6rem;
background: #1c1c1c;
border: 1px solid #2a2a2a;
border-radius: 10px;
color: #ffffff;
font-size: 0.85rem;
}
#blazor-error-ui {
color-scheme: dark;
background: #1f2937;
color: #f8fafc;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
.loading-screen {
position: absolute;
inset: 20vh 0 auto 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.loading-screen img {
width: 96px;
height: 96px;
border-radius: 22px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5);
}
.loading-progress-text {
text-align: center;
font-weight: bold;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
@media (min-width: 640px) {
.form-row {
flex-direction: row;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ASTRAIN</title>
<base href="/" />
<link rel="preload" id="webassembly" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="logo_square.png" />
<link rel="apple-touch-icon" href="logo_square.png" />
<link rel="manifest" href="manifest.json" />
<meta name="theme-color" content="#ff3b3b" />
<link href="ASTRAIN.Client.styles.css" rel="stylesheet" />
<script type="importmap"></script>
</head>
<body>
<div id="app">
<div class="loading-screen">
<img src="logo_square.png" alt="ASTRAIN" />
<div class="loading-progress-text"></div>
</div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

+16
View File
@@ -0,0 +1,16 @@
{
"name": "ASTRAIN",
"short_name": "ASTRAIN",
"start_url": "/",
"display": "standalone",
"background_color": "#0b0b0b",
"theme_color": "#ff3b3b",
"icons": [
{
"src": "logo_square.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
@@ -0,0 +1,27 @@
[
{
"date": "2022-01-06",
"temperatureC": 1,
"summary": "Freezing"
},
{
"date": "2022-01-07",
"temperatureC": 14,
"summary": "Bracing"
},
{
"date": "2022-01-08",
"temperatureC": -13,
"summary": "Freezing"
},
{
"date": "2022-01-09",
"temperatureC": -16,
"summary": "Balmy"
},
{
"date": "2022-01-10",
"temperatureC": -2,
"summary": "Chilly"
}
]