From a4421f993e06d04f249a57e5ccab10b18d78d2a1 Mon Sep 17 00:00:00 2001 From: Alejandro Gil Date: Thu, 5 Feb 2026 12:01:31 -0800 Subject: [PATCH] feat(menu): sync discovery page to Synvya.com on menu publish Integrate unified publish flow into Menu page: - Add fetchAndPublishDiscovery call after both Square and spreadsheet menu publishing succeeds - Track publish step state ("nostr" | "synvya") for progress UI - Display multi-step progress indicator during publishing - Handle Synvya.com errors gracefully with contact support option - Nostr publishing succeeds independently; Synvya errors shown separately Closes #293 Co-authored-by: Cursor --- client/src/pages/Menu.tsx | 142 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 137 insertions(+), 5 deletions(-) diff --git a/client/src/pages/Menu.tsx b/client/src/pages/Menu.tsx index dc5834a..ae2e9a1 100644 --- a/client/src/pages/Menu.tsx +++ b/client/src/pages/Menu.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { PublicationPreview } from "@/components/PublicationPreview"; -import { Store, FileSpreadsheet, ArrowRight, Check, RefreshCw, AlertCircle } from "lucide-react"; +import { Store, FileSpreadsheet, ArrowRight, Check, RefreshCw, AlertCircle, Loader2, Mail } from "lucide-react"; import { buildSquareAuthorizeUrl } from "@/lib/square/auth"; import { fetchSquareStatus, @@ -22,6 +22,7 @@ import { buildDeletionEventByAddress } from "@/lib/handlerEvents"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import sampleSpreadsheetUrl from "@/assets/Sample Menu Importer.xlsx?url"; import { buildSpreadsheetPreviewEvents, parseMenuSpreadsheetXlsx } from "@/lib/spreadsheet/menuSpreadsheet"; +import { fetchAndPublishDiscovery } from "@/services/discoveryPublish"; import { cn } from "@/lib/utils"; type MenuSource = "square" | "spreadsheet"; @@ -89,6 +90,7 @@ export function MenuPage(): JSX.Element { setLocation: state.setLocation, })); const setMenuPublished = useOnboardingProgress((state) => state.setMenuPublished); + const setDiscoveryPageUrl = useOnboardingProgress((state) => state.setDiscoveryPageUrl); const profilePublished = useOnboardingProgress((state) => state.profilePublished); const location = useLocation(); @@ -122,6 +124,10 @@ export function MenuPage(): JSX.Element { const [sheetPreviewLoading, setSheetPreviewLoading] = useState(false); const [sheetPublishBusy, setSheetPublishBusy] = useState(false); + // Multi-step publish progress and Synvya.com error handling + const [publishStep, setPublishStep] = useState<"nostr" | "synvya" | null>(null); + const [synvyaError, setSynvyaError] = useState(null); + // Compute current workflow step const currentStep: WorkflowStep = useMemo(() => { if (selectedSource === "square") { @@ -349,8 +355,11 @@ export function MenuPage(): JSX.Element { } setSheetError(null); setSheetNotice(null); + setSynvyaError(null); setSheetPublishBusy(true); try { + // Step 1: Publish to Nostr + setPublishStep("nostr"); const publishedIds: string[] = []; // Publish products first, then collections (safer for references) const ordered = [ @@ -363,7 +372,6 @@ export function MenuPage(): JSX.Element { await publishToRelays(signed, relays); publishedIds.push(signed.id); } - setSheetNotice(`Published ${publishedIds.length} event${publishedIds.length === 1 ? "" : "s"} from spreadsheet.`); setSheetPreviewViewed(false); setPreviewViewed(false); setSheetPreviewEvents(null); @@ -373,11 +381,26 @@ export function MenuPage(): JSX.Element { if (activeSource === "spreadsheet") { setActiveSource(null); } + + // Step 2: Publish discovery page to Synvya.com + setPublishStep("synvya"); + try { + const discoveryResult = await fetchAndPublishDiscovery(pubkey, relays); + setDiscoveryPageUrl(discoveryResult.url); + setSheetNotice(`Published ${publishedIds.length} event${publishedIds.length === 1 ? "" : "s"} and updated discovery page.`); + } catch (synvyaErr) { + // Nostr publish succeeded, but Synvya.com failed + console.error("Failed to publish to Synvya.com:", synvyaErr); + const errorMessage = synvyaErr instanceof Error ? synvyaErr.message : "Failed to publish discovery page"; + setSynvyaError(errorMessage); + setSheetNotice(`Published ${publishedIds.length} event${publishedIds.length === 1 ? "" : "s"} (discovery page update failed).`); + } } catch (error) { const message = error instanceof Error ? error.message : "Failed to publish spreadsheet menu."; setSheetError(message); } finally { setSheetPublishBusy(false); + setPublishStep(null); } }; @@ -434,8 +457,11 @@ export function MenuPage(): JSX.Element { if (!pubkey) return; setSquareError(null); setSquareNotice(null); + setSynvyaError(null); setResyncBusy(true); try { + // Step 1: Publish to Nostr + setPublishStep("nostr"); const profileLocation = await resolveProfileLocation(pubkey, relays, cachedProfileLocation); if (profileLocation && profileLocation !== cachedProfileLocation) { setCachedProfileLocation(profileLocation); @@ -449,13 +475,35 @@ export function MenuPage(): JSX.Element { if (effectiveLocation && effectiveLocation !== cachedProfileLocation) { setCachedProfileLocation(effectiveLocation); } + + // Helper to publish discovery page after successful Nostr publish + const publishDiscovery = async (): Promise<{ success: boolean; url?: string; error?: string }> => { + setPublishStep("synvya"); + try { + const discoveryResult = await fetchAndPublishDiscovery(pubkey, relays); + setDiscoveryPageUrl(discoveryResult.url); + return { success: true, url: discoveryResult.url }; + } catch (synvyaErr) { + console.error("Failed to publish to Synvya.com:", synvyaErr); + const errorMessage = synvyaErr instanceof Error ? synvyaErr.message : "Failed to publish discovery page"; + setSynvyaError(errorMessage); + return { success: false, error: errorMessage }; + } + }; + if (!events.length) { - setSquareNotice("Square catalog is already up to date."); setStatusVersion((value) => value + 1); setPreviewViewed(false); setPreviewEvents(null); setPublishSuccess(true); setMenuPublished(true); + // Step 2: Publish discovery page + const discoveryResult = await publishDiscovery(); + if (discoveryResult.success) { + setSquareNotice("Square catalog is already up to date. Discovery page updated."); + } else { + setSquareNotice("Square catalog is already up to date (discovery page update failed)."); + } return; } @@ -505,12 +553,19 @@ export function MenuPage(): JSX.Element { ); } if (messages.length > 0) { - setSquareNotice(messages.join(" ")); setStatusVersion((value) => value + 1); setPreviewViewed(false); setPreviewEvents(null); setPublishSuccess(true); setMenuPublished(true); + // Step 2: Publish discovery page after successful Nostr publish + const discoveryResult = await publishDiscovery(); + if (discoveryResult.success) { + messages.push("Discovery page updated."); + } else { + messages.push("(Discovery page update failed)"); + } + setSquareNotice(messages.join(" ")); } const errorMessages: string[] = []; @@ -534,18 +589,25 @@ export function MenuPage(): JSX.Element { !updateFailures.length && !deletionFailures.length ) { - setSquareNotice("No listings required publishing."); setStatusVersion((value) => value + 1); setPreviewViewed(false); setPreviewEvents(null); setPublishSuccess(true); setMenuPublished(true); + // Step 2: Publish discovery page + const discoveryResult = await publishDiscovery(); + if (discoveryResult.success) { + setSquareNotice("No listings required publishing. Discovery page updated."); + } else { + setSquareNotice("No listings required publishing (discovery page update failed)."); + } } } catch (error) { const message = error instanceof Error ? error.message : "Failed to publish catalog to Nostr."; setSquareError(message); } finally { setResyncBusy(false); + setPublishStep(null); } }; @@ -710,6 +772,41 @@ export function MenuPage(): JSX.Element { ) : null} + {/* Publishing progress indicator */} + {resyncBusy && publishStep && ( +
+ +
+ + {publishStep === "nostr" ? "1. Publishing to Nostr" : "1. Published"} + + + + 2. Updating discovery page + +
+
+ )} + + {/* Synvya.com error with contact support option */} + {synvyaError && ( +
+

Discovery page update failed

+

{synvyaError}

+ + + Contact support@synvya.com + +
+ )} + {/* Step 1: Connect */} {currentStep === 1 && (
@@ -937,6 +1034,41 @@ export function MenuPage(): JSX.Element {
) : null} + {/* Publishing progress indicator */} + {sheetPublishBusy && publishStep && ( +
+ +
+ + {publishStep === "nostr" ? "1. Publishing to Nostr" : "1. Published"} + + + + 2. Updating discovery page + +
+
+ )} + + {/* Synvya.com error with contact support option */} + {synvyaError && ( +
+

Discovery page update failed

+

{synvyaError}

+ + + Contact support@synvya.com + +
+ )} + {/* Step 1: Upload */} {currentStep === 1 && (