Files

501 lines
20 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html>
<head>
<title>OnlyScavs Keys</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
--bg: #121212;
--panel: #1a1a1a;
--text: #eee;
--muted: #999;
--border: #2a2a2a;
--accent: #9ccfff;
--accent2: #ffd580;
}
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
margin: 0;
padding-top: 52px;
background-image: url('/assets/onlyscavs.png');
background-attachment: fixed;
background-size: cover;
background-position: center 65%;
}
body::before {
content: '';
position: fixed;
inset: 0;
background: rgba(14,14,14,0.88);
pointer-events: none;
z-index: 0;
}
.page {
max-width: 1020px;
margin: 0 auto;
padding: 24px 16px;
position: relative;
z-index: 1;
}
.site-nav {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
height: 52px;
background: rgba(14,14,14,0.92);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.nav-brand {
font-size: 0.85rem; font-weight: 700; letter-spacing: 0.1em;
text-transform: uppercase; color: var(--accent); text-decoration: none; flex-shrink: 0;
}
.nav-links { display: flex; gap: 2px; flex-wrap: wrap; }
.nav-links a {
color: #666; text-decoration: none; font-size: 0.8rem;
padding: 5px 10px; border-radius: 5px; transition: color 0.15s, background 0.15s;
}
.nav-links a:hover { color: var(--text); background: rgba(255,255,255,0.06); }
.nav-links a.active { color: var(--accent); background: rgba(156,207,255,0.08); }
h1 { font-size: 1.4rem; margin: 0 0 14px; color: var(--accent); letter-spacing: 0.02em; }
a { color: var(--accent); }
/* ── toolbar ───────────────────────────────────────────── */
.toolbar {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 16px;
}
.toolbar input[type="text"] {
flex: 1 1 200px; max-width: 340px;
background: #1e1e1e; color: var(--text); border: 1px solid #3a3a3a;
border-radius: 6px; padding: 7px 11px; font-size: 0.9rem;
}
.toolbar input[type="text"]:focus { outline: 1px solid var(--accent); }
/* ── map sections ──────────────────────────────────────── */
.map-section { margin-bottom: 28px; }
.map-section.empty-section { display: none; }
.map-section-header {
font-size: 0.9rem; font-weight: 700; color: var(--accent2);
letter-spacing: 0.06em; text-transform: uppercase;
padding: 6px 0 5px; border-bottom: 1px solid #2a3a1e; margin-bottom: 0;
}
/* ── table ─────────────────────────────────────────────── */
.key-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
.key-table th {
text-align: left; color: var(--muted); font-weight: 600;
font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em;
padding: 7px 10px 5px; border-bottom: 1px solid var(--border);
}
.key-table td { padding: 8px 10px; border-bottom: 1px solid #1e1e1e; vertical-align: middle; }
.key-table tr:hover td { background: #161616; }
.key-table tr.hidden-row { display: none; }
.key-thumb {
width: 36px; height: 36px; border-radius: 4px;
object-fit: contain; background: #1a1a1a; display: block;
}
/* ── priority badge / picker ───────────────────────────── */
.priority-badge {
display: inline-block; border-radius: 4px; padding: 2px 7px;
font-size: 0.75rem; font-weight: 700; letter-spacing: 0.04em;
white-space: nowrap; cursor: pointer; user-select: none;
transition: opacity 0.1s;
}
.priority-badge:hover { opacity: 0.75; }
.pb-4 { background: rgba(255,107,107,0.15); color: #ff6b6b; border: 1px solid rgba(255,107,107,0.3); }
.pb-3 { background: rgba(255,213,128,0.12); color: var(--accent2); border: 1px solid rgba(255,213,128,0.25); }
.pb-2 { background: rgba(156,207,255,0.1); color: var(--accent); border: 1px solid rgba(156,207,255,0.2); }
.pb-1 { background: rgba(153,153,153,0.1); color: var(--muted); border: 1px solid rgba(153,153,153,0.2); }
.pb-0 { background: rgba(80,80,80,0.1); color: #555; border: 1px solid #333; }
.pb-none { background: transparent; color: #555; border: 1px solid #2a2a2a; font-style: italic; }
/* inline priority dropdown */
.priority-select {
background: #222; color: var(--text); border: 1px solid var(--accent);
border-radius: 4px; padding: 2px 4px; font-size: 0.78rem; font-weight: 700;
cursor: pointer;
}
option[value="4"] { color: #ff6b6b; }
option[value="3"] { color: var(--accent2); }
option[value="2"] { color: #9ccfff; }
option[value="1"] { color: var(--muted); }
option[value="0"] { color: #555; }
/* ── quest toggle ──────────────────────────────────────── */
.quest-cell { text-align: center; }
.quest-dot {
display: inline-block; width: 10px; height: 10px;
border-radius: 50%; background: var(--accent2);
cursor: pointer; transition: opacity 0.15s;
}
.quest-dot:hover { opacity: 0.6; }
.quest-empty {
display: inline-block; width: 10px; height: 10px;
border-radius: 50%; border: 1px solid #333;
cursor: pointer; transition: border-color 0.15s;
}
.quest-empty:hover { border-color: var(--accent2); }
/* ── map tags + popover ────────────────────────────────── */
.map-cell { position: relative; }
.map-tags { display: flex; flex-wrap: wrap; gap: 3px; align-items: center; }
.map-tag {
background: #1e2e1e; border: 1px solid #2a3f2a; color: #8fbc8f;
border-radius: 999px; padding: 1px 8px; font-size: 0.73rem;
}
.map-add-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 18px; height: 18px; border-radius: 50%;
background: transparent; border: 1px dashed #444; color: #555;
font-size: 0.8rem; line-height: 1; cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.map-add-btn:hover { border-color: var(--accent); color: var(--accent); }
.map-popover {
display: none;
position: absolute; top: 100%; left: 0; z-index: 200;
background: #1e1e1e; border: 1px solid #3a3a3a; border-radius: 8px;
padding: 10px 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.6);
min-width: 200px;
}
.map-popover.open { display: block; }
.map-popover label {
display: flex; align-items: center; gap: 7px;
padding: 4px 0; font-size: 0.83rem; cursor: pointer;
}
.map-popover label:hover { color: var(--accent); }
.map-popover input[type="checkbox"] { margin: 0; cursor: pointer; accent-color: var(--accent); }
/* ── note inline edit ──────────────────────────────────── */
.note-cell { max-width: 200px; }
.note-text {
color: var(--muted); font-style: italic; font-size: 0.83rem;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
cursor: text; padding: 2px 4px; border-radius: 4px;
border: 1px solid transparent; display: block;
transition: border-color 0.15s;
}
.note-text:hover { border-color: #3a3a3a; }
.note-text.empty { color: #444; font-style: italic; }
.note-input {
width: 100%; background: #222; color: var(--text);
border: 1px solid var(--accent); border-radius: 4px;
padding: 3px 6px; font-size: 0.83rem; display: none;
}
/* save flash */
@keyframes flash { 0%,100% { opacity:1; } 50% { opacity: 0.4; } }
.saving { animation: flash 0.5s ease; }
.no-results {
color: var(--muted); font-style: italic; padding: 10px; font-size: 0.88rem;
}
</style>
</head>
<body>
<div class="page">
<nav class="site-nav">
<a class="nav-brand" href="/">OnlyScavs</a>
<div class="nav-links">
<a href="/keys" class="active">Keys</a>
<a href="/collector">Collector</a>
<a href="/quests">Quests</a>
<a href="/loadout">Loadout</a>
<a href="/meds">Injectors</a>
<a href="/barters">Barters</a>
</div>
</nav>
<h1>Keys</h1>
<div class="toolbar">
<input type="text" id="key-search" placeholder="Search keys…" autocomplete="off" oninput="applySearch()">
</div>
{# Embed map list for JS #}
<script>
const MAPS = {{ maps | map(attribute='name') | list | tojson }};
const MAP_IDS = {{ maps | map(attribute='id') | list | tojson }};
</script>
{% macro priority_badge(key) %}
{% if key.priority == 4 %}<span class="priority-badge pb-4" title="Click to change">SUPER</span>
{% elif key.priority == 3 %}<span class="priority-badge pb-3" title="Click to change">HIGH</span>
{% elif key.priority == 2 %}<span class="priority-badge pb-2" title="Click to change">MED</span>
{% elif key.priority == 1 %}<span class="priority-badge pb-1" title="Click to change">LOW</span>
{% elif key.priority == 0 %}<span class="priority-badge pb-0" title="Click to change">IGNORE</span>
{% else %}<span class="priority-badge pb-none" title="Click to set priority"></span>{% endif %}
{% endmacro %}
{% macro key_row(key, show_maps=true) %}
{% set selected_maps = key_maps.get(key.id, []) %}
<tr data-key-id="{{ key.id }}"
data-priority="{{ key.priority if key.priority is not none else '' }}"
data-quest="{{ '1' if key.used_in_quest else '0' }}"
data-reason="{{ key.reason or '' }}"
data-map-ids="{{ selected_maps | join(',') }}">
<td><img class="key-thumb" src="{{ key.grid_image_url }}" loading="lazy" alt=""></td>
<td>
<strong>{{ key.name }}</strong>
{% if key.wiki_url %}<br><a href="{{ key.wiki_url }}" target="_blank" style="font-size:0.78rem">wiki ↗</a>{% endif %}
</td>
<td class="priority-cell">{{ priority_badge(key) }}</td>
<td class="quest-cell">
{% if key.used_in_quest %}
<span class="quest-dot" title="Quest key — click to toggle"></span>
{% else %}
<span class="quest-empty" title="Not a quest key — click to toggle"></span>
{% endif %}
</td>
<td class="map-cell">
<div class="map-tags">
{% if show_maps %}
{% for m in maps %}{% if m.id in selected_maps %}<span class="map-tag">{{ m.name }}</span>{% endif %}{% endfor %}
{% endif %}
<span class="map-add-btn" title="Edit maps">+</span>
</div>
<div class="map-popover">
{% for m in maps %}
<label>
<input type="checkbox" value="{{ m.id }}" {% if m.id in selected_maps %}checked{% endif %}>
{{ m.name }}
</label>
{% endfor %}
</div>
</td>
<td class="note-cell">
<span class="note-text {% if not key.reason %}empty{% endif %}" title="Click to edit">{{ key.reason or 'add note…' }}</span>
<input class="note-input" type="text" value="{{ key.reason or '' }}" placeholder="note…">
</td>
</tr>
{% endmacro %}
{% for map, map_keys in keys_by_map %}
<div class="map-section{% if not map_keys %} empty-section{% endif %}" id="section-{{ map.id }}">
<div class="map-section-header">{{ map.name }}</div>
<table class="key-table">
<thead><tr>
<th style="width:40px"></th>
<th>Key</th>
<th>Priority</th>
<th>Quest</th>
<th>Maps</th>
<th>Note</th>
</tr></thead>
<tbody>
{% for key in map_keys %}{{ key_row(key) }}{% endfor %}
{% if not map_keys %}<tr><td colspan="6" class="no-results">No keys assigned to this map.</td></tr>{% endif %}
</tbody>
</table>
</div>
{% endfor %}
<div class="map-section{% if not unassigned_keys %} empty-section{% endif %}" id="section-unassigned">
<div class="map-section-header" style="color: var(--muted)">Unassigned</div>
<table class="key-table">
<thead><tr>
<th style="width:40px"></th>
<th>Key</th>
<th>Priority</th>
<th>Quest</th>
<th>Maps</th>
<th>Note</th>
</tr></thead>
<tbody>
{% for key in unassigned_keys %}{{ key_row(key, show_maps=false) }}{% endfor %}
{% if not unassigned_keys %}<tr><td colspan="6" class="no-results">All keys assigned to maps.</td></tr>{% endif %}
</tbody>
</table>
</div>
</div>
<script>
// ── save helper ─────────────────────────────────────────────
function saveKey(row) {
const id = row.dataset.keyId;
const priority = row.dataset.priority === '' ? null : parseInt(row.dataset.priority);
const payload = {
key_id: id,
priority: priority,
used_in_quest: row.dataset.quest === '1',
reason: row.dataset.reason,
map_ids: row.dataset.mapIds ? row.dataset.mapIds.split(',').filter(Boolean).map(Number) : [],
};
row.classList.add('saving');
fetch('/rate_json', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
}).then(() => row.classList.remove('saving'));
}
// ── priority: click badge → inline select → save on change ─
const PRIORITY_LABELS = ['IGNORE','LOW','MED','HIGH','SUPER'];
const PRIORITY_CLASSES = ['pb-0','pb-1','pb-2','pb-3','pb-4'];
function renderBadge(cell, val) {
const label = val === '' ? '—' : PRIORITY_LABELS[parseInt(val)];
const cls = val === '' ? 'pb-none' : PRIORITY_CLASSES[parseInt(val)];
cell.innerHTML = `<span class="priority-badge ${cls}" title="Click to change">${label}</span>`;
cell.querySelector('.priority-badge').addEventListener('click', () => openPrioritySelect(cell));
}
function openPrioritySelect(cell) {
const row = cell.closest('tr');
const current = row.dataset.priority;
const sel = document.createElement('select');
sel.className = 'priority-select';
sel.innerHTML = `<option value="">— unrated</option>` +
PRIORITY_LABELS.map((l, i) => `<option value="${i}">${l}</option>`).join('');
sel.value = current;
cell.innerHTML = '';
cell.appendChild(sel);
sel.focus();
sel.addEventListener('change', () => {
row.dataset.priority = sel.value;
renderBadge(cell, sel.value);
saveKey(row);
});
sel.addEventListener('blur', () => {
if (cell.contains(sel)) renderBadge(cell, row.dataset.priority);
});
}
// ── quest: click dot to toggle ──────────────────────────────
function renderQuest(cell, val) {
if (val === '1') {
cell.innerHTML = `<span class="quest-dot" title="Quest key — click to toggle"></span>`;
} else {
cell.innerHTML = `<span class="quest-empty" title="Not a quest key — click to toggle"></span>`;
}
cell.firstChild.addEventListener('click', () => {
const row = cell.closest('tr');
row.dataset.quest = row.dataset.quest === '1' ? '0' : '1';
renderQuest(cell, row.dataset.quest);
saveKey(row);
});
}
// ── maps: + button opens popover, checkboxes save on change ─
function bindMapPopover(row) {
const addBtn = row.querySelector('.map-add-btn');
const popover = row.querySelector('.map-popover');
const tagsDiv = row.querySelector('.map-tags');
addBtn.addEventListener('click', e => {
e.stopPropagation();
popover.classList.toggle('open');
});
popover.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', () => {
// rebuild map-ids from checked boxes
const checked = [...popover.querySelectorAll('input:checked')].map(c => c.value);
row.dataset.mapIds = checked.join(',');
// rebuild tag display
const names = [...popover.querySelectorAll('input:checked')]
.map(c => c.closest('label').textContent.trim());
tagsDiv.innerHTML = names.map(n => `<span class="map-tag">${n}</span>`).join('') +
`<span class="map-add-btn" title="Edit maps">+</span>`;
// re-bind the new add btn
tagsDiv.querySelector('.map-add-btn').addEventListener('click', e => {
e.stopPropagation();
popover.classList.toggle('open');
});
saveKey(row);
});
});
}
// ── note: click text → input, blur → save ──────────────────
function bindNote(row) {
const span = row.querySelector('.note-text');
const input = row.querySelector('.note-input');
span.addEventListener('click', () => {
span.style.display = 'none';
input.style.display = 'block';
input.focus();
input.select();
});
function commitNote() {
const val = input.value.trim();
row.dataset.reason = val;
span.textContent = val || 'add note…';
span.classList.toggle('empty', !val);
input.style.display = 'none';
span.style.display = 'block';
saveKey(row);
}
input.addEventListener('blur', commitNote);
input.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); commitNote(); }
if (e.key === 'Escape') { input.value = row.dataset.reason; commitNote(); }
});
}
// ── fuzzy search ────────────────────────────────────────────
function fuzzyMatch(query, target) {
if (!query) return true;
let qi = 0;
for (let i = 0; i < target.length && qi < query.length; i++) {
if (target[i] === query[qi]) qi++;
}
return qi === query.length;
}
function applySearch() {
const q = document.getElementById('key-search').value.toLowerCase().trim();
document.querySelectorAll('.map-section:not(.empty-section)').forEach(section => {
let visible = 0;
section.querySelectorAll('tr[data-key-id]').forEach(row => {
const name = row.querySelector('strong').textContent.toLowerCase();
const match = fuzzyMatch(q, name);
row.classList.toggle('hidden-row', !match);
if (match) visible++;
});
section.style.display = visible === 0 ? 'none' : '';
});
}
// ── close popovers when clicking outside ───────────────────
document.addEventListener('click', () => {
document.querySelectorAll('.map-popover.open').forEach(p => p.classList.remove('open'));
});
// ── init all rows ───────────────────────────────────────────
document.querySelectorAll('tr[data-key-id]').forEach(row => {
const priorityCell = row.querySelector('.priority-cell');
const questCell = row.querySelector('.quest-cell');
priorityCell.querySelector('.priority-badge')
.addEventListener('click', () => openPrioritySelect(priorityCell));
questCell.firstElementChild.addEventListener('click', () => {
row.dataset.quest = row.dataset.quest === '1' ? '0' : '1';
renderQuest(questCell, row.dataset.quest);
saveKey(row);
});
bindMapPopover(row);
bindNote(row);
});
</script>
</body>
</html>