src/app/admin/vitrine/builder/components/header-builder.tsx
Annotation non disponible
Lance npm run annotate (nécessite ANTHROPIC_API_KEY dans .env.local) pour générer une annotation française par Claude Haiku 4.5.
Concepts détectés — comprends la théorie
Hooks React
5 occurrencesCe fichier utilise des hooks React. Les hooks sont la façon moderne de gérer l'état et les effets dans React. Voir l'architecture mobile pour le pattern complet.
Voir l'article général
Appel API (fetch)
4 occurrencesCe fichier fait des appels HTTP. Voir la convention API wari.pro pour les patterns de gestion d'erreur et de Bearer token.
Voir l'article général
1 export
default
Code source· tsx· tronqué à 200 lignes sur 235
'use client'
import { useState } from 'react'
import { useBuilderStore } from '../hooks/use-builder-store'
import { saveVitrineAndMarkSaved } from '../hooks/use-vitrine-sync'
import { useAutosave } from '../hooks/use-autosave'
import { Eye, Save, Upload, ChevronRight, Monitor, Smartphone, History, RotateCcw, Loader2, PenLine } from 'lucide-react'
import VersionsDrawer from './versions-drawer'
import ToastContainer, { addToast } from './toast'
interface HeaderBuilderProps {
tenantId: string
subdomain: string
}
export default function HeaderBuilder({ tenantId, subdomain }: HeaderBuilderProps) {
const { pages, activePageId, isDirty, isSaving, setIsSaving, setIsDirty, getActivePage, previewMode, setPreviewMode } = useBuilderStore()
const [showVersions, setShowVersions] = useState(false)
const [showPublishModal, setShowPublishModal] = useState(false)
const [lastSaveTime, setLastSaveTime] = useState<Date | null>(null)
const [lastSaveLabel, setLastSaveLabel] = useState<string>('')
useAutosave(() => {
setLastSaveTime(new Date())
setLastSaveLabel('Autosave')
addToast('Autosave effectue', 'info')
})
const activePage = getActivePage()
const buildBreadcrumb = (): string[] => {
if (!activePage) return []
const path: string[] = []
const find = (pages: typeof activePage[], targetId: string): boolean => {
for (const p of pages) {
if (p.id === targetId) { path.push(p.titre); return true }
if (find(p.enfants, targetId)) { path.splice(path.length - 1, 0, p.titre); return true }
}
return false
}
find(pages, activePage.id)
return path
}
const breadcrumb = buildBreadcrumb()
const getSaveIndicator = () => {
if (isSaving) return { text: 'Sauvegarde...', color: 'text-gray-400' }
if (isDirty) return { text: 'Non sauvegarde', color: 'text-amber-400' }
if (lastSaveTime) {
const diff = Math.floor((Date.now() - lastSaveTime.getTime()) / 60000)
const label = lastSaveLabel ? lastSaveLabel + ' ' : ''
if (diff < 1) return { text: label + 'sauvegarde a l instant', color: 'text-green-400' }
return { text: label + 'sauvegarde il y a ' + diff + 'min', color: 'text-green-400' }
}
return null
}
const indicator = getSaveIndicator()
const previewUrl = window.location.protocol + '//' + subdomain + '.' + (process.env.NEXT_PUBLIC_DOMAIN || 'wari.pro') + '/?preview=live'
const handleModeToggle = (mode: 'edit' | 'desktop' | 'mobile') => {
setPreviewMode(mode)
}
const handleSave = async () => {
setIsSaving(true)
try {
await saveVitrineAndMarkSaved()
const res = await fetch('/api/admin/vitrine/versions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ statut: 'DRAFT', label: null })
})
if (res.ok) {
setLastSaveTime(new Date())
setLastSaveLabel('')
addToast('Version enregistree', 'success')
}
} catch (e) {
addToast('Erreur lors de l enregistrement', 'error')
} finally {
setIsSaving(false)
}
}
const handlePublish = async () => {
setIsSaving(true)
try {
await saveVitrineAndMarkSaved()
const res = await fetch('/api/admin/vitrine/versions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ statut: 'PUBLISHED', label: null })
})
if (res.ok) {
setLastSaveTime(new Date())
addToast('Vitrine mise en ligne !', 'success')
}
} catch (e) {
addToast('Erreur lors de la publication', 'error')
} finally {
setIsSaving(false)
setShowPublishModal(false)
}
}
const handleRollback = async () => {
if (!confirm('Revenir a la version publiee ? Vos modifications non sauvegardees seront perdues.')) return
setIsSaving(true)
try {
const res = await fetch('/api/admin/vitrine/versions/published')
const data = await res.json()
if (data.snapshot) {
await fetch('/api/admin/vitrine/versions/' + data.snapshot.id, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'load' })
})
addToast('Version publiee restauree', 'success')
window.location.reload()
} else {
addToast('Aucune version publiee trouvee', 'info')
}
} catch (e) {
addToast('Erreur lors du rollback', 'error')
} finally {
setIsSaving(false)
}
}
return (
<>
<header className="h-12 flex items-center justify-between px-4 bg-gray-900 border-b border-white/10 flex-shrink-0">
{/* Breadcrumb */}
<div className="flex items-center gap-1 text-sm min-w-0">
<span className="text-white/40 flex-shrink-0">Vitrine</span>
{breadcrumb.map((crumb, i) => (
<span key={i} className="flex items-center gap-1 min-w-0">
<ChevronRight className="w-3 h-3 text-white/20 flex-shrink-0" />
<span className={'truncate ' + (i === breadcrumb.length - 1 ? 'text-white font-medium' : 'text-white/40')}>
{crumb}
</span>
</span>
))}
</div>
{/* Centre — mode toggle edit / desktop / mobile */}
<div className="flex items-center gap-1 bg-gray-800 rounded-lg p-1 flex-shrink-0">
<button
onClick={() => handleModeToggle('edit')}
title="Mode édition"
className={'p-1.5 rounded-md transition-colors flex items-center gap-1 px-2 text-xs ' + (previewMode === 'edit' ? 'bg-white/10 text-white' : 'text-white/40 hover:text-white')}
>
<PenLine className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Edition</span>
</button>
<button
onClick={() => handleModeToggle('desktop')}
title="Aperçu desktop"
className={'p-1.5 rounded-md transition-colors ' + (previewMode === 'desktop' ? 'bg-white/10 text-white' : 'text-white/40 hover:text-white')}
>
<Monitor className="w-4 h-4" />
</button>
<button
onClick={() => handleModeToggle('mobile')}
title="Aperçu mobile"
className={'p-1.5 rounded-md transition-colors ' + (previewMode === 'mobile' ? 'bg-white/10 text-white' : 'text-white/40 hover:text-white')}
>
<Smartphone className="w-4 h-4" />
</button>
</div>
{/* Actions droite */}
<div className="flex items-center gap-2 flex-shrink-0">
{indicator && (
<span className={'text-xs flex items-center gap-1 ' + indicator.color}>
<span className={'w-1.5 h-1.5 rounded-full ' + (isDirty ? 'bg-amber-400' : 'bg-green-400')} />
{indicator.text}
</span>
)}
<button
onClick={() => window.open(previewUrl, '_blank')}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-white/60 hover:text-white border border-white/10 hover:border-white/20 rounded-lg transition-colors"
>
<Eye className="w-3.5 h-3.5" />
Apercu
</button>
<button onClick={handleRollback} disabled={isSaving} className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-white/60 hover:text-white border border-white/10 hover:border-white/20 rounded-lg transition-colors disabled:opacity-40" title="Revenir a la version publiee">
<RotateCcw className="w-3.5 h-3.5" />
</button>
<button onClick={() => setShowVersions(true)} className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-white/60 hover:text-white border border-white/10 hover:border-white/20 rounded-lg transition-colors">
<History className="w-3.5 h-3.5" />
Versions
</button>
<button onClick={handleSave} disabled={isSaving} className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-white/60 hover:text-white border border-white/10 hover:border-white/20 rounded-lg transition-colors disabled:opacity-40">
{isSaving ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
Enregistrer
</button>'use client'
import { useState } from 'react'
import { useBuilderStore } from '../hooks/use-builder-store'
import { saveVitrineAndMarkSaved } from '../hooks/use-vitrine-sync'
import { useAutosave } from '../hooks/use-autosave'
import { Eye, Save, Upload, ChevronRight, Monitor, Smartphone, History, RotateCcw, Loader2, PenLine } from 'lucide-react'
import VersionsDrawer from './versions-drawer'
import ToastContainer, { addToast } from './toast'
interface HeaderBuilderProps {
tenantId: string
subdomain: string
}
export default function HeaderBuilder({ tenantId, subdomain }: HeaderBuilderProps) {
const { pages, activePageId, isDirty, isSaving, setIsSaving, setIsDirty, getActivePage, previewMode, setPreviewMode } = useBuilderStore()
const [showVersions, setShowVersions] = useState(false)
const [showPublishModal, setShowPublishModal] = useState(false)
const [lastSaveTime, setLastSaveTime] = useState<Date | null>(null)
const [lastSaveLabel, setLastSaveLabel] = useState<string>('')
useAutosave(() => {
setLastSaveTime(new Date())
setLastSaveLabel('Autosave')
addToast('Autosave effectue', 'info')
})
const activePage = getActivePage()
const buildBreadcrumb = (): string[] => {
if (!activePage) return []
const path: string[] = []
const find = (pages: typeof activePage[], targetId: string): boolean => {
for (const p of pages) {
if (p.id === targetId) { path.push(p.titre); return true }
if (find(p.enfants, targetId)) { path.splice(path.length - 1, 0, p.titre); return true }
}
return false
}
find(pages, activePage.id)
return path
}
const breadcrumb = buildBreadcrumb()
const getSaveIndicator = () => {
if (isSaving) return { text: 'Sauvegarde...', color: 'text-gray-400' }
if (isDirty) return { text: 'Non sauvegarde', color: 'text-amber-400' }
if (lastSaveTime) {
const diff = Math.floor((Date.now() - lastSaveTime.getTime()) / 60000)
const label = lastSaveLabel ? lastSaveLabel + ' ' : ''
if (diff < 1) return { text: label + 'sauvegarde a l instant', color: 'text-green-400' }
return { text: label + 'sauvegarde il y a ' + diff + 'min', color: 'text-green-400' }
}
return null
}
const indicator = getSaveIndicator()
const previewUrl = window.location.protocol + '//' + subdomain + '.' + (process.env.NEXT_PUBLIC_DOMAIN || 'wari.pro') + '/?preview=live'
const handleModeToggle = (mode: 'edit' | 'desktop' | 'mobile') => {
setPreviewMode(mode)
}
const handleSave = async () => {
setIsSaving(true)
try {
await saveVitrineAndMarkSaved()
const res = await fetch('/api/admin/vitrine/versions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ statut: 'DRAFT', label: null })
})
if (res.ok) {
setLastSaveTime(new Date())
setLastSaveLabel('')
addToast('Version enregistree', 'success')
}
} catch (e) {
addToast('Erreur lors de l enregistrement', 'error')
} finally {
setIsSaving(false)
}
}
const handlePublish = async () => {
setIsSaving(true)
try {
await saveVitrineAndMarkSaved()
const res = await fetch('/api/admin/vitrine/versions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ statut: 'PUBLISHED', label: null })
})
if (res.ok) {
setLastSaveTime(new Date())
addToast('Vitrine mise en ligne !', 'success')
}
} catch (e) {
addToast('Erreur lors de la publication', 'error')
} finally {
setIsSaving(false)
setShowPublishModal(false)
}
}
const handleRollback = async () => {
if (!confirm('Revenir a la version publiee ? Vos modifications non sauvegardees seront perdues.')) return
setIsSaving(true)
try {
const res = await fetch('/api/admin/vitrine/versions/published')
const data = await res.json()
if (data.snapshot) {
await fetch('/api/admin/vitrine/versions/' + data.snapshot.id, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'load' })
})
addToast('Version publiee restauree', 'success')
window.location.reload()
} else {
addToast('Aucune version publiee trouvee', 'info')
}
} catch (e) {
addToast('Erreur lors du rollback', 'error')
} finally {
setIsSaving(false)
}
}
return (
<>
<header className="h-12 flex items-center justify-between px-4 bg-gray-900 border-b border-white/10 flex-shrink-0">
{/* Breadcrumb */}
<div className="flex items-center gap-1 text-sm min-w-0">
<span className="text-white/40 flex-shrink-0">Vitrine</span>
{breadcrumb.map((crumb, i) => (
<span key={i} className="flex items-center gap-1 min-w-0">
<ChevronRight className="w-3 h-3 text-white/20 flex-shrink-0" />
<span className={'truncate ' + (i === breadcrumb.length - 1 ? 'text-white font-medium' : 'text-white/40')}>
{crumb}
</span>
</span>
))}
</div>
{/* Centre — mode toggle edit / desktop / mobile */}
<div className="flex items-center gap-1 bg-gray-800 rounded-lg p-1 flex-shrink-0">
<button
onClick={() => handleModeToggle('edit')}
title="Mode édition"
className={'p-1.5 rounded-md transition-colors flex items-center gap-1 px-2 text-xs ' + (previewMode === 'edit' ? 'bg-white/10 text-white' : 'text-white/40 hover:text-white')}
>
<PenLine className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Edition</span>
</button>
<button
onClick={() => handleModeToggle('desktop')}
title="Aperçu desktop"
className={'p-1.5 rounded-md transition-colors ' + (previewMode === 'desktop' ? 'bg-white/10 text-white' : 'text-white/40 hover:text-white')}
>
<Monitor className="w-4 h-4" />
</button>
<button
onClick={() => handleModeToggle('mobile')}
title="Aperçu mobile"
className={'p-1.5 rounded-md transition-colors ' + (previewMode === 'mobile' ? 'bg-white/10 text-white' : 'text-white/40 hover:text-white')}
>
<Smartphone className="w-4 h-4" />
</button>
</div>
{/* Actions droite */}
<div className="flex items-center gap-2 flex-shrink-0">
{indicator && (
<span className={'text-xs flex items-center gap-1 ' + indicator.color}>
<span className={'w-1.5 h-1.5 rounded-full ' + (isDirty ? 'bg-amber-400' : 'bg-green-400')} />
{indicator.text}
</span>
)}
<button
onClick={() => window.open(previewUrl, '_blank')}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-white/60 hover:text-white border border-white/10 hover:border-white/20 rounded-lg transition-colors"
>
<Eye className="w-3.5 h-3.5" />
Apercu
</button>
<button onClick={handleRollback} disabled={isSaving} className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-white/60 hover:text-white border border-white/10 hover:border-white/20 rounded-lg transition-colors disabled:opacity-40" title="Revenir a la version publiee">
<RotateCcw className="w-3.5 h-3.5" />
</button>
<button onClick={() => setShowVersions(true)} className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-white/60 hover:text-white border border-white/10 hover:border-white/20 rounded-lg transition-colors">
<History className="w-3.5 h-3.5" />
Versions
</button>
<button onClick={handleSave} disabled={isSaving} className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-white/60 hover:text-white border border-white/10 hover:border-white/20 rounded-lg transition-colors disabled:opacity-40">
{isSaving ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
Enregistrer
</button>