parent
b8c45ed93c
commit
a1c500daee
@ -196,7 +196,16 @@ else
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
@ -211,3 +220,9 @@ else
|
|||||||
{
|
{
|
||||||
@Html.Raw(ViewData["TrackingScript"])
|
@Html.Raw(ViewData["TrackingScript"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script>
|
||||||
|
initHomeBooksShelf();
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|||||||
@ -115,6 +115,24 @@ namespace GanjooRazor.Pages
|
|||||||
return Redirect(pageUrl);
|
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()
|
public async Task<IActionResult> OnGetAsync()
|
||||||
{
|
{
|
||||||
var maintenanceResult = TryGetMaintenanceModeResult();
|
var maintenanceResult = TryGetMaintenanceModeResult();
|
||||||
|
|||||||
@ -342,6 +342,180 @@ select {
|
|||||||
color: var(--ink-black);
|
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 {
|
#guides {
|
||||||
max-width: 860px;
|
max-width: 860px;
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@ -1620,6 +1620,139 @@ function clearHomeSearch() {
|
|||||||
input.focus();
|
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) {
|
function onInlineSearch(value, resultBlockId, itemsClass) {
|
||||||
const foundPoetsNode = document.getElementById(resultBlockId);
|
const foundPoetsNode = document.getElementById(resultBlockId);
|
||||||
foundPoetsNode.innerHTML = '';
|
foundPoetsNode.innerHTML = '';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user