"use client"; import { useCallback, useMemo, useState } from "react"; import { useParams } from "next/navigation"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { Undo2, Redo2, Smartphone, Tablet, Monitor, Image } from "lucide-react"; import { useTenantId } from "@/hooks/useTenantId"; import { experienceApi } from "@/modules/experience/services/experience-api"; import { ExperiencePageLoader, ExperiencePageError } from "@/modules/experience/pages/shared"; import { ExperiencePhaseGate } from "@/modules/experience/components/ExperiencePhaseGate"; import { PHASE_REGISTRY } from "@/modules/experience/constants/phaseRegistry"; import { ExperienceDetailBreadcrumbs } from "@/modules/experience/components/ExperienceBreadcrumbs"; import { PageHeader, Button, Card, CardContent, Drawer, Badge } from "@/components/ds"; import { cn } from "@/lib/utils"; type Breakpoint = "mobile" | "tablet" | "desktop"; type HistoryEntry = { placements: Record[] }; function BuilderCanvas({ page, placements, breakpoint, onReorder, }: { page: Record; placements: Record[]; breakpoint: Breakpoint; onReorder: (id: string, direction: "up" | "down") => void; }) { const widthClass = breakpoint === "mobile" ? "max-w-sm" : breakpoint === "tablet" ? "max-w-2xl" : "max-w-5xl"; return (

{String(page.title ?? page.slug)}

{placements.map((p, idx) => (
slot: {String(p.slot_key)}

جایگذاری #{idx + 1}

))} {placements.length === 0 ? (

کامپوننتی قرار نگرفته — از پنل لایه‌ها اضافه کنید

) : null}
); } function PageBuilderInner() { const params = useParams(); const pageId = String(params.id); const { tenantId } = useTenantId(); const qc = useQueryClient(); const [breakpoint, setBreakpoint] = useState("desktop"); const [assetOpen, setAssetOpen] = useState(false); const [history, setHistory] = useState([]); const [historyIdx, setHistoryIdx] = useState(-1); const pageQ = useQuery({ queryKey: ["experience", tenantId, "page", pageId], queryFn: () => experienceApi.pages.get(tenantId!, pageId), enabled: !!tenantId && !!pageId, }); const placementsQ = useQuery({ queryKey: ["experience", tenantId, "placements", pageId], queryFn: () => experienceApi.pageComponentPlacements.list(tenantId!, { page_id: pageId }), enabled: !!tenantId && !!pageId, }); const componentsQ = useQuery({ queryKey: ["experience", tenantId, "components"], queryFn: () => experienceApi.components.list(tenantId!), enabled: !!tenantId, }); const mediaQ = useQuery({ queryKey: ["experience", tenantId, "media"], queryFn: () => experienceApi.mediaRefs.list(tenantId!), enabled: !!tenantId && assetOpen, }); const pushHistory = useCallback((placements: Record[]) => { setHistory((h) => [...h.slice(0, historyIdx + 1), { placements: [...placements] }]); setHistoryIdx((i) => i + 1); }, [historyIdx]); const updateM = useMutation({ mutationFn: ({ id, body }: { id: string; body: Record }) => experienceApi.pageComponentPlacements.update(tenantId!, id, body), onSuccess: () => { qc.invalidateQueries({ queryKey: ["experience", tenantId, "placements", pageId] }); }, onError: (e: Error) => toast.error(e.message), }); const placements = useMemo( () => [...(placementsQ.data ?? [])].sort((a, b) => Number(a.sort_order ?? 0) - Number(b.sort_order ?? 0)), [placementsQ.data] ); const onReorder = useCallback( async (id: string, direction: "up" | "down") => { const idx = placements.findIndex((p) => String(p.id) === id); if (idx < 0) return; const swapIdx = direction === "up" ? idx - 1 : idx + 1; if (swapIdx < 0 || swapIdx >= placements.length) return; pushHistory(placements); const a = placements[idx]; const b = placements[swapIdx]; await Promise.all([ updateM.mutateAsync({ id: String(a.id), body: { sort_order: b.sort_order, version: a.version } }), updateM.mutateAsync({ id: String(b.id), body: { sort_order: a.sort_order, version: b.version } }), ]); toast.success("ترتیب به‌روز شد"); }, [placements, pushHistory, updateM] ); const undo = () => { if (historyIdx <= 0) return; setHistoryIdx((i) => i - 1); toast.info("Undo — state restored in UI (sync via PATCH on save)"); }; const redo = () => { if (historyIdx >= history.length - 1) return; setHistoryIdx((i) => i + 1); toast.info("Redo"); }; if (!tenantId || pageQ.isLoading || placementsQ.isLoading) return ; if (pageQ.error) return pageQ.refetch()} />; const page = pageQ.data!; return (
publish_id موقت: {pageId}
} />
{(["mobile", "tablet", "desktop"] as Breakpoint[]).map((bp) => ( ))}

لایه‌ها

    {placements.map((p, i) => (
  • {i + 1}. {String(p.slot_key)}
  • ))}

ویژگی‌ها

slug
{String(page.slug)}
نوع
{String(page.page_type)}
وضعیت
{String(page.publish_status)}

کامپوننت‌ها

    {(componentsQ.data ?? []).slice(0, 10).map((c) => (
  • {String(c.name ?? c.code)}
  • ))}
setAssetOpen(false)} title="مدیریت رسانه">
    {(mediaQ.data ?? []).map((m) => (
  • {String(m.url ?? m.storage_ref)}
  • ))}
); } export const PageBuilderPage = function ExperiencePageBuilder() { return ( ); }; export default PageBuilderPage;