First demo

This commit is contained in:
2026-01-11 17:19:43 +01:00
parent ac3114e845
commit d001d20d91
22 changed files with 762 additions and 241 deletions

3
.gitignore vendored
View File

@ -22,3 +22,6 @@ pnpm-debug.log*
# jetbrains setting folder
.idea/
# vscode
*.code-workspace

31
Dockerfile Normal file
View File

@ -0,0 +1,31 @@
# Base image
FROM oven/bun:1 AS base
# Set working directory
WORKDIR /app
# Install dependencies with bun
FROM base AS deps
COPY package.json bun.lock* ./
RUN bun install --no-save --frozen-lockfile
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN bun run build -d
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
# Copy built static site + server
COPY --from=build /app/dist ./dist
COPY server.ts ./server.ts
EXPOSE 3000
CMD ["bun", "run", "server.ts"]

View File

@ -1,5 +1,13 @@
// @ts-check
import { defineConfig } from 'astro/config';
import tailwindcss from "@tailwindcss/vite";
// https://astro.build/config
export default defineConfig({});
export default defineConfig({
output: "static",
vite: {
plugins: [tailwindcss()]
}
});

100
bun.lock
View File

@ -4,7 +4,13 @@
"workspaces": {
"": {
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"astro": "^5.16.8",
"simple-icons": "^16.5.0",
"tailwindcss": "^4.1.18",
},
"devDependencies": {
"@types/bun": "^1.3.5",
},
},
},
@ -133,8 +139,16 @@
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
@ -203,6 +217,38 @@
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
@ -215,6 +261,8 @@
"@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
"@types/node": ["@types/node@25.0.6", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@ -247,6 +295,8 @@
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
"camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
@ -323,6 +373,8 @@
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
@ -351,6 +403,8 @@
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"h3": ["h3@1.15.4", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.2", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ=="],
"hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
@ -393,10 +447,36 @@
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
@ -589,6 +669,8 @@
"shiki": ["shiki@3.21.0", "", { "dependencies": { "@shikijs/core": "3.21.0", "@shikijs/engine-javascript": "3.21.0", "@shikijs/engine-oniguruma": "3.21.0", "@shikijs/langs": "3.21.0", "@shikijs/themes": "3.21.0", "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w=="],
"simple-icons": ["simple-icons@16.5.0", "", {}, "sha512-72nn0oHADKx6Hknu7q6M0vfL8LiCUMKABOHane2+4xdqaFBSHfNNBjuZioihiqVQMz7IvVle4NKAM0IlXvl/9A=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="],
@ -605,6 +687,10 @@
"svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="],
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
@ -629,6 +715,8 @@
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unifont": ["unifont@0.7.1", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-0lg9M1cMYvXof8//wZBq6EDEfbwv4++t7+dYpXeS2ypaLuZJmUFYEwTm412/1ED/Wfo/wyzSu6kNZEr9hgRNfg=="],
@ -691,6 +779,18 @@
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],

17
docker-compose.yml Normal file
View File

@ -0,0 +1,17 @@
services:
serpkah:
build:
context: .
dockerfile: Dockerfile
network: host
# ports:
# - 3000:3000
networks:
- external_network
restart: always
container_name: serpkah
networks:
external_network:
external: true

View File

@ -1,5 +1,5 @@
{
"name": "",
"name": "serpkah-linktree",
"type": "module",
"version": "0.0.1",
"scripts": {
@ -9,6 +9,12 @@
"astro": "astro"
},
"dependencies": {
"astro": "^5.16.8"
"@tailwindcss/vite": "^4.1.18",
"astro": "^5.16.8",
"simple-icons": "^16.5.0",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@types/bun": "^1.3.5"
}
}

BIN
public/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

66
server.ts Normal file
View File

