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,287 @@
"""Contact page - Form UI and direct contact information."""
import dash
import dash_mantine_components as dmc
from dash_iconify import DashIconify
dash.register_page(__name__, path="/contact", name="Contact")
# Contact information
CONTACT_INFO = {
"email": "leobrmi@hotmail.com",
"phone": "(416) 859-7936",
"linkedin": "https://linkedin.com/in/leobmiranda",
"github": "https://github.com/leomiranda",
"location": "Toronto, ON, Canada",
}
# Page intro text
INTRO_TEXT = (
"I'm currently open to Senior Data Analyst and Data Engineer roles in Toronto "
"(or remote). If you're working on something interesting and need someone who can "
"build data infrastructure from scratch, I'd like to hear about it."
)
CONSULTING_TEXT = (
"For consulting inquiries (automation, dashboards, small business data work), "
"reach out about Bandit Labs."
)
# Form subject options
SUBJECT_OPTIONS = [
{"value": "job", "label": "Job Opportunity"},
{"value": "consulting", "label": "Consulting Inquiry"},
{"value": "other", "label": "Other"},
]
def create_intro_section() -> dmc.Stack:
"""Create the intro text section."""
return dmc.Stack(
[
dmc.Title("Get In Touch", order=1, ta="center"),
dmc.Text(INTRO_TEXT, size="md", ta="center", maw=600, mx="auto"),
dmc.Text(
CONSULTING_TEXT, size="md", ta="center", maw=600, mx="auto", c="dimmed"
),
],
gap="md",
mb="xl",
)
def create_contact_form() -> dmc.Paper:
"""Create the contact form (disabled in Phase 1)."""
return dmc.Paper(
dmc.Stack(
[
dmc.Title("Send a Message", order=2, size="h4"),
dmc.Alert(
"Contact form submission is coming soon. Please use the direct contact "
"methods below for now.",
title="Form Coming Soon",
color="blue",
variant="light",
),
dmc.TextInput(
label="Name",
placeholder="Your name",
leftSection=DashIconify(icon="tabler:user", width=18),
disabled=True,
),
dmc.TextInput(
label="Email",
placeholder="your.email@example.com",
leftSection=DashIconify(icon="tabler:mail", width=18),
disabled=True,
),
dmc.Select(
label="Subject",
placeholder="Select a subject",
data=SUBJECT_OPTIONS,
leftSection=DashIconify(icon="tabler:tag", width=18),
disabled=True,
),
dmc.Textarea(
label="Message",
placeholder="Your message...",
minRows=4,
disabled=True,
),
dmc.Button(
"Send Message",
fullWidth=True,
leftSection=DashIconify(icon="tabler:send", width=18),
disabled=True,
),
],
gap="md",
),
p="xl",
radius="md",
withBorder=True,
)
def create_direct_contact() -> dmc.Paper:
"""Create the direct contact information section."""
return dmc.Paper(
dmc.Stack(
[
dmc.Title("Direct Contact", order=2, size="h4"),
dmc.Stack(
[
# Email
dmc.Group(
[
dmc.ThemeIcon(
DashIconify(icon="tabler:mail", width=20),
size="lg",
radius="md",
variant="light",
),
dmc.Stack(
[
dmc.Text("Email", size="sm", c="dimmed"),
dmc.Anchor(
CONTACT_INFO["email"],
href=f"mailto:{CONTACT_INFO['email']}",
size="md",
fw=500,
),
],
gap=0,
),
],
gap="md",
),
# Phone
dmc.Group(
[
dmc.ThemeIcon(
DashIconify(icon="tabler:phone", width=20),
size="lg",
radius="md",
variant="light",
),
dmc.Stack(
[
dmc.Text("Phone", size="sm", c="dimmed"),
dmc.Anchor(
CONTACT_INFO["phone"],
href=f"tel:{CONTACT_INFO['phone'].replace('(', '').replace(')', '').replace(' ', '').replace('-', '')}",
size="md",
fw=500,
),
],
gap=0,
),
],
gap="md",
),
# LinkedIn
dmc.Group(
[
dmc.ThemeIcon(
DashIconify(icon="tabler:brand-linkedin", width=20),
size="lg",
radius="md",
variant="light",
color="blue",
),
dmc.Stack(
[
dmc.Text("LinkedIn", size="sm", c="dimmed"),
dmc.Anchor(
"linkedin.com/in/leobmiranda",
href=CONTACT_INFO["linkedin"],
target="_blank",
size="md",
fw=500,
),
],
gap=0,
),
],
gap="md",
),
# GitHub
dmc.Group(
[
dmc.ThemeIcon(
DashIconify(icon="tabler:brand-github", width=20),
size="lg",
radius="md",
variant="light",
),
dmc.Stack(
[
dmc.Text("GitHub", size="sm", c="dimmed"),
dmc.Anchor(
"github.com/leomiranda",
href=CONTACT_INFO["github"],
target="_blank",
size="md",
fw=500,
),
],
gap=0,
),
],
gap="md",
),
],
gap="lg",
),
],
gap="lg",
),
p="xl",
radius="md",
withBorder=True,
)
def create_location_section() -> dmc.Paper:
"""Create the location and work eligibility section."""
return dmc.Paper(
dmc.Stack(
[
dmc.Title("Location", order=2, size="h4"),
dmc.Group(
[
dmc.ThemeIcon(
DashIconify(icon="tabler:map-pin", width=20),
size="lg",
radius="md",
variant="light",
color="red",
),
dmc.Stack(
[
dmc.Text(CONTACT_INFO["location"], size="md", fw=500),
dmc.Text(
"Canadian Citizen | Eligible to work in Canada and US",
size="sm",
c="dimmed",
),
],
gap=0,
),
],
gap="md",
),
],
gap="md",
),
p="xl",
radius="md",
withBorder=True,
)
layout = dmc.Container(
dmc.Stack(
[
create_intro_section(),
dmc.SimpleGrid(
[
create_contact_form(),
dmc.Stack(
[
create_direct_contact(),
create_location_section(),
],
gap="lg",
),
],
cols={"base": 1, "md": 2},
spacing="xl",
),
dmc.Space(h=40),
],
gap="lg",
),
size="lg",
py="xl",
)