Skip to content

Commit 72cc27a

Browse files
[Dashboard] Add ecosystem logo upload functionality (#7113)
1 parent 46d1dab commit 72cc27a

File tree

2 files changed

+152
-5
lines changed

2 files changed

+152
-5
lines changed

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/EcosystemSlugLayout.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getTeamBySlug } from "@/api/team";
12
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
23
import { redirect } from "next/navigation";
34
import { getAuthToken } from "../../../../../../../../api/lib/getAuthToken";
@@ -25,6 +26,13 @@ export async function EcosystemLayoutSlug({
2526
params.team_slug,
2627
);
2728

29+
// Fetch team details to obtain team ID for further authenticated updates
30+
const team = await getTeamBySlug(params.team_slug);
31+
32+
if (!team) {
33+
redirect(ecosystemLayoutPath);
34+
}
35+
2836
if (!ecosystem) {
2937
redirect(ecosystemLayoutPath);
3038
}
@@ -35,6 +43,8 @@ export async function EcosystemLayoutSlug({
3543
ecosystem={ecosystem}
3644
ecosystemLayoutPath={ecosystemLayoutPath}
3745
teamIdOrSlug={params.team_slug}
46+
authToken={authToken}
47+
teamId={team.id}
3848
/>
3949

4050
<SidebarLayout

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx

Lines changed: 142 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
"use client";
2+
/* eslint-disable */
23
import { Img } from "@/components/blocks/Img";
34
import { CopyTextButton } from "@/components/ui/CopyTextButton";
45
import { Spinner } from "@/components/ui/Spinner/Spinner";
56
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
67
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";
717
import {
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";
1526
import { Skeleton } from "@/components/ui/skeleton";
1627
import { useThirdwebClient } from "@/constants/thirdweb.client";
28+
import { useDashboardRouter } from "@/lib/DashboardRouter";
1729
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
30+
import { cn } from "@/lib/utils";
31+
import { useDashboardStorageUpload } from "@3rdweb-sdk/react/hooks/useDashboardStorageUpload";
1832
import {
1933
AlertTriangleIcon,
2034
CheckIcon,
2135
ChevronsUpDownIcon,
2236
ExternalLinkIcon,
37+
PencilIcon,
2338
PlusCircleIcon,
2439
} from "lucide-react";
2540
import Link from "next/link";
41+
import { useState } from "react";
42+
import { toast } from "sonner";
2643
import { useEcosystemList } from "../../../hooks/use-ecosystem-list";
2744
import type { Ecosystem } from "../../../types";
45+
import { useUpdateEcosystem } from "../configuration/hooks/use-update-ecosystem";
2846
import { useEcosystem } from "../hooks/use-ecosystem";
2947

3048
function 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

Comments
 (0)