1
0
forked from jleibl/jleibl.net

70 Commits

Author SHA1 Message Date
c8467b8b3b chore(deps): update dependency astro to v5.9.2 2025-06-10 00:16:29 +00:00
5a90f11ce0 Merge pull request 'chore(deps): update dependency astro to v5.9.0' (#9) from renovate/astro-5.x into main
Reviewed-on: jleibl/jleibl.net#9
2025-06-06 10:29:24 +00:00
f328839796 chore(deps): update dependency astro to v5.9.0 2025-06-06 00:02:21 +00:00
f4b911a05b Merge pull request 'chore(deps): update dependency astro to v5.8.1' (#8) from renovate/astro-5.x into main
Reviewed-on: jleibl/jleibl.net#8
2025-06-01 17:57:33 +00:00
1184cedc2d chore(deps): update dependency astro to v5.8.1 2025-05-30 00:03:09 +00:00
03bfef7d5b Merge pull request 'chore(deps): update dependency @types/node to v22' (#7) from renovate/node-22.x into main
Reviewed-on: jleibl/jleibl.net#7
2025-05-26 19:52:56 +00:00
ed7b3d1d31 Merge pull request 'chore(deps): update dependency astro to v5.8.0' (#6) from renovate/astro-5.x into main
Reviewed-on: jleibl/jleibl.net#6
2025-05-26 19:52:49 +00:00
0c1032d0d7 Merge pull request 'chore(deps): update dependency @astrojs/react to v4.3.0' (#5) from renovate/astrojs-react-4.x into main
Reviewed-on: jleibl/jleibl.net#5
2025-05-26 19:52:41 +00:00
03dd77724d chore(deps): update dependency astro to v5.8.0 2025-05-24 00:02:10 +00:00
9ee5c0ea23 chore(deps): update dependency @astrojs/react to v4.3.0 2025-05-24 00:01:39 +00:00
97a22ccf24 chore(deps): update dependency @types/node to v22 2025-05-23 20:14:12 +00:00
f854a113d1 Merge pull request 'chore: Configure Renovate' (#4) from renovate/configure into main
Reviewed-on: jleibl/jleibl.net#4
2025-05-23 19:45:11 +00:00
c1124ce45c chore(deps): add renovate.json 2025-05-23 19:44:37 +00:00
fd9545cc10 feat(sitemap): add custom sitemap configuration and XSLT file 2025-05-16 15:11:44 +02:00
14209935f6 feat: add security headers, update manifest and cache assets 2025-05-16 12:04:37 +02:00
31b897c375 build: update dependencies in package.json 2025-05-16 11:59:07 +02:00
531f2c1288 feat: add sitemap integration and update metadata settings 2025-05-16 11:56:21 +02:00
7e41d57515 feat: add hobby section and update navigation links 2025-05-01 14:41:42 +02:00
5b917142f2 refactor: simplify project and tag icon handling 2025-05-01 14:22:51 +02:00
fc5b43d029 feat: add project link modal and redirect functionality 2025-05-01 14:18:57 +02:00
1ecdf57df8 refactor: remove blog section and update navigation items 2025-04-25 13:44:27 +02:00
a129511728 fix: update profile image references in project files 2025-04-25 13:40:15 +02:00
75c6a43203 build: update dependencies in package.json file 2025-04-25 13:33:55 +02:00
92aca15026 fix(package.json): update dependencies and devDependencies 2025-04-03 23:30:20 +02:00
a6b012d6fd build: add react-icons dependency to package.json 2025-03-20 10:47:14 +01:00
5647574c8e build: update astro version in package.json 2025-03-20 10:41:26 +01:00
b057943dde chore: update image references and add .astro to .gitignore 2025-03-20 10:40:55 +01:00
64565ce955 chore: remove deprecated Astro content files 2025-03-20 10:36:11 +01:00
f10ecb37b2 Merge branch 'develop' 2025-03-20 10:30:07 +01:00
2ad119a261 chore: remove unused files and update package dependencies 2025-03-20 10:29:22 +01:00
adca2bae72 Merge pull request 'develop' (#3) from develop into main
Reviewed-on: jleibl/jleibl.net#3
2025-03-13 20:05:49 +00:00
a261bbd60c chore: update project configurations and add Prettier settings 2025-03-13 21:05:32 +01:00
1f2304144c feat: redesign mobile menu with improved structure and styles 2025-03-13 21:00:19 +01:00
cc4ce23dcd feat(service-worker): add auth asset handling and caching logic 2025-03-13 21:00:19 +01:00
4f719353af feat(blog): add "Coming Soon" message and buttons 2025-03-13 21:00:16 +01:00
a7b400d2a8 feat: add portfolio, blog, and contact features 2025-03-13 20:59:42 +01:00
5dc8215ca6 style(theme): update background color for .black class 2025-03-13 20:58:43 +01:00
68ce9f3efd style: update background color for .black class 2025-03-13 20:58:43 +01:00
eafda93bd0 feat: add portfolio, blog, and contact features, update theme styling, refine imports, and clean up unused code (#2)
Reviewed-on: jleibl/jleibl.net#2
2025-03-13 19:50:43 +00:00
3d892a44a8 style(Hero): center-align text and adjust flex properties 2025-03-10 20:52:10 +01:00
3e08b3e514 style(About.astro): add gradient line to About section 2025-03-10 12:09:32 +01:00
8e95fa4937 style: update styles for consistency and responsiveness 2025-03-10 11:34:57 +01:00
93b5e3ae31 docs: update README for personal portfolio website 2025-03-09 14:55:14 +01:00
22d95d0d44 feat(Dockerfile): add allowed-hosts to CMD configuration 2025-03-09 14:47:38 +01:00
9b0ecf18c7 feat(config): update server settings and CORS options 2025-03-09 14:41:43 +01:00
c82aa23b41 chore: add allowedHosts to server and preview config 2025-03-09 14:36:45 +01:00
43ef4336a6 fix(Dockerfile): allow binding to all hosts in CMD 2025-03-09 14:33:41 +01:00
9eb628971d feat(Dockerfile): add host option to CMD for preview 2025-03-09 14:33:18 +01:00
72f2f1c07d feat: update port configuration for Astro application 2025-03-09 14:31:57 +01:00
ab1e92ae54 chore: update Dockerfile and remove vite.config.js 2025-03-09 14:25:46 +01:00
fc90fcdff5 feat: add Vite configuration file for server settings 2025-03-09 14:22:38 +01:00
f3e5aaa2f4 chore: restrict allowedHosts to jleibl.net 2025-03-09 14:18:58 +01:00
ee79833992 chore: update allowedHosts and set preview port 2025-03-09 14:17:07 +01:00
6528c0e79e fix: update allowedHosts configuration to true 2025-03-09 14:14:00 +01:00
fc47a93671 chore: update allowedHosts to accept all hosts 2025-03-09 14:11:32 +01:00
5634704a3f feat(config): add preview allowedHosts configuration 2025-03-09 14:09:59 +01:00
eb126dc6f2 feat: add allowed hosts to Vite server configuration 2025-03-09 14:07:46 +01:00
67400ee308 fix(Dockerfile): update command to start Astro application 2025-03-09 14:06:11 +01:00
60935042a7 chore: update Dockerfile and add references in types.d.ts 2025-03-09 14:04:09 +01:00
7760e8d360 feat(Dockerfile): change CMD to use bun preview 2025-03-09 13:55:03 +01:00
83dd2e5630 fix(Dockerfile): update CMD to remove port specification 2025-03-09 13:53:26 +01:00
777af4215d chore(Dockerfile): update exposed port to 4321 2025-03-09 13:50:14 +01:00
89117b092b build(Dockerfile): update build command for production 2025-03-09 13:44:13 +01:00
4192c882d0 chore: remove unused reference in types and update package.json 2025-03-09 13:43:10 +01:00
6cc0e06918 feat: add initial Astro project structure and components 2025-03-09 13:38:05 +01:00
0ab11c240c fix: update Tailwind CSS and adjust related styles 2025-03-09 12:39:34 +01:00
29a58817be refactor(About): clean up unused imports and code 2025-03-05 10:27:39 +01:00
0dd79cd1ec feat: add technology and interest icons in About section 2025-02-25 21:53:56 +01:00
6e8a7ecf62 refactor: remove unused SVG from Footer component 2025-02-25 14:17:24 +01:00
5a757989a4 Merge pull request 'Add Dockerfile' (#1) from jank/jleibl.net:main into main
Reviewed-on: jleibl/jleibl.net#1
2025-02-25 13:00:32 +00:00
57 changed files with 3700 additions and 1409 deletions

7
.gitignore vendored
View File

@ -10,6 +10,10 @@
!.yarn/releases
!.yarn/versions
# astro
/dist
.astro
# testing
/coverage
@ -17,6 +21,9 @@
/.next/
/out/
# vscode
.vscode
# production
/build

18
.prettierignore Normal file
View File

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

27
.prettierrc Normal file
View File

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

View File

@ -10,9 +10,9 @@ ENV NODE_ENV=production
COPY . .
# Install dependencies
RUN bun install --prod
RUN bun install
# Step 3: Build the Next.js application
# Step 3: Build the Astro application
RUN bun run build
# Step 5: Set up the production image
@ -25,10 +25,14 @@ COPY --from=build /app /app
# Install only production dependencies
RUN bun install --prod
RUN bun next build
RUN bun astro build
# Expose the application port
EXPOSE 3001
EXPOSE 4321
# Start the Next.js application
CMD ["bun", "start", "-p", "3001"]
ENV ASTRO_TELEMENTRY_DISABLED=1
ENV __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=jleibl.net
ENV __VITE_ADDITIONAL_PREVIEW_ALLOWED_HOSTS=jleibl.net
# Start the Astro application
CMD ["bun", "preview", "--port", "4321", "--host", "0.0.0.0", "--allowed-hosts", "jleibl.net"]

View File

@ -1,40 +1,74 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app).
# jleibl.net - Personal Portfolio Website
## Getting Started
<div align="center">
<img src="public/profile-image-futu-style.jpg" alt="Jan-Marlon Leibl" width="150" style="border-radius: 50%;" />
<h3>Jan-Marlon Leibl</h3>
<p>Fullstack Software Developer | PHP & TypeScript Expert</p>
<a href="https://jleibl.net" target="_blank">Live Website</a>
</div>
First, run the development server:
## 🚀 About
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
This is the personal portfolio website for Jan-Marlon Leibl, showcasing professional experience, skills, and projects. Built with modern web technologies for optimal performance and user experience.
## 🛠️ Tech Stack
- **Framework**: [Astro](https://astro.build) - The web framework for content-driven websites
- **UI Libraries**: [React](https://react.dev) - For interactive components
- **Styling**: [TailwindCSS](https://tailwindcss.com) - Utility-first CSS framework
- **Animations**: [Framer Motion](https://www.framer.com/motion/) - For smooth animations
- **Fonts**: DM Sans, Instrument Sans
- **Icons**: Astro Icon, React Icons
- **Runtime**: [Bun](https://bun.sh) - Fast JavaScript runtime & package manager
- **Deployment**: Docker containerization
## 🏗️ Project Structure
```
/
├── public/ # Static assets
├── src/
│ ├── components/ # UI components
│ ├── pages/ # Page routes and templates
│ └── styles/ # Global styles
├── astro.config.mjs # Astro configuration
└── package.json # Project dependencies
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
## 🧞 Development
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
All commands use [Bun](https://bun.sh) as the package manager and runtime:
[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
```bash
# Install dependencies
bun install
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages.
# Start development server
bun dev
This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
# Build for production
bun build
## Learn More
# Preview production build locally
bun preview
```
To learn more about Next.js, take a look at the following resources:
## 🐳 Docker Deployment
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial.
The project includes a Dockerfile for containerized deployment:
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
```bash
# Build the Docker image
docker build -t jleibl-website .
## Deploy on Vercel
# Run the container
docker run -p 4321:4321 jleibl-website
```
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
## 📝 License
Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details.
© 2025 Jan-Marlon Leibl. All rights reserved.
## 🔗 Connect
- Website: [jleibl.net](https://jleibl.net)

77
astro.config.mjs Normal file
View File

@ -0,0 +1,77 @@
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import tailwindcss from "@tailwindcss/vite";
import icon from "astro-icon";
import sitemap from "@astrojs/sitemap";
// https://astro.build/config
export default defineConfig({
site: "https://jleibl.net",
integrations: [
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({
include: {
ph: ["*"],
logos: ["*"],
mdi: ["*"],
},
}),
],
vite: {
plugins: [tailwindcss()],
server: {
host: "0.0.0.0",
allowedHosts: true,
cors: {
origin: "*",
methods: ["GET"],
allowedHeaders: ["X-Requested-With"],
},
},
preview: {
host: "0.0.0.0",
allowedHosts: ["jleibl.net"],
port: 4321,
cors: {
origin: "*",
methods: ["GET"],
allowedHeaders: ["X-Requested-With"],
},
},
},
});

1365
bun.lock

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,8 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
reactStrictMode: true,
};
export default nextConfig;

View File

@ -3,30 +3,40 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"start": "astro dev --host",
"lint": "astro check",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"dependencies": {
"@fontsource/dm-sans": "^5.1.1",
"@fontsource/instrument-sans": "^5.1.1",
"@studio-freight/lenis": "^1.0.42",
"framer-motion": "^12.4.7",
"next": "15.1.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-intersection-observer": "^9.15.1"
"@astrojs/check": "^0.9.4",
"@astrojs/react": "4.3.0",
"@astrojs/sitemap": "^3.4.0",
"@fontsource/dm-sans": "^5.2.5",
"@fontsource/instrument-sans": "^5.2.5",
"@iconify-json/logos": "^1.2.4",
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/ph": "^1.2.2",
"@tailwindcss/postcss": "^4.1.2",
"@tailwindcss/vite": "^4.1.2",
"astro": "5.9.2",
"astro-icon": "^1.1.5",
"framer-motion": "^12.6.3",
"lenis": "^1.2.3",
"react-icons": "^5.5.0"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^9",
"eslint-config-next": "15.1.7",
"@eslint/eslintrc": "^3"
"@eslint/eslintrc": "^3.3.1",
"@types/node": "^22.0.0",
"eslint": "^9.23.0",
"eslint-config-prettier": "^10.1.1",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"tailwindcss": "^4.1.2",
"typescript": "^5.8.2"
}
}

View File

@ -1,7 +1,7 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
"@tailwindcss/postcss": {},
},
};

23
public/_headers Normal file
View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

60
public/manifest.json Normal file
View 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"
}
]
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

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
View 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
View 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
View 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>

View File

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View File

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

3
renovate.json Normal file
View File

@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@ -0,0 +1,75 @@
---
import FadeIn from "../ui/FadeIn.astro";
import { FaGithub, FaLinkedin, FaEnvelope } from "react-icons/fa";
---
<footer role="contentinfo" class="mt-20 sm:mt-40 relative">
<div class="absolute inset-0 theme-bg-gradient opacity-30"></div>
<div
class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"
>
</div>
<div class="max-w-(--breakpoint-xl) mx-auto px-4 sm:px-8 py-20 sm:py-32 relative">
<div class="grid gap-16 sm:gap-24">
<FadeIn>
<div class="text-center space-y-5 sm:space-y-6">
<h2
class="font-['DM_Sans'] text-4xl sm:text-6xl font-semibold tracking-tight theme-primary"
>
Let&apos;s Connect
</h2>
<p
class="font-['Instrument_Sans'] theme-text-70 text-lg sm:text-2xl max-w-2xl mx-auto leading-relaxed"
>
Always interested in new opportunities and collaborations.
</p>
</div>
</FadeIn>
<FadeIn delay={0.1}>
<div class="flex flex-col items-center gap-10 sm:gap-14 font-['Instrument_Sans']">
<a
href="mailto:jleibl@proton.me"
class="group flex items-center gap-3 text-xl sm:text-3xl theme-text-90 hover:theme-primary transition-all hover:scale-105 duration-300"
>
<span class="p-2 rounded-xl theme-bg-05 border theme-border">
<FaEnvelope className="w-5 h-5 sm:w-6 sm:h-6" />
</span>
jleibl@proton.me
</a>
<div class="flex gap-6 sm:gap-8">
<a
href="https://github.com/AtomicWasTaken"
target="_blank"
rel="noopener noreferrer"
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
aria-label="GitHub Profile"
>
<FaGithub className="w-6 h-6 sm:w-7 sm:h-7" />
</a>
<a
href="https://www.linkedin.com/in/janmarlonleibl/"
target="_blank"
rel="noopener noreferrer"
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
aria-label="LinkedIn Profile"
>
<FaLinkedin className="w-6 h-6 sm:w-7 sm:h-7" />
</a>
</div>
</div>
</FadeIn>
<FadeIn delay={0.2}>
<div class="pt-8 sm:pt-16 text-center">
<p class="font-['Instrument_Sans'] theme-text-40 text-xs sm:text-sm tracking-wider">
© {new Date().getFullYear()} Jan-Marlon Leibl. All rights reserved.
</p>
</div>
</FadeIn>
</div>
</div>
</footer>

View File

@ -1,42 +0,0 @@
import { FadeIn } from '../ui/FadeIn';
export const Footer = () => {
return (
<footer role="contentinfo" className="mt-20 sm:mt-32 theme-bg-gradient border-t theme-border">
<div className="max-w-screen-xl mx-auto px-4 sm:px-8 py-20 sm:py-32">
<div className="grid gap-16 sm:gap-24">
<FadeIn>
<div className="text-center space-y-4 sm:space-y-5">
<h2 className="font-['DM_Sans'] text-4xl sm:text-7xl font-semibold tracking-tight theme-primary">Let&apos;s Connect</h2>
<p className="font-['Instrument_Sans'] theme-text-70 text-lg sm:text-2xl max-w-2xl mx-auto">
Always interested in new opportunities and collaborations.
</p>
</div>
</FadeIn>
<FadeIn delay={0.1}>
<div className="flex flex-col items-center gap-8 sm:gap-12">
<a
href="mailto:jleibl@proton.me"
className="group flex items-center gap-2 sm:gap-3 text-xl sm:text-4xl theme-text-90 hover:theme-primary transition-colors"
>
jleibl@proton.me
<svg className="w-5 h-5 sm:w-8 sm:h-8 opacity-0 group-hover:opacity-100 transition-all -translate-x-4 group-hover:translate-x-0 duration-300" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
</div>
</FadeIn>
<FadeIn delay={0.2}>
<div className="pt-6 sm:pt-12 text-center">
<p className="theme-text-40 text-xs sm:text-sm tracking-[0.1em]">
© {new Date().getFullYear()} Jan-Marlon Leibl. All rights reserved.
</p>
</div>
</FadeIn>
</div>
</div>
</footer>
);
};

View File

@ -0,0 +1,221 @@
---
import MobileMenu from "./MobileMenu.astro";
---
<header
class="fixed top-0 left-0 right-0 z-40 nav-glass backdrop-blur-lg theme-transition"
role="banner"
>
<div class="max-w-7xl mx-auto">
<nav
class="py-4 px-6 sm:px-8 flex justify-between items-center font-['Instrument_Sans']"
role="navigation"
aria-label="Main navigation"
>
<div class="flex items-center">
<a href="#hero" class="flex items-center gap-2.5 group" aria-label="Home">
<div
class="relative w-9 h-9 rounded-xl theme-accent flex items-center justify-center border theme-border overflow-hidden group-hover:border-opacity-80 transition-all duration-300 shadow-sm"
>
<span class="text-base font-bold tracking-tight theme-primary theme-transition">JL</span
>
<div
class="absolute inset-0 theme-bg-05 opacity-0 group-hover:opacity-100 transition-opacity duration-500"
>
</div>
</div>
<span
class="text-base font-medium tracking-tight theme-primary theme-transition hidden sm:block"
>
Jan-Marlon Leibl
</span>
</a>
</div>
<div class="hidden md:flex items-center">
<div
class="flex bg-black/5 backdrop-blur-md overflow-hidden theme-border border divide-x divide-white/5 shadow-sm rounded-xl"
>
{
["About", "Hobby", "Work", "Contact"].map((item, index) => (
<a
href={`#${item.toLowerCase()}`}
class={`nav-item px-5 py-2 theme-secondary hover:theme-primary theme-transition ${index === 0 ? "rounded-l-xl" : ""} ${index === 3 ? "rounded-r-xl" : ""}`}
aria-label={`View my ${item.toLowerCase()}`}
>
{item}
</a>
))
}
</div>
<div class="flex items-center gap-4 ml-6">
<a
href="mailto:jleibl@proton.me"
class="flex items-center gap-2 px-5 py-2 theme-bg-05 theme-primary border theme-border rounded-xl hover:bg-opacity-100 transition-all duration-300 group shadow-sm"
aria-label="Contact me via email"
>
<svg
class="w-4 h-4 theme-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
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>
<span class="text-sm font-medium">Contact</span>
<div class="w-0 overflow-hidden group-hover:w-4 transition-all duration-300">
<svg
class="w-4 h-4 theme-primary"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12h14M12 5l7 7-7 7"></path>
</svg>
</div>
</a>
</div>
</div>
<div class="flex md:hidden">
<MobileMenu />
</div>
</nav>
</div>
</header>
<script>
document.addEventListener("DOMContentLoaded", () => {
const header = document.querySelector("header");
if (!header) return;
header.style.transform = "translateY(0)";
header.style.opacity = "1";
let lastScrollY = window.scrollY;
const scrollThreshold = 100;
let isHeaderVisible = true;
const sections = document.querySelectorAll("section[id]");
const navItems = document.querySelectorAll(".nav-item");
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");
}
});
}
}
window.addEventListener("scroll", () => {
const currentScrollY = window.scrollY;
if (currentScrollY > 20) {
header.classList.add("scrolled");
} else {
header.classList.remove("scrolled");
}
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>
<style>
header {
transform: translateY(-100px);
opacity: 0;
transition:
transform 0.4s ease-out,
opacity 0.5s ease-out,
border 0.3s ease;
}
header.scrolled {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.nav-item {
position: relative;
overflow: hidden;
font-weight: 500;
transition: all 0.2s ease;
}
.nav-item::after {
content: "";
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>

View File

@ -1,90 +0,0 @@
import { motion } from 'framer-motion';
import Link from 'next/link';
import dynamic from 'next/dynamic';
// Lazy load the mobile menu component
const MobileMenu = dynamic(() => import('./MobileMenu').then(mod => ({ default: mod.MobileMenu })), {
ssr: false,
loading: () => (
<div className="p-2 theme-accent rounded-lg theme-border">
<svg className="w-5 h-5 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</div>
)
});
export const Header = () => {
return (
<motion.header
className="fixed top-0 left-0 right-0 z-40 nav-glass backdrop-blur-lg theme-transition"
role="banner"
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{
type: "spring",
damping: 25,
stiffness: 100,
mass: 1,
delay: 0.2
}}
>
<div className="max-w-screen-xl mx-auto">
<nav className="py-5 px-6 sm:px-8 flex justify-between items-center font-['DM_Sans']" role="navigation" aria-label="Main navigation">
<div className="flex items-center">
<Link href="/" className="flex items-center gap-2 group" aria-label="Home">
<div className="relative w-10 h-10 rounded-full theme-accent flex items-center justify-center border theme-border overflow-hidden group-hover:border-opacity-80 transition-all duration-300">
<span className="text-lg font-bold tracking-tight theme-primary theme-transition">JL</span>
<div className="absolute inset-0 theme-bg-05 opacity-0 group-hover:opacity-100 transition-opacity duration-500"/>
</div>
<span className="text-lg font-medium tracking-tight theme-primary theme-transition hidden sm:block">
Jan-Marlon Leibl
</span>
</Link>
</div>
<div className="hidden md:flex items-center">
<div className="flex bg-black/10 backdrop-blur-md rounded-full overflow-hidden theme-border border divide-x divide-white/5">
{["Work", "About"].map((item) => (
<a
key={item}
href={`#${item.toLowerCase()}`}
className="nav-item px-6 py-2 theme-secondary hover:theme-primary theme-transition"
aria-label={`View my ${item.toLowerCase()}`}
>
{item}
</a>
))}
</div>
<div className="ml-6">
<motion.a
href="mailto:jleibl@proton.me"
className="flex items-center gap-2 px-6 py-2 theme-bg-05 theme-primary border theme-border rounded-full hover:bg-opacity-100 transition-all duration-300 group"
aria-label="Contact me via email"
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
<svg className="w-4 h-4 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="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 className="text-sm">Contact</span>
<div className="w-0 overflow-hidden group-hover:w-4 transition-all duration-500">
<svg className="w-4 h-4 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</div>
</motion.a>
</div>
</div>
<div className="flex md:hidden">
<MobileMenu />
</div>
</nav>
</div>
<div className="h-px w-full theme-border opacity-50"></div>
</motion.header>
);
};

View 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" />

View File

@ -0,0 +1,259 @@
---
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">
<button
id="menu-btn"
class="menu-btn"
aria-label="Toggle mobile menu"
aria-expanded="false"
aria-controls="mobile-menu-panel"
>
<span class="bar"></span>
<span class="bar"></span>
<span class="bar"></span>
</button>
<div id="mobile-menu-panel" class="menu-panel">
<div class="menu-content">
<nav class="menu-nav">
{
navItems.map((item) => (
<a href={item.href} class="menu-item">
{item.label}
</a>
))
}
</nav>
<div class="menu-footer">
<a href="mailto:jleibl@proton.me" class="contact-button">
<span class="icon-wrapper">
<FaEnvelope className="w-5 h-5" />
</span>
<span>Contact Me</span>
</a>
<div class="social-links">
<a
href="https://github.com/AtomicWasTaken"
target="_blank"
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>
<script>
document.addEventListener("DOMContentLoaded", () => {
const menuBtn = document.getElementById("menu-btn");
const menuPanel = document.getElementById("mobile-menu-panel");
const menuItems = document.querySelectorAll(".menu-item");
function toggleMenu() {
if (!menuBtn || !menuPanel) return;
const isOpen = menuBtn.classList.toggle("active");
menuBtn.setAttribute("aria-expanded", isOpen.toString());
menuPanel.classList.toggle("open", isOpen);
document.body.style.overflow = isOpen ? "hidden" : "";
}
menuBtn?.addEventListener("click", toggleMenu);
menuItems.forEach((item) => {
item.addEventListener("click", toggleMenu);
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && menuPanel?.classList.contains("open")) {
toggleMenu();
}
});
});
</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>

View File

@ -1,147 +0,0 @@
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { GermanyFlag } from '../ui/GermanyFlag';
export const MobileMenu = () => {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
useEffect(() => {
const handleEscKey = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
setIsOpen(false);
}
};
document.addEventListener('keydown', handleEscKey);
return () => document.removeEventListener('keydown', handleEscKey);
}, [isOpen]);
return (
<>
<motion.button
onClick={() => setIsOpen(true)}
className="p-2 theme-accent rounded-lg theme-border"
aria-label="Open menu"
aria-expanded={isOpen}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
<svg className="w-5 h-5 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</motion.button>
<AnimatePresence mode="wait">
{isOpen && (
<>
<motion.div
className="fixed inset-0 bg-black z-[90]"
initial={{ opacity: 0 }}
animate={{ opacity: 0.95 }}
exit={{ opacity: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
onClick={() => setIsOpen(false)}
aria-hidden="true"
/>
<motion.div
className="fixed top-0 bottom-0 right-0 w-full sm:w-80 z-[100] h-[100dvh]"
ref={menuRef}
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", damping: 30, stiffness: 300, mass: 1 }}
>
<div className="nav-glass w-full h-full flex flex-col shadow-2xl"
style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)' }}>
<motion.button
onClick={() => setIsOpen(false)}
className="absolute top-4 right-4 p-2 theme-accent rounded-lg theme-border z-[110]"
aria-label="Close menu"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ type: "spring", stiffness: 300, damping: 20, delay: 0.2 }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<svg className="w-5 h-5 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M6 18L18 6M6 6l12 12"></path>
</svg>
</motion.button>
<div className="w-full h-full p-6 pt-16 flex flex-col overflow-y-auto">
<nav className="mb-8">
<motion.div
className="flex flex-col space-y-2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ type: "spring", stiffness: 100, damping: 20, delay: 0.1 }}
>
{[
{ href: "#work", label: "Work" },
{ href: "#about", label: "About" }
].map((item, index) => (
<motion.a
key={item.href}
href={item.href}
onClick={() => setIsOpen(false)}
className="p-4 text-xl theme-primary rounded-lg theme-accent flex items-center hover:theme-bg-05 transition-colors"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ type: "spring", stiffness: 200, damping: 20, delay: 0.2 + index * 0.1 }}
>
<span>{item.label}</span>
<svg className="ml-auto w-5 h-5 theme-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</motion.a>
))}
</motion.div>
</nav>
<motion.div
className="mt-auto"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 100, damping: 20, delay: 0.4 }}
>
<div className="pt-4 border-t theme-border">
<motion.a
href="mailto:jleibl@proton.me"
onClick={() => setIsOpen(false)}
className="flex items-center justify-center gap-3 w-full py-4 px-6 theme-primary border theme-border rounded-lg hover:theme-bg-05 transition-colors"
whileHover={{ scale: 1.02 }}
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="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>Contact Me</span>
</motion.a>
</div>
<div className="pt-6 flex justify-center pb-6">
<GermanyFlag />
</div>
</motion.div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
);
};

View File

@ -0,0 +1,173 @@
---
import FadeIn from "../ui/FadeIn.astro";
import { Icon } from "astro-icon/components";
import { SiPhp, SiJavascript, SiMysql, SiReact, SiTypescript, SiAstro } from "react-icons/si";
import { FaGithub, FaLinkedin, FaGlobe, FaPalette, FaCamera } from "react-icons/fa";
import { BsLightningChargeFill } from "react-icons/bs";
import { IoServerOutline } from "react-icons/io5";
import { Image } from "astro:assets";
const technologies = [
{ name: "TypeScript", icon: SiTypescript },
{ name: "React", icon: SiReact },
{ name: "PHP", icon: SiPhp },
{ name: "Astro", icon: SiAstro },
];
const interests = [
{ name: "Web Development", icon: FaGlobe },
{ name: "System Architecture", icon: IoServerOutline },
{ name: "UI/UX Design", icon: FaPalette },
{ name: "Creative Vision", icon: FaCamera },
];
---
<section id="about" class="py-24 sm:py-40 px-4 sm:px-8 relative">
<div class="absolute inset-0 theme-bg-gradient opacity-50"></div>
<div
class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"
>
</div>
<div class="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"
>
About
</h2>
<div class="h-px grow theme-border opacity-60"></div>
</div>
</FadeIn>
<div class="grid md:grid-cols-[1fr_2fr] gap-12 sm:gap-24">
<div class="space-y-6 sm:space-y-8">
<FadeIn>
<div
class="aspect-square theme-bg-05 rounded-2xl overflow-hidden border theme-border hover:border-white/20 transition-all duration-300 shadow-md hover:shadow-lg mx-auto md:mx-0 max-w-[280px] md:max-w-none relative group"
>
<Image
src="/profile-image-futu-style.jpg"
alt="Jan-Marlon Leibl - Fullstack Software Developer"
width="400"
height="400"
class="object-cover w-full h-full group-hover:scale-105 transition-transform duration-700"
/>
<div
class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500"
>
</div>
</div>
</FadeIn>
<FadeIn delay={0.1}>
<div class="space-y-2 text-center md:text-left">
<h3 class="font-['DM_Sans'] text-xl sm:text-2xl font-medium theme-primary">
Jan-Marlon Leibl
</h3>
<p class="font-['Instrument_Sans'] theme-text-70 text-base sm:text-lg">
Fullstack Web Developer
<br>
Photographer Hobbyist
</p>
</div>
</FadeIn>
</div>
<FadeIn delay={0.2}>
<div class="space-y-10 sm:space-y-16 font-['Instrument_Sans']">
<div class="space-y-6 sm:space-y-8">
<p class="text-xl sm:text-2xl theme-text-90 leading-relaxed">
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 class="text-base sm:text-xl theme-text-70 leading-relaxed">
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>
</div>
<div class="grid grid-cols-1 sm: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">
Technologies
</h3>
<div class="h-px grow theme-border opacity-30"></div>
</div>
<ul class="space-y-4 sm:space-y-5">
{
technologies.map((tech) => (
<li class="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 cursor-default theme-bg-05 px-4 py-2.5 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
<tech.icon className="w-5 h-5" />
</span>
{tech.name}
</li>
))
}
</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">
Interests
</h3>
<div class="h-px grow theme-border opacity-30"></div>
</div>
<ul class="space-y-4 sm:space-y-5">
{
interests.map((interest) => (
<li class="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 cursor-default theme-bg-05 px-4 py-2.5 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
<interest.icon className="w-5 h-5" />
</span>
{interest.name}
</li>
))
}
</ul>
</div>
</div>
<div class="pt-8 sm:pt-10 flex flex-col sm:flex-row gap-4 sm:gap-6">
<a
href="https://github.com/AtomicWasTaken"
target="_blank"
rel="noopener noreferrer"
class="animate-button w-full text-center px-6 sm:px-8 py-3 sm:py-4 border theme-border rounded-xl theme-button-secondary hover:theme-bg-05 transition-all duration-300 text-sm sm:text-base tracking-tight font-medium flex items-center justify-center gap-2 shadow-sm"
>
<FaGithub className="w-5 h-5" />
View GitHub
</a>
<a
href="https://www.linkedin.com/in/janmarlonleibl/"
target="_blank"
rel="noopener noreferrer"
class="animate-button w-full text-center px-6 sm:px-8 py-3 sm:py-4 border theme-border rounded-xl theme-button-secondary hover:theme-bg-05 transition-all duration-300 text-sm sm:text-base tracking-tight font-medium flex items-center justify-center gap-2 shadow-sm"
>
<FaLinkedin className="w-5 h-5" />
Connect on LinkedIn
</a>
</div>
</div>
</FadeIn>
</div>
</div>
</section>
<style>
.animate-button {
transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}
.animate-button:hover {
transform: translateY(-2px);
}
.animate-button:active {
transform: scale(0.98);
}
</style>

View File

@ -1,118 +0,0 @@
import { motion } from 'framer-motion';
import Image from 'next/image';
import { FadeIn } from '../ui/FadeIn';
export const About = () => {
return (
<section id="about" className="py-20 sm:py-40 px-4 sm:px-8 theme-bg-gradient">
<div className="max-w-screen-xl mx-auto">
<FadeIn>
<div className="flex items-baseline gap-4 mb-12 sm:mb-24">
<h2 className="font-['DM_Sans'] text-3xl sm:text-6xl font-semibold tracking-tight theme-primary">About</h2>
<div className="h-px flex-grow bg-white/10 relative top-[-4px]"></div>
</div>
</FadeIn>
<div className="grid md:grid-cols-[1fr,2fr] gap-12 sm:gap-24">
<div className="space-y-6 sm:space-y-8">
<FadeIn>
<div className="aspect-square bg-gradient-to-tr theme-bg-05 rounded-2xl overflow-hidden border theme-border hover:border-white/10 transition-colors mx-auto md:mx-0 max-w-[280px] md:max-w-none">
<Image
src="/profile-image.jpg"
alt="Jan-Marlon Leibl - Fullstack Software Developer"
width={400}
height={400}
priority
className="object-cover w-full h-full hover:scale-105 transition-transform duration-700"
/>
</div>
</FadeIn>
<FadeIn delay={0.1}>
<div className="space-y-2 text-center md:text-left">
<h3 className="font-['DM_Sans'] text-xl sm:text-2xl font-medium theme-primary">Jan-Marlon Leibl</h3>
<p className="font-['Instrument_Sans'] theme-text-70 text-base sm:text-lg">Fullstack Developer</p>
</div>
</FadeIn>
</div>
<FadeIn delay={0.2}>
<div className="space-y-10 sm:space-y-16 font-['Instrument_Sans']">
<div className="space-y-6 sm:space-y-8">
<p className="text-xl sm:text-2xl theme-text-90 leading-relaxed">
Hello! I&apos;m Jan-Marlon, but please call me Jan. I started my journey in programming at the age of 11 with C#, fascinated by a desktop application my friend created.
</p>
<p className="text-base sm:text-xl theme-text-70 leading-relaxed">
Today, I specialize in PHP and TypeScript development, constantly pushing the boundaries of what&apos;s possible on the web. My journey has led me from creating simple applications to developing complex systems used by thousands.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-10 sm:gap-16">
<div className="space-y-6 sm:space-y-8">
<div className="flex items-baseline gap-4">
<h3 className="text-sm sm:text-base theme-text-40 uppercase tracking-[0.2em]">Technologies</h3>
<div className="h-px flex-grow theme-border"></div>
</div>
<ul className="space-y-4 sm:space-y-5">
{['PHP', 'JavaScript', 'MySQL', 'React'].map((tech) => (
<li
key={tech}
className="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 transition-colors cursor-default"
>
<span className="w-2 h-2 rounded-full theme-text-40 group-hover:theme-text-90 transition-colors"></span>
{tech}
</li>
))}
</ul>
</div>
<div className="space-y-6 sm:space-y-8">
<div className="flex items-baseline gap-4">
<h3 className="text-sm sm:text-base theme-text-40 uppercase tracking-[0.2em]">Interests</h3>
<div className="h-px flex-grow theme-border"></div>
</div>
<ul className="space-y-4 sm:space-y-5">
{[
'Web Development',
'System Architecture',
'UI/UX Design',
'Performance Optimization'
].map((interest) => (
<li
key={interest}
className="text-base sm:text-lg group flex items-center gap-3 theme-text-70 hover:theme-text-90 transition-colors cursor-default"
>
<span className="w-2 h-2 rounded-full theme-text-40 group-hover:theme-text-90 transition-colors"></span>
{interest}
</li>
))}
</ul>
</div>
</div>
<div className="pt-6 sm:pt-8 flex flex-col sm:flex-row gap-3 sm:gap-6">
<motion.a
href="https://github.com/AtomicWasTaken"
target="_blank"
rel="noopener noreferrer"
className="w-full text-center px-6 sm:px-8 py-3 sm:py-4 border theme-border rounded-lg sm:rounded-full hover:theme-bg-05 transition-colors text-sm sm:text-base tracking-wide font-medium theme-text-90"
whileHover={{ scale: 1.02 }}
>
View GitHub
</motion.a>
<motion.a
href="https://www.linkedin.com/in/janmarlonleibl/"
target="_blank"
rel="noopener noreferrer"
className="w-full text-center px-6 sm:px-8 py-3 sm:py-4 border theme-border rounded-lg sm:rounded-full hover:theme-bg-05 transition-colors text-sm sm:text-base tracking-wide font-medium theme-text-90"
whileHover={{ scale: 1.02 }}
>
Connect on LinkedIn
</motion.a>
</div>
</div>
</FadeIn>
</div>
</div>
</section>
);
};

View 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>

View File

@ -0,0 +1,145 @@
---
import FadeIn from "../ui/FadeIn.astro";
import GermanyFlag from "../ui/GermanyFlag.astro";
---
<section
class="min-h-[100dvh] flex items-center justify-center px-4 sm:px-8 relative pt-24 pb-16"
id="hero"
>
<div class="absolute inset-0 theme-bg-gradient opacity-80"></div>
<div class="max-w-7xl w-full relative pt-8 sm:pt-4">
<FadeIn className="space-y-10 sm:space-y-16">
<div class="space-y-8 sm:space-y-10">
<div class="space-y-6 sm:space-y-8">
<div
class="inline-flex items-center gap-2.5 px-4 py-2 rounded-xl theme-accent theme-border border theme-transition shadow-sm"
>
<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']"
>Open to New Projects</span
>
</div>
<div class="space-y-3 sm:space-y-4">
<h2
class="font-['Instrument_Sans'] text-xl sm:text-2xl theme-secondary tracking-tight font-medium theme-transition"
>
Jan-Marlon Leibl
</h2>
<div>
<h1
class="font-['DM_Sans'] text-4xl sm:text-6xl md:text-7xl lg:text-8xl font-bold tracking-tight leading-[1.05] sm:leading-[0.95] max-w-4xl theme-primary theme-transition"
>
Web Developer
<br />
<span class="theme-secondary inline-flex items-center gap-2"
>from <GermanyFlag /></span
>
</h1>
</div>
</div>
</div>
<p
class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-2xl max-w-2xl leading-relaxed tracking-tight theme-transition"
>
Crafting engaging digital experiences with PHP, TypeScript, and a splash of creative thinking.
</p>
<div class="flex flex-col sm:flex-row gap-4 sm:gap-5">
<a
href="#work"
class="animate-button w-full sm:w-auto text-center inline-flex items-center justify-center gap-2 px-6 sm:px-8 py-3 sm:py-4 theme-button-primary rounded-xl transition-all text-sm sm:text-base tracking-tight font-medium group shadow-sm"
>
See My Work
<svg
class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M5 12h14M12 5l7 7-7 7"></path>
</svg>
</a>
<a
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"
>
Say Hello
<svg
class="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M5 12h14M12 5l7 7-7 7"></path>
</svg>
</a>
</div>
</div>
<div class="border-t theme-border pt-8 mt-10">
<div
class="grid grid-cols-1 sm:grid-cols-3 w-full gap-6 sm:gap-0 py-6 sm:py-8 font-['Instrument_Sans'] text-center"
>
<div
class="sm:border-r theme-border last:border-r-0 px-4 flex flex-col items-center justify-center"
>
<div class="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
>
</div>
<a
href="mailto:jleibl@proton.me"
class="theme-text-90 hover:theme-primary transition-colors text-base sm:text-lg block w-full"
>
jleibl@proton.me
</a>
</div>
<div
class="sm:border-r theme-border last:border-r-0 px-4 flex flex-col items-center justify-center"
>
<div class="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
>
</div>
<span class="theme-text-90 text-base sm:text-lg block w-full">
Fullstack Web Dev
</span>
</div>
<div class="px-4 flex flex-col items-center justify-center">
<div class="mb-3 sm:mb-4 w-full text-center">
<span
class="theme-text-40 uppercase tracking-wider text-xs sm:text-sm font-medium block"
>EXPERIENCE</span
>
</div>
<span class="theme-text-90 text-base sm:text-lg block w-full"> 5+ Years </span>
</div>
</div>
</div>
</FadeIn>
</div>
</section>
<style>
.animate-button {
transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
}
.animate-button:hover {
transform: translateY(-2px);
}
.animate-button:active {
transform: scale(0.98);
}
</style>

View File

@ -1,107 +0,0 @@
import { motion } from 'framer-motion';
import { FadeIn } from '../ui/FadeIn';
import { GermanyFlag } from '../ui/GermanyFlag';
export const Hero = () => {
return (
<section className="min-h-[100dvh] flex items-center px-4 sm:px-8 relative pt-24 sm:pt-8">
<div className="absolute inset-0 theme-bg-gradient"></div>
<div className="max-w-screen-xl w-full relative pt-8 sm:pt-24">
<FadeIn className="space-y-8 sm:space-y-16">
<div className="space-y-6 sm:space-y-8">
<div className="space-y-6 sm:space-y-8">
<div className="inline-flex items-center gap-2.5 px-4 py-2 rounded-full theme-accent theme-border theme-transition">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
<span className="theme-secondary uppercase tracking-[0.2em] text-xs sm:text-sm font-['Instrument_Sans']">Available for Work</span>
</div>
<div className="space-y-2 sm:space-y-3">
<h2 className="font-['DM_Sans'] text-xl sm:text-3xl theme-secondary tracking-wide font-medium theme-transition">
Jan-Marlon Leibl
</h2>
<div>
<h1 className="font-['DM_Sans'] text-4xl sm:text-6xl md:text-7xl lg:text-8xl font-bold tracking-tight leading-[1.1] sm:leading-[0.95] max-w-4xl theme-primary theme-transition">
Software Developer
<br />
<span className="theme-secondary">based in <GermanyFlag /></span>
</h1>
</div>
</div>
</div>
<p className="font-['Instrument_Sans'] theme-secondary text-lg sm:text-2xl max-w-2xl leading-relaxed tracking-wide theme-transition">
Passionate about creating digital experiences, with a focus on PHP and modern web technologies.
</p>
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
<motion.a
href="#work"
className="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-lg sm:rounded-full transition-colors text-sm sm:text-base tracking-wide font-medium group"
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
View My Work
<svg
className="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</motion.a>
<motion.a
href="mailto:jleibl@proton.me"
className="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-lg sm:rounded-full theme-button-secondary hover:theme-bg-05 transition-colors text-sm sm:text-base tracking-wide group"
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
>
Get in Touch
<svg
className="w-4 h-4 sm:w-5 sm:h-5 transform group-hover:translate-x-1 transition-transform"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</motion.a>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-8 font-['Instrument_Sans'] text-sm sm:text-base tracking-wide border-t theme-border pt-6 sm:pt-8">
<div className="space-y-1.5 sm:space-y-2.5 group">
<span className="theme-text-40 block uppercase tracking-[0.2em] text-xs sm:text-sm">Email</span>
<a
href="mailto:jleibl@proton.me"
className="theme-text-90 hover:theme-primary transition-colors flex items-center gap-2 sm:gap-2.5 group-hover:gap-3 duration-300"
>
jleibl@proton.me
<svg
className="w-4 h-4 sm:w-5 sm:h-5 opacity-0 group-hover:opacity-100 transition-all -translate-x-4 group-hover:translate-x-0 duration-300"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
</div>
<div className="space-y-1.5 sm:space-y-2.5">
<span className="theme-text-40 block uppercase tracking-[0.2em] text-xs sm:text-sm">Role</span>
<span className="theme-text-90">
Fullstack Developer
</span>
</div>
<div className="space-y-1.5 sm:space-y-2.5">
<span className="theme-text-40 block uppercase tracking-[0.2em] text-xs sm:text-sm">Experience</span>
<span className="theme-text-90">
5+ Years
</span>
</div>
</div>
</FadeIn>
</div>
</section>
);
};

View 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>

View File

@ -0,0 +1,266 @@
---
import {
SiNextdotjs,
SiTailwindcss,
SiTypescript,
SiPhp,
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";
const iconMap = {
"Next.js": SiNextdotjs,
"Tailwind CSS": SiTailwindcss,
"TypeScript": SiTypescript,
"PHP": SiPhp,
"JavaScript": SiJavascript,
"MySQL": SiMysql,
"Performance": BsLightningChargeFill,
"Discord API": SiDiscord,
};
const projects = [
{
title: "ventry.host v2",
year: "2025",
description: "Free file hosting revamped with a modern design and improved user experience.",
icon: MdLaunch,
tags: [
{ name: "Next.js", icon: iconMap["Next.js"] },
{ name: "Tailwind CSS", icon: iconMap["Tailwind CSS"] },
{ name: "TypeScript", icon: iconMap["TypeScript"] },
],
link: "https://ventry.host",
},
{
title: "ventry.host",
year: "2023",
description: "A free file hosting solution with thousands of daily visitors.",
icon: MdLaunch,
tags: [
{ name: "PHP", icon: iconMap["PHP"] },
{ name: "JavaScript", icon: iconMap["JavaScript"] },
{ name: "MySQL", icon: iconMap["MySQL"] },
],
},
{
title: "ShareUpload",
year: "2022",
description: "High-performance file sharing platform with unlimited storage.",
icon: HiOutlineCollection,
tags: [
{ name: "PHP", icon: iconMap["PHP"] },
{ name: "MySQL", icon: iconMap["MySQL"] },
{ name: "Performance", icon: iconMap["Performance"] },
],
},
{
title: "RestoreM",
year: "2023",
description: "Discord server backup and restoration service.",
icon: SiDiscord,
tags: [
{ name: "PHP", icon: iconMap["PHP"] },
{ name: "MySQL", icon: iconMap["MySQL"] },
{ name: "Discord API", icon: iconMap["Discord API"] },
],
},
].sort((a, b) => parseInt(b.year) - parseInt(a.year));
---
<section id="work" class="py-24 sm:py-40 px-4 sm:px-8 relative">
<div class="absolute inset-0 theme-bg-gradient opacity-30"></div>
<div class="max-w-(--breakpoint-xl) mx-auto relative">
<FadeIn>
<div class="flex flex-col gap-3 mb-16 sm:mb-28">
<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
</span>
<div class="flex items-baseline gap-4">
<h2
class="font-['DM_Sans'] text-3xl sm:text-5xl lg:text-6xl font-semibold tracking-tight theme-primary"
>
Selected Work
</h2>
<div class="h-px grow theme-border opacity-60"></div>
</div>
</div>
</FadeIn>
<div class="grid gap-24 sm:gap-32">
{
projects.map((project, index) => (
<FadeIn delay={index * 0.1}>
<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="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}
</div>
<div class="h-px grow theme-border opacity-30" />
</div>
<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-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">
{project.title}
</h3>
<p class="font-['Instrument_Sans'] text-base sm:text-xl theme-text-70 leading-relaxed group-hover:theme-text-90 transition-colors max-w-xl">
{project.description}
</p>
</div>
<div class="space-y-8 sm:space-y-12">
<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 flex items-center before:content-[''] before:w-6 before:h-px before:theme-border before:mr-3">
Technologies
</h4>
<div class="flex flex-wrap items-center gap-3 sm:gap-4 relative z-20 pointer-events-none">
{project.tags.map((tag) => (
<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">
<span class="mr-2 flex items-center">
<tag.icon className="text-lg" />
</span>
{tag.name}
</span>
))}
</div>
</div>
</div>
</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>
</FadeIn>
))
}
</div>
</div>
</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>

View File

@ -1,100 +0,0 @@
import { FadeIn } from '../ui/FadeIn';
interface Project {
title: string;
year: string;
description: string;
tags: string[];
}
const projects: Project[] = [
{
title: 'ventry.host v2',
year: '2025',
description: 'Free file hosting revamped with a modern design and improved user experience.',
tags: ['Next.js', 'Tailwind CSS', 'TypeScript']
},
{
title: 'ventry.host',
year: '2023',
description: 'A free file hosting solution with thousands of daily visitors.',
tags: ['PHP', 'JavaScript', 'MySQL']
},
{
title: 'ShareUpload',
year: '2022',
description: 'High-performance file sharing platform with unlimited storage.',
tags: ['PHP', 'MySQL', 'Performance']
},
{
title: 'RestoreM',
year: '2023',
description: 'Discord server backup and restoration service.',
tags: ['PHP', 'MySQL', 'Discord API']
}
];
export const Work = () => {
return (
<section id="work" className="py-20 sm:py-40 px-4 sm:px-8 relative">
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-white/[0.02] to-transparent pointer-events-none theme-bg-gradient"></div>
<div className="max-w-screen-xl mx-auto relative">
<FadeIn>
<div className="flex flex-col gap-3 mb-12 sm:mb-24">
<span className="theme-text-40 uppercase tracking-[0.2em] text-sm sm:text-base font-['Instrument_Sans']">Portfolio</span>
<div className="flex items-baseline gap-4">
<h2 className="font-['DM_Sans'] text-3xl sm:text-6xl font-semibold tracking-tight theme-primary">Selected Work</h2>
<div className="h-px flex-grow theme-border"></div>
</div>
</div>
</FadeIn>
<div className="grid gap-24 sm:gap-40">
{projects.map((project, index) => (
<FadeIn key={index} delay={index * 0.1}>
<div className="group relative">
<div className="absolute top-0 left-0 right-0 flex items-center gap-4">
<div className="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-[0.2em] py-2 pr-4">
{project.year}
</div>
<div className="h-px flex-grow theme-border"></div>
</div>
<div className="pt-12 sm:pt-16 grid grid-cols-1 lg:grid-cols-[1.5fr,1fr] gap-6 sm:gap-16">
<div className="space-y-4 sm:space-y-8">
<h3 className="font-['DM_Sans'] text-3xl sm:text-6xl font-semibold tracking-tight theme-primary group-hover:theme-text-90 transition-colors">
{project.title}
</h3>
<p className="font-['Instrument_Sans'] text-base sm:text-xl theme-text-70 leading-relaxed group-hover:theme-text-70 transition-colors max-w-xl">
{project.description}
</p>
</div>
<div className="space-y-6 sm:space-y-12">
<div className="space-y-4 sm:space-y-6">
<h4 className="font-['Instrument_Sans'] text-sm sm:text-base theme-text-40 uppercase tracking-[0.2em]">Technologies</h4>
<div className="flex flex-wrap items-center gap-3 sm:gap-4">
{project.tags.map((tag, tagIndex) => (
<span
key={tagIndex}
className="text-sm sm:text-base theme-text-70 font-['Instrument_Sans'] tracking-wide py-1 sm:py-2 group-hover:theme-text-90 transition-colors"
>
{tag}{tagIndex !== project.tags.length - 1 && (
<span className="mx-3 sm:mx-4 theme-text-40 select-none"></span>
)}
</span>
))}
</div>
</div>
</div>
</div>
<div className="absolute -inset-x-4 sm:-inset-x-8 -inset-y-4 sm:-inset-y-6 rounded-2xl sm:rounded-3xl border border-white/0 group-hover:theme-border transition-colors"></div>
</div>
</FadeIn>
))}
</div>
</div>
</section>
);
};

View File

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

View File

@ -1,51 +0,0 @@
import { ReactNode } from 'react';
import { motion } from 'framer-motion';
import { useInView } from 'react-intersection-observer';
interface FadeInProps {
children: ReactNode;
className?: string;
delay?: number;
direction?: 'up' | 'down' | 'left' | 'right';
distance?: number;
}
export const FadeIn = ({
children,
className = "",
delay = 0,
direction = 'up',
distance = 20
}: FadeInProps) => {
const [ref, inView] = useInView({
triggerOnce: true,
threshold: 0.1,
rootMargin: "0px 0px -10% 0px"
});
const directionOffset = {
up: { x: 0, y: distance },
down: { x: 0, y: -distance },
left: { x: distance, y: 0 },
right: { x: -distance, y: 0 }
};
const { x, y } = directionOffset[direction];
return (
<motion.div
ref={ref}
initial={{ opacity: 0, x, y }}
animate={inView ? { opacity: 1, x: 0, y: 0 } : { opacity: 0, x, y }}
transition={{
type: "spring",
damping: 20,
stiffness: 100,
delay
}}
className={className}
>
{children}
</motion.div>
);
};

View File

@ -0,0 +1,58 @@
<div
id="germany-flag"
role="group"
aria-label="Theme switcher with German flag colors"
style="display:inline-flex;width:3em;height:0.6em;margin-left:0.1em;vertical-align:middle;border-radius:1px;overflow:hidden;opacity:0.6;cursor:pointer;"
class="germany-flag-container hover:opacity-80 transition-opacity duration-200"
>
<button
id="theme-black"
aria-label="Switch to dark theme"
data-theme="black"
style="flex:1;background-color:rgba(0, 0, 0, 0.5);border-left:2px solid rgba(255, 255, 255, 0.1);border-top:2px solid rgba(255, 255, 255, 0.1);border-bottom:2px solid rgba(255, 255, 255, 0.1);"
class="theme-button transition-transform duration-200 hover:scale-105 active:scale-95 cursor-pointer"
></button>
<button
id="theme-red"
aria-label="Switch to red theme"
data-theme="red"
style="flex:1;background-color:rgba(255, 0, 0);"
class="theme-button transition-transform duration-200 hover:scale-105 active:scale-95 cursor-pointer"
></button>
<button
id="theme-gold"
aria-label="Switch to gold theme"
data-theme="gold"
style="flex:1;background-color:rgba(255, 204, 0);"
class="theme-button transition-transform duration-200 hover:scale-105 active:scale-95 cursor-pointer"
></button>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const themeButtons = document.querySelectorAll(".theme-button");
const currentTheme = localStorage.getItem("theme") || "black";
document.documentElement.setAttribute("data-theme", currentTheme);
document.getElementById(`theme-${currentTheme}`)?.classList.add("ring-1", "ring-white/20");
themeButtons.forEach((button) => {
button.addEventListener("click", () => {
const theme = button.getAttribute("data-theme");
if (!theme) return;
themeButtons.forEach((btn) => btn.classList.remove("ring-1", "ring-white/20"));
button.classList.add("ring-1", "ring-white/20");
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);
const themeChangedEvent = new CustomEvent("themeChanged", {
detail: { theme },
});
document.dispatchEvent(themeChangedEvent);
});
});
});
</script>

View File

@ -1,57 +0,0 @@
import { useContext } from 'react';
import { motion } from 'framer-motion';
import { ThemeContext } from '../../context/ThemeContext';
export const GermanyFlag = () => {
const { theme, setTheme } = useContext(ThemeContext);
return (
<motion.div
role="group"
aria-label="Theme switcher with German flag colors"
style={{
display: "inline-flex",
width: "3em",
height: "0.6em",
marginLeft: "0.1em",
verticalAlign: "middle",
borderRadius: "1px",
overflow: "hidden",
opacity: 0.6,
cursor: "pointer"
}}
whileHover={{
opacity: 0.8,
transition: { duration: 0.2 }
}}
>
<motion.button
aria-label="Switch to dark theme"
aria-pressed={theme === 'black'}
style={{ flex: 1, backgroundColor: "rgba(0, 0, 0, 0.5)", borderLeft: "2px solid rgba(255, 255, 255, 0.1)", borderTop: "2px solid rgba(255, 255, 255, 0.1)", borderBottom: "2px solid rgba(255, 255, 255, 0.1)" }}
onClick={() => setTheme('black')}
className={theme === 'black' ? 'ring-1 ring-white/20' : ''}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
/>
<motion.button
aria-label="Switch to red theme"
aria-pressed={theme === 'red'}
style={{ flex: 1, backgroundColor: "rgba(255, 0, 0)" }}
onClick={() => setTheme('red')}
className={theme === 'red' ? 'ring-1 ring-white/20' : ''}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
/>
<motion.button
aria-label="Switch to gold theme"
aria-pressed={theme === 'gold'}
style={{ flex: 1, backgroundColor: "rgba(255, 204, 0)" }}
onClick={() => setTheme('gold')}
className={theme === 'gold' ? 'ring-1 ring-white/20' : ''}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
/>
</motion.div>
);
};

View 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>

View File

@ -1,24 +0,0 @@
import { createContext, useState, ReactNode } from 'react';
export type Theme = 'black' | 'red' | 'gold';
export type ThemeContextType = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
export const ThemeContext = createContext<ThemeContextType>({
theme: 'black',
setTheme: () => {},
});
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [theme, setTheme] = useState<Theme>('black');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<div className={theme}>
{children}
</div>
</ThemeContext.Provider>
);
};

View File

@ -1,22 +0,0 @@
import { useEffect } from 'react';
import Lenis from '@studio-freight/lenis';
export const useSmoothScroll = () => {
useEffect(() => {
const lenis = new Lenis({
duration: 1.2,
easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
orientation: 'vertical',
smoothWheel: true,
touchMultiplier: 2,
});
function raf(time: number) {
lenis.raf(time);
requestAnimationFrame(raf);
}
requestAnimationFrame(raf);
return () => { lenis.destroy(); };
}, []);
};

View File

@ -1,22 +0,0 @@
import "@/styles/globals.css";
import type { AppProps } from "next/app";
import { ThemeProvider } from "../context/ThemeContext";
// Import global styles
import "../styles/theme.css";
// Font imports
import '@fontsource/dm-sans/400.css';
import '@fontsource/dm-sans/500.css';
import '@fontsource/dm-sans/600.css';
import '@fontsource/dm-sans/700.css';
import '@fontsource/instrument-sans/400.css';
import '@fontsource/instrument-sans/500.css';
export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
);
}

