people tags #387

This commit is contained in:
Hamid Reza Mohammadi 2026-09-30 18:17:44 +03:30
parent d0efebd2cd
commit f305394ad7
33 changed files with 475 additions and 486 deletions

View File

@ -25,7 +25,7 @@ else
var p = s.Person;
<p>
<a role="button" target="_blank" href="/person/@p.Id" class="up-btn up-btn--ghost up-btn--sm">@p.Name</a>
<a role="button" href="javascript:void(0)" onclick="PersonWindow.open(@p.Id)" class="up-btn up-btn--ghost up-btn--sm">@p.Name</a>
</p>
<table>

View File

@ -90,24 +90,24 @@ else
<tr>
<td>طرف اول</td>
<td>
<a role="button" target="_blank" href="/person/@s.Person1Id" class="up-btn up-btn--ghost up-btn--sm">@(s.Person1 != null ? s.Person1.Name : s.Person1Id.ToString())</a>
<a role="button" href="javascript:void(0)" onclick="PersonWindow.open(@s.Person1Id)" class="up-btn up-btn--ghost up-btn--sm">@(s.Person1 != null ? s.Person1.Name : s.Person1Id.ToString())</a>
</td>
@if (s.Action == PersonRelationSuggestionAction.Modify)
{
<td>
<a role="button" target="_blank" href="/person/@s.Person1Id" class="up-btn up-btn--ghost up-btn--sm">@(s.Person1 != null ? s.Person1.Name : s.Person1Id.ToString())</a>
<a role="button" href="javascript:void(0)" onclick="PersonWindow.open(@s.Person1Id)" class="up-btn up-btn--ghost up-btn--sm">@(s.Person1 != null ? s.Person1.Name : s.Person1Id.ToString())</a>
</td>
}
</tr>
<tr>
<td>طرف دوم</td>
<td>
<a role="button" target="_blank" href="/person/@s.Person2Id" class="up-btn up-btn--ghost up-btn--sm">@(s.Person2 != null ? s.Person2.Name : s.Person2Id.ToString())</a>
<a role="button" href="javascript:void(0)" onclick="PersonWindow.open(@s.Person2Id)" class="up-btn up-btn--ghost up-btn--sm">@(s.Person2 != null ? s.Person2.Name : s.Person2Id.ToString())</a>
</td>
@if (s.Action == PersonRelationSuggestionAction.Modify)
{
<td>
<a role="button" target="_blank" href="/person/@s.Person2Id" class="up-btn up-btn--ghost up-btn--sm">@(s.Person2 != null ? s.Person2.Name : s.Person2Id.ToString())</a>
<a role="button" href="javascript:void(0)" onclick="PersonWindow.open(@s.Person2Id)" class="up-btn up-btn--ghost up-btn--sm">@(s.Person2 != null ? s.Person2.Name : s.Person2Id.ToString())</a>
</td>
}
</tr>

View File

@ -25,7 +25,7 @@ else
<div class="poem">
<h1>
شجره‌نامهٔ @(string.IsNullOrEmpty(Model.RootPerson.FamilyTreeCaption) ? Model.RootPerson.Name : Model.RootPerson.FamilyTreeCaption)
<a href="/person/@Model.RootPerson.Id" class="up-btn up-btn--ghost up-btn--sm">بازگشت به صفحهٔ شخصیت</a>
<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>
برای مشاهدهٔ توضیحات هر شخصیت، روی آن کلیک کنید. برای پیمایش نمودار آن را با ماوس بکشید و با غلتک ماوس بزرگ/کوچک کنید.

View File

@ -11,8 +11,8 @@ 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 Person.cshtml's plain relatives list, reachable from
/// there and from People.cshtml's family-tree index.
/// 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.
/// </summary>
public class FamilyTreeModel : LoginPartialEnabledPageModel
{

View File

@ -86,14 +86,6 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/OverlappingMarkerSpiderfier-Leaflet/0.2.6/oms.min.js"></script>
}
}
@if (Model.IsCatPage || Model.IsPoetPage)
{
@* the on-demand "شخصیت‌ها" tab (_CatPagePartial.cshtml / _PoetPagePartial.cshtml) can be
clicked at any time on any cat/poet page, so this is loaded unconditionally here (unlike
the leaflet.js block above, which only loads when geo tags are already known to exist) *@
<script src="/js/peoplegraph.js"></script>
}
}
@if (Model.OfflineMode)

View File

@ -460,7 +460,7 @@
<p>
@foreach (var taggedPerson in poemTaggedPersons)
{
<a href="/Person/@taggedPerson.Id" class="pagebutton comments-link">@taggedPerson.Name</a>
<a href="javascript:void(0)" onclick="PersonWindow.open(@taggedPerson.Id)" class="pagebutton comments-link">@taggedPerson.Name</a>
}
</p>
</div>

View File

@ -19,7 +19,7 @@ else
<div style="margin-bottom:10px;">
<input type="text" id="pg-cat-search" placeholder="جستجوی نام شخصیت..." style="width:240px;" />
<a role="button" id="pg-cat-reset" class="up-btn up-btn--ghost up-btn--sm">نمایش همه</a>
<a role="button" href="/PeopleGraph" class="up-btn up-btn--ghost up-btn--sm">نمایش شبکهٔ کامل شخصیت‌ها</a>
<a role="button" href="javascript:void(0)" onclick="PeopleExplorer.open()" class="up-btn up-btn--ghost up-btn--sm">نمایش شبکهٔ کامل شخصیت‌ها</a>
</div>
<div id="pg-cat-legend" class="pg-legend"></div>

View File

