const { useState, useEffect, useCallback, useMemo, useRef } = React; function AppShell() { const [bootState, setBootState] = useState("loading"); // loading | auth | pick-site | ready | error const [bootError, setBootError] = useState(""); const [user, setUser] = useState(null); const [sites, setSites] = useState([]); const [activeSiteId, setActiveSiteId] = useState(null); const [live, setLive] = useState(null); const [screen, setScreen] = useState("home"); const [review, setReview] = useState(null); const [obStep, setObStep] = useState(0); const [obDismissed, setObDismissed] = useState(false); const manualObOpenRef = useRef(false); const navGuardRef = useRef(null); const [busy, setBusy] = useState(false); const [generatingArticleId, setGeneratingArticleId] = useState(null); const [mobileNavOpen, setMobileNavOpen] = useState(false); const [wpGateDone, setWpGateDone] = useState(false); const navigateScreen = useCallback((next) => { setScreen(next); setMobileNavOpen(false); try { window.scrollTo({ top: 0, left: 0, behavior: "auto" }); document.documentElement.scrollTop = 0; document.body.scrollTop = 0; } catch { window.scrollTo(0, 0); } }, []); function goToScreen(next) { if (navGuardRef.current) navGuardRef.current(next); else navigateScreen(next); } const isWebsiteSite = live?.session?.site?.platform === "WEBSITE"; const wordpressConnected = !!live?.session?.wordpressConnection?.connected; const onboardingCompleted = !!live?.config?.configuration?.onboardingCompleted; const showWpGate = bootState === "ready" && isWebsiteSite && !wordpressConnected && !wpGateDone && !onboardingCompleted; const showOnboarding = !showWpGate && !obDismissed && obStep > 0 && obStep <= 4; const showMascot = true; useEffect(() => { try { window.scrollTo({ top: 0, left: 0, behavior: "auto" }); document.documentElement.scrollTop = 0; document.body.scrollTop = 0; } catch { window.scrollTo(0, 0); } }, [screen]); const refresh = useCallback(async () => { const bootstrap = await GuyAPI.bootstrap(); setLive(bridgeBootstrap(bootstrap)); return bootstrap; }, []); const init = useCallback(async () => { setBootState("loading"); setBootError(""); try { await GuyAPI.health(); let session; try { session = await GuyAPI.session(); } catch { setUser(null); setBootState("auth"); return; } setUser(session.user); const { sites: list, activeSiteId: active } = await GuyAPI.listSites(); setSites(list); setActiveSiteId(active); if (!list.length) { setBootState("pick-site"); return; } if (!active) { if (list.length === 1) { await GuyAPI.selectSite(list[0].id); setActiveSiteId(list[0].id); await refresh(); setBootState("ready"); return; } setBootState("pick-site"); return; } await refresh(); setBootState("ready"); } catch (e) { setBootError(e.message || "Erreur de connexion"); setBootState("error"); } }, [refresh]); useEffect(() => { init(); }, [init]); const articlesGeneratingCount = live?.home?.dashboard?.articlesGeneratingCount ?? 0; useEffect(() => { if (bootState !== "ready" || articlesGeneratingCount === 0) return undefined; const id = window.setInterval(() => { refresh().catch(() => null); }, 8000); return () => window.clearInterval(id); }, [bootState, articlesGeneratingCount, refresh]); useEffect(() => { if (bootState !== "ready") return undefined; if (live?.billing?.current?.status !== "pending") return undefined; let cancelled = false; async function poll() { try { await GuyAPI.syncBilling(); if (!cancelled) await refresh(); } catch { /* ignore */ } } poll(); const timer = window.setInterval(poll, 4000); return () => { cancelled = true; window.clearInterval(timer); }; }, [bootState, live?.billing?.current?.status, refresh]); useEffect(() => { const params = new URLSearchParams(window.location.search); const authError = params.get("auth_error"); if (!authError) return; const messages = { access_denied: "Connexion Google annulée.", no_shop_access: "Aucune boutique SEO GUY rattachée à ce compte Google.", email_unavailable: "Impossible de récupérer l'email Google.", invalid_state: "Session Google expirée. Réessayez.", missing_code_or_state: "Connexion Google incomplète. Réessayez.", }; setBootError(messages[authError] || `Connexion Google échouée (${authError}).`); setBootState("auth"); params.delete("auth_error"); const qs = params.toString(); const next = window.location.pathname + (qs ? `?${qs}` : ""); window.history.replaceState({}, "", next); }, []); useEffect(() => { if (bootState !== "ready") return; const params = new URLSearchParams(window.location.search); const screenParam = params.get("screen"); if (screenParam) setScreen(screenParam); if (params.get("billing") === "ok" || params.get("session_id")) { GuyAPI.syncBilling().then(() => refresh()).catch(() => null); } }, [bootState, refresh]); useEffect(() => { if (!live?.config?.configuration) return; if (manualObOpenRef.current) return; const cfg = live.config.configuration; if (cfg.onboardingCompleted) { setObStep(0); return; } const saved = typeof cfg.currentStep === "number" ? cfg.currentStep : 0; setObStep(Math.min(Math.max(saved + 1, 1), 4)); }, [live?.config?.configuration]); const obInsights = useMemo(() => { const kwList = live?.keywords?.list ?? []; const sorted = [...kwList].sort((a, b) => (b.volume ?? 0) - (a.volume ?? 0)); const top = sorted[0]; const cfgTop = live?.keywords?.opportunities?.[0]; const topKeyword = top ? { keyword: top.keyword, volume: top.volume, difficulty: top.difficulty } : cfgTop ? { keyword: cfgTop.keyword || cfgTop.title, volume: cfgTop.volume, difficulty: cfgTop.difficulty } : null; return { productCount: null, keywordCount: kwList.length, topKeyword, }; }, [live]); useEffect(() => { if (!showOnboarding || obInsights.productCount != null) return; if (live?.session?.site?.platform === "WEBSITE") return; GuyAPI.shopifyProducts() .then((res) => { const count = res?.products?.length ?? 0; setLive((prev) => { if (!prev) return prev; return { ...prev, onboardingInsights: { ...obInsights, productCount: count }, }; }); }) .catch(() => undefined); }, [showOnboarding, obInsights, live?.session?.site?.platform]); async function selectSite(siteId) { setBusy(true); setBootError(""); try { await GuyAPI.selectSite(siteId); setActiveSiteId(siteId); await refresh(); setBootState("ready"); } catch (e) { setBootError(e.message || "Sélection impossible"); } finally { setBusy(false); } } async function handleLogin({ email, password }) { setBusy(true); setBootError(""); try { const res = await GuyAPI.login(email, password); setUser(res.user); const { sites: list, activeSiteId: active } = await GuyAPI.listSites(); setSites(list); setActiveSiteId(active); if (!list.length) { setBootState("pick-site"); return; } if (!active) { if (list.length === 1) { await GuyAPI.selectSite(list[0].id); setActiveSiteId(list[0].id); await refresh(); setBootState("ready"); return; } setBootState("pick-site"); return; } await refresh(); setBootState("ready"); } catch (e) { setBootError(e.message || "Connexion impossible"); } finally { setBusy(false); } } async function handleGoogleLogin() { const returnTo = `${window.location.origin}${window.location.pathname}`; GuyAPI.googleLogin(returnTo); } async function handleDiscoverShops(email) { const res = await GuyAPI.discoverShops(email); return res.shops || []; } async function finishAuthSession(res) { setUser(res.user); const { sites: list, activeSiteId: active } = await GuyAPI.listSites(); setSites(list); setActiveSiteId(active); if (!list.length) { setBootState("pick-site"); return; } if (!active) { if (list.length === 1) { await GuyAPI.selectSite(list[0].id); setActiveSiteId(list[0].id); await refresh(); setBootState("ready"); return; } setBootState("pick-site"); return; } await refresh(); setBootState("ready"); } async function handleRegister(payload) { setBusy(true); setBootError(""); try { const res = await GuyAPI.register(payload); await finishAuthSession(res); } catch (e) { setBootError(e.message || "Inscription impossible"); } finally { setBusy(false); } } async function handleConnectWebsite({ siteUrl, siteName }) { setBusy(true); setBootError(""); try { const res = await GuyAPI.connectWebsite({ siteUrl, siteName }); const siteId = res.site?.id; if (!siteId) throw new Error("Site créé mais identifiant manquant."); await GuyAPI.selectSite(siteId); setActiveSiteId(siteId); const { sites: list } = await GuyAPI.listSites(); setSites(list); await refresh(); setBootState("ready"); } catch (e) { setBootError(e.message || "Rattachement impossible"); throw e; } finally { setBusy(false); } } async function handleLinkShopify(merchantEmail) { setBusy(true); setBootError(""); try { const res = await GuyAPI.linkShopifyAccount(merchantEmail); if (!res.linked) { throw new Error( "Aucune boutique trouvée pour cet email. Installez l'app SEO GUY sur Shopify.", ); } const { sites: list, activeSiteId: active } = await GuyAPI.listSites(); setSites(list); setActiveSiteId(active); if (list.length === 1 && !active) { await GuyAPI.selectSite(list[0].id); setActiveSiteId(list[0].id); await refresh(); setBootState("ready"); return; } setBootState("pick-site"); } catch (e) { setBootError(e.message || "Rattachement impossible"); throw e; } finally { setBusy(false); } } async function handleLogout() { setBusy(true); setBootError(""); try { await GuyAPI.logout(); setUser(null); setSites([]); setActiveSiteId(null); setLive(null); setBootState("auth"); } catch (e) { setBootError(e.message || "Déconnexion impossible"); } finally { setBusy(false); } } const queue = live?.home?.queue ?? []; const published = live?.home?.publishedThisMonth ?? 0; const articleQuota = live?.quota ?? null; const articlesList = live?.articles?.list ?? []; const savedSuggestions = live?.articles?.saved ?? []; const keywords = live?.keywords?.list ?? []; const alerts = live?.alerts ?? []; const kpis = live?.home?.kpis ?? mapKpisFromPerformance(live?.performance?.data); async function runApi(fn) { setBusy(true); try { await fn(); await refresh(); } catch (e) { alert(e.message || "Erreur"); } finally { setBusy(false); } } async function publishArticle(articleId, bodyHtml) { if (bodyHtml?.trim()) { await GuyAPI.articleAction(articleId, { action: "save_draft", bodyHtml }); } const res = await GuyAPI.articleAction(articleId, { action: "publish" }); if (res?.wordpressWarning) alert(res.wordpressWarning); if (res?.wordpressUrl) window.open(res.wordpressUrl, "_blank", "noopener,noreferrer"); } async function saveDraftArticle(articleId, bodyHtml) { await GuyAPI.articleAction(articleId, { action: "save_draft", bodyHtml }); } async function scheduleArticle(articleId, scheduledFor, bodyHtml) { if (bodyHtml?.trim()) { await GuyAPI.articleAction(articleId, { action: "save_draft", bodyHtml }); } await GuyAPI.articleAction(articleId, { action: "schedule", scheduledFor }); } function openArticleReview(item, articleFromApi) { const raw = articleFromApi ? { ...(item.raw || {}), ...articleFromApi, id: articleFromApi.id || item.id } : item.raw || { id: item.id, title: item.title }; setReview({ id: raw.id || item.id, title: articleFromApi?.title || item.title, keyword: item.keyword || item.kw || raw.keywords || "", volume: item.volume || "", readMin: item.readMin || 5, score: item.score ?? null, intro: item.intro || "", bodyHtml: articleFromApi?.bodyHtml || item.bodyHtml || raw.bodyHtml || "", imageUrl: articleFromApi?.imageUrl || item.imageUrl || raw.imageUrl || null, raw, }); } async function assertQuotaForGeneration() { const { quota } = await GuyAPI.getArticleQuota(); if (quota && !quota.unlimited && !quota.canGenerate) { throw new Error(`Quota hebdomadaire atteint (${quota.used}/${quota.limit}).`); } return quota; } async function generateArticleFromSuggestion(item) { const id = item.raw?.id || item.id; if (!id) return; try { await assertQuotaForGeneration(); } catch (e) { alert(e.message); return; } setGeneratingArticleId(id); setBusy(true); setBootError(""); try { const hasContent = item.raw?.hasContent || item.bodyHtml?.trim(); if (!hasContent) { await GuyAPI.writeArticle(id); await refresh(); setLive((prev) => { if (!prev?.articles?.queue) return prev; return { ...prev, articles: { ...prev.articles, queue: prev.articles.queue.filter( (q) => (q.raw?.id || q.id) !== id, ), }, }; }); } const article = await GuyAPI.pollArticleTextReady(id); await refresh(); openArticleReview(item, article); } catch (e) { setBootError(e.message || "Génération impossible"); alert(e.message || "Génération impossible"); } finally { setBusy(false); setGeneratingArticleId(null); } } async function validateAndPublishNext(item) { const id = item?.raw?.id || item?.id; if (!id) throw new Error("Article introuvable."); setBusy(true); try { const { article: current } = await GuyAPI.getArticle(id); let article = current; if (!(current?.status === "content_ready" && current?.bodyHtml?.trim())) { await assertQuotaForGeneration(); await GuyAPI.writeArticle(id); await refresh(); setLive((prev) => { if (!prev?.articles?.queue) return prev; return { ...prev, articles: { ...prev.articles, queue: prev.articles.queue.filter( (q) => (q.raw?.id || q.id) !== id, ), }, }; }); article = await GuyAPI.pollArticleTextReady(id); } const html = article?.bodyHtml?.trim(); if (!html) throw new Error("Aucun contenu à publier."); const res = await GuyAPI.articleAction(id, { action: "publish" }); if (res?.wordpressWarning) alert(res.wordpressWarning); if (res?.wordpressUrl) window.open(res.wordpressUrl, "_blank", "noopener,noreferrer"); await refresh(); } finally { setBusy(false); } } async function generateTitles() { const cap = articleQuota && !articleQuota.unlimited ? Math.min( articleQuota.remaining > 0 ? articleQuota.remaining : 0, articleQuota.limit > 0 ? articleQuota.limit : 8, ) : 8; const count = cap > 0 ? cap : 8; await runApi(() => GuyAPI.generateTitles(count)); } function startDemarrageAssistant() { manualObOpenRef.current = true; setObDismissed(false); setScreen("home"); setObStep(1); } const heroScreens = ["articles", "performance"]; const showHero = heroScreens.includes(screen); const heroMeta = useMemo(() => ({ articles: { eyebrow: "SEO GUY · Articles", title: "Articles SEO", tip: "Générez des titres, relisez chaque article puis publiez ou programmez.", pills: [`${articlesList.length} articles`, live?.articles?.meta?.canPublishLocally ? "Publication Guy" : "Blog à configurer"], actions: ( <> ), guy: POSE.working, }, performance: { eyebrow: "SEO GUY · Performance", title: "Performance globale", tip: "Suivez visites SEO, publications et l'impact de Guy sur votre trafic.", pills: ["30 jours", live?.gsc?.connected ? "GSC connectée" : "GSC"], actions: , guy: POSE.hero, }, }), [articlesList, live, busy]); function renderScreen() { switch (screen) { case "home": return ( <> { setObDismissed(false); setObStep((s) => (s < 1 ? 1 : s)); }} liveDashboard={live?.home?.dashboard} kpis={kpis} busy={busy} clientName={ (live?.session?.account?.displayName || "").trim().split(/\s+/)[0] || (live?.session?.site?.shopName || "").trim() || "" } articlesPerWeek={live?.quota?.limit} gscConnected={!!live?.gsc?.connected} shopifyConnected={!!live?.session?.shopifyConnection?.connected} /> ); case "assistant-start": case "assistants": return ( ); case "performance": return ( ); case "articles": return ( { void refresh(); }} /> ); case "gsc": return ; case "config": return runApi(() => GuyAPI.saveConfig(payload))} busy={busy} />; case "billing": return ( { const res = await GuyAPI.selectPlan({ planCode: code, cycle: cycle || "monthly", couponCode: coupon || undefined, }); if (res?.redirectUrl) { window.location.href = res.redirectUrl; return; } await refresh(); }} busy={busy} /> ); case "account": return ( ); case "settings": return ( { await runApi(() => GuyAPI.saveSettings(payload)); await refresh(); }} onSaveSchedule={async (payload) => { await GuyAPI.savePublicationSchedule(payload); await refresh(); }} onSaveConfig={async (payload) => { await runApi(() => GuyAPI.saveConfig(payload)); await refresh(); }} onResyncShopify={async () => { const res = await GuyAPI.resyncShopifyArticles(); await refresh(); return res; }} onSaveShopifyConnection={async (payload) => { await runApi(() => GuyAPI.saveShopifyConnection(payload)); await refresh(); }} onTestShopifyConnection={(payload) => GuyAPI.testShopifyConnection(payload)} onDisconnectShopify={async () => { await runApi(() => GuyAPI.disconnectShopifyConnection()); await refresh(); }} wordpress={live?.session?.wordpressConnection || live?.settings?.wordpressConnection} onSaveWordpressConnection={async (payload) => { await runApi(() => GuyAPI.saveWordpressConnection(payload)); await refresh(); }} onTestWordpressConnection={(payload) => GuyAPI.testWordpressConnection(payload)} onDisconnectWordpress={async () => { await runApi(() => GuyAPI.disconnectWordpressConnection()); await refresh(); }} busy={busy} /> ); case "results": return ; default: return null; } } if (bootState === "loading") { return (

Chargement du back-office…

); } if (bootState === "auth" || bootState === "pick-site" || bootState === "error") { const loginMode = bootState === "pick-site" || (bootState === "error" && user) ? "pick-site" : "auth"; return ( ); } const hero = heroMeta[screen]; return (
setMobileNavOpen(true)} /> goToScreen("account")} siteLabel={live?.session?.site?.shopName || live?.session?.site?.shop} planLabel={live?.session?.plan?.name} mobileOpen={mobileNavOpen} onClose={() => setMobileNavOpen(false)} />
{showHero && hero && ( )} {renderScreen()}
{review && ( setReview(null)} busy={busy} onRefresh={refresh} onPublish={(id, html) => publishArticle(id, html)} onSaveDraft={(id, html) => saveDraftArticle(id, html)} onSchedule={(id, when, html) => scheduleArticle(id, when, html)} /> )} {showWpGate && ReactDOM.createPortal( { setWpGateDone(true); await refresh(); }} onSkip={() => setWpGateDone(true)} />, document.body, )} {showOnboarding && ReactDOM.createPortal( { manualObOpenRef.current = false; setObDismissed(false); setObStep(0); refresh(); }} onExit={() => { manualObOpenRef.current = true; setObDismissed(true); refresh(); }} onOpenReview={openArticleReview} insights={live?.onboardingInsights || obInsights} plans={live?.plans ?? PLANS} currentPlanCode={live?.session?.plan?.code || live?.billing?.current?.code} onboardingCompleted={!!live?.config?.configuration?.onboardingCompleted} featuredProducts={live?.config?.configuration?.featuredProducts ?? []} businessSummary={live?.config?.configuration?.editorialCharter ?? ""} personas={live?.config?.configuration?.targetAudiences ?? live?.personas} keywords={live?.keywords?.list ?? keywords} gscConnected={live?.gsc?.connected} quota={articleQuota} isWebsite={live?.session?.site?.platform === "WEBSITE"} />, document.body, )}
); } function NavGuardBridge({ bridgeRef }) { const requestNavigate = useGuardedNavigate(); useEffect(() => { bridgeRef.current = requestNavigate; return () => { bridgeRef.current = null; }; }, [requestNavigate, bridgeRef]); return null; } function App() { return ; } ReactDOM.createRoot(document.getElementById("root")).render();