View File

@ -1,13 +0,0 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body className="antialiased">
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@ -1,13 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
type Data = {
name: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>,
) {
res.status(200).json({ name: "John Doe" });
}

162
src/pages/index.astro Normal file
View File

@ -0,0 +1,162 @@
---
import Header from "../components/layout/Header.astro";
import Meta from "../components/layout/Meta.astro";
import SkipToContent from "../components/ui/SkipToContent.astro";
import Hero from "../components/sections/Hero.astro";
import About from "../components/sections/About.astro";
import Work from "../components/sections/Work.astro";
import 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/theme.css";
import "@fontsource/dm-sans/400.css";
import "@fontsource/dm-sans/500.css";
import "@fontsource/dm-sans/600.css";
import "@fontsource/dm-sans/700.css";
import "@fontsource/instrument-sans/400.css";
import "@fontsource/instrument-sans/500.css";
---
<html lang="en">
<head>
<Meta />
</head>
<body>
<SkipToContent />
<div id="app-root" class="min-h-screen theme-transition black overflow-hidden">
<Header />
<main id="main-content">
<Hero />
<About />
<Hobby />
<Work />
<Contact />
</main>
<Footer />
</div>
</body>
</html>
<script>
document.addEventListener("DOMContentLoaded", () => {
const appRoot = document.getElementById("app-root");
const themeButtons = document.querySelectorAll(".theme-button");
const currentTheme = localStorage.getItem("colorTheme") || "black";
if (appRoot) {
appRoot.classList.remove("black", "red", "gold");
appRoot.classList.add(currentTheme);
}
document.documentElement.setAttribute("data-theme", currentTheme);
document.getElementById(`theme-${currentTheme}`)?.classList.add("ring-1", "ring-white/20");
document.addEventListener("themeChanged", (e) => {
const customEvent = e as Event & { detail: { theme: string } };
const newTheme = customEvent.detail.theme;
if (appRoot && newTheme) {
appRoot.classList.remove("black", "red", "gold");
appRoot.classList.add(newTheme);
localStorage.setItem("colorTheme", newTheme);
}
});
});
</script>
<script>
import Lenis from "lenis";
document.addEventListener("DOMContentLoaded", () => {
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
smoothWheel: true,
wheelMultiplier: 1,
touchMultiplier: 2,
infinite: false,
});
const anchorLinks = document.querySelectorAll('a[href^="#"]');
anchorLinks.forEach((link) => {
link.addEventListener("click", (e) => {
e.preventDefault();
const targetId = link.getAttribute("href");
if (targetId && targetId !== "#") {
const targetElement = document.querySelector(targetId);
if (targetElement) {
lenis.scrollTo(targetElement as HTMLElement, {
offset: 0,
duration: 1.2,
immediate: false,
});
}
}
});
});
let animationId: number;
function raf(time: number) {
lenis.raf(time);
animationId = requestAnimationFrame(raf);
}
animationId = requestAnimationFrame(raf);
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
cancelAnimationFrame(animationId);
} else {
animationId = requestAnimationFrame(raf);
}
});
});
</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>

