Files
personal-portfolio/portfolio_app/pages/blog/index.py
lmiranda 138e6fe497 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>
2026-01-15 15:40:01 -05:00

114 lines
3.3 KiB
Python

"""Blog index page - Article listing."""
import dash
import dash_mantine_components as dmc
from dash import dcc
from dash_iconify import DashIconify
from portfolio_app.utils.markdown_loader import Article, get_all_articles
dash.register_page(__name__, path="/blog", name="Blog")
# Page intro
INTRO_TEXT = (
"I write occasionally about data engineering, automation, and the reality of being "
"a one-person data team. No hot takes, no growth hacking—just things I've learned "
"the hard way."
)
def create_article_card(article: Article) -> dmc.Paper:
"""Create an article preview card."""
meta = article["meta"]
return dmc.Paper(
dcc.Link(
dmc.Stack(
[
dmc.Group(
[
dmc.Text(meta["title"], fw=600, size="lg"),
dmc.Text(meta["date"], size="sm", c="dimmed"),
],
justify="space-between",
align="flex-start",
wrap="wrap",
),
dmc.Text(meta["description"], size="md", c="dimmed", lineClamp=2),
dmc.Group(
[
dmc.Badge(tag, variant="light", size="sm")
for tag in meta.get("tags", [])[:3]
],
gap="xs",
),
],
gap="sm",
),
href=f"/blog/{meta['slug']}",
style={"textDecoration": "none", "color": "inherit"},
),
p="lg",
radius="md",
withBorder=True,
className="article-card",
)
def create_empty_state() -> dmc.Paper:
"""Create empty state when no articles exist."""
return dmc.Paper(
dmc.Stack(
[
dmc.ThemeIcon(
DashIconify(icon="tabler:article-off", width=48),
size=80,
radius="xl",
variant="light",
color="gray",
),
dmc.Title("No Articles Yet", order=3),
dmc.Text(
"Articles are coming soon. Check back later!",
size="md",
c="dimmed",
ta="center",
),
],
align="center",
gap="md",
py="xl",
),
p="xl",
radius="md",
withBorder=True,
)
def layout() -> dmc.Container:
"""Generate the blog index layout dynamically."""
articles = get_all_articles(include_drafts=False)
return dmc.Container(
dmc.Stack(
[
dmc.Title("Blog", order=1, ta="center"),
dmc.Text(
INTRO_TEXT, size="md", c="dimmed", ta="center", maw=600, mx="auto"
),
dmc.Divider(my="lg"),
(
dmc.Stack(
[create_article_card(article) for article in articles],
gap="lg",
)
if articles
else create_empty_state()
),
dmc.Space(h=40),
],
gap="lg",
),
size="md",
py="xl",
)