@ -7,10 +7,10 @@ namespace GanjooRazor.Pages
{
/// <summary>
/// model for the on-demand "شخصیت‌ها" (characters) tab shown on a poet/category page - renders
/// the same force-directed graph as PeopleGraph.cshtml/peoplegraph.js, but scoped to one
/// work/category (see GanjoorRelatedPersonService.GetCatPersonGraphAsync) instead of the whole
/// site. Element ids are prefixed "pg-cat-" so this partial can sit on the same page as (and not
/// collide with) any other graph instance.
/// the same force-directed graph as the PeopleExplorer.open() modal/peoplegraph.js, but scoped
/// to one work/category (see GanjoorRelatedPersonService.GetCatPersonGraphAsync) instead of the
/// whole site. Element ids are prefixed "pg-cat-" so this partial can sit on the same page as
/// (and not collide with) any other graph instance.
/// </summary>
public class _PersonGraphPartialModel : PageModel
{

View File

@ -1,61 +0,0 @@
@page
@model GanjooRazor.Pages.PeopleModel
@{
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>
شجره‌نامه‌ها
<a href="/PeopleGraph" class="up-btn up-btn--ghost up-btn--sm">نمایش شبکهٔ روابط همهٔ شخصیت‌ها</a>
</h1>
@if (Model.FamilyTreeRoots == null || Model.FamilyTreeRoots.Count == 0)
{
<p><small>هنوز شجره‌نامه‌ای ثبت نشده است.</small></p>
}
else
{
<ul>
@foreach (var root in Model.FamilyTreeRoots)
{
<li>
<a href="/person/@root.Id">@root.FamilyTreeCaption</a>
<small>(@root.Name)</small>
— <a href="/FamilyTree/@root.Id"><small>نمایش نمودار</small></a>
</li>
}
</ul>
}
</div>
<div class="spacer">&nbsp;</div>
</div>
<partial name="_FooterPartial" model=@(new _FooterPartialModel(){StickyEnabled = true}) />
</div>
@if (ViewData["TrackingScript"] != null && !string.IsNullOrEmpty(ViewData["TrackingScript"].ToString()))
{
@Html.Raw(ViewData["TrackingScript"])
}
}

View File

@ -1,44 +0,0 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using RMuseum.Models.Ganjoor;
namespace GanjooRazor.Pages
{
/// <summary>
/// entry point for browsing family trees - lists every person that captions a tree
/// (GanjoorRelatedPerson.FamilyTreeCaption not empty), grouped under that caption. A person
/// with no caption of their own still shows up via their relatives on that person's own page
/// (see Person.cshtml) - this index is only for jumping straight to a named tree's root(s).
/// </summary>
public class PeopleModel : LoginPartialEnabledPageModel
{
public PeopleModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration)
{
}
public string LastError { get; set; }
public List<GanjoorRelatedPerson> FamilyTreeRoots { get; set; }
public async Task<IActionResult> OnGetAsync()
{
InitializeCommonPageState();
ViewData["Title"] = "گنجور » شجره‌نامه‌ها";
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/people/familytrees");
if (!response.IsSuccessStatusCode)
{
LastError = await ReadErrorMessageAsync(response);
return Page();
}
FamilyTreeRoots = JsonConvert.DeserializeObject<List<GanjoorRelatedPerson>>(await response.Content.ReadAsStringAsync());
return Page();
}
}
}

View File

@ -0,0 +1,78 @@
@page
@model GanjooRazor.Pages.PeopleExplorerModel
@{
Layout = null;
}
@if (!string.IsNullOrEmpty(Model.LastError))
{
<p>@Model.LastError</p>
}
else
{
<div class="up-modal-explorer">
<h2>کاوش در شخصیت‌ها</h2>
@if (Model.FamilyTreeRoots != null && Model.FamilyTreeRoots.Count > 0)
{
<h3>شجره‌نامه‌ها</h3>
<ul>
@foreach (var root in Model.FamilyTreeRoots)
{
<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>
</li>
}
</ul>
}
<h3>شبکهٔ روابط همهٔ شخصیت‌ها</h3>
<p class="up-hint"><small>
روی یک گره کلیک کنید تا فقط ارتباط‌های همان شخص برجسته شود (کلیک دوباره برای بازگشت)؛ روی نام یک شخصیت (زیر گره یا در جدول) کلیک کنید تا اطلاعات کامل او باز شود.
برای پیمایش نمودار آن را با ماوس بکشید و با غلتک ماوس بزرگ/کوچک کنید.
</small></p>
<div style="margin-bottom:10px;">
<input type="text" id="pge-search" placeholder="جستجوی نام شخصیت..." style="width:240px;" />
<a role="button" id="pge-reset" class="up-btn up-btn--ghost up-btn--sm">نمایش همه</a>
</div>
<div id="pge-legend" class="pg-legend"></div>
<div id="pge-container" style="position:relative;width:100%;height:55vh;border:1px solid #d8cdb8;border-radius:6px;overflow:hidden;background:#faf7f0;">
<svg id="pge-svg" width="100%" height="100%"></svg>
</div>
<div class="spacer">&nbsp;</div>
<div style="max-height:30vh;overflow:auto;">
<table class="up-table">
<thead>
<tr>
<th>شخص اول</th>
<th>نوع ارتباط</th>
<th>شخص دوم</th>
<th>یادداشت</th>
</tr>
</thead>
<tbody id="pge-table-body"></tbody>
</table>
</div>
<style>
.pg-circle { fill: #fdf6e6; stroke: #8b6b4a; stroke-width: 1.5; }
.pg-circle-tree { fill: #f3e2b8; stroke: #7a4a2a; stroke-width: 2; }
.pg-label { font-size: 11px; fill: #4a3520; font-family: inherit; }
.pg-legend { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
.pg-legend-item { font-size: 12px; display: inline-flex; align-items: center; gap: 4px; }
.pg-legend-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; vertical-align: middle; }
.pg-person-link { cursor: pointer; }
</style>
@* not an executable script - just a data holder, parsed and rendered explicitly by
PeopleExplorer.open()'s onReady callback in personwindow.js once this fragment has been
injected into the modal body *@
<script type="application/json" id="pge-data">@Html.Raw(Model.GraphDataJson)</script>
</div>
}

View File

@ -0,0 +1,68 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using RMuseum.Models.Ganjoor;
using RMuseum.Models.Ganjoor.ViewModels;
namespace GanjooRazor.Pages
{
/// <summary>
/// the "explore all characters" modal - the whole-site force-directed relationship graph
/// (formerly the standalone page PeopleGraph.cshtml) plus the family-tree-roots index
/// (formerly the standalone page People.cshtml), merged into one bare fragment (Layout = null)
/// and opened via PeopleExplorer.open() in personwindow.js. Neither of those two pages had an
/// entry point in the site's own navigation, so folding them together here loses nothing -
/// it's reachable from inside PersonWindow and from the category-scoped "شخصیت‌ها" tab's
/// "نمایش شبکهٔ کامل شخصیت‌ها" link.
/// </summary>
public class PeopleExplorerModel : LoginPartialEnabledPageModel
{
public PeopleExplorerModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration)
{
}
public string LastError { get; set; }
public List<GanjoorRelatedPerson> FamilyTreeRoots { get; set; }
public GanjoorPersonGraphViewModel Graph { get; set; }
/// <summary>
/// Graph, re-serialized with an explicit camelCase contract (same convention
/// PeopleGraphModel/_PersonGraphPartialModel used) so peoplegraph.js has a predictable
/// shape to parse
/// </summary>
public string GraphDataJson =>
JsonConvert.SerializeObject(
Graph,
new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }
);
public async Task<IActionResult> OnGetAsync()
{
InitializeCommonPageState();
var treesResponse = await _httpClient.GetAsync($"{APIRoot.Url}/api/people/familytrees");
if (!treesResponse.IsSuccessStatusCode)
{
LastError = await ReadErrorMessageAsync(treesResponse);
return Page();
}
FamilyTreeRoots = JsonConvert.DeserializeObject<List<GanjoorRelatedPerson>>(await treesResponse.Content.ReadAsStringAsync());
var graphResponse = await _httpClient.GetAsync($"{APIRoot.Url}/api/people/graph");
if (!graphResponse.IsSuccessStatusCode)
{
LastError = await ReadErrorMessageAsync(graphResponse);
return Page();
}
Graph = JsonConvert.DeserializeObject<GanjoorPersonGraphViewModel>(await graphResponse.Content.ReadAsStringAsync());
return Page();
}
}
}

View File

@ -1,106 +0,0 @@
@page
@model GanjooRazor.Pages.PeopleGraphModel
@{
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>
شبکهٔ روابط شخصیت‌ها
<a href="/people" class="up-btn up-btn--ghost up-btn--sm">فهرست شجره‌نامه‌ها</a>
</h1>
<p class="up-hint"><small>
این نمودار همهٔ خویشاوندی‌ها و وابستگی‌های غیرخویشاوندی ثبت‌شده را با هم نشان می‌دهد؛ برخلاف نمودار شجره‌نامه که به یک خانواده محدود است.
روی هر گره کلیک کنید تا فقط ارتباط‌های همان شخص برجسته شود (کلیک دوباره برای بازگشت)؛ برای جابه‌جایی یک گره آن را بکشید (برای رها کردنش دوبار کلیک کنید).
برای پیمایش نمودار آن را با ماوس بکشید و با غلتک ماوس بزرگ/کوچک کنید.
</small></p>
<div style="margin-bottom:10px;">
<input type="text" id="pg-search" placeholder="جستجوی نام شخصیت..." style="width:240px;" />
<a role="button" id="pg-reset" class="up-btn up-btn--ghost up-btn--sm">نمایش همه</a>
</div>
<div id="pg-legend" class="pg-legend"></div>
<div id="peoplegraph-container" style="position:relative;width:100%;height:65vh;border:1px solid #d8cdb8;border-radius:6px;overflow:hidden;background:#faf7f0;">
<svg id="peoplegraph-svg" width="100%" height="100%"></svg>
</div>
<div class="spacer">&nbsp;</div>
<h2>فهرست ارتباط‌ها</h2>
<div style="max-height:40vh;overflow:auto;">
<table class="up-table">
<thead>
<tr>
<th>شخص اول</th>
<th>نوع ارتباط</th>
<th>شخص دوم</th>
<th>یادداشت</th>
</tr>
</thead>
<tbody id="pg-table-body"></tbody>
</table>
</div>
</div>
<div class="spacer">&nbsp;</div>
</div>
<partial name="_FooterPartial" model=@(new _FooterPartialModel(){StickyEnabled = true}) />
</div>
<style>
.pg-circle { fill: #fdf6e6; stroke: #8b6b4a; stroke-width: 1.5; }
.pg-circle-tree { fill: #f3e2b8; stroke: #7a4a2a; stroke-width: 2; }
.pg-circle-secondary { stroke-dasharray: 3,2; opacity: 0.7; }
.pg-label { font-size: 11px; fill: #4a3520; font-family: inherit; }
.pg-label-secondary { opacity: 0.7; font-style: italic; }
.pg-legend { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 10px; }
.pg-legend-item { font-size: 12px; display: inline-flex; align-items: center; gap: 4px; }
.pg-legend-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 3px; vertical-align: middle; }
</style>
<script>
var peopleGraphData = @Html.Raw(Model.GraphDataJson);
</script>
<script src="/js/peoplegraph.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
GanjoorPeopleGraph.render({
containerId: 'peoplegraph-container',
svgId: 'peoplegraph-svg',
tableBodyId: 'pg-table-body',
legendId: 'pg-legend',
searchInputId: 'pg-search',
resetButtonId: 'pg-reset',
data: peopleGraphData
});
});
</script>
@if (ViewData["TrackingScript"] != null && !string.IsNullOrEmpty(ViewData["TrackingScript"].ToString()))
{
@Html.Raw(ViewData["TrackingScript"])
}
}

View File

@ -1,54 +0,0 @@
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using RMuseum.Models.Ganjoor.ViewModels;
namespace GanjooRazor.Pages
{
/// <summary>
/// force-directed "ontology" explorer over the whole known people network (kinship edges and
/// non-family ties together), drawn client-side (vanilla JS/SVG - see the &lt;script&gt; block in
/// PeopleGraph.cshtml) from what GET api/people/graph returns. The nowruzgan-style counterpart of
/// the strict-tree /FamilyTree/{id} view: not limited to one family, shows every recorded
/// relationship at once with a synced data table and click-to-focus ego network.
/// </summary>
public class PeopleGraphModel : LoginPartialEnabledPageModel
{
public PeopleGraphModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration)
{
}
public string LastError { get; set; }
public GanjoorPersonGraphViewModel Graph { get; set; }
/// <summary>
/// Graph, re-serialized with an explicit camelCase contract (same convention FamilyTree.cshtml.cs
/// uses for TreeDataJson) so the client-side layout script has a predictable shape to parse
/// </summary>
public string GraphDataJson =>
JsonConvert.SerializeObject(
Graph,
new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }
);
public async Task<IActionResult> OnGetAsync()
{
InitializeCommonPageState();
ViewData["Title"] = "گنجور » شبکهٔ روابط شخصیت‌ها";
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/people/graph");
if (!response.IsSuccessStatusCode)
{
LastError = await ReadErrorMessageAsync(response);
return Page();
}
Graph = JsonConvert.DeserializeObject<GanjoorPersonGraphViewModel>(await response.Content.ReadAsStringAsync());
return Page();
}
}
}

