strade-stx 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.activity_counter +1 -0
- package/.gitattributes +3 -0
- package/.vscode/settings.json +4 -0
- package/.vscode/tasks.json +19 -0
- package/CHANGELOG.md +1 -0
- package/Clarinet.toml +56 -0
- package/Clarinet.toml.backup +174 -0
- package/Clarinet.toml.old +146 -0
- package/DEPLOYMENT_RESULTS.md +160 -0
- package/README.md +344 -0
- package/TODO.md +34 -0
- package/contracts/CoreMarketPlace.clar +227 -0
- package/contracts/DisputeResolution_clar.clar +265 -0
- package/contracts/EscrowService.clar +171 -0
- package/contracts/UserProfile.clar +280 -0
- package/contracts/ft-trait.clar +24 -0
- package/contracts/token.clar +178 -0
- package/costs-reports.json +76026 -0
- package/deployments/default.mainnet-plan.yaml +67 -0
- package/deployments/default.simnet-plan.yaml +105 -0
- package/deployments/default.testnet-plan.yaml +67 -0
- package/deployments/new-contracts.testnet-plan.yaml +32 -0
- package/frontend/README.md +10 -0
- package/frontend/components.json +22 -0
- package/frontend/dist/assets/index-BacuuL66.css +1 -0
- package/frontend/dist/assets/index-jryypd5B.js +194 -0
- package/frontend/dist/favicon.png +0 -0
- package/frontend/dist/index.html +15 -0
- package/frontend/dist/manifest.json +15 -0
- package/frontend/dist/vite.svg +1 -0
- package/frontend/empty-mock.js +1 -0
- package/frontend/eslint.config.js +23 -0
- package/frontend/eslint.config.mjs +25 -0
- package/frontend/index.html +14 -0
- package/frontend/next.config.ts +17 -0
- package/frontend/package-lock.json +14740 -0
- package/frontend/package.json +56 -0
- package/frontend/postcss.config.js +5 -0
- package/frontend/postcss.config.mjs +5 -0
- package/frontend/public/favicon.png +0 -0
- package/frontend/public/file.svg +1 -0
- package/frontend/public/globe.svg +1 -0
- package/frontend/public/manifest.json +15 -0
- package/frontend/public/next.svg +1 -0
- package/frontend/public/vercel.svg +1 -0
- package/frontend/public/vite.svg +1 -0
- package/frontend/public/window.svg +1 -0
- package/frontend/src/App.css +42 -0
- package/frontend/src/App.tsx +177 -0
- package/frontend/src/app/about/page.tsx +208 -0
- package/frontend/src/app/favicon.ico +0 -0
- package/frontend/src/app/globals.css +129 -0
- package/frontend/src/app/help/page.tsx +167 -0
- package/frontend/src/app/how-it-works/page.tsx +274 -0
- package/frontend/src/app/layout.tsx +55 -0
- package/frontend/src/app/marketplace/page.tsx +324 -0
- package/frontend/src/app/my-listings/page.tsx +318 -0
- package/frontend/src/app/page.tsx +15 -0
- package/frontend/src/assets/react.svg +1 -0
- package/frontend/src/components/ConfirmDialog.tsx +54 -0
- package/frontend/src/components/CreateListingForm.tsx +231 -0
- package/frontend/src/components/ErrorBoundary.tsx +73 -0
- package/frontend/src/components/FilterPanel.tsx +10 -0
- package/frontend/src/components/Footer.tsx +100 -0
- package/frontend/src/components/Header.tsx +268 -0
- package/frontend/src/components/ImageUpload.tsx +147 -0
- package/frontend/src/components/LandingPage.tsx +322 -0
- package/frontend/src/components/ListingCard.tsx +154 -0
- package/frontend/src/components/LoadingSkeleton.tsx +44 -0
- package/frontend/src/components/MobileNav.tsx +89 -0
- package/frontend/src/components/NotificationBell.tsx +8 -0
- package/frontend/src/components/NotificationPanel.tsx +14 -0
- package/frontend/src/components/README.md +14 -0
- package/frontend/src/components/SearchBar.tsx +10 -0
- package/frontend/src/components/TestnetBanner.tsx +29 -0
- package/frontend/src/components/ThemeToggle.tsx +32 -0
- package/frontend/src/components/__tests__/Header.test.tsx +70 -0
- package/frontend/src/components/__tests__/ListingCard.test.tsx +86 -0
- package/frontend/src/components/providers/ThemeProvider.tsx +9 -0
- package/frontend/src/components/ui/alert-dialog.tsx +141 -0
- package/frontend/src/components/ui/avatar.tsx +53 -0
- package/frontend/src/components/ui/badge.tsx +46 -0
- package/frontend/src/components/ui/button.tsx +60 -0
- package/frontend/src/components/ui/card.tsx +92 -0
- package/frontend/src/components/ui/dialog.tsx +143 -0
- package/frontend/src/components/ui/dropdown-menu.tsx +257 -0
- package/frontend/src/components/ui/input.tsx +21 -0
- package/frontend/src/components/ui/label.tsx +24 -0
- package/frontend/src/components/ui/select.tsx +187 -0
- package/frontend/src/components/ui/sonner.tsx +40 -0
- package/frontend/src/components/ui/textarea.tsx +18 -0
- package/frontend/src/context/README.md +27 -0
- package/frontend/src/index.css +166 -0
- package/frontend/src/lib/notificationEvents.ts +10 -0
- package/frontend/src/lib/notificationStore.ts +13 -0
- package/frontend/src/lib/notifications.ts +13 -0
- package/frontend/src/lib/search.ts +28 -0
- package/frontend/src/lib/stacks.ts +189 -0
- package/frontend/src/lib/utils.ts +6 -0
- package/frontend/src/main.tsx +10 -0
- package/frontend/src/test/setup.ts +23 -0
- package/frontend/src/types.d.ts +9 -0
- package/frontend/tsconfig.app.json +28 -0
- package/frontend/tsconfig.json +41 -0
- package/frontend/tsconfig.node.json +26 -0
- package/frontend/vercel.json +4 -0
- package/frontend/vite.config.ts +6 -0
- package/frontend/vitest.config.ts +17 -0
- package/lcov.info +31338 -0
- package/mainnetcontracts.md +16 -0
- package/package.json +53 -0
- package/scripts/auto-activity.sh +9 -0
- package/scripts/cancel-pending.ts +67 -0
- package/scripts/check-balances.ts +23 -0
- package/scripts/distribute-evenly.ts +56 -0
- package/scripts/drain-accounts.ts +70 -0
- package/scripts/fund-accounts.ts +88 -0
- package/scripts/fund-active.ts +59 -0
- package/scripts/fund-unfunded.ts +88 -0
- package/scripts/generate-activity.ts +181 -0
- package/scripts/git-activity-generator.ts +154 -0
- package/scripts/mobile-server.ts +123 -0
- package/settings/Devnet.toml +155 -0
- package/settings/Mainnet.toml +7 -0
- package/settings/Testnet.toml +9 -0
- package/tests/CoreMarketPlace.fuzz.test.ts +435 -0
- package/tests/CoreMarketPlace.test.ts +564 -0
- package/tsconfig.json +26 -0
- package/vitest.config.js +49 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import Header from '@/components/Header';
|
|
5
|
+
import Footer from '@/components/Footer';
|
|
6
|
+
import ListingCard from '@/components/ListingCard';
|
|
7
|
+
import CreateListingForm from '@/components/CreateListingForm';
|
|
8
|
+
import TestnetBanner from '@/components/TestnetBanner';
|
|
9
|
+
import { ListingGridSkeleton } from '@/components/LoadingSkeleton';
|
|
10
|
+
import { getListings, userSession, Listing, contractAddress, contractName, getUserBalance, isWalletConnected, getConnectedAddress, network } from '@/lib/stacks';
|
|
11
|
+
import { openContractCall } from '@stacks/connect';
|
|
12
|
+
import { uintCV, stringUtf8CV } from '@stacks/transactions';
|
|
13
|
+
import { toast } from 'sonner';
|
|
14
|
+
import { Input } from '@/components/ui/input';
|
|
15
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
16
|
+
import { Search, AlertTriangle, ArrowUpDown, Package } from 'lucide-react';
|
|
17
|
+
|
|
18
|
+
export default function Home() {
|
|
19
|
+
const [listings, setListings] = useState<Listing[]>([]);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const [userAddress, setUserAddress] = useState<string>('');
|
|
22
|
+
const [searchQuery, setSearchQuery] = useState<string>('');
|
|
23
|
+
const [sortBy, setSortBy] = useState<'newest' | 'price-low' | 'price-high' | 'ending-soon'>('newest');
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
loadListings();
|
|
27
|
+
const address = getConnectedAddress();
|
|
28
|
+
if (address) {
|
|
29
|
+
setUserAddress(address);
|
|
30
|
+
}
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const loadListings = async () => {
|
|
34
|
+
try {
|
|
35
|
+
const fetchedListings = await getListings();
|
|
36
|
+
setListings(fetchedListings);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error loading listings:', error);
|
|
39
|
+
toast.error('Failed to load listings. Please try again.');
|
|
40
|
+
} finally {
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleCreateListing = async (data: {
|
|
46
|
+
name: string;
|
|
47
|
+
description: string;
|
|
48
|
+
price: number;
|
|
49
|
+
duration: number;
|
|
50
|
+
imageUrl?: string;
|
|
51
|
+
}) => {
|
|
52
|
+
if (!isWalletConnected()) {
|
|
53
|
+
toast.error('Please connect your wallet first');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const functionArgs = [
|
|
58
|
+
stringUtf8CV(data.name),
|
|
59
|
+
stringUtf8CV(data.description),
|
|
60
|
+
uintCV(data.price),
|
|
61
|
+
uintCV(data.duration),
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const options = {
|
|
65
|
+
contractAddress,
|
|
66
|
+
contractName,
|
|
67
|
+
functionName: 'create-listing',
|
|
68
|
+
functionArgs,
|
|
69
|
+
network,
|
|
70
|
+
appDetails: {
|
|
71
|
+
name: 'Strade',
|
|
72
|
+
icon: window.location.origin + '/favicon.ico',
|
|
73
|
+
},
|
|
74
|
+
onFinish: (result: { txId: string; stacksTransaction: unknown }) => {
|
|
75
|
+
toast.dismiss();
|
|
76
|
+
toast.success('Listing created successfully!', {
|
|
77
|
+
description: `Transaction ID: ${result.txId.slice(0, 10)}...`,
|
|
78
|
+
});
|
|
79
|
+
// Reload listings immediately
|
|
80
|
+
loadListings();
|
|
81
|
+
},
|
|
82
|
+
onCancel: () => {
|
|
83
|
+
toast.dismiss();
|
|
84
|
+
toast.info('Transaction cancelled');
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
toast.loading('Waiting for transaction approval...');
|
|
90
|
+
await openContractCall(options);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Error creating listing:', error);
|
|
93
|
+
toast.dismiss();
|
|
94
|
+
toast.error('Failed to create listing');
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handlePurchaseListing = async (listingId: number) => {
|
|
99
|
+
if (!isWalletConnected()) {
|
|
100
|
+
toast.error('Please connect your wallet first');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Find the listing to check price
|
|
105
|
+
const listing = listings.find(l => l.listingId === listingId);
|
|
106
|
+
if (!listing) {
|
|
107
|
+
toast.error('Listing not found');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check user balance
|
|
112
|
+
try {
|
|
113
|
+
const address = getConnectedAddress();
|
|
114
|
+
if (!address) {
|
|
115
|
+
toast.error('Unable to get wallet address');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const balance = await getUserBalance(address);
|
|
119
|
+
|
|
120
|
+
if (balance < listing.price) {
|
|
121
|
+
toast.error('Insufficient balance', {
|
|
122
|
+
description: `You need ${(listing.price / 1000000).toFixed(6)} STX but only have ${(balance / 1000000).toFixed(6)} STX`,
|
|
123
|
+
icon: <AlertTriangle className="h-5 w-5" />,
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Error checking balance:', error);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const functionArgs = [uintCV(listingId)];
|
|
132
|
+
|
|
133
|
+
const options = {
|
|
134
|
+
contractAddress,
|
|
135
|
+
contractName,
|
|
136
|
+
functionName: 'purchase-listing',
|
|
137
|
+
functionArgs,
|
|
138
|
+
network,
|
|
139
|
+
appDetails: {
|
|
140
|
+
name: 'Strade',
|
|
141
|
+
icon: window.location.origin + '/favicon.ico',
|
|
142
|
+
},
|
|
143
|
+
onFinish: (result: { txId: string; stacksTransaction: unknown }) => {
|
|
144
|
+
toast.dismiss();
|
|
145
|
+
toast.success('Purchase successful!', {
|
|
146
|
+
description: `Transaction ID: ${result.txId.slice(0, 10)}...`,
|
|
147
|
+
});
|
|
148
|
+
// Reload listings immediately
|
|
149
|
+
loadListings();
|
|
150
|
+
},
|
|
151
|
+
onCancel: () => {
|
|
152
|
+
toast.dismiss();
|
|
153
|
+
toast.info('Purchase cancelled');
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
toast.loading('Processing purchase...');
|
|
159
|
+
await openContractCall(options);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('Error purchasing listing:', error);
|
|
162
|
+
toast.dismiss();
|
|
163
|
+
toast.error('Failed to purchase listing');
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const handleCancelListing = async (listingId: number) => {
|
|
168
|
+
if (!isWalletConnected()) {
|
|
169
|
+
toast.error('Please connect your wallet first');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const functionArgs = [uintCV(listingId)];
|
|
174
|
+
|
|
175
|
+
const options = {
|
|
176
|
+
contractAddress,
|
|
177
|
+
contractName,
|
|
178
|
+
functionName: 'cancel-listing',
|
|
179
|
+
functionArgs,
|
|
180
|
+
network,
|
|
181
|
+
appDetails: {
|
|
182
|
+
name: 'Strade',
|
|
183
|
+
icon: window.location.origin + '/favicon.ico',
|
|
184
|
+
},
|
|
185
|
+
onFinish: (result: { txId: string; stacksTransaction: unknown }) => {
|
|
186
|
+
toast.dismiss();
|
|
187
|
+
toast.success('Listing cancelled successfully!', {
|
|
188
|
+
description: `Transaction ID: ${result.txId.slice(0, 10)}...`,
|
|
189
|
+
});
|
|
190
|
+
// Reload listings immediately
|
|
191
|
+
loadListings();
|
|
192
|
+
},
|
|
193
|
+
onCancel: () => {
|
|
194
|
+
toast.dismiss();
|
|
195
|
+
toast.info('Cancellation cancelled');
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
toast.loading('Cancelling listing...');
|
|
201
|
+
await openContractCall(options);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('Error cancelling listing:', error);
|
|
204
|
+
toast.dismiss();
|
|
205
|
+
toast.error('Failed to cancel listing');
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Filter and sort listings
|
|
210
|
+
const filteredListings = listings
|
|
211
|
+
.filter((listing) => {
|
|
212
|
+
if (!searchQuery) return true;
|
|
213
|
+
const query = searchQuery.toLowerCase();
|
|
214
|
+
return (
|
|
215
|
+
listing.name.toLowerCase().includes(query) ||
|
|
216
|
+
listing.description.toLowerCase().includes(query)
|
|
217
|
+
);
|
|
218
|
+
})
|
|
219
|
+
.sort((a, b) => {
|
|
220
|
+
switch (sortBy) {
|
|
221
|
+
case 'price-low':
|
|
222
|
+
return a.price - b.price;
|
|
223
|
+
case 'price-high':
|
|
224
|
+
return b.price - a.price;
|
|
225
|
+
case 'ending-soon':
|
|
226
|
+
return a.expiresAt - b.expiresAt;
|
|
227
|
+
case 'newest':
|
|
228
|
+
default:
|
|
229
|
+
return b.listingId - a.listingId;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/30 dark:from-slate-950 dark:via-slate-900 dark:to-slate-900 flex flex-col transition-colors">
|
|
235
|
+
<TestnetBanner />
|
|
236
|
+
<Header />
|
|
237
|
+
|
|
238
|
+
<main className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 w-full">
|
|
239
|
+
<div className="mb-8">
|
|
240
|
+
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent mb-2">
|
|
241
|
+
Decentralized Marketplace
|
|
242
|
+
</h1>
|
|
243
|
+
<p className="text-slate-600 dark:text-slate-400">
|
|
244
|
+
Buy and sell goods securely using smart contracts on the Stacks blockchain
|
|
245
|
+
</p>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div className="flex flex-col gap-4 mb-6">
|
|
249
|
+
<div className="flex flex-col sm:flex-row gap-4">
|
|
250
|
+
<CreateListingForm onCreateListing={handleCreateListing} />
|
|
251
|
+
|
|
252
|
+
<div className="flex-1 relative">
|
|
253
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-slate-400" />
|
|
254
|
+
<Input
|
|
255
|
+
type="text"
|
|
256
|
+
placeholder="Search listings..."
|
|
257
|
+
value={searchQuery}
|
|
258
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
259
|
+
className="pl-10"
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<Select value={sortBy} onValueChange={(value: 'newest' | 'price-low' | 'price-high' | 'ending-soon') => setSortBy(value)}>
|
|
264
|
+
<SelectTrigger className="w-full sm:w-[200px]">
|
|
265
|
+
<ArrowUpDown className="h-4 w-4 mr-2" />
|
|
266
|
+
<SelectValue placeholder="Sort by" />
|
|
267
|
+
</SelectTrigger>
|
|
268
|
+
<SelectContent>
|
|
269
|
+
<SelectItem value="newest">Newest First</SelectItem>
|
|
270
|
+
<SelectItem value="price-low">Price: Low to High</SelectItem>
|
|
271
|
+
<SelectItem value="price-high">Price: High to Low</SelectItem>
|
|
272
|
+
<SelectItem value="ending-soon">Ending Soon</SelectItem>
|
|
273
|
+
</SelectContent>
|
|
274
|
+
</Select>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{filteredListings.length > 0 && (
|
|
278
|
+
<div className="text-sm text-slate-600 dark:text-slate-400">
|
|
279
|
+
Showing {filteredListings.length} {filteredListings.length === 1 ? 'listing' : 'listings'}
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
{loading ? (
|
|
285
|
+
<ListingGridSkeleton count={6} />
|
|
286
|
+
) : filteredListings.length === 0 ? (
|
|
287
|
+
<div className="text-center py-12 bg-white dark:bg-slate-800 rounded-lg shadow-sm">
|
|
288
|
+
<Package className="h-16 w-16 mx-auto text-slate-300 dark:text-slate-600 mb-4" />
|
|
289
|
+
<p className="text-slate-600 dark:text-slate-300 text-lg font-medium">
|
|
290
|
+
{searchQuery ? 'No listings match your search.' : 'No active listings found.'}
|
|
291
|
+
</p>
|
|
292
|
+
<p className="text-sm text-slate-500 dark:text-slate-400 mt-2">
|
|
293
|
+
{searchQuery ? 'Try a different search term.' : 'Be the first to create a listing!'}
|
|
294
|
+
</p>
|
|
295
|
+
</div>
|
|
296
|
+
) : (
|
|
297
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
298
|
+
{filteredListings.map((listing) => (
|
|
299
|
+
<ListingCard
|
|
300
|
+
key={listing.listingId}
|
|
301
|
+
listing={listing}
|
|
302
|
+
onPurchase={handlePurchaseListing}
|
|
303
|
+
onCancel={handleCancelListing}
|
|
304
|
+
isOwner={listing.seller === userAddress}
|
|
305
|
+
/>
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
)}
|
|
309
|
+
</main>
|
|
310
|
+
|
|
311
|
+
<Footer />
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
// import SearchBar
|
|
316
|
+
// import FilterPanel
|
|
317
|
+
// search state
|
|
318
|
+
// filter state
|
|
319
|
+
// empty state
|
|
320
|
+
// results count
|
|
321
|
+
// pagination
|
|
322
|
+
// sort
|
|
323
|
+
// debounce
|
|
324
|
+
// useMemo
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import Header from '@/components/Header';
|
|
6
|
+
import Footer from '@/components/Footer';
|
|
7
|
+
import TestnetBanner from '@/components/TestnetBanner';
|
|
8
|
+
import ListingCard from '@/components/ListingCard';
|
|
9
|
+
import { ListingGridSkeleton } from '@/components/LoadingSkeleton';
|
|
10
|
+
import { getListings, userSession, Listing, contractAddress, contractName, isWalletConnected, getConnectedAddress, network } from '@/lib/stacks';
|
|
11
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
12
|
+
import { Button } from '@/components/ui/button';
|
|
13
|
+
import { Badge } from '@/components/ui/badge';
|
|
14
|
+
import { Package, TrendingUp, Clock, CheckCircle, XCircle } from 'lucide-react';
|
|
15
|
+
import { toast } from 'sonner';
|
|
16
|
+
import { openContractCall } from '@stacks/connect';
|
|
17
|
+
import { uintCV } from '@stacks/transactions';
|
|
18
|
+
|
|
19
|
+
type StatusFilter = 'all' | 'active' | 'sold' | 'expired';
|
|
20
|
+
|
|
21
|
+
export default function MyListings() {
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
const [listings, setListings] = useState<Listing[]>([]);
|
|
24
|
+
const [loading, setLoading] = useState(true);
|
|
25
|
+
|
|
26
|
+
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!isWalletConnected()) {
|
|
30
|
+
// Redirect to home if not connected
|
|
31
|
+
router.push('/');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const address = getConnectedAddress();
|
|
36
|
+
if (address) {
|
|
37
|
+
loadMyListings(address);
|
|
38
|
+
}
|
|
39
|
+
}, [router]);
|
|
40
|
+
|
|
41
|
+
const loadMyListings = async (address: string) => {
|
|
42
|
+
try {
|
|
43
|
+
// Fetch all listings including sold and expired ones
|
|
44
|
+
const allListings = await getListings(true);
|
|
45
|
+
// Filter to show all user's listings (not just active)
|
|
46
|
+
const myListings = allListings.filter(
|
|
47
|
+
(listing) => listing.seller.toLowerCase() === address.toLowerCase()
|
|
48
|
+
);
|
|
49
|
+
setListings(myListings);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Error loading listings:', error);
|
|
52
|
+
toast.error('Failed to load your listings');
|
|
53
|
+
} finally {
|
|
54
|
+
setLoading(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleCancelListing = async (listingId: number) => {
|
|
59
|
+
if (!isWalletConnected()) {
|
|
60
|
+
toast.error('Please connect your wallet first');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const functionArgs = [uintCV(listingId)];
|
|
65
|
+
|
|
66
|
+
const options = {
|
|
67
|
+
contractAddress,
|
|
68
|
+
contractName,
|
|
69
|
+
functionName: 'cancel-listing',
|
|
70
|
+
functionArgs,
|
|
71
|
+
network,
|
|
72
|
+
appDetails: {
|
|
73
|
+
name: 'Strade',
|
|
74
|
+
icon: window.location.origin + '/favicon.ico',
|
|
75
|
+
},
|
|
76
|
+
onFinish: (result: { txId: string; stacksTransaction: unknown }) => {
|
|
77
|
+
toast.dismiss();
|
|
78
|
+
toast.success('Listing cancelled successfully!', {
|
|
79
|
+
description: `Transaction ID: ${result.txId.slice(0, 10)}...`,
|
|
80
|
+
});
|
|
81
|
+
// Reload listings immediately
|
|
82
|
+
const address = getConnectedAddress();
|
|
83
|
+
if (address) {
|
|
84
|
+
loadMyListings(address);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
onCancel: () => {
|
|
88
|
+
toast.dismiss();
|
|
89
|
+
toast.info('Cancellation cancelled');
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
toast.loading('Cancelling listing...');
|
|
95
|
+
await openContractCall(options);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('Error cancelling listing:', error);
|
|
98
|
+
toast.dismiss();
|
|
99
|
+
toast.error('Failed to cancel listing');
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Calculate statistics
|
|
104
|
+
const stats = {
|
|
105
|
+
total: listings.length,
|
|
106
|
+
active: listings.filter((l) => l.status === 'active' && l.expiresAt > Date.now() / 1000).length,
|
|
107
|
+
sold: listings.filter((l) => l.status === 'sold').length,
|
|
108
|
+
expired: listings.filter((l) => l.expiresAt < Date.now() / 1000 && l.status === 'active').length,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Filter listings based on selected status
|
|
112
|
+
const filteredListings = listings.filter((listing) => {
|
|
113
|
+
const isExpired = listing.expiresAt < Date.now() / 1000;
|
|
114
|
+
|
|
115
|
+
if (statusFilter === 'all') return true;
|
|
116
|
+
if (statusFilter === 'active') return listing.status === 'active' && !isExpired;
|
|
117
|
+
if (statusFilter === 'sold') return listing.status === 'sold';
|
|
118
|
+
if (statusFilter === 'expired') return isExpired && listing.status === 'active';
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const StatCard = ({
|
|
123
|
+
icon: Icon,
|
|
124
|
+
label,
|
|
125
|
+
value,
|
|
126
|
+
color
|
|
127
|
+
}: {
|
|
128
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
129
|
+
label: string;
|
|
130
|
+
value: number;
|
|
131
|
+
color: string;
|
|
132
|
+
}) => (
|
|
133
|
+
<Card className="hover:shadow-lg transition-all hover:-translate-y-1 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
|
|
134
|
+
<CardContent className="p-6">
|
|
135
|
+
<div className="flex items-center justify-between">
|
|
136
|
+
<div>
|
|
137
|
+
<p className="text-sm font-medium text-slate-600 dark:text-slate-400">{label}</p>
|
|
138
|
+
<p className="text-3xl font-bold text-slate-900 dark:text-white mt-2">{value}</p>
|
|
139
|
+
</div>
|
|
140
|
+
<div className={`p-3 rounded-full ${color}`}>
|
|
141
|
+
<Icon className="h-6 w-6 text-white" />
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</CardContent>
|
|
145
|
+
</Card>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const EmptyState = () => (
|
|
149
|
+
<div className="text-center py-16 px-4 bg-white dark:bg-slate-800 rounded-lg shadow-sm">
|
|
150
|
+
<div className="max-w-md mx-auto">
|
|
151
|
+
<div className="mb-6 flex justify-center">
|
|
152
|
+
<div className="p-6 bg-slate-100 dark:bg-slate-700 rounded-full">
|
|
153
|
+
<Package className="h-16 w-16 text-slate-400 dark:text-slate-500" />
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<h3 className="text-2xl font-bold text-slate-900 dark:text-white mb-3">
|
|
157
|
+
No listings yet
|
|
158
|
+
</h3>
|
|
159
|
+
<p className="text-slate-600 dark:text-slate-400 mb-8">
|
|
160
|
+
Start selling on the decentralized marketplace by creating your first listing.
|
|
161
|
+
</p>
|
|
162
|
+
<Button
|
|
163
|
+
size="lg"
|
|
164
|
+
onClick={() => router.push('/')}
|
|
165
|
+
className="shadow-lg hover:shadow-xl transition-shadow bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white border-0"
|
|
166
|
+
>
|
|
167
|
+
Create Your First Listing
|
|
168
|
+
</Button>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const FilterButton = ({
|
|
174
|
+
filter,
|
|
175
|
+
label,
|
|
176
|
+
count
|
|
177
|
+
}: {
|
|
178
|
+
filter: StatusFilter;
|
|
179
|
+
label: string;
|
|
180
|
+
count: number;
|
|
181
|
+
}) => (
|
|
182
|
+
<button
|
|
183
|
+
onClick={() => setStatusFilter(filter)}
|
|
184
|
+
className={`px-4 py-2 rounded-lg font-medium transition-all ${
|
|
185
|
+
statusFilter === filter
|
|
186
|
+
? 'bg-slate-900 text-white shadow-md'
|
|
187
|
+
: 'bg-white text-slate-600 hover:bg-slate-50 border border-slate-200'
|
|
188
|
+
}`}
|
|
189
|
+
>
|
|
190
|
+
{label}
|
|
191
|
+
<Badge
|
|
192
|
+
variant={statusFilter === filter ? 'secondary' : 'outline'}
|
|
193
|
+
className="ml-2"
|
|
194
|
+
>
|
|
195
|
+
{count}
|
|
196
|
+
</Badge>
|
|
197
|
+
</button>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (loading) {
|
|
201
|
+
return (
|
|
202
|
+
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/30 dark:from-slate-950 dark:via-slate-900 dark:to-slate-900 flex flex-col transition-colors">
|
|
203
|
+
<TestnetBanner />
|
|
204
|
+
<Header />
|
|
205
|
+
<main className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 w-full">
|
|
206
|
+
<div className="mb-8">
|
|
207
|
+
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent mb-2">My Listings</h1>
|
|
208
|
+
</div>
|
|
209
|
+
<ListingGridSkeleton count={6} />
|
|
210
|
+
</main>
|
|
211
|
+
<Footer />
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-purple-50/30 dark:from-slate-950 dark:via-slate-900 dark:to-slate-900 flex flex-col transition-colors">
|
|
218
|
+
<TestnetBanner />
|
|
219
|
+
<Header />
|
|
220
|
+
|
|
221
|
+
<main className="flex-1 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 w-full">
|
|
222
|
+
{/* Page Header */}
|
|
223
|
+
<div className="mb-8">
|
|
224
|
+
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent mb-2">
|
|
225
|
+
My Listings
|
|
226
|
+
</h1>
|
|
227
|
+
<p className="text-slate-600 dark:text-slate-400">
|
|
228
|
+
Manage and track all your marketplace listings
|
|
229
|
+
</p>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{listings.length === 0 ? (
|
|
233
|
+
<EmptyState />
|
|
234
|
+
) : (
|
|
235
|
+
<>
|
|
236
|
+
{/* Statistics Cards */}
|
|
237
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
238
|
+
<StatCard
|
|
239
|
+
icon={Package}
|
|
240
|
+
label="Total Listings"
|
|
241
|
+
value={stats.total}
|
|
242
|
+
color="bg-blue-500"
|
|
243
|
+
/>
|
|
244
|
+
<StatCard
|
|
245
|
+
icon={TrendingUp}
|
|
246
|
+
label="Active"
|
|
247
|
+
value={stats.active}
|
|
248
|
+
color="bg-green-500"
|
|
249
|
+
/>
|
|
250
|
+
<StatCard
|
|
251
|
+
icon={CheckCircle}
|
|
252
|
+
label="Sold"
|
|
253
|
+
value={stats.sold}
|
|
254
|
+
color="bg-purple-500"
|
|
255
|
+
/>
|
|
256
|
+
<StatCard
|
|
257
|
+
icon={Clock}
|
|
258
|
+
label="Expired"
|
|
259
|
+
value={stats.expired}
|
|
260
|
+
color="bg-orange-500"
|
|
261
|
+
/>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
{/* Filter Tabs */}
|
|
265
|
+
<div className="mb-8 flex flex-wrap gap-3">
|
|
266
|
+
<FilterButton filter="all" label="All" count={stats.total} />
|
|
267
|
+
<FilterButton filter="active" label="Active" count={stats.active} />
|
|
268
|
+
<FilterButton filter="sold" label="Sold" count={stats.sold} />
|
|
269
|
+
<FilterButton filter="expired" label="Expired" count={stats.expired} />
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{/* Listings Grid */}
|
|
273
|
+
{filteredListings.length === 0 ? (
|
|
274
|
+
<div className="text-center py-12 bg-white rounded-lg border border-slate-200">
|
|
275
|
+
<XCircle className="h-12 w-12 text-slate-400 mx-auto mb-4" />
|
|
276
|
+
<p className="text-slate-600">
|
|
277
|
+
No {statusFilter !== 'all' ? statusFilter : ''} listings found.
|
|
278
|
+
</p>
|
|
279
|
+
</div>
|
|
280
|
+
) : (
|
|
281
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
282
|
+
{filteredListings.map((listing) => {
|
|
283
|
+
const isExpired = listing.expiresAt < Date.now() / 1000;
|
|
284
|
+
const displayStatus = isExpired && listing.status === 'active' ? 'expired' : listing.status;
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<div key={listing.listingId} className="relative">
|
|
288
|
+
{/* Status Badge Overlay */}
|
|
289
|
+
<div className="absolute top-4 right-4 z-10">
|
|
290
|
+
<Badge
|
|
291
|
+
variant={
|
|
292
|
+
displayStatus === 'active' ? 'default' :
|
|
293
|
+
displayStatus === 'sold' ? 'secondary' :
|
|
294
|
+
'destructive'
|
|
295
|
+
}
|
|
296
|
+
className="shadow-md"
|
|
297
|
+
>
|
|
298
|
+
{displayStatus.toUpperCase()}
|
|
299
|
+
</Badge>
|
|
300
|
+
</div>
|
|
301
|
+
<ListingCard
|
|
302
|
+
listing={listing}
|
|
303
|
+
onCancel={handleCancelListing}
|
|
304
|
+
isOwner={true}
|
|
305
|
+
/>
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
})}
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
</>
|
|
312
|
+
)}
|
|
313
|
+
</main>
|
|
314
|
+
|
|
315
|
+
<Footer />
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Header from '@/components/Header';
|
|
2
|
+
import Footer from '@/components/Footer';
|
|
3
|
+
import TestnetBanner from '@/components/TestnetBanner';
|
|
4
|
+
import LandingPage from '@/components/LandingPage';
|
|
5
|
+
|
|
6
|
+
export default function Home() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="min-h-screen flex flex-col">
|
|
9
|
+
<TestnetBanner />
|
|
10
|
+
<Header />
|
|
11
|
+
<LandingPage />
|
|
12
|
+
<Footer />
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|