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>
305 lines
11 KiB
Python
305 lines
11 KiB
Python
"""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",
|
|
)
|