Filter buttons, prompt counts per platform, and active state highlighting — all from Python dictionaries
Day 29 of 80
Yesterday's app shows all prompts. Today you add a filter bar at the top: "All (12)", "Kling (5)", "Runway (4)", "Veo (3)". Clicking a button shows only prompts from that platform. The active button is highlighted. This requires two things: a counts dictionary (built in Python) and a filter route (a new URL pattern).
The grouping pattern is identical to the scene planner you built in Week 2. Same logic, new context.
Before writing the route, understand the data structure you need. You want a dictionary like {"Kling": 5, "Runway": 4, "Veo": 3}. Here's how to build it:
prompts = load_prompts()
# Start with an empty dictionary
counts = {}
for p in prompts:
plat = p["platform"]
# .get(plat, 0) returns current count, or 0 if platform not seen yet
counts[plat] = counts.get(plat, 0) + 1
# Result: {"Kling": 5, "Runway": 4, "Veo": 3}
print(counts)
counts.get(plat, 0) — The first time you see "Kling", counts["Kling"] doesn't exist yet and would raise a KeyError. Using .get(plat, 0) returns 0 as a default instead of crashing. Then you add 1, giving you 1 for the first occurrence. Each subsequent "Kling" increments the count.
collections.Counter(p["platform"] for p in prompts) — but the manual version makes the logic explicit.
Add this new route to app.py, below your existing three routes:
@app.route("/filter/<platform>")
def filter_by_platform(platform):
all_prompts = load_prompts()
# Build counts across ALL prompts (not filtered)
counts = {}
for p in all_prompts:
plat = p["platform"]
counts[plat] = counts.get(plat, 0) + 1
# Filter down to the requested platform
filtered = [p for p in all_prompts if p["platform"] == platform]
return render_template(
"index.html",
prompts=filtered,
all_count=len(all_prompts),
counts=counts,
active_filter=platform
)
<platform> — No type converter here (unlike <int:index>). Flask passes the URL segment as a string. So /filter/Kling calls this function with platform="Kling".
filtered, the non-active platform tabs would show 0.
active_filter=platform — You pass the currently selected platform name to the template so it can highlight the right button.
The home route now also needs to pass counts, all_count, and active_filter so the filter bar renders correctly on the main page too:
@app.route("/")
def home():
prompts = load_prompts()
counts = {}
for p in prompts:
plat = p["platform"]
counts[plat] = counts.get(plat, 0) + 1
return render_template(
"index.html",
prompts=prompts,
all_count=len(prompts),
counts=counts,
active_filter="all"
)
active_filter="all" — The home route shows everything, so the "All" button should appear active/highlighted. Passing a string "all" lets the template check {% if active_filter == "all" %}.
Add this block to templates/index.html, right after the subtitle line and before the prompt grid:
{# ── Filter Bar ── #}
<div class="filter-bar">
<a href="/"
style="background: {{ '#2563eb' if active_filter == 'all' else '#1a1a1a' }}"
class="filter-btn">
All <span class="count">{{ all_count }}</span>
</a>
{% for platform, count in counts.items() %}
<a href="/filter/{{ platform }}"
style="background: {{ '#2563eb' if active_filter == platform else '#1a1a1a' }}"
class="filter-btn">
{{ platform }} <span class="count">{{ count }}</span>
</a>
{% endfor %}
</div>
{{ 'value_if_true' if condition else 'value_if_false' }}. This is the Jinja2 equivalent of Python's 'a' if x else 'b'. Here it switches the button background between active blue and dark grey.
counts.items() — Works identically to Python. In Jinja2, you can call most dictionary methods directly. This iterates key-value pairs so you get both the platform name and its count.
Add these CSS rules to the <style> block in index.html:
/* Filter bar */
.filter-bar { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
.filter-btn {
display: inline-flex; align-items: center; gap: 0.4rem;
padding: 0.4rem 1rem; border-radius: 20px; font-size: 0.85rem;
font-weight: 500; color: #e5e5e5; text-decoration: none;
border: 1px solid #2a2a2a; transition: background 0.15s;
}
.filter-btn:hover { border-color: #444; }
.filter-btn .count {
background: rgba(255,255,255,0.15); border-radius: 10px;
padding: 0.1rem 0.45rem; font-size: 0.75rem;
}
style attribute from Jinja2 above, which overrides the CSS. This keeps the CSS clean — you only style the shared shape, and Python controls which one is "active."
$ python app.py
* Running on http://127.0.0.1:5000
# Visit http://localhost:5000 — filter bar appears at top
# Click "Kling" — URL becomes /filter/Kling, only Kling cards show
# Click "All" — URL back to /, all cards show
Add a "Copy" button to each prompt card that copies the prompt text to the clipboard. Add this to your index.html:
<!-- Inside each .prompt-card, after the prompt-text paragraph -->
<button class="copy-btn"
onclick="copyPrompt(this, '{{ p.prompt | replace(\"'\", \"\\\\'\") }}')">
Copy
</button>
<script>
function copyPrompt(btn, text) {
navigator.clipboard.writeText(text).then(() => {
btn.textContent = "Copied!";
setTimeout(() => btn.textContent = "Copy", 1500);
});
}
</script>
navigator.clipboard.writeText() — The modern browser API for copying text. Returns a Promise, so you chain .then() to run code after it succeeds. The button text changes to "Copied!" for 1.5 seconds, then resets.
replace("'", "\\'") filter prevents prompt text with single quotes from breaking the JavaScript string. In production you'd use a data attribute (data-prompt="{{ p.prompt }}") instead — safer and cleaner. Try that as a further stretch.
/filter/<platform> route to app.pyhome() to pass counts, all_count, and active_filterindex.html with platform counts{{ 'a' if condition else 'b' }}Tomorrow is the Week 6 review and milestone check. You'll consolidate everything you've built this week, make sure the Prompt Vault web app is solid, and get a preview of Week 7: wiring Claude into the browser so you can generate prompts with a button click.