diff --git a/index.html b/index.html index 442f190..1a0ac06 100644 --- a/index.html +++ b/index.html @@ -1,14 +1,6 @@ - - - @@ -136,13 +128,9 @@ - + - + Sagar Gupta | DevOps & Cloud Consultant at AWS @@ -158,5 +146,38 @@
+ + + diff --git a/src/components/ui/ModalHeaderShell.tsx b/src/components/ui/ModalHeaderShell.tsx new file mode 100644 index 0000000..600563f --- /dev/null +++ b/src/components/ui/ModalHeaderShell.tsx @@ -0,0 +1,69 @@ +import type { ReactNode } from "react"; +import { motion } from "motion/react"; +import { X } from "lucide-react"; +import { TEXT_SECONDARY } from "@/constants/theme"; + +interface ModalHeaderShellProps { + isMobile: boolean; + onClose: () => void; + closeLabel: string; + children: ReactNode; +} + +const EXPO_EASE = [0.16, 1, 0.3, 1] as const; + +/** + * Shared sticky header frame used by ExperienceModal and ProjectModal. + * Provides the animated entrance, sticky positioning, backdrop blur, and the + * close button in the top-right corner. Callers pass their title/metadata as + * children and keep control over the inner layout. + */ +const ModalHeaderShell = ({ + isMobile, + onClose, + closeLabel, + children, +}: ModalHeaderShellProps) => ( + +
+ +
{children}
+
+
+); + +export default ModalHeaderShell; diff --git a/src/components/ui/ModalShell.tsx b/src/components/ui/ModalShell.tsx new file mode 100644 index 0000000..455aa49 --- /dev/null +++ b/src/components/ui/ModalShell.tsx @@ -0,0 +1,97 @@ +import type { ReactNode, RefObject } from "react"; +import { motion, AnimatePresence } from "motion/react"; +import { createPortal } from "react-dom"; +import { EASING } from "@/constants/theme"; + +interface ModalShellProps { + /** When null the modal is closed. Any truthy value renders it. */ + isOpen: boolean; + onClose: () => void; + dialogRef: RefObject; + isMobile: boolean; + /** ID of the h1/h2/h3 that names this dialog for screen readers. */ + titleId: string; + children: ReactNode; +} + +/** + * Full-viewport modal backdrop + card container used by ExperienceModal + * and ProjectModal. Handles portal mount, AnimatePresence, the dim/blur + * backdrop, slide-up-from-bottom on mobile, and the roled dialog frame. + * Callers supply their own header and body as children; this shell only + * owns the outer chrome. + */ +const ModalShell = ({ + isOpen, + onClose, + dialogRef, + isMobile, + titleId, + children, +}: ModalShellProps) => + createPortal( + + {isOpen && ( + + e.stopPropagation()} + onWheel={(e) => e.stopPropagation()} + role="dialog" + aria-modal="true" + aria-labelledby={titleId} + tabIndex={-1} + style={{ + position: "relative", + width: "100%", + maxWidth: isMobile ? "100%" : 720, + maxHeight: isMobile ? "92vh" : "85vh", + overflowY: "auto", + borderRadius: isMobile ? "20px 20px 0 0" : 20, + border: "1px solid rgba(255,255,255,0.1)", + background: + "linear-gradient(180deg, rgba(15,15,30,0.98) 0%, rgba(8,8,20,0.99) 100%)", + boxShadow: + "0 30px 80px rgba(0,0,0,0.6), 0 0 60px rgba(6,182,212,0.04)", + }} + > + {children} + + + )} + , + document.body, + ); + +export default ModalShell; diff --git a/src/pages/experience/ExperienceModal.tsx b/src/pages/experience/ExperienceModal.tsx index 6ab65fb..b88348b 100644 --- a/src/pages/experience/ExperienceModal.tsx +++ b/src/pages/experience/ExperienceModal.tsx @@ -1,9 +1,8 @@ import { useEffect, useCallback } from "react"; -import { motion, AnimatePresence } from "motion/react"; import type { ProfessionalExperience } from "@/types"; import useBreakpoint from "@hooks/useBreakpoint"; import useFocusTrap from "@hooks/useFocusTrap"; -import { EASING } from "@/constants/theme"; +import ModalShell from "@components/ui/ModalShell"; import ModalHeader from "./ModalHeader"; import ModalContent from "./ModalContent"; @@ -34,75 +33,24 @@ const ExperienceModal = ({ experience, onClose }: Props) => { }, [experience, onEsc]); return ( - + {experience && ( - - e.stopPropagation()} - onWheel={(e) => e.stopPropagation()} - role="dialog" - aria-modal="true" - aria-labelledby="experience-modal-title" - tabIndex={-1} - style={{ - position: "relative", - width: "100%", - maxWidth: isMobile ? "100%" : 720, - maxHeight: isMobile ? "92vh" : "85vh", - overflowY: "auto", - borderRadius: isMobile ? "20px 20px 0 0" : 20, - border: "1px solid rgba(255,255,255,0.1)", - background: - "linear-gradient(180deg, rgba(15,15,30,0.98) 0%, rgba(8,8,20,0.99) 100%)", - boxShadow: - "0 30px 80px rgba(0,0,0,0.6), 0 0 60px rgba(6,182,212,0.04)", - }} - > - - - - + <> + + + )} - + ); }; diff --git a/src/pages/experience/ModalHeader.tsx b/src/pages/experience/ModalHeader.tsx index 3d09b51..59dd759 100644 --- a/src/pages/experience/ModalHeader.tsx +++ b/src/pages/experience/ModalHeader.tsx @@ -1,11 +1,6 @@ -import { motion } from "motion/react"; -import { X, Building2 } from "lucide-react"; -import { - TEXT_PRIMARY, - TEXT_SECONDARY, - CYAN, - MONO_FONT, -} from "@/constants/theme"; +import { Building2 } from "lucide-react"; +import ModalHeaderShell from "@components/ui/ModalHeaderShell"; +import { TEXT_PRIMARY, CYAN, MONO_FONT } from "@/constants/theme"; import type { ProfessionalExperience } from "@/types"; interface ModalHeaderProps { @@ -15,21 +10,10 @@ interface ModalHeaderProps { } const ModalHeader = ({ experience, onClose, isMobile }: ModalHeaderProps) => ( -
( > {experience.company} -

( > {experience.title} | {experience.date}

-
+ ); export default ModalHeader; diff --git a/src/pages/experience/PresentIndicator.tsx b/src/pages/experience/PresentIndicator.tsx new file mode 100644 index 0000000..14263a2 --- /dev/null +++ b/src/pages/experience/PresentIndicator.tsx @@ -0,0 +1,39 @@ +import { GREEN } from "@/constants/theme"; + +interface PresentIndicatorProps { + /** Show just the pulsing dot (true) or the dot + "Present" label (false). */ + dotOnly?: boolean; +} + +/** + * Green pulsing dot that marks a currently-active role on the experience timeline. + * When `dotOnly` is false, the dot is rendered with a trailing "Present" label + * suitable for use inside a flex row (the parent controls the gap). + */ +const PresentIndicator = ({ dotOnly = false }: PresentIndicatorProps) => { + const dot = ( +