Vibe Google Analytics
This commit is contained in:
@@ -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"/>
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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 = "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -257,9 +257,6 @@ a, .btn-link {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.inline-card-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
{
|
||||
"navigationFallback": {
|
||||
"rewrite": "/index.html",
|
||||
"exclude": ["/css/*", "/lib/*", "/_framework/*", "/_content/*", "/sample-data/*"]
|
||||
"exclude": [
|
||||
"/css/*",
|
||||
"/lib/*",
|
||||
"/_framework/*",
|
||||
"/_content/*",
|
||||
"/sample-data/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user