diff --git a/organizations-example/.cta.json b/organizations-example/.cta.json new file mode 100644 index 0000000..b2179e3 --- /dev/null +++ b/organizations-example/.cta.json @@ -0,0 +1,13 @@ +{ + "projectName": "organizations-example", + "mode": "code-router", + "typescript": true, + "tailwind": true, + "packageManager": "yarn", + "git": true, + "version": 1, + "framework": "react-cra", + "chosenAddOns": [ + "eslint" + ] +} \ No newline at end of file diff --git a/organizations-example/.gitignore b/organizations-example/.gitignore new file mode 100644 index 0000000..693a705 --- /dev/null +++ b/organizations-example/.gitignore @@ -0,0 +1,9 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +count.txt +.env +.nitro +.tanstack diff --git a/organizations-example/.prettierignore b/organizations-example/.prettierignore new file mode 100644 index 0000000..5322d7f --- /dev/null +++ b/organizations-example/.prettierignore @@ -0,0 +1,3 @@ +package-lock.json +pnpm-lock.yaml +yarn.lock \ No newline at end of file diff --git a/organizations-example/.vscode/settings.json b/organizations-example/.vscode/settings.json new file mode 100644 index 0000000..00b5278 --- /dev/null +++ b/organizations-example/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.watcherExclude": { + "**/routeTree.gen.ts": true + }, + "search.exclude": { + "**/routeTree.gen.ts": true + }, + "files.readonlyInclude": { + "**/routeTree.gen.ts": true + } +} diff --git a/organizations-example/README.md b/organizations-example/README.md new file mode 100644 index 0000000..f8d8151 --- /dev/null +++ b/organizations-example/README.md @@ -0,0 +1,491 @@ +Welcome to your new TanStack app! + +# Getting Started + +To run this application: + +```bash +yarn install +yarn run start +``` + +# Building For Production + +To build this application for production: + +```bash +yarn run build +``` + +## Testing + +This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with: + +```bash +yarn run test +``` + +## Styling + +This project uses [Tailwind CSS](https://tailwindcss.com/) for styling. + + +## Linting & Formatting + + +This project uses [eslint](https://eslint.org/) and [prettier](https://prettier.io/) for linting and formatting. Eslint is configured using [tanstack/eslint-config](https://tanstack.com/config/latest/docs/eslint). The following scripts are available: + +```bash +yarn run lint +yarn run format +yarn run check +``` + + + +## Routing +This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a code based router. Which means that the routes are defined in code (in the `./src/main.tsx` file). If you like you can also use a file based routing setup by following the [File Based Routing](https://tanstack.com/router/latest/docs/framework/react/guide/file-based-routing) guide. + +### Adding A Route + +To add a new route to your application just add another `createRoute` call to the `./src/main.tsx` file. The example below adds a new `/about`route to the root route. + +```tsx +const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/about", + component: () =>

About

