tajik site changes

This commit is contained in:
Hamid Reza Mohammadi 2026-08-16 17:31:37 +03:30
parent 978c6f6e0e
commit 133b65606c
5 changed files with 255 additions and 14 deletions

View File

@ -57,18 +57,60 @@
else
@if (Model.IsHomePage)
{
<div class="tg-poet-grid">
@foreach (var poet in Model.Poets)
{
<a class="tg-poet-card" href="@poet.FullUrl" title="@poet.Name">
<span class="tg-poet-portrait">
<img src="/images/poets/@(poet.Id).png" alt="@poet.Nickname" loading="lazy"
onerror="this.onerror=null;this.src='/images/poets/999.png';" />
</span>
<span class="tg-poet-name">@poet.Nickname</span>
</a>
}
var popularGroup = Model.PoetGroups.SingleOrDefault(g => g.Id == 0);
var centuryGroups = Model.PoetGroups.Where(g => g.Id != 0).ToList();
@if (popularGroup != null)
{
<div class="tg-century-heading">Шоирони маъмул</div>
<div class="tg-poet-grid">
@foreach (var poet in popularGroup.Poets)
{
<a class="tg-poet-card" href="@poet.FullUrl" title="@poet.Name" data-value="@poet.Nickname">
<span class="tg-poet-portrait">
<img src="/images/poets/@(poet.Id).png" alt="@poet.Nickname" loading="lazy"
onerror="this.onerror=null;this.src='/images/poets/999.png';" />
</span>
<span class="tg-poet-name">@poet.Nickname</span>
</a>
}
</div>
}
<div class="tg-poet-search">
<input type="text" id="tg-findpoet" placeholder="Ҷустуҷӯи шоир" oninput="tgInlineSearch(this.value)" />
<div class="tg-poet-grid" id="tg-found-poets"></div>
</div>
@if (centuryGroups.Count > 0)
{
<div class="tg-century-heading">Гурӯҳбандӣ бар асоси аср</div>
<div class="tg-century-nav">
@foreach (var g in centuryGroups)
{
<a href="#tg-pg-@g.Id" class="circled-number">@(g.HalfCenturyOrder + 2)</a>
}
</div>
@foreach (var century in centuryGroups)
{
@if (century.ShowInTimeLine)
{
<div class="tg-century-heading" id="tg-pg-@century.Id">@century.Name</div>
}
<div class="tg-poet-grid">
@foreach (var poet in century.Poets)
{
<a class="tg-poet-card" href="@poet.FullUrl" title="@poet.Name" data-value="@poet.Nickname">
<span class="tg-poet-portrait">
<img src="/images/poets/@(poet.Id).png" alt="@poet.Nickname" loading="lazy"
onerror="this.onerror=null;this.src='/images/poets/999.png';" />
</span>
<span class="tg-poet-name">@poet.Nickname</span>
</a>
}
</div>
}
}
}
else
{

View File

@ -8,6 +8,7 @@ using RMuseum.Models.Ganjoor;
using System.Net;
using System.Text.RegularExpressions;
using RSecurityBackend.Models.Generic;
using RMuseum.Services.Implementation;
@ -32,6 +33,11 @@ namespace TajikGanjoor.Pages
public string PagingToolsHtml { get; set; }
public List<GanjoorPoemCompleteViewModel> Poems { get; set; }
/// <summary>
/// poets grouped by century, shown on the home page - mirrors GanjooRazor's Index.cshtml.cs
/// </summary>
public List<GanjoorCenturyViewModel> PoetGroups { get; set; }
public async Task<IActionResult> OnGetAsync()
{
if (bool.Parse(Configuration["MaintenanceMode"] ?? false.ToString()))
@ -46,6 +52,13 @@ namespace TajikGanjoor.Pages
}
IsSearchPage = !string.IsNullOrEmpty(Request.Query["s"]);
IsHomePage = !IsSearchPage && Request.Path == "/";
if (IsHomePage)
{
if (false == await PreparePoetGroupsAsync())
{
return Page();
}
}
int pageNumber = 1;
if (IsSearchPage)
{
@ -314,6 +327,77 @@ namespace TajikGanjoor.Pages
Poets = poets ?? [];
return true;
}
/// <summary>
/// fetches the same /api/ganjoor/centuries grouping GanjooRazor's homepage uses, then
/// overlays Tajik nickname/description onto each group's poets the same way
/// PreparePoetsAsync does, dropping poets with no Tajik translation and dropping any
/// century that ends up empty as a result - century Name itself has no dedicated Tajik
/// translation source anywhere, so it falls back to LanguageUtils.TransliteratePersianToTajik
/// (a handful of these are short/fixed enough that hand-correcting the wording later,
/// directly in this list, would be easy if the transliteration reads awkwardly)
/// </summary>
private async Task<bool> PreparePoetGroupsAsync()
{
var cacheKey = $"/api/ganjoor/tajik/centuries";
if (!_memoryCache.TryGetValue(cacheKey, out List<GanjoorCenturyViewModel>? poetGroups))
{
try
{
var res1 = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/centuries");
if (!res1.IsSuccessStatusCode)
{
LastError = JsonConvert.DeserializeObject<string>(await res1.Content.ReadAsStringAsync()) ?? "!res1.IsSuccessStatusCode";
return false;
}
var centuries = JArray.Parse(await res1.Content.ReadAsStringAsync()).ToObject<List<GanjoorCenturyViewModel>>();
centuries = centuries ?? [];
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/tajik/poets");
if (!response.IsSuccessStatusCode)
{
LastError = JsonConvert.DeserializeObject<string>(await response.Content.ReadAsStringAsync()) ?? "!response.IsSuccessStatusCode";
return false;
}
var tajikPoets = JArray.Parse(await response.Content.ReadAsStringAsync()).ToObject<List<GanjoorTajikPoet>>() ?? [];
poetGroups = new List<GanjoorCenturyViewModel>();
foreach (var century in centuries)
{
var translatedPoets = new List<GanjoorPoetViewModel>();
foreach (var poet in century.Poets)
{
var tajikPoet = tajikPoets.SingleOrDefault(p => p.Id == poet.Id);
if (tajikPoet != null)
{
poet.Nickname = tajikPoet.TajikNickname;
poet.Description = tajikPoet.TajikDescription;
translatedPoets.Add(poet);
}
}
if (translatedPoets.Count > 0)
{
century.Poets = translatedPoets;
century.Name = LanguageUtils.TransliteratePersianToTajik(century.Name);
poetGroups.Add(century);
}
}
if (AggressiveCacheEnabled)
{
_memoryCache.Set(cacheKey, poetGroups, TimeSpan.FromHours(1));
}
}
catch (Exception e)
{
LastError = e.ToString();
return false;
}
}
PoetGroups = poetGroups ?? [];
return true;
}
private void PrepareNextPre()
{
if (GanjoorPage == null) return;

View File

@ -69,6 +69,26 @@
src: url('/fonts/ptsans/pt-sans-cyrillic-ext-700-italic.woff2') format('woff2');
}
/* Persian original text (interlinear comparison lines) - Cyrillic fonts above have no
Arabic-script glyphs at all, so this needs its own font. Vazirmatn is the same font
the main ganjoor.net site self-hosts for Persian, reused here for a consistent,
recognizable feel across the two sites. */
@font-face {
font-family: 'Vazirmatn';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/vazirmatn/vazirmatn-arabic-400-normal.woff2') format('woff2');
}
@font-face {
font-family: 'Vazirmatn';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/vazirmatn/vazirmatn-arabic-700-normal.woff2') format('woff2');
}
/* ---------- base page: warm paper tone, PT Sans chrome ---------- */
:root {
@ -292,6 +312,27 @@ html, body {
margin: 0;
}
/* Persian original, shown interlinear directly beneath its Tajik line (per
verse.OriginalText - see PrepareBilingualHtmlText). Deliberately smaller
and quieter than the Tajik line above it: this is a Tajik-first site, the
Persian is a reference/comparison aid, not the primary reading text.
Selectors repeat the base verse rules' specificity + the .tg-fa class so
this reliably wins regardless of source order. */
.b .m1 p.tg-fa,
.b .m2 p.tg-fa,
.b2 p.tg-fa,
.n p.tg-fa,
.l p.tg-fa,
.c p.tg-fa {
font-family: 'Vazirmatn', Tahoma, sans-serif;
font-size: 0.82em;
line-height: 1.7;
color: var(--tg-ink-soft);
opacity: 0.85;
margin: 0.2em 0 0 0;
unicode-bidi: isolate;
}
/* poem title */
.text-center h1, .text-center h2 {
font-family: 'PT Serif', serif;
@ -581,6 +622,7 @@ html, body {
border-radius: 50%;
font-family: 'PT Sans', sans-serif;
font-size: 0.9rem;
text-decoration: none;
}
.circled-number {
@ -598,6 +640,59 @@ html, body {
color: #fff;
}
/* ---------- homepage: century grouping + inline poet search ---------- */
.tg-century-heading {
max-width: 980px;
margin: 28px auto 4px auto;
padding: 0 8px;
font-family: 'PT Serif', serif;
font-weight: 700;
font-size: 1.05rem;
color: var(--tg-accent-deep);
text-align: start;
border-bottom: 1px solid var(--tg-rule);
padding-bottom: 6px;
}
.tg-century-nav {
max-width: 980px;
margin: 8px auto 0 auto;
padding: 0 8px;
text-align: center;
}
.tg-poet-search {
max-width: 480px;
margin: 20px auto 0 auto;
padding: 0 8px;
}
.tg-poet-search input {
width: 100%;
padding: 8px 12px;
font-family: 'PT Sans', sans-serif;
font-size: 0.95rem;
border: 1px solid var(--tg-rule);
border-radius: 6px;
background: var(--tg-surface);
color: var(--tg-ink);
}
.tg-poet-search input:focus {
outline: none;
border-color: var(--tg-accent);
}
/* search results grid only takes up space once populated by tgInlineSearch (site.js) */
.tg-poet-search .tg-poet-grid:empty {
margin: 0;
}
.tg-poet-search .tg-poet-grid {
margin-top: 14px;
}
/* ---------- error page ---------- */
.text-danger {

View File

@ -7,14 +7,15 @@
does the highlighting.
Verse elements are counted in document order across .m1, .m2 (the two
halves of a couplet), .b2 > p (rubaʼi-style already-centered couplets),
halves of a couplet), .b2 > p (rubaʼi-style already-centered couplets,
excluding the interlinear Persian .tg-fa line added alongside each one),
.n and .l (paragraph/single-line verses) - same shape PrepareHtmlText
produces for both the Persian and Tajik sites, so the sync XML's
VerseOrder values line up the same way. */
(function () {
function getVerseElements() {
return Array.prototype.slice.call(
document.querySelectorAll('.m1, .m2, .n, .l, .b2 > p')
document.querySelectorAll('.m1, .m2, .n, .l, .b2 > p:not(.tg-fa)')
);
}

View File

@ -1,4 +1,23 @@
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code.
// Homepage inline poet search - simplified port of onInlineSearch (GanjooRazor's wwwroot/js/bk.js):
// substring-matches the typed text against each poet card's data-value (Cyrillic nickname) and
// clones the matches into the results container. No numeral conversion needed here (unlike the
// Persian site, which converts typed digits to Persian-Indic numerals first) since Tajik Cyrillic
// uses plain Western digits.
function tgInlineSearch(value) {
var resultsNode = document.getElementById('tg-found-poets');
if (!resultsNode) return;
resultsNode.innerHTML = '';
if (value.length === 0) return;
var needle = value.toLowerCase();
var cards = document.querySelectorAll('.tg-poet-card[data-value]');
for (var i = 0; i < cards.length; i++) {
var haystack = cards[i].getAttribute('data-value').toLowerCase();
if (haystack.indexOf(needle) !== -1) {
resultsNode.appendChild(cards[i].cloneNode(true));
}
}
}