Initial Commit

This commit is contained in:
Jan-Marlon Leibl
2025-01-12 23:55:59 +01:00
parent 732b6deb17
commit 81f39ea2bd
18 changed files with 924 additions and 121 deletions

BIN
bun.lockb

Binary file not shown.

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@ -9,9 +9,16 @@
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-aspect-ratio": "^1.1.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^11.17.0",
"lucide-react": "^0.471.0",
"next": "15.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.1.4"
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"typescript": "^5",

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

View File

@ -0,0 +1,5 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }

View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -3,7 +3,9 @@ import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<Head>
<meta name="color-scheme" content="dark" />
</Head>
<body className="antialiased">
<Main />
<NextScript />

View File

@ -1,114 +1,724 @@
import { Card } from "@/components/ui/card";
import { Montserrat, Playfair_Display } from "next/font/google";
import { motion, AnimatePresence } from "framer-motion";
import Link from "next/link";
import Image from "next/image";
import { Geist, Geist_Mono } from "next/font/google";
import React from "react";
import Head from 'next/head';
const geistSans = Geist({
variable: "--font-geist-sans",
const playfair = Playfair_Display({
subsets: ["latin"],
display: "swap",
variable: "--font-playfair",
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
const montserrat = Montserrat({
subsets: ["latin"],
variable: "--font-montserrat",
});
const MotionCard = motion(Card);
const focusStyles = `
focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-black
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-black
`;
export default function Home() {
return (
<div
className={`${geistSans.variable} ${geistMono.variable} grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]`}
>
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/pages/index.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
const [isScrolled, setIsScrolled] = React.useState(false);
const [isMenuVisible, setIsMenuVisible] = React.useState(false);
const lastScrollY = React.useRef(0);
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
React.useEffect(() => {
const updateScroll = () => {
const currentScrollY = window.scrollY;
setIsScrolled(currentScrollY > 100);
// Always hide menu when at the very top
if (currentScrollY === 0) {
setIsMenuVisible(false);
return;
}
// Show menu when scrolling up and past initial threshold
if (currentScrollY < lastScrollY.current && currentScrollY > 100) {
setIsMenuVisible(true);
}
// Hide menu when scrolling down
else if (currentScrollY > lastScrollY.current) {
setIsMenuVisible(false);
}
lastScrollY.current = currentScrollY;
};
window.addEventListener('scroll', updateScroll);
updateScroll(); // Initial check
return () => window.removeEventListener('scroll', updateScroll);
}, []);
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const scrollToSection = (sectionId: string) => {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<AnimatePresence>
<>
<Head>
<title>Johannes Behrens - International Model</title>
<meta
name="description"
content="Johannes Behrens is an international model based in Hamburg, Germany. Specializing in commercial, fitness, and editorial modeling with a dynamic and charismatic presence."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<motion.main
className={`${playfair.variable} ${montserrat.variable} min-h-screen font-sans bg-black`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.7, ease: [0.32, 0.72, 0, 1] }}
>
{/* Skip Link */}
<a
href="#main-content"
className={`
sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4
bg-white text-black px-4 py-2 z-50 ${focusStyles}
`}
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
Skip to main content
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
{/* Menu Bar */}
<header>
<motion.nav
className="fixed top-0 left-0 right-0 z-50 bg-black/90 backdrop-blur-md border-b border-white/10"
initial={{ y: -100 }}
animate={{ y: isMenuVisible ? 0 : -100 }}
transition={{ duration: 0.3, ease: [0.32, 0.72, 0, 1] }}
role="navigation"
aria-label="Main navigation"
>
<div className="container mx-auto px-4 sm:px-6 md:px-8 h-16 flex items-center justify-between">
<motion.button
className="flex items-center h-full"
onClick={scrollToTop}
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }}
aria-label="Back to top"
>
<div className="flex flex-col leading-[0.8]">
<span className="font-playfair text-xl text-white font-black tracking-tight">Johannes</span>
<span className="font-playfair text-xl text-white font-black tracking-tight pl-4">Behrens</span>
</div>
</motion.button>
<div className="flex items-center gap-8" role="menubar">
<motion.button
className={`font-montserrat text-sm text-white/60 hover:text-white transition-colors ${focusStyles}`}
onClick={() => scrollToSection('about')}
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }}
aria-label="Go to about section"
role="menuitem"
tabIndex={0}
>
About
</motion.button>
<motion.button
className="font-montserrat text-sm text-white/60 hover:text-white transition-colors"
onClick={() => scrollToSection('portfolio')}
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }}
aria-label="Go to portfolio section"
role="menuitem"
>
Portfolio
</motion.button>
<motion.button
className="font-montserrat text-sm text-white/60 hover:text-white transition-colors"
onClick={() => scrollToSection('contact')}
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.2 }}
aria-label="Go to contact section"
role="menuitem"
>
Contact
</motion.button>
</div>
</div>
</motion.nav>
</header>
{/* Main Content */}
<div id="main-content" tabIndex={-1}>
{/* Hero Section */}
<section
className="relative h-[100svh] bg-black overflow-hidden"
aria-label="Hero section"
>
{/* Background Image */}
<motion.div
className="absolute inset-0 w-full md:w-[80%] left-0 md:left-[-15%]"
initial={{ opacity: 0, scale: 1.1 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1.4, ease: [0.32, 0.72, 0, 1], delay: 0.3 }}
aria-hidden="true"
>
{/* Mobile Image */}
<div className="relative w-full h-full md:hidden">
<Image
src="/images/hero-background.jpg"
alt="Johannes Behrens, professional model, standing confidently in a fashion pose"
fill
className="object-cover object-[center_15%]"
priority
quality={85}
sizes="100vw"
loading="eager"
fetchPriority="high"
/>
<motion.div
className="absolute inset-0 bg-gradient-to-b from-black/80 via-black/30 to-black/80"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1, ease: [0.32, 0.72, 0, 1], delay: 0.6 }}
/>
</div>
{/* Desktop Image */}
<div className="relative w-full h-full hidden md:block">
<Image
src="/images/hero-background.jpg"
alt="Johannes Behrens, professional model, standing confidently in a fashion pose"
fill
className="object-cover"
priority
quality={85}
sizes="(max-width: 768px) 100vw, 80vw"
loading="eager"
fetchPriority="high"
/>
<motion.div
className="absolute inset-0 bg-gradient-to-r from-black via-black/20 to-transparent"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1, ease: [0.32, 0.72, 0, 1], delay: 0.6 }}
/>
</div>
</motion.div>
{/* Content Container */}
<div className="absolute inset-0 flex items-center md:justify-start">
<div className="w-full md:w-[80%] px-6 sm:px-8 md:pl-[25%] md:pr-0 lg:pl-[50%]">
<div className="relative">
{/* Main Title */}
<motion.div
className="relative z-20 text-center md:text-left"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, ease: [0.32, 0.72, 0, 1], delay: 0.7 }}
>
<div className="mix-blend-difference">
<motion.div
className="overflow-hidden mb-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.7, ease: [0.32, 0.72, 0, 1], delay: 0.9 }}
>
<div className="font-montserrat tracking-[0.3em] md:tracking-[0.5em] text-white/90 text-xs sm:text-sm uppercase font-medium">
International Model
</div>
</motion.div>
<motion.h1
className="font-playfair text-[4.5rem] sm:text-[5rem] md:text-[7rem] lg:text-[8rem] font-black text-white leading-[0.8] tracking-tight"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.9, ease: [0.32, 0.72, 0, 1], delay: 1 }}
>
Johannes
</motion.h1>
<span className="font-playfair text-[3.5rem] sm:text-[4rem] md:text-[5rem] lg:text-[6rem] font-black text-white md:leading-[0.7] tracking-tight md:pl-14">
Behrens
</span>
</div>
</motion.div>
{/* Bottom Content */}
<motion.div
className="mt-8 md:mt-12 mix-blend-difference"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, ease: [0.32, 0.72, 0, 1], delay: 1.2 }}
>
<div className="grid grid-cols-3 gap-3 md:gap-4 text-white/80 text-center md:text-left">
<div className="space-y-2">
<h2 className="font-montserrat text-[10px] md:text-xs tracking-[0.2em] uppercase font-semibold">Based in</h2>
<p className="font-montserrat text-lg md:text-xl font-medium">Hamburg</p>
</div>
<div className="space-y-2">
<h2 className="font-montserrat text-[10px] md:text-xs tracking-[0.2em] uppercase font-semibold">Height</h2>
<p className="font-montserrat text-lg md:text-xl font-medium">183 cm</p>
</div>
<div className="space-y-2">
<h2 className="font-montserrat text-[10px] md:text-xs tracking-[0.2em] uppercase font-semibold">Bookings</h2>
<p className="font-montserrat text-lg md:text-xl font-medium">Worldwide</p>
</div>
</div>
</motion.div>
{/* Mobile Categories */}
<motion.div
className="mt-8 text-center md:hidden"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, ease: [0.32, 0.72, 0, 1], delay: 1.4 }}
>
<p className="font-montserrat text-[10px] tracking-[0.3em] text-white/60 uppercase font-medium">
Commercial Fitness
</p>
</motion.div>
</div>
</div>
</div>
{/* Scroll Indicator/To Top Button */}
<motion.div
className="fixed bottom-6 md:bottom-8 right-6 md:right-8 z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.7, ease: [0.32, 0.72, 0, 1], delay: 1.6 }}
>
<motion.button
onClick={scrollToTop}
className={`group relative flex items-center gap-2 h-10 ${focusStyles}`}
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.3, ease: [0.32, 0.72, 0, 1] }}
aria-label="Scroll to top"
tabIndex={0}
>
{/* Background */}
<motion.div
className="absolute right-0 bg-black/50 backdrop-blur-md rounded-full"
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: isScrolled ? 1 : 0,
scale: isScrolled ? 1 : 0.8,
width: isScrolled ? 40 : 120,
}}
transition={{ duration: 0.3, ease: [0.32, 0.72, 0, 1] }}
style={{
height: 40,
}}
/>
{/* Scroll Text and Line */}
<motion.div
className="flex items-center gap-2 px-3"
animate={{
opacity: isScrolled ? 0 : 1,
pointerEvents: isScrolled ? "none" : "auto",
}}
transition={{ duration: 0.2, ease: [0.32, 0.72, 0, 1] }}
>
<div className="font-montserrat tracking-[0.3em] text-white/60 text-[10px] uppercase font-medium mix-blend-difference">
Scroll
</div>
<div className="w-[40px] md:w-[50px] h-px bg-white/60 origin-right mix-blend-difference" />
</motion.div>
{/* Arrow Up */}
<motion.div
className="absolute right-0 flex items-center justify-center w-10 h-10"
initial={{ opacity: 0 }}
animate={{
opacity: isScrolled ? 1 : 0,
pointerEvents: isScrolled ? "auto" : "none",
}}
transition={{ duration: 0.2, ease: [0.32, 0.72, 0, 1] }}
>
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-white/60"
>
<path
d="M7 1L7 13M7 1L13 7M7 1L1 7"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</motion.div>
</motion.button>
</motion.div>
</section>
{/* Details Section */}
<section id="about" className="py-16 sm:py-20 md:py-24 lg:py-32 bg-black border-t border-white/10">
<div className="container mx-auto px-4 sm:px-6 md:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 md:gap-16 lg:gap-24 max-w-7xl mx-auto">
{/* Left Column - About */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.9, ease: [0.32, 0.72, 0, 1] }}
className="space-y-8"
>
<div>
<motion.div
className="font-montserrat tracking-[0.3em] text-white/60 text-xs uppercase mb-4 sm:mb-6 font-semibold"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
>
About
</motion.div>
<motion.p
className="font-montserrat text-white/80 text-base sm:text-lg leading-relaxed font-medium"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.9, ease: [0.32, 0.72, 0, 1], delay: 0.2 }}
>
With a versatile athletic background and a strong stage presence as a drummer, I bring dynamic and charismatic energy to every project. Reliable, adaptable, and passionate about delivering outstanding results.
</motion.p>
</div>
</motion.div>
{/* Right Column - Measurements */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.9, ease: [0.32, 0.72, 0, 1] }}
className="space-y-10 sm:space-y-12"
>
<div>
<motion.div
className="font-montserrat tracking-[0.3em] text-white/60 text-xs uppercase mb-6 font-semibold"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
>
Measurements
</motion.div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-6 sm:gap-x-8 gap-y-6">
<div>
<h2 className="font-montserrat text-white/60 uppercase tracking-[0.2em] text-[10px] sm:text-xs mb-2 font-semibold">Height</h2>
<p className="font-montserrat text-lg sm:text-xl text-white font-medium">183 cm</p>
</div>
<div>
<h2 className="font-montserrat text-white/60 uppercase tracking-[0.2em] text-[10px] sm:text-xs mb-2 font-semibold">Chest</h2>
<p className="font-montserrat text-lg sm:text-xl text-white font-medium">95 cm</p>
</div>
<div>
<h2 className="font-montserrat text-white/60 uppercase tracking-[0.2em] text-[10px] sm:text-xs mb-2 font-semibold">Waist</h2>
<p className="font-montserrat text-lg sm:text-xl text-white font-medium">73 cm</p>
</div>
<div>
<h2 className="font-montserrat text-white/60 uppercase tracking-[0.2em] text-[10px] sm:text-xs mb-2 font-semibold">Hip</h2>
<p className="font-montserrat text-lg sm:text-xl text-white font-medium">88 cm</p>
</div>
<div>
<h2 className="font-montserrat text-white/60 uppercase tracking-[0.2em] text-[10px] sm:text-xs mb-2 font-semibold">Suit Size</h2>
<p className="font-montserrat text-lg sm:text-xl text-white font-medium">48</p>
</div>
<div>
<h2 className="font-montserrat text-white/60 uppercase tracking-[0.2em] text-[10px] sm:text-xs mb-2 font-semibold">Shoe Size</h2>
<p className="font-montserrat text-lg sm:text-xl text-white font-medium">45</p>
</div>
</div>
</div>
<div>
<h3 className="font-montserrat text-white/60 uppercase tracking-[0.2em] text-[10px] sm:text-xs mb-3 font-semibold">Features</h3>
<div className="grid grid-cols-2 gap-x-8 gap-y-2">
<p className="font-montserrat text-white/80 text-sm sm:text-base font-medium">Brown Eyes</p>
<p className="font-montserrat text-white/80 text-sm sm:text-base font-medium">Brown Hair</p>
</div>
</div>
</motion.div>
</div>
</div>
</section>
{/* Categories Section */}
<section className="relative py-16 sm:py-20 md:py-24 lg:py-32 bg-white">
<div className="absolute inset-0" style={{
backgroundImage: `linear-gradient(to right, rgba(0, 0, 0, 0.03) 1px, transparent 1px), linear-gradient(to bottom, rgba(0, 0, 0, 0.03) 1px, transparent 1px)`,
backgroundSize: '48px 48px'
}} />
<div className="container mx-auto px-4 sm:px-6 md:px-8 relative">
<motion.div
className="max-w-screen-lg mx-auto mb-16 sm:mb-20 md:mb-24 lg:mb-32 text-center space-y-4 sm:space-y-6"
>
<motion.div
className="font-montserrat tracking-[0.3em] sm:tracking-[0.5em] text-black/70 text-xs sm:text-sm uppercase font-medium"
>
Categories
</motion.div>
<motion.h2
className="font-playfair text-2xl sm:text-3xl md:text-4xl lg:text-5xl text-black font-bold"
>
Modeling Services
</motion.h2>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12 max-w-5xl mx-auto">
{/* Commercial */}
<motion.div
className="text-center space-y-4"
>
<div className="w-16 h-16 mx-auto mb-6">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" className="w-full h-full text-black/70">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M3 3h18v18H3zM9 3v18M15 3v18M3 9h18M3 15h18" />
</svg>
</div>
<h3 className="font-montserrat text-black uppercase tracking-[0.2em] text-base font-semibold">Commercial</h3>
<p className="font-montserrat text-black/70 text-sm leading-relaxed max-w-xs mx-auto font-medium">
Product campaigns, lifestyle shoots, and brand representation with a focus on authentic engagement and natural presence.
</p>
</motion.div>
{/* Fitness */}
<motion.div
className="text-center space-y-4"
>
<div className="w-16 h-16 mx-auto mb-6">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" className="w-full h-full text-black/70">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M6.5 6.5h11v11h-11z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M3 9V6.5h3.5M21 9V6.5h-3.5M3 15v2.5h3.5M21 15v2.5h-3.5" />
</svg>
</div>
<h3 className="font-montserrat text-black uppercase tracking-[0.2em] text-base font-semibold">Fitness</h3>
<p className="font-montserrat text-black/70 text-sm leading-relaxed max-w-xs mx-auto font-medium">
Athletic wear, sports campaigns, and dynamic movement shoots showcasing strength and functional fitness.
</p>
</motion.div>
{/* Editorial */}
<motion.div
className="text-center space-y-4"
>
<div className="w-16 h-16 mx-auto mb-6">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" className="w-full h-full text-black/70">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M4 21V3h16v18M8 3v18M16 3v18" />
</svg>
</div>
<h3 className="font-montserrat text-black uppercase tracking-[0.2em] text-base font-semibold">Editorial</h3>
<p className="font-montserrat text-black/70 text-sm leading-relaxed max-w-xs mx-auto font-medium">
Fashion editorials, magazine features, and artistic collaborations with a focus on storytelling and style.
</p>
</motion.div>
</div>
</div>
</section>
{/* Portfolio Section */}
<section id="portfolio" className="relative py-16 sm:py-20 md:py-24 lg:py-32 bg-white">
<div className="absolute inset-0" style={{
backgroundImage: `linear-gradient(to right, rgba(0, 0, 0, 0.03) 1px, transparent 1px), linear-gradient(to bottom, rgba(0, 0, 0, 0.03) 1px, transparent 1px)`,
backgroundSize: '48px 48px'
}} />
<div className="container mx-auto px-4 sm:px-6 md:px-8 relative">
<motion.div
className="max-w-screen-lg mx-auto mb-16 sm:mb-20 md:mb-24 lg:mb-32 text-center space-y-4 sm:space-y-6"
>
<motion.div
className="font-montserrat tracking-[0.3em] sm:tracking-[0.5em] text-black/70 text-xs sm:text-sm uppercase font-medium"
>
Portfolio
</motion.div>
<motion.h2
className="font-playfair text-2xl sm:text-3xl md:text-4xl lg:text-5xl text-black font-bold"
>
Selected Work
</motion.h2>
</motion.div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 md:gap-8 lg:gap-12">
{[1, 2, 3, 4, 5, 6].map((index) => (
<MotionCard
key={index}
className="overflow-hidden group border-0 bg-transparent relative"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.9, ease: [0.32, 0.72, 0, 1], delay: index * 0.05 }}
>
<motion.div
className="relative w-full aspect-[3/4] overflow-hidden"
whileHover={{ scale: 1.02 }}
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
>
<Image
src={`/images/portfolio-${index}.jpg`}
alt={`Portfolio image ${index} - Professional modeling work by Johannes Behrens`}
fill
className="object-cover grayscale group-hover:grayscale-0 transition-all duration-200"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
quality={85}
priority={index <= 2}
loading={index <= 2 ? "eager" : "lazy"}
/>
<motion.div
className="absolute inset-0 bg-gradient-to-b from-black/0 to-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
/>
</motion.div>
</MotionCard>
))}
</div>
</div>
</section>
{/* Contact Section */}
<section id="contact" className="py-16 sm:py-20 md:py-24 lg:py-32 bg-black">
<div className="container mx-auto px-4 sm:px-6 md:px-8">
<motion.div
className="max-w-screen-lg mx-auto"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.9, ease: [0.32, 0.72, 0, 1] }}
>
<div className="text-center mb-12 sm:mb-16 md:mb-20 lg:mb-24">
<motion.div
className="font-montserrat tracking-[0.3em] sm:tracking-[0.4em] md:tracking-[0.5em] text-white/60 text-xs sm:text-sm uppercase mb-4 font-semibold"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
>
Contact
</motion.div>
</div>
<motion.div
className="grid grid-cols-1 md:grid-cols-2 gap-12 sm:gap-16 md:gap-24 max-w-5xl mx-auto text-center"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.9, ease: [0.32, 0.72, 0, 1], delay: 0.2 }}
>
<div className="space-y-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.9, ease: [0.32, 0.72, 0, 1] }}
>
<h3 className="font-montserrat text-white/60 uppercase tracking-[0.2em] text-[10px] sm:text-xs mb-2 font-semibold">Email</h3>
<motion.p
className="font-montserrat text-lg sm:text-xl md:text-2xl lg:text-3xl break-words font-medium"
whileHover={{ opacity: 0.8 }}
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
>
<Link
href="mailto:contact@johannesbehrens.com"
className={`hover:opacity-80 transition-opacity ${focusStyles}`}
tabIndex={0}
>
contact@johannesbehrens.com
</Link>
</motion.p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.9, ease: [0.32, 0.72, 0, 1], delay: 0.1 }}
>
<h3 className="font-montserrat text-white/60 uppercase tracking-[0.2em] text-[10px] sm:text-xs mb-2 font-semibold">Based in</h3>
<p className="font-montserrat text-lg sm:text-xl md:text-2xl lg:text-3xl font-medium">Hamburg, Germany</p>
</motion.div>
</div>
<div className="space-y-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.9, ease: [0.32, 0.72, 0, 1], delay: 0.2 }}
>
<h3 className="font-montserrat text-white/60 uppercase tracking-[0.2em] text-[10px] sm:text-xs mb-2 font-semibold">Agency</h3>
<p className="font-montserrat text-lg sm:text-xl md:text-2xl lg:text-3xl font-medium">Model Management</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.9, ease: [0.32, 0.72, 0, 1], delay: 0.3 }}
>
<h3 className="font-montserrat text-white/60 uppercase tracking-[0.2em] text-[10px] sm:text-xs mb-2 font-semibold">Social</h3>
<div className="space-y-3">
<motion.p
className="font-montserrat text-lg sm:text-xl md:text-2xl lg:text-3xl font-medium"
whileHover={{ opacity: 0.8 }}
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
>
<Link
href="https://www.instagram.com/johannes.brh/"
target="_blank"
rel="noopener noreferrer"
className={`hover:opacity-80 transition-opacity duration-200 ${focusStyles}`}
tabIndex={0}
>
Instagram
</Link>
</motion.p>
<motion.p
className="font-montserrat text-lg sm:text-xl md:text-2xl lg:text-3xl font-medium"
whileHover={{ opacity: 0.8 }}
transition={{ duration: 0.4, ease: [0.32, 0.72, 0, 1] }}
>
<Link href="#" className="hover:opacity-80 transition-opacity duration-200">
LinkedIn
</Link>
</motion.p>
</div>
</motion.div>
</div>
</motion.div>
</motion.div>
</div>
</section>
</div>
{/* Footer */}
<footer
className="py-8 bg-black border-t border-white/10"
role="contentinfo"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=default-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
<div className="container mx-auto px-4 text-center">
<p className="font-montserrat text-sm text-white/90">
Made with by{" "}
<Link
href="https://jleibl.net"
target="_blank"
rel="noopener noreferrer"
className={`text-white hover:text-white/80 transition-colors duration-200 ${focusStyles}`}
aria-label="Visit Jan-Marlon Leibl's website (opens in new tab)"
>
Jan-Marlon Leibl
</Link>
</p>
</div>
</footer>
</motion.main>
</>
</AnimatePresence>
);
}