@ -0,0 +1,66 @@
import { serve } from "bun";
const port = Number(process.env.PORT ?? "3000");
function contentType(pathname: string): string | undefined {
if (pathname.endsWith(".html")) return "text/html; charset=utf-8";
if (pathname.endsWith(".css")) return "text/css; charset=utf-8";
if (pathname.endsWith(".js")) return "application/javascript; charset=utf-8";
if (pathname.endsWith(".mjs")) return "application/javascript; charset=utf-8";
if (pathname.endsWith(".json")) return "application/json; charset=utf-8";
if (pathname.endsWith(".svg")) return "image/svg+xml";
if (pathname.endsWith(".png")) return "image/png";
if (pathname.endsWith(".jpg") || pathname.endsWith(".jpeg")) return "image/jpeg";
if (pathname.endsWith(".webp")) return "image/webp";
if (pathname.endsWith(".ico")) return "image/x-icon";
if (pathname.endsWith(".woff")) return "font/woff";
if (pathname.endsWith(".woff2")) return "font/woff2";
if (pathname.endsWith(".txt")) return "text/plain; charset=utf-8";
return undefined;
}
const distDir = `${process.cwd()}/dist`;
serve({
port,
async fetch(req) {
const url = new URL(req.url);
// Normalize paths
let pathname = decodeURIComponent(url.pathname);
// Default document
if (pathname === "/") pathname = "/index.html";
const filePath = `${distDir}${pathname}`;
// Try direct file first
let file = Bun.file(filePath);
if (!(await file.exists())) {
// If request is for a "route", serve index.html (SPA-ish fallback)
// Astro static pages typically exist as real files, but this fallback helps
// if you add client-side routing later.
file = Bun.file(`${distDir}/index.html`);
if (!(await file.exists())) {
return new Response("Not Found", { status: 404 });
}
}
const headers = new Headers();
const ct = contentType(pathname);
if (ct) headers.set("Content-Type", ct);
// Cache policy:
// - HTML: no-cache (so updates show quickly)
// - assets: cache long (Astro fingerprints assets by default)
if (pathname.endsWith(".html")) {
headers.set("Cache-Control", "no-cache");
} else {
headers.set("Cache-Control", "public, max-age=31536000, immutable");
}
return new Response(file, { headers });
}
});
console.log(`Serving dist on http://0.0.0.0:${port}`);

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,120 @@
---
import { resolveIcon } from '../lib/icons';
import type { LinkItem } from '../lib/types';
const { item } = Astro.props as { item: LinkItem };
const icon = resolveIcon(item.icon);
const brand = item.brandColor ?? icon.hex ?? '#ffffff';
// Heuristic: determine if brand color is "dark" using relative luminance.
// Supports #RGB and #RRGGBB only (which matches simple-icons + your overrides).
function hexToRgb(hex: string) {
const h = hex.trim().replace('#', '');
const full =
h.length === 3
? h
.split('')
.map((c) => c + c)
.join('')
: h;
if (full.length !== 6) return null;
const r = parseInt(full.slice(0, 2), 16);
const g = parseInt(full.slice(2, 4), 16);
const b = parseInt(full.slice(4, 6), 16);
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null;
return { r, g, b };
}
function srgbToLinear(c: number) {
const v = c / 255;
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
}
function luminance(hex: string) {
const rgb = hexToRgb(hex);
if (!rgb) return 1; // if unknown, treat as bright
const R = srgbToLinear(rgb.r);
const G = srgbToLinear(rgb.g);
const B = srgbToLinear(rgb.b);
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}
const L = luminance(brand);
const autoForeground = L < 0.22 ? '#ffffff' : L > 0.72 ? '#ffffff' : brand;
const foreground = item.iconForeground ?? autoForeground;
const subtitle = item.subtitle?.trim();
// Stronger tint for dark brands so the tile still reads as “brand”
const tintPct = L < 0.22 ? 34 : L > 0.72 ? 22 : 28;
const borderPct = L < 0.22 ? 65 : L > 0.72 ? 50 : 55;
const tileBg = `color-mix(in srgb, ${brand} ${tintPct}%, transparent)`;
const tileBorder = `color-mix(in srgb, ${brand} ${borderPct}%, rgba(255,255,255,0.15))`;
---
<a
href={item.url}
target="_blank"
rel="noreferrer noopener"
class="group flex items-center gap-5 rounded-2xl border border-white/10 bg-white/[0.06] px-5 py-4 transition
hover:-translate-y-0.5 hover:border-white/20 hover:bg-white/[0.09]
focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30"
>
<span
class="relative flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl border"
style={`--brand:${brand}; background:${tileBg}; border-color:${tileBorder};`}
aria-hidden="true"
>
<span class="h-8 w-8" style={`color: ${foreground};`} set:html={icon.svg} />
<span
class="pointer-events-none absolute inset-0 rounded-2xl"
style="box-shadow: inset 0 1px 0 rgba(255,255,255,0.25);"></span>
</span>
<span class="min-w-0 flex-1">
<span
class="block truncate text-[15px] font-semibold tracking-tight text-white"
>
{item.label}
</span>
{
subtitle ? (
<span class="mt-0.5 block truncate text-sm text-white/65">
{subtitle}
</span>
) : null
}
</span>
<span
class="grid h-9 w-9 place-items-center rounded-xl border border-white/10 bg-white/[0.04] text-white/70 transition
group-hover:border-white/20 group-hover:bg-white/[0.06] group-hover:text-white/85"
aria-hidden="true"
>
<svg
viewBox="0 0 24 24"
class="h-4 w-4"
fill="none"
stroke="currentColor"
stroke-width="2.3"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 18l6-6-6-6"></path>
</svg>
</span>
</a>
<style>
/* Ensure injected SVG inherits currentColor */
:global(svg) {
width: 100%;
height: 100%;
}
:global(svg path) {
fill: currentColor;
}
</style>

