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,322 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { motion } from 'framer-motion';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import Link from 'next/link';
|
|
7
|
+
import {
|
|
8
|
+
Shield,
|
|
9
|
+
Zap,
|
|
10
|
+
Users,
|
|
11
|
+
Lock,
|
|
12
|
+
TrendingUp,
|
|
13
|
+
CheckCircle,
|
|
14
|
+
ArrowRight,
|
|
15
|
+
Sparkles,
|
|
16
|
+
Globe,
|
|
17
|
+
Code
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
|
|
20
|
+
const fadeInUp = {
|
|
21
|
+
initial: { opacity: 0, y: 20 },
|
|
22
|
+
animate: { opacity: 1, y: 0 },
|
|
23
|
+
transition: { duration: 0.5 }
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const staggerContainer = {
|
|
27
|
+
animate: {
|
|
28
|
+
transition: {
|
|
29
|
+
staggerChildren: 0.1
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const features = [
|
|
35
|
+
{
|
|
36
|
+
icon: Shield,
|
|
37
|
+
title: 'Secure Escrow',
|
|
38
|
+
description: 'Trustless transactions with automated smart contract escrow protection'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
icon: Users,
|
|
42
|
+
title: 'Reputation System',
|
|
43
|
+
description: 'Build trust with transparent on-chain ratings and reputation scores'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
icon: Lock,
|
|
47
|
+
title: 'Dispute Resolution',
|
|
48
|
+
description: 'Fair arbitration through community-based voting mechanism'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
icon: Zap,
|
|
52
|
+
title: 'Instant Settlement',
|
|
53
|
+
description: 'Fast transactions powered by Stacks blockchain finality'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
icon: Globe,
|
|
57
|
+
title: 'Decentralized',
|
|
58
|
+
description: 'No middlemen, no censorship, complete ownership of your data'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
icon: Code,
|
|
62
|
+
title: 'Open Source',
|
|
63
|
+
description: 'Transparent smart contracts verified on-chain with Clarity 4'
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const stats = [
|
|
68
|
+
{ value: '5', label: 'Smart Contracts' },
|
|
69
|
+
{ value: '100%', label: 'Decentralized' },
|
|
70
|
+
{ value: '0', label: 'Platform Fees' },
|
|
71
|
+
{ value: '$0.43', label: 'Deploy Cost' }
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
export default function LandingPage() {
|
|
75
|
+
return (
|
|
76
|
+
<div className="min-h-screen bg-white dark:bg-black">
|
|
77
|
+
{/* Hero Section */}
|
|
78
|
+
<section className="relative overflow-hidden border-b border-gray-200 dark:border-gray-800">
|
|
79
|
+
<div className="absolute inset-0 bg-gradient-to-br from-gray-50 to-white dark:from-gray-950 dark:to-black" />
|
|
80
|
+
|
|
81
|
+
{/* Animated background grid */}
|
|
82
|
+
<div className="absolute inset-0 bg-[linear-gradient(to_right,#00000008_1px,transparent_1px),linear-gradient(to_bottom,#00000008_1px,transparent_1px)] dark:bg-[linear-gradient(to_right,#ffffff08_1px,transparent_1px),linear-gradient(to_bottom,#ffffff08_1px,transparent_1px)] bg-[size:4rem_4rem]" />
|
|
83
|
+
|
|
84
|
+
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 sm:py-32">
|
|
85
|
+
<motion.div
|
|
86
|
+
initial="initial"
|
|
87
|
+
animate="animate"
|
|
88
|
+
variants={staggerContainer}
|
|
89
|
+
className="text-center"
|
|
90
|
+
>
|
|
91
|
+
<motion.div variants={fadeInUp} className="mb-8">
|
|
92
|
+
<Badge variant="outline" className="px-4 py-2 text-sm font-medium border-black dark:border-white">
|
|
93
|
+
<Sparkles className="h-3 w-3 mr-2 inline" />
|
|
94
|
+
Built on Stacks • Powered by Bitcoin
|
|
95
|
+
</Badge>
|
|
96
|
+
</motion.div>
|
|
97
|
+
|
|
98
|
+
<motion.h1
|
|
99
|
+
variants={fadeInUp}
|
|
100
|
+
className="text-5xl sm:text-6xl lg:text-7xl font-bold tracking-tight text-black dark:text-white mb-6"
|
|
101
|
+
>
|
|
102
|
+
Trade Without
|
|
103
|
+
<br />
|
|
104
|
+
<span className="bg-gradient-to-r from-gray-900 to-gray-600 dark:from-white dark:to-gray-400 bg-clip-text text-transparent">
|
|
105
|
+
Trust Issues
|
|
106
|
+
</span>
|
|
107
|
+
</motion.h1>
|
|
108
|
+
|
|
109
|
+
<motion.p
|
|
110
|
+
variants={fadeInUp}
|
|
111
|
+
className="max-w-2xl mx-auto text-lg sm:text-xl text-gray-600 dark:text-gray-400 mb-10"
|
|
112
|
+
>
|
|
113
|
+
A decentralized marketplace where smart contracts ensure security,
|
|
114
|
+
escrow protects transactions, and blockchain guarantees transparency.
|
|
115
|
+
</motion.p>
|
|
116
|
+
|
|
117
|
+
<motion.div
|
|
118
|
+
variants={fadeInUp}
|
|
119
|
+
className="flex flex-col sm:flex-row gap-4 justify-center"
|
|
120
|
+
>
|
|
121
|
+
<Link href="/marketplace">
|
|
122
|
+
<Button size="lg" className="bg-black hover:bg-gray-800 dark:bg-white dark:hover:bg-gray-200 text-white dark:text-black px-8 h-12">
|
|
123
|
+
Explore Marketplace
|
|
124
|
+
<ArrowRight className="ml-2 h-4 w-4" />
|
|
125
|
+
</Button>
|
|
126
|
+
</Link>
|
|
127
|
+
<Link href="/about">
|
|
128
|
+
<Button size="lg" variant="outline" className="border-black dark:border-white text-black dark:text-white hover:bg-gray-100 dark:hover:bg-gray-900 px-8 h-12">
|
|
129
|
+
Learn More
|
|
130
|
+
</Button>
|
|
131
|
+
</Link>
|
|
132
|
+
</motion.div>
|
|
133
|
+
|
|
134
|
+
{/* Stats */}
|
|
135
|
+
<motion.div
|
|
136
|
+
variants={fadeInUp}
|
|
137
|
+
className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8"
|
|
138
|
+
>
|
|
139
|
+
{stats.map((stat, index) => (
|
|
140
|
+
<div key={index} className="text-center">
|
|
141
|
+
<div className="text-3xl sm:text-4xl font-bold text-black dark:text-white mb-2">
|
|
142
|
+
{stat.value}
|
|
143
|
+
</div>
|
|
144
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
145
|
+
{stat.label}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
))}
|
|
149
|
+
</motion.div>
|
|
150
|
+
</motion.div>
|
|
151
|
+
</div>
|
|
152
|
+
</section>
|
|
153
|
+
|
|
154
|
+
{/* Features Section */}
|
|
155
|
+
<section className="py-24 border-b border-gray-200 dark:border-gray-800">
|
|
156
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
157
|
+
<motion.div
|
|
158
|
+
initial="initial"
|
|
159
|
+
whileInView="animate"
|
|
160
|
+
viewport={{ once: true }}
|
|
161
|
+
variants={staggerContainer}
|
|
162
|
+
className="text-center mb-16"
|
|
163
|
+
>
|
|
164
|
+
<motion.h2
|
|
165
|
+
variants={fadeInUp}
|
|
166
|
+
className="text-3xl sm:text-4xl font-bold text-black dark:text-white mb-4"
|
|
167
|
+
>
|
|
168
|
+
Everything You Need for Safe Trading
|
|
169
|
+
</motion.h2>
|
|
170
|
+
<motion.p
|
|
171
|
+
variants={fadeInUp}
|
|
172
|
+
className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto"
|
|
173
|
+
>
|
|
174
|
+
Built with security, transparency, and user experience in mind
|
|
175
|
+
</motion.p>
|
|
176
|
+
</motion.div>
|
|
177
|
+
|
|
178
|
+
<motion.div
|
|
179
|
+
initial="initial"
|
|
180
|
+
whileInView="animate"
|
|
181
|
+
viewport={{ once: true }}
|
|
182
|
+
variants={staggerContainer}
|
|
183
|
+
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
|
|
184
|
+
>
|
|
185
|
+
{features.map((feature, index) => (
|
|
186
|
+
<motion.div
|
|
187
|
+
key={index}
|
|
188
|
+
variants={fadeInUp}
|
|
189
|
+
whileHover={{ y: -4, transition: { duration: 0.2 } }}
|
|
190
|
+
className="p-6 border border-gray-200 dark:border-gray-800 rounded-lg hover:shadow-lg transition-shadow bg-white dark:bg-black"
|
|
191
|
+
>
|
|
192
|
+
<div className="w-12 h-12 rounded-lg bg-black dark:bg-white flex items-center justify-center mb-4">
|
|
193
|
+
<feature.icon className="h-6 w-6 text-white dark:text-black" />
|
|
194
|
+
</div>
|
|
195
|
+
<h3 className="text-xl font-semibold text-black dark:text-white mb-2">
|
|
196
|
+
{feature.title}
|
|
197
|
+
</h3>
|
|
198
|
+
<p className="text-gray-600 dark:text-gray-400">
|
|
199
|
+
{feature.description}
|
|
200
|
+
</p>
|
|
201
|
+
</motion.div>
|
|
202
|
+
))}
|
|
203
|
+
</motion.div>
|
|
204
|
+
</div>
|
|
205
|
+
</section>
|
|
206
|
+
|
|
207
|
+
{/* How It Works Section */}
|
|
208
|
+
<section className="py-24 border-b border-gray-200 dark:border-gray-800">
|
|
209
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
210
|
+
<motion.div
|
|
211
|
+
initial="initial"
|
|
212
|
+
whileInView="animate"
|
|
213
|
+
viewport={{ once: true }}
|
|
214
|
+
variants={staggerContainer}
|
|
215
|
+
className="text-center mb-16"
|
|
216
|
+
>
|
|
217
|
+
<motion.h2
|
|
218
|
+
variants={fadeInUp}
|
|
219
|
+
className="text-3xl sm:text-4xl font-bold text-black dark:text-white mb-4"
|
|
220
|
+
>
|
|
221
|
+
How It Works
|
|
222
|
+
</motion.h2>
|
|
223
|
+
<motion.p
|
|
224
|
+
variants={fadeInUp}
|
|
225
|
+
className="text-lg text-gray-600 dark:text-gray-400"
|
|
226
|
+
>
|
|
227
|
+
Trading on Strade is simple and secure
|
|
228
|
+
</motion.p>
|
|
229
|
+
</motion.div>
|
|
230
|
+
|
|
231
|
+
<motion.div
|
|
232
|
+
initial="initial"
|
|
233
|
+
whileInView="animate"
|
|
234
|
+
viewport={{ once: true }}
|
|
235
|
+
variants={staggerContainer}
|
|
236
|
+
className="grid grid-cols-1 md:grid-cols-3 gap-8"
|
|
237
|
+
>
|
|
238
|
+
{[
|
|
239
|
+
{
|
|
240
|
+
step: '01',
|
|
241
|
+
title: 'Connect Your Wallet',
|
|
242
|
+
description: 'Use Leather or any Stacks-compatible wallet to get started'
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
step: '02',
|
|
246
|
+
title: 'Create or Browse',
|
|
247
|
+
description: 'List your items or discover what others are selling'
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
step: '03',
|
|
251
|
+
title: 'Trade Securely',
|
|
252
|
+
description: 'Smart contracts handle escrow and ensure safe transactions'
|
|
253
|
+
}
|
|
254
|
+
].map((item, index) => (
|
|
255
|
+
<motion.div
|
|
256
|
+
key={index}
|
|
257
|
+
variants={fadeInUp}
|
|
258
|
+
className="relative text-center"
|
|
259
|
+
>
|
|
260
|
+
<div className="text-6xl font-bold text-gray-200 dark:text-gray-800 mb-4">
|
|
261
|
+
{item.step}
|
|
262
|
+
</div>
|
|
263
|
+
<h3 className="text-xl font-semibold text-black dark:text-white mb-2">
|
|
264
|
+
{item.title}
|
|
265
|
+
</h3>
|
|
266
|
+
<p className="text-gray-600 dark:text-gray-400">
|
|
267
|
+
{item.description}
|
|
268
|
+
</p>
|
|
269
|
+
{index < 2 && (
|
|
270
|
+
<div className="hidden md:block absolute top-1/2 -right-4 transform -translate-y-1/2">
|
|
271
|
+
<ArrowRight className="h-6 w-6 text-gray-300 dark:text-gray-700" />
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
</motion.div>
|
|
275
|
+
))}
|
|
276
|
+
</motion.div>
|
|
277
|
+
</div>
|
|
278
|
+
</section>
|
|
279
|
+
|
|
280
|
+
{/* CTA Section */}
|
|
281
|
+
<section className="py-24 bg-black dark:bg-white">
|
|
282
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
|
283
|
+
<motion.div
|
|
284
|
+
initial="initial"
|
|
285
|
+
whileInView="animate"
|
|
286
|
+
viewport={{ once: true }}
|
|
287
|
+
variants={staggerContainer}
|
|
288
|
+
>
|
|
289
|
+
<motion.h2
|
|
290
|
+
variants={fadeInUp}
|
|
291
|
+
className="text-3xl sm:text-4xl font-bold text-white dark:text-black mb-4"
|
|
292
|
+
>
|
|
293
|
+
Ready to Start Trading?
|
|
294
|
+
</motion.h2>
|
|
295
|
+
<motion.p
|
|
296
|
+
variants={fadeInUp}
|
|
297
|
+
className="text-lg text-gray-400 dark:text-gray-600 mb-8"
|
|
298
|
+
>
|
|
299
|
+
Join the decentralized marketplace revolution today
|
|
300
|
+
</motion.p>
|
|
301
|
+
<motion.div variants={fadeInUp}>
|
|
302
|
+
<Link href="/marketplace">
|
|
303
|
+
<Button size="lg" className="bg-white hover:bg-gray-200 dark:bg-black dark:hover:bg-gray-800 text-black dark:text-white px-8 h-12">
|
|
304
|
+
Get Started Now
|
|
305
|
+
<ArrowRight className="ml-2 h-4 w-4" />
|
|
306
|
+
</Button>
|
|
307
|
+
</Link>
|
|
308
|
+
</motion.div>
|
|
309
|
+
</motion.div>
|
|
310
|
+
</div>
|
|
311
|
+
</section>
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
// hero responsive
|
|
316
|
+
// flex-col sm:flex-row
|
|
317
|
+
// text-3xl md:text-5xl
|
|
318
|
+
// grid-cols-1 md:grid-cols-3
|
|
319
|
+
// stats mobile
|
|
320
|
+
// overflow-x-auto
|
|
321
|
+
// footer mobile
|
|
322
|
+
// alt texts
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Badge } from '@/components/ui/badge';
|
|
7
|
+
|
|
8
|
+
import { Listing, formatSTX, formatAddress, blockHeightToTimestamp } from '@/lib/stacks';
|
|
9
|
+
import { ShoppingCart, Clock, User, Package } from 'lucide-react';
|
|
10
|
+
import Image from 'next/image';
|
|
11
|
+
|
|
12
|
+
interface ListingCardProps {
|
|
13
|
+
listing: Listing;
|
|
14
|
+
onPurchase?: (listingId: number) => void;
|
|
15
|
+
onCancel?: (listingId: number) => void;
|
|
16
|
+
isOwner?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function ListingCard({ listing, onPurchase, onCancel, isOwner }: ListingCardProps) {
|
|
20
|
+
const [isPurchasing, setIsPurchasing] = useState(false);
|
|
21
|
+
const [isCancelling, setIsCancelling] = useState(false);
|
|
22
|
+
|
|
23
|
+
// For demo purposes: use placeholder image if no imageUrl provided
|
|
24
|
+
const displayImage = listing.imageUrl || `https://picsum.photos/seed/${listing.listingId}/400/300`;
|
|
25
|
+
|
|
26
|
+
const handlePurchase = async () => {
|
|
27
|
+
if (onPurchase) {
|
|
28
|
+
setIsPurchasing(true);
|
|
29
|
+
try {
|
|
30
|
+
await onPurchase(listing.listingId);
|
|
31
|
+
} finally {
|
|
32
|
+
setIsPurchasing(false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleCancel = async () => {
|
|
38
|
+
if (onCancel) {
|
|
39
|
+
setIsCancelling(true);
|
|
40
|
+
try {
|
|
41
|
+
await onCancel(listing.listingId);
|
|
42
|
+
} finally {
|
|
43
|
+
setIsCancelling(false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Convert block heights to timestamps for display
|
|
49
|
+
const expiresAtTimestamp = blockHeightToTimestamp(listing.expiresAt);
|
|
50
|
+
const isExpired = expiresAtTimestamp < Date.now();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Card className="h-full flex flex-col hover:shadow-xl transition-all hover:-translate-y-1 bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700">
|
|
54
|
+
{/* Image Section */}
|
|
55
|
+
<div className="relative w-full h-48 bg-slate-100 dark:bg-slate-700">
|
|
56
|
+
<Image
|
|
57
|
+
src={displayImage}
|
|
58
|
+
alt={listing.name}
|
|
59
|
+
fill
|
|
60
|
+
className="object-cover rounded-t-lg"
|
|
61
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
62
|
+
unoptimized
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<CardHeader>
|
|
67
|
+
<div className="flex items-start justify-between">
|
|
68
|
+
<CardTitle className="text-lg line-clamp-2 text-slate-900 dark:text-white">{listing.name}</CardTitle>
|
|
69
|
+
<Badge variant={listing.status === 'active' ? 'default' : 'secondary'} className={listing.status === 'active' ? 'bg-green-500 dark:bg-green-600' : ''}>
|
|
70
|
+
{listing.status}
|
|
71
|
+
</Badge>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="flex items-center space-x-2 text-sm text-slate-600 dark:text-slate-400">
|
|
74
|
+
<User className="h-4 w-4" />
|
|
75
|
+
<span>{formatAddress(listing.seller)}</span>
|
|
76
|
+
</div>
|
|
77
|
+
</CardHeader>
|
|
78
|
+
|
|
79
|
+
<CardContent className="flex-1">
|
|
80
|
+
<p className="text-slate-700 dark:text-slate-300 text-sm mb-4 line-clamp-3">
|
|
81
|
+
{listing.description}
|
|
82
|
+
</p>
|
|
83
|
+
|
|
84
|
+
<div className="space-y-3">
|
|
85
|
+
<div className="flex items-center justify-between">
|
|
86
|
+
<span className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
|
87
|
+
{formatSTX(listing.price)} STX
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="flex items-center space-x-2 text-sm text-slate-500 dark:text-slate-400">
|
|
92
|
+
<Clock className="h-4 w-4" />
|
|
93
|
+
<span>
|
|
94
|
+
Expires: {new Date(expiresAtTimestamp).toLocaleDateString()}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{isExpired && (
|
|
99
|
+
<Badge variant="destructive" className="text-xs bg-red-500 dark:bg-red-600">
|
|
100
|
+
Expired
|
|
101
|
+
</Badge>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
</CardContent>
|
|
105
|
+
|
|
106
|
+
<CardFooter className="flex-col gap-2 pt-4">
|
|
107
|
+
{!isOwner && listing.status === 'active' && !isExpired && (
|
|
108
|
+
<Button
|
|
109
|
+
onClick={handlePurchase}
|
|
110
|
+
disabled={isPurchasing}
|
|
111
|
+
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white border-0"
|
|
112
|
+
>
|
|
113
|
+
<ShoppingCart className="h-4 w-4 mr-2" />
|
|
114
|
+
{isPurchasing ? 'Purchasing...' : 'Purchase'}
|
|
115
|
+
</Button>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{isOwner && listing.status === 'active' && !isExpired && (
|
|
119
|
+
<div className="w-full space-y-2">
|
|
120
|
+
<div className="text-center text-sm text-blue-600 dark:text-blue-400 mb-2 font-medium">
|
|
121
|
+
Your listing
|
|
122
|
+
</div>
|
|
123
|
+
<Button
|
|
124
|
+
onClick={handleCancel}
|
|
125
|
+
disabled={isCancelling}
|
|
126
|
+
variant="destructive"
|
|
127
|
+
className="w-full bg-red-500 hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-700"
|
|
128
|
+
size="sm"
|
|
129
|
+
>
|
|
130
|
+
{isCancelling ? 'Cancelling...' : 'Cancel Listing'}
|
|
131
|
+
</Button>
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{isOwner && (listing.status !== 'active' || isExpired) && (
|
|
136
|
+
<div className="w-full text-center text-sm text-slate-500 dark:text-slate-400">
|
|
137
|
+
Your listing ({isExpired ? 'expired' : listing.status})
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{(isExpired || listing.status !== 'active') && !isOwner && (
|
|
142
|
+
<div className="w-full text-center text-sm text-slate-500 dark:text-slate-400">
|
|
143
|
+
Not available
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</CardFooter>
|
|
147
|
+
</Card>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
// w-full
|
|
151
|
+
// aspect-ratio
|
|
152
|
+
// flex-col
|
|
153
|
+
// min-h-[44px]
|
|
154
|
+
// truncate
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card';
|
|
2
|
+
|
|
3
|
+
export function ListingCardSkeleton() {
|
|
4
|
+
return (
|
|
5
|
+
<Card className="h-full flex flex-col animate-pulse">
|
|
6
|
+
<CardHeader>
|
|
7
|
+
<div className="flex items-start justify-between">
|
|
8
|
+
<div className="h-6 bg-slate-200 rounded w-3/4"></div>
|
|
9
|
+
<div className="h-6 bg-slate-200 rounded w-16"></div>
|
|
10
|
+
</div>
|
|
11
|
+
<div className="flex items-center space-x-2 mt-2">
|
|
12
|
+
<div className="h-4 bg-slate-200 rounded w-24"></div>
|
|
13
|
+
</div>
|
|
14
|
+
</CardHeader>
|
|
15
|
+
|
|
16
|
+
<CardContent className="flex-1">
|
|
17
|
+
<div className="space-y-2 mb-4">
|
|
18
|
+
<div className="h-4 bg-slate-200 rounded w-full"></div>
|
|
19
|
+
<div className="h-4 bg-slate-200 rounded w-5/6"></div>
|
|
20
|
+
<div className="h-4 bg-slate-200 rounded w-4/6"></div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div className="space-y-2">
|
|
24
|
+
<div className="h-8 bg-slate-200 rounded w-32"></div>
|
|
25
|
+
<div className="h-4 bg-slate-200 rounded w-40"></div>
|
|
26
|
+
</div>
|
|
27
|
+
</CardContent>
|
|
28
|
+
|
|
29
|
+
<CardFooter>
|
|
30
|
+
<div className="h-10 bg-slate-200 rounded w-full"></div>
|
|
31
|
+
</CardFooter>
|
|
32
|
+
</Card>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ListingGridSkeleton({ count = 6 }: { count?: number }) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
39
|
+
{Array.from({ length: count }).map((_, i) => (
|
|
40
|
+
<ListingCardSkeleton key={i} />
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { usePathname } from 'next/navigation';
|
|
6
|
+
import { Menu, X, Home, Package, HelpCircle } from 'lucide-react';
|
|
7
|
+
import { Button } from '@/components/ui/button';
|
|
8
|
+
|
|
9
|
+
export default function MobileNav() {
|
|
10
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
11
|
+
const pathname = usePathname();
|
|
12
|
+
|
|
13
|
+
const toggleMenu = () => setIsOpen(!isOpen);
|
|
14
|
+
|
|
15
|
+
const navItems = [
|
|
16
|
+
{ href: '/', label: 'Marketplace', icon: Home },
|
|
17
|
+
{ href: '/my-listings', label: 'My Listings', icon: Package },
|
|
18
|
+
{ href: '/help', label: 'Help & FAQ', icon: HelpCircle },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
{/* Mobile Menu Button */}
|
|
24
|
+
<button
|
|
25
|
+
onClick={toggleMenu}
|
|
26
|
+
className="md:hidden p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors"
|
|
27
|
+
aria-label="Toggle menu"
|
|
28
|
+
>
|
|
29
|
+
{isOpen ? (
|
|
30
|
+
<X className="h-6 w-6 text-slate-900 dark:text-white" />
|
|
31
|
+
) : (
|
|
32
|
+
<Menu className="h-6 w-6 text-slate-900 dark:text-white" />
|
|
33
|
+
)}
|
|
34
|
+
</button>
|
|
35
|
+
|
|
36
|
+
{/* Mobile Menu Overlay */}
|
|
37
|
+
{isOpen && (
|
|
38
|
+
<>
|
|
39
|
+
<div
|
|
40
|
+
className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden"
|
|
41
|
+
onClick={toggleMenu}
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
<div className="fixed top-16 left-0 right-0 bg-white dark:bg-slate-900 shadow-lg z-50 md:hidden border-t border-slate-200 dark:border-slate-700">
|
|
45
|
+
<nav className="p-4 space-y-2">
|
|
46
|
+
{navItems.map((item) => {
|
|
47
|
+
const Icon = item.icon;
|
|
48
|
+
const isActive = pathname === item.href;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Link
|
|
52
|
+
key={item.href}
|
|
53
|
+
href={item.href}
|
|
54
|
+
onClick={toggleMenu}
|
|
55
|
+
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
|
56
|
+
isActive
|
|
57
|
+
? 'bg-gradient-to-r from-blue-600 to-purple-600 text-white'
|
|
58
|
+
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800'
|
|
59
|
+
}`}
|
|
60
|
+
>
|
|
61
|
+
<Icon className="h-5 w-5" />
|
|
62
|
+
<span className="font-medium">{item.label}</span>
|
|
63
|
+
</Link>
|
|
64
|
+
);
|
|
65
|
+
})}
|
|
66
|
+
</nav>
|
|
67
|
+
</div>
|
|
68
|
+
</>
|
|
69
|
+
)}
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
// MobileNav
|
|
74
|
+
// hamburger
|
|
75
|
+
// drawer
|
|
76
|
+
// close
|
|
77
|
+
// links
|
|
78
|
+
// route change
|
|
79
|
+
// wallet
|
|
80
|
+
// theme
|
|
81
|
+
// transition
|
|
82
|
+
// aria
|
|
83
|
+
// focus trap
|
|
84
|
+
// escape key
|
|
85
|
+
// pb-safe
|
|
86
|
+
// render test
|
|
87
|
+
// useMediaQuery
|
|
88
|
+
// lazy
|
|
89
|
+
// active
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Add your components here
|
|
2
|
+
|
|
3
|
+
Example component structure:
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
export function YourComponent() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="glass rounded-2xl p-8 border border-app-border">
|
|
9
|
+
<h2 className="font-bold text-xl mb-4">Your Component</h2>
|
|
10
|
+
<p className="text-text-dim">Component content</p>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AlertCircle, X } from 'lucide-react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
|
|
6
|
+
export default function TestnetBanner() {
|
|
7
|
+
const [isVisible, setIsVisible] = useState(true);
|
|
8
|
+
|
|
9
|
+
if (!isVisible) return null;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="bg-orange-500 text-white py-3 px-4 relative">
|
|
13
|
+
<div className="max-w-7xl mx-auto flex items-center justify-center gap-2">
|
|
14
|
+
<AlertCircle className="h-5 w-5" />
|
|
15
|
+
<p className="text-sm font-medium">
|
|
16
|
+
<strong>TESTNET MODE:</strong> This is a test network. STX tokens have no real value.
|
|
17
|
+
Do not use real assets.
|
|
18
|
+
</p>
|
|
19
|
+
<button
|
|
20
|
+
onClick={() => setIsVisible(false)}
|
|
21
|
+
className="absolute right-4 hover:bg-orange-600 rounded p-1 transition-colors"
|
|
22
|
+
aria-label="Dismiss banner"
|
|
23
|
+
>
|
|
24
|
+
<X className="h-4 w-4" />
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|