Compare commits
40 Commits
d44cd74d14
...
main
Author | SHA1 | Date | |
---|---|---|---|
e4d7b6a8a7
|
|||
c8467b8b3b
|
|||
5a90f11ce0
|
|||
f328839796
|
|||
f4b911a05b
|
|||
1184cedc2d
|
|||
03bfef7d5b | |||
ed7b3d1d31 | |||
0c1032d0d7 | |||
03dd77724d | |||
9ee5c0ea23 | |||
97a22ccf24 | |||
f854a113d1 | |||
c1124ce45c | |||
fd9545cc10
|
|||
14209935f6
|
|||
31b897c375
|
|||
531f2c1288
|
|||
7e41d57515
|
|||
5b917142f2
|
|||
fc5b43d029
|
|||
1ecdf57df8
|
|||
a129511728
|
|||
75c6a43203
|
|||
92aca15026
|
|||
a6b012d6fd
|
|||
5647574c8e
|
|||
b057943dde
|
|||
64565ce955
|
|||
f10ecb37b2
|
|||
2ad119a261
|
|||
adca2bae72 | |||
a261bbd60c
|
|||
1f2304144c
|
|||
cc4ce23dcd
|
|||
4f719353af
|
|||
a7b400d2a8
|
|||
5dc8215ca6
|
|||
68ce9f3efd
|
|||
eafda93bd0 |
@ -1 +0,0 @@
|
|||||||
export default new Map();
|
|
@ -1 +0,0 @@
|
|||||||
export default new Map();
|
|
156
.astro/content.d.ts
vendored
156
.astro/content.d.ts
vendored
@ -1,156 +0,0 @@
|
|||||||
declare module 'astro:content' {
|
|
||||||
export interface RenderResult {
|
|
||||||
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
|
|
||||||
headings: import('astro').MarkdownHeading[];
|
|
||||||
remarkPluginFrontmatter: Record<string, any>;
|
|
||||||
}
|
|
||||||
interface Render {
|
|
||||||
'.md': Promise<RenderResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RenderedContent {
|
|
||||||
html: string;
|
|
||||||
metadata?: {
|
|
||||||
imagePaths: Array<string>;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'astro:content' {
|
|
||||||
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
|
|
||||||
|
|
||||||
export type CollectionKey = keyof AnyEntryMap;
|
|
||||||
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
|
|
||||||
|
|
||||||
export type ContentCollectionKey = keyof ContentEntryMap;
|
|
||||||
export type DataCollectionKey = keyof DataEntryMap;
|
|
||||||
|
|
||||||
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
|
|
||||||
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
|
|
||||||
ContentEntryMap[C]
|
|
||||||
>['slug'];
|
|
||||||
|
|
||||||
export type ReferenceDataEntry<
|
|
||||||
C extends CollectionKey,
|
|
||||||
E extends keyof DataEntryMap[C] = string,
|
|
||||||
> = {
|
|
||||||
collection: C;
|
|
||||||
id: E;
|
|
||||||
};
|
|
||||||
export type ReferenceContentEntry<
|
|
||||||
C extends keyof ContentEntryMap,
|
|
||||||
E extends ValidContentEntrySlug<C> | (string & {}) = string,
|
|
||||||
> = {
|
|
||||||
collection: C;
|
|
||||||
slug: E;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @deprecated Use `getEntry` instead. */
|
|
||||||
export function getEntryBySlug<
|
|
||||||
C extends keyof ContentEntryMap,
|
|
||||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
|
||||||
>(
|
|
||||||
collection: C,
|
|
||||||
// Note that this has to accept a regular string too, for SSR
|
|
||||||
entrySlug: E,
|
|
||||||
): E extends ValidContentEntrySlug<C>
|
|
||||||
? Promise<CollectionEntry<C>>
|
|
||||||
: Promise<CollectionEntry<C> | undefined>;
|
|
||||||
|
|
||||||
/** @deprecated Use `getEntry` instead. */
|
|
||||||
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
|
|
||||||
collection: C,
|
|
||||||
entryId: E,
|
|
||||||
): Promise<CollectionEntry<C>>;
|
|
||||||
|
|
||||||
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
|
|
||||||
collection: C,
|
|
||||||
filter?: (entry: CollectionEntry<C>) => entry is E,
|
|
||||||
): Promise<E[]>;
|
|
||||||
export function getCollection<C extends keyof AnyEntryMap>(
|
|
||||||
collection: C,
|
|
||||||
filter?: (entry: CollectionEntry<C>) => unknown,
|
|
||||||
): Promise<CollectionEntry<C>[]>;
|
|
||||||
|
|
||||||
export function getEntry<
|
|
||||||
C extends keyof ContentEntryMap,
|
|
||||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
|
||||||
>(
|
|
||||||
entry: ReferenceContentEntry<C, E>,
|
|
||||||
): E extends ValidContentEntrySlug<C>
|
|
||||||
? Promise<CollectionEntry<C>>
|
|
||||||
: Promise<CollectionEntry<C> | undefined>;
|
|
||||||
export function getEntry<
|
|
||||||
C extends keyof DataEntryMap,
|
|
||||||
E extends keyof DataEntryMap[C] | (string & {}),
|
|
||||||
>(
|
|
||||||
entry: ReferenceDataEntry<C, E>,
|
|
||||||
): E extends keyof DataEntryMap[C]
|
|
||||||
? Promise<DataEntryMap[C][E]>
|
|
||||||
: Promise<CollectionEntry<C> | undefined>;
|
|
||||||
export function getEntry<
|
|
||||||
C extends keyof ContentEntryMap,
|
|
||||||
E extends ValidContentEntrySlug<C> | (string & {}),
|
|
||||||
>(
|
|
||||||
collection: C,
|
|
||||||
slug: E,
|
|
||||||
): E extends ValidContentEntrySlug<C>
|
|
||||||
? Promise<CollectionEntry<C>>
|
|
||||||
: Promise<CollectionEntry<C> | undefined>;
|
|
||||||
export function getEntry<
|
|
||||||
C extends keyof DataEntryMap,
|
|
||||||
E extends keyof DataEntryMap[C] | (string & {}),
|
|
||||||
>(
|
|
||||||
collection: C,
|
|
||||||
id: E,
|
|
||||||
): E extends keyof DataEntryMap[C]
|
|
||||||
? string extends keyof DataEntryMap[C]
|
|
||||||
? Promise<DataEntryMap[C][E]> | undefined
|
|
||||||
: Promise<DataEntryMap[C][E]>
|
|
||||||
: Promise<CollectionEntry<C> | undefined>;
|
|
||||||
|
|
||||||
/** Resolve an array of entry references from the same collection */
|
|
||||||
export function getEntries<C extends keyof ContentEntryMap>(
|
|
||||||
entries: ReferenceContentEntry<C, ValidContentEntrySlug<C>>[],
|
|
||||||
): Promise<CollectionEntry<C>[]>;
|
|
||||||
export function getEntries<C extends keyof DataEntryMap>(
|
|
||||||
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
|
|
||||||
): Promise<CollectionEntry<C>[]>;
|
|
||||||
|
|
||||||
export function render<C extends keyof AnyEntryMap>(
|
|
||||||
entry: AnyEntryMap[C][string],
|
|
||||||
): Promise<RenderResult>;
|
|
||||||
|
|
||||||
export function reference<C extends keyof AnyEntryMap>(
|
|
||||||
collection: C,
|
|
||||||
): import('astro/zod').ZodEffects<
|
|
||||||
import('astro/zod').ZodString,
|
|
||||||
C extends keyof ContentEntryMap
|
|
||||||
? ReferenceContentEntry<C, ValidContentEntrySlug<C>>
|
|
||||||
: ReferenceDataEntry<C, keyof DataEntryMap[C]>
|
|
||||||
>;
|
|
||||||
// Allow generic `string` to avoid excessive type errors in the config
|
|
||||||
// if `dev` is not running to update as you edit.
|
|
||||||
// Invalid collection names will be caught at build time.
|
|
||||||
export function reference<C extends string>(
|
|
||||||
collection: C,
|
|
||||||
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
|
|
||||||
|
|
||||||
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
|
|
||||||
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
|
|
||||||
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
|
|
||||||
>;
|
|
||||||
|
|
||||||
type ContentEntryMap = {
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
type DataEntryMap = {
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
type AnyEntryMap = ContentEntryMap & DataEntryMap;
|
|
||||||
|
|
||||||
export type ContentConfig = typeof import("../src/content.config.mjs");
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.4.2","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"static\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":false,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[]},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":\"shiki\",\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"responsiveImages\":false,\"serializeConfig\":false},\"legacy\":{\"collections\":false}}"]
|
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"_variables": {
|
|
||||||
"lastUpdateCheck": 1741520469255
|
|
||||||
}
|
|
||||||
}
|
|
2
.astro/types.d.ts
vendored
2
.astro/types.d.ts
vendored
@ -1,2 +0,0 @@
|
|||||||
/// <reference types="astro/client" />
|
|
||||||
/// <reference path="content.d.ts" />
|
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
# astro
|
# astro
|
||||||
/dist
|
/dist
|
||||||
|
.astro
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
@ -20,6 +21,9 @@
|
|||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
|
||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/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,7 +1,7 @@
|
|||||||
# jleibl.net - Personal Portfolio Website
|
# jleibl.net - Personal Portfolio Website
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="public/profile-image.jpg" alt="Jan-Marlon Leibl" width="150" style="border-radius: 50%;" />
|
<img src="public/profile-image-futu-style.jpg" alt="Jan-Marlon Leibl" width="150" style="border-radius: 50%;" />
|
||||||
<h3>Jan-Marlon Leibl</h3>
|
<h3>Jan-Marlon Leibl</h3>
|
||||||
<p>Fullstack Software Developer | PHP & TypeScript Expert</p>
|
<p>Fullstack Software Developer | PHP & TypeScript Expert</p>
|
||||||
<a href="https://jleibl.net" target="_blank">Live Website</a>
|
<a href="https://jleibl.net" target="_blank">Live Website</a>
|
||||||
|
@ -1,43 +1,77 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from "astro/config";
|
||||||
|
|
||||||
import react from '@astrojs/react';
|
import react from "@astrojs/react";
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import icon from 'astro-icon';
|
import icon from "astro-icon";
|
||||||
|
import sitemap from "@astrojs/sitemap";
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
site: "https://jleibl.net",
|
||||||
integrations: [
|
integrations: [
|
||||||
react(),
|
react(),
|
||||||
|
sitemap({
|
||||||
|
changefreq: "weekly",
|
||||||
|
priority: 1.0,
|
||||||
|
lastmod: new Date("2025-05-16"),
|
||||||
|
customPages: [
|
||||||
|
"https://jleibl.net/#about",
|
||||||
|
"https://jleibl.net/#work",
|
||||||
|
"https://jleibl.net/#hobby",
|
||||||
|
"https://jleibl.net/#contact",
|
||||||
|
],
|
||||||
|
i18n: {
|
||||||
|
defaultLocale: "en",
|
||||||
|
locales: {
|
||||||
|
en: "en-US",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serialize: (item) => {
|
||||||
|
if (item.url === "https://jleibl.net/") {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
img: [
|
||||||
|
{
|
||||||
|
url: "https://jleibl.net/profile-image-futu-style.jpg",
|
||||||
|
caption: "Jan-Marlon Leibl - Fullstack Developer",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
},
|
||||||
|
xslURL: "/sitemap.xsl",
|
||||||
|
}),
|
||||||
icon({
|
icon({
|
||||||
include: {
|
include: {
|
||||||
ph: ['*'],
|
ph: ["*"],
|
||||||
logos: ['*'],
|
logos: ["*"],
|
||||||
mdi: ['*']
|
mdi: ["*"],
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: "0.0.0.0",
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
cors: {
|
cors: {
|
||||||
origin: '*',
|
origin: "*",
|
||||||
methods: ['GET'],
|
methods: ["GET"],
|
||||||
allowedHeaders: ['X-Requested-With']
|
allowedHeaders: ["X-Requested-With"],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
preview: {
|
preview: {
|
||||||
host: '0.0.0.0',
|
host: "0.0.0.0",
|
||||||
allowedHosts: ['jleibl.net'],
|
allowedHosts: ["jleibl.net"],
|
||||||
port: 4321,
|
port: 4321,
|
||||||
cors: {
|
cors: {
|
||||||
origin: '*',
|
origin: "*",
|
||||||
methods: ['GET'],
|
methods: ["GET"],
|
||||||
allowedHeaders: ['X-Requested-With']
|
allowedHeaders: ["X-Requested-With"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
@ -11,6 +11,7 @@ const compat = new FlatCompat({
|
|||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
|
...compat.extends("prettier"),
|
||||||
];
|
];
|
||||||
|
|
||||||
export default eslintConfig;
|
export default eslintConfig;
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import type { NextConfig } from "next";
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
|
||||||
/* config options here */
|
|
||||||
reactStrictMode: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
45
package.json
45
package.json
@ -7,37 +7,36 @@
|
|||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"start": "astro dev --host",
|
"start": "astro dev --host",
|
||||||
"lint": "astro check"
|
"lint": "astro check",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.4",
|
||||||
"@astrojs/react": "^4.2.1",
|
"@astrojs/react": "4.3.0",
|
||||||
"@fontsource/dm-sans": "^5.1.1",
|
"@astrojs/sitemap": "^3.4.0",
|
||||||
"@fontsource/instrument-sans": "^5.1.1",
|
"@fontsource/dm-sans": "^5.2.5",
|
||||||
|
"@fontsource/instrument-sans": "^5.2.5",
|
||||||
"@iconify-json/logos": "^1.2.4",
|
"@iconify-json/logos": "^1.2.4",
|
||||||
"@iconify-json/mdi": "^1.2.3",
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
"@iconify-json/ph": "^1.2.2",
|
"@iconify-json/ph": "^1.2.2",
|
||||||
"@studio-freight/lenis": "^1.0.42",
|
"@tailwindcss/postcss": "^4.1.2",
|
||||||
"@tailwindcss/postcss": "^4.0.12",
|
"@tailwindcss/vite": "^4.1.2",
|
||||||
"@tailwindcss/vite": "^4.0.12",
|
"astro": "5.9.2",
|
||||||
"astro": "^5.4.2",
|
|
||||||
"astro-icon": "^1.1.5",
|
"astro-icon": "^1.1.5",
|
||||||
"framer-motion": "^12.4.7",
|
"framer-motion": "^12.6.3",
|
||||||
"next": "15.1.7",
|
"lenis": "^1.2.3",
|
||||||
"react": "^19.0.0",
|
"react-icons": "^5.5.0"
|
||||||
"react-dom": "^19.0.0",
|
|
||||||
"react-icons": "^5.5.0",
|
|
||||||
"react-intersection-observer": "^9.15.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^22.0.0",
|
||||||
"@types/react": "^19.0.10",
|
"eslint": "^9.23.0",
|
||||||
"@types/react-dom": "^19.0.4",
|
"eslint-config-prettier": "^10.1.1",
|
||||||
"eslint": "^9",
|
"postcss": "^8.5.3",
|
||||||
"eslint-config-next": "15.1.7",
|
"prettier": "^3.5.3",
|
||||||
"postcss": "^8",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"tailwindcss": "^4.0.12",
|
"tailwindcss": "^4.1.2",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.8.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
public/_headers
Normal file
23
public/_headers
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
X-Frame-Options: DENY
|
||||||
|
X-XSS-Protection: 1; mode=block
|
||||||
|
Referrer-Policy: strict-origin-when-cross-origin
|
||||||
|
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
||||||
|
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' *.googleapis.com *.gstatic.com; style-src 'self' 'unsafe-inline' *.googleapis.com; img-src 'self' data: https:; font-src 'self' data: *.googleapis.com *.gstatic.com; connect-src 'self' *.googleapis.com; frame-ancestors 'none';
|
||||||
|
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||||
|
|
||||||
|
/*.html
|
||||||
|
Cache-Control: public, max-age=0, must-revalidate
|
||||||
|
|
||||||
|
/assets/*
|
||||||
|
Cache-Control: public, max-age=31536000, immutable
|
||||||
|
|
||||||
|
/images/*
|
||||||
|
Cache-Control: public, max-age=31536000, immutable
|
||||||
|
|
||||||
|
/*.js
|
||||||
|
Cache-Control: public, max-age=31536000, immutable
|
||||||
|
|
||||||
|
/*.css
|
||||||
|
Cache-Control: public, max-age=31536000, immutable
|
60
public/manifest.json
Normal file
60
public/manifest.json
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "Jan-Marlon Leibl - Fullstack Developer",
|
||||||
|
"short_name": "Jan-Marlon Leibl",
|
||||||
|
"description": "Experienced Fullstack Developer specializing in PHP and TypeScript. Creating high-performance web applications and digital experiences with modern technologies.",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0A0A0A",
|
||||||
|
"theme_color": "#0A0A0A",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "/screenshots/mobile.png",
|
||||||
|
"sizes": "375x812",
|
||||||
|
"type": "image/png",
|
||||||
|
"form_factor": "narrow",
|
||||||
|
"label": "Jan-Marlon Leibl's Portfolio (Mobile)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/screenshots/desktop.png",
|
||||||
|
"sizes": "1280x800",
|
||||||
|
"type": "image/png",
|
||||||
|
"form_factor": "wide",
|
||||||
|
"label": "Jan-Marlon Leibl's Portfolio (Desktop)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shortcuts": [
|
||||||
|
{
|
||||||
|
"name": "About Me",
|
||||||
|
"short_name": "About",
|
||||||
|
"description": "Learn more about Jan-Marlon Leibl",
|
||||||
|
"url": "/#about"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "My Work",
|
||||||
|
"short_name": "Work",
|
||||||
|
"description": "View Jan-Marlon's portfolio projects",
|
||||||
|
"url": "/#work"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Contact",
|
||||||
|
"short_name": "Contact",
|
||||||
|
"description": "Get in touch with Jan-Marlon",
|
||||||
|
"url": "/#contact"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/profile-image-futu-style.jpg",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/jpeg",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/profile-image-futu-style.jpg",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/jpeg",
|
||||||
|
"purpose": "any"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
public/profile-image-futu-style.jpg
Normal file
BIN
public/profile-image-futu-style.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 275 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.7 MiB |
10
public/robots.txt
Normal file
10
public/robots.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# www.robotstxt.org/
|
||||||
|
# Allow crawling of all content
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# Optimize crawling
|
||||||
|
Crawl-delay: 10
|
||||||
|
|
||||||
|
# Sitemaps
|
||||||
|
Sitemap: https://jleibl.net/sitemap-index.xml
|
158
public/service-worker.js
Normal file
158
public/service-worker.js
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
const CACHE_NAME = "jleibl-cache-v1";
|
||||||
|
const urlsToCache = [
|
||||||
|
"/",
|
||||||
|
"/index.html",
|
||||||
|
"/manifest.json",
|
||||||
|
"/profile-image-futu-style.jpg",
|
||||||
|
"/styles/global.css",
|
||||||
|
"/styles/theme.css",
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache)));
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then((response) => {
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
return fetch(event.request).then((response) => {
|
||||||
|
if (!response || response.status !== 200 || response.type !== "basic") {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
const responseToCache = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
cache.put(event.request, responseToCache);
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((cacheNames) => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map((cacheName) => {
|
||||||
|
if (cacheName !== CACHE_NAME) {
|
||||||
|
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")) {
|
||||||
|
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")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlObj.pathname === "/") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAuthProtectedAsset(url) {
|
||||||
|
const urlPath = new URL(url).pathname;
|
||||||
|
return AUTH_ASSETS.some((asset) => asset === urlPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
if (isAuthProtectedAsset(event.request.url)) {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
const responseToCache = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
cache.put(event.request, responseToCache);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("Fetch failed for auth protected asset:", error);
|
||||||
|
return caches.match(event.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.request.mode === "navigate" || shouldCache(event.request.url)) {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then((response) => {
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchRequest = event.request.clone();
|
||||||
|
|
||||||
|
return fetch(fetchRequest)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.url.includes("cloudflareaccess.com")) {
|
||||||
|
console.log("Request redirected to authentication page:", response.url);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response || response.status !== 200 || response.type !== "basic") {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseToCache = response.clone();
|
||||||
|
|
||||||
|
if (shouldCache(new URL(event.request.url))) {
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
cache.put(event.request, responseToCache);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("Fetch failed:", error);
|
||||||
|
|
||||||
|
if (event.request.mode === "navigate") {
|
||||||
|
return caches.match("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("message", (event) => {
|
||||||
|
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
77
public/sitemap.xsl
Normal file
77
public/sitemap.xsl
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xsl:stylesheet version="2.0"
|
||||||
|
xmlns:html="http://www.w3.org/TR/REC-html40"
|
||||||
|
xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
|
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||||
|
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<title>XML Sitemap - Jan-Marlon Leibl</title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<style type="text/css">
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||||
|
color: #333;
|
||||||
|
max-width: 75rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
tr:hover {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #0066cc;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>XML Sitemap</h1>
|
||||||
|
<p>This is the sitemap for jleibl.net</p>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>URL</th>
|
||||||
|
<th>Priority</th>
|
||||||
|
<th>Change Frequency</th>
|
||||||
|
<th>Last Modified</th>
|
||||||
|
</tr>
|
||||||
|
<xsl:for-each select="sitemap:urlset/sitemap:url">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{sitemap:loc}"><xsl:value-of select="sitemap:loc"/></a>
|
||||||
|
</td>
|
||||||
|
<td><xsl:value-of select="sitemap:priority"/></td>
|
||||||
|
<td><xsl:value-of select="sitemap:changefreq"/></td>
|
||||||
|
<td><xsl:value-of select="sitemap:lastmod"/></td>
|
||||||
|
</tr>
|
||||||
|
</xsl:for-each>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
</xsl:stylesheet>
|
3
renovate.json
Normal file
3
renovate.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
@ -3,12 +3,12 @@ import FadeIn from "../ui/FadeIn.astro";
|
|||||||
import { FaGithub, FaLinkedin, FaEnvelope } from "react-icons/fa";
|
import { FaGithub, FaLinkedin, FaEnvelope } from "react-icons/fa";
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer
|
<footer role="contentinfo" class="mt-20 sm:mt-40 relative">
|
||||||
role="contentinfo"
|
|
||||||
class="mt-20 sm:mt-40 relative"
|
|
||||||
>
|
|
||||||
<div class="absolute inset-0 theme-bg-gradient opacity-30"></div>
|
<div class="absolute inset-0 theme-bg-gradient opacity-30"></div>
|
||||||
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"></div>
|
<div
|
||||||
|
class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="max-w-(--breakpoint-xl) mx-auto px-4 sm:px-8 py-20 sm:py-32 relative">
|
<div class="max-w-(--breakpoint-xl) mx-auto px-4 sm:px-8 py-20 sm:py-32 relative">
|
||||||
<div class="grid gap-16 sm:gap-24">
|
<div class="grid gap-16 sm:gap-24">
|
||||||
|
@ -1,51 +1,85 @@
|
|||||||
---
|
---
|
||||||
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"
|
class="fixed top-0 left-0 right-0 z-40 nav-glass backdrop-blur-lg theme-transition"
|
||||||
role="banner"
|
role="banner"
|
||||||
>
|
>
|
||||||
<div class="max-w-(--breakpoint-xl) mx-auto">
|
<div class="max-w-7xl mx-auto">
|
||||||
<nav class="py-4 px-6 sm:px-8 flex justify-between items-center font-['Instrument_Sans']" role="navigation" aria-label="Main navigation">
|
<nav
|
||||||
|
class="py-4 px-6 sm:px-8 flex justify-between items-center font-['Instrument_Sans']"
|
||||||
|
role="navigation"
|
||||||
|
aria-label="Main navigation"
|
||||||
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<a href="#hero" class="flex items-center gap-2.5 group" aria-label="Home">
|
<a href="#hero" class="flex items-center gap-2.5 group" aria-label="Home">
|
||||||
<div class="relative w-9 h-9 rounded-xl theme-accent flex items-center justify-center border theme-border overflow-hidden group-hover:border-opacity-80 transition-all duration-300 shadow-sm">
|
<div
|
||||||
<span class="text-base font-bold tracking-tight theme-primary theme-transition">JL</span>
|
class="relative w-9 h-9 rounded-xl theme-accent flex items-center justify-center border theme-border overflow-hidden group-hover:border-opacity-80 transition-all duration-300 shadow-sm"
|
||||||
<div class="absolute inset-0 theme-bg-05 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
>
|
||||||
|
<span class="text-base font-bold tracking-tight theme-primary theme-transition">JL</span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 theme-bg-05 opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-base font-medium tracking-tight theme-primary theme-transition hidden sm:block">
|
</div>
|
||||||
|
<span
|
||||||
|
class="text-base font-medium tracking-tight theme-primary theme-transition hidden sm:block"
|
||||||
|
>
|
||||||
Jan-Marlon Leibl
|
Jan-Marlon Leibl
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hidden md:flex items-center">
|
<div class="hidden md:flex items-center">
|
||||||
<div class="flex bg-black/5 backdrop-blur-md overflow-hidden theme-border border divide-x divide-white/5 shadow-sm rounded-xl">
|
<div
|
||||||
{["About", "Work"].map((item, index) => (
|
class="flex bg-black/5 backdrop-blur-md overflow-hidden theme-border border divide-x divide-white/5 shadow-sm rounded-xl"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
["About", "Hobby", "Work", "Contact"].map((item, index) => (
|
||||||
<a
|
<a
|
||||||
href={`#${item.toLowerCase()}`}
|
href={`#${item.toLowerCase()}`}
|
||||||
class={`nav-item px-5 py-2 theme-secondary hover:theme-primary theme-transition ${index === 0 ? "rounded-l-xl rounded-r-none" : "rounded-r-xl rounded-l-none"}`}
|
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()}`}
|
aria-label={`View my ${item.toLowerCase()}`}
|
||||||
>
|
>
|
||||||
{item}
|
{item}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-6">
|
<div class="flex items-center gap-4 ml-6">
|
||||||
<a
|
<a
|
||||||
href="mailto:jleibl@proton.me"
|
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"
|
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"
|
aria-label="Contact me via email"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<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"/>
|
class="w-4 h-4 theme-primary"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
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>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-sm font-medium">Contact</span>
|
<span class="text-sm font-medium">Contact</span>
|
||||||
<div class="w-0 overflow-hidden group-hover:w-4 transition-all duration-300">
|
<div class="w-0 overflow-hidden group-hover:w-4 transition-all duration-300">
|
||||||
<svg class="w-4 h-4 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
class="w-4 h-4 theme-primary"
|
||||||
|
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"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@ -60,35 +94,73 @@ import MobileMenu from './MobileMenu.astro';
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const header = document.querySelector('header');
|
const header = document.querySelector("header");
|
||||||
if (header) {
|
|
||||||
header.style.transform = 'translateY(0)';
|
|
||||||
header.style.opacity = '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastScrollY = window.scrollY;
|
|
||||||
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
if (!header) return;
|
if (!header) return;
|
||||||
|
|
||||||
const currentScrollY = window.scrollY;
|
header.style.transform = "translateY(0)";
|
||||||
|
header.style.opacity = "1";
|
||||||
|
|
||||||
// Apply subtle shadow and border when scrolling down
|
let lastScrollY = window.scrollY;
|
||||||
if (currentScrollY > 20) {
|
const scrollThreshold = 100;
|
||||||
header.classList.add('scrolled');
|
let isHeaderVisible = true;
|
||||||
} else {
|
|
||||||
header.classList.remove('scrolled');
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
let currentSection: string | null = null;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentSection) {
|
||||||
|
navItems.forEach((item) => {
|
||||||
|
const href = item.getAttribute("href")?.substring(1);
|
||||||
|
if (href === currentSection) {
|
||||||
|
item.classList.add("nav-item-active");
|
||||||
|
item.setAttribute("aria-current", "true");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide header when scrolling down, show when scrolling up
|
window.addEventListener("scroll", () => {
|
||||||
if (currentScrollY > lastScrollY && currentScrollY > 100) {
|
const currentScrollY = window.scrollY;
|
||||||
header.style.transform = 'translateY(-100%)';
|
|
||||||
|
if (currentScrollY > 20) {
|
||||||
|
header.classList.add("scrolled");
|
||||||
} else {
|
} else {
|
||||||
header.style.transform = 'translateY(0)';
|
header.classList.remove("scrolled");
|
||||||
}
|
}
|
||||||
|
|
||||||
lastScrollY = currentScrollY;
|
lastScrollY = currentScrollY;
|
||||||
|
|
||||||
|
setActiveNavItem();
|
||||||
|
});
|
||||||
|
|
||||||
|
setActiveNavItem();
|
||||||
|
|
||||||
|
navItems.forEach((item) => {
|
||||||
|
item.addEventListener("keydown", (e) => {
|
||||||
|
if (e instanceof KeyboardEvent && (e.key === "Enter" || e.key === " ")) {
|
||||||
|
e.preventDefault();
|
||||||
|
item.dispatchEvent(new MouseEvent("click"));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -97,15 +169,53 @@ import MobileMenu from './MobileMenu.astro';
|
|||||||
header {
|
header {
|
||||||
transform: translateY(-100px);
|
transform: translateY(-100px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: transform 0.4s ease-out, opacity 0.5s ease-out, border 0.3s ease;
|
transition:
|
||||||
|
transform 0.4s ease-out,
|
||||||
|
opacity 0.5s ease-out,
|
||||||
|
border 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
header.scrolled {
|
header.scrolled {
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
border-radius: 0;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
.red .nav-item::after {
|
||||||
|
background-color: rgba(248, 113, 113, 0.8);
|
||||||
|
}
|
||||||
|
.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>
|
117
src/components/layout/Meta.astro
Normal file
117
src/components/layout/Meta.astro
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
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 defaultImage = "/profile-image-futu-style.jpg";
|
||||||
|
const siteUrl = "https://jleibl.net";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
image?: string;
|
||||||
|
canonicalUrl?: string;
|
||||||
|
type?: "website" | "article";
|
||||||
|
publishedDate?: string;
|
||||||
|
modifiedDate?: string;
|
||||||
|
keywords?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = defaultTitle,
|
||||||
|
description = defaultDescription,
|
||||||
|
image = defaultImage,
|
||||||
|
canonicalUrl = siteUrl,
|
||||||
|
type = "website",
|
||||||
|
publishedDate,
|
||||||
|
modifiedDate,
|
||||||
|
keywords = [
|
||||||
|
"Software Development",
|
||||||
|
"PHP Developer",
|
||||||
|
"TypeScript",
|
||||||
|
"Fullstack Engineer",
|
||||||
|
"Web Development",
|
||||||
|
"System Architecture",
|
||||||
|
"MySQL",
|
||||||
|
"React",
|
||||||
|
"Performance Optimization",
|
||||||
|
],
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const personData = {
|
||||||
|
"@type": "Person",
|
||||||
|
name: "Jan-Marlon Leibl",
|
||||||
|
url: siteUrl,
|
||||||
|
image: `${siteUrl}${defaultImage}`,
|
||||||
|
jobTitle: "Fullstack Software Developer",
|
||||||
|
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"],
|
||||||
|
worksFor: {
|
||||||
|
"@type": "Organization",
|
||||||
|
name: "team neusta",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const structuredDataPerson = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
...personData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const structuredDataWebsite = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": type,
|
||||||
|
headline: title,
|
||||||
|
description: description,
|
||||||
|
image: `${siteUrl}${image}`,
|
||||||
|
url: canonicalUrl,
|
||||||
|
author: personData,
|
||||||
|
...(publishedDate && { datePublished: publishedDate }),
|
||||||
|
...(modifiedDate && { dateModified: modifiedDate }),
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<meta name="title" content={title} />
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<meta name="keywords" content={keywords.join(", ")} />
|
||||||
|
<meta name="author" content="Jan-Marlon Leibl" />
|
||||||
|
<meta name="robots" content="index, follow" />
|
||||||
|
<meta name="color-scheme" content="dark light" />
|
||||||
|
<meta name="theme-color" content="#0A0A0A" />
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
|
||||||
|
<link rel="canonical" href={canonicalUrl} />
|
||||||
|
|
||||||
|
<meta property="og:type" content={type} />
|
||||||
|
<meta property="og:url" content={canonicalUrl} />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:image" content="/profile-image-futu-style.jpg" />
|
||||||
|
<meta property="og:image:width" content="1200" />
|
||||||
|
<meta property="og:image:height" content="630" />
|
||||||
|
<meta property="og:image:alt" content="Jan-Marlon Leibl - Fullstack Software Developer" />
|
||||||
|
<meta property="og:site_name" content="Jan-Marlon Leibl" />
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content={title} />
|
||||||
|
<meta name="twitter:description" content={description} />
|
||||||
|
<meta name="twitter:image" content="/profile-image-futu-style.jpg" />
|
||||||
|
<meta name="twitter:image:alt" content="Jan-Marlon Leibl - Fullstack Software Developer" />
|
||||||
|
|
||||||
|
<link rel="icon" href="/profile-image-futu-style.jpg" sizes="any" />
|
||||||
|
<link rel="icon" href="/profile-image-futu-style.jpg" type="image/svg+xml" />
|
||||||
|
<link rel="apple-touch-icon" href="/profile-image-futu-style.jpg" />
|
||||||
|
|
||||||
|
<script type="application/ld+json" set:html={JSON.stringify(structuredDataPerson)} />
|
||||||
|
<script type="application/ld+json" set:html={JSON.stringify(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="preload" href="/profile-image-futu-style.jpg" as="image" />
|
||||||
|
|
||||||
|
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="dns-prefetch" href="https://fonts.gstatic.com" />
|
@ -1,69 +1,64 @@
|
|||||||
---
|
---
|
||||||
import GermanyFlag from '../ui/GermanyFlag.astro';
|
import { FaGithub, FaLinkedin, FaEnvelope } from "react-icons/fa";
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ label: "About", href: "#about" },
|
||||||
|
{ label: "Hobby", href: "#hobby" },
|
||||||
|
{ label: "Work", href: "#work" },
|
||||||
|
{ label: "Contact", href: "#contact" },
|
||||||
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="mobile-menu">
|
<div class="mobile-menu">
|
||||||
<button
|
<button
|
||||||
id="mobile-menu-toggle"
|
id="menu-btn"
|
||||||
class="p-2.5 theme-accent rounded-xl theme-border border shadow-sm hover:theme-bg-05 transition-all duration-300"
|
class="menu-btn"
|
||||||
aria-label="Open menu"
|
aria-label="Toggle mobile menu"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
|
aria-controls="mobile-menu-panel"
|
||||||
>
|
>
|
||||||
<svg class="w-5 h-5 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<span class="bar"></span>
|
||||||
<path d="M4 6h16M4 12h16M4 18h16"></path>
|
<span class="bar"></span>
|
||||||
</svg>
|
<span class="bar"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div id="mobile-menu-overlay" class="fixed inset-0 bg-black opacity-0 z-90 pointer-events-none transition-opacity duration-300"></div>
|
<div id="mobile-menu-panel" class="menu-panel">
|
||||||
|
<div class="menu-content">
|
||||||
<div id="mobile-menu-panel" class="fixed top-0 bottom-0 right-0 w-full sm:w-80 z-100 h-[100dvh] translate-x-full transition-transform duration-300">
|
<nav class="menu-nav">
|
||||||
<div class="nav-glass w-full h-full flex flex-col shadow-2xl" style="backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);">
|
{
|
||||||
<button
|
navItems.map((item) => (
|
||||||
id="mobile-menu-close"
|
<a href={item.href} class="menu-item">
|
||||||
class="absolute top-4 right-4 p-2.5 theme-accent rounded-xl theme-border border z-110 shadow-sm hover:theme-bg-05 transition-all duration-300"
|
{item.label}
|
||||||
aria-label="Close menu"
|
|
||||||
>
|
|
||||||
<svg class="w-5 h-5 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M6 18L18 6M6 6l12 12"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="w-full h-full p-6 pt-16 flex flex-col overflow-y-auto">
|
|
||||||
<nav class="mb-8">
|
|
||||||
<div class="flex flex-col space-y-3">
|
|
||||||
{[
|
|
||||||
{ href: "#about", label: "About" },
|
|
||||||
{ href: "#work", label: "Work" }
|
|
||||||
].map((item) => (
|
|
||||||
<a
|
|
||||||
href={item.href}
|
|
||||||
class="menu-link p-4 text-xl font-medium theme-primary rounded-xl theme-bg-05 border theme-border flex items-center hover:bg-white/[0.03] transition-all duration-300 shadow-sm"
|
|
||||||
>
|
|
||||||
<span>{item.label}</span>
|
|
||||||
<svg class="ml-auto w-5 h-5 theme-primary opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
))}
|
))
|
||||||
</div>
|
}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="mt-auto">
|
<div class="menu-footer">
|
||||||
<div class="pt-6 border-t theme-border">
|
<a href="mailto:jleibl@proton.me" class="contact-button">
|
||||||
<a
|
<span class="icon-wrapper">
|
||||||
href="mailto:jleibl@proton.me"
|
<FaEnvelope className="w-5 h-5" />
|
||||||
class="flex items-center justify-center gap-3 w-full py-4 px-6 theme-button-primary rounded-xl transition-all duration-300 shadow-sm hover:shadow-md"
|
</span>
|
||||||
>
|
<span>Contact Me</span>
|
||||||
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<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"/>
|
|
||||||
</svg>
|
|
||||||
<span class="font-medium">Contact Me</span>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-8 flex justify-center pb-6 opacity-70">
|
<div class="social-links">
|
||||||
<GermanyFlag />
|
<a
|
||||||
</div>
|
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"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="LinkedIn profile"
|
||||||
|
>
|
||||||
|
<FaLinkedin className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -71,38 +66,194 @@ import GermanyFlag from '../ui/GermanyFlag.astro';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const toggle = document.getElementById('mobile-menu-toggle');
|
const menuBtn = document.getElementById("menu-btn");
|
||||||
const close = document.getElementById('mobile-menu-close');
|
const menuPanel = document.getElementById("mobile-menu-panel");
|
||||||
const overlay = document.getElementById('mobile-menu-overlay');
|
const menuItems = document.querySelectorAll(".menu-item");
|
||||||
const panel = document.getElementById('mobile-menu-panel');
|
|
||||||
const menuLinks = document.querySelectorAll('.menu-link');
|
|
||||||
|
|
||||||
function openMenu() {
|
function toggleMenu() {
|
||||||
document.body.style.overflow = 'hidden';
|
if (!menuBtn || !menuPanel) return;
|
||||||
toggle?.setAttribute('aria-expanded', 'true');
|
|
||||||
overlay?.classList.remove('opacity-0', 'pointer-events-none');
|
const isOpen = menuBtn.classList.toggle("active");
|
||||||
overlay?.classList.add('opacity-90');
|
|
||||||
panel?.classList.remove('translate-x-full');
|
menuBtn.setAttribute("aria-expanded", isOpen.toString());
|
||||||
|
menuPanel.classList.toggle("open", isOpen);
|
||||||
|
|
||||||
|
document.body.style.overflow = isOpen ? "hidden" : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeMenu() {
|
menuBtn?.addEventListener("click", toggleMenu);
|
||||||
document.body.style.overflow = '';
|
|
||||||
toggle?.setAttribute('aria-expanded', 'false');
|
|
||||||
overlay?.classList.add('opacity-0', 'pointer-events-none');
|
|
||||||
overlay?.classList.remove('opacity-90');
|
|
||||||
panel?.classList.add('translate-x-full');
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle?.addEventListener('click', openMenu);
|
menuItems.forEach((item) => {
|
||||||
close?.addEventListener('click', closeMenu);
|
item.addEventListener("click", toggleMenu);
|
||||||
overlay?.addEventListener('click', closeMenu);
|
});
|
||||||
menuLinks.forEach(link => link.addEventListener('click', closeMenu));
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === 'Escape' && toggle?.getAttribute('aria-expanded') === 'true') {
|
if (e.key === "Escape" && menuPanel?.classList.contains("open")) {
|
||||||
closeMenu();
|
toggleMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mobile-menu {
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
z-index: 101;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-btn .bar {
|
||||||
|
display: block;
|
||||||
|
width: 20px;
|
||||||
|
height: 2px;
|
||||||
|
background-color: white;
|
||||||
|
transition:
|
||||||
|
transform 0.3s ease,
|
||||||
|
opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-btn.active .bar:nth-child(1) {
|
||||||
|
transform: translateY(8px) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: rgba(0, 0, 0, 0.97);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-panel.open {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding-top: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 24px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-button:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links a:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,35 +1,40 @@
|
|||||||
---
|
---
|
||||||
import FadeIn from '../ui/FadeIn.astro';
|
import FadeIn from "../ui/FadeIn.astro";
|
||||||
import { Icon } from 'astro-icon/components';
|
import { Icon } from "astro-icon/components";
|
||||||
import { SiPhp, SiJavascript, SiMysql, SiReact } from 'react-icons/si';
|
import { SiPhp, SiJavascript, SiMysql, SiReact, SiTypescript, SiAstro } from "react-icons/si";
|
||||||
import { FaGithub, FaLinkedin, FaGlobe, FaPalette } from 'react-icons/fa';
|
import { FaGithub, FaLinkedin, FaGlobe, FaPalette, FaCamera } from "react-icons/fa";
|
||||||
import { BsLightningChargeFill } from 'react-icons/bs';
|
import { BsLightningChargeFill } from "react-icons/bs";
|
||||||
import { IoServerOutline } from 'react-icons/io5';
|
import { IoServerOutline } from "react-icons/io5";
|
||||||
import { Image } from 'astro:assets';
|
import { Image } from "astro:assets";
|
||||||
|
|
||||||
const technologies = [
|
const technologies = [
|
||||||
{ name: 'PHP', icon: SiPhp },
|
{ name: "TypeScript", icon: SiTypescript },
|
||||||
{ name: 'JavaScript', icon: SiJavascript },
|
{ name: "React", icon: SiReact },
|
||||||
{ name: 'MySQL', icon: SiMysql },
|
{ name: "PHP", icon: SiPhp },
|
||||||
{ name: 'React', icon: SiReact }
|
{ name: "Astro", icon: SiAstro },
|
||||||
];
|
];
|
||||||
|
|
||||||
const interests = [
|
const interests = [
|
||||||
{ name: 'Web Development', icon: FaGlobe },
|
{ name: "Web Development", icon: FaGlobe },
|
||||||
{ name: 'System Architecture', icon: IoServerOutline },
|
{ name: "System Architecture", icon: IoServerOutline },
|
||||||
{ name: 'UI/UX Design', icon: FaPalette },
|
{ name: "UI/UX Design", icon: FaPalette },
|
||||||
{ name: 'Performance Optimization', icon: BsLightningChargeFill }
|
{ name: "Creative Vision", icon: FaCamera },
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<section id="about" class="py-24 sm:py-40 px-4 sm:px-8 relative">
|
<section id="about" class="py-24 sm:py-40 px-4 sm:px-8 relative">
|
||||||
<div class="absolute inset-0 theme-bg-gradient opacity-50"></div>
|
<div class="absolute inset-0 theme-bg-gradient opacity-50"></div>
|
||||||
<div class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"></div>
|
<div
|
||||||
|
class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="max-w-(--breakpoint-xl) mx-auto relative">
|
<div class="max-w-(--breakpoint-xl) mx-auto relative">
|
||||||
<FadeIn>
|
<FadeIn>
|
||||||
<div class="flex items-baseline gap-4 mb-12 sm:mb-24">
|
<div class="flex items-baseline gap-4 mb-12 sm:mb-24">
|
||||||
<h2 class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary">
|
<h2
|
||||||
|
class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary"
|
||||||
|
>
|
||||||
About
|
About
|
||||||
</h2>
|
</h2>
|
||||||
<div class="h-px grow theme-border opacity-60"></div>
|
<div class="h-px grow theme-border opacity-60"></div>
|
||||||
@ -39,15 +44,20 @@ const interests = [
|
|||||||
<div class="grid md:grid-cols-[1fr_2fr] gap-12 sm:gap-24">
|
<div class="grid md:grid-cols-[1fr_2fr] gap-12 sm:gap-24">
|
||||||
<div class="space-y-6 sm:space-y-8">
|
<div class="space-y-6 sm:space-y-8">
|
||||||
<FadeIn>
|
<FadeIn>
|
||||||
<div class="aspect-square theme-bg-05 rounded-2xl overflow-hidden border theme-border hover:border-white/20 transition-all duration-300 shadow-md hover:shadow-lg mx-auto md:mx-0 max-w-[280px] md:max-w-none relative group">
|
<div
|
||||||
|
class="aspect-square theme-bg-05 rounded-2xl overflow-hidden border theme-border hover:border-white/20 transition-all duration-300 shadow-md hover:shadow-lg mx-auto md:mx-0 max-w-[280px] md:max-w-none relative group"
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/profile-image.jpg"
|
src="/profile-image-futu-style.jpg"
|
||||||
alt="Jan-Marlon Leibl - Fullstack Software Developer"
|
alt="Jan-Marlon Leibl - Fullstack Software Developer"
|
||||||
width="400"
|
width="400"
|
||||||
height="400"
|
height="400"
|
||||||
class="object-cover w-full h-full group-hover:scale-105 transition-transform duration-700"
|
class="object-cover w-full h-full group-hover:scale-105 transition-transform duration-700"
|
||||||
/>
|
/>
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
<div
|
||||||
|
class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
<FadeIn delay={0.1}>
|
<FadeIn delay={0.1}>
|
||||||
@ -56,7 +66,9 @@ const interests = [
|
|||||||
Jan-Marlon Leibl
|
Jan-Marlon Leibl
|
||||||
</h3>
|
</h3>
|
||||||
<p class="font-['Instrument_Sans'] theme-text-70 text-base sm:text-lg">
|
<p class="font-['Instrument_Sans'] theme-text-70 text-base sm:text-lg">
|
||||||
Fullstack Developer
|
Fullstack Web Developer
|
||||||
|
<br>
|
||||||
|
Photographer Hobbyist
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
@ -66,10 +78,13 @@ const interests = [
|
|||||||
<div class="space-y-10 sm:space-y-16 font-['Instrument_Sans']">
|
<div class="space-y-10 sm:space-y-16 font-['Instrument_Sans']">
|
||||||
<div class="space-y-6 sm:space-y-8">
|
<div class="space-y-6 sm:space-y-8">
|
||||||
<p class="text-xl sm:text-2xl theme-text-90 leading-relaxed">
|
<p class="text-xl sm:text-2xl theme-text-90 leading-relaxed">
|
||||||
Hello! I'm Jan-Marlon, but please call me Jan. I started my journey in programming at the age of 11 with C#, fascinated by a desktop application my friend created.
|
Hey there! I'm Jan-Marlon, but everyone calls me Jan. My coding journey kicked off at age 11 with C#, after being amazed by a simple app my friend built.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-base sm:text-xl theme-text-70 leading-relaxed">
|
<p class="text-base sm:text-xl theme-text-70 leading-relaxed">
|
||||||
Today, I specialize in PHP and TypeScript development, constantly pushing the boundaries of what's possible on the web. My journey has led me from creating simple applications to developing complex systems used by thousands.
|
These days, I work with PHP and TypeScript to build cool stuff on the web. I've grown from tinkering with basic projects to crafting robust systems that thousands of people use daily.
|
||||||
|
</p>
|
||||||
|
<p class="text-base sm:text-xl theme-text-70 leading-relaxed">
|
||||||
|
I bring the same eye for detail to my code that I do to my photography. There's something magical about creating something both functional and beautiful, whether it's a web app or a perfect shot.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -82,14 +97,16 @@ const interests = [
|
|||||||
<div class="h-px grow theme-border opacity-30"></div>
|
<div class="h-px grow theme-border opacity-30"></div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="space-y-4 sm:space-y-5">
|
<ul class="space-y-4 sm:space-y-5">
|
||||||
{technologies.map((tech) => (
|
{
|
||||||
<li class="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 transition-colors cursor-default theme-bg-05 px-4 py-2.5 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
|
technologies.map((tech) => (
|
||||||
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-colors">
|
<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" />
|
<tech.icon className="w-5 h-5" />
|
||||||
</span>
|
</span>
|
||||||
{tech.name}
|
{tech.name}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -101,14 +118,16 @@ const interests = [
|
|||||||
<div class="h-px grow theme-border opacity-30"></div>
|
<div class="h-px grow theme-border opacity-30"></div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="space-y-4 sm:space-y-5">
|
<ul class="space-y-4 sm:space-y-5">
|
||||||
{interests.map((interest) => (
|
{
|
||||||
<li class="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 transition-colors cursor-default theme-bg-05 px-4 py-2.5 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
|
interests.map((interest) => (
|
||||||
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-colors">
|
<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" />
|
<interest.icon className="w-5 h-5" />
|
||||||
</span>
|
</span>
|
||||||
{interest.name}
|
{interest.name}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
202
src/components/sections/Contact.astro
Normal file
202
src/components/sections/Contact.astro
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
---
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
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>
|
||||||
|
<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"
|
||||||
|
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"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<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"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<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">
|
||||||
|
<FaEnvelope className="w-5 h-5" />
|
||||||
|
</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
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<span class="p-2 rounded-xl theme-bg-05 border theme-border theme-text-70 mt-1">
|
||||||
|
<FaMapMarkerAlt className="w-5 h-5" />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h4 class="theme-secondary font-medium font-['Instrument_Sans']">Location</h4>
|
||||||
|
<p class="theme-primary font-['Instrument_Sans']">Bremen, Germany</p>
|
||||||
|
</div>
|
||||||
|
</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"
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<FaGlobe className="w-6 h-6" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FadeIn>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
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");
|
||||||
|
if (!name || !email || !message) {
|
||||||
|
if (formStatus) {
|
||||||
|
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>
|
@ -1,41 +1,57 @@
|
|||||||
---
|
---
|
||||||
import FadeIn from '../ui/FadeIn.astro';
|
import FadeIn from "../ui/FadeIn.astro";
|
||||||
import GermanyFlag from '../ui/GermanyFlag.astro';
|
import GermanyFlag from "../ui/GermanyFlag.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="min-h-[100dvh] flex items-center justify-center px-4 sm:px-8 relative pt-24 pb-16" id="hero">
|
<section
|
||||||
|
class="min-h-[100dvh] flex items-center justify-center px-4 sm:px-8 relative pt-24 pb-16"
|
||||||
|
id="hero"
|
||||||
|
>
|
||||||
<div class="absolute inset-0 theme-bg-gradient opacity-80"></div>
|
<div class="absolute inset-0 theme-bg-gradient opacity-80"></div>
|
||||||
|
|
||||||
<div class="max-w-7xl w-full relative pt-8 sm:pt-24">
|
<div class="max-w-7xl w-full relative pt-8 sm:pt-4">
|
||||||
<FadeIn className="space-y-10 sm:space-y-16">
|
<FadeIn className="space-y-10 sm:space-y-16">
|
||||||
<div class="space-y-8 sm:space-y-10">
|
<div class="space-y-8 sm:space-y-10">
|
||||||
<div class="space-y-6 sm:space-y-8">
|
<div class="space-y-6 sm:space-y-8">
|
||||||
<div class="inline-flex items-center gap-2.5 px-4 py-2 rounded-xl theme-accent theme-border border theme-transition shadow-sm">
|
<div
|
||||||
|
class="inline-flex items-center gap-2.5 px-4 py-2 rounded-xl theme-accent theme-border border theme-transition shadow-sm"
|
||||||
|
>
|
||||||
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
<span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||||
<span class="theme-secondary uppercase tracking-wider text-xs sm:text-sm font-medium font-['Instrument_Sans']">Available for Work</span>
|
<span
|
||||||
|
class="theme-secondary uppercase tracking-wider text-xs sm:text-sm font-medium font-['Instrument_Sans']"
|
||||||
|
>Open to New Projects</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 sm:space-y-4">
|
<div class="space-y-3 sm:space-y-4">
|
||||||
<h2 class="font-['Instrument_Sans'] text-xl sm:text-2xl theme-secondary tracking-tight font-medium theme-transition">
|
<h2
|
||||||
|
class="font-['Instrument_Sans'] text-xl sm:text-2xl theme-secondary tracking-tight font-medium theme-transition"
|
||||||
|
>
|
||||||
Jan-Marlon Leibl
|
Jan-Marlon Leibl
|
||||||
</h2>
|
</h2>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="font-['DM_Sans'] text-4xl sm:text-6xl md:text-7xl lg:text-8xl font-bold tracking-tight leading-[1.05] sm:leading-[0.95] max-w-4xl theme-primary theme-transition">
|
<h1
|
||||||
Software Developer
|
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"
|
||||||
|
>
|
||||||
|
Web Developer
|
||||||
<br />
|
<br />
|
||||||
<span class="theme-secondary inline-flex items-center gap-2">based in <GermanyFlag /></span>
|
<span class="theme-secondary inline-flex items-center gap-2"
|
||||||
|
>from <GermanyFlag /></span
|
||||||
|
>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-2xl max-w-2xl leading-relaxed tracking-tight theme-transition">
|
<p
|
||||||
Passionate about creating digital experiences, with a focus on PHP and modern web technologies.
|
class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-2xl max-w-2xl leading-relaxed tracking-tight theme-transition"
|
||||||
|
>
|
||||||
|
Crafting engaging digital experiences with PHP, TypeScript, and a splash of creative thinking.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col sm:flex-row gap-4 sm:gap-5">
|
<div class="flex flex-col sm:flex-row gap-4 sm:gap-5">
|
||||||
<a
|
<a
|
||||||
href="#work"
|
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"
|
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
|
See My Work
|
||||||
<svg
|
<svg
|
||||||
class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform"
|
class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -43,14 +59,14 @@ import GermanyFlag from '../ui/GermanyFlag.astro';
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
>
|
>
|
||||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
<path d="M5 12h14M12 5l7 7-7 7"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="mailto:jleibl@proton.me"
|
href="mailto:jleibl@proton.me"
|
||||||
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"
|
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
|
Say Hello
|
||||||
<svg
|
<svg
|
||||||
class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform"
|
class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -58,17 +74,24 @@ import GermanyFlag from '../ui/GermanyFlag.astro';
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
>
|
>
|
||||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
<path d="M5 12h14M12 5l7 7-7 7"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t theme-border pt-8 mt-10">
|
<div class="border-t theme-border pt-8 mt-10">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 w-full gap-6 sm:gap-0 py-6 sm:py-8 font-['Instrument_Sans'] text-center">
|
<div
|
||||||
<div class="sm:border-r theme-border last:border-r-0 px-4 flex flex-col items-center justify-center">
|
class="grid grid-cols-1 sm:grid-cols-3 w-full gap-6 sm:gap-0 py-6 sm:py-8 font-['Instrument_Sans'] text-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="sm:border-r theme-border last:border-r-0 px-4 flex flex-col items-center justify-center"
|
||||||
|
>
|
||||||
<div class="mb-3 sm:mb-4 w-full text-center">
|
<div class="mb-3 sm:mb-4 w-full text-center">
|
||||||
<span class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block">EMAIL</span>
|
<span
|
||||||
|
class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block"
|
||||||
|
>EMAIL</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="mailto:jleibl@proton.me"
|
href="mailto:jleibl@proton.me"
|
||||||
@ -78,22 +101,28 @@ import GermanyFlag from '../ui/GermanyFlag.astro';
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sm:border-r theme-border last:border-r-0 px-4 flex flex-col items-center justify-center">
|
<div
|
||||||
|
class="sm:border-r theme-border last:border-r-0 px-4 flex flex-col items-center justify-center"
|
||||||
|
>
|
||||||
<div class="mb-3 sm:mb-4 w-full text-center">
|
<div class="mb-3 sm:mb-4 w-full text-center">
|
||||||
<span class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block">ROLE</span>
|
<span
|
||||||
|
class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block"
|
||||||
|
>ROLE</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<span class="theme-text-90 text-base sm:text-lg block w-full">
|
<span class="theme-text-90 text-base sm:text-lg block w-full">
|
||||||
Fullstack Developer
|
Fullstack Web Dev
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-4 flex flex-col items-center justify-center">
|
<div class="px-4 flex flex-col items-center justify-center">
|
||||||
<div class="mb-3 sm:mb-4 w-full text-center">
|
<div class="mb-3 sm:mb-4 w-full text-center">
|
||||||
<span class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block">EXPERIENCE</span>
|
<span
|
||||||
|
class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block"
|
||||||
|
>EXPERIENCE</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<span class="theme-text-90 text-base sm:text-lg block w-full">
|
<span class="theme-text-90 text-base sm:text-lg block w-full"> 5+ Years </span>
|
||||||
5+ Years
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
104
src/components/sections/Hobby.astro
Normal file
104
src/components/sections/Hobby.astro
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
import FadeIn from "../ui/FadeIn.astro";
|
||||||
|
import { FaCamera, FaPhotoVideo } from "react-icons/fa";
|
||||||
|
import { MdPhotoCamera, MdCameraAlt } from "react-icons/md";
|
||||||
|
|
||||||
|
const cameras = [
|
||||||
|
{ name: "Nikon D7200", icon: MdCameraAlt, description: "Dad's camera that got me hooked on photography" },
|
||||||
|
{ name: "Nikon D3300", icon: MdCameraAlt, description: "My first personal camera where it all began" },
|
||||||
|
{ name: "Sony A7 III", icon: MdPhotoCamera, description: "Current setup that's taken my shots to the next level" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const genres = [
|
||||||
|
{ name: "Portrait", icon: FaCamera, description: "Love capturing people's authentic expressions" },
|
||||||
|
{ name: "Street", icon: FaCamera, description: "Finding beauty in everyday urban moments" },
|
||||||
|
{ name: "Studio", icon: FaPhotoVideo, description: "Creating controlled lighting magic" },
|
||||||
|
{ name: "Landscape", icon: FaPhotoVideo, description: "Chasing perfect light in nature" },
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<section id="hobby" 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="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"
|
||||||
|
>
|
||||||
|
Hobby
|
||||||
|
</h2>
|
||||||
|
<div class="h-px grow theme-border opacity-60"></div>
|
||||||
|
</div>
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
|
<div class="grid gap-12 sm:gap-16">
|
||||||
|
<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">
|
||||||
|
When I'm not coding, you'll find me with a camera in hand, hunting for that perfect shot that tells a story without words.
|
||||||
|
</p>
|
||||||
|
<p class="text-base sm:text-xl theme-text-70 leading-relaxed">
|
||||||
|
It all started with borrowing my dad's Nikon D7200 – I was instantly hooked. Eventually got my own Nikon D3300 to learn the basics. These days, I'm having a blast with my Sony A7 III, experimenting with different styles and techniques across various photography genres.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-10 sm:gap-16">
|
||||||
|
<div class="space-y-6 sm:space-y-8">
|
||||||
|
<div class="flex items-baseline gap-4">
|
||||||
|
<h3 class="text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium">
|
||||||
|
Camera Evolution
|
||||||
|
</h3>
|
||||||
|
<div class="h-px grow theme-border opacity-30"></div>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-4 sm:space-y-5">
|
||||||
|
{
|
||||||
|
cameras.map((camera) => (
|
||||||
|
<li class="text-base sm:text-lg group flex flex-col gap-1 theme-text-70 hover:theme-text-90 cursor-default theme-bg-05 px-4 py-3 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
|
||||||
|
<camera.icon className="w-5 h-5" />
|
||||||
|
</span>
|
||||||
|
{camera.name}
|
||||||
|
</div>
|
||||||
|
<p class="text-sm theme-text-50 pl-9">{camera.description}</p>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-6 sm:space-y-8">
|
||||||
|
<div class="flex items-baseline gap-4">
|
||||||
|
<h3 class="text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium">
|
||||||
|
Favorite Genres
|
||||||
|
</h3>
|
||||||
|
<div class="h-px grow theme-border opacity-30"></div>
|
||||||
|
</div>
|
||||||
|
<ul class="space-y-4 sm:space-y-5">
|
||||||
|
{
|
||||||
|
genres.map((genre) => (
|
||||||
|
<li class="text-base sm:text-lg group flex flex-col gap-1 theme-text-70 hover:theme-text-90 cursor-default theme-bg-05 px-4 py-3 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
|
||||||
|
<genre.icon className="w-5 h-5" />
|
||||||
|
</span>
|
||||||
|
{genre.name}
|
||||||
|
</div>
|
||||||
|
<p class="text-sm theme-text-50 pl-9">{genre.description}</p>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FadeIn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
@ -1,107 +1,76 @@
|
|||||||
---
|
---
|
||||||
import { SiNextdotjs, SiTailwindcss, SiTypescript, SiPhp, SiJavascript, SiMysql, SiDiscord } from 'react-icons/si';
|
import {
|
||||||
import { BsLightningChargeFill } from 'react-icons/bs';
|
SiNextdotjs,
|
||||||
import { FaCode } from 'react-icons/fa';
|
SiTailwindcss,
|
||||||
import { HiOutlineCollection } from 'react-icons/hi';
|
SiTypescript,
|
||||||
import { MdLaunch } from 'react-icons/md';
|
SiPhp,
|
||||||
import FadeIn from '../ui/FadeIn.astro';
|
SiJavascript,
|
||||||
|
SiMysql,
|
||||||
|
SiDiscord,
|
||||||
|
} from "react-icons/si";
|
||||||
|
import { BsLightningChargeFill } from "react-icons/bs";
|
||||||
|
import { HiOutlineCollection } from "react-icons/hi";
|
||||||
|
import { MdLaunch } from "react-icons/md";
|
||||||
|
import FadeIn from "../ui/FadeIn.astro";
|
||||||
|
|
||||||
interface Project {
|
const iconMap = {
|
||||||
title: string;
|
"Next.js": SiNextdotjs,
|
||||||
year: string;
|
"Tailwind CSS": SiTailwindcss,
|
||||||
description: string;
|
"TypeScript": SiTypescript,
|
||||||
icon: any;
|
"PHP": SiPhp,
|
||||||
tags: Array<{
|
"JavaScript": SiJavascript,
|
||||||
name: string;
|
"MySQL": SiMysql,
|
||||||
icon: any;
|
"Performance": BsLightningChargeFill,
|
||||||
}>;
|
"Discord API": SiDiscord,
|
||||||
}
|
|
||||||
|
|
||||||
const getTagIcon = (tag: string) => {
|
|
||||||
switch (tag) {
|
|
||||||
case 'Next.js':
|
|
||||||
return SiNextdotjs;
|
|
||||||
case 'Tailwind CSS':
|
|
||||||
return SiTailwindcss;
|
|
||||||
case 'TypeScript':
|
|
||||||
return SiTypescript;
|
|
||||||
case 'PHP':
|
|
||||||
return SiPhp;
|
|
||||||
case 'JavaScript':
|
|
||||||
return SiJavascript;
|
|
||||||
case 'MySQL':
|
|
||||||
return SiMysql;
|
|
||||||
case 'Performance':
|
|
||||||
return BsLightningChargeFill;
|
|
||||||
case 'Discord API':
|
|
||||||
return SiDiscord;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectIcon = (title: string) => {
|
const projects = [
|
||||||
if (title.includes('ventry')) {
|
|
||||||
return MdLaunch;
|
|
||||||
} else if (title.includes('ShareUpload')) {
|
|
||||||
return HiOutlineCollection;
|
|
||||||
} else if (title.includes('RestoreM')) {
|
|
||||||
return SiDiscord;
|
|
||||||
} else {
|
|
||||||
return FaCode;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const projects: Project[] = [
|
|
||||||
{
|
{
|
||||||
title: 'ventry.host v2',
|
title: "ventry.host v2",
|
||||||
year: '2025',
|
year: "2025",
|
||||||
description: 'Free file hosting revamped with a modern design and improved user experience.',
|
description: "Free file hosting revamped with a modern design and improved user experience.",
|
||||||
icon: getProjectIcon('ventry.host v2'),
|
icon: MdLaunch,
|
||||||
tags: [
|
tags: [
|
||||||
{ name: 'Next.js', icon: getTagIcon('Next.js') },
|
{ name: "Next.js", icon: iconMap["Next.js"] },
|
||||||
{ name: 'Tailwind CSS', icon: getTagIcon('Tailwind CSS') },
|
{ name: "Tailwind CSS", icon: iconMap["Tailwind CSS"] },
|
||||||
{ name: 'TypeScript', icon: getTagIcon('TypeScript') }
|
{ name: "TypeScript", icon: iconMap["TypeScript"] },
|
||||||
]
|
],
|
||||||
|
link: "https://ventry.host",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'ventry.host',
|
title: "ventry.host",
|
||||||
year: '2023',
|
year: "2023",
|
||||||
description: 'A free file hosting solution with thousands of daily visitors.',
|
description: "A free file hosting solution with thousands of daily visitors.",
|
||||||
icon: getProjectIcon('ventry.host'),
|
icon: MdLaunch,
|
||||||
tags: [
|
tags: [
|
||||||
{ name: 'PHP', icon: getTagIcon('PHP') },
|
{ name: "PHP", icon: iconMap["PHP"] },
|
||||||
{ name: 'JavaScript', icon: getTagIcon('JavaScript') },
|
{ name: "JavaScript", icon: iconMap["JavaScript"] },
|
||||||
{ name: 'MySQL', icon: getTagIcon('MySQL') }
|
{ name: "MySQL", icon: iconMap["MySQL"] },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'ShareUpload',
|
title: "ShareUpload",
|
||||||
year: '2022',
|
year: "2022",
|
||||||
description: 'High-performance file sharing platform with unlimited storage.',
|
description: "High-performance file sharing platform with unlimited storage.",
|
||||||
icon: getProjectIcon('ShareUpload'),
|
icon: HiOutlineCollection,
|
||||||
tags: [
|
tags: [
|
||||||
{ name: 'PHP', icon: getTagIcon('PHP') },
|
{ name: "PHP", icon: iconMap["PHP"] },
|
||||||
{ name: 'MySQL', icon: getTagIcon('MySQL') },
|
{ name: "MySQL", icon: iconMap["MySQL"] },
|
||||||
{ name: 'Performance', icon: getTagIcon('Performance') }
|
{ name: "Performance", icon: iconMap["Performance"] },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'RestoreM',
|
title: "RestoreM",
|
||||||
year: '2023',
|
year: "2023",
|
||||||
description: 'Discord server backup and restoration service.',
|
description: "Discord server backup and restoration service.",
|
||||||
icon: getProjectIcon('RestoreM'),
|
icon: SiDiscord,
|
||||||
tags: [
|
tags: [
|
||||||
{ name: 'PHP', icon: getTagIcon('PHP') },
|
{ name: "PHP", icon: iconMap["PHP"] },
|
||||||
{ name: 'MySQL', icon: getTagIcon('MySQL') },
|
{ name: "MySQL", icon: iconMap["MySQL"] },
|
||||||
{ name: 'Discord API', icon: getTagIcon('Discord API') }
|
{ name: "Discord API", icon: iconMap["Discord API"] },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
];
|
].sort((a, b) => parseInt(b.year) - parseInt(a.year));
|
||||||
|
|
||||||
const sortedProjects = [...projects].sort((a, b) => {
|
|
||||||
return parseInt(b.year) - parseInt(a.year);
|
|
||||||
});
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<section id="work" class="py-24 sm:py-40 px-4 sm:px-8 relative">
|
<section id="work" class="py-24 sm:py-40 px-4 sm:px-8 relative">
|
||||||
@ -109,12 +78,16 @@ const sortedProjects = [...projects].sort((a, b) => {
|
|||||||
|
|
||||||
<div class="max-w-(--breakpoint-xl) mx-auto relative">
|
<div class="max-w-(--breakpoint-xl) mx-auto relative">
|
||||||
<FadeIn>
|
<FadeIn>
|
||||||
<div class="flex flex-col gap-3 mb-12 sm:mb-24">
|
<div class="flex flex-col gap-3 mb-16 sm:mb-28">
|
||||||
<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'] relative inline-flex items-center before:content-[''] before:absolute before:-left-8 before:w-6 before:h-px before:theme-border"
|
||||||
|
>
|
||||||
Portfolio
|
Portfolio
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-baseline gap-4">
|
<div class="flex items-baseline gap-4">
|
||||||
<h2 class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary">
|
<h2
|
||||||
|
class="font-['DM_Sans'] text-3xl sm:text-5xl lg:text-6xl font-semibold tracking-tight theme-primary"
|
||||||
|
>
|
||||||
Selected Work
|
Selected Work
|
||||||
</h2>
|
</h2>
|
||||||
<div class="h-px grow theme-border opacity-60"></div>
|
<div class="h-px grow theme-border opacity-60"></div>
|
||||||
@ -122,19 +95,38 @@ const sortedProjects = [...projects].sort((a, b) => {
|
|||||||
</div>
|
</div>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
|
|
||||||
<div class="grid gap-20 sm:gap-28">
|
<div class="grid gap-24 sm:gap-32">
|
||||||
{sortedProjects.map((project, index) => (
|
{
|
||||||
|
projects.map((project, index) => (
|
||||||
<FadeIn delay={index * 0.1}>
|
<FadeIn delay={index * 0.1}>
|
||||||
<div class="group relative">
|
<div class="group relative" data-project-card>
|
||||||
|
{project.link && (
|
||||||
|
<button
|
||||||
|
class="project-link absolute inset-0 z-30 cursor-pointer w-full h-full"
|
||||||
|
data-project-link
|
||||||
|
data-project-title={project.title}
|
||||||
|
data-project-url={project.link}
|
||||||
|
>
|
||||||
|
<span class="sr-only">View {project.title}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<div class="absolute top-0 left-0 right-0 flex items-center gap-4">
|
<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">
|
<div class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium py-2 pr-4 flex items-center">
|
||||||
{project.year}
|
{project.year}
|
||||||
</div>
|
</div>
|
||||||
<div class="h-px grow theme-border opacity-30"></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="pt-16 sm:pt-20 grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 sm:gap-20">
|
||||||
<div class="space-y-4 sm:space-y-6">
|
<div class="space-y-5 sm:space-y-7">
|
||||||
|
{project.link && (
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-2xl sm:text-3xl theme-text-40 opacity-60 group-hover:opacity-90 transition-all duration-300">
|
||||||
|
<project.icon />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<h3 class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary group-hover:theme-text-90 transition-colors">
|
<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}
|
{project.title}
|
||||||
</h3>
|
</h3>
|
||||||
@ -143,37 +135,132 @@ const sortedProjects = [...projects].sort((a, b) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6 sm:space-y-10">
|
<div class="space-y-8 sm:space-y-12">
|
||||||
<div class="space-y-4 sm:space-y-6">
|
<div class="space-y-5 sm:space-y-7">
|
||||||
<h4 class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium">
|
<h4 class="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium flex items-center before:content-[''] before:w-6 before:h-px before:theme-border before:mr-3">
|
||||||
Technologies
|
Technologies
|
||||||
</h4>
|
</h4>
|
||||||
<div class="flex flex-wrap items-center gap-3 sm:gap-4">
|
<div class="flex flex-wrap items-center gap-3 sm:gap-4 relative z-20 pointer-events-none">
|
||||||
{project.tags.map((tag, tagIndex) => {
|
{project.tags.map((tag) => (
|
||||||
const Icon = tag.icon;
|
<span class="flex items-center text-sm sm:text-base theme-text-70 font-['Instrument_Sans'] tracking-tight font-medium py-2 px-5 group-hover:theme-text-90 transition-all duration-300 theme-bg-05 rounded-full shadow-sm border border-transparent group-hover:theme-border">
|
||||||
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">
|
<span class="mr-2 flex items-center">
|
||||||
<Icon className="text-lg" />
|
<tag.icon className="text-lg" />
|
||||||
</span>
|
</span>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
{tagIndex !== project.tags.length - 1 && (
|
|
||||||
<span class="ml-2 opacity-0">•</span>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute -inset-x-6 sm:-inset-x-8 -inset-y-6 sm:-inset-y-8 rounded-2xl sm:rounded-3xl border border-transparent hover:theme-border group-hover:bg-white/[0.01] transition-all duration-300 shadow-sm group-hover:shadow-md"></div>
|
<div class="absolute -inset-x-8 sm:-inset-x-12 -inset-y-8 sm:-inset-y-12 rounded-3xl sm:rounded-[2rem] border border-transparent hover:theme-border group-hover:bg-white/[0.02] transition-all duration-300 shadow-sm group-hover:shadow-lg" />
|
||||||
|
|
||||||
|
{project.link && (
|
||||||
|
<div class="absolute bottom-5 right-5 opacity-0 group-hover:opacity-100 transition-all duration-300 transform translate-y-2 group-hover:translate-y-0 z-20 pointer-events-none">
|
||||||
|
<span class="flex items-center theme-text-90 font-['Instrument_Sans'] text-sm bg-white/10 px-4 py-2 rounded-full shadow-md backdrop-blur-sm">
|
||||||
|
<MdLaunch className="mr-2" /> View Project
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div id="redirect-modal" class="fixed inset-0 z-50 flex items-center justify-center opacity-0 pointer-events-none transition-all duration-300 ease-out">
|
||||||
|
<div class="absolute inset-0 bg-black bg-opacity-0 backdrop-blur-[0px] transition-all duration-300 ease-out" id="modal-overlay"></div>
|
||||||
|
<div class="relative bg-neutral-900 border theme-border rounded-2xl w-full max-w-md p-6 sm:p-8 shadow-xl transform transition-all duration-300 ease-out scale-95 opacity-0">
|
||||||
|
<div class="text-center">
|
||||||
|
<span class="inline-block text-4xl theme-text-40 mb-4">
|
||||||
|
<MdLaunch />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h3 class="font-['DM_Sans'] text-2xl sm:text-3xl font-semibold tracking-tight theme-primary mb-2">
|
||||||
|
External Link
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p class="font-['Instrument_Sans'] theme-text-70 mb-6">
|
||||||
|
You are being redirected to <span id="redirect-url" class="theme-primary font-medium"></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col-reverse sm:flex-row gap-3 sm:gap-4 justify-center">
|
||||||
|
<button
|
||||||
|
id="cancel-redirect"
|
||||||
|
class="theme-primary-gradient text-white py-3 px-6 rounded-xl font-medium hover:opacity-90 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="confirm-redirect"
|
||||||
|
class="theme-bg-05 theme-text-90 py-3 px-6 rounded-xl font-medium border border-transparent hover:theme-border transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
Continue to Site
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const modal = document.getElementById('redirect-modal');
|
||||||
|
const modalOverlay = document.getElementById('modal-overlay');
|
||||||
|
const modalContent = modal?.querySelector('.max-w-md');
|
||||||
|
const redirectUrlEl = document.getElementById('redirect-url');
|
||||||
|
let currentUrl = '';
|
||||||
|
|
||||||
|
function showModal(url) {
|
||||||
|
currentUrl = url;
|
||||||
|
if (redirectUrlEl) redirectUrlEl.textContent = new URL(url).hostname;
|
||||||
|
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('pointer-events-none');
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.classList.add('opacity-100');
|
||||||
|
modalOverlay?.classList.add('bg-opacity-60', 'backdrop-blur-sm');
|
||||||
|
modalContent?.classList.remove('scale-95', 'opacity-0');
|
||||||
|
modalContent?.classList.add('scale-100', 'opacity-100');
|
||||||
|
}, 10);
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideModal() {
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('opacity-100');
|
||||||
|
modalOverlay?.classList.remove('bg-opacity-60', 'backdrop-blur-sm');
|
||||||
|
modalContent?.classList.remove('scale-100', 'opacity-100');
|
||||||
|
modalContent?.classList.add('scale-95', 'opacity-0');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.classList.add('pointer-events-none');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-project-link]').forEach(link => {
|
||||||
|
link.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const url = link.getAttribute('data-project-url');
|
||||||
|
if (url) showModal(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('cancel-redirect')?.addEventListener('click', hideModal);
|
||||||
|
modalOverlay?.addEventListener('click', hideModal);
|
||||||
|
document.getElementById('confirm-redirect')?.addEventListener('click', () => {
|
||||||
|
if (currentUrl) window.open(currentUrl, '_blank');
|
||||||
|
hideModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') hideModal();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@ -2,22 +2,17 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
delay?: number;
|
delay?: number;
|
||||||
direction?: 'up' | 'down' | 'left' | 'right';
|
direction?: "up" | "down" | "left" | "right";
|
||||||
distance?: number;
|
distance?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { className = "", delay = 0, direction = "up", distance = 20 } = Astro.props;
|
||||||
className = "",
|
|
||||||
delay = 0,
|
|
||||||
direction = 'up',
|
|
||||||
distance = 20
|
|
||||||
} = Astro.props;
|
|
||||||
|
|
||||||
const directionMap = {
|
const directionMap = {
|
||||||
up: { x: 0, y: distance },
|
up: { x: 0, y: distance },
|
||||||
down: { x: 0, y: -distance },
|
down: { x: 0, y: -distance },
|
||||||
left: { x: distance, y: 0 },
|
left: { x: distance, y: 0 },
|
||||||
right: { x: -distance, y: 0 }
|
right: { x: -distance, y: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const { x, y } = directionMap[direction];
|
const { x, y } = directionMap[direction];
|
||||||
@ -32,7 +27,9 @@ const uniqueId = `fade-in-${Math.random().toString(36).substring(2, 9)}`;
|
|||||||
.fade-in {
|
.fade-in {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translate(var(--start-x, 0px), var(--start-y, 20px));
|
transform: translate(var(--start-x, 0px), var(--start-y, 20px));
|
||||||
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
transition:
|
||||||
|
opacity 0.6s ease-out,
|
||||||
|
transform 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-in.visible {
|
.fade-in.visible {
|
||||||
@ -42,23 +39,26 @@ const uniqueId = `fade-in-${Math.random().toString(36).substring(2, 9)}`;
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script define:vars={{ uniqueId, x, y, delay }}>
|
<script define:vars={{ uniqueId, x, y, delay }}>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const element = document.getElementById(uniqueId);
|
const element = document.getElementById(uniqueId);
|
||||||
if (element) {
|
if (element) {
|
||||||
element.style.setProperty('--start-x', `${x}px`);
|
element.style.setProperty("--start-x", `${x}px`);
|
||||||
element.style.setProperty('--start-y', `${y}px`);
|
element.style.setProperty("--start-y", `${y}px`);
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver(
|
||||||
entries.forEach(entry => {
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
element.classList.add('visible');
|
element.classList.add("visible");
|
||||||
}, delay * 1000);
|
}, delay * 1000);
|
||||||
|
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, { threshold: 0.1, rootMargin: "0px 0px -10% 0px" });
|
},
|
||||||
|
{ threshold: 0.1, rootMargin: "0px 0px -10% 0px" }
|
||||||
|
);
|
||||||
|
|
||||||
observer.observe(element);
|
observer.observe(element);
|
||||||
}
|
}
|
||||||
|
@ -29,27 +29,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const themeButtons = document.querySelectorAll('.theme-button');
|
const themeButtons = document.querySelectorAll(".theme-button");
|
||||||
const currentTheme = localStorage.getItem('theme') || 'black';
|
const currentTheme = localStorage.getItem("theme") || "black";
|
||||||
|
|
||||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
document.documentElement.setAttribute("data-theme", currentTheme);
|
||||||
document.getElementById(`theme-${currentTheme}`)?.classList.add('ring-1', 'ring-white/20');
|
document.getElementById(`theme-${currentTheme}`)?.classList.add("ring-1", "ring-white/20");
|
||||||
|
|
||||||
themeButtons.forEach(button => {
|
themeButtons.forEach((button) => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener("click", () => {
|
||||||
const theme = button.getAttribute('data-theme');
|
const theme = button.getAttribute("data-theme");
|
||||||
if (!theme) return;
|
if (!theme) return;
|
||||||
|
|
||||||
themeButtons.forEach(btn => btn.classList.remove('ring-1', 'ring-white/20'));
|
themeButtons.forEach((btn) => btn.classList.remove("ring-1", "ring-white/20"));
|
||||||
|
|
||||||
button.classList.add('ring-1', 'ring-white/20');
|
button.classList.add("ring-1", "ring-white/20");
|
||||||
|
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
localStorage.setItem('theme', theme);
|
localStorage.setItem("theme", theme);
|
||||||
|
|
||||||
const themeChangedEvent = new CustomEvent('themeChanged', {
|
const themeChangedEvent = new CustomEvent("themeChanged", {
|
||||||
detail: { theme }
|
detail: { theme },
|
||||||
});
|
});
|
||||||
document.dispatchEvent(themeChangedEvent);
|
document.dispatchEvent(themeChangedEvent);
|
||||||
});
|
});
|
||||||
|
46
src/components/ui/SkipToContent.astro
Normal file
46
src/components/ui/SkipToContent.astro
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<a href="#main-content" class="skip-to-content" aria-label="Skip to main content">
|
||||||
|
<span class="font-['Instrument_Sans']">Skip to content</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.skip-to-content {
|
||||||
|
position: absolute;
|
||||||
|
top: -100px;
|
||||||
|
left: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem;
|
||||||
|
background: black;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
z-index: 100;
|
||||||
|
transition: top 0.3s ease-out;
|
||||||
|
font-weight: 500;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-to-content:focus {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme variations */
|
||||||
|
html[data-theme="red"] .skip-to-content {
|
||||||
|
background: #170c0c;
|
||||||
|
color: #f87171;
|
||||||
|
border-color: rgba(248, 113, 113, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="gold"] .skip-to-content {
|
||||||
|
background: #121107;
|
||||||
|
color: #fbbf24;
|
||||||
|
border-color: rgba(251, 191, 36, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
:root.dark .skip-to-content {
|
||||||
|
background: #0a0a0a;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,127 +1,162 @@
|
|||||||
---
|
---
|
||||||
import Header from '../components/layout/Header.astro';
|
import Header from "../components/layout/Header.astro";
|
||||||
import Hero from '../components/sections/Hero.astro';
|
import Meta from "../components/layout/Meta.astro";
|
||||||
import About from '../components/sections/About.astro';
|
import SkipToContent from "../components/ui/SkipToContent.astro";
|
||||||
import Work from '../components/sections/Work.astro';
|
import Hero from "../components/sections/Hero.astro";
|
||||||
import Footer from '../components/layout/Footer.astro';
|
import About from "../components/sections/About.astro";
|
||||||
|
import Work from "../components/sections/Work.astro";
|
||||||
|
import Hobby from "../components/sections/Hobby.astro";
|
||||||
|
import Contact from "../components/sections/Contact.astro";
|
||||||
|
import Footer from "../components/layout/Footer.astro";
|
||||||
|
|
||||||
import '../styles/global.css';
|
import "../styles/global.css";
|
||||||
import '../styles/theme.css';
|
import "../styles/theme.css";
|
||||||
|
|
||||||
import '@fontsource/dm-sans/400.css';
|
import "@fontsource/dm-sans/400.css";
|
||||||
import '@fontsource/dm-sans/500.css';
|
import "@fontsource/dm-sans/500.css";
|
||||||
import '@fontsource/dm-sans/600.css';
|
import "@fontsource/dm-sans/600.css";
|
||||||
import '@fontsource/dm-sans/700.css';
|
import "@fontsource/dm-sans/700.css";
|
||||||
import '@fontsource/instrument-sans/400.css';
|
import "@fontsource/instrument-sans/400.css";
|
||||||
import '@fontsource/instrument-sans/500.css';
|
import "@fontsource/instrument-sans/500.css";
|
||||||
---
|
---
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Jan-Marlon Leibl • Fullstack Software Developer | PHP & TypeScript Expert</title>
|
<Meta />
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
|
|
||||||
<meta name="title" content="Jan-Marlon Leibl • Fullstack Software Developer | PHP & TypeScript Expert" />
|
|
||||||
<meta name="description" content="Experienced Fullstack Developer specializing in PHP and TypeScript. Creating high-performance web applications and digital experiences with modern technologies." />
|
|
||||||
<meta name="keywords" content="Software Development, PHP Developer, TypeScript, Fullstack Engineer, Web Development, System Architecture, MySQL, React, Performance Optimization" />
|
|
||||||
<meta name="author" content="Jan-Marlon Leibl" />
|
|
||||||
<meta name="robots" content="index, follow" />
|
|
||||||
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:url" content="https://jleibl.net/" />
|
|
||||||
<meta property="og:title" content="Jan-Marlon Leibl • Fullstack Software Developer" />
|
|
||||||
<meta property="og:description" content="Experienced Fullstack Developer specializing in PHP and TypeScript. Creating high-performance web applications and digital experiences with modern technologies." />
|
|
||||||
<meta property="og:image" content="https://jleibl.net/profile-image.jpg" />
|
|
||||||
<meta property="og:image:width" content="400" />
|
|
||||||
<meta property="og:image:height" content="400" />
|
|
||||||
<meta property="og:image:alt" content="Jan-Marlon Leibl - Fullstack Software Developer" />
|
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<meta name="twitter:url" content="https://jleibl.net/" />
|
|
||||||
<meta name="twitter:title" content="Jan-Marlon Leibl • Fullstack Software Developer" />
|
|
||||||
<meta name="twitter:description" content="Experienced Fullstack Developer specializing in PHP and TypeScript. Creating high-performance web applications and digital experiences with modern technologies." />
|
|
||||||
<meta name="twitter:image" content="https://jleibl.net/profile-image.jpg" />
|
|
||||||
<meta name="twitter:image:alt" content="Jan-Marlon Leibl - Fullstack Software Developer" />
|
|
||||||
|
|
||||||
<link rel="icon" href="/profile-image.jpg" type="image/jpeg" />
|
|
||||||
<link rel="canonical" href="https://jleibl.net/" />
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<SkipToContent />
|
||||||
<div id="app-root" class="min-h-screen theme-transition black overflow-hidden">
|
<div id="app-root" class="min-h-screen theme-transition black overflow-hidden">
|
||||||
<Header />
|
<Header />
|
||||||
|
<main id="main-content">
|
||||||
<Hero />
|
<Hero />
|
||||||
<About />
|
<About />
|
||||||
|
<Hobby />
|
||||||
<Work />
|
<Work />
|
||||||
|
<Contact />
|
||||||
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Theme Provider functionality
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
const appRoot = document.getElementById("app-root");
|
||||||
const appRoot = document.getElementById('app-root');
|
const themeButtons = document.querySelectorAll(".theme-button");
|
||||||
const themeButtons = document.querySelectorAll('.theme-button');
|
|
||||||
const currentTheme = localStorage.getItem('theme') || 'black';
|
const currentTheme = localStorage.getItem("colorTheme") || "black";
|
||||||
|
|
||||||
if (appRoot) {
|
if (appRoot) {
|
||||||
appRoot.classList.remove('black', 'red', 'gold');
|
appRoot.classList.remove("black", "red", "gold");
|
||||||
appRoot.classList.add(currentTheme);
|
appRoot.classList.add(currentTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.documentElement.setAttribute('data-theme', currentTheme);
|
document.documentElement.setAttribute("data-theme", currentTheme);
|
||||||
|
|
||||||
document.getElementById(`theme-${currentTheme}`)?.classList.add('ring-1', 'ring-white/20');
|
document.getElementById(`theme-${currentTheme}`)?.classList.add("ring-1", "ring-white/20");
|
||||||
|
|
||||||
document.addEventListener('themeChanged', (e) => {
|
document.addEventListener("themeChanged", (e) => {
|
||||||
const customEvent = e as Event & { detail: { theme: string } };
|
const customEvent = e as Event & { detail: { theme: string } };
|
||||||
const newTheme = customEvent.detail.theme;
|
const newTheme = customEvent.detail.theme;
|
||||||
if (appRoot && newTheme) {
|
if (appRoot && newTheme) {
|
||||||
appRoot.classList.remove('black', 'red', 'gold');
|
appRoot.classList.remove("black", "red", "gold");
|
||||||
appRoot.classList.add(newTheme);
|
appRoot.classList.add(newTheme);
|
||||||
|
|
||||||
|
localStorage.setItem("colorTheme", newTheme);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Lenis from '@studio-freight/lenis';
|
import Lenis from "lenis";
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const lenis = new Lenis({
|
const lenis = new Lenis({
|
||||||
duration: 1.2,
|
duration: 1.2,
|
||||||
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||||
smoothWheel: true,
|
smoothWheel: true,
|
||||||
wheelMultiplier: 1,
|
wheelMultiplier: 1,
|
||||||
touchMultiplier: 2,
|
touchMultiplier: 2,
|
||||||
|
infinite: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const anchorLinks = document.querySelectorAll('a[href^="#"]');
|
const anchorLinks = document.querySelectorAll('a[href^="#"]');
|
||||||
|
|
||||||
anchorLinks.forEach(link => {
|
anchorLinks.forEach((link) => {
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const targetId = link.getAttribute('href');
|
const targetId = link.getAttribute("href");
|
||||||
if (targetId && targetId !== '#') {
|
if (targetId && targetId !== "#") {
|
||||||
const targetElement = document.querySelector(targetId);
|
const targetElement = document.querySelector(targetId);
|
||||||
if (targetElement) {
|
if (targetElement) {
|
||||||
lenis.scrollTo(targetElement as HTMLElement, {
|
lenis.scrollTo(targetElement as HTMLElement, {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
duration: 1.2,
|
duration: 1.2,
|
||||||
immediate: false
|
immediate: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function raf(time) {
|
let animationId: number;
|
||||||
|
|
||||||
|
function raf(time: number) {
|
||||||
lenis.raf(time);
|
lenis.raf(time);
|
||||||
requestAnimationFrame(raf);
|
animationId = requestAnimationFrame(raf);
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(raf);
|
animationId = requestAnimationFrame(raf);
|
||||||
|
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (document.visibilityState === "hidden") {
|
||||||
|
cancelAnimationFrame(animationId);
|
||||||
|
} else {
|
||||||
|
animationId = requestAnimationFrame(raf);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const lazyImageObserver = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const lazyImage = entry.target as HTMLImageElement;
|
||||||
|
if (lazyImage.dataset.src) {
|
||||||
|
lazyImage.src = lazyImage.dataset.src;
|
||||||
|
lazyImage.removeAttribute("data-src");
|
||||||
|
}
|
||||||
|
lazyImageObserver.unobserve(lazyImage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll("img[data-src]").forEach((image) => {
|
||||||
|
lazyImageObserver.observe(image);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
@ -62,11 +62,20 @@
|
|||||||
body {
|
body {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
font-family: 'Instrument Sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
font-family:
|
||||||
|
"Instrument Sans",
|
||||||
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
"Roboto",
|
||||||
|
"Helvetica Neue",
|
||||||
|
Arial,
|
||||||
|
sans-serif;
|
||||||
letter-spacing: -0.025em;
|
letter-spacing: -0.025em;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
@ -1,38 +1,92 @@
|
|||||||
.black { background: #0A0A0A; }
|
.black {
|
||||||
.red { background: #170c0c; }
|
background: #0a0a0a;
|
||||||
.gold { background: #121107; }
|
}
|
||||||
|
.red {
|
||||||
|
background: #170c0c;
|
||||||
|
}
|
||||||
|
.gold {
|
||||||
|
background: #121107;
|
||||||
|
}
|
||||||
|
|
||||||
.black .theme-primary { color: rgba(255, 255, 255, 0.95); }
|
.black .theme-primary {
|
||||||
.red .theme-primary { color: #f87171; }
|
color: rgba(255, 255, 255, 0.95);
|
||||||
.gold .theme-primary { color: #fbbf24; }
|
}
|
||||||
|
.red .theme-primary {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
.gold .theme-primary {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
.black .theme-secondary { color: rgba(255, 255, 255, 0.7); }
|
.black .theme-secondary {
|
||||||
.red .theme-secondary { color: rgba(248, 113, 113, 0.85); }
|
color: rgba(255, 255, 255, 0.7);
|
||||||
.gold .theme-secondary { color: rgba(251, 191, 36, 0.85); }
|
}
|
||||||
|
.red .theme-secondary {
|
||||||
|
color: rgba(248, 113, 113, 0.85);
|
||||||
|
}
|
||||||
|
.gold .theme-secondary {
|
||||||
|
color: rgba(251, 191, 36, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
.black .theme-accent { background-color: rgba(255, 255, 255, 0.05); }
|
.black .theme-accent {
|
||||||
.red .theme-accent { background-color: rgba(248, 113, 113, 0.08); }
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
.gold .theme-accent { background-color: rgba(251, 191, 36, 0.08); }
|
}
|
||||||
|
.red .theme-accent {
|
||||||
|
background-color: rgba(248, 113, 113, 0.08);
|
||||||
|
}
|
||||||
|
.gold .theme-accent {
|
||||||
|
background-color: rgba(251, 191, 36, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.black .theme-border { border-color: rgba(255, 255, 255, 0.1); }
|
.black .theme-border {
|
||||||
.red .theme-border { border-color: rgba(248, 113, 113, 0.15); }
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
.gold .theme-border { border-color: rgba(251, 191, 36, 0.15); }
|
}
|
||||||
|
.red .theme-border {
|
||||||
|
border-color: rgba(248, 113, 113, 0.15);
|
||||||
|
}
|
||||||
|
.gold .theme-border {
|
||||||
|
border-color: rgba(251, 191, 36, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
.black .theme-text-40 { color: rgba(255, 255, 255, 0.5); }
|
.black .theme-text-40 {
|
||||||
.red .theme-text-40 { color: rgba(248, 113, 113, 0.6); }
|
color: rgba(255, 255, 255, 0.5);
|
||||||
.gold .theme-text-40 { color: rgba(251, 191, 36, 0.6); }
|
}
|
||||||
|
.red .theme-text-40 {
|
||||||
|
color: rgba(248, 113, 113, 0.6);
|
||||||
|
}
|
||||||
|
.gold .theme-text-40 {
|
||||||
|
color: rgba(251, 191, 36, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
.black .theme-text-70 { color: rgba(255, 255, 255, 0.7); }
|
.black .theme-text-70 {
|
||||||
.red .theme-text-70 { color: rgba(248, 113, 113, 0.75); }
|
color: rgba(255, 255, 255, 0.7);
|
||||||
.gold .theme-text-70 { color: rgba(251, 191, 36, 0.75); }
|
}
|
||||||
|
.red .theme-text-70 {
|
||||||
|
color: rgba(248, 113, 113, 0.75);
|
||||||
|
}
|
||||||
|
.gold .theme-text-70 {
|
||||||
|
color: rgba(251, 191, 36, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
.black .theme-text-90 { color: rgba(255, 255, 255, 0.9); }
|
.black .theme-text-90 {
|
||||||
.red .theme-text-90 { color: rgba(248, 113, 113, 0.95); }
|
color: rgba(255, 255, 255, 0.9);
|
||||||
.gold .theme-text-90 { color: rgba(251, 191, 36, 0.95); }
|
}
|
||||||
|
.red .theme-text-90 {
|
||||||
|
color: rgba(248, 113, 113, 0.95);
|
||||||
|
}
|
||||||
|
.gold .theme-text-90 {
|
||||||
|
color: rgba(251, 191, 36, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
.black .theme-bg-05 { background-color: rgba(255, 255, 255, 0.05); }
|
.black .theme-bg-05 {
|
||||||
.red .theme-bg-05 { background-color: rgba(248, 113, 113, 0.06); }
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
.gold .theme-bg-05 { background-color: rgba(251, 191, 36, 0.06); }
|
}
|
||||||
|
.red .theme-bg-05 {
|
||||||
|
background-color: rgba(248, 113, 113, 0.06);
|
||||||
|
}
|
||||||
|
.gold .theme-bg-05 {
|
||||||
|
background-color: rgba(251, 191, 36, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
.black .nav-glass {
|
.black .nav-glass {
|
||||||
background: rgba(10, 10, 10, 0.8);
|
background: rgba(10, 10, 10, 0.8);
|
||||||
@ -55,7 +109,6 @@
|
|||||||
|
|
||||||
.black .nav-item-active {
|
.black .nav-item-active {
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
border-bottom: 2px solid rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
}
|
||||||
.red .nav-item-active {
|
.red .nav-item-active {
|
||||||
background: rgba(248, 113, 113, 0.08);
|
background: rgba(248, 113, 113, 0.08);
|
||||||
@ -74,7 +127,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-item::after {
|
.nav-item::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -110,7 +163,7 @@
|
|||||||
|
|
||||||
.black .theme-button-primary {
|
.black .theme-button-primary {
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
color: #0A0A0A;
|
color: #0a0a0a;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
.red .theme-button-primary {
|
.red .theme-button-primary {
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2017",
|
"target": "ES2017",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@ -18,18 +14,10 @@
|
|||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./src/*"]
|
||||||
"./src/*"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"jsxImportSource": "react"
|
"jsxImportSource": "react"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"next-env.d.ts",
|
"exclude": ["node_modules"]
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
Reference in New Issue
Block a user