View File

@ -1,210 +0,0 @@
---
import astroLogo from '../assets/astro.svg';
import background from '../assets/background.svg';
---
<div id="container">
<img id="background" src={background.src} alt="" fetchpriority="high" />
<main>
<section id="hero">
<a href="https://astro.build"
><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
>
<h1>
To get started, open the <code><pre>src/pages</pre></code> directory in your project.
</h1>
<section id="links">
<a class="button" href="https://docs.astro.build">Read our docs</a>
<a href="https://astro.build/chat"
>Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
><path
fill="currentColor"
d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
></path></svg
>
</a>
</section>
</section>
</main>
<a href="https://astro.build/blog/astro-5/" id="news" class="box">
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
><path
d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
fill="#111827"></path></svg
>
<h2>What's New in Astro 5.0?</h2>
<p>
From content layers to server islands, click to learn more about the new features and
improvements in Astro 5.0
</p>
</a>
</div>
<style>
#background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
filter: blur(100px);
}
#container {
font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
height: 100%;
}
main {
height: 100%;
display: flex;
justify-content: center;
}
#hero {
display: flex;
align-items: start;
flex-direction: column;
justify-content: center;
padding: 16px;
}
h1 {
font-size: 22px;
margin-top: 0.25em;
}
#links {
display: flex;
gap: 16px;
}
#links a {
display: flex;
align-items: center;
padding: 10px 12px;
color: #111827;
text-decoration: none;
transition: color 0.2s;
}
#links a:hover {
color: rgb(78, 80, 86);
}
#links a svg {
height: 1em;
margin-left: 8px;
}
#links a.button {
color: white;
background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
inset 0 -2px 0 rgba(0, 0, 0, 0.24);
border-radius: 10px;
}
#links a.button:hover {
color: rgb(230, 230, 230);
box-shadow: none;
}
pre {
font-family:
ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
monospace;
font-weight: normal;
background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
h2 {
margin: 0 0 1em;
font-weight: normal;
color: #111827;
font-size: 20px;
}
p {
color: #4b5563;
font-size: 16px;
line-height: 24px;
letter-spacing: -0.006em;
margin: 0;
}
code {
display: inline-block;
background:
linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
border-radius: 8px;
border: 1px solid transparent;
padding: 6px 8px;
}
.box {
padding: 16px;
background: rgba(255, 255, 255, 1);
border-radius: 16px;
border: 1px solid white;
}
#news {
position: absolute;
bottom: 16px;
right: 16px;
max-width: 300px;
text-decoration: none;
transition: background 0.2s;
backdrop-filter: blur(50px);
}
#news:hover {
background: rgba(255, 255, 255, 0.55);
}
@media screen and (max-height: 368px) {
#news {
display: none;
}
}
@media screen and (max-width: 768px) {
#container {
display: flex;
flex-direction: column;
}
#hero {
display: block;
padding-top: 10%;
}
#links {
flex-wrap: wrap;
}
#links a.button {
padding: 14px 18px;
}
#news {
right: 16px;
left: 16px;
bottom: 2.5rem;
max-width: 100%;
}
h1 {
line-height: 1.5;
}
}
</style>

