feat: Transform HookLab to OBC Maçonnerie showcase site

Complete transformation of the Next.js project into a professional
showcase site for OBC Maçonnerie (Benoît Colin, maçon in Nord 59).

Key changes:
- Remove all HookLab/Sanity/Supabase/Stripe/admin/training infrastructure
- Full OBC Maçonnerie identity: logo, colors, contact info, SIREN
- Schema.org LocalBusiness structured data for Benoît Colin
- SEO metadata for all pages targeting Nord 59 keywords

New pages created (23 total):
- Home page with 10 sections (hero, services, pillars, partners,
  zone, realisations, testimonials, FAQ, contact form, footer)
- Service pages: construction-maison, renovation, assainissement,
  creation-acces, demolition, services
- Secondary pages: realisations, partenaires, contact
- Blog: listing + 6 SEO articles with static content
- 8 local SEO pages: Orchies, Douai, Valenciennes, Mouchin,
  Flines-lès-Raches, Saint-Amand-les-Eaux
- Legal pages: mentions-legales, cgv, confidentialite (OBC adapted)

Components:
- Navbar with OBC branding + mobile menu
- Footer with dark navy theme, services + navigation links
- ContactForm client component (devis request)
- LocalSEOPage reusable component for local SEO pages
- CookieBanner updated with OBC cookie key

Config:
- layout.tsx: OBC metadata, Schema.org, no Sanity CDN
- globals.css: stone color variables added
- next.config.ts: removed Sanity CDN remotePatterns
- sitemap.ts: all 30 OBC pages
- robots.ts: allow all except /api/
- api/contact/route.ts: OBC devis email template

https://claude.ai/code/session_01Uec4iHjcPwB1pU41idWEdF
This commit is contained in:
Claude
2026-02-27 09:05:03 +00:00
parent 45d080197a
commit 3adcec00b7
113 changed files with 3134 additions and 11663 deletions

View File

@@ -7,7 +7,7 @@ export default function CookieBanner() {
const [visible, setVisible] = useState(false);
useEffect(() => {
const consent = localStorage.getItem("hooklab_cookie_consent");
const consent = localStorage.getItem("obc_cookie_consent");
if (!consent) {
// Small delay so it doesn't flash on page load
const timer = setTimeout(() => setVisible(true), 800);
@@ -16,12 +16,12 @@ export default function CookieBanner() {
}, []);
const handleAccept = () => {
localStorage.setItem("hooklab_cookie_consent", "accepted");
localStorage.setItem("obc_cookie_consent", "accepted");
setVisible(false);
};
const handleRefuse = () => {
localStorage.setItem("hooklab_cookie_consent", "refused");
localStorage.setItem("obc_cookie_consent", "refused");
setVisible(false);
};

View File

@@ -1,148 +0,0 @@
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { createClient } from "@/lib/supabase/client";
import { cn } from "@/lib/utils";
interface AdminShellProps {
children: React.ReactNode;
adminName: string;
adminEmail: string;
}
const navItems = [
{
label: "Dashboard",
href: "/admin",
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 5a1 1 0 011-1h4a1 1 0 011 1v5a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v2a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10-2a1 1 0 011-1h4a1 1 0 011 1v6a1 1 0 01-1 1h-4a1 1 0 01-1-1v-6z" />
</svg>
),
exact: true,
},
{
label: "Candidatures",
href: "/admin/candidatures",
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
),
},
{
label: "Cours",
href: "/admin/cours",
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
),
},
{
label: "Images du site",
href: "/admin/images",
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
</svg>
),
},
];
export default function AdminShell({ children, adminName, adminEmail }: AdminShellProps) {
const pathname = usePathname();
const router = useRouter();
const handleLogout = async () => {
const supabase = createClient();
await supabase.auth.signOut();
router.push("/login");
router.refresh();
};
return (
<div className="flex min-h-screen bg-dark">
{/* Sidebar */}
<aside className="w-64 min-h-screen bg-dark-light border-r border-dark-border p-6 flex flex-col">
{/* Logo */}
<Link href="/admin" className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 gradient-bg rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">H</span>
</div>
<span className="text-xl font-bold text-white">
Hook<span className="gradient-text">Lab</span>
</span>
</Link>
<span className="text-xs text-primary font-medium mb-8 ml-10">Admin</span>
{/* Navigation */}
<nav className="flex-1 space-y-1">
{navItems.map((item) => {
const isActive = item.exact
? pathname === item.href
: pathname.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all",
isActive
? "bg-primary/10 text-primary"
: "text-white/50 hover:text-white hover:bg-white/5"
)}
>
{item.icon}
{item.label}
</Link>
);
})}
{/* Séparateur */}
<div className="border-t border-dark-border my-4" />
{/* Lien vers le site */}
<Link
href="/"
className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-white/30 hover:text-white hover:bg-white/5 transition-all"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
Voir le site
</Link>
</nav>
{/* User info */}
<div className="border-t border-dark-border pt-4 mt-4">
<div className="flex items-center gap-3 mb-4">
<div className="w-9 h-9 gradient-bg rounded-full flex items-center justify-center text-sm font-bold text-white">
{(adminName || adminEmail)[0].toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<p className="text-white text-sm font-medium truncate">{adminName || "Admin"}</p>
<p className="text-white/40 text-xs truncate">{adminEmail}</p>
</div>
</div>
<button
onClick={handleLogout}
className="flex items-center gap-2 text-white/40 hover:text-error text-sm transition-colors cursor-pointer w-full"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Déconnexion
</button>
</div>
</aside>
{/* Main content */}
<main className="flex-1 p-6 md:p-10 overflow-y-auto">
{children}
</main>
</div>
);
}

View File

@@ -1,146 +0,0 @@
import Link from "next/link";
import Card from "@/components/ui/Card";
import type { Module, UserProgress } from "@/types/database.types";
interface ModuleCardProps {
module: Module;
progress?: UserProgress;
}
const contentTypeIcons: Record<string, React.ReactNode> = {
video: (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
),
pdf: (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
),
text: (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M4 6h16M4 12h16M4 18h7"
/>
</svg>
),
quiz: (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
),
};
export default function ModuleCard({ module, progress }: ModuleCardProps) {
const isCompleted = progress?.completed;
return (
<Link href={`/formations/${module.id}`}>
<Card hover className="relative overflow-hidden group">
{/* Status indicator */}
{isCompleted && (
<div className="absolute top-4 right-4">
<div className="w-6 h-6 rounded-full bg-success flex items-center justify-center">
<svg
className="w-3.5 h-3.5 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
</div>
)}
{/* Content type + Duration */}
<div className="flex items-center gap-3 mb-3">
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${
isCompleted
? "bg-success/10 text-success"
: "bg-primary/10 text-primary"
}`}
>
{module.content_type && contentTypeIcons[module.content_type]}
{module.content_type?.toUpperCase() || "CONTENU"}
</span>
{module.duration_minutes && (
<span className="text-white/30 text-xs">
{module.duration_minutes} min
</span>
)}
</div>
{/* Title */}
<h3 className="text-white font-semibold mb-2 group-hover:text-primary transition-colors">
{module.title}
</h3>
{/* Description */}
{module.description && (
<p className="text-white/50 text-sm line-clamp-2">
{module.description}
</p>
)}
{/* Week badge */}
<div className="mt-4 pt-3 border-t border-dark-border">
<span className="text-white/30 text-xs">
Semaine {module.week_number}
</span>
</div>
</Card>
</Link>
);
}

View File

@@ -1,36 +0,0 @@
interface ProgressBarProps {
value: number; // 0-100
label?: string;
showPercentage?: boolean;
}
export default function ProgressBar({
value,
label,
showPercentage = true,
}: ProgressBarProps) {
const clampedValue = Math.min(100, Math.max(0, value));
return (
<div className="space-y-2">
{(label || showPercentage) && (
<div className="flex items-center justify-between">
{label && (
<span className="text-white/60 text-sm">{label}</span>
)}
{showPercentage && (
<span className="text-white font-medium text-sm">
{Math.round(clampedValue)}%
</span>
)}
</div>
)}
<div className="h-2 bg-dark-lighter rounded-full overflow-hidden">
<div
className="h-full gradient-bg rounded-full transition-all duration-500 ease-out"
style={{ width: `${clampedValue}%` }}
/>
</div>
</div>
);
}

View File

@@ -1,156 +0,0 @@
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { createClient } from "@/lib/supabase/client";
import { cn } from "@/lib/utils";
import type { Profile } from "@/types/database.types";
interface SidebarProps {
user: Profile;
}
const navItems = [
{
label: "Dashboard",
href: "/dashboard",
icon: (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
),
},
{
label: "Formations",
href: "/formations",
icon: (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
),
},
{
label: "Profil",
href: "/profil",
icon: (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
),
},
];
export default function Sidebar({ user }: SidebarProps) {
const pathname = usePathname();
const router = useRouter();
const handleLogout = async () => {
const supabase = createClient();
await supabase.auth.signOut();
router.push("/login");
router.refresh();
};
return (
<aside className="w-64 min-h-screen bg-dark-light border-r border-dark-border p-6 flex flex-col">
{/* Logo */}
<Link href="/dashboard" className="flex items-center gap-2 mb-10">
<div className="w-8 h-8 gradient-bg rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">H</span>
</div>
<span className="text-xl font-bold text-white">
Hook<span className="gradient-text">Lab</span>
</span>
</Link>
{/* Navigation */}
<nav className="flex-1 space-y-1">
{navItems.map((item) => {
const isActive =
pathname === item.href ||
(item.href !== "/dashboard" && pathname.startsWith(item.href));
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all",
isActive
? "bg-primary/10 text-primary"
: "text-white/50 hover:text-white hover:bg-white/5"
)}
>
{item.icon}
{item.label}
</Link>
);
})}
</nav>
{/* User info + Logout */}
<div className="border-t border-dark-border pt-4 mt-4">
<div className="flex items-center gap-3 mb-4">
<div className="w-9 h-9 gradient-bg rounded-full flex items-center justify-center text-sm font-bold text-white">
{(user.full_name || user.email)[0].toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<p className="text-white text-sm font-medium truncate">
{user.full_name || "Utilisateur"}
</p>
<p className="text-white/40 text-xs truncate">{user.email}</p>
</div>
</div>
<button
onClick={handleLogout}
className="flex items-center gap-2 text-white/40 hover:text-error text-sm transition-colors cursor-pointer w-full"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
Deconnexion
</button>
</div>
</aside>
);
}

View File

@@ -1,102 +0,0 @@
"use client";
import ScrollReveal from "@/components/animations/ScrollReveal";
interface AboutMeProps {
images?: Record<string, string>;
}
export default function AboutMe({ images }: AboutMeProps) {
const photoUrl = images?.about_photo;
return (
<section id="qui-suis-je" className="py-16 md:py-24 bg-orange relative overflow-hidden" aria-label="Qui suis-je">
{/* Subtle pattern */}
<div className="absolute inset-0 opacity-10">
<div className="absolute top-20 right-20 w-40 h-40 border-2 border-white rounded-full" />
<div className="absolute bottom-10 left-10 w-60 h-60 border-2 border-white rounded-full" />
</div>
<div className="relative max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Content */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
{/* Photo */}
<ScrollReveal direction="left">
<div className="flex justify-center">
<div className="relative">
<div className="w-64 h-80 sm:w-72 sm:h-[22rem] rounded-2xl overflow-hidden border-4 border-white/20 shadow-xl">
{photoUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={photoUrl} alt="Enguerrand Ozano" className="w-full h-full object-cover" />
) : (
<div className="w-full h-full bg-orange-hover flex items-center justify-center">
<div className="text-center p-6">
<div className="w-20 h-20 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-10 h-10 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<p className="text-white/60 text-sm">Votre photo ici</p>
<p className="text-white/40 text-xs mt-1">(modifiable dans Admin &gt; Images)</p>
</div>
</div>
)}
</div>
<div className="absolute -bottom-3 left-1/2 -translate-x-1/2 bg-navy text-white text-xs font-bold px-4 py-2 rounded-full shadow-lg whitespace-nowrap">
Bas&eacute; &agrave; Flines-lez-Raches
</div>
</div>
</div>
</ScrollReveal>
{/* Text */}
<ScrollReveal direction="right">
<div>
<span className="inline-block px-3 py-1.5 bg-white/15 rounded-full text-white text-xs font-semibold mb-4">
Votre expert local
</span>
<h2 className="text-2xl md:text-3xl lg:text-4xl font-bold text-white tracking-[-0.02em] mb-4">
Enguerrand Ozano.{" "}
<span className="text-navy">Votre voisin &agrave; Flines-lez-Raches.</span>
</h2>
<p className="text-white/90 text-base leading-relaxed mb-6">
Oubliez les plateformes t&eacute;l&eacute;phoniques &agrave; l&rsquo;autre bout du monde.
Je suis ici, dans le Nord (59). Je connais la r&eacute;alit&eacute; de vos m&eacute;tiers
et vos contraintes g&eacute;ographiques.
</p>
<ul className="space-y-4 mb-6">
{[
{ strong: "Un interlocuteur unique", text: "C\u2019est moi qui g\u00e8re votre dossier du d\u00e9but \u00e0 la fin." },
{ strong: "100% G\u00e9r\u00e9 pour vous", text: "Une fois le site lanc\u00e9, vous n\u2019avez rien \u00e0 faire. Si vous avez une nouvelle photo de chantier, vous me l\u2019envoyez, je la mets en ligne." },
{ strong: "Pas de mauvaise surprise", text: "Tout est clair d\u00e8s le d\u00e9part." },
].map((item, i) => (
<li key={i} className="flex items-start gap-3">
<div className="w-5 h-5 bg-white/20 rounded-full flex items-center justify-center shrink-0 mt-0.5">
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div>
<p className="text-white/80 text-base leading-relaxed">
<strong className="text-white">{item.strong}&nbsp;:</strong> {item.text}
</p>
</li>
))}
</ul>
<a
href="#contact"
className="inline-flex items-center gap-2 bg-navy hover:bg-navy-light text-white font-bold text-sm px-6 py-3 rounded-xl transition-colors"
>
Discutons de votre situation
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
</div>
</ScrollReveal>
</div>
</div>
</section>
);
}

View File

@@ -1,38 +0,0 @@
"use client";
import { useState } from "react";
import Link from "next/link";
export default function AnnouncementBar() {
const [visible, setVisible] = useState(true);
if (!visible) return null;
return (
<div className="relative bg-gradient-to-r from-primary via-primary-hover to-primary text-white text-center py-2 px-10 text-xs sm:text-sm font-medium">
<Link href="/candidature" className="hover:underline">
<span className="hidden sm:inline">
Places limit&eacute;es &mdash; Nouvelle session de formation TikTok Shop ouverte &rarr;{" "}
<span className="underline font-bold">Candidater</span>
</span>
<span className="sm:hidden">
Places limit&eacute;es &mdash;{" "}
<span className="underline font-bold">Candidater maintenant</span>
</span>
</Link>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setVisible(false);
}}
className="absolute right-2 top-1/2 -translate-y-1/2 text-white/70 hover:text-white cursor-pointer p-1"
aria-label="Fermer"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
);
}

View File

@@ -0,0 +1,223 @@
"use client";
import { useState } from "react";
const typesProjets = [
"Construction de maison",
"Rénovation",
"Assainissement",
"Création d'accès",
"Démolition",
"Autre",
];
export default function ContactForm() {
const [form, setForm] = useState({
nom: "",
telephone: "",
email: "",
typeProjet: "",
description: "",
budget: "",
zone: "",
});
const [status, setStatus] = useState<"idle" | "sending" | "success" | "error">("idle");
const [error, setError] = useState("");
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!form.nom || !form.telephone || !form.typeProjet) {
setError("Merci de renseigner au minimum votre nom, téléphone et type de projet.");
return;
}
setError("");
setStatus("sending");
try {
const res = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (res.ok) {
setStatus("success");
setForm({ nom: "", telephone: "", email: "", typeProjet: "", description: "", budget: "", zone: "" });
} else {
setStatus("error");
}
} catch {
setStatus("error");
}
};
if (status === "success") {
return (
<div className="bg-bg-white border border-success rounded-2xl p-8 text-center">
<div className="text-4xl mb-4"></div>
<h3 className="text-navy font-bold text-xl mb-2">Demande envoyée !</h3>
<p className="text-text-light text-sm">
Benoît vous rappellera dans les 24h. En cas d&apos;urgence, appelez directement le{" "}
<a href="tel:0674453089" className="text-orange font-bold">
06 74 45 30 89
</a>
.
</p>
</div>
);
}
return (
<form
onSubmit={handleSubmit}
className="bg-bg-white border border-border rounded-2xl p-6 md:p-8 space-y-4"
>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label htmlFor="nom" className="block text-sm font-semibold text-navy mb-1">
Nom <span className="text-orange">*</span>
</label>
<input
id="nom"
name="nom"
type="text"
value={form.nom}
onChange={handleChange}
placeholder="Votre nom"
className="w-full border border-border rounded-xl px-4 py-3 text-sm text-text focus:outline-none focus:border-orange transition-colors bg-bg"
required
/>
</div>
<div>
<label htmlFor="telephone" className="block text-sm font-semibold text-navy mb-1">
Téléphone <span className="text-orange">*</span>
</label>
<input
id="telephone"
name="telephone"
type="tel"
value={form.telephone}
onChange={handleChange}
placeholder="06 XX XX XX XX"
className="w-full border border-border rounded-xl px-4 py-3 text-sm text-text focus:outline-none focus:border-orange transition-colors bg-bg"
required
/>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-semibold text-navy mb-1">
Email
</label>
<input
id="email"
name="email"
type="email"
value={form.email}
onChange={handleChange}
placeholder="votre@email.fr"
className="w-full border border-border rounded-xl px-4 py-3 text-sm text-text focus:outline-none focus:border-orange transition-colors bg-bg"
/>
</div>
<div>
<label htmlFor="typeProjet" className="block text-sm font-semibold text-navy mb-1">
Type de projet <span className="text-orange">*</span>
</label>
<select
id="typeProjet"
name="typeProjet"
value={form.typeProjet}
onChange={handleChange}
className="w-full border border-border rounded-xl px-4 py-3 text-sm text-text focus:outline-none focus:border-orange transition-colors bg-bg"
required
>
<option value="">Choisissez un type de projet</option>
{typesProjets.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
</div>
<div>
<label htmlFor="description" className="block text-sm font-semibold text-navy mb-1">
Description du projet
</label>
<textarea
id="description"
name="description"
value={form.description}
onChange={handleChange}
rows={4}
placeholder="Décrivez votre projet : surface, localisation, contraintes particulières..."
className="w-full border border-border rounded-xl px-4 py-3 text-sm text-text focus:outline-none focus:border-orange transition-colors bg-bg resize-none"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label htmlFor="budget" className="block text-sm font-semibold text-navy mb-1">
Budget approximatif
</label>
<input
id="budget"
name="budget"
type="text"
value={form.budget}
onChange={handleChange}
placeholder="ex : 80 000 €"
className="w-full border border-border rounded-xl px-4 py-3 text-sm text-text focus:outline-none focus:border-orange transition-colors bg-bg"
/>
</div>
<div>
<label htmlFor="zone" className="block text-sm font-semibold text-navy mb-1">
Commune / Zone
</label>
<input
id="zone"
name="zone"
type="text"
value={form.zone}
onChange={handleChange}
placeholder="ex : Orchies, Douai..."
className="w-full border border-border rounded-xl px-4 py-3 text-sm text-text focus:outline-none focus:border-orange transition-colors bg-bg"
/>
</div>
</div>
{error && (
<p className="text-error text-sm bg-red-50 border border-red-200 rounded-xl px-4 py-3">
{error}
</p>
)}
<button
type="submit"
disabled={status === "sending"}
className="w-full bg-orange hover:bg-orange-hover text-white font-bold py-4 rounded-xl transition-colors disabled:opacity-60 disabled:cursor-not-allowed text-base"
>
{status === "sending" ? "Envoi en cours..." : "Envoyer ma demande de devis"}
</button>
{status === "error" && (
<p className="text-error text-sm text-center">
Une erreur est survenue. Appelez directement le{" "}
<a href="tel:0674453089" className="font-bold underline">
06 74 45 30 89
</a>
.
</p>
)}
<p className="text-text-muted text-xs text-center">
Devis gratuit &amp; sans engagement Réponse sous 24h
</p>
</form>
);
}

View File

@@ -1,108 +0,0 @@
"use client";
import Card from "@/components/ui/Card";
import Link from "next/link";
import ScrollReveal from "@/components/animations/ScrollReveal";
const demos = [
{
title: "Le Mod\u00e8le \u00ab\u00a0Gros \u0152uvre\u00a0\u00bb",
subtitle: "Ma\u00e7ons, Couvreurs",
description: "Id\u00e9al pour montrer la technique. Un site qui met en avant vos photos \u00ab\u00a0Avant / Apr\u00e8s\u00a0\u00bb pour prouver la qualit\u00e9 de vos finitions.",
cta: "Voir un exemple Ma\u00e7onnerie",
href: "/macon",
icon: (
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
),
},
{
title: "Le Mod\u00e8le \u00ab\u00a0Cr\u00e9ation\u00a0\u00bb",
subtitle: "Paysagistes, Peintres",
description: "Id\u00e9al pour vendre du r\u00eave. Un design \u00e9pur\u00e9 qui laisse toute la place \u00e0 la beaut\u00e9 de vos r\u00e9alisations.",
cta: "Voir un exemple Paysagiste",
href: "/paysagiste",
icon: (
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</svg>
),
},
{
title: "Le Mod\u00e8le \u00ab\u00a0Intervention\u00a0\u00bb",
subtitle: "Plombiers, \u00c9lectriciens",
description: "Id\u00e9al pour l\u2019urgence. Un site ultra-rapide avec votre num\u00e9ro de t\u00e9l\u00e9phone bien visible pour \u00eatre appel\u00e9 en un clic.",
cta: "Voir un exemple Plombier",
href: "/plombier",
icon: (
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
),
},
];
interface DemosLiveProps {
images?: Record<string, string>;
}
export default function DemosLive(_props: DemosLiveProps) {
return (
<section id="exemples" className="py-16 md:py-24 bg-bg" aria-label="Exemples">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal direction="up">
<div className="text-center mb-14">
<span className="inline-block px-3 py-1.5 bg-orange/10 border border-orange/20 rounded-full text-orange text-xs font-semibold mb-4">
Exemples
</span>
<h2 className="text-2xl md:text-3xl lg:text-4xl font-bold text-navy tracking-[-0.02em] mb-3">
Ne signez pas les yeux ferm&eacute;s.{" "}
<span className="text-orange">Regardez ce que je peux faire pour vous.</span>
</h2>
<p className="text-text-light text-base md:text-lg max-w-2xl mx-auto">
Je ne vous demande pas de me croire sur parole. J&rsquo;ai pr&eacute;par&eacute; des mod&egrave;les
adapt&eacute;s &agrave; votre m&eacute;tier. Cliquez et imaginez votre logo &agrave; la place.
</p>
</div>
</ScrollReveal>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{demos.map((demo, i) => (
<ScrollReveal key={i} direction="up" delay={i * 200}>
<Card hover className="flex flex-col p-0 overflow-hidden h-full">
{/* Header visuel */}
<div className="bg-navy p-6 text-center">
<div className="w-16 h-16 bg-orange/20 rounded-2xl flex items-center justify-center mx-auto mb-3 text-orange">
{demo.icon}
</div>
<h3 className="text-white font-bold text-lg">{demo.title}</h3>
<p className="text-orange text-sm font-semibold">{demo.subtitle}</p>
</div>
{/* Content */}
<div className="p-5 flex-1 flex flex-col">
<div className="flex-1">
<p className="text-text-light text-sm leading-relaxed">{demo.description}</p>
</div>
{/* CTA */}
<Link
href={demo.href}
className="mt-5 flex items-center justify-center gap-2 bg-orange text-white font-bold text-sm px-5 py-3 rounded-xl hover:bg-orange/90 hover:scale-[1.02] transition-all duration-300"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
{demo.cta}
</Link>
</div>
</Card>
</ScrollReveal>
))}
</div>
</div>
</section>
);
}

View File

@@ -1,141 +0,0 @@
"use client";
import { useState, useEffect, useRef } from "react";
import Link from "next/link";
import Button from "@/components/ui/Button";
export default function ExitIntentPopup() {
const [show, setShow] = useState(false);
const [dismissed, setDismissed] = useState(false);
const lastScrollY = useRef(0);
const maxScrollY = useRef(0);
useEffect(() => {
if (dismissed) return;
// Check if already shown this session
if (typeof window !== "undefined" && sessionStorage.getItem("hooklab_exit_popup")) {
setDismissed(true);
return;
}
const triggerPopup = () => {
if (!show && !dismissed) {
setShow(true);
sessionStorage.setItem("hooklab_exit_popup", "1");
}
};
// Desktop: mouse leaves viewport at top
const handleMouseLeave = (e: MouseEvent) => {
if (e.clientY <= 5) {
triggerPopup();
}
};
// Mobile: user scrolls back up fast after scrolling at least 60% of the page
const handleScroll = () => {
const currentY = window.scrollY;
const pageHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrollPercent = pageHeight > 0 ? currentY / pageHeight : 0;
if (currentY > maxScrollY.current) {
maxScrollY.current = currentY;
}
// Trigger if user scrolled past 60% of page and then scrolls up by 300px+
const scrolledUpAmount = maxScrollY.current - currentY;
const maxScrollPercent = pageHeight > 0 ? maxScrollY.current / pageHeight : 0;
if (maxScrollPercent > 0.6 && scrolledUpAmount > 300 && scrollPercent < 0.4) {
triggerPopup();
}
lastScrollY.current = currentY;
};
// Desktop: mouseleave
document.addEventListener("mouseleave", handleMouseLeave);
// Mobile: scroll-based trigger
window.addEventListener("scroll", handleScroll, { passive: true });
return () => {
document.removeEventListener("mouseleave", handleMouseLeave);
window.removeEventListener("scroll", handleScroll);
};
}, [show, dismissed]);
const handleClose = () => {
setShow(false);
setDismissed(true);
};
if (!show) return null;
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
onClick={handleClose}
/>
{/* Modal */}
<div className="relative bg-dark-light border border-dark-border rounded-3xl p-6 sm:p-8 max-w-md w-full shadow-2xl animate-scale-in">
{/* Close */}
<button
onClick={handleClose}
className="absolute top-4 right-4 text-white/40 hover:text-white cursor-pointer"
aria-label="Fermer"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="text-center">
{/* Icon */}
<div className="w-14 h-14 sm:w-16 sm:h-16 gradient-bg rounded-full flex items-center justify-center mx-auto mb-4 sm:mb-5">
<svg className="w-7 h-7 sm:w-8 sm:h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-xl sm:text-2xl font-bold text-white mb-2 sm:mb-3">
Tu h&eacute;sites encore ?
</h3>
<p className="text-white/60 text-sm mb-5 sm:mb-6 leading-relaxed">
TikTok Shop vient d&apos;arriver en France. Le march&eacute; n&apos;est pas
encore satur&eacute; et les premiers cr&eacute;ateurs captent
l&apos;essentiel des commissions.
</p>
{/* Stats */}
<div className="grid grid-cols-2 gap-3 mb-5 sm:mb-6">
<div className="bg-dark border border-dark-border rounded-xl p-3">
<p className="text-lg sm:text-xl font-bold gradient-text">50,5M&euro;</p>
<p className="text-white/40 text-xs">March&eacute; FR en 2 mois</p>
</div>
<div className="bg-dark border border-dark-border rounded-xl p-3">
<p className="text-lg sm:text-xl font-bold gradient-text">10-30%</p>
<p className="text-white/40 text-xs">Commission par vente</p>
</div>
</div>
<Link href="/candidature" onClick={handleClose}>
<Button size="lg" className="w-full pulse-glow mb-3">
D&eacute;couvrir le programme
</Button>
</Link>
<button
onClick={handleClose}
className="text-white/30 text-xs hover:text-white/50 transition-colors cursor-pointer"
>
Non merci
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,71 +1,101 @@
"use client";
import Link from "next/link";
import ScrollReveal from "@/components/animations/ScrollReveal";
export default function Footer() {
return (
<footer className="border-t border-border py-10 md:py-12 bg-bg-white">
<footer className="bg-navy text-white pt-12 pb-6">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<ScrollReveal direction="up">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Brand */}
<div>
<Link href="/" className="flex items-center gap-2 mb-3" aria-label="HookLab - Accueil">
<div className="w-8 h-8 bg-navy rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">H</span>
</div>
<span className="text-lg font-bold text-navy">
Hook<span className="text-orange">Lab</span>
</span>
</Link>
<p className="text-text-light text-sm leading-relaxed max-w-xs">
Cr&eacute;ation de sites internet pour artisans.
</p>
<p className="text-text-muted text-xs mt-3">
59148 Flines-lez-Raches
</p>
</div>
{/* Expertises SEO */}
<div>
<h4 className="text-navy font-semibold text-sm mb-4">
Expertises
</h4>
<ul className="space-y-2 text-text-light text-sm">
<li>Site internet Couvreur</li>
<li>SEO Maçonnerie</li>
<li>Webmaster Paysagiste</li>
<li>Visibilité Menuisier</li>
</ul>
</div>
{/* Legal */}
<div>
<h4 className="text-navy font-semibold text-sm mb-4">Légal</h4>
<ul className="space-y-2">
<li>
<Link href="/mentions-legales" className="text-text-light hover:text-navy text-sm transition-colors">
Mentions légales
</Link>
</li>
<li>
<Link href="/confidentialite" className="text-text-light hover:text-navy text-sm transition-colors">
Politique de Confidentialité
</Link>
</li>
</ul>
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 pb-10 border-b border-white/10">
{/* Brand */}
<div className="md:col-span-2">
<div className="flex items-center gap-2.5 mb-4">
<div className="w-10 h-10 bg-orange rounded-lg flex items-center justify-center shrink-0">
<span className="text-white font-bold text-xs">OBC</span>
</div>
<div className="flex flex-col leading-tight">
<span className="text-white font-bold text-base leading-none">OBC</span>
<span className="text-orange-light font-bold text-base leading-none">Maçonnerie</span>
</div>
</div>
<p className="text-white/70 text-sm leading-relaxed mb-4 max-w-xs">
Benoît Colin, maçon expert en construction de maison, rénovation et gros œuvre dans le Nord. De la première pierre à la remise des clés.
</p>
<a
href="tel:0674453089"
className="inline-flex items-center gap-2 text-orange-light font-bold text-base hover:text-white transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
06 74 45 30 89
</a>
<p className="text-white/40 text-xs mt-2">
221 Route de Saint-Amand, 59310 Mouchin
</p>
</div>
</ScrollReveal>
{/* Bottom SEO */}
<div className="border-t border-border mt-8 pt-6 flex flex-col md:flex-row items-center justify-between gap-3">
<p className="text-text-muted text-xs">
&copy; {new Date().getFullYear()} HookLab &mdash; Enguerrand Ozano &middot; SIREN 994 538 932
{/* Services */}
<div>
<h4 className="text-white font-semibold text-sm mb-4 uppercase tracking-wide">Services</h4>
<ul className="space-y-2">
{[
{ href: "/construction-maison", label: "Construction de maison" },
{ href: "/renovation", label: "Rénovation" },
{ href: "/assainissement", label: "Assainissement" },
{ href: "/creation-acces", label: "Création d'accès" },
{ href: "/demolition", label: "Démolition" },
].map((item) => (
<li key={item.href}>
<Link href={item.href} className="text-white/60 hover:text-white text-sm transition-colors">
{item.label}
</Link>
</li>
))}
</ul>
</div>
{/* Navigation */}
<div>
<h4 className="text-white font-semibold text-sm mb-4 uppercase tracking-wide">Navigation</h4>
<ul className="space-y-2">
{[
{ href: "/", label: "Accueil" },
{ href: "/realisations", label: "Réalisations" },
{ href: "/partenaires", label: "Partenaires" },
{ href: "/contact", label: "Contact" },
{ href: "/blog", label: "Blog" },
].map((item) => (
<li key={item.href}>
<Link href={item.href} className="text-white/60 hover:text-white text-sm transition-colors">
{item.label}
</Link>
</li>
))}
</ul>
<h4 className="text-white font-semibold text-sm mb-3 mt-5 uppercase tracking-wide">Légal</h4>
<ul className="space-y-2">
{[
{ href: "/mentions-legales", label: "Mentions légales" },
{ href: "/confidentialite", label: "Confidentialité" },
{ href: "/cgv", label: "CGV" },
].map((item) => (
<li key={item.href}>
<Link href={item.href} className="text-white/60 hover:text-white text-sm transition-colors">
{item.label}
</Link>
</li>
))}
</ul>
</div>
</div>
{/* Bottom */}
<div className="pt-6 flex flex-col md:flex-row items-center justify-between gap-3">
<p className="text-white/40 text-xs text-center md:text-left">
&copy; {new Date().getFullYear()} OBC Maçonnerie &mdash; Benoît Colin &middot; SIREN 531 827 871
</p>
<p className="text-text-muted text-xs text-center md:text-right">
Intervention : Douai &middot; Orchies &middot; Arleux &middot; Valenciennes
<p className="text-white/40 text-xs text-center md:text-right">
Orchies &middot; Mouchin &middot; Douai &middot; Valenciennes &middot; Saint-Amand-les-Eaux &mdash;{" "}
<span className="text-white/30">Site réalisé par HookLab</span>
</p>
</div>
</div>

View File

@@ -0,0 +1,148 @@
import Link from "next/link";
import Navbar from "@/components/marketing/Navbar";
import Footer from "@/components/marketing/Footer";
import ScrollReveal from "@/components/animations/ScrollReveal";
import ContactForm from "@/components/marketing/ContactForm";
interface LocalSEOPageProps {
ville: string;
departement?: string;
servicesPrincipaux: string[];
description: string;
texteIntro: string;
texteLocal: string;
distanceMouchin?: string;
}
const services = [
{ icon: "🏠", label: "Construction de maison", href: "/construction-maison" },
{ icon: "🔨", label: "Rénovation", href: "/renovation" },
{ icon: "💧", label: "Assainissement", href: "/assainissement" },
{ icon: "🚧", label: "Création d'accès", href: "/creation-acces" },
{ icon: "🏗️", label: "Démolition", href: "/demolition" },
];
export default function LocalSEOPage({
ville,
departement = "Nord (59)",
servicesPrincipaux,
description,
texteIntro,
texteLocal,
distanceMouchin,
}: LocalSEOPageProps) {
return (
<main id="main-content" className="min-h-screen">
<Navbar />
{/* Hero */}
<section className="bg-navy py-16 md:py-24">
<div className="max-w-5xl mx-auto px-4 sm:px-6">
<div className="max-w-2xl">
<ScrollReveal direction="up">
<div className="flex items-center gap-2 mb-4">
<span className="text-orange">📍</span>
<span className="text-white/60 text-sm">{ville} {departement}</span>
</div>
<h1 className="text-3xl md:text-5xl font-bold text-white mb-4 leading-tight">
Maçon {ville} Construction & Rénovation
</h1>
<p className="text-white/70 text-lg mb-8">{texteIntro}</p>
{distanceMouchin && (
<p className="text-white/40 text-sm mb-6 italic">
{distanceMouchin} de Mouchin (siège OBC Maçonnerie)
</p>
)}
<div className="flex flex-col sm:flex-row gap-4">
<Link href="/contact" className="inline-flex items-center justify-center gap-2 bg-orange hover:bg-orange-hover text-white font-bold px-7 py-3.5 rounded-xl transition-colors pulse-glow">
Demander un devis gratuit
</Link>
<a href="tel:0674453089" className="inline-flex items-center justify-center gap-2 bg-white/10 hover:bg-white/20 text-white font-semibold px-7 py-3.5 rounded-xl transition-colors border border-white/20">
06 74 45 30 89
</a>
</div>
</ScrollReveal>
</div>
</div>
</section>
{/* Services */}
<section className="py-14 bg-bg">
<div className="max-w-5xl mx-auto px-4 sm:px-6">
<ScrollReveal direction="up">
<h2 className="text-2xl font-bold text-navy mb-6 text-center">
Nos services à {ville}
</h2>
</ScrollReveal>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
{services.map((s, i) => (
<ScrollReveal key={s.label} direction="up" delay={i * 60}>
<Link
href={s.href}
className={`group block bg-bg-white border rounded-xl p-4 text-center transition-all hover:shadow-md ${
servicesPrincipaux.includes(s.label)
? "border-orange"
: "border-border hover:border-orange"
}`}
>
<div className="text-2xl mb-2">{s.icon}</div>
<p className="text-navy font-semibold text-xs group-hover:text-orange transition-colors leading-snug">
{s.label}
</p>
</Link>
</ScrollReveal>
))}
</div>
</div>
</section>
{/* Texte SEO local */}
<section className="py-14 bg-stone-bg">
<div className="max-w-3xl mx-auto px-4 sm:px-6">
<ScrollReveal direction="up">
<h2 className="text-2xl font-bold text-navy mb-5">
OBC Maçonnerie intervient à {ville}
</h2>
<div className="text-text-light text-sm leading-relaxed space-y-4">
{texteLocal.split("\n").map((para, i) => (
<p key={i}>{para}</p>
))}
</div>
<div className="mt-8 bg-bg-white border border-border rounded-xl p-5">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-navy rounded-lg flex items-center justify-center shrink-0">
<span className="text-white font-bold text-xs">OBC</span>
</div>
<div>
<p className="text-navy font-bold text-sm">Benoît Colin OBC Maçonnerie</p>
<p className="text-text-muted text-xs">221 Route de Saint-Amand, 59310 Mouchin</p>
<a href="tel:0674453089" className="text-orange font-bold text-sm hover:underline">
06 74 45 30 89
</a>
</div>
</div>
</div>
</ScrollReveal>
</div>
</section>
{/* Formulaire */}
<section className="py-14 bg-bg">
<div className="max-w-xl mx-auto px-4 sm:px-6">
<ScrollReveal direction="up">
<h2 className="text-2xl font-bold text-navy mb-2 text-center">
Votre projet à {ville}
</h2>
<p className="text-text-light text-sm text-center mb-8">Devis gratuit Réponse sous 24h</p>
</ScrollReveal>
<ScrollReveal direction="up" delay={100}>
<ContactForm />
</ScrollReveal>
</div>
</section>
<Footer />
</main>
);
}

View File

@@ -3,46 +3,58 @@
import { useState } from "react";
import Link from "next/link";
const navLinks = [
{ href: "/services", label: "Nos services" },
{ href: "/realisations", label: "Réalisations" },
{ href: "/partenaires", label: "Partenaires" },
{ href: "/contact", label: "Contact" },
];
export default function Navbar() {
const [open, setOpen] = useState(false);
return (
<nav className="sticky top-0 z-50 bg-bg-white/90 backdrop-blur-md border-b border-border" role="navigation" aria-label="Navigation principale">
<nav
className="sticky top-0 z-50 bg-bg-white/95 backdrop-blur-md border-b border-border"
role="navigation"
aria-label="Navigation principale"
>
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<Link href="/" className="flex items-center gap-2" aria-label="HookLab - Accueil">
<div className="w-9 h-9 bg-navy rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-base">H</span>
<Link href="/" className="flex items-center gap-2.5" aria-label="OBC Maçonnerie - Accueil">
<div className="w-9 h-9 bg-navy rounded-lg flex items-center justify-center shrink-0">
<span className="text-white font-bold text-sm">OBC</span>
</div>
<div className="flex flex-col leading-tight">
<span className="text-navy font-bold text-sm leading-none">OBC</span>
<span className="text-orange font-bold text-sm leading-none">Maçonnerie</span>
</div>
<span className="text-xl font-bold text-navy">
Hook<span className="text-orange">Lab</span>
</span>
</Link>
{/* Desktop links */}
<div className="hidden md:flex items-center gap-8">
<a href="#methode" className="text-text-light hover:text-navy text-sm font-medium transition-colors">
Notre M&eacute;thode
</a>
<a href="#exemples" className="text-text-light hover:text-navy text-sm font-medium transition-colors">
Exemples
</a>
<a href="#qui-suis-je" className="text-text-light hover:text-navy text-sm font-medium transition-colors">
Qui suis-je
</a>
<div className="hidden md:flex items-center gap-6">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="text-text-light hover:text-navy text-sm font-medium transition-colors"
>
{link.label}
</Link>
))}
</div>
{/* CTA desktop - Phone */}
{/* CTA desktop */}
<div className="hidden md:block">
<a
href="tel:+33604408157"
className="inline-flex items-center gap-2 bg-orange text-white font-bold text-sm px-5 py-2.5 rounded-xl hover:bg-orange/90 transition-colors"
href="tel:0674453089"
className="inline-flex items-center gap-2 bg-orange text-white font-bold text-sm px-5 py-2.5 rounded-xl hover:bg-orange-hover transition-colors pulse-glow"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
06 04 40 81 57
06 74 45 30 89
</a>
</div>
@@ -67,26 +79,29 @@ export default function Navbar() {
{/* Mobile menu */}
{open && (
<div className="md:hidden border-t border-border py-4 space-y-3">
<a href="#methode" onClick={() => setOpen(false)} className="block text-text-light hover:text-navy text-sm font-medium py-2 transition-colors">
Notre M&eacute;thode
</a>
<a href="#exemples" onClick={() => setOpen(false)} className="block text-text-light hover:text-navy text-sm font-medium py-2 transition-colors">
Exemples
</a>
<a href="#qui-suis-je" onClick={() => setOpen(false)} className="block text-text-light hover:text-navy text-sm font-medium py-2 transition-colors">
Qui suis-je
</a>
<a
href="tel:+33604408157"
onClick={() => setOpen(false)}
className="flex items-center justify-center gap-2 bg-orange text-white font-bold text-sm px-5 py-3 rounded-xl mt-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
06 04 40 81 57
</a>
<div className="md:hidden border-t border-border py-4 space-y-1">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setOpen(false)}
className="block text-text-light hover:text-navy text-sm font-medium py-2.5 px-2 rounded-lg hover:bg-bg-muted transition-colors"
>
{link.label}
</Link>
))}
<div className="pt-2">
<a
href="tel:0674453089"
onClick={() => setOpen(false)}
className="flex items-center justify-center gap-2 bg-orange text-white font-bold text-sm px-5 py-3 rounded-xl mt-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
Appeler Benoît 06 74 45 30 89
</a>
</div>
</div>
)}
</div>

View File

@@ -1,100 +0,0 @@
import Card from "@/components/ui/Card";
const personas = [
{
id: "jeune",
emoji: "🎓",
title: "Étudiant / Jeune actif",
subtitle: "18-25 ans",
description:
"Tu veux générer tes premiers revenus en ligne tout en étudiant ou en début de carrière. TikTok Shop est le levier parfait.",
benefits: [
"Flexibilité totale, travaille quand tu veux",
"Pas besoin de stock ni d'investissement",
"Compétences marketing valorisables sur ton CV",
"Communauté de jeunes entrepreneurs motivés",
],
},
{
id: "parent",
emoji: "👨‍👩‍👧",
title: "Parent / Reconversion",
subtitle: "25-45 ans",
description:
"Tu cherches un complément de revenus ou une reconversion flexible depuis chez toi. TikTok Shop s'adapte à ton emploi du temps.",
benefits: [
"2h par jour suffisent pour démarrer",
"Travaille depuis chez toi, à ton rythme",
"Revenus complémentaires dès le premier mois",
"Accompagnement personnalisé et bienveillant",
],
},
];
export default function PersonaCards() {
return (
<section className="py-20 md:py-32 bg-dark-light/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center max-w-2xl mx-auto mb-16">
<span className="inline-block px-3 py-1.5 bg-primary/10 border border-primary/20 rounded-full text-primary text-xs font-medium mb-4">
Pour qui ?
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-[-0.02em] mb-4">
Un programme adapté à{" "}
<span className="gradient-text">ton profil</span>
</h2>
<p className="text-white/60 text-lg">
Que tu sois étudiant ou parent, notre méthode s&apos;adapte à toi.
</p>
</div>
{/* Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
{personas.map((p) => (
<Card key={p.id} hover className="relative overflow-hidden">
{/* Gradient accent top */}
<div className="absolute top-0 left-0 right-0 h-1 gradient-bg" />
<div className="pt-2">
{/* Emoji + Title */}
<div className="text-4xl mb-4">{p.emoji}</div>
<h3 className="text-xl font-bold text-white mb-1">
{p.title}
</h3>
<p className="text-primary text-sm font-medium mb-3">
{p.subtitle}
</p>
<p className="text-white/60 text-sm mb-6 leading-relaxed">
{p.description}
</p>
{/* Benefits */}
<ul className="space-y-3">
{p.benefits.map((b, i) => (
<li key={i} className="flex items-start gap-3">
<svg
className="w-5 h-5 text-success mt-0.5 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span className="text-white/70 text-sm">{b}</span>
</li>
))}
</ul>
</div>
</Card>
))}
</div>
</div>
</section>
);
}

View File

@@ -1,281 +0,0 @@
import Link from "next/link";
import Button from "@/components/ui/Button";
import Card from "@/components/ui/Card";
const coachingFeatures = [
"8 semaines de coaching intensif",
"Acc\u00e8s \u00e0 tous les modules vid\u00e9o",
"Templates et scripts de contenu",
"Appels de groupe hebdomadaires",
"Support WhatsApp illimit\u00e9",
"Communaut\u00e9 priv\u00e9e d\u2019entrepreneurs",
"Mises \u00e0 jour \u00e0 vie du contenu",
"Certification HookLab",
];
const bonuses = [
"Liste de 50 produits gagnants TikTok Shop",
"Guide de l\u2019algorithme TikTok 2025-2026",
"Templates Canva pour miniatures",
];
const suiviFeatures = [
"R\u00e9ponses \u00e0 tes questions vid\u00e9os",
"Id\u00e9es de concepts et tendances",
"Coaching motivation au quotidien",
"Acc\u00e8s aux nouvelles mises \u00e0 jour",
"Groupe WhatsApp alumni",
];
export default function Pricing() {
return (
<section id="tarif" className="py-20 md:py-32">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center max-w-2xl mx-auto mb-16">
<span className="inline-block px-3 py-1.5 bg-primary/10 border border-primary/20 rounded-full text-primary text-xs font-medium mb-4">
Tarifs
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-[-0.02em] mb-4">
Investis dans ta{" "}
<span className="gradient-text">formation TikTok Shop</span>
</h2>
<p className="text-white/60 text-lg">
Un programme complet avec accompagnement personnalis&eacute;, et une
option de suivi mensuel pour continuer &agrave; progresser.
</p>
</div>
{/* Pricing cards */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 max-w-4xl mx-auto">
{/* Coaching card */}
<Card className="relative overflow-hidden border-primary/30">
{/* Popular badge */}
<div className="absolute top-0 left-0 right-0 gradient-bg py-2.5 text-center">
<span className="text-white text-sm font-semibold">
Programme principal &mdash; Places limit&eacute;es
</span>
</div>
<div className="pt-14">
<h3 className="text-lg font-bold text-white mb-1">
Formation + Coaching
</h3>
<p className="text-white/40 text-sm mb-6">
Programme intensif de 8 semaines
</p>
{/* Price */}
<div className="text-center mb-8">
<div className="flex items-baseline justify-center gap-2 mb-1">
<span className="text-white/40 text-2xl line-through">
690&euro;
</span>
<span className="text-5xl md:text-6xl font-bold text-white">
490&euro;
</span>
<span className="text-white/40 text-lg">/mois</span>
</div>
<p className="text-white/40 mt-2">
x2 mois (980&euro; total) &mdash; Paiement s&eacute;curis&eacute;
via Stripe
</p>
<div className="inline-flex items-center mt-3 px-3 py-1 bg-success/10 border border-success/20 rounded-full">
<span className="text-success text-sm font-medium">
&Eacute;conomise 400&euro; avec l&apos;offre de lancement
</span>
</div>
</div>
{/* Divider */}
<div className="border-t border-dark-border my-6" />
{/* Features */}
<div className="mb-6">
<p className="text-white/50 text-xs uppercase tracking-wider mb-4 font-medium">
Inclus dans le programme
</p>
<ul className="space-y-3">
{coachingFeatures.map((f, i) => (
<li key={i} className="flex items-center gap-3">
<div className="w-5 h-5 rounded-full bg-success/10 flex items-center justify-center shrink-0">
<svg
className="w-3 h-3 text-success"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<span className="text-white/80 text-sm">{f}</span>
</li>
))}
</ul>
</div>
{/* Bonus */}
<div className="bg-primary/5 border border-primary/10 rounded-2xl p-4 mb-6">
<p className="text-primary text-xs uppercase tracking-wider mb-3 font-medium">
Bonus inclus
</p>
<ul className="space-y-2">
{bonuses.map((b, i) => (
<li key={i} className="flex items-center gap-2">
<svg
className="w-4 h-4 text-primary shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"
/>
</svg>
<span className="text-white/70 text-sm">{b}</span>
</li>
))}
</ul>
</div>
{/* CTA */}
<Link href="/candidature">
<Button size="lg" className="w-full pulse-glow">
Candidater pour rejoindre HookLab
</Button>
</Link>
{/* Guarantee */}
<div className="flex items-center justify-center gap-2 mt-5">
<svg
className="w-5 h-5 text-success"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
<span className="text-white/40 text-sm">
Garantie satisfait ou rembours&eacute; 14 jours
</span>
</div>
<p className="text-center text-white/25 text-xs mt-3">
Candidature soumise &agrave; validation. R&eacute;ponse sous
24h.
</p>
</div>
</Card>
{/* Suivi card */}
<Card className="relative overflow-hidden border-dark-border">
<div className="absolute top-0 left-0 right-0 bg-dark-lighter py-2.5 text-center border-b border-dark-border">
<span className="text-white/60 text-sm font-semibold">
Apr&egrave;s la formation
</span>
</div>
<div className="pt-14">
<h3 className="text-lg font-bold text-white mb-1">
Suivi continu
</h3>
<p className="text-white/40 text-sm mb-6">
Pour ceux qui ont termin&eacute; le programme
</p>
{/* Price */}
<div className="text-center mb-8">
<div className="flex items-baseline justify-center gap-2 mb-1">
<span className="text-4xl md:text-5xl font-bold text-white">
49&euro;
</span>
<span className="text-white/40 text-lg">/mois</span>
</div>
<p className="text-white/40 mt-2">
Sans engagement &mdash; Annulable &agrave; tout moment
</p>
</div>
{/* Divider */}
<div className="border-t border-dark-border my-6" />
{/* Condition */}
<div className="bg-warning/5 border border-warning/15 rounded-xl p-3 mb-6">
<p className="text-warning text-xs font-medium flex items-center gap-2">
<svg className="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Accessible uniquement apr&egrave;s avoir termin&eacute; les 8 semaines de coaching
</p>
</div>
{/* Features */}
<div className="mb-6">
<p className="text-white/50 text-xs uppercase tracking-wider mb-4 font-medium">
Inclus dans le suivi
</p>
<ul className="space-y-3">
{suiviFeatures.map((f, i) => (
<li key={i} className="flex items-center gap-3">
<div className="w-5 h-5 rounded-full bg-success/10 flex items-center justify-center shrink-0">
<svg
className="w-3 h-3 text-success"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<span className="text-white/80 text-sm">{f}</span>
</li>
))}
</ul>
</div>
{/* Info */}
<div className="bg-dark border border-dark-border rounded-2xl p-4 mb-6">
<p className="text-white/50 text-sm leading-relaxed">
Le suivi mensuel te permet de continuer &agrave; progresser
apr&egrave;s ta formation initiale. Pose tes questions, re&ccedil;ois
des id&eacute;es de contenus et reste motiv&eacute; avec la
communaut&eacute; d&apos;anciens &eacute;l&egrave;ves.
</p>
</div>
{/* CTA disabled-style */}
<div className="opacity-60">
<Button size="lg" variant="secondary" className="w-full" disabled>
Disponible apr&egrave;s la formation
</Button>
</div>
<p className="text-center text-white/25 text-xs mt-3">
Le lien de souscription sera envoy&eacute; &agrave; la fin de
tes 8 semaines.
</p>
</div>
</Card>
</div>
</div>
</section>
);
}

View File

@@ -1,60 +0,0 @@
"use client";
import { useState, useEffect } from "react";
const notifications = [
{ name: "Mehdi L.", action: "a candidat\u00e9", time: "il y a 3 min" },
{ name: "Laura B.", action: "a rejoint le programme", time: "il y a 12 min" },
{ name: "Yanis K.", action: "a candidat\u00e9", time: "il y a 18 min" },
{ name: "Sarah M.", action: "a g\u00e9n\u00e9r\u00e9 sa 1\u00e8re commission", time: "il y a 1h" },
{ name: "Thomas D.", action: "a candidat\u00e9", time: "il y a 2h" },
{ name: "Amina K.", action: "a atteint 1 000\u20ac de commissions", time: "il y a 3h" },
{ name: "Julien R.", action: "a candidat\u00e9", time: "il y a 4h" },
{ name: "Fatima N.", action: "a rejoint le programme", time: "il y a 5h" },
];
export default function SocialProofTicker() {
const [current, setCurrent] = useState(0);
const [visible, setVisible] = useState(false);
useEffect(() => {
const showTimeout = setTimeout(() => setVisible(true), 5000);
return () => clearTimeout(showTimeout);
}, []);
useEffect(() => {
if (!visible) return;
const interval = setInterval(() => {
setVisible(false);
setTimeout(() => {
setCurrent((prev) => (prev + 1) % notifications.length);
setVisible(true);
}, 500);
}, 4000);
return () => clearInterval(interval);
}, [visible]);
const n = notifications[current];
return (
<div
className={`fixed bottom-4 left-4 z-50 transition-all duration-500 ${
visible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"
}`}
>
<div className="bg-dark-light border border-dark-border rounded-2xl p-4 shadow-2xl max-w-xs">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full gradient-bg flex items-center justify-center text-sm font-bold text-white shrink-0">
{n.name[0]}
</div>
<div>
<p className="text-white text-sm font-medium">
{n.name} <span className="text-white/60 font-normal">{n.action}</span>
</p>
<p className="text-white/40 text-xs">{n.time}</p>
</div>
</div>
</div>
</div>
);
}