11"use client" ;
2+ /* eslint-disable */
23import { Img } from "@/components/blocks/Img" ;
34import { CopyTextButton } from "@/components/ui/CopyTextButton" ;
45import { Spinner } from "@/components/ui/Spinner/Spinner" ;
56import { Alert , AlertDescription , AlertTitle } from "@/components/ui/alert" ;
67import { 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" ;
717import {
818 DropdownMenu ,
919 DropdownMenuContent ,
@@ -12,19 +22,27 @@ import {
1222 DropdownMenuSeparator ,
1323 DropdownMenuTrigger ,
1424} from "@/components/ui/dropdown-menu" ;
25+ import { ImageUpload } from "@/components/ui/image-upload" ;
1526import { Skeleton } from "@/components/ui/skeleton" ;
1627import { useThirdwebClient } from "@/constants/thirdweb.client" ;
28+ import { useDashboardRouter } from "@/lib/DashboardRouter" ;
1729import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler" ;
30+ import { cn } from "@/lib/utils" ;
31+ import { useDashboardStorageUpload } from "@3rdweb-sdk/react/hooks/useDashboardStorageUpload" ;
1832import {
1933 AlertTriangleIcon ,
2034 CheckIcon ,
2135 ChevronsUpDownIcon ,
2236 ExternalLinkIcon ,
37+ PencilIcon ,
2338 PlusCircleIcon ,
2439} from "lucide-react" ;
2540import Link from "next/link" ;
41+ import { useState } from "react" ;
42+ import { toast } from "sonner" ;
2643import { useEcosystemList } from "../../../hooks/use-ecosystem-list" ;
2744import type { Ecosystem } from "../../../types" ;
45+ import { useUpdateEcosystem } from "../configuration/hooks/use-update-ecosystem" ;
2846import { useEcosystem } from "../hooks/use-ecosystem" ;
2947
3048function EcosystemAlertBanner ( { ecosystem } : { ecosystem : Ecosystem } ) {
@@ -113,6 +131,8 @@ export function EcosystemHeader(props: {
113131 ecosystem : Ecosystem ;
114132 ecosystemLayoutPath : string ;
115133 teamIdOrSlug : string ;
134+ authToken : string ;
135+ teamId : string ;
116136} ) {
117137 const { data : fetchedEcosystem } = useEcosystem ( {
118138 teamIdOrSlug : props . teamIdOrSlug ,
@@ -135,6 +155,60 @@ export function EcosystemHeader(props: {
135155 client,
136156 } ) ;
137157
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+
138212 return (
139213 < div className = "border-b py-8" >
140214 < div className = "container flex flex-col gap-8" >
@@ -146,11 +220,74 @@ export function EcosystemHeader(props: {
146220 < Skeleton className = "size-24" />
147221 ) : (
148222 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 >
154291 )
155292 ) }
156293 < div className = "flex flex-col gap-2" >
0 commit comments