feat: Implement Sprint 8 - Portfolio website expansion (MVP)

New pages:
- Home: Redesigned with hero, impact stats, featured project
- About: 6-section professional narrative
- Projects: Hub with 4 project cards and status badges
- Resume: Inline display with download placeholders
- Contact: Form UI (disabled) with contact info
- Blog: Markdown-based system with frontmatter support

Infrastructure:
- Blog system with markdown loader (python-frontmatter, markdown, pygments)
- Sidebar callback for active state highlighting on navigation
- Separated navigation into main pages and projects/dashboards groups

Closes #36, #37, #38, #39, #40, #41, #42, #43

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-15 15:40:01 -05:00
parent cd7b5ce154
commit 138e6fe497
15 changed files with 1937 additions and 134 deletions

View File

@@ -0,0 +1,304 @@
"""Projects overview page - Hub for all portfolio projects."""
from typing import Any
import dash
import dash_mantine_components as dmc
from dash import dcc
from dash_iconify import DashIconify
dash.register_page(__name__, path="/projects", name="Projects")
# Page intro
INTRO_TEXT = (
"These are projects I've built—some professional (anonymized where needed), "
"some personal. Each one taught me something. Use the sidebar to jump directly "
"to live dashboards or explore the overviews below."
)
# Project definitions
PROJECTS: list[dict[str, Any]] = [
{
"title": "Toronto Housing Market Dashboard",
"type": "Personal Project",
"status": "Live",
"status_color": "green",
"problem": (
"Toronto's housing market moves fast, and most publicly available data "
"is either outdated, behind paywalls, or scattered across dozens of sources. "
"I wanted a single dashboard that tracked trends in real-time."
),
"built": [
"Data Pipeline: Python scraper pulling listings data, automated on schedule",
"Transformation Layer: dbt-based SQL architecture (staging -> intermediate -> marts)",
"Visualization: Interactive Plotly-Dash dashboard with filters by neighborhood, price range, property type",
"Infrastructure: PostgreSQL backend, version-controlled in Git",
],
"tech_stack": "Python, dbt, PostgreSQL, Plotly-Dash, GitHub Actions",
"learned": (
"Real estate data is messy as hell. Listings get pulled, prices change, "
"duplicates are everywhere. Building a reliable pipeline meant implementing "
'serious data quality checks and learning to embrace "good enough" over "perfect."'
),
"dashboard_link": "/toronto",
"repo_link": "https://github.com/leomiranda/personal-portfolio",
},
{
"title": "US Retail Energy Price Predictor",
"type": "Personal Project",
"status": "Coming Soon",
"status_color": "yellow",
"problem": (
"Retail energy pricing in deregulated US markets is volatile and opaque. "
"Consumers and analysts lack accessible tools to understand pricing trends "
"and forecast where rates are headed."
),
"built": [
"Data Pipeline: Automated ingestion of public pricing data across multiple US markets",
"ML Model: Price prediction using time series forecasting (ARIMA, Prophet, or similar)",
"Transformation Layer: dbt-based SQL architecture for feature engineering",
"Visualization: Interactive dashboard showing historical trends + predictions by state/market",
],
"tech_stack": "Python, Scikit-learn, dbt, PostgreSQL, Plotly-Dash",
"learned": (
"This showcases the ML side of my skillset—something the Toronto Housing "
"dashboard doesn't cover. It also leverages my domain expertise from 5+ years "
"in retail energy operations."
),
"dashboard_link": None,
"repo_link": None,
},
{
"title": "DataFlow Platform",
"type": "Professional",
"status": "Case Study Pending",
"status_color": "gray",
"problem": (
"When I joined Summitt Energy, there was no data infrastructure. "
"Reports were manual. Insights were guesswork. I was hired to fix that."
),
"built": [
"v1 (2020): Basic ETL scripts pulling Genesys Cloud data into MSSQL",
"v2 (2021): Dimensional model (star schema) with fact/dimension tables",
"v3 (2022): Python refactor with SQLAlchemy ORM, batch processing, error handling",
"v4 (2023-24): dbt-pattern SQL views (staging -> intermediate -> marts), FastAPI layer, CLI tools",
],
"tech_stack": "Python, SQLAlchemy, FastAPI, MSSQL, Power BI, Genesys Cloud API",
"impact": [
"21 tables, 1B+ rows",
"5,000+ daily transactions processed",
"40% improvement in reporting efficiency",
"30% reduction in call abandon rate",
"50% faster Average Speed to Answer",
],
"learned": (
"Building data infrastructure as a team of one forces brutal prioritization. "
"I learned to ship imperfect solutions fast, iterate based on feedback, "
"and never underestimate how long stakeholder buy-in takes."
),
"note": "This is proprietary work. A sanitized case study with architecture patterns (no proprietary data) will be published in Phase 3.",
"dashboard_link": None,
"repo_link": None,
},
{
"title": "AI-Assisted Automation (Bandit Labs)",
"type": "Consulting/Side Business",
"status": "Active",
"status_color": "blue",
"problem": (
"Small businesses don't need enterprise data platforms—they need someone "
"to eliminate the 4 hours/week they spend manually entering receipts."
),
"built": [
"Receipt Processing Automation: OCR pipeline (Tesseract, Google Vision) extracting purchase data from photos",
"Product Margin Tracker: Plotly-Dash dashboard with real-time profitability insights",
"Claude Code Plugins: MCP servers for Gitea, Wiki.js, NetBox integration",
],
"tech_stack": "Python, Tesseract, Google Vision API, Plotly-Dash, QuickBooks API",
"learned": (
"Small businesses are underserved by the data/automation industry. "
"Everyone wants to sell them enterprise software they don't need. "
"I like solving problems at a scale where the impact is immediately visible."
),
"dashboard_link": None,
"repo_link": None,
"external_link": "/lab",
"external_label": "Learn More About Bandit Labs",
},
]
def create_project_card(project: dict[str, Any]) -> dmc.Paper:
"""Create a detailed project card."""
# Build the "What I Built" list
built_items = project.get("built", [])
built_section = (
dmc.Stack(
[
dmc.Text("What I Built:", fw=600, size="sm"),
dmc.List(
[dmc.ListItem(dmc.Text(item, size="sm")) for item in built_items],
spacing="xs",
size="sm",
),
],
gap="xs",
)
if built_items
else None
)
# Build impact section for DataFlow
impact_items = project.get("impact", [])
impact_section = (
dmc.Stack(
[
dmc.Text("Impact:", fw=600, size="sm"),
dmc.Group(
[
dmc.Badge(item, variant="light", size="sm")
for item in impact_items
],
gap="xs",
),
],
gap="xs",
)
if impact_items
else None
)
# Build action buttons
buttons = []
if project.get("dashboard_link"):
buttons.append(
dcc.Link(
dmc.Button(
"View Dashboard",
variant="light",
size="sm",
leftSection=DashIconify(icon="tabler:chart-bar", width=16),
),
href=project["dashboard_link"],
)
)
if project.get("repo_link"):
buttons.append(
dmc.Anchor(
dmc.Button(
"View Repository",
variant="subtle",
size="sm",
leftSection=DashIconify(icon="tabler:brand-github", width=16),
),
href=project["repo_link"],
target="_blank",
)
)
if project.get("external_link"):
buttons.append(
dcc.Link(
dmc.Button(
project.get("external_label", "Learn More"),
variant="outline",
size="sm",
leftSection=DashIconify(icon="tabler:arrow-right", width=16),
),
href=project["external_link"],
)
)
# Handle "Coming Soon" state
if project["status"] == "Coming Soon" and not buttons:
buttons.append(
dmc.Badge("Coming Soon", variant="light", color="yellow", size="lg")
)
return dmc.Paper(
dmc.Stack(
[
# Header
dmc.Group(
[
dmc.Stack(
[
dmc.Text(project["title"], fw=600, size="lg"),
dmc.Text(project["type"], size="sm", c="dimmed"),
],
gap=0,
),
dmc.Badge(
project["status"],
color=project["status_color"],
variant="light",
size="lg",
),
],
justify="space-between",
align="flex-start",
),
# Problem
dmc.Stack(
[
dmc.Text("The Problem:", fw=600, size="sm"),
dmc.Text(project["problem"], size="sm", c="dimmed"),
],
gap="xs",
),
# What I Built
built_section,
# Impact (if exists)
impact_section,
# Tech Stack
dmc.Group(
[
dmc.Text("Tech Stack:", fw=600, size="sm"),
dmc.Text(project["tech_stack"], size="sm", c="dimmed"),
],
gap="xs",
),
# What I Learned
dmc.Stack(
[
dmc.Text("What I Learned:", fw=600, size="sm"),
dmc.Text(project["learned"], size="sm", fs="italic"),
],
gap="xs",
),
# Note (if exists)
(
dmc.Alert(
project["note"],
color="gray",
variant="light",
)
if project.get("note")
else None
),
# Action buttons
dmc.Group(buttons, gap="sm") if buttons else None,
],
gap="md",
),
p="xl",
radius="md",
withBorder=True,
)
layout = dmc.Container(
dmc.Stack(
[
dmc.Title("Projects", order=1, ta="center"),
dmc.Text(
INTRO_TEXT, size="md", c="dimmed", ta="center", maw=700, mx="auto"
),
dmc.Divider(my="lg"),
*[create_project_card(project) for project in PROJECTS],
dmc.Space(h=40),
],
gap="xl",
),
size="md",
py="xl",
)