API Test
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HotChocolate.AspNetCore" Version="16.6.1"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Model\Model.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<base href="/"/>
|
||||
<ResourcePreloader/>
|
||||
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]"/>
|
||||
<link rel="stylesheet" href="@Assets["app.css"]"/>
|
||||
<link rel="stylesheet" href="@Assets["API.styles.css"]"/>
|
||||
<ImportMap/>
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<HeadOutlet/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes/>
|
||||
<ReconnectModal/>
|
||||
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu/>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<div class="top-row px-4">
|
||||
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
|
||||
</div>
|
||||
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui" data-nosnippet>
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
@@ -0,0 +1,98 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
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;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">API</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler"/>
|
||||
|
||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||
<nav class="nav flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="weather">
|
||||
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -0,0 +1,105 @@
|
||||
.navbar-toggler {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 3.5rem;
|
||||
height: 2.5rem;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navbar-toggler:checked {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #d7d7d7;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255, 255, 255, 0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-toggler:checked ~ .nav-scrollable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
|
||||
|
||||
<dialog id="components-reconnect-modal" data-nosnippet>
|
||||
<div class="components-reconnect-container">
|
||||
<div class="components-rejoining-animation" aria-hidden="true">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<p class="components-reconnect-first-attempt-visible">
|
||||
Rejoining the server...
|
||||
</p>
|
||||
<p class="components-reconnect-repeated-attempt-visible">
|
||||
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
|
||||
</p>
|
||||
<p class="components-reconnect-failed-visible">
|
||||
Failed to rejoin.<br/>Please retry or reload the page.
|
||||
</p>
|
||||
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
|
||||
Retry
|
||||
</button>
|
||||
<p class="components-pause-visible">
|
||||
The session has been paused by the server.
|
||||
</p>
|
||||
<p class="components-resume-failed-visible">
|
||||
Failed to resume the session.<br/>Please retry or reload the page.
|
||||
</p>
|
||||
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
|
||||
Resume
|
||||
</button>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -0,0 +1,156 @@
|
||||
.components-reconnect-first-attempt-visible,
|
||||
.components-reconnect-repeated-attempt-visible,
|
||||
.components-reconnect-failed-visible,
|
||||
.components-pause-visible,
|
||||
.components-resume-failed-visible,
|
||||
.components-rejoining-animation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
|
||||
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-failed,
|
||||
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
#components-reconnect-modal {
|
||||
background-color: white;
|
||||
width: 20rem;
|
||||
margin: 20vh auto;
|
||||
padding: 2rem;
|
||||
border: 0;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
|
||||
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
|
||||
|
||||
&[open] {
|
||||
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#components-reconnect-modal::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-slideUp {
|
||||
0% {
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeInOpacity {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeOutOpacity {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.components-reconnect-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#components-reconnect-modal p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button {
|
||||
border: 0;
|
||||
background-color: #6b9ed2;
|
||||
color: white;
|
||||
padding: 4px 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:hover {
|
||||
background-color: #3b6ea2;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:active {
|
||||
background-color: #6b9ed2;
|
||||
}
|
||||
|
||||
.components-rejoining-animation {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div {
|
||||
position: absolute;
|
||||
border: 3px solid #0087ff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes components-rejoining-animation {
|
||||
0% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
4.9% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
5% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Set up event handlers
|
||||
const reconnectModal = document.getElementById("components-reconnect-modal");
|
||||
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
|
||||
|
||||
const retryButton = document.getElementById("components-reconnect-button");
|
||||
retryButton.addEventListener("click", retry);
|
||||
|
||||
const resumeButton = document.getElementById("components-resume-button");
|
||||
resumeButton.addEventListener("click", resume);
|
||||
|
||||
function handleReconnectStateChanged(event) {
|
||||
if (event.detail.state === "show") {
|
||||
reconnectModal.showModal();
|
||||
} else if (event.detail.state === "hide") {
|
||||
reconnectModal.close();
|
||||
} else if (event.detail.state === "failed") {
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
} else if (event.detail.state === "rejected") {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retry() {
|
||||
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
|
||||
try {
|
||||
// Reconnect will asynchronously return:
|
||||
// - true to mean success
|
||||
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
|
||||
// - exception to mean we didn't reach the server (this can be sync or async)
|
||||
const successful = await Blazor.reconnect();
|
||||
if (!successful) {
|
||||
// We have been able to reach the server, but the circuit is no longer available.
|
||||
// We'll reload the page so the user can continue using the app as quickly as possible.
|
||||
const resumeSuccessful = await Blazor.resumeCircuit();
|
||||
if (!resumeSuccessful) {
|
||||
location.reload();
|
||||
} else {
|
||||
reconnectModal.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// We got an exception, server is currently unavailable
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
}
|
||||
}
|
||||
|
||||
async function resume() {
|
||||
try {
|
||||
const successful = await Blazor.resumeCircuit();
|
||||
if (!successful) {
|
||||
location.reload();
|
||||
}
|
||||
} catch {
|
||||
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function retryWhenDocumentBecomesVisible() {
|
||||
if (document.visibilityState === "visible") {
|
||||
await retry();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
@page "/counter"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<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;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that
|
||||
occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong>
|
||||
environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter] private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new app.
|
||||
@@ -0,0 +1,5 @@
|
||||
@page "/not-found"
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
@@ -0,0 +1,67 @@
|
||||
@page "/weather"
|
||||
@attribute [StreamRendering]
|
||||
|
||||
<PageTitle>Weather</PageTitle>
|
||||
|
||||
<h1>Weather</h1>
|
||||
|
||||
<p>This component demonstrates showing data.</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()
|
||||
{
|
||||
// Simulate asynchronous loading to demonstrate streaming rendering
|
||||
await Task.Delay(500);
|
||||
|
||||
var startDate = DateOnly.FromDateTime(DateTime.Now);
|
||||
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
|
||||
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = startDate.AddDays(index),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = summaries[Random.Shared.Next(summaries.Length)]
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@using API.Components.Pages
|
||||
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(NotFound)">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)"/>
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -0,0 +1,11 @@
|
||||
@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 static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using API
|
||||
@using API.Components
|
||||
@using API.Components.Layout
|
||||
@@ -0,0 +1,33 @@
|
||||
using API.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Model;
|
||||
|
||||
namespace API.Data;
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
{
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<User> Users { get; set; } = default!;
|
||||
public DbSet<CardNote> CardNotes { get; set; } = default!;
|
||||
public DbSet<UserDeck> UserDecks { get; set; } = default!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<User>(b => { b.HasKey(u => u.Username); });
|
||||
|
||||
modelBuilder.Entity<CardNote>(b => { b.HasKey(n => new { n.Username, n.CardName }); });
|
||||
|
||||
modelBuilder.Entity<UserDeck>(b =>
|
||||
{
|
||||
b.HasKey(d => d.Id);
|
||||
if (Database.IsNpgsql())
|
||||
{
|
||||
b.Property(d => d.Cards).HasColumnType("jsonb");
|
||||
b.Property(d => d.Divers).HasColumnType("jsonb");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Npgsql;
|
||||
|
||||
namespace API.Data;
|
||||
|
||||
public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
|
||||
{
|
||||
public AppDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var envPassword = Environment.GetEnvironmentVariable("PostgreSQL_Password")
|
||||
?? (OperatingSystem.IsWindows()
|
||||
? Environment.GetEnvironmentVariable("PostgreSQL_Password",
|
||||
EnvironmentVariableTarget.User)
|
||||
: null)
|
||||
?? (OperatingSystem.IsWindows()
|
||||
? Environment.GetEnvironmentVariable("PostgreSQL_Password",
|
||||
EnvironmentVariableTarget.Machine)
|
||||
: null)
|
||||
?? Environment.GetEnvironmentVariable("POSTGRESQL_PASSWORD")
|
||||
?? Environment.GetEnvironmentVariable("POSTGRES_PASSWORD");
|
||||
|
||||
var csb = new NpgsqlConnectionStringBuilder
|
||||
{
|
||||
Host = "localhost",
|
||||
Database = "chrono",
|
||||
Username = "postgres",
|
||||
Password = envPassword
|
||||
};
|
||||
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseNpgsql(csb.ConnectionString)
|
||||
.Options;
|
||||
return new AppDbContext(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
using API.Data;
|
||||
using API.Models;
|
||||
using API.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Model;
|
||||
|
||||
namespace API.GraphQL;
|
||||
|
||||
public class Mutation
|
||||
{
|
||||
private static string EnsureAuthenticated(IHttpContextAccessor? httpContextAccessor, ITokenService tokenService)
|
||||
{
|
||||
var httpContext = httpContextAccessor?.HttpContext;
|
||||
if (httpContext == null)
|
||||
throw new Exception("Authentication required. Please provide a valid authorization token.");
|
||||
|
||||
var authHeader = httpContext.Request.Headers["Authorization"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(authHeader))
|
||||
throw new Exception("Authentication required. Please login to access this resource.");
|
||||
|
||||
var token = authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
|
||||
? authHeader["Bearer ".Length..].Trim()
|
||||
: authHeader.Trim();
|
||||
|
||||
var username = tokenService.ValidateToken(token);
|
||||
if (string.IsNullOrEmpty(username))
|
||||
throw new Exception("Invalid or expired authentication token. Please login again.");
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
public async Task<LoginPayload> Login(
|
||||
[Service] AppDbContext db,
|
||||
[Service] ITokenService tokenService,
|
||||
string username,
|
||||
string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
return new LoginPayload
|
||||
{
|
||||
Success = false,
|
||||
Message = "Username and password are required."
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var user = await db.Users.FirstOrDefaultAsync(u => u.Username == username);
|
||||
if (user == null)
|
||||
{
|
||||
user = new User
|
||||
{
|
||||
Username = username,
|
||||
Password = password,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
db.Users.Add(user);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
else if (user.Password != password)
|
||||
{
|
||||
return new LoginPayload
|
||||
{
|
||||
Success = false,
|
||||
Message = "Invalid credentials."
|
||||
};
|
||||
}
|
||||
|
||||
var token = tokenService.GenerateToken(username);
|
||||
return new LoginPayload
|
||||
{
|
||||
Success = true,
|
||||
Token = token,
|
||||
Username = username,
|
||||
Message = "Login successful."
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new LoginPayload
|
||||
{
|
||||
Success = false,
|
||||
Message = $"Login failed: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CardNote> SaveNote(
|
||||
[Service] AppDbContext db,
|
||||
[Service] IHttpContextAccessor httpContextAccessor,
|
||||
[Service] ITokenService tokenService,
|
||||
string cardName,
|
||||
string note)
|
||||
{
|
||||
var username = EnsureAuthenticated(httpContextAccessor, tokenService);
|
||||
try
|
||||
{
|
||||
var existing =
|
||||
await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == cardName && n.Username == username);
|
||||
if (existing == null)
|
||||
{
|
||||
var cardNote = new CardNote
|
||||
{ Username = username, CardName = cardName, Note = note, UpdatedAt = DateTime.UtcNow };
|
||||
db.CardNotes.Add(cardNote);
|
||||
await db.SaveChangesAsync();
|
||||
return cardNote;
|
||||
}
|
||||
|
||||
existing.Note = note;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[WARNING] Could not save note: {ex.Message}");
|
||||
return new CardNote { Username = username, CardName = cardName, Note = note };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteNote(
|
||||
[Service] AppDbContext db,
|
||||
[Service] IHttpContextAccessor httpContextAccessor,
|
||||
[Service] ITokenService tokenService,
|
||||
string cardName)
|
||||
{
|
||||
var username = EnsureAuthenticated(httpContextAccessor, tokenService);
|
||||
try
|
||||
{
|
||||
var existing =
|
||||
await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == cardName && n.Username == username);
|
||||
if (existing != null)
|
||||
{
|
||||
db.CardNotes.Remove(existing);
|
||||
await db.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[WARNING] Could not delete note: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<UserDeck> SaveDeck(
|
||||
[Service] AppDbContext db,
|
||||
[Service] IHttpContextAccessor httpContextAccessor,
|
||||
[Service] ITokenService tokenService,
|
||||
UserDeckInput input)
|
||||
{
|
||||
EnsureAuthenticated(httpContextAccessor, tokenService);
|
||||
var id = input.Id ?? Guid.NewGuid();
|
||||
var existing = await db.UserDecks.FindAsync(id);
|
||||
if (existing == null)
|
||||
{
|
||||
var deck = new UserDeck
|
||||
{
|
||||
Id = id,
|
||||
Name = input.Name,
|
||||
Cards = input.Cards ?? [],
|
||||
Divers = input.Divers ?? [],
|
||||
Notes = input.Notes,
|
||||
Season = input.Season,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
db.UserDecks.Add(deck);
|
||||
await db.SaveChangesAsync();
|
||||
return deck;
|
||||
}
|
||||
|
||||
existing.Name = input.Name;
|
||||
existing.Cards = input.Cards ?? [];
|
||||
existing.Divers = input.Divers ?? [];
|
||||
existing.Notes = input.Notes;
|
||||
existing.Season = input.Season;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteDeck(
|
||||
[Service] AppDbContext db,
|
||||
[Service] IHttpContextAccessor httpContextAccessor,
|
||||
[Service] ITokenService tokenService,
|
||||
Guid id)
|
||||
{
|
||||
EnsureAuthenticated(httpContextAccessor, tokenService);
|
||||
var existing = await db.UserDecks.FindAsync(id);
|
||||
if (existing != null)
|
||||
{
|
||||
db.UserDecks.Remove(existing);
|
||||
await db.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public record UserDeckInput(
|
||||
Guid? Id,
|
||||
string Name,
|
||||
List<string>? Cards,
|
||||
List<string>? Divers,
|
||||
string? Notes,
|
||||
string? Season);
|
||||
@@ -0,0 +1,102 @@
|
||||
using API.Data;
|
||||
using API.Models;
|
||||
using API.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Model;
|
||||
|
||||
namespace API.GraphQL;
|
||||
|
||||
public class Query
|
||||
{
|
||||
private static string EnsureAuthenticated(IHttpContextAccessor? httpContextAccessor, ITokenService tokenService)
|
||||
{
|
||||
var httpContext = httpContextAccessor?.HttpContext;
|
||||
if (httpContext == null)
|
||||
throw new Exception("Authentication required. Please provide a valid authorization token.");
|
||||
|
||||
var authHeader = httpContext.Request.Headers["Authorization"].FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(authHeader))
|
||||
throw new Exception("Authentication required. Please login to access this resource.");
|
||||
|
||||
var token = authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
|
||||
? authHeader["Bearer ".Length..].Trim()
|
||||
: authHeader.Trim();
|
||||
|
||||
var username = tokenService.ValidateToken(token);
|
||||
if (string.IsNullOrEmpty(username))
|
||||
throw new Exception("Invalid or expired authentication token. Please login again.");
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
public async Task<CardNote?> GetNote(
|
||||
[Service] AppDbContext db,
|
||||
[Service] IHttpContextAccessor httpContextAccessor,
|
||||
[Service] ITokenService tokenService,
|
||||
string cardName)
|
||||
{
|
||||
var username = EnsureAuthenticated(httpContextAccessor, tokenService);
|
||||
try
|
||||
{
|
||||
var note = await db.CardNotes.FirstOrDefaultAsync(n => n.CardName == cardName && n.Username == username);
|
||||
return note ?? new CardNote { Username = username, CardName = cardName, Note = "" };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[WARNING] Could not retrieve note: {ex.Message}");
|
||||
return new CardNote { Username = username, CardName = cardName, Note = "" };
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<CardNote>> GetNotes(
|
||||
[Service] AppDbContext db,
|
||||
[Service] IHttpContextAccessor httpContextAccessor,
|
||||
[Service] ITokenService tokenService)
|
||||
{
|
||||
var username = EnsureAuthenticated(httpContextAccessor, tokenService);
|
||||
try
|
||||
{
|
||||
return await db.CardNotes.Where(n => n.Username == username).ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[WARNING] Could not retrieve notes: {ex.Message}");
|
||||
return new List<CardNote>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<UserDeck>> GetDecks(
|
||||
[Service] AppDbContext db,
|
||||
[Service] IHttpContextAccessor httpContextAccessor,
|
||||
[Service] ITokenService tokenService)
|
||||
{
|
||||
EnsureAuthenticated(httpContextAccessor, tokenService);
|
||||
try
|
||||
{
|
||||
return await db.UserDecks.OrderByDescending(d => d.UpdatedAt).ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[WARNING] Could not retrieve decks: {ex.Message}");
|
||||
return new List<UserDeck>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<UserDeck?> GetDeck(
|
||||
[Service] AppDbContext db,
|
||||
[Service] IHttpContextAccessor httpContextAccessor,
|
||||
[Service] ITokenService tokenService,
|
||||
Guid id)
|
||||
{
|
||||
EnsureAuthenticated(httpContextAccessor, tokenService);
|
||||
try
|
||||
{
|
||||
return await db.UserDecks.FindAsync(id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[WARNING] Could not retrieve deck: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260817043106_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Models.UserDeck", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.PrimitiveCollection<string>("Cards")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.PrimitiveCollection<string>("Divers")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Season")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("UserDecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.CardNote", b =>
|
||||
{
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CardName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Username", "CardName");
|
||||
|
||||
b.ToTable("CardNotes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.User", b =>
|
||||
{
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Username");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CardNotes",
|
||||
columns: table => new
|
||||
{
|
||||
Username = table.Column<string>(type: "text", nullable: false),
|
||||
CardName = table.Column<string>(type: "text", nullable: false),
|
||||
Note = table.Column<string>(type: "text", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CardNotes", x => new { x.Username, x.CardName });
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserDecks",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Cards = table.Column<string>(type: "jsonb", nullable: false),
|
||||
Divers = table.Column<string>(type: "jsonb", nullable: false),
|
||||
Notes = table.Column<string>(type: "text", nullable: true),
|
||||
Season = table.Column<string>(type: "text", nullable: true),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserDecks", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
Username = table.Column<string>(type: "text", nullable: false),
|
||||
Password = table.Column<string>(type: "text", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Username);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "CardNotes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserDecks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using API.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace API.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("API.Models.UserDeck", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.PrimitiveCollection<string>("Cards")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.PrimitiveCollection<string>("Divers")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Season")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("UserDecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.CardNote", b =>
|
||||
{
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CardName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Username", "CardName");
|
||||
|
||||
b.ToTable("CardNotes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Model.User", b =>
|
||||
{
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Username");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace API.Models;
|
||||
|
||||
public class LoginPayload
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? Token { get; set; }
|
||||
public string? Username { get; set; }
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace API.Models;
|
||||
|
||||
public class UserDeck
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string Name { get; set; } = "";
|
||||
public List<string> Cards { get; set; } = [];
|
||||
public List<string> Divers { get; set; } = [];
|
||||
public string? Notes { get; set; }
|
||||
public string? Season { get; set; }
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using API.Data;
|
||||
using API.GraphQL;
|
||||
using API.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Model;
|
||||
using Npgsql;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Register HTTP Context Accessor and Token Service
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddSingleton<ITokenService, TokenService>();
|
||||
|
||||
// Configure CORS for Web / Blazor WebAssembly client
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod();
|
||||
});
|
||||
});
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
|
||||
?? builder.Configuration.GetConnectionString("Postgres")
|
||||
?? Environment.GetEnvironmentVariable("POSTGRES_CONNECTION_STRING");
|
||||
|
||||
var envPassword = Environment.GetEnvironmentVariable("PostgreSQL_Password")
|
||||
?? (OperatingSystem.IsWindows()
|
||||
? Environment.GetEnvironmentVariable("PostgreSQL_Password", EnvironmentVariableTarget.User)
|
||||
: null)
|
||||
?? (OperatingSystem.IsWindows()
|
||||
? Environment.GetEnvironmentVariable("PostgreSQL_Password", EnvironmentVariableTarget.Machine)
|
||||
: null)
|
||||
?? Environment.GetEnvironmentVariable("POSTGRESQL_PASSWORD")
|
||||
?? Environment.GetEnvironmentVariable("POSTGRES_PASSWORD");
|
||||
|
||||
if (!string.IsNullOrEmpty(connectionString))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(envPassword))
|
||||
try
|
||||
{
|
||||
var csb = new NpgsqlConnectionStringBuilder(connectionString)
|
||||
{
|
||||
Password = envPassword
|
||||
};
|
||||
connectionString = csb.ConnectionString;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(envPassword))
|
||||
{
|
||||
var csb = new NpgsqlConnectionStringBuilder
|
||||
{
|
||||
Host = "localhost",
|
||||
Database = "chrono",
|
||||
Username = "postgres",
|
||||
Password = envPassword
|
||||
};
|
||||
connectionString = csb.ConnectionString;
|
||||
}
|
||||
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(connectionString))
|
||||
options.UseNpgsql(connectionString);
|
||||
else
|
||||
options.UseInMemoryDatabase("chrono");
|
||||
});
|
||||
|
||||
// Register Hot Chocolate GraphQL Server with Nitro GraphQL IDE support
|
||||
builder.Services
|
||||
.AddGraphQLServer()
|
||||
.AddQueryType<Query>()
|
||||
.AddMutationType<Mutation>()
|
||||
.ModifyRequestOptions(opt => opt.IncludeExceptionDetails = builder.Environment.IsDevelopment());
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
try
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
if (!string.IsNullOrEmpty(connectionString) && db.Database.IsNpgsql())
|
||||
db.Database.Migrate();
|
||||
else
|
||||
db.Database.EnsureCreated();
|
||||
|
||||
// Seed default users if empty
|
||||
if (!db.Users.Any())
|
||||
{
|
||||
db.Users.AddRange(
|
||||
new User { Username = "admin", Password = "password123", CreatedAt = DateTime.UtcNow },
|
||||
new User { Username = "testuser", Password = "password123", CreatedAt = DateTime.UtcNow },
|
||||
new User { Username = "player1", Password = "password123", CreatedAt = DateTime.UtcNow }
|
||||
);
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[WARNING] Database initialization failed: {ex.Message}. Continuing without DB connection.");
|
||||
}
|
||||
}
|
||||
|
||||
app.UseCors();
|
||||
|
||||
// Map Hot Chocolate GraphQL endpoint and Nitro GraphQL Debugger
|
||||
app.MapGraphQL();
|
||||
|
||||
app.MapGet("/", () => "Chrono CCG GraphQL API");
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5256",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7266;http://localhost:5256",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace API.Services;
|
||||
|
||||
public interface ITokenService
|
||||
{
|
||||
string GenerateToken(string username);
|
||||
string? ValidateToken(string? token);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace API.Services;
|
||||
|
||||
public class TokenService : ITokenService
|
||||
{
|
||||
private readonly byte[] _secretKey;
|
||||
|
||||
public TokenService(IConfiguration? configuration = null)
|
||||
{
|
||||
var secret = configuration?["Jwt:Secret"] ?? "ChronoCCG_Super_Secret_Key_For_Jwt_Authentication_2026_Long_Key!";
|
||||
_secretKey = Encoding.UTF8.GetBytes(secret);
|
||||
}
|
||||
|
||||
public string GenerateToken(string username)
|
||||
{
|
||||
var payload = new TokenPayload
|
||||
{
|
||||
Username = username,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7).ToUnixTimeSeconds()
|
||||
};
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var payloadBytes = Encoding.UTF8.GetBytes(json);
|
||||
var payloadBase64 = Convert.ToBase64String(payloadBytes);
|
||||
|
||||
using var hmac = new HMACSHA256(_secretKey);
|
||||
var sigBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(payloadBase64));
|
||||
var sigBase64 = Convert.ToBase64String(sigBytes);
|
||||
|
||||
return $"{payloadBase64}.{sigBase64}";
|
||||
}
|
||||
|
||||
public string? ValidateToken(string? token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token)) return null;
|
||||
|
||||
var parts = token.Split('.');
|
||||
if (parts.Length != 2) return null;
|
||||
|
||||
var payloadBase64 = parts[0];
|
||||
var sigBase64 = parts[1];
|
||||
|
||||
using var hmac = new HMACSHA256(_secretKey);
|
||||
var expectedSigBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(payloadBase64));
|
||||
var expectedSigBase64 = Convert.ToBase64String(expectedSigBytes);
|
||||
|
||||
if (sigBase64 != expectedSigBase64) return null;
|
||||
|
||||
try
|
||||
{
|
||||
var payloadBytes = Convert.FromBase64String(payloadBase64);
|
||||
var json = Encoding.UTF8.GetString(payloadBytes);
|
||||
var payload = JsonSerializer.Deserialize<TokenPayload>(json);
|
||||
if (payload == null || string.IsNullOrWhiteSpace(payload.Username)) return null;
|
||||
|
||||
if (DateTimeOffset.UtcNow.ToUnixTimeSeconds() > payload.ExpiresAt) return null;
|
||||
|
||||
return payload.Username;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenPayload
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public long ExpiresAt { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Database=chrono;Username=postgres;"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #006bb7;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid #e50000;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: #e50000;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.darker-border-checkbox.form-check-input {
|
||||
border-color: #929292;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||
text-align: start;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
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,609 @@
|
||||
/*!
|
||||
* 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,608 @@
|
||||
/*!
|
||||
* 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
+13721
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
+13705
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
+4684
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,13 @@
|
||||
using Projects;
|
||||
|
||||
var builder = DistributedApplication.CreateBuilder(args);
|
||||
|
||||
var api = builder.AddProject<API>("api");
|
||||
|
||||
var dbTest = builder.AddProject<DatabaseTest>("databasetest");
|
||||
|
||||
var web = builder.AddProject<Web>("web")
|
||||
.WithReference(api)
|
||||
.WaitFor(api);
|
||||
|
||||
builder.Build().Run();
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Aspire.AppHost.Sdk/13.4.6">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>c48f3c51-1d8e-4778-aff9-7ad015fd4e02</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\API\API.csproj"/>
|
||||
<ProjectReference Include="..\DatabaseTest\DatabaseTest.csproj"/>
|
||||
<ProjectReference Include="..\Web\Web.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:17019;http://localhost:15020",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21097",
|
||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22290"
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:15020",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19107",
|
||||
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20209"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Aspire.Hosting.Dcp": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"appHost": {
|
||||
"path": "Aspire.csproj"
|
||||
}
|
||||
}
|
||||
+43
-15
@@ -20,6 +20,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{92ED65
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DatabaseTest", "DatabaseTest\DatabaseTest.csproj", "{2B162358-3332-448C-B0D6-776F9CAAD398}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{42032274-F0AF-409E-92DA-6A294BAC3CE6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire", "Aspire\Aspire.csproj", "{C2472D0A-405E-4BA2-928B-317280DBF6E7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -30,6 +34,18 @@ Global
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Release|x64.Build.0 = Release|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Release|x86.Build.0 = Release|Any CPU
|
||||
{36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{36E3775C-0E28-4EAE-AE92-4FB493E3787F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -114,28 +130,40 @@ Global
|
||||
{2B162358-3332-448C-B0D6-776F9CAAD398}.Release|x64.Build.0 = Release|Any CPU
|
||||
{2B162358-3332-448C-B0D6-776F9CAAD398}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{2B162358-3332-448C-B0D6-776F9CAAD398}.Release|x86.Build.0 = Release|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Release|x64.Build.0 = Release|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{80847188-4B2E-40C9-BCD3-2562259E60DE}.Release|x86.Build.0 = Release|Any CPU
|
||||
{42032274-F0AF-409E-92DA-6A294BAC3CE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{42032274-F0AF-409E-92DA-6A294BAC3CE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{42032274-F0AF-409E-92DA-6A294BAC3CE6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{42032274-F0AF-409E-92DA-6A294BAC3CE6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{42032274-F0AF-409E-92DA-6A294BAC3CE6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{42032274-F0AF-409E-92DA-6A294BAC3CE6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{42032274-F0AF-409E-92DA-6A294BAC3CE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{42032274-F0AF-409E-92DA-6A294BAC3CE6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{42032274-F0AF-409E-92DA-6A294BAC3CE6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{42032274-F0AF-409E-92DA-6A294BAC3CE6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{42032274-F0AF-409E-92DA-6A294BAC3CE6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{42032274-F0AF-409E-92DA-6A294BAC3CE6}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C2472D0A-405E-4BA2-928B-317280DBF6E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C2472D0A-405E-4BA2-928B-317280DBF6E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C2472D0A-405E-4BA2-928B-317280DBF6E7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C2472D0A-405E-4BA2-928B-317280DBF6E7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C2472D0A-405E-4BA2-928B-317280DBF6E7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C2472D0A-405E-4BA2-928B-317280DBF6E7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C2472D0A-405E-4BA2-928B-317280DBF6E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C2472D0A-405E-4BA2-928B-317280DBF6E7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C2472D0A-405E-4BA2-928B-317280DBF6E7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C2472D0A-405E-4BA2-928B-317280DBF6E7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C2472D0A-405E-4BA2-928B-317280DBF6E7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C2472D0A-405E-4BA2-928B-317280DBF6E7}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{BA7A908C-FEF7-4AC7-ABCE-437259A6C34C} = {A4C572E4-E709-4553-B42F-709BFFFE0A7E}
|
||||
{36E3775C-0E28-4EAE-AE92-4FB493E3787F} = {92ED656F-1D94-4348-97AD-B76094B14A8E}
|
||||
{90F32056-6983-4224-8A8C-E797C71633F3} = {92ED656F-1D94-4348-97AD-B76094B14A8E}
|
||||
{30D8468D-01E1-4D8A-99AF-EE4A75A5E956} = {92ED656F-1D94-4348-97AD-B76094B14A8E}
|
||||
{3358AF7A-603B-4BAC-A7F5-07978FB87843} = {92ED656F-1D94-4348-97AD-B76094B14A8E}
|
||||
{E5E9A799-76C8-43B3-B9C2-3CAEC2B5E1DD} = {92ED656F-1D94-4348-97AD-B76094B14A8E}
|
||||
{90F32056-6983-4224-8A8C-E797C71633F3} = {92ED656F-1D94-4348-97AD-B76094B14A8E}
|
||||
{30D8468D-01E1-4D8A-99AF-EE4A75A5E956} = {92ED656F-1D94-4348-97AD-B76094B14A8E}
|
||||
{BA7A908C-FEF7-4AC7-ABCE-437259A6C34C} = {A4C572E4-E709-4553-B42F-709BFFFE0A7E}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -10,19 +10,25 @@ public class AppDbContext : DbContext
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<CardNote> CardNotes { get; set; }
|
||||
public DbSet<UserDeck> UserDecks { get; set; }
|
||||
public DbSet<PasskeyCredential> PasskeyCredentials { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<CardNote>().HasKey(n => n.CardName);
|
||||
modelBuilder.Entity<User>(b => { b.HasKey(u => u.Username); });
|
||||
|
||||
modelBuilder.Entity<CardNote>(b => { b.HasKey(n => new { n.Username, n.CardName }); });
|
||||
|
||||
modelBuilder.Entity<UserDeck>(b =>
|
||||
{
|
||||
b.HasKey(d => d.Id);
|
||||
if (Database.IsNpgsql())
|
||||
{
|
||||
b.Property(d => d.Cards).HasColumnType("jsonb");
|
||||
b.Property(d => d.Divers).HasColumnType("jsonb");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Npgsql;
|
||||
|
||||
namespace Cloud;
|
||||
|
||||
@@ -7,8 +8,28 @@ public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
|
||||
{
|
||||
public AppDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var envPassword = Environment.GetEnvironmentVariable("PostgreSQL_Password")
|
||||
?? (OperatingSystem.IsWindows()
|
||||
? Environment.GetEnvironmentVariable("PostgreSQL_Password",
|
||||
EnvironmentVariableTarget.User)
|
||||
: null)
|
||||
?? (OperatingSystem.IsWindows()
|
||||
? Environment.GetEnvironmentVariable("PostgreSQL_Password",
|
||||
EnvironmentVariableTarget.Machine)
|
||||
: null)
|
||||
?? Environment.GetEnvironmentVariable("POSTGRESQL_PASSWORD")
|
||||
?? Environment.GetEnvironmentVariable("POSTGRES_PASSWORD");
|
||||
|
||||
var csb = new NpgsqlConnectionStringBuilder
|
||||
{
|
||||
Host = "localhost",
|
||||
Database = "chrono",
|
||||
Username = "postgres",
|
||||
Password = envPassword
|
||||
};
|
||||
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseNpgsql("Host=localhost;Database=chrono;Username=postgres;Password=postgres")
|
||||
.UseNpgsql(csb.ConnectionString)
|
||||
.Options;
|
||||
return new AppDbContext(options);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button class="btn btn-sm btn-chrono-secondary" @onclick="RefreshHealthAsync" title="Refresh Health Check">
|
||||
<button class="btn btn-sm btn-chrono-secondary" @onclick="RefreshHealthAsync"
|
||||
title="Refresh Health Check">
|
||||
<i class="bi bi-arrow-clockwise @(isChecking ? "spin" : "")"></i> Refresh
|
||||
</button>
|
||||
<a href="test-suite" class="btn btn-sm btn-chrono-primary">
|
||||
@@ -94,4 +95,5 @@
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,20 +21,20 @@ main {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
@@ -90,9 +90,9 @@ main {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
<div class="p-2 rounded bg-dark border border-secondary border-opacity-25">
|
||||
<div class="d-flex align-items-center justify-content-between mb-1">
|
||||
<span class="text-secondary small fw-semibold">PROVIDER</span>
|
||||
<span class="badge @(ConfigService.ProviderType == DatabaseProviderType.PostgreSQL ? "bg-primary" : "bg-purple")" style="font-size: 0.72rem;">
|
||||
<span
|
||||
class="badge @(ConfigService.ProviderType == DatabaseProviderType.PostgreSQL ? "bg-primary" : "bg-purple")"
|
||||
style="font-size: 0.72rem;">
|
||||
@ConfigService.ProviderType
|
||||
</span>
|
||||
</div>
|
||||
<div class="small text-truncate text-light" style="font-size: 0.78rem;" title="@ConfigService.GetActiveProviderDescription()">
|
||||
<div class="small text-truncate text-light" style="font-size: 0.78rem;"
|
||||
title="@ConfigService.GetActiveProviderDescription()">
|
||||
@(ConfigService.ProviderType == DatabaseProviderType.PostgreSQL ? "PostgreSQL Server" : "In-Memory Test DB")
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,6 +38,11 @@
|
||||
<span>User Decks</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="nav-link" href="users">
|
||||
<i class="bi bi-people-fill"></i>
|
||||
<span>Users</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="nav-link" href="passkeys">
|
||||
<i class="bi bi-key-fill"></i>
|
||||
<span>Passkeys</span>
|
||||
@@ -59,6 +67,7 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
ConfigService.OnConfigurationChanged += HandleConfigChanged;
|
||||
@@ -73,4 +82,5 @@
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
@@ -51,15 +51,15 @@
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link {
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #d7d7d7;
|
||||
background: none;
|
||||
border: none;
|
||||
@@ -69,15 +69,15 @@
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
background-color: rgba(255, 255, 255, 0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,11 @@
|
||||
opacity: 0;
|
||||
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
|
||||
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
|
||||
&[open]
|
||||
|
||||
{
|
||||
&[open] {
|
||||
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -96,13 +95,13 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:hover {
|
||||
#components-reconnect-modal button:hover {
|
||||
background-color: #3b6ea2;
|
||||
}
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:active {
|
||||
#components-reconnect-modal button:active {
|
||||
background-color: #6b9ed2;
|
||||
}
|
||||
}
|
||||
|
||||
.components-rejoining-animation {
|
||||
position: relative;
|
||||
@@ -110,17 +109,17 @@
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div {
|
||||
.components-rejoining-animation div {
|
||||
position: absolute;
|
||||
border: 3px solid #0087ff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-rejoining-animation {
|
||||
0% {
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 gap-3">
|
||||
<div class="input-group" style="max-width: 400px;">
|
||||
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control form-control-chrono" placeholder="Search by card name or note content..."
|
||||
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterNotes" />
|
||||
<input type="text" class="form-control form-control-chrono"
|
||||
placeholder="Search by card name or note content..."
|
||||
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterNotes"/>
|
||||
@if (!string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
<button class="btn btn-outline-secondary" @onclick="ClearSearch"><i class="bi bi-x-lg"></i></button>
|
||||
@@ -73,7 +74,8 @@
|
||||
<table class="chrono-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%;">Card Name (Primary Key)</th>
|
||||
<th style="width: 20%;">User</th>
|
||||
<th style="width: 25%;">Card Name</th>
|
||||
<th>Note Content</th>
|
||||
<th style="width: 140px;" class="text-end">Actions</th>
|
||||
</tr>
|
||||
@@ -82,6 +84,10 @@
|
||||
@foreach (var note in notes)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<span
|
||||
class="badge bg-secondary font-monospace">@(string.IsNullOrEmpty(note.Username) ? "global" : note.Username)</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold text-white d-flex align-items-center gap-2">
|
||||
<i class="bi bi-card-heading text-primary"></i>
|
||||
@@ -93,10 +99,12 @@
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenEditModal(note)" title="Edit Note">
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenEditModal(note)"
|
||||
title="Edit Note">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(note)" title="Delete Note">
|
||||
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(note)"
|
||||
title="Delete Note">
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -122,24 +130,44 @@
|
||||
<button type="button" class="btn-close btn-close-white" @onclick="CloseModal"></button>
|
||||
</div>
|
||||
<div class="modal-body-custom">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary small fw-bold">USERNAME</label>
|
||||
@if (isEditing)
|
||||
{
|
||||
<input type="text" class="form-control form-control-chrono" value="@currentNote.Username"
|
||||
disabled/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="text" class="form-control form-control-chrono"
|
||||
placeholder="Enter username (e.g. player1)..." @bind="currentNote.Username"/>
|
||||
<div class="form-text text-secondary">The username this note is associated with.</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary small fw-bold">CARD NAME</label>
|
||||
@if (isEditing)
|
||||
{
|
||||
<input type="text" class="form-control form-control-chrono" value="@currentNote.CardName" disabled />
|
||||
<div class="form-text text-secondary">Card name acts as the primary key and cannot be altered.</div>
|
||||
<input type="text" class="form-control form-control-chrono" value="@currentNote.CardName"
|
||||
disabled/>
|
||||
<div class="form-text text-secondary">Card name acts as part of the primary key and cannot be
|
||||
altered.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="text" class="form-control form-control-chrono" list="cardSuggestions"
|
||||
placeholder="Enter or select card name..." @bind="currentNote.CardName" />
|
||||
placeholder="Enter or select card name..." @bind="currentNote.CardName"/>
|
||||
<datalist id="cardSuggestions">
|
||||
@foreach (var c in availableCardNames.Take(50))
|
||||
{
|
||||
<option value="@c" />
|
||||
<option value="@c"/>
|
||||
}
|
||||
</datalist>
|
||||
<div class="form-text text-secondary">Select an existing card from the database or enter a custom name.</div>
|
||||
<div class="form-text text-secondary">Select an existing card from the database or enter a
|
||||
custom name.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -170,17 +198,20 @@
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
Confirm Delete Note
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" @onclick="() => showDeleteModal = false"></button>
|
||||
<button type="button" class="btn-close btn-close-white"
|
||||
@onclick="() => showDeleteModal = false"></button>
|
||||
</div>
|
||||
<div class="modal-body-custom">
|
||||
<p class="text-white mb-2">Are you sure you want to delete the note for:</p>
|
||||
<div class="p-2 rounded bg-dark border border-secondary fw-bold text-primary mb-3">
|
||||
@noteToDelete.CardName
|
||||
</div>
|
||||
<p class="text-secondary small mb-0">This action will remove the record from the database permanently.</p>
|
||||
<p class="text-secondary small mb-0">This action will remove the record from the database
|
||||
permanently.</p>
|
||||
</div>
|
||||
<div class="modal-footer-custom">
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showDeleteModal = false">Cancel</button>
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showDeleteModal = false">Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-chrono-danger" @onclick="ConfirmDeleteAsync" disabled="@isSaving">
|
||||
@(isSaving ? "Deleting..." : "Delete Permanently")
|
||||
</button>
|
||||
@@ -194,7 +225,7 @@
|
||||
private List<string> availableCardNames = [];
|
||||
private string searchTerm = "";
|
||||
private bool isLoading = true;
|
||||
private bool isSaving = false;
|
||||
private bool isSaving;
|
||||
private string? alertMessage;
|
||||
private bool alertIsSuccess;
|
||||
|
||||
@@ -272,6 +303,7 @@
|
||||
isEditing = true;
|
||||
currentNote = new CardNote
|
||||
{
|
||||
Username = note.Username,
|
||||
CardName = note.CardName,
|
||||
Note = note.Note
|
||||
};
|
||||
@@ -317,7 +349,7 @@
|
||||
if (noteToDelete == null) return;
|
||||
|
||||
isSaving = true;
|
||||
var result = await DbService.DeleteCardNoteAsync(noteToDelete.CardName);
|
||||
var result = await DbService.DeleteCardNoteAsync(noteToDelete.CardName, noteToDelete.Username);
|
||||
isSaving = false;
|
||||
|
||||
alertIsSuccess = result.Success;
|
||||
@@ -340,4 +372,5 @@
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
private int currentCount;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold mb-1 text-white">Database Settings & Migrations</h2>
|
||||
<p class="text-secondary mb-0">Configure database providers, update connection strings, inspect EF Core migrations, and run maintenance tasks.</p>
|
||||
<p class="text-secondary mb-0">Configure database providers, update connection strings, inspect EF Core
|
||||
migrations, and run maintenance tasks.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-chrono-secondary" @onclick="RefreshAsync">
|
||||
@@ -38,11 +39,13 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary small fw-bold">ACTIVE PROVIDER</label>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn flex-fill @(selectedProvider == DatabaseProviderType.PostgreSQL ? "btn-chrono-primary" : "btn-chrono-secondary")"
|
||||
<button
|
||||
class="btn flex-fill @(selectedProvider == DatabaseProviderType.PostgreSQL ? "btn-chrono-primary" : "btn-chrono-secondary")"
|
||||
@onclick="() => SelectProvider(DatabaseProviderType.PostgreSQL)">
|
||||
<i class="bi bi-database me-1"></i> PostgreSQL (Cloud DB)
|
||||
</button>
|
||||
<button class="btn flex-fill @(selectedProvider == DatabaseProviderType.InMemory ? "btn-chrono-primary" : "btn-chrono-secondary")"
|
||||
<button
|
||||
class="btn flex-fill @(selectedProvider == DatabaseProviderType.InMemory ? "btn-chrono-primary" : "btn-chrono-secondary")"
|
||||
@onclick="() => SelectProvider(DatabaseProviderType.InMemory)">
|
||||
<i class="bi bi-cpu me-1"></i> In-Memory Test DB
|
||||
</button>
|
||||
@@ -80,7 +83,7 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary small fw-bold">IN-MEMORY DATABASE INSTANCE NAME</label>
|
||||
<input type="text" class="form-control form-control-chrono font-monospace"
|
||||
@bind="inMemoryNameInput" />
|
||||
@bind="inMemoryNameInput"/>
|
||||
<div class="form-text text-secondary">
|
||||
In-memory EF Core provider creates an isolated database in RAM for instant testing.
|
||||
</div>
|
||||
@@ -112,29 +115,34 @@
|
||||
@if (health != null)
|
||||
{
|
||||
<div class="d-flex flex-column gap-2 mb-4">
|
||||
<div class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<span class="text-secondary small">Connection Status</span>
|
||||
<span class="fw-bold @(health.IsConnected ? "text-success" : "text-danger")">
|
||||
@(health.IsConnected ? "Connected" : "Disconnected")
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<span class="text-secondary small">Query Latency</span>
|
||||
<span class="text-white font-monospace">@(health.ResponseTimeMs) ms</span>
|
||||
</div>
|
||||
|
||||
<div class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<span class="text-secondary small">CardNotes Table Rows</span>
|
||||
<span class="text-primary fw-bold">@health.CardNotesCount</span>
|
||||
</div>
|
||||
|
||||
<div class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<span class="text-secondary small">UserDecks Table Rows</span>
|
||||
<span class="text-success fw-bold">@health.UserDecksCount</span>
|
||||
</div>
|
||||
|
||||
<div class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-secondary d-flex justify-content-between align-items-center">
|
||||
<span class="text-secondary small">PasskeyCredentials Table Rows</span>
|
||||
<span class="text-purple fw-bold">@health.PasskeysCount</span>
|
||||
</div>
|
||||
@@ -162,7 +170,8 @@
|
||||
@if (migrationReport?.SupportsMigrations == true && migrationReport.PendingMigrations.Any())
|
||||
{
|
||||
<button class="btn btn-chrono-primary btn-sm" @onclick="ApplyMigrationsAsync" disabled="@isActionBusy">
|
||||
<i class="bi bi-arrow-up-circle-fill me-1"></i> Apply Pending Migrations (@migrationReport.PendingMigrations.Count)
|
||||
<i class="bi bi-arrow-up-circle-fill me-1"></i> Apply Pending Migrations
|
||||
(@migrationReport.PendingMigrations.Count)
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@@ -170,14 +179,16 @@
|
||||
@if (migrationReport == null)
|
||||
{
|
||||
<div class="text-center py-4 text-secondary">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div> Loading migration report...
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
Loading migration report...
|
||||
</div>
|
||||
}
|
||||
else if (!migrationReport.SupportsMigrations)
|
||||
{
|
||||
<div class="p-3 bg-dark rounded border border-secondary text-secondary small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Migrations are managed via relational schema (PostgreSQL). The active in-memory provider does not use relational migration tables.
|
||||
Migrations are managed via relational schema (PostgreSQL). The active in-memory provider does not use
|
||||
relational migration tables.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -200,7 +211,8 @@
|
||||
<div class="d-flex flex-column gap-2" style="max-height: 250px; overflow-y: auto;">
|
||||
@foreach (var m in migrationReport.AppliedMigrations)
|
||||
{
|
||||
<div class="p-2 rounded bg-dark border border-success border-opacity-50 d-flex align-items-center justify-content-between">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-success border-opacity-50 d-flex align-items-center justify-content-between">
|
||||
<span class="text-white small font-monospace">@m</span>
|
||||
<span class="badge bg-success"><i class="bi bi-check"></i> Applied</span>
|
||||
</div>
|
||||
@@ -218,7 +230,8 @@
|
||||
@if (!migrationReport.PendingMigrations.Any())
|
||||
{
|
||||
<div class="p-3 bg-dark rounded border border-success border-opacity-25 text-success small">
|
||||
<i class="bi bi-shield-check me-1"></i> All migrations are applied. Schema is in sync with AppDbContext.
|
||||
<i class="bi bi-shield-check me-1"></i> All migrations are applied. Schema is in sync with
|
||||
AppDbContext.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -226,7 +239,8 @@
|
||||
<div class="d-flex flex-column gap-2" style="max-height: 250px; overflow-y: auto;">
|
||||
@foreach (var m in migrationReport.PendingMigrations)
|
||||
{
|
||||
<div class="p-2 rounded bg-dark border border-warning border-opacity-50 d-flex align-items-center justify-content-between">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-warning border-opacity-50 d-flex align-items-center justify-content-between">
|
||||
<span class="text-warning small font-monospace">@m</span>
|
||||
<span class="badge bg-warning text-dark"><i class="bi bi-exclamation-triangle"></i> Pending</span>
|
||||
</div>
|
||||
@@ -357,4 +371,5 @@
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,12 +15,14 @@
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that
|
||||
occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong>
|
||||
environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@@ -30,7 +32,9 @@
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,8 @@
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold mb-1 text-white">Database Test Dashboard</h2>
|
||||
<p class="text-secondary mb-0">Monitor, manage, and validate all database features referenced in Chrono Cloud</p>
|
||||
<p class="text-secondary mb-0">Monitor, manage, and validate all database features referenced in Chrono
|
||||
Cloud</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-chrono-secondary" @onclick="RefreshAsync" disabled="@isLoading">
|
||||
@@ -78,49 +79,66 @@
|
||||
|
||||
<!-- Metrics Row -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="col-md">
|
||||
<a href="card-notes" class="text-decoration-none">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon-box metric-icon-blue">
|
||||
<i class="bi bi-card-text"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="metric-value">@(health?.IsConnected == true ? health.CardNotesCount.ToString() : "-")</div>
|
||||
<div
|
||||
class="metric-value">@(health?.IsConnected == true ? health.CardNotesCount.ToString() : "-")</div>
|
||||
<div class="metric-label">Card Notes</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="col-md">
|
||||
<a href="users" class="text-decoration-none">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon-box metric-icon-yellow">
|
||||
<i class="bi bi-people-fill"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="metric-value">@(health?.IsConnected == true ? health.UsersCount.ToString() : "-")</div>
|
||||
<div class="metric-label">Users</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md">
|
||||
<a href="user-decks" class="text-decoration-none">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon-box metric-icon-green">
|
||||
<i class="bi bi-collection-play"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="metric-value">@(health?.IsConnected == true ? health.UserDecksCount.ToString() : "-")</div>
|
||||
<div
|
||||
class="metric-value">@(health?.IsConnected == true ? health.UserDecksCount.ToString() : "-")</div>
|
||||
<div class="metric-label">User Decks</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="col-md">
|
||||
<a href="passkeys" class="text-decoration-none">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon-box metric-icon-purple">
|
||||
<i class="bi bi-key-fill"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="metric-value">@(health?.IsConnected == true ? health.PasskeysCount.ToString() : "-")</div>
|
||||
<div
|
||||
class="metric-value">@(health?.IsConnected == true ? health.PasskeysCount.ToString() : "-")</div>
|
||||
<div class="metric-label">Passkeys</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="col-md">
|
||||
<a href="diagnostics" class="text-decoration-none">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon-box metric-icon-yellow">
|
||||
@@ -132,7 +150,7 @@
|
||||
? $"{health.AppliedMigrationsCount} / {health.AppliedMigrationsCount + health.PendingMigrationsCount}"
|
||||
: "N/A")
|
||||
</div>
|
||||
<div class="metric-label">Applied Migrations</div>
|
||||
<div class="metric-label">Migrations</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -158,7 +176,8 @@
|
||||
<a href="card-notes" class="btn btn-sm btn-chrono-secondary">Manage Notes</a>
|
||||
</div>
|
||||
<p class="text-secondary small mb-0">
|
||||
Tracks developer and user strategic card notes with primary key indexing on <code>CardName</code>. Supports live search, adding, updating, and deletion.
|
||||
Tracks developer and user strategic card notes with primary key indexing on
|
||||
<code>CardName</code>. Supports live search, adding, updating, and deletion.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -168,7 +187,8 @@
|
||||
<a href="user-decks" class="btn btn-sm btn-chrono-secondary">Manage Decks</a>
|
||||
</div>
|
||||
<p class="text-secondary small mb-0">
|
||||
Stores full player decks with native PostgreSQL <code>jsonb</code> mapping for <code>Cards</code> list (with counts) and <code>Divers</code> list.
|
||||
Stores full player decks with native PostgreSQL <code>jsonb</code> mapping for
|
||||
<code>Cards</code> list (with counts) and <code>Divers</code> list.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -178,7 +198,8 @@
|
||||
<a href="passkeys" class="btn btn-sm btn-chrono-secondary">Manage Passkeys</a>
|
||||
</div>
|
||||
<p class="text-secondary small mb-0">
|
||||
Stores WebAuthn / FIDO2 binary credentials (raw <code>byte[]</code> for CredentialId, PublicKey, UserHandle) with counter verification.
|
||||
Stores WebAuthn / FIDO2 binary credentials (raw <code>byte[]</code> for CredentialId, PublicKey,
|
||||
UserHandle) with counter verification.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,37 +216,47 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<button class="btn btn-chrono-primary py-2 text-start d-flex align-items-center justify-content-between" @onclick="SeedDataAsync" disabled="@isActionBusy">
|
||||
<button class="btn btn-chrono-primary py-2 text-start d-flex align-items-center justify-content-between"
|
||||
@onclick="SeedDataAsync" disabled="@isActionBusy">
|
||||
<span><i class="bi bi-box-arrow-in-down me-2"></i> Seed Realistic Sample Data</span>
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between" @onclick="ApplyMigrationsAsync" disabled="@isActionBusy">
|
||||
<button
|
||||
class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between"
|
||||
@onclick="ApplyMigrationsAsync" disabled="@isActionBusy">
|
||||
<span><i class="bi bi-database-check me-2"></i> Apply Pending Migrations</span>
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</button>
|
||||
|
||||
@if (ConfigService.ProviderType == DatabaseProviderType.PostgreSQL)
|
||||
{
|
||||
<button class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between" @onclick="SwitchToInMemoryAsync">
|
||||
<button
|
||||
class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between"
|
||||
@onclick="SwitchToInMemoryAsync">
|
||||
<span><i class="bi bi-cpu me-2"></i> Switch to In-Memory DB</span>
|
||||
<span class="badge bg-purple">Instant</span>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between" @onclick="SwitchToPostgresAsync">
|
||||
<button
|
||||
class="btn btn-chrono-secondary py-2 text-start d-flex align-items-center justify-content-between"
|
||||
@onclick="SwitchToPostgresAsync">
|
||||
<span><i class="bi bi-database me-2"></i> Switch to PostgreSQL</span>
|
||||
<span class="badge bg-primary">Live</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<a href="test-suite" class="btn btn-chrono-outline-primary py-2 text-start d-flex align-items-center justify-content-between">
|
||||
<a href="test-suite"
|
||||
class="btn btn-chrono-outline-primary py-2 text-start d-flex align-items-center justify-content-between">
|
||||
<span><i class="bi bi-clipboard2-check me-2"></i> Open Test Suite Runner</span>
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
|
||||
<button class="btn btn-chrono-danger py-2 text-start d-flex align-items-center justify-content-between mt-2" @onclick="ResetDatabaseAsync" disabled="@isActionBusy">
|
||||
<button
|
||||
class="btn btn-chrono-danger py-2 text-start d-flex align-items-center justify-content-between mt-2"
|
||||
@onclick="ResetDatabaseAsync" disabled="@isActionBusy">
|
||||
<span><i class="bi bi-trash3-fill me-2"></i> Wipe & Reset Database</span>
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
</button>
|
||||
@@ -310,4 +341,5 @@
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
@page "/passkeys"
|
||||
@implements IDisposable
|
||||
@using System.Security.Cryptography
|
||||
@implements IDisposable
|
||||
|
||||
<PageTitle>Passkeys Management - Chrono DB</PageTitle>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold mb-1 text-white">Passkey Credentials Database (`PasskeyCredential`)</h2>
|
||||
<p class="text-secondary mb-0">Browse, add, edit, and delete WebAuthn / FIDO2 security credentials with raw binary byte arrays.</p>
|
||||
<p class="text-secondary mb-0">Browse, add, edit, and delete WebAuthn / FIDO2 security credentials with raw
|
||||
binary byte arrays.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-chrono-secondary" @onclick="LoadPasskeysAsync">
|
||||
@@ -32,8 +33,9 @@
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 gap-3">
|
||||
<div class="input-group" style="max-width: 400px;">
|
||||
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control form-control-chrono" placeholder="Search by AaGuid, ID, or SignCount..."
|
||||
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterPasskeys" />
|
||||
<input type="text" class="form-control form-control-chrono"
|
||||
placeholder="Search by AaGuid, ID, or SignCount..."
|
||||
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterPasskeys"/>
|
||||
@if (!string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
<button class="btn btn-outline-secondary" @onclick="ClearSearch"><i class="bi bi-x-lg"></i></button>
|
||||
@@ -90,30 +92,38 @@
|
||||
<span class="badge bg-dark border border-secondary text-white">#@p.Id</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-white small font-monospace">@(string.IsNullOrEmpty(p.AaGuid) ? "(None)" : p.AaGuid)</div>
|
||||
<div
|
||||
class="text-white small font-monospace">@(string.IsNullOrEmpty(p.AaGuid) ? "(None)" : p.AaGuid)</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-purple">@p.SignCount</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-secondary small font-monospace text-truncate" style="max-width: 200px;" title="@Convert.ToHexString(p.CredentialId)">
|
||||
@Convert.ToHexString(p.CredentialId)[..Math.Min(16, p.CredentialId.Length * 2)]... (@p.CredentialId.Length bytes)
|
||||
<div class="text-secondary small font-monospace text-truncate" style="max-width: 200px;"
|
||||
title="@Convert.ToHexString(p.CredentialId)">
|
||||
@Convert.ToHexString(p.CredentialId)[..Math.Min(16, p.CredentialId.Length * 2)]...
|
||||
(@p.CredentialId.Length bytes)
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-secondary small font-monospace text-truncate" style="max-width: 200px;" title="@Convert.ToHexString(p.PublicKey)">
|
||||
@Convert.ToHexString(p.PublicKey)[..Math.Min(16, p.PublicKey.Length * 2)]... (@p.PublicKey.Length bytes)
|
||||
<div class="text-secondary small font-monospace text-truncate" style="max-width: 200px;"
|
||||
title="@Convert.ToHexString(p.PublicKey)">
|
||||
@Convert.ToHexString(p.PublicKey)[..Math.Min(16, p.PublicKey.Length * 2)]...
|
||||
(@p.PublicKey.Length bytes)
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenViewModal(p)" title="View Binary Details">
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenViewModal(p)"
|
||||
title="View Binary Details">
|
||||
<i class="bi bi-eye-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenEditModal(p)" title="Edit Passkey">
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenEditModal(p)"
|
||||
title="Edit Passkey">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(p)" title="Delete Passkey">
|
||||
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(p)"
|
||||
title="Delete Passkey">
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -151,13 +161,14 @@
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label text-secondary small fw-bold">AAGUID</label>
|
||||
<input type="text" class="form-control form-control-chrono font-monospace" placeholder="e.g. 00000000-0000-0000-0000-000000000000"
|
||||
@bind="currentPasskey.AaGuid" />
|
||||
<input type="text" class="form-control form-control-chrono font-monospace"
|
||||
placeholder="e.g. 00000000-0000-0000-0000-000000000000"
|
||||
@bind="currentPasskey.AaGuid"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary small fw-bold">SIGN COUNT</label>
|
||||
<input type="number" class="form-control form-control-chrono" min="0"
|
||||
@bind="currentPasskey.SignCount" />
|
||||
@bind="currentPasskey.SignCount"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -188,7 +199,7 @@
|
||||
</div>
|
||||
<input type="text" class="form-control form-control-chrono font-monospace"
|
||||
placeholder="e.g. DEADBEAF01020304"
|
||||
@bind="userHandleHex" />
|
||||
@bind="userHandleHex"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer-custom">
|
||||
@@ -254,7 +265,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer-custom">
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showViewModal = false">Close</button>
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showViewModal = false">Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -270,17 +282,20 @@
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
Confirm Delete Passkey
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" @onclick="() => showDeleteModal = false"></button>
|
||||
<button type="button" class="btn-close btn-close-white"
|
||||
@onclick="() => showDeleteModal = false"></button>
|
||||
</div>
|
||||
<div class="modal-body-custom">
|
||||
<p class="text-white mb-2">Are you sure you want to delete passkey credential:</p>
|
||||
<div class="p-2 rounded bg-dark border border-secondary fw-bold text-primary mb-3 font-monospace">
|
||||
Passkey #@passkeyToDelete.Id (AAGUID: @(passkeyToDelete.AaGuid ?? "N/A"))
|
||||
</div>
|
||||
<p class="text-secondary small mb-0">This will permanently delete the binary security credential from the database.</p>
|
||||
<p class="text-secondary small mb-0">This will permanently delete the binary security credential from
|
||||
the database.</p>
|
||||
</div>
|
||||
<div class="modal-footer-custom">
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showDeleteModal = false">Cancel</button>
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showDeleteModal = false">Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-chrono-danger" @onclick="ConfirmDeleteAsync" disabled="@isSaving">
|
||||
@(isSaving ? "Deleting..." : "Delete Passkey")
|
||||
</button>
|
||||
@@ -293,18 +308,20 @@
|
||||
private List<PasskeyCredential> passkeys = [];
|
||||
private string searchTerm = "";
|
||||
private bool isLoading = true;
|
||||
private bool isSaving = false;
|
||||
private bool isSaving;
|
||||
private string? alertMessage;
|
||||
private bool alertIsSuccess;
|
||||
|
||||
private bool showModal;
|
||||
private bool isEditing;
|
||||
|
||||
private PasskeyCredential currentPasskey = new()
|
||||
{
|
||||
CredentialId = [],
|
||||
PublicKey = [],
|
||||
UserHandle = []
|
||||
};
|
||||
|
||||
private string credIdHex = "";
|
||||
private string pubKeyHex = "";
|
||||
private string userHandleHex = "";
|
||||
@@ -429,12 +446,14 @@
|
||||
alertMessage = "Invalid Credential ID hex string (must be even length).";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cleanPub) || cleanPub.Length % 2 != 0)
|
||||
{
|
||||
alertIsSuccess = false;
|
||||
alertMessage = "Invalid Public Key hex string (must be even length).";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cleanUser) || cleanUser.Length % 2 != 0)
|
||||
{
|
||||
alertIsSuccess = false;
|
||||
@@ -506,4 +525,5 @@
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold mb-1 text-white">Database Feature Test Suite</h2>
|
||||
<p class="text-secondary mb-0">Automated end-to-end verification of all database features, constraints, and JSONB mappings referenced in Chrono Cloud.</p>
|
||||
<p class="text-secondary mb-0">Automated end-to-end verification of all database features, constraints, and
|
||||
JSONB mappings referenced in Chrono Cloud.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-chrono-secondary" @onclick="ClearResults" disabled="@isRunning">
|
||||
@@ -47,7 +48,8 @@
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon-box @(testResults.Any(t => t.Status == TestStatus.Failed) ? "metric-icon-red" : "metric-icon-purple")">
|
||||
<div
|
||||
class="metric-icon-box @(testResults.Any(t => t.Status == TestStatus.Failed) ? "metric-icon-red" : "metric-icon-purple")">
|
||||
<i class="bi @(testResults.Any(t => t.Status == TestStatus.Failed) ? "bi-x-circle-fill text-danger" : "bi-shield-check")"></i>
|
||||
</div>
|
||||
<div>
|
||||
@@ -94,7 +96,8 @@
|
||||
</h5>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="text-secondary small">Target Provider:</span>
|
||||
<span class="badge @(ConfigService.ProviderType == DatabaseProviderType.PostgreSQL ? "bg-primary" : "bg-purple")">
|
||||
<span
|
||||
class="badge @(ConfigService.ProviderType == DatabaseProviderType.PostgreSQL ? "bg-primary" : "bg-purple")">
|
||||
@ConfigService.ProviderType
|
||||
</span>
|
||||
</div>
|
||||
@@ -121,7 +124,8 @@
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-secondary font-monospace">@test.Id</span>
|
||||
<span class="fw-bold text-white fs-6">@test.Name</span>
|
||||
<span class="badge bg-dark border border-secondary text-secondary small">@test.Category</span>
|
||||
<span
|
||||
class="badge bg-dark border border-secondary text-secondary small">@test.Category</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
@@ -187,7 +191,7 @@
|
||||
|
||||
@code {
|
||||
private List<DatabaseTestCaseResult> testResults = [];
|
||||
private HashSet<string> expandedTestLogs = [];
|
||||
private readonly HashSet<string> expandedTestLogs = [];
|
||||
private bool isRunning;
|
||||
private string currentProgressStatus = "";
|
||||
|
||||
@@ -240,24 +244,31 @@
|
||||
expandedTestLogs.Add(testId);
|
||||
}
|
||||
|
||||
private static string GetBorderClass(TestStatus status) => status switch
|
||||
private static string GetBorderClass(TestStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
TestStatus.Passed => "border-success border-opacity-50",
|
||||
TestStatus.Failed => "border-danger border-opacity-75",
|
||||
TestStatus.Running => "border-primary border-opacity-50",
|
||||
_ => "border-secondary border-opacity-25"
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetBadgeClass(TestStatus status) => status switch
|
||||
private static string GetBadgeClass(TestStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
TestStatus.Passed => "bg-success",
|
||||
TestStatus.Failed => "bg-danger",
|
||||
TestStatus.Running => "bg-primary",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
@page "/user-decks"
|
||||
@implements IDisposable
|
||||
@using System.Text.Json
|
||||
@implements IDisposable
|
||||
|
||||
<PageTitle>User Decks Management - Chrono DB</PageTitle>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold mb-1 text-white">User Decks Database (`UserDeck`)</h2>
|
||||
<p class="text-secondary mb-0">Browse, add, edit, and delete deck builds with native JSONB collection mappings.</p>
|
||||
<p class="text-secondary mb-0">Browse, add, edit, and delete deck builds with native JSONB collection
|
||||
mappings.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-chrono-secondary" @onclick="LoadDecksAsync">
|
||||
@@ -21,19 +22,20 @@
|
||||
|
||||
@if (!string.IsNullOrEmpty(alertMessage))
|
||||
{
|
||||
<div class="alert @(alertIsSuccess ? "alert-success" : "alert-danger") alert-dismissible fade show" role="alert">
|
||||
<div class="alert @(alertIsSuccess ? "alert-success" : "alert-danger") alert-dismissible fade show" role="alert">
|
||||
<i class="bi @(alertIsSuccess ? "bi-check-circle-fill" : "bi-exclamation-triangle-fill") me-2"></i>
|
||||
@alertMessage
|
||||
<button type="button" class="btn-close" @onclick="() => alertMessage = null"></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="chrono-card">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 gap-3">
|
||||
<div class="input-group" style="max-width: 400px;">
|
||||
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control form-control-chrono" placeholder="Search decks by name, season, or notes..."
|
||||
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterDecks" />
|
||||
<input type="text" class="form-control form-control-chrono"
|
||||
placeholder="Search decks by name, season, or notes..."
|
||||
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterDecks"/>
|
||||
@if (!string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
<button class="btn btn-outline-secondary" @onclick="ClearSearch"><i class="bi bi-x-lg"></i></button>
|
||||
@@ -96,7 +98,8 @@
|
||||
<div class="text-secondary" style="font-size: 0.75rem;">ID: @deck.Id</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">@(string.IsNullOrEmpty(deck.Season) ? "General" : deck.Season)</span>
|
||||
<span
|
||||
class="badge bg-secondary">@(string.IsNullOrEmpty(deck.Season) ? "General" : deck.Season)</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary" title="@($"{uniqueCards} unique card type(s)")">
|
||||
@@ -104,7 +107,8 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge @(diverCount > 0 ? "bg-purple" : "bg-dark border border-secondary text-secondary")">
|
||||
<span
|
||||
class="badge @(diverCount > 0 ? "bg-purple" : "bg-dark border border-secondary text-secondary")">
|
||||
<i class="bi bi-person-fill me-1"></i> @diverCount
|
||||
</span>
|
||||
</td>
|
||||
@@ -120,13 +124,15 @@
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenViewModal(deck)" title="View JSON & Breakdown">
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenViewModal(deck)"
|
||||
title="View JSON & Breakdown">
|
||||
<i class="bi bi-eye-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenEditModal(deck)" title="Edit Deck">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(deck)" title="Delete Deck">
|
||||
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(deck)"
|
||||
title="Delete Deck">
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -142,7 +148,7 @@
|
||||
<!-- Add / Edit Modal -->
|
||||
@if (showModal)
|
||||
{
|
||||
<div class="modal-backdrop-custom">
|
||||
<div class="modal-backdrop-custom">
|
||||
<div class="modal-dialog-custom" style="max-width: 750px;">
|
||||
<div class="modal-header-custom">
|
||||
<h5 class="modal-title-custom">
|
||||
@@ -156,12 +162,12 @@
|
||||
<div class="col-md-8">
|
||||
<label class="form-label text-secondary small fw-bold">DECK NAME</label>
|
||||
<input type="text" class="form-control form-control-chrono" placeholder="Enter deck name..."
|
||||
@bind="currentDeck.Name" />
|
||||
@bind="currentDeck.Name"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-secondary small fw-bold">SEASON</label>
|
||||
<input type="text" class="form-control form-control-chrono" placeholder="e.g. Season 1, Tournament"
|
||||
@bind="currentDeck.Season" />
|
||||
@bind="currentDeck.Season"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -185,11 +191,11 @@
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-7">
|
||||
<input type="text" class="form-control form-control-chrono form-control-sm"
|
||||
list="deckCardSuggestions" placeholder="Card name..." @bind="newCardName" />
|
||||
list="deckCardSuggestions" placeholder="Card name..." @bind="newCardName"/>
|
||||
<datalist id="deckCardSuggestions">
|
||||
@foreach (var c in allCardNames.Take(50))
|
||||
{
|
||||
<option value="@c" />
|
||||
<option value="@c"/>
|
||||
}
|
||||
</datalist>
|
||||
</div>
|
||||
@@ -210,7 +216,8 @@
|
||||
<!-- Current Cards List (Grouped for display) -->
|
||||
@if (currentDeck.Cards == null || !currentDeck.Cards.Any())
|
||||
{
|
||||
<div class="text-secondary small fst-italic p-2 bg-dark rounded border border-secondary border-opacity-25">
|
||||
<div
|
||||
class="text-secondary small fst-italic p-2 bg-dark rounded border border-secondary border-opacity-25">
|
||||
No cards added yet. Use the selector above to add cards to this deck.
|
||||
</div>
|
||||
}
|
||||
@@ -224,10 +231,12 @@
|
||||
<div class="badge bg-dark border border-secondary p-2 d-flex align-items-center gap-2">
|
||||
<span class="text-white">@cardName</span>
|
||||
<span class="badge bg-primary">x@count</span>
|
||||
<button type="button" class="btn btn-sm p-0 text-secondary" title="Remove one" @onclick="() => RemoveOneCardFromDeck(cardName)">
|
||||
<button type="button" class="btn btn-sm p-0 text-secondary" title="Remove one"
|
||||
@onclick="() => RemoveOneCardFromDeck(cardName)">
|
||||
<i class="bi bi-dash"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm p-0 text-danger" title="Remove all" @onclick="() => RemoveAllCardsFromDeck(cardName)">
|
||||
<button type="button" class="btn btn-sm p-0 text-danger" title="Remove all"
|
||||
@onclick="() => RemoveAllCardsFromDeck(cardName)">
|
||||
<i class="bi bi-x-circle-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -248,11 +257,11 @@
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-md-9">
|
||||
<input type="text" class="form-control form-control-chrono form-control-sm"
|
||||
list="diverSuggestions" placeholder="Agent / Diver name..." @bind="newDiverName" />
|
||||
list="diverSuggestions" placeholder="Agent / Diver name..." @bind="newDiverName"/>
|
||||
<datalist id="diverSuggestions">
|
||||
@foreach (var c in agentCardNames.Take(40))
|
||||
{
|
||||
<option value="@c" />
|
||||
<option value="@c"/>
|
||||
}
|
||||
</datalist>
|
||||
</div>
|
||||
@@ -265,7 +274,8 @@
|
||||
|
||||
@if (currentDeck.Divers == null || !currentDeck.Divers.Any())
|
||||
{
|
||||
<div class="text-secondary small fst-italic p-2 bg-dark rounded border border-secondary border-opacity-25">
|
||||
<div
|
||||
class="text-secondary small fst-italic p-2 bg-dark rounded border border-secondary border-opacity-25">
|
||||
No divers specified for this deck.
|
||||
</div>
|
||||
}
|
||||
@@ -276,7 +286,8 @@
|
||||
{
|
||||
<div class="badge bg-dark border border-purple p-2 d-flex align-items-center gap-2">
|
||||
<span class="text-purple">@diver</span>
|
||||
<button type="button" class="btn btn-sm p-0 text-danger" @onclick="() => RemoveDiverFromDeck(diver)">
|
||||
<button type="button" class="btn btn-sm p-0 text-danger"
|
||||
@onclick="() => RemoveDiverFromDeck(diver)">
|
||||
<i class="bi bi-x-circle-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -292,13 +303,13 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- View JSON & Breakdown Modal -->
|
||||
@if (showViewModal && viewedDeck != null)
|
||||
{
|
||||
<div class="modal-backdrop-custom">
|
||||
<div class="modal-backdrop-custom">
|
||||
<div class="modal-dialog-custom" style="max-width: 650px;">
|
||||
<div class="modal-header-custom">
|
||||
<h5 class="modal-title-custom">
|
||||
@@ -319,7 +330,8 @@
|
||||
@foreach (var group in (viewedDeck.Cards ?? []).GroupBy(c => c))
|
||||
{
|
||||
<div class="col-md-6">
|
||||
<div class="p-2 rounded bg-dark border border-secondary d-flex align-items-center justify-content-between">
|
||||
<div
|
||||
class="p-2 rounded bg-dark border border-secondary d-flex align-items-center justify-content-between">
|
||||
<span class="text-white small">@group.Key</span>
|
||||
<span class="badge bg-primary">x@group.Count()</span>
|
||||
</div>
|
||||
@@ -332,13 +344,13 @@
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showViewModal = false">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
@if (showDeleteModal && deckToDelete != null)
|
||||
{
|
||||
<div class="modal-backdrop-custom">
|
||||
<div class="modal-backdrop-custom">
|
||||
<div class="modal-dialog-custom" style="max-width: 450px;">
|
||||
<div class="modal-header-custom">
|
||||
<h5 class="modal-title-custom text-danger">
|
||||
@@ -352,43 +364,45 @@
|
||||
<div class="p-2 rounded bg-dark border border-secondary fw-bold text-primary mb-3">
|
||||
@deckToDelete.Name
|
||||
</div>
|
||||
<p class="text-secondary small mb-0">This will remove the deck record and its JSONB associations from the database.</p>
|
||||
<p class="text-secondary small mb-0">This will remove the deck record and its JSONB associations from the
|
||||
database.</p>
|
||||
</div>
|
||||
<div class="modal-footer-custom">
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showDeleteModal = false">Cancel</button>
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showDeleteModal = false">Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-chrono-danger" @onclick="ConfirmDeleteAsync" disabled="@isSaving">
|
||||
@(isSaving ? "Deleting..." : "Delete Deck")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<UserDeck> decks = [];
|
||||
private List<string> allCardNames = [];
|
||||
private List<string> agentCardNames = [];
|
||||
private string searchTerm = "";
|
||||
private bool isLoading = true;
|
||||
private bool isSaving = false;
|
||||
private string? alertMessage;
|
||||
private bool alertIsSuccess;
|
||||
private List<UserDeck> decks = [];
|
||||
private List<string> allCardNames = [];
|
||||
private List<string> agentCardNames = [];
|
||||
private string searchTerm = "";
|
||||
private bool isLoading = true;
|
||||
private bool isSaving;
|
||||
private string? alertMessage;
|
||||
private bool alertIsSuccess;
|
||||
|
||||
private bool showModal;
|
||||
private bool isEditing;
|
||||
private UserDeck currentDeck = new();
|
||||
private string newCardName = "";
|
||||
private int newCardCount = 1;
|
||||
private string newDiverName = "";
|
||||
private bool showModal;
|
||||
private bool isEditing;
|
||||
private UserDeck currentDeck = new();
|
||||
private string newCardName = "";
|
||||
private int newCardCount = 1;
|
||||
private string newDiverName = "";
|
||||
|
||||
private bool showViewModal;
|
||||
private UserDeck? viewedDeck;
|
||||
private bool showViewModal;
|
||||
private UserDeck? viewedDeck;
|
||||
|
||||
private bool showDeleteModal;
|
||||
private UserDeck? deckToDelete;
|
||||
private bool showDeleteModal;
|
||||
private UserDeck? deckToDelete;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
ConfigService.OnConfigurationChanged += HandleConfigChanged;
|
||||
|
||||
try
|
||||
@@ -415,15 +429,15 @@
|
||||
}
|
||||
|
||||
await LoadDecksAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConfigChanged()
|
||||
{
|
||||
private void HandleConfigChanged()
|
||||
{
|
||||
_ = LoadDecksAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadDecksAsync()
|
||||
{
|
||||
private async Task LoadDecksAsync()
|
||||
{
|
||||
isLoading = true;
|
||||
try
|
||||
{
|
||||
@@ -438,21 +452,21 @@
|
||||
{
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FilterDecks()
|
||||
{
|
||||
private async Task FilterDecks()
|
||||
{
|
||||
decks = await DbService.GetUserDecksAsync(searchTerm);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ClearSearch()
|
||||
{
|
||||
private async Task ClearSearch()
|
||||
{
|
||||
searchTerm = "";
|
||||
await LoadDecksAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenAddModal()
|
||||
{
|
||||
private void OpenAddModal()
|
||||
{
|
||||
isEditing = false;
|
||||
currentDeck = new UserDeck
|
||||
{
|
||||
@@ -467,10 +481,10 @@
|
||||
newCardCount = 1;
|
||||
newDiverName = "";
|
||||
showModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenEditModal(UserDeck deck)
|
||||
{
|
||||
private void OpenEditModal(UserDeck deck)
|
||||
{
|
||||
isEditing = true;
|
||||
currentDeck = new UserDeck
|
||||
{
|
||||
@@ -479,45 +493,46 @@
|
||||
Season = deck.Season,
|
||||
Notes = deck.Notes,
|
||||
UpdatedAt = deck.UpdatedAt,
|
||||
Cards = deck.Cards?.ToList() ?? new(),
|
||||
Divers = deck.Divers?.ToList() ?? new()
|
||||
Cards = deck.Cards?.ToList() ?? new List<string>(),
|
||||
Divers = deck.Divers?.ToList() ?? new List<string>()
|
||||
};
|
||||
newCardName = "";
|
||||
newCardCount = 1;
|
||||
newDiverName = "";
|
||||
showModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseModal()
|
||||
{
|
||||
private void CloseModal()
|
||||
{
|
||||
showModal = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCardToDeck()
|
||||
{
|
||||
private void AddCardToDeck()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newCardName)) return;
|
||||
|
||||
currentDeck.Cards ??= new List<string>();
|
||||
for (int i = 0; i < newCardCount; i++)
|
||||
for (var i = 0; i < newCardCount; i++)
|
||||
{
|
||||
currentDeck.Cards.Add(newCardName.Trim());
|
||||
}
|
||||
|
||||
newCardName = "";
|
||||
newCardCount = 1;
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveOneCardFromDeck(string cardName)
|
||||
{
|
||||
private void RemoveOneCardFromDeck(string cardName)
|
||||
{
|
||||
currentDeck.Cards?.Remove(cardName);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveAllCardsFromDeck(string cardName)
|
||||
{
|
||||
private void RemoveAllCardsFromDeck(string cardName)
|
||||
{
|
||||
currentDeck.Cards?.RemoveAll(c => c == cardName);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddDiverToDeck()
|
||||
{
|
||||
private void AddDiverToDeck()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newDiverName)) return;
|
||||
|
||||
currentDeck.Divers ??= new List<string>();
|
||||
@@ -525,16 +540,17 @@
|
||||
{
|
||||
currentDeck.Divers.Add(newDiverName.Trim());
|
||||
}
|
||||
|
||||
newDiverName = "";
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveDiverFromDeck(string diver)
|
||||
{
|
||||
private void RemoveDiverFromDeck(string diver)
|
||||
{
|
||||
currentDeck.Divers?.Remove(diver);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveDeckAsync()
|
||||
{
|
||||
private async Task SaveDeckAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currentDeck.Name))
|
||||
{
|
||||
alertIsSuccess = false;
|
||||
@@ -554,27 +570,27 @@
|
||||
showModal = false;
|
||||
await LoadDecksAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenViewModal(UserDeck deck)
|
||||
{
|
||||
private void OpenViewModal(UserDeck deck)
|
||||
{
|
||||
viewedDeck = deck;
|
||||
showViewModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFormattedDeckJson(UserDeck deck)
|
||||
{
|
||||
private string GetFormattedDeckJson(UserDeck deck)
|
||||
{
|
||||
return JsonSerializer.Serialize(deck, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenDeleteModal(UserDeck deck)
|
||||
{
|
||||
private void OpenDeleteModal(UserDeck deck)
|
||||
{
|
||||
deckToDelete = deck;
|
||||
showDeleteModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConfirmDeleteAsync()
|
||||
{
|
||||
private async Task ConfirmDeleteAsync()
|
||||
{
|
||||
if (deckToDelete == null) return;
|
||||
|
||||
isSaving = true;
|
||||
@@ -587,18 +603,18 @@
|
||||
deckToDelete = null;
|
||||
|
||||
await LoadDecksAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SeedSampleDecksAsync()
|
||||
{
|
||||
private async Task SeedSampleDecksAsync()
|
||||
{
|
||||
var result = await DbService.SeedSampleDataAsync();
|
||||
alertIsSuccess = result.Success;
|
||||
alertMessage = result.Message;
|
||||
await LoadDecksAsync();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
@page "/users"
|
||||
@implements IDisposable
|
||||
|
||||
<PageTitle>Users Management - Chrono DB</PageTitle>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold mb-1 text-white">Users Database (`User`)</h2>
|
||||
<p class="text-secondary mb-0">Browse, add, edit credentials, and delete user accounts.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-chrono-secondary" @onclick="LoadUsersAsync">
|
||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
||||
</button>
|
||||
<button class="btn btn-chrono-primary" @onclick="OpenAddModal">
|
||||
<i class="bi bi-plus-lg"></i> Add New User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(alertMessage))
|
||||
{
|
||||
<div class="alert @(alertIsSuccess ? "alert-success" : "alert-danger") alert-dismissible fade show" role="alert">
|
||||
<i class="bi @(alertIsSuccess ? "bi-check-circle-fill" : "bi-exclamation-triangle-fill") me-2"></i>
|
||||
@alertMessage
|
||||
<button type="button" class="btn-close" @onclick="() => alertMessage = null"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="chrono-card">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 gap-3">
|
||||
<div class="input-group" style="max-width: 400px;">
|
||||
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control form-control-chrono" placeholder="Search by username..."
|
||||
@bind="searchTerm" @bind:event="oninput" @bind:after="FilterUsers"/>
|
||||
@if (!string.IsNullOrEmpty(searchTerm))
|
||||
{
|
||||
<button class="btn btn-outline-secondary" @onclick="ClearSearch"><i class="bi bi-x-lg"></i></button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="text-secondary small">
|
||||
Showing @users.Count user(s)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<div class="text-center py-5 text-secondary">
|
||||
<div class="spinner-border text-primary mb-2" role="status"></div>
|
||||
<div>Loading users from database...</div>
|
||||
</div>
|
||||
}
|
||||
else if (!users.Any())
|
||||
{
|
||||
<div class="text-center py-5 text-secondary">
|
||||
<i class="bi bi-people fs-1 d-block mb-2"></i>
|
||||
<h5>No Users Found</h5>
|
||||
<p class="small">There are currently no users in the database.</p>
|
||||
<div class="d-flex justify-content-center gap-2 mt-3">
|
||||
<button class="btn btn-chrono-primary" @onclick="OpenAddModal">
|
||||
<i class="bi bi-plus-lg"></i> Add First User
|
||||
</button>
|
||||
<button class="btn btn-chrono-secondary" @onclick="SeedUsersAsync">
|
||||
<i class="bi bi-box-arrow-in-down"></i> Seed Sample Users
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="chrono-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30%;">Username (Primary Key)</th>
|
||||
<th style="width: 35%;">Password Hash / Credential</th>
|
||||
<th style="width: 20%;">Created At</th>
|
||||
<th style="width: 140px;" class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var user in users)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold text-white d-flex align-items-center gap-2">
|
||||
<i class="bi bi-person-circle text-info"></i>
|
||||
@user.Username
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-secondary font-monospace">••••••••</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-secondary small">@user.CreatedAt.ToString("g")</div>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-chrono-secondary" @onclick="() => OpenEditModal(user)"
|
||||
title="Edit Password">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-chrono-danger" @onclick="() => OpenDeleteModal(user)"
|
||||
title="Delete User">
|
||||
<i class="bi bi-trash3-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (showEditModal)
|
||||
{
|
||||
<div class="modal-backdrop fade show"></div>
|
||||
<div class="modal fade show d-block" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content chrono-modal">
|
||||
<div class="modal-header border-secondary border-opacity-25">
|
||||
<h5 class="modal-title text-white">
|
||||
<i class="bi @(isNewUser ? "bi-person-plus-fill" : "bi-pencil-square") me-2 text-primary"></i>
|
||||
@(isNewUser ? "Add New User" : $"Edit User: {currentUser.Username}")
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" @onclick="CloseModal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary small fw-semibold">USERNAME</label>
|
||||
<input type="text" class="form-control form-control-chrono"
|
||||
@bind="currentUser.Username" disabled="@(!isNewUser)"
|
||||
placeholder="e.g. player1"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-secondary small fw-semibold">PASSWORD</label>
|
||||
<input type="password" class="form-control form-control-chrono"
|
||||
@bind="currentUser.Password"
|
||||
placeholder="Enter password..."/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-secondary border-opacity-25">
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="CloseModal">Cancel</button>
|
||||
<button type="button" class="btn btn-chrono-primary" @onclick="SaveUserAsync" disabled="@isSaving">
|
||||
<i class="bi bi-save me-1"></i> @(isSaving ? "Saving..." : "Save User")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showDeleteModal && userToDelete != null)
|
||||
{
|
||||
<div class="modal-backdrop fade show"></div>
|
||||
<div class="modal fade show d-block" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content chrono-modal">
|
||||
<div class="modal-header border-secondary border-opacity-25">
|
||||
<h5 class="modal-title text-danger">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i> Confirm Delete
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white"
|
||||
@onclick="() => showDeleteModal = false"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-light mb-0">
|
||||
Are you sure you want to delete user <strong class="text-white">@userToDelete.Username</strong>?
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer border-secondary border-opacity-25">
|
||||
<button type="button" class="btn btn-chrono-secondary" @onclick="() => showDeleteModal = false">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-chrono-danger" @onclick="ConfirmDeleteAsync"
|
||||
disabled="@isSaving">
|
||||
<i class="bi bi-trash3-fill me-1"></i> @(isSaving ? "Deleting..." : "Delete")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<User> users = [];
|
||||
private bool isLoading = true;
|
||||
private bool isSaving;
|
||||
private string searchTerm = string.Empty;
|
||||
|
||||
private bool showEditModal;
|
||||
private bool isNewUser;
|
||||
private User currentUser = new();
|
||||
|
||||
private bool showDeleteModal;
|
||||
private User? userToDelete;
|
||||
|
||||
private string? alertMessage;
|
||||
private bool alertIsSuccess;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
ConfigService.OnConfigurationChanged += HandleConfigChanged;
|
||||
await LoadUsersAsync();
|
||||
}
|
||||
|
||||
private async void HandleConfigChanged()
|
||||
{
|
||||
await InvokeAsync(LoadUsersAsync);
|
||||
}
|
||||
|
||||
private async Task LoadUsersAsync()
|
||||
{
|
||||
isLoading = true;
|
||||
alertMessage = null;
|
||||
try
|
||||
{
|
||||
users = await DbService.GetUsersAsync(searchTerm);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
alertMessage = $"Error loading users: {ex.Message}";
|
||||
alertIsSuccess = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FilterUsers()
|
||||
{
|
||||
users = await DbService.GetUsersAsync(searchTerm);
|
||||
}
|
||||
|
||||
private async Task ClearSearch()
|
||||
{
|
||||
searchTerm = string.Empty;
|
||||
await LoadUsersAsync();
|
||||
}
|
||||
|
||||
private void OpenAddModal()
|
||||
{
|
||||
currentUser = new User();
|
||||
isNewUser = true;
|
||||
showEditModal = true;
|
||||
}
|
||||
|
||||
private void OpenEditModal(User user)
|
||||
{
|
||||
currentUser = new User
|
||||
{
|
||||
Username = user.Username,
|
||||
Password = user.Password,
|
||||
CreatedAt = user.CreatedAt
|
||||
};
|
||||
isNewUser = false;
|
||||
showEditModal = true;
|
||||
}
|
||||
|
||||
private void CloseModal()
|
||||
{
|
||||
showEditModal = false;
|
||||
}
|
||||
|
||||
private async Task SaveUserAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currentUser.Username) || string.IsNullOrWhiteSpace(currentUser.Password))
|
||||
{
|
||||
alertMessage = "Username and password cannot be empty.";
|
||||
alertIsSuccess = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isSaving = true;
|
||||
try
|
||||
{
|
||||
var result = await DbService.SaveUserAsync(currentUser, isNewUser);
|
||||
alertMessage = result.Message;
|
||||
alertIsSuccess = result.Success;
|
||||
if (result.Success)
|
||||
{
|
||||
showEditModal = false;
|
||||
await LoadUsersAsync();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenDeleteModal(User user)
|
||||
{
|
||||
userToDelete = user;
|
||||
showDeleteModal = true;
|
||||
}
|
||||
|
||||
private async Task ConfirmDeleteAsync()
|
||||
{
|
||||
if (userToDelete == null) return;
|
||||
|
||||
isSaving = true;
|
||||
try
|
||||
{
|
||||
var result = await DbService.DeleteUserAsync(userToDelete.Username);
|
||||
alertMessage = result.Message;
|
||||
alertIsSuccess = result.Success;
|
||||
showDeleteModal = false;
|
||||
userToDelete = null;
|
||||
await LoadUsersAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SeedUsersAsync()
|
||||
{
|
||||
isLoading = true;
|
||||
try
|
||||
{
|
||||
var res = await DbService.SeedSampleDataAsync();
|
||||
alertMessage = res.Message;
|
||||
alertIsSuccess = res.Success;
|
||||
await LoadUsersAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ConfigService.OnConfigurationChanged -= HandleConfigChanged;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
@using DatabaseTest.Components.Pages
|
||||
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(NotFound)">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/>
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)"/>
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -1,17 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Model\Model.csproj" />
|
||||
<ProjectReference Include="..\Model\Model.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Shared\Generated\Cards.g.cs" Link="Generated\Cards.g.cs" />
|
||||
<Compile Include="..\Shared\Generated\Decks.g.cs" Link="Generated\Decks.g.cs" />
|
||||
<Compile Include="..\Shared\Generated\Cards.g.cs" Link="Generated\Cards.g.cs"/>
|
||||
<Compile Include="..\Shared\Generated\Decks.g.cs" Link="Generated\Decks.g.cs"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.2"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user