Files
Fellowship-Reference/Fellowship/Web/Pages/Docs.razor
T
2026-06-17 22:04:42 -04:00

332 lines
13 KiB
Plaintext

@page "/docs"
@using System.Text.RegularExpressions
<PageTitle>Fellowship Docs</PageTitle>
<div class="docs-page">
<div class="docs-header">
<h1 class="docs-title">Fellowship Reference</h1>
<p class="docs-subtitle">@DocsData.All.Count entries across @Groups.Count() categories</p>
<div class="docs-search">
<span class="search-icon">&#x1F50D;</span>
<input type="text" class="search-input" placeholder="Search docs..." @bind="searchTerm" @bind:after="OnSearchChanged" />
@if (!string.IsNullOrEmpty(searchTerm))
{
<button class="search-clear" @onclick="ClearSearch">&times;</button>
}
</div>
<div class="docs-filter-bar">
<button class="filter-btn @(activeFilter == null ? "active" : "")" @onclick="FilterAll">All</button>
<button class="filter-btn @(activeFilter == "Skill" ? "active" : "")" @onclick="FilterSkills">Skills</button>
<button class="filter-btn @(activeFilter == "Debuff" ? "active" : "")" @onclick="FilterDebuffs">Debuffs</button>
<button class="filter-btn @(activeFilter == "Buff" ? "active" : "")" @onclick="FilterBuffs">Buffs</button>
<button class="filter-btn @(activeFilter == "Key" ? "active" : "")" @onclick="FilterKeys">Keys</button>
</div>
</div>
<div class="docs-body">
<nav class="docs-sidebar">
<div class="sidebar-inner">
@foreach (var group in FilteredGroups)
{
var groupId = GetGroupId(group.Key);
<div class="sidebar-group">
<a class="sidebar-group-title" href="@($"#{groupId}")">@group.Key</a>
<div class="sidebar-items">
@foreach (var doc in group)
{
var docId = GetDocId(doc);
<a class="sidebar-item" href="@($"#{docId}")" title="@doc.FileName">
<span class="sidebar-item-badge @GetTypeClass(doc)"></span>
@GetDisplayName(doc)
</a>
}
</div>
</div>
}
</div>
</nav>
<div class="docs-content">
@if (!FilteredGroups.Any())
{
<div class="no-results">
<p>No docs match "@searchTerm"</p>
<button class="filter-btn" @onclick="ClearSearch">Clear search</button>
</div>
}
@foreach (var group in FilteredGroups)
{
var groupId = GetGroupId(group.Key);
<section class="group-section">
<div class="group-header" id="@groupId">
<h2 class="group-title">@group.Key</h2>
<span class="group-count">@group.Count()</span>
</div>
@foreach (var doc in group)
{
var docId = GetDocId(doc);
var typeName = doc.GetType().Name.Replace("Doc", "");
<article class="doc-card" id="@docId">
<div class="doc-card-header">
<div class="doc-card-title-row">
<h3 class="doc-card-title">@GetDisplayName(doc)</h3>
<span class="doc-type-badge @GetTypeClass(doc)">@typeName</span>
</div>
@if (doc is SkillDoc { Key: { } key } && !string.IsNullOrEmpty(key))
{
<div class="doc-key-badge">
<span class="key-icon">&#x2328;</span>
<span class="key-text">@key</span>
</div>
}
</div>
<div class="doc-card-body">
@if (doc is SkillDoc { Description: { } desc } && !string.IsNullOrEmpty(desc))
{
<div class="doc-description">
@{
var lines = desc.Split('\n');
foreach (var line in lines)
{
<p>@RenderWikiLinks(line)</p>
}
}
</div>
}
<div class="doc-fields">
@foreach (var field in GetOrderedFields(doc))
{
<div class="doc-field">
<span class="doc-field-label">@field.Label</span>
<span class="doc-field-value">@field.Value</span>
</div>
}
</div>
@if (GetBody(doc) is { } body && !string.IsNullOrWhiteSpace(body) && (doc is not SkillDoc || string.IsNullOrWhiteSpace(((SkillDoc)doc).Description)))
{
<div class="doc-body">
<pre class="doc-body-text">@body.Trim()</pre>
</div>
}
</div>
@if (doc is SkillDoc { Tags: { } tags } && tags.Count > 0)
{
<div class="doc-card-footer">
@foreach (var tag in tags)
{
<span class="tag-badge">@tag</span>
}
</div>
}
</article>
}
</section>
}
</div>
</div>
</div>
@code {
private string? searchTerm;
private string? activeFilter;
private List<IGrouping<string, DocEntry>> Groups = [];
private IEnumerable<IGrouping<string, DocEntry>> FilteredGroups => Groups
.Where(g => g.Any(d => MatchesFilter(d)))
.OrderBy(g => SortOrder(g.Key));
protected override void OnInitialized()
{
Groups = DocsData.All
.GroupBy(d => GetCharacterOrType(d))
.ToList();
}
private bool MatchesFilter(DocEntry doc)
{
if (!string.IsNullOrEmpty(searchTerm))
{
var term = searchTerm.Trim().ToLowerInvariant();
var name = GetDisplayName(doc).ToLowerInvariant();
if (!name.Contains(term)) return false;
}
if (activeFilter != null)
{
var type = doc.GetType().Name.Replace("Doc", "");
if (type != activeFilter) return false;
}
return true;
}
private static int SortOrder(string groupKey) => groupKey switch
{
"Xavian" => 0,
"Rime" => 1,
"Vigour" => 2,
"Keys" => 3,
_ => 99
};
private void OnSearchChanged()
{
StateHasChanged();
}
private void ClearSearch()
{
searchTerm = null;
activeFilter = null;
}
private void FilterAll() { activeFilter = null; }
private void FilterSkills() { activeFilter = activeFilter == "Skill" ? null : "Skill"; }
private void FilterDebuffs() { activeFilter = activeFilter == "Debuff" ? null : "Debuff"; }
private void FilterBuffs() { activeFilter = activeFilter == "Buff" ? null : "Buff"; }
private void FilterKeys() { activeFilter = activeFilter == "Key" ? null : "Key"; }
private static string GetCharacterOrType(DocEntry doc) => doc switch
{
SkillDoc s => s.Character,
DebuffDoc d => d.Character,
BuffDoc b => b.Character,
CharacterDoc c => c.Character,
KeyDoc => "Keys",
_ => "Other"
};
private static string GetDisplayName(DocEntry doc) =>
Path.GetFileNameWithoutExtension(doc.FileName);
private static string GetGroupId(string group) =>
$"group-{group.GetHashCode():x}";
private static string GetDocId(DocEntry doc) =>
$"doc-{doc.FileName.GetHashCode():x}";
private static string GetTypeClass(DocEntry doc) => doc switch
{
SkillDoc => "type-skill",
DebuffDoc => "type-debuff",
BuffDoc => "type-buff",
KeyDoc => "type-key",
CharacterDoc => "type-character",
_ => ""
};
private static MarkupString RenderWikiLinks(string line)
{
var result = Regex.Replace(line, @"\[\[([^\]]+)\]\]", m =>
{
var name = m.Groups[1].Value;
return $"<span class=\"wiki-link\">{name}</span>";
});
return new MarkupString(result);
}
private static readonly Dictionary<string, string> FieldLabels = new()
{
["Character"] = "Character",
["Cast"] = "Cast Time",
["Key"] = "Key Bind",
["Range"] = "Range",
["Damage"] = "Damage",
["DamageType"] = "Damage Type",
["Heal"] = "Healing",
["Shield"] = "Shield",
["Cooldown"] = "Cooldown",
["Mana"] = "Mana Cost",
["OffGlobalCooldown"] = "Off GCD",
["Gdc"] = "GDC",
["Duration"] = "Duration",
["DamageReduction"] = "Dmg Reduction",
["DamageTickTime"] = "Tick Interval",
["IsToggle"] = "Toggle",
["ManaUpkeepTick"] = "Mana Upkeep",
["ParryChance"] = "Parry Chance",
["DamageRedirection"] = "Dmg Redirection",
["SpiritCost"] = "Spirit Cost",
["SecondEffectDuration"] = "Effect 2 Duration",
["SecondEffectDamageReduction"] = "Effect 2 Dmg Reduction",
["HealingDuration"] = "Healing Duration",
["HealingTickTime"] = "Healing Tick",
["CostSwiftReprieval"] = "Cost: Swift Reprieval",
["GdcDuration"] = "GDC Duration",
["Effect"] = "Effect",
["GeneratesSpirit"] = "Generates Spirit",
["AreaDamagePercentage"] = "Cleave %",
["SwiftReprievalChance"] = "Swift Reprieval %",
["MaxStacks"] = "Max Stacks",
["Action"] = "Action",
["ParryChanceBonus"] = "Parry Bonus",
["ManaRestoreBase"] = "Mana Restore Base",
["ManaRestorePerStack"] = "Mana Per Stack",
["Order"] = "Order",
["Priority"] = "Priority",
["Completed"] = "Completed",
["Raw"] = "Raw",
};
private static readonly Dictionary<Type, string[]> FieldOrder = new()
{
[typeof(SkillDoc)] = ["Character", "Cast", "Key", "Range", "Damage", "DamageType", "Heal", "Shield", "Cooldown", "Mana", "OffGlobalCooldown", "Gdc", "Duration", "DamageReduction", "DamageTickTime", "IsToggle", "ManaUpkeepTick", "ParryChance", "DamageRedirection", "GeneratesSpirit", "AreaDamagePercentage", "SwiftReprievalChance", "Effect", "SpiritCost", "SecondEffectDuration", "SecondEffectDamageReduction", "HealingDuration", "HealingTickTime", "CostSwiftReprieval", "GdcDuration"],
[typeof(DebuffDoc)] = ["Character", "MaxStacks", "Duration", "ParryChanceBonus", "ManaRestoreBase", "ManaRestorePerStack"],
[typeof(BuffDoc)] = ["Character", "MaxStacks"],
[typeof(KeyDoc)] = ["Action"],
[typeof(CharacterDoc)] = ["Character"],
};
private static List<FieldEntry> GetOrderedFields(DocEntry doc)
{
var result = new List<FieldEntry>();
var type = doc.GetType();
var order = FieldOrder.GetValueOrDefault(type, []);
var allProps = new Dictionary<string, string>();
foreach (var prop in type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
{
var name = prop.Name;
if (name is "FileName" or "FilePath" or "Body" or "Description") continue;
var value = prop.GetValue(doc);
if (value == null) continue;
if (value is bool bVal && !bVal) continue;
if (value is string s && string.IsNullOrEmpty(s)) continue;
var display = value switch
{
List<string> list => string.Join(", ", list),
_ => value.ToString()
};
if (string.IsNullOrEmpty(display)) continue;
allProps[name] = display;
}
foreach (var key in order)
{
if (allProps.Remove(key, out var val))
{
result.Add(new FieldEntry(FieldLabels.GetValueOrDefault(key, key), val));
}
}
foreach (var (key, val) in allProps)
{
result.Add(new FieldEntry(FieldLabels.GetValueOrDefault(key, key), val));
}
return result;
}
private static string? GetBody(DocEntry doc) => doc.Body;
private record FieldEntry(string Label, string Value);
}