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

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

@ -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,23 +150,11 @@ 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 ? (
<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 <button
onClick={togglePreview} onClick={togglePreview}
disabled={togglingPreview || !meta} disabled={togglingPreview || (!apiDown && !meta)}
title={isInPreview ? "Убрать из превью страницы" : "Включить в превью страницы"} 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 ? { style={isInPreview ? {
background: "#dcfce7", background: "#dcfce7",
color: "#16a34a", color: "#16a34a",
@ -159,9 +173,8 @@ export function BlockMetaBar({ path, defaultVersion, defaultIsInPreview }: Block
? '...' ? '...'
: isInPreview : isInPreview
? "В превью" ? "В превью"
: "Добавить в превью"} : "Не в превью"}
</button> </button>
)}
{apiDown && ( {apiDown && (
<> <>

Loading…
Cancel
Save