41
src/data/links.json Normal file
View File

@ -0,0 +1,41 @@
{
"profile": {
"name": "Serpkah",
"bio": "Bio oder sowas...",
"avatar": "/avatar.png"
},
"sections": [
{
"title": "Social",
"items": [
{ "label": "Mastodon", "url": "https://example.com", "icon": "mastodon" },
{ "label": "Bluesky", "url": "https://example.com", "icon": "bluesky" },
{ "label": "VRChat", "url": "https://example.com", "icon": "vrchat", "brandColor": "#000000", "iconForeground": "#ffffff" },
{ "label": "Fur Affinity", "url": "https://example.com", "icon": "furaffinity" },
{ "label": "Barq", "url": "https://example.com", "icon": "barq", "brandColor": "#ff6a00" }
]
},
{
"title": "Messaging",
"items": [
{ "label": "Telegram", "url": "https://example.com", "icon": "telegram" },
{ "label": "Signal", "url": "https://example.com", "icon": "signal" },
{ "label": "Matrix", "url": "https://example.com", "icon": "matrix", "iconForeground": "#ffffff" },
{ "label": "iMessage", "url": "https://example.com", "icon": "imessage", "brandColor": "#34C759", "iconForeground": "#ffffff" }
]
},
{
"title": "Self-hosted",
"items": [
{ "label": "Nextcloud", "url": "https://example.com", "icon": "nextcloud" }
]
},
{
"title": "Contact",
"items": [
{ "label": "Email", "url": "mailto:you@example.com", "icon": "mail" },
{ "label": "Website", "url": "https://example.com", "icon": "link" }
]
}
]
}

78
src/icons/barq.svg Normal file
View File

