Browse Source

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>
main
AR 15 M4 7 days ago
parent
commit
36d5faf67d
  1. 14
      apps/web/app/pages/preview/PreviewClient.tsx
  2. 47
      apps/web/components/ui/BlockMetaBar.tsx

14
apps/web/app/pages/preview/PreviewClient.tsx

@ -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;
}

47
apps/web/components/ui/BlockMetaBar.tsx

@ -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,23 +150,11 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
<span style={{ color: "var(--bb-border)" }}>·</span>
{/* Preview toggle */}
{apiDown ? (
<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}
disabled={togglingPreview || (!apiDown && !meta)}
title={isInPreview ? "Убрать из превью страницы" : "Включить в превью страницы"}
className="flex items-center gap-1.5 px-2.5 py-1 rounded font-medium transition-colors"
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",
@ -159,9 +173,8 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
? '...'
: isInPreview
? "В превью"
: "Добавить в превью"}
: "Не в превью"}
</button>
)}
{apiDown && (
<>

Loading…
Cancel
Save