diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..538556f --- /dev/null +++ b/CLAUDE.md @@ -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 +``` diff --git a/src/app/page.tsx b/src/app/page.tsx index c9b4477..c7881f3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,34 +1,109 @@ "use client"; import Image from "next/image"; -import { useState, useEffect } from "react"; +import React, { useState, useEffect } from "react"; import { resolveImage } from "@/data/stitch-images"; +function Reveal({ children, delay = 0, className = '' }: { children: React.ReactNode; delay?: number; className?: string }) { + const ref = React.useRef(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 ( +
+ {children} +
+ ); +} + export default function Home() { const [isScrolled, setIsScrolled] = useState(false); - + const [navDark, setNavDark] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); const [selectedProject, setSelectedProject] = useState(null); const [showAllProjects, setShowAllProjects] = useState(false); 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(() => { const handleScroll = () => { 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); 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) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + return (
{/* Navigation */} + {/* Mobile Menu Backdrop */} +
setIsMenuOpen(false)} + /> + + {/* Mobile Menu Panel */} +
+
+
+ +
+ +
+
+ {/* Hero Section */}
@@ -121,9 +237,9 @@ export default function Home() {
{/* Services Section */} -
+
-
+

Expertise @@ -136,7 +252,7 @@ export default function Home() { Des solutions sur-mesure conçues avec une rigueur mathématique et une vision centrée utilisateur.

-

+
{/* Service Cards */} @@ -160,8 +276,8 @@ export default function Home() { img: resolveImage("SERVICE_3D"), }, ].map((service, idx) => ( +
@@ -183,6 +299,7 @@ export default function Home() { />
+
))}
@@ -190,41 +307,35 @@ export default function Home() { {/* Technologies Marquee - UPDATED LIST */}
-
- {[...Array(2)].map((_, i) => ( -
- {[ - { name: "Python", icon: "code" }, - { name: "TypeScript", icon: "terminal" }, - { name: "OpenAI", icon: "smart_toy" }, - { name: "Flutter", icon: "mobile_friendly" }, - { name: "C#", icon: "integration_instructions" }, - { name: "ASP.NET", icon: "settings_ethernet" }, - { name: "Angular", icon: "change_history" }, - { name: "IoT", icon: "sensors" }, - ].map((tech, j) => ( - - - {tech.icon} - {" "} - {tech.name} - - ))} -
- ))} +
+ {[1, 2].flatMap((copy) => + [ + { name: "Python", icon: "code" }, + { name: "TypeScript", icon: "terminal" }, + { name: "OpenAI", icon: "smart_toy" }, + { name: "Flutter", icon: "mobile_friendly" }, + { name: "C#", icon: "integration_instructions" }, + { name: "ASP.NET", icon: "settings_ethernet" }, + { name: "Angular", icon: "change_history" }, + { name: "IoT", icon: "sensors" }, + ].map((tech, j) => [ + + {tech.icon} + {tech.name} + , + ·, + ]) + )}
{/* Portfolio Section */} -
+
-
+

Réalisations

@@ -232,7 +343,7 @@ export default function Home() { Portfolio Sélectionné
-
+ {(() => { const allProjects = [ @@ -323,11 +434,11 @@ export default function Home() { return (
{/* Projects Grid */} -
+
{firstProjects.map((project, idx) => ( +
{ setSelectedProject(project); setCurrentImageIndex(0); @@ -358,20 +469,21 @@ export default function Home() {
+ ))}
{/* Professional Reveal Zone */}
-
+
{extraProjects.map((project, idx) => (
{ setSelectedProject(project); setCurrentImageIndex(0); @@ -425,9 +537,9 @@ export default function Home() {
{/* Contact Form Section - REVISED LAYOUT to match Image */} -
+
-
+ {/* Sidebar (Teal) */}
@@ -456,61 +568,106 @@ export default function Home() {
{/* Form Area (White/Right) */}
-
-
-
- - + {formStatus === 'success' ? ( +
+
+ task_alt +
+

Message envoyé !

+

On revient vers vous très vite.

+ +
+ ) : ( + + {formStatus === 'error' && ( +
+ error + Une erreur est survenue. Réessayez ou contactez-nous par email. +
+ )} +
+
+ + +
+
+ + +
- +
+ + + expand_more + +
-
-
- -
- - - expand_more - +
+ +
-
-
- - -
- - + + + )}
-
+
diff --git a/src/app/portfolio/page.tsx b/src/app/portfolio/page.tsx index 82f0e06..23d1958 100644 --- a/src/app/portfolio/page.tsx +++ b/src/app/portfolio/page.tsx @@ -3,7 +3,37 @@ import Image from "next/image"; import Link from "next/link"; 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(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 ( +
+ {children} +
+ ); +} export default function PortfolioPage() { const [activeCategory, setActiveCategory] = useState("All"); @@ -114,6 +144,7 @@ export default function PortfolioPage() {
+

Nos Réalisations @@ -130,21 +161,23 @@ export default function PortfolioPage() { ))}

+ -
+
{filteredProjects.length > 0 ? ( filteredProjects.map((p, i) => ( -
-
+ +
+
{p.title}
-
- +
+ {p.categories.join(" / ")} -

{p.title}

-

{p.desc}

+

{p.title}

+

{p.desc}

+ )) ) : (