diff --git a/.env.example b/.env.example index 1012a5c0a..eaf4fd17e 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,8 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY= NEXT_PUBLIC_SUPABASE_URL= OPENAI_API_KEY= NEXT_PUBLIC_PRIVY_APP_ID= +# Optional: local api or prod (see docs/PRE_TRIAL_LOCAL.md). Default local = test-recoup-api. +NEXT_PUBLIC_RECOUP_API_URL=http://localhost:3001 PERPLEXITY_MCP_SERVER= XAI_API_KEY= TWILIO_ACCOUNT_SID=your_account_sid diff --git a/components/Header/Header.tsx b/components/Header/Header.tsx index eda5084f5..219fd5528 100644 --- a/components/Header/Header.tsx +++ b/components/Header/Header.tsx @@ -1,7 +1,7 @@ "use client"; import { MenuIcon, PlusCircle } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import SideMenu from "../SideMenu"; import { useArtistProvider } from "@/providers/ArtistProvider"; import ImageWithFallback from "../ImageWithFallback"; @@ -10,8 +10,10 @@ import SideArtists from "../SideArtists"; import type { ArtistRecord } from "@/types/Artist"; const Header = () => { + const [mounted, setMounted] = useState(false); const [isOpenMobileMenu, setIsOpenMobileMenu] = useState(false); const [isOpenSideArtists, setIsOpenSideArtists] = useState(false); + useEffect(() => setMounted(true), []); const { selectedArtist, toggleSettingModal, @@ -20,6 +22,7 @@ const Header = () => { sorted, } = useArtistProvider(); const isMobile = useIsMobile(); + const showMobileChrome = mounted && isMobile; const isArtistSelected = selectedArtist !== null; const handleClickPfp = () => { @@ -38,7 +41,8 @@ const Header = () => { return ( <> -
+
+ {showMobileChrome ? ( + ) : null} - {/* Show Add/Select Artist button when on mobile, logged in, and no artist selected */} - {isMobile && !isArtistSelected && ( + {/* After mount + mobile only — avoids hydration mismatch (useMediaQuery false on server) */} + {showMobileChrome && !isArtistSelected && (
)} - {isMobile && ( + {showMobileChrome && ( <> chat +cd chat +pnpm install +# If pnpm complains about TTY: $env:CI='true'; pnpm install +``` + +## 2. Env (minimum to boot) + +Copy `.env.example` → `.env.local`. + +**Required for the app to start** + +- `NEXT_PUBLIC_PRIVY_APP_ID` — create a free app at [Privy](https://dashboard.privy.io), add **http://localhost:3000** to allowed URLs. + +**Required for account / DB features** + +- `NEXT_PUBLIC_SUPABASE_URL` +- `NEXT_PUBLIC_SUPABASE_ANON_KEY` +- Often `SUPABASE_SERVICE_ROLE_KEY` (server routes) + +Pre-trial devs: doc says you may **obtain your own** env or ask Sweetman for **specific** keys — not a generic “send everything” request. + +## 3. Run + +```bash +pnpm dev +``` + +Open **http://localhost:3000** → screenshot (that’s the pre-trial check). + +## 4. Recoup API (401 locally) + +Local/preview uses **`https://test-recoup-api.vercel.app`** by default. If calls return **401**, the test deployment may not accept your Privy JWT yet. Options: + +| Option | What to do | +|--------|------------| +| A | Ask Sweetman to align **test** API with your Privy app (proper fix for “all local”). | +| B | Temporarily set `NEXT_PUBLIC_RECOUP_API_URL=https://recoup-api.vercel.app` only if you’re allowed to hit prod from localhost. | +| C | Run **api** locally — see **`api/docs/RUN_LOCAL.md`** — then `NEXT_PUBLIC_RECOUP_API_URL=http://localhost:3001` in chat. | + +**Pre-trial goal** is usually: app runs + login works + screenshot — not necessarily every API green on day one. + +## 5. “Run everything locally” + +Not required for pre-trial. Full stack is roughly: + +| Repo | Role | +|------|------| +| **chat** | UI (this repo) | +| **api** | Recoup HTTP API + sandbox setup route | +| **tasks** | Trigger.dev jobs (needs Trigger credentials) | +| **mono** | Cloned inside sandboxes by tasks — not run as a separate local server for chat | + +For daily work, most devs use **deployed** test/prod API + local chat. + +## 6. Windows gotchas + +- **EBUSY on `.next`**: stop all `pnpm dev`, delete `.next`, one dev server only; exclude `.next` from OneDrive/antivirus if needed. +- **Node**: LTS **20** or **22** if Next acts up on Node 24. diff --git a/hooks/useSandboxSetupOnLogin.tsx b/hooks/useSandboxSetupOnLogin.tsx new file mode 100644 index 000000000..45a65d86e --- /dev/null +++ b/hooks/useSandboxSetupOnLogin.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useEffect } from "react"; +import { usePrivy } from "@privy-io/react-auth"; +import { useAccessToken } from "@/hooks/useAccessToken"; +import { setupSandbox } from "@/lib/sandboxes/setupSandbox"; + +const SESSION_KEY = "sandboxSetupDone"; + +export function useSandboxSetupOnLogin() { + const { authenticated } = usePrivy(); + const accessToken = useAccessToken(); + + useEffect(() => { + if (!authenticated || !accessToken) { + return; + } + + if (typeof window === "undefined") { + return; + } + + if (window.sessionStorage.getItem(SESSION_KEY) === "1") { + return; + } + + setupSandbox(accessToken) + .then(() => { + window.sessionStorage.setItem(SESSION_KEY, "1"); + }) + .catch((error) => { + // We deliberately log and continue so login UX is not blocked + console.error("Failed to setup sandbox on login:", error); + }); + }, [authenticated, accessToken]); +} + diff --git a/lib/consts.ts b/lib/consts.ts index ccd36201b..8f523ad41 100644 --- a/lib/consts.ts +++ b/lib/consts.ts @@ -3,7 +3,7 @@ import { Address } from "viem"; export const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production"; export const NEW_API_BASE_URL = IS_PROD ? "https://recoup-api.vercel.app" - : "https://test-recoup-api.vercel.app"; + : "http://localhost:3001"; export const IN_PROCESS_PROTOCOL_ADDRESS = IS_PROD ? ("0x540C18B7f99b3b599c6FeB99964498931c211858" as Address) : ("0x6832A997D8616707C7b68721D6E9332E77da7F6C" as Address); diff --git a/lib/sandboxes/setupSandbox.ts b/lib/sandboxes/setupSandbox.ts new file mode 100644 index 000000000..eef27a623 --- /dev/null +++ b/lib/sandboxes/setupSandbox.ts @@ -0,0 +1,28 @@ +import { NEW_API_BASE_URL } from "@/lib/consts"; + +type SetupSandboxResponse = + | { + status: "success"; + runId: string; + } + | { + status: "error"; + error: string; + }; + +export async function setupSandbox(accessToken: string): Promise { + const response = await fetch(`${NEW_API_BASE_URL}/api/sandboxes/setup`, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to setup sandbox: ${response.status} ${response.statusText}`); + } + + return (await response.json()) as SetupSandboxResponse; +} + diff --git a/next.config.mjs b/next.config.mjs index c5387a5b5..53618720b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -21,42 +21,55 @@ const nextConfig = { ], }, images: { - domains: [ - "i.imgur.com", - "ipfs.decentralized-content.com", - "pbs.twimg.com", // Twitter profile images - "abs.twimg.com", // Twitter media - "cdn.discordapp.com", // Discord - "scontent.xx.fbcdn.net", // Facebook - "scontent.cdninstagram.com", // Instagram - "instagram.fyvr4-1.fna.fbcdn.net", // Instagram - "platform-lookaside.fbsbx.com", // Facebook - "static-cdn.jtvnw.net", // Twitch - "yt3.ggpht.com", // YouTube - "i.ytimg.com", // YouTube - "avatars.githubusercontent.com", // GitHub - "example.com", // Example domain from our mock data - "arweave.net", // Arweave - "storage.googleapis.com", // Fal AI image hosting (backup) - ], remotePatterns: [ { - protocol: 'https', - hostname: '*.fal.media', - port: '', - pathname: '/**', + protocol: "https", + hostname: "*.fal.media", + pathname: "/**", + }, + { + protocol: "https", + hostname: "ipfs.decentralized-content.com", + pathname: "/**", + }, + { + protocol: "https", + hostname: "arweave.net", + pathname: "/**", + }, + { protocol: "https", hostname: "i.imgur.com", pathname: "/**" }, + { protocol: "https", hostname: "pbs.twimg.com", pathname: "/**" }, + { protocol: "https", hostname: "abs.twimg.com", pathname: "/**" }, + { protocol: "https", hostname: "cdn.discordapp.com", pathname: "/**" }, + { protocol: "https", hostname: "scontent.xx.fbcdn.net", pathname: "/**" }, + { + protocol: "https", + hostname: "scontent.cdninstagram.com", + pathname: "/**", + }, + { + protocol: "https", + hostname: "instagram.fyvr4-1.fna.fbcdn.net", + pathname: "/**", + }, + { + protocol: "https", + hostname: "platform-lookaside.fbsbx.com", + pathname: "/**", }, + { protocol: "https", hostname: "static-cdn.jtvnw.net", pathname: "/**" }, + { protocol: "https", hostname: "yt3.ggpht.com", pathname: "/**" }, + { protocol: "https", hostname: "i.ytimg.com", pathname: "/**" }, { - protocol: 'https', - hostname: 'ipfs.decentralized-content.com', - port: '', - pathname: '/**', + protocol: "https", + hostname: "avatars.githubusercontent.com", + pathname: "/**", }, + { protocol: "https", hostname: "example.com", pathname: "/**" }, { - protocol: 'https', - hostname: 'arweave.net', - port: '', - pathname: '/**', + protocol: "https", + hostname: "storage.googleapis.com", + pathname: "/**", }, ], }, diff --git a/providers/Providers.tsx b/providers/Providers.tsx index daf9e895a..80a261485 100644 --- a/providers/Providers.tsx +++ b/providers/Providers.tsx @@ -13,6 +13,7 @@ import WagmiProvider from "./WagmiProvider"; import { MiniAppProvider } from "./MiniAppProvider"; import { ThemeProvider } from "./ThemeProvider"; import { OrganizationProvider } from "./OrganizationProvider"; +import SandboxSetupOnLogin from "./SandboxSetupOnLogin"; const queryClient = new QueryClient(); @@ -30,6 +31,7 @@ const Providers = ({ children }: { children: React.ReactNode }) => ( + diff --git a/providers/SandboxSetupOnLogin.tsx b/providers/SandboxSetupOnLogin.tsx new file mode 100644 index 000000000..655ecfd31 --- /dev/null +++ b/providers/SandboxSetupOnLogin.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { useSandboxSetupOnLogin } from "@/hooks/useSandboxSetupOnLogin"; + +const SandboxSetupOnLogin = () => { + useSandboxSetupOnLogin(); + return null; +}; + +export default SandboxSetupOnLogin; +