1
1
"use client" ;
2
+ /* eslint-disable */
2
3
import { Img } from "@/components/blocks/Img" ;
3
4
import { CopyTextButton } from "@/components/ui/CopyTextButton" ;
4
5
import { Spinner } from "@/components/ui/Spinner/Spinner" ;
5
6
import { Alert , AlertDescription , AlertTitle } from "@/components/ui/alert" ;
6
7
import { Button } from "@/components/ui/button" ;
8
+ import {
9
+ Dialog ,
10
+ DialogClose ,
11
+ DialogContent ,
12
+ DialogFooter ,
13
+ DialogHeader ,
14
+ DialogTitle ,
15
+ DialogTrigger ,
16
+ } from "@/components/ui/dialog" ;
7
17
import {
8
18
DropdownMenu ,
9
19
DropdownMenuContent ,
@@ -12,19 +22,27 @@ import {
12
22
DropdownMenuSeparator ,
13
23
DropdownMenuTrigger ,
14
24
} from "@/components/ui/dropdown-menu" ;
25
+ import { ImageUpload } from "@/components/ui/image-upload" ;
15
26
import { Skeleton } from "@/components/ui/skeleton" ;
16
27
import { useThirdwebClient } from "@/constants/thirdweb.client" ;
28
+ import { useDashboardRouter } from "@/lib/DashboardRouter" ;
17
29
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler" ;
30
+ import { cn } from "@/lib/utils" ;
31
+ import { useDashboardStorageUpload } from "@3rdweb-sdk/react/hooks/useDashboardStorageUpload" ;
18
32
import {
19
33
AlertTriangleIcon ,
20
34
CheckIcon ,
21
35
ChevronsUpDownIcon ,
22
36
ExternalLinkIcon ,
37
+ PencilIcon ,
23
38
PlusCircleIcon ,
24
39
} from "lucide-react" ;
25
40
import Link from "next/link" ;
41
+ import { useState } from "react" ;
42
+ import { toast } from "sonner" ;
26
43
import { useEcosystemList } from "../../../hooks/use-ecosystem-list" ;
27
44
import type { Ecosystem } from "../../../types" ;
45
+ import { useUpdateEcosystem } from "../configuration/hooks/use-update-ecosystem" ;
28
46
import { useEcosystem } from "../hooks/use-ecosystem" ;
29
47
30
48
function EcosystemAlertBanner ( { ecosystem } : { ecosystem : Ecosystem } ) {
@@ -113,6 +131,8 @@ export function EcosystemHeader(props: {
113
131
ecosystem : Ecosystem ;
114
132
ecosystemLayoutPath : string ;
115
133
teamIdOrSlug : string ;
134
+ authToken : string ;
135
+ teamId : string ;
116
136
} ) {
117
137
const { data : fetchedEcosystem } = useEcosystem ( {
118
138
teamIdOrSlug : props . teamIdOrSlug ,
@@ -135,6 +155,60 @@ export function EcosystemHeader(props: {
135
155
client,
136
156
} ) ;
137
157
158
+ // ------------------- Image Upload Logic -------------------
159
+ const [ isDialogOpen , setIsDialogOpen ] = useState ( false ) ;
160
+ const [ selectedFile , setSelectedFile ] = useState < File | null > ( null ) ;
161
+
162
+ const storageUpload = useDashboardStorageUpload ( ) ;
163
+ const router = useDashboardRouter ( ) ;
164
+
165
+ const { mutateAsync : updateEcosystem , isPending : isUpdating } =
166
+ useUpdateEcosystem (
167
+ {
168
+ authToken : props . authToken ,
169
+ teamId : props . teamId ,
170
+ } ,
171
+ {
172
+ onSuccess : ( ) => {
173
+ toast . success ( "Ecosystem image updated" ) ;
174
+ setIsDialogOpen ( false ) ;
175
+ router . refresh ( ) ;
176
+ } ,
177
+ onError : ( error ) => {
178
+ const message =
179
+ error instanceof Error ? error . message : "Failed to update image" ;
180
+ toast . error ( message ) ;
181
+ } ,
182
+ } ,
183
+ ) ;
184
+
185
+ const isUploading = storageUpload . isPending || isUpdating ;
186
+
187
+ async function handleUpload ( ) {
188
+ if ( ! selectedFile ) {
189
+ toast . error ( "Please select an image to upload" ) ;
190
+ return ;
191
+ }
192
+
193
+ // Validate file type
194
+ const validTypes = [ "image/png" , "image/jpeg" , "image/webp" ] ;
195
+ if ( ! validTypes . includes ( selectedFile . type ) ) {
196
+ toast . error ( "Only PNG, JPG or WEBP images are allowed" ) ;
197
+ return ;
198
+ }
199
+
200
+ try {
201
+ const [ uri ] = await storageUpload . mutateAsync ( [ selectedFile ] ) ;
202
+ await updateEcosystem ( {
203
+ ...ecosystem ,
204
+ imageUrl : uri ,
205
+ } ) ;
206
+ } catch ( err ) {
207
+ console . error ( err ) ;
208
+ toast . error ( "Failed to upload image" ) ;
209
+ }
210
+ }
211
+
138
212
return (
139
213
< div className = "border-b py-8" >
140
214
< div className = "container flex flex-col gap-8" >
@@ -146,11 +220,74 @@ export function EcosystemHeader(props: {
146
220
< Skeleton className = "size-24" />
147
221
) : (
148
222
ecosystemImageLink && (
149
- < Img
150
- src = { ecosystemImageLink }
151
- alt = { ecosystem . name }
152
- className = "size-24 rounded-full border object-contain object-center"
153
- />
223
+ < div className = "relative" >
224
+ < Img
225
+ src = { ecosystemImageLink }
226
+ alt = { ecosystem . name }
227
+ className = { cn (
228
+ "size-24" ,
229
+ "border" ,
230
+ "rounded-full" ,
231
+ "object-contain object-center" ,
232
+ ) }
233
+ />
234
+
235
+ { /* Upload Dialog */ }
236
+ < Dialog open = { isDialogOpen } onOpenChange = { setIsDialogOpen } >
237
+ < DialogTrigger asChild >
238
+ < Button
239
+ variant = "ghost"
240
+ size = "icon"
241
+ className = { cn (
242
+ "absolute" ,
243
+ "right-0 bottom-0" ,
244
+ "h-6 w-6" ,
245
+ "p-1" ,
246
+ "rounded-full" ,
247
+ "bg-background" ,
248
+ "hover:bg-accent" ,
249
+ ) }
250
+ aria-label = "Change logo"
251
+ >
252
+ < PencilIcon className = "h-4 w-4" />
253
+ </ Button >
254
+ </ DialogTrigger >
255
+ < DialogContent className = "max-w-[480px]" >
256
+ < DialogHeader >
257
+ < DialogTitle > Update Ecosystem Logo</ DialogTitle >
258
+ </ DialogHeader >
259
+
260
+ < div className = "flex flex-col gap-4 py-2" >
261
+ < ImageUpload
262
+ onUpload = { ( files ) => {
263
+ if ( files ?. [ 0 ] ) {
264
+ setSelectedFile ( files [ 0 ] ) ;
265
+ }
266
+ } }
267
+ accept = "image/png,image/jpeg,image/webp"
268
+ />
269
+ </ div >
270
+
271
+ < DialogFooter className = "mt-4" >
272
+ < DialogClose asChild >
273
+ < Button variant = "outline" disabled = { isUploading } >
274
+ Cancel
275
+ </ Button >
276
+ </ DialogClose >
277
+ < Button
278
+ onClick = { handleUpload }
279
+ disabled = { isUploading || ! selectedFile }
280
+ >
281
+ { isUploading ? (
282
+ < Spinner className = "h-4 w-4" />
283
+ ) : (
284
+ "Upload"
285
+ ) }
286
+ </ Button >
287
+ </ DialogFooter >
288
+ </ DialogContent >
289
+ </ Dialog >
290
+ </ div >
154
291
)
155
292
) }
156
293
< div className = "flex flex-col gap-2" >
0 commit comments