View File

@ -1,21 +1,48 @@
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@layer base {
html {
scroll-behavior: smooth;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
body {
@apply bg-black text-white;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
@layer utilities {
.vertical-text {
writing-mode: vertical-rl;
text-orientation: mixed;
transform: rotate(180deg);
}
}
:root {
--background: 0 0% 0%;
--foreground: 0 0% 100%;
--card: 0 0% 0%;
--card-foreground: 0 0% 100%;
--popover: 0 0% 0%;
--popover-foreground: 0 0% 100%;
--primary: 0 0% 100%;
--primary-foreground: 0 0% 0%;
--secondary: 0 0% 5%;
--secondary-foreground: 0 0% 100%;
--muted: 0 0% 10%;
--muted-foreground: 0 0% 70%;
--accent: 0 0% 100%;
--accent-foreground: 0 0% 0%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 20%;
--input: 0 0% 20%;
--ring: 0 0% 100%;
--radius: 0;
}

View File

@ -1,18 +1,67 @@
import type { Config } from "tailwindcss";
import animate from "tailwindcss-animate";
export default {
content: [
darkMode: ["class"],
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)",
},
},
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
fontFamily: {
prompt:['Prompt', 'sans-serif'],
playfair: ['Playfair Display', 'serif'],
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [],
plugins: [animate],
} satisfies Config;