Merge pull request 'develop' (#3) from develop into main

Reviewed-on: #3
This commit is contained in:
2025-03-13 20:05:49 +00:00
25 changed files with 1277 additions and 928 deletions

3
.gitignore vendored
View File

@ -20,6 +20,9 @@
/.next/ /.next/
/out/ /out/
# vscode
.vscode
# production # production
/build /build

18
.prettierignore Normal file
View File

@ -0,0 +1,18 @@
# Build directories
dist/
.astro/
.next/
# Node modules
node_modules/
# Package lock files
bun.lock
package-lock.json
yarn.lock
# Other generated files
.DS_Store
.env
.env.*
!.env.example

27
.prettierrc Normal file
View File

@ -0,0 +1,27 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"bracketSameLine": false,
"jsxSingleQuote": false,
"printWidth": 100,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"plugins": ["prettier-plugin-astro"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}

View File

@ -1,8 +1,8 @@
import { defineConfig } from 'astro/config'; import { defineConfig } from "astro/config";
import react from '@astrojs/react'; import react from "@astrojs/react";
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from "@tailwindcss/vite";
import icon from 'astro-icon'; import icon from "astro-icon";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
@ -10,34 +10,34 @@ export default defineConfig({
react(), react(),
icon({ icon({
include: { include: {
ph: ['*'], ph: ["*"],
logos: ['*'], logos: ["*"],
mdi: ['*'] mdi: ["*"],
} },
}) }),
], ],
vite: { vite: {
plugins: [tailwindcss()], plugins: [tailwindcss()],
server: { server: {
host: '0.0.0.0', host: "0.0.0.0",
allowedHosts: true, allowedHosts: true,
cors: { cors: {
origin: '*', origin: "*",
methods: ['GET'], methods: ["GET"],
allowedHeaders: ['X-Requested-With'] allowedHeaders: ["X-Requested-With"],
} },
}, },
preview: { preview: {
host: '0.0.0.0', host: "0.0.0.0",
allowedHosts: ['jleibl.net'], allowedHosts: ["jleibl.net"],
port: 4321, port: 4321,
cors: { cors: {
origin: '*', origin: "*",
methods: ['GET'], methods: ["GET"],
allowedHeaders: ['X-Requested-With'] allowedHeaders: ["X-Requested-With"],
} },
} },
} },
}); });

View File