View File

@ -1,162 +0,0 @@
@page "{id:int}"
@model GanjooRazor.Pages.PersonModel
@using DNTPersianUtils.Core
@{
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>
@Model.Person.Name
@if (Model.LoggedIn)
{
<a href="/SuggestPersonEdit/@Model.Person.Id" class="up-icon-btn" title="پیشنهاد ویرایش این شخصیت" aria-label="پیشنهاد ویرایش این شخصیت">✎</a>
}
</h1>
@if (!string.IsNullOrEmpty(Model.Person.FamilyTreeCaption))
{
<p><strong>شجره‌نامه: @Model.Person.FamilyTreeCaption</strong></p>
}
@if (Model.Person.BirthYearInLHijri != 0 || Model.Person.DeathYearInLHijri != 0)
{
<p>
@if (Model.Person.BirthYearInLHijri != 0)
{
<text>تولد: @Model.Person.BirthYearInLHijri.ToPersianNumbers() (قمری) @(Model.Person.ValidBirthDate ? "" : "- تخمینی")</text>
}
@if (Model.Person.DeathYearInLHijri != 0)
{
<text> — وفات: @Model.Person.DeathYearInLHijri.ToPersianNumbers() (قمری) @(Model.Person.ValidDeathDate ? "" : "- تخمینی")</text>
}
</p>
}
@if (!string.IsNullOrEmpty(Model.Person.Description))
{
<p>@Model.Person.Description</p>
}
@if (!string.IsNullOrEmpty(Model.Person.WikiUrl))
{
<p><a href="@Model.Person.WikiUrl" target="_blank" rel="noopener">صفحهٔ ویکی‌پدیا</a></p>
}
</div>
@if (Model.RelationRows.Count > 0)
{
<div class="spacer">&nbsp;</div>
<div class="poem">
<h2>
خویشاوندان
<a href="/FamilyTree/@Model.Person.Id" class="up-btn up-btn--ghost up-btn--sm">نمایش نمودار شجره‌نامه</a>
@if (Model.LoggedIn)
{
<a href="/SuggestNewPersonRelation/@Model.Person.Id" class="up-btn up-btn--ghost up-btn--sm">افزودن خویشاوند جدید</a>
}
</h2>
<ul>
@foreach (var row in Model.RelationRows)
{
<li>
@row.Label — <a href="/person/@row.OtherPersonId">@row.OtherPersonName</a>
@if (!string.IsNullOrEmpty(row.Note))
{
<text> (@row.Note)</text>
}
@if (Model.LoggedIn)
{
<a href="/SuggestPersonRelationEdit/@row.RelationId" class="up-icon-btn" title="پیشنهاد ویرایش/حذف این نسبت" aria-label="پیشنهاد ویرایش/حذف این نسبت">✎</a>
}
</li>
}
</ul>
</div>
}
else if (Model.LoggedIn)
{
<div class="spacer">&nbsp;</div>
<div class="poem">
<h2>
خویشاوندان
<a href="/SuggestNewPersonRelation/@Model.Person.Id" class="up-btn up-btn--ghost up-btn--sm">افزودن خویشاوند جدید</a>
</h2>
</div>
}
@if (Model.AffiliationRows.Count > 0)
{
<div class="spacer">&nbsp;</div>
<div class="poem">
<h2>وابستگی‌های غیرخویشاوندی</h2>
<ul>
@foreach (var row in Model.AffiliationRows)
{
<li>
@row.SentenceBeforeOtherName<a href="/person/@row.OtherPersonId">@row.OtherPersonName</a>@row.SentenceAfterOtherName
@if (!string.IsNullOrEmpty(row.Note))
{
<text> (@row.Note)</text>
}
</li>
}
</ul>
</div>
}
@if (Model.Poems != null && Model.Poems.Length > 0)
{
<div class="spacer">&nbsp;</div>
<div class="poem">
<h2>اشعار برچسب‌خورده با این شخصیت</h2>
<ul>
@foreach (var tag in Model.Poems)
{
if (tag.Poem != null)
{
<li><a href="@tag.Poem.FullUrl">@tag.Poem.FullTitle</a></li>
}
}
</ul>
</div>
}
else
{
<div class="spacer">&nbsp;</div>
<div class="poem">
<p><small>هنوز شعری با این شخصیت برچسب‌گذاری نشده است.</small></p>
</div>
}
<div class="spacer">&nbsp;</div>
</div>
<partial name="_FooterPartial" model=@(new _FooterPartialModel(){StickyEnabled = true}) />
</div>
@if (ViewData["TrackingScript"] != null && !string.IsNullOrEmpty(ViewData["TrackingScript"].ToString()))
{
@Html.Raw(ViewData["TrackingScript"])
}
}

