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";
|
import { FooterBlock } from "@/components/blocks/FooterBlock";
|
||||||
|
|
||||||
const STORAGE_KEY = "bb-preview-created";
|
const STORAGE_KEY = "bb-preview-created";
|
||||||
|
const LS_PREFIX = "bb-block-preview:";
|
||||||
|
|
||||||
function BlockPlaceholder({ name, href }: { name: string; href: string }) {
|
function BlockPlaceholder({ name, href }: { name: string; href: string }) {
|
||||||
return (
|
return (
|
||||||
@@ -119,11 +120,17 @@ export function PreviewClient() {
|
|||||||
|
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
||||||
|
// Counter to force re-render when localStorage preview toggles change
|
||||||
|
const [, setRefresh] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
if (localStorage.getItem(STORAGE_KEY) === "true") {
|
if (localStorage.getItem(STORAGE_KEY) === "true") {
|
||||||
setCreated(true);
|
setCreated(true);
|
||||||
}
|
}
|
||||||
|
const handler = () => setRefresh((n) => n + 1);
|
||||||
|
window.addEventListener("bb-preview-change", handler);
|
||||||
|
return () => window.removeEventListener("bb-preview-change", handler);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -141,9 +148,16 @@ export function PreviewClient() {
|
|||||||
}, [apiUrl]);
|
}, [apiUrl]);
|
||||||
|
|
||||||
function isReady(block: BlockDef): boolean {
|
function isReady(block: BlockDef): boolean {
|
||||||
|
// 1. API online → use API data
|
||||||
if (apiMeta !== null && block.path in apiMeta) {
|
if (apiMeta !== null && block.path in apiMeta) {
|
||||||
return apiMeta[block.path] && !!block.component;
|
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;
|
return block.defaultReady && !!block.component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,20 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
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 {
|
interface BlockMeta {
|
||||||
path: string;
|
path: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -22,9 +36,14 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [togglingPreview, setTogglingPreview] = useState(false);
|
const [togglingPreview, setTogglingPreview] = useState(false);
|
||||||
const [apiDown, setApiDown] = useState(false);
|
const [apiDown, setApiDown] = useState(false);
|
||||||
|
const [localPreview, setLocalPreview] = useState(defaultIsInPreview);
|
||||||
|
|
||||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalPreview(readLocalPreview(path, defaultIsInPreview));
|
||||||
|
}, [path, defaultIsInPreview]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!apiUrl) { setApiDown(true); return; }
|
if (!apiUrl) { setApiDown(true); return; }
|
||||||
fetch(`${apiUrl}/blocks/by-path?path=${encodeURIComponent(path)}`)
|
fetch(`${apiUrl}/blocks/by-path?path=${encodeURIComponent(path)}`)
|
||||||
@@ -58,6 +77,13 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function togglePreview() {
|
async function togglePreview() {
|
||||||
|
if (apiDown) {
|
||||||
|
// Fallback: toggle via localStorage
|
||||||
|
const newVal = !localPreview;
|
||||||
|
setLocalPreview(newVal);
|
||||||
|
writeLocalPreview(path, newVal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!meta) return;
|
if (!meta) return;
|
||||||
setTogglingPreview(true);
|
setTogglingPreview(true);
|
||||||
try {
|
try {
|
||||||
@@ -69,7 +95,7 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
|
|||||||
}
|
}
|
||||||
|
|
||||||
const version = meta?.version ?? defaultVersion;
|
const version = meta?.version ?? defaultVersion;
|
||||||
const isInPreview = meta?.isInPreview ?? defaultIsInPreview;
|
const isInPreview = apiDown ? localPreview : (meta?.isInPreview ?? localPreview);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -124,44 +150,31 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
|
|||||||
<span style={{ color: "var(--bb-border)" }}>·</span>
|
<span style={{ color: "var(--bb-border)" }}>·</span>
|
||||||
|
|
||||||
{/* Preview toggle */}
|
{/* 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
|
<span
|
||||||
className="flex items-center gap-1"
|
className="inline-block w-1.5 h-1.5 rounded-full"
|
||||||
style={{ color: isInPreview ? "#16a34a" : "var(--bb-text-muted)" }}
|
style={{ background: isInPreview ? "#22c55e" : "#d1d5db" }}
|
||||||
>
|
/>
|
||||||
<span
|
{togglingPreview
|
||||||
className="inline-block w-1.5 h-1.5 rounded-full"
|
? '...'
|
||||||
style={{ background: isInPreview ? "#22c55e" : "#d1d5db" }}
|
: isInPreview
|
||||||
/>
|
? "В превью"
|
||||||
{isInPreview ? "В превью" : "Не в превью"}
|
: "Не в превью"}
|
||||||
</span>
|
</button>
|
||||||
) : (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{apiDown && (
|
{apiDown && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user