, +}); +``` + +You will also need to add the route to the `routeTree` in the `./src/main.tsx` file. + +```tsx +const routeTree = rootRoute.addChildren([indexRoute, aboutRoute]); +``` + +With this set up you should be able to navigate to `/about` and see the about page. + +Of course you don't need to implement the About page in the `main.tsx` file. You can create that component in another file and import it into the `main.tsx` file, then use it in the `component` property of the `createRoute` call, like so: + +```tsx +import About from "./components/About.tsx"; + +const aboutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/about", + component: About, +}); +``` + +That is how we have the `App` component set up with the home page. + +For more information on the options you have when you are creating code based routes check out the [Code Based Routing](https://tanstack.com/router/latest/docs/framework/react/guide/code-based-routing) documentation. + +Now that you have two routes you can use a `Link` component to navigate between them. + +### Adding Links + +To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. + +```tsx +import { Link } from "@tanstack/react-router"; +``` + +Then anywhere in your JSX you can use it like so: + +```tsx +About +``` + +This will create a link that will navigate to the `/about` route. + +More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent). + +### Using A Layout + + +Layouts can be used to wrap the contents of the routes in menus, headers, footers, etc. + +There is already a layout in the `src/main.tsx` file: + +```tsx +const rootRoute = createRootRoute({ + component: () => ( + <> + + + + ), +}); +``` + +You can use the React component specified in the `component` property of the `rootRoute` to wrap the contents of the routes. The `` component is used to render the current route within the body of the layout. For example you could add a header to the layout like so: + +```tsx +import { Link } from "@tanstack/react-router"; + +const rootRoute = createRootRoute({ + component: () => ( + <> +
+ +
+ + + + ), +}); +``` + +The `` component is not required so you can remove it if you don't want it in your layout. + +More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts). + + +### Migrating To File Base Routing + +First you need to add the Vite plugin for Tanstack Router: + +```bash +yarn add @tanstack/router-plugin --dev +``` + +From there you need to update your `vite.config.js` file to use the plugin: + +```ts +import { defineConfig } from "vite"; +import viteReact from "@vitejs/plugin-react"; +import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; +import tailwindcss from "@tailwindcss/vite"; + + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + TanStackRouterVite(), + viteReact(), + tailwindcss() + ], +}); +``` + +Now you'll need to rearrange your files a little bit. That starts with creating a `routes` directory in the `src` directory: + +```bash +mkdir src/routes +``` + +Then you'll need to create a `src/routes/__root.tsx` file with the contents of the root route that was in `main.tsx`. + +```tsx +import { Outlet, createRootRoute } from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; + +export const Route = createRootRoute({ + component: () => ( + <> + + + + ), +}); +``` + +Next up you'll need to move your home route code into `src/routes/index.tsx` + +```tsx +import { createFileRoute } from "@tanstack/react-router"; + +import logo from "../logo.svg"; +import "../App.css"; + +export const Route = createFileRoute("/")({ + component: App, +}); + +function App() { + return ( +
+
+ logo +

+ Edit src/App.tsx and save to reload. +

+ + Learn React + + + Learn TanStack + +
+
+ ); +} +``` + +At this point you can delete `src/App.tsx`, you will no longer need it as the contents have moved into `src/routes/index.tsx`. + +The only additional code is the `createFileRoute` function that tells TanStack Router where to render the route. Helpfully the Vite plugin will keep the path argument that goes to `createFileRoute` automatically in sync with the file system. + +Finally the `src/main.tsx` file can be simplified down to this: + +```tsx +import { StrictMode } from "react"; +import ReactDOM from "react-dom/client"; +import { RouterProvider, createRouter } from "@tanstack/react-router"; + +// Import the generated route tree +import { routeTree } from "./routeTree.gen"; + +import "./styles.css"; +import reportWebVitals from "./reportWebVitals.ts"; + +// Create a new router instance +const router = createRouter({ + routeTree, + defaultPreload: "intent", + defaultPreloadStaleTime: 0, + scrollRestoration: true, + defaultStructuralSharing: true +}); + +// Register the router instance for type safety +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +// Render the app +const rootElement = document.getElementById("app")!; +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement); + root.render( + + + + ); +} + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); +``` + +Now you've got a file based routing setup in your project! Let's have some fun with it! Just create a file in `about.tsx` in `src/routes` and it if the application is running TanStack will automatically add contents to the file and you'll have the start of your `/about` route ready to go with no additional work. You can see why folks find File Based Routing so easy to use. + +You can find out everything you need to know on how to use file based routing in the [File Based Routing](https://tanstack.com/router/latest/docs/framework/react/guide/file-based-routing) documentation. + +## Data Fetching + +There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered. + +For example: + +```tsx +const peopleRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/people", + loader: async () => { + const response = await fetch("https://swapi.dev/api/people"); + return response.json() as Promise<{ + results: { + name: string; + }[]; + }>; + }, + component: () => { + const data = peopleRoute.useLoaderData(); + return ( +
    + {data.results.map((person) => ( +
  • {person.name}
  • + ))} +
+ ); + }, +}); +``` + +Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters). + +### React-Query + +React-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze. + +First add your dependencies: + +```bash +yarn add @tanstack/react-query @tanstack/react-query-devtools +``` + +Next we'll need to create a query client and provider. We recommend putting those in `main.tsx`. + +```tsx +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +// ... + +const queryClient = new QueryClient(); + +// ... + +if (!rootElement.innerHTML) { + const root = ReactDOM.createRoot(rootElement); + + root.render( + + + + ); +} +``` + +You can also add TanStack Query Devtools to the root route (optional). + +```tsx +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; + +const rootRoute = createRootRoute({ + component: () => ( + <> + + + + + ), +}); +``` + +Now you can use `useQuery` to fetch your data. + +```tsx +import { useQuery } from "@tanstack/react-query"; + +import "./App.css"; + +function App() { + const { data } = useQuery({ + queryKey: ["people"], + queryFn: () => + fetch("https://swapi.dev/api/people") + .then((res) => res.json()) + .then((data) => data.results as { name: string }[]), + initialData: [], + }); + + return ( +
+
    + {data.map((person) => ( +
  • {person.name}
  • + ))} +
