|
| 1 | +import React, { useMemo, useState } from 'react'; |
| 2 | +import Link from 'next/link'; |
| 3 | +import { |
| 4 | + buildOneStepMarketPlan, |
| 5 | + buildPermissionlessStackPlan, |
| 6 | + createMarketWizardDefaults, |
| 7 | + KNOWN_ORGANIZATIONS, |
| 8 | +} from '../../../features/marketCreation/marketCreationWorkflow'; |
| 9 | +import RootLayout from '../../layout/RootLayout'; |
| 10 | +import PageLayout from '../../layout/PageLayout'; |
| 11 | + |
| 12 | +const panelClass = 'border border-futarchyGray6 dark:border-futarchyGray7 bg-white dark:bg-futarchyGray2 rounded-lg'; |
| 13 | +const inputClass = 'w-full px-3 py-2 bg-futarchyGray2 dark:bg-futarchyGray3 border border-futarchyGray6 dark:border-futarchyGray7 rounded-md text-sm text-futarchyGray12 dark:text-white focus:outline-none focus:ring-2 focus:ring-futarchyBlue9'; |
| 14 | +const labelClass = 'text-xs font-semibold uppercase tracking-wide text-futarchyGray10 dark:text-futarchyGray11'; |
| 15 | + |
| 16 | +function formatDate(timestamp) { |
| 17 | + if (!timestamp) return 'Not set'; |
| 18 | + return new Date(Number(timestamp) * 1000).toISOString().replace('T', ' ').slice(0, 16) + ' UTC'; |
| 19 | +} |
| 20 | + |
| 21 | +function StageList({ stages }) { |
| 22 | + return ( |
| 23 | + <ol className="divide-y divide-futarchyGray5 dark:divide-futarchyGray7"> |
| 24 | + {stages.map((stage) => ( |
| 25 | + <li key={stage.id} className="grid gap-2 md:grid-cols-[48px_220px_1fr] px-4 py-3"> |
| 26 | + <div className="text-sm font-semibold text-futarchyBlue9">{String(stage.order).padStart(2, '0')}</div> |
| 27 | + <div> |
| 28 | + <div className="text-sm font-semibold text-futarchyGray12 dark:text-white">{stage.title}</div> |
| 29 | + {stage.dependsOn?.length ? ( |
| 30 | + <div className="mt-1 text-xs text-futarchyGray10">After: {stage.dependsOn.join(', ')}</div> |
| 31 | + ) : null} |
| 32 | + </div> |
| 33 | + <div> |
| 34 | + <p className="text-sm text-futarchyGray11 dark:text-futarchyGray11">{stage.summary}</p> |
| 35 | + {stage.requiredEvidence?.length ? ( |
| 36 | + <p className="mt-1 text-xs text-futarchyGray9"> |
| 37 | + Evidence: {stage.requiredEvidence.join(', ')} |
| 38 | + </p> |
| 39 | + ) : null} |
| 40 | + </div> |
| 41 | + </li> |
| 42 | + ))} |
| 43 | + </ol> |
| 44 | + ); |
| 45 | +} |
| 46 | + |
| 47 | +function MetadataPreview({ metadata }) { |
| 48 | + return ( |
| 49 | + <pre className="max-h-[420px] overflow-auto rounded-md bg-futarchyGray2 dark:bg-futarchyDarkGray3 border border-futarchyGray6 dark:border-futarchyGray7 p-4 text-xs text-futarchyGray12 dark:text-futarchyGray11"> |
| 50 | + {JSON.stringify(metadata, null, 2)} |
| 51 | + </pre> |
| 52 | + ); |
| 53 | +} |
| 54 | + |
| 55 | +export default function CreateMarketFlow() { |
| 56 | + const [organizationId, setOrganizationId] = useState('kleros'); |
| 57 | + const defaults = useMemo( |
| 58 | + () => createMarketWizardDefaults({ organizationId }), |
| 59 | + [organizationId] |
| 60 | + ); |
| 61 | + const [form, setForm] = useState(defaults); |
| 62 | + |
| 63 | + const selectedOrganization = KNOWN_ORGANIZATIONS[organizationId]; |
| 64 | + const marketPlan = useMemo(() => buildOneStepMarketPlan({ ...form, organizationId }), [form, organizationId]); |
| 65 | + const permissionlessPlan = useMemo(() => buildPermissionlessStackPlan(), []); |
| 66 | + |
| 67 | + const updateOrganization = (nextOrganizationId) => { |
| 68 | + setOrganizationId(nextOrganizationId); |
| 69 | + setForm(createMarketWizardDefaults({ organizationId: nextOrganizationId })); |
| 70 | + }; |
| 71 | + |
| 72 | + const updateField = (field, value) => { |
| 73 | + setForm((previous) => ({ ...previous, [field]: value })); |
| 74 | + }; |
| 75 | + |
| 76 | + const updateCloseDate = (value) => { |
| 77 | + const nextTimestamp = Math.floor(new Date(value).getTime() / 1000); |
| 78 | + setForm((previous) => ({ |
| 79 | + ...previous, |
| 80 | + closeDateTimeLocal: value, |
| 81 | + closeTimestamp: nextTimestamp, |
| 82 | + twapStartTimestamp: nextTimestamp - (48 * 60 * 60), |
| 83 | + startCandleUnix: nextTimestamp - (49 * 60 * 60), |
| 84 | + })); |
| 85 | + }; |
| 86 | + |
| 87 | + return ( |
| 88 | + <RootLayout headerConfig="app" footerConfig="main"> |
| 89 | + <PageLayout contentClassName="max-w-7xl"> |
| 90 | + <div className="py-8"> |
| 91 | + <div className="mb-8 flex flex-col gap-3 md:flex-row md:items-end md:justify-between"> |
| 92 | + <div> |
| 93 | + <h1 className="text-2xl font-semibold text-futarchyGray12 dark:text-white">Create Market</h1> |
| 94 | + <p className="mt-2 max-w-3xl text-sm text-futarchyGray11 dark:text-futarchyGray11"> |
| 95 | + A single operational flow for organization setup, proposal metadata, market creation, |
| 96 | + FLM liquidity, Snapshot linking, candle readiness, arbitrage setup, and publishing. |
| 97 | + </p> |
| 98 | + </div> |
| 99 | + <Link |
| 100 | + href="/companies" |
| 101 | + className="inline-flex h-10 items-center justify-center rounded-md border border-futarchyGray6 px-4 text-sm font-medium text-futarchyGray12 dark:border-futarchyGray7 dark:text-white" |
| 102 | + > |
| 103 | + Companies |
| 104 | + </Link> |
| 105 | + </div> |
| 106 | + |
| 107 | + <section className={`${panelClass} mb-6`}> |
| 108 | + <div className="border-b border-futarchyGray6 px-4 py-3 dark:border-futarchyGray7"> |
| 109 | + <h2 className="text-lg font-semibold text-futarchyGray12 dark:text-white">Permissionless Chiado Stack</h2> |
| 110 | + <p className="mt-1 text-sm text-futarchyGray11"> |
| 111 | + This is the target testnet lifecycle: any wallet creates an organization, it is listed |
| 112 | + automatically, and the organization receives a default FLM for proposal liquidity. |
| 113 | + </p> |
| 114 | + </div> |
| 115 | + <StageList stages={permissionlessPlan.stages} /> |
| 116 | + </section> |
| 117 | + |
| 118 | + <div className="grid gap-6 lg:grid-cols-[360px_1fr]"> |
| 119 | + <section className={`${panelClass} p-4`}> |
| 120 | + <h2 className="text-lg font-semibold text-futarchyGray12 dark:text-white">Market Defaults</h2> |
| 121 | + |
| 122 | + <div className="mt-5 space-y-4"> |
| 123 | + <div> |
| 124 | + <label className={labelClass} htmlFor="organization">Organization</label> |
| 125 | + <select |
| 126 | + id="organization" |
| 127 | + className={`${inputClass} mt-1`} |
| 128 | + value={organizationId} |
| 129 | + onChange={(event) => updateOrganization(event.target.value)} |
| 130 | + > |
| 131 | + {Object.values(KNOWN_ORGANIZATIONS).map((org) => ( |
| 132 | + <option key={org.id} value={org.id}>{org.name}</option> |
| 133 | + ))} |
| 134 | + </select> |
| 135 | + </div> |
| 136 | + |
| 137 | + <div> |
| 138 | + <label className={labelClass} htmlFor="proposalCode">Proposal Code</label> |
| 139 | + <input |
| 140 | + id="proposalCode" |
| 141 | + className={`${inputClass} mt-1`} |
| 142 | + value={form.proposalCode} |
| 143 | + onChange={(event) => updateField('proposalCode', event.target.value)} |
| 144 | + /> |
| 145 | + </div> |
| 146 | + |
| 147 | + <div> |
| 148 | + <label className={labelClass} htmlFor="displayTitle0">Display Title</label> |
| 149 | + <input |
| 150 | + id="displayTitle0" |
| 151 | + className={`${inputClass} mt-1`} |
| 152 | + value={form.displayTitle0} |
| 153 | + onChange={(event) => updateField('displayTitle0', event.target.value)} |
| 154 | + /> |
| 155 | + <input |
| 156 | + aria-label="Display title event" |
| 157 | + className={`${inputClass} mt-2`} |
| 158 | + value={form.displayTitle1} |
| 159 | + onChange={(event) => updateField('displayTitle1', event.target.value)} |
| 160 | + /> |
| 161 | + </div> |
| 162 | + |
| 163 | + <div> |
| 164 | + <label className={labelClass} htmlFor="question">Resolution Question</label> |
| 165 | + <textarea |
| 166 | + id="question" |
| 167 | + className={`${inputClass} mt-1 min-h-[88px]`} |
| 168 | + value={form.question} |
| 169 | + onChange={(event) => updateField('question', event.target.value)} |
| 170 | + /> |
| 171 | + </div> |
| 172 | + |
| 173 | + <div> |
| 174 | + <label className={labelClass} htmlFor="snapshotId">Snapshot Proposal Hash</label> |
| 175 | + <input |
| 176 | + id="snapshotId" |
| 177 | + className={`${inputClass} mt-1 font-mono`} |
| 178 | + placeholder="0x..." |
| 179 | + value={form.snapshotId} |
| 180 | + onChange={(event) => updateField('snapshotId', event.target.value)} |
| 181 | + /> |
| 182 | + </div> |
| 183 | + |
| 184 | + <div> |
| 185 | + <label className={labelClass} htmlFor="closeDate">Vote Close Time</label> |
| 186 | + <input |
| 187 | + id="closeDate" |
| 188 | + type="datetime-local" |
| 189 | + className={`${inputClass} mt-1`} |
| 190 | + value={form.closeDateTimeLocal} |
| 191 | + onChange={(event) => updateCloseDate(event.target.value)} |
| 192 | + /> |
| 193 | + </div> |
| 194 | + |
| 195 | + <div className="grid grid-cols-2 gap-3"> |
| 196 | + <div> |
| 197 | + <div className={labelClass}>Company Token</div> |
| 198 | + <div className="mt-1 rounded-md border border-futarchyGray6 p-3 text-sm dark:border-futarchyGray7"> |
| 199 | + <div className="font-semibold text-futarchyGray12 dark:text-white">{selectedOrganization.companyToken.symbol}</div> |
| 200 | + <div className="mt-1 break-all font-mono text-xs text-futarchyGray10">{selectedOrganization.companyToken.address}</div> |
| 201 | + </div> |
| 202 | + </div> |
| 203 | + <div> |
| 204 | + <div className={labelClass}>Currency Token</div> |
| 205 | + <div className="mt-1 rounded-md border border-futarchyGray6 p-3 text-sm dark:border-futarchyGray7"> |
| 206 | + <div className="font-semibold text-futarchyGray12 dark:text-white">{selectedOrganization.currencyToken.symbol}</div> |
| 207 | + <div className="mt-1 break-all font-mono text-xs text-futarchyGray10">{selectedOrganization.currencyToken.address}</div> |
| 208 | + </div> |
| 209 | + </div> |
| 210 | + </div> |
| 211 | + </div> |
| 212 | + </section> |
| 213 | + |
| 214 | + <section className={`${panelClass}`}> |
| 215 | + <div className="border-b border-futarchyGray6 px-4 py-3 dark:border-futarchyGray7"> |
| 216 | + <h2 className="text-lg font-semibold text-futarchyGray12 dark:text-white">One-Step Execution Plan</h2> |
| 217 | + <div className="mt-2 grid gap-2 text-xs text-futarchyGray10 md:grid-cols-3"> |
| 218 | + <span>Org: {marketPlan.values.organizationName}</span> |
| 219 | + <span>Close: {formatDate(marketPlan.values.closeTimestamp)}</span> |
| 220 | + <span>Liquidity: {marketPlan.values.initialLiquidityMode.toUpperCase()}</span> |
| 221 | + </div> |
| 222 | + </div> |
| 223 | + <StageList stages={marketPlan.stages} /> |
| 224 | + </section> |
| 225 | + </div> |
| 226 | + |
| 227 | + <section className={`${panelClass} mt-6 p-4`}> |
| 228 | + <div className="mb-3 flex flex-col gap-2 md:flex-row md:items-center md:justify-between"> |
| 229 | + <div> |
| 230 | + <h2 className="text-lg font-semibold text-futarchyGray12 dark:text-white">Generated Metadata Draft</h2> |
| 231 | + <p className="mt-1 text-sm text-futarchyGray11"> |
| 232 | + This is the registry metadata shape the one-step flow will pass into the market |
| 233 | + creation and proposal metadata writes. |
| 234 | + </p> |
| 235 | + </div> |
| 236 | + <Link |
| 237 | + href={`/milestones?company_id=${selectedOrganization.organizationAddress}`} |
| 238 | + className="inline-flex h-9 items-center justify-center rounded-md border border-futarchyGray6 px-3 text-sm text-futarchyGray12 dark:border-futarchyGray7 dark:text-white" |
| 239 | + > |
| 240 | + Open organization |
| 241 | + </Link> |
| 242 | + </div> |
| 243 | + <MetadataPreview metadata={marketPlan.metadataDraft} /> |
| 244 | + </section> |
| 245 | + </div> |
| 246 | + </PageLayout> |
| 247 | + </RootLayout> |
| 248 | + ); |
| 249 | +} |
0 commit comments