restore portfolio

This commit is contained in:
Semyon 2026-08-10 15:09:14 +03:00
parent 82c21e9479
commit 13a67bbf8d
28 changed files with 7747 additions and 19 deletions

View 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
View 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>

View 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
View 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>

View 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>

View 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>

View 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>