View File

@ -0,0 +1,121 @@
@page "{id:int}"
@model GanjooRazor.Pages.PersonWindowModel
@using DNTPersianUtils.Core
@{
Layout = null;
}
@if (!string.IsNullOrEmpty(Model.LastError))
{
<p>@Model.LastError</p>
}
else
{
<div class="up-modal-person">
<h2>
@Model.Person.Name
@if (Model.LoggedIn)
{
<a href="/SuggestPersonEdit/@Model.Person.Id" class="up-icon-btn" title="پیشنهاد ویرایش این شخصیت" aria-label="پیشنهاد ویرایش این شخصیت">✎</a>
}
</h2>
@if (!string.IsNullOrEmpty(Model.Person.FamilyTreeCaption))
{
<p><strong>شجره‌نامه: @Model.Person.FamilyTreeCaption</strong></p>
}
@if (Model.Person.BirthYearInLHijri != 0 || Model.Person.DeathYearInLHijri != 0)
{
<p>
@if (Model.Person.BirthYearInLHijri != 0)
{
<text>تولد: @Model.Person.BirthYearInLHijri.ToPersianNumbers() (قمری) @(Model.Person.ValidBirthDate ? "" : "- تخمینی")</text>
}
@if (Model.Person.DeathYearInLHijri != 0)
{
<text> — وفات: @Model.Person.DeathYearInLHijri.ToPersianNumbers() (قمری) @(Model.Person.ValidDeathDate ? "" : "- تخمینی")</text>
}
</p>
}
@if (!string.IsNullOrEmpty(Model.Person.Description))
{
<p>@Model.Person.Description</p>
}
@if (!string.IsNullOrEmpty(Model.Person.WikiUrl))
{
<p><a href="@Model.Person.WikiUrl" target="_blank" rel="noopener">صفحهٔ ویکی‌پدیا</a></p>
}
@if (Model.RelationRows.Count > 0)
{
<h3>
خویشاوندان
<a href="/FamilyTree/@Model.Person.Id" class="up-btn up-btn--ghost up-btn--sm">نمایش نمودار شجره‌نامه</a>
@if (Model.LoggedIn)
{
<a href="/SuggestNewPersonRelation/@Model.Person.Id" class="up-btn up-btn--ghost up-btn--sm">افزودن خویشاوند جدید</a>
}
</h3>
<ul>
@foreach (var row in Model.RelationRows)
{
<li>
@row.Label — <a href="javascript:void(0)" onclick="PersonWindow.open(@row.OtherPersonId)">@row.OtherPersonName</a>
@if (!string.IsNullOrEmpty(row.Note))
{
<text> (@row.Note)</text>
}
@if (Model.LoggedIn)
{
<a href="/SuggestPersonRelationEdit/@row.RelationId" class="up-icon-btn" title="پیشنهاد ویرایش/حذف این نسبت" aria-label="پیشنهاد ویرایش/حذف این نسبت">✎</a>
}
</li>
}
</ul>
}
else if (Model.LoggedIn)
{
<h3>
خویشاوندان
<a href="/SuggestNewPersonRelation/@Model.Person.Id" class="up-btn up-btn--ghost up-btn--sm">افزودن خویشاوند جدید</a>
</h3>
}
@if (Model.AffiliationRows.Count > 0)
{
<h3>وابستگی‌های غیرخویشاوندی</h3>
<ul>
@foreach (var row in Model.AffiliationRows)
{
<li>
@row.SentenceBeforeOtherName<a href="javascript:void(0)" onclick="PersonWindow.open(@row.OtherPersonId)">@row.OtherPersonName</a>@row.SentenceAfterOtherName
@if (!string.IsNullOrEmpty(row.Note))
{
<text> (@row.Note)</text>
}
</li>
}
</ul>
}
@if (Model.Poems != null && Model.Poems.Length > 0)
{
<h3>اشعار برچسب‌خورده با این شخصیت</h3>
<ul>
@foreach (var tag in Model.Poems)
{
if (tag.Poem != null)
{
<li><a href="@tag.Poem.FullUrl">@tag.Poem.FullTitle</a></li>
}
}
</ul>
}
else
{
<p><small>هنوز شعری با این شخصیت برچسب‌گذاری نشده است.</small></p>
}
</div>
}

View File

@ -11,14 +11,17 @@ using RMuseum.Models.Ganjoor.ViewModels;
namespace GanjooRazor.Pages
{
/// <summary>
/// read-only person/family-tree browsing page - shows one GanjoorRelatedPerson, their kinship
/// and non-family ties (see GanjoorPersonRelation/GanjoorPersonAffiliation), and the poems
/// tagged with them. Public counterpart of the Editor.cshtml suggestion form and
/// ReviewEdits.cshtml moderation view for the same data, once it's approved and materialized.
/// one person's bio, kinship/affiliation ties and tagged poems, rendered as a bare fragment
/// (Layout = null - no header, footer or site chrome) and opened as an inline modal via
/// PersonWindow.open(id) in personwindow.js, from wherever a person is referenced: a poem's
/// tagged-persons list, the category/whole-site graph, the family-tree chart, the
/// suggestion/moderation forms. This used to be the standalone page Person.cshtml; it has no
/// entry point of its own any more; every "view this person" link on the site now calls
/// PersonWindow.open(id) instead of navigating here.
/// </summary>
public class PersonModel : LoginPartialEnabledPageModel
public class PersonWindowModel : LoginPartialEnabledPageModel
{
public PersonModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration)
public PersonWindowModel(HttpClient httpClient, IConfiguration configuration) : base(httpClient, configuration)
{
}
@ -127,8 +130,6 @@ namespace GanjooRazor.Pages
return Page();
}
ViewData["Title"] = $"گنجور » {Person.Name}";
var relationsResponse = await _httpClient.GetAsync($"{APIRoot.Url}/api/people/{id}/relations");
if (!relationsResponse.IsSuccessStatusCode)
{

View File

@ -39,6 +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 *@
<script src="~/js/peoplegraph.js" asp-append-version="true"></script>
<script src="~/js/personwindow.js" asp-append-version="true"></script>
<script>
function editBanner(bannerId) {
var url = '?handler=Edit';

View File

@ -37,6 +37,11 @@
<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 *@
<script src="~/js/peoplegraph.js" asp-append-version="true"></script>
<script src="~/js/personwindow.js" asp-append-version="true"></script>
<script>
var cookieValue = getCookie("playerScrollLock");
var playerScrollLock = cookieValue === "true";

View File

@ -29,7 +29,7 @@ else
<p class="up-hint">
این پیشنهاد مستقیماً اعمال نمی‌شود؛ پس از بررسی و تأیید توسط مدیران، به شجره‌نامهٔ
<a href="/person/@Model.Person.Id">@Model.Person.Name</a> افزوده خواهد شد.
<a href="javascript:void(0)" onclick="PersonWindow.open(@Model.Person.Id)">@Model.Person.Name</a> افزوده خواهد شد.
</p>
@if (!string.IsNullOrEmpty(Model.LastResult))
@ -84,7 +84,7 @@ else
</table>
<input type="submit" value="ثبت پیشنهاد" class="up-btn up-btn--success" />
<a role="button" href="/person/@Model.Person.Id" class="up-btn up-btn--ghost">انصراف</a>
<a role="button" href="javascript:void(0)" onclick="history.back()" class="up-btn up-btn--ghost">انصراف</a>
</form>
</div>

View File

@ -176,7 +176,7 @@ namespace GanjooRazor.Pages
return Page();
}
LastResult = $"پیشنهاد شما ثبت شد و پس از بررسی توسط مدیران اعمال خواهد شد. <a role=\"button\" href=\"/person/{personId}\" class=\"actionlink\">بازگشت به صفحهٔ شخصیت</a>";
LastResult = $"پیشنهاد شما ثبت شد و پس از بررسی توسط مدیران اعمال خواهد شد. <a role=\"button\" href=\"javascript:void(0)\" onclick=\"PersonWindow.open({personId})\" class=\"actionlink\">مشاهدهٔ اطلاعات این شخصیت</a>";
return Page();
}

