feat(admin): visual inline editor for the homepage
All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 2m55s

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 <noreply@anthropic.com>
This commit is contained in:
ordinarthur 2026-04-21 18:53:45 +02:00
parent 1dbcef4660
commit b6fd89978e
8 changed files with 750 additions and 293 deletions

View File

@ -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<unknown>({ path })
const fileInput = useRef<HTMLInputElement | null>(null)
const [uploading, setUploading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [doc, setDoc] = useState<MediaDoc | null>(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<HTMLInputElement>) => {
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 (
<div
className="rebours-admin-home__img-slot"
onClick={onPick}
role="button"
tabIndex={0}
>
{doc?.url ? (
<img src={doc.url} alt={doc.alt ?? alt} />
) : (
<div className="rebours-admin-home__img-placeholder">{placeholder}</div>
)}
<div className="rebours-admin-home__img-overlay">
{error ? `Erreur: ${error}` : 'Cliquez pour changer'}
</div>
{uploading && <div className="rebours-admin-home__img-uploading">Upload</div>}
{doc?.url && (
<button
type="button"
className="rebours-admin-home__img-remove"
onClick={clear}
aria-label="Retirer l'image"
title="Retirer l'image"
>
</button>
)}
<input
ref={fileInput}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={onFile}
/>
</div>
)
}

View File

@ -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 (
<div className="rebours-admin-home">
<div className="rebours-admin-home__hint">
Cliquez sur un texte ou sur une image pour modifier. Les changements sont enregistrés automatiquement.
</div>
{/* ---------- Header (preview only) ---------- */}
<header className="rebours-admin-home__header">
<span className="rebours-admin-home__logo">REBOURS</span>
<nav className="rebours-admin-home__nav">
<span>COLLECTION_001</span>
<span>CONTACT</span>
<span className="rebours-admin-home__wip">
<span className="rebours-admin-home__blink"></span> W.I.P
</span>
</nav>
</header>
{/* ---------- HERO ---------- */}
<section className="rebours-admin-home__hero">
<div className="rebours-admin-home__hero-left">
<p className="rebours-admin-home__label">
<InlineEditable path="heroLabel" placeholder="// ARCHIVE_001 — 2026" />
</p>
<h1>
<InlineEditable
path="heroTitle"
as="span"
separator="|"
placeholder="REBOURS|STUDIO"
/>
</h1>
<p className="rebours-admin-home__hero-sub">
<InlineEditable
path="heroSubtitle"
as="span"
multiline
placeholder={'Mobilier d\u2019art contemporain.\nSpace Age × Memphis.'}
/>
</p>
<p className="rebours-admin-home__hero-sub rebours-admin-home__mono-sm">
<InlineEditable
path="heroStatus"
as="span"
multiline
placeholder={'STATUS: [PROTOTYPE EN COURS]\nCOLLECTION_001 — BIENTÔT DISPONIBLE'}
/>
</p>
</div>
<div className="rebours-admin-home__hero-right">
<HeroImageUploadSlot path="heroImage" alt="Hero REBOURS Studio" placeholder="Image hero (optionnel)" />
</div>
</section>
<hr />
{/* ---------- COLLECTION HEADER ---------- */}
<section className="rebours-admin-home__collection">
<div className="rebours-admin-home__collection-header">
<p className="rebours-admin-home__label">
<InlineEditable path="collectionLabel" placeholder="// COLLECTION_001" />
</p>
<span className="rebours-admin-home__label">
N OBJETS {' '}
<InlineEditable path="collectionCta" placeholder="CLIQUER POUR OUVRIR" />
</span>
</div>
<div className="rebours-admin-home__grid-preview">
<div className="rebours-admin-home__card-placeholder">PRODUIT 001</div>
<div className="rebours-admin-home__card-placeholder">PRODUIT 002</div>
<div className="rebours-admin-home__card-placeholder">PRODUIT 003</div>
</div>
<p className="rebours-admin-home__hint-inline">
Les produits sont gérés dans la section <strong>Produits</strong>.
</p>
</section>
{/* ---------- CONTACT ---------- */}
<section className="rebours-admin-home__newsletter">
<div className="rebours-admin-home__nl-left">
<p className="rebours-admin-home__label">
<InlineEditable path="contactLabel" placeholder="// CONTACT" />
</p>
<h2>
<InlineEditable
path="contactTitle"
as="span"
separator="|"
placeholder="UNE QUESTION ?|PARLONS-EN"
/>
</h2>
</div>
<div className="rebours-admin-home__nl-right">
<p className="rebours-admin-home__mono-sm">
<InlineEditable
path="contactDescription"
as="span"
multiline
placeholder={'Commandes sur mesure, questions techniques,\nou simplement dire bonjour.'}
/>
</p>
<div className="rebours-admin-home__whatsapp-btn">
<span className="rebours-admin-home__wa-icon"></span>
<InlineEditable
path="whatsappButtonText"
placeholder="CONTACTEZ-NOUS SUR WHATSAPP"
/>
</div>
<p className="rebours-admin-home__mono-sm">
<span className="rebours-admin-home__blink"></span>{' '}
<InlineEditable
path="contactResponseTime"
placeholder="RÉPONSE SOUS 24H"
/>
</p>
</div>
</section>
{/* ---------- FOOTER ---------- */}
<footer className="rebours-admin-home__footer">
<InlineEditable path="footerText" placeholder="© 2026 REBOURS STUDIO — PARIS" />
<nav>
<span className="rebours-admin-home__muted">INSTAGRAM</span>
&nbsp;/&nbsp;
<span className="rebours-admin-home__muted">CONTACT</span>
</nav>
</footer>
<HomeSettingsDrawer />
</div>
)
}

