From f8e39ce1fb442fc530d514359867263c00cc89b6 Mon Sep 17 00:00:00 2001 From: Jan-Marlon Leibl Date: Tue, 25 Feb 2025 10:01:02 +0100 Subject: [PATCH] feat: add initial layout components and styles for site --- src/components/layout/Footer.tsx | 42 ++ src/components/layout/Header.tsx | 90 +++ src/components/layout/MobileMenu.tsx | 147 +++++ src/components/sections/About.tsx | 118 ++++ src/components/sections/Hero.tsx | 107 ++++ src/components/sections/Work.tsx | 100 +++ src/components/ui/FadeIn.tsx | 51 ++ src/components/ui/GermanyFlag.tsx | 57 ++ src/context/ThemeContext.tsx | 24 + src/hooks/useSmoothScroll.ts | 22 + src/pages/_app.tsx | 18 +- src/pages/index.tsx | 904 ++------------------------- src/styles/theme.css | 115 ++++ 13 files changed, 930 insertions(+), 865 deletions(-) create mode 100644 src/components/layout/Footer.tsx create mode 100644 src/components/layout/Header.tsx create mode 100644 src/components/layout/MobileMenu.tsx create mode 100644 src/components/sections/About.tsx create mode 100644 src/components/sections/Hero.tsx create mode 100644 src/components/sections/Work.tsx create mode 100644 src/components/ui/FadeIn.tsx create mode 100644 src/components/ui/GermanyFlag.tsx create mode 100644 src/context/ThemeContext.tsx create mode 100644 src/hooks/useSmoothScroll.ts create mode 100644 src/styles/theme.css diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx new file mode 100644 index 0000000..556c2ac --- /dev/null +++ b/src/components/layout/Footer.tsx @@ -0,0 +1,42 @@ +import { FadeIn } from '../ui/FadeIn'; + +export const Footer = () => { + return ( + + ); +}; \ No newline at end of file diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..4afa983 --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,90 @@ +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: () => ( +
+ + + +
+ ) +}); + +export const Header = () => { + return ( + +
+ +
+ +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/layout/MobileMenu.tsx b/src/components/layout/MobileMenu.tsx new file mode 100644 index 0000000..2255105 --- /dev/null +++ b/src/components/layout/MobileMenu.tsx @@ -0,0 +1,147 @@ +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(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 ( + <> + 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 }} + > + + + + + + + {isOpen && ( + <> + setIsOpen(false)} + aria-hidden="true" + /> + + +
+ 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 }} + > + + + + + +
+ + + +
+ 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 }} + > + + + + Contact Me + +
+ +
+ +
+
+
+
+
+ + )} +
+ + ); +}; \ No newline at end of file diff --git a/src/components/sections/About.tsx b/src/components/sections/About.tsx new file mode 100644 index 0000000..a1990e5 --- /dev/null +++ b/src/components/sections/About.tsx @@ -0,0 +1,118 @@ +import { motion } from 'framer-motion'; +import Image from 'next/image'; +import { FadeIn } from '../ui/FadeIn'; + +export const About = () => { + return ( +
+
+ +
+

About

+
+
+
+ +
+
+ +
+ Jan-Marlon Leibl - Fullstack Software Developer +
+
+ +
+

Jan-Marlon Leibl

+

Fullstack Developer

+
+
+
+ + +
+
+

+ 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. +

+

+ 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. +

+
+ +
+
+
+

Technologies

+
+
+
    + {['PHP', 'JavaScript', 'MySQL', 'React'].map((tech) => ( +
  • + + {tech} +
  • + ))} +
+
+ +
+
+

Interests

+
+
+
    + {[ + 'Web Development', + 'System Architecture', + 'UI/UX Design', + 'Performance Optimization' + ].map((interest) => ( +
  • + + {interest} +
  • + ))} +
+
+
+ +
+ + View GitHub + + + Connect on LinkedIn + +
+
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/sections/Hero.tsx b/src/components/sections/Hero.tsx new file mode 100644 index 0000000..f30c8df --- /dev/null +++ b/src/components/sections/Hero.tsx @@ -0,0 +1,107 @@ +import { motion } from 'framer-motion'; +import { FadeIn } from '../ui/FadeIn'; +import { GermanyFlag } from '../ui/GermanyFlag'; + +export const Hero = () => { + return ( +
+
+
+ +
+
+
+ + Available for Work +
+
+

+ Jan-Marlon Leibl +

+
+

+ Software Developer +
+ based in +

+
+
+
+

+ Passionate about creating digital experiences, with a focus on PHP and modern web technologies. +

+
+ + View My Work + + + + + + Get in Touch + + + + +
+
+ +
+ +
+ Role + + Fullstack Developer + +
+
+ Experience + + 5+ Years + +
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/sections/Work.tsx b/src/components/sections/Work.tsx new file mode 100644 index 0000000..75259a3 --- /dev/null +++ b/src/components/sections/Work.tsx @@ -0,0 +1,100 @@ +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 ( +
+
+
+ +
+ Portfolio +
+

Selected Work

+
+
+
+
+ +
+ {projects.map((project, index) => ( + +
+
+
+ {project.year} +
+
+
+ +
+
+

+ {project.title} +

+

+ {project.description} +

+
+ +
+
+

Technologies

+
+ {project.tags.map((tag, tagIndex) => ( + + {tag}{tagIndex !== project.tags.length - 1 && ( + + )} + + ))} +
+
+
+
+ +
+
+
+ ))} +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/ui/FadeIn.tsx b/src/components/ui/FadeIn.tsx new file mode 100644 index 0000000..c585135 --- /dev/null +++ b/src/components/ui/FadeIn.tsx @@ -0,0 +1,51 @@ +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 ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/components/ui/GermanyFlag.tsx b/src/components/ui/GermanyFlag.tsx new file mode 100644 index 0000000..5b16373 --- /dev/null +++ b/src/components/ui/GermanyFlag.tsx @@ -0,0 +1,57 @@ +import { useContext } from 'react'; +import { motion } from 'framer-motion'; +import { ThemeContext } from '../../context/ThemeContext'; + +export const GermanyFlag = () => { + const { theme, setTheme } = useContext(ThemeContext); + + return ( + + setTheme('black')} + className={theme === 'black' ? 'ring-1 ring-white/20' : ''} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + /> + setTheme('red')} + className={theme === 'red' ? 'ring-1 ring-white/20' : ''} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + /> + setTheme('gold')} + className={theme === 'gold' ? 'ring-1 ring-white/20' : ''} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + /> + + ); +}; \ No newline at end of file diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx new file mode 100644 index 0000000..45e7ccf --- /dev/null +++ b/src/context/ThemeContext.tsx @@ -0,0 +1,24 @@ +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({ + theme: 'black', + setTheme: () => {}, +}); + +export const ThemeProvider = ({ children }: { children: ReactNode }) => { + const [theme, setTheme] = useState('black'); + return ( + +
+ {children} +
+
+ ); +}; \ No newline at end of file diff --git a/src/hooks/useSmoothScroll.ts b/src/hooks/useSmoothScroll.ts new file mode 100644 index 0000000..afe2fee --- /dev/null +++ b/src/hooks/useSmoothScroll.ts @@ -0,0 +1,22 @@ +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(); }; + }, []); +}; \ No newline at end of file diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index a7a790f..593defd 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,6 +1,22 @@ 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 ; + return ( + + + + ); } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 096d943..153acfc 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,878 +1,54 @@ -import { useEffect, createContext, useContext, useState } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { useInView } from 'react-intersection-observer'; -import Lenis from '@studio-freight/lenis'; +import dynamic from 'next/dynamic'; import Head from 'next/head'; -import Image from "next/image"; -import React from 'react'; +import { useSmoothScroll } from '../hooks/useSmoothScroll'; -import '@fontsource/dm-sans/400.css'; -import '@fontsource/dm-sans/500.css'; -import '@fontsource/dm-sans/600.css'; -import '@fontsource/dm-sans/700.css'; -import '@fontsource/instrument-sans/400.css'; -import '@fontsource/instrument-sans/500.css'; -import Link from 'next/link'; - -type Theme = 'black' | 'red' | 'gold'; -type ThemeContextType = { - theme: Theme; - setTheme: (theme: Theme) => void; -}; - -const ThemeContext = createContext({ - theme: 'black', - setTheme: () => {}, -}); - -const ThemeProvider = ({ children }: { children: React.ReactNode }) => { - const [theme, setTheme] = useState('black'); - return ( - -
- {children} -
-
- ); -}; - -const useSmoothScroll = () => { - useEffect(() => { - const lenis = new Lenis({ - duration: 1.2, - easing: (t) => 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(); }; - }, []); -}; - -const FadeIn = ({ children, className = "", delay = 0 }: { children: React.ReactNode; className?: string; delay?: number }) => { - const [ref, inView] = useInView({ - triggerOnce: true, - threshold: 0.1, - rootMargin: "0px 0px -10% 0px" - }); - - return ( - - {children} - - ); -}; - -const GermanyText = () => { - const { theme, setTheme } = useContext(ThemeContext); - - return ( - - setTheme('black')} - className={theme === 'black' ? 'ring-1 ring-white/20' : ''} - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - /> - setTheme('red')} - className={theme === 'red' ? 'ring-1 ring-white/20' : ''} - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - /> - setTheme('gold')} - className={theme === 'gold' ? 'ring-1 ring-white/20' : ''} - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - /> - - ); -}; - -const MobileMenu = () => { - const [isOpen, setIsOpen] = useState(false); - const menuRef = React.useRef(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 ( - <> - 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 }} - > - - - - - - - {isOpen && ( - <> - setIsOpen(false)} - aria-hidden="true" - /> - - - {/* Glass background and content wrapper with explicit backdrop filter */} -
- {/* Close button */} - 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 }} - > - - - - - -
- - - -
- 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 }} - > - - - - Contact Me - -
- -
- -
-
-
-
-
- - )} -
- - ); -}; +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 ( - - -
- - Jan-Marlon Leibl • Fullstack Software Developer | PHP & TypeScript Expert - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
-
- -
-
-
- -
-
-
- - Available for Work -
-
-

- Jan-Marlon Leibl -

-
-

- Software Developer -
- based in -

-
-
-
-

- Passionate about creating digital experiences, with a focus on PHP and modern web technologies. -

-
- - View My Work - - - - - - Get in Touch - - - - -
-
- -
- -
- Role - - Fullstack Developer - -
-
- Experience - - 5+ Years - -
-
-
-
-
- -
-
- -
-

About

-
-
-
- -
-
- -
- Jan-Marlon Leibl - Fullstack Software Developer -
-
- -
-

Jan-Marlon Leibl

-

Fullstack Developer

-
-
-
- - -
-
-

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

-

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

-
- -
-
-
-

Technologies

-
-
-
    - {['PHP', 'JavaScript', 'MySQL', 'React'].map((tech) => ( -
  • - - {tech} -
  • - ))} -
-
- -
-
-

Interests

-
-
-
    - {[ - 'Web Development', - 'System Architecture', - 'UI/UX Design', - 'Performance Optimization' - ].map((interest) => ( -
  • - - {interest} -
  • - ))} -
-
-
- -
- - View GitHub - - - Connect on LinkedIn - -
-
-
-
-
-
- -
-
-
- -
- Portfolio -
-

Selected Work

-
-
-
-
- -
- {[ - { - 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'] - } - ].map((project, index) => ( - -
-
-
- {project.year} -
-
-
- -
-
-

- {project.title} -

-

- {project.description} -

-
- -
-
-

Technologies

-
- {project.tags.map((tag, tagIndex) => ( - - {tag}{tagIndex !== project.tags.length - 1 && ( - - )} - - ))} -
-
-
-
- -
-
-
- ))} -
-
-
- -
-
-
- -
-

Let's Connect

-

- Always interested in new opportunities and collaborations. -

-
-
- - - - - - -
-

- © {new Date().getFullYear()} Jan-Marlon Leibl. All rights reserved. -

-
-
-
-
-
-
-
+
+ + + +