View File

@ -29,7 +29,7 @@ else
<p class="up-hint">
این پیشنهاد مستقیماً اعمال نمی‌شود؛ پس از بررسی و تأیید توسط مدیران، در مشخصات
<a href="/person/@Model.Person.Id">@Model.Person.Name</a> اعمال خواهد شد.
<a href="javascript:void(0)" onclick="PersonWindow.open(@Model.Person.Id)">@Model.Person.Name</a> اعمال خواهد شد.
</p>
@if (!string.IsNullOrEmpty(Model.LastResult))
@ -98,7 +98,7 @@ else
<tr>
<td>
عنوان شجره‌نامه:
<p class="up-hint"><small>پُر کنید تا این شخصیت روی <a href="/people" target="_blank">صفحهٔ شجره‌نامه‌ها</a> با این عنوان فهرست شود.</small></p>
<p class="up-hint"><small>پُر کنید تا این شخصیت در <a href="javascript:void(0)" onclick="PeopleExplorer.open()">فهرست شجره‌نامه‌ها</a> با این عنوان دیده شود.</small></p>
</td>
<td><input asp-for="Suggestion.SuggestedFamilyTreeCaption" size="40" /></td>
</tr>
@ -113,7 +113,7 @@ else
</table>
<input type="submit" value="ثبت پیشنهاد" class="up-btn up-btn--success" />
<a role="button" href="/person/@Model.Person.Id" class="up-btn up-btn--ghost">انصراف</a>
<a role="button" href="javascript:void(0)" onclick="history.back()" class="up-btn up-btn--ghost">انصراف</a>
</form>
</div>

View File

@ -15,7 +15,7 @@ namespace GanjooRazor.Pages
/// contributor-facing form for suggesting an edit to an already-approved GanjoorRelatedPerson.
/// Any logged-in user may submit here (same bar as suggesting a poem correction); nothing is
/// changed until a moderator approves it via Admin/ReviewPersonEdits. Reachable both from a
/// person's own public page (Person.cshtml) and from the poem-correction moderation page
/// person's PersonWindow.open() modal and from the poem-correction moderation page
/// (ReviewEdits.cshtml, when a correction links to an existing person).
/// </summary>
public class SuggestPersonEditModel : LoginPartialEnabledPageModel
@ -145,7 +145,7 @@ namespace GanjooRazor.Pages
return Page();
}
LastResult = $"پیشنهاد ویرایش شما ثبت شد و پس از بررسی توسط مدیران اعمال خواهد شد. <a role=\"button\" href=\"/person/{id}\" class=\"actionlink\">بازگشت به صفحهٔ شخصیت</a>";
LastResult = $"پیشنهاد ویرایش شما ثبت شد و پس از بررسی توسط مدیران اعمال خواهد شد. <a role=\"button\" href=\"javascript:void(0)\" onclick=\"PersonWindow.open({id})\" class=\"actionlink\">مشاهدهٔ اطلاعات این شخصیت</a>";
FillSuggestionFromCurrentPerson();

View File

@ -28,9 +28,9 @@ else
<div class="poem">
<h1>
پیشنهاد ویرایش نسبت:
<a href="/person/@Model.Relation.Person1Id">@(Model.Relation.Person1 != null ? Model.Relation.Person1.Name : Model.Relation.Person1Id.ToString())</a>
<a href="javascript:void(0)" onclick="PersonWindow.open(@Model.Relation.Person1Id)">@(Model.Relation.Person1 != null ? Model.Relation.Person1.Name : Model.Relation.Person1Id.ToString())</a>
و
<a href="/person/@Model.Relation.Person2Id">@(Model.Relation.Person2 != null ? Model.Relation.Person2.Name : Model.Relation.Person2Id.ToString())</a>
<a href="javascript:void(0)" onclick="PersonWindow.open(@Model.Relation.Person2Id)">@(Model.Relation.Person2 != null ? Model.Relation.Person2.Name : Model.Relation.Person2Id.ToString())</a>
</h1>
<p class="up-hint">
@ -86,7 +86,7 @@ else
</table>
<input type="submit" value="ثبت پیشنهاد" class="up-btn up-btn--success" />
<a role="button" href="/person/@Model.Relation.Person1Id" class="up-btn up-btn--ghost">انصراف</a>
<a role="button" href="javascript:void(0)" onclick="history.back()" class="up-btn up-btn--ghost">انصراف</a>
</form>
</div>

View File

@ -14,7 +14,7 @@ namespace GanjooRazor.Pages
/// contributor-facing form for suggesting a change to, or removal of, an existing
/// GanjoorPersonRelation - the kinship-edge counterpart of SuggestPersonEdit.cshtml. Any
/// logged-in user may submit here; nothing is changed until a moderator approves it via
/// Admin/ReviewPersonRelationEdits. Reachable from a person's own public page (Person.cshtml).
/// Admin/ReviewPersonRelationEdits. Reachable from a person's PersonWindow.open() modal.
/// </summary>
public class SuggestPersonRelationEditModel : LoginPartialEnabledPageModel
{
@ -122,7 +122,7 @@ namespace GanjooRazor.Pages
return Page();
}
LastResult = $"پیشنهاد شما ثبت شد و پس از بررسی توسط مدیران اعمال خواهد شد. <a role=\"button\" href=\"/person/{Relation.Person1Id}\" class=\"actionlink\">بازگشت به صفحهٔ شخصیت</a>";
LastResult = $"پیشنهاد شما ثبت شد و پس از بررسی توسط مدیران اعمال خواهد شد. <a role=\"button\" href=\"javascript:void(0)\" onclick=\"PersonWindow.open({Relation.Person1Id})\" class=\"actionlink\">مشاهدهٔ اطلاعات این شخصیت</a>";
FillSuggestionFromCurrentRelation();

View File

