Compare commits

...
5 Commits
Author SHA1 Message Date
selimaj-devandclaude 2333ecf874 Pin Node 22 for Nixpacks builds
Next 16 needs Node >= 20.9; Nixpacks defaulted to 18.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-03 21:40:05 +02:00
selimaj-devandclaude e268f2a359 Use Mono.png as profile picture and favicon; sync proof statuses
- Dossier proof statuses now come from projects.ts
- Mono.png replaces the placeholder silhouette and the SVG favicon
- Header, hero and bust tweaks

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-03 21:20:37 +02:00
selimaj-devandclaude fd72e1082a Hero decode effect, static terrain, and selimaj.dev rebrand
- Brute-force scramble that decodes hero values left to right
- Full-screen hero on desktop with telemetry pinned to the bottom
- Terrain no longer drifts; home skips the page transition
- selimaj.dev is the root brand; Orus becomes one brand with its own
  projects, and independent projects branch from the root

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-03 21:04:01 +02:00
selimaj-devandclaude 85dbdb3db4 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]>
2026-10-03 20:40:10 +02:00
selimaj-dev 3ea4946164 back to fresh 2026-10-03 18:56:07 +02:00
67 changed files with 7641 additions and 10927 deletions
+38 -16
View File
@@ -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?
+9
View File
@@ -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 -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
-16
View File
@@ -1,16 +0,0 @@
# Stage 1: Build the application
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Serve the static files with Nginx
FROM nginx:alpine
# Copy the build files from the builder stage to the Nginx web directory
COPY --from=builder /app/dist /usr/share/nginx/html
# Expose port 80 where the Nginx server runs
EXPOSE 80
# Command to start Nginx server
CMD ["nginx", "-g", "daemon off;"]
+22 -67
View File
@@ -1,73 +1,28 @@
# React + TypeScript + Vite # selimaj.dev
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Portfolio site. Next.js (App Router, static export) and Tailwind CSS v4.
Currently, two official plugins are available: ```bash
npm install
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) npm run dev # http://localhost:3000
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) npm run build # static site in out/
npm run lint
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
``` ```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: `npm run build` writes plain HTML/CSS/JS to `out/`, which any static host can serve.
```js ## Where things live
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([ - `src/content/`: all copy and data, including every placeholder (marked `PLACEHOLDER`).
globalIgnores(['dist']), - `profile.ts`: name, title, stance, dossier fields, links, map lock.
{ - `dossier.ts`: the five About files.
files: ['**/*.{ts,tsx}'], - `projects.ts`: projects, statuses, node-map positions, featured nodes.
extends: [ - `public/profile/Mono.png`: profile picture (the favicon is a copy at `src/app/icon.png`). Replace the file, or point `profile.pfp` at a new one.
// Other configs... - `public/terrain.svg`: hero terrain contours (Dajt elevation data).
// Enable lint rules for React - `src/app/globals.css`: colour tokens, bracket/grid utilities, and all motion.
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM ## Rules worth keeping
reactDom.configs.recommended,
], - Green (`accent`) means active or detected: hover, focus, current page, live telemetry, the lock. Nothing at rest is green.
languageOptions: { - The map shows Tirana city centre only. Never add anything finer than city scale.
parserOptions: { - All motion sits behind `prefers-reduced-motion: no-preference`. Text renders without JS.
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
-23
View File
@@ -1,23 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
+18
View File
@@ -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;
-13
View File
@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Selimaj.dev | Klesti Selimaj</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+10
View File
@@ -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;
+5233 -8452
View File
File diff suppressed because it is too large Load Diff
+21 -76
View File
@@ -1,84 +1,29 @@
{ {
"name": "selimaj-portfolio", "name": "selimaj-dev",
"version": "1.0.0", "version": "0.1.0",
"type": "module", "private": true,
"license": "MIT",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "next dev",
"build": "vite build", "build": "next build",
"preview": "vite preview --host", "start": "next start",
"check": "tsc --noEmit", "lint": "eslint"
"format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.2.2", "next": "16.3.8",
"@radix-ui/react-accordion": "^1.2.12", "react": "19.2.8",
"@radix-ui/react-alert-dialog": "^1.1.15", "react-dom": "19.2.8"
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.22",
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"nanoid": "^5.1.5",
"next-themes": "^0.4.6",
"react": "^19.2.1",
"react-day-picker": "^9.11.1",
"react-dom": "^19.2.1",
"react-hook-form": "^7.64.0",
"react-resizable-panels": "^3.0.6",
"recharts": "^2.15.2",
"sonner": "^2.0.7",
"streamdown": "^1.4.0",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"wouter": "^3.3.5",
"zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/postcss": "^4",
"@tailwindcss/vite": "^4.2.1", "@types/node": "^20",
"@types/node": "^24.7.0", "@types/react": "^19",
"@types/react": "^19.2.1", "@types/react-dom": "^19",
"@types/react-dom": "^19.2.1", "eslint": "^9",
"@vitejs/plugin-react": "^5.0.4", "eslint-config-next": "16.3.8",
"autoprefixer": "^10.4.20", "tailwindcss": "^4",
"pnpm": "^10.15.1", "typescript": "^5"
"postcss": "^8.4.47",
"prettier": "^3.6.2",
"tailwindcss": "^4.2.1",
"tsx": "^4.19.1",
"tw-animate-css": "^1.4.0",
"typescript": "5.6.3",
"vite": "^7.1.7",
"vitest": "^2.1.4"
}, },
"packageManager": "[email protected]" "engines": {
"node": "22.x"
}
} }
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.8 KiB

-11
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.8 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 304 KiB

-32
View File
@@ -1,32 +0,0 @@
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import NotFound from "@/pages/NotFound";
import { Route, Switch } from "wouter";
import ErrorBoundary from "./components/ErrorBoundary";
import { ThemeProvider } from "./contexts/ThemeContext";
import Home from "./pages/Home";
function Router() {
return (
<Switch>
<Route path={"/"} component={Home} />
<Route path={"/404"} component={NotFound} />
<Route component={NotFound} />
</Switch>
);
}
function App() {
return (
<ErrorBoundary>
<ThemeProvider defaultTheme="dark">
<TooltipProvider>
<Toaster />
<Router />
</TooltipProvider>
</ThemeProvider>
</ErrorBoundary>
);
}
export default App;
+131
View File
@@ -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>
);
}
+68
View File
@@ -0,0 +1,68 @@
import type { Metadata } from "next";
import Link from "next/link";
import { dossier } from "@/content/dossier";
import { 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">DOMAIN</dt>
<dd>selimaj.dev</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>
);
}
+342
View File
@@ -0,0 +1,342 @@
@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;
}
.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 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);
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+41
View File
@@ -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. 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>
);
}
+16
View File
@@ -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>
);
}
+29
View File
@@ -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>
</>
);
}
+23
View File
@@ -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 I’ve built and conceived, from Enclave and Osias 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 selimaj.dev · {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&apos;ve built and conceived. Concepts are labelled as concepts.
</p>
<ProjectIndex />
</div>
</main>
);
}
+11
View File
@@ -0,0 +1,11 @@
"use client";
import { usePathname } from "next/navigation";
// Re-mounts on every navigation, so each page resolves in from wireframe.
// Home is skipped: the hero runs its own sequence and the terrain stays still.
export default function Template({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
if (pathname === "/") return <>{children}</>;
return <div className="page-resolve seq">{children}</div>;
}
-198
View File
@@ -1,198 +0,0 @@
/*
* ABOUT SECTION — selimaj.dev
* Design: Glassmorphic Terminal
* Asymmetric layout: text left, abstract visual right
*/
import { EarthIcon, LayersIcon, TargetIcon, ZapIcon } from "lucide-react";
import { useEffect, useRef } from "react";
const ABOUT_VISUAL =
"/logo.png";
function useReveal() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.classList.add("visible");
observer.disconnect();
}
},
{ threshold: 0.15 },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
return ref;
}
const highlights = [
{ Icon: ZapIcon, label: "Currently", value: "Grinding on SaaS products" },
{ Icon: LayersIcon, label: "Fav Stack", value: "Rust · TypeScript · Linux" },
{ Icon: TargetIcon, label: "Focus", value: "Low-level systems & UI/UX" },
{ Icon: EarthIcon, label: "Based in", value: "Albania" },
];
export default function AboutSection() {
const ref1 = useReveal();
const ref2 = useReveal();
return (
<section
id="about"
className="py-32 relative"
style={{ background: "#080808" }}
>
{/* Subtle top border */}
<div className="absolute top-0 left-0 right-0 h-px bg-linear-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-6xl mx-auto px-6">
{/* Section label */}
<div className="terminal-label mb-4">&gt; about.me</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
{/* Left: Text */}
<div ref={ref1} className="reveal">
<h2 className="section-heading text-4xl sm:text-5xl text-white mb-8 leading-tight">
Building the future, <span className="text-white/40">one commit at a time.</span>
</h2>
<div
className="space-y-4 text-white/60 leading-relaxed"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
<p>
I'm{" "}
<strong className="text-white font-semibold">
Klesti Selimaj
</strong>{" "}
— a 16-year-old software developer and entrepreneur from Albania
with 6 years of hands-on experience. I started coding at 10 and
never stopped.
</p>
<p>
My passion lies in{" "}
<strong className="text-white/80">
low-level systems programming
</strong>
, building developer tools, and crafting polished user
experiences. I work primarily in{" "}
<strong className="text-white/80">Rust</strong> and{" "}
<strong className="text-white/80">TypeScript</strong>, and I'm
deeply interested in hacking, Linux internals, and TUI
applications.
</p>
<p>
Beyond code, I'm building towards a successful SaaS product —
combining my technical depth with an entrepreneurial mindset to
create tools that developers actually want to use.
</p>
</div>
{/* Highlights grid */}
<div className="mt-10 grid grid-cols-2 gap-3">
{highlights.map((h) => (
<div key={h.label} className="glass rounded-xl p-4 card-hover">
<div className="flex items-center gap-3">
<div className="text-lg">{<h.Icon />}</div>
<div>
<div className="terminal-label text-xs mb-0.5">
{h.label}
</div>
<div
className="text-sm text-white/80 font-medium"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
{h.value}
</div>
</div>
</div>
</div>
))}
</div>
{/* Social links */}
<div className="mt-8 flex flex-wrap gap-3">
{[
{
label: "GitHub",
href: "https://github.com/selimaj-dev",
icon: "⌥",
},
{
label: "YouTube",
href: "https://youtube.com/@SelimajDev",
icon: "▶",
},
{
label: "Instagram",
href: "https://instagram.com/selimaj.dev",
icon: "◈",
},
].map((s) => (
<a
key={s.label}
href={s.href}
target="_blank"
rel="noopener noreferrer"
className="skill-chip hover:no-underline"
>
<span className="text-white/40">{s.icon}</span>
{s.label}
</a>
))}
</div>
</div>
{/* Right: Visual */}
<div ref={ref2} className="reveal flex justify-center lg:justify-end">
<div className="relative w-full max-w-md">
{/* Glow behind image */}
<div
className="absolute inset-0 rounded-2xl blur-3xl opacity-20"
style={{
background:
"radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 70%)",
}}
/>
<div className="relative glass rounded-2xl overflow-hidden float-anim">
<img
src={ABOUT_VISUAL}
alt="Abstract tech visualization"
className="w-full h-auto object-cover"
style={{ aspectRatio: "1/1" }}
/>
{/* Overlay with terminal text */}
<div
className="absolute bottom-0 left-0 right-0 p-5"
style={{
background:
"linear-gradient(to top, rgba(8,8,8,0.9) 0%, transparent 100%)",
}}
>
<div className="terminal-label text-xs">
<span className="text-white/30">$</span>{" "}
<span className="text-white/60">whoami</span>
</div>
<div
className="text-white/80 text-sm mt-1"
style={{ fontFamily: "'JetBrains Mono', monospace" }}
>
Klesti Selimaj
</div>
<div className="terminal-label text-xs mt-0.5">
uid=1000(klesti) groups=ORUS,Axiom,Saturn,Nexa
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
+85
View File
@@ -0,0 +1,85 @@
"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");
window.dispatchEvent(new Event("boot:done"));
}, 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">&gt;</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">&gt;</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>
);
}
+91
View File
@@ -0,0 +1,91 @@
"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-square [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>
);
}
-175
View File
@@ -1,175 +0,0 @@
/*
* CONTACT SECTION — selimaj.dev
* Design: Glassmorphic Terminal
* Clean contact links with glass cards
*/
import { useEffect, useRef } from "react";
import { Github, Youtube, Instagram, ExternalLink, Mail, MailIcon } from "lucide-react";
function useReveal(threshold = 0.1) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.classList.add("visible");
observer.disconnect();
}
},
{ threshold }
);
observer.observe(el);
return () => observer.disconnect();
}, [threshold]);
return ref;
}
const contactLinks = [
{
icon: Github,
label: "GitHub",
handle: "@selimaj-dev",
href: "https://github.com/selimaj-dev",
description: "97+ repositories · 2,670 contributions/year",
},
{
icon: Youtube,
label: "YouTube",
handle: "@SelimajDev",
href: "https://youtube.com/@SelimajDev",
description: "Dev content, tutorials, and builds",
},
{
icon: Instagram,
label: "Instagram",
handle: "@selimaj.dev",
href: "https://instagram.com/selimaj.dev",
description: "Behind the scenes and updates",
},
{
icon: MailIcon,
label: "Email",
handle: "[email protected]",
href: "mailto:[email protected]",
description: "For business inquiries, collaborations, and contact",
},
];
export default function ContactSection() {
const headerRef = useReveal();
const contentRef = useReveal(0.1);
return (
<section id="contact" className="py-32 relative" style={{ background: "#080808" }}>
<div className="absolute top-0 left-0 right-0 h-px bg-linear-to-r from-transparent via-white/10 to-transparent" />
{/* Subtle radial glow */}
<div
className="absolute inset-0 pointer-events-none"
style={{
background: "radial-gradient(ellipse 60% 40% at 50% 100%, rgba(255,255,255,0.03) 0%, transparent 70%)",
}}
/>
<div className="max-w-6xl mx-auto px-6">
{/* Header */}
<div ref={headerRef} className="reveal mb-16 text-center">
<div className="terminal-label mb-4 text-center">&gt; contact --open</div>
<h2 className="section-heading text-4xl sm:text-5xl text-white leading-tight mb-6">
Let's Build
<br />
<span className="text-white/40">Something Together</span>
</h2>
<p
className="text-white/50 max-w-xl mx-auto text-base leading-relaxed"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
I'm always open to interesting projects, collaborations, or just a good conversation
about tech. Reach out through any of the channels below.
</p>
</div>
{/* Contact cards */}
<div ref={contentRef} className="reveal">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-12">
{contactLinks.map((link) => {
const Icon = link.icon;
return (
<a
key={link.label}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="glass rounded-2xl p-6 card-hover group no-underline flex flex-col gap-4"
>
<div className="flex items-center justify-between">
<div className="w-10 h-10 glass-strong rounded-xl flex items-center justify-center">
<Icon size={18} className="text-white/70 group-hover:text-white transition-colors" />
</div>
<ExternalLink size={12} className="text-white/20 group-hover:text-white/50 transition-colors" />
</div>
<div>
<div
className="text-white font-semibold text-sm mb-0.5"
style={{ fontFamily: "'Syne', sans-serif" }}
>
{link.label}
</div>
<div className="terminal-label text-xs mb-2">{link.handle}</div>
<div
className="text-white/40 text-xs leading-relaxed"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
{link.description}
</div>
</div>
</a>
);
})}
</div>
{/* Big CTA */}
<div className="glass rounded-2xl p-10 text-center">
<div className="terminal-label mb-4">&gt; open_to_work --status</div>
<h3
className="text-2xl sm:text-3xl font-bold text-white mb-4"
style={{ fontFamily: "'Syne', sans-serif" }}
>
Open to freelance &amp; collaboration
</h3>
<p
className="text-white/50 mb-8 max-w-lg mx-auto text-sm leading-relaxed"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
Looking for interesting projects, SaaS co-founders, or clients who need
a developer who ships fast and cares about quality.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<a
href="https://github.com/selimaj-dev"
target="_blank"
rel="noopener noreferrer"
className="px-8 py-3.5 bg-white text-black font-semibold rounded-xl hover:bg-white/90 transition-all duration-200 text-sm no-underline"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
View GitHub Profile
</a>
<a
href="https://instagram.com/selimaj.dev"
target="_blank"
rel="noopener noreferrer"
className="px-8 py-3.5 glass border border-white/10 text-white/70 font-medium rounded-xl hover:text-white hover:border-white/20 transition-all duration-200 text-sm no-underline"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
DM on Instagram
</a>
</div>
</div>
</div>
</div>
</section>
);
}
-62
View File
@@ -1,62 +0,0 @@
import { cn } from "@/lib/utils";
import { AlertTriangle, RotateCcw } from "lucide-react";
import { Component, type ReactNode } from "react";
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return (
<div className="flex items-center justify-center min-h-screen p-8 bg-background">
<div className="flex flex-col items-center w-full max-w-2xl p-8">
<AlertTriangle
size={48}
className="text-destructive mb-6 shrink-0"
/>
<h2 className="text-xl mb-4">An unexpected error occurred.</h2>
<div className="p-4 w-full rounded bg-muted overflow-auto mb-6">
<pre className="text-sm text-muted-foreground whitespace-break-spaces">
{this.state.error?.stack}
</pre>
</div>
<button
onClick={() => window.location.reload()}
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-lg",
"bg-primary text-primary-foreground",
"hover:opacity-90 cursor-pointer"
)}
>
<RotateCcw size={16} />
Reload Page
</button>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
+105
View File
@@ -0,0 +1,105 @@
"use client";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { brandOf, featured, featuredRoot, projectById, projects, root } from "@/content/projects";
import { Segment } from "./Segment";
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.map((n) => (
<Segment
key={`${n.id}-${active === n.id}`}
x1={(featuredRoot.x / 100) * size.w}
y1={(featuredRoot.y / 100) * size.h}
x2={(n.x / 100) * size.w}
y2={(n.y / 100) * size.h}
on={active === n.id}
dashed
strong
/>
))}
</div>
)}
{/* Root: the site itself, labelled to the left so no line crosses it. */}
<div
className="relative min-h-12 md:absolute md:top-(--y) md:left-(--x) md:min-h-0"
style={{ "--x": `${featuredRoot.x}%`, "--y": `${featuredRoot.y}%` } as React.CSSProperties}
>
<span
aria-hidden="true"
className="absolute top-2.5 left-0 box-border size-7 rounded-full border border-ink/70 bg-night md:-top-3.5 md:-left-3.5"
>
<span className="absolute top-[9px] left-[9px] size-2 rounded-full bg-ink" />
</span>
<span className="ml-10 block px-3 py-2.5 md:absolute md:-top-[22px] md:right-6 md:ml-0 md:w-max md:text-right">
<span className="flex items-baseline gap-3 md:flex-row-reverse">
<span className="font-display text-lg font-semibold tracking-[0.08em] uppercase">{root.name}</span>
<span className="text-[9px] tracking-[0.22em] text-mute uppercase">Root</span>
</span>
</span>
</div>
{nodes.map((n) => {
const on = active === n.id;
const brand = brandOf(n.project);
const meta = n.id === "orus" ? "BRAND" : brand ? `${n.project.status} · ${brand.toUpperCase()}` : n.project.status;
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 top-4 left-2 box-border size-3 border md:-top-1.5 md:-left-1.5 ${n.id === "orus" ? "rotate-45" : ""} ${
on ? "border-accent bg-accent shadow-[0_0_14px_var(--color-accent)]" : "border-ink/60 bg-night"
}`}
/>
<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:left-6 md:ml-0 md:w-[300px] ${
on ? "[--bc:var(--color-accent)]" : "[--bc:transparent]"
}`}
>
<span className="flex items-baseline gap-3">
<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">{meta}</span>
</span>
<span className={`mt-1.5 block text-[11px] leading-[1.55] text-ink-2 ${on ? "" : "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 uppercase md:block">
Root {root.sub} · {featured.length} of {projects.length} nodes
</div>
</div>
);
}
-59
View File
@@ -1,59 +0,0 @@
/*
* FOOTER — selimaj.dev
* Design: Glassmorphic Terminal
* Minimal footer with branding and links
*/
export default function Footer() {
const year = new Date().getFullYear();
return (
<footer
className="py-10 relative"
style={{
background: "#080808",
borderTop: "1px solid rgba(255,255,255,0.06)",
}}
>
<div className="max-w-6xl mx-auto px-6">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
{/* Brand */}
<div className="flex items-center gap-3">
<span
className="text-white font-bold text-base tracking-tight"
style={{ fontFamily: "'Syne', sans-serif" }}
>
selimaj<span className="text-white/30">.dev</span>
</span>
<span className="text-white/20 text-xs">·</span>
<span className="terminal-label text-xs">Klesti Selimaj</span>
</div>
{/* Links */}
<div className="flex items-center gap-6">
{[
{ label: "GitHub", href: "https://github.com/selimaj-dev" },
{ label: "YouTube", href: "https://youtube.com/@SelimajDev" },
{ label: "Instagram", href: "https://instagram.com/selimaj.dev" },
].map((link) => (
<a
key={link.label}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="terminal-label text-xs hover:text-white/70 transition-colors"
>
{link.label}
</a>
))}
</div>
{/* Copyright */}
<div className="terminal-label text-xs">
© {year} selimaj.dev
</div>
</div>
</div>
</footer>
);
}
+232
View File
@@ -0,0 +1,232 @@
import Link from "next/link";
import {
dossierFields,
lock,
profile,
type DossierField,
} from "@/content/profile";
import { Bust } from "./Bust";
import { Scramble } from "./Scramble";
import { LocalTime, SessionStamp } from "./Telemetry";
import { TerrainLayer } from "./TerrainLayer";
import { LockReticle, TiranaMap } from "./TiranaMap";
const d = (s: number) => ({ "--d": `${s}s` }) as React.CSSProperties;
/** Dossier values decode one after another, starting once the card has built. */
const DOSSIER_START = 1.35;
const DOSSIER_STAGGER = 0.14;
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, i) => {
const delay = DOSSIER_START + i * DOSSIER_STAGGER;
return (
<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, j) => (
<a
key={l.href}
href={l.href}
className="border-b border-white/25 hover:border-accent"
>
<Scramble
text={l.label}
delay={delay + j * 0.2}
step={30}
/>
</a>
))
) : (
<Scramble text={f.value} delay={delay} step={30} />
)}
</dd>
</div>
);
})}
</dl>
);
}
export function Hero() {
return (
<section
aria-labelledby="hero-title"
className="relative overflow-hidden border-b border-line xl:flex xl:min-h-dvh xl:items-center"
>
<TerrainLayer />
<div className="relative z-10 mx-auto w-full 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="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]">
<div className="text-accent">
<Scramble text="LOCK ACQUIRED" delay={0.85} step={35} nowrap />
</div>
<div>
<Scramble
text={`${lock.lat} · ${lock.lon}`}
delay={0.95}
step={30}
nowrap
/>
</div>
<div className="uppercase">
<Scramble text={lock.place} delay={1.05} step={30} nowrap />
</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="mb-3 text-[9px] tracking-[0.28em] text-mute xl:mb-5 xl:text-[10px] xl:tracking-[0.3em]">
<Scramble
text="TARGET RESOLVED · SUBJECT 0001"
delay={0.2}
step={15}
/>
</p>
<h1
id="hero-title"
className="font-display text-[46px] leading-[0.96] font-semibold uppercase sm:text-[64px] xl:text-[84px] xl:leading-[0.94] xl:tracking-[0.01em]"
>
<Scramble text="ENTREPRENEUR" delay={0.3} step={35} nowrap />
<br />
<Scramble text="& BUILDER" delay={0.5} step={35} nowrap />
</h1>
{/*<p className="mt-4 text-[15px] leading-normal text-ink-2 xl:mt-6 xl:text-[17px]">
<Scramble text={profile.stance} delay={0.7} step={15} />
</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)]"
>
ABOUT ME <span aria-hidden="true">↗</span>
</Link>
</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"
>
<Scramble
text={profile.displayName.toUpperCase()}
delay={1.2}
step={90}
nowrap
/>
</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>
</div>
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 hidden xl:block">
<div className="relative mx-auto h-20 max-w-[1440px]">
{/* Corner telemetry, pinned to the bottom of the screen-tall hero */}
<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>
</div>
</section>
);
}
-167
View File
@@ -1,167 +0,0 @@
/*
* HERO SECTION — selimaj.dev
* Design: Glassmorphic Terminal
* Full-viewport dark hero with animated background image, typewriter effect, and glass CTA
*/
import { useEffect, useState } from "react";
const HERO_BG = "https://d2xsxph8kpxj0f.cloudfront.net/310519663439135967/RkRL4wd7YzNoLyaN7suWbR/hero-bg-G3393XyTzqxu3D4dR63od8.webp";
const typewriterPhrases = [
"Software Developer.",
"Entrepreneur.",
"SaaS Builder.",
"Systems Programmer.",
"Open Source Contributor.",
];
function useTypewriter(phrases: string[], speed = 60, pause = 1800) {
const [display, setDisplay] = useState("");
const [phraseIdx, setPhraseIdx] = useState(0);
const [charIdx, setCharIdx] = useState(0);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
const current = phrases[phraseIdx];
let timeout: ReturnType<typeof setTimeout>;
if (!deleting && charIdx < current.length) {
timeout = setTimeout(() => setCharIdx((c) => c + 1), speed);
} else if (!deleting && charIdx === current.length) {
timeout = setTimeout(() => setDeleting(true), pause);
} else if (deleting && charIdx > 0) {
timeout = setTimeout(() => setCharIdx((c) => c - 1), speed / 2);
} else if (deleting && charIdx === 0) {
setDeleting(false);
setPhraseIdx((i) => (i + 1) % phrases.length);
}
setDisplay(current.slice(0, charIdx));
return () => clearTimeout(timeout);
}, [charIdx, deleting, phraseIdx, phrases, speed, pause]);
return display;
}
export default function HeroSection() {
const typed = useTypewriter(typewriterPhrases);
return (
<section
id="hero"
className="relative min-h-screen flex items-center justify-center overflow-hidden"
style={{ background: "#080808" }}
>
{/* Background image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url(${HERO_BG})`,
opacity: 0.55,
}}
/>
{/* Gradient overlays */}
<div className="absolute inset-0 bg-linear-to-b from-transparent via-transparent to-[#080808]" />
<div className="absolute inset-0 bg-linear-to-r from-[#080808]/60 via-transparent to-[#080808]/60" />
{/* Animated glow orb */}
<div
className="absolute top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2 w-150 h-150 rounded-full pointer-events-none"
style={{
background: "radial-gradient(circle, rgba(255,255,255,0.04) 0%, transparent 70%)",
animation: "float 6s ease-in-out infinite",
}}
/>
{/* Content */}
<div className="relative z-10 max-w-5xl mx-auto px-6 text-center">
{/* Terminal label */}
<div className="inline-flex items-center gap-2 mb-8 px-4 py-2 glass rounded-full">
<span className="w-2 h-2 rounded-full bg-white/60 inline-block" />
<span className="terminal-label text-xs">
&gt; klesti@selimaj.dev ~ $
</span>
</div>
{/* Main heading */}
<h1
className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-bold text-white mb-6 leading-none tracking-tight fade-in-up"
style={{ fontFamily: "'Syne', sans-serif" }}
>
Klesti
<br />
<span className="text-white/30">Selimaj</span>
</h1>
{/* Typewriter subtitle */}
<div
className="text-xl sm:text-2xl md:text-3xl font-medium text-white/70 mb-4 h-10 fade-in-up delay-200"
style={{ fontFamily: "'Syne', sans-serif" }}
>
{typed}
<span className="cursor-blink text-white/50 ml-0.5">|</span>
</div>
{/* Description */}
<p
className="text-base sm:text-lg text-white/45 max-w-2xl mx-auto mb-12 leading-relaxed fade-in-up delay-400"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
16 years old · 6 years of experience · Albania
<br />
Passionate about low-level systems, hacking, UI/UX, and making cool tools.
</p>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 fade-in-up delay-500">
<button
onClick={() => {
document.querySelector("#projects")?.scrollIntoView({ behavior: "smooth" });
}}
className="px-8 py-3.5 bg-white text-black font-semibold rounded-xl hover:bg-white/90 transition-all duration-200 text-sm"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
View Projects
</button>
<button
onClick={() => {
document.querySelector("#contact")?.scrollIntoView({ behavior: "smooth" });
}}
className="px-8 py-3.5 glass text-white/80 font-medium rounded-xl hover:text-white hover:border-white/20 transition-all duration-200 text-sm border border-white/10"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
Get in Touch
</button>
</div>
{/* Stats row */}
<div className="mt-20 flex flex-row flex-wrap items-center justify-center gap-6 sm:gap-12 fade-in-up delay-700">
{[
{ value: "6+", label: "Years Experience" },
{ value: "97+", label: "Repositories" },
{ value: "2,670+", label: "Contributions / Year" },
{ value: "#2", label: "National GitHub user" },
].map((stat) => (
<div key={stat.label} className="text-center">
<div
className="text-3xl font-bold text-white"
style={{ fontFamily: "'Syne', sans-serif" }}
>
{stat.value}
</div>
<div className="terminal-label mt-1">{stat.label}</div>
</div>
))}
</div>
</div>
{/* Scroll indicator */}
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 opacity-0 fade-in-up delay-800" style={{animationFillMode:'forwards'}}>
<div className="w-px h-8 bg-linear-to-b from-white/30 to-transparent" />
<span className="terminal-label text-xs">scroll</span>
</div>
</section>
);
}
-124
View File
@@ -1,124 +0,0 @@
/*
* NAVBAR — selimaj.dev
* Design: Glassmorphic Terminal
* Glass effect: backdrop-blur + rgba background
* Becomes more opaque on scroll
*/
import { useEffect, useState } from "react";
const navLinks = [
{ label: "About", href: "#about" },
{ label: "Projects", href: "#projects" },
{ label: "Skills", href: "#skills" },
{ label: "Contact", href: "#contact" },
];
export default function Navbar() {
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 40);
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const handleNavClick = (href: string) => {
setMenuOpen(false);
const el = document.querySelector(href);
if (el) {
el.scrollIntoView({ behavior: "smooth" });
}
};
return (
<nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${
scrolled ? "navbar-glass-solid" : "navbar-glass"
}`}
>
<div className="max-w-6xl mx-auto px-6 flex items-center justify-between h-16">
{/* Logo */}
<a
href="#"
onClick={(e) => { e.preventDefault(); window.scrollTo({ top: 0, behavior: "smooth" }); }}
className="flex items-center gap-2 group"
>
<span
className="text-white font-bold text-lg tracking-tight"
style={{ fontFamily: "'Syne', sans-serif" }}
>
selimaj<span className="text-white/40">.dev</span>
</span>
</a>
{/* Desktop nav links */}
<div className="hidden md:flex items-center gap-1">
{navLinks.map((link) => (
<button
key={link.href}
onClick={() => handleNavClick(link.href)}
className="px-4 py-2 text-sm text-white/60 hover:text-white transition-colors duration-200 rounded-lg hover:bg-white/5"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
{link.label}
</button>
))}
<a
href="https://github.com/selimaj-dev"
target="_blank"
rel="noopener noreferrer"
className="ml-3 px-4 py-2 text-sm font-medium text-black bg-white rounded-lg hover:bg-white/90 transition-all duration-200"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
GitHub
</a>
</div>
{/* Mobile hamburger */}
<button
className="md:hidden flex flex-col gap-1.5 p-2"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
>
<span className={`block w-5 h-0.5 bg-white/70 transition-all duration-300 ${menuOpen ? "rotate-45 translate-y-2" : ""}`} />
<span className={`block w-5 h-0.5 bg-white/70 transition-all duration-300 ${menuOpen ? "opacity-0" : ""}`} />
<span className={`block w-5 h-0.5 bg-white/70 transition-all duration-300 ${menuOpen ? "-rotate-45 -translate-y-2" : ""}`} />
</button>
</div>
{/* Mobile menu */}
<div
className={`md:hidden overflow-hidden transition-all duration-300 ${
menuOpen ? "max-h-64 opacity-100" : "max-h-0 opacity-0"
}`}
style={{ background: "rgba(8,8,8,0.95)", borderTop: "1px solid rgba(255,255,255,0.06)" }}
>
<div className="px-6 py-4 flex flex-col gap-1">
{navLinks.map((link) => (
<button
key={link.href}
onClick={() => handleNavClick(link.href)}
className="text-left px-3 py-3 text-sm text-white/60 hover:text-white transition-colors rounded-lg hover:bg-white/5"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
{link.label}
</button>
))}
<a
href="https://github.com/selimaj-dev"
target="_blank"
rel="noopener noreferrer"
className="mt-2 px-3 py-3 text-sm font-medium text-black bg-white rounded-lg text-center hover:bg-white/90 transition-all"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
GitHub
</a>
</div>
</div>
</nav>
);
}
+282
View File
@@ -0,0 +1,282 @@
"use client";
import { useEffect, useState } from "react";
import { brandOf, groups, projects, root, type GroupId, type Project } from "@/content/projects";
import { Segment } from "./Segment";
import { StatusTag } from "./StatusTag";
const W = 1200;
const H = 720;
const groupIds = Object.keys(groups) as GroupId[];
const isHub = (p: Project) => groups[p.group].node === p.id;
function Marker({ p, on }: { p: Project; on: boolean }) {
const hub = isHub(p);
const size = hub ? 22 : 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 ${hub ? "rotate-45" : ""} ${
on ? "bg-accent shadow-[0_0_12px_var(--color-accent)]" : `${rest} bg-surface`
}`}
style={{ width: size, height: size }}
/>
);
}
function ProjectCard({ p }: { p: Project }) {
const brand = brandOf(p);
const body = (
<>
<span className="flex items-center justify-between gap-3">
<span className="flex items-center gap-3">
<StatusTag status={p.status} />
{brand && <span className="text-[9px] tracking-[0.22em] text-mute uppercase">{brand}</span>}
</span>
<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 | null>(null);
// 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) ?? null;
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 && sel.group === k} strong />
))}
{projects
.filter((p) => !isHub(p))
.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"}
/>
))}
{/* Plain hubs (groups without a project of their own). */}
{groupIds
.filter((k) => !groups[k].node)
.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>
);
})}
{/* Root: the site itself. */}
<div className="absolute flex items-center gap-3 whitespace-nowrap" style={{ left: root.map.x - 15, top: root.map.y - 15 }}>
<span aria-hidden="true" className="relative box-border size-[30px] flex-none rounded-full border border-ink/70 bg-surface">
<span className="absolute top-[10px] left-[10px] size-2 rounded-full bg-ink" />
</span>
<span className="flex flex-col gap-[3px]">
<span className="font-display text-xl font-semibold tracking-[0.08em] uppercase">{root.name}</span>
<span className="text-[8px] tracking-[0.22em] text-mute uppercase">Root · {root.sub}</span>
</span>
</div>
{projects.map((p) => {
const on = active === p.id;
const g = groups[p.group];
const labelLeft = (p.label ?? (p.map.x < g.x ? "l" : "r")) === "l";
const m = isHub(p) ? 22 : 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} />
{/* A hub's label sits a little high so its root line passes underneath. */}
<span className={`flex flex-col gap-[3px] ${isHub(p) ? "-translate-y-4" : ""}`}>
<span
className={`font-display font-semibold tracking-[0.08em] uppercase ${isHub(p) ? "text-base" : "text-[13px]"} ${
p.status === "ARCHIVED" && !on ? "text-ink/70" : "text-ink"
}`}
>
{p.name}
</span>
<span className="text-[8px] tracking-[0.22em] text-mute">{isHub(p) ? "BRAND" : 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] uppercase">
{sel ? <span className="text-accent">TRACKING ▸ {sel.name}</span> : <span className="text-mute/70">AWAITING TARGET</span>}
</div>
</div>
</div>
<section
aria-live="polite"
aria-label="Selected node"
className={`bracket mt-4 grid min-h-[176px] gap-6 border border-white/6 bg-surface px-7 py-6 md:grid-cols-[minmax(0,1fr)_auto] md:items-center md:gap-x-12 ${
sel ? "[--bc:var(--color-accent)]" : "[--bc:rgb(242_244_245/0.3)]"
}`}
>
{sel ? (
<>
<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">
{isHub(sel) ? "Brand" : (brandOf(sel) ?? 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>
)}
</>
) : (
<p className="text-[11px] tracking-[0.2em] text-mute">HOVER OR SELECT A NODE TO TRACK IT.</p>
)}
</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>
</>
);
}
-299
View File
@@ -1,299 +0,0 @@
/*
* PROJECTS SECTION — selimaj.dev
* Design: Glassmorphic Terminal
* Masonry-style grid with glass cards, language badges, and star counts
*/
import { useEffect, useRef } from "react";
import { ExternalLink, Star, GitFork } from "lucide-react";
function useReveal(threshold = 0.1) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.classList.add("visible");
observer.disconnect();
}
},
{ threshold },
);
observer.observe(el);
return () => observer.disconnect();
}, [threshold]);
return ref;
}
const LANG_COLORS: Record<string, string> = {
TypeScript: "#3178c6",
Rust: "#ce422b",
Java: "#b07219",
JavaScript: "#f1e05a",
Python: "#3572A5",
Shell: "#89e051",
"C#": "#178600",
};
interface Project {
name: string;
org?: string;
description: string;
language: string;
stars: number;
forks?: number;
href: string;
featured?: boolean;
tag?: string;
}
const projects: Project[] = [
{
name: "osui",
org: "osui-rs",
description:
"A TUI library written in Rust — build terminal UIs with a component-based approach.",
language: "Rust",
stars: 7,
href: "https://github.com/osui-rs/osui",
featured: true,
tag: "Library",
},
{
name: "saturnclient",
org: "saturnclientmc",
description:
"Saturn Client — a Minecraft client full of customizability and free cosmetics.",
language: "Java",
stars: 7,
forks: 2,
href: "https://github.com/saturnclientmc/saturnclient",
featured: true,
tag: "Game Client",
},
{
name: "dev-run",
org: "orus-dev",
description:
"DevRun — a community-driven competitive programming platform inspired by speedrunning.",
language: "TypeScript",
stars: 3,
href: "https://github.com/orus-dev/dev-run",
featured: true,
tag: "Platform",
},
{
name: "nexa-trade",
org: "nexa-sol",
description:
"The Solana trading terminal supercharged by AI — trade smarter.",
language: "TypeScript",
stars: 1,
href: "https://github.com/nexa-sol/nexa-trade",
featured: true,
tag: "FinTech",
},
{
name: "dockship",
org: "orus-dev",
description:
"A powerful self-hosted deployment platform for managing your applications.",
language: "TypeScript",
stars: 1,
href: "https://github.com/orus-dev/dockship",
tag: "DevOps",
},
{
name: "axiom",
org: "AxiomChat",
description: "A modern real-time chat application built with TypeScript.",
language: "TypeScript",
stars: 1,
href: "https://github.com/AxiomChat/axiom",
tag: "App",
},
{
name: "linear-linux",
description:
"Linux support for linear.app — bringing the popular project management tool to Linux.",
language: "JavaScript",
stars: 27,
forks: 5,
href: "https://github.com/selimaj-dev/linear-linux",
tag: "Tool",
},
{
name: "osmium",
description: "A TUI component-based game engine built on top of OSUI.",
language: "Rust",
stars: 1,
href: "https://github.com/selimaj-dev/osmium",
tag: "Engine",
},
{
name: "spark",
description:
"AI documentation generator that takes context from your repository's source code.",
language: "Python",
stars: 2,
href: "https://github.com/selimaj-dev/spark",
tag: "AI Tool",
},
{
name: "flux",
description:
"A tool and library for using multiple languages in one codebase seamlessly.",
language: "Rust",
stars: 1,
href: "https://github.com/selimaj-dev/flux",
tag: "Tool",
},
{
name: "session-rs",
description: "A lightweight async WebSocket protocol library for Rust.",
language: "Rust",
stars: 1,
href: "https://github.com/selimaj-dev/session-rs",
tag: "Library",
},
{
name: "tailwind-motlib",
description: "A motion and stagger animation library for Tailwind CSS.",
language: "TypeScript",
stars: 0,
href: "https://github.com/selimaj-dev/tailwind-motlib",
tag: "Library",
},
];
function ProjectCard({ project, delay }: { project: Project; delay: number }) {
const langColor = LANG_COLORS[project.language] || "#888";
return (
<a
href={project.href}
target="_blank"
rel="noopener noreferrer"
className="block glass rounded-2xl p-6 card-hover group no-underline"
style={{ animationDelay: `${delay}ms` }}
>
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1 min-w-0">
{project.tag && (
<span className="terminal-label text-xs inline-block mb-2 px-2 py-0.5 rounded-full border border-white/10 bg-white/5">
{project.tag}
</span>
)}
<h3
className="text-white font-semibold text-base leading-tight truncate"
style={{ fontFamily: "'Syne', sans-serif" }}
>
{project.org ? (
<span className="text-white/40">{project.org}/</span>
) : null}
{project.name}
</h3>
</div>
<ExternalLink
size={14}
className="text-white/20 group-hover:text-white/60 transition-colors ml-3 mt-1 shrink-0"
/>
</div>
{/* Description */}
<p
className="text-white/50 text-sm leading-relaxed mb-5 line-clamp-2"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
{project.description}
</p>
{/* Footer */}
<div className="flex items-center gap-4">
<span className="lang-badge">
<span className="lang-dot" style={{ backgroundColor: langColor }} />
{project.language}
</span>
<span className="lang-badge">
<Star size={11} className="text-white/40" />
{project.stars}
</span>
{project.forks !== undefined && (
<span className="lang-badge">
<GitFork size={11} className="text-white/40" />
{project.forks}
</span>
)}
</div>
</a>
);
}
export default function ProjectsSection() {
const headerRef = useReveal();
const gridRef = useReveal(0.05);
const featured = projects.filter((p) => p.featured);
const rest = projects.filter((p) => !p.featured);
return (
<section
id="projects"
className="py-32 relative"
style={{ background: "#080808" }}
>
<div className="absolute top-0 left-0 right-0 h-px bg-linear-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-6xl mx-auto px-6">
{/* Header */}
<div ref={headerRef} className="reveal mb-16">
<div className="terminal-label mb-4">&gt; ls projects/</div>
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4">
<h2 className="section-heading text-4xl sm:text-5xl text-white leading-tight">
Things I've <span className="text-white/40">Built</span>
</h2>
<a
href="https://github.com/selimaj-dev?tab=repositories"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-white/40 hover:text-white transition-colors terminal-label"
>
View all 35+ repos →
</a>
</div>
</div>
{/* Featured projects (larger cards) */}
<div ref={gridRef} className="reveal">
<div className="terminal-label mb-4 text-xs">// featured</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
{featured.map((project, i) => (
<ProjectCard
key={project.name}
project={project}
delay={i * 80}
/>
))}
</div>
{/* Rest of projects (smaller grid) */}
<div className="terminal-label mb-4 text-xs mt-8">
// more projects
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
{rest.map((project, i) => (
<ProjectCard
key={project.name}
project={project}
delay={i * 60}
/>
))}
</div>
</div>
</div>
</section>
);
}
+110
View File
@@ -0,0 +1,110 @@
"use client";
import { useEffect, useState } from "react";
const UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&@$*+=<>/";
const LOWER = "abcdefghijklmnopqrstuvwxyz0123456789#%&@$*+=<>/";
const DIGIT = "0123456789#%&@$*";
/** Characters that stay put while everything around them is brute-forced. */
const FIXED = /[\s.,·()\-/]/;
const FRAME_MS = 45;
function noise(ch: string) {
if (FIXED.test(ch)) return ch;
const pool = /[0-9]/.test(ch) ? DIGIT : ch === ch.toLowerCase() && ch !== ch.toUpperCase() ? LOWER : UPPER;
return pool[Math.floor(Math.random() * pool.length)];
}
type Frame = { done: string; head: string; rest: string };
/**
* Text that brute-forces itself: every character cycles through random
* glyphs, then the real value locks in from left to right.
*
* Server and no-JS output is the plain text. With reduced motion it never
* animates. While the boot screen is up it waits for `boot:done`.
*/
export function Scramble({
text,
delay = 0,
step = 45,
nowrap = false,
}: {
text: string;
/** Seconds before characters start locking in (noise runs from the start). */
delay?: number;
/** Milliseconds per character as the value locks in. */
step?: number;
nowrap?: boolean;
}) {
const [frame, setFrame] = useState<Frame | null>(null);
useEffect(() => {
if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const chars = Array.from(text);
let raf = 0;
let start = 0;
let last = -Infinity;
const build = (locked: number): Frame => ({
done: chars.slice(0, locked).join(""),
head: locked < chars.length ? chars[locked] : "",
rest: chars
.slice(locked + 1)
.map(noise)
.join(""),
});
const loop = (t: number) => {
if (!start) start = t;
const elapsed = t - start - delay * 1000;
const locked = elapsed < 0 ? 0 : Math.floor(elapsed / step);
if (locked >= chars.length) {
setFrame(null);
return;
}
if (t - last >= FRAME_MS) {
last = t;
// Before the reveal starts, the first character is noise too.
setFrame(elapsed < 0 ? { done: "", head: noise(chars[0]), rest: build(0).rest } : build(locked));
}
raf = requestAnimationFrame(loop);
};
const begin = () => {
raf = requestAnimationFrame(loop);
};
if (document.documentElement.hasAttribute("data-boot")) {
// Hide the real value under the boot screen, then start when it lifts.
raf = requestAnimationFrame(() => setFrame({ done: "", head: "", rest: chars.map(noise).join("") }));
window.addEventListener("boot:done", begin, { once: true });
} else {
begin();
}
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("boot:done", begin);
};
}, [text, delay, step]);
if (!frame) return <>{text}</>;
return (
<span className="relative inline-block max-w-full align-top">
<span className="sr-only">{text}</span>
{/* Invisible copy keeps the box exactly the size of the real text. */}
<span aria-hidden="true" className={`invisible ${nowrap ? "whitespace-nowrap" : ""}`}>
{text}
</span>
<span aria-hidden="true" className={`absolute inset-0 ${nowrap ? "whitespace-nowrap" : ""}`}>
{frame.done}
<span className="text-accent">{frame.head}</span>
<span className="opacity-50">{frame.rest}</span>
</span>
</span>
);
}
+35
View File
@@ -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,
}}
/>
);
}
+18
View File
@@ -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>SELIMAJ.DEV · TIRANA, AL</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>
);
}
+98
View File
@@ -0,0 +1,98 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
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="selimaj.dev home"
>
<span className="font-display text-sm font-semibold tracking-[0.24em]">
SELIMAJ<span className="text-mute">.DEV</span>
</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>
);
}
-248
View File
@@ -1,248 +0,0 @@
/*
* SKILLS SECTION — selimaj.dev
* Design: Glassmorphic Terminal
* Flowing chip layout with category grouping
*/
import {
CpuIcon,
PaletteIcon,
RocketIcon,
ShieldCheckIcon,
TerminalIcon,
ZapIcon,
} from "lucide-react";
import { useEffect, useRef } from "react";
function useReveal(threshold = 0.1) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.classList.add("visible");
observer.disconnect();
}
},
{ threshold },
);
observer.observe(el);
return () => observer.disconnect();
}, [threshold]);
return ref;
}
const CODE_VISUAL =
"https://d2xsxph8kpxj0f.cloudfront.net/310519663439135967/RkRL4wd7YzNoLyaN7suWbR/code-visual-96fdFEYLcFy2ct3UAAmiG6.webp";
interface SkillCategory {
label: string;
terminalLabel: string;
skills: { name: string; level?: "expert" | "proficient" | "learning" }[];
}
const skillCategories: SkillCategory[] = [
{
label: "Languages",
terminalLabel: "languages/",
skills: [
{ name: "Rust", level: "expert" },
{ name: "TypeScript", level: "expert" },
{ name: "JavaScript", level: "expert" },
{ name: "Java", level: "proficient" },
{ name: "Python", level: "proficient" },
{ name: "C#", level: "proficient" },
{ name: "Shell / Bash", level: "proficient" },
{ name: "Go", level: "learning" },
],
},
{
label: "Frontend",
terminalLabel: "frontend/",
skills: [
{ name: "React", level: "expert" },
{ name: "Next.js", level: "expert" },
{ name: "Tailwind CSS", level: "expert" },
{ name: "Framer Motion", level: "proficient" },
{ name: "WebSockets", level: "expert" },
{ name: "Vite", level: "expert" },
],
},
{
label: "Backend & Systems",
terminalLabel: "backend/",
skills: [
{ name: "Node.js", level: "expert" },
{ name: "Express", level: "expert" },
{ name: "Linux", level: "expert" },
{ name: "Docker", level: "proficient" },
{ name: "TUI / CLI", level: "expert" },
{ name: "Low-level Systems", level: "expert" },
{ name: "WebAssembly", level: "proficient" },
],
},
{
label: "Tools & Platforms",
terminalLabel: "tools/",
skills: [
{ name: "Git / GitHub", level: "expert" },
{ name: "Kali Linux", level: "proficient" },
{ name: "Neovim", level: "proficient" },
{ name: "PostgreSQL", level: "proficient" },
{ name: "Redis", level: "proficient" },
{ name: "Figma", level: "proficient" },
{ name: "Solana / Web3", level: "proficient" },
],
},
];
const levelColors: Record<string, string> = {
expert: "rgba(255,255,255,0.12)",
proficient: "rgba(255,255,255,0.06)",
learning: "rgba(255,255,255,0.03)",
};
const levelBorders: Record<string, string> = {
expert: "rgba(255,255,255,0.2)",
proficient: "rgba(255,255,255,0.1)",
learning: "rgba(255,255,255,0.06)",
};
export default function SkillsSection() {
const headerRef = useReveal();
const contentRef = useReveal(0.05);
return (
<section
id="skills"
className="py-32 relative"
style={{ background: "#080808" }}
>
<div className="absolute top-0 left-0 right-0 h-px bg-linear-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-6xl mx-auto px-6">
{/* Header */}
<div ref={headerRef} className="reveal mb-16">
<div className="terminal-label mb-4">&gt; cat skills.json</div>
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4">
<h2 className="section-heading text-4xl sm:text-5xl text-white leading-tight">
Tech I Work <span className="text-white/40">With</span>
</h2>
<div className="flex items-center gap-4 terminal-label text-xs">
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full inline-block bg-white/50" />
Expert
</span>
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full inline-block bg-white/25" />
Proficient
</span>
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full inline-block bg-white/10" />
Learning
</span>
</div>
</div>
</div>
<div ref={contentRef} className="reveal">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: skill categories */}
<div className="lg:col-span-2 space-y-8">
{skillCategories.map((cat) => (
<div key={cat.label}>
<div className="flex items-center gap-3 mb-4">
<span className="terminal-label text-xs">
{cat.terminalLabel}
</span>
<div className="flex-1 h-px bg-white/5" />
</div>
<div className="flex flex-wrap gap-2">
{cat.skills.map((skill) => (
<span
key={skill.name}
className="skill-chip"
style={{
background: skill.level
? levelColors[skill.level]
: undefined,
borderColor: skill.level
? levelBorders[skill.level]
: undefined,
}}
>
{skill.name}
</span>
))}
</div>
</div>
))}
</div>
{/* Right: code visual + interests */}
<div className="space-y-4">
{/* Code visual */}
<div className="glass rounded-2xl overflow-hidden">
<img
src={CODE_VISUAL}
alt="Code visualization"
className="w-full h-48 object-cover opacity-70"
/>
<div className="p-4">
<div className="terminal-label text-xs mb-1">
$ git log --oneline
</div>
<div
className="text-white/50 text-xs leading-relaxed"
style={{ fontFamily: "'JetBrains Mono', monospace" }}
>
<div>214 commits this month</div>
<div>2,670 contributions/year</div>
<div>35+ repositories</div>
</div>
</div>
</div>
{/* Interests */}
<div className="glass rounded-2xl p-5">
<div className="terminal-label text-xs mb-4">
&gt; interests/
</div>
<div className="space-y-3">
{[
{
icon: <ShieldCheckIcon size={20} />,
label: "Hacking & Security",
},
{ icon: <CpuIcon size={20} />, label: "Low-level Systems" },
{ icon: <PaletteIcon size={20} />, label: "UI/UX Design" },
{
icon: <RocketIcon size={20} />,
label: "SaaS & Startups",
},
{ icon: <ZapIcon size={20} />, label: "Rust Ecosystem" },
{
icon: <TerminalIcon size={20} />,
label: "Linux & Open Source",
},
].map((item) => (
<div
key={item.label}
className="flex items-center gap-2 text-sm text-white/60"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
<span className="text-base">{item.icon}</span>
{item.label}
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
+17
View File
@@ -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>
);
}
+90
View File
@@ -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)}` : "--:--:--"}
</>
);
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Hero background, bottom to top: terrain contours (real Dajt elevation data),
* grid, scanlines, vignette. Purely decorative and deliberately static.
*/
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="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>
);
}
+59
View File
@@ -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>
);
}
-60
View File
@@ -1,60 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-transparent shadow-xs hover:bg-accent dark:bg-transparent dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
-92
View File
@@ -1,92 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};
-23
View File
@@ -1,23 +0,0 @@
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };
-59
View File
@@ -1,59 +0,0 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
-17
View File
@@ -1,17 +0,0 @@
export { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
// Generate login URL at runtime so redirect URI reflects the current origin.
export const getLoginUrl = () => {
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL;
const appId = import.meta.env.VITE_APP_ID;
const redirectUri = `${window.location.origin}/api/oauth/callback`;
const state = btoa(redirectUri);
const url = new URL(`${oauthPortalUrl}/app-auth`);
url.searchParams.set("appId", appId);
url.searchParams.set("redirectUri", redirectUri);
url.searchParams.set("state", state);
url.searchParams.set("type", "signIn");
return url.toString();
};
+114
View File
@@ -0,0 +1,114 @@
import { projectById, type Status } from "./projects";
/** Proof statuses always come from the project list, so they can't drift. */
const statusOf = (id: string): Status => projectById(id)!.status;
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: "My open-source brand for privacy and opsec tools",
href: "/projects/#orus",
status: statusOf("orus"),
},
],
},
{
slug: "entrepreneurship",
no: "02",
title: "Entrepreneurship",
summary: "Brands and ventures. Systems where incentives and behavior matter.",
proofSummary: "Osias · Orus · EquitySMP",
lead: "I build brands, and ventures that stand on their own.",
paragraphs: [
"Orus is one of the brands: open-source software for privacy, independence, and opsec. Outside 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: statusOf("osias") },
{ label: "Orus", note: "Open-source privacy and opsec brand", href: "/projects/#orus", status: statusOf("orus") },
{ label: "EquitySMP", note: "Real-world-style server economy", href: "/projects/#equitysmp", status: statusOf("equitysmp") },
],
},
{
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: statusOf("saturn") },
{ label: "Enclave + transport protocol", note: "Chat platform, and the protocol it runs on", href: "/projects/#enclave", status: statusOf("enclave") },
{ label: "git.selimaj.dev", note: "Gitea on a Docker and Coolify homelab", href: "https://git.selimaj.dev", status: statusOf("git") },
],
},
{
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: statusOf("briefcase-deck") },
{ label: "RC planes", note: "Build photos and logs to come", href: "/projects/#rc-planes", status: statusOf("rc-planes") },
],
},
];
export const fileBySlug = (slug: string) => dossier.find((f) => f.slug === slug);
+50
View File
@@ -0,0 +1,50 @@
/**
* 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.",
// Replace this one file to swap the profile picture (the favicon is src/app/icon.png).
pfp: "/profile/Mono.png",
pfpIsPlaceholder: false,
github: "https://github.com/selimaj-dev",
git: "https://git.selimaj.dev",
} 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: "Brands", value: "Orus" },
// PLACEHOLDER decision: remove this entry to stop stating it publicly.
{ label: "Age", value: "16" },
{ 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 },
],
},
];
+179
View File
@@ -0,0 +1,179 @@
export type Status = "LIVE" | "BUILDING" | "CONCEPT" | "ARCHIVED";
export type GroupId = "orus" | "ventures" | "infra" | "hardware";
export type Project = {
id: string;
name: string;
group: GroupId;
status: Status;
desc: string;
tags: string[];
href?: string;
/** Position on the 1200×720 node map. */
map: { x: number; y: number };
/** Which side of the marker the label sits on (defaults to away from its hub). */
label?: "l" | "r";
};
/** The map's root: the site itself. Not a project. */
export const root = {
name: "Selimaj",
sub: "selimaj.dev",
map: { x: 600, y: 360 },
} as const;
/**
* Groups branch off the root. A group with `node` uses that project as its hub
* (Orus is a brand with its own projects); the rest get a plain labelled hub.
*/
export const groups: Record<
GroupId,
{ label: string; x: number; y: number; side: "l" | "r"; node?: string }
> = {
orus: { label: "Orus", x: 340, y: 240, side: "r", node: "orus" },
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 other than Orus and the self-hosted infrastructure are
// best guesses. Links are filled in where known.
export const projects: Project[] = [
{
id: "orus",
name: "Orus",
group: "orus",
status: "LIVE",
desc: "One of my brands: open-source software for privacy, independence and opsec. Home to Enclave, Saturn and the cyberdecks.",
tags: ["brand", "oss", "privacy"],
map: { x: 340, y: 240 },
label: "r",
},
{
id: "saturn",
name: "Saturn",
group: "orus",
status: "BUILDING",
desc: "Highly advanced and customizable Minecraft client with free emotes and cosmetics",
tags: ["minecraft", "client"],
map: { x: 190, y: 100 },
},
{
id: "enclave",
name: "Enclave",
group: "orus",
status: "BUILDING",
desc: "Discord-like chat platform, running on a custom transport protocol.",
tags: ["chat", "realtime"],
map: { x: 430, y: 80 },
label: "r",
},
{
id: "enclave-protocol",
name: "Enclave protocol",
group: "orus",
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: 200, y: 330 },
},
{
id: "briefcase-deck",
name: "Briefcase deck",
group: "orus",
status: "CONCEPT",
desc: "Briefcase cyberdeck: the most versatile and doable build.",
tags: ["hardware", "cyberdeck"],
map: { x: 160, y: 215 },
},
{
id: "compact-deck",
name: "Compact deck",
group: "orus",
status: "CONCEPT",
desc: "Compact premium cyberdeck: a Flipper Zero, but better.",
tags: ["hardware", "cyberdeck"],
map: { x: 290, y: 400 },
},
{
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: "ARCHIVED",
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: 470 },
},
{
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: "rc-planes",
name: "RC planes",
group: "hardware",
status: "CONCEPT",
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: 1000, y: 620 },
},
];
export const projectById = (id: string) => projects.find((p) => p.id === id);
/** The brand a project is published under, if any. */
export const brandOf = (p: Project) =>
p.group === "orus" && p.id !== "orus" ? "Orus" : undefined;
/** Home page featured strip, branching from the root. Positions are % of the stage. */
export const featuredRoot = { x: 16, y: 50 };
export const featured = [
{ id: "orus", x: 42, y: 16 },
{ id: "enclave", x: 66, y: 36 },
{ id: "saturn", x: 62, y: 64 },
{ id: "osias", x: 40, y: 84 },
] as const;
-64
View File
@@ -1,64 +0,0 @@
import React, { createContext, useContext, useEffect, useState } from "react";
type Theme = "light" | "dark";
interface ThemeContextType {
theme: Theme;
toggleTheme?: () => void;
switchable: boolean;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: Theme;
switchable?: boolean;
}
export function ThemeProvider({
children,
defaultTheme = "light",
switchable = false,
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(() => {
if (switchable) {
const stored = localStorage.getItem("theme");
return (stored as Theme) || defaultTheme;
}
return defaultTheme;
});
useEffect(() => {
const root = document.documentElement;
if (theme === "dark") {
root.classList.add("dark");
} else {
root.classList.remove("dark");
}
if (switchable) {
localStorage.setItem("theme", theme);
}
}, [theme, switchable]);
const toggleTheme = switchable
? () => {
setTheme(prev => (prev === "light" ? "dark" : "light"));
}
: undefined;
return (
<ThemeContext.Provider value={{ theme, toggleTheme, switchable }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}
-21
View File
@@ -1,21 +0,0 @@
import * as React from "react";
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
undefined
);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}
-280
View File
@@ -1,280 +0,0 @@
/*
* DESIGN SYSTEM — selimaj.dev
* Philosophy: Glassmorphic Terminal
* Background: near-black (#080808), Glass cards: rgba(255,255,255,0.04) + blur
* Typography: Syne (headings) + JetBrains Mono (labels/code) + Geist (body)
* Accent: White only — no color distractions
*/
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
:root {
--radius: 0.75rem;
--background: oklch(0.06 0 0);
--foreground: oklch(0.95 0 0);
--card: oklch(0.09 0 0);
--card-foreground: oklch(0.95 0 0);
--popover: oklch(0.09 0 0);
--popover-foreground: oklch(0.95 0 0);
--primary: oklch(0.95 0 0);
--primary-foreground: oklch(0.06 0 0);
--secondary: oklch(0.12 0 0);
--secondary-foreground: oklch(0.85 0 0);
--muted: oklch(0.14 0 0);
--muted-foreground: oklch(0.55 0 0);
--accent: oklch(0.14 0 0);
--accent-foreground: oklch(0.95 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 8%);
--input: oklch(1 0 0 / 10%);
--ring: oklch(0.95 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
html {
scroll-behavior: smooth;
}
body {
@apply bg-background text-foreground;
font-family: 'Geist', 'DM Sans', sans-serif;
background-color: #080808;
color: #f0f0f0;
overflow-x: hidden;
}
button:not(:disabled),
[role="button"]:not([aria-disabled="true"]),
[type="button"]:not(:disabled),
[type="submit"]:not(:disabled),
a[href],
select:not(:disabled) {
@apply cursor-pointer;
}
}
@layer components {
.container {
width: 100%;
margin-left: auto;
margin-right: auto;
padding-left: 1rem;
padding-right: 1rem;
}
.flex {
min-height: 0;
min-width: 0;
}
@media (min-width: 640px) {
.container {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
}
@media (min-width: 1024px) {
.container {
padding-left: 2rem;
padding-right: 2rem;
max-width: 1280px;
}
}
/* Glass card utility */
.glass {
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.glass-strong {
background: rgba(255, 255, 255, 0.07);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
}
/* Navbar glass */
.navbar-glass {
background: rgba(8, 8, 8, 0.6);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.navbar-glass-solid {
background: rgba(8, 8, 8, 0.92);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
/* Terminal label style */
.terminal-label {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.45);
letter-spacing: 0.05em;
}
/* Section heading style */
.section-heading {
font-family: 'Syne', sans-serif;
font-weight: 700;
letter-spacing: -0.02em;
}
/* Subtle glow on hover for cards */
.card-hover {
transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
}
.card-hover:hover {
border-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 30px rgba(255, 255, 255, 0.04), 0 8px 32px rgba(0,0,0,0.4);
transform: translateY(-2px);
}
/* Animated gradient text */
.gradient-text {
background: linear-gradient(135deg, #ffffff 0%, rgba(255,255,255,0.6) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Noise overlay */
.noise-overlay::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 0;
}
/* Scroll reveal animation */
.reveal {
opacity: 0;
transform: translateY(24px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
/* Skill chip */
.skill-chip {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.875rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 9999px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.75);
transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.skill-chip:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
}
/* Cursor blink */
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.cursor-blink {
animation: blink 1s step-end infinite;
}
/* Floating animation */
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-8px); }
}
.float-anim {
animation: float 4s ease-in-out infinite;
}
/* Fade in up */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in-up {
opacity: 0;
animation: fadeInUp 0.7s ease forwards;
}
/* Stagger delays */
.delay-100 { animation-delay: 0.1s; }
.delay-200 { animation-delay: 0.2s; }
.delay-300 { animation-delay: 0.3s; }
.delay-400 { animation-delay: 0.4s; }
.delay-500 { animation-delay: 0.5s; }
.delay-600 { animation-delay: 0.6s; }
.delay-700 { animation-delay: 0.7s; }
.delay-800 { animation-delay: 0.8s; }
/* White dot separator */
.dot-sep::after {
content: '·';
margin: 0 0.5rem;
color: rgba(255,255,255,0.3);
}
/* Project language badge */
.lang-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
color: rgba(255,255,255,0.5);
}
.lang-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
}
-6
View File
@@ -1,6 +0,0 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
-5
View File
@@ -1,5 +0,0 @@
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
createRoot(document.getElementById("root")!).render(<App />);
-30
View File
@@ -1,30 +0,0 @@
/*
* HOME PAGE — selimaj.dev
* Design: Glassmorphic Terminal
* Assembles all sections: Navbar, Hero, About, Projects, Skills, Contact, Footer
*/
import Navbar from "@/components/Navbar";
import HeroSection from "@/components/HeroSection";
import AboutSection from "@/components/AboutSection";
import ProjectsSection from "@/components/ProjectsSection";
import SkillsSection from "@/components/SkillsSection";
import ContactSection from "@/components/ContactSection";
import Footer from "@/components/Footer";
export default function Home() {
return (
<div
className="min-h-screen"
style={{ background: "#080808", color: "#f0f0f0" }}
>
<Navbar />
<HeroSection />
<AboutSection />
<ProjectsSection />
<SkillsSection />
<ContactSection />
<Footer />
</div>
);
}
-44
View File
@@ -1,44 +0,0 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { AlertCircle, Home } from "lucide-react";
import { useLocation } from "wouter";
export default function NotFound() {
const [, setLocation] = useLocation();
const handleGoHome = () => setLocation("/");
return (
<div className="min-h-screen w-full flex items-center justify-center bg-background">
<Card className="w-full max-w-lg mx-4 shadow-lg border-none">
<CardContent className="pt-8 pb-8 text-center">
<div className="flex justify-center mb-6">
<div className="relative">
<div className="absolute inset-0 rounded-full animate-pulse bg-destructive/20" />
<AlertCircle className="relative h-16 w-16 text-destructive" />
</div>
</div>
<h1 className="text-4xl font-bold text-foreground mb-2">404</h1>
<h2 className="text-xl font-semibold text-muted-foreground mb-4">
Page Not Found
</h2>
<p className="text-muted-foreground mb-8 leading-relaxed">
Sorry, the page you are looking for doesn't exist.
<br />
It may have been moved or deleted.
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Button variant="default" size="lg" onClick={handleGoHome}>
<Home className="w-4 h-4 mr-2" />
Go Home
</Button>
</div>
</CardContent>
</Card>
</div>
);
}
-97
View File
@@ -1,97 +0,0 @@
/** @type {import('tailwindcss').Config} */
const plugin = require('tailwindcss/plugin');
module.exports = {
darkMode: 'class', // supports .dark class
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {
colors: {
background: 'var(--color-background)',
foreground: 'var(--color-foreground)',
card: 'var(--color-card)',
'card-foreground': 'var(--color-card-foreground)',
popover: 'var(--color-popover)',
'popover-foreground': 'var(--color-popover-foreground)',
primary: 'var(--color-primary)',
'primary-foreground': 'var(--color-primary-foreground)',
secondary: 'var(--color-secondary)',
'secondary-foreground': 'var(--color-secondary-foreground)',
muted: 'var(--color-muted)',
'muted-foreground': 'var(--color-muted-foreground)',
accent: 'var(--color-accent)',
'accent-foreground': 'var(--color-accent-foreground)',
destructive: 'var(--color-destructive)',
'destructive-foreground': 'var(--color-destructive-foreground)',
border: 'var(--color-border)',
input: 'var(--color-input)',
ring: 'var(--color-ring)',
},
borderRadius: {
sm: 'var(--radius-sm)',
md: 'var(--radius-md)',
lg: 'var(--radius-lg)',
xl: 'var(--radius-xl)',
},
fontFamily: {
heading: ['Syne', 'sans-serif'],
body: ['Geist', 'DM Sans', 'sans-serif'],
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
},
backdropBlur: {
sm: '12px',
md: '20px',
},
keyframes: {
blink: {
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0' },
},
float: {
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-8px)' },
},
fadeInUp: {
'0%': { opacity: '0', transform: 'translateY(30px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
},
animation: {
blink: 'blink 1s step-end infinite',
float: 'float 4s ease-in-out infinite',
fadeInUp: 'fadeInUp 0.7s ease forwards',
},
},
},
plugins: [
require('tw-animate-css'),
plugin(function({ addUtilities }) {
addUtilities({
'.glass': {
background: 'rgba(255, 255, 255, 0.04)',
'backdrop-filter': 'blur(12px)',
'-webkit-backdrop-filter': 'blur(12px)',
border: '1px solid rgba(255, 255, 255, 0.08)',
},
'.glass-strong': {
background: 'rgba(255, 255, 255, 0.07)',
'backdrop-filter': 'blur(20px)',
'-webkit-backdrop-filter': 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.12)',
},
'.navbar-glass': {
background: 'rgba(8, 8, 8, 0.6)',
'backdrop-filter': 'blur(20px)',
'-webkit-backdrop-filter': 'blur(20px)',
'border-bottom': '1px solid rgba(255, 255, 255, 0.06)',
},
'.navbar-glass-solid': {
background: 'rgba(8, 8, 8, 0.92)',
'backdrop-filter': 'blur(20px)',
'-webkit-backdrop-filter': 'blur(20px)',
'border-bottom': '1px solid rgba(255, 255, 255, 0.08)',
},
});
}),
],
};
-32
View File
@@ -1,32 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2023",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
+32 -5
View File
@@ -1,7 +1,34 @@
{ {
"files": [], "compilerOptions": {
"references": [ "target": "ES2017",
{ "path": "./tsconfig.app.json" }, "lib": ["dom", "dom.iterable", "esnext"],
{ "path": "./tsconfig.node.json" } "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"]
} }
-30
View File
@@ -1,30 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["vite.config.ts"]
}
-13
View File
@@ -1,13 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})