feat: add landing page, Improved key rating page UI
This commit is contained in:
41
app.py
41
app.py
@@ -69,20 +69,17 @@ _migrate_key_ids_and_maps()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
def landing():
|
||||
return render_template("landing.html")
|
||||
|
||||
|
||||
def _keys_context():
|
||||
conn = get_db()
|
||||
maps = conn.execute("""
|
||||
SELECT id, name
|
||||
FROM maps
|
||||
ORDER BY name
|
||||
""").fetchall()
|
||||
maps = conn.execute("SELECT id, name FROM maps ORDER BY name").fetchall()
|
||||
map_filter = request.args.get("map_id", type=int)
|
||||
sort = request.args.get("sort", "priority_desc")
|
||||
show = request.args.get("show", "all")
|
||||
key_map_rows = conn.execute("""
|
||||
SELECT key_id, map_id
|
||||
FROM key_maps
|
||||
""").fetchall()
|
||||
key_map_rows = conn.execute("SELECT key_id, map_id FROM key_maps").fetchall()
|
||||
key_maps = {}
|
||||
for row in key_map_rows:
|
||||
key_maps.setdefault(row["key_id"], set()).add(row["map_id"])
|
||||
@@ -107,9 +104,7 @@ def index():
|
||||
AND kmf.map_id = ?
|
||||
"""
|
||||
params.append(map_filter)
|
||||
key_query += """
|
||||
LEFT JOIN key_ratings r ON k.id = r.key_id
|
||||
"""
|
||||
key_query += " LEFT JOIN key_ratings r ON k.id = r.key_id "
|
||||
if show == "rated":
|
||||
key_query += " WHERE r.priority IS NOT NULL "
|
||||
elif show == "unrated":
|
||||
@@ -131,15 +126,13 @@ def index():
|
||||
conn.close()
|
||||
|
||||
key_maps = {k: sorted(v) for k, v in key_maps.items()}
|
||||
return render_template(
|
||||
"index.html",
|
||||
keys=keys,
|
||||
maps=maps,
|
||||
key_maps=key_maps,
|
||||
map_filter=map_filter,
|
||||
sort=sort,
|
||||
show=show,
|
||||
)
|
||||
return dict(keys=keys, maps=maps, key_maps=key_maps,
|
||||
map_filter=map_filter, sort=sort, show=show)
|
||||
|
||||
|
||||
@app.route("/keys")
|
||||
def keys_page():
|
||||
return render_template("keys.html", **_keys_context())
|
||||
|
||||
|
||||
@app.route("/rate", methods=["POST"])
|
||||
@@ -186,7 +179,7 @@ def rate_key():
|
||||
redirect_args["sort"] = sort
|
||||
if show:
|
||||
redirect_args["show"] = show
|
||||
base_url = url_for("index", **redirect_args)
|
||||
base_url = url_for("keys_page", **redirect_args)
|
||||
return redirect(f"{base_url}#key-{key_id}")
|
||||
|
||||
|
||||
@@ -245,7 +238,7 @@ def rate_all():
|
||||
redirect_args["sort"] = sort
|
||||
if show:
|
||||
redirect_args["show"] = show
|
||||
base_url = url_for("index", **redirect_args)
|
||||
base_url = url_for("keys_page", **redirect_args)
|
||||
if save_one:
|
||||
return redirect(f"{base_url}#key-{save_one}")
|
||||
return redirect(base_url)
|
||||
|
||||
@@ -290,7 +290,9 @@
|
||||
<body>
|
||||
<div class="page">
|
||||
<nav>
|
||||
<a href="/">← Keys</a>
|
||||
<a href="/">Home</a>
|
||||
|
|
||||
<a href="/keys">Keys</a>
|
||||
|
|
||||
<a href="/quests">Quest Trees</a>
|
||||
|
|
||||
|
||||
342
templates/keys.html
Normal file
342
templates/keys.html
Normal file
@@ -0,0 +1,342 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OnlyScavs – Key Ratings</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: sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
.page {
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
nav a {
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
padding: 4px 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
nav a:hover { color: var(--text); }
|
||||
nav a.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
margin: 0 0 16px;
|
||||
color: var(--accent);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* ── filters ───────────────────────────────────────────── */
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.filters label { color: var(--muted); font-size: 0.85rem; }
|
||||
select, input[type="text"] {
|
||||
background: #222;
|
||||
color: var(--text);
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 6px;
|
||||
padding: 7px 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
select:focus, input:focus { outline: 1px solid var(--accent); }
|
||||
button {
|
||||
background: #2a2a2a;
|
||||
color: var(--text);
|
||||
border: 1px solid #444;
|
||||
cursor: pointer;
|
||||
padding: 7px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
button:hover { background: #333; }
|
||||
|
||||
/* ── save-all bar ──────────────────────────────────────── */
|
||||
.save-all {
|
||||
margin: 4px 0 18px;
|
||||
}
|
||||
.save-all button {
|
||||
background: #1d3a52;
|
||||
border-color: #2d5a7a;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.save-all button:hover { background: #254a66; }
|
||||
|
||||
/* ── key card ──────────────────────────────────────────── */
|
||||
.key {
|
||||
display: grid;
|
||||
grid-template-columns: 56px 1fr auto;
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
padding: 14px 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.key:hover { background: #161616; }
|
||||
|
||||
.key-thumb {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 6px;
|
||||
background: #1a1a1a;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* left column: name + tags */
|
||||
.key-info { min-width: 0; }
|
||||
.key-title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.key-title strong { font-size: 0.95rem; line-height: 1.3; }
|
||||
.key-title a { color: var(--accent); font-size: 0.8rem; }
|
||||
.map-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.map-tag {
|
||||
background: #1e2e1e;
|
||||
border: 1px solid #2a3f2a;
|
||||
color: #8fbc8f;
|
||||
border-radius: 999px;
|
||||
padding: 1px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* right column: controls */
|
||||
.key-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.priority-select {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
/* priority colour coding */
|
||||
option[value="4"], select.p4 { color: #ff6b6b; }
|
||||
option[value="3"], select.p3 { color: var(--accent2); }
|
||||
option[value="2"], select.p2 { color: #9ccfff; }
|
||||
option[value="1"], select.p1 { color: var(--muted); }
|
||||
option[value="0"], select.p0 { color: #555; }
|
||||
|
||||
.quest-flag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 5px 9px;
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.quest-flag input { margin: 0; cursor: pointer; }
|
||||
|
||||
.note-input {
|
||||
flex: 1 1 160px;
|
||||
min-width: 120px;
|
||||
max-width: 260px;
|
||||
font-size: 0.85rem;
|
||||
padding: 6px 9px;
|
||||
}
|
||||
|
||||
.map-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
.map-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.map-checkbox input { margin: 0; cursor: pointer; }
|
||||
|
||||
.save-btn {
|
||||
font-size: 0.82rem;
|
||||
padding: 6px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a { color: var(--accent); }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.key {
|
||||
grid-template-columns: 48px 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
.key-controls {
|
||||
grid-column: 1 / -1;
|
||||
min-width: unset;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.key { grid-template-columns: 1fr; }
|
||||
.key-thumb { width: 48px; height: 48px; }
|
||||
select, input, button { min-height: 38px; font-size: 0.95rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page">
|
||||
<nav>
|
||||
<a href="/">Home</a>
|
||||
<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>
|
||||
</nav>
|
||||
<h1>Key Ratings</h1>
|
||||
|
||||
<form method="get" class="filters">
|
||||
<label for="map_id">Map</label>
|
||||
<select id="map_id" name="map_id">
|
||||
<option value="">All maps</option>
|
||||
{% for map in maps %}
|
||||
<option value="{{ map.id }}" {% if map_filter == map.id %}selected{% endif %}>
|
||||
{{ map.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="show">Show</label>
|
||||
<select id="show" name="show">
|
||||
<option value="all" {% if show == "all" %}selected{% endif %}>All</option>
|
||||
<option value="rated" {% if show == "rated" %}selected{% endif %}>Rated</option>
|
||||
<option value="unrated" {% if show == "unrated" %}selected{% endif %}>Unrated</option>
|
||||
<option value="quest" {% if show == "quest" %}selected{% endif %}>Quest keys</option>
|
||||
</select>
|
||||
|
||||
<label for="sort">Sort</label>
|
||||
<select id="sort" name="sort">
|
||||
<option value="priority_desc" {% if sort == "priority_desc" %}selected{% endif %}>Priority ↓</option>
|
||||
<option value="priority_asc" {% if sort == "priority_asc" %}selected{% endif %}>Priority ↑</option>
|
||||
<option value="name_asc" {% if sort == "name_asc" %}selected{% endif %}>Name A–Z</option>
|
||||
<option value="name_desc" {% if sort == "name_desc" %}selected{% endif %}>Name Z–A</option>
|
||||
</select>
|
||||
|
||||
<button type="submit">Apply</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/rate_all">
|
||||
{% if map_filter %}<input type="hidden" name="map_id" value="{{ map_filter }}">{% endif %}
|
||||
{% if sort %}<input type="hidden" name="sort" value="{{ sort }}">{% endif %}
|
||||
{% if show %}<input type="hidden" name="show" value="{{ show }}">{% endif %}
|
||||
|
||||
<div class="save-all">
|
||||
<button type="submit" name="save_all" value="1">Save all changes</button>
|
||||
</div>
|
||||
|
||||
{% for key in keys %}
|
||||
{% set selected_maps = key_maps.get(key.id, []) %}
|
||||
<div class="key" id="key-{{ key.id }}">
|
||||
|
||||
<img class="key-thumb" src="{{ key.grid_image_url }}" loading="lazy" alt="">
|
||||
|
||||
<div class="key-info">
|
||||
<div class="key-title">
|
||||
<strong>{{ key.name }}</strong>
|
||||
{% if key.wiki_url %}<a href="{{ key.wiki_url }}" target="_blank">wiki ↗</a>{% endif %}
|
||||
</div>
|
||||
{% if selected_maps %}
|
||||
<div class="map-tags">
|
||||
{% for map in maps %}
|
||||
{% if map.id in selected_maps %}
|
||||
<span class="map-tag">{{ map.name }}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="key-controls">
|
||||
<input type="hidden" name="key_ids" value="{{ key.id }}">
|
||||
|
||||
<select name="priority_{{ key.id }}" class="priority-select">
|
||||
<option value="" {% if key.priority is none %}selected{% endif %}>— unrated</option>
|
||||
{% for i, label in [(0,'IGNORE'),(1,'LOW'),(2,'MED'),(3,'HIGH'),(4,'SUPER')] %}
|
||||
<option value="{{ i }}" {% if key.priority == i %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label class="quest-flag" title="Used in a quest?">
|
||||
<input type="checkbox" name="used_in_quest_{{ key.id }}" {% if key.used_in_quest %}checked{% endif %}>
|
||||
<span>Quest</span>
|
||||
</label>
|
||||
|
||||
<input class="note-input" name="reason_{{ key.id }}" placeholder="note…" value="{{ key.reason or '' }}">
|
||||
|
||||
<div class="map-list">
|
||||
{% for map in maps %}
|
||||
<label class="map-checkbox">
|
||||
<input type="checkbox" name="map_ids_{{ key.id }}" value="{{ map.id }}"
|
||||
{% if map.id in selected_maps %}checked{% endif %}>
|
||||
<span>{{ map.name }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button class="save-btn" type="submit" name="save_one" value="{{ key.id }}">Save</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="save-all">
|
||||
<button type="submit" name="save_all" value="1">Save all changes</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
120
templates/landing.html
Normal file
120
templates/landing.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OnlyScavs</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #121212;
|
||||
--panel: #1a1a1a;
|
||||
--text: #eee;
|
||||
--muted: #888;
|
||||
--border: #2a2a2a;
|
||||
--accent: #9ccfff;
|
||||
--accent2: #ffd580;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
.hero {
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: 2.4rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--accent);
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.hero p {
|
||||
color: var(--muted);
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 860px;
|
||||
}
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 24px 20px;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
transition: border-color 0.15s, background 0.15s, transform 0.1s;
|
||||
}
|
||||
.card:hover {
|
||||
border-color: #444;
|
||||
background: #1e1e1e;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.card-icon {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
}
|
||||
.card-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hero">
|
||||
<h1>OnlyScavs</h1>
|
||||
<p>Escape from Tarkov reference tools</p>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<a class="card" href="/keys">
|
||||
<div class="card-icon">🗝</div>
|
||||
<div class="card-title">Key Ratings</div>
|
||||
<div class="card-desc">Rate and filter keys by map, priority, and quest use.</div>
|
||||
</a>
|
||||
<a class="card" href="/collector">
|
||||
<div class="card-icon">★</div>
|
||||
<div class="card-title">Collector</div>
|
||||
<div class="card-desc">Track your progress toward the Kappa container.</div>
|
||||
</a>
|
||||
<a class="card" href="/quests">
|
||||
<div class="card-icon">📋</div>
|
||||
<div class="card-title">Quest Trees</div>
|
||||
<div class="card-desc">Visualize quest chains and trader dependencies.</div>
|
||||
</a>
|
||||
<a class="card" href="/loadout">
|
||||
<div class="card-icon">🎽</div>
|
||||
<div class="card-title">Loadout Planner</div>
|
||||
<div class="card-desc">Browse and compare guns, armor, rigs, and more.</div>
|
||||
</a>
|
||||
<a class="card" href="/meds">
|
||||
<div class="card-icon">💉</div>
|
||||
<div class="card-title">Injectors</div>
|
||||
<div class="card-desc">Compare stim effects, skills, and side effects.</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -222,8 +222,10 @@
|
||||
<body>
|
||||
<div class="page">
|
||||
<nav>
|
||||
<a href="/">← Keys</a> |
|
||||
<a href="/">Home</a> |
|
||||
<a href="/keys">Keys</a> |
|
||||
<a href="/collector">Collector</a> |
|
||||
<a href="/quests">Quests</a> |
|
||||
<a href="/meds">Injectors</a>
|
||||
</nav>
|
||||
<h1>Loadout Planner</h1>
|
||||
|
||||
@@ -247,7 +247,8 @@
|
||||
<div class="page">
|
||||
|
||||
<nav>
|
||||
<a href="/">Key Ratings</a>
|
||||
<a href="/">Home</a>
|
||||
<a href="/keys">Keys</a>
|
||||
<a href="/quests">Quest Tree</a>
|
||||
<a href="/collector">Collector</a>
|
||||
<a href="/loadout">Loadout</a>
|
||||
|
||||
@@ -227,7 +227,9 @@
|
||||
<body>
|
||||
<div class="page">
|
||||
<nav>
|
||||
<a href="/">← Keys</a>
|
||||
<a href="/">Home</a>
|
||||
|
|
||||
<a href="/keys">Keys</a>
|
||||
|
|
||||
<a href="/collector">Collector Checklist</a>
|
||||
|
|
||||
|
||||
Reference in New Issue
Block a user