// --------------------------------------------------------------------------- // useSaas + useSaasMutation — ready-made SaaS API hooks (tyga.run) // --------------------------------------------------------------------------- // PRE-INSTALLED in every React project. Works for ANY tyga.run SaaS module — // scheduling (bookings/eventtypes/availability), crm, ecommerce, billing, // notifications, support, surveys... CodeWriterAgent just imports: // // import { useSaas, useSaasMutation } from "@/hooks/use-tygarun" // const { data: bookings, loading } = useSaas("scheduling", "/bookings", { query: { status: "confirmed" } }) // const { mutate: book, loading: booking } = useSaasMutation("scheduling", "/bookings") // await book({ eventTypeId, customer: { name, email }, scheduledAt }) // // Falls back to per-module seed data until tyga.run is connected, so the // generated site looks alive out of the box (same pattern as use-anydb.ts). // --------------------------------------------------------------------------- import { useState, useEffect, useCallback } from "react"; import tygarun, { isConfigured, auth } from "@/lib/tygarun"; // Seed data keyed by "module:endpoint-prefix" — extend freely. const SEED_DATA: Record = { "scheduling:/bookings": { bookings: [ { id: "bk1", eventTypeName: "30 Minute Consultation", customer: { name: "Alice Johnson", email: "alice@example.com" }, status: "confirmed", scheduledAt: "2026-07-10T10:00:00Z", duration: 30 }, { id: "bk2", eventTypeName: "Discovery Call", customer: { name: "Bob Smith", email: "bob@example.com" }, status: "pending", scheduledAt: "2026-07-11T14:30:00Z", duration: 15 }, { id: "bk3", eventTypeName: "Strategy Session", customer: { name: "Carol Williams", email: "carol@example.com" }, status: "completed", scheduledAt: "2026-07-02T09:00:00Z", duration: 60 }, ], pagination: { page: 1, limit: 20, total: 3, totalPages: 1 }, }, "scheduling:/eventtypes": { eventTypes: [ { id: "evt1", name: "30 Minute Consultation", slug: "30min-consult", duration: 30, isActive: true, isPublic: true, description: "Quick intro call to discuss your needs." }, { id: "evt2", name: "Strategy Session", slug: "strategy-session", duration: 60, isActive: true, isPublic: true, description: "Deep-dive planning session." }, ], }, "scheduling:/availability": { availability: { timezone: "Europe/London", workingHours: [ { day: "monday", start: "09:00", end: "17:00" }, { day: "tuesday", start: "09:00", end: "17:00" }, { day: "wednesday", start: "09:00", end: "17:00" }, { day: "thursday", start: "09:00", end: "17:00" }, { day: "friday", start: "09:00", end: "15:00" }, ], }, }, "crm:/contacts": { contacts: [ { id: "c1", name: "Alice Johnson", email: "alice@example.com", company: "Acme Corp", stage: "customer" }, { id: "c2", name: "Bob Smith", email: "bob@example.com", company: "Globex", stage: "lead" }, ], }, "ecommerce:/products": { products: [ { id: "p1", name: "Classic Leather Jacket", priceCents: 18900, stock: 45, rating: 4.8 }, { id: "p2", name: "Premium Denim Jeans", priceCents: 8900, stock: 120, rating: 4.6 }, ], }, "ecommerce:/catalog": { products: [ { id: "p1", name: "Classic Leather Jacket", priceCents: 18900, stock: 45, rating: 4.8, image: "" }, { id: "p2", name: "Premium Denim Jeans", priceCents: 8900, stock: 120, rating: 4.6, image: "" }, { id: "p3", name: "Merino Wool Sweater", priceCents: 12900, stock: 60, rating: 4.9, image: "" }, ], }, "event-ticketing:/availability": { events: [ { id: "ev1", name: "Live at The Fillmore", date: "2026-08-14T20:00:00Z", venue: "The Fillmore", tiers: [{ id: "t1", name: "General", priceCents: 3500, remaining: 220 }, { id: "t2", name: "VIP", priceCents: 9500, remaining: 30 }] }, { id: "ev2", name: "Summer Sessions", date: "2026-09-02T19:30:00Z", venue: "Open Air Park", tiers: [{ id: "t3", name: "Early Bird", priceCents: 2500, remaining: 0 }, { id: "t4", name: "Standard", priceCents: 4000, remaining: 150 }] }, ], }, "surveys:/": { surveys: [ { id: "sv1", title: "Dark mode", votes: 128, status: "planned", description: "Full dark theme across the app." }, { id: "sv2", title: "Mobile app", votes: 342, status: "in-progress", description: "Native iOS + Android apps." }, { id: "sv3", title: "API access", votes: 87, status: "under-review", description: "Public REST + webhooks." }, ], }, "community:/comments": { comments: [ { id: "cm1", author: "Jamie R.", body: "Would love to see this shipped!", createdAt: "2026-07-01T12:00:00Z" }, { id: "cm2", author: "Priya S.", body: "+1, this blocks our team.", createdAt: "2026-07-03T09:20:00Z" }, ], }, "file-management:/list": { files: [ { id: "f1", name: "gallery-01.jpg", url: "", contentType: "image/jpeg", size: 245000 }, { id: "f2", name: "gallery-02.jpg", url: "", contentType: "image/jpeg", size: 312000 }, { id: "f3", name: "gallery-03.jpg", url: "", contentType: "image/jpeg", size: 198000 }, ], }, "billing:/plans": { plans: [ { id: "starter", name: "Starter", priceCents: 0, interval: "month", features: ["1 project", "Community support"] }, { id: "pro", name: "Pro", priceCents: 2900, interval: "month", features: ["Unlimited projects", "Priority support", "Custom domain"] }, { id: "team", name: "Team", priceCents: 9900, interval: "month", features: ["Everything in Pro", "5 seats", "SSO"] }, ], }, "notifications:/": { notifications: [] }, "support:/tickets": { tickets: [] }, }; function seedFor(module: string, endpoint: string): any { const path = endpoint.startsWith("/") ? endpoint : `/${endpoint}`; // Longest matching prefix wins: "scheduling:/bookings/stats" → "scheduling:/bookings" const keys = Object.keys(SEED_DATA) .filter((k) => `${module}:${path}`.startsWith(k)) .sort((a, b) => b.length - a.length); return keys.length ? SEED_DATA[keys[0]] : { data: [] }; } // Extract the payload array/object from a tyga.run response regardless of // envelope key ({ bookings: [...] }, { data: [...] }, plain array...) export function unwrap(res: any): T { if (Array.isArray(res)) return res as T; if (res?.data !== undefined) return res.data as T; const arrKey = Object.keys(res || {}).find((k) => Array.isArray(res[k])); return (arrKey ? res[arrKey] : res) as T; } interface SaasOptions { query?: Record; autoFetch?: boolean; } interface SaasResult { data: T; raw: any; loading: boolean; error: string | null; connected: boolean; refetch: () => Promise; } /** * Read from ANY tyga.run SaaS module with loading/error states + seed fallback. * * const { data: bookings } = useSaas("scheduling", "/bookings", { query: { status: "confirmed" } }) * const { data: eventTypes } = useSaas("scheduling", "/eventtypes") * const { data: contacts } = useSaas("crm", "/contacts") */ export function useSaas( module: string, endpoint = "/", options: SaasOptions = {} ): SaasResult { const { query = {}, autoFetch = true } = options; const [raw, setRaw] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const connected = isConfigured(); const fetchData = useCallback(async () => { setLoading(true); setError(null); try { if (!connected) { setRaw(seedFor(module, endpoint)); return; } const res = await tygarun(module, endpoint, { query }); setRaw(res); } catch (err: any) { // Connected but call failed → show seed so the UI never breaks setRaw(seedFor(module, endpoint)); setError(err.message || "Request failed"); console.error(`[useSaas] ${module}${endpoint}:`, err); } finally { setLoading(false); } }, [module, endpoint, JSON.stringify(query), connected]); useEffect(() => { if (autoFetch) fetchData(); }, [fetchData, autoFetch]); return { data: unwrap(raw), raw, loading, error, connected, refetch: fetchData }; } interface SaasMutationResult { mutate: (body?: Record, endpointOverride?: string) => Promise; update: (id: string, body: Record) => Promise; remove: (id: string) => Promise; loading: boolean; error: string | null; connected: boolean; } /** * Write to ANY tyga.run SaaS module. * * const { mutate: book } = useSaasMutation("scheduling", "/bookings") * await book({ eventTypeId: "evt1", customer: { name, email }, scheduledAt }) * * const { mutate: addContact, update, remove } = useSaasMutation("crm", "/contacts") * await update("c1", { stage: "customer" }); await remove("c2") */ export function useSaasMutation(module: string, endpoint = "/"): SaasMutationResult { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const connected = isConfigured(); const run = useCallback(async (method: "POST" | "PUT" | "DELETE", path: string, body?: any) => { setLoading(true); setError(null); try { if (!connected) { // Demo mode — pretend success so forms feel functional pre-connection return { success: true, demo: true, id: `demo_${Date.now()}`, ...(body || {}) }; } return await tygarun(module, path, { method, body }); } catch (err: any) { setError(err.message || "Request failed"); throw err; } finally { setLoading(false); } }, [module, connected]); const base = endpoint.replace(/\/+$/, "") || ""; return { mutate: (body = {}, endpointOverride) => run("POST", endpointOverride || endpoint, body), update: (id, body) => run("PUT", `${base}/${id}`, body), remove: (id) => run("DELETE", `${base}/${id}`), loading, error, connected, }; } // ── useAuth — capability: login (tyga.run auth module) ────────────────────── // Reactive wrapper over lib/tygarun's auth object. Gates protected UI (voting, // booking, checkout) behind sign-in. Demo mode: any signin "succeeds" with a // fake user so gated flows are explorable pre-connection. interface AuthUser { email: string; firstName?: string; lastName?: string; [k: string]: any } interface AuthResult { user: AuthUser | null; isSignedIn: boolean; loading: boolean; error: string | null; connected: boolean; signup: (data: { email: string; password: string; [k: string]: any }) => Promise; signin: (email: string, password: string, rememberMe?: boolean) => Promise; signout: () => void; } export function useAuth(): AuthResult { const connected = isConfigured(); const [user, setUser] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { // Rehydrate: a stored token means a prior sign-in this session. if (auth.isSignedIn() && !user) setUser({ email: "member@example.com" }); }, []); // eslint-disable-line react-hooks/exhaustive-deps const wrap = useCallback(async (fn: () => Promise, demoUser: AuthUser) => { setLoading(true); setError(null); try { if (!connected) { setUser(demoUser); return { success: true, demo: true, user: demoUser }; } const res = await fn(); setUser(res?.user || { email: demoUser.email }); return res; } catch (err: any) { setError(err.message || "Authentication failed"); throw err; } finally { setLoading(false); } }, [connected]); return { user, isSignedIn: Boolean(user) || auth.isSignedIn(), loading, error, connected, signup: (data) => wrap(() => auth.signup(data), { email: data.email, firstName: data.firstName, lastName: data.lastName }), signin: (email, password, rememberMe = false) => wrap(() => auth.signin(email, password, rememberMe), { email }), signout: () => { auth.signout(); setUser(null); }, }; } // ── useCart — capability: store (client-side cart + tyga.run checkout) ─────── // Cart lives in React state (+ localStorage); checkout hits ecommerce. Demo // mode returns a fake checkout URL so the flow is walkable pre-connection. export interface CartItem { id: string; name: string; priceCents: number; quantity: number; [k: string]: any } export function useCart() { const connected = isConfigured(); const CART_KEY = "tygarun_cart"; const [items, setItems] = useState(() => { try { return JSON.parse(localStorage.getItem(CART_KEY) || "[]"); } catch { return []; } }); const [loading, setLoading] = useState(false); const persist = useCallback((next: CartItem[]) => { setItems(next); try { localStorage.setItem(CART_KEY, JSON.stringify(next)); } catch { /* SSR */ } }, []); const add = useCallback((item: Omit, qty = 1) => { persist((() => { const existing = items.find((i) => i.id === item.id); return existing ? items.map((i) => (i.id === item.id ? { ...i, quantity: i.quantity + qty } : i)) : [...items, { ...item, quantity: qty }]; })()); }, [items, persist]); const remove = useCallback((id: string) => persist(items.filter((i) => i.id !== id)), [items, persist]); const clear = useCallback(() => persist([]), [persist]); const count = items.reduce((n, i) => n + i.quantity, 0); const totalCents = items.reduce((n, i) => n + i.priceCents * i.quantity, 0); const checkout = useCallback(async () => { setLoading(true); try { if (!connected) return { url: "#demo-checkout", demo: true }; return await tygarun("ecommerce", "/checkout", { method: "POST", body: { items } }); } finally { setLoading(false); } }, [connected, items]); return { items, count, totalCents, add, remove, clear, checkout, loading, connected }; }