Skip to content

Commit f78affb

Browse files
mish-codesclaude
andcommitted
feat: replace Welcome tab with collapsible All projects sections
Remove Welcome tab entirely. All projects tab now shows categories as st.expander sections (Half-baked collapsed by default). Strips ~150 lines of dead code — stats counter, marquee, featured grid, search, and unused style constants. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 23e401f commit f78affb

1 file changed

Lines changed: 12 additions & 301 deletions

File tree

dashboard/pages/0_QuantLabs.py

Lines changed: 12 additions & 301 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""QuantLabs — Welcome (portfolio) + All projects + System Health.
1+
"""QuantLabs — All projects + System Health.
22
33
Lives at /QuantLabs. The app root (/) is the resume page in app.py; this
44
page is the actual project hub that users reach by clicking `ENTER
@@ -7,12 +7,9 @@
77

88
from pathlib import Path
99
import sys
10-
import json
1110
import time
1211

13-
import pandas as pd
1412
import streamlit as st
15-
import streamlit.components.v1 as components
1613
import requests
1714

1815
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "lib"))
@@ -23,13 +20,9 @@
2320
from ci_status import fetch_ci_status
2421
from projects import (
2522
PROJECTS_BY_CATEGORY,
26-
FEATURED_KEYS,
27-
all_projects,
28-
featured,
2923
category_with_capstones_last,
3024
)
3125

32-
HERE = Path(__file__).parent
3326
GITHUB_REPO = "mish-codes/QuantLab"
3427

3528
st.set_page_config(
@@ -56,23 +49,6 @@ def _page_url(page_link: str) -> str:
5649
return "/" + name
5750

5851

59-
_FEATURED_CARD_STYLE = (
60-
"display:block;text-decoration:none;color:#1a1a1a;"
61-
"background:#ffffff;border:1px solid #e5e5e5;border-radius:4px;"
62-
"padding:1.6rem 1.5rem;transition:border-color 0.15s;"
63-
)
64-
_FEATURED_TITLE_STYLE = (
65-
"font-family:'Fraunces',Georgia,serif !important;font-size:1.3rem;font-weight:600;"
66-
"color:#d97706;margin:0 0 0.5rem;line-height:1.2;letter-spacing:-0.01em;"
67-
)
68-
_FEATURED_DESC_STYLE = (
69-
"font-family:'Inter',system-ui,sans-serif !important;font-size:0.92rem;line-height:1.5;"
70-
"color:#1a1a1a;margin:0 0 0.9rem;"
71-
)
72-
_FEATURED_TECH_STYLE = (
73-
"font-family:'Inter',system-ui,sans-serif !important;font-size:0.74rem;color:#6b6b6b;"
74-
"letter-spacing:0.01em;"
75-
)
7652
_CAT_CARD_STYLE = (
7753
"display:block;text-decoration:none;color:#1a1a1a;"
7854
"background:#ffffff;border:1px solid #e5e5e5;border-radius:4px;"
@@ -109,56 +85,11 @@ def _page_url(page_link: str) -> str:
10985
"Tech demos & references": {"bg": "#fef3c7", "fg": "#92400e"},
11086
"Half-baked": {"bg": "#f4f4f4", "fg": "#6b6b6b"},
11187
}
112-
_CAT_CHIP_FALLBACK = {"bg": "#f4f4f4", "fg": "#6b6b6b"}
113-
_CARD_CTA_STYLE = (
114-
"font-family:'Inter',system-ui,sans-serif !important;font-size:0.68rem;"
115-
"font-weight:600;color:#d97706;letter-spacing:0.04em;margin-top:0.5rem;"
116-
)
117-
_FEATURED_GRID_STYLE = (
118-
"display:grid;grid-template-columns:repeat(3,1fr);gap:1.25rem;margin-bottom:1rem;"
119-
)
12088
_CAT_GRID_STYLE = (
12189
"display:grid;grid-template-columns:repeat(3,1fr);gap:0.75rem;margin-bottom:0.5rem;"
12290
)
12391

12492

125-
def _featured_card_html(p) -> str:
126-
tech = " · ".join(_escape(t) for t in p.tech)
127-
return (
128-
f'<a style="{_FEATURED_CARD_STYLE}" href="{_escape(_page_url(p.page_link))}" target="_self">'
129-
f'<div style="{_FEATURED_TITLE_STYLE}">{_escape(p.label)}</div>'
130-
f'<div style="{_FEATURED_DESC_STYLE}">{_escape(p.description)}</div>'
131-
f'<div style="{_FEATURED_TECH_STYLE}">{tech}</div>'
132-
f'</a>'
133-
)
134-
135-
136-
def _all_projects_card_html(p, category: str) -> str:
137-
"""Card for the All projects tab — includes a category-coloured chip
138-
since these cards are not grouped by category, plus an explicit `Open →`
139-
footer so users can see that the card is clickable."""
140-
tech = " · ".join(_escape(t) for t in p.tech)
141-
capstone = (
142-
f'<span style="{_CAPSTONE_STYLE}">Capstone</span>' if p.is_capstone else ""
143-
)
144-
colors = _CAT_CHIP_PALETTE.get(category, _CAT_CHIP_FALLBACK)
145-
chip_style = (
146-
f'{_CAT_CHIP_BASE_STYLE}background:{colors["bg"]};color:{colors["fg"]};'
147-
)
148-
cat_chip = f'<div style="{chip_style}">{_escape(category)}</div>'
149-
cta = f'<div style="{_CARD_CTA_STYLE}">OPEN →</div>'
150-
return (
151-
f'<a class="ql-all-card" style="{_CAT_CARD_STYLE}" '
152-
f'href="{_escape(_page_url(p.page_link))}" target="_self">'
153-
f'{cat_chip}'
154-
f'<div style="{_CAT_TITLE_STYLE}">{_escape(p.label)}{capstone}</div>'
155-
f'<div style="{_CAT_DESC_STYLE}">{_escape(p.description)}</div>'
156-
f'<div style="{_CAT_TECH_STYLE}">{tech}</div>'
157-
f'{cta}'
158-
f'</a>'
159-
)
160-
161-
16293
def _cat_card_html(p) -> str:
16394
tech = " · ".join(_escape(t) for t in p.tech)
16495
capstone = (
@@ -173,121 +104,11 @@ def _cat_card_html(p) -> str:
173104
)
174105

175106

176-
def _matches_query(p, query: str) -> bool:
177-
if not query:
178-
return True
179-
q = query.lower()
180-
if q in p.label.lower():
181-
return True
182-
if q in p.description.lower():
183-
return True
184-
if any(q in t.lower() for t in p.tech):
185-
return True
186-
return False
187-
188-
189-
# ─────────────────────────────────────────────────────────────
190-
# Animated stats counter (replaces the graph)
191-
# ─────────────────────────────────────────────────────────────
192-
193-
def _build_stats_counter_html(total: int, n_cats: int, n_featured: int) -> str:
194-
"""Self-contained HTML+JS that animates three counters from 0 to target."""
195-
return f"""
196-
<!doctype html>
197-
<html><head><meta charset="utf-8"><style>
198-
body {{ margin: 0; font-family: 'JetBrains Mono', Menlo, Consolas, monospace;
199-
background: transparent; color: #6b6b6b; }}
200-
.stats {{ display: flex; justify-content: center; gap: 2.2rem;
201-
font-size: 0.86rem; letter-spacing: 0.05em; padding-top: 4px; }}
202-
.stat {{ display: inline-flex; gap: 0.4em; align-items: baseline; }}
203-
.num {{ color: #1a1a1a; font-weight: 600; font-size: 1.05rem; }}
204-
.sep {{ color: #cccccc; }}
205-
</style></head>
206-
<body>
207-
<div class="stats">
208-
<span class="stat"><span class="num" data-target="{total}">0</span> projects</span>
209-
<span class="sep">·</span>
210-
<span class="stat"><span class="num" data-target="{n_cats}">0</span> categories</span>
211-
<span class="sep">·</span>
212-
<span class="stat"><span class="num" data-target="{n_featured}">0</span> featured</span>
213-
<span class="sep">·</span>
214-
<span class="stat">open source</span>
215-
</div>
216-
<script>
217-
document.querySelectorAll('.num').forEach(el => {{
218-
const target = parseInt(el.dataset.target, 10);
219-
const duration = 900;
220-
const start = performance.now();
221-
function tick(now) {{
222-
const t = Math.min(1, (now - start) / duration);
223-
const eased = 1 - Math.pow(1 - t, 3);
224-
el.textContent = Math.round(target * eased);
225-
if (t < 1) requestAnimationFrame(tick);
226-
}}
227-
requestAnimationFrame(tick);
228-
}});
229-
</script>
230-
</body></html>
231-
"""
232-
233-
234-
def _build_marquee_html() -> str:
235-
"""Horizontal infinite-scroll marquee of project names, rendered inside
236-
a components iframe so the @keyframes animation survives Streamlit's
237-
HTML sanitizer."""
238-
names = [p.label for p in all_projects()]
239-
items = " &nbsp;·&nbsp; ".join(_escape(n) for n in names)
240-
return f"""
241-
<!doctype html>
242-
<html><head><meta charset="utf-8"><style>
243-
html, body {{ margin: 0; background: transparent; }}
244-
.wrap {{
245-
overflow: hidden;
246-
border-top: 1px solid #e5e5e5;
247-
border-bottom: 1px solid #e5e5e5;
248-
padding: 0.85rem 0;
249-
background: #fafafa;
250-
-webkit-mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
251-
mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
252-
}}
253-
.track {{
254-
display: flex;
255-
width: max-content;
256-
animation: ql-scroll 55s linear infinite;
257-
}}
258-
.content {{
259-
font-family: 'JetBrains Mono', Menlo, Consolas, monospace;
260-
font-size: 0.82rem;
261-
color: #6b6b6b;
262-
letter-spacing: 0.04em;
263-
padding-right: 2.5rem;
264-
white-space: nowrap;
265-
}}
266-
@keyframes ql-scroll {{
267-
from {{ transform: translateX(0); }}
268-
to {{ transform: translateX(-50%); }}
269-
}}
270-
</style></head>
271-
<body>
272-
<div class="wrap">
273-
<div class="track">
274-
<span class="content">{items}</span>
275-
<span class="content" aria-hidden="true">{items}</span>
276-
</div>
277-
</div>
278-
</body></html>
279-
"""
280-
281-
282107
# ─────────────────────────────────────────────────────────────
283108
# Tabs
284109
# ─────────────────────────────────────────────────────────────
285110

286-
tab_all, tab_welcome, tab_health = st.tabs(["All projects", "Welcome", "System Health"])
287-
288-
# ─────────────────────────────────────────────────────────────
289-
# Welcome — landing portfolio view
290-
# ─────────────────────────────────────────────────────────────
111+
tab_all, tab_health = st.tabs(["All projects", "System Health"])
291112

292113
def _ql_html(html: str) -> None:
293114
"""Render raw HTML without Streamlit's markdown processor eating class
@@ -299,105 +120,17 @@ def _ql_html(html: str) -> None:
299120
st.markdown(html, unsafe_allow_html=True)
300121

301122

302-
with tab_welcome:
303-
# Hero — use st.markdown so Streamlit wraps the heading with its action
304-
# elements, which plays with our .ql-hero-title * descendant selector.
305-
st.markdown(
306-
'<div class="ql-hero">'
307-
'<h1 class="ql-hero-title">QuantLabs</h1>'
308-
'<p class="ql-hero-subtitle">Interactive finance and data experiments in Python</p>'
309-
'</div>',
310-
unsafe_allow_html=True,
311-
)
312-
313-
total = len(all_projects())
314-
n_cats = len(PROJECTS_BY_CATEGORY)
315-
n_featured = len(featured())
316-
317-
# Search box
318-
_ql_html('<div class="ql-search-wrap">')
319-
search_query = st.text_input(
320-
"Search",
321-
value="",
322-
placeholder="Search projects by name, description, or tech…",
323-
label_visibility="collapsed",
324-
key="ql_landing_search",
325-
)
326-
_ql_html('</div>')
327-
328-
# Animated stats counter
329-
components.html(
330-
_build_stats_counter_html(total, n_cats, n_featured),
331-
height=60,
332-
scrolling=False,
333-
)
334-
335-
# Tech marquee — rendered via components.html so @keyframes survives
336-
components.html(_build_marquee_html(), height=90, scrolling=False)
337-
338-
# About paragraph
339-
_ql_html(
340-
'<p style="font-family:\'Inter\',system-ui,sans-serif;font-size:0.95rem;'
341-
'line-height:1.7;color:#4a4a4a;max-width:680px;margin:1.5rem 0 2rem;">'
342-
"Two threads run through these projects: risk and pricing — VaR, portfolio "
343-
"optimisation, Monte Carlo simulation, full-stack risk pipelines — and market "
344-
"data engineering — time series decomposition, anomaly detection, benchmark "
345-
"rate feeds. Everything is built in Python, deployed on Streamlit Cloud and "
346-
"Render, with source on GitHub."
347-
"</p>"
348-
)
349-
350-
# Featured grid
351-
st.markdown('<h2 class="ql-section-heading">Featured</h2>', unsafe_allow_html=True)
352-
featured_html = (
353-
f'<div style="{_FEATURED_GRID_STYLE}">'
354-
+ ''.join(_featured_card_html(p) for p in featured())
355-
+ '</div>'
356-
)
357-
_ql_html(featured_html)
358-
359-
# Categorised grids (filtered by search)
360-
for category in PROJECTS_BY_CATEGORY.keys():
361-
matching = [
362-
p for p in category_with_capstones_last(category)
363-
if _matches_query(p, search_query)
364-
]
365-
if not matching:
366-
continue
367-
st.markdown(
368-
f'<h2 class="ql-section-heading">{_escape(category)}</h2>',
369-
unsafe_allow_html=True,
370-
)
371-
grid_html = (
372-
f'<div style="{_CAT_GRID_STYLE}">'
373-
+ ''.join(_cat_card_html(p) for p in matching)
374-
+ '</div>'
375-
)
376-
_ql_html(grid_html)
377-
378-
if search_query and not any(
379-
_matches_query(p, search_query) for p in all_projects()
380-
):
381-
_ql_html(
382-
f'<p style="text-align:center;color:#6b6b6b;margin-top:2rem;">'
383-
f'No projects match "{_escape(search_query)}".</p>'
384-
)
385-
386-
_ql_html('<div style="height: 4rem;"></div>')
387-
388123
# ─────────────────────────────────────────────────────────────
389-
# All projects — sortable table view
124+
# All projects — collapsible category sections
390125
# ─────────────────────────────────────────────────────────────
391126

392127
with tab_all:
393128
st.markdown(
394129
'<h1 class="ql-page-title">All projects</h1>'
395-
'<p class="ql-page-subtitle">Every QuantLabs project at a glance, alphabetical</p>',
130+
'<p class="ql-page-subtitle">Every QuantLabs project, grouped by category</p>',
396131
unsafe_allow_html=True,
397132
)
398133

399-
# Local CSS: staggered fade-in on load + lift on hover. Scoped to
400-
# .ql-all-card so nothing else on the page inherits these animations.
401134
_ql_html(
402135
"""
403136
<style>
@@ -416,8 +149,6 @@ def _ql_html(html: str) -> None:
416149
box-shadow: 0 6px 18px rgba(26, 26, 26, 0.08);
417150
border-color: #d97706 !important;
418151
}
419-
/* Stagger: each card waits a bit longer before its fade-in starts.
420-
Covers up to ~30 cards; anything beyond that just snaps in. */
421152
.ql-all-card:nth-child(1) { animation-delay: 0.00s; }
422153
.ql-all-card:nth-child(2) { animation-delay: 0.03s; }
423154
.ql-all-card:nth-child(3) { animation-delay: 0.06s; }
@@ -433,34 +164,14 @@ def _ql_html(html: str) -> None:
433164
"""
434165
)
435166

436-
# Flatten the category map into [(category, project), …] then sort so
437-
# that projects in FEATURED_KEYS appear first in that order ("funnest
438-
# on top") and everything else falls through alphabetically.
439-
# Category becomes a colour-coded chip on each card so the info that
440-
# used to live in the table's Category column stays visible.
441-
all_with_category: list[tuple[str, object]] = []
442-
for category, projs in PROJECTS_BY_CATEGORY.items():
443-
for p in projs:
444-
all_with_category.append((category, p))
445-
446-
_featured_index = {key: i for i, key in enumerate(FEATURED_KEYS)}
447-
448-
def _sort_key(item: tuple[str, object]):
449-
_, proj = item
450-
if proj.key in _featured_index:
451-
# Featured group — (0, position-in-FEATURED_KEYS, label)
452-
return (0, _featured_index[proj.key], proj.label.lower())
453-
# Everything else — (1, 0, label) so it sorts alphabetically after
454-
return (1, 0, proj.label.lower())
455-
456-
all_with_category.sort(key=_sort_key)
457-
458-
grid_html = (
459-
f'<div style="{_CAT_GRID_STYLE}">'
460-
+ ''.join(_all_projects_card_html(p, category) for category, p in all_with_category)
461-
+ '</div>'
462-
)
463-
_ql_html(grid_html)
167+
for category in PROJECTS_BY_CATEGORY:
168+
projs = category_with_capstones_last(category)
169+
with st.expander(f"{category} ({len(projs)})", expanded=(category != "Half-baked")):
170+
_ql_html(
171+
f'<div style="{_CAT_GRID_STYLE}">'
172+
+ ''.join(_cat_card_html(p) for p in projs)
173+
+ '</div>'
174+
)
464175

465176
# ─────────────────────────────────────────────────────────────
466177
# System Health — preserved from original

0 commit comments

Comments
 (0)