diff --git a/src/App.test.tsx b/src/App.test.tsx
index 1c2e440..2382012 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -51,10 +51,6 @@ describe("App", () => {
"href",
"#contact",
);
- expect(screen.getByRole("link", { name: /resume\.pdf/i })).toHaveAttribute(
- "href",
- "/Rashell-Guerrero-Resume.pdf",
- );
});
it("has accessible social links", () => {
diff --git a/src/components/layout/Navbar.css b/src/components/layout/Navbar.css
index 6503d29..e5d5bbd 100644
--- a/src/components/layout/Navbar.css
+++ b/src/components/layout/Navbar.css
@@ -65,6 +65,9 @@
text-transform: uppercase;
font-size: 0.8rem;
padding: 0.5rem 1.5rem;
+ border: 0;
+ font-family: inherit;
+ cursor: pointer;
}
.resume-section:hover {
diff --git a/src/components/layout/Navbar.test.tsx b/src/components/layout/Navbar.test.tsx
index a774ef7..96a1f71 100644
--- a/src/components/layout/Navbar.test.tsx
+++ b/src/components/layout/Navbar.test.tsx
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it } from "vitest";
-import { render, screen, within } from "@testing-library/react";
+import { fireEvent, render, screen, within } from "@testing-library/react";
import Navbar from "./Navbar";
describe("Navbar", () => {
@@ -31,9 +31,11 @@ describe("Navbar", () => {
);
});
- it("renders the resume download link", () => {
- const resumeLink = within(nav).getByRole("link", { name: /resume\.pdf/i });
- expect(resumeLink).toHaveAttribute("href", "/Rashell-Guerrero-Resume.pdf");
- expect(resumeLink).toHaveAttribute("download");
+ it("opens the resume modal", () => {
+ fireEvent.click(within(nav).getByRole("button", { name: /resume\.pdf/i }));
+
+ expect(
+ screen.getByRole("dialog", { name: /hey there/i }),
+ ).toBeInTheDocument();
});
});
diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx
index ef69542..48c4eea 100644
--- a/src/components/layout/Navbar.tsx
+++ b/src/components/layout/Navbar.tsx
@@ -1,11 +1,18 @@
import { useEffect, useState } from "react";
import { RiDownloadLine } from "react-icons/ri";
import { navigationLinks } from "../../data/navigation";
+import ResumeModal from "../ui/ResumeModal";
import "./Navbar.css";
function Navbar() {
const [activeSection, setActiveSection] = useState("hero");
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+ const [isResumeModalOpen, setIsResumeModalOpen] = useState(false);
+
+ function handleGetInTouch() {
+ setIsResumeModalOpen(false);
+ document.getElementById("contact")?.scrollIntoView({ behavior: "smooth" });
+ }
useEffect(() => {
const sections = navigationLinks
@@ -50,14 +57,14 @@ function Navbar() {
{navLinks()}
)}
+
+ setIsResumeModalOpen(false)}
+ onGetInTouch={handleGetInTouch}
+ />
);
}
diff --git a/src/components/ui/ResumeModal.css b/src/components/ui/ResumeModal.css
new file mode 100644
index 0000000..76bf268
--- /dev/null
+++ b/src/components/ui/ResumeModal.css
@@ -0,0 +1,127 @@
+.resume-modal {
+ position: fixed;
+ inset: 0;
+ z-index: 100;
+ padding: 1.5rem;
+ background: rgba(0, 0, 0, 0.85);
+ backdrop-filter: blur(2px);
+ animation: modal-fade 0.15s ease-out;
+}
+
+.resume-modal > div {
+ width: 100%;
+ max-width: 30rem;
+ border: 1px solid var(--primary);
+ background: #000;
+ box-shadow: 0 0 24px rgba(242, 119, 165, 0.25);
+ outline: none;
+ animation: modal-pop 0.18s ease-out;
+}
+
+@keyframes modal-fade {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes modal-pop {
+ from {
+ opacity: 0;
+ transform: scale(0.96) translateY(6px);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+.resume-modal .titlebar {
+ justify-content: space-between;
+ padding: 0.6rem 0.9rem;
+ border-bottom: 1px solid var(--primary);
+ color: var(--primary);
+}
+
+.resume-modal .titlebar > span {
+ font-size: 0.8rem;
+ font-weight: 700;
+ letter-spacing: 0.05em;
+}
+
+.resume-modal .titlebar > div {
+ gap: 0.7rem;
+ font-size: 0.85rem;
+}
+
+.resume-modal .titlebar button {
+ padding: 0;
+ border: 0;
+ background: none;
+ color: inherit;
+ font-size: 0.95rem;
+ cursor: pointer;
+}
+
+.resume-modal .body {
+ padding: 1.75rem 1.5rem 1rem;
+ color: rgba(255, 255, 255, 0.85);
+ font-size: 0.85rem;
+ line-height: 1.55;
+}
+
+.resume-modal h2 {
+ margin: 0;
+ color: #fff;
+ font-size: 1.25rem;
+ font-weight: 800;
+}
+
+.resume-modal p { margin: 0.5rem 0; }
+
+.resume-modal .body::after {
+ content: "";
+ display: block;
+ width: 1.6rem;
+ height: 2px;
+ margin: 0.7rem 0;
+ background: var(--primary);
+}
+
+.resume-modal .actions {
+ gap: 0.75rem;
+ padding: 0.25rem 1.5rem 1.25rem;
+}
+
+.resume-modal .actions button {
+ flex: 1;
+ gap: 0.5rem;
+ padding: 0.7rem 0.9rem;
+ border: 1px solid var(--primary);
+ background: transparent;
+ color: #fff;
+ font-family: inherit;
+ font-size: 0.78rem;
+ letter-spacing: 0.03em;
+ text-transform: uppercase;
+ cursor: pointer;
+ transition: box-shadow 0.15s;
+}
+
+.resume-modal .actions .primary {
+ background: var(--primary);
+ color: #000;
+ font-weight: 700;
+}
+
+.resume-modal .actions button:hover {
+ box-shadow: 0 0 10px var(--primary);
+}
+
+@media (max-width: 480px) {
+ .resume-modal .body {
+ padding-top: 1.5rem;
+ text-align: center;
+ }
+
+ .resume-modal .actions { flex-direction: column; }
+}
\ No newline at end of file
diff --git a/src/components/ui/ResumeModal.tsx b/src/components/ui/ResumeModal.tsx
new file mode 100644
index 0000000..c6a5a16
--- /dev/null
+++ b/src/components/ui/ResumeModal.tsx
@@ -0,0 +1,95 @@
+import { useEffect, useRef } from "react";
+import { FiArrowRight, FiMail, FiX } from "react-icons/fi";
+import "./ResumeModal.css";
+
+type Props = {
+ isOpen: boolean;
+ onClose: () => void;
+ onGetInTouch: () => void;
+};
+
+export default function ResumeModal({ isOpen, onClose, onGetInTouch }: Props) {
+ const modalRef = useRef(null);
+
+ useEffect(() => {
+ if (!isOpen) return;
+
+ const closeOnEscape = ({ key }: KeyboardEvent) =>
+ key === "Escape" && onClose();
+ const { style } = document.body;
+ const overflow = style.overflow;
+
+ document.addEventListener("keydown", closeOnEscape);
+ style.overflow = "hidden";
+ modalRef.current?.focus();
+
+ return () => {
+ document.removeEventListener("keydown", closeOnEscape);
+ style.overflow = overflow;
+ };
+ }, [isOpen, onClose]);
+
+ if (!isOpen) return null;
+
+ return (
+
+ target === currentTarget && onClose()
+ }
+ >
+
+
+
RESUME.EXE
+
+
+ _
+ □
+
+
+
+
+
+
+
+ Hey there! 👋
+
+
+
+ Thanks for your interest in my work. I'd love to connect and share
+ my resume with you personally.
+
+
+
+
+
+
+
+
+
+
+ );
+}