@ -0,0 +1,78 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="100.000000pt" height="100.000000pt" viewBox="0 0 100.000000 100.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,100.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M845 820 c10 -11 20 -20 23 -20 3 0 -3 9 -13 20 -10 11 -20 20 -23
20 -3 0 3 -9 13 -20z"/>
<path d="M168 813 c7 -3 16 -2 19 1 4 3 -2 6 -13 5 -11 0 -14 -3 -6 -6z"/>
<path d="M217 808 c-6 -4 104 -7 245 -8 141 0 259 3 261 7 6 10 -491 11 -506
1z"/>
<path d="M821 804 c12 -15 11 -16 -11 -10 -22 6 -23 5 -7 -5 13 -7 22 -8 29
-1 11 11 3 32 -14 32 -6 0 -5 -7 3 -16z"/>
<path d="M758 803 c7 -3 16 -2 19 1 4 3 -2 6 -13 5 -11 0 -14 -3 -6 -6z"/>
<path d="M165 790 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
<path d="M843 772 c-9 -5 -10 -18 -5 -38 4 -16 7 -92 7 -169 -3 -215 -2 -213
-30 -240 -24 -25 -26 -25 -205 -25 l-180 0 -68 -42 c-86 -53 -102 -67 -95 -83
3 -9 -7 -17 -29 -23 l-33 -8 27 -6 c15 -3 25 -1 22 3 -9 14 6 10 22 -6 17 -17
44 -20 44 -5 0 5 -8 7 -17 4 -10 -3 4 8 30 25 26 18 52 29 58 26 5 -4 8 -1 7
7 -2 7 5 12 14 12 10 -1 15 3 11 9 -4 6 0 8 11 4 10 -4 15 -3 11 3 -14 22 42
30 219 30 114 0 187 4 191 10 4 6 11 7 17 4 7 -4 8 -2 4 5 -4 6 -3 18 3 24 6
8 11 89 11 207 l2 194 -26 33 c-24 30 -25 33 -9 42 10 6 14 10 8 11 -5 0 -16
-4 -22 -8z"/>
<path d="M85 750 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0 -8
-4 -11 -10z"/>
<path d="M125 749 c-11 -16 -1 -19 13 -3 7 8 8 14 3 14 -5 0 -13 -5 -16 -11z"/>
<path d="M890 720 c0 -5 5 -10 11 -10 5 0 7 5 4 10 -3 6 -8 10 -11 10 -2 0 -4
-4 -4 -10z"/>
<path d="M98 592 c4 -169 9 -188 10 -47 1 72 -2 135 -6 142 -4 6 -6 -37 -4
-95z"/>
<path d="M118 583 c4 -185 9 -203 10 -35 0 83 -3 152 -6 152 -4 0 -6 -53 -4
-117z"/>
<path d="M180 539 l0 -70 46 3 c55 4 77 26 56 58 -6 11 -12 28 -12 38 0 27
-20 42 -57 42 l-33 0 0 -71z m60 32 c0 -6 -4 -13 -10 -16 -5 -3 -10 1 -10 9 0
9 5 16 10 16 6 0 10 -4 10 -9z m10 -56 c0 -8 -7 -15 -15 -15 -8 0 -15 7 -15
15 0 8 7 15 15 15 8 0 15 -7 15 -15z"/>
<path d="M336 563 c-38 -77 -41 -93 -17 -93 12 0 21 5 21 10 0 6 11 10 24 10
14 0 28 -4 31 -10 3 -5 15 -10 25 -10 17 0 16 6 -12 70 -17 39 -35 70 -39 70
-5 0 -19 -21 -33 -47z m44 -44 c0 -5 -7 -9 -15 -9 -9 0 -12 6 -9 15 6 15 24
11 24 -6z"/>
<path d="M460 540 c0 -56 3 -70 15 -70 10 0 15 10 15 28 l1 27 22 -27 c13 -16
31 -28 41 -28 17 0 17 1 1 26 -12 19 -14 29 -6 37 31 31 -2 77 -55 77 l-34 0
0 -70z m58 22 c2 -7 -3 -12 -12 -12 -9 0 -16 7 -16 16 0 17 22 14 28 -4z"/>
<path d="M601 584 c-12 -15 -21 -34 -21 -44 0 -25 38 -70 60 -70 10 0 30 -7
46 -14 15 -8 31 -13 36 -10 12 8 10 34 -4 34 -9 0 -9 3 0 12 19 19 14 76 -8
98 -30 30 -83 27 -109 -6z m84 -20 c19 -20 16 -43 -7 -58 -35 -22 -74 27 -48
59 16 19 35 19 55 -1z"/>
<path d="M760 565 c0 -33 4 -45 15 -45 8 0 15 6 15 14 0 7 3 28 6 45 6 27 4
31 -15 31 -18 0 -21 -6 -21 -45z"/>
<path d="M760 486 c0 -10 9 -16 21 -16 24 0 21 23 -4 28 -10 2 -17 -3 -17 -12z"/>
<path d="M102 390 c0 -14 2 -19 5 -12 2 6 2 18 0 25 -3 6 -5 1 -5 -13z"/>
<path d="M126 355 c5 -29 5 -29 -14 -12 -14 13 -22 15 -27 7 -4 -6 -4 -14 -1
-17 3 -4 6 0 6 7 0 7 9 1 21 -14 11 -14 18 -30 14 -36 -3 -5 0 -7 8 -4 8 3 18
0 22 -6 5 -8 11 -7 22 2 12 10 17 9 28 -5 14 -19 32 -6 20 13 -8 13 -42 13
-50 0 -7 -12 -25 -4 -25 11 0 6 5 7 10 4 6 -3 10 -1 10 5 0 7 -8 9 -22 5 -17
-6 -20 -4 -15 9 4 9 2 26 -4 38 -8 20 -9 19 -3 -7z"/>
<path d="M940 344 c0 -8 5 -12 10 -9 6 4 8 11 5 16 -9 14 -15 11 -15 -7z"/>
<path d="M921 334 c0 -11 3 -14 6 -6 3 7 2 16 -1 19 -3 4 -6 -2 -5 -13z"/>
<path d="M900 295 c-10 -12 -10 -15 4 -15 9 0 16 7 16 15 0 8 -2 15 -4 15 -2
0 -9 -7 -16 -15z"/>
<path d="M185 260 c3 -5 8 -10 11 -10 2 0 4 5 4 10 0 6 -5 10 -11 10 -5 0 -7
-4 -4 -10z"/>
<path d="M170 241 c0 -5 7 -12 16 -15 14 -5 15 -4 4 9 -14 17 -20 19 -20 6z"/>
<path d="M210 208 c-3 -26 -1 -43 3 -39 5 5 7 26 5 47 l-3 39 -5 -47z"/>
<path d="M805 230 c-3 -6 1 -7 9 -4 18 7 21 14 7 14 -6 0 -13 -4 -16 -10z"/>
<path d="M171 203 c7 -12 15 -20 18 -17 3 2 -3 12 -13 22 -17 16 -18 16 -5 -5z"/>
<path d="M865 210 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0
-8 -4 -11 -10z"/>
<path d="M170 160 c0 -5 5 -10 10 -10 6 0 10 5 10 10 0 6 -4 10 -10 10 -5 0
-10 -4 -10 -10z"/>
<path d="M377 156 c-3 -8 0 -17 7 -19 10 -4 12 1 9 14 -6 23 -9 24 -16 5z"/>
<path d="M265 100 c3 -5 8 -10 11 -10 2 0 4 5 4 10 0 6 -5 10 -11 10 -5 0 -7
-4 -4 -10z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