@ -30,7 +30,10 @@
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.1.7", "eslint-config-next": "15.1.7",
"eslint-config-prettier": "^10.1.1",
"postcss": "^8", "postcss": "^8",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"tailwindcss": "^4.0.12", "tailwindcss": "^4.0.12",
"typescript": "^5.7.3", "typescript": "^5.7.3",
}, },
@ -705,6 +708,8 @@
"eslint-config-next": ["eslint-config-next@15.1.7", "", { "dependencies": { "@next/eslint-plugin-next": "15.1.7", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-zXoMnYUIy3XHaAoOhrcYkT9UQWvXqWju2K7NNsmb5wd/7XESDwof61eUdW4QhERr3eJ9Ko/vnXqIrj8kk/drYw=="], "eslint-config-next": ["eslint-config-next@15.1.7", "", { "dependencies": { "@next/eslint-plugin-next": "15.1.7", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-zXoMnYUIy3XHaAoOhrcYkT9UQWvXqWju2K7NNsmb5wd/7XESDwof61eUdW4QhERr3eJ9Ko/vnXqIrj8kk/drYw=="],
"eslint-config-prettier": ["eslint-config-prettier@10.1.1", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw=="],
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
"eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.8.3", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.3.7", "enhanced-resolve": "^5.15.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^1.0.2", "stable-hash": "^0.0.4", "tinyglobby": "^0.2.12" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A0bu4Ks2QqDWNpeEgTQMPTngaMhuDu4yv6xpftBMAf+1ziXnpx+eSR1WRfoPTe2BAiAjHFZ7kSNx1fvr5g5pmQ=="], "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.8.3", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.3.7", "enhanced-resolve": "^5.15.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^1.0.2", "stable-hash": "^0.0.4", "tinyglobby": "^0.2.12" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A0bu4Ks2QqDWNpeEgTQMPTngaMhuDu4yv6xpftBMAf+1ziXnpx+eSR1WRfoPTe2BAiAjHFZ7kSNx1fvr5g5pmQ=="],
@ -1243,7 +1248,9 @@
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@2.8.7", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw=="], "prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
"prettier-plugin-astro": ["prettier-plugin-astro@0.14.1", "", { "dependencies": { "@astrojs/compiler": "^2.9.1", "prettier": "^3.0.0", "sass-formatter": "^0.7.6" } }, "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw=="],
"prismjs": ["prismjs@1.29.0", "", {}, "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="], "prismjs": ["prismjs@1.29.0", "", {}, "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q=="],
@ -1333,6 +1340,8 @@
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"s.color": ["s.color@0.0.15", "", {}, "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA=="],
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
@ -1341,6 +1350,8 @@
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"sass-formatter": ["sass-formatter@0.7.9", "", { "dependencies": { "suf-log": "^2.5.3" } }, "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw=="],
"scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], "scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="],
"semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
@ -1407,6 +1418,8 @@
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"suf-log": ["suf-log@2.5.3", "", { "dependencies": { "s.color": "0.0.15" } }, "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
@ -1683,6 +1696,8 @@
"yaml-language-server/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], "yaml-language-server/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"yaml-language-server/prettier": ["prettier@2.8.7", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw=="],
"yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
"yaml-language-server/vscode-languageserver": ["vscode-languageserver@7.0.0", "", { "dependencies": { "vscode-languageserver-protocol": "3.16.0" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw=="], "yaml-language-server/vscode-languageserver": ["vscode-languageserver@7.0.0", "", { "dependencies": { "vscode-languageserver-protocol": "3.16.0" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw=="],

View File

@ -11,6 +11,7 @@ const compat = new FlatCompat({
const eslintConfig = [ const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"), ...compat.extends("next/core-web-vitals", "next/typescript"),
...compat.extends("prettier"),
]; ];
export default eslintConfig; export default eslintConfig;

View File

@ -7,7 +7,9 @@
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"start": "astro dev --host", "start": "astro dev --host",
"lint": "astro check" "lint": "astro check",
"format": "prettier --write .",
"format:check": "prettier --check ."
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.4",
@ -36,7 +38,10 @@
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.1.7", "eslint-config-next": "15.1.7",
"eslint-config-prettier": "^10.1.1",
"postcss": "^8", "postcss": "^8",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"tailwindcss": "^4.0.12", "tailwindcss": "^4.0.12",
"typescript": "^5.7.3" "typescript": "^5.7.3"
} }

View File

@ -1,44 +1,32 @@
const CACHE_NAME = 'jleibl-portfolio-v1'; const CACHE_NAME = "jleibl-portfolio-v1";
const PRECACHE_ASSETS = [ const PRECACHE_ASSETS = ["/", "/index.html"];
'/',
'/index.html',
'/manifest.json',
];
const STYLE_ASSETS = [ const AUTH_ASSETS = ["/manifest.json"];
'/styles/global.css',
'/styles/theme.css'
];
const IMAGE_ASSETS = [ const STYLE_ASSETS = ["/styles/global.css", "/styles/theme.css"];
'/images/profile-image.jpg'
];
const ALL_ASSETS = [ const IMAGE_ASSETS = ["/images/profile-image.jpg"];
...PRECACHE_ASSETS,
...STYLE_ASSETS,
...IMAGE_ASSETS
];
self.addEventListener('install', event => { const ALL_ASSETS = [...PRECACHE_ASSETS, ...AUTH_ASSETS, ...STYLE_ASSETS, ...IMAGE_ASSETS];
self.addEventListener("install", (event) => {
self.skipWaiting(); self.skipWaiting();
event.waitUntil( event.waitUntil(
caches.open(CACHE_NAME) caches.open(CACHE_NAME).then((cache) => {
.then(cache => { console.log("Opened cache");
console.log('Opened cache'); return cache.addAll(PRECACHE_ASSETS);
return cache.addAll(PRECACHE_ASSETS); })
})
); );
}); });
self.addEventListener('activate', event => { self.addEventListener("activate", (event) => {
event.waitUntil( event.waitUntil(
caches.keys().then(cacheNames => { caches.keys().then((cacheNames) => {
return Promise.all( return Promise.all(
cacheNames.map(cacheName => { cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) { if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName); console.log("Deleting old cache:", cacheName);
return caches.delete(cacheName); return caches.delete(cacheName);
} }
}) })
@ -50,22 +38,30 @@ self.addEventListener('activate', event => {
}); });
function shouldCache(url) { function shouldCache(url) {
if (url.origin === self.location.origin) { const urlObj = new URL(url);
if (url.pathname.endsWith('.html') ||
url.pathname.endsWith('.css') || if (urlObj.hostname.includes("cloudflareaccess.com")) {
url.pathname.endsWith('.js') || return false;
url.pathname.endsWith('.jpg') || }
url.pathname.endsWith('.jpeg') ||
url.pathname.endsWith('.png') || if (urlObj.origin === self.location.origin) {
url.pathname.endsWith('.svg') || if (
url.pathname.endsWith('.webp') || urlObj.pathname.endsWith(".html") ||
url.pathname.endsWith('.woff') || urlObj.pathname.endsWith(".css") ||
url.pathname.endsWith('.woff2') || urlObj.pathname.endsWith(".js") ||
url.pathname.endsWith('.json')) { urlObj.pathname.endsWith(".jpg") ||
urlObj.pathname.endsWith(".jpeg") ||
urlObj.pathname.endsWith(".png") ||
urlObj.pathname.endsWith(".svg") ||
urlObj.pathname.endsWith(".webp") ||
urlObj.pathname.endsWith(".woff") ||
urlObj.pathname.endsWith(".woff2") ||
urlObj.pathname.endsWith(".json")
) {
return true; return true;
} }
if (url.pathname === '/') { if (urlObj.pathname === "/") {
return true; return true;
} }
} }
@ -73,50 +69,78 @@ function shouldCache(url) {
return false; return false;
} }
self.addEventListener('fetch', event => { function isAuthProtectedAsset(url) {
if (event.request.mode === 'navigate' || shouldCache(new URL(event.request.url))) { const urlPath = new URL(url).pathname;
return AUTH_ASSETS.some((asset) => asset === urlPath);
}
self.addEventListener("fetch", (event) => {
if (isAuthProtectedAsset(event.request.url)) {
event.respondWith( event.respondWith(
caches.match(event.request) fetch(event.request)
.then(response => { .then((response) => {
if (response) { if (response.status === 200) {
return response; const responseToCache = response.clone();
} caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
const fetchRequest = event.request.clone();
return fetch(fetchRequest)
.then(response => {
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
const responseToCache = response.clone();
if (shouldCache(new URL(event.request.url))) {
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
}
return response;
})
.catch(error => {
console.log('Fetch failed:', error);
if (event.request.mode === 'navigate') {
return caches.match('/');
}
return null;
}); });
}
return response;
}) })
.catch((error) => {
console.log("Fetch failed for auth protected asset:", error);
return caches.match(event.request);
})
);
return;
}
if (event.request.mode === "navigate" || shouldCache(event.request.url)) {
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response;
}
const fetchRequest = event.request.clone();
return fetch(fetchRequest)
.then((response) => {
if (response.url.includes("cloudflareaccess.com")) {
console.log("Request redirected to authentication page:", response.url);
return response;
}
if (!response || response.status !== 200 || response.type !== "basic") {
return response;
}
const responseToCache = response.clone();
if (shouldCache(new URL(event.request.url))) {
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
}
return response;
})
.catch((error) => {
console.log("Fetch failed:", error);
if (event.request.mode === "navigate") {
return caches.match("/");
}
return null;
});
})
); );
} }
}); });
self.addEventListener('message', event => { self.addEventListener("message", (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') { if (event.data && event.data.type === "SKIP_WAITING") {
self.skipWaiting(); self.skipWaiting();
} }
}); });

View File

@ -3,12 +3,12 @@ import FadeIn from "../ui/FadeIn.astro";
import { FaGithub, FaLinkedin, FaEnvelope } from "react-icons/fa"; import { FaGithub, FaLinkedin, FaEnvelope } from "react-icons/fa";
--- ---
<footer <footer role="contentinfo" class="mt-20 sm:mt-40 relative">
role="contentinfo"
class="mt-20 sm:mt-40 relative"
>
<div class="absolute inset-0 theme-bg-gradient opacity-30"></div> <div class="absolute inset-0 theme-bg-gradient opacity-30"></div>
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"></div> <div
class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"
>
</div>
<div class="max-w-(--breakpoint-xl) mx-auto px-4 sm:px-8 py-20 sm:py-32 relative"> <div class="max-w-(--breakpoint-xl) mx-auto px-4 sm:px-8 py-20 sm:py-32 relative">
<div class="grid gap-16 sm:gap-24"> <div class="grid gap-16 sm:gap-24">

View File

@ -1,5 +1,5 @@
--- ---
import MobileMenu from './MobileMenu.astro'; import MobileMenu from "./MobileMenu.astro";
--- ---
<header <header
@ -7,30 +7,46 @@ import MobileMenu from './MobileMenu.astro';
role="banner" role="banner"
> >
<div class="max-w-7xl mx-auto"> <div class="max-w-7xl mx-auto">
<nav class="py-4 px-6 sm:px-8 flex justify-between items-center font-['Instrument_Sans']" role="navigation" aria-label="Main navigation"> <nav
class="py-4 px-6 sm:px-8 flex justify-between items-center font-['Instrument_Sans']"
role="navigation"
aria-label="Main navigation"
>
<div class="flex items-center"> <div class="flex items-center">
<a href="#hero" class="flex items-center gap-2.5 group" aria-label="Home"> <a href="#hero" class="flex items-center gap-2.5 group" aria-label="Home">
<div class="relative w-9 h-9 rounded-xl theme-accent flex items-center justify-center border theme-border overflow-hidden group-hover:border-opacity-80 transition-all duration-300 shadow-sm"> <div
<span class="text-base font-bold tracking-tight theme-primary theme-transition">JL</span> class="relative w-9 h-9 rounded-xl theme-accent flex items-center justify-center border theme-border overflow-hidden group-hover:border-opacity-80 transition-all duration-300 shadow-sm"
<div class="absolute inset-0 theme-bg-05 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div> >
<span class="text-base font-bold tracking-tight theme-primary theme-transition">JL</span
>
<div
class="absolute inset-0 theme-bg-05 opacity-0 group-hover:opacity-100 transition-opacity duration-500"
>
</div>
</div> </div>
<span class="text-base font-medium tracking-tight theme-primary theme-transition hidden sm:block"> <span
class="text-base font-medium tracking-tight theme-primary theme-transition hidden sm:block"
>
Jan-Marlon Leibl Jan-Marlon Leibl
</span> </span>
</a> </a>
</div> </div>
<div class="hidden md:flex items-center"> <div class="hidden md:flex items-center">
<div class="flex bg-black/5 backdrop-blur-md overflow-hidden theme-border border divide-x divide-white/5 shadow-sm rounded-xl"> <div
{["About", "Work", "Blog", "Contact"].map((item, index) => ( class="flex bg-black/5 backdrop-blur-md overflow-hidden theme-border border divide-x divide-white/5 shadow-sm rounded-xl"
<a >
href={`#${item.toLowerCase()}`} {
class={`nav-item px-5 py-2 theme-secondary hover:theme-primary theme-transition ${index === 0 ? "rounded-l-xl" : ""} ${index === 3 ? "rounded-r-xl" : ""}`} ["About", "Work", "Blog", "Contact"].map((item, index) => (
aria-label={`View my ${item.toLowerCase()}`} <a
> href={`#${item.toLowerCase()}`}
{item} class={`nav-item px-5 py-2 theme-secondary hover:theme-primary theme-transition ${index === 0 ? "rounded-l-xl" : ""} ${index === 3 ? "rounded-r-xl" : ""}`}
</a> aria-label={`View my ${item.toLowerCase()}`}
))} >
{item}
</a>
))
}
</div> </div>
<div class="flex items-center gap-4 ml-6"> <div class="flex items-center gap-4 ml-6">
@ -48,7 +64,9 @@ import MobileMenu from './MobileMenu.astro';
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
<path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/> <path
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg> </svg>
<span class="text-sm font-medium">Contact</span> <span class="text-sm font-medium">Contact</span>
<div class="w-0 overflow-hidden group-hover:w-4 transition-all duration-300"> <div class="w-0 overflow-hidden group-hover:w-4 transition-all duration-300">
@ -61,7 +79,7 @@ import MobileMenu from './MobileMenu.astro';
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
<path d="M5 12h14M12 5l7 7-7 7"/> <path d="M5 12h14M12 5l7 7-7 7"></path>
</svg> </svg>
</div> </div>
</a> </a>
@ -76,57 +94,57 @@ import MobileMenu from './MobileMenu.astro';
</header> </header>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
const header = document.querySelector('header'); const header = document.querySelector("header");
if (!header) return; if (!header) return;
header.style.transform = 'translateY(0)'; header.style.transform = "translateY(0)";
header.style.opacity = '1'; header.style.opacity = "1";
let lastScrollY = window.scrollY; let lastScrollY = window.scrollY;
const scrollThreshold = 100; const scrollThreshold = 100;
let isHeaderVisible = true; let isHeaderVisible = true;
const sections = document.querySelectorAll('section[id]'); const sections = document.querySelectorAll("section[id]");
const navItems = document.querySelectorAll('.nav-item'); const navItems = document.querySelectorAll(".nav-item");
function setActiveNavItem() { function setActiveNavItem() {
const scrollPosition = window.scrollY + 200; const scrollPosition = window.scrollY + 200;
navItems.forEach(item => { navItems.forEach((item) => {
item.classList.remove('nav-item-active'); item.classList.remove("nav-item-active");
item.removeAttribute('aria-current'); item.removeAttribute("aria-current");
}); });
let currentSection: string | null = null; let currentSection: string | null = null;
sections.forEach(section => { sections.forEach((section) => {
const sectionTop = section.getBoundingClientRect().top + window.scrollY; const sectionTop = section.getBoundingClientRect().top + window.scrollY;
const sectionHeight = section.getBoundingClientRect().height; const sectionHeight = section.getBoundingClientRect().height;
if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) { if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) {
currentSection = section.getAttribute('id'); currentSection = section.getAttribute("id");
} }
}); });
if (currentSection) { if (currentSection) {
navItems.forEach(item => { navItems.forEach((item) => {
const href = item.getAttribute('href')?.substring(1); const href = item.getAttribute("href")?.substring(1);
if (href === currentSection) { if (href === currentSection) {
item.classList.add('nav-item-active'); item.classList.add("nav-item-active");
item.setAttribute('aria-current', 'true'); item.setAttribute("aria-current", "true");
} }
}); });
} }
} }
window.addEventListener('scroll', () => { window.addEventListener("scroll", () => {
const currentScrollY = window.scrollY; const currentScrollY = window.scrollY;
if (currentScrollY > 20) { if (currentScrollY > 20) {
header.classList.add('scrolled'); header.classList.add("scrolled");
} else { } else {
header.classList.remove('scrolled'); header.classList.remove("scrolled");
} }
lastScrollY = currentScrollY; lastScrollY = currentScrollY;
@ -136,11 +154,11 @@ import MobileMenu from './MobileMenu.astro';
setActiveNavItem(); setActiveNavItem();
navItems.forEach(item => { navItems.forEach((item) => {
item.addEventListener('keydown', (e) => { item.addEventListener("keydown", (e) => {
if (e instanceof KeyboardEvent && (e.key === 'Enter' || e.key === ' ')) { if (e instanceof KeyboardEvent && (e.key === "Enter" || e.key === " ")) {
e.preventDefault(); e.preventDefault();
item.dispatchEvent(new MouseEvent('click')); item.dispatchEvent(new MouseEvent("click"));
} }
}); });
}); });
@ -151,7 +169,10 @@ import MobileMenu from './MobileMenu.astro';
header { header {
transform: translateY(-100px); transform: translateY(-100px);
opacity: 0; opacity: 0;
transition: transform 0.4s ease-out, opacity 0.5s ease-out, border 0.3s ease; transition:
transform 0.4s ease-out,
opacity 0.5s ease-out,
border 0.3s ease;
} }
header.scrolled { header.scrolled {
@ -166,7 +187,7 @@ import MobileMenu from './MobileMenu.astro';
} }
.nav-item::after { .nav-item::after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;

View File

@ -1,6 +1,7 @@
--- ---
const defaultTitle = "Jan-Marlon Leibl • Fullstack Software Developer | PHP & TypeScript Expert"; const defaultTitle = "Jan-Marlon Leibl • Fullstack Software Developer | PHP & TypeScript Expert";
const defaultDescription = "Experienced Fullstack Developer specializing in PHP and TypeScript. Creating high-performance web applications and digital experiences with modern technologies."; const defaultDescription =
"Experienced Fullstack Developer specializing in PHP and TypeScript. Creating high-performance web applications and digital experiences with modern technologies.";
const defaultImage = "/images/profile-image.jpg"; const defaultImage = "/images/profile-image.jpg";
const siteUrl = "https://jleibl.net"; const siteUrl = "https://jleibl.net";
@ -32,43 +33,40 @@ const {
"System Architecture", "System Architecture",
"MySQL", "MySQL",
"React", "React",
"Performance Optimization" "Performance Optimization",
] ],
} = Astro.props; } = Astro.props;
const structuredDataPerson = { const structuredDataPerson = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Person", "@type": "Person",
"name": "Jan-Marlon Leibl", name: "Jan-Marlon Leibl",
"url": siteUrl, url: siteUrl,
"image": `${siteUrl}${image}`, image: `${siteUrl}${image}`,
"jobTitle": "Fullstack Software Developer", jobTitle: "Fullstack Software Developer",
"worksFor": { worksFor: {
"@type": "Organization", "@type": "Organization",
"name": "team neusta" name: "team neusta",
}, },
"description": "Fullstack Software Developer specializing in PHP and TypeScript", description: "Fullstack Software Developer specializing in PHP and TypeScript",
"sameAs": [ sameAs: ["https://github.com/AtomicWasTaken", "https://www.linkedin.com/in/janmarlonleibl/"],
"https://github.com/AtomicWasTaken", knowsAbout: ["PHP", "TypeScript", "Web Development", "System Architecture", "Database Design"],
"https://www.linkedin.com/in/janmarlonleibl/",
],
"knowsAbout": ["PHP", "TypeScript", "Web Development", "System Architecture", "Database Design"]
}; };
const structuredDataWebsite = { const structuredDataWebsite = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": type, "@type": type,
"headline": title, headline: title,
"description": description, description: description,
"image": `${siteUrl}${image}`, image: `${siteUrl}${image}`,
"url": canonicalUrl, url: canonicalUrl,
"author": { author: {
"@type": "Person", "@type": "Person",
"name": "Jan-Marlon Leibl", name: "Jan-Marlon Leibl",
"url": siteUrl url: siteUrl,
}, },
...(publishedDate && {"datePublished": publishedDate}), ...(publishedDate && { datePublished: publishedDate }),
...(modifiedDate && {"dateModified": modifiedDate}) ...(modifiedDate && { dateModified: modifiedDate }),
}; };
--- ---

