Vibe Google Analytics

This commit is contained in:
2026-06-25 18:55:04 -04:00
parent fdf8e754f5
commit 73ae3eac53
86 changed files with 53925 additions and 46039 deletions
View File
View File
+1
View File
@@ -0,0 +1 @@
[]
+1
View File
@@ -0,0 +1 @@
3.0
View File
+2 -2
View File
@@ -60,7 +60,7 @@ foreach (var file in mdFiles)
ImmortalizeWhen = NullIfNa(StripWikiLinks(yaml.GetValueOrDefault("immortalizeWhen"))), ImmortalizeWhen = NullIfNa(StripWikiLinks(yaml.GetValueOrDefault("immortalizeWhen"))),
ImageFile = imageFile, ImageFile = imageFile,
Artist = yaml.GetValueOrDefault("artist"), Artist = yaml.GetValueOrDefault("artist"),
Rarity = yaml.GetValueOrDefault("rarity"), Rarity = yaml.GetValueOrDefault("rarity")
}; };
cards.Add(card); cards.Add(card);
@@ -117,7 +117,7 @@ for (var i = 0; i < cards.Count; i++)
WriteStrProp(writer, "ImmortalizeFrom", c.ImmortalizeFrom, 3); WriteStrProp(writer, "ImmortalizeFrom", c.ImmortalizeFrom, 3);
WriteStrProp(writer, "ImmortalizeWhen", c.ImmortalizeWhen, 3); WriteStrProp(writer, "ImmortalizeWhen", c.ImmortalizeWhen, 3);
WriteStrProp(writer, "ImageFile", c.ImageFile, 3); WriteStrProp(writer, "ImageFile", c.ImageFile, 3);
WriteStrProp(writer, "Artist", c.Artist, 3); WriteStrProp(writer, "Artist", c.Artist, 3);
WriteStrProp(writer, "Rarity", c.Rarity, 3); WriteStrProp(writer, "Rarity", c.Rarity, 3);
+2 -2
View File
@@ -8,8 +8,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Model\Model.csproj" /> <ProjectReference Include="..\Model\Model.csproj"/>
<ProjectReference Include="..\Shared\Shared.csproj" /> <ProjectReference Include="..\Shared\Shared.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -21,20 +21,20 @@ main {
align-items: center; align-items: center;
} }
.top-row ::deep a, .top-row ::deep .btn-link { .top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap; white-space: nowrap;
margin-left: 1.5rem; margin-left: 1.5rem;
text-decoration: none; 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; text-decoration: underline;
} }
.top-row ::deep a:first-child { .top-row ::deep a:first-child {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@media (max-width: 640.98px) { @media (max-width: 640.98px) {
.top-row { .top-row {
@@ -90,9 +90,9 @@ main {
z-index: 1000; z-index: 1000;
} }
#blazor-error-ui .dismiss { #blazor-error-ui .dismiss {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
right: 0.75rem; right: 0.75rem;
top: 0.5rem; top: 0.5rem;
} }
@@ -17,7 +17,7 @@
.top-row { .top-row {
min-height: 3.5rem; min-height: 3.5rem;
background-color: rgba(0,0,0,0.4); background-color: rgba(0, 0, 0, 0.4);
} }
.navbar-brand { .navbar-brand {
@@ -51,33 +51,33 @@
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
.nav-item:first-of-type { .nav-item:first-of-type {
padding-top: 1rem; padding-top: 1rem;
} }
.nav-item:last-of-type { .nav-item:last-of-type {
padding-bottom: 1rem; padding-bottom: 1rem;
} }
.nav-item ::deep .nav-link { .nav-item ::deep .nav-link {
color: #d7d7d7; color: #d7d7d7;
background: none; background: none;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
height: 3rem; height: 3rem;
display: flex; display: flex;
align-items: center; align-items: center;
line-height: 3rem; line-height: 3rem;
width: 100%; width: 100%;
} }
.nav-item ::deep a.active { .nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37); background-color: rgba(255, 255, 255, 0.37);
color: white; color: white;
} }
.nav-item ::deep .nav-link:hover { .nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1); background-color: rgba(255, 255, 255, 0.1);
color: white; color: white;
} }
@@ -31,12 +31,11 @@
opacity: 0; opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both; 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: 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; animation-fill-mode: both;
} }
} }
@@ -96,13 +95,13 @@
border-radius: 4px; border-radius: 4px;
} }
#components-reconnect-modal button:hover { #components-reconnect-modal button:hover {
background-color: #3b6ea2; background-color: #3b6ea2;
} }
#components-reconnect-modal button:active { #components-reconnect-modal button:active {
background-color: #6b9ed2; background-color: #6b9ed2;
} }
.components-rejoining-animation { .components-rejoining-animation {
position: relative; position: relative;
@@ -110,17 +109,17 @@
height: 80px; height: 80px;
} }
.components-rejoining-animation div { .components-rejoining-animation div {
position: absolute; position: absolute;
border: 3px solid #0087ff; border: 3px solid #0087ff;
opacity: 1; opacity: 1;
border-radius: 50%; border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; 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; animation-delay: -0.5s;
} }
@keyframes components-rejoining-animation { @keyframes components-rejoining-animation {
0% { 0% {
+1 -1
View File
@@ -10,7 +10,7 @@
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code { @code {
private int currentCount = 0; private int currentCount;
private void IncrementCount() private void IncrementCount()
{ {
+7 -3
View File
@@ -15,12 +15,14 @@
<h3>Development Mode</h3> <h3>Development Mode</h3>
<p> <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>
<p> <p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong> <strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users. 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. and restarting the app.
</p> </p>
@@ -30,7 +32,9 @@
private string? RequestId { get; set; } private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() => protected override void OnInitialized()
{
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}
} }
+4 -2
View File
@@ -1,7 +1,9 @@
@using Shared.Pages @using Shared.Pages
<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Home).Assembly }" NotFoundPage="typeof(Pages.NotFound)"> @using NotFound = Cloud.Components.Pages.NotFound
<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Home).Assembly }"
NotFoundPage="typeof(NotFound)">
<Found Context="routeData"> <Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"/> <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)"/>
<FocusOnNavigate RouteData="routeData" Selector="h1"/> <FocusOnNavigate RouteData="routeData" Selector="h1"/>
</Found> </Found>
</Router> </Router>
+1 -1
View File
@@ -11,7 +11,7 @@ var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
app.UseExceptionHandler("/Error", createScopeForErrors: true); app.UseExceptionHandler("/Error", true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts(); app.UseHsts();
} }
+18 -18
View File
@@ -1,23 +1,23 @@
{ {
"$schema": "https://json.schemastore.org/launchsettings.json", "$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": { "profiles": {
"http": { "http": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "http://localhost:5230", "applicationUrl": "http://localhost:5230",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
}, },
"https": { "https": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "https://localhost:7086;http://localhost:5230", "applicationUrl": "https://localhost:7086;http://localhost:5230",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}
} }
} }
} }
}
+4 -4
View File
@@ -13,7 +13,7 @@ a, .btn-link {
} }
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { .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; box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
} }
.content { .content {
@@ -42,9 +42,9 @@ h1:focus {
color: white; color: white;
} }
.blazor-error-boundary::after { .blazor-error-boundary::after {
content: "An error has occurred." content: "An error has occurred."
} }
.darker-border-checkbox.form-check-input { .darker-border-checkbox.form-check-input {
border-color: #929292; border-color: #929292;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+339 -327
View File
@@ -5,427 +5,435 @@
*/ */
:root, :root,
[data-bs-theme=light] { [data-bs-theme=light] {
--bs-blue: #0d6efd; --bs-blue: #0d6efd;
--bs-indigo: #6610f2; --bs-indigo: #6610f2;
--bs-purple: #6f42c1; --bs-purple: #6f42c1;
--bs-pink: #d63384; --bs-pink: #d63384;
--bs-red: #dc3545; --bs-red: #dc3545;
--bs-orange: #fd7e14; --bs-orange: #fd7e14;
--bs-yellow: #ffc107; --bs-yellow: #ffc107;
--bs-green: #198754; --bs-green: #198754;
--bs-teal: #20c997; --bs-teal: #20c997;
--bs-cyan: #0dcaf0; --bs-cyan: #0dcaf0;
--bs-black: #000; --bs-black: #000;
--bs-white: #fff; --bs-white: #fff;
--bs-gray: #6c757d; --bs-gray: #6c757d;
--bs-gray-dark: #343a40; --bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa; --bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef; --bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6; --bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da; --bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd; --bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d; --bs-gray-600: #6c757d;
--bs-gray-700: #495057; --bs-gray-700: #495057;
--bs-gray-800: #343a40; --bs-gray-800: #343a40;
--bs-gray-900: #212529; --bs-gray-900: #212529;
--bs-primary: #0d6efd; --bs-primary: #0d6efd;
--bs-secondary: #6c757d; --bs-secondary: #6c757d;
--bs-success: #198754; --bs-success: #198754;
--bs-info: #0dcaf0; --bs-info: #0dcaf0;
--bs-warning: #ffc107; --bs-warning: #ffc107;
--bs-danger: #dc3545; --bs-danger: #dc3545;
--bs-light: #f8f9fa; --bs-light: #f8f9fa;
--bs-dark: #212529; --bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253; --bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125; --bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84; --bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240; --bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7; --bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69; --bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250; --bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41; --bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65; --bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32; --bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622; --bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160; --bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03; --bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c; --bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057; --bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057; --bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff; --bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5; --bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd; --bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc; --bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd; --bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da; --bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd; --bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da; --bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe; --bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb; --bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb; --bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9; --bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c; --bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5; --bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef; --bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd; --bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255; --bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0; --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-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-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-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-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem; --bs-body-font-size: 1rem;
--bs-body-font-weight: 400; --bs-body-font-weight: 400;
--bs-body-line-height: 1.5; --bs-body-line-height: 1.5;
--bs-body-color: #212529; --bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41; --bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff; --bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255; --bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000; --bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0; --bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75); --bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41; --bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef; --bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239; --bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5); --bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41; --bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa; --bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250; --bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit; --bs-heading-color: inherit;
--bs-link-color: #0d6efd; --bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253; --bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline; --bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca; --bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202; --bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384; --bs-code-color: #d63384;
--bs-highlight-color: #212529; --bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd; --bs-highlight-bg: #fff3cd;
--bs-border-width: 1px; --bs-border-width: 1px;
--bs-border-style: solid; --bs-border-style: solid;
--bs-border-color: #dee2e6; --bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175); --bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem; --bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem; --bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem; --bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem; --bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem; --bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl); --bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem; --bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); --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-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-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-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem; --bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25; --bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25); --bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754; --bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754; --bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545; --bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545; --bs-form-invalid-border-color: #dc3545;
} }
[data-bs-theme=dark] { [data-bs-theme=dark] {
color-scheme: dark; color-scheme: dark;
--bs-body-color: #dee2e6; --bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230; --bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529; --bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41; --bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff; --bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255; --bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75); --bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230; --bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40; --bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64; --bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5); --bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230; --bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035; --bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53; --bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe; --bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1; --bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798; --bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6; --bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a; --bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f; --bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa; --bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6; --bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633; --bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719; --bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11; --bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830; --bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701; --bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e; --bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40; --bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20; --bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298; --bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b; --bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132; --bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990; --bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404; --bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029; --bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057; --bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40; --bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit; --bs-heading-color: inherit;
--bs-link-color: #6ea8fe; --bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe; --bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254; --bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254; --bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5; --bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6; --bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03; --bs-highlight-bg: #664d03;
--bs-border-color: #495057; --bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15); --bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798; --bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798; --bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f; --bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f; --bs-form-invalid-border-color: #ea868f;
} }
*, *,
*::before, *::before,
*::after { *::after {
box-sizing: border-box; box-sizing: border-box;
} }
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
:root { :root {
scroll-behavior: smooth; scroll-behavior: smooth;
} }
} }
body { body {
margin: 0; margin: 0;
font-family: var(--bs-body-font-family); font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size); font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight); font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height); line-height: var(--bs-body-line-height);
color: var(--bs-body-color); color: var(--bs-body-color);
text-align: var(--bs-body-text-align); text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg); background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
} }
hr { hr {
margin: 1rem 0; margin: 1rem 0;
color: inherit; color: inherit;
border: 0; border: 0;
border-top: var(--bs-border-width) solid; border-top: var(--bs-border-width) solid;
opacity: 0.25; opacity: 0.25;
} }
h6, h5, h4, h3, h2, h1 { h6, h5, h4, h3, h2, h1 {
margin-top: 0; margin-top: 0;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-weight: 500; font-weight: 500;
line-height: 1.2; line-height: 1.2;
color: var(--bs-heading-color); color: var(--bs-heading-color);
} }
h1 { h1 {
font-size: calc(1.375rem + 1.5vw); font-size: calc(1.375rem + 1.5vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h1 { h1 {
font-size: 2.5rem; font-size: 2.5rem;
} }
} }
h2 { h2 {
font-size: calc(1.325rem + 0.9vw); font-size: calc(1.325rem + 0.9vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h2 { h2 {
font-size: 2rem; font-size: 2rem;
} }
} }
h3 { h3 {
font-size: calc(1.3rem + 0.6vw); font-size: calc(1.3rem + 0.6vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h3 { h3 {
font-size: 1.75rem; font-size: 1.75rem;
} }
} }
h4 { h4 {
font-size: calc(1.275rem + 0.3vw); font-size: calc(1.275rem + 0.3vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h4 { h4 {
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
h5 { h5 {
font-size: 1.25rem; font-size: 1.25rem;
} }
h6 { h6 {
font-size: 1rem; font-size: 1rem;
} }
p { p {
margin-top: 0; margin-top: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
abbr[title] { abbr[title] {
-webkit-text-decoration: underline dotted; -webkit-text-decoration: underline dotted;
text-decoration: underline dotted; text-decoration: underline dotted;
cursor: help; cursor: help;
-webkit-text-decoration-skip-ink: none; -webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none; text-decoration-skip-ink: none;
} }
address { address {
margin-bottom: 1rem; margin-bottom: 1rem;
font-style: normal; font-style: normal;
line-height: inherit; line-height: inherit;
} }
ol, ol,
ul { ul {
padding-left: 2rem; padding-left: 2rem;
} }
ol, ol,
ul, ul,
dl { dl {
margin-top: 0; margin-top: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
ol ol, ol ol,
ul ul, ul ul,
ol ul, ol ul,
ul ol { ul ol {
margin-bottom: 0; margin-bottom: 0;
} }
dt { dt {
font-weight: 700; font-weight: 700;
} }
dd { dd {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
margin-left: 0; margin-left: 0;
} }
blockquote { blockquote {
margin: 0 0 1rem; margin: 0 0 1rem;
} }
b, b,
strong { strong {
font-weight: bolder; font-weight: bolder;
} }
small { small {
font-size: 0.875em; font-size: 0.875em;
} }
mark { mark {
padding: 0.1875em; padding: 0.1875em;
color: var(--bs-highlight-color); color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg); background-color: var(--bs-highlight-bg);
} }
sub, sub,
sup { sup {
position: relative; position: relative;
font-size: 0.75em; font-size: 0.75em;
line-height: 0; line-height: 0;
vertical-align: baseline; vertical-align: baseline;
} }
sub { sub {
bottom: -0.25em; bottom: -0.25em;
} }
sup { sup {
top: -0.5em; top: -0.5em;
} }
a { a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline; text-decoration: underline;
} }
a:hover { a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb); --bs-link-color-rgb: var(--bs-link-hover-color-rgb);
} }
a:not([href]):not([class]), a:not([href]):not([class]):hover { a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
} }
pre, pre,
code, code,
kbd, kbd,
samp { samp {
font-family: var(--bs-font-monospace); font-family: var(--bs-font-monospace);
font-size: 1em; font-size: 1em;
} }
pre { pre {
display: block; display: block;
margin-top: 0; margin-top: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
overflow: auto; overflow: auto;
font-size: 0.875em; font-size: 0.875em;
} }
pre code { pre code {
font-size: inherit; font-size: inherit;
color: inherit; color: inherit;
word-break: normal; word-break: normal;
} }
code { code {
font-size: 0.875em; font-size: 0.875em;
color: var(--bs-code-color); color: var(--bs-code-color);
word-wrap: break-word; word-wrap: break-word;
} }
a > code { a > code {
color: inherit; color: inherit;
} }
kbd { kbd {
padding: 0.1875rem 0.375rem; padding: 0.1875rem 0.375rem;
font-size: 0.875em; font-size: 0.875em;
color: var(--bs-body-bg); color: var(--bs-body-bg);
background-color: var(--bs-body-color); background-color: var(--bs-body-color);
border-radius: 0.25rem; border-radius: 0.25rem;
} }
kbd kbd { kbd kbd {
padding: 0; padding: 0;
font-size: 1em; font-size: 1em;
} }
figure { figure {
margin: 0 0 1rem; margin: 0 0 1rem;
} }
img, img,
svg { svg {
vertical-align: middle; vertical-align: middle;
} }
table { table {
caption-side: bottom; caption-side: bottom;
border-collapse: collapse; border-collapse: collapse;
} }
caption { caption {
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
color: var(--bs-secondary-color); color: var(--bs-secondary-color);
text-align: left; text-align: left;
} }
th { th {
text-align: inherit; text-align: inherit;
text-align: -webkit-match-parent; text-align: -webkit-match-parent;
} }
thead, thead,
@@ -434,21 +442,21 @@ tfoot,
tr, tr,
td, td,
th { th {
border-color: inherit; border-color: inherit;
border-style: solid; border-style: solid;
border-width: 0; border-width: 0;
} }
label { label {
display: inline-block; display: inline-block;
} }
button { button {
border-radius: 0; border-radius: 0;
} }
button:focus:not(:focus-visible) { button:focus:not(:focus-visible) {
outline: 0; outline: 0;
} }
input, input,
@@ -456,76 +464,80 @@ button,
select, select,
optgroup, optgroup,
textarea { textarea {
margin: 0; margin: 0;
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
line-height: inherit; line-height: inherit;
} }
button, button,
select { select {
text-transform: none; text-transform: none;
} }
[role=button] { [role=button] {
cursor: pointer; cursor: pointer;
} }
select { select {
word-wrap: normal; word-wrap: normal;
} }
select:disabled { select:disabled {
opacity: 1; opacity: 1;
} }
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important; display: none !important;
} }
button, button,
[type=button], [type=button],
[type=reset], [type=reset],
[type=submit] { [type=submit] {
-webkit-appearance: button; -webkit-appearance: button;
} }
button:not(:disabled), button:not(:disabled),
[type=button]:not(:disabled), [type=button]:not(:disabled),
[type=reset]:not(:disabled), [type=reset]:not(:disabled),
[type=submit]:not(:disabled) { [type=submit]:not(:disabled) {
cursor: pointer; cursor: pointer;
} }
::-moz-focus-inner { ::-moz-focus-inner {
padding: 0; padding: 0;
border-style: none; border-style: none;
} }
textarea { textarea {
resize: vertical; resize: vertical;
} }
fieldset { fieldset {
min-width: 0; min-width: 0;
padding: 0; padding: 0;
margin: 0; margin: 0;
border: 0; border: 0;
} }
legend { legend {
float: left; float: left;
width: 100%; width: 100%;
padding: 0; padding: 0;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw); font-size: calc(1.275rem + 0.3vw);
line-height: inherit; line-height: inherit;
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
legend { legend {
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
legend + * { legend + * {
clear: left; clear: left;
} }
::-webkit-datetime-edit-fields-wrapper, ::-webkit-datetime-edit-fields-wrapper,
@@ -535,16 +547,16 @@ legend + * {
::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field { ::-webkit-datetime-edit-year-field {
padding: 0; padding: 0;
} }
::-webkit-inner-spin-button { ::-webkit-inner-spin-button {
height: auto; height: auto;
} }
[type=search] { [type=search] {
-webkit-appearance: textfield; -webkit-appearance: textfield;
outline-offset: -2px; outline-offset: -2px;
} }
/* rtl:raw: /* rtl:raw:
@@ -556,42 +568,42 @@ legend + * {
} }
*/ */
::-webkit-search-decoration { ::-webkit-search-decoration {
-webkit-appearance: none; -webkit-appearance: none;
} }
::-webkit-color-swatch-wrapper { ::-webkit-color-swatch-wrapper {
padding: 0; padding: 0;
} }
::-webkit-file-upload-button { ::-webkit-file-upload-button {
font: inherit; font: inherit;
-webkit-appearance: button; -webkit-appearance: button;
} }
::file-selector-button { ::file-selector-button {
font: inherit; font: inherit;
-webkit-appearance: button; -webkit-appearance: button;
} }
output { output {
display: inline-block; display: inline-block;
} }
iframe { iframe {
border: 0; border: 0;
} }
summary { summary {
display: list-item; display: list-item;
cursor: pointer; cursor: pointer;
} }
progress { progress {
vertical-align: baseline; vertical-align: baseline;
} }
[hidden] { [hidden] {
display: none !important; display: none !important;
} }
/*# sourceMappingURL=bootstrap-reboot.css.map */ /*# sourceMappingURL=bootstrap-reboot.css.map */
@@ -5,427 +5,435 @@
*/ */
:root, :root,
[data-bs-theme=light] { [data-bs-theme=light] {
--bs-blue: #0d6efd; --bs-blue: #0d6efd;
--bs-indigo: #6610f2; --bs-indigo: #6610f2;
--bs-purple: #6f42c1; --bs-purple: #6f42c1;
--bs-pink: #d63384; --bs-pink: #d63384;
--bs-red: #dc3545; --bs-red: #dc3545;
--bs-orange: #fd7e14; --bs-orange: #fd7e14;
--bs-yellow: #ffc107; --bs-yellow: #ffc107;
--bs-green: #198754; --bs-green: #198754;
--bs-teal: #20c997; --bs-teal: #20c997;
--bs-cyan: #0dcaf0; --bs-cyan: #0dcaf0;
--bs-black: #000; --bs-black: #000;
--bs-white: #fff; --bs-white: #fff;
--bs-gray: #6c757d; --bs-gray: #6c757d;
--bs-gray-dark: #343a40; --bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa; --bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef; --bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6; --bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da; --bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd; --bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d; --bs-gray-600: #6c757d;
--bs-gray-700: #495057; --bs-gray-700: #495057;
--bs-gray-800: #343a40; --bs-gray-800: #343a40;
--bs-gray-900: #212529; --bs-gray-900: #212529;
--bs-primary: #0d6efd; --bs-primary: #0d6efd;
--bs-secondary: #6c757d; --bs-secondary: #6c757d;
--bs-success: #198754; --bs-success: #198754;
--bs-info: #0dcaf0; --bs-info: #0dcaf0;
--bs-warning: #ffc107; --bs-warning: #ffc107;
--bs-danger: #dc3545; --bs-danger: #dc3545;
--bs-light: #f8f9fa; --bs-light: #f8f9fa;
--bs-dark: #212529; --bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253; --bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125; --bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84; --bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240; --bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7; --bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69; --bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250; --bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41; --bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65; --bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32; --bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622; --bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160; --bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03; --bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c; --bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057; --bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057; --bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff; --bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5; --bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd; --bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc; --bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd; --bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da; --bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd; --bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da; --bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe; --bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb; --bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb; --bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9; --bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c; --bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5; --bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef; --bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd; --bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255; --bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0; --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-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-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-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-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem; --bs-body-font-size: 1rem;
--bs-body-font-weight: 400; --bs-body-font-weight: 400;
--bs-body-line-height: 1.5; --bs-body-line-height: 1.5;
--bs-body-color: #212529; --bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41; --bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff; --bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255; --bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000; --bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0; --bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75); --bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41; --bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef; --bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239; --bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5); --bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41; --bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa; --bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250; --bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit; --bs-heading-color: inherit;
--bs-link-color: #0d6efd; --bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253; --bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline; --bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca; --bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202; --bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384; --bs-code-color: #d63384;
--bs-highlight-color: #212529; --bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd; --bs-highlight-bg: #fff3cd;
--bs-border-width: 1px; --bs-border-width: 1px;
--bs-border-style: solid; --bs-border-style: solid;
--bs-border-color: #dee2e6; --bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175); --bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem; --bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem; --bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem; --bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem; --bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem; --bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl); --bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem; --bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); --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-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-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-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem; --bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25; --bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25); --bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754; --bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754; --bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545; --bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545; --bs-form-invalid-border-color: #dc3545;
} }
[data-bs-theme=dark] { [data-bs-theme=dark] {
color-scheme: dark; color-scheme: dark;
--bs-body-color: #dee2e6; --bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230; --bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529; --bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41; --bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff; --bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255; --bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75); --bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230; --bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40; --bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64; --bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5); --bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230; --bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035; --bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53; --bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe; --bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1; --bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798; --bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6; --bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a; --bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f; --bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa; --bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6; --bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633; --bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719; --bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11; --bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830; --bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701; --bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e; --bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40; --bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20; --bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298; --bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b; --bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132; --bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990; --bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404; --bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029; --bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057; --bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40; --bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit; --bs-heading-color: inherit;
--bs-link-color: #6ea8fe; --bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe; --bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254; --bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254; --bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5; --bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6; --bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03; --bs-highlight-bg: #664d03;
--bs-border-color: #495057; --bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15); --bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798; --bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798; --bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f; --bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f; --bs-form-invalid-border-color: #ea868f;
} }
*, *,
*::before, *::before,
*::after { *::after {
box-sizing: border-box; box-sizing: border-box;
} }
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
:root { :root {
scroll-behavior: smooth; scroll-behavior: smooth;
} }
} }
body { body {
margin: 0; margin: 0;
font-family: var(--bs-body-font-family); font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size); font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight); font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height); line-height: var(--bs-body-line-height);
color: var(--bs-body-color); color: var(--bs-body-color);
text-align: var(--bs-body-text-align); text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg); background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
} }
hr { hr {
margin: 1rem 0; margin: 1rem 0;
color: inherit; color: inherit;
border: 0; border: 0;
border-top: var(--bs-border-width) solid; border-top: var(--bs-border-width) solid;
opacity: 0.25; opacity: 0.25;
} }
h6, h5, h4, h3, h2, h1 { h6, h5, h4, h3, h2, h1 {
margin-top: 0; margin-top: 0;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-weight: 500; font-weight: 500;
line-height: 1.2; line-height: 1.2;
color: var(--bs-heading-color); color: var(--bs-heading-color);
} }
h1 { h1 {
font-size: calc(1.375rem + 1.5vw); font-size: calc(1.375rem + 1.5vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h1 { h1 {
font-size: 2.5rem; font-size: 2.5rem;
} }
} }
h2 { h2 {
font-size: calc(1.325rem + 0.9vw); font-size: calc(1.325rem + 0.9vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h2 { h2 {
font-size: 2rem; font-size: 2rem;
} }
} }
h3 { h3 {
font-size: calc(1.3rem + 0.6vw); font-size: calc(1.3rem + 0.6vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h3 { h3 {
font-size: 1.75rem; font-size: 1.75rem;
} }
} }
h4 { h4 {
font-size: calc(1.275rem + 0.3vw); font-size: calc(1.275rem + 0.3vw);
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
h4 { h4 {
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
h5 { h5 {
font-size: 1.25rem; font-size: 1.25rem;
} }
h6 { h6 {
font-size: 1rem; font-size: 1rem;
} }
p { p {
margin-top: 0; margin-top: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
abbr[title] { abbr[title] {
-webkit-text-decoration: underline dotted; -webkit-text-decoration: underline dotted;
text-decoration: underline dotted; text-decoration: underline dotted;
cursor: help; cursor: help;
-webkit-text-decoration-skip-ink: none; -webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none; text-decoration-skip-ink: none;
} }
address { address {
margin-bottom: 1rem; margin-bottom: 1rem;
font-style: normal; font-style: normal;
line-height: inherit; line-height: inherit;
} }
ol, ol,
ul { ul {
padding-right: 2rem; padding-right: 2rem;
} }
ol, ol,
ul, ul,
dl { dl {
margin-top: 0; margin-top: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
ol ol, ol ol,
ul ul, ul ul,
ol ul, ol ul,
ul ol { ul ol {
margin-bottom: 0; margin-bottom: 0;
} }
dt { dt {
font-weight: 700; font-weight: 700;
} }
dd { dd {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
margin-right: 0; margin-right: 0;
} }
blockquote { blockquote {
margin: 0 0 1rem; margin: 0 0 1rem;
} }
b, b,
strong { strong {
font-weight: bolder; font-weight: bolder;
} }
small { small {
font-size: 0.875em; font-size: 0.875em;
} }
mark { mark {
padding: 0.1875em; padding: 0.1875em;
color: var(--bs-highlight-color); color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg); background-color: var(--bs-highlight-bg);
} }
sub, sub,
sup { sup {
position: relative; position: relative;
font-size: 0.75em; font-size: 0.75em;
line-height: 0; line-height: 0;
vertical-align: baseline; vertical-align: baseline;
} }
sub { sub {
bottom: -0.25em; bottom: -0.25em;
} }
sup { sup {
top: -0.5em; top: -0.5em;
} }
a { a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline; text-decoration: underline;
} }
a:hover { a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb); --bs-link-color-rgb: var(--bs-link-hover-color-rgb);
} }
a:not([href]):not([class]), a:not([href]):not([class]):hover { a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
} }
pre, pre,
code, code,
kbd, kbd,
samp { samp {
font-family: var(--bs-font-monospace); font-family: var(--bs-font-monospace);
font-size: 1em; font-size: 1em;
} }
pre { pre {
display: block; display: block;
margin-top: 0; margin-top: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
overflow: auto; overflow: auto;
font-size: 0.875em; font-size: 0.875em;
} }
pre code { pre code {
font-size: inherit; font-size: inherit;
color: inherit; color: inherit;
word-break: normal; word-break: normal;
} }
code { code {
font-size: 0.875em; font-size: 0.875em;
color: var(--bs-code-color); color: var(--bs-code-color);
word-wrap: break-word; word-wrap: break-word;
} }
a > code { a > code {
color: inherit; color: inherit;
} }
kbd { kbd {
padding: 0.1875rem 0.375rem; padding: 0.1875rem 0.375rem;
font-size: 0.875em; font-size: 0.875em;
color: var(--bs-body-bg); color: var(--bs-body-bg);
background-color: var(--bs-body-color); background-color: var(--bs-body-color);
border-radius: 0.25rem; border-radius: 0.25rem;
} }
kbd kbd { kbd kbd {
padding: 0; padding: 0;
font-size: 1em; font-size: 1em;
} }
figure { figure {
margin: 0 0 1rem; margin: 0 0 1rem;
} }
img, img,
svg { svg {
vertical-align: middle; vertical-align: middle;
} }
table { table {
caption-side: bottom; caption-side: bottom;
border-collapse: collapse; border-collapse: collapse;
} }
caption { caption {
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
color: var(--bs-secondary-color); color: var(--bs-secondary-color);
text-align: right; text-align: right;
} }
th { th {
text-align: inherit; text-align: inherit;
text-align: -webkit-match-parent; text-align: -webkit-match-parent;
} }
thead, thead,
@@ -434,21 +442,21 @@ tfoot,
tr, tr,
td, td,
th { th {
border-color: inherit; border-color: inherit;
border-style: solid; border-style: solid;
border-width: 0; border-width: 0;
} }
label { label {
display: inline-block; display: inline-block;
} }
button { button {
border-radius: 0; border-radius: 0;
} }
button:focus:not(:focus-visible) { button:focus:not(:focus-visible) {
outline: 0; outline: 0;
} }
input, input,
@@ -456,76 +464,80 @@ button,
select, select,
optgroup, optgroup,
textarea { textarea {
margin: 0; margin: 0;
font-family: inherit; font-family: inherit;
font-size: inherit; font-size: inherit;
line-height: inherit; line-height: inherit;
} }
button, button,
select { select {
text-transform: none; text-transform: none;
} }
[role=button] { [role=button] {
cursor: pointer; cursor: pointer;
} }
select { select {
word-wrap: normal; word-wrap: normal;
} }
select:disabled { select:disabled {
opacity: 1; opacity: 1;
} }
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important; display: none !important;
} }
button, button,
[type=button], [type=button],
[type=reset], [type=reset],
[type=submit] { [type=submit] {
-webkit-appearance: button; -webkit-appearance: button;
} }
button:not(:disabled), button:not(:disabled),
[type=button]:not(:disabled), [type=button]:not(:disabled),
[type=reset]:not(:disabled), [type=reset]:not(:disabled),
[type=submit]:not(:disabled) { [type=submit]:not(:disabled) {
cursor: pointer; cursor: pointer;
} }
::-moz-focus-inner { ::-moz-focus-inner {
padding: 0; padding: 0;
border-style: none; border-style: none;
} }
textarea { textarea {
resize: vertical; resize: vertical;
} }
fieldset { fieldset {
min-width: 0; min-width: 0;
padding: 0; padding: 0;
margin: 0; margin: 0;
border: 0; border: 0;
} }
legend { legend {
float: right; float: right;
width: 100%; width: 100%;
padding: 0; padding: 0;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw); font-size: calc(1.275rem + 0.3vw);
line-height: inherit; line-height: inherit;
} }
@media (min-width: 1200px) { @media (min-width: 1200px) {
legend { legend {
font-size: 1.5rem; font-size: 1.5rem;
} }
} }
legend + * { legend + * {
clear: right; clear: right;
} }
::-webkit-datetime-edit-fields-wrapper, ::-webkit-datetime-edit-fields-wrapper,
@@ -535,60 +547,62 @@ legend + * {
::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field { ::-webkit-datetime-edit-year-field {
padding: 0; padding: 0;
} }
::-webkit-inner-spin-button { ::-webkit-inner-spin-button {
height: auto; height: auto;
} }
[type=search] { [type=search] {
-webkit-appearance: textfield; -webkit-appearance: textfield;
outline-offset: -2px; outline-offset: -2px;
} }
[type="tel"], [type="tel"],
[type="url"], [type="url"],
[type="email"], [type="email"],
[type="number"] { [type="number"] {
direction: ltr; direction: ltr;
} }
::-webkit-search-decoration { ::-webkit-search-decoration {
-webkit-appearance: none; -webkit-appearance: none;
} }
::-webkit-color-swatch-wrapper { ::-webkit-color-swatch-wrapper {
padding: 0; padding: 0;
} }
::-webkit-file-upload-button { ::-webkit-file-upload-button {
font: inherit; font: inherit;
-webkit-appearance: button; -webkit-appearance: button;
} }
::file-selector-button { ::file-selector-button {
font: inherit; font: inherit;
-webkit-appearance: button; -webkit-appearance: button;
} }
output { output {
display: inline-block; display: inline-block;
} }
iframe { iframe {
border: 0; border: 0;
} }
summary { summary {
display: list-item; display: list-item;
cursor: pointer; cursor: pointer;
} }
progress { progress {
vertical-align: baseline; vertical-align: baseline;
} }
[hidden] { [hidden] {
display: none !important; display: none !important;
} }
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */ /*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -21,10 +21,10 @@ public class CardData
public string? ImmortalizeFrom { get; init; } public string? ImmortalizeFrom { get; init; }
public string? ImmortalizeWhen { get; init; } public string? ImmortalizeWhen { get; init; }
public string? ImageFile { get; init; } public string? ImageFile { get; init; }
public string? Artist { get; init; } public string? Artist { get; init; }
public string? Rarity { get; init; } public string? Rarity { get; init; }
public bool IsAgent => Category == "Agent"; public bool IsAgent => Category == "Agent";
public bool IsSpell => Category == "Spell"; public bool IsSpell => Category == "Spell";
public bool IsToken => Category == "Token"; public bool IsToken => Category == "Token";
+1 -1
View File
@@ -7,7 +7,7 @@ public class DeckData
public List<string> Keycards { get; init; } = []; public List<string> Keycards { get; init; } = [];
public List<string> Divers { get; init; } = []; public List<string> Divers { get; init; } = [];
public string? Description { get; init; } public string? Description { get; init; }
public string? DeckCode { get; init; } public string? DeckCode { get; init; }
public string? DeckType { get; init; } public string? DeckType { get; init; }
+2 -2
View File
@@ -5,5 +5,5 @@ public class DocData
public string Title { get; init; } = ""; public string Title { get; init; } = "";
public string Category { get; init; } = ""; public string Category { get; init; } = "";
public string Content { get; init; } = ""; public string Content { get; init; } = "";
public System.Collections.Generic.Dictionary<string, string> Frontmatter { get; init; } = []; public Dictionary<string, string> Frontmatter { get; init; } = [];
} }
+2 -2
View File
@@ -20,8 +20,8 @@
</head> </head>
<body> <body>
<Routes /> <Routes/>
<script src="_framework/blazor.web.js"></script> <script src="_framework/blazor.web.js"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -1,4 +1,4 @@
<Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Shared.Pages.Home).Assembly }"> <Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Home).Assembly }">
<Found Context="routeData"> <Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/> <FocusOnNavigate RouteData="@routeData" Selector="h1"/>
+8 -8
View File
@@ -1,5 +1,8 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Server; using Server;
using Server.Components;
using Shared.Pages;
using Shared.Services;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -7,7 +10,7 @@ builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddRazorComponents().AddInteractiveServerComponents(); builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddTelerikBlazor(); builder.Services.AddTelerikBlazor();
builder.Services.AddSingleton<Shared.Services.CardRenderingService>(); builder.Services.AddSingleton<CardRenderingService>();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<AppDbContext>(options => builder.Services.AddDbContext<AppDbContext>(options =>
@@ -32,17 +35,14 @@ using (var scope = app.Services.CreateScope())
} }
} }
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment()) app.UseExceptionHandler("/Error");
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles(); app.UseStaticFiles();
app.UseAntiforgery(); app.UseAntiforgery();
app.MapControllers(); app.MapControllers();
app.MapRazorComponents<Server.Components.App>() app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode() .AddInteractiveServerRenderMode()
.AddAdditionalAssemblies(typeof(Shared.Pages.Home).Assembly); .AddAdditionalAssemblies(typeof(Home).Assembly);
app.Run(); app.Run();
-3
View File
@@ -257,9 +257,6 @@ a, .btn-link {
} }
.inline-card-btn { .inline-card-btn {
background: none; background: none;
border: none; border: none;
+16 -8
View File
@@ -1,5 +1,4 @@
@namespace Shared.Components @namespace Shared.Components
@using Chrono.Model
@if (Card != null) @if (Card != null)
{ {
@@ -11,7 +10,7 @@
{ {
<div class="detail-image"> <div class="detail-image">
<img src="@Card.ImagePath" alt="@Card.Name" <img src="@Card.ImagePath" alt="@Card.Name"
onerror="this.style.display='none';this.parentElement.style.display='none'" /> onerror="this.style.display='none';this.parentElement.style.display='none'"/>
</div> </div>
} }
<div class="detail-info"> <div class="detail-info">
@@ -61,7 +60,7 @@
<span class="field-value">@Card.Set</span> <span class="field-value">@Card.Set</span>
</div> </div>
} }
@if (Card.Artist != null) @if (Card.Artist != null)
{ {
<div class="detail-field"> <div class="detail-field">
@@ -69,7 +68,7 @@
<span class="field-value">@Card.Artist</span> <span class="field-value">@Card.Artist</span>
</div> </div>
} }
@if (Card.Rarity != null) @if (Card.Rarity != null)
{ {
<div class="detail-field"> <div class="detail-field">
@@ -105,7 +104,8 @@
var target = LookupCard(targetName); var target = LookupCard(targetName);
if (target != null) if (target != null)
{ {
<button class="inline-card-btn" @onclick="() => SelectCard(target)">@targetName</button> <button class="inline-card-btn"
@onclick="() => SelectCard(target)">@targetName</button>
} }
else else
{ {
@@ -125,7 +125,8 @@
var fromCard = LookupCard(Card.ImmortalizeFrom); var fromCard = LookupCard(Card.ImmortalizeFrom);
if (fromCard != null) if (fromCard != null)
{ {
<button class="inline-card-btn" @onclick="() => SelectCard(fromCard)">@Card.ImmortalizeFrom</button> <button class="inline-card-btn"
@onclick="() => SelectCard(fromCard)">@Card.ImmortalizeFrom</button>
} }
else else
{ {
@@ -148,9 +149,15 @@
private bool HasImage => Card?.ImageFile is { Length: > 0 } && Card.ImageFile != "placeholder.png"; private bool HasImage => Card?.ImageFile is { Length: > 0 } && Card.ImageFile != "placeholder.png";
private async Task Close() => await OnClose.InvokeAsync(); private async Task Close()
{
await OnClose.InvokeAsync();
}
private async Task SelectCard(CardData card) => await OnSelectCard.InvokeAsync(card); private async Task SelectCard(CardData card)
{
await OnSelectCard.InvokeAsync(card);
}
private RenderFragment RenderCardDescription(string description) private RenderFragment RenderCardDescription(string description)
{ {
@@ -163,4 +170,5 @@
return CardDatabase.Cards.FirstOrDefault(c => return CardDatabase.Cards.FirstOrDefault(c =>
string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase)); string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase));
} }
} }
@@ -10,8 +10,12 @@
} }
@keyframes fade-in { @keyframes fade-in {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
.card-detail { .card-detail {
+6 -3
View File
@@ -1,5 +1,4 @@
@namespace Shared.Components @namespace Shared.Components
@using Chrono.Model
<div class="deck-card-row"> <div class="deck-card-row">
@foreach (var group in Cards.GroupBy(x => x)) @foreach (var group in Cards.GroupBy(x => x))
@@ -29,10 +28,13 @@
</div> </div>
@code { @code {
[Parameter, EditorRequired] public List<string> Cards { get; set; } = []; [Parameter][EditorRequired] public List<string> Cards { get; set; } = [];
[Parameter] public EventCallback<CardData?> OnSelectCard { get; set; } [Parameter] public EventCallback<CardData?> OnSelectCard { get; set; }
private async Task SelectCard(CardData? card) => await OnSelectCard.InvokeAsync(card); private async Task SelectCard(CardData? card)
{
await OnSelectCard.InvokeAsync(card);
}
private static readonly Dictionary<string, CardData> CardLookup = CardDatabase.Cards private static readonly Dictionary<string, CardData> CardLookup = CardDatabase.Cards
.Where(c => !string.IsNullOrWhiteSpace(c.Name)) .Where(c => !string.IsNullOrWhiteSpace(c.Name))
@@ -43,4 +45,5 @@
{ {
return CardLookup.GetValueOrDefault(cardName); return CardLookup.GetValueOrDefault(cardName);
} }
} }
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="mana-curve"> <div class="mana-curve">
@foreach (var entry in Distribution) @foreach (var entry in Distribution)
{ {
var height = MaxCount > 0 ? (entry.Count * 100 / MaxCount) : 0; var height = MaxCount > 0 ? entry.Count * 100 / MaxCount : 0;
<div class="mana-bar-container"> <div class="mana-bar-container">
<div class="mana-bar-label">@entry.Count</div> <div class="mana-bar-label">@entry.Count</div>
<div class="mana-bar" style="height: @($"{Math.Max(height, 5)}%")"></div> <div class="mana-bar" style="height: @($"{Math.Max(height, 5)}%")"></div>
@@ -13,7 +13,7 @@
</div> </div>
@code { @code {
[Parameter, EditorRequired] public List<(int Cost, int Count)> Distribution { get; set; } = []; [Parameter][EditorRequired] public List<(int Cost, int Count)> Distribution { get; set; } = [];
[Parameter] public int MaxCostLabel { get; set; } = 10; [Parameter] public int MaxCostLabel { get; set; } = 10;
private int MaxCount => Distribution.Count > 0 ? Distribution.Max(x => x.Count) : 0; private int MaxCount => Distribution.Count > 0 ? Distribution.Max(x => x.Count) : 0;
@@ -0,0 +1,40 @@
@implements IDisposable
@inject NavigationManager NavigationManager
@inject AnalyticsService AnalyticsService
@code {
protected override void OnInitialized()
{
NavigationManager.LocationChanged += OnLocationChanged;
}
private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
await TrackPageView(e.Location);
}
private async Task TrackPageView(string url)
{
try
{
var relativePath = NavigationManager.ToBaseRelativePath(url);
if (!relativePath.StartsWith("/"))
{
relativePath = "/" + relativePath;
}
await AnalyticsService.TrackPageView(relativePath);
}
catch
{
// Ignore errors during tracking
}
}
public void Dispose()
{
NavigationManager.LocationChanged -= OnLocationChanged;
}
}
+8 -8
View File
@@ -154,7 +154,7 @@ public static class DocDatabase
{ {
Title = "Lifeblood", Title = "Lifeblood",
Category = "Faction", Category = "Faction",
Content = "Concepts:\r\n- Wide Boards\r\n- Overpower", Content = "Concepts:\r\n\r\n- Wide Boards\r\n- Overpower",
Frontmatter = new() Frontmatter = new()
{ {
{ "category", "Faction" }, { "category", "Faction" },
@@ -164,7 +164,7 @@ public static class DocDatabase
{ {
Title = "Phasetide", Title = "Phasetide",
Category = "Faction", Category = "Faction",
Content = "Concepts:\r\n- Healing\r\n-", Content = "Concepts:\r\n\r\n- Healing\r\n-",
Frontmatter = new() Frontmatter = new()
{ {
{ "category", "Faction" }, { "category", "Faction" },
@@ -174,7 +174,7 @@ public static class DocDatabase
{ {
Title = "Silence", Title = "Silence",
Category = "Faction", Category = "Faction",
Content = "Concepts:\r\n- Activatable Agents\r\n-", Content = "Concepts:\r\n\r\n- Activatable Agents\r\n-",
Frontmatter = new() Frontmatter = new()
{ {
{ "category", "Faction" }, { "category", "Faction" },
@@ -184,7 +184,7 @@ public static class DocDatabase
{ {
Title = "Singularity", Title = "Singularity",
Category = "Faction", Category = "Faction",
Content = "Concepts:\r\n- Discard", Content = "Concepts:\r\n\r\n- Discard",
Frontmatter = new() Frontmatter = new()
{ {
{ "category", "Faction" }, { "category", "Faction" },
@@ -194,7 +194,7 @@ public static class DocDatabase
{ {
Title = "Splintergleam", Title = "Splintergleam",
Category = "Faction", Category = "Faction",
Content = "# Self Damage\r\n\r\nCan hurt it's own units for power. Has [[Bleed]] to make reoccurring damage on units very easy.\r\n\r\n# Core Damage \r\n\r\nCan hurt both cores easily. Has [[Breakdown]] to take advantage of having an injured core for more damage.", Content = "# Self Damage\r\n\r\nCan hurt it's own units for power. Has [[Bleed]] to make reoccurring damage on units very easy.\r\n\r\n# Core Damage\r\n\r\nCan hurt both cores easily. Has [[Breakdown]] to take advantage of having an injured core for more damage.",
Frontmatter = new() Frontmatter = new()
{ {
{ "category", "Faction" }, { "category", "Faction" },
@@ -204,7 +204,7 @@ public static class DocDatabase
{ {
Title = "Sungrace", Title = "Sungrace",
Category = "Faction", Category = "Faction",
Content = "# Energy Reserves\r\n\r\nCare about always having banked energy. Spells can refund themselves when using [[Energy Reserves]], or get stronger is you [[Overflow]] your energy reserve.\r\n\r\nConcepts:\r\n- Ramp\r\n- Using and Replenishing Reserved Energy\r\n-", Content = "# Energy Reserves\r\n\r\nCare about always having banked energy. Spells can refund themselves when using [[Energy Reserves]], or get stronger is\r\nyou [[Overflow]] your energy reserve.\r\n\r\nConcepts:\r\n\r\n- Ramp\r\n- Using and Replenishing Reserved Energy\r\n-",
Frontmatter = new() Frontmatter = new()
{ {
{ "category", "Faction" }, { "category", "Faction" },
@@ -479,7 +479,7 @@ public static class DocDatabase
{ {
Title = "Onboarding Track", Title = "Onboarding Track",
Category = "Pass", Category = "Pass",
Content = "| Tier | Reward |\r\n| ---- | -------------------------------- |\r\n| 1 | Sprouts Record |\r\n| 2 | 120 Core Charges |\r\n| 3 | Burn Precon |\r\n| 4 | 120 Core Charges, 25 Flux |\r\n| 5 | Profile Pictures |\r\n| 6 | Emotes |\r\n| 7 | 120 Core Charges |\r\n| 8 | Profile Pictures |\r\n| 9 | 60 Core Charges, 50 Flux |\r\n| 10 | 240 Core Charges, 1 Draft Ticket |\r\n\r\n| Core Charges | Flux |\r\n| ------------ | ---- |\r\n| 660 | 75 |", Content = "| Tier | Reward |\r\n|------|----------------------------------|\r\n| 1 | Sprouts Record |\r\n| 2 | 120 Core Charges |\r\n| 3 | Burn Precon |\r\n| 4 | 120 Core Charges, 25 Flux |\r\n| 5 | Profile Pictures |\r\n| 6 | Emotes |\r\n| 7 | 120 Core Charges |\r\n| 8 | Profile Pictures |\r\n| 9 | 60 Core Charges, 50 Flux |\r\n| 10 | 240 Core Charges, 1 Draft Ticket |\r\n\r\n| Core Charges | Flux |\r\n|--------------|------|\r\n| 660 | 75 |",
Frontmatter = new() Frontmatter = new()
{ {
{ "expPer", "30" }, { "expPer", "30" },
@@ -491,7 +491,7 @@ public static class DocDatabase
{ {
Title = "Daily Wins", Title = "Daily Wins",
Category = "Quest", Category = "Quest",
Content = "1: 30cc 30xp \n2: 20cc 25xp \n3: 20cc 20xp \n4: 10cc 20xp \n5: 10cc 15xp \n6-12 are all simply 5xp. \n\nSo the daily total win reward for 12 wins is 90cc/145xp.", Content = "1: 30cc 30xp\n2: 20cc 25xp\n3: 20cc 20xp\n4: 10cc 20xp\n5: 10cc 15xp\n6-12 are all simply 5xp.\n\nSo the daily total win reward for 12 wins is 90cc/145xp.",
Frontmatter = new() Frontmatter = new()
{ {
{ "category", "Quest" }, { "category", "Quest" },
+1
View File
@@ -6,6 +6,7 @@
</div> </div>
<main> <main>
<NavigationTracker/>
<article class="content px-4"> <article class="content px-4">
<TelerikRootComponent> <TelerikRootComponent>
@Body @Body
+2 -1
View File
@@ -4,7 +4,8 @@
<PageTitle>Chrono CCG - Agents</PageTitle> <PageTitle>Chrono CCG - Agents</PageTitle>
<HeadContent> <HeadContent>
<meta name="description" content="View and compare all Chrono CCG agents. Analyze stats, factions, and efficiency to build the perfect deck." /> <meta name="description"
content="View and compare all Chrono CCG agents. Analyze stats, factions, and efficiency to build the perfect deck."/>
</HeadContent> </HeadContent>
<div class="agents-page"> <div class="agents-page">
+14 -11
View File
@@ -5,7 +5,8 @@
<PageTitle>Chrono CCG - @(deck?.Name ?? "Deck Details")</PageTitle> <PageTitle>Chrono CCG - @(deck?.Name ?? "Deck Details")</PageTitle>
<HeadContent> <HeadContent>
<meta name="description" content="@(deck != null ? $"View details for the {deck.Name} deck. {deck.Cards.Count} cards, {deck.Factions.Count} factions." : "Deck details and card list.")" /> <meta name="description"
content="@(deck != null ? $"View details for the {deck.Name} deck. {deck.Cards.Count} cards, {deck.Factions.Count} factions." : "Deck details and card list.")"/>
</HeadContent> </HeadContent>
<div class="deck-detail-page"> <div class="deck-detail-page">
@@ -23,7 +24,8 @@
<article class="deck-article"> <article class="deck-article">
<header class="deck-header"> <header class="deck-header">
<h1>@deck.Name</h1> <h1>@deck.Name</h1>
<b>Deck Code:</b> <pre>@deck.DeckCode</pre> <b>Deck Code:</b>
<pre>@deck.DeckCode</pre>
@if (deck.Factions.Count > 0) @if (deck.Factions.Count > 0)
{ {
<div class="deck-factions"> <div class="deck-factions">
@@ -44,14 +46,14 @@
} }
</div> </div>
<ManaCurve Distribution="GetManaDistribution()" MaxCostLabel="10" /> <ManaCurve Distribution="GetManaDistribution()" MaxCostLabel="10"/>
</header> </header>
@if (deck.Keycards.Count > 0) @if (deck.Keycards.Count > 0)
{ {
<section class="deck-section"> <section class="deck-section">
<h2><i class="bi bi-star-fill" style="color: var(--gold);"></i> Keycards</h2> <h2><i class="bi bi-star-fill" style="color: var(--gold);"></i> Keycards</h2>
<CardGrid Cards="deck.Keycards" OnSelectCard="SelectCard" /> <CardGrid Cards="deck.Keycards" OnSelectCard="SelectCard"/>
</section> </section>
} }
@@ -59,13 +61,13 @@
{ {
<section class="deck-section"> <section class="deck-section">
<h2><i class="bi bi-shuffle"></i> Divers</h2> <h2><i class="bi bi-shuffle"></i> Divers</h2>
<CardGrid Cards="deck.Divers" OnSelectCard="SelectCard" /> <CardGrid Cards="deck.Divers" OnSelectCard="SelectCard"/>
</section> </section>
} }
<section class="deck-section"> <section class="deck-section">
<h2><i class="bi bi-collection-fill"></i> Cards</h2> <h2><i class="bi bi-collection-fill"></i> Cards</h2>
<CardGrid Cards="deck.Cards" OnSelectCard="SelectCard" /> <CardGrid Cards="deck.Cards" OnSelectCard="SelectCard"/>
</section> </section>
@if (deck.Description != null) @if (deck.Description != null)
@@ -81,10 +83,10 @@
} }
</div> </div>
<CardDetailModal Card="selectedCard" <CardDetailModal Card="selectedCard"
OnClose="CloseDetail" OnClose="CloseDetail"
OnSelectCard="SelectCard" OnSelectCard="SelectCard"
DescriptionRenderer="@(desc => CardRenderer.RenderDescription(desc, SelectCard))" /> DescriptionRenderer="@(desc => CardRenderer.RenderDescription(desc, SelectCard))"/>
@code { @code {
[Parameter] public string Name { get; set; } = ""; [Parameter] public string Name { get; set; } = "";
@@ -124,7 +126,7 @@
{ {
if (deck == null) return []; if (deck == null) return [];
var counts = new Dictionary<int, int>(); var counts = new Dictionary<int, int>();
for (int i = 0; i <= 10; i++) counts[i] = 0; for (var i = 0; i <= 10; i++) counts[i] = 0;
foreach (var cardName in deck.Cards) foreach (var cardName in deck.Cards)
{ {
@@ -139,4 +141,5 @@
return counts.OrderBy(x => x.Key).Select(x => (x.Key, x.Value)).ToList(); return counts.OrderBy(x => x.Key).Select(x => (x.Key, x.Value)).ToList();
} }
} }
+11 -9
View File
@@ -4,7 +4,8 @@
<PageTitle>Chrono CCG - Decks</PageTitle> <PageTitle>Chrono CCG - Decks</PageTitle>
<HeadContent> <HeadContent>
<meta name="description" content="Explore community and official Chrono CCG decks. Find the best strategies and card combinations." /> <meta name="description"
content="Explore community and official Chrono CCG decks. Find the best strategies and card combinations."/>
</HeadContent> </HeadContent>
<div class="decks-page"> <div class="decks-page">
@@ -13,7 +14,8 @@
<div class="decks-header-row"> <div class="decks-header-row">
<p class="text-secondary">@decks.Count deck@(decks.Count != 1 ? "s" : "")</p> <p class="text-secondary">@decks.Count deck@(decks.Count != 1 ? "s" : "")</p>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="showPrecons" @bind="showPrecons" @bind:after="RefreshDecks"> <input class="form-check-input" type="checkbox" id="showPrecons" @bind="showPrecons"
@bind:after="RefreshDecks">
<label class="form-check-label" for="showPrecons">Show Precons</label> <label class="form-check-label" for="showPrecons">Show Precons</label>
</div> </div>
</div> </div>
@@ -34,11 +36,11 @@
} }
</div> </div>
} }
<div class="deck-card-factions"> <div class="deck-card-factions">
<span class="faction-badge">@deck.DeckType</span> <span class="faction-badge">@deck.DeckType</span>
</div> </div>
<div class="deck-card-meta"> <div class="deck-card-meta">
<span>@deck.Cards.Distinct().Count() unique, @deck.Cards.Count total</span> <span>@deck.Cards.Distinct().Count() unique, @deck.Cards.Count total</span>
@if (deck.Keycards.Count > 0) @if (deck.Keycards.Count > 0)
@@ -70,7 +72,7 @@
@code { @code {
private List<DeckData> decks = []; private List<DeckData> decks = [];
private bool showPrecons = false; private bool showPrecons;
protected override void OnInitialized() protected override void OnInitialized()
{ {
@@ -81,10 +83,10 @@
{ {
decks = DeckDatabase.Decks decks = DeckDatabase.Decks
.Where(d => d.IsVisible) .Where(d => d.IsVisible)
.Where(d => .Where(d =>
showPrecons showPrecons
? d.DeckType is "precon" or "store" : ? d.DeckType is "precon" or "store"
d.DeckType is "custom") : d.DeckType is "custom")
.ToList(); .ToList();
} }
+24 -18
View File
@@ -13,17 +13,19 @@
<aside class="docs-sidebar"> <aside class="docs-sidebar">
<div class="search-box"> <div class="search-box">
<i class="bi bi-search"></i> <i class="bi bi-search"></i>
<input type="text" placeholder="Search docs..." @bind="searchQuery" @bind:event="oninput" /> <input type="text" placeholder="Search docs..." @bind="searchQuery" @bind:event="oninput"/>
</div> </div>
<div class="category-list"> <div class="category-list">
<div class="category-item @(selectedCategory == null ? "active" : "")" @onclick="() => SelectCategory(null)"> <div class="category-item @(selectedCategory == null ? "active" : "")"
@onclick="() => SelectCategory(null)">
All Pages All Pages
</div> </div>
@foreach (var category in categories) @foreach (var category in categories)
{ {
var cat = category; var cat = category;
<div class="category-item @(selectedCategory == cat ? "active" : "")" @onclick="() => SelectCategory(cat)"> <div class="category-item @(selectedCategory == cat ? "active" : "")"
@onclick="() => SelectCategory(cat)">
@cat @cat
</div> </div>
} }
@@ -134,20 +136,24 @@
return doc.Frontmatter.TryGetValue(key, out var val) && !string.IsNullOrWhiteSpace(val) ? val : null; return doc.Frontmatter.TryGetValue(key, out var val) && !string.IsNullOrWhiteSpace(val) ? val : null;
} }
private static string GetLabel(string key) => key switch private static string GetLabel(string key)
{ {
"category" => "Category", return key switch
"description" => "Description", {
"essencePrice" => "Essence Price", "category" => "Category",
"crytalPrice" => "Crystal Price", "description" => "Description",
"coreChargesPrice" => "Core Charges Price", "essencePrice" => "Essence Price",
"pricePerPack" => "Price per Pack", "crytalPrice" => "Crystal Price",
"codeCharges" => "Code Charges", "coreChargesPrice" => "Core Charges Price",
"coreCharges" => "Core Charges", "pricePerPack" => "Price per Pack",
"currency" => "Currency", "codeCharges" => "Code Charges",
"faction" => "Faction", "coreCharges" => "Core Charges",
"exp" => "EXP Reward", "currency" => "Currency",
"see" => "See Also", "faction" => "Faction",
_ => string.Concat(key.Select((c, i) => i > 0 && char.IsUpper(c) ? " " + c : c.ToString())), "exp" => "EXP Reward",
}; "see" => "See Also",
_ => string.Concat(key.Select((c, i) => i > 0 && char.IsUpper(c) ? " " + c : c.ToString()))
};
}
} }
@@ -0,0 +1,26 @@
using Microsoft.JSInterop;
namespace Shared.Services;
public class AnalyticsService
{
private readonly IJSRuntime _jsRuntime;
public AnalyticsService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async ValueTask TrackPageView(string path)
{
await _jsRuntime.InvokeVoidAsync("trackPageView", path);
}
public async ValueTask TrackEvent(string eventName, object? eventData = null)
{
if (eventData != null)
await _jsRuntime.InvokeVoidAsync("gtag", "event", eventName, eventData);
else
await _jsRuntime.InvokeVoidAsync("gtag", "event", eventName);
}
}
+9 -13
View File
@@ -1,13 +1,13 @@
using Microsoft.AspNetCore.Components; using System.Text.RegularExpressions;
using Chrono.Model; using Chrono.Model;
using System.Text.RegularExpressions; using Microsoft.AspNetCore.Components;
namespace Shared.Services; namespace Shared.Services;
public class CardRenderingService public class CardRenderingService
{ {
private readonly List<string> _cardNames;
private readonly Dictionary<string, CardData> _cardLookup; private readonly Dictionary<string, CardData> _cardLookup;
private readonly List<string> _cardNames;
private readonly Regex _nameRegex; private readonly Regex _nameRegex;
public CardRenderingService() public CardRenderingService()
@@ -33,7 +33,7 @@ public class CardRenderingService
{ {
return builder => return builder =>
{ {
int sequence = 0; var sequence = 0;
var paragraphs = description.Split('\n', StringSplitOptions.RemoveEmptyEntries); var paragraphs = description.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var paragraph in paragraphs) foreach (var paragraph in paragraphs)
@@ -41,31 +41,27 @@ public class CardRenderingService
builder.OpenElement(sequence++, "p"); builder.OpenElement(sequence++, "p");
var matches = _nameRegex.Matches(paragraph); var matches = _nameRegex.Matches(paragraph);
int lastIndex = 0; var lastIndex = 0;
foreach (Match match in matches) foreach (Match match in matches)
{ {
if (match.Index > lastIndex) if (match.Index > lastIndex)
{
builder.AddContent(sequence++, paragraph.Substring(lastIndex, match.Index - lastIndex)); builder.AddContent(sequence++, paragraph.Substring(lastIndex, match.Index - lastIndex));
}
var cardName = match.Value; var cardName = match.Value;
var card = LookupCard(cardName); var card = LookupCard(cardName);
builder.OpenElement(sequence++, "button"); builder.OpenElement(sequence++, "button");
builder.AddAttribute(sequence++, "class", "inline-card-btn"); builder.AddAttribute(sequence++, "class", "inline-card-btn");
builder.AddAttribute(sequence++, "onclick", EventCallback.Factory.Create(this, () => onCardClick(card))); builder.AddAttribute(sequence++, "onclick",
EventCallback.Factory.Create(this, () => onCardClick(card)));
builder.AddContent(sequence++, cardName); builder.AddContent(sequence++, cardName);
builder.CloseElement(); builder.CloseElement();
lastIndex = match.Index + match.Length; lastIndex = match.Index + match.Length;
} }
if (lastIndex < paragraph.Length) if (lastIndex < paragraph.Length) builder.AddContent(sequence++, paragraph.Substring(lastIndex));
{
builder.AddContent(sequence++, paragraph.Substring(lastIndex));
}
builder.CloseElement(); builder.CloseElement();
} }
@@ -76,4 +72,4 @@ public class CardRenderingService
{ {
return _cardLookup.GetValueOrDefault(cardName); return _cardLookup.GetValueOrDefault(cardName);
} }
} }
+9 -10
View File
@@ -1,5 +1,4 @@
using Microsoft.Playwright; using Microsoft.Playwright.NUnit;
using Microsoft.Playwright.NUnit;
using Tests.PageObjects; using Tests.PageObjects;
namespace Tests; namespace Tests;
@@ -8,10 +7,6 @@ namespace Tests;
[TestFixture] [TestFixture]
public class FeatureTests : PageTest public class FeatureTests : PageTest
{ {
private DecksPage _decksPage;
private DeckDetailPage _deckDetailPage;
private AgentsPage _agentsPage;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
@@ -20,17 +15,21 @@ public class FeatureTests : PageTest
_agentsPage = new AgentsPage(Page); _agentsPage = new AgentsPage(Page);
} }
private DecksPage _decksPage;
private DeckDetailPage _deckDetailPage;
private AgentsPage _agentsPage;
[Test] [Test]
public async Task DeckManaCurve_ShouldBeVisibleOnDetail() public async Task DeckManaCurve_ShouldBeVisibleOnDetail()
{ {
await _decksPage.GotoAsync(); await _decksPage.GotoAsync();
// Assuming there is at least one deck. If not, this might need a more robust approach. // Assuming there is at least one deck. If not, this might need a more robust approach.
var deckCards = _decksPage.GetDeckCards(); var deckCards = _decksPage.GetDeckCards();
await Expect(deckCards.First).ToBeVisibleAsync(); await Expect(deckCards.First).ToBeVisibleAsync();
await deckCards.First.ClickAsync(); await deckCards.First.ClickAsync();
await Expect(_deckDetailPage.ManaCurve).ToBeVisibleAsync(); await Expect(_deckDetailPage.ManaCurve).ToBeVisibleAsync();
var barCount = await _deckDetailPage.GetManaBarCountAsync(); var barCount = await _deckDetailPage.GetManaBarCountAsync();
Assert.That(barCount, Is.GreaterThan(0)); Assert.That(barCount, Is.GreaterThan(0));
@@ -43,4 +42,4 @@ public class FeatureTests : PageTest
await _agentsPage.WaitForGridAsync(); await _agentsPage.WaitForGridAsync();
await Expect(_agentsPage.Grid).ToBeVisibleAsync(); await Expect(_agentsPage.Grid).ToBeVisibleAsync();
} }
} }
+10 -5
View File
@@ -4,15 +4,20 @@ namespace Tests.PageObjects;
public class AgentsPage : BasePage public class AgentsPage : BasePage
{ {
public AgentsPage(IPage page) : base(page) { } public AgentsPage(IPage page) : base(page)
{
public async Task GotoAsync() => await NavigateToAsync("/agents"); }
public ILocator Grid => Page.Locator(".agents-grid"); public ILocator Grid => Page.Locator(".agents-grid");
public async Task GotoAsync()
{
await NavigateToAsync("/agents");
}
public async Task WaitForGridAsync() public async Task WaitForGridAsync()
{ {
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
await Grid.WaitForAsync(); await Grid.WaitForAsync();
} }
} }
+6 -3
View File
@@ -4,8 +4,8 @@ namespace Tests.PageObjects;
public abstract class BasePage public abstract class BasePage
{ {
protected readonly IPage Page;
protected readonly string BaseUrl = "http://localhost:5256"; protected readonly string BaseUrl = "http://localhost:5256";
protected readonly IPage Page;
protected BasePage(IPage page) protected BasePage(IPage page)
{ {
@@ -17,5 +17,8 @@ public abstract class BasePage
await Page.GotoAsync($"{BaseUrl}{path}"); await Page.GotoAsync($"{BaseUrl}{path}");
} }
public ILocator GetHeading() => Page.Locator("h1"); public ILocator GetHeading()
} {
return Page.Locator("h1");
}
}
+8 -3
View File
@@ -4,12 +4,17 @@ namespace Tests.PageObjects;
public class DeckDetailPage : BasePage public class DeckDetailPage : BasePage
{ {
public DeckDetailPage(IPage page) : base(page) { } public DeckDetailPage(IPage page) : base(page)
{
}
public ILocator ManaCurve => Page.Locator(".mana-curve"); public ILocator ManaCurve => Page.Locator(".mana-curve");
public ILocator ManaBars => Page.Locator(".mana-bar"); public ILocator ManaBars => Page.Locator(".mana-bar");
public ILocator ManaBarLabels => Page.Locator(".mana-bar-label"); public ILocator ManaBarLabels => Page.Locator(".mana-bar-label");
public ILocator ManaCosts => Page.Locator(".mana-cost"); public ILocator ManaCosts => Page.Locator(".mana-cost");
public async Task<int> GetManaBarCountAsync() => await ManaBars.CountAsync(); public async Task<int> GetManaBarCountAsync()
} {
return await ManaBars.CountAsync();
}
}
+15 -6
View File
@@ -4,14 +4,23 @@ namespace Tests.PageObjects;
public class DecksPage : BasePage public class DecksPage : BasePage
{ {
public DecksPage(IPage page) : base(page) { } public DecksPage(IPage page) : base(page)
{
}
public async Task GotoAsync() => await NavigateToAsync("/decks"); public async Task GotoAsync()
{
await NavigateToAsync("/decks");
}
public ILocator GetDeckCards()
{
return Page.Locator(".deck-card");
}
public ILocator GetDeckCards() => Page.Locator(".deck-card");
public async Task ClickDeckByNameAsync(string name) public async Task ClickDeckByNameAsync(string name)
{ {
await Page.Locator(".deck-card-title").GetByText(name, new() { Exact = true }).ClickAsync(); await Page.Locator(".deck-card-title").GetByText(name, new LocatorGetByTextOptions { Exact = true })
.ClickAsync();
} }
} }
+3 -2
View File
@@ -1,3 +1,4 @@
using System.Text.RegularExpressions;
using Microsoft.Playwright; using Microsoft.Playwright;
using Microsoft.Playwright.NUnit; using Microsoft.Playwright.NUnit;
@@ -13,8 +14,8 @@ public class PlaywrightTests : PageTest
public async Task HomePage_ShouldLoad() public async Task HomePage_ShouldLoad()
{ {
await Page.GotoAsync(BaseUrl); await Page.GotoAsync(BaseUrl);
await Expect(Page).ToHaveTitleAsync(new System.Text.RegularExpressions.Regex("Chrono CCG")); await Expect(Page).ToHaveTitleAsync(new Regex("Chrono CCG"));
// Check for some content that should be on the home page // Check for some content that should be on the home page
var heading = Page.Locator("h1"); var heading = Page.Locator("h1");
await Expect(heading).ToBeVisibleAsync(); await Expect(heading).ToBeVisibleAsync();
+1 -2
View File
@@ -7,14 +7,13 @@ namespace Tests;
[TestFixture] [TestFixture]
public class TelerikLicenseTests : PageTest public class TelerikLicenseTests : PageTest
{ {
private const string BaseUrl = "http://localhost:5256"; private const string BaseUrl = "http://localhost:5256";
[Test] [Test]
public async Task TelerikLicenseBannerIsNotVisible() public async Task TelerikLicenseBannerIsNotVisible()
{ {
await Page.GotoAsync(BaseUrl + "/agents"); await Page.GotoAsync(BaseUrl + "/agents");
await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); await Page.WaitForLoadStateAsync(LoadState.NetworkIdle);
var grid = Page.Locator(".agents-grid"); var grid = Page.Locator(".agents-grid");
await Expect(grid).ToBeVisibleAsync(); await Expect(grid).ToBeVisibleAsync();
+16 -21
View File
@@ -1,30 +1,24 @@
using System.Diagnostics; using System.Diagnostics;
using System.Net.Http;
namespace Tests; namespace Tests;
[SetUpFixture] [SetUpFixture]
public class TestSetup public class TestSetup
{ {
private Process? _serverProcess;
private const string ServerUrl = "http://localhost:5256"; private const string ServerUrl = "http://localhost:5256";
private Process? _serverProcess;
[OneTimeSetUp] [OneTimeSetUp]
public async Task BeforeAllTests() public async Task BeforeAllTests()
{ {
Console.WriteLine("[DEBUG_LOG] Starting Server..."); Console.WriteLine("[DEBUG_LOG] Starting Server...");
// Find solution root // Find solution root
var currentDir = AppContext.BaseDirectory; var currentDir = AppContext.BaseDirectory;
while (currentDir != null && !File.Exists(Path.Combine(currentDir, "Chrono.sln"))) while (currentDir != null && !File.Exists(Path.Combine(currentDir, "Chrono.sln")))
{
currentDir = Path.GetDirectoryName(currentDir); currentDir = Path.GetDirectoryName(currentDir);
}
if (currentDir == null) if (currentDir == null) throw new Exception("Could not find Chrono.sln to determine project path.");
{
throw new Exception("Could not find Chrono.sln to determine project path.");
}
var projectPath = Path.Combine(currentDir, "Server", "Server.csproj"); var projectPath = Path.Combine(currentDir, "Server", "Server.csproj");
@@ -40,25 +34,27 @@ public class TestSetup
}; };
_serverProcess = Process.Start(startInfo); _serverProcess = Process.Start(startInfo);
_serverProcess.OutputDataReceived += (s, e) => { if (e.Data != null) Console.WriteLine($"[SERVER OUT] {e.Data}"); }; _serverProcess.OutputDataReceived += (s, e) =>
_serverProcess.ErrorDataReceived += (s, e) => { if (e.Data != null) Console.WriteLine($"[SERVER ERR] {e.Data}"); }; {
if (e.Data != null) Console.WriteLine($"[SERVER OUT] {e.Data}");
};
_serverProcess.ErrorDataReceived += (s, e) =>
{
if (e.Data != null) Console.WriteLine($"[SERVER ERR] {e.Data}");
};
_serverProcess.BeginOutputReadLine(); _serverProcess.BeginOutputReadLine();
_serverProcess.BeginErrorReadLine(); _serverProcess.BeginErrorReadLine();
if (_serverProcess == null) if (_serverProcess == null) throw new Exception("Failed to start server process.");
{
throw new Exception("Failed to start server process.");
}
// Wait for the server to be ready // Wait for the server to be ready
using var client = new HttpClient(); using var client = new HttpClient();
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
var timeout = TimeSpan.FromSeconds(30); var timeout = TimeSpan.FromSeconds(30);
bool isReady = false; var isReady = false;
while (sw.Elapsed < timeout) while (sw.Elapsed < timeout)
{
try try
{ {
var response = await client.GetAsync(ServerUrl); var response = await client.GetAsync(ServerUrl);
@@ -73,14 +69,13 @@ public class TestSetup
// Wait and retry // Wait and retry
await Task.Delay(1000); await Task.Delay(1000);
} }
}
if (!isReady) if (!isReady)
{ {
_serverProcess.Kill(true); _serverProcess.Kill(true);
throw new Exception($"Server did not start within {timeout.TotalSeconds} seconds."); throw new Exception($"Server did not start within {timeout.TotalSeconds} seconds.");
} }
Console.WriteLine("[DEBUG_LOG] Server is ready."); Console.WriteLine("[DEBUG_LOG] Server is ready.");
} }
@@ -94,4 +89,4 @@ public class TestSetup
_serverProcess.Dispose(); _serverProcess.Dispose();
} }
} }
} }
+2 -1
View File
@@ -1,4 +1,5 @@
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(Home).Assembly }" NotFoundPage="typeof(NotFound)"> <Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="new[] { typeof(Home).Assembly }"
NotFoundPage="typeof(NotFound)">
<Found Context="routeData"> <Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/> <FocusOnNavigate RouteData="@routeData" Selector="h1"/>
+57 -48
View File
@@ -1,3 +1,4 @@
@using System.Text.RegularExpressions
@namespace Chrono.Components @namespace Chrono.Components
@if (Card != null) @if (Card != null)
@@ -10,7 +11,7 @@
{ {
<div class="detail-image"> <div class="detail-image">
<img src="@Card.ImagePath" alt="@Card.Name" <img src="@Card.ImagePath" alt="@Card.Name"
onerror="this.style.display='none';this.parentElement.style.display='none'" /> onerror="this.style.display='none';this.parentElement.style.display='none'"/>
</div> </div>
} }
<div class="detail-info"> <div class="detail-info">
@@ -88,7 +89,8 @@
var target = LookupCard(targetName); var target = LookupCard(targetName);
if (target != null) if (target != null)
{ {
<button class="inline-card-btn" @onclick="() => Navigate(target)">@targetName</button> <button class="inline-card-btn"
@onclick="() => Navigate(target)">@targetName</button>
} }
else else
{ {
@@ -108,7 +110,8 @@
var fromCard = LookupCard(Card.ImmortalizeFrom); var fromCard = LookupCard(Card.ImmortalizeFrom);
if (fromCard != null) if (fromCard != null)
{ {
<button class="inline-card-btn" @onclick="() => Navigate(fromCard)">@Card.ImmortalizeFrom</button> <button class="inline-card-btn"
@onclick="() => Navigate(fromCard)">@Card.ImmortalizeFrom</button>
} }
else else
{ {
@@ -181,7 +184,9 @@
{ {
await Http.PostAsJsonAsync("api/notes", new CardNote { CardName = Card.Name, Note = currentNote }); await Http.PostAsJsonAsync("api/notes", new CardNote { CardName = Card.Name, Note = currentNote });
} }
catch { } catch
{
}
finally finally
{ {
isSaving = false; isSaving = false;
@@ -209,64 +214,68 @@
string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase)); string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase));
} }
private RenderFragment RenderDescription(string description) => builder => private RenderFragment RenderDescription(string description)
{ {
var cardNames = CardDatabase.Cards return builder =>
.Select(c => c.Name)
.Where(n => !string.IsNullOrWhiteSpace(n))
.OrderByDescending(n => n.Length)
.ToList();
int seq = 0;
var remaining = description;
while (!string.IsNullOrEmpty(remaining))
{ {
int bestIdx = -1; var cardNames = CardDatabase.Cards
string? bestName = null; .Select(c => c.Name)
.Where(n => !string.IsNullOrWhiteSpace(n))
.OrderByDescending(n => n.Length)
.ToList();
foreach (var name in cardNames) var seq = 0;
var remaining = description;
while (!string.IsNullOrEmpty(remaining))
{ {
var pattern = $@"\b{System.Text.RegularExpressions.Regex.Escape(name)}\b"; var bestIdx = -1;
var match = System.Text.RegularExpressions.Regex.Match(remaining, pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase); string? bestName = null;
if (match.Success) foreach (var name in cardNames)
{ {
if (bestIdx == -1 || match.Index < bestIdx) var pattern = $@"\b{Regex.Escape(name)}\b";
var match = Regex.Match(remaining, pattern, RegexOptions.IgnoreCase);
if (match.Success)
{ {
bestIdx = match.Index; if (bestIdx == -1 || match.Index < bestIdx)
bestName = match.Value; {
bestIdx = match.Index;
bestName = match.Value;
}
} }
} }
}
if (bestIdx != -1 && bestName != null) if (bestIdx != -1 && bestName != null)
{
if (bestIdx > 0)
builder.AddContent(seq++, remaining[..bestIdx]);
var card = LookupCard(bestName);
if (card != null)
{ {
builder.OpenElement(seq++, "button"); if (bestIdx > 0)
builder.AddAttribute(seq++, "class", "inline-card-btn"); builder.AddContent(seq++, remaining[..bestIdx]);
builder.AddAttribute(seq++, "onclick",
EventCallback.Factory.Create(this, () => Navigate(card))); var card = LookupCard(bestName);
builder.AddContent(seq++, bestName); if (card != null)
builder.CloseElement(); {
builder.OpenElement(seq++, "button");
builder.AddAttribute(seq++, "class", "inline-card-btn");
builder.AddAttribute(seq++, "onclick",
EventCallback.Factory.Create(this, () => Navigate(card)));
builder.AddContent(seq++, bestName);
builder.CloseElement();
}
else
{
builder.AddContent(seq++, bestName);
}
remaining = remaining[(bestIdx + bestName.Length)..];
} }
else else
{ {
builder.AddContent(seq++, bestName); builder.AddContent(seq++, remaining);
remaining = "";
} }
}
};
}
remaining = remaining[(bestIdx + bestName.Length)..];
}
else
{
builder.AddContent(seq++, remaining);
remaining = "";
}
}
};
} }
+8 -3
View File
@@ -8,8 +8,12 @@
} }
@keyframes fade-in { @keyframes fade-in {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
.card-detail { .card-detail {
@@ -231,7 +235,8 @@
text-decoration-color: #7b73ff; text-decoration-color: #7b73ff;
} }
@@media (max-width: 768px) { @
@media (max-width: 768px) {
.detail-layout { .detail-layout {
flex-direction: column; flex-direction: column;
padding: 1rem; padding: 1rem;
+20 -14
View File
@@ -1,10 +1,11 @@
@page "/cards" @page "/cards"
@inject HttpClient Http @inject HttpClient Http
@inject AnalyticsService AnalyticsService
<PageTitle>Chrono CCG - Card Gallery</PageTitle> <PageTitle>Chrono CCG - Card Gallery</PageTitle>
<HeadContent> <HeadContent>
<meta name="description" content="Browse and filter all Chrono CCG cards. Explore agents, spells, and more." /> <meta name="description" content="Browse and filter all Chrono CCG cards. Explore agents, spells, and more."/>
</HeadContent> </HeadContent>
<div class="gallery-container"> <div class="gallery-container">
@@ -17,18 +18,19 @@
<button class="tab @(categoryFilter == "" ? "active" : "")" @onclick='@(() => SetCategory(""))'> <button class="tab @(categoryFilter == "" ? "active" : "")" @onclick='@(() => SetCategory(""))'>
<i class="bi bi-grid-3x3-gap-fill"></i> All <i class="bi bi-grid-3x3-gap-fill"></i> All
</button> </button>
<button class="tab agent @(categoryFilter == "Agent" ? "active" : "")" @onclick='@(() => SetCategory("Agent"))'> <button class="tab agent @(categoryFilter == "Agent" ? "active" : "")" @onclick='@(() => SetCategory("Agent"))'>
<i class="bi bi-person-fill"></i> Agents <i class="bi bi-person-fill"></i> Agents
</button> </button>
<button class="tab agent @(categoryFilter == "Immortalized" ? "active" : "")" @onclick='@(() => SetCategory("Immortalized"))'> <button class="tab agent @(categoryFilter == "Immortalized" ? "active" : "")"
@onclick='@(() => SetCategory("Immortalized"))'>
<i class="bi bi-person-fill"></i> Immortalized <i class="bi bi-person-fill"></i> Immortalized
</button> </button>
<!-- //Todo make agent being linked to immortal for side by side sorting <!-- //Todo make agent being linked to immortal for side by side sorting
<button class="tab agent @(agentLink ? "active" : "")" @onclick='@(() => ToggleAgentLink())'> <button class="tab agent @(agentLink ? "active" : "")" @onclick='@(() => ToggleAgentLink())'>
<i class="bi bi-person-fill"></i> Link <i class="bi bi-person-fill"></i> Link
</button> </button>
--> -->
<button class="tab spell @(categoryFilter == "Spell" ? "active" : "")" @onclick='@(() => SetCategory("Spell"))'> <button class="tab spell @(categoryFilter == "Spell" ? "active" : "")" @onclick='@(() => SetCategory("Spell"))'>
<i class="bi bi-wand"></i> Spells <i class="bi bi-wand"></i> Spells
</button> </button>
@@ -118,7 +120,7 @@
{ {
<div class="card-grid @(showDetailedView ? "detailed-view" : "")"> <div class="card-grid @(showDetailedView ? "detailed-view" : "")">
@{ var idx = 0; } @{ var idx = 0; }
@foreach (var card in filteredCards.Where(a=>a.Category is "Agent" or "Spell" or "Immortalized")) @foreach (var card in filteredCards.Where(a => a.Category is "Agent" or "Spell" or "Immortalized"))
{ {
<div class="card-cell @(selectedCard == card ? "selected" : "")" <div class="card-cell @(selectedCard == card ? "selected" : "")"
style="--i: @idx" style="--i: @idx"
@@ -149,7 +151,7 @@
</div> </div>
<div class="card-description-preview">@card.Description</div> <div class="card-description-preview">@card.Description</div>
<div class="card-category-badge @card.Category?.ToLowerInvariant()">@card.Category</div> <div class="card-category-badge @card.Category?.ToLowerInvariant()">@card.Category</div>
</div> </div>
} }
</div> </div>
@@ -171,7 +173,7 @@
@if (selectedCard != null) @if (selectedCard != null)
{ {
<CardDialog Card="selectedCard" OnClose="CloseDetail" OnNavigate="SelectCard" /> <CardDialog Card="selectedCard" OnClose="CloseDetail" OnNavigate="SelectCard"/>
} }
@code { @code {
@@ -220,12 +222,12 @@
_ => sortDescending ? filtered.OrderByDescending(c => c.Name) : filtered.OrderBy(c => c.Name) _ => sortDescending ? filtered.OrderByDescending(c => c.Name) : filtered.OrderBy(c => c.Name)
}; };
} }
private void SetImmortal(string imm) private void SetImmortal(string imm)
{ {
immortalFilter = immortalFilter == imm ? "" : imm; immortalFilter = immortalFilter == imm ? "" : imm;
} }
private void ClearImmortalFilter() private void ClearImmortalFilter()
{ {
immortalFilter = ""; immortalFilter = "";
@@ -234,11 +236,13 @@
private void SetCategory(string cat) private void SetCategory(string cat)
{ {
categoryFilter = categoryFilter == cat ? "" : cat; categoryFilter = categoryFilter == cat ? "" : cat;
_ = AnalyticsService.TrackEvent("filter_category", new { category = categoryFilter });
} }
private void ClearCategoryFilter() private void ClearCategoryFilter()
{ {
categoryFilter = ""; categoryFilter = "";
_ = AnalyticsService.TrackEvent("filter_category", new { category = "All" });
} }
private void ClearFactionFilter() private void ClearFactionFilter()
@@ -250,7 +254,7 @@
{ {
agentLink = !agentLink; agentLink = !agentLink;
} }
private void ClearCostFilter() private void ClearCostFilter()
{ {
costFilter = ""; costFilter = "";
@@ -264,6 +268,7 @@
private void SelectCard(CardData card) private void SelectCard(CardData card)
{ {
selectedCard = card; selectedCard = card;
_ = AnalyticsService.TrackEvent("select_item", new { item_name = card.Name, item_category = card.Category });
} }
private void CloseDetail() private void CloseDetail()
@@ -278,4 +283,5 @@
factionFilter = ""; factionFilter = "";
costFilter = ""; costFilter = "";
} }
} }
+3 -1
View File
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Shared.Services;
using Web; using Web;
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
@@ -8,6 +9,7 @@ builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddTelerikBlazor(); builder.Services.AddTelerikBlazor();
builder.Services.AddSingleton<Shared.Services.CardRenderingService>(); builder.Services.AddSingleton<CardRenderingService>();
builder.Services.AddScoped<AnalyticsService>();
await builder.Build().RunAsync(); await builder.Build().RunAsync();
+2 -2
View File
@@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" /> <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -21,6 +21,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Model\Model.csproj"/> <ProjectReference Include="..\Model\Model.csproj"/>
<ProjectReference Include="..\Shared\Shared.csproj" /> <ProjectReference Include="..\Shared\Shared.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>
+2
View File
@@ -9,6 +9,8 @@
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Shared.Layout @using Shared.Layout
@using Shared.Pages @using Shared.Pages
@using Shared.Services
@using Shared.Components
@using Chrono.Components @using Chrono.Components
@using Telerik.Blazor @using Telerik.Blazor
@using Telerik.Blazor.Components @using Telerik.Blazor.Components
-3
View File
@@ -257,9 +257,6 @@ a, .btn-link {
} }
.inline-card-btn { .inline-card-btn {
background: none; background: none;
border: none; border: none;
+39 -16
View File
@@ -6,26 +6,29 @@
<meta content="width=device-width, initial-scale=1.0" name="viewport"/> <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Chrono CCG - Card Gallery</title> <title>Chrono CCG - Card Gallery</title>
<base href="/"/> <base href="/"/>
<meta name="description" content="Explore the full collection of Chrono CCG cards. Filter by cost, faction, and type."/> <meta content="Explore the full collection of Chrono CCG cards. Filter by cost, faction, and type."
<meta name="keywords" content="Chrono CCG, Reference, Card Gallery, DB"/> name="description"/>
<meta content="Chrono CCG, Reference, Card Gallery, DB" name="keywords"/>
<!-- Open Graph / Facebook --> <!-- Open Graph / Facebook -->
<meta property="og:type" content="website"/> <meta content="website" property="og:type"/>
<meta property="og:url" content="https://chronoccg.com/"/> <meta content="https://chronoccg.com/" property="og:url"/>
<meta property="og:title" content="Chrono CCG - Card Gallery"/> <meta content="Chrono CCG - Card Gallery" property="og:title"/>
<meta property="og:description" content="Explore the full collection of Chrono CCG cards. Filter by cost, faction, and type."/> <meta content="Explore the full collection of Chrono CCG cards. Filter by cost, faction, and type."
<meta property="og:image" content="https://chronoccg.com/favicon.png"/> property="og:description"/>
<meta content="https://chronoccg.com/favicon.png" property="og:image"/>
<!-- Twitter --> <!-- Twitter -->
<meta property="twitter:card" content="summary_large_image"/> <meta content="summary_large_image" property="twitter:card"/>
<meta property="twitter:url" content="https://chronoccg.com/"/> <meta content="https://chronoccg.com/" property="twitter:url"/>
<meta property="twitter:title" content="Chrono CCG - Card Gallery"/> <meta content="Chrono CCG - Card Gallery" property="twitter:title"/>
<meta property="twitter:description" content="Explore the full collection of Chrono CCG cards. Filter by cost, faction, and type."/> <meta content="Explore the full collection of Chrono CCG cards. Filter by cost, faction, and type."
<meta property="twitter:image" content="https://chronoccg.com/favicon.png"/> property="twitter:description"/>
<meta content="https://chronoccg.com/favicon.png" property="twitter:image"/>
<link href="manifest.json" rel="manifest" /> <link href="manifest.json" rel="manifest"/>
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" /> <link href="icon-512.png" rel="apple-touch-icon" sizes="512x512"/>
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" /> <link href="icon-192.png" rel="apple-touch-icon" sizes="192x192"/>
<link href="https://fonts.googleapis.com" rel="preconnect"/> <link href="https://fonts.googleapis.com" rel="preconnect"/>
<link crossorigin href="https://fonts.gstatic.com" rel="preconnect"/> <link crossorigin href="https://fonts.gstatic.com" rel="preconnect"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"/> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
@@ -37,6 +40,26 @@
<script defer src="/_content/Telerik.UI.for.Blazor/js/telerik-blazor.js"></script> <script defer src="/_content/Telerik.UI.for.Blazor/js/telerik-blazor.js"></script>
<link href="/favicon.png" rel="icon" type="image/png"/> <link href="/favicon.png" rel="icon" type="image/png"/>
<script type="importmap"></script> <script type="importmap"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-HVMP2QEJLR"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-HVMP2QEJLR');
window.trackPageView = (path) => {
gtag('event', 'page_view', {
page_path: path,
page_location: window.location.origin + path
});
};
</script>
</head> </head>
<body> <body>
+2 -1
View File
@@ -1,3 +1,4 @@
// In development, always fetch from the network and do not cache. // In development, always fetch from the network and do not cache.
// This is appropriate during development as the service worker will wake up and fetch resources from the network. // This is appropriate during development as the service worker will wake up and fetch resources from the network.
self.addEventListener('fetch', () => { }); self.addEventListener('fetch', () => {
});
@@ -8,8 +8,8 @@ self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-'; const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; const offlineAssetsInclude = [/\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/];
const offlineAssetsExclude = [ /^service-worker\.js$/ ]; const offlineAssetsExclude = [/^service-worker\.js$/];
async function onInstall(event) { async function onInstall(event) {
console.info('Service worker installing'); console.info('Service worker installing');
@@ -18,7 +18,7 @@ async function onInstall(event) {
const assetsRequests = self.assetsManifest.assets const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); .map(asset => new Request(asset.url, {integrity: asset.hash, cache: 'no-cache'}));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
} }
+7 -1
View File
@@ -1,6 +1,12 @@
{ {
"navigationFallback": { "navigationFallback": {
"rewrite": "/index.html", "rewrite": "/index.html",
"exclude": ["/css/*", "/lib/*", "/_framework/*", "/_content/*", "/sample-data/*"] "exclude": [
"/css/*",
"/lib/*",
"/_framework/*",
"/_content/*",
"/sample-data/*"
]
} }
} }
@@ -1,10 +1,10 @@
{ {
"id": "frontmatter-folder-organizer", "id": "frontmatter-folder-organizer",
"name": "Frontmatter Folder Organizer", "name": "Frontmatter Folder Organizer",
"version": "1.0.0", "version": "1.0.0",
"minAppVersion": "1.0.0", "minAppVersion": "1.0.0",
"description": "Organize markdown files into folders based on frontmatter hierarchy.", "description": "Organize markdown files into folders based on frontmatter hierarchy.",
"author": "Obsidian", "author": "Obsidian",
"authorUrl": "https://obsidian.md", "authorUrl": "https://obsidian.md",
"isDesktopOnly": false "isDesktopOnly": false
} }
@@ -19,6 +19,8 @@ archetypes:
imageLink: "[[Aardvark Precinct Captain.png]]" imageLink: "[[Aardvark Precinct Captain.png]]"
ranking: A ranking: A
--- ---
I feel like it easy to always make this card sprout 5, so +5/+5 worth of stats for two man. Plus at that point, it will Immortalize as a +3/+3 and activate [[Deadly Fauna]].
I feel like it easy to always make this card sprout 5, so +5/+5 worth of stats for two man. Plus at that point, it will
Immortalize as a +3/+3 and activate [[Deadly Fauna]].
![[Aardvark Precinct Captain.png]] ![[Aardvark Precinct Captain.png]]
@@ -17,6 +17,7 @@ archetypes:
- "[[Rewound]]" - "[[Rewound]]"
imageLink: "[[Divergence Assassin.png]]" imageLink: "[[Divergence Assassin.png]]"
--- ---
![[Divergence Assassin.png]] ![[Divergence Assassin.png]]
@@ -18,6 +18,7 @@ archetypes:
imageLink: "[[Scuttling Spares.png]]" imageLink: "[[Scuttling Spares.png]]"
rank: S rank: S
--- ---
I haven't played this card, but it sounds like it obviously enables discard. I haven't played this card, but it sounds like it obviously enables discard.
![[Scuttling Spares.png]] ![[Scuttling Spares.png]]
+1
View File
@@ -2,4 +2,5 @@
category: Debuff category: Debuff
description: Set this Agent's strength to 0 until Round End. description: Set this Agent's strength to 0 until Round End.
--- ---
Reduce attack to zero. Reduce attack to zero.
+18 -8
View File
@@ -74,20 +74,30 @@ factions:
deckCode: AUHUE2LHEBCW4ZLSM54SAQ3POB4QWY3PNZZXI4TVMN2GKZBTAADACASPCICESCQCAUFDCAYGAMCAGAQDDYBQCAYBAOTQCAYQAMDQG deckCode: AUHUE2LHEBCW4ZLSM54SAQ3POB4QWY3PNZZXI4TVMN2GKZBTAADACASPCICESCQCAUFDCAYGAMCAGAQDDYBQCAYBAOTQCAYQAMDQG
deckType: custom deckType: custom
--- ---
The idea of this deck is to go heavy on stat-efficient cards. Like 2 for a 2/3 [[Kinetic Absorber]], 5/6 [[Lumbering Starseeker]], and 8 for a 9/14 [[Lightsteel Colossus]].
You got [[Devourer Spawn]] to help push for lethal. It's going to have a bunch of keywords on it, given how popular timelines are in the meta. The idea of this deck is to go heavy on stat-efficient cards. Like 2 for a 2/3 [[Kinetic Absorber]],
5/6 [[Lumbering Starseeker]], and 8 for a 9/14 [[Lightsteel Colossus]].
You need to Overflow your mana once in a while, or some of your [[Supernova]] removal will be useless. Be pass heavy. And if you do end up overflowing many times [[Supernova]] can refill for mana bar, and it's worth adding some [[Snap Back]]s to the deck to use that spell multiple times. You got [[Devourer Spawn]] to help push for lethal. It's going to have a bunch of keywords on it, given how popular
timelines are in the meta.
You need to Overflow your mana once in a while, or some of your [[Supernova]] removal will be useless. Be pass heavy.
And if you do end up overflowing many times [[Supernova]] can refill for mana bar, and it's worth adding
some [[Snap Back]]s to the deck to use that spell multiple times.
Attack aggressively with [[Brilliant Martyr]]. You really want [[Star Siphon]] to ramp. Attack aggressively with [[Brilliant Martyr]]. You really want [[Star Siphon]] to ramp.
As divers, [[Peaceful Synthesizer]] is a 1 drop that will quickly turn into a 3/3. Can't get more stat efficient than that. [[Nonlethal Special Forces]] is to enable [[Phasetide]] for AOE [[Mute]] cards given we don't care about stat buffs. Also as it stands, it always seems to punch a above it's weight on the board given the [[Disarm]] effect. As divers, [[Peaceful Synthesizer]] is a 1 drop that will quickly turn into a 3/3. Can't get more stat efficient than
that. [[Nonlethal Special Forces]] is to enable [[Phasetide]] for AOE [[Mute]] cards given we don't care about stat
buffs. Also as it stands, it always seems to punch a above it's weight on the board given the [[Disarm]] effect.
So ya, [[Curb the Anomalies]] can weaken the enemy board, by shutting off all there better stats, activates, and synergies. Hopefully destroy the entire enemy board in combat. So ya, [[Curb the Anomalies]] can weaken the enemy board, by shutting off all there better stats, activates, and
synergies. Hopefully destroy the entire enemy board in combat.
And I suppose [[Bearer of the Broth]]. Since the deck has so much raw stats, you will probably find somewhere to heal. Like healing [[Kinetic Absorber]] so it can keep killing after it Immortalizes. And I suppose [[Bearer of the Broth]]. Since the deck has so much raw stats, you will probably find somewhere to heal.
Like healing [[Kinetic Absorber]] so it can keep killing after it Immortalizes.
Also in practice [[Bearer of the Broth]] has proven to be an insane and reliable draw engine for this deck. Also in practice [[Bearer of the Broth]] has proven to be an insane and reliable draw engine for this deck.
[[Gunnery Captain]] helps clear up the board. Tokens are just 1 to 2 cost units that grew to absurd sizes. And [[Army of the Sun]] helps in the mirror match, given how common this deck is due to the precon. [[Gunnery Captain]] helps clear up the board. Tokens are just 1 to 2 cost units that grew to absurd sizes.
And [[Army of the Sun]] helps in the mirror match, given how common this deck is due to the precon.
+3 -1
View File
@@ -62,11 +62,13 @@ factions:
deckCode: AUMU65TFOJTGY33XNFXGOICTMVRXK4TJOR4SAQ3POB4QWY3PNZZXI4TVMN2GKZBRACQAAANZAEBZEAI6AQFTSAYCAMOQGBADAEBQCAZ2AMCAGGQDAQBQCAY deckCode: AUMU65TFOJTGY33XNFXGOICTMVRXK4TJOR4SAQ3POB4QWY3PNZZXI4TVMN2GKZBRACQAAANZAEBZEAI6AQFTSAYCAMOQGBADAEBQCAZ2AMCAGGQDAQBQCAY
deckType: custom deckType: custom
--- ---
Built deck as a joke, but it kind of feels nuts in limited testing. Built deck as a joke, but it kind of feels nuts in limited testing.
Mulligan everything for [[Radiant Channeling]] and [[Supernova]]. You are hoping that limited removal keeps you alive. Mulligan everything for [[Radiant Channeling]] and [[Supernova]]. You are hoping that limited removal keeps you alive.
If they heavy buff something out from your removal range, you might still have enough reserve to [[Throw into the Sun]] that big target. If they heavy buff something out from your removal range, you might still have enough reserve to [[Throw into the Sun]]
that big target.
If you survive, start scaling your board with [[Awakened Security System]] or [[Limit Breaker]]. If you survive, start scaling your board with [[Awakened Security System]] or [[Limit Breaker]].
+2
View File
@@ -1,6 +1,8 @@
--- ---
category: Faction category: Faction
--- ---
Concepts: Concepts:
- Wide Boards - Wide Boards
- Overpower - Overpower
+2
View File
@@ -1,6 +1,8 @@
--- ---
category: Faction category: Faction
--- ---
Concepts: Concepts:
- Healing - Healing
- -
+2
View File
@@ -1,6 +1,8 @@
--- ---
category: Faction category: Faction
--- ---
Concepts: Concepts:
- Activatable Agents - Activatable Agents
- -
+2
View File
@@ -1,5 +1,7 @@
--- ---
category: Faction category: Faction
--- ---
Concepts: Concepts:
- Discard - Discard
+1 -1
View File
@@ -6,6 +6,6 @@ category: Faction
Can hurt it's own units for power. Has [[Bleed]] to make reoccurring damage on units very easy. Can hurt it's own units for power. Has [[Bleed]] to make reoccurring damage on units very easy.
# Core Damage # Core Damage
Can hurt both cores easily. Has [[Breakdown]] to take advantage of having an injured core for more damage. Can hurt both cores easily. Has [[Breakdown]] to take advantage of having an injured core for more damage.
+4 -1
View File
@@ -1,11 +1,14 @@
--- ---
category: Faction category: Faction
--- ---
# Energy Reserves # Energy Reserves
Care about always having banked energy. Spells can refund themselves when using [[Energy Reserves]], or get stronger is you [[Overflow]] your energy reserve. Care about always having banked energy. Spells can refund themselves when using [[Energy Reserves]], or get stronger is
you [[Overflow]] your energy reserve.
Concepts: Concepts:
- Ramp - Ramp
- Using and Replenishing Reserved Energy - Using and Replenishing Reserved Energy
- -
@@ -17,4 +17,5 @@ archetypes:
- "[[Keyword/Sprout]]" - "[[Keyword/Sprout]]"
imageLink: "[[Horticulturist Oswald.png]]" imageLink: "[[Horticulturist Oswald.png]]"
--- ---
![[Horticulturist Oswald.png]] ![[Horticulturist Oswald.png]]
@@ -17,4 +17,5 @@ archetypes:
- "[[Rewound]]" - "[[Rewound]]"
imageLink: "[[Violet Inquisitioner]]" imageLink: "[[Violet Inquisitioner]]"
--- ---
![[Violent Inquisitioner.png]] ![[Violent Inquisitioner.png]]
@@ -17,4 +17,5 @@ archetypes:
- N/A - N/A
imageLink: "[[Scoot Sparkles.png]]" imageLink: "[[Scoot Sparkles.png]]"
--- ---
![[Scoot Sparkles.png]] ![[Scoot Sparkles.png]]
@@ -17,4 +17,5 @@ archetypes:
- N/A - N/A
imageLink: "[[The Ripper.png]]" imageLink: "[[The Ripper.png]]"
--- ---
![[The Ripper.png]] ![[The Ripper.png]]
+2 -2
View File
@@ -5,7 +5,7 @@ category: Pass
--- ---
| Tier | Reward | | Tier | Reward |
| ---- | -------------------------------- | |------|----------------------------------|
| 1 | Sprouts Record | | 1 | Sprouts Record |
| 2 | 120 Core Charges | | 2 | 120 Core Charges |
| 3 | Burn Precon | | 3 | Burn Precon |
@@ -18,5 +18,5 @@ category: Pass
| 10 | 240 Core Charges, 1 Draft Ticket | | 10 | 240 Core Charges, 1 Draft Ticket |
| Core Charges | Flux | | Core Charges | Flux |
| ------------ | ---- | |--------------|------|
| 660 | 75 | | 660 | 75 |
+7 -6
View File
@@ -1,11 +1,12 @@
--- ---
category: Quest category: Quest
--- ---
1: 30cc 30xp
2: 20cc 25xp 1: 30cc 30xp
3: 20cc 20xp 2: 20cc 25xp
4: 10cc 20xp 3: 20cc 20xp
5: 10cc 15xp 4: 10cc 20xp
6-12 are all simply 5xp. 5: 10cc 15xp
6-12 are all simply 5xp.
So the daily total win reward for 12 wins is 90cc/145xp. So the daily total win reward for 12 wins is 90cc/145xp.