Files
TafsirQuran/src/TafsirQuran.UI/Pages/TafsirPage.razor
T
admin 3ca743ca12
build-and-deploy / deploy (push) Successful in 4m45s
Polish ayah UX with fixed player dock and progressive skeletons.
Compact header, sidebar surah list, closed notes, resume CTA, and larger ayah typography.
2026-10-10 02:15:35 +03:30

294 lines
10 KiB
Plaintext

@page "/tafsir/{Slug}"
@page "/tafsir/{Slug}/surah/{SurahNumber:int}"
@page "/tafsir/{Slug}/surah/{SurahNumber:int}/ayah/{AyahNumber:int}"
@inject TafsirApiClient Api
@inject IDeviceIdStore DeviceStore
@inject NavigationManager Nav
@inject PlayerSessionService Player
<section class="tafsir-page">
<header class="page-header page-header-row">
@if (loadingMeta)
{
<SkeletonBlock Variant="lines" Lines="1" />
}
else
{
<h1 class="page-title-inline">
<span>@title</span>
@if (!string.IsNullOrWhiteSpace(author))
{
<span class="title-author">(@author)</span>
}
</h1>
<button type="button" class="back-btn" @onclick="GoBack" aria-label="بازگشت">←</button>
}
</header>
@if (view == View.Surahs)
{
@if (loadingSurahs)
{
<SkeletonBlock Variant="cards" Lines="8" />
}
else
{
<div class="surah-grid">
@foreach (var s in surahs)
{
<div class="surah-card">
<button type="button" class="surah-main" @onclick="() => OpenSurah(s.Number)">
<span class="surah-num">@s.Number</span>
<span class="surah-names">
<strong>@s.NameFa</strong>
<span class="muted">@s.NameAr</span>
</span>
<span class="muted">@s.AyahCount آیه</span>
</button>
<button type="button" class="chip @(s.IsBookmarked ? "on" : null)"
@onclick="() => ToggleSurahBookmark(s)">نشان</button>
</div>
}
</div>
}
}
else if (view == View.Ayahs)
{
<h2 class="section-title">سوره @(selectedSurah?.NameFa ?? "…")</h2>
@if (loadingAyahs)
{
<SkeletonBlock Variant="cards" Lines="10" />
}
else
{
<div class="ayah-list">
@foreach (var a in ayahs)
{
<button type="button" class="ayah-row" @onclick="() => OpenAyah(a.AyahNumber)">
<span class="ayah-num">@a.AyahNumber</span>
<span class="ayah-preview">@(a.TextFaPreview ?? "آیه " + a.AyahNumber)</span>
<span class="ayah-flags">
@if (a.IsFavorite) { <span class="dot fav" title="علاقه"></span> }
@if (a.IsBookmarked) { <span class="dot mark" title="نشان"></span> }
@if (a.HasAudio) { <span class="dot audio" title="صوت"></span> }
</span>
</button>
}
</div>
}
}
else if (view == View.Detail)
{
<div class="ayah-detail">
<div class="ayah-nav">
<button type="button" class="chip" disabled="@(detail?.PrevAyah is null)"
@onclick="() => GoPrevAyah()">آیه قبل</button>
<button type="button" class="chip" @onclick="() => OpenSurah(SurahNumber ?? 1)">فهرست آیات</button>
<button type="button" class="chip" disabled="@(detail?.NextAyah is null)"
@onclick="() => GoNextAyah()">آیه بعد</button>
</div>
@if (loadingDetail || detail is null)
{
<SkeletonBlock Variant="lines" Lines="5" />
<SkeletonBlock Variant="cards" Lines="2" />
}
else
{
<div class="ayah-block">
<p class="ayah-ar">@detail.TextAr</p>
@if (!string.IsNullOrWhiteSpace(detail.TextFa))
{
<p class="ayah-fa">@detail.TextFa</p>
}
</div>
@if (!string.IsNullOrWhiteSpace(detail.NotesHtml))
{
<details class="content-block">
<summary>نکته‌ها</summary>
<div class="html-body">@((MarkupString)detail.NotesHtml)</div>
</details>
}
@if (!string.IsNullOrWhiteSpace(detail.MessagesHtml))
{
<details class="content-block">
<summary>پیام‌ها</summary>
<div class="html-body">@((MarkupString)detail.MessagesHtml)</div>
</details>
}
@if (!string.IsNullOrWhiteSpace(detail.VideoEmbedUrl))
{
<details class="content-block">
<summary>فیلم</summary>
<div class="video-wrap">
<iframe src="@detail.VideoEmbedUrl" allowfullscreen loading="lazy"></iframe>
</div>
</details>
}
}
</div>
}
</section>
@code {
private enum View { Surahs, Ayahs, Detail }
[Parameter] public string Slug { get; set; } = "";
[Parameter] public int? SurahNumber { get; set; }
[Parameter] public int? AyahNumber { get; set; }
private string title = "تفسیر";
private string author = "";
private Guid tafsirId;
private bool loadingMeta = true;
private bool loadingSurahs;
private bool loadingAyahs;
private bool loadingDetail;
private string? routeKey;
private View view = View.Surahs;
private List<SurahListItemDto> surahs = [];
private List<AyahListItemDto> ayahs = [];
private SurahListItemDto? selectedSurah;
private AyahDetailDto? detail;
private string deviceId = "";
private bool ready;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
ready = true;
deviceId = await DeviceStore.GetOrCreateAsync();
await LoadRouteAsync();
}
protected override async Task OnParametersSetAsync()
{
if (!ready) return;
await LoadRouteAsync();
}
private async Task LoadRouteAsync()
{
var key = $"{Slug}|{SurahNumber}|{AyahNumber}";
if (routeKey == key) return;
routeKey = key;
if (AyahNumber is not null && SurahNumber is not null) view = View.Detail;
else if (SurahNumber is not null) view = View.Ayahs;
else view = View.Surahs;
loadingMeta = true;
loadingSurahs = view == View.Surahs;
loadingAyahs = view == View.Ayahs;
loadingDetail = view == View.Detail;
detail = null;
await InvokeAsync(StateHasChanged);
try
{
if (string.IsNullOrWhiteSpace(deviceId))
deviceId = await DeviceStore.GetOrCreateAsync();
var tafsirs = await Api.GetTafsirsAsync();
var tafsir = tafsirs.FirstOrDefault(t => t.Slug == Slug);
title = tafsir?.Title ?? "تفسیر";
author = tafsir?.Author ?? "";
tafsirId = tafsir?.Id ?? Guid.Empty;
loadingMeta = false;
await InvokeAsync(StateHasChanged);
if (view == View.Detail && SurahNumber is int s && AyahNumber is int a)
await LoadDetail(s, a);
else if (view == View.Ayahs && SurahNumber is int sn)
await LoadAyahs(sn);
else
await LoadSurahs();
}
finally
{
loadingMeta = false;
loadingSurahs = false;
loadingAyahs = false;
loadingDetail = false;
await InvokeAsync(StateHasChanged);
}
}
private void GoBack()
{
if (view == View.Detail && SurahNumber is int s)
Nav.NavigateTo($"/tafsir/{Slug}/surah/{s}");
else if (view == View.Ayahs)
Nav.NavigateTo($"/tafsir/{Slug}");
else
Nav.NavigateTo("/");
}
private void OpenSurah(int n) => Nav.NavigateTo($"/tafsir/{Slug}/surah/{n}");
private void OpenAyah(int n)
{
var s = SurahNumber ?? detail?.SurahNumber ?? selectedSurah?.Number ?? 1;
Nav.NavigateTo($"/tafsir/{Slug}/surah/{s}/ayah/{n}");
}
private void GoPrevAyah()
{
if (detail?.PrevAyah is int p) OpenAyah(p);
}
private void GoNextAyah()
{
if (detail?.NextAyah is int n) OpenAyah(n);
}
private async Task LoadSurahs()
{
loadingSurahs = true;
await InvokeAsync(StateHasChanged);
surahs = (await Api.GetSurahsAsync(deviceId, tafsirId == Guid.Empty ? null : tafsirId)).ToList();
loadingSurahs = false;
}
private async Task LoadAyahs(int surahNumber)
{
loadingAyahs = true;
await InvokeAsync(StateHasChanged);
if (surahs.Count == 0)
surahs = (await Api.GetSurahsAsync(deviceId, tafsirId == Guid.Empty ? null : tafsirId)).ToList();
selectedSurah = surahs.FirstOrDefault(x => x.Number == surahNumber);
ayahs = (await Api.GetAyahsAsync(Slug, surahNumber, deviceId)).ToList();
loadingAyahs = false;
}
private async Task LoadDetail(int surahNumber, int ayahNumber)
{
loadingDetail = true;
await InvokeAsync(StateHasChanged);
detail = await Api.GetAyahAsync(Slug, surahNumber, ayahNumber);
loadingDetail = false;
await InvokeAsync(StateHasChanged);
if (detail is null) return;
tafsirId = detail.TafsirId;
var resumePos = Player.HasTrack
&& Player.SurahNumber == surahNumber
&& Player.AyahNumber == ayahNumber
? Player.PositionSeconds
: 0;
await Player.SetTrackAsync(detail, resumePos);
}
private async Task ToggleSurahBookmark(SurahListItemDto s)
{
if (tafsirId == Guid.Empty) return;
if (s.IsBookmarked)
await Api.RemoveSurahBookmarkAsync(tafsirId, s.Number, deviceId);
else
await Api.AddSurahBookmarkAsync(new ToggleSurahBookmarkRequest(tafsirId, s.Number, deviceId));
surahs = (await Api.GetSurahsAsync(deviceId, tafsirId)).ToList();
}
}