parent
b8c45ed93c
commit
a1c500daee
@ -196,7 +196,16 @@ else
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="century" id="books-shelf-caption">
|
||||
مرور کتابها
|
||||
</div>
|
||||
<div id="books-shelf-outer">
|
||||
<button type="button" class="books-shelf-handle books-shelf-handle-right" aria-label="نمایش کتابهای بیشتر در سمت راست" onclick="scrollBooksShelf(220)">‹</button>
|
||||
<div id="books-shelf-wrap">
|
||||
<div id="books-shelf" aria-label="مرور کتابها"></div>
|
||||
</div>
|
||||
<button type="button" class="books-shelf-handle books-shelf-handle-left" aria-label="نمایش کتابهای بیشتر در سمت چپ" onclick="scrollBooksShelf(-220)">›</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -211,3 +220,9 @@ else
|
||||
{
|
||||
@Html.Raw(ViewData["TrackingScript"])
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
initHomeBooksShelf();
|
||||
</script>
|
||||
}
|
||||
|
||||
@ -115,6 +115,24 @@ namespace GanjooRazor.Pages
|
||||
return Redirect(pageUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON endpoint for the home page's "مرور کتابها" horizontal book shelf (see bk.js's
|
||||
/// initHomeBooksShelf()). Fetched client-side via "?Handler=BookCatalog" rather than hitting
|
||||
/// the api/ganjoor/book-catalog API route directly from browser JS, same as every other
|
||||
/// client-side data fetch on this site - the browser only ever talks to this page's own
|
||||
/// handlers, which proxy to the API server-side.
|
||||
/// </summary>
|
||||
public async Task<IActionResult> OnGetBookCatalogAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"{APIRoot.Url}/api/ganjoor/book-catalog");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return new BadRequestObjectResult(await ReadErrorMessageAsync(response));
|
||||
}
|
||||
var books = JsonConvert.DeserializeObject<GanjoorBookViewModel[]>(await response.Content.ReadAsStringAsync());
|
||||
return new OkObjectResult(books);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnGetAsync()
|
||||
{
|
||||
var maintenanceResult = TryGetMaintenanceModeResult();
|
||||
|
||||
@ -342,6 +342,180 @@ select {
|
||||
color: var(--ink-black);
|
||||
}
|
||||
|
||||
/* Home page "مرور کتابها" book shelf - see initHomeBooksShelf()/buildBookSpine() in bk.js. */
|
||||
#books-shelf-outer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#books-shelf-wrap {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
padding: 10px 34px 26px;
|
||||
}
|
||||
|
||||
#books-shelf-wrap::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.books-shelf-handle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 2;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--accent-border);
|
||||
background-color: var(--card-paper);
|
||||
color: var(--ink-black);
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.books-shelf-handle:hover {
|
||||
background-color: var(--brand);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.books-shelf-handle-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.books-shelf-handle-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#books-shelf {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 14px;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.book-spine {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 56px;
|
||||
height: 230px;
|
||||
border-radius: 4px 4px 3px 3px;
|
||||
cursor: pointer;
|
||||
box-shadow: 2px 3px 8px rgba(0, 0, 0, 0.28);
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease, width 0.3s ease, height 0.3s ease;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.book-spine:hover,
|
||||
.book-spine:focus-visible {
|
||||
transform: translateY(-12px);
|
||||
box-shadow: 4px 12px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.book-spine.expanded {
|
||||
width: 170px;
|
||||
height: 262px;
|
||||
}
|
||||
|
||||
.book-spine.expanded:hover {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
|
||||
.spine-compact {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.book-spine.expanded .spine-compact {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.spine-title,
|
||||
.spine-poet {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform-origin: center center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
color: #fdf8ee;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spine-title {
|
||||
top: 95px;
|
||||
width: 150px;
|
||||
transform: translate(-50%, -50%) rotate(-90deg);
|
||||
font-family: 'IranNastaliq', serif;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.spine-poet {
|
||||
top: 200px;
|
||||
width: 50px;
|
||||
transform: translate(-50%, -50%) rotate(-90deg);
|
||||
font-size: 0.68rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.spine-cover {
|
||||
display: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 18px 12px;
|
||||
text-align: center;
|
||||
color: #fdf8ee;
|
||||
}
|
||||
|
||||
.book-spine.expanded .spine-cover {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.spine-cover-title {
|
||||
font-family: 'IranNastaliq', serif;
|
||||
font-size: 1.7rem;
|
||||
line-height: 1.5;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.spine-cover-poet {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.spine-cover-close {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
inset-inline-end: 6px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: #fdf8ee;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spine-cover-close:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
#guides {
|
||||
max-width: 860px;
|
||||
display: block;
|
||||
|
||||
@ -1620,6 +1620,139 @@ function clearHomeSearch() {
|
||||
input.focus();
|
||||
}
|
||||
|
||||
// Home page (Index.cshtml) only: "مرور کتابها" - a horizontally scrollable shelf of book "spines",
|
||||
// Grokipedia-homepage-style. Fetched once from this page's own "?Handler=BookCatalog" (which proxies
|
||||
// RMuseum's api/ganjoor/book-catalog server-side) rather than calling that API directly from browser
|
||||
// JS - same convention every other client-side data fetch on this site follows. There's no cover-art
|
||||
// image for any book in the database, so each spine's color is a deterministic hash of its id instead
|
||||
// of a real illustration - stable across visits/reloads (not reshuffled on every page load), while
|
||||
// still giving the shelf the varied-color look a real bookshelf has.
|
||||
var expandedBookSpine = null;
|
||||
|
||||
function hashBookColorHue(key) {
|
||||
var str = String(key);
|
||||
var hash = 0;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
hash = (hash * 31 + str.charCodeAt(i)) | 0;
|
||||
}
|
||||
return Math.abs(hash) % 360;
|
||||
}
|
||||
|
||||
function collapseExpandedBookSpine() {
|
||||
if (expandedBookSpine) {
|
||||
expandedBookSpine.classList.remove('expanded');
|
||||
expandedBookSpine = null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildBookSpine(book) {
|
||||
var spine = document.createElement('div');
|
||||
spine.className = 'book-spine';
|
||||
spine.tabIndex = 0;
|
||||
spine.setAttribute('role', 'button');
|
||||
spine.setAttribute('aria-label', book.name + (book.poetName ? ' - ' + book.poetName : ''));
|
||||
|
||||
var hue = hashBookColorHue(book.id);
|
||||
spine.style.background =
|
||||
'linear-gradient(90deg, rgba(0,0,0,.22), transparent 12%, transparent 88%, rgba(255,255,255,.12)), ' +
|
||||
'hsl(' + hue + ', 42%, 30%)';
|
||||
|
||||
// The compact (spine) face: book name on top, poet name below (smaller) - both laid out as a
|
||||
// normal horizontal line of text first, so Persian letter-joining/shaping happens correctly,
|
||||
// then rotated 90deg as a whole block. True CSS vertical writing-mode (the "right" way to do
|
||||
// this for Latin text, which is how Grokipedia's own spines work) breaks cursive Arabic/Persian
|
||||
// script - it uprights each letter separately instead of rotating the shaped line - so rotating
|
||||
// the finished line is the only way to get this to read correctly.
|
||||
var compact = document.createElement('div');
|
||||
compact.className = 'spine-compact';
|
||||
var title = document.createElement('span');
|
||||
title.className = 'spine-title';
|
||||
title.textContent = book.name;
|
||||
var poet = document.createElement('span');
|
||||
poet.className = 'spine-poet';
|
||||
poet.textContent = book.poetName || '';
|
||||
compact.appendChild(title);
|
||||
compact.appendChild(poet);
|
||||
|
||||
// The expanded (cover) face: shown once this spine is clicked - see openOrNavigateBookSpine().
|
||||
var cover = document.createElement('div');
|
||||
cover.className = 'spine-cover';
|
||||
var closeBtn = document.createElement('button');
|
||||
closeBtn.type = 'button';
|
||||
closeBtn.className = 'spine-cover-close';
|
||||
closeBtn.setAttribute('aria-label', 'بستن');
|
||||
closeBtn.textContent = '×';
|
||||
closeBtn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
collapseExpandedBookSpine();
|
||||
});
|
||||
var coverTitle = document.createElement('div');
|
||||
coverTitle.className = 'spine-cover-title';
|
||||
coverTitle.textContent = book.name;
|
||||
var coverPoet = document.createElement('div');
|
||||
coverPoet.className = 'spine-cover-poet';
|
||||
coverPoet.textContent = book.poetName || '';
|
||||
cover.appendChild(closeBtn);
|
||||
cover.appendChild(coverTitle);
|
||||
cover.appendChild(coverPoet);
|
||||
|
||||
spine.appendChild(compact);
|
||||
spine.appendChild(cover);
|
||||
|
||||
function openOrNavigateBookSpine() {
|
||||
if (spine.classList.contains('expanded')) {
|
||||
if (book.fullUrl) window.location.href = book.fullUrl;
|
||||
return;
|
||||
}
|
||||
// Only one spine open at a time - collapsing the previous one first is what makes a growing
|
||||
// spine read as "picking a different book" rather than stacking expanded covers.
|
||||
collapseExpandedBookSpine();
|
||||
spine.classList.add('expanded');
|
||||
expandedBookSpine = spine;
|
||||
// The spine's own width grows a lot (56px -> 170px) - without this, it can end up partly
|
||||
// scrolled out of view, which looks like nothing happened.
|
||||
spine.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
||||
}
|
||||
|
||||
spine.addEventListener('click', openOrNavigateBookSpine);
|
||||
spine.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
openOrNavigateBookSpine();
|
||||
}
|
||||
});
|
||||
|
||||
return spine;
|
||||
}
|
||||
|
||||
// Called by the "<"/">" handles next to the shelf (see Index.cshtml). amount is signed in the
|
||||
// ordinary "positive = move the view right, negative = move it left" sense - evergreen browsers
|
||||
// (Chrome/Firefox/Safari) all follow the spec's "negative" RTL scrollLeft model, where scrollLeft
|
||||
// is 0 at the right/start edge and goes negative toward the left/end edge, so a plain scrollBy
|
||||
// without any RTL sign-flip already moves the view the way its own delta's sign says.
|
||||
function scrollBooksShelf(amount) {
|
||||
var wrap = document.getElementById('books-shelf-wrap');
|
||||
if (!wrap) return;
|
||||
wrap.scrollBy({ left: amount, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function initHomeBooksShelf() {
|
||||
var shelf = document.getElementById('books-shelf');
|
||||
if (!shelf) return;
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '?Handler=BookCatalog',
|
||||
success: function (books) {
|
||||
if (!books || books.length === 0) return;
|
||||
var frag = document.createDocumentFragment();
|
||||
for (var i = 0; i < books.length; i++) {
|
||||
frag.appendChild(buildBookSpine(books[i]));
|
||||
}
|
||||
shelf.appendChild(frag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onInlineSearch(value, resultBlockId, itemsClass) {
|
||||
const foundPoetsNode = document.getElementById(resultBlockId);
|
||||
foundPoetsNode.innerHTML = '';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user