diff --git a/docs/superpowers/plans/2026-06-22-solisting-app-foundation.md b/docs/superpowers/plans/2026-06-22-solisting-app-foundation.md new file mode 100644 index 0000000..ac371c2 --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-solisting-app-foundation.md @@ -0,0 +1,1156 @@ +# Solisting App — Foundation (Part 2 of 4) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Scaffold the Next.js 16 App Router project with all shared infrastructure: package setup, CSS theming, wallet providers, React Query, Tailwind (for connectorkit components), copied connectorkit `ui-base` and connector components, shared UI primitives, the Nav, and the root layout. + +**Architecture:** All pages are `'use client'`. CSS custom properties drive the Nebula dark theme; inline styles use these variables. Tailwind v4 (PostCSS plugin only) is included for the copied connectorkit components. The `WalletProviders` component wraps `AppProvider` (from `@solana/connector/react`) and `QueryClientProvider` (React Query). + +**Tech Stack:** Next.js 16, React 19, `@solana/connector ^0.2.4`, `@solana/kit ^6`, `@tanstack/react-query ^5`, `@base-ui/react ^1`, `class-variance-authority`, `clsx`, `tailwind-merge`, `lucide-react`, Tailwind v4 via `@tailwindcss/postcss`. + +**Prerequisites:** Part 1 (SDK) complete; `sdk/` package built; `yarn install` run at workspace root. + +--- + +## File Map + +| Path | Purpose | +|---|---| +| `app/package.json` | App package with all deps | +| `app/tsconfig.json` | TS config with `@/*` path alias | +| `app/next.config.ts` | Minimal Next.js config | +| `app/postcss.config.mjs` | Tailwind v4 PostCSS plugin | +| `app/src/styles/globals.css` | Nebula CSS variables + Tailwind import + fonts | +| `app/src/lib/utils.ts` | `cn()` helper (clsx + tailwind-merge) | +| `app/src/lib/format.ts` | `abbrev`, `fmtSol`, `fmtTok`, `relTime`, `exact` | +| `app/src/lib/queryClient.ts` | React Query `QueryClient` singleton | +| `app/src/providers/WalletProviders.tsx` | `AppProvider` + `QueryClientProvider` | +| `app/src/components/ui-base/button.tsx` | Copied from connectorkit (ui-base) | +| `app/src/components/ui-base/menu.tsx` | Copied from connectorkit (ui-base) | +| `app/src/components/ui-base/dialog.tsx` | Copied from connectorkit (ui-base) | +| `app/src/components/ui-base/collapsible.tsx` | Copied from connectorkit (ui-base) | +| `app/src/components/connector/connect-button.tsx` | Copied from connectorkit (adapted) | +| `app/src/components/connector/wallet-modal.tsx` | Copied from connectorkit (no WC/QR) | +| `app/src/components/connector/wallet-dropdown-content.tsx` | Simplified dropdown | +| `app/src/components/ui/Button.tsx` | App button primitive (inline styles) | +| `app/src/components/ui/Badge.tsx` | Status pill | +| `app/src/components/ui/FieldRow.tsx` | Label + value row | +| `app/src/components/ui/Toast.tsx` | Toast context + fixed notification | +| `app/src/components/Nav.tsx` | Sticky navbar | +| `app/src/app/layout.tsx` | Root layout (fonts, providers, toast) | +| `app/src/app/page.tsx` | Redirect to `/listings` | + +--- + +### Task 6: App Package Scaffolding + +**Files:** `app/package.json`, `app/tsconfig.json`, `app/next.config.ts`, `app/postcss.config.mjs` + +- [ ] **Step 1: Create `app/package.json`** + +```json +{ + "name": "solisting-app", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@base-ui/react": "^1.0.0", + "@descro/sdk": "file:../../descro/sdk", + "@solana/connector": "^0.2.4", + "@solana/connector-debugger": "^0.1.1", + "@solana/kit": "^6.0.0", + "@solisting/sdk": "workspace:*", + "@tanstack/react-query": "^5.90.5", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.546.0", + "motion": "^12.23.24", + "next": "^16.2.6", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "tailwind-merge": "^3.3.1" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.14", + "@types/node": "^25.9.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "tailwindcss": "^4.1.14", + "typescript": "^6.0.3" + } +} +``` + +- [ ] **Step 2: Create `app/tsconfig.json`** + +```json +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./src/*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} +``` + +- [ ] **Step 3: Create `app/next.config.ts`** + +```ts +import type { NextConfig } from 'next' + +const config: NextConfig = { + reactStrictMode: true, +} + +export default config +``` + +- [ ] **Step 4: Create `app/postcss.config.mjs`** + +```js +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} +``` + +- [ ] **Step 5: Install dependencies** + +```bash +yarn install +``` + +Expected: `app/node_modules/` populated with all deps including `@solisting/sdk` resolved from the local workspace. + +- [ ] **Step 6: Commit** + +```bash +git add app/package.json app/tsconfig.json app/next.config.ts app/postcss.config.mjs +git commit -m "feat: scaffold solisting-app Next.js package" +``` + +--- + +### Task 7: CSS Globals + format.ts + utils.ts + queryClient.ts + +**Files:** `app/src/styles/globals.css`, `app/src/lib/utils.ts`, `app/src/lib/format.ts`, `app/src/lib/queryClient.ts` + +- [ ] **Step 1: Create `app/src/styles/globals.css`** + +```css +@import "tailwindcss"; + +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap'); + +:root { + --bg: oklch(0.165 0.022 282); + --bg2: oklch(0.205 0.026 282); + --bg3: oklch(0.25 0.03 283); + --bd: oklch(0.33 0.032 286); + --bdSoft: oklch(0.27 0.028 284); + --navbg: oklch(0.18 0.024 282 / .82); + --tx: oklch(0.96 0.008 285); + --mut: oklch(0.68 0.022 284); + --radius: 13px; + --acc: #9945FF; + --acc2: #14F195; + --acc2light: #7df5c4; + --accGlow: rgba(153,69,255,.55); + --accSoft: rgba(153,69,255,.13); + --accBd: rgba(153,69,255,.4); + --danger: #FF7A59; + --dangerSoft: rgba(255,122,89,.12); + --font-body: 'Space Grotesk', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', monospace; +} + +/* Tailwind theme tokens for connectorkit components */ +@theme { + --color-primary: var(--acc); + --color-primary-foreground: #0b0613; + --color-background: var(--bg); + --color-foreground: var(--tx); + --color-popover: var(--bg2); + --color-popover-foreground: var(--tx); + --color-border: var(--bd); + --color-input: var(--bd); + --color-muted: var(--bg3); + --color-accent: var(--bg3); + --color-accent-foreground: var(--tx); + --color-destructive: var(--danger); +} + +* { box-sizing: border-box; } +body { + margin: 0; + background: var(--bg); + color: var(--tx); + font-family: var(--font-body); +} +input, button, select, textarea { font-family: inherit; font-size: inherit; color: inherit; } +button { cursor: pointer; border: none; background: none; } +::selection { background: rgba(153,69,255,.35); } +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-thumb { background: rgba(255,255,255,.13); border-radius: 8px; } +::-webkit-scrollbar-track { background: transparent; } +@keyframes scToast { from { opacity: 0; transform: translate(-50%,10px); } to { opacity: 1; transform: translate(-50%,0); } } +@keyframes scSpin { to { transform: rotate(360deg); } } +@keyframes scPulse { 0%,100% { opacity: 1; } 50% { opacity: .45; } } +``` + +- [ ] **Step 2: Create `app/src/lib/utils.ts`** + +```ts +import { clsx, type ClassValue } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} +``` + +- [ ] **Step 3: Create `app/src/lib/format.ts`** + +```ts +export function abbrev(pk: string | null | undefined): string { + if (!pk) return '—' + return `${pk.slice(0, 4)}…${pk.slice(-4)}` +} + +export function fmtSol(lamports: bigint | number): string { + const v = Number(lamports) / 1e9 + return v.toLocaleString(undefined, { maximumFractionDigits: 4 }) + ' SOL' +} + +export function fmtTok(raw: bigint | number, decimals: number, symbol: string): string { + const v = Number(raw) / Math.pow(10, decimals) + return v.toLocaleString(undefined, { maximumFractionDigits: Math.min(decimals, 2) }) + ' ' + symbol +} + +export function relTime(ts: number): string { + const d = Math.floor(Date.now() / 1000) - ts + if (d < 60) return `${d}s ago` + if (d < 3600) return `${Math.floor(d / 60)}m ago` + if (d < 86400) return `${Math.floor(d / 3600)}h ago` + return `${Math.floor(d / 86400)}d ago` +} + +export function exact(ts: number): string { + return new Date(ts * 1000).toUTCString() +} + +export function priceStr( + currency: { __kind: 'Sol' } | { __kind: 'Spl'; decimals: number; symbol?: string }, + price: bigint | number, +): string { + if (currency.__kind === 'Sol') return fmtSol(price) + return fmtTok(price, currency.decimals, (currency as { symbol?: string }).symbol ?? 'SPL') +} +``` + +Note: The `__kind` field is how codama renders Rust enums in TypeScript. After running Codama (Task 3), inspect `src/generated/solisting/types/currency.ts` to confirm the exact shape and adjust `priceStr` if needed. + +- [ ] **Step 4: Create `app/src/lib/queryClient.ts`** + +```ts +import { QueryClient } from '@tanstack/react-query' + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 10_000, + gcTime: 5 * 60 * 1000, + retry: 2, + }, + }, +}) +``` + +- [ ] **Step 5: Type-check** + +```bash +cd app && yarn typecheck +``` + +Expected: No errors (or only missing Next.js module errors before `next-env.d.ts` is generated — run `next dev` once if needed). + +- [ ] **Step 6: Commit** + +```bash +git add app/src/ +git commit -m "feat: add CSS globals, format utils, and queryClient" +``` + +--- + +### Task 8: WalletProviders + +**Files:** `app/src/providers/WalletProviders.tsx` + +- [ ] **Step 1: Create `app/src/providers/WalletProviders.tsx`** + +```tsx +'use client' + +import type { ReactNode } from 'react' +import { useMemo } from 'react' +import { AppProvider, getDefaultConfig, getDefaultMobileConfig } from '@solana/connector/react' +import { QueryClientProvider } from '@tanstack/react-query' +import { queryClient } from '@/lib/queryClient' + +function getOrigin() { + if (typeof window !== 'undefined') return window.location.origin + return 'http://localhost:3000' +} + +export function WalletProviders({ children }: { children: ReactNode }) { + const connectorConfig = useMemo( + () => + getDefaultConfig({ + appName: 'Solisting Explorer', + appUrl: getOrigin(), + autoConnect: true, + clusters: [ + { id: 'solana:localnet' as const, label: 'Localnet', url: 'http://localhost:8899' }, + { id: 'solana:devnet' as const, label: 'Devnet', url: 'https://api.devnet.solana.com' }, + { + id: 'solana:mainnet' as const, + label: 'Mainnet', + url: 'https://api.mainnet-beta.solana.com', + }, + ], + }), + [], + ) + + const mobile = useMemo( + () => getDefaultMobileConfig({ appName: 'Solisting Explorer', appUrl: getOrigin() }), + [], + ) + + return ( + + {children} + + ) +} +``` + +- [ ] **Step 2: Type-check** + +```bash +cd app && yarn typecheck +``` + +- [ ] **Step 3: Commit** + +```bash +git add app/src/providers/ +git commit -m "feat: add WalletProviders with connector + react-query" +``` + +--- + +### Task 9: Copy Connectorkit ui-base Components + +Fetch the four `ui-base` wrapper components directly from the connectorkit GitHub repo and write them into `app/src/components/ui-base/`. + +- [ ] **Step 1: Fetch and write `button.tsx`** + +```bash +gh api repos/solana-foundation/connectorkit/contents/examples/next-js/components/ui-base/button.tsx \ + --jq '.content' | base64 -d > app/src/components/ui-base/button.tsx +``` + +- [ ] **Step 2: Fetch and write `menu.tsx`** + +```bash +gh api repos/solana-foundation/connectorkit/contents/examples/next-js/components/ui-base/menu.tsx \ + --jq '.content' | base64 -d > app/src/components/ui-base/menu.tsx +``` + +- [ ] **Step 3: Fetch and write `dialog.tsx`** + +```bash +gh api repos/solana-foundation/connectorkit/contents/examples/next-js/components/ui-base/dialog.tsx \ + --jq '.content' | base64 -d > app/src/components/ui-base/dialog.tsx +``` + +- [ ] **Step 4: Fetch and write `collapsible.tsx`** + +```bash +gh api repos/solana-foundation/connectorkit/contents/examples/next-js/components/ui-base/collapsible.tsx \ + --jq '.content' | base64 -d > app/src/components/ui-base/collapsible.tsx +``` + +- [ ] **Step 5: Fix imports in all four files** + +All four files import from `@/lib/utils`. That path is already correct since we have `app/src/lib/utils.ts`. No change needed. + +Run a quick check: +```bash +grep -r "from '@/" app/src/components/ui-base/ +``` +Expected: Only `@/lib/utils` imports. If you see other `@/components/...` imports, those files also need to be copied. + +- [ ] **Step 6: Type-check** + +```bash +cd app && yarn typecheck +``` + +Fix any import errors by checking if additional dependencies are missing from `package.json`. + +- [ ] **Step 7: Commit** + +```bash +git add app/src/components/ui-base/ +git commit -m "feat: copy connectorkit ui-base components" +``` + +--- + +### Task 10: Copy and Adapt Connector Components + +Fetch `connect-button.tsx` and `wallet-modal.tsx` from connectorkit. Adapt `wallet-modal.tsx` to remove WalletConnect/QR code (heavy dep, not needed). Write a simplified `wallet-dropdown-content.tsx` matching the prototype (address + copy + disconnect). + +- [ ] **Step 1: Fetch `connect-button.tsx`** + +```bash +mkdir -p app/src/components/connector +gh api repos/solana-foundation/connectorkit/contents/examples/next-js/components/connector/base-ui/connect-button.tsx \ + --jq '.content' | base64 -d > app/src/components/connector/connect-button.tsx +``` + +- [ ] **Step 2: Fetch `wallet-modal.tsx` and strip WalletConnect** + +```bash +gh api repos/solana-foundation/connectorkit/contents/examples/next-js/components/connector/base-ui/wallet-modal.tsx \ + --jq '.content' | base64 -d > app/src/components/connector/wallet-modal.tsx +``` + +Then open `wallet-modal.tsx` and: +- Remove `import { CustomQRCode } from '@/components/ui/custom-qr-code'` +- Remove `walletConnectUri` and `onClearWalletConnectUri` from the props interface and destructuring +- Remove the entire QR code section (the `{walletConnectUri && ...}` block) +- Remove the `walletConnectUri?: string | null` and `onClearWalletConnectUri?: () => void` from `WalletModalProps` + +- [ ] **Step 3: Update `connect-button.tsx` to remove WalletConnect props** + +In `connect-button.tsx`, the `WalletModal` is called with `walletConnectUri` and `onClearWalletConnectUri` props. Remove these: + +```tsx +// Before: + { + setIsModalOpen(open); + if (!open) { clearWalletConnectUri(); } + }} + walletConnectUri={walletConnectUri} + onClearWalletConnectUri={clearWalletConnectUri} +/> + +// After: + +``` + +Also remove `walletConnectUri` and `clearWalletConnectUri` from the `useConnector()` destructure. + +- [ ] **Step 4: Write simplified `wallet-dropdown-content.tsx`** + +Replace whatever was fetched with this version matching the prototype's dropdown: + +```tsx +'use client' + +import { useConnector } from '@solana/connector/react' +import { useState } from 'react' + +interface Props { + selectedAccount: string + walletIcon?: string + walletName: string +} + +export function WalletDropdownContent({ selectedAccount, walletIcon, walletName }: Props) { + const { disconnectWallet } = useConnector() + const [copied, setCopied] = useState(false) + + function copyAddress() { + navigator.clipboard.writeText(selectedAccount) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } + + return ( +
+ {walletIcon && ( +
+ {walletName} + {walletName} +
+ )} +
+ CONNECTED WALLET +
+
+ {selectedAccount} +
+ + +
+ ) +} +``` + +- [ ] **Step 5: Type-check** + +```bash +cd app && yarn typecheck +``` + +Fix any remaining import issues (e.g. if `connect-button.tsx` imports from `@/components/connector/shared/hidden-wallet-icons`, fetch that file too or stub it). + +- [ ] **Step 6: Commit** + +```bash +git add app/src/components/connector/ +git commit -m "feat: add connector wallet components (ConnectButton, WalletModal)" +``` + +--- + +### Task 11: App UI Primitives + +**Files:** `app/src/components/ui/Button.tsx`, `Badge.tsx`, `FieldRow.tsx`, `Toast.tsx` + +These use inline styles + CSS variables. No Tailwind. + +- [ ] **Step 1: Create `app/src/components/ui/Button.tsx`** + +```tsx +import type { CSSProperties, ReactNode, ButtonHTMLAttributes } from 'react' + +export type ButtonVariant = 'primary' | 'outline' | 'danger' | 'ghost' + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: ButtonVariant + children: ReactNode +} + +const variantStyles: Record = { + primary: { + background: 'linear-gradient(135deg, var(--acc), var(--acc2))', + color: '#0b0613', + border: 'none', + }, + outline: { + background: 'transparent', + border: '1px solid var(--bd)', + color: 'var(--tx)', + }, + danger: { + background: 'transparent', + border: '1px solid var(--danger)', + color: 'var(--danger)', + }, + ghost: { + background: 'var(--bg3)', + border: '1px solid var(--bd)', + color: 'var(--tx)', + }, +} + +export function Button({ variant = 'primary', children, style, disabled, ...rest }: ButtonProps) { + return ( + + ) +} +``` + +- [ ] **Step 2: Create `app/src/components/ui/Badge.tsx`** + +```tsx +import type { ReactNode } from 'react' + +interface BadgeProps { + children: ReactNode + color: string + bg: string +} + +export function Badge({ children, color, bg }: BadgeProps) { + return ( + + {children} + + ) +} + +export const STATUS_COLORS = { + active: { color: '#14F195', bg: 'rgba(20,241,149,.15)' }, + inactive: { color: '#8A8FA3', bg: 'rgba(138,143,163,.16)' }, + awaitingConfirm: { color: '#F5B23E', bg: 'rgba(245,178,62,.15)' }, + disputed: { color: '#FF7A59', bg: 'rgba(255,122,89,.15)' }, + complete: { color: '#4FD1E0', bg: 'rgba(79,209,224,.15)' }, + cancelled: { color: '#8A8FA3', bg: 'rgba(138,143,163,.16)' }, +} +``` + +- [ ] **Step 3: Create `app/src/components/ui/FieldRow.tsx`** + +```tsx +import type { ReactNode } from 'react' + +interface FieldRowProps { + label: string + children: ReactNode + last?: boolean +} + +export function FieldRow({ label, children, last }: FieldRowProps) { + return ( +
+
+ {label} +
+
{children}
+
+ ) +} + +export function MonoChip({ + value, + onCopy, + onClick, + sub, +}: { + value: string + onCopy?: () => void + onClick?: () => void + sub?: string +}) { + function copy() { + navigator.clipboard.writeText(value) + onCopy?.() + } + return ( +
+
+ + {value} + + +
+ {sub &&
{sub}
} +
+ ) +} +``` + +- [ ] **Step 4: Create `app/src/components/ui/Toast.tsx`** + +```tsx +'use client' + +import { createContext, useCallback, useContext, useState, type ReactNode } from 'react' + +const ToastCtx = createContext<(msg: string) => void>(() => {}) + +export function useToast() { + return useContext(ToastCtx) +} + +export function ToastProvider({ children }: { children: ReactNode }) { + const [msg, setMsg] = useState('') + + const show = useCallback((m: string) => { + setMsg(m) + setTimeout(() => setMsg(''), 2800) + }, []) + + return ( + + {children} + {msg && ( +
+ + {msg} +
+ )} +
+ ) +} +``` + +- [ ] **Step 5: Type-check** + +```bash +cd app && yarn typecheck +``` + +- [ ] **Step 6: Commit** + +```bash +git add app/src/components/ui/ +git commit -m "feat: add app UI primitives (Button, Badge, FieldRow, Toast)" +``` + +--- + +### Task 12: Nav Component + +**Files:** `app/src/components/Nav.tsx` + +- [ ] **Step 1: Create `app/src/components/Nav.tsx`** + +```tsx +'use client' + +import { usePathname, useRouter } from 'next/navigation' +import { useState, useRef, useEffect } from 'react' +import { useCluster } from '@solana/connector/react' +import { ConnectButton } from '@/components/connector/connect-button' + +const NAV_LINKS = [ + { href: '/listings', label: 'Listings' }, + { href: '/escrows', label: 'Escrows' }, + { href: '/resolvers', label: 'Resolvers' }, + { href: '/dashboard', label: 'Dashboard' }, +] + +const NET_DOTS: Record = { + 'solana:localnet': '#FF7A59', + 'solana:devnet': '#14F195', + 'solana:mainnet': '#9945FF', +} + +export function Nav() { + const pathname = usePathname() + const router = useRouter() + const { cluster, clusters, setCluster } = useCluster() + const [netOpen, setNetOpen] = useState(false) + const [search, setSearch] = useState('') + const netRef = useRef(null) + + useEffect(() => { + function close(e: MouseEvent) { + if (netRef.current && !netRef.current.contains(e.target as Node)) setNetOpen(false) + } + document.addEventListener('mousedown', close) + return () => document.removeEventListener('mousedown', close) + }, []) + + function handleSearch(e: React.KeyboardEvent) { + if (e.key === 'Enter' && search.trim()) { + router.push(`/search?q=${encodeURIComponent(search.trim())}`) + setSearch('') + } + } + + const dot = NET_DOTS[cluster?.id ?? ''] ?? '#8A8FA3' + + return ( + + ) +} +``` + +- [ ] **Step 2: Type-check** + +```bash +cd app && yarn typecheck +``` + +- [ ] **Step 3: Commit** + +```bash +git add app/src/components/Nav.tsx +git commit -m "feat: add Nav component" +``` + +--- + +### Task 13: Root Layout + Redirect Page + +**Files:** `app/src/app/layout.tsx`, `app/src/app/page.tsx` + +- [ ] **Step 1: Create `app/src/app/layout.tsx`** + +```tsx +import type { ReactNode } from 'react' +import { WalletProviders } from '@/providers/WalletProviders' +import { ToastProvider } from '@/components/ui/Toast' +import { Nav } from '@/components/Nav' +import '@/styles/globals.css' + +export const metadata = { + title: 'Solisting Explorer', + description: 'Explorer for the Solisting protocol on Solana', +} + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + + +