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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env
.env.astro
3 changes: 2 additions & 1 deletion src/layouts/Base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const ogImage = new URL("/og.png", Astro.site);
<meta property="og:image" content={ogImage} />
<meta name="twitter:card" content="summary_large_image" />
<script is:inline>
// Apply the saved theme before first paint to avoid a flash.
// Mark JS as available (enables reveal animations) and apply the saved theme before first paint.
document.documentElement.classList.add("js");
try {
var t = localStorage.getItem("theme");
if (t === "light" || t === "dark") document.documentElement.dataset.theme = t;
Expand Down
79 changes: 78 additions & 1 deletion src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const jsonLd = {
<Base>
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />

<div class="progress" id="progress" aria-hidden="true"></div>

<header class="site">
<div class="wrap">
<a class="brand" href="#top">ersin<span>@</span>mutlu</a>
Expand Down Expand Up @@ -47,7 +49,7 @@ const jsonLd = {
<a class="btn" href={profile.links.linkedin} target="_blank" rel="noopener">LinkedIn</a>
</div>
</div>
<Image src={profilePic} alt="Portrait of Ersin Mutlu" width={352} height={352} loading="eager" />
<div class="avatar"><Image src={profilePic} alt="Portrait of Ersin Mutlu" width={352} height={352} loading="eager" /></div>
</div>
</section>

Expand Down Expand Up @@ -150,6 +152,9 @@ const jsonLd = {

<script>
const root = document.documentElement;
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;

// Theme toggle
document.getElementById("theme-toggle")?.addEventListener("click", () => {
const current =
root.dataset.theme ??
Expand All @@ -158,5 +163,77 @@ const jsonLd = {
root.dataset.theme = next;
try { localStorage.setItem("theme", next); } catch {}
});

// Scroll progress bar
const bar = document.getElementById("progress");
const onScroll = () => {
const max = root.scrollHeight - innerHeight;
if (bar) bar.style.transform = `scaleX(${max > 0 ? scrollY / max : 0})`;
};
addEventListener("scroll", onScroll, { passive: true });
onScroll();

// Reveal on scroll (staggered inside each group)
const targets = document.querySelectorAll<HTMLElement>(
"section:not(.hero) h2, .about p, .skills .card, .job, .grid .card, .post, .note, .contact p, .contact .actions",
);
if (reduced || !("IntersectionObserver" in window)) {
targets.forEach((el) => el.classList.add("in"));
} else {
const io = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (e.isIntersecting) {
e.target.classList.add("in");
io.unobserve(e.target);
}
}
},
{ threshold: 0.12, rootMargin: "0px 0px -40px 0px" },
);
targets.forEach((el) => {
const siblings = el.parentElement ? [...el.parentElement.children] : [];
el.style.setProperty("--i", String(Math.min(siblings.indexOf(el), 6)));
el.classList.add("reveal");
io.observe(el);
});
}

// Active nav link (scrollspy)
const links = new Map<string, HTMLAnchorElement>();
document.querySelectorAll<HTMLAnchorElement>("nav a[href^='#']").forEach((a) => links.set(a.hash.slice(1), a));
const spy = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (e.isIntersecting) {
links.forEach((a) => a.removeAttribute("aria-current"));
links.get(e.target.id)?.setAttribute("aria-current", "true");
}
}
},
{ rootMargin: "-45% 0px -50% 0px" },
);
document.querySelectorAll("main section[id]").forEach((s) => spy.observe(s));

// Cursor spotlight on cards
document.querySelectorAll<HTMLElement>(".card").forEach((card) => {
card.addEventListener("pointermove", (e) => {
const r = card.getBoundingClientRect();
card.style.setProperty("--mx", `${e.clientX - r.left}px`);
card.style.setProperty("--my", `${e.clientY - r.top}px`);
});
});