+
+ ); +} + +export default App; +``` + +You can find out everything you need to know on how to use React-Query in the [React-Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview). + +## State Management + +Another common requirement for React applications is state management. There are many options for state management in React. TanStack Store provides a great starting point for your project. + +First you need to add TanStack Store as a dependency: + +```bash +yarn add @tanstack/store +``` + +Now let's create a simple counter in the `src/App.tsx` file as a demonstration. + +```tsx +import { useStore } from "@tanstack/react-store"; +import { Store } from "@tanstack/store"; +import "./App.css"; + +const countStore = new Store(0); + +function App() { + const count = useStore(countStore); + return ( +
+ +
+ ); +} + +export default App; +``` + +One of the many nice features of TanStack Store is the ability to derive state from other state. That derived state will update when the base state updates. + +Let's check this out by doubling the count using derived state. + +```tsx +import { useStore } from "@tanstack/react-store"; +import { Store, Derived } from "@tanstack/store"; +import "./App.css"; + +const countStore = new Store(0); + +const doubledStore = new Derived({ + fn: () => countStore.state * 2, + deps: [countStore], +}); +doubledStore.mount(); + +function App() { + const count = useStore(countStore); + const doubledCount = useStore(doubledStore); + + return ( +
+ +
Doubled - {doubledCount}
+
+ ); +} + +export default App; +``` + +We use the `Derived` class to create a new store that is derived from another store. The `Derived` class has a `mount` method that will start the derived store updating. + +Once we've created the derived store we can use it in the `App` component just like we would any other store using the `useStore` hook. + +You can find out everything you need to know on how to use TanStack Store in the [TanStack Store documentation](https://tanstack.com/store/latest). + +# Demo files + +Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed. + +# Learn More + +You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com). diff --git a/organizations-example/eslint.config.js b/organizations-example/eslint.config.js new file mode 100644 index 0000000..676b32a --- /dev/null +++ b/organizations-example/eslint.config.js @@ -0,0 +1,5 @@ +// @ts-check + +import { tanstackConfig } from '@tanstack/eslint-config' + +export default [...tanstackConfig] diff --git a/organizations-example/index.html b/organizations-example/index.html new file mode 100644 index 0000000..d0f3085 --- /dev/null +++ b/organizations-example/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + Create TanStack App - organizations-example + + +
+ + + diff --git a/organizations-example/package.json b/organizations-example/package.json new file mode 100644 index 0000000..165bfb3 --- /dev/null +++ b/organizations-example/package.json @@ -0,0 +1,42 @@ +{ + "name": "organizations-example", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --port 3000", + "start": "vite --port 3000", + "build": "vite build && tsc", + "serve": "vite preview", + "test": "vitest run", + "lint": "eslint", + "format": "prettier", + "check": "prettier --write . && eslint --fix" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@tailwindcss/vite": "^4.0.6", + "@tanstack/react-devtools": "^0.2.2", + "@tanstack/react-router": "^1.130.2", + "@tanstack/react-router-devtools": "^1.131.5", + "lucide-react": "^0.544.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.0.6", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@tanstack/eslint-config": "^0.3.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.2.0", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^26.0.0", + "prettier": "^3.5.3", + "typescript": "^5.7.2", + "vite": "^6.3.5", + "vitest": "^3.0.5", + "web-vitals": "^4.2.4" + } +} diff --git a/organizations-example/prettier.config.js b/organizations-example/prettier.config.js new file mode 100644 index 0000000..aea1c48 --- /dev/null +++ b/organizations-example/prettier.config.js @@ -0,0 +1,10 @@ +// @ts-check + +/** @type {import('prettier').Config} */ +const config = { + semi: false, + singleQuote: true, + trailingComma: "all", +}; + +export default config; diff --git a/organizations-example/public/favicon.ico b/organizations-example/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/organizations-example/public/favicon.ico differ diff --git a/organizations-example/public/logo192.png b/organizations-example/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/organizations-example/public/logo192.png differ diff --git a/organizations-example/public/logo512.png b/organizations-example/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/organizations-example/public/logo512.png differ diff --git a/organizations-example/public/manifest.json b/organizations-example/public/manifest.json new file mode 100644 index 0000000..078ef50 --- /dev/null +++ b/organizations-example/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "TanStack App", + "name": "Create TanStack App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/organizations-example/public/robots.txt b/organizations-example/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/organizations-example/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/organizations-example/src/App.test.tsx b/organizations-example/src/App.test.tsx new file mode 100644 index 0000000..253e723 --- /dev/null +++ b/organizations-example/src/App.test.tsx @@ -0,0 +1,10 @@ +import { describe, expect, test } from 'vitest' +import { render, screen } from '@testing-library/react' +import App from './App.tsx' + +describe('App', () => { + test('renders', () => { + render() + expect(screen.getByText('Learn React')).toBeDefined() + }) +}) diff --git a/organizations-example/src/App.tsx b/organizations-example/src/App.tsx new file mode 100644 index 0000000..3211746 --- /dev/null +++ b/organizations-example/src/App.tsx @@ -0,0 +1,388 @@ +import { Link } from 'lucide-react' +import { useEffect, useState } from 'react' +import { AgentsTable } from './components/AgentsTable' +import { CreateAgentForm } from './components/CreateAgentForm' +import StripeConnectionModal from './components/StripeConnectionModal' +import { UserDropdown } from './components/UserDropdown' +import { getCheckoutUrl } from './config/environment' +import { createAgentAndPlan, createUserByOrganization, generateAgentAndPlanData, getProfile, getUserAgents } from './services/nevermined-api' +import { clearWalletResult, getWalletResult } from './services/storage' +import type { ApiError, CreateAgentPlanRequest, WalletResult } from './types/api' + +function App() { + const [message, setMessage] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [walletResult, setWalletResult] = useState(null) + const [createdAgent, setCreatedAgent] = useState(null) + const [showStripeModal, setShowStripeModal] = useState(false); + const [userEmail, setUserEmail] = useState('') + const [userCountryCode, setUserCountryCode] = useState('') + const [profile, setProfile] = useState(null) + const [userId, setUserId] = useState('') + const [userAgents, setUserAgents] = useState(null) + + // Check localStorage on component mount + useEffect(() => { + const savedWalletResult = getWalletResult() + if (savedWalletResult) { + setWalletResult(savedWalletResult) + } + }, []) + + const handleConnectNevermined = async () => { + setIsLoading(true) + setError(null) + setMessage('') + + try { + + const userData = { + uniqueExternalId: userId, + email: userEmail, + } + + // Make API call + const response = await createUserByOrganization(userData) + + setMessage(`✅ Success! User created: ${userData.uniqueExternalId}`) + console.log('✅ Success:', response) + + // Update wallet result state if available + if (response.walletResult) { + setWalletResult(response.walletResult) + } + + } catch (err) { + const apiError = err as ApiError + const errorMessage = `❌ Error: ${apiError.message}` + setError(errorMessage) + console.error('❌ Connection failed:', apiError) + } finally { + setIsLoading(false) + } + } + + const handleCreateAgentPlan = async (formData: CreateAgentPlanRequest) => { + setIsLoading(true) + setError(null) + setMessage('') + + try { + if (!walletResult) { + throw new Error('Wallet result not available') + } + + console.log('🔄 Creating agent and plan with data:', formData) + console.log('📋 Using wallet result:', walletResult) + + // Generate complete agent and plan data + const agentPlanData = generateAgentAndPlanData(formData, walletResult) + console.log('🏗️ Generated agent and plan data:', agentPlanData) + + // Make API call to create agent and plan + const response = await createAgentAndPlan(agentPlanData, walletResult) + + // Extract agent data from response + const agentId = response?.data?.agentId || response?.agentId; + const planId = response?.data?.planId || response?.planId; + + // Store created agent data + const agentData = { + agentId, + planId, + price: formData.price, + currency: formData.currency || 'USD', + name: formData.name || `Agent-${Date.now()}`, + checkoutUrl: agentId ? getCheckoutUrl(agentId) : null, + createdAt: new Date().toISOString(), + }; + + setCreatedAgent(agentData) + setMessage('success') // Simple success flag + + console.log('✅ Agent and Plan created:', response) + console.log('🔗 Checkout URL:', agentData.checkoutUrl) + + } catch (err) { + const apiError = err as ApiError + const errorMessage = `❌ Error creating agent: ${apiError.message}` + setError(errorMessage) + console.error('❌ Agent creation failed:', apiError) + } finally { + setIsLoading(false) + } + } + + const handleStripeConnection = () => { + setShowStripeModal(true); + }; + + // Email validation function + const isValidEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + // Check if form is valid + const isFormValid = (): boolean => { + return userEmail.trim() !== '' && + userCountryCode.trim() !== '' && + isValidEmail(userEmail); + }; + + useEffect(() => { + if (walletResult) { + getProfile(walletResult.hash, walletResult.userId).then(setProfile) + } + }, [walletResult]) + + + const handleDisconnect = () => { + clearWalletResult() + setWalletResult(null) + setMessage('') + setError(null) + setCreatedAgent(null) + } + + const handleCleanLocalStorage = () => { + clearWalletResult() + setWalletResult(null) + setMessage('🗑️ localStorage cleaned successfully!') + setError(null) + setCreatedAgent(null) + console.log('🗑️ localStorage has been cleaned') + } + useEffect(() => { + if (walletResult) { + getUserAgents(walletResult.hash).then(setUserAgents) + } + else { + setUserAgents(null) + } + }, [walletResult]) + console.log('🔍 User agents:', userAgents) + + return ( +
0 ? 'pt-48' : '' + }`}> + {walletResult && ( + + )} + + {/* User Agents Table */} + {walletResult && } + + + {profile?.sandbox?.isStripeEnabled && walletResult ? ( + // Show Create Agent Form when wallet is connected +
+ + + {/* Disconnect Button */} +
+ +
+
+ ) : ( + // Show Connect Button when wallet is not connected +
+

+ Organizations Example 🏢 +

+ + {showStripeModal && walletResult && profile && + <> + setShowStripeModal(false)} walletResult={walletResult} userEmail={userEmail} userCountryCode={userCountryCode} /> + + } + {walletResult && !profile?.sandbox?.isStripeEnabled && +
+ + {/* Country Code Input */} +
+ + setUserCountryCode(e.target.value.toUpperCase())} + placeholder="e.g., US, ES, GB" + maxLength={2} + className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-purple-500 focus:ring-purple-200 focus:outline-none focus:ring-2 transition-colors duration-200" + /> +

Enter 2-letter country code (ISO 3166-1 alpha-2)

+
+ + {/* Connect Stripe Button */} + +
+ } + + {!walletResult && + +
+ {/* User ID Input */} +
+ + setUserId(e.target.value)} + placeholder="Enter your user ID" + className={`w-full px-4 py-3 rounded-lg border transition-colors duration-200 ${!userId + ? 'border-red-300 focus:border-red-500 focus:ring-red-200' + : 'border-gray-300 focus:border-purple-500 focus:ring-purple-200' + } focus:outline-none focus:ring-2`} + /> +
+ {/* Email Input */} +
+ + setUserEmail(e.target.value)} + placeholder="Enter your email address" + className={`w-full px-4 py-3 rounded-lg border transition-colors duration-200 ${userEmail && !isValidEmail(userEmail) + ? 'border-red-300 focus:border-red-500 focus:ring-red-200' + : 'border-gray-300 focus:border-purple-500 focus:ring-purple-200' + } focus:outline-none focus:ring-2`} + /> + {userEmail && !isValidEmail(userEmail) && ( +

Please enter a valid email address

+ )} +
+ +
+ } +
+ )} + + {/* Success Message */} + {message === 'success' && createdAgent ? ( +
+
+
+
+ + + +
+
+
+

+ 🎉 Agent & Plan Created Successfully! +

+
+
+ Agent Name: + {createdAgent.name} +
+
+ Price: + {createdAgent.price} {createdAgent.currency} +
+
+

Agent ID:

+

+ {createdAgent.agentId} +

+
+
+

Plan ID:

+

+ {createdAgent.planId} +

+
+
+ {createdAgent.checkoutUrl && ( + + )} +
+ +
+
+ ) : message && message !== 'success' ? ( +
+ {message} +
+ ) : null} + + {/* Error Message */} + {error && ( +
+ {error} +
+ )} +
+ ) +} + +export default App diff --git a/organizations-example/src/components/AgentsTable.tsx b/organizations-example/src/components/AgentsTable.tsx new file mode 100644 index 0000000..e45ea43 --- /dev/null +++ b/organizations-example/src/components/AgentsTable.tsx @@ -0,0 +1,173 @@ +import { useEffect, useState } from 'react' +import { getCheckoutUrl } from '../config/environment' + +interface Agent { + id: string + metadata?: { + main?: { + name?: string + description?: string + } + created?: string + } +} + +interface AgentsTableProps { + userAgents: { + agents: Agent[] + } | null +} + +export function AgentsTable({ userAgents }: AgentsTableProps) { + // Pagination states + const [currentPage, setCurrentPage] = useState(1) + const [itemsPerPage] = useState(5) + + // Reset to first page when agents change + useEffect(() => { + setCurrentPage(1) + }, [userAgents]) + + if (!userAgents || !userAgents.agents || userAgents.agents.length === 0) { + return null + } + + // Pagination calculations + const totalAgents = userAgents.agents.length + const totalPages = Math.ceil(totalAgents / itemsPerPage) + const startIndex = (currentPage - 1) * itemsPerPage + const endIndex = startIndex + itemsPerPage + const currentAgents = userAgents.agents.slice(startIndex, endIndex) + + // Pagination handlers + const goToPage = (page: number) => { + setCurrentPage(Math.max(1, Math.min(page, totalPages))) + } + + const goToPreviousPage = () => { + setCurrentPage(prev => Math.max(1, prev - 1)) + } + + const goToNextPage = () => { + setCurrentPage(prev => Math.min(totalPages, prev + 1)) + } + + return ( +
+
+ {/* Header */} +
+

+
+ 🤖 + My Agents ({totalAgents}) +
+ {totalPages > 1 && ( +
+ Page {currentPage} of {totalPages} +
+ )} +

+
+ + {/* Table */} +
+ + + + + + + + + + {currentAgents.map((agent: Agent, index: number) => ( + + + + + + ))} + +
+ Agent ID + + Name + + Checkout URL +
+
+ {agent.id} +
+
+
+ {agent.metadata?.main?.name || `Agent ${startIndex + index + 1}`} +
+
+ {agent.metadata?.main?.description || 'No description'} +
+
+ + {getCheckoutUrl(agent.id)} + +
+
+ + {/* Pagination Controls */} + {totalPages > 1 && ( +
+
+
+ + Showing {startIndex + 1} to {Math.min(endIndex, totalAgents)} of {totalAgents} agents + +
+ +
+ {/* Previous Button */} + + + {/* Page Numbers */} +
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNum) => ( + + ))} +
+ + {/* Next Button */} + +
+
+
+ )} +
+
+ ) +} diff --git a/organizations-example/src/components/CreateAgentForm.tsx b/organizations-example/src/components/CreateAgentForm.tsx new file mode 100644 index 0000000..a334159 --- /dev/null +++ b/organizations-example/src/components/CreateAgentForm.tsx @@ -0,0 +1,153 @@ +import { useState } from 'react'; +import type { CreateAgentPlanRequest, WalletResult } from '../types/api'; + +interface CreateAgentFormProps { + walletResult: WalletResult; + onSubmit: (formData: CreateAgentPlanRequest) => Promise; + isLoading: boolean; +} + +export function CreateAgentForm({ walletResult, onSubmit, isLoading }: CreateAgentFormProps) { + const [price, setPrice] = useState(0n); + const [currency, setCurrency] = useState('USD'); + const [credits, setCredits] = useState(0n); + const [description, setDescription] = useState(''); + const [agentName, setAgentName] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const numericPrice = parseFloat(price.toString()); + if (isNaN(numericPrice) || numericPrice <= 0) { + alert('Please enter a valid price'); + return; + } + + const formData: CreateAgentPlanRequest = { + price: numericPrice, + currency: currency, + description: description.trim() || undefined, + name: agentName.trim() || undefined, + }; + + await onSubmit(formData); + }; + + return ( +
+

+ Create Agent & Plan 🤖 +

+ + {/* Wallet Info */} +
+

Connected Wallet 💳

+
+

User ID: {walletResult.userId}

+

Hash: {walletResult.hash.slice(0, 20)}...

+

Wallet: {walletResult.userWallet.slice(0, 20)}...

+
+
+ + {/* Form */} +
+ {/* Agent Name Input */} +
+ + setAgentName(e.target.value)} + placeholder="My AI Agent" + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ + {/* Price Input */} +
+ +
+ setPrice(BigInt(e.target.value))} + placeholder="0.00" + step="0.01" + min="0" + required + className="flex-1 px-3 py-2 border border-gray-300 rounded-l-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> + +
+
+ {/* Credits Input */} +
+ +
+ setCredits(BigInt(e.target.value))} + placeholder="0.00" + step="0.01" + min="0" + required + className="flex-1 px-3 py-2 border border-gray-300 rounded-l-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ + + {/* Description Input */} +
+ +