diff --git a/app/admin/images/page.tsx b/app/admin/images/page.tsx new file mode 100644 index 0000000..198d59a --- /dev/null +++ b/app/admin/images/page.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { useEffect, useState } from "react"; + +interface SiteImage { + key: string; + url: string; + label: string; + updated_at: string | null; +} + +export default function AdminImages() { + const [images, setImages] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(null); + const [editUrls, setEditUrls] = useState>({}); + const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); + + useEffect(() => { + fetch("/api/admin/site-images") + .then((r) => r.json()) + .then((data) => { + setImages(data.images || []); + const urls: Record = {}; + for (const img of data.images || []) { + urls[img.key] = img.url; + } + setEditUrls(urls); + }) + .finally(() => setLoading(false)); + }, []); + + const handleSave = async (key: string) => { + setSaving(key); + setMessage(null); + try { + const res = await fetch("/api/admin/site-images", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ key, url: editUrls[key] }), + }); + const data = await res.json(); + if (res.ok) { + setMessage({ type: "success", text: `Image "${key}" mise à jour !` }); + setImages((prev) => + prev.map((img) => + img.key === key ? { ...img, url: editUrls[key], updated_at: new Date().toISOString() } : img + ) + ); + } else { + setMessage({ type: "error", text: data.error || "Erreur" }); + } + } catch { + setMessage({ type: "error", text: "Erreur réseau" }); + } + setSaving(null); + }; + + if (loading) { + return ( +
+

Images du site

+

Chargement...

+
+ ); + } + + return ( +
+
+

Images du site

+

+ Changez les URLs des images affichées sur le site. Collez un lien Noelshack, Unsplash, ou tout autre hébergeur d'images. +

+
+ + {message && ( +
+ {message.text} +
+ )} + + {/* Info SQL */} +
+

+ Si la sauvegarde échoue, créez la table dans Supabase (SQL Editor) : +

+
+{`CREATE TABLE site_images (
+  key TEXT PRIMARY KEY,
+  url TEXT NOT NULL,
+  label TEXT,
+  updated_at TIMESTAMPTZ DEFAULT NOW()
+);`}
+        
+
+ +
+ {images.map((img) => ( +
+
+ {/* Preview */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {img.label} { + (e.target as HTMLImageElement).src = + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23666' viewBox='0 0 24 24'%3E%3Cpath d='M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z'/%3E%3C/svg%3E"; + }} + /> +
+ + {/* Form */} +
+
+

{img.label}

+ {img.key} +
+ {img.updated_at && ( +

+ Modifié le{" "} + {new Date(img.updated_at).toLocaleDateString("fr-FR", { + day: "numeric", + month: "short", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + })} +

+ )} +
+ setEditUrls((prev) => ({ ...prev, [img.key]: e.target.value }))} + placeholder="https://..." + className="flex-1 px-4 py-2.5 bg-black/30 border border-dark-border rounded-xl text-white text-sm placeholder:text-white/20 focus:border-orange focus:ring-1 focus:ring-orange outline-none" + /> + +
+
+
+
+ ))} +
+
+ ); +} diff --git a/app/api/admin/site-images/route.ts b/app/api/admin/site-images/route.ts new file mode 100644 index 0000000..7f77b5a --- /dev/null +++ b/app/api/admin/site-images/route.ts @@ -0,0 +1,96 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createClient, createAdminClient } from "@/lib/supabase/server"; +import { DEFAULT_IMAGES, updateSiteImage } from "@/lib/site-images"; +import type { Profile } from "@/types/database.types"; + +interface SiteImageRow { + key: string; + url: string; + label: string | null; + updated_at: string; +} + +async function checkAdmin() { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) return false; + + const adminClient = createAdminClient(); + const { data: profile } = await adminClient + .from("profiles") + .select("is_admin") + .eq("id", user.id) + .single(); + + return (profile as Pick | null)?.is_admin === true; +} + +// GET - Récupérer toutes les images +export async function GET() { + const isAdmin = await checkAdmin(); + if (!isAdmin) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + try { + const supabase = createAdminClient(); + const { data } = await supabase.from("site_images").select("*"); + const rows = (data ?? []) as unknown as SiteImageRow[]; + + // Merge defaults avec les valeurs en base + const images = Object.entries(DEFAULT_IMAGES).map(([key, def]) => { + const saved = rows.find((d) => d.key === key); + return { + key, + url: saved?.url || def.url, + label: def.label, + updated_at: saved?.updated_at || null, + }; + }); + + return NextResponse.json({ images }); + } catch { + // Si la table n'existe pas, retourner les defaults + const images = Object.entries(DEFAULT_IMAGES).map(([key, def]) => ({ + key, + url: def.url, + label: def.label, + updated_at: null, + })); + return NextResponse.json({ images }); + } +} + +// PUT - Mettre à jour une image +export async function PUT(request: NextRequest) { + const isAdmin = await checkAdmin(); + if (!isAdmin) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } + + const body = await request.json(); + const { key, url } = body; + + if (!key || !url) { + return NextResponse.json({ error: "key et url requis" }, { status: 400 }); + } + + // Vérifier que l'URL est valide + try { + new URL(url); + } catch { + return NextResponse.json({ error: "URL invalide" }, { status: 400 }); + } + + const success = await updateSiteImage(key, url); + if (!success) { + return NextResponse.json( + { error: "Erreur lors de la sauvegarde. Vérifiez que la table site_images existe dans Supabase." }, + { status: 500 } + ); + } + + return NextResponse.json({ success: true }); +} diff --git a/app/page.tsx b/app/page.tsx index ac75e67..8aa6022 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,19 +1,19 @@ import Navbar from "@/components/marketing/Navbar"; import Hero from "@/components/marketing/Hero"; import Problematique from "@/components/marketing/Problematique"; -import System from "@/components/marketing/System"; +import Process from "@/components/marketing/Process"; import DemosLive from "@/components/marketing/DemosLive"; import AboutMe from "@/components/marketing/AboutMe"; import FAQ from "@/components/marketing/FAQ"; import Contact from "@/components/marketing/Contact"; import Footer from "@/components/marketing/Footer"; -import { getSiteSettings } from "@/lib/sanity/queries"; +import { getSiteImages } from "@/lib/site-images"; -// Revalider les données Sanity toutes les 60 secondes +// Revalider les images toutes les 60 secondes export const revalidate = 60; export default async function LandingPage() { - const siteSettings = await getSiteSettings(); + const images = await getSiteImages(); return (
@@ -21,19 +21,19 @@ export default async function LandingPage() { {/* Hero - Le Choc Visuel */} - + {/* La Problématique - L'Identification */} - {/* La Solution HookLab Tech */} - + {/* Le Triptyque HookLab - Les 3 Piliers */} + {/* Démos Live - 3 Dossiers de Confiance */} - + - {/* Qui suis-je - Ancrage Local (Sanity) */} - + {/* Qui suis-je - Ancrage Local */} + {/* FAQ - Objections */} diff --git a/components/admin/AdminShell.tsx b/components/admin/AdminShell.tsx index ec190ea..91aba46 100644 --- a/components/admin/AdminShell.tsx +++ b/components/admin/AdminShell.tsx @@ -40,6 +40,15 @@ const navItems = [ ), }, + { + label: "Images du site", + href: "/admin/images", + icon: ( + + + + ), + }, ]; export default function AdminShell({ children, adminName, adminEmail }: AdminShellProps) { diff --git a/components/marketing/AboutMe.tsx b/components/marketing/AboutMe.tsx index 0a4a19b..c045abc 100644 --- a/components/marketing/AboutMe.tsx +++ b/components/marketing/AboutMe.tsx @@ -1,139 +1,114 @@ "use client"; -import Image from "next/image"; -import { urlFor } from "@/lib/sanity/client"; -import type { SiteSettings } from "@/lib/sanity/queries"; import ScrollReveal from "@/components/animations/ScrollReveal"; import AnimatedCounter from "@/components/animations/AnimatedCounter"; interface AboutMeProps { - settings?: SiteSettings | null; + images?: Record; } -export default function AboutMe({ settings }: AboutMeProps) { - const name = settings?.ownerName || "Enguerrand"; - const bio = settings?.ownerBio; - const address = settings?.address || "Flines-lez-Raches, Nord (59)"; - const lat = settings?.lat || 50.4267; - const lng = settings?.lng || 3.2372; - const photoUrl = settings?.ownerPhoto ? urlFor(settings.ownerPhoto)?.width(400).height(480).url() : null; +export default function AboutMe({ images }: AboutMeProps) { + const photoUrl = images?.about_photo; return ( -
-
- {/* Header */} +
+ {/* Subtle pattern */} +
+
+
+
+ +
+ {/* Stats top row */} -
- - Votre expert local - -

- Pas une plateforme anonyme.{" "} - Un voisin. -

+
+ {[ + { value: 100, suffix: "%", label: "Local Nord" }, + { value: 24, suffix: "h", label: "Délai de réponse" }, + { value: 0, suffix: "€", label: "L'audit" }, + { value: 3, suffix: "", label: "Piliers du système" }, + ].map((stat, i) => ( +
+

+ +

+

{stat.label}

+
+ ))}
-
- {/* Left - Photo */} + {/* Content */} +
+ {/* Photo */}
-
+
{photoUrl ? ( - {`Photo + // eslint-disable-next-line @next/next/no-img-element + Enguerrand Ozano ) : ( -
-
- - - +
+
+
+ + + +
+

Votre photo ici

+

(modifiable dans Admin > Images)

-

Votre photo ici

-

(configurable via Sanity)

)}
-
- Basé à {address.split(",")[0]} +
+ Basé à Flines-lez-Raches
- {/* Right - Text */} + {/* Text */}
- {bio ? ( -

- {bio} -

- ) : ( - <> -

- Je suis {name}, spécialisé dans la - visibilité locale et la construction de{" "} - systèmes de confiance en ligne{" "} - pour les TPE/PME du Nord. -

-

- Je ne suis pas un call center parisien. Je connais la réalité de vos - chantiers à Douai, Orchies ou Valenciennes. Je sais que vous n’avez pas - le temps de gérer “un truc internet” et que vous voulez des résultats - concrets : des appels de vrais clients. -

- - )} -

- Mon approche : je vous construis un dossier de confiance{" "} - (Google + site + preuves) qui transforme votre bouche-à-oreille en système + + Votre expert local + +

+ Pas une plateforme anonyme.{" "} + Un voisin. +

+

+ Je suis Enguerrand, spécialisé dans la + visibilité locale et la construction de{" "} + systèmes de confiance en ligne{" "} + pour les artisans du Nord. +

+

+ Je ne suis pas un call center parisien. Je connais la réalité de vos + chantiers à Douai, Orchies ou Valenciennes. Je sais que vous n’avez pas + le temps de gérer “un truc internet” et que vous voulez des résultats + concrets : des appels de vrais clients. +

+

+ Mon approche : je vous construis un système complet{" "} + (Google + Facebook + Site) qui transforme votre bouche-à-oreille en système permanent. Pas de jargon, pas de blabla — du concret.

-
-
-

- -

-

Local Nord

-
-
-

- -

-

Délai de réponse

-
-
+ + Discutons de votre situation + + + +
- - {/* Map */} - -
-
- - - - - Zone d’intervention : Douai, Orchies, Arleux, Valenciennes et environs -
-
-