75
src/icons/imessage.svg Normal file
View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="49.609371mm"
height="49.609375mm"
viewBox="0 0 49.609371 49.609375"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<linearGradient
id="rect826_1_"
gradientUnits="userSpaceOnUse"
x1="-199.231"
y1="295.18689"
x2="-199.231"
y2="349.70929"
gradientTransform="matrix(2.7839,0,0,-2.7839,670.4032,1178.1656)">
<stop
offset="0"
style="stop-color:#0CBD2A"
id="stop1" />
<stop
offset="1"
style="stop-color:#5BF675"
id="stop2" />
</linearGradient>
</defs>
<g
id="layer1"
transform="translate(98.689583,-2.9104166)">
<g
id="g963"
transform="matrix(0.26458333,0,0,0.26458333,-104.51042,-45.058541)">
<linearGradient
id="linearGradient4"
gradientUnits="userSpaceOnUse"
x1="-199.231"
y1="295.18689"
x2="-199.231"
y2="349.70929"
gradientTransform="matrix(2.7839,0,0,-2.7839,670.4032,1178.1656)">
<stop
offset="0"
style="stop-color:#0CBD2A"
id="stop3" />
<stop
offset="1"
style="stop-color:#5BF675"
id="stop4" />
</linearGradient>
<path
id="rect826"
class="st0"
d="m 63.3,181.3 h 104.9 c 22.8,0 41.3,18.5 41.3,41.3 v 104.9 c 0,22.8 -18.5,41.3 -41.3,41.3 H 63.3 C 40.5,368.8 22,350.3 22,327.5 V 222.6 c 0,-22.8 18.5,-41.3 41.3,-41.3 z"
style="fill:url(#rect826_1_)" />
<path
id="path922"
class="st1"
d="m 115.8,213.8 c -38,0 -68.8,25.7 -68.8,57.3 0,20.1 12.7,38.7 33.4,49.1 -2.7,6.1 -6.8,11.8 -12,16.8 10.2,-1.8 19.8,-5.5 28,-11 6.3,1.6 12.9,2.4 19.5,2.4 38,0 68.8,-25.7 68.8,-57.3 -0.1,-31.6 -30.9,-57.3 -68.9,-57.3 z"
style="fill:#ffffff" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

