1- """QuantLabs — Welcome (portfolio) + All projects + System Health.
1+ """QuantLabs — All projects + System Health.
22
33Lives at /QuantLabs. The app root (/) is the resume page in app.py; this
44page is the actual project hub that users reach by clicking `ENTER
77
88from pathlib import Path
99import sys
10- import json
1110import time
1211
13- import pandas as pd
1412import streamlit as st
15- import streamlit .components .v1 as components
1613import requests
1714
1815sys .path .insert (0 , str (Path (__file__ ).resolve ().parents [1 ] / "lib" ))
2320from ci_status import fetch_ci_status
2421from 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
3326GITHUB_REPO = "mish-codes/QuantLab"
3427
3528st .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-
16293def _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 = " · " .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
292113def _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
392127with 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