View File

@ -5,137 +5,91 @@ const navItems = [
{ label: "About", href: "#about" }, { label: "About", href: "#about" },
{ label: "Work", href: "#work" }, { label: "Work", href: "#work" },
{ label: "Blog", href: "#blog" }, { label: "Blog", href: "#blog" },
{ label: "Contact", href: "#contact" } { label: "Contact", href: "#contact" },
]; ];
--- ---
<div class="mobile-menu-container"> <div class="mobile-menu">
<button <button
id="menu-toggle" id="menu-btn"
class="p-2 theme-accent rounded-xl flex flex-col justify-center items-center gap-1.5 border theme-border overflow-hidden" class="menu-btn"
aria-label="Toggle mobile menu" aria-label="Toggle mobile menu"
aria-expanded="false" aria-expanded="false"
aria-controls="mobile-nav" aria-controls="mobile-menu-panel"
> >
<span class="block w-5 h-0.5 theme-primary transition-all duration-300 transform bar-1"></span> <span class="bar"></span>
<span class="block w-5 h-0.5 theme-primary transition-all duration-300 transform bar-2"></span> <span class="bar"></span>
<span class="block w-5 h-0.5 theme-primary transition-all duration-300 transform bar-3"></span> <span class="bar"></span>
</button> </button>
<div <div id="mobile-menu-panel" class="menu-panel">
id="mobile-nav" <div class="menu-content">
class="fixed inset-0 z-50 invisible opacity-0 flex flex-col justify-start items-stretch pt-24 nav-glass transition-all duration-300 transform translate-x-full" <nav class="menu-nav">
aria-hidden="true" {
> navItems.map((item) => (
<nav class="px-6 sm:px-8 pt-6 pb-12 flex flex-col gap-4"> <a href={item.href} class="menu-item">
{navItems.map((item) => ( {item.label}
<a </a>
href={item.href} ))
class="mobile-nav-item py-3 px-6 theme-secondary text-lg font-medium rounded-xl theme-accent border theme-border transition-all duration-300 hover:theme-primary" }
aria-label={`Go to ${item.label} section`} </nav>
>
{item.label}
</a>
))}
</nav>
<div class="px-6 sm:px-8 py-6 mt-auto border-t theme-border flex flex-col gap-5"> <div class="menu-footer">
<a <a href="mailto:jleibl@proton.me" class="contact-button">
href="mailto:jleibl@proton.me" <span class="icon-wrapper">
class="flex items-center justify-center gap-2 py-3 bg-white/10 text-white rounded-xl hover:bg-white/15 transition-all duration-300 shadow-sm" <FaEnvelope className="w-5 h-5" />
aria-label="Contact me via email" </span>
> <span>Contact Me</span>
<span class="p-1 rounded-lg bg-white/5"> </a>
<FaEnvelope className="w-5 h-5" />
</span>
<span class="text-base font-medium font-['Instrument_Sans']">Contact Me</span>
</a>
<div class="flex gap-4 justify-center mt-2"> <div class="social-links">
<a <a
href="https://github.com/AtomicWasTaken" href="https://github.com/AtomicWasTaken"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="GitHub profile" aria-label="GitHub profile"
class="p-3 rounded-xl bg-black/10 hover:bg-black/15 transition-all" >
> <FaGithub className="w-5 h-5" />
<FaGithub className="w-5 h-5 text-white" /> </a>
</a> <a
<a href="https://www.linkedin.com/in/janmarlonleibl/"
href="https://www.linkedin.com/in/janmarlonleibl/" target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" aria-label="LinkedIn profile"
aria-label="LinkedIn profile" >
class="p-3 rounded-xl bg-black/10 hover:bg-black/15 transition-all" <FaLinkedin className="w-5 h-5" />
> </a>
<FaLinkedin className="w-5 h-5 text-white" /> </div>
</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
const menuToggle = document.getElementById('menu-toggle'); const menuBtn = document.getElementById("menu-btn");
const mobileNav = document.getElementById('mobile-nav'); const menuPanel = document.getElementById("mobile-menu-panel");
const mobileNavItems = document.querySelectorAll('.mobile-nav-item'); const menuItems = document.querySelectorAll(".menu-item");
let isMenuOpen = false;
function toggleMenu() { function toggleMenu() {
isMenuOpen = !isMenuOpen; if (!menuBtn || !menuPanel) return;
if (menuToggle) { const isOpen = menuBtn.classList.toggle("active");
menuToggle.setAttribute('aria-expanded', isMenuOpen.toString());
menuToggle.classList.toggle('active', isMenuOpen);
}
if (mobileNav) { menuBtn.setAttribute("aria-expanded", isOpen.toString());
mobileNav.setAttribute('aria-hidden', (!isMenuOpen).toString()); menuPanel.classList.toggle("open", isOpen);
if (isMenuOpen) { document.body.style.overflow = isOpen ? "hidden" : "";
mobileNav.classList.remove('invisible', 'opacity-0', 'translate-x-full');
mobileNav.classList.add('opacity-100', 'translate-x-0');
document.body.style.overflow = 'hidden';
mobileNavItems.forEach((item, index) => {
setTimeout(() => {
item.classList.add('show');
}, 100 + (index * 50));
});
} else {
mobileNav.classList.remove('opacity-100', 'translate-x-0');
mobileNav.classList.add('opacity-0', 'translate-x-full');
document.body.style.overflow = '';
setTimeout(() => {
if (!isMenuOpen) {
mobileNav.classList.add('invisible');
mobileNavItems.forEach(item => {
item.classList.remove('show');
});
}
}, 300);
}
}
} }
menuToggle?.addEventListener('click', toggleMenu); menuBtn?.addEventListener("click", toggleMenu);
mobileNavItems.forEach(item => { menuItems.forEach((item) => {
item.addEventListener('click', () => { item.addEventListener("click", toggleMenu);
toggleMenu();
});
}); });
document.addEventListener('click', (e) => { document.addEventListener("keydown", (e) => {
if (isMenuOpen && mobileNav && e.target instanceof Node && !mobileNav.contains(e.target) && e.target !== menuToggle) { if (e.key === "Escape" && menuPanel?.classList.contains("open")) {
toggleMenu();
}
});
document.addEventListener('keydown', (e) => {
if (isMenuOpen && e.key === 'Escape') {
toggleMenu(); toggleMenu();
} }
}); });
@ -143,26 +97,163 @@ const navItems = [
</script> </script>
<style> <style>
#menu-toggle.active .bar-1 { .mobile-menu {
position: relative;
z-index: 100;
}
.menu-btn {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 6px;
width: 40px;
height: 40px;
padding: 8px;
border-radius: 12px;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
position: relative;
z-index: 101;
transition: background-color 0.3s ease;
}
.menu-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.menu-btn .bar {
display: block;
width: 20px;
height: 2px;
background-color: white;
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
.menu-btn.active .bar:nth-child(1) {
transform: translateY(8px) rotate(45deg); transform: translateY(8px) rotate(45deg);
} }
#menu-toggle.active .bar-2 { .menu-btn.active .bar:nth-child(2) {
opacity: 0; opacity: 0;
} }
#menu-toggle.active .bar-3 { .menu-btn.active .bar:nth-child(3) {
transform: translateY(-8px) rotate(-45deg); transform: translateY(-8px) rotate(-45deg);
} }
.mobile-nav-item { .menu-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-color: rgba(0, 0, 0, 0.97);
display: flex;
align-items: center;
justify-content: center;
visibility: hidden;
opacity: 0; opacity: 0;
transform: translateY(10px); transition: all 0.3s ease;
transition: opacity 0.3s ease, transform 0.3s ease, background-color 0.3s ease, color 0.3s ease; z-index: 100;
} }
.mobile-nav-item.show { .menu-panel.open {
visibility: visible;
opacity: 1; opacity: 1;
transform: translateY(0); }
.menu-content {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding-top: 80px;
}
.menu-nav {
display: flex;
flex-direction: column;
gap: 16px;
padding: 24px;
}
.menu-item {
display: block;
padding: 12px 24px;
font-size: 18px;
font-weight: 500;
color: white;
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
transition: all 0.2s ease;
}
.menu-item:hover {
background-color: rgba(255, 255, 255, 0.1);
color: white;
}
.menu-footer {
margin-top: auto;
padding: 24px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
gap: 20px;
}
.contact-button {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: white;
font-weight: 500;
transition: background-color 0.2s ease;
}
.contact-button:hover {
background-color: rgba(255, 255, 255, 0.15);
}
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 8px;
}
.social-links {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 8px;
}
.social-links a {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 12px;
color: white;
transition: background-color 0.2s ease;
}
.social-links a:hover {
background-color: rgba(255, 255, 255, 0.1);
} }
</style> </style>

View File