// Subtle tilt on the avatar
const avatar = document.querySelector<HTMLElement>(".avatar");
if (avatar && !reduced) {
avatar.addEventListener("pointermove", (e) => {
const r = avatar.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width - 0.5;
const y = (e.clientY - r.top) / r.height - 0.5;
avatar.style.transform = `perspective(600px) rotateY(${x * 14}deg) rotateX(${-y * 14}deg) scale(1.04)`;
});
avatar.addEventListener("pointerleave", () => (avatar.style.transform = ""));
}
</script>
</Base>
96 changes: 94 additions & 2 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,9 @@ nav a:hover { color: var(--text); }
.hero h1 { font-size: clamp(2.5rem, 7vw, 4rem); letter-spacing: -0.02em; }
.hero .role { font-size: clamp(1.1rem, 3vw, 1.5rem); color: var(--muted); margin: .5rem 0 1.5rem; }
.hero .tagline { max-width: 36rem; color: var(--muted); margin: 0 0 2rem; }
.hero img { width: 11rem; height: 11rem; border-radius: 50%; object-fit: cover; border: 2px solid var(--border); }
.hero img { border-radius: 50%; object-fit: cover; border: 2px solid var(--border); display: block; }
@media (max-width: 40rem) {
.hero .wrap { grid-template-columns: 1fr; }
.hero img { order: -1; width: 7.5rem; height: 7.5rem; }
}
.actions { display: flex; flex-wrap: wrap; gap: .75rem; }
.btn {
Expand Down Expand Up @@ -138,3 +137,96 @@ a.project:hover { border-color: var(--accent); transform: translateY(-2px); }
footer.site { border-top: 1px solid var(--border); padding: 2rem 0; color: var(--muted); font-size: .88rem; }
footer.site .wrap { display: flex; flex-wrap: wrap; justify-content: space-between; gap: .5rem; }


/* ---------- Motion & interaction ---------- */
.progress {
position: fixed; inset: 0 0 auto 0; height: 3px; z-index: 20;
background: var(--accent); transform: scaleX(0); transform-origin: left;
}

/* Nav: animated underline + active state */
nav a:not(.theme-toggle) { position: relative; padding: .25rem 0; transition: color .2s; }
nav a:not(.theme-toggle)::after {
content: ""; position: absolute; left: 0; right: 0; bottom: -2px; height: 2px;
background: var(--accent); transform: scaleX(0); transform-origin: left; transition: transform .25s ease;
}
nav a:hover::after, nav a[aria-current="true"]::after { transform: scaleX(1); }
nav a[aria-current="true"] { color: var(--text); }
.brand { transition: letter-spacing .25s; }
.brand:hover { letter-spacing: .04em; }
.theme-toggle { transition: transform .4s ease, border-color .2s, background .2s; }
.theme-toggle:hover { transform: rotate(180deg); background: var(--surface); }

/* Buttons */
.btn { transition: transform .2s ease, box-shadow .2s ease, border-color .2s, background .2s; }
.btn:hover { transform: translateY(-3px); box-shadow: 0 8px 20px -8px color-mix(in srgb, var(--accent) 60%, transparent); }
.btn:active { transform: translateY(-1px); }

/* Cards: lift + cursor spotlight */
.card { position: relative; overflow: hidden; transition: transform .25s ease, border-color .25s, box-shadow .25s; }
.card::before {
content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0; transition: opacity .25s;
background: radial-gradient(18rem circle at var(--mx, 50%) var(--my, 50%), color-mix(in srgb, var(--accent) 16%, transparent), transparent 70%);
}
.card:hover { transform: translateY(-4px); border-color: var(--accent); box-shadow: 0 14px 30px -18px color-mix(in srgb, var(--accent) 55%, transparent); }
.card:hover::before { opacity: 1; }
.card > * { position: relative; }
a.project:hover { transform: translateY(-4px); }
.project::after { content: "↗"; position: absolute; top: 1rem; right: 1.1rem; color: var(--accent); opacity: 0; transform: translate(-4px, 4px); transition: .25s; }
a.project:hover::after { opacity: 1; transform: none; }

/* Chips */
.chips li { transition: color .2s, border-color .2s, background .2s, transform .2s; }
.chips li:hover { color: var(--accent); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); transform: translateY(-2px); }

/* Language links */
.langs a { transition: transform .2s, border-color .2s, color .2s, background .2s; }
.langs a:hover { transform: translateY(-2px); background: color-mix(in srgb, var(--accent) 10%, transparent); }

/* Timeline dot pulse + job hover */
.job::before { transition: transform .25s; }
.job:hover::before { transform: scale(1.5); }
.job:first-child::before { animation: pulse 2.4s ease-out infinite; }
@keyframes pulse {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); }
80%, 100% { box-shadow: 0 0 0 10px transparent; }
}

/* Avatar */
.avatar { width: 11rem; height: 11rem; border-radius: 50%; position: relative; transition: transform .2s ease-out; will-change: transform; }
.avatar::before {
content: ""; position: absolute; inset: -6px; border-radius: 50%; z-index: -1;
background: conic-gradient(from 0deg, var(--accent), transparent 40%, var(--accent));
opacity: .55; animation: spin 8s linear infinite; filter: blur(8px);
}
.avatar img { width: 100%; height: 100%; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 40rem) { .avatar { order: -1; width: 7.5rem; height: 7.5rem; } }

/* Hero entrance + blinking cursor */
.hero .wrap > div > *, .hero .avatar { animation: rise .7s cubic-bezier(.2,.7,.2,1) both; }
.hero .wrap > div > :nth-child(1) { animation-delay: .05s; }
.hero .wrap > div > :nth-child(2) { animation-delay: .15s; }
.hero .wrap > div > :nth-child(3) { animation-delay: .25s; }
.hero .wrap > div > :nth-child(4) { animation-delay: .35s; }
.hero .wrap > div > :nth-child(5) { animation-delay: .45s; }
.hero .avatar { animation-delay: .3s; }
@keyframes rise { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
.eyebrow::after { content: "▍"; margin-left: .25rem; animation: blink 1s steps(2, start) infinite; }
@keyframes blink { to { visibility: hidden; } }

/* Section headings: animated prompt */
h2::before { display: inline-block; transition: transform .3s; }
h2:hover::before { transform: translateX(4px); }

/* Scroll reveal (only when JS is available) */
.js .reveal { opacity: 0; transform: translateY(24px); transition: opacity .6s ease, transform .6s cubic-bezier(.2,.7,.2,1); transition-delay: calc(var(--i, 0) * 80ms); }
.js .reveal.in { opacity: 1; transform: none; }
/* Keep hover transforms snappy once revealed */
.js .card.reveal.in:hover { transform: translateY(-4px); }
.js .card.reveal.in { transition: opacity .6s ease, transform .25s ease, border-color .25s, box-shadow .25s; transition-delay: calc(var(--i, 0) * 80ms), 0s, 0s, 0s; }

@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: none !important; }
.js .reveal { opacity: 1; transform: none; }
}
Loading