Compare commits
71 Commits
Author | SHA1 | Date | |
---|---|---|---|
e4d7b6a8a7
|
|||
c8467b8b3b
|
|||
5a90f11ce0
|
|||
f328839796
|
|||
f4b911a05b
|
|||
1184cedc2d
|
|||
03bfef7d5b | |||
ed7b3d1d31 | |||
0c1032d0d7 | |||
03dd77724d | |||
9ee5c0ea23 | |||
97a22ccf24 | |||
f854a113d1 | |||
c1124ce45c | |||
fd9545cc10
|
|||
14209935f6
|
|||
31b897c375
|
|||
531f2c1288
|
|||
7e41d57515
|
|||
5b917142f2
|
|||
fc5b43d029
|
|||
1ecdf57df8
|
|||
a129511728
|
|||
75c6a43203
|
|||
92aca15026
|
|||
a6b012d6fd
|
|||
5647574c8e
|
|||
b057943dde
|
|||
64565ce955
|
|||
f10ecb37b2
|
|||
2ad119a261
|
|||
adca2bae72 | |||
a261bbd60c
|
|||
1f2304144c
|
|||
cc4ce23dcd
|
|||
4f719353af
|
|||
a7b400d2a8
|
|||
5dc8215ca6
|
|||
68ce9f3efd
|
|||
eafda93bd0 | |||
3d892a44a8
|
|||
3e08b3e514
|
|||
8e95fa4937
|
|||
93b5e3ae31
|
|||
22d95d0d44
|
|||
9b0ecf18c7
|
|||
c82aa23b41
|
|||
43ef4336a6
|
|||
9eb628971d
|
|||
72f2f1c07d
|
|||
ab1e92ae54
|
|||
fc90fcdff5
|
|||
f3e5aaa2f4
|
|||
ee79833992
|
|||
6528c0e79e
|
|||
fc47a93671
|
|||
5634704a3f
|
|||
eb126dc6f2
|
|||
67400ee308
|
|||
60935042a7
|
|||
7760e8d360
|
|||
83dd2e5630
|
|||
777af4215d
|
|||
89117b092b
|
|||
4192c882d0
|
|||
6cc0e06918
|
|||
0ab11c240c
|
|||
29a58817be
|
|||
0dd79cd1ec
|
|||
6e8a7ecf62
|
|||
5a757989a4 |
7
.gitignore
vendored
@ -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
@ -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
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
16
Dockerfile
@ -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"]
|
||||
|
82
README.md
@ -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
@ -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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -11,6 +11,7 @@ const compat = new FlatCompat({
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
...compat.extends("prettier"),
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
@ -1,8 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
52
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
|
23
public/_headers
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
X-XSS-Protection: 1; mode=block
|
||||
Referrer-Policy: strict-origin-when-cross-origin
|
||||
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
||||
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' *.googleapis.com *.gstatic.com; style-src 'self' 'unsafe-inline' *.googleapis.com; img-src 'self' data: https:; font-src 'self' data: *.googleapis.com *.gstatic.com; connect-src 'self' *.googleapis.com; frame-ancestors 'none';
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||
|
||||
/*.html
|
||||
Cache-Control: public, max-age=0, must-revalidate
|
||||
|
||||
/assets/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/images/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/*.js
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/*.css
|
||||
Cache-Control: public, max-age=31536000, immutable
|
Before Width: | Height: | Size: 25 KiB |
@ -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 |
@ -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
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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 |
BIN
public/profile-image-futu-style.jpg
Normal file
After Width: | Height: | Size: 275 KiB |
Before Width: | Height: | Size: 7.7 MiB |
10
public/robots.txt
Normal file
@ -0,0 +1,10 @@
|
||||
# www.robotstxt.org/
|
||||
# Allow crawling of all content
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Optimize crawling
|
||||
Crawl-delay: 10
|
||||
|
||||
# Sitemaps
|
||||
Sitemap: https://jleibl.net/sitemap-index.xml
|
158
public/service-worker.js
Normal file
@ -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
@ -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>
|
@ -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 |
@ -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
@ -0,0 +1,3 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||
}
|
75
src/components/layout/Footer.astro
Normal 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'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>
|
@ -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'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>
|
||||
);
|
||||
};
|
221
src/components/layout/Header.astro
Normal 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>
|
@ -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>
|
||||
);
|
||||
};
|
117
src/components/layout/Meta.astro
Normal file
@ -0,0 +1,117 @@
|
||||
---
|
||||
const defaultTitle = "Jan-Marlon Leibl • Fullstack Software Developer | PHP & TypeScript Expert";
|
||||
const defaultDescription =
|
||||
"Experienced Fullstack Developer specializing in PHP and TypeScript. Creating high-performance web applications and digital experiences with modern technologies.";
|
||||
const defaultImage = "/profile-image-futu-style.jpg";
|
||||
const siteUrl = "https://jleibl.net";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
canonicalUrl?: string;
|
||||
type?: "website" | "article";
|
||||
publishedDate?: string;
|
||||
modifiedDate?: string;
|
||||
keywords?: string[];
|
||||
}
|
||||
|
||||
const {
|
||||
title = defaultTitle,
|
||||
description = defaultDescription,
|
||||
image = defaultImage,
|
||||
canonicalUrl = siteUrl,
|
||||
type = "website",
|
||||
publishedDate,
|
||||
modifiedDate,
|
||||
keywords = [
|
||||
"Software Development",
|
||||
"PHP Developer",
|
||||
"TypeScript",
|
||||
"Fullstack Engineer",
|
||||
"Web Development",
|
||||
"System Architecture",
|
||||
"MySQL",
|
||||
"React",
|
||||
"Performance Optimization",
|
||||
],
|
||||
} = Astro.props;
|
||||
|
||||
const personData = {
|
||||
"@type": "Person",
|
||||
name: "Jan-Marlon Leibl",
|
||||
url: siteUrl,
|
||||
image: `${siteUrl}${defaultImage}`,
|
||||
jobTitle: "Fullstack Software Developer",
|
||||
description: "Fullstack Software Developer specializing in PHP and TypeScript",
|
||||
sameAs: ["https://github.com/AtomicWasTaken", "https://www.linkedin.com/in/janmarlonleibl/"],
|
||||
knowsAbout: ["PHP", "TypeScript", "Web Development", "System Architecture", "Database Design"],
|
||||
worksFor: {
|
||||
"@type": "Organization",
|
||||
name: "team neusta",
|
||||
},
|
||||
};
|
||||
|
||||
const structuredDataPerson = {
|
||||
"@context": "https://schema.org",
|
||||
...personData,
|
||||
};
|
||||
|
||||
const structuredDataWebsite = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": type,
|
||||
headline: title,
|
||||
description: description,
|
||||
image: `${siteUrl}${image}`,
|
||||
url: canonicalUrl,
|
||||
author: personData,
|
||||
...(publishedDate && { datePublished: publishedDate }),
|
||||
...(modifiedDate && { dateModified: modifiedDate }),
|
||||
};
|
||||
---
|
||||
|
||||
<title>{title}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="keywords" content={keywords.join(", ")} />
|
||||
<meta name="author" content="Jan-Marlon Leibl" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<meta name="theme-color" content="#0A0A0A" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
|
||||
<meta property="og:type" content={type} />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content="/profile-image-futu-style.jpg" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content="Jan-Marlon Leibl - Fullstack Software Developer" />
|
||||
<meta property="og:site_name" content="Jan-Marlon Leibl" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content="/profile-image-futu-style.jpg" />
|
||||
<meta name="twitter:image:alt" content="Jan-Marlon Leibl - Fullstack Software Developer" />
|
||||
|
||||
<link rel="icon" href="/profile-image-futu-style.jpg" sizes="any" />
|
||||
<link rel="icon" href="/profile-image-futu-style.jpg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="/profile-image-futu-style.jpg" />
|
||||
|
||||
<script type="application/ld+json" set:html={JSON.stringify(structuredDataPerson)} />
|
||||
<script type="application/ld+json" set:html={JSON.stringify(structuredDataWebsite)} />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preload" href="/profile-image-futu-style.jpg" as="image" />
|
||||
|
||||
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
|
||||
<link rel="dns-prefetch" href="https://fonts.gstatic.com" />
|
259
src/components/layout/MobileMenu.astro
Normal 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>
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
173
src/components/sections/About.astro
Normal 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>
|
@ -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'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'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>
|
||||
);
|
||||
};
|
202
src/components/sections/Contact.astro
Normal file
@ -0,0 +1,202 @@
|
||||
---
|
||||
import FadeIn from "../ui/FadeIn.astro";
|
||||
import { FaGithub, FaLinkedin, FaEnvelope, FaPhone, FaMapMarkerAlt, FaGlobe } from "react-icons/fa";
|
||||
---
|
||||
|
||||
<section id="contact" class="py-20 sm:py-32 px-4 sm:px-8 relative">
|
||||
<div class="absolute inset-0 theme-bg-gradient opacity-30"></div>
|
||||
|
||||
<div class="max-w-7xl mx-auto relative">
|
||||
<FadeIn className="space-y-16">
|
||||
<div class="space-y-6">
|
||||
<h2
|
||||
class="text-3xl sm:text-4xl md:text-5xl font-bold font-['DM_Sans'] tracking-tight theme-primary"
|
||||
>
|
||||
Get In Touch
|
||||
</h2>
|
||||
<p class="font-['Instrument_Sans'] theme-secondary text-lg sm:text-xl max-w-3xl">
|
||||
Have a project in mind or want to chat? Feel free to reach out.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-16">
|
||||
<div class="space-y-8">
|
||||
<form id="contact-form" class="space-y-6" onsubmit="return openEmailClient(event)">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label
|
||||
for="name"
|
||||
class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']"
|
||||
>Your Name</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
class="w-full px-4 py-3 rounded-xl theme-accent border theme-border theme-secondary focus:ring-2 focus:ring-white/20 focus:border-white/30 transition-all bg-transparent font-['Instrument_Sans']"
|
||||
required
|
||||
aria-required="true"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="email"
|
||||
class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']"
|
||||
>Your Email</label
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
class="w-full px-4 py-3 rounded-xl theme-accent border theme-border theme-secondary focus:ring-2 focus:ring-white/20 focus:border-white/30 transition-all bg-transparent font-['Instrument_Sans']"
|
||||
required
|
||||
aria-required="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="subject"
|
||||
class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']"
|
||||
>Subject</label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
name="subject"
|
||||
class="w-full px-4 py-3 rounded-xl theme-accent border theme-border theme-secondary focus:ring-2 focus:ring-white/20 focus:border-white/30 transition-all bg-transparent font-['Instrument_Sans']"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
for="message"
|
||||
class="block mb-2 text-sm font-medium theme-secondary font-['Instrument_Sans']"
|
||||
>Your Message</label
|
||||
>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
rows="5"
|
||||
class="w-full px-4 py-3 rounded-xl theme-accent border theme-border theme-secondary focus:ring-2 focus:ring-white/20 focus:border-white/30 bg-transparent font-['Instrument_Sans']"
|
||||
required
|
||||
aria-required="true"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
class="animate-button w-full sm:w-auto inline-flex items-center justify-center gap-2 px-8 py-4 theme-button-primary rounded-xl transition-all text-base tracking-tight font-medium group shadow-sm font-['Instrument_Sans']"
|
||||
>
|
||||
Send Message
|
||||
<svg
|
||||
class="w-5 h-5 transform group-hover:translate-x-1 transition-transform"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14M12 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="form-status" class="mt-4 text-sm hidden font-['Instrument_Sans']"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h3 class="text-2xl font-semibold theme-primary mb-4 font-['DM_Sans']">
|
||||
Contact Information
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="p-2 rounded-xl theme-bg-05 border theme-border theme-text-70 mt-1">
|
||||
<FaEnvelope className="w-5 h-5" />
|
||||
</span>
|
||||
<div>
|
||||
<h4 class="theme-secondary font-medium font-['Instrument_Sans']">Email</h4>
|
||||
<a
|
||||
href="mailto:jleibl@proton.me"
|
||||
class="theme-primary hover:underline font-['Instrument_Sans']"
|
||||
>jleibl@proton.me</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="p-2 rounded-xl theme-bg-05 border theme-border theme-text-70 mt-1">
|
||||
<FaMapMarkerAlt className="w-5 h-5" />
|
||||
</span>
|
||||
<div>
|
||||
<h4 class="theme-secondary font-medium font-['Instrument_Sans']">Location</h4>
|
||||
<p class="theme-primary font-['Instrument_Sans']">Bremen, Germany</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-2xl font-semibold theme-primary mb-4 font-['DM_Sans']">Connect</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<a
|
||||
href="https://github.com/AtomicWasTaken"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub profile"
|
||||
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
|
||||
>
|
||||
<FaGithub className="w-6 h-6" />
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/in/janmarlonleibl/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="LinkedIn profile"
|
||||
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
|
||||
>
|
||||
<FaLinkedin className="w-6 h-6" />
|
||||
</a>
|
||||
<a
|
||||
href="https://jleibl.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="My website"
|
||||
class="p-3 sm:p-4 rounded-xl theme-bg-05 border theme-border theme-text-70 hover:theme-text-90 hover:scale-105 transition-all duration-300 shadow-sm"
|
||||
>
|
||||
<FaGlobe className="w-6 h-6" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FadeIn>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
(window as any).openEmailClient = (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const name = formData.get("name") as string;
|
||||
const email = formData.get("email") as string;
|
||||
const subject = (formData.get("subject") as string) || "Contact Form Message";
|
||||
const message = formData.get("message") as string;
|
||||
|
||||
const formStatus = document.getElementById("form-status");
|
||||
if (!name || !email || !message) {
|
||||
if (formStatus) {
|
||||
formStatus.textContent = "Please fill in all required fields.";
|
||||
formStatus.classList.remove("hidden");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const body = `Name: ${name}\nEmail: ${email}\n\n${message}`;
|
||||
window.location.href = `mailto:jleibl@proton.me?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
</script>
|
145
src/components/sections/Hero.astro
Normal 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>
|
@ -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>
|
||||
);
|
||||
};
|
104
src/components/sections/Hobby.astro
Normal file
@ -0,0 +1,104 @@
|
||||
---
|
||||
import FadeIn from "../ui/FadeIn.astro";
|
||||
import { FaCamera, FaPhotoVideo } from "react-icons/fa";
|
||||
import { MdPhotoCamera, MdCameraAlt } from "react-icons/md";
|
||||
|
||||
const cameras = [
|
||||
{ name: "Nikon D7200", icon: MdCameraAlt, description: "Dad's camera that got me hooked on photography" },
|
||||
{ name: "Nikon D3300", icon: MdCameraAlt, description: "My first personal camera where it all began" },
|
||||
{ name: "Sony A7 III", icon: MdPhotoCamera, description: "Current setup that's taken my shots to the next level" },
|
||||
];
|
||||
|
||||
const genres = [
|
||||
{ name: "Portrait", icon: FaCamera, description: "Love capturing people's authentic expressions" },
|
||||
{ name: "Street", icon: FaCamera, description: "Finding beauty in everyday urban moments" },
|
||||
{ name: "Studio", icon: FaPhotoVideo, description: "Creating controlled lighting magic" },
|
||||
{ name: "Landscape", icon: FaPhotoVideo, description: "Chasing perfect light in nature" },
|
||||
];
|
||||
---
|
||||
|
||||
<section id="hobby" class="py-24 sm:py-40 px-4 sm:px-8 relative">
|
||||
<div class="absolute inset-0 theme-bg-gradient opacity-50"></div>
|
||||
<div
|
||||
class="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="max-w-(--breakpoint-xl) mx-auto relative">
|
||||
<FadeIn>
|
||||
<div class="flex items-baseline gap-4 mb-12 sm:mb-24">
|
||||
<h2
|
||||
class="font-['DM_Sans'] text-3xl sm:text-5xl font-semibold tracking-tight theme-primary"
|
||||
>
|
||||
Hobby
|
||||
</h2>
|
||||
<div class="h-px grow theme-border opacity-60"></div>
|
||||
</div>
|
||||
</FadeIn>
|
||||
|
||||
<div class="grid gap-12 sm:gap-16">
|
||||
<FadeIn delay={0.2}>
|
||||
<div class="space-y-10 sm:space-y-16 font-['Instrument_Sans']">
|
||||
<div class="space-y-6 sm:space-y-8">
|
||||
<p class="text-xl sm:text-2xl theme-text-90 leading-relaxed">
|
||||
When I'm not coding, you'll find me with a camera in hand, hunting for that perfect shot that tells a story without words.
|
||||
</p>
|
||||
<p class="text-base sm:text-xl theme-text-70 leading-relaxed">
|
||||
It all started with borrowing my dad's Nikon D7200 – I was instantly hooked. Eventually got my own Nikon D3300 to learn the basics. These days, I'm having a blast with my Sony A7 III, experimenting with different styles and techniques across various photography genres.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-10 sm:gap-16">
|
||||
<div class="space-y-6 sm:space-y-8">
|
||||
<div class="flex items-baseline gap-4">
|
||||
<h3 class="text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium">
|
||||
Camera Evolution
|
||||
</h3>
|
||||
<div class="h-px grow theme-border opacity-30"></div>
|
||||
</div>
|
||||
<ul class="space-y-4 sm:space-y-5">
|
||||
{
|
||||
cameras.map((camera) => (
|
||||
<li class="text-base sm:text-lg group flex flex-col gap-1 theme-text-70 hover:theme-text-90 cursor-default theme-bg-05 px-4 py-3 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
|
||||
<camera.icon className="w-5 h-5" />
|
||||
</span>
|
||||
{camera.name}
|
||||
</div>
|
||||
<p class="text-sm theme-text-50 pl-9">{camera.description}</p>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 sm:space-y-8">
|
||||
<div class="flex items-baseline gap-4">
|
||||
<h3 class="text-sm sm:text-base theme-text-40 uppercase tracking-wider font-medium">
|
||||
Favorite Genres
|
||||
</h3>
|
||||
<div class="h-px grow theme-border opacity-30"></div>
|
||||
</div>
|
||||
<ul class="space-y-4 sm:space-y-5">
|
||||
{
|
||||
genres.map((genre) => (
|
||||
<li class="text-base sm:text-lg group flex flex-col gap-1 theme-text-70 hover:theme-text-90 cursor-default theme-bg-05 px-4 py-3 rounded-xl shadow-sm hover:shadow-md border border-transparent hover:theme-border transition-all duration-300">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="theme-text-40 group-hover:theme-text-90 flex items-center justify-center w-6 h-6 transition-all">
|
||||
<genre.icon className="w-5 h-5" />
|
||||
</span>
|
||||
{genre.name}
|
||||
</div>
|
||||
<p class="text-sm theme-text-50 pl-9">{genre.description}</p>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FadeIn>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
266
src/components/sections/Work.astro
Normal 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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
66
src/components/ui/FadeIn.astro
Normal 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>
|
@ -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>
|
||||
);
|
||||
};
|
58
src/components/ui/GermanyFlag.astro
Normal 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>
|
@ -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>
|
||||
);
|
||||
};
|
46
src/components/ui/SkipToContent.astro
Normal file
@ -0,0 +1,46 @@
|
||||
<a href="#main-content" class="skip-to-content" aria-label="Skip to main content">
|
||||
<span class="font-['Instrument_Sans']">Skip to content</span>
|
||||
</a>
|
||||
|
||||
<style>
|
||||
.skip-to-content {
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
left: 0;
|
||||
padding: 1rem;
|
||||
margin: 1rem;
|
||||
background: black;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
z-index: 100;
|
||||
transition: top 0.3s ease-out;
|
||||
font-weight: 500;
|
||||
font-family: "Instrument Sans", sans-serif;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.skip-to-content:focus {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/* Theme variations */
|
||||
html[data-theme="red"] .skip-to-content {
|
||||
background: #170c0c;
|
||||
color: #f87171;
|
||||
border-color: rgba(248, 113, 113, 0.15);
|
||||
}
|
||||
|
||||
html[data-theme="gold"] .skip-to-content {
|
||||
background: #121107;
|
||||
color: #fbbf24;
|
||||
border-color: rgba(251, 191, 36, 0.15);
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
:root.dark .skip-to-content {
|
||||
background: #0a0a0a;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
</style>
|
@ -1,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>
|
||||
);
|
||||
};
|
@ -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(); };
|
||||
}, []);
|
||||
};
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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
@ -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>
|
@ -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
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
@ -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"]
|
||||
|