26
src/icons/vrchat.svg Normal file
View File

@ -0,0 +1,26 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="100.000000pt" height="100.000000pt" viewBox="0 0 100.000000 100.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,100.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M79 911 l-29 -29 0 -267 c0 -289 3 -310 55 -333 18 -8 98 -12 253
-12 l227 0 90 -112 c69 -86 96 -114 117 -116 44 -5 58 28 58 133 l0 91 29 11
c61 22 66 44 69 293 4 268 0 303 -38 342 l-28 28 -387 0 -387 0 -29 -29z m797
-19 c30 -20 34 -55 34 -290 0 -227 -1 -241 -21 -266 -14 -17 -30 -26 -49 -26
l-29 0 -3 -112 c-2 -66 -7 -113 -13 -115 -6 -2 -51 49 -100 112 l-88 115 -237
0 c-197 0 -239 3 -258 16 -22 15 -22 18 -22 276 0 229 2 264 17 280 15 17 40
18 387 18 204 0 376 -4 382 -8z"/>
<path d="M208 778 c-16 -12 -14 -25 33 -172 27 -88 55 -166 61 -173 13 -17 53
-17 65 0 11 13 103 311 103 333 0 17 -39 29 -52 16 -6 -6 -27 -71 -48 -144
l-37 -133 -12 45 c-64 238 -74 257 -113 228z"/>
<path d="M517 783 c-4 -3 -7 -84 -7 -179 0 -165 1 -173 20 -179 36 -11 50 12
50 81 0 57 2 64 20 64 14 0 29 -19 55 -70 39 -77 58 -92 83 -66 15 15 14 21
-16 81 l-31 64 29 30 c51 50 37 141 -25 166 -34 15 -166 21 -178 8z m151 -62
c14 -9 19 -61 8 -80 -4 -6 -27 -11 -52 -11 l-44 0 0 50 0 50 38 0 c20 0 43 -4
50 -9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,22 +1,38 @@
---
import '../styles/global.css';
const { title = 'Links' } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>Astro Basics</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
</head>
<body>
<slot />
<body class="min-h-screen bg-neutral-950 text-neutral-50">
<div class="pointer-events-none fixed inset-0">
<div
class="absolute -top-40 left-1/2 h-[520px] w-[520px] -translate-x-1/2 rounded-full bg-violet-500/15 blur-3xl"
>
</div>
<div
class="absolute top-24 right-[-120px] h-[420px] w-[420px] rounded-full bg-sky-500/10 blur-3xl"
>
</div>
</div>
<main class="relative mx-auto w-full max-w-xl px-4 py-10 sm:py-14">
<div
class="rounded-3xl border border-white/10 bg-white/5 shadow-[0_18px_60px_rgba(0,0,0,0.45)] backdrop-blur"
>
<slot />
</div>
<footer class="mt-6 text-center text-sm text-white/60">
© {new Date().getFullYear()}
</footer>
</main>
</body>
</html>
<style>
html,
body {
margin: 0;
width: 100%;
height: 100%;
}
</style>

62
src/lib/icons.ts Normal file
View File

