restore portfolio
This commit is contained in:
parent
82c21e9479
commit
13a67bbf8d
28 changed files with 7747 additions and 19 deletions
73
src/components/Header.astro
Normal file
73
src/components/Header.astro
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<header class="border-b border-[var(--border)]">
|
||||
<div class="mx-auto flex max-w-7xl items-center justify-between px-6 py-5 lg:px-8">
|
||||
|
||||
<a
|
||||
href="/"
|
||||
class="font-mono text-sm font-bold tracking-tight"
|
||||
>
|
||||
SEMYON<span class="text-[var(--accent)]">.</span>DEV
|
||||
</a>
|
||||
|
||||
<nav class="hidden items-center gap-7 font-mono text-[11px] md:flex">
|
||||
|
||||
<a
|
||||
href="/projects"
|
||||
class="text-[var(--muted)] transition-colors hover:text-[var(--text)]"
|
||||
>
|
||||
WORK
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="#lab"
|
||||
class="text-[var(--muted)] transition-colors hover:text-[var(--text)]"
|
||||
>
|
||||
LAB
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/blog"
|
||||
class="text-[var(--muted)] transition-colors hover:text-[var(--text)]"
|
||||
>
|
||||
BLOG
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="#about"
|
||||
class="text-[var(--muted)] transition-colors hover:text-[var(--text)]"
|
||||
>
|
||||
ABOUT
|
||||
</a>
|
||||
|
||||
<span class="h-4 w-px bg-[var(--border)]"></span>
|
||||
|
||||
<!-- TODO: заменить # на реальную ссылку DonationAlerts -->
|
||||
<a
|
||||
href="https://www.donationalerts.com/r/alastor3301"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-[var(--accent)] transition-opacity hover:opacity-70"
|
||||
>
|
||||
SUPPORT ↗
|
||||
</a>
|
||||
|
||||
<!-- TODO: заменить # на GitHub -->
|
||||
<a
|
||||
href="https://github.com/Trytonottry"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-[var(--muted)] transition-colors hover:text-[var(--text)]"
|
||||
>
|
||||
GITHUB ↗
|
||||
</a>
|
||||
|
||||
</nav>
|
||||
|
||||
<button
|
||||
class="font-mono text-xs text-[var(--muted)] md:hidden"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
MENU
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
186
src/components/Hero.astro
Normal file
186
src/components/Hero.astro
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
<section class="relative overflow-hidden">
|
||||
<div
|
||||
class="mx-auto grid min-h-[calc(100vh-73px)] max-w-7xl gap-16 px-6 py-20 lg:grid-cols-[minmax(0,1fr)_380px] lg:px-8 lg:py-24"
|
||||
>
|
||||
|
||||
<div class="flex flex-col justify-center">
|
||||
|
||||
<div class="mb-8 flex items-center gap-3 font-mono text-[10px] uppercase tracking-[0.25em] text-[var(--muted)]">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-[var(--accent)] opacity-40"></span>
|
||||
<span class="relative inline-flex h-2 w-2 rounded-full bg-[var(--accent)]"></span>
|
||||
</span>
|
||||
|
||||
SYSTEM ONLINE
|
||||
</div>
|
||||
|
||||
<div class="mb-6 font-mono text-[10px] uppercase tracking-[0.25em] text-[var(--muted)]">
|
||||
SYSTEMS / AUTOMATION / INFRASTRUCTURE
|
||||
</div>
|
||||
|
||||
<h1
|
||||
class="max-w-5xl text-[4rem] font-bold leading-[0.88] tracking-[-0.065em] sm:text-[5.5rem] lg:text-[7.5rem]"
|
||||
>
|
||||
BUILD.
|
||||
<br />
|
||||
BREAK.
|
||||
<br />
|
||||
<span class="text-[var(--accent)]">UNDERSTAND.</span>
|
||||
<br />
|
||||
REPEAT.
|
||||
</h1>
|
||||
|
||||
<div class="mt-10 flex flex-col gap-8 sm:flex-row sm:items-end sm:justify-between">
|
||||
|
||||
<p class="max-w-xl text-base leading-7 text-[var(--muted)] sm:text-lg">
|
||||
Systems administrator and infrastructure engineer.
|
||||
I build infrastructure, automate systems and investigate
|
||||
things when they inevitably break.
|
||||
</p>
|
||||
|
||||
<div class="hidden font-mono text-right text-[10px] leading-5 text-[var(--muted)] sm:block">
|
||||
<div>ALASTOR.LOL</div>
|
||||
<div>SYS / 001</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-10 flex flex-wrap gap-3">
|
||||
|
||||
<a
|
||||
href="#projects"
|
||||
class="group border border-[var(--text)] px-5 py-3 font-mono text-[11px] uppercase tracking-wider transition-all duration-200 hover:bg-[var(--text)] hover:text-black"
|
||||
>
|
||||
<span>View projects</span>
|
||||
<span class="ml-3 transition-transform duration-200 group-hover:translate-x-1">
|
||||
→
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/blog"
|
||||
class="border border-[var(--border)] px-5 py-3 font-mono text-[11px] uppercase tracking-wider text-[var(--muted)] transition-all duration-200 hover:border-[var(--text)] hover:text-[var(--text)]"
|
||||
>
|
||||
Read blog →
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex items-end">
|
||||
|
||||
<div class="w-full border border-[var(--border)] bg-[var(--surface)] font-mono text-xs">
|
||||
|
||||
<div class="flex items-center justify-between border-b border-[var(--border)] px-4 py-3">
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-[var(--accent)]"></span>
|
||||
|
||||
<span class="text-[var(--muted)]">
|
||||
SYSTEM STATUS
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span class="text-[var(--accent)]">
|
||||
OPERATIONAL
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="p-5">
|
||||
|
||||
<div class="mb-6 text-[10px] uppercase tracking-[0.2em] text-[var(--muted)]">
|
||||
CAPABILITIES
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--muted)]">Linux</span>
|
||||
<span class="text-[var(--accent)]">●</span>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-[var(--border)]"></div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--muted)]">Infrastructure</span>
|
||||
<span class="text-[var(--accent)]">●</span>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-[var(--border)]"></div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--muted)]">Networking</span>
|
||||
<span class="text-[var(--accent)]">●</span>
|
||||
</div>
|
||||
|
||||
<div class="h-px bg-[var(--border)]"></div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[var(--muted)]">Automation</span>
|
||||
<span class="text-[var(--accent)]">●</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-[var(--border)] pt-6">
|
||||
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<span class="text-[var(--muted)]">HOMELAB</span>
|
||||
<span class="text-[var(--accent)]">87%</span>
|
||||
</div>
|
||||
|
||||
<div class="h-1 bg-[var(--border)]">
|
||||
<div class="h-full w-[87%] bg-[var(--accent)]"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid grid-cols-2 gap-4 border-t border-[var(--border)] pt-6">
|
||||
|
||||
<div>
|
||||
<div class="text-[10px] text-[var(--muted)]">HOSTS</div>
|
||||
<div class="mt-2 text-xl">06</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-[10px] text-[var(--muted)]">SERVICES</div>
|
||||
<div class="mt-2 text-xl">30+</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-[var(--border)] pt-5 text-[10px] text-[var(--muted)]">
|
||||
|
||||
<div class="flex justify-between">
|
||||
<span>UPTIME</span>
|
||||
<span class="text-[var(--text)]">99.9%</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-between">
|
||||
<span>STATUS</span>
|
||||
<span class="text-[var(--accent)]">BUILDING</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mx-auto hidden max-w-7xl px-6 pb-6 lg:block lg:px-8">
|
||||
|
||||
<div class="flex items-center justify-between font-mono text-[9px] uppercase tracking-[0.2em] text-[var(--muted)]">
|
||||
<span>SCROLL TO EXPLORE</span>
|
||||
<span>ALASTOR.LOL / 2026</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
90
src/components/LatestPosts.astro
Normal file
90
src/components/LatestPosts.astro
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
const posts = (await getCollection("blog"))
|
||||
.filter((post) => !post.data.draft)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
b.data.pubDate.valueOf() -
|
||||
a.data.pubDate.valueOf(),
|
||||
)
|
||||
.slice(0, 3);
|
||||
---
|
||||
|
||||
<section class="border-t border-[var(--border)]">
|
||||
|
||||
<div class="mx-auto max-w-7xl px-6 py-28 lg:px-8 lg:py-36">
|
||||
|
||||
<div class="flex flex-col justify-between gap-6 md:flex-row md:items-end">
|
||||
|
||||
<div>
|
||||
|
||||
<div class="font-mono text-[10px] tracking-widest text-[var(--muted)]">
|
||||
04 / NOTES
|
||||
</div>
|
||||
|
||||
<h2 class="mt-4 text-4xl font-bold tracking-tight sm:text-5xl">
|
||||
Latest notes
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="/blog"
|
||||
class="font-mono text-[10px] text-[var(--muted)] transition-colors hover:text-[var(--text)]"
|
||||
>
|
||||
VIEW ALL NOTES →
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-16 space-y-px border border-[var(--border)] bg-[var(--border)]">
|
||||
|
||||
{posts.map((post) => (
|
||||
<a
|
||||
href={`/blog/${post.id}/`}
|
||||
class="group block bg-[var(--surface)] p-6 transition-colors hover:bg-[var(--surface-hover)] lg:p-8"
|
||||
>
|
||||
|
||||
<div class="grid gap-5 md:grid-cols-[150px_1fr_auto] md:items-center">
|
||||
|
||||
<time
|
||||
datetime={post.data.pubDate.toISOString()}
|
||||
class="font-mono text-[10px] text-[var(--muted)]"
|
||||
>
|
||||
{post.data.pubDate.toLocaleDateString(
|
||||
"en-US",
|
||||
{
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
},
|
||||
)}
|
||||
</time>
|
||||
|
||||
<div>
|
||||
|
||||
<h3 class="text-lg font-semibold transition-colors group-hover:text-[var(--accent)]">
|
||||
{post.data.title}
|
||||
</h3>
|
||||
|
||||
<p class="mt-2 text-sm text-[var(--muted)]">
|
||||
{post.data.description}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="font-mono text-[10px] text-[var(--muted)]">
|
||||
READ →
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</a>
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
275
src/components/Now.astro
Normal file
275
src/components/Now.astro
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
<div class="border-y border-[var(--border)]">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
|
||||
<div class="flex items-center justify-between border-b border-[var(--border)] py-4">
|
||||
<div class="font-mono text-[10px] uppercase tracking-[0.25em] text-[var(--muted)]">
|
||||
01 / NOW
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="now-status"
|
||||
class="flex items-center gap-2 font-mono text-[10px] uppercase tracking-widest text-[var(--muted)]"
|
||||
>
|
||||
<span
|
||||
id="status-dot"
|
||||
class="h-1.5 w-1.5 rounded-full bg-[var(--muted)]"
|
||||
></span>
|
||||
|
||||
<span id="status-text">SYNCING</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid divide-y divide-[var(--border)] md:grid-cols-4 md:divide-x md:divide-y-0">
|
||||
|
||||
<div class="py-7 md:pr-8">
|
||||
<div class="font-mono text-[10px] uppercase tracking-widest text-[var(--muted)]">
|
||||
CURRENT STATE
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-sm leading-relaxed">
|
||||
Building things,<br />
|
||||
break fixing things.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-7 md:px-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-mono text-[10px] uppercase tracking-widest text-[var(--muted)]">
|
||||
♪ LISTENING
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="spotify-indicator"
|
||||
class="hidden font-mono text-[9px] uppercase tracking-widest text-[var(--accent)]"
|
||||
>
|
||||
LIVE
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
id="spotify-link"
|
||||
href="#"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="mt-3 block text-sm font-medium transition-colors hover:text-[var(--accent)]"
|
||||
>
|
||||
<span id="spotify-title">Loading...</span>
|
||||
</a>
|
||||
|
||||
<div
|
||||
id="spotify-artist"
|
||||
class="mt-1 text-xs text-[var(--muted)]"
|
||||
>
|
||||
Spotify
|
||||
</div>
|
||||
|
||||
<div class="mt-3 font-mono text-[9px] uppercase tracking-widest text-[var(--muted)]">
|
||||
SPOTIFY
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-7 md:px-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-mono text-[10px] uppercase tracking-widest text-[var(--muted)]">
|
||||
PLAYING
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="steam-indicator"
|
||||
class="hidden font-mono text-[9px] uppercase tracking-widest text-[var(--accent)]"
|
||||
>
|
||||
LIVE
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
id="steam-link"
|
||||
href="#"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="mt-3 block text-sm font-medium transition-colors hover:text-[var(--accent)]"
|
||||
>
|
||||
<span id="steam-game">Loading...</span>
|
||||
</a>
|
||||
|
||||
<div class="mt-1 text-xs text-[var(--muted)]">
|
||||
Steam
|
||||
</div>
|
||||
|
||||
<div class="mt-3 font-mono text-[9px] uppercase tracking-widest text-[var(--muted)]">
|
||||
STEAM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-7 md:pl-8">
|
||||
<div class="font-mono text-[10px] uppercase tracking-widest text-[var(--muted)]">
|
||||
BUILDING
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-sm font-medium">
|
||||
alastor.lol
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-xs text-[var(--muted)]">
|
||||
This website
|
||||
</div>
|
||||
|
||||
<div class="mt-3 font-mono text-[9px] uppercase tracking-widest text-[var(--muted)]">
|
||||
ASTRO · TYPESCRIPT
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const spotifyTitle =
|
||||
document.querySelector("#spotify-title");
|
||||
|
||||
const spotifyArtist =
|
||||
document.querySelector("#spotify-artist");
|
||||
|
||||
const spotifyLink =
|
||||
document.querySelector("#spotify-link") as HTMLAnchorElement | null;
|
||||
|
||||
const spotifyIndicator =
|
||||
document.querySelector("#spotify-indicator");
|
||||
|
||||
const steamGame =
|
||||
document.querySelector("#steam-game");
|
||||
|
||||
const steamLink =
|
||||
document.querySelector("#steam-link") as HTMLAnchorElement | null;
|
||||
|
||||
const steamIndicator =
|
||||
document.querySelector("#steam-indicator");
|
||||
|
||||
const statusText =
|
||||
document.querySelector("#status-text");
|
||||
|
||||
const statusDot =
|
||||
document.querySelector("#status-dot");
|
||||
|
||||
function updateOverallStatus() {
|
||||
const spotifyLive =
|
||||
spotifyIndicator?.classList.contains("inline");
|
||||
|
||||
const steamLive =
|
||||
steamIndicator?.classList.contains("inline");
|
||||
|
||||
if (spotifyLive || steamLive) {
|
||||
statusText!.textContent = "LIVE";
|
||||
statusDot!.classList.remove("bg-[var(--muted)]");
|
||||
statusDot!.classList.add(
|
||||
"bg-[var(--accent)]",
|
||||
"animate-pulse",
|
||||
);
|
||||
} else {
|
||||
statusText!.textContent = "IDLE";
|
||||
statusDot!.classList.remove(
|
||||
"bg-[var(--accent)]",
|
||||
"animate-pulse",
|
||||
);
|
||||
statusDot!.classList.add("bg-[var(--muted)]");
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSpotify() {
|
||||
try {
|
||||
const response = await fetch("/api/spotify", {
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Spotify request failed");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.playing) {
|
||||
spotifyTitle!.textContent = "Nothing right now";
|
||||
spotifyArtist!.textContent = "Spotify";
|
||||
spotifyLink?.removeAttribute("href");
|
||||
|
||||
spotifyIndicator?.classList.remove("inline");
|
||||
spotifyIndicator?.classList.add("hidden");
|
||||
|
||||
updateOverallStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
spotifyTitle!.textContent = data.title;
|
||||
spotifyArtist!.textContent = data.artist;
|
||||
|
||||
if (spotifyLink && data.url) {
|
||||
spotifyLink.href = data.url;
|
||||
}
|
||||
|
||||
spotifyIndicator?.classList.remove("hidden");
|
||||
spotifyIndicator?.classList.add("inline");
|
||||
|
||||
updateOverallStatus();
|
||||
} catch {
|
||||
spotifyTitle!.textContent = "Unavailable";
|
||||
spotifyArtist!.textContent = "Spotify";
|
||||
spotifyLink?.removeAttribute("href");
|
||||
|
||||
spotifyIndicator?.classList.remove("inline");
|
||||
spotifyIndicator?.classList.add("hidden");
|
||||
|
||||
updateOverallStatus();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSteam() {
|
||||
try {
|
||||
const response = await fetch("/api/steam", {
|
||||
cache: "no-store",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Steam request failed");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.playing) {
|
||||
steamGame!.textContent = "Not playing";
|
||||
steamLink?.removeAttribute("href");
|
||||
|
||||
steamIndicator?.classList.remove("inline");
|
||||
steamIndicator?.classList.add("hidden");
|
||||
|
||||
updateOverallStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
steamGame!.textContent = data.game;
|
||||
|
||||
if (steamLink && data.url) {
|
||||
steamLink.href = data.url;
|
||||
}
|
||||
|
||||
steamIndicator?.classList.remove("hidden");
|
||||
steamIndicator?.classList.add("inline");
|
||||
|
||||
updateOverallStatus();
|
||||
} catch {
|
||||
steamGame!.textContent = "Unavailable";
|
||||
steamLink?.removeAttribute("href");
|
||||
|
||||
steamIndicator?.classList.remove("inline");
|
||||
steamIndicator?.classList.add("hidden");
|
||||
|
||||
updateOverallStatus();
|
||||
}
|
||||
}
|
||||
|
||||
updateSpotify();
|
||||
updateSteam();
|
||||
|
||||
setInterval(updateSpotify, 30_000);
|
||||
setInterval(updateSteam, 60_000);
|
||||
</script>
|
||||
43
src/components/NowPlaying.astro
Normal file
43
src/components/NowPlaying.astro
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<section class="border-y border-[var(--border)]">
|
||||
<div class="mx-auto max-w-7xl px-6 py-8 lg:px-8">
|
||||
<div class="grid gap-8 md:grid-cols-3">
|
||||
<div>
|
||||
<div class="mb-2 font-mono text-[10px] uppercase tracking-widest text-[var(--muted)]">
|
||||
Now
|
||||
</div>
|
||||
|
||||
<div class="font-mono text-sm">
|
||||
Current state of the machine.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-2 font-mono text-[10px] uppercase tracking-widest text-[var(--muted)]">
|
||||
♪ Listening
|
||||
</div>
|
||||
|
||||
<div class="text-sm font-medium">
|
||||
Venjent — U R Gonna Move
|
||||
</div>
|
||||
|
||||
<div class="mt-1 font-mono text-xs text-[var(--muted)]">
|
||||
Spotify
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mb-2 font-mono text-[10px] uppercase tracking-widest text-[var(--muted)]">
|
||||
🎮 Playing
|
||||
</div>
|
||||
|
||||
<div class="text-sm font-medium">
|
||||
Factorio
|
||||
</div>
|
||||
|
||||
<div class="mt-1 font-mono text-xs text-[var(--muted)]">
|
||||
Steam
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
62
src/components/ProjectCard.astro
Normal file
62
src/components/ProjectCard.astro
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
---
|
||||
interface Props {
|
||||
number: string;
|
||||
title: string;
|
||||
description: string;
|
||||
stack: string[];
|
||||
href: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const {
|
||||
number,
|
||||
title,
|
||||
description,
|
||||
stack,
|
||||
href,
|
||||
status,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<a
|
||||
href={href}
|
||||
class="group block bg-[var(--surface)] transition-colors hover:bg-[var(--surface-hover)]"
|
||||
>
|
||||
<div class="flex items-center justify-between border-b border-[var(--border)] px-5 py-4 font-mono text-[10px]">
|
||||
<span class="text-[var(--muted)]">
|
||||
{number}
|
||||
</span>
|
||||
|
||||
<span class="text-[var(--accent)]">
|
||||
● {status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="p-6 lg:p-7">
|
||||
|
||||
<h3 class="text-2xl font-semibold tracking-tight transition-colors group-hover:text-[var(--accent)]">
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
<p class="mt-4 max-w-lg text-sm leading-relaxed text-[var(--muted)]">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
<div class="mt-8 flex flex-wrap gap-2">
|
||||
|
||||
{stack.map((item) => (
|
||||
<span
|
||||
class="border border-[var(--border)] px-2 py-1 font-mono text-[9px] text-[var(--muted)]"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-8 font-mono text-[10px] text-[var(--muted)] transition-colors group-hover:text-[var(--text)]">
|
||||
EXPLORE PROJECT →
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
163
src/components/Projects.astro
Normal file
163
src/components/Projects.astro
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
const projects = await getCollection("projects", ({ data }) => {
|
||||
return data.draft !== true;
|
||||
});
|
||||
---
|
||||
|
||||
<section
|
||||
id="projects"
|
||||
class="border-t border-[var(--border)]"
|
||||
>
|
||||
<div class="mx-auto max-w-7xl px-6 py-28 lg:px-8">
|
||||
|
||||
<!-- Header -->
|
||||
|
||||
<div class="flex flex-col gap-8 md:flex-row md:items-end md:justify-between">
|
||||
|
||||
<div>
|
||||
<div class="font-mono text-[10px] tracking-[0.25em] text-[var(--muted)]">
|
||||
02 / PROJECTS
|
||||
</div>
|
||||
|
||||
<h2 class="mt-6 text-4xl font-bold tracking-tight sm:text-6xl">
|
||||
Things I build.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="max-w-md text-sm leading-7 text-[var(--muted)]">
|
||||
Infrastructure, automation, networking and experiments.
|
||||
Some are production systems. Some are glorified
|
||||
learning exercises.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Projects -->
|
||||
|
||||
<div class="mt-16 border-t border-[var(--border)]">
|
||||
|
||||
{
|
||||
projects.map((project, index) => (
|
||||
<a
|
||||
href={`/projects/${project.id}`}
|
||||
class="group block border-b border-[var(--border)] transition-colors duration-200 hover:bg-[var(--surface)]"
|
||||
>
|
||||
|
||||
<div class="grid gap-6 px-0 py-8 md:grid-cols-[64px_minmax(0,1fr)_220px_110px] md:items-center">
|
||||
|
||||
<!-- Number -->
|
||||
|
||||
<div class="font-mono text-[10px] text-[var(--muted)]">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Project -->
|
||||
|
||||
<div class="min-w-0">
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
<h3 class="truncate text-2xl font-semibold tracking-tight transition-colors duration-200 group-hover:text-[var(--accent)]">
|
||||
{project.data.title}
|
||||
</h3>
|
||||
|
||||
<span class="font-mono text-xs text-[var(--muted)] transition-transform duration-200 group-hover:translate-x-1">
|
||||
→
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<p class="mt-3 max-w-2xl text-sm leading-6 text-[var(--muted)]">
|
||||
{project.data.description}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Stack + status -->
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
|
||||
{
|
||||
project.data.stack.map((item) => (
|
||||
<span class="border border-[var(--border)] px-2 py-1 font-mono text-[9px] uppercase tracking-wider text-[var(--muted)]">
|
||||
{item}
|
||||
</span>
|
||||
))
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 font-mono text-[9px] uppercase tracking-widest">
|
||||
|
||||
<span
|
||||
class:list={[
|
||||
"h-1.5 w-1.5 rounded-full",
|
||||
project.data.status === "RUNNING"
|
||||
? "bg-[var(--accent)]"
|
||||
: "bg-[var(--muted)]",
|
||||
]}
|
||||
></span>
|
||||
|
||||
<span
|
||||
class:list={[
|
||||
project.data.status === "RUNNING"
|
||||
? "text-[var(--accent)]"
|
||||
: "text-[var(--muted)]",
|
||||
]}
|
||||
>
|
||||
{project.data.status}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- View -->
|
||||
|
||||
<div class="hidden text-right font-mono text-[10px] uppercase tracking-widest text-[var(--muted)] md:block">
|
||||
|
||||
<span class="transition-colors group-hover:text-[var(--text)]">
|
||||
VIEW →
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</a>
|
||||
))
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
<div class="mt-8 flex flex-col gap-4 font-mono text-[10px] uppercase tracking-widest text-[var(--muted)] sm:flex-row sm:items-center sm:justify-between">
|
||||
|
||||
<span>
|
||||
{projects.length} PROJECTS
|
||||
</span>
|
||||
|
||||
<a
|
||||
href="/projects"
|
||||
class="transition-colors hover:text-[var(--accent)]"
|
||||
>
|
||||
VIEW ALL PROJECTS →
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue