Move search results into poster grid

This commit is contained in:
Dymas
2026-06-02 11:06:13 +02:00
parent d075aa11f9
commit 94740ebb76
7 changed files with 232 additions and 29 deletions
+159 -25
View File
@@ -135,9 +135,9 @@ INDEX_HTML = r"""<!doctype html>
}
main {
display: grid;
grid-template-rows: auto auto 1fr;
gap: 20px;
min-width: 0;
align-content: start;
}
h1, h2, h3, p { margin: 0; }
h1 {
@@ -232,47 +232,119 @@ INDEX_HTML = r"""<!doctype html>
color: var(--text);
font-weight: 700;
}
.results {
display: grid;
gap: 10px;
min-width: 0;
}
.result, .settings, .episodes-panel, .deps {
.settings, .episodes-panel, .results-panel, .deps {
border: 1px solid var(--line);
border-radius: 22px;
background: linear-gradient(180deg, rgba(29, 22, 47, 0.88), rgba(17, 13, 29, 0.94));
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.result {
.settings, .episodes-panel, .results-panel, .deps {
padding: 18px;
display: grid;
gap: 14px;
}
.results-panel {
min-width: 0;
}
.results-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
min-width: 0;
}
.result-card {
border: 1px solid var(--line);
border-radius: 20px;
background: linear-gradient(180deg, rgba(32, 24, 54, 0.9) 0%, rgba(17, 13, 29, 0.96) 100%);
padding: 14px;
display: grid;
gap: 6px;
gap: 12px;
min-height: auto;
min-width: 0;
box-shadow: 0 18px 38px rgba(5, 2, 15, 0.32);
text-align: left;
transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease;
transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.result:hover {
.result-card:hover {
border-color: var(--line-strong);
transform: translateY(-1px);
background: linear-gradient(180deg, rgba(38, 28, 64, 0.94) 0%, rgba(19, 14, 32, 0.98) 100%);
transform: translateY(-2px);
}
.result.active {
border-color: rgba(157, 123, 255, 0.34);
.result-card.active {
border-color: rgba(157, 123, 255, 0.58);
box-shadow:
inset 0 0 0 1px rgba(157, 123, 255, 0.16),
0 18px 38px rgba(5, 2, 15, 0.32);
background: linear-gradient(180deg, rgba(48, 36, 80, 0.96), rgba(20, 15, 35, 0.96));
}
.cover-tile {
aspect-ratio: 3 / 4;
position: relative;
border-radius: 16px;
overflow: hidden;
border: 1px solid rgba(157, 123, 255, 0.16);
background:
radial-gradient(circle at top, rgba(157, 123, 255, 0.28), transparent 44%),
linear-gradient(180deg, #221734 0%, #0c0915 100%);
}
.cover-tile img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
opacity: 0;
transition: opacity 0.16s ease;
}
.cover-tile.has-image img {
opacity: 1;
}
.cover-fallback {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: #d8e5ef;
font-size: 28px;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
background: linear-gradient(180deg, rgba(157, 123, 255, 0.24), rgba(9, 7, 15, 0.18));
}
.cover-tile.has-image .cover-fallback {
display: none;
}
.result-title {
text-align: left;
color: var(--text);
font-weight: 700;
overflow-wrap: anywhere;
font-size: 14px;
line-height: 1.35;
min-height: 2.7em;
}
.result-meta {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
}
.result-pill {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 999px;
padding: 6px 10px;
color: var(--muted);
background: rgba(255, 255, 255, 0.04);
font-size: 12px;
white-space: nowrap;
}
.muted {
color: var(--muted);
font-size: 13px;
}
.settings, .episodes-panel, .deps {
padding: 18px;
display: grid;
gap: 14px;
}
.chips {
display: flex;
flex-wrap: wrap;
@@ -304,15 +376,28 @@ INDEX_HTML = r"""<!doctype html>
color: #ddd0ff;
font-size: 13px;
}
.results-panel .toolbar {
align-items: end;
}
.statline {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
@media (max-width: 1400px) {
.results-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 1160px) {
.results-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 900px) {
.app { grid-template-columns: 1fr; }
aside { border-right: 0; border-bottom: 1px solid var(--line); }
.controls, .row, .toolbar { align-items: stretch; flex-direction: column; }
.results-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
.results-grid { grid-template-columns: 1fr; }
}
</style>
</head>
@@ -346,7 +431,6 @@ INDEX_HTML = r"""<!doctype html>
<button class="primary" id="searchBtn" type="button">Search</button>
</div>
<div class="notice" id="notice"></div>
<div class="results" id="results"></div>
</aside>
<main>
@@ -400,6 +484,15 @@ INDEX_HTML = r"""<!doctype html>
</div>
</div>
</section>
<section class="results-panel">
<div class="toolbar">
<div>
<h2>Select results</h2>
<p class="muted" id="resultsMeta">Poster cards appear here after a search.</p>
</div>
</div>
<div class="results-grid" id="results"></div>
</section>
</main>
</div>
@@ -425,31 +518,70 @@ INDEX_HTML = r"""<!doctype html>
$("search-mode").value = mode;
}
function coverInitials(title) {
const parts = String(title || "")
.trim()
.split(/\s+/)
.filter(Boolean)
.slice(0, 2);
if (!parts.length) return "AN";
return parts.map((part) => part[0]).join("").slice(0, 2);
}
function resultThumbnailUrl(title) {
return `/api/search/thumb?title=${encodeURIComponent(title || "")}`;
}
function renderResults(results) {
const el = $("results");
el.innerHTML = "";
if (!results.length) {
el.innerHTML = '<div class="empty">No matching anime found.</div>';
$("resultsMeta").textContent = "No matching anime found.";
return;
}
for (const item of results) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "result";
btn.className = "result-card";
btn.innerHTML = `
<span class="result-title"></span>
<span class="muted"></span>
<div class="cover-tile">
<img alt="" loading="lazy" decoding="async">
<span class="cover-fallback"></span>
</div>
<div class="result-title"></div>
<div class="result-meta">
<span class="result-pill"></span>
<span class="result-pill"></span>
</div>
`;
btn.querySelector(".result-title").textContent = item.title;
btn.querySelector(".muted").textContent = `${item.episodes} episodes · result ${item.index}`;
btn.querySelector(".cover-fallback").textContent = coverInitials(item.title);
const pills = btn.querySelectorAll(".result-pill");
pills[0].textContent = `${item.episodes} episodes`;
pills[1].textContent = `Result ${item.index}`;
const img = btn.querySelector("img");
const tile = btn.querySelector(".cover-tile");
img.alt = item.title;
img.src = resultThumbnailUrl(item.title);
if (img.complete && img.naturalWidth > 0) {
tile.classList.add("has-image");
}
img.addEventListener("load", () => {
if (img.naturalWidth > 0) tile.classList.add("has-image");
});
img.addEventListener("error", () => {
tile.classList.remove("has-image");
});
btn.addEventListener("click", () => selectResult(item, btn));
el.appendChild(btn);
}
$("resultsMeta").textContent = `${results.length} matching anime`;
}
async function selectResult(item, button) {
const requestId = ++state.episodeRequestId;
document.querySelectorAll(".result").forEach((node) => node.classList.remove("active"));
document.querySelectorAll(".result-card").forEach((node) => node.classList.remove("active"));
button.classList.add("active");
state.selected = item;
state.episodes = [];
@@ -495,6 +627,7 @@ INDEX_HTML = r"""<!doctype html>
if (!query) return setNotice("Type a title first.");
setMode($("search-mode").value);
setNotice("Searching...");
$("resultsMeta").textContent = "Searching...";
state.selected = null;
state.episodeRequestId += 1;
$("addBtn").disabled = true;
@@ -508,6 +641,7 @@ INDEX_HTML = r"""<!doctype html>
} catch (error) {
if (requestId !== state.searchRequestId) return;
setNotice(error.message);
$("resultsMeta").textContent = "Search failed.";
}
}