View File

@ -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<string>({ path })
return (
<div className="rebours-admin-settings__field">
<label>{label}</label>
<input
type="text"
value={value ?? ''}
placeholder={placeholder}
onChange={(e) => setValue(e.target.value)}
/>
</div>
)
}
function TextAreaInput({ path, label, placeholder }: { path: string; label: string; placeholder?: string }) {
const { value, setValue } = useField<string>({ path })
return (
<div className="rebours-admin-settings__field">
<label>{label}</label>
<textarea
rows={3}
value={value ?? ''}
placeholder={placeholder}
onChange={(e) => setValue(e.target.value)}
/>
</div>
)
}
export function HomeSettingsDrawer() {
return (
<details className="rebours-admin-settings">
<summary>Réglages avancés</summary>
<div className="rebours-admin-settings__grid">
<TextInput path="whatsappNumber" label="Numéro WhatsApp" placeholder="33651755191" />
<TextInput path="contactResponseTime" label="Délai de réponse" placeholder="RÉPONSE SOUS 24H" />
<TextInput path="instagramUrl" label="URL Instagram" placeholder="https://instagram.com/..." />
<TextInput path="seoTitle" label="Titre SEO" />
<TextAreaInput path="seoDescription" label="Description SEO" />
</div>
</details>
)
}

View File