@ -0,0 +1,62 @@
import type { SimpleIcon } from "simple-icons";
import * as SI from "simple-icons";
type ResolvedIcon = {
title: string;
svg: string; // full <svg ...>...</svg>
hex?: string; // brand color, if known
};
const customSvgs = import.meta.glob("../icons/*.svg", {
as: "raw",
eager: true
}) as Record<string, string>;
function getCustomSvgBySlug(slug: string): string | undefined {
const normalized = slug.trim().toLowerCase();
const matchKey = Object.keys(customSvgs).find((k) => k.endsWith(`/${normalized}.svg`));
return matchKey ? customSvgs[matchKey] : undefined;
}
function getSimpleIconBySlug(slug: string): SimpleIcon | undefined {
const pascal = slug
.trim()
.toLowerCase()
.replace(/(^\w|-\w)/g, (m) => m.replace("-", "").toUpperCase());
const key = `si${pascal}` as keyof typeof SI;
return SI[key] as unknown as SimpleIcon | undefined;
}
const FALLBACK: Record<string, ResolvedIcon> = {
mail: {
title: "Email",
svg: `<svg viewBox="0 0 24 24" role="img" aria-label="Email"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4-8 5L4 8V6l8 5 8-5v2z"/></svg>`
},
link: {
title: "Link",
svg: `<svg viewBox="0 0 24 24" role="img" aria-label="Link"><path d="M3.9 12a5 5 0 0 1 5-5h3v2h-3a3 3 0 1 0 0 6h3v2h-3a5 5 0 0 1-5-5zm7-1h4v2h-4v-2zm6.2-4h-3V5h3a5 5 0 0 1 0 10h-3v-2h3a3 3 0 1 0 0-6z"/></svg>`
}
};
export function resolveIcon(slug: string): ResolvedIcon {
const normalized = slug.trim().toLowerCase();
// 1) Custom icon file wins
const custom = getCustomSvgBySlug(normalized);
if (custom) {
return { title: normalized, svg: custom };
}
// 2) Built-in fallback icons
if (FALLBACK[normalized]) return FALLBACK[normalized];
// 3) Simple Icons
const si = getSimpleIconBySlug(normalized);
if (!si) return FALLBACK.link;
return {
title: si.title,
hex: `#${si.hex}`,
svg: `<svg viewBox="0 0 24 24" role="img" aria-label="${si.title}"><path d="${si.path}"/></svg>`
};
}

24
src/lib/types.ts Normal file
View File

@ -0,0 +1,24 @@
export type Profile = {
name: string;
bio?: string;
avatar?: string;
};
export type LinkItem = {
label: string;
url: string;
icon: string;
subtitle?: string;
brandColor?: string;
iconForeground?: string;
};
export type LinkSection = {
title: string;
items: LinkItem[];
};
export type LinksData = {
profile: Profile;
sections: LinkSection[];
};

View File

@ -1,11 +1,60 @@
---
import Welcome from '../components/Welcome.astro';
import Layout from '../layouts/Layout.astro';
import LinkCard from '../components/LinkCard.astro';
import dataRaw from '../data/links.json';
import BaseLayout from '../layouts/Layout.astro';
import type { LinksData } from '../lib/types';
// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
const data = dataRaw as LinksData;
const profile = data.profile;
const sections = data.sections ?? [];
---
<Layout>
<Welcome />
</Layout>
<BaseLayout title={`${profile.name} | Links`}>
<section
class="flex flex-col items-center px-6 pb-6 pt-10 text-center sm:px-10"
>
{
profile.avatar ? (
<img
class="h-24 w-24 rounded-full border border-white/10 bg-white/5 object-cover shadow-md"
src={profile.avatar}
alt={`${profile.name} avatar`}
loading="eager"
/>
) : (
<div class="h-24 w-24 rounded-full border border-white/10 bg-white/5" />
)
}
<h1 class="mt-5 text-2xl font-semibold tracking-tight sm:text-[28px]">
{profile.name}
</h1>
{
profile.bio ? (
<p class="mt-3 max-w-md text-[15px] leading-relaxed text-white/70">
{profile.bio}
</p>
) : null
}
</section>
<section class="px-4 pb-8 sm:px-6 sm:pb-10">
<div class="grid gap-8">
{
sections.map((section) => (
<div>
<h2 class="mb-3 text-xs font-semibold uppercase tracking-[0.18em] text-white/55">
{section.title}
</h2>
<div class="grid gap-3">
{section.items.map((item) => (
<LinkCard item={item} />
))}
</div>
</div>
))
}
</div>
</section>
</BaseLayout>

1
src/styles/global.css Normal file
View File

@ -0,0 +1 @@
@import "tailwindcss";

View File

@ -1,5 +1,15 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist"
],
"compilerOptions": {
"types": [
"astro/client"
]
}
}