people tags #387

This commit is contained in:
Hamid Reza Mohammadi 2026-10-02 09:12:19 +03:30
parent 1f80d03fef
commit 872a8aab61
10 changed files with 116 additions and 120 deletions

View File

@ -1,82 +0,0 @@
@page "{id:int}"
@model GanjooRazor.Pages.FamilyTreeModel
@{
Layout = "_Layout";
}
@section Head {
<meta name="robots" content="noindex, nofollow" />
}
@if (!string.IsNullOrEmpty(Model.LastError))
{
@Model.LastError
}
else
{
<!-- #hdr2 -->
<div id="fa">
<div id="maincnt">
<partial name="_InlineLoginPartial" model="Model" />
<div class="spacer">&nbsp;</div>
<div class="poem">
<h1>
تبارنامهٔ @(string.IsNullOrEmpty(Model.RootPerson.FamilyTreeCaption) ? Model.RootPerson.Name : Model.RootPerson.FamilyTreeCaption)
<a href="javascript:void(0)" onclick="PersonWindow.open(@Model.RootPerson.Id)" class="up-btn up-btn--ghost up-btn--sm">مشاهدهٔ اطلاعات این شخصیت</a>
</h1>
<p class="up-hint"><small>
برای مشاهدهٔ توضیحات هر شخصیت، روی آن کلیک کنید. برای پیمایش نمودار آن را با ماوس بکشید و با غلتک ماوس بزرگ/کوچک کنید.
این نمودار محدود به فرزندان این شخصیت نیست؛ تمام خویشاوندان به‌هم‌پیوسته (از جمله نیاکان و خانوادهٔ همسر) را نشان می‌دهد.
</small></p>
<div id="familytree-container" style="position:relative;width:100%;height:70vh;border:1px solid #d8cdb8;border-radius:6px;overflow:hidden;background:#faf7f0;">
<svg id="familytree-svg" width="100%" height="100%"></svg>
<div id="familytree-tooltip" class="ft-tooltip-panel" style="display:none;"></div>
</div>
</div>
<div class="spacer">&nbsp;</div>
</div>
<partial name="_FooterPartial" model=@(new _FooterPartialModel(){StickyEnabled = true}) />
</div>
<style>
.ft-box { fill: #fdf6e6; stroke: #8b6b4a; stroke-width: 1.5; }
.ft-box-current { fill: #f3e2b8; stroke: #7a4a2a; stroke-width: 2.5; }
.ft-box-text { font-size: 14px; fill: #4a3520; font-family: inherit; }
.ft-heart { font-size: 13px; fill: #a0455c; }
.ft-line { fill: none; stroke: #a99a7c; stroke-width: 1.5; }
.ft-line-spouse { stroke-dasharray: 3,3; }
.ft-tooltip-panel {
position: absolute; top: 10px; left: 10px; max-width: 300px;
background: #fffdf7; border: 1px solid #8b6b4a; border-radius: 8px;
padding: 12px 16px; box-shadow: 0 2px 10px rgba(0,0,0,.25); z-index: 10;
direction: rtl; text-align: right;
}
.ft-tooltip-close {
float: left; border: none; background: none; font-size: 18px; cursor: pointer; color: #7a4a2a;
}
</style>
<script>
var familyTreeData = @Html.Raw(Model.TreeDataJson);
var familyTreeRootId = @Model.RootPerson.Id;
</script>
<script src="/js/familytree.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
GanjoorFamilyTree.render('familytree-container', 'familytree-svg', 'familytree-tooltip', familyTreeData, familyTreeRootId);
});
</script>
@if (ViewData["TrackingScript"] != null && !string.IsNullOrEmpty(ViewData["TrackingScript"].ToString()))
{
@Html.Raw(ViewData["TrackingScript"])
}
}

View File

@ -0,0 +1,53 @@
@page "{id:int}"
@model GanjooRazor.Pages.FamilyTreeWindowModel
@{
Layout = null;
}
@if (!string.IsNullOrEmpty(Model.LastError))
{
<p>@Model.LastError</p>
}
else
{
<div class="up-modal-familytree">
<h2>
تبارنامهٔ @(string.IsNullOrEmpty(Model.RootPerson.FamilyTreeCaption) ? Model.RootPerson.Name : Model.RootPerson.FamilyTreeCaption)
<a href="javascript:void(0)" onclick="PersonWindow.open(@Model.RootPerson.Id)" class="up-btn up-btn--ghost up-btn--sm">مشاهدهٔ اطلاعات این شخصیت</a>
</h2>
<p class="up-hint"><small>
برای مشاهدهٔ توضیحات هر شخصیت، روی آن کلیک کنید. برای پیمایش نمودار آن را با ماوس بکشید و با غلتک ماوس بزرگ/کوچک کنید.
این نمودار محدود به فرزندان این شخصیت نیست؛ تمام خویشاوندان به‌هم‌پیوسته (از جمله نیاکان و خانوادهٔ همسر) را نشان می‌دهد.
</small></p>
<div id="familytree-container" style="position:relative;width:100%;height:65vh;border:1px solid #d8cdb8;border-radius:6px;overflow:hidden;background:#faf7f0;">
<svg id="familytree-svg" width="100%" height="100%"></svg>
<div id="familytree-tooltip" class="ft-tooltip-panel" style="display:none;"></div>
</div>
<style>
.ft-box { fill: #fdf6e6; stroke: #8b6b4a; stroke-width: 1.5; }
.ft-box-current { fill: #f3e2b8; stroke: #7a4a2a; stroke-width: 2.5; }
.ft-box-text { font-size: 14px; fill: #4a3520; font-family: inherit; }
.ft-heart { font-size: 13px; fill: #a0455c; }
.ft-line { fill: none; stroke: #a99a7c; stroke-width: 1.5; }
.ft-line-spouse { stroke-dasharray: 3,3; }
.ft-tooltip-panel {
position: absolute; top: 10px; left: 10px; max-width: 300px;
background: #fffdf7; border: 1px solid #8b6b4a; border-radius: 8px;
padding: 12px 16px; box-shadow: 0 2px 10px rgba(0,0,0,.25); z-index: 10;
direction: rtl; text-align: right;
}
.ft-tooltip-close {
float: left; border: none; background: none; font-size: 18px; cursor: pointer; color: #7a4a2a;
}
</style>
@* not an executable script - just a data holder, parsed and rendered explicitly by
FamilyTreeWindow.open()'s onReady callback in personwindow.js once this fragment has
been injected into the modal body (innerHTML never executes a <script> tag, so unlike
the old standalone FamilyTree.cshtml page this can't just call GanjoorFamilyTree.render()
directly here) - same pattern _PersonGraphPartial.cshtml uses for pge-data *@
<script type="application/json" id="ftw-data">@Html.Raw(Model.TreeDataJson)</script>
<input type="hidden" id="ftw-root-id" value="@Model.RootPerson.Id" />
</div>
}

View File

@ -9,14 +9,16 @@ using RMuseum.Models.Ganjoor.ViewModels;
namespace GanjooRazor.Pages
{
/// <summary>
/// interactive family-tree chart, drawn client-side (vanilla JS/SVG - see the &lt;script&gt; block
/// in FamilyTree.cshtml) from the connected kinship component GET api/people/{id}/familytree
/// returns. Public/read-only counterpart of the PersonWindow.open() modal's plain relatives
/// list, reachable from there and from the PeopleExplorer.open() modal's family-tree index.
/// interactive family-tree chart, drawn client-side (vanilla JS/SVG - see familytree.js) from
/// the connected kinship component GET api/people/{id}/familytree returns. Inline-modal
/// counterpart of the old standalone FamilyTree.cshtml page (now removed) - fetched as a
/// fragment (Layout = null, no site chrome) and opened over whatever page the user is already
/// on via FamilyTreeWindow.open(id) in personwindow.js, the same pattern PersonWindow.cshtml/
/// PeopleExplorer.cshtml use.
/// </summary>
public class FamilyTreeModel : LoginPartialEnabledPageModel
public class FamilyTreeWindowModel : LoginPartialEnabledPageModel
{
public FamilyTreeModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration)
public FamilyTreeWindowModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration)
{
}
@ -27,9 +29,10 @@ namespace GanjooRazor.Pages
public GanjoorFamilyTreeViewModel Tree { get; set; }
/// <summary>
/// Tree, re-serialized with an explicit camelCase contract (same convention ReviewEdits.cshtml.cs
/// uses for AllLocationsJson/AllPeopleJson) so the client-side layout script has a predictable
/// shape to parse, regardless of the API's own default casing
/// Tree, re-serialized with an explicit camelCase contract (same convention
/// _PersonGraphPartial.cshtml.cs's GraphDataJson uses) so familytree.js has a predictable
/// shape to parse once this fragment's data block is read, regardless of the API's own
/// default casing
/// </summary>
public string TreeDataJson =>
JsonConvert.SerializeObject(
@ -54,8 +57,6 @@ namespace GanjooRazor.Pages
return Page();
}
ViewData["Title"] = $"گنجور » تبارنامهٔ {(string.IsNullOrEmpty(RootPerson.FamilyTreeCaption) ? RootPerson.Name : RootPerson.FamilyTreeCaption)}";
var treeResponse = await _httpClient.GetAsync($"{APIRoot.Url}/api/people/{id}/familytree");
if (!treeResponse.IsSuccessStatusCode)
{

View File

@ -21,7 +21,7 @@ else
<li>
<a href="javascript:void(0)" onclick="PersonWindow.open(@root.Id)">@root.FamilyTreeCaption</a>
<small>(@root.Name)</small>
— <a href="/FamilyTree/@root.Id"><small>نمایش نمودار</small></a>
— <a href="javascript:void(0)" onclick="FamilyTreeWindow.open(@root.Id)"><small>نمایش نمودار</small></a>
</li>
}
</ul>

View File

@ -52,7 +52,7 @@ else
{
<h3>
خویشاوندان
<a href="/FamilyTree/@Model.Person.Id" class="up-btn up-btn--ghost up-btn--sm">نمایش نمودار تبارنامه</a>
<a href="javascript:void(0)" onclick="FamilyTreeWindow.open(@Model.Person.Id)" class="up-btn up-btn--ghost up-btn--sm">نمایش نمودار تبارنامه</a>
@if (Model.LoggedIn)
{
<a href="/User/SuggestNewPersonRelation?personId=@Model.Person.Id" class="up-btn up-btn--ghost up-btn--sm">افزودن خویشاوند جدید</a>

View File

@ -39,10 +39,11 @@
</script>
<script src="~/js/bk.js" asp-append-version="true"></script>
@* person profile / people-explorer inline modal - see _Layout.cshtml for the same include on
the public site; needed here too since ReviewPersonEdits/ReviewPersonRelationEdits link to
a person's profile *@
@* person profile / people-explorer / family-tree inline modals - see _Layout.cshtml for the
same includes on the public site; needed here too since ReviewPersonEdits/
ReviewPersonRelationEdits link to a person's profile *@
<script src="~/js/peoplegraph.js" asp-append-version="true"></script>
<script src="~/js/familytree.js" asp-append-version="true"></script>
<script src="~/js/personwindow.js" asp-append-version="true"></script>
<script>
function editBanner(bannerId) {

View File

@ -37,10 +37,12 @@
<link rel="search" type="application/opensearchdescription+xml" title="گنجور" href="https://ganjoor.net/search.xml" />
<script src="/lib/jquery/dist/jquery.min.js"></script>
<script src="~/js/bk.js" asp-append-version="true"></script>
@* person profile / people-explorer inline modal - global since a person's name can turn up
almost anywhere (poem pages, family tree, suggestion forms); peoplegraph.js is the shared
force-directed renderer both PeopleExplorer.open() and the category-scoped "شخصیت‌ها" tab use *@
@* person profile / people-explorer / family-tree inline modals - global since a person's
name can turn up almost anywhere (poem pages, family tree, suggestion forms); peoplegraph.js
and familytree.js are the shared renderers PeopleExplorer.open()/FamilyTreeWindow.open() and
the category-scoped "شخصیت‌ها" tab use *@
<script src="~/js/peoplegraph.js" asp-append-version="true"></script>
<script src="~/js/familytree.js" asp-append-version="true"></script>
<script src="~/js/personwindow.js" asp-append-version="true"></script>
<script>
var cookieValue = getCookie("playerScrollLock");

View File

@ -39,10 +39,11 @@
</script>
<script src="/lib/diff.js"></script>
<script src="~/js/bk.js" asp-append-version="true"></script>
<!-- PersonWindow.open()/PeopleExplorer.open() are used by pages under Areas/User/Pages (e.g. the
Suggest* person-editing forms), so they're loaded globally here, same as _Layout.cshtml and
_AdminLayout.cshtml do for the rest of the site. -->
<!-- PersonWindow.open()/PeopleExplorer.open()/FamilyTreeWindow.open() are used by pages under
Areas/User/Pages (e.g. the Suggest* person-editing forms), so they're loaded globally here,
same as _Layout.cshtml and _AdminLayout.cshtml do for the rest of the site. -->
<script src="~/js/peoplegraph.js" asp-append-version="true"></script>
<script src="~/js/familytree.js" asp-append-version="true"></script>
<script src="~/js/personwindow.js" asp-append-version="true"></script>
<script>
function compareCorrectionBlocks(originalElemenyId, editedElementId, resultElementId) {

View File

@ -1,9 +1,12 @@
// Renders the interactive family-tree chart on /FamilyTree/{id}. Pure vanilla JS + SVG - no external
// charting library - since the shape of the problem (a handful to a few hundred nodes, drawn once,
// Renders the interactive family-tree chart inside the FamilyTreeWindow.open(id) modal (see
// personwindow.js and FamilyTreeWindow.cshtml). Pure vanilla JS + SVG - no external charting
// library - since the shape of the problem (a handful to a few hundred nodes, drawn once,
// panned/zoomed by the user) doesn't need one, and the project prefers self-hosted, dependency-light
// front-end code.
//
// Input (window.familyTreeData, set inline by FamilyTree.cshtml): the JSON of
// Input (passed as the render() call's data/requestedRootId arguments - see personwindow.js's
// openFamilyTree(), which reads them from the #ftw-data/#ftw-root-id elements
// FamilyTreeWindow.cshtml embeds): the JSON of
// RMuseum.Models.Ganjoor.ViewModels.GanjoorFamilyTreeViewModel, camelCased -
// { rootId, persons: [{id, name, birthYearInLHijri, deathYearInLHijri, validBirthDate,
// validDeathDate, description, wikiUrl, familyTreeCaption, ...}], relations: [{person1Id,

View File

@ -1,10 +1,10 @@
// Generic inline-modal shell, loaded on every page (see _Layout.cshtml / _AdminLayout.cshtml),
// used to replace what used to be three standalone pages: Person.cshtml (a person's profile),
// People.cshtml (family-tree-roots index) and PeopleGraph.cshtml (whole-site relationship graph).
// Those pages had no independent purpose of their own - they only ever existed to show data
// reachable from somewhere else (a poem's tagged persons, a category's "شخصیت‌ها" tab, an admin
// moderation queue, the family-tree chart) - so instead of navigating away, callers open one of
// these two windows over whatever page they're already on:
// used to replace what used to be four standalone pages: Person.cshtml (a person's profile),
// People.cshtml (family-tree-roots index), PeopleGraph.cshtml (whole-site relationship graph) and
// FamilyTree.cshtml (one person's family-tree chart). Those pages had no independent purpose of
// their own - they only ever existed to show data reachable from somewhere else (a poem's tagged
// persons, a category's "شخصیت‌ها" tab, an admin moderation queue) - so instead of navigating
// away, callers open one of these three windows over whatever page they're already on:
//
// PersonWindow.open(id) - a small modal: one person's bio/relations/affiliations/poems,
// fetched from /PersonWindow/{id} (Person.cshtml's old content,
@ -12,8 +12,11 @@
// PeopleExplorer.open() - a large modal: the whole-site force-directed graph (formerly
// /PeopleGraph) plus the family-tree-roots list (formerly /People),
// merged into one fragment fetched from /PeopleExplorer.
// FamilyTreeWindow.open(id) - a large modal: one person's connected-kinship-component tree
// chart (formerly /FamilyTree/{id}), fetched from
// /FamilyTreeWindow/{id} - see FamilyTreeWindow.cshtml.
//
// Both render into the same reusable overlay/modal shell built here on first use.
// All three render into the same reusable overlay/modal shell built here on first use.
(function () {
'use strict';
@ -117,6 +120,20 @@
});
}
function openFamilyTree(id) {
openModal(true);
loadInto('/FamilyTreeWindow/' + String(id), function () {
var dataEl = document.getElementById('ftw-data');
var rootIdEl = document.getElementById('ftw-root-id');
if (dataEl && rootIdEl && window.GanjoorFamilyTree) {
var treeData = JSON.parse(dataEl.textContent);
var rootId = parseInt(rootIdEl.value, 10);
GanjoorFamilyTree.render('familytree-container', 'familytree-svg', 'familytree-tooltip', treeData, rootId);
}
});
}
window.PersonWindow = { open: openPerson, close: closeModal };
window.PeopleExplorer = { open: openExplorer, close: closeModal };
window.FamilyTreeWindow = { open: openFamilyTree, close: closeModal };
})();