@ -1,35 +1,40 @@
--- ---
import FadeIn from '../ui/FadeIn.astro'; import FadeIn from "../ui/FadeIn.astro";
import { Icon } from 'astro-icon/components'; import { Icon } from "astro-icon/components";
import { SiPhp, SiJavascript, SiMysql, SiReact } from 'react-icons/si'; import { SiPhp, SiJavascript, SiMysql, SiReact } from "react-icons/si";
import { FaGithub, FaLinkedin, FaGlobe, FaPalette } from 'react-icons/fa'; import { FaGithub, FaLinkedin, FaGlobe, FaPalette } from "react-icons/fa";
import { BsLightningChargeFill } from 'react-icons/bs'; import { BsLightningChargeFill } from "react-icons/bs";
import { IoServerOutline } from 'react-icons/io5'; import { IoServerOutline } from "react-icons/io5";
import { Image } from 'astro:assets'; import { Image } from "astro:assets";
const technologies = [ const technologies = [
{ name: 'PHP', icon: SiPhp }, { name: "PHP", icon: SiPhp },
{ name: 'JavaScript', icon: SiJavascript }, { name: "JavaScript", icon: SiJavascript },
{ name: 'MySQL', icon: SiMysql }, { name: "MySQL", icon: SiMysql },
{ name: 'React', icon: SiReact } { name: "React", icon: SiReact },
]; ];
const interests = [ const interests = [
{ name: 'Web Development', icon: FaGlobe }, { name: "Web Development", icon: FaGlobe },
{ name: 'System Architecture', icon: IoServerOutline }, { name: "System Architecture", icon: IoServerOutline },
{ name: 'UI/UX Design', icon: FaPalette }, { name: "UI/UX Design", icon: FaPalette },
{ name: 'Performance Optimization', icon: BsLightningChargeFill } { name: "Performance Optimization", icon: BsLightningChargeFill },
]; ];
--- ---
<section id="about" class="py-24 sm:py-40 px-4 sm:px-8 relative"> <section id="about" class="py-24 sm:py-40 px-4 sm:px-8 relative">
<div class="absolute inset-0 theme-bg-gradient opacity-50"></div> <div class="absolute inset-0 theme-bg-gradient opacity-50"></div>
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"></div> <div
class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"
>
</div>
<div class="max-w-(--breakpoint-xl) mx-auto relative"> <div class="max-w-(--breakpoint-xl) mx-auto relative">
<FadeIn> <FadeIn>
<div class="flex items-baseline gap-4 mb-12 sm:mb-24"> <div class="flex items-baseline gap-4 mb-12 sm:mb-24">
<h2 class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary"> <h2
class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary"
>
About About
</h2> </h2>
<div class="h-px grow theme-border opacity-60"></div> <div class="h-px grow theme-border opacity-60"></div>
@ -39,7 +44,9 @@ const interests = [
<div class="grid md:grid-cols-[1fr_2fr] gap-12 sm:gap-24"> <div class="grid md:grid-cols-[1fr_2fr] gap-12 sm:gap-24">
<div class="space-y-6 sm:space-y-8"> <div class="space-y-6 sm:space-y-8">
<FadeIn> <FadeIn>
<div class="aspect-square theme-bg-05 rounded-2xl overflow-hidden border theme-border hover:border-white/20 transition-all duration-300 shadow-md hover:shadow-lg mx-auto md:mx-0 max-w-[280px] md:max-w-none relative group"> <div
class="aspect-square theme-bg-05 rounded-2xl overflow-hidden border theme-border hover:border-white/20 transition-all duration-300 shadow-md hover:shadow-lg mx-auto md:mx-0 max-w-[280px] md:max-w-none relative group"
>
<Image <Image
src="/profile-image.jpg" src="/profile-image.jpg"
alt="Jan-Marlon Leibl - Fullstack Software Developer" alt="Jan-Marlon Leibl - Fullstack Software Developer"
@ -47,7 +54,10 @@ const interests = [
height="400" height="400"
class="object-cover w-full h-full group-hover:scale-105 transition-transform duration-700" class="object-cover w-full h-full group-hover:scale-105 transition-transform duration-700"
/> />
<div class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div> <div
class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
>
</div>
</div> </div>
</FadeIn> </FadeIn>
<FadeIn delay={0.1}> <FadeIn delay={0.1}>
@ -66,10 +76,13 @@ const interests = [
<div class="space-y-10 sm:space-y-16 font-['Instrument_Sans']"> <div class="space-y-10 sm:space-y-16 font-['Instrument_Sans']">
<div class="space-y-6 sm:space-y-8"> <div class="space-y-6 sm:space-y-8">
<p class="text-xl sm:text-2xl theme-text-90 leading-relaxed"> <p class="text-xl sm:text-2xl theme-text-90 leading-relaxed">
Hello! I'm Jan-Marlon, but please call me Jan. I started my journey in programming at the age of 11 with C#, fascinated by a desktop application my friend created. Hello! I'm Jan-Marlon, but please call me Jan. I started my journey in programming at
the age of 11 with C#, fascinated by a desktop application my friend created.
</p> </p>
<p class="text-base sm:text-xl theme-text-70 leading-relaxed"> <p class="text-base sm:text-xl theme-text-70 leading-relaxed">
Today, I specialize in PHP and TypeScript development, constantly pushing the boundaries of what's possible on the web. My journey has led me from creating simple applications to developing complex systems used by thousands. Today, I specialize in PHP and TypeScript development, constantly pushing the
boundaries of what's possible on the web. My journey has led me from creating simple
applications to developing complex systems used by thousands.
</p> </p>
</div> </div>
@ -82,14 +95,16 @@ const interests = [
<div class="h-px grow theme-border opacity-30"></div> <div class="h-px grow theme-border opacity-30"></div>
</div> </div>
<ul class="space-y-4 sm:space-y-5"> <ul class="space-y-4 sm:space-y-5">
{technologies.map((tech) => ( {
<li class="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 cursor-default theme-bg-05 px-4 py-2.5 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300"> technologies.map((tech) => (
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all"> <li class="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 cursor-default theme-bg-05 px-4 py-2.5 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
<tech.icon className="w-5 h-5" /> <span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
</span> <tech.icon className="w-5 h-5" />
{tech.name} </span>
</li> {tech.name}
))} </li>
))
}
</ul> </ul>
</div> </div>
@ -101,14 +116,16 @@ const interests = [
<div class="h-px grow theme-border opacity-30"></div> <div class="h-px grow theme-border opacity-30"></div>
</div> </div>
<ul class="space-y-4 sm:space-y-5"> <ul class="space-y-4 sm:space-y-5">
{interests.map((interest) => ( {
<li class="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 cursor-default theme-bg-05 px-4 py-2.5 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300"> interests.map((interest) => (
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all"> <li class="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 cursor-default theme-bg-05 px-4 py-2.5 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
<interest.icon className="w-5 h-5" /> <span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
</span> <interest.icon className="w-5 h-5" />
{interest.name} </span>
</li> {interest.name}
))} </li>
))
}
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -1,28 +1,31 @@
--- ---
import FadeIn from '../ui/FadeIn.astro'; import FadeIn from "../ui/FadeIn.astro";
const blogPosts = [ const blogPosts = [
{ {
title: "Best Practices for Modern PHP Development", title: "Best Practices for Modern PHP Development",
excerpt: "Explore how to leverage PHP 8.x features and modern tools to create maintainable and performant PHP applications.", excerpt:
"Explore how to leverage PHP 8.x features and modern tools to create maintainable and performant PHP applications.",
date: "May 15, 2023", date: "May 15, 2023",
slug: "best-practices-modern-php-development", slug: "best-practices-modern-php-development",
tags: ["PHP", "Development", "Best Practices"] tags: ["PHP", "Development", "Best Practices"],
}, },
{ {
title: "Building Type-Safe APIs with TypeScript", title: "Building Type-Safe APIs with TypeScript",
excerpt: "How to use TypeScript's powerful type system to create robust and maintainable API integrations in your frontend applications.", excerpt:
"How to use TypeScript's powerful type system to create robust and maintainable API integrations in your frontend applications.",
date: "April 3, 2023", date: "April 3, 2023",
slug: "building-type-safe-apis-typescript", slug: "building-type-safe-apis-typescript",
tags: ["TypeScript", "API", "Frontend"] tags: ["TypeScript", "API", "Frontend"],
}, },
{ {
title: "Performance Optimization Techniques for Web Applications", title: "Performance Optimization Techniques for Web Applications",
excerpt: "A comprehensive guide to identifying and solving performance bottlenecks in modern web applications.", excerpt:
"A comprehensive guide to identifying and solving performance bottlenecks in modern web applications.",
date: "March 12, 2023", date: "March 12, 2023",
slug: "performance-optimization-web-applications", slug: "performance-optimization-web-applications",
tags: ["Performance", "Web", "Optimization"] tags: ["Performance", "Web", "Optimization"],
} },
]; ];
--- ---
@ -34,12 +37,13 @@ const blogPosts = [
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h2 class="text-3xl sm:text-4xl md:text-5xl font-bold font-['DM_Sans'] tracking-tight theme-primary"> <h2
class="text-3xl sm:text-4xl md:text-5xl font-bold font-['DM_Sans'] tracking-tight theme-primary"
>
Blog Blog
</h2> </h2>
<p class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-xl max-w-3xl mt-4"> <p class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-xl max-w-3xl mt-4">
Thoughts, insights, and tutorials on software development and tech. Thoughts, insights, and tutorials on software development and tech.
<span class="inline-flex items-center px-3 py-1 ml-2 rounded-full text-xs font-medium bg-black/20 text-white font-['Instrument_Sans']">Coming Soon</span>
</p> </p>
</div> </div>
<div class="hidden sm:block"> <div class="hidden sm:block">
@ -57,7 +61,7 @@ const blogPosts = [
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
<path d="M5 12h14M12 5l7 7-7 7"/> <path d="M5 12h14M12 5l7 7-7 7"></path>
</svg> </svg>
</button> </button>
</div> </div>
@ -65,47 +69,60 @@ const blogPosts = [
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 sm:gap-8 relative p-12"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 sm:gap-8 relative p-12">
<div class="absolute inset-0 backdrop-blur-sm z-10 flex items-center justify-center rounded-xl"> <div
class="absolute inset-0 backdrop-blur-sm z-10 flex items-center justify-center rounded-xl"
>
<div class="bg-black/80 px-6 py-5 rounded-xl border border-white/20 text-center"> <div class="bg-black/80 px-6 py-5 rounded-xl border border-white/20 text-center">
<span class="text-xl font-semibold text-white font-['DM_Sans']">Blog Coming Soon!</span> <span class="text-xl font-semibold text-white font-['DM_Sans']">Blog Coming Soon!</span>
<p class="text-white/80 mt-2 font-['Instrument_Sans']">The blog feature is currently in development.</p> <p class="text-white/80 mt-2 font-['Instrument_Sans']">
The blog feature is currently in development.
</p>
</div> </div>
</div> </div>
{blogPosts.map((post) => ( {
<div blogPosts.map((post) => (
class="flex flex-col h-full theme-accent border theme-border rounded-xl overflow-hidden opacity-80 group" <a
> href={`/blog/${post.slug}`}
<div class="p-6 sm:p-8 flex flex-col h-full"> class="flex flex-col h-full theme-accent border theme-border rounded-xl overflow-hidden hover:shadow-lg transition-all duration-300 group"
<div class="flex-grow space-y-4"> >
<div class="space-y-1.5"> <div class="p-6 sm:p-8 flex flex-col h-full">
<div class="flex gap-2 flex-wrap"> <div class="flex-grow space-y-4">
{post.tags.map((tag) => ( <div class="space-y-1.5">
<span class="inline-flex px-2.5 py-1 rounded-lg text-xs font-medium theme-text-70 bg-black/10 font-['Instrument_Sans']">{tag}</span> <div class="flex gap-2 flex-wrap">
))} {post.tags.map((tag) => (
<span class="inline-flex px-2.5 py-1 rounded-lg text-xs font-medium theme-text-70 bg-black/10 font-['Instrument_Sans']">
{tag}
</span>
))}
</div>
<h3 class="text-xl font-bold theme-primary group-hover:underline decoration-1 underline-offset-2 font-['DM_Sans']">
{post.title}
</h3>
</div> </div>
<h3 class="text-xl font-bold theme-primary font-['DM_Sans']">{post.title}</h3> <p class="theme-secondary font-['Instrument_Sans'] text-sm sm:text-base leading-relaxed">
{post.excerpt}
</p>
</div> </div>
<p class="theme-secondary font-['Instrument_Sans'] text-sm sm:text-base leading-relaxed">{post.excerpt}</p>
</div>
<div class="mt-6 pt-6 border-t theme-border flex items-center justify-between"> <div class="mt-6 pt-6 border-t theme-border flex items-center justify-between">
<span class="text-sm theme-text-70 font-['Instrument_Sans']">{post.date}</span> <span class="text-sm theme-text-70 font-['Instrument_Sans']">{post.date}</span>
<div class="p-2 rounded-full theme-accent"> <div class="p-2 rounded-full theme-accent group-hover:bg-opacity-100 transition-all">
<svg <svg
class="w-4 h-4 theme-primary" class="w-4 h-4 theme-primary transform group-hover:translate-x-1 transition-transform"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="2"
> >
<path d="M5 12h14M12 5l7 7-7 7"/> <path d="M5 12h14M12 5l7 7-7 7" />
</svg> </svg>
</div>
</div> </div>
</div> </div>
</div> </a>
</div> ))
))} }
</div> </div>
<div class="sm:hidden flex justify-center mt-8"> <div class="sm:hidden flex justify-center mt-8">
@ -123,10 +140,11 @@ const blogPosts = [
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
<path d="M5 12h14M12 5l7 7-7 7"/> <path d="M5 12h14M12 5l7 7-7 7"></path>
</svg> </svg>
</button> </button>
</div> </div>
</FadeIn> </FadeIn>
</div> </div>
</section> </section>

View File

@ -1,5 +1,5 @@
--- ---
import FadeIn from '../ui/FadeIn.astro'; import FadeIn from "../ui/FadeIn.astro";
import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } from "react-icons/fa"; import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } from "react-icons/fa";
--- ---
@ -9,7 +9,9 @@ import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } fr
<div class="max-w-7xl mx-auto relative"> <div class="max-w-7xl mx-auto relative">
<FadeIn className="space-y-16"> <FadeIn className="space-y-16">
<div class="space-y-6"> <div class="space-y-6">
<h2 class="text-3xl sm:text-4xl md:text-5xl font-bold font-['DM_Sans'] tracking-tight theme-primary"> <h2
class="text-3xl sm:text-4xl md:text-5xl font-bold font-['DM_Sans'] tracking-tight theme-primary"
>
Get In Touch Get In Touch
</h2> </h2>
<p class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-xl max-w-3xl"> <p class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-xl max-w-3xl">
@ -22,7 +24,11 @@ import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } fr
<form id="contact-form" class="space-y-6" onsubmit="return openEmailClient(event)"> <form id="contact-form" class="space-y-6" onsubmit="return openEmailClient(event)">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div> <div>
<label for="name" class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']">Your Name</label> <label
for="name"
class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']"
>Your Name</label
>
<input <input
type="text" type="text"
id="name" id="name"
@ -33,7 +39,11 @@ import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } fr
/> />
</div> </div>
<div> <div>
<label for="email" class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']">Your Email</label> <label
for="email"
class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']"
>Your Email</label
>
<input <input
type="email" type="email"
id="email" id="email"
@ -45,7 +55,11 @@ import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } fr
</div> </div>
</div> </div>
<div> <div>
<label for="subject" class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']">Subject</label> <label
for="subject"
class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']"
>Subject</label
>
<input <input
type="text" type="text"
id="subject" id="subject"
@ -54,15 +68,18 @@ import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } fr
/> />
</div> </div>
<div> <div>
<label for="message" class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']">Your Message</label> <label
for="message"
class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']"
>Your Message</label
>
<textarea <textarea
id="message" id="message"
name="message" name="message"
rows="5" rows="5"
class="w-full px-4 py-3 rounded-xl theme-accent border theme-border theme-secondary focus:ring-2 focus:ring-white/20 focus:border-white/30 bg-transparent font-['Instrument_Sans']" class="w-full px-4 py-3 rounded-xl theme-accent border theme-border theme-secondary focus:ring-2 focus:ring-white/20 focus:border-white/30 bg-transparent font-['Instrument_Sans']"
required required
aria-required="true" aria-required="true"></textarea>
></textarea>
</div> </div>
<div> <div>
<button <button
@ -79,7 +96,7 @@ import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } fr
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
<path d="M5 12h14M12 5l7 7-7 7"/> <path d="M5 12h14M12 5l7 7-7 7"></path>
</svg> </svg>
</button> </button>
<div id="form-status" class="mt-4 text-sm hidden font-['Instrument_Sans']"></div> <div id="form-status" class="mt-4 text-sm hidden font-['Instrument_Sans']"></div>
@ -89,7 +106,9 @@ import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } fr
<div class="space-y-8"> <div class="space-y-8">
<div> <div>
<h3 class="text-2xl font-semibold theme-primary mb-4 font-['DM_Sans']">Contact Information</h3> <h3 class="text-2xl font-semibold theme-primary mb-4 font-['DM_Sans']">
Contact Information
</h3>
<div class="space-y-4"> <div class="space-y-4">
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<span class="p-2 rounded-xl theme-bg-05 border theme-border theme-text-70 mt-1"> <span class="p-2 rounded-xl theme-bg-05 border theme-border theme-text-70 mt-1">
@ -97,7 +116,11 @@ import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } fr
</span> </span>
<div> <div>
<h4 class="theme-secondary font-medium font-['Instrument_Sans']">Email</h4> <h4 class="theme-secondary font-medium font-['Instrument_Sans']">Email</h4>
<a href="mailto:jleibl@proton.me" class="theme-primary hover:underline font-['Instrument_Sans']">jleibl@proton.me</a> <a
href="mailto:jleibl@proton.me"
class="theme-primary hover:underline font-['Instrument_Sans']"
>jleibl@proton.me</a
>
</div> </div>
</div> </div>
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
@ -151,21 +174,21 @@ import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } fr
</section> </section>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
(window as any).openEmailClient = (e: Event) => { (window as any).openEmailClient = (e: Event) => {
e.preventDefault(); e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement); const formData = new FormData(e.target as HTMLFormElement);
const name = formData.get('name') as string; const name = formData.get("name") as string;
const email = formData.get('email') as string; const email = formData.get("email") as string;
const subject = formData.get('subject') as string || 'Contact Form Message'; const subject = (formData.get("subject") as string) || "Contact Form Message";
const message = formData.get('message') as string; const message = formData.get("message") as string;
const formStatus = document.getElementById('form-status'); const formStatus = document.getElementById("form-status");
if (!name || !email || !message) { if (!name || !email || !message) {
if (formStatus) { if (formStatus) {
formStatus.textContent = 'Please fill in all required fields.'; formStatus.textContent = "Please fill in all required fields.";
formStatus.classList.remove('hidden'); formStatus.classList.remove("hidden");
} }
return false; return false;
} }

View File

@ -1,34 +1,51 @@
--- ---
import FadeIn from '../ui/FadeIn.astro'; import FadeIn from "../ui/FadeIn.astro";
import GermanyFlag from '../ui/GermanyFlag.astro'; import GermanyFlag from "../ui/GermanyFlag.astro";
--- ---
<section class="min-h-[100dvh] flex items-center justify-center px-4 sm:px-8 relative pt-24 pb-16" id="hero"> <section
class="min-h-[100dvh] flex items-center justify-center px-4 sm:px-8 relative pt-24 pb-16"
id="hero"
>
<div class="absolute inset-0 theme-bg-gradient opacity-80"></div> <div class="absolute inset-0 theme-bg-gradient opacity-80"></div>
<div class="max-w-7xl w-full relative pt-8 sm:pt-4"> <div class="max-w-7xl w-full relative pt-8 sm:pt-4">
<FadeIn className="space-y-10 sm:space-y-16"> <FadeIn className="space-y-10 sm:space-y-16">
<div class="space-y-8 sm:space-y-10"> <div class="space-y-8 sm:space-y-10">
<div class="space-y-6 sm:space-y-8"> <div class="space-y-6 sm:space-y-8">
<div class="inline-flex items-center gap-2.5 px-4 py-2 rounded-xl theme-accent theme-border border theme-transition shadow-sm"> <div
class="inline-flex items-center gap-2.5 px-4 py-2 rounded-xl theme-accent theme-border border theme-transition shadow-sm"
>
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span> <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
<span class="theme-secondary uppercase tracking-wider text-xs sm:text-sm font-medium font-['Instrument_Sans']">Available for Work</span> <span
class="theme-secondary uppercase tracking-wider text-xs sm:text-sm font-medium font-['Instrument_Sans']"
>Available for Work</span
>
</div> </div>
<div class="space-y-3 sm:space-y-4"> <div class="space-y-3 sm:space-y-4">
<h2 class="font-['Instrument_Sans'] text-xl sm:text-2xl theme-secondary tracking-tight font-medium theme-transition"> <h2
class="font-['Instrument_Sans'] text-xl sm:text-2xl theme-secondary tracking-tight font-medium theme-transition"
>
Jan-Marlon Leibl Jan-Marlon Leibl
</h2> </h2>
<div> <div>
<h1 class="font-['DM_Sans'] text-4xl sm:text-6xl md:text-7xl lg:text-8xl font-bold tracking-tight leading-[1.05] sm:leading-[0.95] max-w-4xl theme-primary theme-transition"> <h1
class="font-['DM_Sans'] text-4xl sm:text-6xl md:text-7xl lg:text-8xl font-bold tracking-tight leading-[1.05] sm:leading-[0.95] max-w-4xl theme-primary theme-transition"
>
Software Developer Software Developer
<br /> <br />
<span class="theme-secondary inline-flex items-center gap-2">based in <GermanyFlag /></span> <span class="theme-secondary inline-flex items-center gap-2"
>based in <GermanyFlag /></span
>
</h1> </h1>
</div> </div>
</div> </div>
</div> </div>
<p class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-2xl max-w-2xl leading-relaxed tracking-tight theme-transition"> <p
Passionate about creating digital experiences, with a focus on PHP and modern web technologies. class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-2xl max-w-2xl leading-relaxed tracking-tight theme-transition"
>
Passionate about creating digital experiences, with a focus on PHP and modern web
technologies.
</p> </p>
<div class="flex flex-col sm:flex-row gap-4 sm:gap-5"> <div class="flex flex-col sm:flex-row gap-4 sm:gap-5">
<a <a
@ -43,7 +60,7 @@ import GermanyFlag from '../ui/GermanyFlag.astro';
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="2"
> >
<path d="M5 12h14M12 5l7 7-7 7"/> <path d="M5 12h14M12 5l7 7-7 7"></path>
</svg> </svg>
</a> </a>
<a <a
@ -58,17 +75,24 @@ import GermanyFlag from '../ui/GermanyFlag.astro';
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="2"
> >
<path d="M5 12h14M12 5l7 7-7 7"/> <path d="M5 12h14M12 5l7 7-7 7"></path>
</svg> </svg>
</a> </a>
</div> </div>
</div> </div>
<div class="border-t theme-border pt-8 mt-10"> <div class="border-t theme-border pt-8 mt-10">
<div class="grid grid-cols-1 sm:grid-cols-3 w-full gap-6 sm:gap-0 py-6 sm:py-8 font-['Instrument_Sans'] text-center"> <div
<div class="sm:border-r theme-border last:border-r-0 px-4 flex flex-col items-center justify-center"> class="grid grid-cols-1 sm:grid-cols-3 w-full gap-6 sm:gap-0 py-6 sm:py-8 font-['Instrument_Sans'] text-center"
>
<div
class="sm:border-r theme-border last:border-r-0 px-4 flex flex-col items-center justify-center"
>
<div class="mb-3 sm:mb-4 w-full text-center"> <div class="mb-3 sm:mb-4 w-full text-center">
<span class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block">EMAIL</span> <span
class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block"
>EMAIL</span
>
</div> </div>
<a <a
href="mailto:jleibl@proton.me" href="mailto:jleibl@proton.me"
@ -78,9 +102,14 @@ import GermanyFlag from '../ui/GermanyFlag.astro';
</a> </a>
</div> </div>
<div class="sm:border-r theme-border last:border-r-0 px-4 flex flex-col items-center justify-center"> <div
class="sm:border-r theme-border last:border-r-0 px-4 flex flex-col items-center justify-center"
>
<div class="mb-3 sm:mb-4 w-full text-center"> <div class="mb-3 sm:mb-4 w-full text-center">
<span class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block">ROLE</span> <span
class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block"
>ROLE</span
>
</div> </div>
<span class="theme-text-90 text-base sm:text-lg block w-full"> <span class="theme-text-90 text-base sm:text-lg block w-full">
Fullstack Developer Fullstack Developer
@ -89,11 +118,12 @@ import GermanyFlag from '../ui/GermanyFlag.astro';
<div class="px-4 flex flex-col items-center justify-center"> <div class="px-4 flex flex-col items-center justify-center">
<div class="mb-3 sm:mb-4 w-full text-center"> <div class="mb-3 sm:mb-4 w-full text-center">
<span class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block">EXPERIENCE</span> <span
class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block"
>EXPERIENCE</span
>
</div> </div>
<span class="theme-text-90 text-base sm:text-lg block w-full"> <span class="theme-text-90 text-base sm:text-lg block w-full"> 5+ Years </span>
5+ Years
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,10 +1,18 @@
--- ---
import { SiNextdotjs, SiTailwindcss, SiTypescript, SiPhp, SiJavascript, SiMysql, SiDiscord } from 'react-icons/si'; import {
import { BsLightningChargeFill } from 'react-icons/bs'; SiNextdotjs,
import { FaCode } from 'react-icons/fa'; SiTailwindcss,
import { HiOutlineCollection } from 'react-icons/hi'; SiTypescript,
import { MdLaunch } from 'react-icons/md'; SiPhp,
import FadeIn from '../ui/FadeIn.astro'; SiJavascript,
SiMysql,
SiDiscord,
} from "react-icons/si";
import { BsLightningChargeFill } from "react-icons/bs";
import { FaCode } from "react-icons/fa";
import { HiOutlineCollection } from "react-icons/hi";
import { MdLaunch } from "react-icons/md";
import FadeIn from "../ui/FadeIn.astro";
interface Project { interface Project {
title: string; title: string;
@ -19,21 +27,21 @@ interface Project {
const getTagIcon = (tag: string) => { const getTagIcon = (tag: string) => {
switch (tag) { switch (tag) {
case 'Next.js': case "Next.js":
return SiNextdotjs; return SiNextdotjs;
case 'Tailwind CSS': case "Tailwind CSS":
return SiTailwindcss; return SiTailwindcss;
case 'TypeScript': case "TypeScript":
return SiTypescript; return SiTypescript;
case 'PHP': case "PHP":
return SiPhp; return SiPhp;
case 'JavaScript': case "JavaScript":
return SiJavascript; return SiJavascript;
case 'MySQL': case "MySQL":
return SiMysql; return SiMysql;
case 'Performance': case "Performance":
return BsLightningChargeFill; return BsLightningChargeFill;
case 'Discord API': case "Discord API":
return SiDiscord; return SiDiscord;
default: default:
return null; return null;
@ -41,11 +49,11 @@ const getTagIcon = (tag: string) => {
}; };
const getProjectIcon = (title: string) => { const getProjectIcon = (title: string) => {
if (title.includes('ventry')) { if (title.includes("ventry")) {
return MdLaunch; return MdLaunch;
} else if (title.includes('ShareUpload')) { } else if (title.includes("ShareUpload")) {
return HiOutlineCollection; return HiOutlineCollection;
} else if (title.includes('RestoreM')) { } else if (title.includes("RestoreM")) {
return SiDiscord; return SiDiscord;
} else { } else {
return FaCode; return FaCode;
@ -54,49 +62,49 @@ const getProjectIcon = (title: string) => {
const projects: Project[] = [ const projects: Project[] = [
{ {
title: 'ventry.host v2', title: "ventry.host v2",
year: '2025', year: "2025",
description: 'Free file hosting revamped with a modern design and improved user experience.', description: "Free file hosting revamped with a modern design and improved user experience.",
icon: getProjectIcon('ventry.host v2'), icon: getProjectIcon("ventry.host v2"),
tags: [ tags: [
{ name: 'Next.js', icon: getTagIcon('Next.js') }, { name: "Next.js", icon: getTagIcon("Next.js") },
{ name: 'Tailwind CSS', icon: getTagIcon('Tailwind CSS') }, { name: "Tailwind CSS", icon: getTagIcon("Tailwind CSS") },
{ name: 'TypeScript', icon: getTagIcon('TypeScript') } { name: "TypeScript", icon: getTagIcon("TypeScript") },
] ],
}, },
{ {
title: 'ventry.host', title: "ventry.host",
year: '2023', year: "2023",
description: 'A free file hosting solution with thousands of daily visitors.', description: "A free file hosting solution with thousands of daily visitors.",
icon: getProjectIcon('ventry.host'), icon: getProjectIcon("ventry.host"),
tags: [ tags: [
{ name: 'PHP', icon: getTagIcon('PHP') }, { name: "PHP", icon: getTagIcon("PHP") },
{ name: 'JavaScript', icon: getTagIcon('JavaScript') }, { name: "JavaScript", icon: getTagIcon("JavaScript") },
{ name: 'MySQL', icon: getTagIcon('MySQL') } { name: "MySQL", icon: getTagIcon("MySQL") },
] ],
}, },
{ {
title: 'ShareUpload', title: "ShareUpload",
year: '2022', year: "2022",
description: 'High-performance file sharing platform with unlimited storage.', description: "High-performance file sharing platform with unlimited storage.",
icon: getProjectIcon('ShareUpload'), icon: getProjectIcon("ShareUpload"),
tags: [ tags: [
{ name: 'PHP', icon: getTagIcon('PHP') }, { name: "PHP", icon: getTagIcon("PHP") },
{ name: 'MySQL', icon: getTagIcon('MySQL') }, { name: "MySQL", icon: getTagIcon("MySQL") },
{ name: 'Performance', icon: getTagIcon('Performance') } { name: "Performance", icon: getTagIcon("Performance") },
] ],
}, },
{ {
title: 'RestoreM', title: "RestoreM",
year: '2023', year: "2023",
description: 'Discord server backup and restoration service.', description: "Discord server backup and restoration service.",
icon: getProjectIcon('RestoreM'), icon: getProjectIcon("RestoreM"),
tags: [ tags: [
{ name: 'PHP', icon: getTagIcon('PHP') }, { name: "PHP", icon: getTagIcon("PHP") },
{ name: 'MySQL', icon: getTagIcon('MySQL') }, { name: "MySQL", icon: getTagIcon("MySQL") },
{ name: 'Discord API', icon: getTagIcon('Discord API') } { name: "Discord API", icon: getTagIcon("Discord API") },
] ],
} },
]; ];
const sortedProjects = [...projects].sort((a, b) => { const sortedProjects = [...projects].sort((a, b) => {
@ -110,11 +118,15 @@ const sortedProjects = [...projects].sort((a, b) => {
<div class="max-w-(--breakpoint-xl) mx-auto relative"> <div class="max-w-(--breakpoint-xl) mx-auto relative">
<FadeIn> <FadeIn>
<div class="flex flex-col gap-3 mb-12 sm:mb-24"> <div class="flex flex-col gap-3 mb-12 sm:mb-24">
<span class="theme-text-40 uppercase tracking-wider text-sm sm:text-base font-medium font-['Instrument_Sans']"> <span
class="theme-text-40 uppercase tracking-wider text-sm sm:text-base font-medium font-['Instrument_Sans']"
>
Portfolio Portfolio
</span> </span>
<div class="flex items-baseline gap-4"> <div class="flex items-baseline gap-4">
<h2 class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary"> <h2
class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary"
>
Selected Work Selected Work
</h2> </h2>
<div class="h-px grow theme-border opacity-60"></div> <div class="h-px grow theme-border opacity-60"></div>
@ -123,57 +135,57 @@ const sortedProjects = [...projects].sort((a, b) => {
</FadeIn> </FadeIn>
<div class="grid gap-20 sm:gap-28"> <div class="grid gap-20 sm:gap-28">
{sortedProjects.map((project, index) => ( {
<FadeIn delay={index * 0.1}> sortedProjects.map((project, index) => (
<div class="group relative"> <FadeIn delay={index * 0.1}>
<div class="absolute top-0 left-0 right-0 flex items-center gap-4"> <div class="group relative">
<div class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium py-2 pr-4"> <div class="absolute top-0 left-0 right-0 flex items-center gap-4">
{project.year} <div class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium py-2 pr-4">
</div> {project.year}
<div class="h-px grow theme-border opacity-30"></div> </div>
</div> <div class="h-px grow theme-border opacity-30" />
<div class="pt-12 sm:pt-16 grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-8 sm:gap-16">
<div class="space-y-4 sm:space-y-6">
<h3 class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary group-hover:theme-text-90 transition-colors">
{project.title}
</h3>
<p class="font-['Instrument_Sans'] text-base sm:text-xl theme-text-70 leading-relaxed group-hover:theme-text-90 transition-colors max-w-xl">
{project.description}
</p>
</div> </div>
<div class="space-y-6 sm:space-y-10"> <div class="pt-12 sm:pt-16 grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-8 sm:gap-16">
<div class="space-y-4 sm:space-y-6"> <div class="space-y-4 sm:space-y-6">
<h4 class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium"> <h3 class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary group-hover:theme-text-90 transition-colors">
Technologies {project.title}
</h4> </h3>
<div class="flex flex-wrap items-center gap-3 sm:gap-4"> <p class="font-['Instrument_Sans'] text-base sm:text-xl theme-text-70 leading-relaxed group-hover:theme-text-90 transition-colors max-w-xl">
{project.tags.map((tag, tagIndex) => { {project.description}
const Icon = tag.icon; </p>
return ( </div>
<span
class="flex items-center text-sm sm:text-base theme-text-70 font-['Instrument_Sans'] tracking-tight font-medium py-1.5 sm:py-2 group-hover:theme-text-90 transition-all duration-300 theme-bg-05 px-4 sm:px-5 rounded-xl shadow-sm border border-transparent group-hover:theme-border" <div class="space-y-6 sm:space-y-10">
> <div class="space-y-4 sm:space-y-6">
<span class="mr-2 flex items-center"> <h4 class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium">
<Icon className="text-lg" /> Technologies
</h4>
<div class="flex flex-wrap items-center gap-3 sm:gap-4">
{project.tags.map((tag, tagIndex) => {
const Icon = tag.icon;
return (
<span class="flex items-center text-sm sm:text-base theme-text-70 font-['Instrument_Sans'] tracking-tight font-medium py-1.5 sm:py-2 group-hover:theme-text-90 transition-all duration-300 theme-bg-05 px-4 sm:px-5 rounded-xl shadow-sm border border-transparent group-hover:theme-border">
<span class="mr-2 flex items-center">
<Icon className="text-lg" />
</span>
{tag.name}
{tagIndex !== project.tags.length - 1 && (
<span class="ml-2 opacity-0">•</span>
)}
</span> </span>
{tag.name} );
{tagIndex !== project.tags.length - 1 && ( })}
<span class="ml-2 opacity-0">•</span> </div>
)}
</span>
);
})}
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="absolute -inset-x-6 sm:-inset-x-8 -inset-y-6 sm:-inset-y-8 rounded-2xl sm:rounded-3xl border border-transparent hover:theme-border group-hover:bg-white/[0.01] transition-all duration-300 shadow-sm group-hover:shadow-md"></div> <div class="absolute -inset-x-6 sm:-inset-x-8 -inset-y-6 sm:-inset-y-8 rounded-2xl sm:rounded-3xl border border-transparent hover:theme-border group-hover:bg-white/[0.01] transition-all duration-300 shadow-sm group-hover:shadow-md" />
</div> </div>
</FadeIn> </FadeIn>
))} ))
}
</div> </div>
</div> </div>
</section> </section>

View File

@ -2,22 +2,17 @@
interface Props { interface Props {
className?: string; className?: string;
delay?: number; delay?: number;
direction?: 'up' | 'down' | 'left' | 'right'; direction?: "up" | "down" | "left" | "right";
distance?: number; distance?: number;
} }
const { const { className = "", delay = 0, direction = "up", distance = 20 } = Astro.props;
className = "",
delay = 0,
direction = 'up',
distance = 20
} = Astro.props;
const directionMap = { const directionMap = {
up: { x: 0, y: distance }, up: { x: 0, y: distance },
down: { x: 0, y: -distance }, down: { x: 0, y: -distance },
left: { x: distance, y: 0 }, left: { x: distance, y: 0 },
right: { x: -distance, y: 0 } right: { x: -distance, y: 0 },
}; };
const { x, y } = directionMap[direction]; const { x, y } = directionMap[direction];
@ -32,7 +27,9 @@ const uniqueId = `fade-in-${Math.random().toString(36).substring(2, 9)}`;
.fade-in { .fade-in {
opacity: 0; opacity: 0;
transform: translate(var(--start-x, 0px), var(--start-y, 20px)); transform: translate(var(--start-x, 0px), var(--start-y, 20px));
transition: opacity 0.6s ease-out, transform 0.6s ease-out; transition:
opacity 0.6s ease-out,
transform 0.6s ease-out;
} }
.fade-in.visible { .fade-in.visible {
@ -42,23 +39,26 @@ const uniqueId = `fade-in-${Math.random().toString(36).substring(2, 9)}`;
</style> </style>
<script define:vars={{ uniqueId, x, y, delay }}> <script define:vars={{ uniqueId, x, y, delay }}>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
const element = document.getElementById(uniqueId); const element = document.getElementById(uniqueId);
if (element) { if (element) {
element.style.setProperty('--start-x', `${x}px`); element.style.setProperty("--start-x", `${x}px`);
element.style.setProperty('--start-y', `${y}px`); element.style.setProperty("--start-y", `${y}px`);
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver(
entries.forEach(entry => { (entries) => {
if (entry.isIntersecting) { entries.forEach((entry) => {
setTimeout(() => { if (entry.isIntersecting) {
element.classList.add('visible'); setTimeout(() => {
}, delay * 1000); element.classList.add("visible");
}, delay * 1000);
observer.disconnect(); observer.disconnect();
} }
}); });
}, { threshold: 0.1, rootMargin: "0px 0px -10% 0px" }); },
{ threshold: 0.1, rootMargin: "0px 0px -10% 0px" }
);
observer.observe(element); observer.observe(element);
} }

View File

@ -29,27 +29,27 @@
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
const themeButtons = document.querySelectorAll('.theme-button'); const themeButtons = document.querySelectorAll(".theme-button");
const currentTheme = localStorage.getItem('theme') || 'black'; const currentTheme = localStorage.getItem("theme") || "black";
document.documentElement.setAttribute('data-theme', currentTheme); document.documentElement.setAttribute("data-theme", currentTheme);
document.getElementById(`theme-${currentTheme}`)?.classList.add('ring-1', 'ring-white/20'); document.getElementById(`theme-${currentTheme}`)?.classList.add("ring-1", "ring-white/20");
themeButtons.forEach(button => { themeButtons.forEach((button) => {
button.addEventListener('click', () => { button.addEventListener("click", () => {
const theme = button.getAttribute('data-theme'); const theme = button.getAttribute("data-theme");
if (!theme) return; if (!theme) return;
themeButtons.forEach(btn => btn.classList.remove('ring-1', 'ring-white/20')); themeButtons.forEach((btn) => btn.classList.remove("ring-1", "ring-white/20"));
button.classList.add('ring-1', 'ring-white/20'); button.classList.add("ring-1", "ring-white/20");
document.documentElement.setAttribute('data-theme', theme); document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem('theme', theme); localStorage.setItem("theme", theme);
const themeChangedEvent = new CustomEvent('themeChanged', { const themeChangedEvent = new CustomEvent("themeChanged", {
detail: { theme } detail: { theme },
}); });
document.dispatchEvent(themeChangedEvent); document.dispatchEvent(themeChangedEvent);
}); });

View File

@ -1,8 +1,4 @@
<a <a href="#main-content" class="skip-to-content" aria-label="Skip to main content">
href="#main-content"
class="skip-to-content"
aria-label="Skip to main content"
>
<span class="font-['Instrument_Sans']">Skip to content</span> <span class="font-['Instrument_Sans']">Skip to content</span>
</a> </a>
@ -18,7 +14,7 @@
z-index: 100; z-index: 100;
transition: top 0.3s ease-out; transition: top 0.3s ease-out;
font-weight: 500; font-weight: 500;
font-family: 'Instrument Sans', sans-serif; font-family: "Instrument Sans", sans-serif;
border-radius: 0.75rem; border-radius: 0.75rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
@ -43,7 +39,7 @@
/* Dark mode support */ /* Dark mode support */
:root.dark .skip-to-content { :root.dark .skip-to-content {
background: #0A0A0A; background: #0a0a0a;
color: rgba(255, 255, 255, 0.95); color: rgba(255, 255, 255, 0.95);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
} }

View File

@ -1,23 +1,23 @@
--- ---
import Header from '../components/layout/Header.astro'; import Header from "../components/layout/Header.astro";
import Meta from '../components/layout/Meta.astro'; import Meta from "../components/layout/Meta.astro";
import SkipToContent from '../components/ui/SkipToContent.astro'; import SkipToContent from "../components/ui/SkipToContent.astro";
import Hero from '../components/sections/Hero.astro'; import Hero from "../components/sections/Hero.astro";
import About from '../components/sections/About.astro'; import About from "../components/sections/About.astro";
import Work from '../components/sections/Work.astro'; import Work from "../components/sections/Work.astro";
import BlogPreview from '../components/sections/BlogPreview.astro'; import BlogPreview from "../components/sections/BlogPreview.astro";
import Contact from '../components/sections/Contact.astro'; import Contact from "../components/sections/Contact.astro";
import Footer from '../components/layout/Footer.astro'; import Footer from "../components/layout/Footer.astro";
import '../styles/global.css'; import "../styles/global.css";
import '../styles/theme.css'; import "../styles/theme.css";
import '@fontsource/dm-sans/400.css'; import "@fontsource/dm-sans/400.css";
import '@fontsource/dm-sans/500.css'; import "@fontsource/dm-sans/500.css";
import '@fontsource/dm-sans/600.css'; import "@fontsource/dm-sans/600.css";
import '@fontsource/dm-sans/700.css'; import "@fontsource/dm-sans/700.css";
import '@fontsource/instrument-sans/400.css'; import "@fontsource/instrument-sans/400.css";
import '@fontsource/instrument-sans/500.css'; import "@fontsource/instrument-sans/500.css";
--- ---
<html lang="en"> <html lang="en">
@ -41,61 +41,61 @@ import '@fontsource/instrument-sans/500.css';
</html> </html>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
const appRoot = document.getElementById('app-root'); const appRoot = document.getElementById("app-root");
const themeButtons = document.querySelectorAll('.theme-button'); const themeButtons = document.querySelectorAll(".theme-button");
const currentTheme = localStorage.getItem('colorTheme') || 'black'; const currentTheme = localStorage.getItem("colorTheme") || "black";
if (appRoot) { if (appRoot) {
appRoot.classList.remove('black', 'red', 'gold'); appRoot.classList.remove("black", "red", "gold");
appRoot.classList.add(currentTheme); appRoot.classList.add(currentTheme);
} }
document.documentElement.setAttribute('data-theme', currentTheme); document.documentElement.setAttribute("data-theme", currentTheme);
document.getElementById(`theme-${currentTheme}`)?.classList.add('ring-1', 'ring-white/20'); document.getElementById(`theme-${currentTheme}`)?.classList.add("ring-1", "ring-white/20");
document.addEventListener('themeChanged', (e) => { document.addEventListener("themeChanged", (e) => {
const customEvent = e as Event & { detail: { theme: string } }; const customEvent = e as Event & { detail: { theme: string } };
const newTheme = customEvent.detail.theme; const newTheme = customEvent.detail.theme;
if (appRoot && newTheme) { if (appRoot && newTheme) {
appRoot.classList.remove('black', 'red', 'gold'); appRoot.classList.remove("black", "red", "gold");
appRoot.classList.add(newTheme); appRoot.classList.add(newTheme);
localStorage.setItem('colorTheme', newTheme); localStorage.setItem("colorTheme", newTheme);
} }
}); });
}); });
</script> </script>
<script> <script>
import Lenis from 'lenis'; import Lenis from "lenis";
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
const lenis = new Lenis({ const lenis = new Lenis({
duration: 1.2, duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
smoothWheel: true, smoothWheel: true,
wheelMultiplier: 1, wheelMultiplier: 1,
touchMultiplier: 2, touchMultiplier: 2,
infinite: false infinite: false,
}); });
const anchorLinks = document.querySelectorAll('a[href^="#"]'); const anchorLinks = document.querySelectorAll('a[href^="#"]');
anchorLinks.forEach(link => { anchorLinks.forEach((link) => {
link.addEventListener('click', (e) => { link.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
const targetId = link.getAttribute('href'); const targetId = link.getAttribute("href");
if (targetId && targetId !== '#') { if (targetId && targetId !== "#") {
const targetElement = document.querySelector(targetId); const targetElement = document.querySelector(targetId);
if (targetElement) { if (targetElement) {
lenis.scrollTo(targetElement as HTMLElement, { lenis.scrollTo(targetElement as HTMLElement, {
offset: 0, offset: 0,
duration: 1.2, duration: 1.2,
immediate: false immediate: false,
}); });
} }
} }
@ -111,8 +111,8 @@ import '@fontsource/instrument-sans/500.css';
animationId = requestAnimationFrame(raf); animationId = requestAnimationFrame(raf);
document.addEventListener('visibilitychange', () => { document.addEventListener("visibilitychange", () => {
if (document.visibilityState === 'hidden') { if (document.visibilityState === "hidden") {
cancelAnimationFrame(animationId); cancelAnimationFrame(animationId);
} else { } else {
animationId = requestAnimationFrame(raf); animationId = requestAnimationFrame(raf);
@ -122,13 +122,13 @@ import '@fontsource/instrument-sans/500.css';
</script> </script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
if ('loading' in HTMLImageElement.prototype) { if ("loading" in HTMLImageElement.prototype) {
const lazyImages = document.querySelectorAll('img[data-src]'); const lazyImages = document.querySelectorAll("img[data-src]");
lazyImages.forEach(img => { lazyImages.forEach((img) => {
const imgElement = img as HTMLImageElement; const imgElement = img as HTMLImageElement;
imgElement.src = imgElement.dataset.src || ''; imgElement.src = imgElement.dataset.src || "";
imgElement.removeAttribute('data-src'); imgElement.removeAttribute("data-src");
}); });
} else { } else {
const lazyImageObserver = new IntersectionObserver((entries) => { const lazyImageObserver = new IntersectionObserver((entries) => {
@ -137,14 +137,14 @@ import '@fontsource/instrument-sans/500.css';
const lazyImage = entry.target as HTMLImageElement; const lazyImage = entry.target as HTMLImageElement;
if (lazyImage.dataset.src) { if (lazyImage.dataset.src) {
lazyImage.src = lazyImage.dataset.src; lazyImage.src = lazyImage.dataset.src;
lazyImage.removeAttribute('data-src'); lazyImage.removeAttribute("data-src");
} }
lazyImageObserver.unobserve(lazyImage); lazyImageObserver.unobserve(lazyImage);
} }
}); });
}); });
document.querySelectorAll('img[data-src]').forEach(image => { document.querySelectorAll("img[data-src]").forEach((image) => {
lazyImageObserver.observe(image); lazyImageObserver.observe(image);
}); });
} }
@ -152,12 +152,11 @@ import '@fontsource/instrument-sans/500.css';
</script> </script>
<script> <script>
if ('serviceWorker' in navigator && import.meta.env.PROD) { if ("serviceWorker" in navigator && import.meta.env.PROD) {
window.addEventListener('load', () => { window.addEventListener("load", () => {
navigator.serviceWorker.register('/service-worker.js') navigator.serviceWorker.register("/service-worker.js").catch((error) => {
.catch(error => { console.error("Service worker registration failed:", error);
console.error('Service worker registration failed:', error); });
});
}); });
} }
</script> </script>

View File

@ -1,72 +1,81 @@
@import "tailwindcss"; @import "tailwindcss";
@theme { @theme {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
} }
@layer base { @layer base {
*, *,
::after, ::after,
::before, ::before,
::backdrop, ::backdrop,
::file-selector-button { ::file-selector-button {
border-color: var(--color-gray-200, currentColor); border-color: var(--color-gray-200, currentColor);
}
} }
}
:root {
--background: #ffffff;
--foreground: #171717;
--primary: #2563eb;
--primary-hover: #1d4ed8;
--primary-light: #dbeafe;
--gray-50: #f9fafb;
--gray-100: #f3f4f6;
--gray-200: #e5e7eb;
--gray-300: #d1d5db;
--gray-400: #9ca3af;
--gray-500: #6b7280;
--gray-600: #4b5563;
--gray-700: #374151;
--gray-800: #1f2937;
--gray-900: #111827;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-full: 9999px;
}
@media (prefers-color-scheme: dark) {
:root { :root {
--background: #ffffff; --background: #0a0a0a;
--foreground: #171717; --foreground: #f3f4f6;
--primary: #2563eb; --primary: #3b82f6;
--primary-hover: #1d4ed8; --primary-hover: #60a5fa;
--primary-light: #dbeafe; --primary-light: #1e3a8a;
--gray-50: #f9fafb; --gray-50: #18181b;
--gray-100: #f3f4f6; --gray-100: #27272a;
--gray-200: #e5e7eb; --gray-200: #3f3f46;
--gray-300: #d1d5db; --gray-300: #52525b;
--gray-400: #9ca3af; --gray-400: #71717a;
--gray-500: #6b7280; --gray-500: #a1a1aa;
--gray-600: #4b5563; --gray-600: #d4d4d8;
--gray-700: #374151; --gray-700: #e4e4e7;
--gray-800: #1f2937; --gray-800: #f4f4f5;
--gray-900: #111827; --gray-900: #fafafa;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-full: 9999px;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #f3f4f6;
--primary: #3b82f6;
--primary-hover: #60a5fa;
--primary-light: #1e3a8a;
--gray-50: #18181b;
--gray-100: #27272a;
--gray-200: #3f3f46;
--gray-300: #52525b;
--gray-400: #71717a;
--gray-500: #a1a1aa;
--gray-600: #d4d4d8;
--gray-700: #e4e4e7;
--gray-800: #f4f4f5;
--gray-900: #fafafa;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: 'Instrument Sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
letter-spacing: -0.025em;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
} }
}
body {
color: var(--foreground);
background: var(--background);
font-family:
"Instrument Sans",
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"Roboto",
"Helvetica Neue",
Arial,
sans-serif;
letter-spacing: -0.025em;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}

View File

@ -1,38 +1,92 @@
.black { background: #0A0A0A; } .black {
.red { background: #170c0c; } background: #0a0a0a;
.gold { background: #121107; } }
.red {
background: #170c0c;
}
.gold {
background: #121107;
}
.black .theme-primary { color: rgba(255, 255, 255, 0.95); } .black .theme-primary {
.red .theme-primary { color: #f87171; } color: rgba(255, 255, 255, 0.95);
.gold .theme-primary { color: #fbbf24; } }
.red .theme-primary {
color: #f87171;
}
.gold .theme-primary {
color: #fbbf24;
}
.black .theme-secondary { color: rgba(255, 255, 255, 0.7); } .black .theme-secondary {
.red .theme-secondary { color: rgba(248, 113, 113, 0.85); } color: rgba(255, 255, 255, 0.7);
.gold .theme-secondary { color: rgba(251, 191, 36, 0.85); } }
.red .theme-secondary {
color: rgba(248, 113, 113, 0.85);
}
.gold .theme-secondary {
color: rgba(251, 191, 36, 0.85);
}
.black .theme-accent { background-color: rgba(255, 255, 255, 0.05); } .black .theme-accent {
.red .theme-accent { background-color: rgba(248, 113, 113, 0.08); } background-color: rgba(255, 255, 255, 0.05);
.gold .theme-accent { background-color: rgba(251, 191, 36, 0.08); } }
.red .theme-accent {
background-color: rgba(248, 113, 113, 0.08);
}
.gold .theme-accent {
background-color: rgba(251, 191, 36, 0.08);
}
.black .theme-border { border-color: rgba(255, 255, 255, 0.1); } .black .theme-border {
.red .theme-border { border-color: rgba(248, 113, 113, 0.15); } border-color: rgba(255, 255, 255, 0.1);
.gold .theme-border { border-color: rgba(251, 191, 36, 0.15); } }
.red .theme-border {
border-color: rgba(248, 113, 113, 0.15);
}
.gold .theme-border {
border-color: rgba(251, 191, 36, 0.15);
}
.black .theme-text-40 { color: rgba(255, 255, 255, 0.5); } .black .theme-text-40 {
.red .theme-text-40 { color: rgba(248, 113, 113, 0.6); } color: rgba(255, 255, 255, 0.5);
.gold .theme-text-40 { color: rgba(251, 191, 36, 0.6); } }
.red .theme-text-40 {
color: rgba(248, 113, 113, 0.6);
}
.gold .theme-text-40 {
color: rgba(251, 191, 36, 0.6);
}
.black .theme-text-70 { color: rgba(255, 255, 255, 0.7); } .black .theme-text-70 {
.red .theme-text-70 { color: rgba(248, 113, 113, 0.75); } color: rgba(255, 255, 255, 0.7);
.gold .theme-text-70 { color: rgba(251, 191, 36, 0.75); } }
.red .theme-text-70 {
color: rgba(248, 113, 113, 0.75);
}
.gold .theme-text-70 {
color: rgba(251, 191, 36, 0.75);
}
.black .theme-text-90 { color: rgba(255, 255, 255, 0.9); } .black .theme-text-90 {
.red .theme-text-90 { color: rgba(248, 113, 113, 0.95); } color: rgba(255, 255, 255, 0.9);
.gold .theme-text-90 { color: rgba(251, 191, 36, 0.95); } }
.red .theme-text-90 {
color: rgba(248, 113, 113, 0.95);
}
.gold .theme-text-90 {
color: rgba(251, 191, 36, 0.95);
}
.black .theme-bg-05 { background-color: rgba(255, 255, 255, 0.05); } .black .theme-bg-05 {
.red .theme-bg-05 { background-color: rgba(248, 113, 113, 0.06); } background-color: rgba(255, 255, 255, 0.05);
.gold .theme-bg-05 { background-color: rgba(251, 191, 36, 0.06); } }
.red .theme-bg-05 {
background-color: rgba(248, 113, 113, 0.06);
}
.gold .theme-bg-05 {
background-color: rgba(251, 191, 36, 0.06);
}
.black .nav-glass { .black .nav-glass {
background: rgba(10, 10, 10, 0.8); background: rgba(10, 10, 10, 0.8);
@ -73,7 +127,7 @@
} }
.nav-item::after { .nav-item::after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
@ -109,7 +163,7 @@
.black .theme-button-primary { .black .theme-button-primary {
background-color: rgba(255, 255, 255, 0.9); background-color: rgba(255, 255, 255, 0.9);
color: #0A0A0A; color: #0a0a0a;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
} }
.red .theme-button-primary { .red .theme-button-primary {

View File

@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2017",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@ -18,18 +14,10 @@
"jsx": "react-jsx", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"paths": { "paths": {
"@/*": [ "@/*": ["./src/*"]
"./src/*"
]
}, },
"jsxImportSource": "react" "jsxImportSource": "react"
}, },
"include": [ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"next-env.d.ts", "exclude": ["node_modules"]
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
} }