Skip to content

Commit 44d83a7

Browse files
authored
Merge pull request #14 from TaskOpenSystem/landing
add purchase details
2 parents 05270b6 + 64ba733 commit 44d83a7

2 files changed

Lines changed: 167 additions & 38 deletions

File tree

app/marketplace/my-data/page.tsx

Lines changed: 107 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import React, { useState, useRef, useCallback } from 'react';
44
import Link from 'next/link';
55
import { useCurrentAccount, useSignAndExecuteTransaction } from '@mysten/dapp-kit';
6-
import { useOwnedListings, useAccountBalance } from '@/hooks/useMarketplace';
6+
import { useOwnedListings, useAccountBalance, usePurchasedDatasets } from '@/hooks/useMarketplace';
77
import { formatSize, bytesToHex, formatPrice } from '@/lib/marketplace';
88
import { getFullnodeUrl } from '@mysten/sui/client';
99
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
@@ -58,6 +58,7 @@ export default function MyDataPage() {
5858
const { mutate: signAndExecute, isPending: isSigning } = useSignAndExecuteTransaction();
5959
const { data: listings, isLoading, refetch } = useOwnedListings(account?.address);
6060
const { data: balance } = useAccountBalance();
61+
const { data: purchases, isLoading: isPurchasesLoading } = usePurchasedDatasets(account?.address);
6162

6263
const [activeTab, setActiveTab] = useState<Tab>('uploads');
6364
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
@@ -583,27 +584,116 @@ export default function MyDataPage() {
583584
<section>
584585
<div className="flex items-center justify-between mb-6">
585586
<h2 className="text-xl font-bold tracking-tight text-ink flex items-center gap-2">
586-
Recent Purchases <span className="text-gray-400 text-base font-normal ml-1">(0 items)</span>
587+
Recent Purchases <span className="text-gray-400 text-base font-normal ml-1">({purchases?.length || 0} items)</span>
587588
</h2>
588589
</div>
589590

590591
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
591-
<article className="flex flex-col rounded-xl border-2 border-ink bg-white p-4 shadow-hard-sm opacity-60">
592-
<div className="flex items-start justify-between mb-4">
593-
<div className="h-12 w-12 rounded-lg border-2 border-ink bg-gray-200 flex items-center justify-center">
594-
<span className="material-symbols-outlined text-gray-400">shopping_cart</span>
592+
{isPurchasesLoading && (
593+
<>
594+
{[1, 2, 3].map(i => (
595+
<article key={i} className="flex flex-col rounded-xl border-2 border-ink bg-white overflow-hidden animate-pulse">
596+
<div className="h-32 bg-gray-200" />
597+
<div className="p-4 flex flex-col flex-1">
598+
<div className="h-5 bg-gray-200 rounded w-3/4 mb-2" />
599+
<div className="h-3 bg-gray-100 rounded w-1/2 mb-3" />
600+
<div className="flex gap-2 mt-auto pt-3 border-t border-gray-200">
601+
<div className="flex-1 h-9 bg-gray-200 rounded-lg" />
602+
</div>
603+
</div>
604+
</article>
605+
))}
606+
</>
607+
)}
608+
609+
{!isPurchasesLoading && purchases && purchases.length > 0 && purchases.map(purchase => (
610+
<article key={purchase.id} className="flex flex-col rounded-xl border-2 border-ink bg-white overflow-hidden">
611+
{/* Header */}
612+
<div className="relative h-32 bg-gradient-to-br from-primary to-blue-600 flex items-center justify-center">
613+
<div className="absolute inset-0 opacity-20">
614+
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
615+
<path d="M0,50 Q25,30 50,50 T100,50" stroke="currentColor" strokeWidth="0.5" fill="none" className="text-white" />
616+
<path d="M0,60 Q25,40 50,60 T100,60" stroke="currentColor" strokeWidth="0.5" fill="none" className="text-white" />
617+
</svg>
618+
</div>
619+
<span className="material-symbols-outlined text-5xl text-white/80">shopping_bag</span>
620+
<span className="absolute top-3 left-3 rounded bg-green-500 text-white px-2 py-0.5 text-[10px] font-bold">PURCHASED</span>
595621
</div>
596-
<span className="rounded bg-gray-100 text-gray-600 px-2 py-1 text-[10px] font-bold border border-gray-200">PENDING</span>
597-
</div>
598-
<h3 className="text-lg font-bold text-gray-400 mb-1">No purchases yet</h3>
599-
<p className="text-sm text-gray-400 mb-4">Start exploring the marketplace to find datasets.</p>
600-
<Link
601-
href="/marketplace"
602-
className="mt-auto w-full h-10 rounded-lg border-2 border-ink bg-gray-100 text-gray-500 font-bold transition-colors flex items-center justify-center gap-2"
603-
>
604-
<span className="material-symbols-outlined text-lg">explore</span> Browse Marketplace
605-
</Link>
606-
</article>
622+
623+
{/* Content */}
624+
<div className="p-4 flex flex-col flex-1">
625+
<h3 className="text-base font-bold text-ink mb-1 line-clamp-2">
626+
{purchase.dataset?.name || 'Dataset'}
627+
</h3>
628+
629+
{/* IDs */}
630+
<p className="text-[10px] text-gray-400 font-mono mb-1 truncate" title={purchase.datasetId}>
631+
Dataset: {purchase.datasetId.slice(0, 8)}...{purchase.datasetId.slice(-6)}
632+
</p>
633+
<p className="text-[10px] text-gray-400 font-mono mb-3 truncate" title={purchase.id}>
634+
Receipt: {purchase.id.slice(0, 8)}...{purchase.id.slice(-6)}
635+
</p>
636+
637+
{/* Stats */}
638+
<div className="grid grid-cols-2 gap-4 py-3 border-t border-gray-200">
639+
<div>
640+
<p className="text-[10px] text-gray-400 uppercase tracking-wide">Paid</p>
641+
<p className="text-sm font-bold text-ink">{formatPrice(purchase.price)}</p>
642+
</div>
643+
<div>
644+
<p className="text-[10px] text-gray-400 uppercase tracking-wide">Size</p>
645+
<p className="text-sm font-bold text-accent-lime">
646+
{purchase.dataset ? formatSize(Number(purchase.dataset.totalSize)) : '-'}
647+
</p>
648+
</div>
649+
</div>
650+
651+
{/* Purchase Date */}
652+
<div className="pt-3 border-t border-gray-200 text-xs text-gray-400">
653+
<span className="material-symbols-outlined text-sm align-middle mr-1">schedule</span>
654+
{new Date(purchase.timestamp).toLocaleDateString()}
655+
</div>
656+
657+
{/* Actions */}
658+
<div className="flex gap-2 mt-auto pt-3 border-t border-gray-200">
659+
<Link
660+
href={`/marketplace/dataset/${purchase.datasetId}`}
661+
className="flex-1 h-9 rounded-lg border-2 border-ink bg-primary text-white text-sm font-bold hover:bg-primary/90 transition-colors flex items-center justify-center gap-1"
662+
>
663+
<span className="material-symbols-outlined text-sm">download</span>
664+
Download
665+
</Link>
666+
<a
667+
href={`https://suiscan.xyz/testnet/object/${purchase.id}`}
668+
target="_blank"
669+
rel="noopener noreferrer"
670+
className="h-9 w-9 rounded-lg border-2 border-ink bg-white text-ink hover:bg-gray-50 transition-colors flex items-center justify-center"
671+
title="View Receipt on SuiScan"
672+
>
673+
<span className="material-symbols-outlined text-sm">open_in_new</span>
674+
</a>
675+
</div>
676+
</div>
677+
</article>
678+
))}
679+
680+
{!isPurchasesLoading && (!purchases || purchases.length === 0) && (
681+
<article className="flex flex-col rounded-xl border-2 border-ink bg-white p-4 shadow-hard-sm opacity-60">
682+
<div className="flex items-start justify-between mb-4">
683+
<div className="h-12 w-12 rounded-lg border-2 border-ink bg-gray-200 flex items-center justify-center">
684+
<span className="material-symbols-outlined text-gray-400">shopping_cart</span>
685+
</div>
686+
</div>
687+
<h3 className="text-lg font-bold text-gray-400 mb-1">No purchases yet</h3>
688+
<p className="text-sm text-gray-400 mb-4">Start exploring the marketplace to find datasets.</p>
689+
<Link
690+
href="/marketplace"
691+
className="mt-auto w-full h-10 rounded-lg border-2 border-ink bg-gray-100 text-gray-500 font-bold transition-colors flex items-center justify-center gap-2"
692+
>
693+
<span className="material-symbols-outlined text-lg">explore</span> Browse Marketplace
694+
</Link>
695+
</article>
696+
)}
607697
</div>
608698
</section>
609699
)}

