Initial commit
This commit is contained in:
@@ -0,0 +1,712 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Pinkudex — Filter Bar (Option 2 refined)</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-0: #0e0a14;
|
||||
--bg-1: #18121f;
|
||||
--fg: #ece6f5;
|
||||
--fg-dim: #b9b1c6;
|
||||
--fg-muted: #7d7388;
|
||||
--cyan: #4dd9e6;
|
||||
--violet: #b87cf6;
|
||||
--coral: #ff7a8a;
|
||||
--mint: #79e6b2;
|
||||
--amber: #fbbf24;
|
||||
--glass-border: #2a2434;
|
||||
--glass-border-strong: #3d3548;
|
||||
--glass: rgba(40, 32, 56, 0.5);
|
||||
--glass-strong: rgba(56, 46, 76, 0.7);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0;
|
||||
background: radial-gradient(1200px 600px at 70% -10%, rgba(184,124,246,0.08), transparent),
|
||||
radial-gradient(800px 500px at 10% 110%, rgba(77,217,230,0.06), transparent),
|
||||
var(--bg-0);
|
||||
color: var(--fg);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
.page { max-width: 1600px; margin: 0 auto; padding: 32px 24px 64px; }
|
||||
h1 { font-size: 28px; margin: 0 0 8px; font-weight: 600; letter-spacing: -0.01em; }
|
||||
.lede { color: var(--fg-dim); margin: 0 0 32px; max-width: 820px; line-height: 1.5; }
|
||||
h2 {
|
||||
margin: 32px 0 4px;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--cyan);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
.desc { color: var(--fg-dim); font-size: 13px; margin: 0 0 12px; max-width: 820px; line-height: 1.5; }
|
||||
|
||||
.stage {
|
||||
background: var(--bg-1);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
}
|
||||
.stage-label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--fg-muted);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
/* bar */
|
||||
.bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 6px 12px; border-radius: 9999px;
|
||||
border: 1px solid var(--glass-border); background: var(--glass);
|
||||
color: var(--fg-dim); font-size: 14px; cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
.chip:hover { color: var(--fg); border-color: var(--glass-border-strong); }
|
||||
.chip.active { background: rgba(77,217,230,0.15); border-color: rgba(77,217,230,0.4); color: var(--cyan); }
|
||||
.chip .caret { font-size: 10px; opacity: 0.7; }
|
||||
.chip .badge {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
min-width: 16px; height: 16px; padding: 0 5px;
|
||||
border-radius: 9999px; background: var(--cyan); color: #000;
|
||||
font-size: 10px; font-weight: 700; margin-left: 2px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
.right-group { display: flex; align-items: center; gap: 8px; margin-left: auto; }
|
||||
.search { position: relative; width: 224px; }
|
||||
.search input {
|
||||
width: 100%; background: var(--glass); border: 1px solid var(--glass-border);
|
||||
border-radius: 8px; padding: 6px 28px 6px 30px; color: var(--fg);
|
||||
font-size: 13px; outline: none;
|
||||
}
|
||||
.search input::placeholder { color: var(--fg-muted); }
|
||||
.search .ico { position: absolute; left: 9px; top: 50%; transform: translateY(-50%); color: var(--fg-muted); width: 14px; height: 14px; }
|
||||
.sort {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 6px 12px; border-radius: 9999px;
|
||||
border: 1px solid var(--glass-border); color: var(--fg-dim);
|
||||
font-size: 13px; cursor: pointer; background: var(--glass);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* active criteria strip */
|
||||
.crit-strip {
|
||||
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
|
||||
padding: 10px 12px; border: 1px dashed var(--glass-border-strong);
|
||||
border-radius: 12px; margin-bottom: 12px;
|
||||
background: rgba(77,217,230,0.04);
|
||||
min-height: 48px;
|
||||
}
|
||||
.crit-strip .label {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em;
|
||||
color: var(--fg-muted); margin-right: 4px;
|
||||
}
|
||||
.crit-empty { color: var(--fg-muted); font-size: 12px; font-style: italic; }
|
||||
.crit-pill {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 4px 6px 4px 10px; border-radius: 9999px;
|
||||
background: rgba(77,217,230,0.12); border: 1px solid rgba(77,217,230,0.4);
|
||||
color: var(--cyan); font-size: 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
.crit-pill .x {
|
||||
width: 16px; height: 16px; display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: 9999px; background: rgba(0,0,0,0.4); color: var(--cyan);
|
||||
font-size: 10px; cursor: pointer;
|
||||
}
|
||||
.crit-pill .x:hover { background: rgba(255,122,138,0.3); color: #fff; }
|
||||
.crit-pill .kind { color: var(--fg-muted); margin-right: 2px; }
|
||||
.crit-pill .conn { color: var(--fg-muted); font-size: 10px; padding: 0 2px; }
|
||||
.clear-all {
|
||||
margin-left: auto;
|
||||
color: var(--fg-muted); font-size: 11px;
|
||||
background: none; border: none; cursor: pointer;
|
||||
text-decoration: underline; padding: 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
.clear-all:hover { color: var(--coral); }
|
||||
|
||||
/* popover */
|
||||
.popup-wrap { position: relative; display: inline-block; }
|
||||
.popup {
|
||||
position: absolute; top: calc(100% + 6px); left: 0;
|
||||
background: var(--bg-0);
|
||||
border: 1px solid var(--glass-border-strong);
|
||||
border-radius: 14px;
|
||||
padding: 12px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,0.7);
|
||||
width: 540px;
|
||||
z-index: 20;
|
||||
display: none;
|
||||
}
|
||||
.popup.open { display: block; }
|
||||
|
||||
.tabs {
|
||||
display: flex; gap: 2px; border-bottom: 1px solid var(--glass-border);
|
||||
padding-bottom: 8px; margin-bottom: 10px; flex-wrap: wrap;
|
||||
}
|
||||
.tab {
|
||||
padding: 5px 10px; border-radius: 6px;
|
||||
font-size: 12px; color: var(--fg-muted); cursor: pointer;
|
||||
user-select: none;
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
background: transparent; border: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
.tab:hover { background: var(--glass); color: var(--fg-dim); }
|
||||
.tab.active { background: var(--glass-strong); color: var(--cyan); }
|
||||
.tab .badge {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
min-width: 14px; height: 14px; padding: 0 4px;
|
||||
border-radius: 9999px; background: var(--cyan); color: #000;
|
||||
font-size: 9px; font-weight: 700;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
.popup-controls {
|
||||
display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
|
||||
}
|
||||
.popup-search { position: relative; flex: 1; }
|
||||
.popup-search input {
|
||||
width: 100%; background: var(--glass); border: 1px solid var(--glass-border);
|
||||
border-radius: 8px; padding: 6px 10px 6px 28px; color: var(--fg);
|
||||
font-size: 13px; outline: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
.popup-search .ico { position: absolute; left: 8px; top: 50%; transform: translateY(-50%); color: var(--fg-muted); width: 13px; height: 13px; }
|
||||
|
||||
.mode-toggle {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
user-select: none;
|
||||
}
|
||||
.mode-toggle .seg {
|
||||
padding: 5px 10px; cursor: pointer;
|
||||
color: var(--fg-muted); background: transparent;
|
||||
border: none; font-family: inherit; font-size: 11px;
|
||||
}
|
||||
.mode-toggle .seg.on { background: var(--cyan); color: #000; font-weight: 700; }
|
||||
|
||||
.popup-list { max-height: 240px; overflow-y: auto; padding: 2px; }
|
||||
.popup-list .row {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 6px 8px; border-radius: 6px; cursor: pointer;
|
||||
font-size: 13px; color: var(--fg-dim);
|
||||
user-select: none;
|
||||
}
|
||||
.popup-list .row:hover { background: var(--glass); color: var(--fg); }
|
||||
.popup-list .row.checked { color: var(--cyan); }
|
||||
.popup-list .row .name { flex: 1; }
|
||||
.popup-list .row .count {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
color: var(--fg-muted); font-size: 11px;
|
||||
}
|
||||
.check {
|
||||
width: 14px; height: 14px; border-radius: 3px;
|
||||
border: 1.5px solid var(--glass-border-strong);
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
font-size: 9px; color: var(--cyan);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.check.on { background: rgba(77,217,230,0.18); border-color: var(--cyan); }
|
||||
|
||||
.popup-footer {
|
||||
margin-top: 8px; padding-top: 8px;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-size: 11px; color: var(--fg-muted);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
.popup-footer button {
|
||||
background: none; border: none; color: var(--coral);
|
||||
font-size: 11px; cursor: pointer; font-family: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* missing popover */
|
||||
.missing-popup {
|
||||
position: absolute; top: calc(100% + 6px); left: 0;
|
||||
background: var(--bg-0);
|
||||
border: 1px solid var(--glass-border-strong);
|
||||
border-radius: 12px;
|
||||
padding: 8px;
|
||||
box-shadow: 0 16px 40px rgba(0,0,0,0.55);
|
||||
width: 220px;
|
||||
z-index: 20;
|
||||
display: none;
|
||||
}
|
||||
.missing-popup.open { display: block; }
|
||||
.check-row {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 6px 6px; border-radius: 6px; cursor: pointer;
|
||||
color: var(--fg-dim); font-size: 13px;
|
||||
user-select: none;
|
||||
}
|
||||
.check-row:hover { background: var(--glass); color: var(--fg); }
|
||||
.check-row .count {
|
||||
margin-left: auto;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
color: var(--fg-muted); font-size: 11px;
|
||||
}
|
||||
|
||||
/* letter bar */
|
||||
.letters { display: flex; gap: 4px; margin-top: 12px; }
|
||||
.letter {
|
||||
flex: 1; text-align: center;
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 8px; padding: 6px 0 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 13px; color: var(--fg-muted); background: var(--glass);
|
||||
}
|
||||
.letter.on { background: var(--cyan); color: #000; border-color: transparent; font-weight: 600; }
|
||||
.letter.dim { opacity: 0.35; border-color: transparent; }
|
||||
.letter .num { display: block; font-size: 9px; margin-top: 2px; }
|
||||
|
||||
.note {
|
||||
margin-top: 14px; font-size: 11px; color: var(--fg-muted);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
code {
|
||||
background: var(--glass); padding: 1px 5px; border-radius: 4px;
|
||||
font-size: 12px; color: var(--cyan);
|
||||
}
|
||||
|
||||
/* SQL preview */
|
||||
.sql-preview {
|
||||
margin-top: 14px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(0,0,0,0.4);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 8px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 11px;
|
||||
color: var(--fg-dim);
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.sql-preview .kw { color: var(--violet); }
|
||||
.sql-preview .lit { color: var(--mint); }
|
||||
.sql-preview .col { color: var(--cyan); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>Filter Bar — Option 2 refined</h1>
|
||||
<p class="lede">
|
||||
Single <code>Filter by ▾</code> popover with tabs. Each tab supports multi-select and a per-type AND/OR toggle.
|
||||
Active criteria render as removable pills above the letter bar. Single-entity pages (e.g. <code>/actress/aika</code>) stay
|
||||
as-is — selecting an entity here adds an extra criterion on top of whatever you're viewing.
|
||||
</p>
|
||||
|
||||
<div class="stage">
|
||||
<div class="stage-label">Live mockup — click anything</div>
|
||||
|
||||
<div class="bar">
|
||||
<button class="chip active">All</button>
|
||||
|
||||
<div class="popup-wrap" id="filterWrap">
|
||||
<button class="chip" id="filterChip" onclick="togglePopup('filterPopup')">
|
||||
⛓ Filter by <span id="filterBadge" class="badge" style="display:none">0</span>
|
||||
<span class="caret">▾</span>
|
||||
</button>
|
||||
<div class="popup" id="filterPopup">
|
||||
<div class="tabs" id="tabs"></div>
|
||||
|
||||
<div class="popup-controls">
|
||||
<div class="popup-search">
|
||||
<span class="ico">🔍</span>
|
||||
<input id="popupSearch" placeholder="Filter list…" oninput="renderList()" />
|
||||
</div>
|
||||
<div class="mode-toggle" id="modeToggle">
|
||||
<button class="seg on" onclick="setMode('OR')">OR</button>
|
||||
<button class="seg" onclick="setMode('AND')">AND</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="popup-list" id="popupList"></div>
|
||||
|
||||
<div class="popup-footer">
|
||||
<span id="footerHint">tap row to toggle · OR = match any · AND = match all</span>
|
||||
<button onclick="clearTab()">Clear this tab</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="popup-wrap" id="missingWrap">
|
||||
<button class="chip" id="missingChip" onclick="togglePopup('missingPopup')">
|
||||
⊘ Missing <span id="missingBadge" class="badge" style="display:none">0</span>
|
||||
<span class="caret">▾</span>
|
||||
</button>
|
||||
<div class="missing-popup" id="missingPopup">
|
||||
<div class="check-row" onclick="toggleMissing('unwatched')">
|
||||
<span class="check" id="m-unwatched">·</span>Unwatched <span class="count">12</span>
|
||||
</div>
|
||||
<div class="check-row" onclick="toggleMissing('uncollected')">
|
||||
<span class="check" id="m-uncollected">·</span>No Collection <span class="count">8</span>
|
||||
</div>
|
||||
<div class="check-row" onclick="toggleMissing('untagged')">
|
||||
<span class="check" id="m-untagged">·</span>No Tags <span class="count">17</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-group">
|
||||
<div class="search">
|
||||
<span class="ico">🔍</span>
|
||||
<input placeholder="Search Code, Title, Notes…" />
|
||||
</div>
|
||||
<button class="sort">⏱ Newest First ▾</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active criteria strip -->
|
||||
<div class="crit-strip" id="critStrip">
|
||||
<span class="label">Active</span>
|
||||
<span id="critList"></span>
|
||||
<span id="critEmpty" class="crit-empty">no filters — showing all covers</span>
|
||||
<button id="clearAll" class="clear-all" onclick="clearAll()" style="display:none">clear all</button>
|
||||
</div>
|
||||
|
||||
<!-- Letter bar (untouched) -->
|
||||
<div class="letters">
|
||||
<div class="letter on">ALL<span class="num">26</span></div>
|
||||
<div class="letter">A<span class="num">4</span></div>
|
||||
<div class="letter">B<span class="num">3</span></div>
|
||||
<div class="letter">C<span class="num">1</span></div>
|
||||
<div class="letter dim">D<span class="num">·</span></div>
|
||||
<div class="letter">E<span class="num">1</span></div>
|
||||
<div class="letter">I<span class="num">3</span></div>
|
||||
<div class="letter">K<span class="num">3</span></div>
|
||||
<div class="letter">L<span class="num">1</span></div>
|
||||
<div class="letter">M<span class="num">6</span></div>
|
||||
<div class="letter">N<span class="num">1</span></div>
|
||||
<div class="letter">P<span class="num">1</span></div>
|
||||
<div class="letter">R<span class="num">2</span></div>
|
||||
<div class="letter">S<span class="num">3</span></div>
|
||||
<div class="letter">U<span class="num">1</span></div>
|
||||
<div class="letter">Y<span class="num">1</span></div>
|
||||
<div class="letter dim">Z<span class="num">·</span></div>
|
||||
</div>
|
||||
|
||||
<!-- SQL preview -->
|
||||
<div class="sql-preview" id="sqlPreview"></div>
|
||||
</div>
|
||||
|
||||
<p class="note">URL example with current selection: <code id="urlExample">/?</code></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Mock catalog data
|
||||
const data = {
|
||||
actresses: { icon: "👤", items: [
|
||||
{ id: 1, name: "Aoi Kururugi", count: 3 },
|
||||
{ id: 2, name: "Aika", count: 0 },
|
||||
{ id: 3, name: "Eru Sato", count: 1 },
|
||||
{ id: 4, name: "Ichigo Aoi", count: 2 },
|
||||
{ id: 5, name: "Ichika Matsumoto", count: 5 },
|
||||
{ id: 6, name: "Kana Kusakabe", count: 1 },
|
||||
{ id: 7, name: "Lala Kudo", count: 2 },
|
||||
{ id: 8, name: "Mitsuki Nagisa", count: 1 },
|
||||
{ id: 9, name: "Reika Aiba", count: 2 },
|
||||
{ id: 10, name: "Shuri Atomi", count: 4 },
|
||||
]},
|
||||
studios: { icon: "🏢", items: [
|
||||
{ id: 1, name: "SOD", count: 12 },
|
||||
{ id: 2, name: "S1", count: 8 },
|
||||
{ id: 3, name: "Moodyz", count: 6 },
|
||||
{ id: 4, name: "Madonna", count: 3 },
|
||||
{ id: 5, name: "IdeaPocket", count: 4 },
|
||||
]},
|
||||
series: { icon: "🎬", items: [
|
||||
{ id: 1, name: "First Time", count: 4 },
|
||||
{ id: 2, name: "Schoolgirl Diaries", count: 6 },
|
||||
{ id: 3, name: "Office Lady", count: 3 },
|
||||
]},
|
||||
genres: { icon: "#", items: [
|
||||
{ id: 1, name: "Schoolgirl", count: 12 },
|
||||
{ id: 2, name: "Drama", count: 5 },
|
||||
{ id: 3, name: "Comedy", count: 2 },
|
||||
{ id: 4, name: "Romance", count: 7 },
|
||||
{ id: 5, name: "Solo", count: 3 },
|
||||
]},
|
||||
collections: { icon: "📁", items: [
|
||||
{ id: 1, name: "Watchlist", count: 9 },
|
||||
{ id: 2, name: "Best Of 2025", count: 14 },
|
||||
]},
|
||||
tags: { icon: "🏷", items: [
|
||||
{ id: 1, name: "favorite", count: 11 },
|
||||
{ id: 2, name: "vip", count: 3 },
|
||||
{ id: 3, name: "rewatch", count: 5 },
|
||||
{ id: 4, name: "saved", count: 8 },
|
||||
]},
|
||||
};
|
||||
|
||||
// State
|
||||
const state = {
|
||||
selected: { actresses: new Set(), studios: new Set(), series: new Set(), genres: new Set(), collections: new Set(), tags: new Set() },
|
||||
mode: { actresses: "OR", studios: "OR", series: "OR", genres: "OR", collections: "OR", tags: "OR" },
|
||||
activeTab: "actresses",
|
||||
missing: new Set(),
|
||||
};
|
||||
|
||||
function renderTabs() {
|
||||
const el = document.getElementById("tabs");
|
||||
el.innerHTML = "";
|
||||
for (const key of Object.keys(data)) {
|
||||
const t = data[key];
|
||||
const selectedCount = state.selected[key].size;
|
||||
const cap = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "tab" + (state.activeTab === key ? " active" : "");
|
||||
btn.onclick = () => { state.activeTab = key; document.getElementById("popupSearch").value = ""; renderTabs(); renderList(); renderModeToggle(); };
|
||||
btn.innerHTML = `${t.icon} ${cap}` + (selectedCount > 0 ? ` <span class="badge">${selectedCount}</span>` : "");
|
||||
el.appendChild(btn);
|
||||
}
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const el = document.getElementById("popupList");
|
||||
const q = (document.getElementById("popupSearch")?.value ?? "").toLowerCase();
|
||||
const tab = data[state.activeTab];
|
||||
const sel = state.selected[state.activeTab];
|
||||
el.innerHTML = "";
|
||||
for (const it of tab.items) {
|
||||
if (q && !it.name.toLowerCase().includes(q)) continue;
|
||||
const row = document.createElement("div");
|
||||
const checked = sel.has(it.id);
|
||||
row.className = "row" + (checked ? " checked" : "");
|
||||
row.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (checked) sel.delete(it.id); else sel.add(it.id);
|
||||
renderAll();
|
||||
};
|
||||
row.innerHTML = `
|
||||
<span class="check ${checked ? "on" : ""}">${checked ? "✓" : ""}</span>
|
||||
<span class="name">${it.name}</span>
|
||||
<span class="count">${it.count}</span>
|
||||
`;
|
||||
el.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function renderModeToggle() {
|
||||
const segs = document.querySelectorAll("#modeToggle .seg");
|
||||
const cur = state.mode[state.activeTab];
|
||||
segs[0].classList.toggle("on", cur === "OR");
|
||||
segs[1].classList.toggle("on", cur === "AND");
|
||||
}
|
||||
|
||||
function setMode(mode) {
|
||||
state.mode[state.activeTab] = mode;
|
||||
renderModeToggle();
|
||||
renderCriteria();
|
||||
renderSql();
|
||||
}
|
||||
|
||||
function clearTab() {
|
||||
state.selected[state.activeTab].clear();
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
for (const k of Object.keys(state.selected)) state.selected[k].clear();
|
||||
state.missing.clear();
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function toggleMissing(key) {
|
||||
if (state.missing.has(key)) state.missing.delete(key); else state.missing.add(key);
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function renderMissing() {
|
||||
for (const k of ["unwatched", "uncollected", "untagged"]) {
|
||||
const el = document.getElementById("m-" + k);
|
||||
const on = state.missing.has(k);
|
||||
el.classList.toggle("on", on);
|
||||
el.textContent = on ? "✓" : "·";
|
||||
}
|
||||
const total = state.missing.size;
|
||||
const badge = document.getElementById("missingBadge");
|
||||
badge.textContent = total;
|
||||
badge.style.display = total > 0 ? "inline-flex" : "none";
|
||||
document.getElementById("missingChip").classList.toggle("active", total > 0);
|
||||
}
|
||||
|
||||
function renderFilterChip() {
|
||||
let total = 0;
|
||||
for (const k of Object.keys(state.selected)) total += state.selected[k].size;
|
||||
const badge = document.getElementById("filterBadge");
|
||||
badge.textContent = total;
|
||||
badge.style.display = total > 0 ? "inline-flex" : "none";
|
||||
document.getElementById("filterChip").classList.toggle("active", total > 0);
|
||||
}
|
||||
|
||||
function renderCriteria() {
|
||||
const list = document.getElementById("critList");
|
||||
const empty = document.getElementById("critEmpty");
|
||||
const clearAllBtn = document.getElementById("clearAll");
|
||||
list.innerHTML = "";
|
||||
let total = 0;
|
||||
|
||||
for (const key of Object.keys(state.selected)) {
|
||||
const sel = state.selected[key];
|
||||
if (sel.size === 0) continue;
|
||||
const items = data[key].items.filter((i) => sel.has(i.id));
|
||||
const mode = state.mode[key];
|
||||
const conn = mode === "AND" ? "AND" : "OR";
|
||||
|
||||
items.forEach((it, i) => {
|
||||
const pill = document.createElement("span");
|
||||
pill.className = "crit-pill";
|
||||
pill.innerHTML = `
|
||||
<span class="kind">${data[key].icon}</span>
|
||||
${it.name}
|
||||
<span class="x" title="Remove">✕</span>
|
||||
`;
|
||||
pill.querySelector(".x").onclick = () => { sel.delete(it.id); renderAll(); };
|
||||
list.appendChild(pill);
|
||||
if (i < items.length - 1) {
|
||||
const connector = document.createElement("span");
|
||||
connector.className = "conn";
|
||||
connector.textContent = conn;
|
||||
list.appendChild(connector);
|
||||
}
|
||||
total++;
|
||||
});
|
||||
|
||||
if (key !== Object.keys(state.selected).filter((k) => state.selected[k].size > 0).slice(-1)[0]) {
|
||||
const cross = document.createElement("span");
|
||||
cross.className = "conn";
|
||||
cross.style.color = "var(--violet)";
|
||||
cross.textContent = "AND";
|
||||
list.appendChild(cross);
|
||||
}
|
||||
}
|
||||
|
||||
for (const m of state.missing) {
|
||||
if (total > 0 || state.missing.size > 1) {
|
||||
// already handled by section connector if applicable
|
||||
}
|
||||
const pill = document.createElement("span");
|
||||
pill.className = "crit-pill";
|
||||
pill.style.background = "rgba(255,122,138,0.12)";
|
||||
pill.style.borderColor = "rgba(255,122,138,0.4)";
|
||||
pill.style.color = "var(--coral)";
|
||||
const labels = { unwatched: "Unwatched", uncollected: "No Collection", untagged: "No Tags" };
|
||||
pill.innerHTML = `
|
||||
<span class="kind">⊘</span>
|
||||
${labels[m]}
|
||||
<span class="x" title="Remove">✕</span>
|
||||
`;
|
||||
pill.querySelector(".x").onclick = () => { state.missing.delete(m); renderAll(); };
|
||||
list.appendChild(pill);
|
||||
total++;
|
||||
}
|
||||
|
||||
empty.style.display = total === 0 ? "inline" : "none";
|
||||
clearAllBtn.style.display = total === 0 ? "none" : "inline-block";
|
||||
}
|
||||
|
||||
function renderSql() {
|
||||
const parts = [];
|
||||
for (const key of Object.keys(state.selected)) {
|
||||
const sel = state.selected[key];
|
||||
if (sel.size === 0) continue;
|
||||
const ids = [...sel];
|
||||
const items = data[key].items.filter((i) => sel.has(i.id));
|
||||
const names = items.map((i) => `'${i.name}'`).join(", ");
|
||||
const mode = state.mode[key];
|
||||
const tableMap = {
|
||||
actresses: "image_actresses ia",
|
||||
studios: "i.studio_id",
|
||||
series: "i.series_id",
|
||||
genres: "image_genres ig",
|
||||
collections: "collection_images ci",
|
||||
tags: "image_tags it",
|
||||
};
|
||||
if (key === "studios" || key === "series") {
|
||||
parts.push(`<span class="kw">AND</span> ${tableMap[key]} <span class="kw">IN</span> (<span class="lit">${names}</span>)`);
|
||||
} else if (mode === "OR") {
|
||||
parts.push(`<span class="kw">AND</span> <span class="col">${key}</span> <span class="kw">IN</span> (<span class="lit">${names}</span>)`);
|
||||
} else {
|
||||
parts.push(`<span class="kw">AND</span> covers w/ <span class="kw">ALL</span> of (<span class="lit">${names}</span>)`);
|
||||
}
|
||||
}
|
||||
for (const m of state.missing) {
|
||||
parts.push(`<span class="kw">AND</span> <span class="col">${m}</span>`);
|
||||
}
|
||||
const sql = parts.length === 0
|
||||
? '<span class="kw">SELECT</span> * <span class="kw">FROM</span> <span class="col">images</span> <span class="kw">WHERE</span> deleted_at <span class="kw">IS NULL</span>'
|
||||
: '<span class="kw">SELECT</span> * <span class="kw">FROM</span> <span class="col">images</span> <span class="kw">WHERE</span> deleted_at <span class="kw">IS NULL</span>\n ' + parts.join("\n ");
|
||||
document.getElementById("sqlPreview").innerHTML = sql;
|
||||
}
|
||||
|
||||
function renderUrl() {
|
||||
const sp = new URLSearchParams();
|
||||
for (const key of Object.keys(state.selected)) {
|
||||
const sel = state.selected[key];
|
||||
if (sel.size === 0) continue;
|
||||
sp.set(key, [...sel].join(","));
|
||||
if (state.mode[key] === "AND") sp.set(`${key}_mode`, "and");
|
||||
}
|
||||
for (const m of state.missing) sp.set(m, "1");
|
||||
const s = sp.toString();
|
||||
document.getElementById("urlExample").textContent = "/" + (s ? "?" + s : "");
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
renderTabs();
|
||||
renderList();
|
||||
renderModeToggle();
|
||||
renderCriteria();
|
||||
renderFilterChip();
|
||||
renderMissing();
|
||||
renderSql();
|
||||
renderUrl();
|
||||
}
|
||||
|
||||
function togglePopup(id) {
|
||||
const p = document.getElementById(id);
|
||||
const opening = !p.classList.contains("open");
|
||||
document.querySelectorAll(".popup, .missing-popup").forEach((x) => x.classList.remove("open"));
|
||||
if (opening) p.classList.add("open");
|
||||
}
|
||||
|
||||
// Any click inside a popover stays inside — prevents the document listener
|
||||
// from incorrectly closing it when an inner click rebuilds the DOM.
|
||||
document.querySelectorAll(".popup, .missing-popup").forEach((p) => {
|
||||
p.addEventListener("click", (e) => e.stopPropagation());
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!e.target.closest(".popup-wrap")) {
|
||||
document.querySelectorAll(".popup, .missing-popup").forEach((x) => x.classList.remove("open"));
|
||||
}
|
||||
});
|
||||
|
||||
// Pre-seed a demo selection so the user sees the chips on load.
|
||||
state.selected.actresses.add(2); // Aika
|
||||
state.selected.studios.add(1); // SOD
|
||||
state.selected.genres.add(1);
|
||||
state.selected.genres.add(2); // Schoolgirl OR Drama
|
||||
state.selected.tags.add(1); // favorite
|
||||
|
||||
renderAll();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user