View File

@ -1,54 +0,0 @@
import dynamic from 'next/dynamic';
import Head from 'next/head';
import { useSmoothScroll } from '../hooks/useSmoothScroll';
const Header = dynamic(() => import('../components/layout/Header').then(mod => ({ default: mod.Header })));
const Hero = dynamic(() => import('../components/sections/Hero').then(mod => ({ default: mod.Hero })));
const About = dynamic(() => import('../components/sections/About').then(mod => ({ default: mod.About })));
const Work = dynamic(() => import('../components/sections/Work').then(mod => ({ default: mod.Work })));
const Footer = dynamic(() => import('../components/layout/Footer').then(mod => ({ default: mod.Footer })));
export default function Home() {
useSmoothScroll();
return (
<div className="min-h-screen theme-transition">
<Head>
<title>Jan-Marlon Leibl Fullstack Software Developer | PHP & TypeScript Expert</title>
<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>
<Header />
<Hero />
<About />
<Work />
<Footer />
</div>
);
}

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

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

View File

@ -1,21 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}

View File

@ -1,115 +1,230 @@
.black { background: #0A0A0A; }
.red { background: #1A0000; }
.gold { background: #1A1400; }
.black {
background: #0a0a0a;
}
.red {
background: #170c0c;
}
.gold {
background: #121107;
}
.black .theme-primary { color: rgba(255, 255, 255, 0.9); }
.red .theme-primary { color: rgba(255, 0, 0, 0.9); }
.gold .theme-primary { color: rgba(255, 204, 0, 0.9); }
.black .theme-primary {
color: rgba(255, 255, 255, 0.95);
}
.red .theme-primary {
color: #f87171;
}
.gold .theme-primary {
color: #fbbf24;
}
.black .theme-secondary { color: rgba(255, 255, 255, 0.6); }
.red .theme-secondary { color: rgba(255, 0, 0, 0.6); }
.gold .theme-secondary { color: rgba(255, 204, 0, 0.6); }
.black .theme-secondary {
color: rgba(255, 255, 255, 0.7);
}
.red .theme-secondary {
color: rgba(248, 113, 113, 0.85);
}
.gold .theme-secondary {
color: rgba(251, 191, 36, 0.85);
}
.black .theme-accent { background-color: rgba(255, 255, 255, 0.03); }
.red .theme-accent { background-color: rgba(255, 0, 0, 0.03); }
.gold .theme-accent { background-color: rgba(255, 204, 0, 0.03); }
.black .theme-accent {
background-color: rgba(255, 255, 255, 0.05);
}
.red .theme-accent {
background-color: rgba(248, 113, 113, 0.08);
}
.gold .theme-accent {
background-color: rgba(251, 191, 36, 0.08);
}
.black .theme-border { border-color: rgba(255, 255, 255, 0.1); }
.red .theme-border { border-color: rgba(255, 0, 0, 0.1); }
.gold .theme-border { border-color: rgba(255, 204, 0, 0.1); }
.black .theme-border {
border-color: rgba(255, 255, 255, 0.1);
}
.red .theme-border {
border-color: rgba(248, 113, 113, 0.15);
}
.gold .theme-border {
border-color: rgba(251, 191, 36, 0.15);
}
.black .theme-text-40 { color: rgba(255, 255, 255, 0.4); }
.red .theme-text-40 { color: rgba(255, 0, 0, 0.4); }
.gold .theme-text-40 { color: rgba(255, 204, 0, 0.4); }
.black .theme-text-40 {
color: rgba(255, 255, 255, 0.5);
}
.red .theme-text-40 {
color: rgba(248, 113, 113, 0.6);
}
.gold .theme-text-40 {
color: rgba(251, 191, 36, 0.6);
}
.black .theme-text-70 { color: rgba(255, 255, 255, 0.7); }
.red .theme-text-70 { color: rgba(255, 0, 0, 0.7); }
.gold .theme-text-70 { color: rgba(255, 204, 0, 0.7); }
.black .theme-text-70 {
color: rgba(255, 255, 255, 0.7);
}
.red .theme-text-70 {
color: rgba(248, 113, 113, 0.75);
}
.gold .theme-text-70 {
color: rgba(251, 191, 36, 0.75);
}
.black .theme-text-90 { color: rgba(255, 255, 255, 0.9); }
.red .theme-text-90 { color: rgba(255, 0, 0, 0.9); }
.gold .theme-text-90 { color: rgba(255, 204, 0, 0.9); }
.black .theme-text-90 {
color: rgba(255, 255, 255, 0.9);
}
.red .theme-text-90 {
color: rgba(248, 113, 113, 0.95);
}
.gold .theme-text-90 {
color: rgba(251, 191, 36, 0.95);
}
.black .theme-bg-05 { background-color: rgba(255, 255, 255, 0.05); }
.red .theme-bg-05 { background-color: rgba(255, 0, 0, 0.05); }
.gold .theme-bg-05 { background-color: rgba(255, 204, 0, 0.05); }
.black .theme-bg-05 {
background-color: rgba(255, 255, 255, 0.05);
}
.red .theme-bg-05 {
background-color: rgba(248, 113, 113, 0.06);
}
.gold .theme-bg-05 {
background-color: rgba(251, 191, 36, 0.06);
}
.black .nav-glass {
.black .nav-glass {
background: rgba(10, 10, 10, 0.8);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(12px);
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.red .nav-glass {
background: rgba(26, 0, 0, 0.8);
box-shadow: 0 8px 32px rgba(26, 0, 0, 0.1);
.red .nav-glass {
background: rgba(23, 12, 12, 0.8);
backdrop-filter: blur(12px);
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(248, 113, 113, 0.1);
}
.gold .nav-glass {
background: rgba(26, 20, 0, 0.8);
box-shadow: 0 8px 32px rgba(26, 20, 0, 0.1);
.gold .nav-glass {
background: rgba(18, 17, 7, 0.8);
backdrop-filter: blur(12px);
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.1);
border-bottom: 1px solid rgba(251, 191, 36, 0.1);
}
.black .nav-item-active {
background: rgba(255, 255, 255, 0.05);
border-bottom: 2px solid rgba(255, 255, 255, 0.8);
background: rgba(255, 255, 255, 0.08);
}
.red .nav-item-active {
background: rgba(255, 0, 0, 0.05);
border-bottom: 2px solid rgba(255, 0, 0, 0.8);
background: rgba(248, 113, 113, 0.08);
border-bottom: 2px solid rgba(248, 113, 113, 0.8);
}
.gold .nav-item-active {
background: rgba(255, 204, 0, 0.05);
border-bottom: 2px solid rgba(255, 204, 0, 0.8);
background: rgba(251, 191, 36, 0.08);
border-bottom: 2px solid rgba(251, 191, 36, 0.8);
}
.nav-item {
position: relative;
overflow: hidden;
font-weight: 500;
transition: all 0.2s ease;
}
.nav-item::after {
content: '';
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
transform: translateX(-100%);
transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
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(255, 0, 0, 0.8);
background-color: rgba(248, 113, 113, 0.8);
}
.gold .nav-item::after {
background-color: rgba(255, 204, 0, 0.8);
background-color: rgba(251, 191, 36, 0.8);
}
.nav-item:hover::after {
transform: translateX(0);
}
.black .theme-bg-gradient { background: linear-gradient(to bottom, rgba(255, 255, 255, 0.02), transparent); }
.red .theme-bg-gradient { background: linear-gradient(to bottom, rgba(255, 0, 0, 0.02), transparent); }
.gold .theme-bg-gradient { background: linear-gradient(to bottom, rgba(255, 204, 0, 0.02), transparent); }
.black .theme-bg-gradient {
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.03), transparent);
}
.red .theme-bg-gradient {
background: linear-gradient(to bottom, rgba(248, 113, 113, 0.04), transparent);
}
.gold .theme-bg-gradient {
background: linear-gradient(to bottom, rgba(251, 191, 36, 0.04), transparent);
}
.black .theme-button-primary { background-color: rgba(255, 255, 255, 0.9); color: #0A0A0A; }
.red .theme-button-primary { background-color: rgba(255, 0, 0, 0.9); color: #1A0000; }
.gold .theme-button-primary { background-color: rgba(255, 204, 0, 0.9); color: #1A1400; }
.black .theme-button-primary {
background-color: rgba(255, 255, 255, 0.9);
color: #0a0a0a;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.red .theme-button-primary {
background-color: #f87171;
color: #0f0606;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.gold .theme-button-primary {
background-color: #fbbf24;
color: #0f0b02;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.black .theme-button-primary:hover { background-color: rgba(255, 255, 255, 0.8); }
.red .theme-button-primary:hover { background-color: rgba(255, 0, 0, 0.8); }
.gold .theme-button-primary:hover { background-color: rgba(255, 204, 0, 0.8); }
.black .theme-button-primary:hover {
background-color: rgba(255, 255, 255, 1);
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.red .theme-button-primary:hover {
background-color: #ef4444;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.gold .theme-button-primary:hover {
background-color: #f59e0b;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.black .theme-button-secondary { border-color: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.9); }
.red .theme-button-secondary { border-color: rgba(255, 0, 0, 0.1); color: rgba(255, 0, 0, 0.9); }
.gold .theme-button-secondary { border-color: rgba(255, 204, 0, 0.1); color: rgba(255, 204, 0, 0.9); }
.black .theme-button-secondary {
border-color: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.9);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.red .theme-button-secondary {
border-color: rgba(248, 113, 113, 0.2);
color: rgba(248, 113, 113, 1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.gold .theme-button-secondary {
border-color: rgba(251, 191, 36, 0.2);
color: rgba(251, 191, 36, 1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.black .theme-button-secondary:hover { background-color: rgba(255, 255, 255, 0.05); }
.red .theme-button-secondary:hover { background-color: rgba(255, 0, 0, 0.05); }
.gold .theme-button-secondary:hover { background-color: rgba(255, 204, 0, 0.05); }
.black .theme-button-secondary:hover {
background-color: rgba(255, 255, 255, 0.08);
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.red .theme-button-secondary:hover {
background-color: rgba(248, 113, 113, 0.08);
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.gold .theme-button-secondary:hover {
background-color: rgba(251, 191, 36, 0.08);
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.theme-transition {
transition: color 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
}
transition: all 0.25s ease;
}

View File

@ -1,18 +0,0 @@
import type { Config } from "tailwindcss";
export default {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
},
},
},
plugins: [],
} satisfies Config;

View File

@ -11,11 +11,12 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./src/*"]
}
},
"jsxImportSource": "react"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]