All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 2m39s
Overriding admin.components.views.edit.default makes /admin/collections/ products/:id and /create render the product-detail panel directly — text fields are contentEditable, the image is click-to-upload, and price is inline-editable in the checkout-price-line. Fields that don't fit the public template (slug, name, currency, availability, SEO, isPublished, sortOrder, stripeID) live in a collapsible "Réglages avancés" drawer below the panel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
'use client'
|
|
|
|
import { useField } from '@payloadcms/ui'
|
|
import { useRef, useState } from 'react'
|
|
|
|
type MediaDoc = { id: number | string; url?: string | null; alt?: string | null }
|
|
type ImageEntry = { id?: string; image?: MediaDoc | string | null } | null | undefined
|
|
|
|
type Props = {
|
|
displayName?: string
|
|
}
|
|
|
|
function resolveFirstImage(entries: ImageEntry[] | undefined | null): { url: string; alt: string } {
|
|
const first = entries?.[0]?.image
|
|
if (!first) return { url: '', alt: '' }
|
|
if (typeof first === 'string') return { url: '', alt: '' }
|
|
return { url: first.url ?? '', alt: first.alt ?? '' }
|
|
}
|
|
|
|
export function ImageUploadSlot({ displayName }: Props) {
|
|
const { value: images, setValue } = useField<ImageEntry[]>({ path: 'images' })
|
|
const fileInput = useRef<HTMLInputElement | null>(null)
|
|
const [uploading, setUploading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const { url, alt } = resolveFirstImage(images)
|
|
|
|
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: displayName || 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 } = await res.json()
|
|
const media: MediaDoc = { id: doc.id, url: doc.url, alt: doc.alt }
|
|
const next = [{ image: media }, ...(images ?? []).slice(1)]
|
|
setValue(next)
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'upload failed')
|
|
} finally {
|
|
setUploading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="rebours-admin-panel__img-slot" onClick={onPick} role="button" tabIndex={0}>
|
|
{url ? (
|
|
<img src={url} alt={alt || displayName || 'Produit'} />
|
|
) : (
|
|
<div className="rebours-admin-panel__img-placeholder">
|
|
Cliquez pour<br />uploader une image
|
|
</div>
|
|
)}
|
|
<div className="rebours-admin-panel__img-overlay">
|
|
{error ? `Erreur: ${error}` : 'Cliquez pour changer l\u2019image'}
|
|
</div>
|
|
{uploading && <div className="rebours-admin-panel__img-uploading">Upload…</div>}
|
|
<input
|
|
ref={fileInput}
|
|
type="file"
|
|
accept="image/*"
|
|
style={{ display: 'none' }}
|
|
onChange={onFile}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|