@ -1694,7 +1694,7 @@ function loadCatRecitations(catId) {
// on-demand "شخصیت‌ها" tab on a poet/category page - loads the category-scoped person/relation
// graph (see _PersonGraphPartial.cshtml / OnGetPersonGraphAsync) and hands its JSON payload to the
// same peoplegraph.js force-directed renderer used by /PeopleGraph, with element ids prefixed
// same peoplegraph.js force-directed renderer used by the PeopleExplorer.open() modal, with element ids prefixed
// "pg-cat-" so it doesn't collide with anything else on the page
function loadPersonGraph(catId) {

View File

@ -278,7 +278,7 @@
if (p.description) {
html += '<p style="margin:4px 0"><small>' + escapeHtml(p.description) + '</small></p>';
}
html += '<p style="margin:8px 0 0 0"><a href="/person/' + p.id + '" target="_blank">مشاهدهٔ صفحهٔ شخصیت</a></p>';
html += '<p style="margin:8px 0 0 0"><a href="javascript:void(0)" onclick="PersonWindow.open(' + p.id + ')">مشاهدهٔ اطلاعات کامل</a></p>';
tooltip.innerHTML = html;
tooltip.style.display = 'block';
tooltip.querySelector('.ft-tooltip-close').addEventListener('click', function () {

View File

@ -11,12 +11,14 @@
// 4=Ally,5=Rival,6=Servant,7=Companion,8=Successor,99=Other). directlyTagged is false only on
// the category-scoped graph (GET api/ganjoor/cat/{id}/persongraph, the "characters in this work"
// tab): such a node was pulled in as a one-hop relative/affiliate of someone actually named in the
// work's verses, and is never false on the whole-site graph fed by PeopleGraph.cshtml.
// work's verses, and is never false on the whole-site graph fed by PeopleExplorer.cshtml.
//
// Called with an options object (see PeopleGraph.cshtml and _PersonGraphPartial.cshtml for two call
// sites with different element-id prefixes and data payloads), so the same renderer serves both the
// whole-site /PeopleGraph explorer and any number of category-scoped "شخصیت‌ها" tabs on cat/poet
// pages.
// Called with an options object (see PeopleExplorer.cshtml and _PersonGraphPartial.cshtml for two
// call sites with different element-id prefixes and data payloads), so the same renderer serves
// both the whole-site "explore all characters" modal (PeopleExplorer.open() in personwindow.js)
// and any number of category-scoped "شخصیت‌ها" tabs on cat/poet pages. A clicked node label or
// table-row name opens that person's own profile via PersonWindow.open(id) (personwindow.js),
// which is why this file assumes personwindow.js is also loaded on the page.
//
// Unlike FamilyTree.cshtml (a strict tree layout for one connected component), this lays out the
// WHOLE graph with a simple force simulation (mutual repulsion + spring edges + light centering,
@ -218,9 +220,17 @@
});
var text = el('text', {
'text-anchor': 'middle', dy: -(nodeRadius(n) + 6),
'class': n.directlyTagged ? 'pg-label' : 'pg-label pg-label-secondary', direction: 'rtl'
'class': n.directlyTagged ? 'pg-label' : 'pg-label pg-label-secondary', direction: 'rtl',
style: 'cursor:pointer'
});
text.textContent = n.name;
// the label opens the person's profile window; the circle (handled below, on the
// whole <g>) keeps its own click behaviour of focusing this node's ego-network, so the
// label needs its own listener with stopPropagation to not also trigger that
text.addEventListener('click', function (evt) {
evt.stopPropagation();
if (window.PersonWindow) window.PersonWindow.open(n.id);
});
g.appendChild(circle);
g.appendChild(text);
nodesGroup.appendChild(g);
@ -263,11 +273,19 @@
tr.setAttribute('data-edge-index', idx);
tr.style.cursor = 'pointer';
tr.innerHTML =
'<td>' + escapeHtml(e.raw.person1Name) + '</td>' +
'<td><a href="javascript:void(0)" class="pg-person-link" data-person-id="' + e.raw.person1Id + '">' + escapeHtml(e.raw.person1Name) + '</a></td>' +
'<td><span class="pg-legend-swatch" style="background:' + e.meta.color + '"></span> ' + escapeHtml(e.meta.label) + '</td>' +
'<td>' + escapeHtml(e.raw.person2Name) + '</td>' +
'<td><a href="javascript:void(0)" class="pg-person-link" data-person-id="' + e.raw.person2Id + '">' + escapeHtml(e.raw.person2Name) + '</a></td>' +
'<td><small>' + escapeHtml(e.note || '') + '</small></td>';
tr.addEventListener('click', function () { focusOnPersons([e.raw.person1Id, e.raw.person2Id]); });
// the name links open the person's profile window without also triggering the
// row's own focus-on-click behaviour above
tr.querySelectorAll('.pg-person-link').forEach(function (link) {
link.addEventListener('click', function (evt) {
evt.stopPropagation();
if (window.PersonWindow) window.PersonWindow.open(Number(link.getAttribute('data-person-id')));
});
});
tableBody.appendChild(tr);
tableRows.push(tr);
});

View File

