Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
262 changes: 177 additions & 85 deletions web/app/admin/disputes/page.tsx

Large diffs are not rendered by default.

163 changes: 99 additions & 64 deletions web/app/create/page.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,57 @@
'use client';
import { createScopedLogger } from '@/app/lib/logger';
const log = createScopedLogger('page');
"use client";
import { createScopedLogger } from "@/app/lib/logger";
const log = createScopedLogger("page");

import { FormEvent, useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { ArrowLeft, ArrowRight, Loader2 } from 'lucide-react';
import Navbar from '@/components/Navbar';
import AuthGuard from '@/components/AuthGuard';
import { useWallet } from '@/components/WalletAdapterProvider';
import { useToast } from '../../providers/ToastProvider';
import { predinexContract } from '../lib/adapters/predinex-contract';
import { predinexReadApi } from '../lib/adapters/predinex-read-api';
import { invalidateOnCreatePool } from '../lib/cache-invalidation';
import { TxStage } from '../lib/soroban-transaction-service';
import { TransactionFeeModal } from '@/components/TransactionFeeModal';
import RouteErrorBoundary from '../../components/RouteErrorBoundary';
import { useCreateWizard, type WizardStep } from './_wizard/useCreateWizard';
import { StepIndicator } from './_wizard/StepIndicator';
import { StepTemplate } from './_wizard/StepTemplate';
import { StepBasics } from './_wizard/StepBasics';
import { StepOutcomes } from './_wizard/StepOutcomes';
import { StepParameters } from './_wizard/StepParameters';
import { StepReview } from './_wizard/StepReview';
import { FormEvent, useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
import { ArrowLeft, ArrowRight, Loader2 } from "lucide-react";
import Navbar from "@/components/Navbar";
import AuthGuard from "@/components/AuthGuard";
import { useWallet } from "@/components/WalletAdapterProvider";
import { useToast } from "../../providers/ToastProvider";
import { useTransactionToast } from "../../lib/hooks/useTransactionToast";
import { predinexContract } from "../lib/adapters/predinex-contract";
import { predinexReadApi } from "../lib/adapters/predinex-read-api";
import { invalidateOnCreatePool } from "../lib/cache-invalidation";
import { TxStage } from "../lib/soroban-transaction-service";
import { TransactionFeeModal } from "@/components/TransactionFeeModal";
import RouteErrorBoundary from "../../components/RouteErrorBoundary";
import { useCreateWizard, type WizardStep } from "./_wizard/useCreateWizard";
import { StepIndicator } from "./_wizard/StepIndicator";
import { StepTemplate } from "./_wizard/StepTemplate";
import { StepBasics } from "./_wizard/StepBasics";
import { StepOutcomes } from "./_wizard/StepOutcomes";
import { StepParameters } from "./_wizard/StepParameters";
import { StepReview } from "./_wizard/StepReview";
import {
buildPoolMetadataUri,
loadSavedTemplates,
parseTemplateDeepLink,
saveTemplateToLocalStorage,
} from './_wizard/pool-templates';
} from "./_wizard/pool-templates";

export default function CreateMarket() {
const wallet = useWallet();
const { showToast } = useToast();
const {
onStageChange: onTransactionStageChange,
showError,
showSuccess,
} = useTransactionToast();
const searchParams = useSearchParams();
const wizard = useCreateWizard();
const [isSubmitting, setIsSubmitting] = useState(false);
const [stage, setStage] = useState<TxStage>('idle');
const [stage, setStage] = useState<TxStage>("idle");
const [txId, setTxId] = useState<string | null>(null);
const [deepLinkHandled, setDeepLinkHandled] = useState(false);
const [feePrompt, setFeePrompt] = useState<
{ feeStroops: string; resolve: (v: boolean) => void } | null
>(null);
const [feePrompt, setFeePrompt] = useState<{
feeStroops: string;
resolve: (v: boolean) => void;
} | null>(null);

useEffect(() => {
if (deepLinkHandled) return;
const parsed = parseTemplateDeepLink(searchParams.get('template'));
const parsed = parseTemplateDeepLink(searchParams.get("template"));
if (!parsed) {
setDeepLinkHandled(true);
return;
Expand All @@ -59,34 +66,40 @@ export default function CreateMarket() {
parsed.source,
parsed.id,
publicTemplates,
savedTemplates
savedTemplates,
);
if (applied) {
wizard.goTo(2);
showToast('Template loaded from link', 'success');
showToast("Template loaded from link", "success");
} else {
showToast('Template from link was not found', 'error');
showToast("Template from link was not found", "error");
}
setDeepLinkHandled(true);
})();

return () => {
cancelled = true;
};
}, [deepLinkHandled, searchParams, showToast, wizard.applyDeepLinkTemplate, wizard.goTo]);
}, [
deepLinkHandled,
searchParams,
showToast,
wizard.applyDeepLinkTemplate,
wizard.goTo,
]);

const getStageLabel = (s: TxStage) => {
switch (s) {
case 'simulating':
return 'Simulating transaction…';
case 'signing':
return 'Waiting for signature…';
case 'submitting':
return 'Submitting to network…';
case 'polling':
return 'Confirming transaction…';
case "simulating":
return "Simulating transaction…";
case "signing":
return "Waiting for signature…";
case "submitting":
return "Submitting to network…";
case "polling":
return "Confirming transaction…";
default:
return 'Submitting…';
return "Submitting…";
}
};