@ -9,11 +9,26 @@ type Props = {
className?: string
multiline?: boolean
placeholder?: string
/**
* Stored separator that should display as a line-break in the editor.
* When set (e.g. '|'), the stored value "FOO|BAR" is shown as two lines,
* and newlines typed by the user are serialized back to the separator.
* Implies `multiline`.
*/
separator?: string
}
export function InlineEditable({ path, as: Tag = 'span', className, multiline, placeholder }: Props) {
export function InlineEditable({
path,
as: Tag = 'span',
className,
multiline,
placeholder,
separator,
}: Props) {
const { value, setValue } = useField<string>({ path })
const ref = useRef<HTMLElement | null>(null)
const isMultiline = multiline || !!separator
// Sync textContent from value. React doesn't reconcile children of
// contentEditable elements, so we drive the DOM imperatively — and
@ -22,19 +37,23 @@ export function InlineEditable({ path, as: Tag = 'span', className, multiline, p
const el = ref.current
if (!el) return
if (typeof document !== 'undefined' && document.activeElement === el) return
const next = value ?? ''
const raw = value ?? ''
// Convert stored separator to newlines for display.
const next = separator ? raw.split(separator).join('\n') : raw
if (el.textContent !== next) el.textContent = next
el.classList.toggle('rebours-editable--empty', !value)
}, [value])
el.classList.toggle('rebours-editable--empty', !raw)
}, [value, separator])
const commit = (e: React.FocusEvent<HTMLElement>) => {
const next = (e.currentTarget.textContent ?? '').replace(/\u00A0/g, ' ')
let next = (e.currentTarget.textContent ?? '').replace(/\u00A0/g, ' ')
// Serialize newlines back to the stored separator.
if (separator) next = next.replace(/\n/g, separator)
if (next !== (value ?? '')) setValue(next)
e.currentTarget.classList.toggle('rebours-editable--empty', !next)
}
const onKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
if (!multiline && e.key === 'Enter') {
if (!isMultiline && e.key === 'Enter') {
e.preventDefault()
;(e.currentTarget as HTMLElement).blur()
}

View File

@ -1,77 +0,0 @@
import type { ReactNode } from 'react'
type Props = {
indexNode: ReactNode
nameNode: ReactNode
typeNode: ReactNode
materialsNode: ReactNode
yearNode: ReactNode
statusNode: ReactNode
descriptionNode: ReactNode
specsNode: ReactNode
notesNode: ReactNode
imageUrl?: string | null
imageAlt?: string
}
export function ProductPanelInfo({
indexNode,
nameNode,
typeNode,
materialsNode,
yearNode,
statusNode,
descriptionNode,
specsNode,
notesNode,
imageUrl,
imageAlt,
}: Props) {
return (
<div className="panel-inner">
<div className="panel-img-col">
<div className="panel-gallery">
<img src={imageUrl ?? ''} alt={imageAlt ?? 'Image produit REBOURS Studio'} />
</div>
</div>
<div className="panel-info-col">
<p className="panel-index">{indexNode}</p>
<h2>{nameNode}</h2>
<hr />
<div className="panel-meta">
<div className="panel-meta-row">
<span className="meta-key">TYPE</span>
<span>{typeNode}</span>
</div>
<div className="panel-meta-row">
<span className="meta-key">MATÉRIAUX</span>
<span>{materialsNode}</span>
</div>
<div className="panel-meta-row">
<span className="meta-key">ANNÉE</span>
<span>{yearNode}</span>
</div>
<div className="panel-meta-row">
<span className="meta-key">STATUS</span>
<span className="red">{statusNode}</span>
</div>
</div>
<hr />
<p className="panel-desc">{descriptionNode}</p>
<hr />
<details className="accordion" open>
<summary>
SPÉCIFICATIONS TECHNIQUES <span></span>
</summary>
<div className="accordion-body">{specsNode}</div>
</details>
<details className="accordion" open>
<summary>
NOTES DE CONCEPTION <span></span>
</summary>
<div className="accordion-body">{notesNode}</div>
</details>
</div>
</div>
)
}

View File

@ -1,147 +0,0 @@
.rebours-visual-editor {
padding: 2rem;
background: #0a0a0a;
color: #ededed;
min-height: 100%;
font-family: 'Space Mono', ui-monospace, Menlo, monospace;
}
.rebours-visual-editor__hint {
max-width: 960px;
margin: 0 auto 1.5rem;
padding: 0.75rem 1rem;
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.03);
color: rgba(255, 255, 255, 0.6);
}
.rebours-visual-editor__stage {
max-width: 1200px;
margin: 0 auto;
background: #111;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.rebours-visual-editor .product-panel--static {
position: static;
width: 100%;
transform: none;
background: #111;
color: #ededed;
padding: 2rem;
}
.rebours-visual-editor .panel-inner {
display: grid;
grid-template-columns: minmax(260px, 1fr) 1fr;
gap: 2rem;
align-items: start;
}
.rebours-visual-editor .panel-img-col img {
width: 100%;
height: auto;
display: block;
background: #1a1a1a;
aspect-ratio: 1 / 1;
object-fit: cover;
}
.rebours-visual-editor .panel-info-col h2 {
font-size: 1.75rem;
line-height: 1.15;
margin: 0.25rem 0 0.75rem;
letter-spacing: 0.02em;
}
.rebours-visual-editor .panel-index {
font-size: 0.72rem;
letter-spacing: 0.12em;
opacity: 0.6;
margin: 0;
}
.rebours-visual-editor hr {
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.1);
margin: 0.75rem 0;
}
.rebours-visual-editor .panel-meta {
display: grid;
gap: 0.35rem;
font-size: 0.78rem;
}
.rebours-visual-editor .panel-meta-row {
display: grid;
grid-template-columns: 110px 1fr;
gap: 0.5rem;
}
.rebours-visual-editor .meta-key {
opacity: 0.5;
letter-spacing: 0.08em;
}
.rebours-visual-editor .red {
color: #ff3b2f;
}
.rebours-visual-editor .panel-desc {
font-size: 0.9rem;
line-height: 1.55;
}
.rebours-visual-editor .accordion {
margin-top: 0.5rem;
}
.rebours-visual-editor .accordion summary {
cursor: pointer;
font-size: 0.75rem;
letter-spacing: 0.08em;
padding: 0.5rem 0;
display: flex;
justify-content: space-between;
opacity: 0.85;
}
.rebours-visual-editor .accordion-body {
font-size: 0.82rem;
line-height: 1.55;
padding: 0.25rem 0 0.75rem;
white-space: pre-wrap;
}
.rebours-editable {
outline: none;
transition: box-shadow 120ms ease, background 120ms ease;
border-radius: 2px;
padding: 2px 4px;
margin: -2px -4px;
cursor: text;
}
.rebours-editable:hover {
background: rgba(255, 255, 255, 0.04);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
}
.rebours-editable:focus {
background: rgba(255, 255, 255, 0.06);
box-shadow: inset 0 0 0 1px #ff3b2f;
}
.rebours-editable--empty::before {
content: attr(data-placeholder);
opacity: 0.3;
pointer-events: none;
}
.rebours-editable--empty:focus::before {
content: none;
}

