parent
56af4599a2
commit
f64e9d2e4c
22 changed files with 757 additions and 366 deletions
149
src/components/About.astro
Normal file
149
src/components/About.astro
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
---
|
||||
import ExperienceRow from "@/components/ExperienceRow.astro";
|
||||
---
|
||||
|
||||
<section
|
||||
id="about"
|
||||
data-node-id="4:104"
|
||||
class="min-h-full snap-start snap-always bg-background text-foreground lg:h-full lg:overflow-hidden"
|
||||
aria-labelledby="about-title"
|
||||
>
|
||||
<div class="grid min-h-full grid-cols-1 lg:h-full lg:grid-cols-2">
|
||||
<div
|
||||
class="relative z-10 px-6 py-12 sm:px-10 lg:px-[clamp(52px,5.208vw,100px)] lg:py-[clamp(58px,9.26vh,100px)]"
|
||||
>
|
||||
<div
|
||||
class="max-w-[654px] text-[24px] leading-[1.2] tracking-[-0.02em] lg:leading-none"
|
||||
>
|
||||
<p id="about-title">저는 이런 것들을 해왔어요.</p>
|
||||
|
||||
<section class="mt-[0.75em]" aria-labelledby="security-title">
|
||||
<h2
|
||||
id="security-title"
|
||||
class="text-[inherit] font-bold leading-[inherit]"
|
||||
>
|
||||
Information security
|
||||
</h2>
|
||||
<div class="mt-[0.5em] flex flex-col gap-[0.34em]">
|
||||
<ExperienceRow year="2026" connector="wave">
|
||||
<img
|
||||
class="h-[1em] w-[1em] shrink-0"
|
||||
src="/about/layer7-mark.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>선린인터넷고등학교 정보보안 동아리</span>
|
||||
<img
|
||||
class="h-[1em] w-[1em] shrink-0 [filter:var(--mono-filter)]"
|
||||
src="/about/layer7-wordmark.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>Layer7</span>
|
||||
</ExperienceRow>
|
||||
|
||||
<ExperienceRow year="2026" connector="wave">
|
||||
<img
|
||||
class="h-[1em] w-[1em] shrink-0"
|
||||
src="/about/sunrin.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>선린인터넷고등학교 정보보호과</span>
|
||||
</ExperienceRow>
|
||||
|
||||
<ExperienceRow year="2025" connector="line">
|
||||
<img
|
||||
class="h-[1em] w-[1em] shrink-0 object-contain"
|
||||
src="/about/whitehat.png"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>화이트햇스쿨 3기 이수</span>
|
||||
</ExperienceRow>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-[0.75em]" aria-labelledby="software-title">
|
||||
<h2
|
||||
id="software-title"
|
||||
class="text-[inherit] font-bold leading-[inherit]"
|
||||
>
|
||||
Software engineering
|
||||
</h2>
|
||||
<div class="mt-[0.5em] flex flex-col gap-[0.34em]">
|
||||
<ExperienceRow year="2025" connector="line">
|
||||
<img
|
||||
class="h-[1em] w-[1em] shrink-0"
|
||||
src="/about/adofai.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>ADOFAI.gg</span>
|
||||
<span class="text-foreground/70"
|
||||
>Full Stack, Mod Developer</span
|
||||
>
|
||||
</ExperienceRow>
|
||||
|
||||
<div
|
||||
class="grid min-w-0 grid-cols-[2.25em_0.55em_minmax(0,1fr)] items-start gap-[0.5em]"
|
||||
>
|
||||
<time class="whitespace-nowrap" datetime="2026"
|
||||
>2026</time
|
||||
>
|
||||
<img
|
||||
class="mt-[0.08em] h-[1em] w-[0.55em] [filter:var(--mono-filter)]"
|
||||
src="/about/contest-separator.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
class="grid min-w-0 grid-cols-[1em_minmax(0,1fr)] items-start gap-[0.34em]"
|
||||
>
|
||||
<img
|
||||
class="mt-[0.22em] h-[0.55em] w-[1em] [filter:var(--mono-filter)]"
|
||||
src="/about/contest.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
class="grid min-w-0 grid-cols-1 gap-[0.35em] lg:grid-cols-[minmax(11em,1.5fr)_minmax(8.6em,1fr)] lg:gap-[0.75em]"
|
||||
>
|
||||
<div
|
||||
class="flex flex-wrap content-start gap-x-[0.5em]"
|
||||
>
|
||||
<span>Effect Playing Contest</span>
|
||||
<span class="text-foreground/70"
|
||||
>2025 · Finale</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col text-foreground/70"
|
||||
>
|
||||
<span>Broadcast Engineer</span>
|
||||
<span>ARG Creator</span>
|
||||
<span>Full Stack Developer</span>
|
||||
<span>Mod Developer</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="relative h-[min(150vw,720px)] min-h-[600px] overflow-hidden lg:h-auto lg:min-h-0"
|
||||
aria-label="활동 사진"
|
||||
>
|
||||
<img
|
||||
class="absolute inset-0 h-full w-full select-none object-contain"
|
||||
src="/side.png"
|
||||
alt="활동 사진 콜라주"
|
||||
width="3840"
|
||||
height="4320"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -1,121 +1,187 @@
|
|||
---
|
||||
const contacts = [
|
||||
{ name: "Email", icon: "mail", action: "dialog" },
|
||||
{ name: "Blog", icon: "rss", url: "https://blog.imnya.ng" },
|
||||
{ name: "GitHub", icon: "github", url: "https://github.com/imnyang" },
|
||||
{ name: "git.mizuki.guru", icon: "git", url: "https://git.mizuki.guru/imnyang" },
|
||||
{ name: "Instagram", icon: "instagram", url: "https://instagram.com/imnya.ng" },
|
||||
{ name: "𝕏", icon: "x", url: "https://x.com/imnya_ng" },
|
||||
{ name: "Email", icon: "mail", action: "dialog" },
|
||||
{ name: "Blog", icon: "rss", url: "https://blog.imnya.ng" },
|
||||
{ name: "GitHub", icon: "github", url: "https://github.com/imnyang" },
|
||||
{
|
||||
name: "git.mizuki.guru",
|
||||
icon: "git",
|
||||
url: "https://git.mizuki.guru/imnyang",
|
||||
},
|
||||
{
|
||||
name: "Instagram",
|
||||
icon: "instagram",
|
||||
url: "https://instagram.com/imnya.ng",
|
||||
},
|
||||
{ name: "𝕏", icon: "x", url: "https://x.com/imnya_ng" },
|
||||
];
|
||||
|
||||
const emailContacts = [
|
||||
{ label: "Personal", address: "me@imnya.ng" },
|
||||
{ label: "Dazzle.st", address: "imnyang@dazzle.st" },
|
||||
{ label: "Personal", address: "me@imnya.ng" },
|
||||
{ label: "Dazzle.st", address: "imnyang@dazzle.st" },
|
||||
];
|
||||
|
||||
const icons: Record<string, string> = {
|
||||
mail: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>`,
|
||||
rss: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/></svg>`,
|
||||
github: `<img src="/icon/github.svg" width="24" height="24" alt="GitHub" class="w-6 h-6" />`,
|
||||
git: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>`,
|
||||
instagram: `<img src="/icon/instagram.svg" width="18" height="18" alt="Instagram" class="w-4 h-4" />`,
|
||||
x: `<p class="text-[20px] font-bold leading-none">𝕏</p>`,
|
||||
mail: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>`,
|
||||
rss: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/></svg>`,
|
||||
github: `<img src="/icon/github.svg" width="24" height="24" alt="GitHub" class="h-6 w-6 transition-[filter] duration-300 [filter:var(--mono-filter)]" />`,
|
||||
git: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>`,
|
||||
instagram: `<img src="/icon/instagram.svg" width="18" height="18" alt="Instagram" class="h-4 w-4 transition-[filter] duration-300 [filter:var(--mono-filter)]" />`,
|
||||
x: `<p class="text-[20px] font-bold leading-none">𝕏</p>`,
|
||||
};
|
||||
---
|
||||
|
||||
<div class="flex flex-row space-x-4 relative">
|
||||
{contacts.map((c) => (
|
||||
c.action === "dialog" ? (
|
||||
<button
|
||||
type="button"
|
||||
id="email-btn"
|
||||
title={c.name}
|
||||
class="flex items-center space-x-2 text-foreground/80 hover:text-foreground transition-colors hover:cursor-pointer"
|
||||
set:html={icons[c.icon]}
|
||||
/>
|
||||
) : (
|
||||
<a
|
||||
href={c.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={c.name}
|
||||
class="flex items-center space-x-2 text-foreground/80 hover:text-foreground transition-colors"
|
||||
set:html={icons[c.icon]}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
<div class="flex flex-row space-x-4 relative select-none">
|
||||
{
|
||||
contacts.map((c) =>
|
||||
c.action === "dialog" ? (
|
||||
<button
|
||||
type="button"
|
||||
id="email-btn"
|
||||
title={c.name}
|
||||
class="flex items-center space-x-2 text-foreground/80 hover:text-foreground transition-colors hover:cursor-pointer"
|
||||
set:html={icons[c.icon]}
|
||||
/>
|
||||
) : (
|
||||
<a
|
||||
href={c.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={c.name}
|
||||
class="flex items-center space-x-2 text-foreground/80 hover:text-foreground transition-colors"
|
||||
set:html={icons[c.icon]}
|
||||
/>
|
||||
),
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Email Dialog -->
|
||||
<div
|
||||
id="email-dialog"
|
||||
class="hidden fixed inset-0 z-50 flex items-center justify-center"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Contact Me"
|
||||
id="email-dialog"
|
||||
class="hidden fixed inset-0 z-50 flex items-center justify-center"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Contact Me"
|
||||
>
|
||||
<div id="email-dialog-backdrop" class="absolute inset-0 bg-black/50"></div>
|
||||
<div class="relative z-10 bg-background border border-border rounded-xl shadow-xl p-6 w-full max-w-sm mx-4">
|
||||
<h2 class="text-lg font-semibold mb-1">Contact Me</h2>
|
||||
<p class="text-sm text-muted-foreground mb-4">이메일 주소를 클릭하면 복사되고, 더블클릭하면 메일 앱이 열립니다.</p>
|
||||
<div class="flex flex-col gap-3">
|
||||
{emailContacts.map((ec) => (
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground mb-1">{ec.label}</p>
|
||||
<button
|
||||
type="button"
|
||||
data-email={ec.address}
|
||||
class="email-copy-btn w-full flex items-center gap-3 border border-border rounded-lg px-4 py-2.5 hover:bg-muted transition-colors text-left active:scale-95 transition-transform"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
|
||||
<span class="flex-1 text-sm">{ec.address}</span>
|
||||
<span class="copy-icon text-muted-foreground">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
||||
</span>
|
||||
</button>
|
||||
<div id="email-dialog-backdrop" class="absolute inset-0 bg-black/50"></div>
|
||||
<div
|
||||
class="relative z-10 bg-background border border-border rounded-xl shadow-xl p-6 w-full max-w-sm mx-4"
|
||||
>
|
||||
<h2 class="text-lg font-semibold mb-1">Contact Me</h2>
|
||||
<p class="text-sm text-muted-foreground mb-4">
|
||||
이메일 주소를 클릭하면 복사되고, 더블클릭하면 메일 앱이 열립니다.
|
||||
</p>
|
||||
<div class="flex flex-col gap-3">
|
||||
{
|
||||
emailContacts.map((ec) => (
|
||||
<div>
|
||||
<p class="text-xs text-muted-foreground mb-1">
|
||||
{ec.label}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
data-email={ec.address}
|
||||
class="email-copy-btn w-full flex items-center gap-3 border border-border rounded-lg px-4 py-2.5 hover:bg-muted transition-colors text-left active:scale-95 transition-transform"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="shrink-0"
|
||||
>
|
||||
<>
|
||||
<rect
|
||||
width="20"
|
||||
height="16"
|
||||
x="2"
|
||||
y="4"
|
||||
rx="2"
|
||||
/>
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
</>
|
||||
</svg>
|
||||
<span class="flex-1 text-sm">{ec.address}</span>
|
||||
<span class="copy-icon text-muted-foreground">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<>
|
||||
<rect
|
||||
width="14"
|
||||
height="14"
|
||||
x="8"
|
||||
y="8"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||
</>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
id="email-dialog-close"
|
||||
class="px-4 py-2 rounded bg-muted text-foreground text-sm hover:bg-muted/80 transition-colors cursor-pointer"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
id="email-dialog-close"
|
||||
class="px-4 py-2 rounded bg-muted text-foreground text-sm hover:bg-muted/80 transition-colors cursor-pointer"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const dialog = document.getElementById("email-dialog")!;
|
||||
const openBtn = document.getElementById("email-btn")!;
|
||||
const closeBtn = document.getElementById("email-dialog-close")!;
|
||||
const backdrop = document.getElementById("email-dialog-backdrop")!;
|
||||
const dialog = document.getElementById("email-dialog")!;
|
||||
const openBtn = document.getElementById("email-btn")!;
|
||||
const closeBtn = document.getElementById("email-dialog-close")!;
|
||||
const backdrop = document.getElementById("email-dialog-backdrop")!;
|
||||
|
||||
const open = () => dialog.classList.remove("hidden");
|
||||
const close = () => dialog.classList.add("hidden");
|
||||
const open = () => dialog.classList.remove("hidden");
|
||||
const close = () => dialog.classList.add("hidden");
|
||||
|
||||
openBtn.addEventListener("click", open);
|
||||
closeBtn.addEventListener("click", close);
|
||||
backdrop.addEventListener("click", close);
|
||||
document.addEventListener("keydown", (e) => { if (e.key === "Escape") close(); });
|
||||
|
||||
document.querySelectorAll<HTMLButtonElement>(".email-copy-btn").forEach((btn) => {
|
||||
const email = btn.dataset.email!;
|
||||
const copyIcon = btn.querySelector(".copy-icon")!;
|
||||
|
||||
btn.addEventListener("click", async () => {
|
||||
await navigator.clipboard.writeText(email);
|
||||
copyIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>`;
|
||||
setTimeout(() => {
|
||||
copyIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
|
||||
}, 1200);
|
||||
openBtn.addEventListener("click", open);
|
||||
closeBtn.addEventListener("click", close);
|
||||
backdrop.addEventListener("click", close);
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") close();
|
||||
});
|
||||
|
||||
btn.addEventListener("dblclick", () => {
|
||||
window.location.href = `mailto:${email}`;
|
||||
});
|
||||
});
|
||||
document
|
||||
.querySelectorAll<HTMLButtonElement>(".email-copy-btn")
|
||||
.forEach((btn) => {
|
||||
const email = btn.dataset.email!;
|
||||
const copyIcon = btn.querySelector(".copy-icon")!;
|
||||
|
||||
btn.addEventListener("click", async () => {
|
||||
await navigator.clipboard.writeText(email);
|
||||
copyIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>`;
|
||||
setTimeout(() => {
|
||||
copyIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
|
||||
}, 1200);
|
||||
});
|
||||
|
||||
btn.addEventListener("dblclick", () => {
|
||||
window.location.href = `mailto:${email}`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
29
src/components/ExperienceRow.astro
Normal file
29
src/components/ExperienceRow.astro
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
interface Props {
|
||||
year: string;
|
||||
connector: "wave" | "line";
|
||||
}
|
||||
|
||||
const { year, connector } = Astro.props;
|
||||
const connectorClass =
|
||||
connector === "wave"
|
||||
? "h-[0.17em] w-[0.52em] [filter:var(--mono-filter)]"
|
||||
: "h-[0.1em] w-[0.55em] [filter:var(--mono-filter)]";
|
||||
---
|
||||
|
||||
<div
|
||||
class="grid min-w-0 grid-cols-[2.25em_0.55em_minmax(0,1fr)] items-center gap-[0.5em]"
|
||||
>
|
||||
<time class="whitespace-nowrap" datetime={year}>{year}</time>
|
||||
<img
|
||||
class={connectorClass}
|
||||
src={`/about/separator-${connector}.svg`}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div
|
||||
class="flex min-w-0 flex-wrap items-center gap-x-[0.34em] gap-y-[0.2em] lg:flex-nowrap"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
117
src/components/Header.astro
Normal file
117
src/components/Header.astro
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
---
|
||||
const navigationItems = [
|
||||
{ label: "Home", href: "#home", nodeId: "43:29" },
|
||||
{ label: "About", href: "#about", nodeId: "43:30" },
|
||||
{ label: "Projects", href: "#projects", nodeId: "43:32" },
|
||||
{ label: "Timeline", href: "#timeline", nodeId: "43:36" },
|
||||
] as const;
|
||||
|
||||
const desktopLinkClass =
|
||||
"text-foreground no-underline transition-opacity duration-150 hover:opacity-55 focus-visible:opacity-55";
|
||||
const mobileLinkClass =
|
||||
"block rounded-xl px-3 py-2 text-sm font-semibold text-foreground no-underline transition-colors hover:bg-foreground/5";
|
||||
---
|
||||
|
||||
<header
|
||||
data-node-id="43:26"
|
||||
class="fixed inset-x-0 top-0 z-50 flex h-[64px] items-center justify-between overflow-clip border-b border-foreground/[0.06] bg-background/90 px-[clamp(26px,2.5vw,48px)] backdrop-blur-xl transition-colors duration-300 max-md:overflow-visible max-md:px-6"
|
||||
>
|
||||
<a
|
||||
class="shrink-0 text-2xl font-bold leading-[normal] text-foreground no-underline max-md:text-xl"
|
||||
href="#home"
|
||||
data-node-id="43:28">@imnya.ng</a
|
||||
>
|
||||
|
||||
<div id="navigation" class="relative flex shrink-0 items-center">
|
||||
<nav
|
||||
class="flex shrink-0 items-center gap-4 whitespace-nowrap text-base font-semibold leading-[1.5] tracking-[-0.02em] max-md:hidden"
|
||||
aria-label="주요 메뉴"
|
||||
data-node-id="43:40"
|
||||
>
|
||||
{
|
||||
navigationItems.map((item) => (
|
||||
<a
|
||||
class={desktopLinkClass}
|
||||
href={item.href}
|
||||
data-node-id={item.nodeId}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
|
||||
<button
|
||||
id="menu-toggle"
|
||||
type="button"
|
||||
class="relative hidden h-10 w-10 items-center justify-center rounded-full text-foreground transition-colors hover:bg-foreground/5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground max-md:flex"
|
||||
aria-label="메뉴 열기"
|
||||
aria-controls="mobile-nav"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">메뉴</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="absolute h-px w-5 -translate-y-1.5 bg-foreground"
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="absolute h-px w-5 bg-foreground"></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="absolute h-px w-5 translate-y-1.5 bg-foreground"
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<nav
|
||||
id="mobile-nav"
|
||||
class="absolute right-0 top-[calc(100%+12px)] z-50 hidden min-w-40 flex-col gap-1 rounded-2xl border border-border bg-background p-2 shadow-[0_12px_30px_rgba(0,0,0,0.18)] md:hidden"
|
||||
aria-label="모바일 메뉴"
|
||||
>
|
||||
{
|
||||
navigationItems.map((item) => (
|
||||
<a class={mobileLinkClass} href={item.href}>
|
||||
{item.label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script>
|
||||
const menuToggle =
|
||||
document.querySelector<HTMLButtonElement>("#menu-toggle");
|
||||
const mobileNav = document.querySelector<HTMLElement>("#mobile-nav");
|
||||
const navigation = document.querySelector<HTMLElement>("#navigation");
|
||||
|
||||
if (menuToggle && mobileNav && navigation) {
|
||||
const setMenuOpen = (isOpen: boolean) => {
|
||||
mobileNav.classList.toggle("hidden", !isOpen);
|
||||
mobileNav.classList.toggle("flex", isOpen);
|
||||
menuToggle.setAttribute("aria-expanded", String(isOpen));
|
||||
menuToggle.setAttribute(
|
||||
"aria-label",
|
||||
isOpen ? "메뉴 닫기" : "메뉴 열기",
|
||||
);
|
||||
};
|
||||
|
||||
menuToggle.addEventListener("click", () => {
|
||||
setMenuOpen(menuToggle.getAttribute("aria-expanded") !== "true");
|
||||
});
|
||||
|
||||
mobileNav.addEventListener("click", (event) => {
|
||||
if (event.target instanceof HTMLAnchorElement) setMenuOpen(false);
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") setMenuOpen(false);
|
||||
});
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (event.target instanceof Node && !navigation.contains(event.target)) {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
94
src/components/Hero.astro
Normal file
94
src/components/Hero.astro
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<div
|
||||
id="home"
|
||||
data-node-id="1:3"
|
||||
data-name="imnyang"
|
||||
class="flex h-full min-h-0 snap-start snap-always flex-col overflow-hidden"
|
||||
>
|
||||
<section
|
||||
class="flex h-[250px] min-h-[250px] min-w-0 flex-none basis-[250px] items-center overflow-clip px-[clamp(26px,2.5vw,48px)] max-md:px-6"
|
||||
aria-labelledby="hero-title"
|
||||
data-node-id="43:24"
|
||||
>
|
||||
<div class="w-full min-w-0" data-node-id="43:23">
|
||||
<h1
|
||||
id="hero-title"
|
||||
class="m-0 flex h-[250px] w-full max-w-full flex-col justify-center break-words whitespace-nowrap text-[clamp(34px,3.333333vw,64px)] font-bold leading-[normal] tracking-[0] max-md:whitespace-normal max-md:text-[clamp(24px,8vw,48px)] max-md:leading-[1.12]"
|
||||
>
|
||||
<span class="block" data-node-id="43:14">
|
||||
<span class="font-bold">항상 꿈꾸던 것</span><span
|
||||
class="font-normal text-foreground/50">들을</span
|
||||
>
|
||||
</span>
|
||||
<span class="block" data-node-id="43:16">
|
||||
<span class="font-bold">현실</span><span
|
||||
class="font-normal text-foreground/50"
|
||||
>로 만들어나가는</span
|
||||
><br class="hidden max-md:block md:hidden" /><span
|
||||
class="font-bold"> 암냥</span
|
||||
><span class="font-normal text-foreground/50"
|
||||
>입니다.</span
|
||||
>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="min-h-0 w-full min-w-0 flex-1 basis-0 px-[clamp(26px,2.5vw,48px)] max-md:px-6"
|
||||
data-node-id="44:41"
|
||||
>
|
||||
<div
|
||||
class="relative isolate h-full min-h-0 w-full overflow-hidden rounded-tl-[clamp(10px,0.833333vw,16px)] rounded-tr-[clamp(10px,0.833333vw,16px)]"
|
||||
data-node-id="42:10"
|
||||
>
|
||||
<picture class="contents">
|
||||
<source
|
||||
id="hero-theme-source"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="/hero-dark.png"
|
||||
type="image/png"
|
||||
/>
|
||||
<img
|
||||
class="block h-full w-full object-cover object-center transition-[scale,border-radius,box-shadow] duration-500 ease-[var(--ease-out-expo)] hover:z-10 hover:scale-105 hover:rounded-lg hover:shadow-[0_15px_45px_#0006] motion-reduce:transition-none"
|
||||
src="/hero-light.png"
|
||||
alt="벚꽃이 핀 수로에서 보트를 타고 있는 암냥"
|
||||
width="3840"
|
||||
height="2160"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
</picture>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute inset-x-0 bottom-0 z-20 h-[min(150px,35%)] bg-gradient-to-b from-transparent via-background/20 to-background backdrop-blur-[2px] [mask-image:linear-gradient(to_bottom,transparent,black_42%)]"
|
||||
></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const heroThemeSource =
|
||||
document.querySelector<HTMLSourceElement>("#hero-theme-source");
|
||||
|
||||
if (heroThemeSource) {
|
||||
const syncHeroTheme = () => {
|
||||
const root = document.documentElement;
|
||||
|
||||
if (root.classList.contains("dark")) {
|
||||
heroThemeSource.media = "all";
|
||||
heroThemeSource.srcset = "/hero-dark.png";
|
||||
} else if (root.classList.contains("light")) {
|
||||
heroThemeSource.media = "all";
|
||||
heroThemeSource.srcset = "/hero-light.png";
|
||||
} else {
|
||||
heroThemeSource.media = "(prefers-color-scheme: dark)";
|
||||
heroThemeSource.srcset = "/hero-dark.png";
|
||||
}
|
||||
};
|
||||
|
||||
syncHeroTheme();
|
||||
new MutationObserver(syncHeroTheme).observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
}
|
||||
</script>
|
||||
116
src/globals.css
116
src/globals.css
|
|
@ -1,7 +1,5 @@
|
|||
@import url("https://cdn.jsdelivr.net/gh/wanteddev/wanted-sans@v1.0.3/packages/wanted-sans/fonts/webfonts/variable/split/WantedSansVariable.min.css");
|
||||
@import url('https://cdn.jsdelivr.net/npm/galmuri@latest/dist/galmuri.css');
|
||||
@import "@sun-typeface/suit/fonts/variable/woff2/SUIT-Variable.css";
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
|
|
@ -12,81 +10,73 @@
|
|||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-ring: var(--ring);
|
||||
--font-sans: "SUIT Variable", "SUIT", system-ui, sans-serif;
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'BonmyeongjoSourceHanSerif';
|
||||
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_two@1.0/NotoSerifKR.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.font-BonmyeongjoSourceHanSerif {
|
||||
font-family: "BonmyeongjoSourceHanSerif", sans-serif !important;
|
||||
}
|
||||
|
||||
.font-galmuri {
|
||||
font-family: "Galmuri", sans-serif !important;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: hsl(340 40% 98%);
|
||||
--foreground: hsl(315 21% 8%);
|
||||
--muted: hsl(340 20% 95%);
|
||||
--muted-foreground: hsl(340 10% 60%);
|
||||
--border: hsl(340 25% 90%);
|
||||
--ring: hsl(315 21% 8%);
|
||||
color-scheme: light;
|
||||
|
||||
--light-background: hsl(340 40% 98%);
|
||||
--light-foreground: hsl(315 21% 8%);
|
||||
--light-muted: hsl(340 20% 95%);
|
||||
--light-muted-foreground: hsl(340 10% 60%);
|
||||
--light-border: hsl(340 25% 90%);
|
||||
--light-ring: hsl(315 21% 8%);
|
||||
|
||||
--dark-background: hsl(315 21% 8%);
|
||||
--dark-foreground: hsl(0 0% 98%);
|
||||
--dark-muted: hsl(296 18% 15%);
|
||||
--dark-muted-foreground: hsl(240 5% 68%);
|
||||
--dark-border: hsl(296 18% 15%);
|
||||
--dark-ring: hsl(240 4.9% 83.9%);
|
||||
|
||||
--background: var(--light-background);
|
||||
--foreground: var(--light-foreground);
|
||||
--muted: var(--light-muted);
|
||||
--muted-foreground: var(--light-muted-foreground);
|
||||
--border: var(--light-border);
|
||||
--ring: var(--light-ring);
|
||||
--radius: 0.625rem;
|
||||
|
||||
--scrollbar: hsla(340 10% 60% / 0.5);
|
||||
--scrollbar-hover: hsla(340 10% 60% / 0.8);
|
||||
--mono-filter: none;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: hsl(315 21% 8%);
|
||||
--foreground: hsl(0 0% 98%);
|
||||
--muted: hsl(296, 18%, 15%);
|
||||
--muted-foreground: hsl(240 5% 68%);
|
||||
--border: hsl(296, 18%, 15%);
|
||||
--ring: hsl(240 4.9% 83.9%);
|
||||
|
||||
--scrollbar: hsla(340 10% 60% / 0.5);
|
||||
--scrollbar-hover: hsla(340 10% 60% / 0.8);
|
||||
color-scheme: dark;
|
||||
--background: var(--dark-background);
|
||||
--foreground: var(--dark-foreground);
|
||||
--muted: var(--dark-muted);
|
||||
--muted-foreground: var(--dark-muted-foreground);
|
||||
--border: var(--dark-border);
|
||||
--ring: var(--dark-ring);
|
||||
--mono-filter: invert(1);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
border-color: var(--border);
|
||||
outline-color: color-mix(in srgb, var(--ring) 50%, transparent);
|
||||
}
|
||||
.light {
|
||||
color-scheme: light;
|
||||
--background: var(--light-background);
|
||||
--foreground: var(--light-foreground);
|
||||
--muted: var(--light-muted);
|
||||
--muted-foreground: var(--light-muted-foreground);
|
||||
--border: var(--light-border);
|
||||
--ring: var(--light-ring);
|
||||
--mono-filter: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: "Wanted Sans Variable", "Wanted Sans", -apple-system,
|
||||
BlinkMacSystemFont, system-ui, "Segoe UI", "Apple SD Gothic Neo",
|
||||
"Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol", sans-serif;
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not(.light) {
|
||||
color-scheme: dark;
|
||||
--background: var(--dark-background);
|
||||
--foreground: var(--dark-foreground);
|
||||
--muted: var(--dark-muted);
|
||||
--muted-foreground: var(--dark-muted-foreground);
|
||||
--border: var(--dark-border);
|
||||
--ring: var(--dark-ring);
|
||||
--mono-filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fullscreen Scroll Snap */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
scroll-snap-type: y mandatory;
|
||||
}
|
||||
|
||||
.snap-section {
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
@ -1,70 +1,84 @@
|
|||
---
|
||||
import DDay from "@/components/DDay.astro";
|
||||
import Projects from "@/components/Projects.astro";
|
||||
import Timeline from "@/components/Timeline.astro";
|
||||
import Contact from "@/components/Contact.astro";
|
||||
import About from "@/components/About.astro";
|
||||
import Header from "@/components/Header.astro";
|
||||
import Hero from "@/components/Hero.astro";
|
||||
import "@/globals.css";
|
||||
import "@/scrollbar.css";
|
||||
---
|
||||
|
||||
<html lang="ko" class="font-sans">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>암냥 | imnya.ng</title>
|
||||
<meta name="description" content="imnyang's Portfolio Site" />
|
||||
</head>
|
||||
<body class="antialiased">
|
||||
<main
|
||||
class="min-h-screen w-screen overflow-y-scroll snap-y snap-mandatory relative"
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
content="#fcf8f9"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
content="#191014"
|
||||
/>
|
||||
<title>암냥 | imnya.ng</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="항상 꿈꾸던 것들을 현실로 만들어나가기 위해..."
|
||||
/>
|
||||
</head>
|
||||
<body
|
||||
class="m-0 min-w-[320px] overflow-hidden bg-background font-sans text-foreground transition-colors duration-300"
|
||||
>
|
||||
<div
|
||||
class="max-w-3xl w-full flex flex-row h-auto mx-auto my-8 items-center justify-between lg:px-0 px-8"
|
||||
>
|
||||
<div class="flex flex-row gap-4 items-center">
|
||||
<img
|
||||
src="/Frame.svg"
|
||||
alt="logo"
|
||||
class="w-fit h-fit"
|
||||
width="30"
|
||||
height="30"
|
||||
/>
|
||||
<a
|
||||
href="mailto:me@imnya.ng"
|
||||
class="inline-block align-middle leading-none font-BonmyeongjoSourceHanSerif text-3xl text-foreground/70 h-fit"
|
||||
>@imnya.ng</a
|
||||
>
|
||||
</div>
|
||||
<DDay targetDate="2010-11-08T03:00:00+09:00" label="🎂 Birth" />
|
||||
</div>
|
||||
<section
|
||||
id="about"
|
||||
class="w-full snap-start snap-always flex flex-col items-center justify-center mx-auto px-8 lg:px-0"
|
||||
>
|
||||
<div class="max-w-3xl w-full">
|
||||
<Contact />
|
||||
<br />
|
||||
<p>항상 꿈에만 있던 것들을 현실로 만들어보는 암냥입니다.</p>
|
||||
<p>언제나 새로운 것을 배우고, 도전하고 있습니다.</p>
|
||||
<p>
|
||||
현재는 정보보안 분야쪽에 관심이 많으며 원래는 소프트웨어 개발에 관심
|
||||
많았습니다.
|
||||
</p>
|
||||
<!-- <p>모두에게 밝은 면만 있는 것은 아닙니다.</p> -->
|
||||
<br />
|
||||
<p id="a" class="select-none text-transparent text-[1pt]"></p>
|
||||
<Projects />
|
||||
<br />
|
||||
<Timeline />
|
||||
<div class="text-muted-foreground text-sm">
|
||||
<br />
|
||||
<p>© 2026 imnyang. All rights reserved.</p>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
<Header />
|
||||
|
||||
<main
|
||||
class="mt-[64px] h-[calc(100svh-64px)] snap-y snap-mandatory overflow-x-hidden overflow-y-auto scroll-smooth overscroll-y-contain bg-background transition-colors duration-300 [&::-webkit-scrollbar]:h-[11px] [&::-webkit-scrollbar]:w-[19px] [&::-webkit-scrollbar-thumb]:rounded-2xl [&::-webkit-scrollbar-thumb]:border-[5px] [&::-webkit-scrollbar-thumb]:border-background [&::-webkit-scrollbar-thumb]:bg-[var(--scrollbar)] [&::-webkit-scrollbar-thumb:hover]:bg-[var(--scrollbar-hover)] [&::-webkit-scrollbar-track]:bg-transparent"
|
||||
>
|
||||
<Hero />
|
||||
<About />
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script>
|
||||
const scrollContainer = document.querySelector<HTMLElement>("main");
|
||||
const inPageLinks =
|
||||
document.querySelectorAll<HTMLAnchorElement>('a[href^="#"]');
|
||||
|
||||
const getTargetScrollTop = (target: HTMLElement) =>
|
||||
target.offsetTop - (scrollContainer?.offsetTop ?? 0);
|
||||
|
||||
if (scrollContainer && window.location.hash) {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const initialTarget = document.querySelector<HTMLElement>(
|
||||
window.location.hash,
|
||||
);
|
||||
|
||||
if (initialTarget) {
|
||||
scrollContainer.scrollTop =
|
||||
getTargetScrollTop(initialTarget);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
inPageLinks.forEach((link) => {
|
||||
link.addEventListener("click", (event) => {
|
||||
const hash = link.getAttribute("href");
|
||||
const target = hash
|
||||
? document.querySelector<HTMLElement>(hash)
|
||||
: null;
|
||||
|
||||
if (!scrollContainer || !target) return;
|
||||
|
||||
event.preventDefault();
|
||||
scrollContainer.scrollTo({
|
||||
top: getTargetScrollTop(target),
|
||||
behavior: "smooth",
|
||||
});
|
||||
history.pushState(null, "", hash);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
/* from reset */
|
||||
::-webkit-scrollbar-track {
|
||||
background: 0 0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar);
|
||||
border: 5px solid var(--background);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-hover);
|
||||
}
|
||||
|
||||
/* ::-webkit-scrollbar:not(.highlighttable, .highlight table, .gist .highlight) {
|
||||
background: var(--theme);
|
||||
}
|
||||
*/
|
||||
/* reset */
|
||||
::-webkit-scrollbar {
|
||||
width: 19px;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
/* from PaperMod https://github.com/adityatelange/hugo-PaperMod/blob/c98a924842fc7ee0c14212c316c69ede3ad76ca3/assets/css/includes/scroll-bar.css */
|
||||
Loading…
Reference in a new issue