strade-stx 1.0.0 → 1.0.1
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/package.json +10 -1
- package/contracts/CoreMarketPlace.clar +0 -227
- package/contracts/DisputeResolution_clar.clar +0 -265
- package/contracts/EscrowService.clar +0 -171
- package/contracts/UserProfile.clar +0 -280
- package/contracts/ft-trait.clar +0 -24
- package/contracts/token.clar +0 -178
- package/frontend/README.md +0 -10
- package/frontend/components.json +0 -22
- package/frontend/dist/assets/index-BacuuL66.css +0 -1
- package/frontend/dist/assets/index-jryypd5B.js +0 -194
- package/frontend/dist/favicon.png +0 -0
- package/frontend/dist/index.html +0 -15
- package/frontend/dist/manifest.json +0 -15
- package/frontend/dist/vite.svg +0 -1
- package/frontend/empty-mock.js +0 -1
- package/frontend/eslint.config.js +0 -23
- package/frontend/eslint.config.mjs +0 -25
- package/frontend/index.html +0 -14
- package/frontend/next.config.ts +0 -17
- package/frontend/package-lock.json +0 -14740
- package/frontend/package.json +0 -56
- package/frontend/postcss.config.js +0 -5
- package/frontend/postcss.config.mjs +0 -5
- package/frontend/public/favicon.png +0 -0
- package/frontend/public/file.svg +0 -1
- package/frontend/public/globe.svg +0 -1
- package/frontend/public/manifest.json +0 -15
- package/frontend/public/next.svg +0 -1
- package/frontend/public/vercel.svg +0 -1
- package/frontend/public/vite.svg +0 -1
- package/frontend/public/window.svg +0 -1
- package/frontend/src/App.css +0 -42
- package/frontend/src/App.tsx +0 -177
- package/frontend/src/app/about/page.tsx +0 -208
- package/frontend/src/app/favicon.ico +0 -0
- package/frontend/src/app/globals.css +0 -129
- package/frontend/src/app/help/page.tsx +0 -167
- package/frontend/src/app/how-it-works/page.tsx +0 -274
- package/frontend/src/app/layout.tsx +0 -55
- package/frontend/src/app/marketplace/page.tsx +0 -324
- package/frontend/src/app/my-listings/page.tsx +0 -318
- package/frontend/src/app/page.tsx +0 -15
- package/frontend/src/assets/react.svg +0 -1
- package/frontend/src/components/ConfirmDialog.tsx +0 -54
- package/frontend/src/components/CreateListingForm.tsx +0 -231
- package/frontend/src/components/ErrorBoundary.tsx +0 -73
- package/frontend/src/components/FilterPanel.tsx +0 -10
- package/frontend/src/components/Footer.tsx +0 -100
- package/frontend/src/components/Header.tsx +0 -268
- package/frontend/src/components/ImageUpload.tsx +0 -147
- package/frontend/src/components/LandingPage.tsx +0 -322
- package/frontend/src/components/ListingCard.tsx +0 -154
- package/frontend/src/components/LoadingSkeleton.tsx +0 -44
- package/frontend/src/components/MobileNav.tsx +0 -89
- package/frontend/src/components/NotificationBell.tsx +0 -8
- package/frontend/src/components/NotificationPanel.tsx +0 -14
- package/frontend/src/components/README.md +0 -14
- package/frontend/src/components/SearchBar.tsx +0 -10
- package/frontend/src/components/TestnetBanner.tsx +0 -29
- package/frontend/src/components/ThemeToggle.tsx +0 -32
- package/frontend/src/components/__tests__/Header.test.tsx +0 -70
- package/frontend/src/components/__tests__/ListingCard.test.tsx +0 -86
- package/frontend/src/components/providers/ThemeProvider.tsx +0 -9
- package/frontend/src/components/ui/alert-dialog.tsx +0 -141
- package/frontend/src/components/ui/avatar.tsx +0 -53
- package/frontend/src/components/ui/badge.tsx +0 -46
- package/frontend/src/components/ui/button.tsx +0 -60
- package/frontend/src/components/ui/card.tsx +0 -92
- package/frontend/src/components/ui/dialog.tsx +0 -143
- package/frontend/src/components/ui/dropdown-menu.tsx +0 -257
- package/frontend/src/components/ui/input.tsx +0 -21
- package/frontend/src/components/ui/label.tsx +0 -24
- package/frontend/src/components/ui/select.tsx +0 -187
- package/frontend/src/components/ui/sonner.tsx +0 -40
- package/frontend/src/components/ui/textarea.tsx +0 -18
- package/frontend/src/context/README.md +0 -27
- package/frontend/src/index.css +0 -166
- package/frontend/src/lib/notificationEvents.ts +0 -10
- package/frontend/src/lib/notificationStore.ts +0 -13
- package/frontend/src/lib/notifications.ts +0 -13
- package/frontend/src/lib/search.ts +0 -28
- package/frontend/src/lib/stacks.ts +0 -189
- package/frontend/src/lib/utils.ts +0 -6
- package/frontend/src/main.tsx +0 -10
- package/frontend/src/test/setup.ts +0 -23
- package/frontend/src/types.d.ts +0 -9
- package/frontend/tsconfig.app.json +0 -28
- package/frontend/tsconfig.json +0 -41
- package/frontend/tsconfig.node.json +0 -26
- package/frontend/vercel.json +0 -4
- package/frontend/vite.config.ts +0 -6
- package/frontend/vitest.config.ts +0 -17
- package/scripts/auto-activity.sh +0 -9
- package/scripts/cancel-pending.ts +0 -67
- package/scripts/check-balances.ts +0 -23
- package/scripts/distribute-evenly.ts +0 -56
- package/scripts/drain-accounts.ts +0 -70
- package/scripts/fund-accounts.ts +0 -88
- package/scripts/fund-active.ts +0 -59
- package/scripts/fund-unfunded.ts +0 -88
- package/scripts/generate-activity.ts +0 -181
- package/scripts/git-activity-generator.ts +0 -154
- package/scripts/mobile-server.ts +0 -123
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
'use client';
|
|
3
|
-
|
|
4
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
5
|
-
import { usePathname } from 'next/navigation';
|
|
6
|
-
import Link from 'next/link';
|
|
7
|
-
import { Button } from '@/components/ui/button';
|
|
8
|
-
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
9
|
-
import { userSession, formatAddress, getUserBalance, formatSTX, isWalletConnected, getConnectedAddress } from '@/lib/stacks';
|
|
10
|
-
import { connect } from '@stacks/connect';
|
|
11
|
-
import { Wallet, LogOut, Copy, CheckCircle2 } from 'lucide-react';
|
|
12
|
-
import MobileNav from '@/components/MobileNav';
|
|
13
|
-
import { ThemeToggle } from '@/components/ThemeToggle';
|
|
14
|
-
import {
|
|
15
|
-
DropdownMenu,
|
|
16
|
-
DropdownMenuContent,
|
|
17
|
-
DropdownMenuItem,
|
|
18
|
-
DropdownMenuLabel,
|
|
19
|
-
DropdownMenuSeparator,
|
|
20
|
-
DropdownMenuTrigger,
|
|
21
|
-
} from '@/components/ui/dropdown-menu';
|
|
22
|
-
|
|
23
|
-
export default function Header() {
|
|
24
|
-
const pathname = usePathname();
|
|
25
|
-
const [isConnected, setIsConnected] = useState(false);
|
|
26
|
-
const [userAddress, setUserAddress] = useState<string>('');
|
|
27
|
-
const [balance, setBalance] = useState<number>(0);
|
|
28
|
-
const [loadingBalance, setLoadingBalance] = useState(false);
|
|
29
|
-
const [copied, setCopied] = useState(false);
|
|
30
|
-
|
|
31
|
-
// Function to load user data and balance
|
|
32
|
-
const loadUserData = useCallback(() => {
|
|
33
|
-
if (typeof window === 'undefined') return;
|
|
34
|
-
|
|
35
|
-
const address = getConnectedAddress();
|
|
36
|
-
|
|
37
|
-
if (address) {
|
|
38
|
-
setIsConnected(true);
|
|
39
|
-
setUserAddress(address);
|
|
40
|
-
loadBalance(address);
|
|
41
|
-
} else {
|
|
42
|
-
setIsConnected(false);
|
|
43
|
-
setUserAddress('');
|
|
44
|
-
setBalance(0);
|
|
45
|
-
}
|
|
46
|
-
}, []);
|
|
47
|
-
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
loadUserData();
|
|
50
|
-
|
|
51
|
-
// Listen for storage changes (wallet disconnect via another tab)
|
|
52
|
-
window.addEventListener('storage', loadUserData);
|
|
53
|
-
return () => window.removeEventListener('storage', loadUserData);
|
|
54
|
-
}, [loadUserData]);
|
|
55
|
-
|
|
56
|
-
const loadBalance = async (address: string) => {
|
|
57
|
-
setLoadingBalance(true);
|
|
58
|
-
try {
|
|
59
|
-
const bal = await getUserBalance(address);
|
|
60
|
-
setBalance(bal);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.error('Error loading balance:', error);
|
|
63
|
-
setBalance(0);
|
|
64
|
-
} finally {
|
|
65
|
-
setLoadingBalance(false);
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const connectWallet = async () => {
|
|
70
|
-
// Always sign out and clear all session data first
|
|
71
|
-
if (userSession.isUserSignedIn()) {
|
|
72
|
-
userSession.signUserOut();
|
|
73
|
-
|
|
74
|
-
// Clear all Stacks-related data from localStorage
|
|
75
|
-
Object.keys(localStorage).forEach(key => {
|
|
76
|
-
if (key.includes('blockstack') || key.includes('stacks')) {
|
|
77
|
-
localStorage.removeItem(key);
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
setIsConnected(false);
|
|
82
|
-
setUserAddress('');
|
|
83
|
-
setBalance(0);
|
|
84
|
-
|
|
85
|
-
// Wait a bit for storage to clear
|
|
86
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
const result = await connect();
|
|
91
|
-
|
|
92
|
-
// Poll for user session to be ready
|
|
93
|
-
let attempts = 0;
|
|
94
|
-
const checkSession = setInterval(() => {
|
|
95
|
-
attempts++;
|
|
96
|
-
|
|
97
|
-
if (userSession.isUserSignedIn()) {
|
|
98
|
-
clearInterval(checkSession);
|
|
99
|
-
loadUserData();
|
|
100
|
-
} else if (attempts >= 20) {
|
|
101
|
-
clearInterval(checkSession);
|
|
102
|
-
// Reload the page as fallback
|
|
103
|
-
window.location.reload();
|
|
104
|
-
}
|
|
105
|
-
}, 200);
|
|
106
|
-
|
|
107
|
-
} catch (error) {
|
|
108
|
-
console.error('Error calling connect:', error);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const disconnectWallet = () => {
|
|
113
|
-
userSession.signUserOut();
|
|
114
|
-
|
|
115
|
-
// Clear all Stacks-related data from localStorage
|
|
116
|
-
Object.keys(localStorage).forEach(key => {
|
|
117
|
-
if (key.includes('blockstack') || key.includes('stacks')) {
|
|
118
|
-
localStorage.removeItem(key);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
setIsConnected(false);
|
|
123
|
-
setUserAddress('');
|
|
124
|
-
setBalance(0);
|
|
125
|
-
|
|
126
|
-
window.location.reload();
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const copyAddress = async () => {
|
|
130
|
-
await navigator.clipboard.writeText(userAddress);
|
|
131
|
-
setCopied(true);
|
|
132
|
-
setTimeout(() => setCopied(false), 2000);
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
<header className="bg-white dark:bg-slate-900 shadow-sm border-b border-slate-200 dark:border-slate-700 transition-colors">
|
|
138
|
-
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
139
|
-
<div className="flex justify-between items-center h-16">
|
|
140
|
-
<div className="flex items-center gap-4">
|
|
141
|
-
<div className="flex items-center">
|
|
142
|
-
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">Strade</h1>
|
|
143
|
-
<span className="ml-2 text-sm text-slate-500 dark:text-slate-400 hidden sm:inline">Decentralized Marketplace</span>
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
146
|
-
|
|
147
|
-
{/* Center Navigation - Desktop Only */}
|
|
148
|
-
<nav className="hidden md:flex items-center space-x-2">
|
|
149
|
-
<Link href="/">
|
|
150
|
-
<span
|
|
151
|
-
className={`px-4 py-2 rounded-md font-medium transition-all cursor-pointer ${
|
|
152
|
-
pathname === '/'
|
|
153
|
-
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30'
|
|
154
|
-
: 'text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-slate-50 dark:hover:bg-slate-800'
|
|
155
|
-
}`}
|
|
156
|
-
>
|
|
157
|
-
Home
|
|
158
|
-
</span>
|
|
159
|
-
</Link>
|
|
160
|
-
<Link href="/marketplace">
|
|
161
|
-
<span
|
|
162
|
-
className={`px-4 py-2 rounded-md font-medium transition-all cursor-pointer ${
|
|
163
|
-
pathname === '/marketplace'
|
|
164
|
-
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30'
|
|
165
|
-
: 'text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-slate-50 dark:hover:bg-slate-800'
|
|
166
|
-
}`}
|
|
167
|
-
>
|
|
168
|
-
Marketplace
|
|
169
|
-
</span>
|
|
170
|
-
</Link>
|
|
171
|
-
<Link href="/my-listings">
|
|
172
|
-
<span
|
|
173
|
-
className={`px-4 py-2 rounded-md font-medium transition-all cursor-pointer ${
|
|
174
|
-
pathname === '/my-listings'
|
|
175
|
-
? 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30'
|
|
176
|
-
: 'text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-slate-50 dark:hover:bg-slate-800'
|
|
177
|
-
}`}
|
|
178
|
-
>
|
|
179
|
-
My Listings
|
|
180
|
-
</span>
|
|
181
|
-
</Link>
|
|
182
|
-
</nav>
|
|
183
|
-
|
|
184
|
-
{/* Wallet Connection & Mobile Menu */}
|
|
185
|
-
<div className="flex items-center gap-2 md:gap-3">
|
|
186
|
-
<ThemeToggle />
|
|
187
|
-
{isConnected ? (
|
|
188
|
-
<DropdownMenu>
|
|
189
|
-
<DropdownMenuTrigger asChild>
|
|
190
|
-
<Avatar className="h-8 w-8 cursor-pointer ring-2 ring-blue-500/20 hover:ring-blue-500/50 transition-all">
|
|
191
|
-
<AvatarFallback className="text-xs bg-gradient-to-br from-blue-500 to-purple-600 text-white font-semibold">
|
|
192
|
-
{userAddress.slice(0, 2).toUpperCase()}
|
|
193
|
-
</AvatarFallback>
|
|
194
|
-
</Avatar>
|
|
195
|
-
</DropdownMenuTrigger>
|
|
196
|
-
<DropdownMenuContent align="end" className="w-64 bg-white dark:bg-slate-800">
|
|
197
|
-
<DropdownMenuLabel className="text-slate-900 dark:text-white">Account</DropdownMenuLabel>
|
|
198
|
-
<DropdownMenuSeparator />
|
|
199
|
-
|
|
200
|
-
<div className="px-2 py-2">
|
|
201
|
-
<div className="flex items-center justify-between mb-2">
|
|
202
|
-
<span className="text-sm text-slate-600 dark:text-slate-300 font-medium">Address</span>
|
|
203
|
-
<button
|
|
204
|
-
onClick={copyAddress}
|
|
205
|
-
className="text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 flex items-center gap-1"
|
|
206
|
-
>
|
|
207
|
-
{copied ? (
|
|
208
|
-
<>
|
|
209
|
-
<CheckCircle2 className="h-3 w-3" />
|
|
210
|
-
Copied
|
|
211
|
-
</>
|
|
212
|
-
) : (
|
|
213
|
-
<>
|
|
214
|
-
<Copy className="h-3 w-3" />
|
|
215
|
-
Copy
|
|
216
|
-
</>
|
|
217
|
-
)}
|
|
218
|
-
</button>
|
|
219
|
-
</div>
|
|
220
|
-
<p className="text-sm font-mono bg-slate-100 dark:bg-slate-700 text-slate-900 dark:text-slate-100 p-2 rounded break-all">
|
|
221
|
-
{formatAddress(userAddress)}
|
|
222
|
-
</p>
|
|
223
|
-
</div>
|
|
224
|
-
|
|
225
|
-
<div className="px-2 py-2">
|
|
226
|
-
<div className="flex items-center justify-between">
|
|
227
|
-
<span className="text-sm text-slate-600 dark:text-slate-300 font-medium">Balance</span>
|
|
228
|
-
<div className="flex items-center gap-1">
|
|
229
|
-
<Wallet className="h-3 w-3 text-slate-600 dark:text-slate-300" />
|
|
230
|
-
<span className="text-sm font-medium text-slate-900 dark:text-white">
|
|
231
|
-
{loadingBalance ? '...' : formatSTX(balance)} STX
|
|
232
|
-
</span>
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
235
|
-
</div>
|
|
236
|
-
|
|
237
|
-
<DropdownMenuSeparator />
|
|
238
|
-
|
|
239
|
-
<DropdownMenuItem
|
|
240
|
-
onClick={disconnectWallet}
|
|
241
|
-
className="cursor-pointer text-red-600 dark:text-red-400 focus:text-red-600 dark:focus:text-red-400"
|
|
242
|
-
>
|
|
243
|
-
<LogOut className="mr-2 h-4 w-4" />
|
|
244
|
-
Disconnect
|
|
245
|
-
</DropdownMenuItem>
|
|
246
|
-
</DropdownMenuContent>
|
|
247
|
-
</DropdownMenu>
|
|
248
|
-
) : (
|
|
249
|
-
<Button onClick={connectWallet} size="sm" className="cursor-pointer bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white border-0">
|
|
250
|
-
<Wallet className="h-4 w-4 mr-2" />
|
|
251
|
-
Connect Wallet
|
|
252
|
-
</Button>
|
|
253
|
-
)}
|
|
254
|
-
|
|
255
|
-
<MobileNav />
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
</div>
|
|
259
|
-
</header>
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
// md:flex hidden
|
|
263
|
-
// md:hidden
|
|
264
|
-
// px-4
|
|
265
|
-
// text-sm
|
|
266
|
-
// overflow-hidden
|
|
267
|
-
// skip link
|
|
268
|
-
// z-50
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useRef } from 'react';
|
|
4
|
-
import { Button } from '@/components/ui/button';
|
|
5
|
-
import { Upload, X, Image as ImageIcon } from 'lucide-react';
|
|
6
|
-
import { toast } from 'sonner';
|
|
7
|
-
|
|
8
|
-
interface ImageUploadProps {
|
|
9
|
-
onImageSelect: (imageUrl: string) => void;
|
|
10
|
-
currentImage?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export default function ImageUpload({ onImageSelect, currentImage }: ImageUploadProps) {
|
|
14
|
-
const [preview, setPreview] = useState<string>(currentImage || '');
|
|
15
|
-
const [uploading, setUploading] = useState(false);
|
|
16
|
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
17
|
-
|
|
18
|
-
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
19
|
-
const file = event.target.files?.[0];
|
|
20
|
-
if (!file) return;
|
|
21
|
-
|
|
22
|
-
// Validate file type
|
|
23
|
-
if (!file.type.startsWith('image/')) {
|
|
24
|
-
toast.error('Please select an image file');
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Validate file size (max 5MB)
|
|
29
|
-
if (file.size > 5 * 1024 * 1024) {
|
|
30
|
-
toast.error('Image size must be less than 5MB');
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Create preview
|
|
35
|
-
const reader = new FileReader();
|
|
36
|
-
reader.onloadend = () => {
|
|
37
|
-
setPreview(reader.result as string);
|
|
38
|
-
};
|
|
39
|
-
reader.readAsDataURL(file);
|
|
40
|
-
|
|
41
|
-
// Upload to IPFS (using Pinata or similar service)
|
|
42
|
-
await uploadToIPFS(file);
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const uploadToIPFS = async (file: File) => {
|
|
46
|
-
setUploading(true);
|
|
47
|
-
try {
|
|
48
|
-
// TODO: Replace with actual IPFS upload
|
|
49
|
-
// For now, we'll use a placeholder approach with base64
|
|
50
|
-
// In production, integrate with Pinata, NFT.Storage, or Web3.Storage
|
|
51
|
-
|
|
52
|
-
const reader = new FileReader();
|
|
53
|
-
reader.onloadend = () => {
|
|
54
|
-
const base64String = reader.result as string;
|
|
55
|
-
onImageSelect(base64String);
|
|
56
|
-
toast.success('Image uploaded successfully');
|
|
57
|
-
};
|
|
58
|
-
reader.readAsDataURL(file);
|
|
59
|
-
|
|
60
|
-
// Example Pinata integration (commented out):
|
|
61
|
-
/*
|
|
62
|
-
const formData = new FormData();
|
|
63
|
-
formData.append('file', file);
|
|
64
|
-
|
|
65
|
-
const response = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', {
|
|
66
|
-
method: 'POST',
|
|
67
|
-
headers: {
|
|
68
|
-
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_PINATA_JWT}`,
|
|
69
|
-
},
|
|
70
|
-
body: formData,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const data = await response.json();
|
|
74
|
-
const ipfsUrl = `https://gateway.pinata.cloud/ipfs/${data.IpfsHash}`;
|
|
75
|
-
onImageSelect(ipfsUrl);
|
|
76
|
-
toast.success('Image uploaded to IPFS');
|
|
77
|
-
*/
|
|
78
|
-
} catch (error) {
|
|
79
|
-
console.error('Error uploading image:', error);
|
|
80
|
-
toast.error('Failed to upload image');
|
|
81
|
-
} finally {
|
|
82
|
-
setUploading(false);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const removeImage = () => {
|
|
87
|
-
setPreview('');
|
|
88
|
-
onImageSelect('');
|
|
89
|
-
if (fileInputRef.current) {
|
|
90
|
-
fileInputRef.current.value = '';
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<div className="space-y-2">
|
|
96
|
-
<input
|
|
97
|
-
ref={fileInputRef}
|
|
98
|
-
type="file"
|
|
99
|
-
accept="image/*"
|
|
100
|
-
onChange={handleFileSelect}
|
|
101
|
-
className="hidden"
|
|
102
|
-
/>
|
|
103
|
-
|
|
104
|
-
{preview ? (
|
|
105
|
-
<div className="relative w-full h-48 bg-slate-100 rounded-lg overflow-hidden group">
|
|
106
|
-
<img
|
|
107
|
-
src={preview}
|
|
108
|
-
alt="Preview"
|
|
109
|
-
className="w-full h-full object-cover"
|
|
110
|
-
/>
|
|
111
|
-
<div className="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-50 transition-all flex items-center justify-center">
|
|
112
|
-
<Button
|
|
113
|
-
type="button"
|
|
114
|
-
variant="destructive"
|
|
115
|
-
size="sm"
|
|
116
|
-
onClick={removeImage}
|
|
117
|
-
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
|
118
|
-
>
|
|
119
|
-
<X className="h-4 w-4 mr-2" />
|
|
120
|
-
Remove
|
|
121
|
-
</Button>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
) : (
|
|
125
|
-
<button
|
|
126
|
-
type="button"
|
|
127
|
-
onClick={() => fileInputRef.current?.click()}
|
|
128
|
-
disabled={uploading}
|
|
129
|
-
className="w-full h-48 border-2 border-dashed border-slate-300 rounded-lg hover:border-slate-400 transition-colors flex flex-col items-center justify-center gap-2 text-slate-500 hover:text-slate-700 disabled:opacity-50"
|
|
130
|
-
>
|
|
131
|
-
{uploading ? (
|
|
132
|
-
<>
|
|
133
|
-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-slate-900"></div>
|
|
134
|
-
<span className="text-sm">Uploading...</span>
|
|
135
|
-
</>
|
|
136
|
-
) : (
|
|
137
|
-
<>
|
|
138
|
-
<ImageIcon className="h-12 w-12" />
|
|
139
|
-
<span className="text-sm font-medium">Click to upload image</span>
|
|
140
|
-
<span className="text-xs">PNG, JPG, GIF up to 5MB</span>
|
|
141
|
-
</>
|
|
142
|
-
)}
|
|
143
|
-
</button>
|
|
144
|
-
)}
|
|
145
|
-
</div>
|
|
146
|
-
);
|
|
147
|
-
}
|