Add claude.md + beautiful smooth animations on scroll
This commit is contained in:
parent
75b8e6b7b8
commit
cfa70c4ee0
34
CLAUDE.md
Normal file
34
CLAUDE.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# unov-landing
|
||||||
|
|
||||||
|
Site vitrine de la société **Unov** (qui développe MyInfoMate).
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
- Next.js 16 / React 19 / TypeScript
|
||||||
|
- Tailwind CSS v4
|
||||||
|
- Babel React Compiler
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── app/
|
||||||
|
│ ├── layout.tsx # Layout global, font "Public Sans"
|
||||||
|
│ ├── page.tsx # Page d'accueil
|
||||||
|
│ ├── globals.css # Thème Tailwind v4
|
||||||
|
│ └── portfolio/
|
||||||
|
│ └── page.tsx # Page portfolio
|
||||||
|
└── data/
|
||||||
|
└── stitch-images.ts # Assets Stitch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Thème
|
||||||
|
- Couleur primaire : `#309CB0` (teal), variantes `#72B9C4`, `#ABD3DC`
|
||||||
|
- Support dark mode
|
||||||
|
- Utilities custom : `glass-card`, `material-symbols-outlined`
|
||||||
|
- Animation : `marquee` (30s linear infinite)
|
||||||
|
|
||||||
|
## Commandes
|
||||||
|
```bash
|
||||||
|
npm run dev # Dev local
|
||||||
|
npm run build # Build production
|
||||||
|
npm run lint # ESLint
|
||||||
|
```
|
||||||
253
src/app/page.tsx
253
src/app/page.tsx
@ -1,34 +1,109 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { resolveImage } from "@/data/stitch-images";
|
import { resolveImage } from "@/data/stitch-images";
|
||||||
|
|
||||||
|
function Reveal({ children, delay = 0, className = '' }: { children: React.ReactNode; delay?: number; className?: string }) {
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
const [visible, setVisible] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => { if (entry.isIntersecting) setVisible(true); },
|
||||||
|
{ threshold: 0.12 }
|
||||||
|
);
|
||||||
|
observer.observe(el);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
opacity: visible ? 1 : 0,
|
||||||
|
transform: visible ? 'translateY(0)' : 'translateY(22px)',
|
||||||
|
transition: `opacity 0.55s ease-out ${delay}ms, transform 0.55s ease-out ${delay}ms`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
const [navDark, setNavDark] = useState(false);
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const [selectedProject, setSelectedProject] = useState<any>(null);
|
const [selectedProject, setSelectedProject] = useState<any>(null);
|
||||||
const [showAllProjects, setShowAllProjects] = useState(false);
|
const [showAllProjects, setShowAllProjects] = useState(false);
|
||||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||||
|
const [formData, setFormData] = useState({ nom: '', email: '', service: 'Software Development', message: '' });
|
||||||
|
const [formStatus, setFormStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
setIsScrolled(window.scrollY > 20);
|
setIsScrolled(window.scrollY > 20);
|
||||||
|
|
||||||
|
const lightSections = ['services', 'technologies', 'portfolio', 'contact'];
|
||||||
|
const navH = 80;
|
||||||
|
const onLight = lightSections.some((id) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) return false;
|
||||||
|
const { top, bottom } = el.getBoundingClientRect();
|
||||||
|
return top <= navH && bottom > navH;
|
||||||
|
});
|
||||||
|
setNavDark(onLight);
|
||||||
};
|
};
|
||||||
window.addEventListener("scroll", handleScroll);
|
window.addEventListener("scroll", handleScroll);
|
||||||
return () => window.removeEventListener("scroll", handleScroll);
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = isMenuOpen ? 'hidden' : 'auto';
|
||||||
|
return () => { document.body.style.overflow = 'auto'; };
|
||||||
|
}, [isMenuOpen]);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setFormStatus('loading');
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://formspree.io/f/xnjgewyk", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", "Accept": "application/json" },
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
setFormStatus('success');
|
||||||
|
setFormData({ nom: '', email: '', service: 'Software Development', message: '' });
|
||||||
|
} else {
|
||||||
|
setFormStatus('error');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setFormStatus('error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white text-slate-900">
|
<div className="min-h-screen bg-white text-slate-900">
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav
|
<nav
|
||||||
className={`fixed top-0 w-full z-50 transition-all duration-300 ${isScrolled
|
className={`fixed top-0 w-full z-50 transition-[padding] duration-300 ease-in-out ${isScrolled || navDark ? 'py-4' : 'py-6'}`}
|
||||||
? "bg-background-light/80 dark:bg-background-dark/80 backdrop-blur-md py-4 shadow-sm"
|
|
||||||
: "bg-transparent py-6"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between">
|
{/* Subtle overlay (hero scroll) */}
|
||||||
|
<div className={`absolute inset-0 bg-black/20 backdrop-blur-sm transition-opacity duration-500 ease-in-out ${isScrolled && !navDark ? 'opacity-100' : 'opacity-0'}`} />
|
||||||
|
{/* Dark overlay */}
|
||||||
|
<div className={`absolute inset-0 bg-[#0A0F1C]/95 backdrop-blur-md shadow-lg shadow-black/20 transition-opacity duration-500 ease-in-out ${navDark ? 'opacity-100' : 'opacity-0'}`} />
|
||||||
|
|
||||||
|
<div className="relative z-10 max-w-7xl mx-auto px-6 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Image
|
<Image
|
||||||
src={resolveImage("{{DATA:IMAGE:IMAGE_2}}")}
|
src={resolveImage("{{DATA:IMAGE:IMAGE_2}}")}
|
||||||
@ -41,19 +116,19 @@ export default function Home() {
|
|||||||
<div className="hidden md:flex items-center gap-10">
|
<div className="hidden md:flex items-center gap-10">
|
||||||
<a
|
<a
|
||||||
href="#services"
|
href="#services"
|
||||||
className="text-sm font-medium hover:text-primary transition-colors uppercase tracking-widest"
|
className={`text-sm font-medium hover:text-primary transition-colors duration-500 uppercase tracking-widest ${navDark ? 'text-white/80' : 'text-white/90'}`}
|
||||||
>
|
>
|
||||||
Services
|
Services
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#portfolio"
|
href="#portfolio"
|
||||||
className="text-sm font-medium hover:text-primary transition-colors uppercase tracking-widest"
|
className={`text-sm font-medium hover:text-primary transition-colors duration-500 uppercase tracking-widest ${navDark ? 'text-white/80' : 'text-white/90'}`}
|
||||||
>
|
>
|
||||||
Portfolio
|
Portfolio
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#technologies"
|
href="#technologies"
|
||||||
className="text-sm font-medium hover:text-primary transition-colors uppercase tracking-widest"
|
className={`text-sm font-medium hover:text-primary transition-colors duration-500 uppercase tracking-widest ${navDark ? 'text-white/80' : 'text-white/90'}`}
|
||||||
>
|
>
|
||||||
Technologies
|
Technologies
|
||||||
</a>
|
</a>
|
||||||
@ -64,12 +139,53 @@ export default function Home() {
|
|||||||
Let's Talk
|
Let's Talk
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button className="md:hidden">
|
<button className="md:hidden" onClick={() => setIsMenuOpen(true)}>
|
||||||
<span className="material-symbols-outlined">menu</span>
|
<span className="material-symbols-outlined">menu</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{/* Mobile Menu Backdrop */}
|
||||||
|
<div
|
||||||
|
className={`md:hidden fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-[150] transition-opacity duration-300 ${isMenuOpen ? 'opacity-100 visible' : 'opacity-0 invisible pointer-events-none'}`}
|
||||||
|
onClick={() => setIsMenuOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Mobile Menu Panel */}
|
||||||
|
<div className={`md:hidden fixed inset-y-0 right-0 w-[300px] h-screen bg-white z-[200] shadow-2xl transition-transform duration-500 ease-in-out transform ${isMenuOpen ? 'translate-x-0' : 'translate-x-full'}`}>
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex justify-end p-6 border-b border-slate-50">
|
||||||
|
<button onClick={() => setIsMenuOpen(false)} className="w-10 h-10 flex items-center justify-center text-slate-600 hover:text-slate-900 transition-colors">
|
||||||
|
<span className="material-symbols-outlined text-3xl">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<nav className="flex-1 flex flex-col p-8 gap-6">
|
||||||
|
{[
|
||||||
|
{ label: 'Services', href: '#services' },
|
||||||
|
{ label: 'Portfolio', href: '#portfolio' },
|
||||||
|
{ label: 'Technologies', href: '#technologies' },
|
||||||
|
].map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.label}
|
||||||
|
href={item.href}
|
||||||
|
className="text-sm font-bold tracking-[0.2em] uppercase text-slate-900 hover:text-primary transition-colors py-3 border-b border-slate-50"
|
||||||
|
onClick={() => setIsMenuOpen(false)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
<div className="mt-auto pt-8">
|
||||||
|
<button
|
||||||
|
onClick={() => { document.getElementById("contact")?.scrollIntoView({ behavior: "smooth" }); setIsMenuOpen(false); }}
|
||||||
|
className="w-full py-4 bg-primary text-white font-black text-xs tracking-widest rounded-xl shadow-xl shadow-primary/20 active:scale-95 transition-all"
|
||||||
|
>
|
||||||
|
Let's Talk
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="relative h-screen flex items-center pt-20 overflow-hidden">
|
<section className="relative h-screen flex items-center pt-20 overflow-hidden">
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
@ -121,9 +237,9 @@ export default function Home() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Services Section */}
|
{/* Services Section */}
|
||||||
<section className="bg-white dark:bg-background-dark/50 py-32" id="services">
|
<section className="bg-white dark:bg-background-dark/50 py-20 lg:py-28" id="services">
|
||||||
<div className="container mx-auto px-6">
|
<div className="container mx-auto px-6">
|
||||||
<div className="flex flex-col md:flex-row justify-between items-start mb-20 gap-8">
|
<Reveal className="flex flex-col md:flex-row justify-between items-start mb-12 lg:mb-16 gap-8">
|
||||||
<div className="max-w-xl">
|
<div className="max-w-xl">
|
||||||
<h2 className="text-primary font-bold uppercase tracking-widest text-xs mb-4">
|
<h2 className="text-primary font-bold uppercase tracking-widest text-xs mb-4">
|
||||||
Expertise
|
Expertise
|
||||||
@ -136,7 +252,7 @@ export default function Home() {
|
|||||||
Des solutions sur-mesure conçues avec une rigueur mathématique et
|
Des solutions sur-mesure conçues avec une rigueur mathématique et
|
||||||
une vision centrée utilisateur.
|
une vision centrée utilisateur.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</Reveal>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-10">
|
||||||
{/* Service Cards */}
|
{/* Service Cards */}
|
||||||
@ -160,8 +276,8 @@ export default function Home() {
|
|||||||
img: resolveImage("SERVICE_3D"),
|
img: resolveImage("SERVICE_3D"),
|
||||||
},
|
},
|
||||||
].map((service, idx) => (
|
].map((service, idx) => (
|
||||||
|
<Reveal key={idx} delay={idx * 80}>
|
||||||
<div
|
<div
|
||||||
key={idx}
|
|
||||||
className="group bg-white dark:bg-white/5 rounded-[2.5rem] p-10 shadow-[0_10px_40px_-15px_rgba(0,0,0,0.05)] hover:shadow-2xl transition-all duration-500 border border-slate-100 dark:border-white/10 hover:-translate-y-2"
|
className="group bg-white dark:bg-white/5 rounded-[2.5rem] p-10 shadow-[0_10px_40px_-15px_rgba(0,0,0,0.05)] hover:shadow-2xl transition-all duration-500 border border-slate-100 dark:border-white/10 hover:-translate-y-2"
|
||||||
>
|
>
|
||||||
<div className="size-16 bg-primary/10 rounded-2xl flex items-center justify-center mb-8 group-hover:bg-primary group-hover:text-white transition-colors duration-500">
|
<div className="size-16 bg-primary/10 rounded-2xl flex items-center justify-center mb-8 group-hover:bg-primary group-hover:text-white transition-colors duration-500">
|
||||||
@ -183,6 +299,7 @@ export default function Home() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -190,13 +307,12 @@ export default function Home() {
|
|||||||
|
|
||||||
{/* Technologies Marquee - UPDATED LIST */}
|
{/* Technologies Marquee - UPDATED LIST */}
|
||||||
<section
|
<section
|
||||||
className="py-24 border-y border-slate-100 dark:border-white/5 overflow-hidden bg-white dark:bg-background-dark"
|
className="py-20 overflow-hidden bg-slate-950"
|
||||||
id="technologies"
|
id="technologies"
|
||||||
>
|
>
|
||||||
<div className="flex whitespace-nowrap animate-marquee">
|
<div className="flex items-center gap-14 whitespace-nowrap animate-marquee">
|
||||||
{[...Array(2)].map((_, i) => (
|
{[1, 2].flatMap((copy) =>
|
||||||
<div key={i} className="flex items-center gap-24 px-12">
|
[
|
||||||
{[
|
|
||||||
{ name: "Python", icon: "code" },
|
{ name: "Python", icon: "code" },
|
||||||
{ name: "TypeScript", icon: "terminal" },
|
{ name: "TypeScript", icon: "terminal" },
|
||||||
{ name: "OpenAI", icon: "smart_toy" },
|
{ name: "OpenAI", icon: "smart_toy" },
|
||||||
@ -205,26 +321,21 @@ export default function Home() {
|
|||||||
{ name: "ASP.NET", icon: "settings_ethernet" },
|
{ name: "ASP.NET", icon: "settings_ethernet" },
|
||||||
{ name: "Angular", icon: "change_history" },
|
{ name: "Angular", icon: "change_history" },
|
||||||
{ name: "IoT", icon: "sensors" },
|
{ name: "IoT", icon: "sensors" },
|
||||||
].map((tech, j) => (
|
].map((tech, j) => [
|
||||||
<span
|
<span key={`${copy}-tech-${j}`} className="text-3xl font-black text-slate-600 uppercase tracking-tighter flex items-center gap-3 hover:text-primary transition-colors cursor-default shrink-0">
|
||||||
key={j}
|
<span className="material-symbols-outlined text-3xl">{tech.icon}</span>
|
||||||
className="text-4xl font-black text-slate-200 dark:text-slate-800 uppercase tracking-tighter flex items-center gap-4 hover:text-primary transition-colors cursor-default"
|
|
||||||
>
|
|
||||||
<span className="material-symbols-outlined text-4xl">
|
|
||||||
{tech.icon}
|
|
||||||
</span>{" "}
|
|
||||||
{tech.name}
|
{tech.name}
|
||||||
</span>
|
</span>,
|
||||||
))}
|
<span key={`${copy}-dot-${j}`} className="text-slate-700 text-2xl font-light select-none shrink-0">·</span>,
|
||||||
</div>
|
])
|
||||||
))}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Portfolio Section */}
|
{/* Portfolio Section */}
|
||||||
<section className="bg-slate-50 dark:bg-background-dark/30 py-32" id="portfolio">
|
<section className="bg-slate-50 dark:bg-background-dark/30 py-20 lg:py-28" id="portfolio">
|
||||||
<div className="container mx-auto px-6">
|
<div className="container mx-auto px-6">
|
||||||
<div className="text-center mb-24">
|
<Reveal className="text-center mb-24">
|
||||||
<h2 className="text-primary font-bold uppercase tracking-widest text-sm mb-4">
|
<h2 className="text-primary font-bold uppercase tracking-widest text-sm mb-4">
|
||||||
Réalisations
|
Réalisations
|
||||||
</h2>
|
</h2>
|
||||||
@ -232,7 +343,7 @@ export default function Home() {
|
|||||||
Portfolio Sélectionné
|
Portfolio Sélectionné
|
||||||
</h3>
|
</h3>
|
||||||
<div className="w-24 h-1.5 mx-auto bg-primary-light rounded-full"></div>
|
<div className="w-24 h-1.5 mx-auto bg-primary-light rounded-full"></div>
|
||||||
</div>
|
</Reveal>
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const allProjects = [
|
const allProjects = [
|
||||||
@ -323,11 +434,11 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Projects Grid */}
|
{/* Projects Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 md:gap-16">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-10">
|
||||||
{firstProjects.map((project, idx) => (
|
{firstProjects.map((project, idx) => (
|
||||||
|
<Reveal key={idx} delay={idx * 80}>
|
||||||
<div
|
<div
|
||||||
key={idx}
|
className="group relative overflow-hidden rounded-[3rem] aspect-[16/10] cursor-pointer shadow-2xl"
|
||||||
className="group relative overflow-hidden rounded-[3rem] aspect-[4/3] cursor-pointer shadow-2xl"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedProject(project);
|
setSelectedProject(project);
|
||||||
setCurrentImageIndex(0);
|
setCurrentImageIndex(0);
|
||||||
@ -358,20 +469,21 @@ export default function Home() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Professional Reveal Zone */}
|
{/* Professional Reveal Zone */}
|
||||||
<div className="relative mt-12 md:mt-16">
|
<div className="relative mt-12 md:mt-16">
|
||||||
<div
|
<div
|
||||||
className={`relative w-full transition-all duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)] overflow-hidden
|
className={`relative w-full transition-all duration-[1800ms] ease-[cubic-bezier(0.16,1,0.3,1)] overflow-hidden
|
||||||
${showAllProjects ? "max-h-[2000px] opacity-100" : "max-h-0 opacity-0"}`}
|
${showAllProjects ? "max-h-[2000px] opacity-100" : "max-h-0 opacity-0"}`}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 md:gap-16 pb-32">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-10 pb-16">
|
||||||
{extraProjects.map((project, idx) => (
|
{extraProjects.map((project, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx + 4}
|
key={idx + 4}
|
||||||
className="group relative overflow-hidden rounded-[3rem] aspect-[4/3] cursor-pointer shadow-2xl"
|
className="group relative overflow-hidden rounded-[3rem] aspect-[16/10] cursor-pointer shadow-2xl"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedProject(project);
|
setSelectedProject(project);
|
||||||
setCurrentImageIndex(0);
|
setCurrentImageIndex(0);
|
||||||
@ -425,9 +537,9 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/* Contact Form Section - REVISED LAYOUT to match Image */}
|
{/* Contact Form Section - REVISED LAYOUT to match Image */}
|
||||||
<section className="bg-white dark:bg-background-dark py-32" id="contact">
|
<section className="bg-white dark:bg-background-dark py-20 lg:py-28" id="contact">
|
||||||
<div className="container mx-auto px-6">
|
<div className="container mx-auto px-6">
|
||||||
<div className="max-w-6xl mx-auto bg-white dark:bg-zinc-900 rounded-[4rem] overflow-hidden shadow-[0_50px_100px_-20px_rgba(0,0,0,0.15)] flex flex-col md:flex-row border border-slate-100 dark:border-white/5">
|
<Reveal className="max-w-6xl mx-auto bg-white dark:bg-zinc-900 rounded-[4rem] overflow-hidden shadow-[0_50px_100px_-20px_rgba(0,0,0,0.15)] flex flex-col md:flex-row border border-slate-100 dark:border-white/5">
|
||||||
{/* Sidebar (Teal) */}
|
{/* Sidebar (Teal) */}
|
||||||
<div className="md:w-[40%] bg-primary p-16 text-white flex flex-col relative overflow-hidden">
|
<div className="md:w-[40%] bg-primary p-16 text-white flex flex-col relative overflow-hidden">
|
||||||
<div className="relative z-10 flex-grow">
|
<div className="relative z-10 flex-grow">
|
||||||
@ -456,13 +568,35 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
{/* Form Area (White/Right) */}
|
{/* Form Area (White/Right) */}
|
||||||
<div className="md:w-[60%] p-16 bg-[#F9FAFB] dark:bg-zinc-800/20">
|
<div className="md:w-[60%] p-16 bg-[#F9FAFB] dark:bg-zinc-800/20">
|
||||||
<form className="space-y-12">
|
{formStatus === 'success' ? (
|
||||||
|
<div className="h-full flex flex-col items-center justify-center text-center py-12">
|
||||||
|
<div className="w-16 h-16 bg-primary/20 text-primary rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="material-symbols-outlined text-4xl">task_alt</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-black text-slate-900 dark:text-white mb-3">Message envoyé !</h3>
|
||||||
|
<p className="text-slate-500 mb-8">On revient vers vous très vite.</p>
|
||||||
|
<button onClick={() => setFormStatus('idle')} className="text-primary font-bold hover:underline">
|
||||||
|
Envoyer un autre message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-12">
|
||||||
|
{formStatus === 'error' && (
|
||||||
|
<div className="p-4 bg-red-50 text-red-600 rounded-xl text-sm font-medium border border-red-100 flex items-center gap-2">
|
||||||
|
<span className="material-symbols-outlined text-sm">error</span>
|
||||||
|
Une erreur est survenue. Réessayez ou contactez-nous par email.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<label className="text-xs font-black uppercase tracking-widest text-[#2D3748] dark:text-white/60">
|
<label className="text-xs font-black uppercase tracking-widest text-[#2D3748] dark:text-white/60">
|
||||||
Nom
|
Nom
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
name="nom"
|
||||||
|
required
|
||||||
|
value={formData.nom}
|
||||||
|
onChange={handleChange}
|
||||||
className="w-full bg-white dark:bg-white/5 border-none rounded-2xl p-5 shadow-sm focus:ring-4 focus:ring-primary/10 transition-all outline-none text-[#1A202C] dark:text-white"
|
className="w-full bg-white dark:bg-white/5 border-none rounded-2xl p-5 shadow-sm focus:ring-4 focus:ring-primary/10 transition-all outline-none text-[#1A202C] dark:text-white"
|
||||||
placeholder="John Doe"
|
placeholder="John Doe"
|
||||||
type="text"
|
type="text"
|
||||||
@ -473,6 +607,10 @@ export default function Home() {
|
|||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
className="w-full bg-white dark:bg-white/5 border-none rounded-2xl p-5 shadow-sm focus:ring-4 focus:ring-primary/10 transition-all outline-none text-[#1A202C] dark:text-white"
|
className="w-full bg-white dark:bg-white/5 border-none rounded-2xl p-5 shadow-sm focus:ring-4 focus:ring-primary/10 transition-all outline-none text-[#1A202C] dark:text-white"
|
||||||
placeholder="john@example.com"
|
placeholder="john@example.com"
|
||||||
type="email"
|
type="email"
|
||||||
@ -484,7 +622,12 @@ export default function Home() {
|
|||||||
Service
|
Service
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<select className="w-full bg-white dark:bg-white/5 border-none rounded-2xl p-5 shadow-sm focus:ring-4 focus:ring-primary/10 transition-all outline-none appearance-none cursor-pointer text-[#1A202C] dark:text-white font-medium">
|
<select
|
||||||
|
name="service"
|
||||||
|
value={formData.service}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full bg-white dark:bg-white/5 border-none rounded-2xl p-5 shadow-sm focus:ring-4 focus:ring-primary/10 transition-all outline-none appearance-none cursor-pointer text-[#1A202C] dark:text-white font-medium"
|
||||||
|
>
|
||||||
<option className="text-[#1A202C] bg-white">Software Development</option>
|
<option className="text-[#1A202C] bg-white">Software Development</option>
|
||||||
<option className="text-[#1A202C] bg-white">AI Integration</option>
|
<option className="text-[#1A202C] bg-white">AI Integration</option>
|
||||||
<option className="text-[#1A202C] bg-white">3D Modeling & Printing</option>
|
<option className="text-[#1A202C] bg-white">3D Modeling & Printing</option>
|
||||||
@ -500,17 +643,31 @@ export default function Home() {
|
|||||||
Message
|
Message
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
name="message"
|
||||||
|
required
|
||||||
|
value={formData.message}
|
||||||
|
onChange={handleChange}
|
||||||
className="w-full bg-white dark:bg-white/5 border-none rounded-2xl p-5 shadow-sm focus:ring-4 focus:ring-primary/10 transition-all outline-none resize-none text-[#1A202C] dark:text-white"
|
className="w-full bg-white dark:bg-white/5 border-none rounded-2xl p-5 shadow-sm focus:ring-4 focus:ring-primary/10 transition-all outline-none resize-none text-[#1A202C] dark:text-white"
|
||||||
placeholder="Tell us about your project..."
|
placeholder="Tell us about your project..."
|
||||||
rows={6}
|
rows={6}
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button className="w-full bg-primary text-white py-6 rounded-2xl font-black text-xl shadow-xl shadow-primary/20 hover:brightness-110 active:scale-[0.98] transition-all">
|
<button
|
||||||
Envoyer la demande
|
type="submit"
|
||||||
|
disabled={formStatus === 'loading'}
|
||||||
|
className="w-full bg-primary text-white py-6 rounded-2xl font-black text-xl shadow-xl shadow-primary/20 hover:brightness-110 active:scale-[0.98] transition-all disabled:opacity-70 flex items-center justify-center gap-3"
|
||||||
|
>
|
||||||
|
{formStatus === 'loading' ? (
|
||||||
|
<>
|
||||||
|
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></span>
|
||||||
|
Envoi en cours...
|
||||||
|
</>
|
||||||
|
) : 'Envoyer la demande'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,37 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { resolveImage } from "@/data/stitch-images";
|
import { resolveImage } from "@/data/stitch-images";
|
||||||
import { useState, useMemo } from "react";
|
import React, { useState, useMemo } from "react";
|
||||||
|
|
||||||
|
function Reveal({ children, delay = 0, className = '' }: { children: React.ReactNode; delay?: number; className?: string }) {
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
const [visible, setVisible] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => { if (entry.isIntersecting) setVisible(true); },
|
||||||
|
{ threshold: 0.12 }
|
||||||
|
);
|
||||||
|
observer.observe(el);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
opacity: visible ? 1 : 0,
|
||||||
|
transform: visible ? 'translateY(0)' : 'translateY(22px)',
|
||||||
|
transition: `opacity 0.55s ease-out ${delay}ms, transform 0.55s ease-out ${delay}ms`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function PortfolioPage() {
|
export default function PortfolioPage() {
|
||||||
const [activeCategory, setActiveCategory] = useState("All");
|
const [activeCategory, setActiveCategory] = useState("All");
|
||||||
@ -114,6 +144,7 @@ export default function PortfolioPage() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main className="pt-32 pb-20 px-6 max-w-7xl mx-auto flex-grow w-full">
|
<main className="pt-32 pb-20 px-6 max-w-7xl mx-auto flex-grow w-full">
|
||||||
|
<Reveal>
|
||||||
<header className="mb-20">
|
<header className="mb-20">
|
||||||
<h1 className="text-5xl md:text-7xl font-black mb-8 text-slate-900 dark:text-white">
|
<h1 className="text-5xl md:text-7xl font-black mb-8 text-slate-900 dark:text-white">
|
||||||
Nos <span className="text-primary italic">Réalisations</span>
|
Nos <span className="text-primary italic">Réalisations</span>
|
||||||
@ -130,21 +161,23 @@ export default function PortfolioPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
</Reveal>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-12">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
{filteredProjects.length > 0 ? (
|
{filteredProjects.length > 0 ? (
|
||||||
filteredProjects.map((p, i) => (
|
filteredProjects.map((p, i) => (
|
||||||
<div key={i} className="group bg-white dark:bg-slate-900 rounded-[3rem] overflow-hidden shadow-2xl border border-slate-100 dark:border-slate-800 hover:scale-[1.02] transition-all duration-500">
|
<Reveal key={i} delay={i * 80}>
|
||||||
<div className="relative h-72 overflow-hidden">
|
<div className="group bg-white dark:bg-slate-900 rounded-[3rem] overflow-hidden shadow-2xl border border-slate-100 dark:border-slate-800 hover:scale-[1.02] transition-all duration-500">
|
||||||
|
<div className="relative h-56 overflow-hidden">
|
||||||
<Image src={p.img} alt={p.title} fill className="object-cover transition-transform duration-700 group-hover:scale-110" />
|
<Image src={p.img} alt={p.title} fill className="object-cover transition-transform duration-700 group-hover:scale-110" />
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||||
</div>
|
</div>
|
||||||
<div className="p-12">
|
<div className="p-8">
|
||||||
<span className="text-primary font-black text-xs uppercase tracking-[0.2em] mb-4 block">
|
<span className="text-primary font-black text-xs uppercase tracking-[0.2em] mb-3 block">
|
||||||
{p.categories.join(" / ")}
|
{p.categories.join(" / ")}
|
||||||
</span>
|
</span>
|
||||||
<h2 className="text-3xl font-black mb-6 text-slate-900 dark:text-white leading-tight">{p.title}</h2>
|
<h2 className="text-2xl font-black mb-4 text-slate-900 dark:text-white leading-tight">{p.title}</h2>
|
||||||
<p className="text-slate-500 dark:text-slate-400 mb-10 leading-relaxed text-sm">{p.desc}</p>
|
<p className="text-slate-500 dark:text-slate-400 mb-6 leading-relaxed text-sm">{p.desc}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedProject(p);
|
setSelectedProject(p);
|
||||||
@ -157,6 +190,7 @@ export default function PortfolioPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Reveal>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="col-span-full py-20 text-center">
|
<div className="col-span-full py-20 text-center">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user