fix(preview): enable block toggle via localStorage when API is offline
BlockMetaBar now falls back to localStorage for preview toggle when NestJS API is unavailable. PreviewClient reads localStorage state and listens for bb-preview-change events to stay in sync. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { ContactFormsBlock } from "@/components/blocks/ContactFormsBlock";
|
||||
import { FooterBlock } from "@/components/blocks/FooterBlock";
|
||||
|
||||
const STORAGE_KEY = "bb-preview-created";
|
||||
const LS_PREFIX = "bb-block-preview:";
|
||||
|
||||
function BlockPlaceholder({ name, href }: { name: string; href: string }) {
|
||||
return (
|
||||
@@ -119,11 +120,17 @@ export function PreviewClient() {
|
||||
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||
|
||||
// Counter to force re-render when localStorage preview toggles change
|
||||
const [, setRefresh] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
if (localStorage.getItem(STORAGE_KEY) === "true") {
|
||||
setCreated(true);
|
||||
}
|
||||
const handler = () => setRefresh((n) => n + 1);
|
||||
window.addEventListener("bb-preview-change", handler);
|
||||
return () => window.removeEventListener("bb-preview-change", handler);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -141,9 +148,16 @@ export function PreviewClient() {
|
||||
}, [apiUrl]);
|
||||
|
||||
function isReady(block: BlockDef): boolean {
|
||||
// 1. API online → use API data
|
||||
if (apiMeta !== null && block.path in apiMeta) {
|
||||
return apiMeta[block.path] && !!block.component;
|
||||
}
|
||||
// 2. Check localStorage (set by BlockMetaBar toggle)
|
||||
const lsVal = typeof window !== "undefined" ? localStorage.getItem(LS_PREFIX + block.path) : null;
|
||||
if (lsVal !== null) {
|
||||
return lsVal === "true" && !!block.component;
|
||||
}
|
||||
// 3. Fallback to defaultReady
|
||||
return block.defaultReady && !!block.component;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
|
||||
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;
|
||||
@@ -22,9 +36,14 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
|
||||
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)}`)
|
||||
@@ -58,6 +77,13 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
|
||||
}
|
||||
|
||||
async function togglePreview() {
|
||||
if (apiDown) {
|
||||
// Fallback: toggle via localStorage
|
||||
const newVal = !localPreview;
|
||||
setLocalPreview(newVal);
|
||||
writeLocalPreview(path, newVal);
|
||||
return;
|
||||
}
|
||||
if (!meta) return;
|
||||
setTogglingPreview(true);
|
||||
try {
|
||||
@@ -69,7 +95,7 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
|
||||
}
|
||||
|
||||
const version = meta?.version ?? defaultVersion;
|
||||
const isInPreview = meta?.isInPreview ?? defaultIsInPreview;
|
||||
const isInPreview = apiDown ? localPreview : (meta?.isInPreview ?? localPreview);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -124,44 +150,31 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
|
||||
<span style={{ color: "var(--bb-border)" }}>·</span>
|
||||
|
||||
{/* Preview toggle */}
|
||||
{apiDown ? (
|
||||
<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="flex items-center gap-1"
|
||||
style={{ color: isInPreview ? "#16a34a" : "var(--bb-text-muted)" }}
|
||||
>
|
||||
<span
|
||||
className="inline-block w-1.5 h-1.5 rounded-full"
|
||||
style={{ background: isInPreview ? "#22c55e" : "#d1d5db" }}
|
||||
/>
|
||||
{isInPreview ? "В превью" : "Не в превью"}
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={togglePreview}
|
||||
disabled={togglingPreview || !meta}
|
||||
title={isInPreview ? "Убрать из превью страницы" : "Включить в превью страницы"}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1 rounded font-medium transition-colors"
|
||||
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>
|
||||
)}
|
||||
className="inline-block w-1.5 h-1.5 rounded-full"
|
||||
style={{ background: isInPreview ? "#22c55e" : "#d1d5db" }}
|
||||
/>
|
||||
{togglingPreview
|
||||
? '...'
|
||||
: isInPreview
|
||||
? "В превью"
|
||||
: "Не в превью"}
|
||||
</button>
|
||||
|
||||
{apiDown && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user