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:
304
portfolio_app/pages/projects.py
Normal file
304
portfolio_app/pages/projects.py
Normal 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",
|
||||
)
|
||||
Reference in New Issue
Block a user