"use client" import { useEffect, useState } from "react" import { Store, Plus, Check, Loader2, AlertCircle, Building2, Trash2 } from "lucide-react" import AddStoreModal from "@/components/AddStoreModal" import DeleteStoreModal from "@/components/DeleteStoreModal" import StorefrontChatCustomizeModal, { type StorefrontChatCustomization } from "@/components/StorefrontChatCustomizeModal" interface StoreData { id: string businessName: string shopifyDomain: string isActive: boolean onboardingComplete: boolean storefrontChatEnabled: boolean storefrontChatName: string | null storefrontChatTagline: string | null storefrontChatBrandColor: string | null storefrontChatFontFamily: string | null storefrontChatLogoUrl: string | null createdAt: string } const DEFAULT_CUSTOMIZATION: StorefrontChatCustomization = { storefrontChatName: "JUNO Chat", storefrontChatTagline: "Ask about products", storefrontChatBrandColor: "#4f46e5", storefrontChatFontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif", storefrontChatLogoUrl: "", } export default function StoresPage() { const [stores, setStores] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState("") const [switching, setSwitching] = useState(null) const [togglingStorefrontChat, setTogglingStorefrontChat] = useState(null) const [customizeStoreId, setCustomizeStoreId] = useState(null) const [injectingStoreId, setInjectingStoreId] = useState(null) const [isAddModalOpen, setIsAddModalOpen] = useState(false) /* * Alignment Fix Details: * - Use React `createPortal` to render the modal backdrop and content at the end of `document.body`. * - This ensures `fixed inset-0` is always relative to the viewport, bypassing any parent `transform` styles (like in `PageTransition`) that create new containing blocks. * - Verify that the `fixed inset-0` flex container correctly centers its child vertically and horizontally. */ const [deleteModal, setDeleteModal] = useState<{ isOpen: boolean; storeId: string | null; storeName: string; isActive: boolean }>({ isOpen: false, storeId: null, storeName: "", isActive: false }) useEffect(() => { fetchStores() }, []) const fetchStores = async () => { try { const res = await fetch("/api/stores") if (!res.ok) throw new Error("Failed to fetch stores") const data = await res.json() setStores(data.stores || []) } catch { setError("Could not load stores") } finally { setLoading(false) } } const handleToggleStorefrontChat = async (storeId: string, enabled: boolean) => { setTogglingStorefrontChat(storeId) try { const res = await fetch(`/api/stores/${storeId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ storefrontChatEnabled: enabled }) }) if (!res.ok) { const data = await res.json().catch(() => ({ message: "Failed to update store setting" })) throw new Error(data.message || "Failed to update store setting") } await fetchStores() window.dispatchEvent(new CustomEvent("storeUpdated")) if (enabled) { setCustomizeStoreId(storeId) } } catch (err) { console.error("Toggle storefront chat error:", err) alert("Could not update storefront chat setting. Please try again.") } finally { setTogglingStorefrontChat(null) } } const openCustomize = (storeId: string) => { setCustomizeStoreId(storeId) } const getCustomizationValues = (store: StoreData | undefined): StorefrontChatCustomization => { if (!store) return DEFAULT_CUSTOMIZATION return { storefrontChatName: store.storefrontChatName || DEFAULT_CUSTOMIZATION.storefrontChatName, storefrontChatTagline: store.storefrontChatTagline || DEFAULT_CUSTOMIZATION.storefrontChatTagline, storefrontChatBrandColor: store.storefrontChatBrandColor || DEFAULT_CUSTOMIZATION.storefrontChatBrandColor, storefrontChatFontFamily: store.storefrontChatFontFamily || DEFAULT_CUSTOMIZATION.storefrontChatFontFamily, storefrontChatLogoUrl: store.storefrontChatLogoUrl || "", } } const saveStorefrontCustomization = async (storeId: string, payload: StorefrontChatCustomization) => { const res = await fetch(`/api/stores/${storeId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) if (!res.ok) { const data = await res.json().catch(() => ({ message: "Failed to save storefront customization" })) throw new Error(data.message || "Failed to save storefront customization") } await fetchStores() } const injectStorefrontCustomization = async (storeId: string) => { setInjectingStoreId(storeId) try { const res = await fetch(`/api/stores/${storeId}`, { method: "POST", }) if (!res.ok) { const data = await res.json().catch(() => ({ message: "Failed to inject storefront chat" })) throw new Error(data.message || "Failed to inject storefront chat") } } finally { setInjectingStoreId(null) } } const handleSwitchStore = async (storeId: string) => { setSwitching(storeId) try { const res = await fetch("/api/stores/switch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ storeId }) }) if (!res.ok) { const data = await res.json().catch(() => ({ message: "Failed to switch store" })) alert(data.message || "Failed to switch store. Please ensure the store has valid Shopify credentials.") setSwitching(null) return } // Refresh the page to reload data for the new active store window.location.reload() } catch (err) { console.error("Switch store error:", err) alert("An error occurred while switching stores. Please try again.") setSwitching(null) } } const handleAddStore = () => { setIsAddModalOpen(true) } const handleStoreAdded = () => { fetchStores() // Notify StoreSwitcher to refresh window.dispatchEvent(new CustomEvent("storeUpdated")) } const handleDeleteClick = (store: StoreData) => { setDeleteModal({ isOpen: true, storeId: store.id, storeName: store.businessName, isActive: store.isActive }) } const handleDeleteConfirm = async () => { if (!deleteModal.storeId) return try { const res = await fetch(`/api/stores/${deleteModal.storeId}`, { method: "DELETE" }) if (!res.ok) { const data = await res.json().catch(() => ({ message: "Failed to delete store" })) throw new Error(data.message || "Failed to delete store") } // Refresh stores list await fetchStores() // Notify StoreSwitcher to refresh window.dispatchEvent(new CustomEvent("storeUpdated")) // If deleted store was active, reload page to refresh dashboard data if (deleteModal.isActive) { window.location.reload() } } catch (err) { throw err } } return (

Stores

Manage your connected Shopify stores.

{loading ? (

Loading stores...

) : error ? (

{error}

) : stores.length === 0 ? (

No Stores Connected

Connect your first Shopify store to get started.

) : (
{stores.map((store) => (
{store.isActive && ( Active )}

{store.businessName}

{store.shopifyDomain}

Connected {new Date(store.createdAt).toLocaleDateString()}

Storefront Chat (Customers)

Show chat widget on the storefront

{!store.isActive ? ( ) : null}
))}
)} setIsAddModalOpen(false)} onSuccess={handleStoreAdded} /> setDeleteModal({ isOpen: false, storeId: null, storeName: "", isActive: false })} onConfirm={handleDeleteConfirm} storeName={deleteModal.storeName} isActive={deleteModal.isActive} /> s.id === customizeStoreId)?.businessName || "Store"} initialValues={getCustomizationValues(stores.find((s) => s.id === customizeStoreId))} onClose={() => setCustomizeStoreId(null)} onSave={async (payload) => { if (!customizeStoreId) return await saveStorefrontCustomization(customizeStoreId, payload) }} onInject={async () => { if (!customizeStoreId) return await injectStorefrontCustomization(customizeStoreId) }} />
) }