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
+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">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
+57 -48
View File
@@ -1,3 +1,4 @@
@using System.Text.RegularExpressions
@namespace Chrono.Components
@if (Card != null)
@@ -10,7 +11,7 @@
{
<div class="detail-image">
<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 class="detail-info">
@@ -88,7 +89,8 @@
var target = LookupCard(targetName);
if (target != null)
{
<button class="inline-card-btn" @onclick="() => Navigate(target)">@targetName</button>
<button class="inline-card-btn"
@onclick="() => Navigate(target)">@targetName</button>
}
else
{
@@ -108,7 +110,8 @@
var fromCard = LookupCard(Card.ImmortalizeFrom);
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
{
@@ -181,7 +184,9 @@
{
await Http.PostAsJsonAsync("api/notes", new CardNote { CardName = Card.Name, Note = currentNote });
}
catch { }
catch
{
}
finally
{
isSaving = false;
@@ -209,64 +214,68 @@
string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase));
}
private RenderFragment RenderDescription(string description) => builder =>
private RenderFragment RenderDescription(string description)
{
var cardNames = CardDatabase.Cards
.Select(c => c.Name)
.Where(n => !string.IsNullOrWhiteSpace(n))
.OrderByDescending(n => n.Length)
.ToList();
int seq = 0;
var remaining = description;
while (!string.IsNullOrEmpty(remaining))
return builder =>
{
int bestIdx = -1;
string? bestName = null;
var cardNames = CardDatabase.Cards
.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 match = System.Text.RegularExpressions.Regex.Match(remaining, pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
var bestIdx = -1;
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;
bestName = match.Value;
if (bestIdx == -1 || match.Index < bestIdx)
{
bestIdx = match.Index;
bestName = match.Value;
}
}
}
}
if (bestIdx != -1 && bestName != null)
{
if (bestIdx > 0)
builder.AddContent(seq++, remaining[..bestIdx]);
var card = LookupCard(bestName);
if (card != null)
if (bestIdx != -1 && bestName != null)
{
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();
if (bestIdx > 0)
builder.AddContent(seq++, remaining[..bestIdx]);
var card = LookupCard(bestName);
if (card != null)
{
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
{
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 {
from { opacity: 0; }
to { opacity: 1; }
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.card-detail {
@@ -231,7 +235,8 @@
text-decoration-color: #7b73ff;
}
@@media (max-width: 768px) {
@
@media (max-width: 768px) {
.detail-layout {
flex-direction: column;
padding: 1rem;
+20 -14
View File
@@ -1,10 +1,11 @@
@page "/cards"
@inject HttpClient Http
@inject AnalyticsService AnalyticsService
<PageTitle>Chrono CCG - Card Gallery</PageTitle>
<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>
<div class="gallery-container">
@@ -17,18 +18,19 @@
<button class="tab @(categoryFilter == "" ? "active" : "")" @onclick='@(() => SetCategory(""))'>
<i class="bi bi-grid-3x3-gap-fill"></i> All
</button>
<button class="tab agent @(categoryFilter == "Agent" ? "active" : "")" @onclick='@(() => SetCategory("Agent"))'>
<i class="bi bi-person-fill"></i> Agents
</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
</button>
<!-- //Todo make agent being linked to immortal for side by side sorting
<button class="tab agent @(agentLink ? "active" : "")" @onclick='@(() => ToggleAgentLink())'>
<i class="bi bi-person-fill"></i> Link
</button>
-->
<!-- //Todo make agent being linked to immortal for side by side sorting
<button class="tab agent @(agentLink ? "active" : "")" @onclick='@(() => ToggleAgentLink())'>
<i class="bi bi-person-fill"></i> Link
</button>
-->
<button class="tab spell @(categoryFilter == "Spell" ? "active" : "")" @onclick='@(() => SetCategory("Spell"))'>
<i class="bi bi-wand"></i> Spells
</button>
@@ -118,7 +120,7 @@
{
<div class="card-grid @(showDetailedView ? "detailed-view" : "")">
@{ 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" : "")"
style="--i: @idx"
@@ -149,7 +151,7 @@
</div>
<div class="card-description-preview">@card.Description</div>
<div class="card-category-badge @card.Category?.ToLowerInvariant()">@card.Category</div>
</div>
}
</div>
@@ -171,7 +173,7 @@
@if (selectedCard != null)
{
<CardDialog Card="selectedCard" OnClose="CloseDetail" OnNavigate="SelectCard" />
<CardDialog Card="selectedCard" OnClose="CloseDetail" OnNavigate="SelectCard"/>
}
@code {
@@ -220,12 +222,12 @@
_ => sortDescending ? filtered.OrderByDescending(c => c.Name) : filtered.OrderBy(c => c.Name)
};
}
private void SetImmortal(string imm)
{
immortalFilter = immortalFilter == imm ? "" : imm;
}
private void ClearImmortalFilter()
{
immortalFilter = "";
@@ -234,11 +236,13 @@
private void SetCategory(string cat)
{
categoryFilter = categoryFilter == cat ? "" : cat;
_ = AnalyticsService.TrackEvent("filter_category", new { category = categoryFilter });
}
private void ClearCategoryFilter()
{
categoryFilter = "";
_ = AnalyticsService.TrackEvent("filter_category", new { category = "All" });
}
private void ClearFactionFilter()
@@ -250,7 +254,7 @@
{
agentLink = !agentLink;
}
private void ClearCostFilter()
{
costFilter = "";
@@ -264,6 +268,7 @@
private void SelectCard(CardData card)
{
selectedCard = card;
_ = AnalyticsService.TrackEvent("select_item", new { item_name = card.Name, item_category = card.Category });
}
private void CloseDetail()
@@ -278,4 +283,5 @@
factionFilter = "";
costFilter = "";
}
}
+3 -1
View File
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Shared.Services;
using Web;
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.AddTelerikBlazor();
builder.Services.AddSingleton<Shared.Services.CardRenderingService>();
builder.Services.AddSingleton<CardRenderingService>();
builder.Services.AddScoped<AnalyticsService>();
await builder.Build().RunAsync();
+2 -2
View File
@@ -10,7 +10,7 @@
</PropertyGroup>
<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>
@@ -21,6 +21,6 @@
<ItemGroup>
<ProjectReference Include="..\Model\Model.csproj"/>
<ProjectReference Include="..\Shared\Shared.csproj" />
<ProjectReference Include="..\Shared\Shared.csproj"/>
</ItemGroup>
</Project>
+2
View File
@@ -9,6 +9,8 @@
@using Microsoft.JSInterop
@using Shared.Layout
@using Shared.Pages
@using Shared.Services
@using Shared.Components
@using Chrono.Components
@using Telerik.Blazor
@using Telerik.Blazor.Components
-3
View File
@@ -257,9 +257,6 @@ a, .btn-link {
}
.inline-card-btn {
background: none;
border: none;
+39 -16
View File
@@ -6,26 +6,29 @@
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Chrono CCG - Card Gallery</title>
<base href="/"/>
<meta name="description" 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"/>
<meta content="Explore the full collection of Chrono CCG cards. Filter by cost, faction, and type."
name="description"/>
<meta content="Chrono CCG, Reference, Card Gallery, DB" name="keywords"/>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://chronoccg.com/"/>
<meta property="og:title" content="Chrono CCG - Card Gallery"/>
<meta property="og:description" 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"/>
<meta content="website" property="og:type"/>
<meta content="https://chronoccg.com/" property="og:url"/>
<meta content="Chrono CCG - Card Gallery" property="og:title"/>
<meta content="Explore the full collection of Chrono CCG cards. Filter by cost, faction, and type."
property="og:description"/>
<meta content="https://chronoccg.com/favicon.png" property="og:image"/>
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image"/>
<meta property="twitter:url" content="https://chronoccg.com/"/>
<meta property="twitter:title" content="Chrono CCG - Card Gallery"/>
<meta property="twitter:description" 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"/>
<meta content="summary_large_image" property="twitter:card"/>
<meta content="https://chronoccg.com/" property="twitter:url"/>
<meta content="Chrono CCG - Card Gallery" property="twitter:title"/>
<meta content="Explore the full collection of Chrono CCG cards. Filter by cost, faction, and type."
property="twitter:description"/>
<meta content="https://chronoccg.com/favicon.png" property="twitter:image"/>
<link href="manifest.json" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
<link href="manifest.json" rel="manifest"/>
<link href="icon-512.png" rel="apple-touch-icon" sizes="512x512"/>
<link href="icon-192.png" rel="apple-touch-icon" sizes="192x192"/>
<link href="https://fonts.googleapis.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"/>
@@ -37,6 +40,26 @@
<script defer src="/_content/Telerik.UI.for.Blazor/js/telerik-blazor.js"></script>
<link href="/favicon.png" rel="icon" type="image/png"/>
<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>
<body>
+2 -1
View File
@@ -1,3 +1,4 @@
// 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.
self.addEventListener('fetch', () => { });
self.addEventListener('fetch', () => {
});
@@ -8,8 +8,8 @@ self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
const offlineAssetsInclude = [/\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/];
const offlineAssetsExclude = [/^service-worker\.js$/];
async function onInstall(event) {
console.info('Service worker installing');
@@ -18,7 +18,7 @@ async function onInstall(event) {
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.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));
}
+7 -1
View File
@@ -1,6 +1,12 @@
{
"navigationFallback": {
"rewrite": "/index.html",
"exclude": ["/css/*", "/lib/*", "/_framework/*", "/_content/*", "/sample-data/*"]
"exclude": [
"/css/*",
"/lib/*",
"/_framework/*",
"/_content/*",
"/sample-data/*"
]
}
}