wip unov landing

This commit is contained in:
Thomas Fransolet 2026-02-26 21:41:19 +01:00
parent 11f4fcda1c
commit 9dd74c4853
16 changed files with 861 additions and 261 deletions

View File

@ -3,6 +3,20 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
reactCompiler: true,
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'unov.be',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
pathname: '/**',
},
],
},
};
export default nextConfig;

BIN
public/desk_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

BIN
public/desk_02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
public/desk_03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
public/desk_04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
public/desk_05.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,12 +1,11 @@
@import "tailwindcss";
@variant dark (&:is(.dark *));
@theme {
--color-primary: #309CB0;
--color-primary-light: #72B9C4;
--color-primary-lighter: #ABD3DC;
--color-background-light: #F9F6F9;
--color-background-dark: #221610;
--font-display: "Public Sans", sans-serif;
@keyframes marquee {
@ -20,24 +19,28 @@
}
}
:root {
--background: #F9F6F9;
--foreground: #0f172a;
--primary: #309CB0;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #221610;
--foreground: #f8fafc;
@layer base {
body {
background-color: #FFFFFF;
color: #0f172a;
font-family: var(--font-display);
@apply antialiased;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: var(--font-display);
transition: background-color 0.3s ease, color 0.3s ease;
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
}
.animate-marquee {

BIN
src/app/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -23,7 +23,7 @@ export default function RootLayout({
<head>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght@100..700,0..1&display=swap"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,0&display=swap"
/>
</head>
<body

View File

@ -2,10 +2,15 @@
import Image from "next/image";
import { useState, useEffect } from "react";
import { resolveImage } from "@/data/stitch-images";
export default function Home() {
const [isScrolled, setIsScrolled] = useState(false);
const [selectedProject, setSelectedProject] = useState<any>(null);
const [showAllProjects, setShowAllProjects] = useState(false);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
@ -15,7 +20,7 @@ export default function Home() {
}, []);
return (
<div className="min-h-screen">
<div className="min-h-screen bg-white text-slate-900">
{/* Navigation */}
<nav
className={`fixed top-0 w-full z-50 transition-all duration-300 ${isScrolled
@ -26,7 +31,7 @@ export default function Home() {
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between">
<div className="flex items-center gap-3">
<Image
src="/unov.png"
src={resolveImage("{{DATA:IMAGE:IMAGE_2}}")}
alt="UNOV Logo"
width={120}
height={40}
@ -36,24 +41,27 @@ export default function Home() {
<div className="hidden md:flex items-center gap-10">
<a
href="#services"
className="text-sm font-medium hover:text-primary transition-colors"
className="text-sm font-medium hover:text-primary transition-colors uppercase tracking-widest"
>
Services
</a>
<a
href="#portfolio"
className="text-sm font-medium hover:text-primary transition-colors"
className="text-sm font-medium hover:text-primary transition-colors uppercase tracking-widest"
>
Portfolio
</a>
<a
href="#technologies"
className="text-sm font-medium hover:text-primary transition-colors"
className="text-sm font-medium hover:text-primary transition-colors uppercase tracking-widest"
>
Technologies
</a>
<button className="bg-primary hover:bg-primary-light text-white px-6 py-2.5 rounded-lg text-sm font-bold transition-all shadow-lg shadow-primary/20 hover:scale-105 active:scale-95">
Let's Build
<button
onClick={() => document.getElementById("contact")?.scrollIntoView({ behavior: "smooth" })}
className="bg-primary hover:bg-primary-light text-white px-6 py-2.5 rounded-lg text-sm font-bold transition-all shadow-lg shadow-primary/20 hover:scale-105 active:scale-95"
>
Let's Talk
</button>
</div>
<button className="md:hidden">
@ -66,65 +74,67 @@ export default function Home() {
<section className="relative h-screen flex items-center pt-20 overflow-hidden">
<div className="absolute inset-0 z-0">
<div className="absolute inset-0 bg-black/40 z-10"></div>
{/* Using a placeholder high-quality image URL as fallback,
but since generate_image failed, I'll use a reliable source */}
<img
src="https://images.unsplash.com/photo-1550751827-4bd374c3f58b?q=80&w=2070&auto=format&fit=crop"
src={resolveImage("{{DATA:IMAGE:IMAGE_6}}")}
alt="Hero Background"
className="w-full h-full object-cover opacity-80 dark:opacity-40"
/>
</div>
<div className="container mx-auto px-6 relative z-20">
<div className="max-w-4xl mx-auto text-center flex flex-col items-center">
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full text-primary text-xs font-bold uppercase tracking-widest mb-8 bg-primary/10 backdrop-blur-sm border border-primary/20">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
</span>
Tech Precision Agency
</div>
<h1 className="text-5xl md:text-8xl font-black leading-[1.05] mb-8 tracking-tight text-white drop-shadow-2xl">
L'innovation au service de{" "}
<span className="text-primary italic">l'utilisateur</span>
<div className="max-w-4xl flex flex-col items-start">
<h1 className="text-4xl md:text-7xl font-black leading-[1.1] mb-8 tracking-tight text-white drop-shadow-2xl">
L'innovation au service <br className="hidden md:block" />
de <span className="text-primary italic">l'utilisateur</span>
</h1>
<p className="text-lg md:text-2xl mb-12 leading-relaxed max-w-2xl text-white/90 drop-shadow-lg font-light">
<p className="text-lg md:text-2xl mb-12 leading-relaxed max-w-2xl text-white/80 drop-shadow-lg font-light">
Développement web, mobile, desktop & embarqué, intégration IA,
modélisation et impression 3D.
</p>
<div className="flex flex-col sm:flex-row gap-6 justify-center">
<button className="bg-primary text-white px-10 py-4 rounded-xl text-lg font-bold hover:brightness-110 hover:scale-[1.02] active:scale-95 transition-all flex items-center justify-center gap-2 shadow-xl shadow-primary/30">
<div className="flex flex-col sm:flex-row gap-4">
<button
onClick={() => document.getElementById("contact")?.scrollIntoView({ behavior: "smooth" })}
className="bg-primary text-white px-10 py-4 rounded-xl text-lg font-bold hover:brightness-110 hover:scale-[1.02] active:scale-95 transition-all flex items-center justify-center gap-2 shadow-xl shadow-primary/30"
>
Start a Project{" "}
<span className="material-symbols-outlined">arrow_forward</span>
<span className="material-symbols-outlined font-light">arrow_forward</span>
</button>
<button className="bg-white/10 backdrop-blur-md text-white border border-white/20 px-10 py-4 rounded-xl text-lg font-bold hover:bg-white/20 transition-all">
<button
onClick={() => document.getElementById("portfolio")?.scrollIntoView({ behavior: "smooth" })}
className="bg-slate-500/30 backdrop-blur-md text-white border border-white/20 px-10 py-4 rounded-xl text-lg font-bold hover:bg-slate-500/40 transition-all font-display shadow-lg"
>
View Portfolio
</button>
</div>
</div>
</div>
{/* Subtle Scroll Indicator */}
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 animate-bounce opacity-70">
<span className="material-symbols-outlined text-white text-4xl">
expand_more
</span>
</div>
<button
onClick={() => document.getElementById("services")?.scrollIntoView({ behavior: "smooth" })}
className="absolute bottom-10 left-1/2 -translate-x-1/2 animate-bounce opacity-70 hover:opacity-100 transition-opacity cursor-pointer"
>
<div className="border border-white/30 rounded-full size-12 flex items-center justify-center backdrop-blur-sm">
<span className="material-symbols-outlined text-white text-2xl">
expand_more
</span>
</div>
</button>
</section>
{/* Services Section */}
<section className="bg-background-light dark:bg-background-dark/50 py-32" id="services">
<section className="bg-white dark:bg-background-dark/50 py-32" id="services">
<div className="container mx-auto px-6">
<div className="flex flex-col md:flex-row justify-between items-end mb-20 gap-8">
<div className="flex flex-col md:flex-row justify-between items-start mb-20 gap-8">
<div className="max-w-xl">
<h2 className="text-primary font-bold uppercase tracking-widest text-sm mb-4">
<h2 className="text-primary font-bold uppercase tracking-widest text-xs mb-4">
Expertise
</h2>
<h3 className="text-4xl md:text-6xl font-black leading-tight text-slate-900 dark:text-white">
Nos Services Experts
</h3>
</div>
<p className="text-slate-500 dark:text-slate-400 max-w-sm mb-2 text-lg">
<p className="text-slate-500 dark:text-slate-400 max-w-sm mt-8 text-sm leading-relaxed">
Des solutions sur-mesure conçues avec une rigueur mathématique et
une vision artistique.
une vision centrée utilisateur.
</p>
</div>
@ -133,26 +143,26 @@ export default function Home() {
{[
{
title: "Software Development",
desc: "Architectures évolutives et robustes. Nous transformons des concepts complexes en applications fluides.",
icon: "code_blocks",
img: "https://images.unsplash.com/photo-1517694712202-14dd9538aa97?q=80&w=2070&auto=format&fit=crop",
desc: "Architectures évolutives et robustes. Nous transformons des concepts complexes en applications fluides et performantes.",
icon: "code",
img: resolveImage("SERVICE_SOFTWARE"),
},
{
title: "AI Integration",
desc: "Automatisation intelligente et machine learning avancé pour propulser votre efficacité opérationnelle.",
icon: "psychology",
img: "https://images.unsplash.com/photo-1677442136019-21780ecad995?q=80&w=2070&auto=format&fit=crop",
img: resolveImage("SERVICE_AI"),
},
{
title: "3D & Printing",
desc: "De la conception virtuelle à la réalité physique. Prototypage rapide et ingénierie de précision.",
icon: "precision_manufacturing",
img: "https://images.unsplash.com/photo-1581092160562-40aa08e78837?q=80&w=2070&auto=format&fit=crop",
desc: "De la conception virtuelle à la réalité physique. Prototypage rapide et ingénierie de précision pour vos projets.",
icon: "view_in_ar",
img: resolveImage("SERVICE_3D"),
},
].map((service, idx) => (
<div
key={idx}
className="group bg-white dark:bg-white/5 rounded-[2.5rem] p-10 shadow-sm 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">
<span className="material-symbols-outlined text-primary group-hover:text-white text-3xl transition-colors duration-500">
@ -162,10 +172,10 @@ export default function Home() {
<h4 className="text-2xl font-bold mb-4 dark:text-white">
{service.title}
</h4>
<p className="text-slate-500 dark:text-slate-400 leading-relaxed mb-8">
<p className="text-slate-500 dark:text-slate-400 leading-relaxed text-sm mb-8">
{service.desc}
</p>
<div className="overflow-hidden rounded-2xl aspect-video grayscale group-hover:grayscale-0 transition-all duration-700">
<div className="overflow-hidden rounded-2xl aspect-[16/10] grayscale group-hover:grayscale-0 transition-all duration-700">
<img
src={service.img}
alt={service.title}
@ -178,26 +188,27 @@ export default function Home() {
</div>
</section>
{/* Technologies Marquee */}
{/* Technologies Marquee - UPDATED LIST */}
<section
className="py-20 border-y border-slate-200 dark:border-white/5 overflow-hidden bg-white dark:bg-background-dark"
className="py-24 border-y border-slate-100 dark:border-white/5 overflow-hidden bg-white dark:bg-background-dark"
id="technologies"
>
<div className="flex whitespace-nowrap animate-marquee">
{[...Array(2)].map((_, i) => (
<div key={i} className="flex items-center gap-24 px-12">
{[
{ name: "React", icon: "data_object" },
{ name: "Python", icon: "terminal" },
{ name: "AWS", icon: "cloud" },
{ name: "Python", icon: "code" },
{ name: "TypeScript", icon: "terminal" },
{ name: "OpenAI", icon: "smart_toy" },
{ name: "Three.js", icon: "view_in_ar" },
{ name: "TypeScript", icon: "layers" },
{ name: "Node.js", icon: "settings_input_component" },
{ 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) => (
<span
key={j}
className="text-3xl font-black text-slate-300 dark:text-slate-700 uppercase tracking-tighter flex items-center gap-3 hover:text-primary transition-colors cursor-default"
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}
@ -223,214 +234,459 @@ export default function Home() {
<div className="w-24 h-1.5 mx-auto bg-primary-light rounded-full"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-16">
{[
{(() => {
const allProjects = [
{
title: "Solaris Ecosystem",
category: "Software",
desc: "Une plateforme IoT complète pour la gestion d'énergie renouvelable.",
img: "https://images.unsplash.com/photo-1497435334941-8c899ee9e8e9?q=80&w=1974&auto=format&fit=crop",
title: "MyInfoMate",
categories: ["Web", "Mobile"],
desc: "Solution SaaS pour la digitalisation des musées et sites culturels.",
img: resolveImage("PORTFOLIO_MYINFOMATE"),
fullDesc: "MyInfoMate est une plateforme innovante permettant aux institutions culturelles de créer, gérer et diffuser du contenu interactif sans connaissances techniques. Cartes interactives, guides multimédias et jeux de piste sont gérés via un CMS intuitif centralisé.",
link: "https://myinfomate.be",
images: [
resolveImage("PORTFOLIO_MYINFOMATE"),
"https://images.unsplash.com/photo-1542744173-8e7e53415bb0?q=80&w=2070",
"https://images.unsplash.com/photo-1551434678-e076c223a692?q=80&w=2070"
],
offset: false,
},
{
title: "InsightCore AI",
category: "Artificial Intelligence",
desc: "Analyse prédictive en temps réel pour le secteur de la finance.",
img: "https://images.unsplash.com/photo-1551288049-bb6c9b468a79?q=80&w=2070&auto=format&fit=crop",
title: "Office du Tourisme de Namur",
categories: ["Web", "Mobile"],
desc: "Interface tactile interactive sur grande tablette pour l'accueil touristique.",
img: resolveImage("PORTFOLIO_NAMUR"),
fullDesc: "Déploiement de la technologie MyInfoMate sur les bornes interactives de l'Office du Tourisme de Namur. Cette installation permet aux visiteurs d'explorer la ville de manière tactile, avec des points d'intérêt géo-localisés et une planification d'itinéraire simplifiée.",
images: [
resolveImage("PORTFOLIO_NAMUR"),
"https://images.unsplash.com/photo-1517048676732-d65bc937f952?q=80&w=2070",
"https://images.unsplash.com/photo-1531403001884-48a690c749ea?q=80&w=2070"
],
offset: true,
},
{
title: "AeroPro Modules",
category: "3D Engineering",
desc: "Composants imprimés en 3D haute performance pour l'industrie aéronautique.",
img: "https://images.unsplash.com/photo-1581092580497-e0d23cbdf1dc?q=80&w=2070&auto=format&fit=crop",
title: "Carnaval de Marche",
categories: ["Impression 3D"],
desc: "Design et production de boucles d'oreilles personnalisées pour l'événement.",
img: resolveImage("PORTFOLIO_MARCHE"),
fullDesc: "Nous avons designé, imprimé et assemblé des boucles d'oreilles pour le Carnaval de Marche. Plus d'une centaine de paires ont été produites, démontrant notre capacité de prototypage et de production en petite série via l'impression 3D.",
images: [
resolveImage("PORTFOLIO_MARCHE"),
"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1964",
"https://images.unsplash.com/photo-1562408590-e32931084e23?q=80&w=2070"
],
offset: false,
},
{
title: "Vortex Dashboard",
category: "Cloud Services",
desc: "Interface de gestion cloud pour équipes distribuées.",
img: "https://images.unsplash.com/photo-1551434678-e076c223a692?q=80&w=2070&auto=format&fit=crop",
title: "Fort de Saint-Héribert",
categories: ["Mobile", "Embedded", "Web"],
desc: "Application mobile immersive pour la visite du fort via QR codes et localisation. Finalisée en mai 2023.",
img: resolveImage("PORTFOLIO_HERIBERT"),
fullDesc: "Notre réalisation pour le Fort de Saint-Héribert consiste en une application mobile dédiée à la visite du fort. Cette application offre une expérience immersive, où le contenu peut être consulté soit en scannant un QR code, soit en activant l'affichage de contenu basé sur la localisation du visiteur. La détection de proximité permet une interaction fluide avec le patrimoine.",
images: [
resolveImage("PORTFOLIO_HERIBERT"),
"https://images.unsplash.com/photo-1533106418989-88406c7cc8ca?q=80&w=2070",
"https://images.unsplash.com/photo-1496307653780-42ee777d4833?q=80&w=2070"
],
offset: true,
},
].map((project, idx) => (
<div
key={idx}
className={`group relative overflow-hidden rounded-[3rem] aspect-[4/3] cursor-pointer shadow-2xl ${project.offset ? "md:translate-y-20" : ""
}`}
>
<img
src={project.img}
alt={project.title}
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 flex flex-col justify-end p-12">
<span className="text-primary font-bold text-sm mb-3 uppercase tracking-widest">
{project.category}
</span>
<h4 className="text-white text-4xl font-bold mb-4">
{project.title}
</h4>
<p className="text-slate-300 text-lg mb-8 max-w-sm">
{project.desc}
{
title: "Musée de la fraise",
categories: ["Desktop", "Mobile"],
desc: "Tablettes interactives et gestionnaire de contenu pour enrichir l'expérience musée.",
img: resolveImage("PORTFOLIO_FRAISE"),
fullDesc: "Le projet pour le musée de la fraise de Wépion avait pour objectif d'intégrer des tablettes interactives. Nous avons créé un gestionnaire dédié (MyMuseum) permettant au personnel de personnaliser le contenu, ainsi que l'application mobile chargée sur les tablettes pour la consultation des visiteurs.",
images: [
resolveImage("PORTFOLIO_FRAISE"),
"https://images.unsplash.com/photo-1581291518062-c07a09ea0937?q=80&w=2070",
"https://images.unsplash.com/photo-1558655146-d09347e92766?q=80&w=1964"
],
offset: false,
},
{
title: "Houblionnière de la Chistrée",
categories: ["Web"],
desc: "Site vitrine et boutique en ligne pour investir dans une coopérative locale.",
img: resolveImage("PORTFOLIO_CHISTREE"),
fullDesc: "La coopérative souhaitait un site internet vitrine permettant aux visiteurs d'investir dans ce beau projet via une petite boutique en ligne intégrée. Une solution Web complète alliant esthétique et fonctionnalité e-commerce.",
images: [
resolveImage("PORTFOLIO_CHISTREE"),
"https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?q=80&w=2070",
"https://images.unsplash.com/photo-1460925895917-afdab827c52f?q=80&w=2015"
],
offset: true,
},
];
const initialProjects = allProjects.slice(0, 4);
const extraProjects = allProjects.slice(4);
return (
<div className="flex flex-col gap-16 relative">
{/* Initial 4 projects */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-16">
{initialProjects.map((project, idx) => (
<div
key={idx}
className={`group relative overflow-hidden rounded-[3rem] aspect-[4/3] cursor-pointer shadow-2xl ${project.offset ? "md:translate-y-20" : ""
}`}
>
<img
src={project.img}
alt={project.title}
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 flex flex-col justify-end p-12">
<div className="flex flex-col">
<span className="text-primary font-bold text-xs uppercase tracking-widest mb-1">
{project.categories.join(" / ")}
</span>
<h3 className="text-white text-2xl font-black mb-4">
{project.title}
</h3>
</div>
<p className="text-white/60 mb-8 text-sm leading-relaxed line-clamp-2">
{project.desc}
</p>
<button
onClick={() => {
setSelectedProject(project);
setCurrentImageIndex(0);
}}
className="group/btn flex items-center gap-2 text-white font-bold text-sm tracking-widest uppercase"
>
Voir plus
<span className="material-symbols-outlined text-sm group-hover/btn:translate-x-1 transition-transform">
arrow_forward
</span>
</button>
</div>
</div>
))}
</div>
{(() => {
const extraProjects = allProjects.slice(4, 12); // Adjusted slice for extra projects
return (
<div className="relative mt-32 flex flex-col items-center">
{/* Liquid Reveal Projects Container */}
<div
className={`relative w-full transition-all duration-[2000ms] ease-[cubic-bezier(0.85,0,0.15,1)] origin-top overflow-hidden
${showAllProjects ? "max-h-[5000px] opacity-100 pb-64" : "max-h-0 opacity-0"}`}
>
{/* Branded Expansion Background */}
<div className="absolute inset-0 -mx-[50vw] left-1/2 w-[200vw] h-full bg-zinc-50 dark:bg-white/5 pointer-events-none" />
<div className="absolute inset-0 -mx-[50vw] left-1/2 w-[200vw] h-full pointer-events-none opacity-50"
style={{ background: 'radial-gradient(circle at 50% 0%, var(--primary), transparent 70%)' }} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 relative z-10 container mx-auto px-6 pt-32">
{extraProjects.map((project, idx) => (
<div
key={idx + 4}
className={`group relative overflow-hidden rounded-[3rem] aspect-[4/3] cursor-pointer shadow-2xl ${project.offset ? "md:translate-y-20" : ""
}`}
>
<img
src={project.img}
alt={project.title}
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 flex flex-col justify-end p-12">
<div className="flex flex-col">
<span className="text-primary font-bold text-xs uppercase tracking-widest mb-1">
{project.categories.join(" / ")}
</span>
<h3 className="text-white text-2xl font-black mb-4">
{project.title}
</h3>
</div>
<p className="text-white/60 mb-8 text-sm leading-relaxed line-clamp-2">
{project.desc}
</p>
<button
onClick={() => {
setSelectedProject(project);
setCurrentImageIndex(0);
}}
className="group/btn flex items-center gap-2 text-white font-bold text-sm tracking-widest uppercase"
>
Voir plus
<span className="material-symbols-outlined text-sm group-hover/btn:translate-x-1 transition-transform">
arrow_forward
</span>
</button>
</div>
</div>
))}
</div>
</div>
{/* Surface & Button (Shown when not expanded) */}
{!showAllProjects && (
<div className="w-full flex flex-col items-center">
{/* The Liquid Base - High Visibility Curve */}
<div className="w-screen -mx-[50vw] left-1/2 relative h-80 overflow-visible pointer-events-none -mb-40">
{/* Deep Curved Glow */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-screen h-[400px] bg-primary/20 blur-[120px] rounded-full opacity-60" />
<svg viewBox="0 0 1440 200" preserveAspectRatio="none" className="w-full h-40 absolute top-0 fill-primary/10 stroke-primary/20 stroke-2">
<path d="M0,200 Q720,0 1440,200 Z" />
</svg>
</div>
<button
onClick={() => setShowAllProjects(true)}
className="group relative inline-flex items-center gap-8 px-14 py-7 bg-[#0A0F1C] text-white rounded-[4rem] font-black text-lg tracking-[0.2em] uppercase transition-all duration-500 hover:scale-105 hover:shadow-[0_20px_60px_-15px_rgba(30,174,188,0.5)] z-20"
>
<div className="flex flex-col items-start text-left">
<span className="text-[10px] text-primary mb-1 tracking-[0.4em]">Découverte</span>
<span className="relative z-10">Découvrir plus de projets</span>
</div>
<div className="relative z-10 w-14 h-14 rounded-2xl bg-gradient-to-br from-primary to-primary-hover flex items-center justify-center group-hover:translate-y-2 transition-all duration-500 shadow-lg shadow-primary/20">
<span className="material-symbols-outlined text-white text-3xl font-bold">expand_more</span>
</div>
<div className="absolute inset-x-0 -bottom-1 h-2 bg-gradient-to-r from-transparent via-primary to-transparent opacity-0 group-hover:opacity-100 transition-opacity blur-md" />
</button>
</div>
)}
</div>
);
})()}
</div>
);
})()}
{/* Contact Form Section - REVISED LAYOUT to match Image */}
<section className="bg-white dark:bg-background-dark py-32" id="contact">
<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">
{/* Sidebar (Teal) */}
<div className="md:w-[40%] bg-primary p-16 text-white flex flex-col relative overflow-hidden">
<div className="relative z-10 flex-grow">
<h4 className="text-5xl font-black mb-10 leading-tight">
Let's talk about the future
</h4>
<p className="opacity-90 text-xl leading-relaxed mb-12 font-light max-w-xs">
Prêt à donner vie à votre vision ? Notre équipe est prête à
relever vos défis les plus complexes.
</p>
</div>
<div className="space-y-10 relative z-10 pt-12 border-t border-white/20">
<div className="flex items-center gap-6">
<div className="size-12 bg-white/10 rounded-xl flex items-center justify-center">
<span className="material-symbols-outlined text-white">mail</span>
</div>
<span className="text-lg font-medium">contact@unov.be</span>
</div>
<div className="flex items-center gap-6">
<div className="size-12 bg-white/10 rounded-xl flex items-center justify-center">
<span className="material-symbols-outlined text-white">location_on</span>
</div>
<span className="text-lg font-medium">Vedrin, Belgique</span>
</div>
</div>
</div>
{/* Form Area (White/Right) */}
<div className="md:w-[60%] p-16 bg-[#F9FAFB] dark:bg-zinc-800/20">
<form className="space-y-12">
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
<div className="space-y-4">
<label className="text-xs font-black uppercase tracking-widest text-[#2D3748] dark:text-white/60">
Nom
</label>
<input
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"
type="text"
/>
</div>
<div className="space-y-4">
<label className="text-xs font-black uppercase tracking-widest text-[#2D3748] dark:text-white/60">
Email
</label>
<input
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"
type="email"
/>
</div>
</div>
<div className="space-y-4">
<label className="text-xs font-black uppercase tracking-widest text-[#2D3748] dark:text-white/60">
Service
</label>
<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">
<option className="text-[#1A202C] bg-white">Software Development</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">Autre</option>
</select>
<span className="material-symbols-outlined absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-slate-400">
expand_more
</span>
</div>
</div>
<div className="space-y-4">
<label className="text-xs font-black uppercase tracking-widest text-[#2D3748] dark:text-white/60">
Message
</label>
<textarea
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..."
rows={6}
></textarea>
</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">
Envoyer la demande
</button>
</form>
</div>
</div>
</div>
</section>
{/* Footer - DARK for logo contrast */}
<footer className="py-24 bg-[#0A0F1C] text-white/90">
<div className="container mx-auto px-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-16">
<div className="flex flex-col items-center md:items-start gap-6">
<div className="flex items-center gap-3 underline-offset-4">
<Image
src={resolveImage("{{DATA:IMAGE:IMAGE_2}}")}
alt="UNOV Logo"
width={120}
height={35}
className="h-8 w-auto object-contain"
/>
</div>
<p className="text-slate-400 text-sm max-w-xs text-center md:text-left leading-relaxed">
Agence technologique de précision spécialisée dans les logiciels haut de gamme et les solutions d'IA. La perfection dans chaque ligne de code.
</p>
<button className="w-fit text-white flex items-center gap-2 font-bold group/btn text-lg">
View Case Study{" "}
<span className="material-symbols-outlined group-hover/btn:translate-x-2 transition-transform">
arrow_right_alt
</span>
</button>
</div>
</div>
))}
</div>
<div className="mt-48 text-center">
<button className="px-12 py-5 border-2 border-primary text-primary hover:bg-primary hover:text-white transition-all duration-300 font-bold rounded-2xl text-xl shadow-xl shadow-primary/10">
See More Excellence
</button>
</div>
</div>
</section>
{/* Contact Form Section */}
<section className="bg-white dark:bg-background-dark py-32">
<div className="container mx-auto px-6">
<div className="max-w-6xl mx-auto bg-slate-50 dark:bg-white/5 rounded-[4rem] overflow-hidden shadow-2xl flex flex-col md:row border border-slate-200/50 dark:border-white/5">
<div className="md:w-2/5 bg-primary p-16 text-white flex flex-col justify-between relative overflow-hidden">
<div className="relative z-10">
<h4 className="text-4xl font-black mb-8 leading-tight">
Let's build the future together
</h4>
<p className="opacity-90 text-xl leading-relaxed font-light">
Prêt à donner vie à votre vision ? Notre équipe est prête à
relever vos défis les plus complexes.
</p>
</div>
<div className="space-y-8 relative z-10 pt-12">
<div className="flex items-center gap-6">
<div className="size-12 bg-white/10 rounded-xl flex items-center justify-center backdrop-blur-md">
<span className="material-symbols-outlined">mail</span>
<div className="flex flex-col items-center md:items-end gap-8">
<div className="flex gap-10">
<a
href="#"
className="text-slate-400 hover:text-primary transition-all hover:-translate-y-1"
>
<span className="material-symbols-outlined text-3xl">
language
</span>
</a>
<a
href="#"
className="text-slate-400 hover:text-primary transition-all hover:-translate-y-1"
>
<span className="material-symbols-outlined text-3xl">
public
</span>
</a>
<a
href="#"
className="text-slate-400 hover:text-primary transition-all hover:-translate-y-1"
>
<span className="material-symbols-outlined text-3xl">
groups
</span>
</a>
</div>
<span className="text-lg font-medium">hello@unov.tech</span>
</div>
<div className="flex items-center gap-6">
<div className="size-12 bg-white/10 rounded-xl flex items-center justify-center backdrop-blur-md">
<span className="material-symbols-outlined">location_on</span>
<div className="text-slate-500 text-xs font-bold uppercase tracking-widest">
© {new Date().getFullYear()} UNOV. Tous droits réservés.
</div>
<span className="text-lg font-medium">Paris, France</span>
</div>
</div>
{/* Decorative circle */}
<div className="absolute -bottom-24 -right-24 size-64 bg-white/10 rounded-full blur-3xl"></div>
</div>
<div className="md:w-3/5 p-16 bg-white dark:bg-zinc-900/50">
<form className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-3">
<label className="text-xs font-bold uppercase tracking-widest opacity-60 ml-2">
Nom complet
</label>
<input
className="w-full bg-slate-100 dark:bg-white/5 border border-transparent focus:border-primary focus:bg-white dark:focus:bg-zinc-800 focus:ring-4 focus:ring-primary/10 rounded-2xl p-5 transition-all outline-none"
placeholder="John Doe"
type="text"
/>
</div>
<div className="space-y-3">
<label className="text-xs font-bold uppercase tracking-widest opacity-60 ml-2">
Email professionnel
</label>
<input
className="w-full bg-slate-100 dark:bg-white/5 border border-transparent focus:border-primary focus:bg-white dark:focus:bg-zinc-800 focus:ring-4 focus:ring-primary/10 rounded-2xl p-5 transition-all outline-none"
placeholder="john@example.com"
type="email"
/>
</div>
</div>
<div className="space-y-3">
<label className="text-xs font-bold uppercase tracking-widest opacity-60 ml-2">
Service souhaité
</label>
<select className="w-full bg-slate-100 dark:bg-white/5 border border-transparent focus:border-primary focus:bg-white dark:focus:bg-zinc-800 focus:ring-4 focus:ring-primary/10 rounded-2xl p-5 transition-all outline-none appearance-none cursor-pointer">
<option>Software Development</option>
<option>AI Integration</option>
<option>3D Modeling & Printing</option>
<option>Autre</option>
</select>
</div>
<div className="space-y-3">
<label className="text-xs font-bold uppercase tracking-widest opacity-60 ml-2">
Votre Message
</label>
<textarea
className="w-full bg-slate-100 dark:bg-white/5 border border-transparent focus:border-primary focus:bg-white dark:focus:bg-zinc-800 focus:ring-4 focus:ring-primary/10 rounded-2xl p-5 transition-all outline-none resize-none"
placeholder="Parlez-nous de votre projet..."
rows={5}
></textarea>
</div>
<button className="w-full bg-primary text-white py-6 rounded-2xl font-black text-xl shadow-2xl shadow-primary/20 hover:brightness-110 active:scale-[0.98] transition-all">
Envoyer la demande
</button>
</form>
</div>
</div>
</div>
</section>
</footer>
{/* Footer */}
<footer className="py-20 border-t border-slate-200 dark:border-white/5 bg-background-light dark:bg-background-dark">
<div className="container mx-auto px-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-12">
<div className="flex flex-col items-center md:items-start gap-4">
<div className="flex items-center gap-3">
<Image
src="/unov.png"
alt="UNOV Logo"
width={100}
height={30}
className="h-6 w-auto object-contain"
{/* Project Modal */}
{
selectedProject && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-8">
<div
className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm"
onClick={() => setSelectedProject(null)}
/>
<div className="relative bg-white dark:bg-slate-900 w-full max-w-4xl max-h-[90vh] overflow-y-auto rounded-3xl shadow-2xl flex flex-col md:flex-row overflow-hidden border border-slate-200 dark:border-slate-800 animate-in fade-in zoom-in duration-300">
<button
onClick={() => setSelectedProject(null)}
className="absolute top-6 right-6 z-20 bg-white/10 backdrop-blur-md text-slate-900 dark:text-white w-10 h-10 rounded-full flex items-center justify-center hover:bg-white/20 transition-all"
>
<span className="material-symbols-outlined text-xl">close</span>
</button>
<div className="md:w-1/2 relative h-[400px] md:h-auto overflow-hidden group/modal">
<Image
src={selectedProject.images[currentImageIndex]}
alt={selectedProject.title}
fill
className="object-cover transition-all duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/60 to-transparent pointer-events-none" />
{/* Gallery Controls */}
{selectedProject.images && selectedProject.images.length > 1 && (
<>
<div className="absolute inset-x-0 bottom-8 flex justify-center gap-3 z-10">
{selectedProject.images.map((_: any, i: number) => (
<button
key={i}
onClick={() => setCurrentImageIndex(i)}
className={`w-2.5 h-2.5 rounded-full transition-all ${i === currentImageIndex ? "bg-primary w-8" : "bg-white/40 hover:bg-white/60"}`}
/>
))}
</div>
<button
onClick={() => setCurrentImageIndex((prev) => (prev > 0 ? prev - 1 : selectedProject.images.length - 1))}
className="absolute left-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center text-white opacity-0 group-hover/modal:opacity-100 transition-all hover:bg-white/20"
>
<span className="material-symbols-outlined">chevron_left</span>
</button>
<button
onClick={() => setCurrentImageIndex((prev) => (prev < selectedProject.images.length - 1 ? prev + 1 : 0))}
className="absolute right-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center text-white opacity-0 group-hover/modal:opacity-100 transition-all hover:bg-white/20"
>
<span className="material-symbols-outlined">chevron_right</span>
</button>
</>
)}
</div>
<div className="md:w-1/2 p-8 md:p-12 flex flex-col justify-center">
<span className="text-primary font-bold uppercase tracking-[0.2em] text-xs mb-4 block">
{selectedProject.categories.join(" / ")}
</span>
<h2 className="text-3xl md:text-5xl font-black mb-8 leading-tight text-slate-900 dark:text-white">
{selectedProject.title}
</h2>
<div className="w-12 h-1 bg-primary mb-8" />
<p className="text-slate-600 dark:text-slate-400 text-lg leading-relaxed mb-10">
{selectedProject.fullDesc || selectedProject.desc}
</p>
<div className="flex justify-between items-center mt-auto">
{selectedProject.link && (
<a
href={selectedProject.link}
target="_blank"
rel="noopener noreferrer"
className="text-primary font-bold flex items-center gap-2 group/link"
>
<span className="material-symbols-outlined">language</span>
<span className="border-b-2 border-transparent group-hover/link:border-primary transition-all pb-0.5">Visiter le site</span>
</a>
)}
<div className="flex justify-end flex-grow">
<button
onClick={() => setSelectedProject(null)}
className="bg-primary hover:bg-primary-hover text-white px-10 py-4 rounded-xl font-bold transition-all w-fit shadow-xl"
>
Fermer
</button>
</div>
</div>
</div>
</div>
</div>
<p className="text-slate-500 text-sm max-w-xs text-center md:text-left">
Precision technology agency focusing on high-end software and AI
solutions.
</p>
</div>
<div className="flex flex-col items-center gap-6">
<div className="flex gap-8">
<a
href="#"
className="text-slate-400 hover:text-primary transition-all hover:scale-110"
>
<span className="material-symbols-outlined text-3xl">
language
</span>
</a>
<a
href="#"
className="text-slate-400 hover:text-primary transition-all hover:scale-110"
>
<span className="material-symbols-outlined text-3xl">
public
</span>
</a>
<a
href="#"
className="text-slate-400 hover:text-primary transition-all hover:scale-110"
>
<span className="material-symbols-outlined text-3xl">
groups
</span>
</a>
</div>
<div className="text-slate-500 text-sm font-medium">
© {new Date().getFullYear()} UNOV. Perfection in every line.
</div>
</div>
</div>
)
}
</div>
</footer>
</div>
);
);
}

302
src/app/portfolio/page.tsx Normal file
View File

@ -0,0 +1,302 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { resolveImage } from "@/data/stitch-images";
import { useState, useMemo } from "react";
export default function PortfolioPage() {
const [activeCategory, setActiveCategory] = useState("All");
const [selectedProject, setSelectedProject] = useState<any>(null);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const allProjects = [
{
title: "MyInfoMate",
categories: ["Web", "Mobile"],
desc: "Solution SaaS pour la digitalisation des musées et sites culturels.",
img: resolveImage("PORTFOLIO_MYINFOMATE"),
fullDesc: "MyInfoMate est une plateforme innovante permettant aux institutions culturelles de créer, gérer et diffuser du contenu interactif sans connaissances techniques. Cartes interactives, guides multimédias et jeux de piste sont gérés via un CMS intuitif centralisé.",
link: "https://myinfomate.be",
images: [
resolveImage("PORTFOLIO_MYINFOMATE"),
"https://images.unsplash.com/photo-1542744173-8e7e53415bb0?q=80&w=2070",
"https://images.unsplash.com/photo-1551434678-e076c223a692?q=80&w=2070"
],
},
{
title: "Office du Tourisme de Namur",
categories: ["Web", "Mobile"],
desc: "Interface tactile interactive sur grande tablette pour l'accueil touristique.",
img: resolveImage("PORTFOLIO_NAMUR"),
fullDesc: "Déploiement de la technologie MyInfoMate sur les bornes interactives de l'Office du Tourisme de Namur. Cette installation permet aux visiteurs d'explorer la ville de manière tactile, avec des points d'intérêt géo-localisés et une planification d'itinéraire simplifiée.",
images: [
resolveImage("PORTFOLIO_NAMUR"),
"https://images.unsplash.com/photo-1517048676732-d65bc937f952?q=80&w=2070",
"https://images.unsplash.com/photo-1531403001884-48a690c749ea?q=80&w=2070"
],
},
{
title: "Carnaval de Marche",
categories: ["Impression 3D"],
desc: "Design et production de boucles d'oreilles personnalisées pour l'événement.",
img: resolveImage("PORTFOLIO_MARCHE"),
fullDesc: "Nous avons designé, imprimé et assemblé des boucles d'oreilles pour le Carnaval de Marche. Plus d'une centaine de paires ont été produites, démontrant notre capacité de prototypage et de production en petite série via l'impression 3D.",
images: [
resolveImage("PORTFOLIO_MARCHE"),
"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1964",
"https://images.unsplash.com/photo-1562408590-e32931084e23?q=80&w=2070"
],
},
{
title: "Fort de Saint-Héribert",
categories: ["Mobile", "Embedded", "Web"],
desc: "Application mobile immersive pour la visite du fort via QR codes et localisation. Finalisée en mai 2023.",
img: resolveImage("PORTFOLIO_HERIBERT"),
fullDesc: "Notre réalisation pour le Fort de Saint-Héribert consiste en une application mobile dédiée à la visite du fort. Cette application offre une expérience immersive, où le contenu peut être consulté soit en scannant un QR code, soit en activant l'affichage de contenu basé sur la localisation du visiteur. La détection de proximité permet une interaction fluide avec le patrimoine.",
images: [
resolveImage("PORTFOLIO_HERIBERT"),
"https://images.unsplash.com/photo-1533106418989-88406c7cc8ca?q=80&w=2070",
"https://images.unsplash.com/photo-1496307653780-42ee777d4833?q=80&w=2070"
],
},
{
title: "Musée de la fraise",
categories: ["Desktop", "Mobile"],
desc: "Tablettes interactives et gestionnaire de contenu pour enrichir l'expérience musée.",
img: resolveImage("PORTFOLIO_FRAISE"),
fullDesc: "Le projet pour le musée de la fraise de Wépion avait pour objectif d'intégrer des tablettes interactives. Nous avons créé un gestionnaire dédié (MyMuseum) permettant au personnel de personnaliser le contenu, ainsi que l'application mobile chargée sur les tablettes pour la consultation des visiteurs.",
images: [
resolveImage("PORTFOLIO_FRAISE"),
"https://images.unsplash.com/photo-1581291518062-c07a09ea0937?q=80&w=2070",
"https://images.unsplash.com/photo-1558655146-d09347e92766?q=80&w=1964"
],
},
{
title: "Houblionnière de la Chistrée",
categories: ["Web"],
desc: "Site vitrine et boutique en ligne pour investir dans une coopérative locale.",
img: resolveImage("PORTFOLIO_CHISTREE"),
fullDesc: "La coopérative souhaitait un site internet vitrine permettant aux visiteurs d'investir dans ce beau projet via une petite boutique en ligne intégrée. Une solution Web complète alliant esthétique et fonctionnalité e-commerce.",
images: [
resolveImage("PORTFOLIO_CHISTREE"),
"https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?q=80&w=2070",
"https://images.unsplash.com/photo-1460925895917-afdab827c52f?q=80&w=2015"
],
},
];
const filteredProjects = useMemo(() => {
if (activeCategory === "All") return allProjects;
return allProjects.filter(p => p.categories.includes(activeCategory));
}, [activeCategory, allProjects]);
const categories = ["All", "Desktop", "Embedded", "Impression 3D", "Mobile", "Web"];
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 font-sans flex flex-col">
<nav className="fixed top-0 w-full z-50 bg-[#0A0F1C] py-6 shadow-xl shadow-black/20">
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between">
<Link href="/" className="flex items-center gap-3">
<Image
src={resolveImage("{{DATA:IMAGE:IMAGE_2}}")}
alt="UNOV Logo"
width={120}
height={40}
className="h-8 w-auto object-contain"
/>
</Link>
<Link href="/" className="text-sm font-black uppercase tracking-widest text-primary hover:text-primary-light transition-all flex items-center gap-2">
<span className="material-symbols-outlined text-sm">arrow_back</span>
Retour à l'accueil
</Link>
</div>
</nav>
<main className="pt-32 pb-20 px-6 max-w-7xl mx-auto flex-grow w-full">
<header className="mb-20">
<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>
</h1>
<div className="flex flex-wrap gap-4">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`px-8 py-3 rounded-2xl text-sm font-black tracking-widest uppercase transition-all ${cat === activeCategory ? "bg-primary text-white shadow-lg shadow-primary/30 scale-105" : "bg-white dark:bg-slate-900 text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-800 hover:scale-105"}`}
>
{cat}
</button>
))}
</div>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-12">
{filteredProjects.length > 0 ? (
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">
<div className="relative h-72 overflow-hidden">
<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>
<div className="p-12">
<span className="text-primary font-black text-xs uppercase tracking-[0.2em] mb-4 block">
{p.categories.join(" / ")}
</span>
<h2 className="text-3xl font-black mb-6 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>
<button
onClick={() => {
setSelectedProject(p);
setCurrentImageIndex(0);
}}
className="text-slate-900 dark:text-white font-black text-xs uppercase tracking-widest flex items-center gap-2 group/btn"
>
Voir plus
<span className="material-symbols-outlined text-sm transition-transform group-hover/btn:translate-x-1">arrow_forward</span>
</button>
</div>
</div>
))
) : (
<div className="col-span-full py-20 text-center">
<span className="material-symbols-outlined text-6xl text-slate-300 mb-6">inventory_2</span>
<p className="text-slate-400 font-medium">Aucun projet trouvé dans cette catégorie pour le moment.</p>
<button
onClick={() => setActiveCategory("All")}
className="mt-8 text-primary font-bold hover:underline"
>
Voir tous les projets
</button>
</div>
)}
</div>
</main>
{/* Project Modal */}
{selectedProject && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 md:p-8">
<div
className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm"
onClick={() => setSelectedProject(null)}
/>
<div className="relative bg-white dark:bg-slate-900 w-full max-w-4xl max-h-[90vh] overflow-y-auto rounded-3xl shadow-2xl flex flex-col md:flex-row overflow-hidden border border-slate-200 dark:border-slate-800 animate-in fade-in zoom-in duration-300">
<button
onClick={() => setSelectedProject(null)}
className="absolute top-6 right-6 z-20 bg-white/10 backdrop-blur-md text-slate-900 dark:text-white w-10 h-10 rounded-full flex items-center justify-center hover:bg-white/20 transition-all"
>
<span className="material-symbols-outlined text-xl">close</span>
</button>
<div className="md:w-1/2 relative h-[400px] md:h-auto overflow-hidden group/modal">
<Image
src={selectedProject.images[currentImageIndex]}
alt={selectedProject.title}
fill
className="object-cover transition-all duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/60 to-transparent pointer-events-none" />
{/* Gallery Controls */}
{selectedProject.images && selectedProject.images.length > 1 && (
<>
<div className="absolute inset-x-0 bottom-8 flex justify-center gap-3 z-10">
{selectedProject.images.map((_: any, i: number) => (
<button
key={i}
onClick={() => setCurrentImageIndex(i)}
className={`w-2.5 h-2.5 rounded-full transition-all ${i === currentImageIndex ? "bg-primary w-8" : "bg-white/40 hover:bg-white/60"}`}
/>
))}
</div>
<button
onClick={() => setCurrentImageIndex((prev) => (prev > 0 ? prev - 1 : selectedProject.images.length - 1))}
className="absolute left-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center text-white opacity-0 group-hover/modal:opacity-100 transition-all hover:bg-white/20"
>
<span className="material-symbols-outlined">chevron_left</span>
</button>
<button
onClick={() => setCurrentImageIndex((prev) => (prev < selectedProject.images.length - 1 ? prev + 1 : 0))}
className="absolute right-4 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center text-white opacity-0 group-hover/modal:opacity-100 transition-all hover:bg-white/20"
>
<span className="material-symbols-outlined">chevron_right</span>
</button>
</>
)}
</div>
<div className="md:w-1/2 p-8 md:p-12 flex flex-col justify-center">
<span className="text-primary font-bold uppercase tracking-[0.2em] text-xs mb-4 block">
{selectedProject.categories.join(" / ")}
</span>
<h2 className="text-3xl md:text-5xl font-black mb-8 leading-tight text-slate-900 dark:text-white">
{selectedProject.title}
</h2>
<div className="w-12 h-1 bg-primary mb-8" />
<p className="text-slate-600 dark:text-slate-400 text-lg leading-relaxed mb-10">
{selectedProject.fullDesc || selectedProject.desc}
</p>
<div className="flex justify-between items-center mt-auto">
{selectedProject.link && (
<a
href={selectedProject.link}
target="_blank"
rel="noopener noreferrer"
className="text-primary font-bold flex items-center gap-2 group/link"
>
<span className="material-symbols-outlined">language</span>
<span className="border-b-2 border-transparent group-hover/link:border-primary transition-all pb-0.5">Visiter le site</span>
</a>
)}
<div className="flex justify-end flex-grow">
<button
onClick={() => setSelectedProject(null)}
className="bg-primary hover:bg-primary-hover text-white px-10 py-4 rounded-xl font-bold transition-all w-fit shadow-xl"
>
Fermer
</button>
</div>
</div>
</div>
</div>
</div>
)}
<footer className="py-24 bg-[#0A0F1C] text-white/90">
<div className="max-w-7xl mx-auto px-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-16">
<div className="flex flex-col items-center md:items-start gap-6">
<Image
src={resolveImage("{{DATA:IMAGE:IMAGE_2}}")}
alt="UNOV Logo"
width={120}
height={35}
className="h-8 w-auto object-contain"
/>
<p className="text-slate-400 text-sm max-w-xs text-center md:text-left leading-relaxed">
Agence technologique de précision spécialisée dans les logiciels haut de gamme et les solutions d'IA. La perfection dans chaque ligne de code.
</p>
</div>
<div className="flex flex-col items-center md:items-end gap-8">
<div className="flex gap-10">
<a href="#" className="text-slate-400 hover:text-primary transition-all hover:-translate-y-1">
<span className="material-symbols-outlined text-3xl">language</span>
</a>
<a href="#" className="text-slate-400 hover:text-primary transition-all hover:-translate-y-1">
<span className="material-symbols-outlined text-3xl">public</span>
</a>
<a href="#" className="text-slate-400 hover:text-primary transition-all hover:-translate-y-1">
<span className="material-symbols-outlined text-3xl">groups</span>
</a>
</div>
<div className="text-slate-500 text-xs font-bold uppercase tracking-widest">
© {new Date().getFullYear()} UNOV. Tous droits réservés.
</div>
</div>
</div>
</div>
</footer>
</div>
);
}

25
src/data/stitch-images.ts Normal file
View File

@ -0,0 +1,25 @@
/**
* Mapping object for Stitch image placeholders.
* Centralized management of landing page assets.
*/
export const STITCH_IMAGES: Record<string, string> = {
"{{DATA:IMAGE:IMAGE_2}}": "/unov.png",
"{{DATA:IMAGE:IMAGE_6}}": "/desk_04.png",
"SERVICE_SOFTWARE": "https://images.unsplash.com/photo-1517694712202-14dd9538aa97?q=80&w=2070&auto=format&fit=crop",
"SERVICE_AI": "/images/service_ai_v2.png",
"SERVICE_3D": "https://images.unsplash.com/photo-1581092160562-40aa08e78837?q=80&w=2070&auto=format&fit=crop",
"PORTFOLIO_HERIBERT": "https://unov.be/wp-content/uploads/2023/06/Screenshot_8.jpg",
"PORTFOLIO_MARCHE": "https://unov.be/wp-content/uploads/2022/11/Screenshot_1-370x370.jpg",
"PORTFOLIO_CHISTREE": "https://unov.be/wp-content/uploads/2022/04/houblon_0-370x370.jpg",
"PORTFOLIO_FRAISE": "https://unov.be/wp-content/uploads/2022/04/mdlf_3-370x370.jpg",
"PORTFOLIO_MYINFOMATE": "/images/myinfomate_logo.png",
"PORTFOLIO_NAMUR": "/images/portfolio_namur.png",
};
/**
* Helper to resolve a Stitch placeholder.
* If the placeholder is not found, it returns the placeholder itself.
*/
export function resolveImage(key: string): string {
return STITCH_IMAGES[key] || key;
}