Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 35 additions & 14 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
<!doctype html>
<html lang="en">
<head>
<!-- Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-PFFMG7D8DP"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-PFFMG7D8DP');
</script>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
Expand Down Expand Up @@ -136,13 +128,9 @@
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="manifest" href="/favicon_io/site.webmanifest" />

<!-- Analytics -->
<!-- Preconnect hints for analytics origins (scripts load at end of <body>) -->
<link rel="preconnect" href="https://scripts.simpleanalyticscdn.com" />
<script
async
defer
src="https://scripts.simpleanalyticscdn.com/latest.js"
></script>
<link rel="preconnect" href="https://www.googletagmanager.com" />

<title>Sagar Gupta | DevOps & Cloud Consultant at AWS</title>
</head>
Expand All @@ -158,5 +146,38 @@
</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>

<!--
Analytics are injected at runtime (below) rather than declared as
static <script> tags so they don't block first paint AND so SRI
requirements don't apply -- both GTM and SimpleAnalytics serve
continuously-updated bundles that would break any fixed integrity hash.
-->
<script>
globalThis.addEventListener("DOMContentLoaded", function () {
var d = globalThis.document;

// SimpleAnalytics
var sa = d.createElement("script");
sa.async = true;
sa.defer = true;
sa.src = "https://scripts.simpleanalyticscdn.com/latest.js";
d.body.appendChild(sa);

// Google Analytics (gtag)
var ga = d.createElement("script");
ga.async = true;
ga.src =
"https://www.googletagmanager.com/gtag/js?id=G-PFFMG7D8DP";
d.body.appendChild(ga);

globalThis.dataLayer = globalThis.dataLayer || [];
function gtag() {
globalThis.dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-PFFMG7D8DP");
});
</script>
</body>
</html>
69 changes: 69 additions & 0 deletions src/components/ui/ModalHeaderShell.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1, duration: 0.4, ease: EXPO_EASE }}
style={{
position: "sticky",
top: 0,
zIndex: 10,
padding: isMobile ? "16px 16px 12px" : "20px 24px 16px",
background: "rgba(12,12,28,0.9)",
backdropFilter: "blur(20px)",
WebkitBackdropFilter: "blur(20px)",
borderBottom: "1px solid rgba(255,255,255,0.06)",
}}
>
<div style={{ position: "relative" }}>
<button
onClick={onClose}
aria-label={closeLabel}
style={{
position: "absolute",
top: 0,
right: 0,
padding: 8,
background: "rgba(255,255,255,0.06)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: 10,
cursor: "pointer",
color: TEXT_SECONDARY,
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<X size={16} />
</button>
<div style={{ paddingRight: 44 }}>{children}</div>
</div>
</motion.div>
);

export default ModalHeaderShell;
97 changes: 97 additions & 0 deletions src/components/ui/ModalShell.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement | null>;
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(
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
onClick={onClose}
style={{
position: "fixed",
inset: 0,
zIndex: 1000,
display: "flex",
alignItems: isMobile ? "flex-end" : "center",
justifyContent: "center",
padding: isMobile ? 0 : 20,
background: "rgba(0,0,0,0.3)",
backdropFilter: "blur(12px)",
WebkitBackdropFilter: "blur(12px)",
overscrollBehavior: "contain",
}}
>
<motion.div
ref={dialogRef}
initial={{
opacity: 0,
y: isMobile ? 100 : 50,
scale: 0.95,
}}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{
opacity: 0,
y: isMobile ? 100 : 30,
scale: 0.97,
}}
transition={{ duration: 0.4, ease: EASING.cinematic }}
onClick={(e) => 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}
</motion.div>
</motion.div>
)}
</AnimatePresence>,
document.body,
);

export default ModalShell;
86 changes: 17 additions & 69 deletions src/pages/experience/ExperienceModal.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -34,75 +33,24 @@ const ExperienceModal = ({ experience, onClose }: Props) => {
}, [experience, onEsc]);

return (
<AnimatePresence>
<ModalShell
isOpen={Boolean(experience)}
onClose={onClose}
dialogRef={dialogRef}
isMobile={isMobile}
titleId="experience-modal-title"
>
{experience && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
onClick={onClose}
style={{
position: "fixed",
inset: 0,
zIndex: 1000,
display: "flex",
alignItems: isMobile ? "flex-end" : "center",
justifyContent: "center",
padding: isMobile ? 0 : 20,
background: "rgba(0,0,0,0.3)",
backdropFilter: "blur(12px)",
WebkitBackdropFilter: "blur(12px)",
overscrollBehavior: "contain",
}}
>
<motion.div
ref={dialogRef}
initial={{
opacity: 0,
y: isMobile ? 100 : 50,
scale: 0.95,
}}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{
opacity: 0,
y: isMobile ? 100 : 30,
scale: 0.97,
}}
transition={{
duration: 0.4,
ease: EASING.cinematic,
}}
onClick={(e) => 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)",
}}
>
<ModalHeader
experience={experience}
onClose={onClose}
isMobile={isMobile}
/>
<ModalContent experience={experience} isMobile={isMobile} />
</motion.div>
</motion.div>
<>
<ModalHeader
experience={experience}
onClose={onClose}
isMobile={isMobile}
/>
<ModalContent experience={experience} isMobile={isMobile} />
</>
)}
</AnimatePresence>
</ModalShell>
);
};

Expand Down
Loading
Loading