Vibe documents
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
@namespace Shared.Pages
|
||||
@page "/docs"
|
||||
|
||||
<PageTitle>Docs</PageTitle>
|
||||
|
||||
<div class="docs-page">
|
||||
<div class="docs-header">
|
||||
<h1>Documentation</h1>
|
||||
<p class="text-muted">Rules, keywords, factions, and more.</p>
|
||||
</div>
|
||||
|
||||
<div class="docs-container">
|
||||
<aside class="docs-sidebar">
|
||||
<div class="search-box">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" placeholder="Search docs..." @bind="searchQuery" @bind:event="oninput" />
|
||||
</div>
|
||||
|
||||
<div class="category-list">
|
||||
<div class="category-item @(selectedCategory == null ? "active" : "")" @onclick="() => SelectCategory(null)">
|
||||
All Pages
|
||||
</div>
|
||||
@foreach (var category in categories)
|
||||
{
|
||||
var cat = category;
|
||||
<div class="category-item @(selectedCategory == cat ? "active" : "")" @onclick="() => SelectCategory(cat)">
|
||||
@cat
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="docs-content">
|
||||
@if (selectedDoc == null)
|
||||
{
|
||||
<div class="doc-list">
|
||||
@foreach (var doc in filteredDocs)
|
||||
{
|
||||
<div class="doc-card" @onclick="() => SelectDoc(doc)">
|
||||
<div class="doc-card-category">@doc.Category</div>
|
||||
<h3 class="doc-card-title">@doc.Title</h3>
|
||||
@{
|
||||
var desc = GetDescription(doc);
|
||||
}
|
||||
@if (desc != null)
|
||||
{
|
||||
<p class="doc-card-desc">@desc</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!filteredDocs.Any())
|
||||
{
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-search"></i>
|
||||
<p>No documentation found matching your search.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<article class="doc-article">
|
||||
<button class="btn-back" @onclick="() => SelectDoc(null)">
|
||||
<i class="bi bi-arrow-left"></i> Back to list
|
||||
</button>
|
||||
|
||||
<header class="doc-header">
|
||||
<div class="doc-meta">@selectedDoc.Category</div>
|
||||
<h1>@selectedDoc.Title</h1>
|
||||
@{
|
||||
var desc = GetDescription(selectedDoc);
|
||||
}
|
||||
@if (desc != null)
|
||||
{
|
||||
<p class="doc-description">@desc</p>
|
||||
}
|
||||
</header>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(selectedDoc.Content))
|
||||
{
|
||||
<div class="doc-body">
|
||||
@((MarkupString)FormatContent(selectedDoc.Content))
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (HasExtraFrontmatter(selectedDoc))
|
||||
{
|
||||
<div class="doc-extra">
|
||||
<h3>Details</h3>
|
||||
<div class="doc-frontmatter">
|
||||
@foreach (var kvp in selectedDoc.Frontmatter.Where(kvp => kvp.Key != "category" && kvp.Key != "description"))
|
||||
{
|
||||
<div class="fm-item">
|
||||
<span class="fm-key">@kvp.Key</span>
|
||||
<span class="fm-value">@kvp.Value</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string searchQuery = "";
|
||||
private string? selectedCategory;
|
||||
private DocData? selectedDoc;
|
||||
|
||||
private List<string> categories = [];
|
||||
private List<DocData> allDocs = [];
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
allDocs = DocDatabase.Docs.OrderBy(d => d.Category).ThenBy(d => d.Title).ToList();
|
||||
categories = allDocs.Select(d => d.Category).Distinct().OrderBy(c => c).ToList();
|
||||
}
|
||||
|
||||
private IEnumerable<DocData> filteredDocs => allDocs
|
||||
.Where(d => (selectedCategory == null || d.Category == selectedCategory) &&
|
||||
(string.IsNullOrWhiteSpace(searchQuery) ||
|
||||
d.Title.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ||
|
||||
(GetDescription(d)?.Contains(searchQuery, StringComparison.OrdinalIgnoreCase) ?? false) ||
|
||||
d.Content.Contains(searchQuery, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
private void SelectCategory(string? category)
|
||||
{
|
||||
selectedCategory = category;
|
||||
selectedDoc = null;
|
||||
}
|
||||
|
||||
private void SelectDoc(DocData? doc)
|
||||
{
|
||||
selectedDoc = doc;
|
||||
}
|
||||
|
||||
private static string? GetDescription(DocData doc)
|
||||
{
|
||||
return doc.Frontmatter.TryGetValue("description", out var desc) && !string.IsNullOrWhiteSpace(desc) ? desc : null;
|
||||
}
|
||||
|
||||
private static bool HasExtraFrontmatter(DocData doc)
|
||||
{
|
||||
var count = doc.Frontmatter.Count;
|
||||
if (count == 0) return false;
|
||||
if (count == 1 && doc.Frontmatter.ContainsKey("category")) return false;
|
||||
if (count == 2 && doc.Frontmatter.ContainsKey("category") && doc.Frontmatter.ContainsKey("description")) return false;
|
||||
return doc.Frontmatter.Any(kvp => kvp.Key != "category" && kvp.Key != "description");
|
||||
}
|
||||
|
||||
private string FormatContent(string content)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content)) return "";
|
||||
|
||||
var lines = content.Replace("\r\n", "\n").Split('\n');
|
||||
var html = new System.Text.StringBuilder();
|
||||
var inList = false;
|
||||
|
||||
foreach (var rawLine in lines)
|
||||
{
|
||||
var line = rawLine.TrimEnd();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
if (inList) { html.AppendLine("</ul>"); inList = false; }
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("- "))
|
||||
{
|
||||
var item = line[2..].Trim();
|
||||
if (item.Length > 0)
|
||||
{
|
||||
if (!inList) { html.AppendLine("<ul class=\"doc-list-items\">"); inList = true; }
|
||||
html.Append("<li>");
|
||||
html.Append(EncodeInline(item));
|
||||
html.AppendLine("</li>");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inList) { html.AppendLine("</ul>"); inList = false; }
|
||||
|
||||
if (line.EndsWith(":") || line.Contains(":\n"))
|
||||
{
|
||||
var trimmed = line.TrimEnd(':');
|
||||
if (trimmed.Length > 0)
|
||||
{
|
||||
html.Append("<p class=\"doc-label\">");
|
||||
html.Append(System.Web.HttpUtility.HtmlEncode(trimmed));
|
||||
html.AppendLine("</p>");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
html.Append("<p>");
|
||||
html.Append(EncodeInline(line));
|
||||
html.AppendLine("</p>");
|
||||
}
|
||||
}
|
||||
|
||||
if (inList) html.AppendLine("</ul>");
|
||||
|
||||
return html.ToString();
|
||||
}
|
||||
|
||||
private string EncodeInline(string text)
|
||||
{
|
||||
var result = new System.Text.StringBuilder();
|
||||
var remaining = text;
|
||||
|
||||
while (remaining.Length > 0)
|
||||
{
|
||||
var boldIdx = remaining.IndexOf("**", StringComparison.Ordinal);
|
||||
if (boldIdx >= 0)
|
||||
{
|
||||
var endBold = remaining.IndexOf("**", boldIdx + 2, StringComparison.Ordinal);
|
||||
if (endBold >= 0)
|
||||
{
|
||||
result.Append(System.Web.HttpUtility.HtmlEncode(remaining[..boldIdx]));
|
||||
result.Append("<strong>");
|
||||
result.Append(System.Web.HttpUtility.HtmlEncode(remaining[(boldIdx + 2)..endBold]));
|
||||
result.Append("</strong>");
|
||||
remaining = remaining[(endBold + 2)..];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.Append(System.Web.HttpUtility.HtmlEncode(remaining));
|
||||
break;
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user