diff --git a/apps/client-web/components.json b/apps/client-web/components.json new file mode 100644 index 0000000..5e8cc89 --- /dev/null +++ b/apps/client-web/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/apps/client-web/index.html b/apps/client-web/index.html index 1f85e1a..7c5ace0 100644 --- a/apps/client-web/index.html +++ b/apps/client-web/index.html @@ -5,7 +5,7 @@ PARCNET Client - +
diff --git a/apps/client-web/package.json b/apps/client-web/package.json index dc31106..6d9bbe3 100644 --- a/apps/client-web/package.json +++ b/apps/client-web/package.json @@ -16,11 +16,18 @@ "@pcd/gpc": "0.0.8", "@pcd/pod": "0.1.7", "@pcd/proto-pod-gpc-artifacts": "^0.9.0", + "@radix-ui/react-dialog": "^1.1.1", "@semaphore-protocol/core": "^4.0.3", "@semaphore-protocol/identity": "3.15.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", "eventemitter3": "^5.0.1", + "lucide-react": "^0.445.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "tailwind-merge": "^2.5.2", + "tailwindcss-animate": "^1.0.7", "vite-plugin-node-polyfills": "^0.22.0" }, "devDependencies": { diff --git a/apps/client-web/src/App.tsx b/apps/client-web/src/App.tsx index 3398773..13d61cc 100644 --- a/apps/client-web/src/App.tsx +++ b/apps/client-web/src/App.tsx @@ -6,27 +6,21 @@ import { gpcProve } from "@pcd/gpc"; import type { POD } from "@pcd/pod"; import { POD_INT_MAX, POD_INT_MIN } from "@pcd/pod"; import type { Dispatch, ReactNode } from "react"; -import { Fragment, useEffect, useReducer, useState } from "react"; +import { Fragment, useEffect, useState } from "react"; import { ParcnetClientProcessor } from "./client/client"; -import { PODCollection } from "./client/pod_collection"; -import { - getIdentity, - loadPODsFromStorage, - savePODsToStorage -} from "./client/utils"; -import { Rabbit } from "./rabbit"; +import { getIdentity, savePODsToStorage } from "./client/utils"; +import { Layout } from "./components/Layout"; import type { ClientAction, ClientState } from "./state"; -import { clientReducer } from "./state"; +import { useAppState } from "./state"; function App() { - const [state, dispatch] = useReducer(clientReducer, { - loggedIn: false, - advice: null, - zapp: null, - authorized: false, - proofInProgress: undefined, - identity: getIdentity() - }); + const { state, dispatch } = useAppState(); + + useEffect(() => { + state.pods.onUpdate(() => { + savePODsToStorage(state.pods.getAll()); + }); + }, [state.pods]); useEffect(() => { void (async () => { @@ -34,7 +28,7 @@ function App() { dispatch({ type: "set-zapp", zapp }); dispatch({ type: "set-advice", advice }); })(); - }, []); + }, [dispatch]); useEffect(() => { if (state.advice && !state.loggedIn) { @@ -43,46 +37,39 @@ function App() { }, [state.advice, state.loggedIn]); useEffect(() => { - if (state.authorized && state.advice) { + if (state.advice && state.authorized) { state.advice.hideClient(); - const pods = new PODCollection(loadPODsFromStorage()); - pods.onUpdate(() => { - savePODsToStorage(pods.getAll()); - }); state.advice.ready( - new ParcnetClientProcessor(state.advice, pods, dispatch, getIdentity()) + new ParcnetClientProcessor( + state.advice, + state.pods, + dispatch, + state.identity + ) ); } - }, [state.authorized, state.advice]); + }, [state.advice, state.authorized, state.pods, state.identity, dispatch]); return ( -
-
-
-
Welcome to PARCNET
-
- -
-
- {!state.loggedIn && ( - - )} - {state.loggedIn && !state.authorized && state.zapp && ( - + + {!state.loggedIn && ( + + )} + {state.loggedIn && !state.authorized && state.zapp && ( + + )} + {state.loggedIn && + state.authorized && + state.zapp && + state.proofInProgress && ( + )} - {state.loggedIn && - state.authorized && - state.zapp && - state.proofInProgress && ( - - )} -
-
+ ); } @@ -238,7 +225,7 @@ function ProvePODInfo({ ); } -function Prove({ +export function Prove({ proveOperation, dispatch }: { diff --git a/apps/client-web/src/client/gpc.ts b/apps/client-web/src/client/gpc.ts index e8fb5b3..3aaab9f 100644 --- a/apps/client-web/src/client/gpc.ts +++ b/apps/client-web/src/client/gpc.ts @@ -30,10 +30,6 @@ export class ParcnetGPCProcessor implements ParcnetGPCRPC { public async prove(request: PodspecProofRequest): Promise { const prs = proofRequest(request); - - console.dir(prs, { depth: null }); - console.dir(this.pods.getAll(), { depth: null }); - const inputPods = prs.queryForInputs(this.pods.getAll()); if ( Object.values(inputPods).some((candidates) => candidates.length === 0) diff --git a/apps/client-web/src/components/Layout.tsx b/apps/client-web/src/components/Layout.tsx new file mode 100644 index 0000000..8325796 --- /dev/null +++ b/apps/client-web/src/components/Layout.tsx @@ -0,0 +1,17 @@ +import { Rabbit } from "../rabbit"; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( +
+
+
+
Welcome to PARCNET
+
+ +
+
+ {children} +
+
+ ); +} diff --git a/apps/client-web/src/components/ui/dialog.tsx b/apps/client-web/src/components/ui/dialog.tsx new file mode 100644 index 0000000..66ef625 --- /dev/null +++ b/apps/client-web/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription +}; diff --git a/apps/client-web/src/index.css b/apps/client-web/src/index.css index b5c61c9..9adcfd4 100644 --- a/apps/client-web/src/index.css +++ b/apps/client-web/src/index.css @@ -1,3 +1,66 @@ @tailwind base; @tailwind components; @tailwind utilities; +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/client-web/src/lib/utils.ts b/apps/client-web/src/lib/utils.ts new file mode 100644 index 0000000..365058c --- /dev/null +++ b/apps/client-web/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/apps/client-web/src/main.tsx b/apps/client-web/src/main.tsx index 0ebf5a7..292ab80 100644 --- a/apps/client-web/src/main.tsx +++ b/apps/client-web/src/main.tsx @@ -2,9 +2,25 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import App from "./App.tsx"; import "./index.css"; +import { RouterProvider, createBrowserRouter } from "react-router-dom"; +import { HostedZapp } from "./pages/HostedZapp.tsx"; +import { StateProvider } from "./state.tsx"; + +const router = createBrowserRouter([ + { + path: "/", + element: + }, + { + path: "zapps/:zappId", + element: + } +]); createRoot(document.getElementById("root")!).render( - + + + ); diff --git a/apps/client-web/src/pages/HostedZapp.tsx b/apps/client-web/src/pages/HostedZapp.tsx new file mode 100644 index 0000000..abafd19 --- /dev/null +++ b/apps/client-web/src/pages/HostedZapp.tsx @@ -0,0 +1,80 @@ +import { ParcnetClientProcessor } from "@/client/client.ts"; +import { listen } from "@parcnet-js/client-helpers/connection/iframe"; +import { type ReactNode, useEffect, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { Prove } from "../App.tsx"; +import { Layout } from "../components/Layout"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle +} from "../components/ui/dialog"; +import { useAppState } from "../state"; + +export function HostedZapp(): ReactNode { + const { zappId } = useParams(); + const { state, dispatch } = useAppState(); + const zappUrl = state.zapps.get(zappId ?? ""); + + useEffect(() => { + void (async () => { + if (window.parent) { + const { zapp, advice } = await listen(); + dispatch({ type: "set-zapp", zapp }); + dispatch({ type: "set-advice", advice }); + } + })(); + }, [dispatch]); + + useEffect(() => { + if (state.advice) { + state.advice.hideClient(); + state.advice.ready( + new ParcnetClientProcessor( + state.advice, + state.pods, + dispatch, + state.identity + ) + ); + } + }, [state.advice, state.authorized, state.pods, state.identity, dispatch]); + + const modalVisible = useMemo(() => { + return state.proofInProgress !== undefined; + }, [state.proofInProgress]); + + if (!zappUrl) { + return
Unknown zapp!
; + } + + return ( + + + + + Proof in progress + + {state.proofInProgress && ( + + )} + + +
+