src/app/admin/vitrine/builder/components/builder-shell.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
9 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)
1 occurrenceCe 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 { useEffect, useState } from 'react'
import { useBuilderStore } from '../hooks/use-builder-store'
import HeaderBuilder from './header-builder'
import SidebarPalette from './sidebar-palette'
import CanvasVitrine from './canvas-vitrine'
import PanelProprietes from './panel-proprietes'
import {
DndContext, DragEndEvent, DragOverlay, DragStartEvent,
PointerSensor, useSensor, useSensors, closestCenter
} from '@dnd-kit/core'
import { arrayMove } from '@dnd-kit/sortable'
import type { Section } from '../hooks/use-builder-store'
import { useVitrineSync } from '../hooks/use-vitrine-sync'
import BuilderMobile from './builder-mobile'
interface BuilderShellProps {
tenantId: string
subdomain: string
modulesActifs: string[]
}
export default function BuilderShell({ tenantId, subdomain, modulesActifs }: BuilderShellProps) {
const {
isLoading, setPages, setIsLoading,
getActivePage, addSection, reorderSections, setSelectedElement, addBloc, getSectionById,
setGlobalConfig, setSavedState, setTenantId, previewMode
} = useBuilderStore()
const [activeId, setActiveId] = useState<string | null>(null)
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const check = () => setIsMobile(window.innerWidth < 768)
check()
window.addEventListener('resize', check)
return () => window.removeEventListener('resize', check)
}, [])
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }))
useVitrineSync()
useEffect(() => {
const load = async () => {
try {
setTenantId(tenantId)
const res = await fetch(`/api/admin/vitrine/pages?tenantId=${tenantId}`)
const data = await res.json()
const pages = data.pages ?? []
// Injecter les pages applicatives selon modules actifs
// Lire l'ordre sauvegarde depuis la config navbar si disponible
const navbarPageData = pages.find((p: any) => p.slug === 'navbar')
const savedNavConfig = (navbarPageData?.sections?.[0]?.config as any) ?? {}
const appOrders: Record<string, number> = savedNavConfig.appPagesOrdre ?? {}
const appPages: any[] = []
if (modulesActifs.includes('catalogue')) {
appPages.push({
id: 'app-boutique',
tenantId,
titre: 'Boutique',
slug: 'boutique',
parentId: null,
ordre: appOrders['app-boutique'] ?? 900,
visible: true,
estAccueil: false,
type: 'APPLICATIVE',
enfants: [],
sections: [],
})
}
if (modulesActifs.includes('services')) {
appPages.push({
id: 'app-services',
tenantId,
titre: 'Services',
slug: 'services',
parentId: null,
ordre: appOrders['app-services'] ?? 901,
visible: true,
estAccueil: false,
type: 'APPLICATIVE',
enfants: [],
sections: [],
})
}
setPages([...pages, ...appPages])
const navbarPage = pages.find((p: any) => p.slug === 'navbar')
const footerPage = pages.find((p: any) => p.slug === 'footer')
const navbarConfig = navbarPage?.sections?.[0]?.config ?? {}
const footerConfig = footerPage?.sections?.[0]?.config ?? {}
setGlobalConfig({ navbar: navbarConfig, footer: footerConfig })
setSavedState()
} catch (e) {
console.error('Erreur chargement pages vitrine', e)
setIsLoading(false)
}
}
load()
}, [tenantId, setPages, setIsLoading])
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string)
}
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
setActiveId(null)
const activePage = getActivePage()
if (!activePage) return
const activeIdStr = String(active.id)
if (activeIdStr.startsWith('palette-section-')) {
const type = activeIdStr.replace('palette-section-', '') as Section['type']
const LABELS: Record<string, string> = {
HERO: 'Hero', CATALOGUE: 'Catalogue', SERVICES: 'Services',
GALERIE: 'Galerie', A_PROPOS: 'A propos', CONTACT: 'Contact', CUSTOM: 'Personnalise'
}
const newSection: Section = {
id: crypto.randomUUID(),
pageId: activePage.id,
tenantId: activePage.tenantId,
type,
nom: LABELS[type] ?? type,
ordre: activePage.sections.length,
visible: true,
config: {},
blocs: [],
}
addSection(activePage.id, newSection)
setSelectedElement({ type: 'section', id: newSection.id })
return
}
if (activeIdStr.startsWith('palette-bloc-')) {
const blocType = activeIdStr.replace('palette-bloc-', '')
const overId = over ? String(over.id) : ''
let targetSectionId: string | null = null
if (overId.startsWith('drop-section-')) {
targetSectionId = overId.replace('drop-section-', '')
} else {
for (const sec of activePage.sections) {
if (sec.id === overId) { targetSectionId = sec.id; break }
if (sec.blocs.find((b: any) => b.id === overId)) { targetSectionId = sec.id; break }
}
}
if (!targetSectionId) {
const { selectedElement } = useBuilderStore.getState()
if (selectedElement?.type === 'section') targetSectionId = selectedElement.id
}
if (targetSectionId) {
const sec = getSectionById(targetSectionId)
const newBloc = {
id: crypto.randomUUID(),
sectionId: targetSectionId,
type: blocType as any,
ordre: sec ? sec.blocs.length : 0,
contenu: {},
}
addBloc(targetSectionId, newBloc)
setSelectedElement({ type: 'bloc', id: newBloc.id })
}
return
}
const sections = [...activePage.sections].sort((a, b) => a.ordre - b.ordre)
if (over && active.id !== over.id) {
const oldIndex = sections.findIndex(s => s.id === active.id)
const newIndex = sections.findIndex(s => s.id === over.id)
if (oldIndex !== -1 && newIndex !== -1) {
const reordered = arrayMove(sections, oldIndex, newIndex).map((s, i) => ({ ...s, ordre: i }))
reorderSections(activePage.id, reordered)
}
}
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-screen bg-gray-950">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-white/20 border-t-white rounded-full animate-spin" />
<p className="text-white/50 text-sm">Chargement du builder...</p>
</div>
</div>
)
}
if (isMobile) return <BuilderMobile subdomain={subdomain} />
// En mode preview, on masque sidebar et panel pour maximiser l'espace
const isPreview = previewMode === 'desktop' || previewMode === 'mobile'
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex flex-col h-screen bg-gray-950 overflow-hidden">'use client'
import { useEffect, useState } from 'react'
import { useBuilderStore } from '../hooks/use-builder-store'
import HeaderBuilder from './header-builder'
import SidebarPalette from './sidebar-palette'
import CanvasVitrine from './canvas-vitrine'
import PanelProprietes from './panel-proprietes'
import {
DndContext, DragEndEvent, DragOverlay, DragStartEvent,
PointerSensor, useSensor, useSensors, closestCenter
} from '@dnd-kit/core'
import { arrayMove } from '@dnd-kit/sortable'
import type { Section } from '../hooks/use-builder-store'
import { useVitrineSync } from '../hooks/use-vitrine-sync'
import BuilderMobile from './builder-mobile'
interface BuilderShellProps {
tenantId: string
subdomain: string
modulesActifs: string[]
}
export default function BuilderShell({ tenantId, subdomain, modulesActifs }: BuilderShellProps) {
const {
isLoading, setPages, setIsLoading,
getActivePage, addSection, reorderSections, setSelectedElement, addBloc, getSectionById,
setGlobalConfig, setSavedState, setTenantId, previewMode
} = useBuilderStore()
const [activeId, setActiveId] = useState<string | null>(null)
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const check = () => setIsMobile(window.innerWidth < 768)
check()
window.addEventListener('resize', check)
return () => window.removeEventListener('resize', check)
}, [])
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }))
useVitrineSync()
useEffect(() => {
const load = async () => {
try {
setTenantId(tenantId)
const res = await fetch(`/api/admin/vitrine/pages?tenantId=${tenantId}`)
const data = await res.json()
const pages = data.pages ?? []
// Injecter les pages applicatives selon modules actifs
// Lire l'ordre sauvegarde depuis la config navbar si disponible
const navbarPageData = pages.find((p: any) => p.slug === 'navbar')
const savedNavConfig = (navbarPageData?.sections?.[0]?.config as any) ?? {}
const appOrders: Record<string, number> = savedNavConfig.appPagesOrdre ?? {}
const appPages: any[] = []
if (modulesActifs.includes('catalogue')) {
appPages.push({
id: 'app-boutique',
tenantId,
titre: 'Boutique',
slug: 'boutique',
parentId: null,
ordre: appOrders['app-boutique'] ?? 900,
visible: true,
estAccueil: false,
type: 'APPLICATIVE',
enfants: [],
sections: [],
})
}
if (modulesActifs.includes('services')) {
appPages.push({
id: 'app-services',
tenantId,
titre: 'Services',
slug: 'services',
parentId: null,
ordre: appOrders['app-services'] ?? 901,
visible: true,
estAccueil: false,
type: 'APPLICATIVE',
enfants: [],
sections: [],
})
}
setPages([...pages, ...appPages])
const navbarPage = pages.find((p: any) => p.slug === 'navbar')
const footerPage = pages.find((p: any) => p.slug === 'footer')
const navbarConfig = navbarPage?.sections?.[0]?.config ?? {}
const footerConfig = footerPage?.sections?.[0]?.config ?? {}
setGlobalConfig({ navbar: navbarConfig, footer: footerConfig })
setSavedState()
} catch (e) {
console.error('Erreur chargement pages vitrine', e)
setIsLoading(false)
}
}
load()
}, [tenantId, setPages, setIsLoading])
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string)
}
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
setActiveId(null)
const activePage = getActivePage()
if (!activePage) return
const activeIdStr = String(active.id)
if (activeIdStr.startsWith('palette-section-')) {
const type = activeIdStr.replace('palette-section-', '') as Section['type']
const LABELS: Record<string, string> = {
HERO: 'Hero', CATALOGUE: 'Catalogue', SERVICES: 'Services',
GALERIE: 'Galerie', A_PROPOS: 'A propos', CONTACT: 'Contact', CUSTOM: 'Personnalise'
}
const newSection: Section = {
id: crypto.randomUUID(),
pageId: activePage.id,
tenantId: activePage.tenantId,
type,
nom: LABELS[type] ?? type,
ordre: activePage.sections.length,
visible: true,
config: {},
blocs: [],
}
addSection(activePage.id, newSection)
setSelectedElement({ type: 'section', id: newSection.id })
return
}
if (activeIdStr.startsWith('palette-bloc-')) {
const blocType = activeIdStr.replace('palette-bloc-', '')
const overId = over ? String(over.id) : ''
let targetSectionId: string | null = null
if (overId.startsWith('drop-section-')) {
targetSectionId = overId.replace('drop-section-', '')
} else {
for (const sec of activePage.sections) {
if (sec.id === overId) { targetSectionId = sec.id; break }
if (sec.blocs.find((b: any) => b.id === overId)) { targetSectionId = sec.id; break }
}
}
if (!targetSectionId) {
const { selectedElement } = useBuilderStore.getState()
if (selectedElement?.type === 'section') targetSectionId = selectedElement.id
}
if (targetSectionId) {
const sec = getSectionById(targetSectionId)
const newBloc = {
id: crypto.randomUUID(),
sectionId: targetSectionId,
type: blocType as any,
ordre: sec ? sec.blocs.length : 0,
contenu: {},
}
addBloc(targetSectionId, newBloc)
setSelectedElement({ type: 'bloc', id: newBloc.id })
}
return
}
const sections = [...activePage.sections].sort((a, b) => a.ordre - b.ordre)
if (over && active.id !== over.id) {
const oldIndex = sections.findIndex(s => s.id === active.id)
const newIndex = sections.findIndex(s => s.id === over.id)
if (oldIndex !== -1 && newIndex !== -1) {
const reordered = arrayMove(sections, oldIndex, newIndex).map((s, i) => ({ ...s, ordre: i }))
reorderSections(activePage.id, reordered)
}
}
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-screen bg-gray-950">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-white/20 border-t-white rounded-full animate-spin" />
<p className="text-white/50 text-sm">Chargement du builder...</p>
</div>
</div>
)
}
if (isMobile) return <BuilderMobile subdomain={subdomain} />
// En mode preview, on masque sidebar et panel pour maximiser l'espace
const isPreview = previewMode === 'desktop' || previewMode === 'mobile'
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="flex flex-col h-screen bg-gray-950 overflow-hidden">