Rebuild portfolio in Next.js and Tailwind
Static-export Next.js 16 app with the tactical-map hero, Dajt terrain background, dossier pages and the projects node index. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
+38
-16
@@ -1,24 +1,46 @@
|
|||||||
# Logs
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
logs
|
|
||||||
*.log
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
# env files (can opt-in for committing if needed)
|
||||||
dist
|
.env*
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# editors
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<!-- BEGIN:nextjs-agent-rules -->
|
||||||
|
|
||||||
|
# This is NOT the Next.js you know
|
||||||
|
|
||||||
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||||
|
|
||||||
|
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||||
|
|
||||||
|
<!-- END:nextjs-agent-rules -->
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# selimaj.dev
|
||||||
|
|
||||||
|
Portfolio site. Next.js (App Router, static export) and Tailwind CSS v4.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev # http://localhost:3000
|
||||||
|
npm run build # static site in out/
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
`npm run build` writes plain HTML/CSS/JS to `out/`, which any static host can serve.
|
||||||
|
|
||||||
|
## Where things live
|
||||||
|
|
||||||
|
- `src/content/`: all copy and data, including every placeholder (marked `PLACEHOLDER`).
|
||||||
|
- `profile.ts`: name, title, stance, dossier fields, links, map lock.
|
||||||
|
- `dossier.ts`: the five About files.
|
||||||
|
- `projects.ts`: projects, statuses, node-map positions, featured nodes.
|
||||||
|
- `public/profile/pfp.svg`: placeholder profile picture. Replace this file (or point `profile.pfp` at a new one).
|
||||||
|
- `public/terrain.svg`: hero terrain contours (Dajt elevation data).
|
||||||
|
- `src/app/globals.css`: colour tokens, bracket/grid utilities, and all motion.
|
||||||
|
|
||||||
|
## Rules worth keeping
|
||||||
|
|
||||||
|
- Green (`accent`) means active or detected: hover, focus, current page, live telemetry, the lock. Nothing at rest is green.
|
||||||
|
- The map shows Tirana city centre only. Never add anything finer than city scale.
|
||||||
|
- All motion sits behind `prefers-reduced-motion: no-preference`. Text renders without JS.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTs from "eslint-config-next/typescript";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
...nextTs,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
// Static-first: `next build` emits plain HTML/CSS/JS into `out/`.
|
||||||
|
output: "export",
|
||||||
|
trailingSlash: true,
|
||||||
|
images: { unoptimized: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
Generated
+6892
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "selimaj-dev",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "16.3.8",
|
||||||
|
"react": "19.2.8",
|
||||||
|
"react-dom": "19.2.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.3.8",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 200">
|
||||||
|
<!-- PLACEHOLDER profile picture: replace this file with the real one. -->
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0" stop-color="#3a4247"/>
|
||||||
|
<stop offset="1" stop-color="#101315"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path fill="url(#g)" d="M84,28 C108,28 122,46 122,72 C122,96 112,114 100,122 L102,136 C130,142 156,156 164,182 L166,200 L2,200 L4,182 C12,156 38,142 66,136 L68,122 C56,114 46,96 46,72 C46,46 60,28 84,28Z"/>
|
||||||
|
<path fill="none" stroke="#f2f4f5" stroke-opacity=".3" d="M84,28 C108,28 122,46 122,72 C122,96 112,114 100,122 L102,136 C130,142 156,156 164,182 L166,200 L2,200 L4,182 C12,156 38,142 66,136 L68,122 C56,114 46,96 46,72 C46,46 60,28 84,28Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 788 B |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 304 KiB |
@@ -0,0 +1,131 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { StatusTag } from "@/components/StatusTag";
|
||||||
|
import { dossier, fileBySlug, type Proof } from "@/content/dossier";
|
||||||
|
|
||||||
|
export const dynamicParams = false;
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return dossier.map((f) => ({ slug: f.slug }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: PageProps<"/about/[slug]">): Promise<Metadata> {
|
||||||
|
const { slug } = await params;
|
||||||
|
const f = fileBySlug(slug);
|
||||||
|
return f ? { title: f.title, description: f.summary } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProofRow({ p }: { p: Proof }) {
|
||||||
|
const inner = (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
<span className="font-display text-[17px] font-semibold tracking-[0.06em] uppercase">{p.label}</span>
|
||||||
|
<span className="mt-0.5 block text-[11px] text-mute">{p.note}</span>
|
||||||
|
</span>
|
||||||
|
{p.status ? <StatusTag status={p.status} /> : <span />}
|
||||||
|
<span aria-hidden="true" className="text-mute group-hover:text-accent group-focus-visible:text-accent">
|
||||||
|
{p.href ? (p.href.startsWith("http") ? "↗" : "→") : ""}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
const cls = "group bracket grid min-h-14 grid-cols-[minmax(0,1fr)_auto_16px] items-center gap-5 px-3 [--bc:transparent]";
|
||||||
|
if (!p.href) return <div className={cls}>{inner}</div>;
|
||||||
|
const interactive = `${cls} hover:[--bc:var(--color-accent)] focus-visible:outline-none focus-visible:[--bc:var(--color-accent)]`;
|
||||||
|
return p.href.startsWith("http") ? (
|
||||||
|
<a href={p.href} className={interactive}>
|
||||||
|
{inner}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<Link href={p.href} className={interactive}>
|
||||||
|
{inner}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function DossierFilePage({ params }: PageProps<"/about/[slug]">) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const f = fileBySlug(slug);
|
||||||
|
if (!f) notFound();
|
||||||
|
const i = dossier.indexOf(f);
|
||||||
|
const prev = dossier[i - 1];
|
||||||
|
const next = dossier[i + 1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="pt-[68px] md:pt-[85px]">
|
||||||
|
<div className="mx-auto flex max-w-[1240px] flex-col gap-8 px-4 pt-8 pb-20 md:px-16 md:pt-[72px] md:pb-24 lg:flex-row lg:items-start lg:gap-24">
|
||||||
|
<aside aria-label="Dossier files" className="lg:sticky lg:top-8 lg:w-[220px] lg:flex-none">
|
||||||
|
<Link href="/about/" className="inline-flex min-h-11 items-center gap-2 text-[10px] tracking-[0.22em] text-mute hover:text-accent">
|
||||||
|
<span aria-hidden="true">←</span> DOSSIER INDEX
|
||||||
|
</Link>
|
||||||
|
<ol className="mt-4 flex flex-wrap gap-x-4 text-[11px] tracking-[0.16em] lg:block">
|
||||||
|
{dossier.map((d) => {
|
||||||
|
const current = d.slug === f.slug;
|
||||||
|
return (
|
||||||
|
<li key={d.slug}>
|
||||||
|
<Link
|
||||||
|
href={`/about/${d.slug}/`}
|
||||||
|
aria-current={current ? "page" : undefined}
|
||||||
|
className={`flex min-h-10 items-center gap-3 uppercase ${current ? "text-ink" : "text-mute hover:text-ink"}`}
|
||||||
|
>
|
||||||
|
<span className={`w-6 ${current ? "text-accent" : ""}`}>{d.no}</span>
|
||||||
|
{d.title}
|
||||||
|
{current && <span aria-hidden="true" className="size-1.5 bg-accent" />}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<article className="min-w-0 max-w-[720px] flex-1">
|
||||||
|
<div className="flex flex-wrap gap-5 text-[10px] tracking-[0.24em] text-mute">
|
||||||
|
<span>
|
||||||
|
FILE {f.no} / {String(dossier.length).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
<span>CLASS · OPEN</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="mt-3.5 font-display text-[52px] leading-[0.95] font-semibold uppercase md:text-[80px]">{f.title}</h1>
|
||||||
|
<div aria-hidden="true" className="mt-8 h-px bg-white/10" />
|
||||||
|
|
||||||
|
<p className="mt-10 text-xl leading-[1.55] md:text-[22px]">{f.lead}</p>
|
||||||
|
{f.paragraphs.map((t) => (
|
||||||
|
<p key={t} className="mt-5 text-[15px] leading-[1.8] text-ink-2">
|
||||||
|
{t}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<section aria-labelledby="proof-title" className="bracket mt-14 border border-white/6 bg-surface p-6 [--bc:rgb(242_244_245/0.45)]">
|
||||||
|
<h2 id="proof-title" className="text-[10px] font-normal tracking-[0.3em] text-mute">
|
||||||
|
PROOF
|
||||||
|
</h2>
|
||||||
|
<ul className="mt-4 flex flex-col gap-1">
|
||||||
|
{f.proof.map((p) => (
|
||||||
|
<li key={p.label}>
|
||||||
|
<ProofRow p={p} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav aria-label="Adjacent files" className="mt-16 flex flex-col justify-between gap-4 border-t border-white/8 pt-6 sm:flex-row">
|
||||||
|
{prev ? (
|
||||||
|
<Link href={`/about/${prev.slug}/`} className="group flex min-h-11 flex-col gap-1.5">
|
||||||
|
<span className="text-[9px] tracking-[0.24em] text-mute">← FILE {prev.no}</span>
|
||||||
|
<span className="font-display text-lg font-semibold tracking-[0.06em] uppercase group-hover:text-accent">{prev.title}</span>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{next && (
|
||||||
|
<Link href={`/about/${next.slug}/`} className="group flex min-h-11 flex-col gap-1.5 sm:text-right">
|
||||||
|
<span className="text-[9px] tracking-[0.24em] text-mute">FILE {next.no} →</span>
|
||||||
|
<span className="font-display text-lg font-semibold tracking-[0.06em] uppercase group-hover:text-accent">{next.title}</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { dossier } from "@/content/dossier";
|
||||||
|
import { orus, profile } from "@/content/profile";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Dossier",
|
||||||
|
description: "Five short files: leadership, entrepreneurship, building, discipline, passions.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const cols = "md:grid-cols-[96px_240px_minmax(0,1fr)_200px_32px] xl:grid-cols-[96px_260px_minmax(0,1fr)_240px_32px]";
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
return (
|
||||||
|
<main className="grid-bg min-h-full pt-[68px] md:pt-[85px]">
|
||||||
|
<div className="mx-auto max-w-[1440px] px-4 pt-10 pb-20 md:px-16 md:pt-[72px] md:pb-24">
|
||||||
|
<div className="flex flex-col justify-between gap-10 md:flex-row md:items-end md:gap-16">
|
||||||
|
<div>
|
||||||
|
<div className="label tracking-[0.3em]">Case file · Subject 0001</div>
|
||||||
|
<h1 className="mt-3 font-display text-[52px] leading-[0.95] font-semibold uppercase md:text-7xl">Dossier</h1>
|
||||||
|
<p className="mt-5 max-w-[520px] text-sm leading-[1.7] text-ink-2">
|
||||||
|
Five files. Each one is short: two or three paragraphs and one piece of proof.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<dl className="bracket grid min-w-[280px] grid-cols-[110px_minmax(0,1fr)] gap-y-2 border border-white/6 bg-surface px-[22px] py-[18px] text-[11px] [--bc:rgb(242_244_245/0.4)]">
|
||||||
|
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">SUBJECT</dt>
|
||||||
|
<dd>{profile.displayName}</dd>
|
||||||
|
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">ROOT</dt>
|
||||||
|
<dd>{orus.name}</dd>
|
||||||
|
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">FILES</dt>
|
||||||
|
<dd>{String(dossier.length).padStart(2, "0")}</dd>
|
||||||
|
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">ACCESS</dt>
|
||||||
|
<dd>Open</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div aria-hidden="true" className={`mt-16 hidden gap-6 border-b border-white/10 px-5 pb-3 text-[9px] tracking-[0.24em] text-mute md:grid ${cols}`}>
|
||||||
|
<span>ID</span>
|
||||||
|
<span>FILE</span>
|
||||||
|
<span>SUMMARY</span>
|
||||||
|
<span>PROOF</span>
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
<ol className="mt-8 border-t border-white/7 md:mt-0 md:border-t-0">
|
||||||
|
{dossier.map((f) => (
|
||||||
|
<li key={f.slug} className="border-b border-white/7">
|
||||||
|
<Link
|
||||||
|
href={`/about/${f.slug}/`}
|
||||||
|
className={`group bracket grid min-h-[88px] grid-cols-[72px_minmax(0,1fr)_24px] items-center gap-6 px-3 transition-colors [--bc:transparent] hover:bg-white/2 hover:[--bc:var(--color-accent)] focus-visible:outline-none focus-visible:[--bc:var(--color-accent)] md:px-5 ${cols}`}
|
||||||
|
>
|
||||||
|
<span className="text-[11px] tracking-[0.2em] text-mute group-hover:text-accent group-focus-visible:text-accent">FILE {f.no}</span>
|
||||||
|
<span className="font-display text-lg font-semibold tracking-[0.06em] uppercase md:text-[22px]">{f.title}</span>
|
||||||
|
<span className="hidden text-xs leading-[1.6] text-ink-2 md:block">{f.summary}</span>
|
||||||
|
<span className="hidden text-[10px] tracking-[0.18em] text-mute uppercase md:block">{f.proofSummary}</span>
|
||||||
|
<span aria-hidden="true" className="text-mute transition-transform group-hover:translate-x-1 group-hover:text-accent">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
<p aria-hidden="true" className="mt-5 text-[9px] tracking-[0.22em] text-mute/70">
|
||||||
|
{String(dossier.length).padStart(2, "0")} FILES · END OF INDEX
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* `js:` applies only once the head script has marked <html class="js">. */
|
||||||
|
@custom-variant js (&:where(.js *));
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-night: #050607;
|
||||||
|
--color-surface: #0b0d0e;
|
||||||
|
--color-surface-2: #121517;
|
||||||
|
--color-ink: #f2f4f5;
|
||||||
|
--color-ink-2: #c4cacd;
|
||||||
|
--color-mute: #8b9296;
|
||||||
|
/* Green means active or detected. Never decoration. */
|
||||||
|
--color-accent: #4dff95;
|
||||||
|
--color-line: rgb(255 255 255 / 0.07);
|
||||||
|
--color-line-2: rgb(255 255 255 / 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--font-display: var(--font-chakra), ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-mono: var(--font-jetbrains), ui-monospace, SFMono-Regular, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html {
|
||||||
|
background: var(--color-night);
|
||||||
|
color: var(--color-ink);
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
:focus-visible {
|
||||||
|
outline: 1px solid var(--color-accent);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
::selection {
|
||||||
|
background: rgb(77 255 149 / 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corner brackets. Set --bc for colour, --bl for arm length. Uses only the
|
||||||
|
background-image longhands so it composes with bg-* colour utilities. */
|
||||||
|
@utility bracket {
|
||||||
|
--bc: rgb(242 244 245 / 0.28);
|
||||||
|
--bl: 10px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(var(--bc), var(--bc)), linear-gradient(var(--bc), var(--bc)),
|
||||||
|
linear-gradient(var(--bc), var(--bc)), linear-gradient(var(--bc), var(--bc)),
|
||||||
|
linear-gradient(var(--bc), var(--bc)), linear-gradient(var(--bc), var(--bc)),
|
||||||
|
linear-gradient(var(--bc), var(--bc)), linear-gradient(var(--bc), var(--bc));
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 0 0, 0 0, 100% 0, 100% 0, 0 100%, 0 100%, 100% 100%, 100% 100%;
|
||||||
|
background-size:
|
||||||
|
var(--bl) 1px, 1px var(--bl), var(--bl) 1px, 1px var(--bl),
|
||||||
|
var(--bl) 1px, 1px var(--bl), var(--bl) 1px, 1px var(--bl);
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility grid-bg {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgb(255 255 255 / 0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgb(255 255 255 / 0.03) 1px, transparent 1px);
|
||||||
|
background-size: 48px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility scan-bg {
|
||||||
|
background-image: repeating-linear-gradient(0deg, rgb(255 255 255 / 0.016) 0 1px, transparent 1px 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility label {
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.24em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Boot overlay (only ever shown when the head script sets data-boot) ---------- */
|
||||||
|
.boot {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
html[data-boot] .boot {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
html[data-boot] body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
/* Hold the hero sequence until boot finishes; removing data-boot restarts it. */
|
||||||
|
html[data-boot] .seq {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Motion (all of it opt-in; reduced motion gets the final state) ---------- */
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.seq {
|
||||||
|
animation-delay: var(--d, 0s);
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drift {
|
||||||
|
animation: drift 80s ease-in-out infinite alternate;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
.spin {
|
||||||
|
animation: spin 16s linear infinite;
|
||||||
|
transform-origin: 60px 60px;
|
||||||
|
}
|
||||||
|
.blink {
|
||||||
|
animation: blink 1.1s steps(2) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text resolves from outline to solid with a short glitch. */
|
||||||
|
.resolve {
|
||||||
|
animation-name: resolve;
|
||||||
|
animation-duration: 0.55s;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
}
|
||||||
|
/* Panels build as a dashed wireframe, then fill. */
|
||||||
|
.panel {
|
||||||
|
animation-name: panel;
|
||||||
|
animation-duration: 0.6s;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
}
|
||||||
|
/* Map strokes draw in (paths need pathLength="1"). */
|
||||||
|
.draw path,
|
||||||
|
.draw circle,
|
||||||
|
.draw rect {
|
||||||
|
stroke-dasharray: 1;
|
||||||
|
animation: draw 0.9s ease-out both;
|
||||||
|
animation-delay: var(--d, 0s);
|
||||||
|
}
|
||||||
|
html[data-boot] .draw path,
|
||||||
|
html[data-boot] .draw circle,
|
||||||
|
html[data-boot] .draw rect {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
.reticle {
|
||||||
|
animation-name: reticle;
|
||||||
|
animation-duration: 0.7s;
|
||||||
|
animation-timing-function: cubic-bezier(0.6, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
.trace {
|
||||||
|
animation-name: trace;
|
||||||
|
animation-duration: 0.3s;
|
||||||
|
animation-timing-function: ease-out;
|
||||||
|
transform-origin: 0 50%;
|
||||||
|
}
|
||||||
|
.appear {
|
||||||
|
animation-name: appear;
|
||||||
|
animation-duration: 0.01s;
|
||||||
|
}
|
||||||
|
.bust-solid {
|
||||||
|
animation-name: bust-solid;
|
||||||
|
animation-duration: 0.8s;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
}
|
||||||
|
.bust-scan {
|
||||||
|
animation-name: bust-scan;
|
||||||
|
animation-duration: 0.8s;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
}
|
||||||
|
.page-resolve {
|
||||||
|
animation: page-resolve 0.45s linear both;
|
||||||
|
}
|
||||||
|
.type-line {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
animation: type 0.3s steps(24) both;
|
||||||
|
animation-delay: var(--d, 0s);
|
||||||
|
}
|
||||||
|
.boot-out {
|
||||||
|
animation: boot-out 0.25s linear both;
|
||||||
|
}
|
||||||
|
.seg-on {
|
||||||
|
animation: seg-grow 0.45s ease-out both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.page-resolve {
|
||||||
|
animation: fade 0.2s ease-out both;
|
||||||
|
}
|
||||||
|
.bust-scan {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes drift {
|
||||||
|
to {
|
||||||
|
transform: translate3d(-28px, 14px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes blink {
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes fade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes appear {
|
||||||
|
from {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes resolve {
|
||||||
|
0% {
|
||||||
|
color: transparent;
|
||||||
|
-webkit-text-stroke: 1px rgb(242 244 245 / 0.45);
|
||||||
|
clip-path: inset(0 100% 0 0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
color: transparent;
|
||||||
|
-webkit-text-stroke: 1px rgb(242 244 245 / 0.45);
|
||||||
|
clip-path: inset(0 0 0 0);
|
||||||
|
}
|
||||||
|
55% {
|
||||||
|
transform: translateX(2px);
|
||||||
|
text-shadow: -2px 0 rgb(255 70 90 / 0.45), 2px 0 rgb(70 150 255 / 0.45);
|
||||||
|
}
|
||||||
|
62% {
|
||||||
|
transform: translateX(-1px);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: none;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes panel {
|
||||||
|
0% {
|
||||||
|
clip-path: inset(0 0 100% 0);
|
||||||
|
background-color: transparent;
|
||||||
|
outline: 1px dashed rgb(242 244 245 / 0.35);
|
||||||
|
}
|
||||||
|
45% {
|
||||||
|
clip-path: inset(0 0 0 0);
|
||||||
|
background-color: transparent;
|
||||||
|
outline: 1px dashed rgb(242 244 245 / 0.35);
|
||||||
|
}
|
||||||
|
58% {
|
||||||
|
transform: translateX(2px);
|
||||||
|
filter: drop-shadow(-1px 0 rgb(255 70 90 / 0.5)) drop-shadow(1px 0 rgb(70 150 255 / 0.5));
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: none;
|
||||||
|
filter: none;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes draw {
|
||||||
|
from {
|
||||||
|
stroke-dashoffset: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes reticle {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-90px, 50px) scale(1.4);
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
45% {
|
||||||
|
transform: translate(40px, -24px) scale(1.4);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: translate(0, 0) scale(1.4);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes trace {
|
||||||
|
from {
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes bust-solid {
|
||||||
|
from {
|
||||||
|
clip-path: inset(0 100% 0 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
clip-path: inset(0 0 0 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes bust-scan {
|
||||||
|
0% {
|
||||||
|
left: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
95% {
|
||||||
|
left: 100%;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes page-resolve {
|
||||||
|
0% {
|
||||||
|
clip-path: inset(0 0 100% 0);
|
||||||
|
filter: drop-shadow(-2px 0 rgb(255 70 90 / 0.5)) drop-shadow(2px 0 rgb(70 150 255 / 0.5));
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
clip-path: inset(0 0 0 0);
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateX(-1px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: none;
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes type {
|
||||||
|
from {
|
||||||
|
max-width: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
max-width: 760px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes boot-out {
|
||||||
|
40% {
|
||||||
|
transform: translateX(3px);
|
||||||
|
filter: drop-shadow(-2px 0 rgb(255 70 90 / 0.6)) drop-shadow(2px 0 rgb(70 150 255 / 0.6));
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
clip-path: inset(50% 0 50% 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes seg-grow {
|
||||||
|
from {
|
||||||
|
clip-path: inset(0 100% 0 0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
clip-path: inset(0 0 0 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" fill="#050607"/>
|
||||||
|
<circle cx="12" cy="12" r="8.5" stroke="#f2f4f5" stroke-width="1.6" stroke-dasharray="44 9"/>
|
||||||
|
<circle cx="12" cy="12" r="2.6" fill="#4dff95"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,41 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Chakra_Petch, JetBrains_Mono } from "next/font/google";
|
||||||
|
import { SiteHeader } from "@/components/SiteHeader";
|
||||||
|
import { SiteFooter } from "@/components/SiteFooter";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const chakra = Chakra_Petch({
|
||||||
|
variable: "--font-chakra",
|
||||||
|
subsets: ["latin"],
|
||||||
|
weight: ["400", "500", "600", "700"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const jetbrains = JetBrains_Mono({
|
||||||
|
variable: "--font-jetbrains",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: { default: "Selimaj · Entrepreneur & Builder", template: "%s · Selimaj" },
|
||||||
|
description: "Selimaj, entrepreneur and builder. Founder of Orus. I build the layer underneath.",
|
||||||
|
metadataBase: new URL("https://selimaj.dev"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Runs before paint: marks JS as available, and arms the boot sequence once
|
||||||
|
// per session on the home page (never with reduced motion).
|
||||||
|
const headScript = `(function(){var d=document.documentElement;d.classList.add('js');try{if(location.pathname==='/'&&!sessionStorage.getItem('boot')&&!matchMedia('(prefers-reduced-motion: reduce)').matches){d.setAttribute('data-boot','')}}catch(e){}})();`;
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning className={`${chakra.variable} ${jetbrains.variable}`}>
|
||||||
|
<head>
|
||||||
|
<script dangerouslySetInnerHTML={{ __html: headScript }} />
|
||||||
|
</head>
|
||||||
|
<body className="flex min-h-dvh flex-col overflow-x-hidden bg-night text-ink">
|
||||||
|
<SiteHeader />
|
||||||
|
<div className="flex-1">{children}</div>
|
||||||
|
<SiteFooter />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<main className="grid-bg flex min-h-[70dvh] items-center pt-[85px]">
|
||||||
|
<div className="mx-auto w-full max-w-[1440px] px-4 md:px-16">
|
||||||
|
<div className="label tracking-[0.3em]">404 · Signal lost</div>
|
||||||
|
<h1 className="mt-3 font-display text-[52px] leading-[0.95] font-semibold uppercase md:text-7xl">No trace</h1>
|
||||||
|
<p className="mt-5 text-sm text-ink-2">Nothing resolves at this address.</p>
|
||||||
|
<Link href="/" className="mt-8 inline-flex min-h-12 items-center gap-3 bg-ink px-[22px] text-xs tracking-[0.2em] text-night hover:bg-accent">
|
||||||
|
RETURN TO BASE <span aria-hidden="true">→</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { BootSequence } from "@/components/BootSequence";
|
||||||
|
import { FeaturedNodes } from "@/components/FeaturedNodes";
|
||||||
|
import { Hero } from "@/components/Hero";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BootSequence />
|
||||||
|
<main>
|
||||||
|
<Hero />
|
||||||
|
<section aria-labelledby="nodes-title" className="mx-auto max-w-[1440px] px-4 pt-16 pb-16 md:px-16 md:pt-24 md:pb-20">
|
||||||
|
<div className="flex flex-wrap items-end justify-between gap-6">
|
||||||
|
<div>
|
||||||
|
<div className="label tracking-[0.3em]">02 / Nodes</div>
|
||||||
|
<h2 id="nodes-title" className="mt-2.5 font-display text-[28px] font-semibold tracking-[0.04em] uppercase md:text-[40px]">
|
||||||
|
Featured
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<Link href="/projects/" className="inline-flex min-h-11 items-center gap-2.5 text-[11px] tracking-[0.2em] text-mute hover:text-accent">
|
||||||
|
ALL PROJECTS <span aria-hidden="true">→</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<FeaturedNodes />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { ProjectIndex } from "@/components/ProjectIndex";
|
||||||
|
import { projects } from "@/content/projects";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Projects",
|
||||||
|
description: "Everything built and conceived under Orus, from Saturn and Enclave to cyberdecks and RC planes.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProjectsPage() {
|
||||||
|
return (
|
||||||
|
<main className="pt-[68px] md:pt-[85px]">
|
||||||
|
<div className="mx-auto max-w-[1440px] px-4 pt-10 pb-20 md:px-16 md:pt-16 md:pb-24">
|
||||||
|
<div className="label tracking-[0.3em]">Node index · Root Orus · {projects.length} nodes</div>
|
||||||
|
<h1 className="mt-3 font-display text-[52px] leading-[0.95] font-semibold uppercase md:text-7xl">Projects</h1>
|
||||||
|
<p className="mt-[18px] max-w-[560px] text-[13px] leading-[1.7] text-ink-2">
|
||||||
|
Most of what I've built and conceived. Concepts are labelled as concepts.
|
||||||
|
</p>
|
||||||
|
<ProjectIndex />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// Re-mounts on every navigation, so each page resolves in from wireframe.
|
||||||
|
export default function Template({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div className="page-resolve seq">{children}</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useSession } from "./Telemetry";
|
||||||
|
|
||||||
|
const DURATION = 1900;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Once-per-session boot screen on the home page. It only shows when the head
|
||||||
|
* script has set <html data-boot>, so without JS (or with reduced motion) it
|
||||||
|
* never appears. Click, tap or any key skips it.
|
||||||
|
*/
|
||||||
|
export function BootSequence() {
|
||||||
|
const s = useSession();
|
||||||
|
const [leaving, setLeaving] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (!root.hasAttribute("data-boot")) return;
|
||||||
|
|
||||||
|
let done = false;
|
||||||
|
let outTimer: number | undefined;
|
||||||
|
const finish = () => {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem("boot", "1");
|
||||||
|
} catch {}
|
||||||
|
setLeaving(true);
|
||||||
|
outTimer = window.setTimeout(() => root.removeAttribute("data-boot"), 250);
|
||||||
|
};
|
||||||
|
const timer = window.setTimeout(finish, DURATION);
|
||||||
|
window.addEventListener("keydown", finish);
|
||||||
|
window.addEventListener("pointerdown", finish);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
clearTimeout(outTimer);
|
||||||
|
window.removeEventListener("keydown", finish);
|
||||||
|
window.removeEventListener("pointerdown", finish);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stamp = s ? new Date(s.start).toISOString() : "";
|
||||||
|
const lines = [
|
||||||
|
{ text: "SESSION OPEN", tail: "[ OK ]" },
|
||||||
|
{ text: stamp },
|
||||||
|
{ text: s ? `HASH ${s.hash}` : "HASH" },
|
||||||
|
{ text: "SIGNAL ACQUIRED · TRACE ▸ TIRANA, AL" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="status"
|
||||||
|
aria-label="Loading"
|
||||||
|
className={`boot fixed inset-0 z-50 flex-col justify-end bg-night ${leaving ? "boot-out" : ""}`}
|
||||||
|
>
|
||||||
|
<div aria-hidden="true" className="grid-bg absolute inset-0" />
|
||||||
|
<div aria-hidden="true" className="scan-bg absolute inset-0" />
|
||||||
|
<div className="relative px-6 pb-[18vh] text-[13px] leading-[2.1] tracking-[0.08em] md:px-24 md:text-sm">
|
||||||
|
{lines.map((l, i) => (
|
||||||
|
<div key={i} className="type-line flex gap-[18px] text-ink-2 first:text-ink" style={{ "--d": `${0.1 + i * 0.3}s` } as React.CSSProperties}>
|
||||||
|
<span className="text-mute">></span>
|
||||||
|
{l.text}
|
||||||
|
{l.tail && <span className="text-mute">{l.tail}</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="type-line flex gap-[18px] text-accent" style={{ "--d": "1.3s" } as React.CSSProperties}>
|
||||||
|
<span className="text-mute">></span>LOCKING<span className="blink">▌</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 w-full max-w-[420px]">
|
||||||
|
<div className="flex justify-between text-[9px] tracking-[0.22em] text-mute">
|
||||||
|
<span>TRACE</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 h-px bg-white/12">
|
||||||
|
<div className="h-px origin-left bg-accent shadow-[0_0_8px_var(--color-accent)] [animation:trace_1.8s_ease-out_both]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="absolute right-6 bottom-8 text-[10px] tracking-[0.24em] text-mute md:right-16">CLICK OR PRESS ANY KEY TO SKIP</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2.5D identity reveal. The picture first shows as a high-contrast scanline
|
||||||
|
* "wireframe", a scan sweeps across and resolves it to solid, and on desktop
|
||||||
|
* the layers tilt apart slightly with the pointer. No 3D library needed.
|
||||||
|
*/
|
||||||
|
export function Bust({ src, delay = "0s", className = "" }: { src: string; delay?: string; className?: string }) {
|
||||||
|
const tiltRef = useRef<HTMLDivElement>(null);
|
||||||
|
const meshRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
function onMove(e: React.PointerEvent<HTMLDivElement>) {
|
||||||
|
if (e.pointerType !== "mouse" || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
||||||
|
const r = e.currentTarget.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - r.left) / r.width - 0.5;
|
||||||
|
const y = (e.clientY - r.top) / r.height - 0.5;
|
||||||
|
if (tiltRef.current) tiltRef.current.style.transform = `rotateX(${(-y * 8).toFixed(2)}deg) rotateY(${(x * 10).toFixed(2)}deg)`;
|
||||||
|
if (meshRef.current) meshRef.current.style.transform = `translate(${(x * -6).toFixed(1)}px, ${(y * -6).toFixed(1)}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLeave() {
|
||||||
|
if (tiltRef.current) tiltRef.current.style.transform = "";
|
||||||
|
if (meshRef.current) meshRef.current.style.transform = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative aspect-[168/200] [perspective:600px] ${className}`}
|
||||||
|
onPointerMove={onMove}
|
||||||
|
onPointerLeave={onLeave}
|
||||||
|
>
|
||||||
|
<div ref={tiltRef} className="absolute inset-0 transition-transform duration-200 ease-out [transform-style:preserve-3d]">
|
||||||
|
{/* wireframe pass */}
|
||||||
|
<div className="absolute inset-0 [mask-image:repeating-linear-gradient(0deg,#000_0_1px,transparent_1px_5px)]">
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt=""
|
||||||
|
fill
|
||||||
|
sizes="168px"
|
||||||
|
className="object-cover opacity-80 [filter:grayscale(1)_contrast(2.4)_brightness(1.6)_drop-shadow(1px_0_0_rgb(255_70_90/0.35))_drop-shadow(-1px_0_0_rgb(70_150_255/0.35))]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* solid pass */}
|
||||||
|
<div className="bust-solid seq absolute inset-0" style={{ "--d": delay } as React.CSSProperties}>
|
||||||
|
<Image src={src} alt="Profile picture" fill sizes="168px" className="object-cover" />
|
||||||
|
</div>
|
||||||
|
{/* residual mesh that floats a few px above the photo */}
|
||||||
|
<div
|
||||||
|
ref={meshRef}
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute inset-0 opacity-[0.12] transition-transform duration-200 ease-out [background-image:repeating-linear-gradient(0deg,#f2f4f5_0_1px,transparent_1px_8px)] [mask-image:linear-gradient(90deg,#000,transparent_70%)]"
|
||||||
|
/>
|
||||||
|
{/* scan line */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="bust-scan seq absolute top-0 bottom-0 left-full w-px bg-accent opacity-0 shadow-[0_0_8px_var(--color-accent)]"
|
||||||
|
style={{ "--d": delay } as React.CSSProperties}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { featured, projectById, projects } from "@/content/projects";
|
||||||
|
import { Segment } from "./Segment";
|
||||||
|
|
||||||
|
const ROOT = featured[0];
|
||||||
|
|
||||||
|
export function FeaturedNodes() {
|
||||||
|
const [active, setActive] = useState<string | null>(null);
|
||||||
|
const nodes = featured.map((f) => ({ ...f, project: projectById(f.id)! }));
|
||||||
|
const stageRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [size, setSize] = useState<{ w: number; h: number } | null>(null);
|
||||||
|
|
||||||
|
// Lines are drawn in px so they stay 1px crisp at any stage size.
|
||||||
|
useEffect(() => {
|
||||||
|
const el = stageRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const ro = new ResizeObserver(([e]) => setSize({ w: e.contentRect.width, h: e.contentRect.height }));
|
||||||
|
ro.observe(el);
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={stageRef}
|
||||||
|
className="bracket grid-bg relative mt-8 flex flex-col gap-1 border border-white/6 bg-surface p-6 [--bc:rgb(242_244_245/0.35)] md:block md:h-[460px] md:p-0"
|
||||||
|
onMouseLeave={() => setActive(null)}
|
||||||
|
>
|
||||||
|
{size && (
|
||||||
|
<div aria-hidden="true" className="hidden md:block">
|
||||||
|
{nodes.slice(1).map((n) => (
|
||||||
|
<Segment
|
||||||
|
key={`${n.id}-${active === n.id}`}
|
||||||
|
x1={(ROOT.x / 100) * size.w}
|
||||||
|
y1={(ROOT.y / 100) * size.h}
|
||||||
|
x2={(n.x / 100) * size.w}
|
||||||
|
y2={(n.y / 100) * size.h}
|
||||||
|
on={active === n.id}
|
||||||
|
dashed
|
||||||
|
strong
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{nodes.map((n) => {
|
||||||
|
const on = active === n.id;
|
||||||
|
const root = n.project.root;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={n.id}
|
||||||
|
href={`/projects/#${n.id}`}
|
||||||
|
onMouseEnter={() => setActive(n.id)}
|
||||||
|
onFocus={() => setActive(n.id)}
|
||||||
|
onBlur={() => setActive(null)}
|
||||||
|
className="group relative block min-h-12 outline-none md:absolute md:top-(--y) md:left-(--x) md:min-h-0"
|
||||||
|
style={{ "--x": `${n.x}%`, "--y": `${n.y}%` } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`absolute box-border border ${
|
||||||
|
root
|
||||||
|
? "top-2.5 left-0 size-7 rounded-full md:-top-3.5 md:-left-3.5"
|
||||||
|
: "top-4 left-2 size-3 md:-top-1.5 md:-left-1.5"
|
||||||
|
} ${on ? "border-accent shadow-[0_0_14px_var(--color-accent)]" : "border-ink/60"} ${on && !root ? "bg-accent" : "bg-night"}`}
|
||||||
|
>
|
||||||
|
{root && <span className={`absolute top-[9px] left-[9px] size-2 rounded-full ${on ? "bg-accent" : "bg-ink"}`} />}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`bracket ml-10 block min-h-11 px-3 py-2.5 group-focus-visible:outline group-focus-visible:outline-accent md:absolute md:-top-[22px] md:ml-0 ${
|
||||||
|
root ? "md:right-6 md:w-max md:text-right" : "md:left-6 md:w-[280px]"
|
||||||
|
} ${
|
||||||
|
on ? "[--bc:var(--color-accent)]" : "[--bc:transparent]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className={`flex items-baseline gap-3 ${root ? "md:flex-row-reverse" : ""}`}>
|
||||||
|
<span className="font-display text-lg font-semibold tracking-[0.08em] uppercase">{n.project.name}</span>
|
||||||
|
<span className="text-[9px] tracking-[0.22em] text-mute">{root ? "ROOT" : n.project.status}</span>
|
||||||
|
</span>
|
||||||
|
<span className={`mt-1.5 block text-[11px] leading-[1.55] text-ink-2 ${on && !root ? "" : "md:sr-only"}`}>{n.project.desc}</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div aria-hidden="true" className="absolute right-4 bottom-3 hidden text-[9px] tracking-[0.2em] text-mute/70 md:block">
|
||||||
|
ROOT ORUS · {featured.length} OF {projects.length} NODES
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { dossierFields, lock, orus, profile, type DossierField } from "@/content/profile";
|
||||||
|
import { Bust } from "./Bust";
|
||||||
|
import { LocalTime, SessionStamp } from "./Telemetry";
|
||||||
|
import { TerrainLayer } from "./TerrainLayer";
|
||||||
|
import { LockReticle, TiranaMap } from "./TiranaMap";
|
||||||
|
|
||||||
|
const d = (s: number) => ({ "--d": `${s}s` }) as React.CSSProperties;
|
||||||
|
|
||||||
|
function DossierList({ fields, className = "" }: { fields: DossierField[]; className?: string }) {
|
||||||
|
return (
|
||||||
|
<dl className={`grid grid-cols-[124px_minmax(0,1fr)] gap-y-2.5 border-t border-white/8 pt-[18px] text-xs leading-[1.45] xl:grid-cols-[116px_minmax(0,1fr)] ${className}`}>
|
||||||
|
{fields.map((f) => (
|
||||||
|
<div key={f.label} className="contents">
|
||||||
|
<dt className="pt-[3px] text-[9px] uppercase tracking-[0.22em] text-mute">{f.label}</dt>
|
||||||
|
<dd className="flex flex-wrap gap-x-3.5">
|
||||||
|
{f.links
|
||||||
|
? f.links.map((l) => (
|
||||||
|
<a key={l.href} href={l.href} className="border-b border-white/25 hover:border-accent">
|
||||||
|
{l.label}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
: f.value}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Hero() {
|
||||||
|
return (
|
||||||
|
<section aria-labelledby="hero-title" className="relative overflow-hidden border-b border-line">
|
||||||
|
<TerrainLayer />
|
||||||
|
|
||||||
|
<div className="relative z-10 mx-auto max-w-[1440px] pt-[68px] md:pt-[84px] xl:h-[864px]">
|
||||||
|
{/* Lock assembly: a strip on small screens, the full stage on desktop. */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="relative h-[240px] overflow-hidden border-b border-line [--lx:50%] xl:pointer-events-none xl:[--lx:45%] 2xl:[--lx:50%] xl:absolute xl:inset-0 xl:h-auto xl:overflow-visible xl:border-0"
|
||||||
|
>
|
||||||
|
<TiranaMap className="absolute top-[calc(50%-320px)] left-[calc(var(--lx)-320px)] size-[640px] xl:top-[-86px]" />
|
||||||
|
<LockReticle
|
||||||
|
className="reticle seq absolute top-[calc(50%-60px)] left-[calc(var(--lx)-60px)] max-xl:scale-75 xl:top-[174px]"
|
||||||
|
style={d(0.25)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="resolve seq absolute bottom-3 left-4 text-[9px] leading-[1.7] tracking-[0.14em] text-mute xl:bottom-auto xl:left-[calc(var(--lx)-60px)] xl:top-[306px] xl:text-[10px]"
|
||||||
|
style={d(0.85)}
|
||||||
|
>
|
||||||
|
<div className="text-accent">LOCK ACQUIRED</div>
|
||||||
|
<div>
|
||||||
|
{lock.lat} · {lock.lon}
|
||||||
|
</div>
|
||||||
|
<div className="uppercase">{lock.place}</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="trace seq absolute top-[233px] right-[510px] left-[calc(var(--lx)+66px)] hidden h-px bg-accent/55 xl:block"
|
||||||
|
style={d(0.9)}
|
||||||
|
>
|
||||||
|
<span className="absolute -top-4 left-0 text-[9px] whitespace-nowrap tracking-[0.2em] text-accent">TRACE ▸ 0001</span>
|
||||||
|
<span className="absolute -top-[3px] -right-[3px] size-[7px] bg-accent" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-9 px-4 pt-6 pb-16 md:px-16 xl:block xl:p-0">
|
||||||
|
{/* Headline */}
|
||||||
|
<div className="xl:absolute xl:top-[364px] xl:left-16 xl:w-[620px]">
|
||||||
|
<p className="resolve seq mb-3 text-[9px] tracking-[0.28em] text-mute xl:mb-5 xl:text-[10px] xl:tracking-[0.3em]" style={d(1.1)}>
|
||||||
|
TARGET RESOLVED · SUBJECT 0001
|
||||||
|
</p>
|
||||||
|
<h1
|
||||||
|
id="hero-title"
|
||||||
|
className="resolve seq font-display text-[46px] leading-[0.96] font-semibold uppercase sm:text-[64px] xl:text-[84px] xl:leading-[0.94] xl:tracking-[0.01em]"
|
||||||
|
style={d(1.2)}
|
||||||
|
>
|
||||||
|
Entrepreneur
|
||||||
|
<br />
|
||||||
|
& Builder
|
||||||
|
</h1>
|
||||||
|
<p className="resolve seq mt-4 text-[15px] leading-normal text-ink-2 xl:mt-6 xl:text-[17px]" style={d(1.3)}>
|
||||||
|
{profile.stance}
|
||||||
|
</p>
|
||||||
|
<div className="resolve seq mt-7 flex flex-col gap-2.5 sm:flex-row sm:flex-wrap sm:gap-3 xl:mt-10" style={d(1.4)}>
|
||||||
|
<Link
|
||||||
|
href="/projects/"
|
||||||
|
className="inline-flex min-h-12 items-center justify-between gap-3 bg-ink px-[22px] text-xs font-medium tracking-[0.2em] text-night transition-colors hover:bg-accent hover:text-night"
|
||||||
|
>
|
||||||
|
VIEW PROJECTS <span aria-hidden="true">→</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/about/"
|
||||||
|
className="bracket inline-flex min-h-12 items-center justify-between gap-3 border border-white/12 px-[22px] text-xs tracking-[0.2em] transition-colors [--bc:rgb(242_244_245/0.6)] hover:border-transparent hover:text-accent hover:[--bc:var(--color-accent)]"
|
||||||
|
>
|
||||||
|
OPEN DOSSIER <span aria-hidden="true">↗</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="resolve seq mt-10 hidden max-w-[580px] items-start gap-3.5 xl:flex" style={d(1.5)}>
|
||||||
|
<span className="pt-[3px] text-[9px] whitespace-nowrap tracking-[0.24em] text-mute">ROOT ─ ORUS</span>
|
||||||
|
<span className="text-xs leading-[1.6] text-mute">{orus.oneLiner}</span>
|
||||||
|
</div>
|
||||||
|
<DossierList fields={dossierFields} className="mt-9 xl:hidden" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile card */}
|
||||||
|
<article
|
||||||
|
aria-labelledby="profile-name"
|
||||||
|
className="bracket panel seq order-first border border-white/7 bg-surface/90 p-4 [--bc:rgb(242_244_245/0.55)] xl:absolute xl:top-[124px] xl:right-16 xl:order-none xl:w-[446px] xl:p-[22px]"
|
||||||
|
style={d(1.0)}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between text-[9px] tracking-[0.2em] text-mute xl:text-[10px]">
|
||||||
|
<span>PROFILE / 0001</span>
|
||||||
|
<span className="text-accent">● MATCH CONFIRMED</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3.5 flex items-end gap-4 xl:mt-[18px] xl:items-start xl:gap-[22px]">
|
||||||
|
<figure className="bracket w-[112px] flex-none [--bc:rgb(242_244_245/0.4)] xl:w-[168px]">
|
||||||
|
<Bust src={profile.pfp} delay="1.2s" />
|
||||||
|
{profile.pfpIsPlaceholder && (
|
||||||
|
<figcaption className="pt-2 pb-0.5 text-[9px] tracking-[0.2em] text-mute max-xl:hidden">PFP · PLACEHOLDER</figcaption>
|
||||||
|
)}
|
||||||
|
</figure>
|
||||||
|
<div className="min-w-0 xl:pt-1">
|
||||||
|
<div className="text-[9px] tracking-[0.24em] text-mute">ALIAS</div>
|
||||||
|
<h2 id="profile-name" className="mt-1.5 font-display text-[28px] leading-none font-semibold tracking-[0.06em] uppercase xl:text-4xl">
|
||||||
|
{profile.displayName}
|
||||||
|
</h2>
|
||||||
|
<div className="mt-2.5 text-[9px] leading-[1.9] tracking-[0.16em] text-mute xl:mt-[22px]">
|
||||||
|
<div className="max-xl:hidden">MESH 2.5D · RESOLVED</div>
|
||||||
|
<div className="max-xl:hidden">CLEARANCE · PUBLIC</div>
|
||||||
|
{profile.pfpIsPlaceholder && <div className="xl:hidden">PFP · PLACEHOLDER</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DossierList fields={dossierFields} className="mt-[22px] hidden xl:grid" />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Corner telemetry */}
|
||||||
|
<div aria-hidden="true" className="appear seq absolute bottom-[22px] left-16 hidden text-[9px] leading-[1.8] tracking-[0.16em] text-mute/70 xl:block" style={d(0.2)}>
|
||||||
|
<SessionStamp />
|
||||||
|
</div>
|
||||||
|
<div aria-hidden="true" className="absolute bottom-[22px] left-1/2 hidden -translate-x-1/2 text-[9px] tracking-[0.3em] text-mute xl:block">
|
||||||
|
SCROLL ▾ NODES
|
||||||
|
</div>
|
||||||
|
<div aria-hidden="true" className="appear seq absolute right-16 bottom-[22px] hidden text-right text-[9px] leading-[1.8] tracking-[0.16em] xl:block" style={d(0.2)}>
|
||||||
|
<div className="text-accent">
|
||||||
|
<LocalTime />
|
||||||
|
</div>
|
||||||
|
<div className="text-mute/70">VISITOR · UNRESOLVED</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function OrusMark({ size = 24, className }: { size?: number; className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
|
||||||
|
<circle cx="12" cy="12" r="9.5" stroke="currentColor" strokeWidth="1.2" strokeDasharray="50 10" />
|
||||||
|
<circle cx="12" cy="12" r="2.6" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { groups, projects, type GroupId, type Project } from "@/content/projects";
|
||||||
|
import { Segment } from "./Segment";
|
||||||
|
import { StatusTag } from "./StatusTag";
|
||||||
|
|
||||||
|
const W = 1200;
|
||||||
|
const H = 720;
|
||||||
|
const root = projects.find((p) => p.root)!;
|
||||||
|
const groupIds = Object.keys(groups) as GroupId[];
|
||||||
|
|
||||||
|
function Marker({ p, on }: { p: Project; on: boolean }) {
|
||||||
|
const size = p.root ? 30 : 14;
|
||||||
|
const rest =
|
||||||
|
p.status === "CONCEPT" ? "border border-dashed border-ink/60" : p.status === "ARCHIVED" ? "border border-ink/30" : "border border-ink/65";
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`relative box-border flex-none ${p.root ? "rounded-full" : ""} ${
|
||||||
|
on ? (p.root ? "border border-accent shadow-[0_0_14px_var(--color-accent)]" : "bg-accent shadow-[0_0_12px_var(--color-accent)]") : `${rest} bg-surface`
|
||||||
|
}`}
|
||||||
|
style={{ width: size, height: size }}
|
||||||
|
>
|
||||||
|
{p.root && <span className={`absolute top-[10px] left-[10px] size-2 rounded-full ${on ? "bg-accent" : "bg-ink"}`} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectCard({ p }: { p: Project }) {
|
||||||
|
const body = (
|
||||||
|
<>
|
||||||
|
<span className="flex items-center justify-between">
|
||||||
|
<StatusTag status={p.status} />
|
||||||
|
<span aria-hidden="true" className="text-mute transition-colors group-hover:text-accent group-focus-visible:text-accent">
|
||||||
|
{p.href ? "↗" : "→"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="font-display text-[22px] font-semibold tracking-[0.04em] uppercase">{p.name}</span>
|
||||||
|
<span className="flex-1 text-xs leading-[1.65] text-ink-2">{p.desc}</span>
|
||||||
|
<span className="flex flex-wrap gap-1.5">
|
||||||
|
{p.tags.map((t) => (
|
||||||
|
<span key={t} className="bg-surface-2 px-[7px] py-[3px] text-[10px] text-mute">
|
||||||
|
{t}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
const cls =
|
||||||
|
"group bracket flex min-h-[200px] scroll-mt-24 flex-col gap-3 border border-white/6 bg-surface p-[22px] [--bc:rgb(242_244_245/0.3)] hover:[--bc:var(--color-accent)] focus-visible:[--bc:var(--color-accent)]";
|
||||||
|
return p.href ? (
|
||||||
|
<a id={p.id} href={p.href} className={cls}>
|
||||||
|
{body}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<article id={p.id} className={cls}>
|
||||||
|
{body}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectIndex() {
|
||||||
|
const [view, setView] = useState<"map" | "list">("map");
|
||||||
|
const [active, setActive] = useState<string>("orus");
|
||||||
|
|
||||||
|
// Deep links (/projects/#enclave) select that node; small screens start on the list.
|
||||||
|
useEffect(() => {
|
||||||
|
const id = decodeURIComponent(location.hash.slice(1));
|
||||||
|
const small = matchMedia("(max-width: 767px)").matches;
|
||||||
|
if (projects.some((p) => p.id === id) || small) {
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- syncing with browser-only state once on mount
|
||||||
|
if (projects.some((p) => p.id === id)) setActive(id);
|
||||||
|
if (small) setView("list");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const sel = projects.find((p) => p.id === active) ?? root;
|
||||||
|
const count = (s: Project["status"]) => String(projects.filter((p) => p.status === s).length).padStart(2, "0");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mt-8 flex flex-wrap items-end justify-between gap-6">
|
||||||
|
<ul aria-label="Status legend" className="flex flex-wrap gap-[18px] text-[9px] tracking-[0.2em] text-mute">
|
||||||
|
<li className="flex items-center gap-1.5">
|
||||||
|
<span aria-hidden="true" className="size-[5px] rounded-full bg-accent" />
|
||||||
|
LIVE {count("LIVE")}
|
||||||
|
</li>
|
||||||
|
<li>BUILDING {count("BUILDING")}</li>
|
||||||
|
<li>CONCEPT {count("CONCEPT")}</li>
|
||||||
|
<li>ARCHIVED {count("ARCHIVED")}</li>
|
||||||
|
</ul>
|
||||||
|
<div role="group" aria-label="View" className="hidden border border-white/12 js:flex">
|
||||||
|
{(["map", "list"] as const).map((v) => (
|
||||||
|
<button
|
||||||
|
key={v}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={view === v}
|
||||||
|
onClick={() => setView(v)}
|
||||||
|
className={`min-h-11 min-w-[76px] border-b px-4 text-[11px] tracking-[0.22em] uppercase ${
|
||||||
|
view === v ? "border-accent bg-surface-2 text-ink" : "border-transparent text-mute hover:text-ink"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* MAP: needs JS to be useful, so it is hidden without it. */}
|
||||||
|
<div className={`hidden ${view === "map" ? "js:block" : ""}`}>
|
||||||
|
<div className="mt-6 overflow-x-auto border border-white/6">
|
||||||
|
<div className="grid-bg relative mx-auto bg-surface [background-size:40px_40px]" style={{ width: W, height: H }}>
|
||||||
|
{groupIds.map((k) => (
|
||||||
|
<Segment key={k} x1={root.map.x} y1={root.map.y} x2={groups[k].x} y2={groups[k].y} on={!sel.root && sel.group === k} strong />
|
||||||
|
))}
|
||||||
|
{projects
|
||||||
|
.filter((p) => !p.root)
|
||||||
|
.map((p) => (
|
||||||
|
<Segment
|
||||||
|
key={`${p.id}-${active === p.id}`}
|
||||||
|
x1={groups[p.group].x}
|
||||||
|
y1={groups[p.group].y}
|
||||||
|
x2={p.map.x}
|
||||||
|
y2={p.map.y}
|
||||||
|
on={active === p.id}
|
||||||
|
dashed={p.status === "CONCEPT"}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{groupIds.map((k) => {
|
||||||
|
const g = groups[k];
|
||||||
|
return (
|
||||||
|
<div key={k} aria-hidden="true">
|
||||||
|
<div
|
||||||
|
className="absolute text-[9px] whitespace-nowrap tracking-[0.24em] text-mute uppercase"
|
||||||
|
style={{ top: g.y - 7, ...(g.side === "l" ? { right: W - g.x + 14 } : { left: g.x + 14 }) }}
|
||||||
|
>
|
||||||
|
{g.label}
|
||||||
|
</div>
|
||||||
|
<div className="absolute box-border size-1.5 rotate-45 border border-ink/40" style={{ left: g.x - 3, top: g.y - 3 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{projects.map((p) => {
|
||||||
|
const on = active === p.id;
|
||||||
|
const g = groups[p.group];
|
||||||
|
const labelLeft = !p.root && p.map.x < g.x;
|
||||||
|
const m = p.root ? 30 : 14;
|
||||||
|
const pad = 8;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={on}
|
||||||
|
onClick={() => setActive(p.id)}
|
||||||
|
onMouseEnter={() => setActive(p.id)}
|
||||||
|
onFocus={() => setActive(p.id)}
|
||||||
|
className={`bracket absolute flex min-h-12 items-center gap-2.5 whitespace-nowrap focus-visible:outline-none ${
|
||||||
|
on ? "[--bc:var(--color-accent)]" : "[--bc:transparent] hover:[--bc:var(--color-accent)]"
|
||||||
|
} ${labelLeft ? "flex-row-reverse text-right" : ""}`}
|
||||||
|
style={{
|
||||||
|
top: p.map.y - 24,
|
||||||
|
padding: `0 ${pad}px`,
|
||||||
|
...(labelLeft ? { right: W - p.map.x - m / 2 - pad } : { left: p.map.x - m / 2 - pad }),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Marker p={p} on={on} />
|
||||||
|
<span className="flex flex-col gap-[3px]">
|
||||||
|
<span
|
||||||
|
className={`font-display font-semibold tracking-[0.08em] uppercase ${p.root ? "text-xl" : "text-[13px]"} ${
|
||||||
|
p.status === "ARCHIVED" && !on ? "text-ink/70" : "text-ink"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{p.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-[8px] tracking-[0.22em] text-mute">{p.root ? "ROOT" : p.status}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div aria-hidden="true" className="absolute bottom-3 left-4 text-[9px] tracking-[0.2em] text-mute/70">
|
||||||
|
GRID 40 · ROOT {root.map.x},{root.map.y}
|
||||||
|
</div>
|
||||||
|
<div aria-hidden="true" className="absolute right-4 bottom-3 text-[9px] tracking-[0.2em] text-accent uppercase">
|
||||||
|
TRACKING ▸ {sel.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section
|
||||||
|
aria-live="polite"
|
||||||
|
aria-label="Selected node"
|
||||||
|
className="bracket mt-4 grid gap-6 border border-white/6 bg-surface px-7 py-6 [--bc:var(--color-accent)] md:grid-cols-[minmax(0,1fr)_auto] md:items-center md:gap-x-12"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-wrap items-center gap-3.5">
|
||||||
|
<StatusTag status={sel.status} />
|
||||||
|
<span className="text-[9px] tracking-[0.24em] text-mute uppercase">{sel.root ? "Root" : groups[sel.group].label}</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="mt-3 font-display text-3xl font-semibold tracking-[0.04em] uppercase">{sel.name}</h2>
|
||||||
|
<p className="mt-2 max-w-[720px] text-[13px] leading-[1.7] text-ink-2">{sel.desc}</p>
|
||||||
|
<div className="mt-3.5 flex flex-wrap gap-2">
|
||||||
|
{sel.tags.map((t) => (
|
||||||
|
<span key={t} className="bg-surface-2 px-2 py-1 text-[10px] tracking-[0.1em] text-mute">
|
||||||
|
{t}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{sel.href ? (
|
||||||
|
<a href={sel.href} className="inline-flex min-h-12 items-center gap-3 justify-self-start border border-white/14 px-5 text-[11px] tracking-[0.2em] hover:border-accent">
|
||||||
|
VISIT <span aria-hidden="true">↗</span>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setView("list");
|
||||||
|
requestAnimationFrame(() => document.getElementById(sel.id)?.scrollIntoView({ block: "center" }));
|
||||||
|
}}
|
||||||
|
className="inline-flex min-h-12 items-center gap-3 justify-self-start border border-white/14 px-5 text-[11px] tracking-[0.2em] hover:border-accent hover:text-accent"
|
||||||
|
>
|
||||||
|
VIEW IN LIST <span aria-hidden="true">→</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LIST: always rendered; hidden only when JS is on and the map is chosen. */}
|
||||||
|
<div className={`mt-10 flex flex-col gap-14 ${view === "map" ? "js:hidden" : ""}`}>
|
||||||
|
{groupIds.map((k) => {
|
||||||
|
const items = projects.filter((p) => p.group === k);
|
||||||
|
return (
|
||||||
|
<section key={k} aria-labelledby={`g-${k}`}>
|
||||||
|
<div className="flex items-baseline gap-4 border-b border-white/8 pb-3.5">
|
||||||
|
<h2 id={`g-${k}`} className="font-display text-xl font-semibold tracking-[0.14em] uppercase">
|
||||||
|
{groups[k].label}
|
||||||
|
</h2>
|
||||||
|
<span className="text-[9px] tracking-[0.22em] text-mute">{String(items.length).padStart(2, "0")}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4">
|
||||||
|
{items.map((p) => (
|
||||||
|
<ProjectCard key={p.id} p={p} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/** A 1px tracking line between two points, in px, inside a positioned parent. */
|
||||||
|
export function Segment({
|
||||||
|
x1,
|
||||||
|
y1,
|
||||||
|
x2,
|
||||||
|
y2,
|
||||||
|
on,
|
||||||
|
dashed,
|
||||||
|
strong,
|
||||||
|
}: {
|
||||||
|
x1: number;
|
||||||
|
y1: number;
|
||||||
|
x2: number;
|
||||||
|
y2: number;
|
||||||
|
on: boolean;
|
||||||
|
dashed?: boolean;
|
||||||
|
strong?: boolean;
|
||||||
|
}) {
|
||||||
|
const len = Math.hypot(x2 - x1, y2 - y1);
|
||||||
|
const ang = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI;
|
||||||
|
const c = on ? "var(--color-accent)" : strong ? "rgb(242 244 245 / 0.2)" : "rgb(242 244 245 / 0.11)";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`pointer-events-none absolute h-px origin-[0_50%] ${on ? "seg-on shadow-[0_0_6px_var(--color-accent)]" : ""}`}
|
||||||
|
style={{
|
||||||
|
left: x1,
|
||||||
|
top: y1,
|
||||||
|
width: len,
|
||||||
|
transform: `rotate(${ang}deg)`,
|
||||||
|
background: dashed && !on ? `repeating-linear-gradient(90deg, ${c} 0 4px, transparent 4px 9px)` : c,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { profile } from "@/content/profile";
|
||||||
|
|
||||||
|
export function SiteFooter() {
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-line">
|
||||||
|
<div className="mx-auto flex max-w-[1440px] flex-wrap justify-between gap-6 px-4 pt-7 pb-10 text-[10px] tracking-[0.18em] text-mute md:px-16">
|
||||||
|
<span>{profile.displayName.toUpperCase()} · BUILT UNDER ORUS</span>
|
||||||
|
<span className="flex gap-6">
|
||||||
|
<a href={profile.github} className="hover:text-accent">GITHUB</a>
|
||||||
|
<a href={profile.git} className="hover:text-accent">GIT.SELIMAJ.DEV</a>
|
||||||
|
</span>
|
||||||
|
<span aria-hidden="true">
|
||||||
|
41.33 N · 19.82 E · <span className="blink text-accent">▌</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { OrusMark } from "./OrusMark";
|
||||||
|
import { SessionClock } from "./Telemetry";
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{ href: "/", label: "Home" },
|
||||||
|
{ href: "/about/", label: "About" },
|
||||||
|
{ href: "/projects/", label: "Projects" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const isCurrent = (pathname: string, href: string) =>
|
||||||
|
href === "/" ? pathname === "/" : pathname.startsWith(href.replace(/\/$/, ""));
|
||||||
|
|
||||||
|
export function SiteHeader() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const isHome = pathname === "/";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={`absolute inset-x-0 top-0 z-30 ${isHome ? "" : "border-b border-line"}`}
|
||||||
|
>
|
||||||
|
<div className="mx-auto flex max-w-[1440px] items-center justify-between gap-6 px-4 py-3 md:px-16 md:py-[22px]">
|
||||||
|
<Link href="/" className="flex min-h-11 items-center gap-3" aria-label="Orus · selimaj.dev home">
|
||||||
|
<OrusMark />
|
||||||
|
<span className="font-display text-sm font-semibold tracking-[0.24em]">ORUS</span>
|
||||||
|
<span className="hidden text-[10px] tracking-[0.18em] text-mute sm:inline">/ SELIMAJ.DEV</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex min-h-11 items-center gap-2.5 px-1 text-[10px] tracking-[0.22em] md:hidden"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls="site-nav"
|
||||||
|
onClick={() => setOpen((o) => !o)}
|
||||||
|
>
|
||||||
|
{open ? "CLOSE" : "MENU"}
|
||||||
|
<svg width="18" height="12" viewBox="0 0 18 12" fill="none" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path d="M0,1 H18 M6,6 H18 M0,11 H18" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
id="site-nav"
|
||||||
|
aria-label="Primary"
|
||||||
|
className={`${open ? "block" : "hidden"} absolute inset-x-0 top-full border-y border-line bg-night/95 px-4 backdrop-blur md:static md:block md:border-0 md:bg-transparent md:p-0 md:backdrop-blur-none`}
|
||||||
|
>
|
||||||
|
<ul className="flex flex-col text-[11px] tracking-[0.2em] md:flex-row md:gap-9">
|
||||||
|
{links.map((l) => {
|
||||||
|
const current = isCurrent(pathname, l.href);
|
||||||
|
return (
|
||||||
|
<li key={l.href}>
|
||||||
|
<Link
|
||||||
|
href={l.href}
|
||||||
|
aria-current={current ? "page" : undefined}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className={`flex min-h-11 items-center gap-2 uppercase md:py-3.5 ${current ? "text-ink" : "text-mute hover:text-accent"}`}
|
||||||
|
>
|
||||||
|
{current && <span aria-hidden="true" className="size-1.5 bg-accent shadow-[0_0_8px_var(--color-accent)]" />}
|
||||||
|
{l.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div aria-hidden="true" className="hidden text-right text-[10px] leading-relaxed tracking-[0.14em] text-mute lg:block">
|
||||||
|
<SessionClock />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Status } from "@/content/projects";
|
||||||
|
|
||||||
|
const styles: Record<Status, string> = {
|
||||||
|
LIVE: "text-ink-2 border border-white/30",
|
||||||
|
BUILDING: "text-ink-2 border border-white/30",
|
||||||
|
CONCEPT: "text-ink-2 border border-dashed border-white/40",
|
||||||
|
ARCHIVED: "text-mute border border-white/12",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function StatusTag({ status }: { status: Status }) {
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center gap-1.5 px-2 py-1 text-[9px] tracking-[0.2em] ${styles[status]}`}>
|
||||||
|
{status === "LIVE" && <span aria-hidden="true" className="size-[5px] rounded-full bg-accent" />}
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSyncExternalStore } from "react";
|
||||||
|
|
||||||
|
// Real data: when this session opened, a random per-session hash, and the
|
||||||
|
// visitor's local clock. All of it is client-only and never leaves the page.
|
||||||
|
|
||||||
|
type Session = { start: number; hash: string };
|
||||||
|
let session: Session | null = null;
|
||||||
|
|
||||||
|
function getSession(): Session {
|
||||||
|
if (session) return session;
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem("session");
|
||||||
|
if (raw) return (session = JSON.parse(raw) as Session);
|
||||||
|
} catch {}
|
||||||
|
const bytes = crypto.getRandomValues(new Uint8Array(8));
|
||||||
|
const hash = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
||||||
|
session = { start: Date.now(), hash };
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem("session", JSON.stringify(session));
|
||||||
|
} catch {}
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noop = () => () => {};
|
||||||
|
const tick = (cb: () => void) => {
|
||||||
|
const id = setInterval(cb, 1000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
};
|
||||||
|
const nowSeconds = () => Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
export function useSession() {
|
||||||
|
return useSyncExternalStore(noop, getSession, () => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNow() {
|
||||||
|
const s = useSyncExternalStore(tick, nowSeconds, () => null);
|
||||||
|
return s === null ? null : new Date(s * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = (n: number) => String(n).padStart(2, "0");
|
||||||
|
|
||||||
|
export function formatTime(d: Date) {
|
||||||
|
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatOffset(d: Date) {
|
||||||
|
const off = -d.getTimezoneOffset();
|
||||||
|
const a = Math.abs(off);
|
||||||
|
return `UTC${off >= 0 ? "+" : "-"}${p(Math.floor(a / 60))}:${p(a % 60)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatElapsed(ms: number) {
|
||||||
|
const s = Math.max(0, Math.floor(ms / 1000));
|
||||||
|
return `${p(Math.floor(s / 60))}:${p(s % 60)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SessionClock() {
|
||||||
|
const s = useSession();
|
||||||
|
const now = useNow();
|
||||||
|
const id = s ? `${s.hash.slice(0, 4)}·${s.hash.slice(4, 8)}`.toUpperCase() : "····";
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>SESSION {id}</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-accent">● LIVE</span> T+{s && now ? formatElapsed(now.getTime() - s.start) : "--:--"}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SessionStamp() {
|
||||||
|
const s = useSession();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>SESSION OPEN · {s ? new Date(s.start).toISOString().slice(0, 19) + "Z" : "…"}</div>
|
||||||
|
<div>HASH {s ? `${s.hash.slice(0, 8)}·${s.hash.slice(8)}` : "…"} · RES CITY-SCALE</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocalTime({ prefix = "LOCAL" }: { prefix?: string }) {
|
||||||
|
const now = useNow();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{prefix} {now ? `${formatTime(now)} ${formatOffset(now)}` : "--:--:--"}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Hero background, bottom to top: terrain contours (real Dajt elevation data),
|
||||||
|
* grid, scanlines, vignette. Purely decorative.
|
||||||
|
*/
|
||||||
|
export function TerrainLayer({ className = "" }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<div aria-hidden="true" className={`pointer-events-none absolute inset-0 overflow-hidden ${className}`}>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element -- decorative SVG, no optimisation needed */}
|
||||||
|
<img
|
||||||
|
src="/terrain.svg"
|
||||||
|
alt=""
|
||||||
|
className="drift absolute -inset-10 h-[calc(100%+80px)] w-[calc(100%+80px)] max-w-none object-cover opacity-20 max-xl:object-[60%_50%]"
|
||||||
|
/>
|
||||||
|
<div className="grid-bg absolute inset-0" />
|
||||||
|
<div className="scan-bg absolute inset-0" />
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_55%_35%,transparent_35%,rgb(0_0_0/0.75)_100%)]" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Stylised Tirana city centre, drawn by hand at city scale only.
|
||||||
|
* Centred on Skanderbeg Square. No street-level detail beyond that.
|
||||||
|
*/
|
||||||
|
export function TiranaMap({ className = "" }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 640 640" aria-hidden="true" className={className}>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="map-fade">
|
||||||
|
<stop offset="0.45" stopColor="#fff" />
|
||||||
|
<stop offset="1" stopColor="#000" />
|
||||||
|
</radialGradient>
|
||||||
|
<mask id="map-mask">
|
||||||
|
<rect width="640" height="640" fill="url(#map-fade)" />
|
||||||
|
</mask>
|
||||||
|
</defs>
|
||||||
|
<g mask="url(#map-mask)" fill="none" stroke="#fff" strokeWidth="1" className="draw">
|
||||||
|
<circle pathLength={1} cx="320" cy="320" r="300" strokeOpacity="0.08" style={{ "--d": "0.1s" } as React.CSSProperties} />
|
||||||
|
<path pathLength={1} strokeOpacity="0.14" d="M150,250 C170,170 250,130 330,135 C420,140 495,190 505,280 C515,370 470,450 380,480 C290,505 190,480 155,400 C135,350 138,300 150,250Z" />
|
||||||
|
<path pathLength={1} strokeOpacity="0.18" d="M320,640 L320,354" />
|
||||||
|
<path pathLength={1} strokeOpacity="0.18" d="M320,286 L310,0" />
|
||||||
|
<path pathLength={1} strokeOpacity="0.13" d="M286,316 L0,262" />
|
||||||
|
<path pathLength={1} strokeOpacity="0.13" d="M354,322 L640,352" />
|
||||||
|
<path pathLength={1} strokeOpacity="0.08" d="M296,296 L120,120" style={{ "--d": "0.2s" } as React.CSSProperties} />
|
||||||
|
<path pathLength={1} strokeOpacity="0.08" d="M346,344 L560,560" style={{ "--d": "0.2s" } as React.CSSProperties} />
|
||||||
|
<path pathLength={1} strokeOpacity="0.08" d="M346,296 L540,110" style={{ "--d": "0.2s" } as React.CSSProperties} />
|
||||||
|
<path pathLength={1} strokeOpacity="0.08" d="M294,346 L90,560" style={{ "--d": "0.2s" } as React.CSSProperties} />
|
||||||
|
<path pathLength={1} strokeOpacity="0.14" d="M0,470 C110,452 200,492 320,482 C440,472 530,500 640,490" />
|
||||||
|
<path pathLength={1} strokeOpacity="0.14" d="M0,478 C110,460 200,500 320,490 C440,480 530,508 640,498" />
|
||||||
|
<g strokeOpacity="0.07" style={{ "--d": "0.35s" } as React.CSSProperties}>
|
||||||
|
{[
|
||||||
|
[360, 200, 60, 44], [432, 212, 46, 56], [360, 380, 70, 40], [208, 372, 64, 50], [200, 212, 56, 40],
|
||||||
|
[442, 380, 44, 60], [232, 150, 40, 34], [380, 150, 54, 30], [236, 268, 40, 30], [364, 262, 34, 34],
|
||||||
|
].map(([x, y, w, h]) => (
|
||||||
|
<rect key={`${x}-${y}`} pathLength={1} x={x} y={y} width={w} height={h} style={{ "--d": "0.35s" } as React.CSSProperties} />
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
<circle pathLength={1} cx="320" cy="320" r="34" strokeOpacity="0.24" style={{ "--d": "0.4s" } as React.CSSProperties} />
|
||||||
|
<circle pathLength={1} cx="320" cy="320" r="16" strokeOpacity="0.12" style={{ "--d": "0.45s" } as React.CSSProperties} />
|
||||||
|
</g>
|
||||||
|
<text x="362" y="352" fill="#8b9296" fillOpacity="0.7" fontSize="9" letterSpacing="1.6" fontFamily="var(--font-mono)">
|
||||||
|
SKANDERBEG SQ.
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LockReticle({ className = "", style }: { className?: string; style?: React.CSSProperties }) {
|
||||||
|
return (
|
||||||
|
<div aria-hidden="true" className={`bracket size-[120px] [--bc:var(--color-accent)] [--bl:16px] ${className}`} style={style}>
|
||||||
|
<svg viewBox="0 0 120 120" width="120" height="120" fill="none" stroke="var(--color-accent)">
|
||||||
|
<circle className="spin" cx="60" cy="60" r="34" strokeDasharray="3 5" strokeOpacity="0.8" />
|
||||||
|
<circle cx="60" cy="60" r="20" strokeOpacity="0.35" />
|
||||||
|
<path d="M60,14 L60,30 M60,90 L60,106 M14,60 L30,60 M90,60 L106,60" />
|
||||||
|
<circle cx="60" cy="60" r="2.5" fill="var(--color-accent)" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import type { Status } from "./projects";
|
||||||
|
|
||||||
|
export type Proof = {
|
||||||
|
label: string;
|
||||||
|
note: string;
|
||||||
|
href?: string;
|
||||||
|
status?: Status;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DossierFile = {
|
||||||
|
slug: string;
|
||||||
|
no: string;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
proofSummary: string;
|
||||||
|
lead: string;
|
||||||
|
paragraphs: string[];
|
||||||
|
proof: Proof[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// PLACEHOLDER: all copy below is a first draft from the spec. Edit freely.
|
||||||
|
export const dossier: DossierFile[] = [
|
||||||
|
{
|
||||||
|
slug: "leadership",
|
||||||
|
no: "01",
|
||||||
|
title: "Leadership",
|
||||||
|
summary: "Leading groups since I was ten. Clans first, then Orus.",
|
||||||
|
proofSummary: "Orus",
|
||||||
|
lead: "Leading groups and getting people to follow, since I was ten.",
|
||||||
|
paragraphs: [
|
||||||
|
"It started with gaming clans at 10 and 11, and the content around them.",
|
||||||
|
"The same instinct became Orus at 12 and 13.",
|
||||||
|
],
|
||||||
|
proof: [
|
||||||
|
{
|
||||||
|
label: "Orus",
|
||||||
|
note: "A community that has grown around the work",
|
||||||
|
href: "/projects/#orus",
|
||||||
|
status: "LIVE",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "entrepreneurship",
|
||||||
|
no: "02",
|
||||||
|
title: "Entrepreneurship",
|
||||||
|
summary: "Orus as the umbrella. Systems where incentives and behavior matter.",
|
||||||
|
proofSummary: "Osias · Orus · EquitySMP",
|
||||||
|
lead: "Orus is the umbrella for everything serious I make.",
|
||||||
|
paragraphs: [
|
||||||
|
"Beyond it I design systems where incentives and behavior matter.",
|
||||||
|
"Osias is a blockchain-powered AI agent for trading emotions and behavior. Alongside it sits a Minecraft economy concept with jurisdictions, taxes, and IPOs.",
|
||||||
|
],
|
||||||
|
proof: [
|
||||||
|
{ label: "Osias", note: "osias.trade", href: "https://osias.trade", status: "BUILDING" },
|
||||||
|
{ label: "Orus", note: "The umbrella org", href: "/projects/#orus", status: "LIVE" },
|
||||||
|
{ label: "EquitySMP", note: "Real-world-style server economy", href: "/projects/#equitysmp", status: "BUILDING" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "builder",
|
||||||
|
no: "03",
|
||||||
|
title: "Builder",
|
||||||
|
summary: "The interesting problems sit one layer lower than where most people stop.",
|
||||||
|
proofSummary: "Saturn · Enclave · Gitea",
|
||||||
|
lead: "I don’t accept the layer underneath as a given.",
|
||||||
|
paragraphs: [
|
||||||
|
"I wrote my own client for Minecraft (Saturn). I’m designing a transport protocol to replace HTTP and WebSockets for Enclave’s chat. I host my own Git.",
|
||||||
|
"The interesting problems are usually one layer lower than where most people stop.",
|
||||||
|
],
|
||||||
|
proof: [
|
||||||
|
{ label: "Saturn", note: "Minecraft client, written from scratch", href: "/projects/#saturn", status: "ARCHIVED" },
|
||||||
|
{ label: "Enclave + transport protocol", note: "Chat platform, and the protocol it runs on", href: "/projects/#enclave", status: "BUILDING" },
|
||||||
|
{ label: "git.selimaj.dev", note: "Gitea on a Docker and Coolify homelab", href: "https://git.selimaj.dev", status: "LIVE" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "discipline",
|
||||||
|
no: "04",
|
||||||
|
title: "Discipline",
|
||||||
|
summary: "Formerly #1 on Discipline in Albania. Consistency, tracked in public.",
|
||||||
|
proofSummary: "Contribution graph · rank",
|
||||||
|
lead: "Formerly ranked #1 on Discipline in Albania.",
|
||||||
|
paragraphs: [
|
||||||
|
"I track consistency through contribution graphs.",
|
||||||
|
"A long project list reads as sustained output, not abandoned experiments.",
|
||||||
|
],
|
||||||
|
proof: [
|
||||||
|
{ label: "Contribution graph", note: "github.com/selimaj-dev", href: "https://github.com/selimaj-dev" },
|
||||||
|
{ label: "Ranking", note: "Formerly #1 on Discipline, Albania" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "passions",
|
||||||
|
no: "05",
|
||||||
|
title: "Passions",
|
||||||
|
summary: "Cyberdecks, long-range RC planes, algorithmic trading, Minecraft with friends.",
|
||||||
|
proofSummary: "Build logs",
|
||||||
|
lead: "The things I do because I can’t not.",
|
||||||
|
paragraphs: [
|
||||||
|
"Custom hardware: a briefcase cyberdeck, and a compact one that aims to be a Flipper Zero but better.",
|
||||||
|
"Long-range PC and 4G-controlled RC planes built around a custom watercooled powertrain. Algorithmic trading. Minecraft with friends.",
|
||||||
|
],
|
||||||
|
proof: [
|
||||||
|
{ label: "Cyberdecks", note: "Build photos and logs to come", href: "/projects/#briefcase-deck", status: "BUILDING" },
|
||||||
|
{ label: "RC planes", note: "Build photos and logs to come", href: "/projects/#rc-planes", status: "BUILDING" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const fileBySlug = (slug: string) => dossier.find((f) => f.slug === slug);
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Everything personal or placeholder lives in src/content/.
|
||||||
|
* Items marked PLACEHOLDER are meant to be swapped later.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const profile = {
|
||||||
|
// PLACEHOLDER: final display form of the name is still TBD.
|
||||||
|
displayName: "Selimaj",
|
||||||
|
title: "Entrepreneur & Builder",
|
||||||
|
stance: "I build the layer underneath.",
|
||||||
|
// PLACEHOLDER: replace this one file to swap the profile picture.
|
||||||
|
pfp: "/profile/pfp.svg",
|
||||||
|
pfpIsPlaceholder: true,
|
||||||
|
github: "https://github.com/selimaj-dev",
|
||||||
|
git: "https://git.selimaj.dev",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const orus = {
|
||||||
|
name: "Orus",
|
||||||
|
oneLiner:
|
||||||
|
"A community and toolset for privacy, independence, and opsec, plus the dev tools built under it.",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map lock. Privacy rule: city centre only (Skanderbeg Square, a public
|
||||||
|
* landmark). Never put anything finer than city scale here.
|
||||||
|
*/
|
||||||
|
export const lock = {
|
||||||
|
lat: "41.3275 N",
|
||||||
|
lon: "19.8187 E",
|
||||||
|
place: "Tirana · City centre",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type DossierField = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
links?: { label: string; href: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dossierFields: DossierField[] = [
|
||||||
|
{ label: "Based", value: "Tirana, Albania" },
|
||||||
|
{ label: "Affiliation", value: "Orus (founder)" },
|
||||||
|
// PLACEHOLDER decision: remove this entry to stop stating it publicly.
|
||||||
|
{ label: "Building since", value: "12" },
|
||||||
|
{ label: "Focus", value: "Privacy, independence, opsec, dev tooling" },
|
||||||
|
{ label: "Known for", value: "Saturn, Enclave" },
|
||||||
|
{ label: "Record", value: "Formerly #1 on Discipline, Albania" },
|
||||||
|
{
|
||||||
|
label: "Links",
|
||||||
|
value: "",
|
||||||
|
links: [
|
||||||
|
{ label: "GitHub", href: profile.github },
|
||||||
|
{ label: "git.selimaj.dev", href: profile.git },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
export type Status = "LIVE" | "BUILDING" | "CONCEPT" | "ARCHIVED";
|
||||||
|
|
||||||
|
export type GroupId = "core" | "ventures" | "infra" | "hardware";
|
||||||
|
|
||||||
|
export type Project = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
group: GroupId;
|
||||||
|
status: Status;
|
||||||
|
desc: string;
|
||||||
|
tags: string[];
|
||||||
|
href?: string;
|
||||||
|
root?: boolean;
|
||||||
|
/** Position on the 1200×720 node map. */
|
||||||
|
map: { x: number; y: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const groups: Record<GroupId, { label: string; x: number; y: number; side: "l" | "r" }> = {
|
||||||
|
core: { label: "Core", x: 360, y: 220, side: "l" },
|
||||||
|
ventures: { label: "Ventures & systems", x: 840, y: 220, side: "r" },
|
||||||
|
infra: { label: "Infrastructure", x: 360, y: 520, side: "l" },
|
||||||
|
hardware: { label: "Hardware", x: 840, y: 520, side: "r" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// PLACEHOLDER: statuses for everything except Orus and the self-hosted
|
||||||
|
// infrastructure are best guesses. Links are filled in where known.
|
||||||
|
export const projects: Project[] = [
|
||||||
|
{
|
||||||
|
id: "orus",
|
||||||
|
name: "Orus",
|
||||||
|
group: "core",
|
||||||
|
root: true,
|
||||||
|
status: "LIVE",
|
||||||
|
desc: "Community and org for privacy, independence, opsec, and dev tools. Root of everything below.",
|
||||||
|
tags: ["community", "opsec", "org"],
|
||||||
|
map: { x: 600, y: 360 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "saturn",
|
||||||
|
name: "Saturn",
|
||||||
|
group: "core",
|
||||||
|
status: "ARCHIVED",
|
||||||
|
desc: "Minecraft client. Did most of what I set out to do with it.",
|
||||||
|
tags: ["minecraft", "client"],
|
||||||
|
map: { x: 200, y: 90 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "enclave",
|
||||||
|
name: "Enclave",
|
||||||
|
group: "core",
|
||||||
|
status: "BUILDING",
|
||||||
|
desc: "Discord-like chat platform, running on a custom transport protocol.",
|
||||||
|
tags: ["chat", "realtime"],
|
||||||
|
map: { x: 420, y: 70 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "enclave-protocol",
|
||||||
|
name: "Enclave protocol",
|
||||||
|
group: "core",
|
||||||
|
status: "CONCEPT",
|
||||||
|
desc: "Transport protocol replacing HTTP and WSS; Enclave’s chat protocol runs on top of it. Concept and design notes.",
|
||||||
|
tags: ["networking", "protocol design"],
|
||||||
|
map: { x: 190, y: 300 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "osias",
|
||||||
|
name: "Osias",
|
||||||
|
group: "ventures",
|
||||||
|
status: "BUILDING",
|
||||||
|
desc: "Blockchain-powered AI agent for trading emotions and behavior.",
|
||||||
|
tags: ["ai", "blockchain"],
|
||||||
|
href: "https://osias.trade",
|
||||||
|
map: { x: 700, y: 90 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "equitysmp",
|
||||||
|
name: "EquitySMP",
|
||||||
|
group: "ventures",
|
||||||
|
status: "BUILDING",
|
||||||
|
desc: "Minecraft server built around an advanced real-world-style economy.",
|
||||||
|
tags: ["minecraft", "economy"],
|
||||||
|
map: { x: 990, y: 90 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mc-economy",
|
||||||
|
name: "MC economy",
|
||||||
|
group: "ventures",
|
||||||
|
status: "CONCEPT",
|
||||||
|
desc: "Jurisdictions and taxes, world-healing tied to liquidity, trading and IPOs, sandbox GTA-style elements.",
|
||||||
|
tags: ["systems design", "economy"],
|
||||||
|
map: { x: 1020, y: 280 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "algo-trading",
|
||||||
|
name: "Algo trading",
|
||||||
|
group: "ventures",
|
||||||
|
status: "BUILDING",
|
||||||
|
desc: "Several repos exploring automated crypto trading strategies.",
|
||||||
|
tags: ["trading", "crypto"],
|
||||||
|
map: { x: 980, y: 400 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "git",
|
||||||
|
name: "Self-hosted Git",
|
||||||
|
group: "infra",
|
||||||
|
status: "LIVE",
|
||||||
|
desc: "Gitea at git.selimaj.dev, on a Docker and Coolify homelab server behind Cloudflare.",
|
||||||
|
tags: ["gitea", "docker", "coolify"],
|
||||||
|
href: "https://git.selimaj.dev",
|
||||||
|
map: { x: 190, y: 450 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "homelab",
|
||||||
|
name: "HomeLab",
|
||||||
|
group: "infra",
|
||||||
|
status: "LIVE",
|
||||||
|
desc: "The server environment everything self-hosted runs on.",
|
||||||
|
tags: ["infra", "docker"],
|
||||||
|
map: { x: 300, y: 660 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "briefcase-deck",
|
||||||
|
name: "Briefcase deck",
|
||||||
|
group: "hardware",
|
||||||
|
status: "BUILDING",
|
||||||
|
desc: "Briefcase cyberdeck: the most versatile and doable build.",
|
||||||
|
tags: ["hardware", "cyberdeck"],
|
||||||
|
map: { x: 1020, y: 470 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "compact-deck",
|
||||||
|
name: "Compact deck",
|
||||||
|
group: "hardware",
|
||||||
|
status: "CONCEPT",
|
||||||
|
desc: "Compact premium cyberdeck: a Flipper Zero, but better.",
|
||||||
|
tags: ["hardware", "cyberdeck"],
|
||||||
|
map: { x: 1000, y: 640 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rc-planes",
|
||||||
|
name: "RC planes",
|
||||||
|
group: "hardware",
|
||||||
|
status: "BUILDING",
|
||||||
|
desc: "Custom long-range, PC and 4G-controlled planes pushing speed and efficiency, built around a custom watercooled powertrain.",
|
||||||
|
tags: ["hardware", "rc", "4g"],
|
||||||
|
map: { x: 720, y: 660 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const projectById = (id: string) => projects.find((p) => p.id === id);
|
||||||
|
|
||||||
|
/** Home page featured strip: positions are percentages of the stage. */
|
||||||
|
export const featured = [
|
||||||
|
{ id: "orus", x: 18, y: 50 },
|
||||||
|
{ id: "saturn", x: 46, y: 22 },
|
||||||
|
{ id: "enclave", x: 68, y: 52 },
|
||||||
|
{ id: "equitysmp", x: 44, y: 80 },
|
||||||
|
] as const;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts",
|
||||||
|
"**/*.mts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user