You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
187 lines
5.9 KiB
187 lines
5.9 KiB
"use client"; |
|
|
|
import { useState, useEffect } from "react"; |
|
|
|
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; |
|
} |
|
|
|
interface BlockMetaBarProps { |
|
path: string; |
|
defaultVersion: string; |
|
defaultIsInPreview: boolean; |
|
} |
|
|
|
export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: BlockMetaBarProps) { |
|
const [meta, setMeta] = useState<BlockMeta | null>(null); |
|
const [editing, setEditing] = useState(false); |
|
const [versionInput, setVersionInput] = useState(defaultVersion); |
|
const [saving, setSaving] = useState(false); |
|
const [togglingPreview, setTogglingPreview] = useState(false); |
|
const [apiDown, setApiDown] = useState(false); |
|
const [localPreview, setLocalPreview] = useState(defaultIsInPreview); |
|
|
|
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]); |
|
|
|
async function patch(data: { version?: string; isInPreview?: boolean }) { |
|
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<BlockMeta>; |
|
} |
|
|
|
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 togglePreview() { |
|
if (apiDown) { |
|
// Fallback: toggle via localStorage |
|
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); |
|
} |
|
} |
|
|
|
const version = meta?.version ?? defaultVersion; |
|
const isInPreview = apiDown ? localPreview : (meta?.isInPreview ?? localPreview); |
|
|
|
return ( |
|
<div |
|
className="flex items-center flex-wrap gap-3 px-4 py-2 rounded-lg text-xs mb-6" |
|
style={{ background: "var(--bb-sidebar-bg)", border: "1px solid var(--bb-border)" }} |
|
> |
|
{/* Version badge */} |
|
<span className="font-semibold" style={{ color: "var(--bb-text-muted)" }}> |
|
Версия: |
|
</span> |
|
{editing ? ( |
|
<span className="flex items-center gap-1.5"> |
|
<input |
|
className="px-2 py-0.5 rounded border text-xs font-mono w-20" |
|
style={{ borderColor: "var(--bb-border)", color: "var(--bb-text)" }} |
|
value={versionInput} |
|
onChange={(e) => setVersionInput(e.target.value)} |
|
onKeyDown={(e) => { if (e.key === 'Enter') saveVersion(); if (e.key === 'Escape') setEditing(false); }} |
|
autoFocus |
|
/> |
|
<button |
|
onClick={saveVersion} |
|
disabled={saving} |
|
className="px-2 py-0.5 rounded text-xs font-medium" |
|
style={{ background: "var(--brand-053m)", color: "#fff" }} |
|
> |
|
{saving ? '...' : 'Сохранить'} |
|
</button> |
|
<button |
|
onClick={() => { setEditing(false); setVersionInput(version); }} |
|
className="px-2 py-0.5 rounded text-xs" |
|
style={{ color: "var(--bb-text-muted)" }} |
|
> |
|
Отмена |
|
</button> |
|
</span> |
|
) : ( |
|
<button |
|
onClick={() => !apiDown && setEditing(true)} |
|
title={apiDown ? 'API недоступен' : 'Изменить версию'} |
|
className="font-mono px-2 py-0.5 rounded" |
|
style={{ |
|
background: "var(--bb-sidebar-active-bg, #dff0fa)", |
|
color: "var(--brand-053m)", |
|
cursor: apiDown ? 'default' : 'pointer', |
|
}} |
|
> |
|
{version} |
|
</button> |
|
)} |
|
|
|
<span style={{ color: "var(--bb-border)" }}>·</span> |
|
|
|
{/* Preview toggle */} |
|
<button |
|
onClick={togglePreview} |
|
disabled={togglingPreview || (!apiDown && !meta)} |
|
title={isInPreview ? "Убрать из превью страницы" : "Включить в превью страницы"} |
|
className="flex items-center gap-1.5 px-2.5 py-1 rounded font-medium transition-colors cursor-pointer" |
|
style={isInPreview ? { |
|
background: "#dcfce7", |
|
color: "#16a34a", |
|
border: "1px solid #86efac", |
|
} : { |
|
background: "var(--bb-sidebar-bg)", |
|
color: "var(--bb-text-muted)", |
|
border: "1px solid var(--bb-border)", |
|
}} |
|
> |
|
<span |
|
className="inline-block w-1.5 h-1.5 rounded-full" |
|
style={{ background: isInPreview ? "#22c55e" : "#d1d5db" }} |
|
/> |
|
{togglingPreview |
|
? '...' |
|
: isInPreview |
|
? "В превью" |
|
: "Не в превью"} |
|
</button> |
|
|
|
{apiDown && ( |
|
<> |
|
<span style={{ color: "var(--bb-border)" }}>·</span> |
|
<span style={{ color: "#f59e0b" }}>API офлайн</span> |
|
</> |
|
)} |
|
</div> |
|
); |
|
}
|
|
|