From b6fd89978eb6ed55c37113da95e1f34d75ed07fc Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Tue, 21 Apr 2026 18:53:45 +0200 Subject: [PATCH] feat(admin): visual inline editor for the homepage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Page d'accueil global now uses the same edit-in-place pattern as products: hero (with image upload), collection header, contact, and footer are all clickable-to-edit directly on a scoped replica of the live page. Technical fields (WhatsApp number, Instagram URL, response time, SEO) move to a "Réglages avancés" drawer. - HomePreviewEditor + home-panel.css mirror the public layout - HeroImageUploadSlot: single-image click-to-replace variant - HomeSettingsDrawer for non-visual fields - InlineEditable: add `separator` prop so stored `|` line-breaks render as newlines in the editor and roundtrip on save (used for heroTitle, contactTitle) - Remove unused ProductPanelInfo + editor.css Co-Authored-By: Claude Opus 4.7 --- .../components/admin/HeroImageUploadSlot.tsx | 127 ++++++ .../components/admin/HomePreviewEditor.tsx | 140 +++++++ .../components/admin/HomeSettingsDrawer.tsx | 48 +++ .../src/components/admin/InlineEditable.tsx | 31 +- .../src/components/admin/ProductPanelInfo.tsx | 77 ---- nextjs/src/components/admin/editor.css | 147 ------- nextjs/src/components/admin/home-panel.css | 371 ++++++++++++++++++ nextjs/src/globals/HomePage.ts | 102 ++--- 8 files changed, 750 insertions(+), 293 deletions(-) create mode 100644 nextjs/src/components/admin/HeroImageUploadSlot.tsx create mode 100644 nextjs/src/components/admin/HomePreviewEditor.tsx create mode 100644 nextjs/src/components/admin/HomeSettingsDrawer.tsx delete mode 100644 nextjs/src/components/admin/ProductPanelInfo.tsx delete mode 100644 nextjs/src/components/admin/editor.css create mode 100644 nextjs/src/components/admin/home-panel.css diff --git a/nextjs/src/components/admin/HeroImageUploadSlot.tsx b/nextjs/src/components/admin/HeroImageUploadSlot.tsx new file mode 100644 index 0000000..14ce458 --- /dev/null +++ b/nextjs/src/components/admin/HeroImageUploadSlot.tsx @@ -0,0 +1,127 @@ +'use client' + +import { useField } from '@payloadcms/ui' +import { useEffect, useRef, useState } from 'react' + +type MediaDoc = { id: number | string; url?: string | null; alt?: string | null } + +function extractMediaId(value: unknown): string | number | null { + if (value == null) return null + if (typeof value === 'number' || typeof value === 'string') return value + if (typeof value === 'object' && 'id' in (value as object)) { + return (value as { id: number | string }).id ?? null + } + return null +} + +type Props = { + path: string + placeholder?: string + alt?: string +} + +/** Single-upload click-to-replace slot for the hero image. */ +export function HeroImageUploadSlot({ path, placeholder = 'Cliquez pour uploader une image', alt = 'Hero' }: Props) { + const { value, setValue } = useField({ path }) + const fileInput = useRef(null) + const [uploading, setUploading] = useState(false) + const [error, setError] = useState(null) + const [doc, setDoc] = useState(null) + + const mediaId = extractMediaId(value) + + // Fetch current media doc to display its URL. + useEffect(() => { + let cancelled = false + if (mediaId == null) { + setDoc(null) + return + } + if (doc && String(doc.id) === String(mediaId)) return + ;(async () => { + try { + const res = await fetch(`/api/media/${mediaId}`, { credentials: 'include' }) + if (!res.ok) return + const fetched = await res.json() + if (cancelled) return + setDoc({ id: fetched.id, url: fetched.url, alt: fetched.alt }) + } catch { + /* ignore */ + } + })() + return () => { + cancelled = true + } + }, [mediaId, doc]) + + const onPick = () => fileInput.current?.click() + + const onFile = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + e.target.value = '' + if (!file) return + setError(null) + setUploading(true) + try { + const fd = new FormData() + fd.append('file', file) + fd.append('_payload', JSON.stringify({ alt: alt || file.name })) + const res = await fetch('/api/media', { method: 'POST', body: fd, credentials: 'include' }) + if (!res.ok) throw new Error(`upload ${res.status}`) + const { doc: uploaded } = await res.json() + setDoc({ id: uploaded.id, url: uploaded.url, alt: uploaded.alt }) + setValue(uploaded.id) + } catch (err) { + setError(err instanceof Error ? err.message : 'upload failed') + } finally { + setUploading(false) + } + } + + const clear = (e: React.MouseEvent) => { + e.stopPropagation() + setValue(null) + setDoc(null) + } + + return ( +
+ {doc?.url ? ( + {doc.alt + ) : ( +
{placeholder}
+ )} + +
+ {error ? `Erreur: ${error}` : 'Cliquez pour changer'} +
+ + {uploading &&
Upload…
} + + {doc?.url && ( + + )} + + +
+ ) +} diff --git a/nextjs/src/components/admin/HomePreviewEditor.tsx b/nextjs/src/components/admin/HomePreviewEditor.tsx new file mode 100644 index 0000000..dd9899b --- /dev/null +++ b/nextjs/src/components/admin/HomePreviewEditor.tsx @@ -0,0 +1,140 @@ +'use client' + +import { HeroImageUploadSlot } from './HeroImageUploadSlot' +import { HomeSettingsDrawer } from './HomeSettingsDrawer' +import { InlineEditable } from './InlineEditable' +import './home-panel.css' + +export default function HomePreviewEditor() { + return ( +
+
+ Cliquez sur un texte ou sur une image pour modifier. Les changements sont enregistrés automatiquement. +
+ + {/* ---------- Header (preview only) ---------- */} +
+ REBOURS + +
+ + {/* ---------- HERO ---------- */} +
+
+

+ +

+

+ +

+

+ +

+

+ +

+
+
+ +
+
+ +
+ + {/* ---------- COLLECTION HEADER ---------- */} +
+
+

+ +

+ + N OBJETS —{' '} + + +
+
+
PRODUIT 001
+
PRODUIT 002
+
PRODUIT 003
+
+

+ ↳ Les produits sont gérés dans la section Produits. +

+
+ + {/* ---------- CONTACT ---------- */} +
+
+

+ +

+

+ +

+
+
+

+ +

+
+ + +
+

+ {' '} + +

+
+
+ + {/* ---------- FOOTER ---------- */} +
+ + +
+ + +
+ ) +} diff --git a/nextjs/src/components/admin/HomeSettingsDrawer.tsx b/nextjs/src/components/admin/HomeSettingsDrawer.tsx new file mode 100644 index 0000000..4d9b6b3 --- /dev/null +++ b/nextjs/src/components/admin/HomeSettingsDrawer.tsx @@ -0,0 +1,48 @@ +'use client' + +import { useField } from '@payloadcms/ui' + +function TextInput({ path, label, placeholder }: { path: string; label: string; placeholder?: string }) { + const { value, setValue } = useField({ path }) + return ( +
+ + setValue(e.target.value)} + /> +
+ ) +} + +function TextAreaInput({ path, label, placeholder }: { path: string; label: string; placeholder?: string }) { + const { value, setValue } = useField({ path }) + return ( +
+ +