View File

@ -0,0 +1,371 @@
/* Scoped replica of the public homepage, for the Payload admin edit view.
Root class: .rebours-admin-home */
.rebours-admin-home {
--clr-bg: #c8c8c8;
--clr-black: #111;
--clr-white: #dcdcdc;
--clr-card-bg: #d0d0d0;
--clr-amber: #e8a800;
--font-mono: 'Space Mono', ui-monospace, Menlo, monospace;
--border: 1px solid #111;
--pad: 2rem;
position: relative;
width: 100%;
background: var(--clr-bg);
color: var(--clr-black);
font-family: var(--font-mono);
}
/* Neutralize admin crosshair cursor leaks */
.rebours-admin-home,
.rebours-admin-home * {
cursor: auto;
}
.rebours-admin-home__hint {
padding: 0.65rem var(--pad);
font-size: 0.7rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #555;
background: #b6b6b6;
border-bottom: var(--border);
}
.rebours-admin-home__hint-inline {
padding: 0.75rem var(--pad) 1.25rem;
font-size: 0.7rem;
letter-spacing: 0.04em;
color: #555;
margin: 0;
}
/* ---- HEADER ---- */
.rebours-admin-home__header {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 1.1rem var(--pad);
border-bottom: var(--border);
background: var(--clr-bg);
}
.rebours-admin-home__logo {
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.18em;
}
.rebours-admin-home__nav {
display: flex;
gap: 2.5rem;
font-size: 0.82rem;
color: var(--clr-black);
}
.rebours-admin-home__wip {
color: #555;
font-size: 0.78rem;
}
.rebours-admin-home__blink {
color: var(--clr-amber);
animation: rebours-blink 1.4s step-end infinite;
}
@keyframes rebours-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* ---- LABELS & UTILS ---- */
.rebours-admin-home__label {
font-size: 0.75rem;
color: #555;
letter-spacing: 0.04em;
margin: 0;
}
.rebours-admin-home__mono-sm {
font-size: 0.78rem;
line-height: 1.9;
color: #555;
margin: 0;
white-space: pre-line;
}
.rebours-admin-home__muted {
color: #555;
}
.rebours-admin-home hr {
border: none;
border-top: var(--border);
margin: 0;
}
/* ---- HERO ---- */
.rebours-admin-home__hero {
display: grid;
grid-template-columns: 1fr 1fr;
min-height: 460px;
}
.rebours-admin-home__hero-left {
padding: 3rem var(--pad);
border-right: var(--border);
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 1.4rem;
overflow: hidden;
}
.rebours-admin-home__hero-left h1 {
font-size: clamp(2.6rem, 5vw, 4.5rem);
font-weight: 700;
line-height: 0.95;
letter-spacing: -0.02em;
margin: 0;
}
.rebours-admin-home__hero-left h1 .rebours-editable {
white-space: pre-line;
display: inline-block;
}
.rebours-admin-home__hero-sub {
font-size: 0.85rem;
line-height: 1.75;
max-width: 360px;
margin: 0;
white-space: pre-line;
}
.rebours-admin-home__hero-right {
background: #1c1c1c;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
min-height: 460px;
}
/* ---- IMAGE SLOT ---- */
.rebours-admin-home__img-slot {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.rebours-admin-home__img-slot img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
opacity: 0.92;
transition: opacity 0.15s;
}
.rebours-admin-home__img-slot:hover img {
opacity: 0.7;
}
.rebours-admin-home__img-placeholder {
color: rgba(255, 255, 255, 0.5);
font-size: 0.78rem;
letter-spacing: 0.1em;
text-align: center;
padding: 2rem;
border: 1px dashed rgba(255, 255, 255, 0.25);
}
.rebours-admin-home__img-overlay {
position: absolute;
inset: auto 0 0 0;
padding: 0.75rem 1rem;
background: rgba(0, 0, 0, 0.65);
color: #fff;
font-size: 0.7rem;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0;
transition: opacity 0.15s;
text-align: center;
}
.rebours-admin-home__img-slot:hover .rebours-admin-home__img-overlay {
opacity: 1;
}
.rebours-admin-home__img-uploading {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.75);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
letter-spacing: 0.1em;
}
.rebours-admin-home__img-remove {
position: absolute;
top: 0.75rem;
right: 0.75rem;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.65);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.25);
font-family: var(--font-mono);
font-size: 0.85rem;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, background 0.15s;
z-index: 2;
}
.rebours-admin-home__img-slot:hover .rebours-admin-home__img-remove {
opacity: 1;
}
.rebours-admin-home__img-remove:hover {
background: #c0392b;
}
/* ---- COLLECTION ---- */
.rebours-admin-home__collection {
background: var(--clr-bg);
}
.rebours-admin-home__collection-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem var(--pad);
}
.rebours-admin-home__grid-preview {
display: grid;
grid-template-columns: repeat(3, 1fr);
border-top: 2px solid #111;
border-left: 2px solid #111;
}
.rebours-admin-home__card-placeholder {
border-right: 2px solid #111;
border-bottom: 2px solid #111;
background: var(--clr-card-bg);
aspect-ratio: 1 / 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.72rem;
color: #777;
letter-spacing: 0.08em;
font-weight: 700;
}
/* ---- NEWSLETTER / CONTACT ---- */
.rebours-admin-home__newsletter {
display: grid;
grid-template-columns: 1fr 1fr;
border-top: var(--border);
}
.rebours-admin-home__nl-left {
padding: 3rem var(--pad);
border-right: var(--border);
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 1rem;
}
.rebours-admin-home__nl-left h2 {
font-size: clamp(1.8rem, 3.5vw, 2.6rem);
font-weight: 700;
line-height: 0.95;
letter-spacing: -0.02em;
margin: 0;
}
.rebours-admin-home__nl-left h2 .rebours-editable {
white-space: pre-line;
display: inline-block;
}
.rebours-admin-home__nl-right {
padding: 3rem var(--pad);
display: flex;
flex-direction: column;
gap: 0.7rem;
justify-content: flex-end;
}
.rebours-admin-home__whatsapp-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.7rem;
width: 100%;
background: #25D366;
color: #fff;
border: 2px solid #111;
font-family: var(--font-mono);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
padding: 1.1rem 1.5rem;
text-align: center;
}
.rebours-admin-home__wa-icon {
font-size: 0.9rem;
}
/* ---- FOOTER ---- */
.rebours-admin-home__footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.1rem var(--pad);
border-top: var(--border);
font-size: 0.75rem;
background: var(--clr-bg);
}
.rebours-admin-home__footer nav {
color: var(--clr-black);
}
/* ---- Responsive ---- */
@media (max-width: 900px) {
.rebours-admin-home__hero,
.rebours-admin-home__newsletter {
grid-template-columns: 1fr;
}
.rebours-admin-home__hero-left,
.rebours-admin-home__nl-left {
border-right: none;
border-bottom: var(--border);
}
.rebours-admin-home__hero-right {
min-height: 280px;
}
.rebours-admin-home__grid-preview {
grid-template-columns: 1fr 1fr;
}
}