@ -0,0 +1,122 @@
// 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:
//
// PersonWindow.open(id) - a small modal: one person's bio/relations/affiliations/poems,
// fetched from /PersonWindow/{id} (Person.cshtml's old content,
// stripped of site chrome - see PersonWindow.cshtml).
// 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.
//
// Both render into the same reusable overlay/modal shell built here on first use.
(function () {
'use strict';
var STYLE_ID = 'up-modal-styles';
var overlay, modal, body, closeBtn;
function ensureStyles() {
if (document.getElementById(STYLE_ID)) return;
var style = document.createElement('style');
style.id = STYLE_ID;
style.textContent =
'.up-modal-overlay{position:fixed;inset:0;background:rgba(30,20,10,.55);z-index:9000;' +
'display:none;align-items:flex-start;justify-content:center;overflow-y:auto;padding:5vh 16px;}' +
'.up-modal-overlay--open{display:flex;}' +
'.up-modal{position:relative;background:#fffdf7;border-radius:10px;box-shadow:0 8px 30px rgba(0,0,0,.35);' +
'padding:20px 24px;max-width:640px;width:100%;max-height:90vh;overflow-y:auto;direction:rtl;text-align:right;}' +
'.up-modal--lg{max-width:900px;}' +
'.up-modal-close{position:absolute;left:12px;top:10px;border:none;background:none;font-size:22px;' +
'line-height:1;cursor:pointer;color:#7a4a2a;}' +
'.up-modal-loading{text-align:center;padding:30px 0;}' +
'body.up-modal-noscroll{overflow:hidden;}';
document.head.appendChild(style);
}
function ensureShell() {
if (overlay) return;
ensureStyles();
overlay = document.createElement('div');
overlay.id = 'up-modal-overlay';
overlay.className = 'up-modal-overlay';
overlay.innerHTML =
'<div class="up-modal" id="up-modal">' +
'<button type="button" class="up-modal-close" id="up-modal-close" aria-label="بستن">×</button>' +
'<div class="up-modal-body" id="up-modal-body"></div>' +
'</div>';
document.body.appendChild(overlay);
modal = document.getElementById('up-modal');
body = document.getElementById('up-modal-body');
closeBtn = document.getElementById('up-modal-close');
closeBtn.addEventListener('click', closeModal);
overlay.addEventListener('click', function (evt) {
if (evt.target === overlay) closeModal();
});
document.addEventListener('keydown', function (evt) {
if (evt.key === 'Escape' && overlay.classList.contains('up-modal-overlay--open')) {
closeModal();
}
});
}
function openModal(large) {
ensureShell();
modal.classList.toggle('up-modal--lg', !!large);
body.innerHTML = '<div class="up-modal-loading"><img src="/image/loading.gif" alt="بارگذاری" /></div>';
overlay.classList.add('up-modal-overlay--open');
document.body.classList.add('up-modal-noscroll');
}
function closeModal() {
if (!overlay) return;
overlay.classList.remove('up-modal-overlay--open');
document.body.classList.remove('up-modal-noscroll');
}
function loadInto(url, onReady) {
$.ajax({
type: 'GET',
url: url,
error: function () {
if (body) body.innerHTML = '<p>خطا در بارگذاری اطلاعات.</p>';
},
success: function (data) {
body.innerHTML = data;
if (onReady) onReady();
}
});
}
function openPerson(id) {
openModal(false);
loadInto('/PersonWindow/' + String(id));
}
function openExplorer() {
openModal(true);
loadInto('/PeopleExplorer', function () {
var dataEl = document.getElementById('pge-data');
if (dataEl && window.GanjoorPeopleGraph) {
var graphData = JSON.parse(dataEl.textContent);
GanjoorPeopleGraph.render({
containerId: 'pge-container',
svgId: 'pge-svg',
tableBodyId: 'pge-table-body',
legendId: 'pge-legend',
searchInputId: 'pge-search',
resetButtonId: 'pge-reset',
data: graphData
});
}
});
}
window.PersonWindow = { open: openPerson, close: closeModal };
window.PeopleExplorer = { open: openExplorer, close: closeModal };
})();

View File

@ -62,7 +62,8 @@ namespace RMuseum.Controllers
/// <summary>
/// get the whole known network of people (every person with at least one kinship edge or
/// non-family tie, plus every one of those edges/ties), for the force-directed "ontology"
/// explorer at /PeopleGraph. Registered before the "{id}" route below (and constrained to
/// explorer opened via the PeopleExplorer.open() modal (formerly the standalone page
/// /PeopleGraph). Registered before the "{id}" route below (and constrained to
/// int there) so this literal segment isn't swallowed as an id.
/// </summary>
/// <returns></returns>

View File

@ -5,7 +5,8 @@ namespace RMuseum.Models.Ganjoor.ViewModels
/// <summary>
/// the whole known network of people - every GanjoorRelatedPerson that has at least one kinship
/// edge or non-family tie, plus every one of those edges/ties - served by GET api/people/graph
/// for the force-directed "ontology" explorer at /PeopleGraph, the nowruzgan-style counterpart of
/// for the force-directed "ontology" explorer opened via the PeopleExplorer.open() modal
/// (formerly the standalone page /PeopleGraph), the nowruzgan-style counterpart of
/// the strict-tree /FamilyTree/{id} view. Loaded whole (like GetFamilyTreeAsync does for a single
/// component) since the graph as a whole is small - a few hundred rows at most.
/// </summary>

View File

@ -2859,7 +2859,8 @@
<summary>
get the whole known network of people (every person with at least one kinship edge or
non-family tie, plus every one of those edges/ties), for the force-directed "ontology"
explorer at /PeopleGraph. Registered before the "{id}" route below (and constrained to
explorer opened via the PeopleExplorer.open() modal (formerly the standalone page
/PeopleGraph). Registered before the "{id}" route below (and constrained to
int there) so this literal segment isn't swallowed as an id.
</summary>
<returns></returns>
@ -13340,7 +13341,8 @@
<summary>
the whole known network of people - every GanjoorRelatedPerson that has at least one kinship
edge or non-family tie, plus every one of those edges/ties - served by GET api/people/graph
for the force-directed "ontology" explorer at /PeopleGraph, the nowruzgan-style counterpart of
for the force-directed "ontology" explorer opened via the PeopleExplorer.open() modal
(formerly the standalone page /PeopleGraph), the nowruzgan-style counterpart of
the strict-tree /FamilyTree/{id} view. Loaded whole (like GetFamilyTreeAsync does for a single
component) since the graph as a whole is small - a few hundred rows at most.
</summary>
@ -18407,7 +18409,8 @@
<summary>
get the whole known network of people (every person with at least one kinship edge or
non-family tie, plus every one of those edges/ties) for the force-directed "ontology"
explorer at /PeopleGraph - see GanjoorPersonGraphViewModel
explorer opened via the PeopleExplorer.open() modal (formerly the standalone page
/PeopleGraph) - see GanjoorPersonGraphViewModel
</summary>
<returns></returns>
</member>

View File

@ -142,7 +142,8 @@ namespace RMuseum.Services
/// <summary>
/// get the whole known network of people (every person with at least one kinship edge or
/// non-family tie, plus every one of those edges/ties) for the force-directed "ontology"
/// explorer at /PeopleGraph - see GanjoorPersonGraphViewModel
/// explorer opened via the PeopleExplorer.open() modal (formerly the standalone page
/// /PeopleGraph) - see GanjoorPersonGraphViewModel
/// </summary>
/// <returns></returns>
Task<RServiceResult<GanjoorPersonGraphViewModel>> GetPersonGraphAsync();