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
+1 -1
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);
@@ -31,9 +31,8 @@
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;
} }
+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();
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -224,6 +224,7 @@ h6, h5, h4, h3, h2, h1 {
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;
@@ -233,6 +234,7 @@ h1 {
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;
@@ -242,6 +244,7 @@ h2 {
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;
@@ -251,6 +254,7 @@ h3 {
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;
@@ -351,6 +355,7 @@ 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);
} }
@@ -375,6 +380,7 @@ pre {
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;
@@ -386,6 +392,7 @@ code {
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;
} }
@@ -397,6 +404,7 @@ kbd {
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;
@@ -474,6 +482,7 @@ select {
select { select {
word-wrap: normal; word-wrap: normal;
} }
select:disabled { select:disabled {
opacity: 1; opacity: 1;
} }
@@ -488,6 +497,7 @@ button,
[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),
@@ -519,11 +529,13 @@ legend {
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;
} }
@@ -224,6 +224,7 @@ h6, h5, h4, h3, h2, h1 {
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;
@@ -233,6 +234,7 @@ h1 {
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;
@@ -242,6 +244,7 @@ h2 {
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;
@@ -251,6 +254,7 @@ h3 {
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;
@@ -351,6 +355,7 @@ 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);
} }
@@ -375,6 +380,7 @@ pre {
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;
@@ -386,6 +392,7 @@ code {
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;
} }
@@ -397,6 +404,7 @@ kbd {
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;
@@ -474,6 +482,7 @@ select {
select { select {
word-wrap: normal; word-wrap: normal;
} }
select:disabled { select:disabled {
opacity: 1; opacity: 1;
} }
@@ -488,6 +497,7 @@ button,
[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),
@@ -519,11 +529,13 @@ legend {
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;
} }
@@ -553,6 +565,7 @@ legend + * {
[type="number"] { [type="number"] {
direction: ltr; direction: ltr;
} }
::-webkit-search-decoration { ::-webkit-search-decoration {
-webkit-appearance: none; -webkit-appearance: none;
} }
@@ -591,4 +604,5 @@ progress {
[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
+1 -1
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; } = [];
} }
+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"/>
+7 -7
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;
+13 -5
View File
@@ -1,5 +1,4 @@
@namespace Shared.Components @namespace Shared.Components
@using Chrono.Model
@if (Card != null) @if (Card != null)
{ {
@@ -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;
}
}
+6 -6
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" },
@@ -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" },
+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">
+6 -3
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">
@@ -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();
} }
} }
+7 -5
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>
@@ -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()
{ {
@@ -83,8 +85,8 @@
.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();
} }
+10 -4
View File
@@ -17,13 +17,15 @@
</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,7 +136,9 @@
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)
{
return key switch
{ {
"category" => "Category", "category" => "Category",
"description" => "Description", "description" => "Description",
@@ -148,6 +152,8 @@
"faction" => "Faction", "faction" => "Faction",
"exp" => "EXP Reward", "exp" => "EXP Reward",
"see" => "See Also", "see" => "See Also",
_ => string.Concat(key.Select((c, i) => i > 0 && char.IsUpper(c) ? " " + c : c.ToString())), _ => 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);
}
}
+8 -12
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();
} }
+5 -6
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,6 +15,10 @@ 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()
{ {
+8 -3
View File
@@ -4,12 +4,17 @@ 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);
+5 -2
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");
}
} }
+7 -2
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();
}
} }
+13 -4
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() => Page.Locator(".deck-card"); public ILocator GetDeckCards()
{
return 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();
} }
} }
+2 -1
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,7 +14,7 @@ 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");
-1
View File
@@ -7,7 +7,6 @@ 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]
+12 -17
View File
@@ -1,13 +1,12 @@
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()
@@ -17,14 +16,9 @@ public class TestSetup
// 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");
@@ -41,24 +35,26 @@ 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,7 +69,6 @@ public class TestSetup
// Wait and retry // Wait and retry
await Task.Delay(1000); await Task.Delay(1000);
} }
}
if (!isReady) if (!isReady)
{ {
+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"/>
+17 -8
View File
@@ -1,3 +1,4 @@
@using System.Text.RegularExpressions
@namespace Chrono.Components @namespace Chrono.Components
@if (Card != null) @if (Card != null)
@@ -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,7 +214,9 @@
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)
{
return builder =>
{ {
var cardNames = CardDatabase.Cards var cardNames = CardDatabase.Cards
.Select(c => c.Name) .Select(c => c.Name)
@@ -217,18 +224,18 @@
.OrderByDescending(n => n.Length) .OrderByDescending(n => n.Length)
.ToList(); .ToList();
int seq = 0; var seq = 0;
var remaining = description; var remaining = description;
while (!string.IsNullOrEmpty(remaining)) while (!string.IsNullOrEmpty(remaining))
{ {
int bestIdx = -1; var bestIdx = -1;
string? bestName = null; string? bestName = null;
foreach (var name in cardNames) foreach (var name in cardNames)
{ {
var pattern = $@"\b{System.Text.RegularExpressions.Regex.Escape(name)}\b"; var pattern = $@"\b{Regex.Escape(name)}\b";
var match = System.Text.RegularExpressions.Regex.Match(remaining, pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase); var match = Regex.Match(remaining, pattern, RegexOptions.IgnoreCase);
if (match.Success) if (match.Success)
{ {
@@ -270,3 +277,5 @@
} }
}; };
} }
}
+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;
+7 -1
View File
@@ -1,5 +1,6 @@
@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>
@@ -21,7 +22,8 @@
<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
@@ -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()
@@ -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
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;
+37 -14
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', () => {
});
+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/*"
]
} }
} }
@@ -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.
+17 -7
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
+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 |
+1
View File
@@ -1,6 +1,7 @@
--- ---
category: Quest category: Quest
--- ---
1: 30cc 30xp 1: 30cc 30xp
2: 20cc 25xp 2: 20cc 25xp
3: 20cc 20xp 3: 20cc 20xp