View File

@ -5,76 +5,52 @@ export const HomePage: GlobalConfig = {
label: 'Page d\u2019accueil',
admin: {
group: 'Contenu',
livePreview: {
url: () => `${process.env.NEXT_PUBLIC_SERVER_URL ?? 'http://localhost:3000'}/?preview=true`,
},
},
access: {
read: () => true,
},
fields: [
{
type: 'tabs',
tabs: [
{
label: 'Hero',
fields: [
{ name: 'heroLabel', label: 'Label', type: 'text', defaultValue: '// ARCHIVE_001 — 2026' },
{
name: 'heroTitle',
label: 'Titre',
type: 'text',
admin: { description: 'Utilisez | pour passer à la ligne' },
},
{ name: 'heroSubtitle', label: 'Sous-titre', type: 'textarea' },
{ name: 'heroStatus', label: 'Statut', type: 'textarea' },
{
name: 'heroImage',
label: 'Image hero',
type: 'upload',
relationTo: 'media',
admin: { description: 'Si vide, utilise la première image de la collection' },
},
],
name: 'previewPanel',
type: 'ui',
admin: {
components: {
Field: '@/components/admin/HomePreviewEditor#default',
},
{
label: 'Collection',
fields: [
{ name: 'collectionLabel', type: 'text' },
{ name: 'collectionCta', label: 'Texte CTA', type: 'text', defaultValue: 'CLIQUER POUR OUVRIR' },
],
},
{
label: 'Contact',
fields: [
{ name: 'contactLabel', type: 'text' },
{
name: 'contactTitle',
label: 'Titre',
type: 'text',
admin: { description: 'Utilisez | pour passer à la ligne' },
},
{ name: 'contactDescription', type: 'textarea' },
{ name: 'whatsappNumber', label: 'Numéro WhatsApp', type: 'text', required: true, defaultValue: '33651755191' },
{ name: 'whatsappButtonText', type: 'text' },
{ name: 'contactResponseTime', type: 'text' },
],
},
{
label: 'Footer',
fields: [
{ name: 'footerText', type: 'text' },
{ name: 'instagramUrl', type: 'text' },
],
},
{
label: 'SEO',
fields: [
{ name: 'seoTitle', type: 'text' },
{ name: 'seoDescription', type: 'textarea' },
],
},
],
},
},
// ----- Hero -----
{ name: 'heroLabel', label: 'Label', type: 'text', defaultValue: '// ARCHIVE_001 — 2026', admin: { hidden: true } },
{ name: 'heroTitle', label: 'Titre', type: 'text', admin: { hidden: true } },
{ name: 'heroSubtitle', label: 'Sous-titre', type: 'textarea', admin: { hidden: true } },
{ name: 'heroStatus', label: 'Statut', type: 'textarea', admin: { hidden: true } },
{
name: 'heroImage',
label: 'Image hero',
type: 'upload',
relationTo: 'media',
admin: { hidden: true },
},
// ----- Collection -----
{ name: 'collectionLabel', type: 'text', admin: { hidden: true } },
{ name: 'collectionCta', label: 'Texte CTA', type: 'text', defaultValue: 'CLIQUER POUR OUVRIR', admin: { hidden: true } },
// ----- Contact -----
{ name: 'contactLabel', type: 'text', admin: { hidden: true } },
{ name: 'contactTitle', label: 'Titre', type: 'text', admin: { hidden: true } },
{ name: 'contactDescription', type: 'textarea', admin: { hidden: true } },
{ name: 'whatsappNumber', label: 'Numéro WhatsApp', type: 'text', required: true, defaultValue: '33651755191', admin: { hidden: true } },
{ name: 'whatsappButtonText', type: 'text', admin: { hidden: true } },
{ name: 'contactResponseTime', type: 'text', admin: { hidden: true } },
// ----- Footer -----
{ name: 'footerText', type: 'text', admin: { hidden: true } },
{ name: 'instagramUrl', type: 'text', admin: { hidden: true } },
// ----- SEO -----
{ name: 'seoTitle', type: 'text', admin: { hidden: true } },
{ name: 'seoDescription', type: 'textarea', admin: { hidden: true } },
],
}