feat: Add floating sidebar navigation and dark theme support
- Add floating pill-shaped sidebar with navigation icons - Implement dark/light theme toggle with localStorage persistence - Update all figure factories for transparent backgrounds - Use carto-darkmatter map style for choropleths - Add methodology link button to Toronto dashboard header - Add back to dashboard button on methodology page - Remove social links from home page (now in sidebar) - Update CLAUDE.md to Sprint 7 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ Working context for Claude Code on the Analytics Portfolio project.
|
||||
|
||||
## Project Status
|
||||
|
||||
**Current Sprint**: 1 (Project Bootstrap)
|
||||
**Current Sprint**: 7 (Navigation & Theme Modernization)
|
||||
**Phase**: 1 - Toronto Housing Dashboard
|
||||
**Branch**: `development` (feature branches merge here)
|
||||
|
||||
@@ -254,4 +254,4 @@ All scripts in `scripts/`:
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: Sprint 1*
|
||||
*Last Updated: Sprint 7*
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
import dash
|
||||
import dash_mantine_components as dmc
|
||||
from dash import dcc, html
|
||||
|
||||
from .components import create_sidebar
|
||||
from .config import get_settings
|
||||
|
||||
|
||||
@@ -17,14 +19,31 @@ def create_app() -> dash.Dash:
|
||||
)
|
||||
|
||||
app.layout = dmc.MantineProvider(
|
||||
id="mantine-provider",
|
||||
children=[
|
||||
dcc.Location(id="url", refresh=False),
|
||||
dcc.Store(id="theme-store", storage_type="local", data="dark"),
|
||||
dcc.Store(id="theme-init-dummy"), # Dummy store for theme init callback
|
||||
html.Div(
|
||||
[
|
||||
create_sidebar(),
|
||||
html.Div(
|
||||
dash.page_container,
|
||||
className="page-content-wrapper",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
theme={
|
||||
"primaryColor": "blue",
|
||||
"fontFamily": "'Inter', sans-serif",
|
||||
},
|
||||
forceColorScheme="light",
|
||||
defaultColorScheme="dark",
|
||||
)
|
||||
|
||||
# Import callbacks to register them
|
||||
from . import callbacks # noqa: F401
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
139
portfolio_app/assets/sidebar.css
Normal file
139
portfolio_app/assets/sidebar.css
Normal file
@@ -0,0 +1,139 @@
|
||||
/* Floating sidebar navigation styles */
|
||||
|
||||
/* Sidebar container */
|
||||
.floating-sidebar {
|
||||
position: fixed;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 60px;
|
||||
padding: 16px 8px;
|
||||
border-radius: 32px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Page content offset to prevent sidebar overlap */
|
||||
.page-content-wrapper {
|
||||
margin-left: 92px; /* sidebar width (60px) + left margin (16px) + gap (16px) */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Dark theme (default) */
|
||||
[data-mantine-color-scheme="dark"] .floating-sidebar {
|
||||
background-color: #141414;
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] body {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
/* Light theme */
|
||||
[data-mantine-color-scheme="light"] .floating-sidebar {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="light"] body {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Brand initials styling */
|
||||
.sidebar-brand {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background-color: var(--mantine-color-blue-filled);
|
||||
margin-bottom: 4px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-brand:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.sidebar-brand-link {
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Divider between sections */
|
||||
.sidebar-divider {
|
||||
width: 32px;
|
||||
height: 1px;
|
||||
background-color: var(--mantine-color-dimmed);
|
||||
margin: 4px 0;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Active nav icon indicator */
|
||||
.nav-icon-active {
|
||||
background-color: var(--mantine-color-blue-filled) !important;
|
||||
}
|
||||
|
||||
/* Navigation icon hover effects */
|
||||
.floating-sidebar .mantine-ActionIcon-root {
|
||||
transition: transform 0.15s ease, background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.floating-sidebar .mantine-ActionIcon-root:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Ensure links don't have underlines */
|
||||
.floating-sidebar a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Theme toggle specific styling */
|
||||
#theme-toggle {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
#theme-toggle:hover {
|
||||
transform: rotate(15deg) scale(1.1);
|
||||
}
|
||||
|
||||
/* Responsive adjustments for smaller screens */
|
||||
@media (max-width: 768px) {
|
||||
.floating-sidebar {
|
||||
left: 8px;
|
||||
width: 50px;
|
||||
padding: 12px 6px;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
.page-content-wrapper {
|
||||
margin-left: 70px;
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.sidebar-brand-link {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very small screens - hide sidebar, show minimal navigation */
|
||||
@media (max-width: 480px) {
|
||||
.floating-sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-content-wrapper {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
5
portfolio_app/callbacks/__init__.py
Normal file
5
portfolio_app/callbacks/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Application-level callbacks for the portfolio app."""
|
||||
|
||||
from . import theme
|
||||
|
||||
__all__ = ["theme"]
|
||||
38
portfolio_app/callbacks/theme.py
Normal file
38
portfolio_app/callbacks/theme.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Theme toggle callbacks using clientside JavaScript."""
|
||||
|
||||
from dash import Input, Output, State, clientside_callback
|
||||
|
||||
# Toggle theme on button click
|
||||
# Stores new theme value and updates the DOM attribute
|
||||
clientside_callback(
|
||||
"""
|
||||
function(n_clicks, currentTheme) {
|
||||
if (n_clicks === undefined || n_clicks === null) {
|
||||
return window.dash_clientside.no_update;
|
||||
}
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-mantine-color-scheme', newTheme);
|
||||
return newTheme;
|
||||
}
|
||||
""",
|
||||
Output("theme-store", "data"),
|
||||
Input("theme-toggle", "n_clicks"),
|
||||
State("theme-store", "data"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
|
||||
# Initialize theme from localStorage on page load
|
||||
# Uses a dummy output since we only need the side effect of setting the DOM attribute
|
||||
clientside_callback(
|
||||
"""
|
||||
function(theme) {
|
||||
if (theme) {
|
||||
document.documentElement.setAttribute('data-mantine-color-scheme', theme);
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
""",
|
||||
Output("theme-init-dummy", "data"),
|
||||
Input("theme-store", "data"),
|
||||
prevent_initial_call=False,
|
||||
)
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
from .map_controls import create_map_controls, create_metric_selector
|
||||
from .metric_card import MetricCard, create_metric_cards_row
|
||||
from .sidebar import create_sidebar
|
||||
from .time_slider import create_time_slider, create_year_selector
|
||||
|
||||
__all__ = [
|
||||
"create_map_controls",
|
||||
"create_metric_selector",
|
||||
"create_sidebar",
|
||||
"create_time_slider",
|
||||
"create_year_selector",
|
||||
"MetricCard",
|
||||
|
||||
179
portfolio_app/components/sidebar.py
Normal file
179
portfolio_app/components/sidebar.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""Floating sidebar navigation component."""
|
||||
|
||||
import dash_mantine_components as dmc
|
||||
from dash import dcc, html
|
||||
from dash_iconify import DashIconify
|
||||
|
||||
# Navigation items configuration
|
||||
NAV_ITEMS = [
|
||||
{"path": "/", "icon": "tabler:home", "label": "Home"},
|
||||
{"path": "/toronto", "icon": "tabler:map-2", "label": "Toronto Housing"},
|
||||
]
|
||||
|
||||
# External links configuration
|
||||
EXTERNAL_LINKS = [
|
||||
{
|
||||
"url": "https://github.com/leomiranda",
|
||||
"icon": "tabler:brand-github",
|
||||
"label": "GitHub",
|
||||
},
|
||||
{
|
||||
"url": "https://linkedin.com/in/leobmiranda",
|
||||
"icon": "tabler:brand-linkedin",
|
||||
"label": "LinkedIn",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def create_brand_logo() -> html.Div:
|
||||
"""Create the brand initials logo."""
|
||||
return html.Div(
|
||||
dcc.Link(
|
||||
"LM",
|
||||
href="/",
|
||||
className="sidebar-brand-link",
|
||||
),
|
||||
className="sidebar-brand",
|
||||
)
|
||||
|
||||
|
||||
def create_nav_icon(
|
||||
icon: str,
|
||||
label: str,
|
||||
path: str,
|
||||
current_path: str,
|
||||
) -> dmc.Tooltip:
|
||||
"""Create a navigation icon with tooltip.
|
||||
|
||||
Args:
|
||||
icon: Iconify icon string.
|
||||
label: Tooltip label.
|
||||
path: Navigation path.
|
||||
current_path: Current page path for active state.
|
||||
|
||||
Returns:
|
||||
Tooltip-wrapped navigation icon.
|
||||
"""
|
||||
is_active = current_path == path or (path != "/" and current_path.startswith(path))
|
||||
|
||||
return dmc.Tooltip(
|
||||
dcc.Link(
|
||||
dmc.ActionIcon(
|
||||
DashIconify(icon=icon, width=20),
|
||||
variant="subtle" if not is_active else "filled",
|
||||
size="lg",
|
||||
radius="xl",
|
||||
color="blue" if is_active else "gray",
|
||||
className="nav-icon-active" if is_active else "",
|
||||
),
|
||||
href=path,
|
||||
),
|
||||
label=label,
|
||||
position="right",
|
||||
withArrow=True,
|
||||
)
|
||||
|
||||
|
||||
def create_theme_toggle(current_theme: str = "dark") -> dmc.Tooltip:
|
||||
"""Create the theme toggle button.
|
||||
|
||||
Args:
|
||||
current_theme: Current theme ('dark' or 'light').
|
||||
|
||||
Returns:
|
||||
Tooltip-wrapped theme toggle icon.
|
||||
"""
|
||||
icon = "tabler:sun" if current_theme == "dark" else "tabler:moon"
|
||||
label = "Switch to light mode" if current_theme == "dark" else "Switch to dark mode"
|
||||
|
||||
return dmc.Tooltip(
|
||||
dmc.ActionIcon(
|
||||
DashIconify(icon=icon, width=20, id="theme-toggle-icon"),
|
||||
id="theme-toggle",
|
||||
variant="subtle",
|
||||
size="lg",
|
||||
radius="xl",
|
||||
color="gray",
|
||||
),
|
||||
label=label,
|
||||
position="right",
|
||||
withArrow=True,
|
||||
)
|
||||
|
||||
|
||||
def create_external_link(url: str, icon: str, label: str) -> dmc.Tooltip:
|
||||
"""Create an external link icon with tooltip.
|
||||
|
||||
Args:
|
||||
url: External URL.
|
||||
icon: Iconify icon string.
|
||||
label: Tooltip label.
|
||||
|
||||
Returns:
|
||||
Tooltip-wrapped external link icon.
|
||||
"""
|
||||
return dmc.Tooltip(
|
||||
dmc.Anchor(
|
||||
dmc.ActionIcon(
|
||||
DashIconify(icon=icon, width=20),
|
||||
variant="subtle",
|
||||
size="lg",
|
||||
radius="xl",
|
||||
color="gray",
|
||||
),
|
||||
href=url,
|
||||
target="_blank",
|
||||
),
|
||||
label=label,
|
||||
position="right",
|
||||
withArrow=True,
|
||||
)
|
||||
|
||||
|
||||
def create_sidebar_divider() -> html.Div:
|
||||
"""Create a horizontal divider for the sidebar."""
|
||||
return html.Div(className="sidebar-divider")
|
||||
|
||||
|
||||
def create_sidebar(current_path: str = "/", current_theme: str = "dark") -> html.Div:
|
||||
"""Create the floating sidebar navigation.
|
||||
|
||||
Args:
|
||||
current_path: Current page path for active state highlighting.
|
||||
current_theme: Current theme for toggle icon state.
|
||||
|
||||
Returns:
|
||||
Complete sidebar component.
|
||||
"""
|
||||
return html.Div(
|
||||
[
|
||||
# Brand logo
|
||||
create_brand_logo(),
|
||||
create_sidebar_divider(),
|
||||
# Navigation icons
|
||||
*[
|
||||
create_nav_icon(
|
||||
icon=item["icon"],
|
||||
label=item["label"],
|
||||
path=item["path"],
|
||||
current_path=current_path,
|
||||
)
|
||||
for item in NAV_ITEMS
|
||||
],
|
||||
create_sidebar_divider(),
|
||||
# Theme toggle
|
||||
create_theme_toggle(current_theme),
|
||||
create_sidebar_divider(),
|
||||
# External links
|
||||
*[
|
||||
create_external_link(
|
||||
url=link["url"],
|
||||
icon=link["icon"],
|
||||
label=link["label"],
|
||||
)
|
||||
for link in EXTERNAL_LINKS
|
||||
],
|
||||
],
|
||||
className="floating-sidebar",
|
||||
id="floating-sidebar",
|
||||
)
|
||||
@@ -39,6 +39,10 @@ def create_choropleth_figure(
|
||||
if center is None:
|
||||
center = {"lat": 43.7, "lon": -79.4}
|
||||
|
||||
# Use dark-mode friendly map style by default
|
||||
if map_style == "carto-positron":
|
||||
map_style = "carto-darkmatter"
|
||||
|
||||
# If no geojson provided, create a placeholder map
|
||||
if geojson is None or not data:
|
||||
fig = go.Figure(go.Scattermapbox())
|
||||
@@ -51,6 +55,9 @@ def create_choropleth_figure(
|
||||
margin={"l": 0, "r": 0, "t": 40, "b": 0},
|
||||
title=title or "Toronto Housing Map",
|
||||
height=500,
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
)
|
||||
fig.add_annotation(
|
||||
text="No geometry data available. Complete QGIS digitization to enable map.",
|
||||
@@ -59,7 +66,7 @@ def create_choropleth_figure(
|
||||
x=0.5,
|
||||
y=0.5,
|
||||
showarrow=False,
|
||||
font={"size": 14, "color": "gray"},
|
||||
font={"size": 14, "color": "#888888"},
|
||||
)
|
||||
return fig
|
||||
|
||||
@@ -68,6 +75,11 @@ def create_choropleth_figure(
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# Use dark-mode friendly map style
|
||||
effective_map_style = (
|
||||
"carto-darkmatter" if map_style == "carto-positron" else map_style
|
||||
)
|
||||
|
||||
fig = px.choropleth_mapbox(
|
||||
df,
|
||||
geojson=geojson,
|
||||
@@ -76,7 +88,7 @@ def create_choropleth_figure(
|
||||
color=color_column,
|
||||
color_continuous_scale=color_scale,
|
||||
hover_data=hover_data,
|
||||
mapbox_style=map_style,
|
||||
mapbox_style=effective_map_style,
|
||||
center=center,
|
||||
zoom=zoom,
|
||||
opacity=0.7,
|
||||
@@ -86,10 +98,17 @@ def create_choropleth_figure(
|
||||
margin={"l": 0, "r": 0, "t": 40, "b": 0},
|
||||
title=title,
|
||||
height=500,
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
coloraxis_colorbar={
|
||||
"title": color_column.replace("_", " ").title(),
|
||||
"title": {
|
||||
"text": color_column.replace("_", " ").title(),
|
||||
"font": {"color": "#c9c9c9"},
|
||||
},
|
||||
"thickness": 15,
|
||||
"len": 0.7,
|
||||
"tickfont": {"color": "#c9c9c9"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -69,7 +69,8 @@ def create_metric_card_figure(
|
||||
height=120,
|
||||
margin={"l": 20, "r": 20, "t": 40, "b": 20},
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
font={"family": "Inter, sans-serif"},
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font={"family": "Inter, sans-serif", "color": "#c9c9c9"},
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
@@ -38,8 +38,15 @@ def create_price_time_series(
|
||||
x=0.5,
|
||||
y=0.5,
|
||||
showarrow=False,
|
||||
font={"color": "#888888"},
|
||||
)
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
height=350,
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
)
|
||||
fig.update_layout(title=title, height=350)
|
||||
return fig
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
@@ -69,6 +76,11 @@ def create_price_time_series(
|
||||
yaxis_tickprefix="$",
|
||||
yaxis_tickformat=",",
|
||||
hovermode="x unified",
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
xaxis={"gridcolor": "#333333", "linecolor": "#444444"},
|
||||
yaxis={"gridcolor": "#333333", "linecolor": "#444444"},
|
||||
)
|
||||
|
||||
return fig
|
||||
@@ -106,8 +118,15 @@ def create_volume_time_series(
|
||||
x=0.5,
|
||||
y=0.5,
|
||||
showarrow=False,
|
||||
font={"color": "#888888"},
|
||||
)
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
height=350,
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
)
|
||||
fig.update_layout(title=title, height=350)
|
||||
return fig
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
@@ -153,6 +172,11 @@ def create_volume_time_series(
|
||||
yaxis_title=volume_column.replace("_", " ").title(),
|
||||
yaxis_tickformat=",",
|
||||
hovermode="x unified",
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
xaxis={"gridcolor": "#333333", "linecolor": "#444444"},
|
||||
yaxis={"gridcolor": "#333333", "linecolor": "#444444"},
|
||||
)
|
||||
|
||||
return fig
|
||||
@@ -187,8 +211,15 @@ def create_market_comparison_chart(
|
||||
x=0.5,
|
||||
y=0.5,
|
||||
showarrow=False,
|
||||
font={"color": "#888888"},
|
||||
)
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
height=400,
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
)
|
||||
fig.update_layout(title=title, height=400)
|
||||
return fig
|
||||
|
||||
if metrics is None:
|
||||
@@ -221,12 +252,18 @@ def create_market_comparison_chart(
|
||||
height=400,
|
||||
margin={"l": 40, "r": 40, "t": 50, "b": 40},
|
||||
hovermode="x unified",
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font_color="#c9c9c9",
|
||||
xaxis={"gridcolor": "#333333", "linecolor": "#444444"},
|
||||
yaxis={"gridcolor": "#333333", "linecolor": "#444444"},
|
||||
legend={
|
||||
"orientation": "h",
|
||||
"yanchor": "bottom",
|
||||
"y": 1.02,
|
||||
"xanchor": "right",
|
||||
"x": 1,
|
||||
"font": {"color": "#c9c9c9"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import dash
|
||||
import dash_mantine_components as dmc
|
||||
from dash_iconify import DashIconify
|
||||
|
||||
dash.register_page(__name__, path="/", name="Home")
|
||||
|
||||
@@ -52,19 +51,6 @@ PROJECTS = [
|
||||
},
|
||||
]
|
||||
|
||||
SOCIAL_LINKS = [
|
||||
{
|
||||
"platform": "LinkedIn",
|
||||
"url": "https://linkedin.com/in/leobmiranda",
|
||||
"icon": "mdi:linkedin",
|
||||
},
|
||||
{
|
||||
"platform": "GitHub",
|
||||
"url": "https://github.com/leomiranda",
|
||||
"icon": "mdi:github",
|
||||
},
|
||||
]
|
||||
|
||||
AVAILABILITY = "Open to Senior Data Analyst, Analytics Engineer, and BI Developer opportunities in Toronto or remote."
|
||||
|
||||
|
||||
@@ -160,27 +146,6 @@ def create_projects_section() -> dmc.Paper:
|
||||
)
|
||||
|
||||
|
||||
def create_social_links() -> dmc.Group:
|
||||
"""Create social media links."""
|
||||
return dmc.Group(
|
||||
[
|
||||
dmc.Anchor(
|
||||
dmc.Button(
|
||||
link["platform"],
|
||||
leftSection=DashIconify(icon=link["icon"], width=20),
|
||||
variant="outline",
|
||||
size="md",
|
||||
),
|
||||
href=link["url"],
|
||||
target="_blank",
|
||||
)
|
||||
for link in SOCIAL_LINKS
|
||||
],
|
||||
justify="center",
|
||||
gap="md",
|
||||
)
|
||||
|
||||
|
||||
def create_availability_section() -> dmc.Text:
|
||||
"""Create the availability statement."""
|
||||
return dmc.Text(AVAILABILITY, size="sm", c="dimmed", ta="center", fs="italic")
|
||||
@@ -193,7 +158,6 @@ layout = dmc.Container(
|
||||
create_summary_section(),
|
||||
create_tech_stack_section(),
|
||||
create_projects_section(),
|
||||
create_social_links(),
|
||||
dmc.Divider(my="lg"),
|
||||
create_availability_section(),
|
||||
dmc.Space(h=40),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import dash
|
||||
import dash_mantine_components as dmc
|
||||
from dash import dcc, html
|
||||
from dash_iconify import DashIconify
|
||||
|
||||
from portfolio_app.components import (
|
||||
create_map_controls,
|
||||
@@ -76,6 +77,17 @@ def create_header() -> dmc.Group:
|
||||
),
|
||||
dmc.Group(
|
||||
[
|
||||
dcc.Link(
|
||||
dmc.Button(
|
||||
"Methodology",
|
||||
leftSection=DashIconify(
|
||||
icon="tabler:info-circle", width=18
|
||||
),
|
||||
variant="subtle",
|
||||
color="gray",
|
||||
),
|
||||
href="/toronto/methodology",
|
||||
),
|
||||
create_year_selector(
|
||||
id_prefix="toronto",
|
||||
min_year=2020,
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import dash
|
||||
import dash_mantine_components as dmc
|
||||
from dash import html
|
||||
from dash import dcc, html
|
||||
from dash_iconify import DashIconify
|
||||
|
||||
dash.register_page(
|
||||
__name__,
|
||||
@@ -18,8 +19,18 @@ def layout() -> dmc.Container:
|
||||
size="md",
|
||||
py="xl",
|
||||
children=[
|
||||
# Back to Dashboard button
|
||||
dcc.Link(
|
||||
dmc.Button(
|
||||
"Back to Dashboard",
|
||||
leftSection=DashIconify(icon="tabler:arrow-left", width=18),
|
||||
variant="subtle",
|
||||
color="gray",
|
||||
),
|
||||
href="/toronto",
|
||||
),
|
||||
# Header
|
||||
dmc.Title("Methodology", order=1, mb="lg"),
|
||||
dmc.Title("Methodology", order=1, mb="lg", mt="md"),
|
||||
dmc.Text(
|
||||
"This page documents the data sources, processing methodology, "
|
||||
"and known limitations of the Toronto Housing Dashboard.",
|
||||
|
||||
Reference in New Issue
Block a user