"use client"; import { useState, useEffect } from "react"; import { BlockChangelog, type ChangelogEntry } from "./BlockChangelog"; const LS_PREFIX = "bb-block-preview:"; function readLocalPreview(path: string, fallback: boolean): boolean { if (typeof window === "undefined") return fallback; const v = localStorage.getItem(LS_PREFIX + path); if (v === null) return fallback; return v === "true"; } function writeLocalPreview(path: string, value: boolean) { localStorage.setItem(LS_PREFIX + path, String(value)); window.dispatchEvent(new Event("bb-preview-change")); } interface BlockMeta { path: string; name: string; version: string; isInPreview: boolean; changelog: ChangelogEntry[]; } interface SnapshotListItem { id: string; version: string; createdAt: string; } export interface SnapshotData { html: string; css: string; } interface BlockMetaBarProps { path: string; defaultVersion: string; defaultIsInPreview: boolean; defaultChangelog?: ChangelogEntry[]; onSnapshotSelect?: (snapshot: SnapshotData | null) => void; } function captureBlockHtml(path: string): { html: string; css: string } { const container = document.querySelector(`[data-block-capture="${path}"]`); if (!container) throw new Error("Block container not found"); const html = container.innerHTML; const cssRules: string[] = []; for (const sheet of document.styleSheets) { try { for (const rule of sheet.cssRules) { const text = rule.cssText; if (text.includes("bb-") || text.includes("--brand") || text.includes("--bb-")) { cssRules.push(text); } } } catch { /* cross-origin sheets */ } } return { html, css: cssRules.join("\n") }; } export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview, defaultChangelog = [], onSnapshotSelect }: BlockMetaBarProps) { const [meta, setMeta] = useState(null); const [editing, setEditing] = useState(false); const [versionInput, setVersionInput] = useState(defaultVersion); const [saving, setSaving] = useState(false); const [savingVersion, setSavingVersion] = useState(false); const [togglingPreview, setTogglingPreview] = useState(false); const [apiDown, setApiDown] = useState(false); const [localPreview, setLocalPreview] = useState(defaultIsInPreview); const [snapshots, setSnapshots] = useState([]); const [selectedSnapshotId, setSelectedSnapshotId] = useState("current"); const [loadingSnapshot, setLoadingSnapshot] = useState(false); const apiUrl = process.env.NEXT_PUBLIC_API_URL; useEffect(() => { setLocalPreview(readLocalPreview(path, defaultIsInPreview)); }, [path, defaultIsInPreview]); useEffect(() => { if (!apiUrl) { setApiDown(true); return; } fetch(`${apiUrl}/blocks/by-path?path=${encodeURIComponent(path)}`) .then((r) => r.json()) .then((data: BlockMeta) => { setMeta(data); setVersionInput(data.version); }) .catch(() => setApiDown(true)); }, [apiUrl, path]); useEffect(() => { if (!apiUrl || apiDown) return; fetchSnapshots(); }, [apiUrl, apiDown, path]); async function fetchSnapshots() { if (!apiUrl) return; try { const r = await fetch(`${apiUrl}/blocks/snapshots?path=${encodeURIComponent(path)}`); const data: SnapshotListItem[] = await r.json(); setSnapshots(data); } catch { /* ignore */ } } async function patch(data: { version?: string; isInPreview?: boolean; changelog?: ChangelogEntry[] }) { if (!apiUrl) return null; const r = await fetch(`${apiUrl}/blocks/by-path?path=${encodeURIComponent(path)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); return r.json() as Promise; } async function saveVersion() { if (!meta) return; setSaving(true); try { const updated = await patch({ version: versionInput }); if (updated) { setMeta(updated); setEditing(false); } } finally { setSaving(false); } } async function saveFullVersion() { if (apiDown || !apiUrl) return; setSavingVersion('saving'); try { // 1. Update block metadata (prefer current API version over hardcoded default) const currentVersion = meta?.version ?? defaultVersion; const updated = await patch({ version: currentVersion, changelog: defaultChangelog }); if (updated) { setMeta(updated); setVersionInput(updated.version); } // 2. Capture and save HTML snapshot try { const { html, css } = captureBlockHtml(path); await fetch(`${apiUrl}/blocks/snapshots?path=${encodeURIComponent(path)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: currentVersion, changelog: defaultChangelog, html, css }), }); await fetchSnapshots(); } catch { /* snapshot capture may fail if container not found */ } setSavingVersion('done'); setTimeout(() => setSavingVersion(false), 1500); } catch { setSavingVersion(false); } } async function togglePreview() { if (apiDown) { const newVal = !localPreview; setLocalPreview(newVal); writeLocalPreview(path, newVal); return; } if (!meta) return; setTogglingPreview(true); try { const updated = await patch({ isInPreview: !meta.isInPreview }); if (updated) setMeta(updated); } finally { setTogglingPreview(false); } } async function handleVersionSelect(value: string) { setSelectedSnapshotId(value); if (value === "current") { onSnapshotSelect?.(null); return; } if (!apiUrl) return; setLoadingSnapshot(true); try { const r = await fetch(`${apiUrl}/blocks/snapshots/${value}`); const data = await r.json(); onSnapshotSelect?.({ html: data.html, css: data.css }); } catch { onSnapshotSelect?.(null); setSelectedSnapshotId("current"); } finally { setLoadingSnapshot(false); } } const version = meta?.version ?? defaultVersion; const isInPreview = apiDown ? localPreview : (meta?.isInPreview ?? localPreview); const changelog: ChangelogEntry[] = (meta?.changelog && meta.changelog.length > 0) ? meta.changelog : defaultChangelog; const isViewingSnapshot = selectedSnapshotId !== "current"; return ( <>
{/* Version selector */} Версия: {snapshots.length > 0 ? ( ) : editing ? ( setVersionInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') saveVersion(); if (e.key === 'Escape') setEditing(false); }} autoFocus /> ) : ( )} {loadingSnapshot && ( загрузка... )} · {/* Preview toggle */} · {/* Save version button */} {/* Subtle offline dot */} {apiDown && ( )}
{/* Archive notice */} {isViewingSnapshot && (
Архивная версия (только просмотр).
)} {/* Changelog rendered from API or fallback */} {changelog.length > 0 && } ); }