chore: update project configurations and add Prettier settings
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -20,6 +20,9 @@
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
|
18
.prettierignore
Normal file
18
.prettierignore
Normal 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
27
.prettierrc
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import { defineConfig } from "astro/config";
|
||||
|
||||
import react from '@astrojs/react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import icon from 'astro-icon';
|
||||
import react from "@astrojs/react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import icon from "astro-icon";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
@ -10,34 +10,34 @@ export default defineConfig({
|
||||
react(),
|
||||
icon({
|
||||
include: {
|
||||
ph: ['*'],
|
||||
logos: ['*'],
|
||||
mdi: ['*']
|
||||
}
|
||||
})
|
||||
ph: ["*"],
|
||||
logos: ["*"],
|
||||
mdi: ["*"],
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
host: "0.0.0.0",
|
||||
allowedHosts: true,
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET'],
|
||||
allowedHeaders: ['X-Requested-With']
|
||||
}
|
||||
origin: "*",
|
||||
methods: ["GET"],
|
||||
allowedHeaders: ["X-Requested-With"],
|
||||
},
|
||||
},
|
||||
|
||||
preview: {
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: ['jleibl.net'],
|
||||
host: "0.0.0.0",
|
||||
allowedHosts: ["jleibl.net"],
|
||||
port: 4321,
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET'],
|
||||
allowedHeaders: ['X-Requested-With']
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
origin: "*",
|
||||
methods: ["GET"],
|
||||
allowedHeaders: ["X-Requested-With"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
17
bun.lock
17
bun.lock
@ -30,7 +30,10 @@
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"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-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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@ -1333,6 +1340,8 @@
|
||||
|
||||
"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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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-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/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/vscode-languageserver": ["vscode-languageserver@7.0.0", "", { "dependencies": { "vscode-languageserver-protocol": "3.16.0" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw=="],
|
||||
|
@ -11,6 +11,7 @@ const compat = new FlatCompat({
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
...compat.extends("prettier"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
@ -7,7 +7,9 @@
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"start": "astro dev --host",
|
||||
"lint": "astro check"
|
||||
"lint": "astro check",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
@ -36,7 +38,10 @@
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
|
@ -46,4 +46,4 @@
|
||||
"icons": [{ "src": "/icons/contact-icon.png", "sizes": "192x192" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,163 +1,146 @@
|
||||
const CACHE_NAME = 'jleibl-portfolio-v1';
|
||||
const CACHE_NAME = "jleibl-portfolio-v1";
|
||||
|
||||
const PRECACHE_ASSETS = [
|
||||
'/',
|
||||
'/index.html'
|
||||
];
|
||||
const PRECACHE_ASSETS = ["/", "/index.html"];
|
||||
|
||||
const AUTH_ASSETS = [
|
||||
'/manifest.json'
|
||||
];
|
||||
const AUTH_ASSETS = ["/manifest.json"];
|
||||
|
||||
const STYLE_ASSETS = [
|
||||
'/styles/global.css',
|
||||
'/styles/theme.css'
|
||||
];
|
||||
const STYLE_ASSETS = ["/styles/global.css", "/styles/theme.css"];
|
||||
|
||||
const IMAGE_ASSETS = [
|
||||
'/images/profile-image.jpg'
|
||||
];
|
||||
const IMAGE_ASSETS = ["/images/profile-image.jpg"];
|
||||
|
||||
const ALL_ASSETS = [
|
||||
...PRECACHE_ASSETS,
|
||||
...AUTH_ASSETS,
|
||||
...STYLE_ASSETS,
|
||||
...IMAGE_ASSETS
|
||||
];
|
||||
const ALL_ASSETS = [...PRECACHE_ASSETS, ...AUTH_ASSETS, ...STYLE_ASSETS, ...IMAGE_ASSETS];
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
self.addEventListener("install", (event) => {
|
||||
self.skipWaiting();
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => {
|
||||
console.log('Opened cache');
|
||||
return cache.addAll(PRECACHE_ASSETS);
|
||||
})
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
console.log("Opened cache");
|
||||
return cache.addAll(PRECACHE_ASSETS);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', event => {
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
console.log('Deleting old cache:', cacheName);
|
||||
console.log("Deleting old cache:", cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
return self.clients.claim();
|
||||
});
|
||||
|
||||
function shouldCache(url) {
|
||||
const urlObj = new URL(url);
|
||||
|
||||
if (urlObj.hostname.includes('cloudflareaccess.com')) {
|
||||
|
||||
if (urlObj.hostname.includes("cloudflareaccess.com")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (urlObj.origin === self.location.origin) {
|
||||
if (urlObj.pathname.endsWith('.html') ||
|
||||
urlObj.pathname.endsWith('.css') ||
|
||||
urlObj.pathname.endsWith('.js') ||
|
||||
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')) {
|
||||
if (
|
||||
urlObj.pathname.endsWith(".html") ||
|
||||
urlObj.pathname.endsWith(".css") ||
|
||||
urlObj.pathname.endsWith(".js") ||
|
||||
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;
|
||||
}
|
||||
|
||||
if (urlObj.pathname === '/') {
|
||||
|
||||
if (urlObj.pathname === "/") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isAuthProtectedAsset(url) {
|
||||
const urlPath = new URL(url).pathname;
|
||||
return AUTH_ASSETS.some(asset => asset === urlPath);
|
||||
return AUTH_ASSETS.some((asset) => asset === urlPath);
|
||||
}
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
self.addEventListener("fetch", (event) => {
|
||||
if (isAuthProtectedAsset(event.request.url)) {
|
||||
event.respondWith(
|
||||
fetch(event.request)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
const responseToCache = response.clone();
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => {
|
||||
cache.put(event.request, responseToCache);
|
||||
});
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(event.request, responseToCache);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Fetch failed for auth protected asset:', error);
|
||||
.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)) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Fetch failed:', error);
|
||||
|
||||
if (event.request.mode === 'navigate') {
|
||||
return caches.match('/');
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
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 => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -3,13 +3,13 @@ import FadeIn from "../ui/FadeIn.astro";
|
||||
import { FaGithub, FaLinkedin, FaEnvelope } from "react-icons/fa";
|
||||
---
|
||||
|
||||
<footer
|
||||
role="contentinfo"
|
||||
class="mt-20 sm:mt-40 relative"
|
||||
>
|
||||
<footer role="contentinfo" class="mt-20 sm:mt-40 relative">
|
||||
<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="grid gap-16 sm:gap-24">
|
||||
<FadeIn>
|
||||
@ -38,10 +38,10 @@ import { FaGithub, FaLinkedin, FaEnvelope } from "react-icons/fa";
|
||||
</span>
|
||||
jleibl@proton.me
|
||||
</a>
|
||||
|
||||
|
||||
<div class="flex gap-6 sm:gap-8">
|
||||
<a
|
||||
href="https://github.com/AtomicWasTaken"
|
||||
<a
|
||||
href="https://github.com/AtomicWasTaken"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
|
||||
@ -49,9 +49,9 @@ import { FaGithub, FaLinkedin, FaEnvelope } from "react-icons/fa";
|
||||
>
|
||||
<FaGithub className="w-6 h-6 sm:w-7 sm:h-7" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://www.linkedin.com/in/janmarlonleibl/"
|
||||
|
||||
<a
|
||||
href="https://www.linkedin.com/in/janmarlonleibl/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
|
||||
|
@ -1,73 +1,91 @@
|
||||
---
|
||||
import MobileMenu from './MobileMenu.astro';
|
||||
import MobileMenu from "./MobileMenu.astro";
|
||||
---
|
||||
|
||||
<header
|
||||
<header
|
||||
class="fixed top-0 left-0 right-0 z-40 nav-glass backdrop-blur-lg theme-transition"
|
||||
role="banner"
|
||||
>
|
||||
<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">
|
||||
<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">
|
||||
<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
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
<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
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<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">
|
||||
{["About", "Work", "Blog", "Contact"].map((item, index) => (
|
||||
<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" : ""}`}
|
||||
aria-label={`View my ${item.toLowerCase()}`}
|
||||
>
|
||||
{item}
|
||||
</a>
|
||||
))}
|
||||
<div
|
||||
class="flex bg-black/5 backdrop-blur-md overflow-hidden theme-border border divide-x divide-white/5 shadow-sm rounded-xl"
|
||||
>
|
||||
{
|
||||
["About", "Work", "Blog", "Contact"].map((item, index) => (
|
||||
<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" : ""}`}
|
||||
aria-label={`View my ${item.toLowerCase()}`}
|
||||
>
|
||||
{item}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 ml-6">
|
||||
<a
|
||||
href="mailto:jleibl@proton.me"
|
||||
|
||||
<div class="flex items-center gap-4 ml-6">
|
||||
<a
|
||||
href="mailto:jleibl@proton.me"
|
||||
class="flex items-center gap-2 px-5 py-2 theme-bg-05 theme-primary border theme-border rounded-xl hover:bg-opacity-100 transition-all duration-300 group shadow-sm"
|
||||
aria-label="Contact me via email"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 theme-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
<svg
|
||||
class="w-4 h-4 theme-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linecap="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>
|
||||
<span class="text-sm font-medium">Contact</span>
|
||||
<div class="w-0 overflow-hidden group-hover:w-4 transition-all duration-300">
|
||||
<svg
|
||||
class="w-4 h-4 theme-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
<svg
|
||||
class="w-4 h-4 theme-primary"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex md:hidden">
|
||||
<MobileMenu />
|
||||
</div>
|
||||
@ -76,71 +94,71 @@ import MobileMenu from './MobileMenu.astro';
|
||||
</header>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const header = document.querySelector('header');
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const header = document.querySelector("header");
|
||||
if (!header) return;
|
||||
|
||||
header.style.transform = 'translateY(0)';
|
||||
header.style.opacity = '1';
|
||||
|
||||
|
||||
header.style.transform = "translateY(0)";
|
||||
header.style.opacity = "1";
|
||||
|
||||
let lastScrollY = window.scrollY;
|
||||
const scrollThreshold = 100;
|
||||
let isHeaderVisible = true;
|
||||
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
const navItems = document.querySelectorAll('.nav-item');
|
||||
|
||||
|
||||
const sections = document.querySelectorAll("section[id]");
|
||||
const navItems = document.querySelectorAll(".nav-item");
|
||||
|
||||
function setActiveNavItem() {
|
||||
const scrollPosition = window.scrollY + 200;
|
||||
|
||||
navItems.forEach(item => {
|
||||
item.classList.remove('nav-item-active');
|
||||
item.removeAttribute('aria-current');
|
||||
|
||||
navItems.forEach((item) => {
|
||||
item.classList.remove("nav-item-active");
|
||||
item.removeAttribute("aria-current");
|
||||
});
|
||||
|
||||
|
||||
let currentSection: string | null = null;
|
||||
|
||||
sections.forEach(section => {
|
||||
|
||||
sections.forEach((section) => {
|
||||
const sectionTop = section.getBoundingClientRect().top + window.scrollY;
|
||||
const sectionHeight = section.getBoundingClientRect().height;
|
||||
|
||||
|
||||
if (scrollPosition >= sectionTop && scrollPosition < sectionTop + sectionHeight) {
|
||||
currentSection = section.getAttribute('id');
|
||||
currentSection = section.getAttribute("id");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (currentSection) {
|
||||
navItems.forEach(item => {
|
||||
const href = item.getAttribute('href')?.substring(1);
|
||||
navItems.forEach((item) => {
|
||||
const href = item.getAttribute("href")?.substring(1);
|
||||
if (href === currentSection) {
|
||||
item.classList.add('nav-item-active');
|
||||
item.setAttribute('aria-current', 'true');
|
||||
item.classList.add("nav-item-active");
|
||||
item.setAttribute("aria-current", "true");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
|
||||
window.addEventListener("scroll", () => {
|
||||
const currentScrollY = window.scrollY;
|
||||
|
||||
|
||||
if (currentScrollY > 20) {
|
||||
header.classList.add('scrolled');
|
||||
header.classList.add("scrolled");
|
||||
} else {
|
||||
header.classList.remove('scrolled');
|
||||
header.classList.remove("scrolled");
|
||||
}
|
||||
|
||||
|
||||
lastScrollY = currentScrollY;
|
||||
|
||||
|
||||
setActiveNavItem();
|
||||
});
|
||||
|
||||
|
||||
setActiveNavItem();
|
||||
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('keydown', (e) => {
|
||||
if (e instanceof KeyboardEvent && (e.key === 'Enter' || e.key === ' ')) {
|
||||
|
||||
navItems.forEach((item) => {
|
||||
item.addEventListener("keydown", (e) => {
|
||||
if (e instanceof KeyboardEvent && (e.key === "Enter" || e.key === " ")) {
|
||||
e.preventDefault();
|
||||
item.dispatchEvent(new MouseEvent('click'));
|
||||
item.dispatchEvent(new MouseEvent("click"));
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -151,22 +169,25 @@ import MobileMenu from './MobileMenu.astro';
|
||||
header {
|
||||
transform: translateY(-100px);
|
||||
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 {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
|
||||
.nav-item::after {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
@ -175,7 +196,7 @@ import MobileMenu from './MobileMenu.astro';
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
|
||||
.black .nav-item::after {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
@ -185,16 +206,16 @@ import MobileMenu from './MobileMenu.astro';
|
||||
.gold .nav-item::after {
|
||||
background-color: rgba(251, 191, 36, 0.8);
|
||||
}
|
||||
|
||||
|
||||
.nav-item:hover::after {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
|
||||
.nav-item-active {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
|
||||
.nav-item-active::after {
|
||||
transform: translateX(0);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,6 +1,7 @@
|
||||
---
|
||||
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 siteUrl = "https://jleibl.net";
|
||||
|
||||
@ -24,51 +25,48 @@ const {
|
||||
publishedDate,
|
||||
modifiedDate,
|
||||
keywords = [
|
||||
"Software Development",
|
||||
"PHP Developer",
|
||||
"TypeScript",
|
||||
"Fullstack Engineer",
|
||||
"Web Development",
|
||||
"System Architecture",
|
||||
"MySQL",
|
||||
"React",
|
||||
"Performance Optimization"
|
||||
]
|
||||
"Software Development",
|
||||
"PHP Developer",
|
||||
"TypeScript",
|
||||
"Fullstack Engineer",
|
||||
"Web Development",
|
||||
"System Architecture",
|
||||
"MySQL",
|
||||
"React",
|
||||
"Performance Optimization",
|
||||
],
|
||||
} = Astro.props;
|
||||
|
||||
const structuredDataPerson = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Person",
|
||||
"name": "Jan-Marlon Leibl",
|
||||
"url": siteUrl,
|
||||
"image": `${siteUrl}${image}`,
|
||||
"jobTitle": "Fullstack Software Developer",
|
||||
"worksFor": {
|
||||
name: "Jan-Marlon Leibl",
|
||||
url: siteUrl,
|
||||
image: `${siteUrl}${image}`,
|
||||
jobTitle: "Fullstack Software Developer",
|
||||
worksFor: {
|
||||
"@type": "Organization",
|
||||
"name": "team neusta"
|
||||
name: "team neusta",
|
||||
},
|
||||
"description": "Fullstack Software Developer specializing in PHP and TypeScript",
|
||||
"sameAs": [
|
||||
"https://github.com/AtomicWasTaken",
|
||||
"https://www.linkedin.com/in/janmarlonleibl/",
|
||||
],
|
||||
"knowsAbout": ["PHP", "TypeScript", "Web Development", "System Architecture", "Database Design"]
|
||||
description: "Fullstack Software Developer specializing in PHP and TypeScript",
|
||||
sameAs: ["https://github.com/AtomicWasTaken", "https://www.linkedin.com/in/janmarlonleibl/"],
|
||||
knowsAbout: ["PHP", "TypeScript", "Web Development", "System Architecture", "Database Design"],
|
||||
};
|
||||
|
||||
const structuredDataWebsite = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": type,
|
||||
"headline": title,
|
||||
"description": description,
|
||||
"image": `${siteUrl}${image}`,
|
||||
"url": canonicalUrl,
|
||||
"author": {
|
||||
headline: title,
|
||||
description: description,
|
||||
image: `${siteUrl}${image}`,
|
||||
url: canonicalUrl,
|
||||
author: {
|
||||
"@type": "Person",
|
||||
"name": "Jan-Marlon Leibl",
|
||||
"url": siteUrl
|
||||
name: "Jan-Marlon Leibl",
|
||||
url: siteUrl,
|
||||
},
|
||||
...(publishedDate && {"datePublished": publishedDate}),
|
||||
...(modifiedDate && {"dateModified": modifiedDate})
|
||||
...(publishedDate && { datePublished: publishedDate }),
|
||||
...(modifiedDate && { dateModified: modifiedDate }),
|
||||
};
|
||||
---
|
||||
|
||||
@ -105,4 +103,4 @@ const structuredDataWebsite = {
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
@ -5,14 +5,14 @@ const navItems = [
|
||||
{ label: "About", href: "#about" },
|
||||
{ label: "Work", href: "#work" },
|
||||
{ label: "Blog", href: "#blog" },
|
||||
{ label: "Contact", href: "#contact" }
|
||||
{ label: "Contact", href: "#contact" },
|
||||
];
|
||||
---
|
||||
|
||||
<div class="mobile-menu">
|
||||
<button
|
||||
id="menu-btn"
|
||||
class="menu-btn"
|
||||
<button
|
||||
id="menu-btn"
|
||||
class="menu-btn"
|
||||
aria-label="Toggle mobile menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="mobile-menu-panel"
|
||||
@ -21,43 +21,39 @@ const navItems = [
|
||||
<span class="bar"></span>
|
||||
<span class="bar"></span>
|
||||
</button>
|
||||
|
||||
|
||||
<div id="mobile-menu-panel" class="menu-panel">
|
||||
<div class="menu-content">
|
||||
<nav class="menu-nav">
|
||||
{navItems.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
class="menu-item"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
{
|
||||
navItems.map((item) => (
|
||||
<a href={item.href} class="menu-item">
|
||||
{item.label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="menu-footer">
|
||||
<a
|
||||
href="mailto:jleibl@proton.me"
|
||||
class="contact-button"
|
||||
>
|
||||
<a href="mailto:jleibl@proton.me" class="contact-button">
|
||||
<span class="icon-wrapper">
|
||||
<FaEnvelope className="w-5 h-5" />
|
||||
</span>
|
||||
<span>Contact Me</span>
|
||||
</a>
|
||||
|
||||
|
||||
<div class="social-links">
|
||||
<a
|
||||
href="https://github.com/AtomicWasTaken"
|
||||
target="_blank"
|
||||
<a
|
||||
href="https://github.com/AtomicWasTaken"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub profile"
|
||||
>
|
||||
<FaGithub className="w-5 h-5" />
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/in/janmarlonleibl/"
|
||||
target="_blank"
|
||||
<a
|
||||
href="https://www.linkedin.com/in/janmarlonleibl/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="LinkedIn profile"
|
||||
>
|
||||
@ -70,30 +66,30 @@ const navItems = [
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const menuBtn = document.getElementById('menu-btn');
|
||||
const menuPanel = document.getElementById('mobile-menu-panel');
|
||||
const menuItems = document.querySelectorAll('.menu-item');
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const menuBtn = document.getElementById("menu-btn");
|
||||
const menuPanel = document.getElementById("mobile-menu-panel");
|
||||
const menuItems = document.querySelectorAll(".menu-item");
|
||||
|
||||
function toggleMenu() {
|
||||
if (!menuBtn || !menuPanel) return;
|
||||
|
||||
const isOpen = menuBtn.classList.toggle('active');
|
||||
|
||||
menuBtn.setAttribute('aria-expanded', isOpen.toString());
|
||||
menuPanel.classList.toggle('open', isOpen);
|
||||
|
||||
document.body.style.overflow = isOpen ? 'hidden' : '';
|
||||
|
||||
const isOpen = menuBtn.classList.toggle("active");
|
||||
|
||||
menuBtn.setAttribute("aria-expanded", isOpen.toString());
|
||||
menuPanel.classList.toggle("open", isOpen);
|
||||
|
||||
document.body.style.overflow = isOpen ? "hidden" : "";
|
||||
}
|
||||
|
||||
menuBtn?.addEventListener('click', toggleMenu);
|
||||
|
||||
menuItems.forEach(item => {
|
||||
item.addEventListener('click', toggleMenu);
|
||||
|
||||
menuBtn?.addEventListener("click", toggleMenu);
|
||||
|
||||
menuItems.forEach((item) => {
|
||||
item.addEventListener("click", toggleMenu);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && menuPanel?.classList.contains('open')) {
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && menuPanel?.classList.contains("open")) {
|
||||
toggleMenu();
|
||||
}
|
||||
});
|
||||
@ -105,7 +101,7 @@ const navItems = [
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
.menu-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -123,31 +119,33 @@ const navItems = [
|
||||
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;
|
||||
transition:
|
||||
transform 0.3s ease,
|
||||
opacity 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.menu-btn.active .bar:nth-child(1) {
|
||||
transform: translateY(8px) rotate(45deg);
|
||||
}
|
||||
|
||||
|
||||
.menu-btn.active .bar:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.menu-btn.active .bar:nth-child(3) {
|
||||
transform: translateY(-8px) rotate(-45deg);
|
||||
}
|
||||
|
||||
|
||||
.menu-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@ -163,12 +161,12 @@ const navItems = [
|
||||
transition: all 0.3s ease;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
.menu-panel.open {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
.menu-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -176,14 +174,14 @@ const navItems = [
|
||||
height: 100%;
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
|
||||
.menu-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
|
||||
.menu-item {
|
||||
display: block;
|
||||
padding: 12px 24px;
|
||||
@ -195,12 +193,12 @@ const navItems = [
|
||||
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;
|
||||
@ -209,7 +207,7 @@ const navItems = [
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
|
||||
.contact-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -222,11 +220,11 @@ const navItems = [
|
||||
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;
|
||||
@ -235,14 +233,14 @@ const navItems = [
|
||||
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;
|
||||
@ -254,8 +252,8 @@ const navItems = [
|
||||
color: white;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
|
||||
.social-links a:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,45 +1,52 @@
|
||||
---
|
||||
import FadeIn from '../ui/FadeIn.astro';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { SiPhp, SiJavascript, SiMysql, SiReact } from 'react-icons/si';
|
||||
import { FaGithub, FaLinkedin, FaGlobe, FaPalette } from 'react-icons/fa';
|
||||
import { BsLightningChargeFill } from 'react-icons/bs';
|
||||
import { IoServerOutline } from 'react-icons/io5';
|
||||
import { Image } from 'astro:assets';
|
||||
import FadeIn from "../ui/FadeIn.astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { SiPhp, SiJavascript, SiMysql, SiReact } from "react-icons/si";
|
||||
import { FaGithub, FaLinkedin, FaGlobe, FaPalette } from "react-icons/fa";
|
||||
import { BsLightningChargeFill } from "react-icons/bs";
|
||||
import { IoServerOutline } from "react-icons/io5";
|
||||
import { Image } from "astro:assets";
|
||||
|
||||
const technologies = [
|
||||
{ name: 'PHP', icon: SiPhp },
|
||||
{ name: 'JavaScript', icon: SiJavascript },
|
||||
{ name: 'MySQL', icon: SiMysql },
|
||||
{ name: 'React', icon: SiReact }
|
||||
{ name: "PHP", icon: SiPhp },
|
||||
{ name: "JavaScript", icon: SiJavascript },
|
||||
{ name: "MySQL", icon: SiMysql },
|
||||
{ name: "React", icon: SiReact },
|
||||
];
|
||||
|
||||
const interests = [
|
||||
{ name: 'Web Development', icon: FaGlobe },
|
||||
{ name: 'System Architecture', icon: IoServerOutline },
|
||||
{ name: 'UI/UX Design', icon: FaPalette },
|
||||
{ name: 'Performance Optimization', icon: BsLightningChargeFill }
|
||||
{ name: "Web Development", icon: FaGlobe },
|
||||
{ name: "System Architecture", icon: IoServerOutline },
|
||||
{ name: "UI/UX Design", icon: FaPalette },
|
||||
{ name: "Performance Optimization", icon: BsLightningChargeFill },
|
||||
];
|
||||
---
|
||||
|
||||
<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 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">
|
||||
<FadeIn>
|
||||
<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
|
||||
</h2>
|
||||
<div class="h-px grow theme-border opacity-60"></div>
|
||||
</div>
|
||||
</FadeIn>
|
||||
|
||||
|
||||
<div class="grid md:grid-cols-[1fr_2fr] gap-12 sm:gap-24">
|
||||
<div class="space-y-6 sm:space-y-8">
|
||||
<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
|
||||
src="/profile-image.jpg"
|
||||
alt="Jan-Marlon Leibl - Fullstack Software Developer"
|
||||
@ -47,7 +54,10 @@ const interests = [
|
||||
height="400"
|
||||
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>
|
||||
</FadeIn>
|
||||
<FadeIn delay={0.1}>
|
||||
@ -61,15 +71,18 @@ const interests = [
|
||||
</div>
|
||||
</FadeIn>
|
||||
</div>
|
||||
|
||||
|
||||
<FadeIn delay={0.2}>
|
||||
<div class="space-y-10 sm:space-y-16 font-['Instrument_Sans']">
|
||||
<div class="space-y-6 sm:space-y-8">
|
||||
<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 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>
|
||||
</div>
|
||||
|
||||
@ -82,14 +95,16 @@ const interests = [
|
||||
<div class="h-px grow theme-border opacity-30"></div>
|
||||
</div>
|
||||
<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">
|
||||
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
|
||||
<tech.icon className="w-5 h-5" />
|
||||
</span>
|
||||
{tech.name}
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
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">
|
||||
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
|
||||
<tech.icon className="w-5 h-5" />
|
||||
</span>
|
||||
{tech.name}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -101,21 +116,23 @@ const interests = [
|
||||
<div class="h-px grow theme-border opacity-30"></div>
|
||||
</div>
|
||||
<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">
|
||||
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
|
||||
<interest.icon className="w-5 h-5" />
|
||||
</span>
|
||||
{interest.name}
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
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">
|
||||
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
|
||||
<interest.icon className="w-5 h-5" />
|
||||
</span>
|
||||
{interest.name}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-8 sm:pt-10 flex flex-col sm:flex-row gap-4 sm:gap-6">
|
||||
<a
|
||||
href="https://github.com/AtomicWasTaken"
|
||||
<a
|
||||
href="https://github.com/AtomicWasTaken"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="animate-button w-full text-center px-6 sm:px-8 py-3 sm:py-4 border theme-border rounded-xl theme-button-secondary hover:theme-bg-05 transition-all duration-300 text-sm sm:text-base tracking-tight font-medium flex items-center justify-center gap-2 shadow-sm"
|
||||
@ -124,7 +141,7 @@ const interests = [
|
||||
View GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/in/janmarlonleibl/"
|
||||
href="https://www.linkedin.com/in/janmarlonleibl/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="animate-button w-full text-center px-6 sm:px-8 py-3 sm:py-4 border theme-border rounded-xl theme-button-secondary hover:theme-bg-05 transition-all duration-300 text-sm sm:text-base tracking-tight font-medium flex items-center justify-center gap-2 shadow-sm"
|
||||
@ -143,12 +160,12 @@ const interests = [
|
||||
.animate-button {
|
||||
transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
|
||||
.animate-button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
|
||||
.animate-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,187 +1,150 @@
|
||||
---
|
||||
import FadeIn from '../ui/FadeIn.astro';
|
||||
import FadeIn from "../ui/FadeIn.astro";
|
||||
|
||||
const blogPosts = [
|
||||
{
|
||||
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",
|
||||
slug: "best-practices-modern-php-development",
|
||||
tags: ["PHP", "Development", "Best Practices"]
|
||||
tags: ["PHP", "Development", "Best Practices"],
|
||||
},
|
||||
{
|
||||
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",
|
||||
slug: "building-type-safe-apis-typescript",
|
||||
tags: ["TypeScript", "API", "Frontend"]
|
||||
tags: ["TypeScript", "API", "Frontend"],
|
||||
},
|
||||
{
|
||||
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",
|
||||
slug: "performance-optimization-web-applications",
|
||||
tags: ["Performance", "Web", "Optimization"]
|
||||
}
|
||||
tags: ["Performance", "Web", "Optimization"],
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<section id="blog" class="py-20 sm:py-32 px-4 sm:px-8 relative">
|
||||
<div class="absolute inset-0 theme-bg-gradient opacity-30"></div>
|
||||
|
||||
|
||||
<div class="max-w-7xl mx-auto relative">
|
||||
<FadeIn className="space-y-16">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<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
|
||||
</h2>
|
||||
<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.
|
||||
<<<<<<< HEAD
|
||||
<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>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<button
|
||||
<button
|
||||
class="animate-button inline-flex items-center justify-center gap-2 px-6 py-3 bg-transparent border theme-border theme-text-50 rounded-xl transition-all text-sm tracking-tight font-medium shadow-sm font-['Instrument_Sans'] cursor-not-allowed opacity-75"
|
||||
title="Coming soon!"
|
||||
>
|
||||
View All Posts
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{blogPosts.map((post) => (
|
||||
<div
|
||||
class="flex flex-col h-full theme-accent border theme-border rounded-xl overflow-hidden opacity-80 group"
|
||||
=======
|
||||
<p class="text-white/80 mt-2 font-['Instrument_Sans']">
|
||||
The blog feature is currently in development.
|
||||
</p>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<button
|
||||
class="animate-button inline-flex items-center justify-center gap-2 px-6 py-3 bg-transparent border theme-border theme-text-50 rounded-xl transition-all text-sm tracking-tight font-medium shadow-sm font-['Instrument_Sans'] cursor-not-allowed opacity-75"
|
||||
title="Coming soon!"
|
||||
</div>
|
||||
|
||||
{
|
||||
blogPosts.map((post) => (
|
||||
<a
|
||||
href={`/blog/${post.slug}`}
|
||||
class="flex flex-col h-full theme-accent border theme-border rounded-xl overflow-hidden hover:shadow-lg transition-all duration-300 group"
|
||||
>
|
||||
View All Posts
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="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>
|
||||
<p class="text-white/80 mt-2 font-['Instrument_Sans']">The blog feature is currently in development.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{blogPosts.map((post) => (
|
||||
<a
|
||||
href={`/blog/${post.slug}`}
|
||||
class="flex flex-col h-full theme-accent border theme-border rounded-xl overflow-hidden hover:shadow-lg transition-all duration-300 group"
|
||||
>>>>>>> 69efdf7 (feat: add portfolio, blog, and contact features)
|
||||
>
|
||||
<div class="p-6 sm:p-8 flex flex-col h-full">
|
||||
<div class="flex-grow space-y-4">
|
||||
<div class="space-y-1.5">
|
||||
<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 class="p-6 sm:p-8 flex flex-col h-full">
|
||||
<div class="flex-grow space-y-4">
|
||||
<div class="space-y-1.5">
|
||||
<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>
|
||||
<<<<<<< HEAD
|
||||
<h3 class="text-xl font-bold theme-primary font-['DM_Sans']">{post.title}</h3>
|
||||
=======
|
||||
<h3 class="text-xl font-bold theme-primary group-hover:underline decoration-1 underline-offset-2 font-['DM_Sans']">{post.title}</h3>
|
||||
>>>>>>> 69efdf7 (feat: add portfolio, blog, and contact features)
|
||||
<p class="theme-secondary font-['Instrument_Sans'] text-sm sm:text-base leading-relaxed">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
</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">
|
||||
<span class="text-sm theme-text-70 font-['Instrument_Sans']">{post.date}</span>
|
||||
<<<<<<< HEAD
|
||||
<div class="p-2 rounded-full theme-accent">
|
||||
<svg
|
||||
class="w-4 h-4 theme-primary"
|
||||
=======
|
||||
<div class="p-2 rounded-full theme-accent group-hover:bg-opacity-100 transition-all">
|
||||
<svg
|
||||
class="w-4 h-4 theme-primary transform group-hover:translate-x-1 transition-transform"
|
||||
>>>>>>> 69efdf7 (feat: add portfolio, blog, and contact features)
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
|
||||
<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>
|
||||
<div class="p-2 rounded-full theme-accent group-hover:bg-opacity-100 transition-all">
|
||||
<svg
|
||||
class="w-4 h-4 theme-primary transform group-hover:translate-x-1 transition-transform"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M5 12h14M12 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<<<<<<< HEAD
|
||||
</div>
|
||||
))}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sm:hidden flex justify-center mt-8">
|
||||
<button
|
||||
<button
|
||||
class="animate-button inline-flex items-center justify-center gap-2 px-8 py-4 bg-transparent border theme-border theme-text-50 rounded-xl transition-all text-base tracking-tight font-medium shadow-sm font-['Instrument_Sans'] cursor-not-allowed opacity-75"
|
||||
title="Coming soon!"
|
||||
>
|
||||
View All Posts
|
||||
<svg
|
||||
class="w-5 h-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
<svg
|
||||
class="w-5 h-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
=======
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
>>>>>>> 69efdf7 (feat: add portfolio, blog, and contact features)
|
||||
</FadeIn>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
@ -1,43 +1,53 @@
|
||||
---
|
||||
import FadeIn from '../ui/FadeIn.astro';
|
||||
import FadeIn from "../ui/FadeIn.astro";
|
||||
import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } from "react-icons/fa";
|
||||
---
|
||||
|
||||
<section id="contact" class="py-20 sm:py-32 px-4 sm:px-8 relative">
|
||||
<div class="absolute inset-0 theme-bg-gradient opacity-30"></div>
|
||||
|
||||
|
||||
<div class="max-w-7xl mx-auto relative">
|
||||
<FadeIn className="space-y-16">
|
||||
<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
|
||||
</h2>
|
||||
<p class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-xl max-w-3xl">
|
||||
Have a project in mind or want to chat? Feel free to reach out.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-16">
|
||||
<div class="space-y-8">
|
||||
<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>
|
||||
<label for="name" class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']">Your Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
<label
|
||||
for="name"
|
||||
class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']"
|
||||
>Your Name</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
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 transition-all bg-transparent font-['Instrument_Sans']"
|
||||
required
|
||||
aria-required="true"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']">Your Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
<label
|
||||
for="email"
|
||||
class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']"
|
||||
>Your Email</label
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
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 transition-all bg-transparent font-['Instrument_Sans']"
|
||||
required
|
||||
aria-required="true"
|
||||
@ -45,51 +55,60 @@ import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } fr
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="subject" class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']">Subject</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
name="subject"
|
||||
<label
|
||||
for="subject"
|
||||
class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']"
|
||||
>Subject</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
name="subject"
|
||||
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 transition-all bg-transparent font-['Instrument_Sans']"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="message" class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']">Your Message</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
rows="5"
|
||||
<label
|
||||
for="message"
|
||||
class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']"
|
||||
>Your Message</label
|
||||
>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
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']"
|
||||
required
|
||||
aria-required="true"
|
||||
></textarea>
|
||||
aria-required="true"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
<button
|
||||
type="submit"
|
||||
class="animate-button w-full sm:w-auto inline-flex items-center justify-center gap-2 px-8 py-4 theme-button-primary rounded-xl transition-all text-base tracking-tight font-medium group shadow-sm font-['Instrument_Sans']"
|
||||
>
|
||||
Send Message
|
||||
<svg
|
||||
class="w-5 h-5 transform group-hover:translate-x-1 transition-transform"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
<svg
|
||||
class="w-5 h-5 transform group-hover:translate-x-1 transition-transform"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="form-status" class="mt-4 text-sm hidden font-['Instrument_Sans']"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="space-y-8">
|
||||
<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="flex items-start gap-4">
|
||||
<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>
|
||||
<div>
|
||||
<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 class="flex items-start gap-4">
|
||||
@ -111,31 +134,31 @@ import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } fr
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h3 class="text-2xl font-semibold theme-primary mb-4 font-['DM_Sans']">Connect</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<a
|
||||
href="https://github.com/AtomicWasTaken"
|
||||
target="_blank"
|
||||
<a
|
||||
href="https://github.com/AtomicWasTaken"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub profile"
|
||||
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
|
||||
>
|
||||
<FaGithub className="w-6 h-6" />
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/in/janmarlonleibl/"
|
||||
target="_blank"
|
||||
<a
|
||||
href="https://www.linkedin.com/in/janmarlonleibl/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="LinkedIn profile"
|
||||
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
|
||||
>
|
||||
<FaLinkedin className="w-6 h-6" />
|
||||
</a>
|
||||
<a
|
||||
href="https://jleibl.net"
|
||||
target="_blank"
|
||||
<a
|
||||
href="https://jleibl.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="My website"
|
||||
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
|
||||
@ -151,29 +174,29 @@ import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } fr
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
(window as any).openEmailClient = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const name = formData.get('name') as string;
|
||||
const email = formData.get('email') as string;
|
||||
const subject = formData.get('subject') as string || 'Contact Form Message';
|
||||
const message = formData.get('message') as string;
|
||||
|
||||
const formStatus = document.getElementById('form-status');
|
||||
const name = formData.get("name") as string;
|
||||
const email = formData.get("email") as string;
|
||||
const subject = (formData.get("subject") as string) || "Contact Form Message";
|
||||
const message = formData.get("message") as string;
|
||||
|
||||
const formStatus = document.getElementById("form-status");
|
||||
if (!name || !email || !message) {
|
||||
if (formStatus) {
|
||||
formStatus.textContent = 'Please fill in all required fields.';
|
||||
formStatus.classList.remove('hidden');
|
||||
formStatus.textContent = "Please fill in all required fields.";
|
||||
formStatus.classList.remove("hidden");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const body = `Name: ${name}\nEmail: ${email}\n\n${message}`;
|
||||
window.location.href = `mailto:jleibl@proton.me?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||||
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
@ -1,49 +1,66 @@
|
||||
---
|
||||
import FadeIn from '../ui/FadeIn.astro';
|
||||
import GermanyFlag from '../ui/GermanyFlag.astro';
|
||||
import FadeIn from "../ui/FadeIn.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="max-w-7xl w-full relative pt-8 sm:pt-4">
|
||||
<FadeIn className="space-y-10 sm:space-y-16">
|
||||
<div class="space-y-8 sm:space-y-10">
|
||||
<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="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 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
|
||||
</h2>
|
||||
<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
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p 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
|
||||
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>
|
||||
<div class="flex flex-col sm:flex-row gap-4 sm:gap-5">
|
||||
<a
|
||||
<a
|
||||
href="#work"
|
||||
class="animate-button w-full sm:w-auto text-center inline-flex items-center justify-center gap-2 px-6 sm:px-8 py-3 sm:py-4 theme-button-primary rounded-xl transition-all text-sm sm:text-base tracking-tight font-medium group shadow-sm"
|
||||
>
|
||||
View My Work
|
||||
<svg
|
||||
class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
<svg
|
||||
class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
@ -51,49 +68,62 @@ import GermanyFlag from '../ui/GermanyFlag.astro';
|
||||
class="animate-button w-full sm:w-auto text-center inline-flex items-center justify-center gap-2 px-6 sm:px-8 py-3 sm:py-4 border theme-border rounded-xl theme-button-secondary hover:theme-bg-05 transition-all text-sm sm:text-base tracking-tight font-medium group shadow-sm"
|
||||
>
|
||||
Get in Touch
|
||||
<svg
|
||||
class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
<svg
|
||||
class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 class="sm:border-r theme-border last:border-r-0 px-4 flex flex-col items-center justify-center">
|
||||
<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
|
||||
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">
|
||||
<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>
|
||||
<a
|
||||
href="mailto:jleibl@proton.me"
|
||||
<a
|
||||
href="mailto:jleibl@proton.me"
|
||||
class="theme-text-90 hover:theme-primary transition-colors text-base sm:text-lg block w-full"
|
||||
>
|
||||
jleibl@proton.me
|
||||
</a>
|
||||
</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">
|
||||
<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>
|
||||
<span class="theme-text-90 text-base sm:text-lg block w-full">
|
||||
Fullstack Developer
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="px-4 flex flex-col items-center justify-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>
|
||||
<span class="theme-text-90 text-base sm:text-lg block w-full">
|
||||
5+ Years
|
||||
</span>
|
||||
<span class="theme-text-90 text-base sm:text-lg block w-full"> 5+ Years </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -105,12 +135,12 @@ import GermanyFlag from '../ui/GermanyFlag.astro';
|
||||
.animate-button {
|
||||
transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
|
||||
.animate-button:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
|
||||
.animate-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,10 +1,18 @@
|
||||
---
|
||||
import { SiNextdotjs, SiTailwindcss, SiTypescript, SiPhp, 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';
|
||||
import {
|
||||
SiNextdotjs,
|
||||
SiTailwindcss,
|
||||
SiTypescript,
|
||||
SiPhp,
|
||||
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 {
|
||||
title: string;
|
||||
@ -19,21 +27,21 @@ interface Project {
|
||||
|
||||
const getTagIcon = (tag: string) => {
|
||||
switch (tag) {
|
||||
case 'Next.js':
|
||||
case "Next.js":
|
||||
return SiNextdotjs;
|
||||
case 'Tailwind CSS':
|
||||
case "Tailwind CSS":
|
||||
return SiTailwindcss;
|
||||
case 'TypeScript':
|
||||
case "TypeScript":
|
||||
return SiTypescript;
|
||||
case 'PHP':
|
||||
case "PHP":
|
||||
return SiPhp;
|
||||
case 'JavaScript':
|
||||
case "JavaScript":
|
||||
return SiJavascript;
|
||||
case 'MySQL':
|
||||
case "MySQL":
|
||||
return SiMysql;
|
||||
case 'Performance':
|
||||
case "Performance":
|
||||
return BsLightningChargeFill;
|
||||
case 'Discord API':
|
||||
case "Discord API":
|
||||
return SiDiscord;
|
||||
default:
|
||||
return null;
|
||||
@ -41,11 +49,11 @@ const getTagIcon = (tag: string) => {
|
||||
};
|
||||
|
||||
const getProjectIcon = (title: string) => {
|
||||
if (title.includes('ventry')) {
|
||||
if (title.includes("ventry")) {
|
||||
return MdLaunch;
|
||||
} else if (title.includes('ShareUpload')) {
|
||||
} else if (title.includes("ShareUpload")) {
|
||||
return HiOutlineCollection;
|
||||
} else if (title.includes('RestoreM')) {
|
||||
} else if (title.includes("RestoreM")) {
|
||||
return SiDiscord;
|
||||
} else {
|
||||
return FaCode;
|
||||
@ -54,49 +62,49 @@ const getProjectIcon = (title: string) => {
|
||||
|
||||
const projects: Project[] = [
|
||||
{
|
||||
title: 'ventry.host v2',
|
||||
year: '2025',
|
||||
description: 'Free file hosting revamped with a modern design and improved user experience.',
|
||||
icon: getProjectIcon('ventry.host v2'),
|
||||
title: "ventry.host v2",
|
||||
year: "2025",
|
||||
description: "Free file hosting revamped with a modern design and improved user experience.",
|
||||
icon: getProjectIcon("ventry.host v2"),
|
||||
tags: [
|
||||
{ name: 'Next.js', icon: getTagIcon('Next.js') },
|
||||
{ name: 'Tailwind CSS', icon: getTagIcon('Tailwind CSS') },
|
||||
{ name: 'TypeScript', icon: getTagIcon('TypeScript') }
|
||||
]
|
||||
{ name: "Next.js", icon: getTagIcon("Next.js") },
|
||||
{ name: "Tailwind CSS", icon: getTagIcon("Tailwind CSS") },
|
||||
{ name: "TypeScript", icon: getTagIcon("TypeScript") },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'ventry.host',
|
||||
year: '2023',
|
||||
description: 'A free file hosting solution with thousands of daily visitors.',
|
||||
icon: getProjectIcon('ventry.host'),
|
||||
title: "ventry.host",
|
||||
year: "2023",
|
||||
description: "A free file hosting solution with thousands of daily visitors.",
|
||||
icon: getProjectIcon("ventry.host"),
|
||||
tags: [
|
||||
{ name: 'PHP', icon: getTagIcon('PHP') },
|
||||
{ name: 'JavaScript', icon: getTagIcon('JavaScript') },
|
||||
{ name: 'MySQL', icon: getTagIcon('MySQL') }
|
||||
]
|
||||
{ name: "PHP", icon: getTagIcon("PHP") },
|
||||
{ name: "JavaScript", icon: getTagIcon("JavaScript") },
|
||||
{ name: "MySQL", icon: getTagIcon("MySQL") },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'ShareUpload',
|
||||
year: '2022',
|
||||
description: 'High-performance file sharing platform with unlimited storage.',
|
||||
icon: getProjectIcon('ShareUpload'),
|
||||
title: "ShareUpload",
|
||||
year: "2022",
|
||||
description: "High-performance file sharing platform with unlimited storage.",
|
||||
icon: getProjectIcon("ShareUpload"),
|
||||
tags: [
|
||||
{ name: 'PHP', icon: getTagIcon('PHP') },
|
||||
{ name: 'MySQL', icon: getTagIcon('MySQL') },
|
||||
{ name: 'Performance', icon: getTagIcon('Performance') }
|
||||
]
|
||||
{ name: "PHP", icon: getTagIcon("PHP") },
|
||||
{ name: "MySQL", icon: getTagIcon("MySQL") },
|
||||
{ name: "Performance", icon: getTagIcon("Performance") },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'RestoreM',
|
||||
year: '2023',
|
||||
description: 'Discord server backup and restoration service.',
|
||||
icon: getProjectIcon('RestoreM'),
|
||||
title: "RestoreM",
|
||||
year: "2023",
|
||||
description: "Discord server backup and restoration service.",
|
||||
icon: getProjectIcon("RestoreM"),
|
||||
tags: [
|
||||
{ name: 'PHP', icon: getTagIcon('PHP') },
|
||||
{ name: 'MySQL', icon: getTagIcon('MySQL') },
|
||||
{ name: 'Discord API', icon: getTagIcon('Discord API') }
|
||||
]
|
||||
}
|
||||
{ name: "PHP", icon: getTagIcon("PHP") },
|
||||
{ name: "MySQL", icon: getTagIcon("MySQL") },
|
||||
{ name: "Discord API", icon: getTagIcon("Discord API") },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const sortedProjects = [...projects].sort((a, b) => {
|
||||
@ -106,74 +114,78 @@ const sortedProjects = [...projects].sort((a, b) => {
|
||||
|
||||
<section id="work" class="py-24 sm:py-40 px-4 sm:px-8 relative">
|
||||
<div class="absolute inset-0 theme-bg-gradient opacity-30"></div>
|
||||
|
||||
|
||||
<div class="max-w-(--breakpoint-xl) mx-auto relative">
|
||||
<FadeIn>
|
||||
<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
|
||||
</span>
|
||||
<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
|
||||
</h2>
|
||||
<div class="h-px grow theme-border opacity-60"></div>
|
||||
</div>
|
||||
</div>
|
||||
</FadeIn>
|
||||
|
||||
|
||||
<div class="grid gap-20 sm:gap-28">
|
||||
{sortedProjects.map((project, index) => (
|
||||
<FadeIn delay={index * 0.1}>
|
||||
<div class="group relative">
|
||||
<div class="absolute top-0 left-0 right-0 flex items-center gap-4">
|
||||
<div class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium py-2 pr-4">
|
||||
{project.year}
|
||||
</div>
|
||||
<div class="h-px grow theme-border opacity-30"></div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{
|
||||
sortedProjects.map((project, index) => (
|
||||
<FadeIn delay={index * 0.1}>
|
||||
<div class="group relative">
|
||||
<div class="absolute top-0 left-0 right-0 flex items-center gap-4">
|
||||
<div class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium py-2 pr-4">
|
||||
{project.year}
|
||||
</div>
|
||||
<div class="h-px grow theme-border opacity-30" />
|
||||
</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">
|
||||
<h4 class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium">
|
||||
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" />
|
||||
<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 class="space-y-6 sm:space-y-10">
|
||||
<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">
|
||||
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>
|
||||
{tag.name}
|
||||
{tagIndex !== project.tags.length - 1 && (
|
||||
<span class="ml-2 opacity-0">•</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
</FadeIn>
|
||||
))}
|
||||
<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>
|
||||
</FadeIn>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
@ -2,22 +2,17 @@
|
||||
interface Props {
|
||||
className?: string;
|
||||
delay?: number;
|
||||
direction?: 'up' | 'down' | 'left' | 'right';
|
||||
direction?: "up" | "down" | "left" | "right";
|
||||
distance?: number;
|
||||
}
|
||||
|
||||
const {
|
||||
className = "",
|
||||
delay = 0,
|
||||
direction = 'up',
|
||||
distance = 20
|
||||
} = Astro.props;
|
||||
const { className = "", delay = 0, direction = "up", distance = 20 } = Astro.props;
|
||||
|
||||
const directionMap = {
|
||||
up: { x: 0, y: distance },
|
||||
down: { x: 0, y: -distance },
|
||||
left: { x: distance, y: 0 },
|
||||
right: { x: -distance, y: 0 }
|
||||
right: { x: -distance, y: 0 },
|
||||
};
|
||||
|
||||
const { x, y } = directionMap[direction];
|
||||
@ -32,9 +27,11 @@ const uniqueId = `fade-in-${Math.random().toString(36).substring(2, 9)}`;
|
||||
.fade-in {
|
||||
opacity: 0;
|
||||
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 {
|
||||
opacity: 1;
|
||||
transform: translate(0px, 0px);
|
||||
@ -42,25 +39,28 @@ const uniqueId = `fade-in-${Math.random().toString(36).substring(2, 9)}`;
|
||||
</style>
|
||||
|
||||
<script define:vars={{ uniqueId, x, y, delay }}>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const element = document.getElementById(uniqueId);
|
||||
if (element) {
|
||||
element.style.setProperty('--start-x', `${x}px`);
|
||||
element.style.setProperty('--start-y', `${y}px`);
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
setTimeout(() => {
|
||||
element.classList.add('visible');
|
||||
}, delay * 1000);
|
||||
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.1, rootMargin: "0px 0px -10% 0px" });
|
||||
|
||||
element.style.setProperty("--start-x", `${x}px`);
|
||||
element.style.setProperty("--start-y", `${y}px`);
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
setTimeout(() => {
|
||||
element.classList.add("visible");
|
||||
}, delay * 1000);
|
||||
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: "0px 0px -10% 0px" }
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div
|
||||
<div
|
||||
id="germany-flag"
|
||||
role="group"
|
||||
aria-label="Theme switcher with German flag colors"
|
||||
@ -29,30 +29,30 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const themeButtons = document.querySelectorAll('.theme-button');
|
||||
const currentTheme = localStorage.getItem('theme') || 'black';
|
||||
|
||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
||||
document.getElementById(`theme-${currentTheme}`)?.classList.add('ring-1', 'ring-white/20');
|
||||
|
||||
themeButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const theme = button.getAttribute('data-theme');
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const themeButtons = document.querySelectorAll(".theme-button");
|
||||
const currentTheme = localStorage.getItem("theme") || "black";
|
||||
|
||||
document.documentElement.setAttribute("data-theme", currentTheme);
|
||||
document.getElementById(`theme-${currentTheme}`)?.classList.add("ring-1", "ring-white/20");
|
||||
|
||||
themeButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const theme = button.getAttribute("data-theme");
|
||||
if (!theme) return;
|
||||
|
||||
themeButtons.forEach(btn => btn.classList.remove('ring-1', 'ring-white/20'));
|
||||
|
||||
button.classList.add('ring-1', 'ring-white/20');
|
||||
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
|
||||
const themeChangedEvent = new CustomEvent('themeChanged', {
|
||||
detail: { theme }
|
||||
|
||||
themeButtons.forEach((btn) => btn.classList.remove("ring-1", "ring-white/20"));
|
||||
|
||||
button.classList.add("ring-1", "ring-white/20");
|
||||
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
localStorage.setItem("theme", theme);
|
||||
|
||||
const themeChangedEvent = new CustomEvent("themeChanged", {
|
||||
detail: { theme },
|
||||
});
|
||||
document.dispatchEvent(themeChangedEvent);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
@ -1,8 +1,4 @@
|
||||
<a
|
||||
href="#main-content"
|
||||
class="skip-to-content"
|
||||
aria-label="Skip to main content"
|
||||
>
|
||||
<a href="#main-content" class="skip-to-content" aria-label="Skip to main content">
|
||||
<span class="font-['Instrument_Sans']">Skip to content</span>
|
||||
</a>
|
||||
|
||||
@ -18,7 +14,7 @@
|
||||
z-index: 100;
|
||||
transition: top 0.3s ease-out;
|
||||
font-weight: 500;
|
||||
font-family: 'Instrument Sans', sans-serif;
|
||||
font-family: "Instrument Sans", sans-serif;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
@ -40,11 +36,11 @@
|
||||
color: #fbbf24;
|
||||
border-color: rgba(251, 191, 36, 0.15);
|
||||
}
|
||||
|
||||
|
||||
/* Dark mode support */
|
||||
:root.dark .skip-to-content {
|
||||
background: #0A0A0A;
|
||||
background: #0a0a0a;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
@ -1,23 +1,23 @@
|
||||
---
|
||||
import Header from '../components/layout/Header.astro';
|
||||
import Meta from '../components/layout/Meta.astro';
|
||||
import SkipToContent from '../components/ui/SkipToContent.astro';
|
||||
import Hero from '../components/sections/Hero.astro';
|
||||
import About from '../components/sections/About.astro';
|
||||
import Work from '../components/sections/Work.astro';
|
||||
import BlogPreview from '../components/sections/BlogPreview.astro';
|
||||
import Contact from '../components/sections/Contact.astro';
|
||||
import Footer from '../components/layout/Footer.astro';
|
||||
import Header from "../components/layout/Header.astro";
|
||||
import Meta from "../components/layout/Meta.astro";
|
||||
import SkipToContent from "../components/ui/SkipToContent.astro";
|
||||
import Hero from "../components/sections/Hero.astro";
|
||||
import About from "../components/sections/About.astro";
|
||||
import Work from "../components/sections/Work.astro";
|
||||
import BlogPreview from "../components/sections/BlogPreview.astro";
|
||||
import Contact from "../components/sections/Contact.astro";
|
||||
import Footer from "../components/layout/Footer.astro";
|
||||
|
||||
import '../styles/global.css';
|
||||
import '../styles/theme.css';
|
||||
import "../styles/global.css";
|
||||
import "../styles/theme.css";
|
||||
|
||||
import '@fontsource/dm-sans/400.css';
|
||||
import '@fontsource/dm-sans/500.css';
|
||||
import '@fontsource/dm-sans/600.css';
|
||||
import '@fontsource/dm-sans/700.css';
|
||||
import '@fontsource/instrument-sans/400.css';
|
||||
import '@fontsource/instrument-sans/500.css';
|
||||
import "@fontsource/dm-sans/400.css";
|
||||
import "@fontsource/dm-sans/500.css";
|
||||
import "@fontsource/dm-sans/600.css";
|
||||
import "@fontsource/dm-sans/700.css";
|
||||
import "@fontsource/instrument-sans/400.css";
|
||||
import "@fontsource/instrument-sans/500.css";
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
@ -41,78 +41,78 @@ import '@fontsource/instrument-sans/500.css';
|
||||
</html>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const appRoot = document.getElementById('app-root');
|
||||
const themeButtons = document.querySelectorAll('.theme-button');
|
||||
|
||||
const currentTheme = localStorage.getItem('colorTheme') || 'black';
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const appRoot = document.getElementById("app-root");
|
||||
const themeButtons = document.querySelectorAll(".theme-button");
|
||||
|
||||
const currentTheme = localStorage.getItem("colorTheme") || "black";
|
||||
|
||||
if (appRoot) {
|
||||
appRoot.classList.remove('black', 'red', 'gold');
|
||||
appRoot.classList.remove("black", "red", "gold");
|
||||
appRoot.classList.add(currentTheme);
|
||||
}
|
||||
|
||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
||||
|
||||
document.getElementById(`theme-${currentTheme}`)?.classList.add('ring-1', 'ring-white/20');
|
||||
|
||||
document.addEventListener('themeChanged', (e) => {
|
||||
|
||||
document.documentElement.setAttribute("data-theme", currentTheme);
|
||||
|
||||
document.getElementById(`theme-${currentTheme}`)?.classList.add("ring-1", "ring-white/20");
|
||||
|
||||
document.addEventListener("themeChanged", (e) => {
|
||||
const customEvent = e as Event & { detail: { theme: string } };
|
||||
const newTheme = customEvent.detail.theme;
|
||||
if (appRoot && newTheme) {
|
||||
appRoot.classList.remove('black', 'red', 'gold');
|
||||
appRoot.classList.remove("black", "red", "gold");
|
||||
appRoot.classList.add(newTheme);
|
||||
|
||||
localStorage.setItem('colorTheme', newTheme);
|
||||
|
||||
localStorage.setItem("colorTheme", newTheme);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Lenis from 'lenis';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
import Lenis from "lenis";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const lenis = new Lenis({
|
||||
duration: 1.2,
|
||||
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||
smoothWheel: true,
|
||||
wheelMultiplier: 1,
|
||||
touchMultiplier: 2,
|
||||
infinite: false
|
||||
infinite: false,
|
||||
});
|
||||
|
||||
|
||||
const anchorLinks = document.querySelectorAll('a[href^="#"]');
|
||||
|
||||
anchorLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
|
||||
anchorLinks.forEach((link) => {
|
||||
link.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = link.getAttribute('href');
|
||||
if (targetId && targetId !== '#') {
|
||||
|
||||
const targetId = link.getAttribute("href");
|
||||
if (targetId && targetId !== "#") {
|
||||
const targetElement = document.querySelector(targetId);
|
||||
if (targetElement) {
|
||||
lenis.scrollTo(targetElement as HTMLElement, {
|
||||
offset: 0,
|
||||
duration: 1.2,
|
||||
immediate: false
|
||||
immediate: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
let animationId: number;
|
||||
|
||||
|
||||
function raf(time: number) {
|
||||
lenis.raf(time);
|
||||
animationId = requestAnimationFrame(raf);
|
||||
}
|
||||
|
||||
|
||||
animationId = requestAnimationFrame(raf);
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.visibilityState === "hidden") {
|
||||
cancelAnimationFrame(animationId);
|
||||
} else {
|
||||
animationId = requestAnimationFrame(raf);
|
||||
@ -122,13 +122,13 @@ import '@fontsource/instrument-sans/500.css';
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if ('loading' in HTMLImageElement.prototype) {
|
||||
const lazyImages = document.querySelectorAll('img[data-src]');
|
||||
lazyImages.forEach(img => {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
if ("loading" in HTMLImageElement.prototype) {
|
||||
const lazyImages = document.querySelectorAll("img[data-src]");
|
||||
lazyImages.forEach((img) => {
|
||||
const imgElement = img as HTMLImageElement;
|
||||
imgElement.src = imgElement.dataset.src || '';
|
||||
imgElement.removeAttribute('data-src');
|
||||
imgElement.src = imgElement.dataset.src || "";
|
||||
imgElement.removeAttribute("data-src");
|
||||
});
|
||||
} else {
|
||||
const lazyImageObserver = new IntersectionObserver((entries) => {
|
||||
@ -137,14 +137,14 @@ import '@fontsource/instrument-sans/500.css';
|
||||
const lazyImage = entry.target as HTMLImageElement;
|
||||
if (lazyImage.dataset.src) {
|
||||
lazyImage.src = lazyImage.dataset.src;
|
||||
lazyImage.removeAttribute('data-src');
|
||||
lazyImage.removeAttribute("data-src");
|
||||
}
|
||||
lazyImageObserver.unobserve(lazyImage);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('img[data-src]').forEach(image => {
|
||||
|
||||
document.querySelectorAll("img[data-src]").forEach((image) => {
|
||||
lazyImageObserver.observe(image);
|
||||
});
|
||||
}
|
||||
@ -152,12 +152,11 @@ import '@fontsource/instrument-sans/500.css';
|
||||
</script>
|
||||
|
||||
<script>
|
||||
if ('serviceWorker' in navigator && import.meta.env.PROD) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/service-worker.js')
|
||||
.catch(error => {
|
||||
console.error('Service worker registration failed:', error);
|
||||
});
|
||||
if ("serviceWorker" in navigator && import.meta.env.PROD) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker.register("/service-worker.js").catch((error) => {
|
||||
console.error("Service worker registration failed:", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
@ -1,72 +1,81 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
border-color: var(--color-gray-200, currentColor);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
*,
|
||||
::after,
|
||||
::before,
|
||||
::backdrop,
|
||||
::file-selector-button {
|
||||
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 {
|
||||
--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;
|
||||
--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;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
@ -1,52 +1,106 @@
|
||||
.black { background: #0A0A0A; }
|
||||
.red { background: #170c0c; }
|
||||
.gold { background: #121107; }
|
||||
.black {
|
||||
background: #0a0a0a;
|
||||
}
|
||||
.red {
|
||||
background: #170c0c;
|
||||
}
|
||||
.gold {
|
||||
background: #121107;
|
||||
}
|
||||
|
||||
.black .theme-primary { color: rgba(255, 255, 255, 0.95); }
|
||||
.red .theme-primary { color: #f87171; }
|
||||
.gold .theme-primary { color: #fbbf24; }
|
||||
.black .theme-primary {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
.red .theme-primary {
|
||||
color: #f87171;
|
||||
}
|
||||
.gold .theme-primary {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.black .theme-secondary { color: rgba(255, 255, 255, 0.7); }
|
||||
.red .theme-secondary { color: rgba(248, 113, 113, 0.85); }
|
||||
.gold .theme-secondary { color: rgba(251, 191, 36, 0.85); }
|
||||
.black .theme-secondary {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.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); }
|
||||
.red .theme-accent { background-color: rgba(248, 113, 113, 0.08); }
|
||||
.gold .theme-accent { background-color: rgba(251, 191, 36, 0.08); }
|
||||
.black .theme-accent {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.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); }
|
||||
.red .theme-border { border-color: rgba(248, 113, 113, 0.15); }
|
||||
.gold .theme-border { border-color: rgba(251, 191, 36, 0.15); }
|
||||
.black .theme-border {
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.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); }
|
||||
.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-40 {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.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); }
|
||||
.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-70 {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.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); }
|
||||
.red .theme-text-90 { color: rgba(248, 113, 113, 0.95); }
|
||||
.gold .theme-text-90 { color: rgba(251, 191, 36, 0.95); }
|
||||
.black .theme-text-90 {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.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); }
|
||||
.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 .theme-bg-05 {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.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);
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.red .nav-glass {
|
||||
.red .nav-glass {
|
||||
background: rgba(23, 12, 12, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(248, 113, 113, 0.1);
|
||||
}
|
||||
.gold .nav-glass {
|
||||
.gold .nav-glass {
|
||||
background: rgba(18, 17, 7, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.1);
|
||||
@ -73,7 +127,7 @@
|
||||
}
|
||||
|
||||
.nav-item::after {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
@ -97,75 +151,75 @@
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.black .theme-bg-gradient {
|
||||
.black .theme-bg-gradient {
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.03), transparent);
|
||||
}
|
||||
.red .theme-bg-gradient {
|
||||
.red .theme-bg-gradient {
|
||||
background: linear-gradient(to bottom, rgba(248, 113, 113, 0.04), transparent);
|
||||
}
|
||||
.gold .theme-bg-gradient {
|
||||
.gold .theme-bg-gradient {
|
||||
background: linear-gradient(to bottom, rgba(251, 191, 36, 0.04), transparent);
|
||||
}
|
||||
|
||||
.black .theme-button-primary {
|
||||
.black .theme-button-primary {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
color: #0A0A0A;
|
||||
color: #0a0a0a;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.red .theme-button-primary {
|
||||
.red .theme-button-primary {
|
||||
background-color: #f87171;
|
||||
color: #0f0606;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.gold .theme-button-primary {
|
||||
.gold .theme-button-primary {
|
||||
background-color: #fbbf24;
|
||||
color: #0f0b02;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.black .theme-button-primary:hover {
|
||||
.black .theme-button-primary:hover {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.red .theme-button-primary:hover {
|
||||
.red .theme-button-primary:hover {
|
||||
background-color: #ef4444;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.gold .theme-button-primary:hover {
|
||||
.gold .theme-button-primary:hover {
|
||||
background-color: #f59e0b;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.black .theme-button-secondary {
|
||||
.black .theme-button-secondary {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.red .theme-button-secondary {
|
||||
.red .theme-button-secondary {
|
||||
border-color: rgba(248, 113, 113, 0.2);
|
||||
color: rgba(248, 113, 113, 1);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.gold .theme-button-secondary {
|
||||
.gold .theme-button-secondary {
|
||||
border-color: rgba(251, 191, 36, 0.2);
|
||||
color: rgba(251, 191, 36, 1);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.black .theme-button-secondary:hover {
|
||||
.black .theme-button-secondary:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.red .theme-button-secondary:hover {
|
||||
.red .theme-button-secondary:hover {
|
||||
background-color: rgba(248, 113, 113, 0.08);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.gold .theme-button-secondary:hover {
|
||||
.gold .theme-button-secondary:hover {
|
||||
background-color: rgba(251, 191, 36, 0.08);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
@ -173,4 +227,4 @@
|
||||
|
||||
.theme-transition {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@ -18,18 +14,10 @@
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"jsxImportSource": "react"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
Reference in New Issue
Block a user