hooks/useMarketplace.ts

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import { useCurrentAccount, useSignAndExecuteTransaction, useSuiClient } from '@
55
import { Transaction } from '@mysten/sui/transactions';
66
import { useQuery } from '@tanstack/react-query';
77
import { marketplaceConfig, MIST_PER_SUI } from '@/config/marketplace';
8-
import {
9-
CreateListingInput,
10-
CreateListingResult,
11-
DatasetListing,
12-
PurchaseResult
8+
import {
9+
CreateListingInput,
10+
CreateListingResult,
11+
DatasetListing,
12+
PurchaseResult
1313
} from '@/types/marketplace';
14-
import {
15-
getMarketplaceTarget,
14+
import {
15+
getMarketplaceTarget,
1616
parseDatasetListing
1717
} from '@/lib/marketplace';
1818

@@ -120,7 +120,7 @@ export function useListDataset() {
120120

121121
// Build PTB with optimized structure
122122
const tx = new Transaction();
123-
123+
124124
// Set gas budget based on operation complexity
125125
tx.setGasBudget(10_000_000); // 0.01 SUI - sufficient for single listing
126126

@@ -195,7 +195,7 @@ export function useListDataset() {
195195
}
196196

197197
const tx = new Transaction();
198-
198+
199199
// Dynamic gas budget based on number of listings
200200
// Base: 10M MIST + 5M per additional listing
201201
const gasBudget = 10_000_000 + (inputs.length - 1) * 5_000_000;
@@ -239,12 +239,12 @@ export function useListDataset() {
239239
onSuccess: (result) => {
240240
const effects = result.effects as { created?: Array<{ reference: { objectId: string } }> } | undefined;
241241
const createdIds = effects?.created?.map((obj) => obj.reference.objectId) || [];
242-
242+
243243
const results: CreateListingResult[] = createdIds.map((id) => ({
244244
listingId: id,
245245
digest: result.digest,
246246
}));
247-
247+
248248
onSuccess(results);
249249
},
250250
}
@@ -369,10 +369,10 @@ export function useOwnedListings(address?: string) {
369369
queryKey: ['owned-listings', address, marketplaceConfig.packageId],
370370
queryFn: async () => {
371371
if (!address) return [];
372-
372+
373373
// Build type string dynamically to ensure env vars are loaded
374374
const listingType = `${marketplaceConfig.packageId}::${marketplaceConfig.moduleName}::DatasetListing`;
375-
375+
376376
const { data } = await suiClient.getOwnedObjects({
377377
owner: address,
378378
filter: { StructType: listingType },
@@ -414,7 +414,7 @@ export function useListing(listingId: string | undefined) {
414414
queryKey: ['listing', listingId],
415415
queryFn: async () => {
416416
if (!listingId) return null;
417-
417+
418418
const object = await suiClient.getObject({
419419
id: listingId,
420420
options: { showContent: true },
@@ -441,12 +441,12 @@ export function useAccountBalance() {
441441
queryKey: ['account-balance', account?.address],
442442
queryFn: async () => {
443443
if (!account?.address) return null;
444-
444+
445445
const balance = await suiClient.getBalance({
446446
owner: account.address,
447447
coinType: '0x2::sui::SUI',
448448
});
449-
449+
450450
return {
451451
mist: BigInt(balance.totalBalance),
452452
sui: Number(balance.totalBalance) / Number(MIST_PER_SUI),
@@ -464,29 +464,68 @@ export function usePurchasedDatasets(address?: string) {
464464
queryKey: ['purchased-datasets', address],
465465
queryFn: async () => {
466466
if (!address) return [];
467-
467+
468468
const RECEIPT_TYPE = `${marketplaceConfig.packageId}::${marketplaceConfig.moduleName}::PurchaseReceipt`;
469-
469+
470+
// Step 1: Get all purchase receipts owned by the user
470471
const { data } = await suiClient.getOwnedObjects({
471472
owner: address,
472473
filter: { StructType: RECEIPT_TYPE },
473474
options: { showContent: true, showType: true },
474475
});
475476

476-
return data
477+
const receipts = data
477478
.map((obj) => {
478479
if (!obj.data?.content || obj.data.content.dataType !== 'moveObject') return null;
479480
const fields = obj.data.content.fields as Record<string, unknown>;
480481
return {
481482
id: obj.data.objectId || '',
482-
datasetId: (fields.dataset_id as { id: string })?.id || '',
483+
datasetId: fields.dataset_id as string,
483484
buyer: fields.buyer as string,
484485
seller: fields.seller as string,
485486
price: BigInt(fields.price as string),
486487
timestamp: Number(fields.timestamp),
487488
};
488489
})
489490
.filter((receipt): receipt is { id: string; datasetId: string; buyer: string; seller: string; price: bigint; timestamp: number } => receipt !== null);
491+
492+
if (receipts.length === 0) return [];
493+
494+
// Step 2: Fetch the dataset details for each purchase receipt
495+
const datasetIds = receipts.map(r => r.datasetId).filter(id => id && id !== '');
496+
497+
if (datasetIds.length === 0) return receipts.map(r => ({ ...r, dataset: null }));
498+
499+
const datasetObjects = await suiClient.multiGetObjects({
500+
ids: datasetIds,
501+
options: { showContent: true },
502+
});
503+
504+
// Create a map of dataset ID -> dataset details
505+
const datasetMap = new Map<string, DatasetListing | null>();
506+
datasetObjects.forEach((obj) => {
507+
if (obj.data?.content?.dataType === 'moveObject') {
508+
const fields = obj.data.content.fields as Record<string, unknown>;
509+
const dataset: DatasetListing = {
510+
id: obj.data.objectId || '',
511+
seller: fields.seller as string,
512+
price: BigInt(fields.price as string),
513+
blobId: fields.blob_id as string,
514+
encryptedObject: fields.encrypted_object as string,
515+
name: fields.name as string,
516+
description: fields.description as string,
517+
previewSize: BigInt(fields.preview_size as string),
518+
totalSize: BigInt(fields.total_size as string),
519+
};
520+
datasetMap.set(obj.data.objectId || '', dataset);
521+
}
522+
});
523+
524+
// Step 3: Combine receipts with dataset details
525+
return receipts.map(receipt => ({
526+
...receipt,
527+
dataset: datasetMap.get(receipt.datasetId) || null,
528+
}));
490529
},
491530
enabled: !!address,
492531
refetchInterval: 10000,
@@ -583,7 +622,7 @@ export function useDownloadDataset() {
583622
}
584623

585624
const result = await response.json();
586-
625+
587626
if (result.error) {
588627
throw new Error(result.error);
589628
}

0 commit comments

Comments
 (0)