diff --git a/app/components/shared/GuideModal.tsx b/app/components/shared/GuideModal.tsx index 4585da0..f8e8d9b 100644 --- a/app/components/shared/GuideModal.tsx +++ b/app/components/shared/GuideModal.tsx @@ -9,26 +9,59 @@ interface GuideModalProps { onClose: () => void; } +const FOCUSABLE_SELECTORS = + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; + const GuideModal: React.FC = ({ isOpen, onClose }) => { const modalRef = useRef(null); + const closeButtonRef = useRef(null); - // Focus management + // Focus the close button when modal opens useEffect(() => { if (isOpen) { - modalRef.current?.focus(); + closeButtonRef.current?.focus(); } }, [isOpen]); + // Focus trap + close key handling const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (!isOpen) return; - switch (e.key) { - case "Escape": - case "?": + if (e.key === "Escape" || e.key === "?") { + e.preventDefault(); + onClose(); + return; + } + + // Tab trap: keep focus inside the modal + if (e.key === "Tab") { + const modal = modalRef.current; + if (!modal) return; + + const focusable = Array.from( + modal.querySelectorAll(FOCUSABLE_SELECTORS), + ).filter((el) => !el.hasAttribute("disabled")); + + if (focusable.length === 0) { e.preventDefault(); - onClose(); - break; + return; + } + + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + + if (e.shiftKey) { + if (document.activeElement === first) { + e.preventDefault(); + last.focus(); + } + } else { + if (document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } } }, [isOpen, onClose], @@ -52,6 +85,9 @@ const GuideModal: React.FC = ({ isOpen, onClose }) => { > = ({ isOpen, onClose }) => { tabIndex={-1} >
-

Guide

+

Guide

diff --git a/app/globals.scss b/app/globals.scss index ef84c90..8e5ea1b 100644 --- a/app/globals.scss +++ b/app/globals.scss @@ -35,6 +35,19 @@ code { source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } +/* Visually hidden but available to screen readers */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + ::-webkit-scrollbar { display: none; } diff --git a/app/layout.tsx b/app/layout.tsx index fca2c45..46b80c3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,28 +3,47 @@ import "./globals.scss"; import "./styles/guide-modal.scss"; import Script from "next/script"; +const BASE_URL = "https://carsonsgit.github.io"; + export const metadata: Metadata = { - title: "carsonSgit Portfolio", + title: "Carson Spriggs — Software Developer & Student", description: - "My personal portfolio. Showcasing my projects, experience, and more.", + "Portfolio of Carson Spriggs, a software developer and engineering student at Memorial University. Co-Chair of CUSEC, CS alumni of John Abbott College. Projects in AI, IoT, full-stack, and more.", + authors: [{ name: "Carson Spriggs" }], + keywords: [ + "Carson Spriggs", + "software developer", + "portfolio", + "full-stack", + "AI", + "React", + "Next.js", + "TypeScript", + "Memorial University", + "CUSEC", + "John Abbott College", + ], icons: { icon: "/favicon.ico", }, manifest: "/manifest.json", + alternates: { + canonical: BASE_URL, + }, openGraph: { - title: "carsonSgit Portfolio", + title: "Carson Spriggs — Software Developer & Student", description: - "My personal portfolio. Showcasing my projects, experience, and more.", - url: "https://carsonsgit.github.io", - siteName: "carsonSgit Portfolio", + "Portfolio of Carson Spriggs, a software developer and engineering student at Memorial University. Co-Chair of CUSEC, CS alumni of John Abbott College.", + url: BASE_URL, + siteName: "Carson Spriggs Portfolio", locale: "en_US", type: "website", }, twitter: { card: "summary", - title: "carsonSgit Portfolio", + title: "Carson Spriggs — Software Developer & Student", description: - "My personal portfolio. Showcasing my projects, experience, and more.", + "Portfolio of Carson Spriggs, a software developer and engineering student at Memorial University. Co-Chair of CUSEC, CS alumni of John Abbott College.", }, }; @@ -32,6 +51,42 @@ export const viewport: Viewport = { themeColor: "#0d0d0f", }; +const jsonLd = { + "@context": "https://schema.org", + "@type": "Person", + name: "Carson Spriggs", + url: BASE_URL, + sameAs: [ + "https://github.com/carsonSgit", + "https://linkedin.com/in/carsonspriggs", + ], + jobTitle: "Software Developer", + worksFor: { + "@type": "Organization", + name: "Botpress", + }, + alumniOf: [ + { + "@type": "CollegeOrUniversity", + name: "Memorial University of Newfoundland", + url: "https://www.mun.ca/", + }, + { + "@type": "CollegeOrUniversity", + name: "John Abbott College", + url: "https://www.johnabbott.qc.ca/", + }, + ], + knowsAbout: [ + "Software Development", + "Artificial Intelligence", + "IoT", + "Full-Stack Development", + "DevOps", + "Machine Learning", + ], +}; + export default function RootLayout({ children, }: { @@ -40,6 +95,8 @@ export default function RootLayout({ return ( + {/* Preconnect to analytics origin to reduce latency */} + +