Expand All @@ -102,7 +115,9 @@ export default function CreateMarket() {
if (!valid) {
if (wizard.errors.title || wizard.errors.description) {
wizard.goTo(2);
} else if (Object.keys(wizard.errors).some((key) => key.startsWith('outcome'))) {
} else if (
Object.keys(wizard.errors).some((key) => key.startsWith("outcome"))
) {
wizard.goTo(3);
} else {
wizard.goTo(4);
Expand All @@ -122,7 +137,7 @@ export default function CreateMarket() {
};

setIsSubmitting(true);
setStage('idle');
setStage("idle");
try {
const { txHash, poolId } = await predinexContract.createMarketSoroban({
wallet,
Expand All @@ -131,7 +146,10 @@ export default function CreateMarket() {
outcomeA: wizard.draft.outcomeA,
outcomeB: wizard.draft.outcomeB,
durationSeconds: duration,
onStageChange: (s) => setStage(s),
onStageChange: (s) => {
setStage(s);
onTransactionStageChange(s);
},
onFeeEstimated: (fee) => {
return new Promise((resolve) => {
setFeePrompt({ feeStroops: fee, resolve });
Expand All @@ -152,24 +170,24 @@ export default function CreateMarket() {
coverImage: coverImage || undefined,
});
} catch (metaError) {
log.warn('Extended metadata submission failed (non-fatal):', metaError);
showToast('Pool created — metadata could not be saved.', 'error');
log.warn(
"Extended metadata submission failed (non-fatal):",
metaError,
);
showToast("Pool created — metadata could not be saved.", "error");
}
}

setTxId(txHash);
wizard.resetDraft();
invalidateOnCreatePool();
showToast('Pool created successfully!', 'success');
showSuccess("Pool created successfully!");
} catch (error) {
log.error('Failed to create pool:', error);
showToast(
`Failed to create pool: ${error instanceof Error ? error.message : 'Unknown error'}`,
'error'
);
log.error("Failed to create pool:", error);
showError(error instanceof Error ? error.message : "Unknown error");
} finally {
setIsSubmitting(false);
setStage('idle');
setStage("idle");
setFeePrompt(null);
}
};
Expand All @@ -180,15 +198,18 @@ export default function CreateMarket() {
<RouteErrorBoundary routeName="CreateMarket">
<AuthGuard>
<div className="container mx-auto px-4 py-8 sm:py-12 max-w-2xl">
<h1 className="text-2xl sm:text-3xl font-bold mb-2">Create prediction pool</h1>
<h1 className="text-2xl sm:text-3xl font-bold mb-2">
Create prediction pool
</h1>
<p className="text-sm text-muted-foreground mb-8">
Guided wizard with templates, draft auto-save, and on-chain preview before you sign.
Guided wizard with templates, draft auto-save, and on-chain
preview before you sign.
</p>

<TransactionFeeModal
isOpen={!!feePrompt}
actionName="Create Pool"
feeStroops={feePrompt?.feeStroops || '0'}
feeStroops={feePrompt?.feeStroops || "0"}
onConfirm={() => {
feePrompt?.resolve(true);
setFeePrompt(null);
Expand All @@ -197,9 +218,13 @@ export default function CreateMarket() {
feePrompt?.resolve(false);
setFeePrompt(null);
setIsSubmitting(false);
setStage('idle');
setStage("idle");
}}
isConfirming={stage === 'signing' || stage === 'submitting' || stage === 'polling'}
isConfirming={
stage === "signing" ||
stage === "submitting" ||
stage === "polling"
}
/>

{txId && (
Expand All @@ -212,12 +237,18 @@ export default function CreateMarket() {
</div>
)}

<StepIndicator current={wizard.step} onJump={(target) => wizard.goTo(target)} />
<StepIndicator
current={wizard.step}
onJump={(target) => wizard.goTo(target)}
/>

<form onSubmit={handleSubmit} noValidate>
<div className="p-4 sm:p-6 rounded-xl border border-border">
{wizard.step === 1 && (
<StepTemplate draft={wizard.draft} selectTemplate={wizard.selectTemplate} />
<StepTemplate
draft={wizard.draft}
selectTemplate={wizard.selectTemplate}
/>
)}
{wizard.step === 2 && (
<StepBasics
Expand Down Expand Up @@ -286,7 +317,7 @@ export default function CreateMarket() {
disabled={isSubmitting}
aria-disabled={!wizard.canAdvance}
className={`px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-semibold inline-flex items-center gap-2 ${
wizard.canAdvance ? '' : 'opacity-60'
wizard.canAdvance ? "" : "opacity-60"
}`}
>
Next
Expand All @@ -298,8 +329,12 @@ export default function CreateMarket() {
disabled={isSubmitting}
className="px-6 py-2 rounded-lg bg-primary text-primary-foreground font-bold inline-flex items-center gap-2 disabled:opacity-60"
>
{isSubmitting && <Loader2 className="w-4 h-4 animate-spin" />}
{isSubmitting ? getStageLabel(stage) : 'Create pool on-chain'}
{isSubmitting && (
<Loader2 className="w-4 h-4 animate-spin" />
)}
{isSubmitting
? getStageLabel(stage)
: "Create pool on-chain"}
</button>
)}
</div>
Expand Down
Loading
Loading