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
4 changes: 0 additions & 4 deletions src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
3 changes: 3 additions & 0 deletions src/components/layout/Navbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 7 additions & 5 deletions src/components/layout/Navbar.test.tsx
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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();
});
});
23 changes: 18 additions & 5 deletions src/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -50,14 +57,14 @@ function Navbar() {
<div className="navbar-links">{navLinks()}</div>

<div className="resume-and-menu">
<a
<button
type="button"
className="resume-section"
href="/Rashell-Guerrero-Resume.pdf"
download
onClick={() => setIsResumeModalOpen(true)}
>
Resume.pdf
<RiDownloadLine />
</a>
<RiDownloadLine aria-hidden="true" />
</button>

<button
className="mobile-menu-button"
Expand All @@ -74,6 +81,12 @@ function Navbar() {
{isMobileMenuOpen && (
<div className="mobile-navbar-menu">{navLinks(true)}</div>
)}

<ResumeModal
isOpen={isResumeModalOpen}
onClose={() => setIsResumeModalOpen(false)}
onGetInTouch={handleGetInTouch}
/>
</header>
);
}
Expand Down
127 changes: 127 additions & 0 deletions src/components/ui/ResumeModal.css
Original file line number Diff line number Diff line change
@@ -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; }
}
95 changes: 95 additions & 0 deletions src/components/ui/ResumeModal.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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 (
<div
className="resume-modal center-box"
onMouseDown={({ target, currentTarget }) =>
target === currentTarget && onClose()
}
>
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="resume-modal-title"
tabIndex={-1}
>
<div className="titlebar center-row">
<span>RESUME.EXE</span>

<div className="center-row">
<span aria-hidden="true">_</span>
<span aria-hidden="true">□</span>

<button
type="button"
className="ui-button"
onClick={onClose}
aria-label="Close dialog"
>
<FiX aria-hidden="true" />
</button>
</div>
</div>

<div className="body">
<h2 id="resume-modal-title">
Hey there! <span aria-hidden="true">👋</span>
</h2>

<p>
Thanks for your interest in my work. I'd love to connect and share
my resume with you personally.
</p>
</div>

<div className="actions center-row">
<button
type="button"
className="primary ui-button"
onClick={onGetInTouch}
>
Get in touch
<FiArrowRight aria-hidden="true" />
<FiMail aria-hidden="true" />
</button>

<button type="button" className="ui-button" onClick={onClose}>
Maybe later
</button>
</div>
</div>
</div>
);
}
Loading