Move search results into poster grid
This commit is contained in:
+159
-25
@@ -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.";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user