src/app/admin/vitrine/builder/components/builder-shell.tsx

component·app·8.5 KB · 235 lignes· Voir l'itinéraire
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.

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">