diff --git a/app/src/app/listing/[pk]/page.tsx b/app/src/app/listing/[pk]/page.tsx index f16b441..acfa81c 100644 --- a/app/src/app/listing/[pk]/page.tsx +++ b/app/src/app/listing/[pk]/page.tsx @@ -1,18 +1,34 @@ 'use client' import { use } from 'react' -import { useWallet } from '@solana/connector/react' +import { useRouter } from 'next/navigation' +import { useWallet, useKitTransactionSigner } from '@solana/connector/react' import { ListingDetail } from '@/components/ListingDetail' +import { useTx } from '@/hooks/useTx' +import { getCloseListingInstruction } from '@solisting/sdk' import type { Address } from '@solisting/sdk' export default function ListingPage({ params }: { params: Promise<{ pk: string }> }) { const { pk } = use(params) + const router = useRouter() const { account } = useWallet() + const { signer } = useKitTransactionSigner() + const sendTx = useTx() + + async function handleClose() { + if (!signer) return + const ix = getCloseListingInstruction({ seller: signer, listingAccount: pk as Address }) + await sendTx([ix as never], [['listings'], ['listing', pk]], 'close_listing') + router.push('/listings') + } return ( router.push(`/listing/create?edit=${pk}`)} + onClose={handleClose} + onPlaceOrder={() => {}} /> ) } diff --git a/app/src/app/listing/create/page.tsx b/app/src/app/listing/create/page.tsx new file mode 100644 index 0000000..130e173 --- /dev/null +++ b/app/src/app/listing/create/page.tsx @@ -0,0 +1,20 @@ +'use client' + +import { Suspense } from 'react' +import { useSearchParams } from 'next/navigation' +import { CreateListingForm } from '@/components/CreateListingForm' +import type { Address } from '@solisting/sdk' + +function CreateOrEdit() { + const searchParams = useSearchParams() + const editPk = searchParams.get('edit') as Address | null + return +} + +export default function CreateListingPage() { + return ( + + + + ) +} diff --git a/app/src/components/CreateListingForm.tsx b/app/src/components/CreateListingForm.tsx new file mode 100644 index 0000000..1873d61 --- /dev/null +++ b/app/src/components/CreateListingForm.tsx @@ -0,0 +1,255 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { useKitTransactionSigner, useWallet } from '@solana/connector/react' +import { isSome } from '@solana/kit' +import { useTx } from '@/hooks/useTx' +import { useListing } from '@/hooks/useListing' +import { Button } from '@/components/ui/Button' +import { useToast } from '@/components/ui/Toast' +import { + getCreateListingInstructionAsync, + getUpdateListingInstruction, +} from '@solisting/sdk' +import type { Address } from '@solisting/sdk' + +interface FormState { + price: string + quantity: string + oracle: string + metadataUri: string +} + +function blank(): FormState { + return { price: '', quantity: '', oracle: '', metadataUri: '' } +} + +interface Props { + editPk?: Address +} + +export function CreateListingForm({ editPk }: Props) { + const router = useRouter() + const { signer } = useKitTransactionSigner() + const { account } = useWallet() + const sendTx = useTx() + const toast = useToast() + + const { data: existing } = useListing(editPk ?? ('' as Address)) + const [form, setForm] = useState(blank()) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [initialized, setInitialized] = useState(false) + + const isUpdate = !!editPk + + if (!initialized && existing && isUpdate) { + setInitialized(true) + const d = existing.data + setForm({ + price: (Number(d.price) / 1e9).toString(), + quantity: String(d.quantity), + oracle: isSome(d.canonicalOracle) ? d.canonicalOracle.value : '', + metadataUri: d.metadataUri ?? '', + }) + } + + function set(key: keyof FormState) { + return (e: React.ChangeEvent) => + setForm((f) => ({ ...f, [key]: e.target.value })) + } + + async function submit(e: React.FormEvent) { + e.preventDefault() + if (!signer || !account) { + setError('Connect your wallet first.') + return + } + setError(null) + setLoading(true) + try { + const priceN = parseFloat(form.price) + if (isNaN(priceN) || priceN <= 0) throw new Error('Invalid price') + const priceLamports = BigInt(Math.round(priceN * 1e9)) + const quantityN = parseInt(form.quantity, 10) + if (isNaN(quantityN) || quantityN <= 0) throw new Error('Invalid quantity') + const canonicalOracle = form.oracle ? (form.oracle as Address) : null + + if (isUpdate && editPk) { + const existingData = existing?.data + const ix = getUpdateListingInstruction({ + seller: signer, + listingAccount: editPk, + canonicalCurrency: existingData?.canonicalCurrency ?? { __kind: 'Sol' }, + price: priceLamports, + canonicalOracle, + altCurrencies: existingData?.altCurrencies ?? [], + acceptedResolvers: existingData?.acceptedResolvers ?? [], + quantity: quantityN, + metadataUri: form.metadataUri, + }) + await sendTx( + [ix as never], + [['listings'], ['listing', editPk]], + 'update_listing', + ) + } else { + const listingId = BigInt(Date.now()) + const ix = await getCreateListingInstructionAsync({ + seller: signer, + listingId, + canonicalCurrency: { __kind: 'Sol' }, + price: priceLamports, + canonicalOracle, + altCurrencies: [], + acceptedResolvers: [], + quantity: quantityN, + metadataUri: form.metadataUri, + }) + await sendTx([ix as never], [['listings']], 'create_listing') + router.push('/listings') + } + } catch (err: unknown) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setLoading(false) + } + } + + const LABEL_STYLE = { + display: 'block', + fontSize: 12, + fontWeight: 600, + color: 'var(--mut)', + marginBottom: 8, + letterSpacing: '.04em', + } as const + const INPUT_STYLE = { + width: '100%', + height: 42, + padding: '0 13px', + background: 'var(--bg3)', + border: '1px solid var(--bd)', + borderRadius: 10, + fontSize: 13, + outline: 'none', + color: 'inherit', + boxSizing: 'border-box' as const, + } as const + + return ( +
+ +

+ {isUpdate ? 'Update Listing' : 'Create Listing'} +

+

+ {isUpdate + ? `Editing listing ${editPk?.slice(0, 8)}…` + : 'New SOL-priced listing on the solisting program'} +

+ +
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+
+ ) +}