diff --git a/src/components/DashboardLayout.tsx b/src/components/DashboardLayout.tsx index 18b0d08..e185c3e 100644 --- a/src/components/DashboardLayout.tsx +++ b/src/components/DashboardLayout.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react' import { motion } from 'framer-motion' import { TopBar } from './TopBar' +import { SubNav } from './SubNav' import Sidebar from './Sidebar' import { CommandPalette } from './CommandPalette' import { PatientSummaryTile } from './tiles/PatientSummaryTile' @@ -10,6 +11,7 @@ import { LastConsultationTile } from './tiles/LastConsultationTile' import { CareerActivityTile } from './tiles/CareerActivityTile' import { EducationTile } from './tiles/EducationTile' import { ProjectsTile } from './tiles/ProjectsTile' +import { useActiveSection } from '@/hooks/useActiveSection' import type { PaletteAction } from '@/lib/search' const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches @@ -48,6 +50,7 @@ const contentVariants = { export function DashboardLayout() { const [commandPaletteOpen, setCommandPaletteOpen] = useState(false) + const activeSection = useActiveSection() const handleSearchClick = () => { setCommandPaletteOpen(true) @@ -57,6 +60,11 @@ export function DashboardLayout() { setCommandPaletteOpen(false) }, []) + const handleSectionClick = useCallback((_sectionId: string) => { + // Section click is already handled in SubNav component + // This is just a placeholder for any additional logic needed + }, []) + // Global Ctrl+K listener to open command palette useEffect(() => { function handleKeyDown(e: KeyboardEvent) { @@ -114,12 +122,15 @@ export function DashboardLayout() { - {/* Layout below TopBar: Sidebar + Main */} + {/* SubNav — sticky below TopBar */} + + + {/* Layout below TopBar + SubNav: Sidebar + Main */}
{/* Sidebar — hidden on mobile/tablet, visible on desktop */} diff --git a/src/components/SubNav.tsx b/src/components/SubNav.tsx new file mode 100644 index 0000000..4824da4 --- /dev/null +++ b/src/components/SubNav.tsx @@ -0,0 +1,88 @@ +interface NavSection { + id: string + label: string + tileId: string // data-tile-id to scroll to +} + +interface SubNavProps { + activeSection: string + onSectionClick: (sectionId: string) => void +} + +const sections: NavSection[] = [ + { id: 'overview', label: 'Overview', tileId: 'patient-summary' }, + { id: 'skills', label: 'Skills', tileId: 'core-skills' }, + { id: 'experience', label: 'Experience', tileId: 'career-activity' }, + { id: 'projects', label: 'Projects', tileId: 'projects' }, + { id: 'education', label: 'Education', tileId: 'education' }, +] + +export function SubNav({ activeSection, onSectionClick }: SubNavProps) { + const handleSectionClick = (section: NavSection) => { + // Scroll to the tile + const tileEl = document.querySelector(`[data-tile-id="${section.tileId}"]`) + if (tileEl) { + tileEl.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + // Notify parent of section change + onSectionClick(section.id) + } + + return ( + + ) +} diff --git a/src/hooks/useActiveSection.ts b/src/hooks/useActiveSection.ts new file mode 100644 index 0000000..ad193d9 --- /dev/null +++ b/src/hooks/useActiveSection.ts @@ -0,0 +1,66 @@ +import { useState, useEffect } from 'react' + +// Map tile IDs to section IDs for SubNav +const sectionTileMap: Record = { + 'patient-summary': 'overview', + 'core-skills': 'skills', + 'career-activity': 'experience', + 'projects': 'projects', + 'education': 'education', +} + +/** + * Hook to track which section is currently visible using IntersectionObserver. + * Observes tiles by their data-tile-id attribute and maps them to section IDs. + * + * @returns The currently active section ID + */ +export function useActiveSection(): string { + const [activeSection, setActiveSection] = useState('overview') + + useEffect(() => { + // Find all tiles with data-tile-id attribute + const tiles = Array.from( + document.querySelectorAll('[data-tile-id]') + ) as HTMLElement[] + + if (tiles.length === 0) return + + // IntersectionObserver to track which tile is visible + const observer = new IntersectionObserver( + (entries) => { + // Find the entry with the highest intersection ratio + const visibleEntries = entries.filter((entry) => entry.isIntersecting) + + if (visibleEntries.length === 0) return + + // Get the most visible tile (highest intersection ratio) + const mostVisible = visibleEntries.reduce((prev, current) => + current.intersectionRatio > prev.intersectionRatio ? current : prev + ) + + // Get the tile ID and map to section ID + const tileId = mostVisible.target.getAttribute('data-tile-id') + if (tileId && sectionTileMap[tileId]) { + setActiveSection(sectionTileMap[tileId]) + } + }, + { + // Trigger when tile is 25% visible + threshold: [0, 0.25, 0.5, 0.75, 1], + // Use viewport as root, with some margin for better UX + rootMargin: '-80px 0px -80% 0px', + } + ) + + // Observe all tiles + tiles.forEach((tile) => observer.observe(tile)) + + // Cleanup + return () => { + tiles.forEach((tile) => observer.unobserve(tile)) + } + }, []) + + return activeSection +}