Skip to content

[Dashboard] Add ecosystem logo upload functionality #7113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getTeamBySlug } from "@/api/team";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import { redirect } from "next/navigation";
import { getAuthToken } from "../../../../../../../../api/lib/getAuthToken";
Expand Down Expand Up @@ -25,6 +26,13 @@ export async function EcosystemLayoutSlug({
params.team_slug,
);

// Fetch team details to obtain team ID for further authenticated updates
const team = await getTeamBySlug(params.team_slug);

if (!team) {
redirect(ecosystemLayoutPath);
}

if (!ecosystem) {
redirect(ecosystemLayoutPath);
}
Expand All @@ -35,6 +43,8 @@ export async function EcosystemLayoutSlug({
ecosystem={ecosystem}
ecosystemLayoutPath={ecosystemLayoutPath}
teamIdOrSlug={params.team_slug}
authToken={authToken}
teamId={team.id}
/>

<SidebarLayout
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
"use client";
/* eslint-disable */
import { Img } from "@/components/blocks/Img";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -12,19 +22,27 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ImageUpload } from "@/components/ui/image-upload";
import { Skeleton } from "@/components/ui/skeleton";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
import { cn } from "@/lib/utils";
import { useDashboardStorageUpload } from "@3rdweb-sdk/react/hooks/useDashboardStorageUpload";
import {
AlertTriangleIcon,
CheckIcon,
ChevronsUpDownIcon,
ExternalLinkIcon,
PencilIcon,
PlusCircleIcon,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { useEcosystemList } from "../../../hooks/use-ecosystem-list";
import type { Ecosystem } from "../../../types";
import { useUpdateEcosystem } from "../configuration/hooks/use-update-ecosystem";
import { useEcosystem } from "../hooks/use-ecosystem";

function EcosystemAlertBanner({ ecosystem }: { ecosystem: Ecosystem }) {
Expand Down Expand Up @@ -113,6 +131,8 @@ export function EcosystemHeader(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
teamIdOrSlug: string;
authToken: string;
teamId: string;
}) {
const { data: fetchedEcosystem } = useEcosystem({
teamIdOrSlug: props.teamIdOrSlug,
Expand All @@ -135,6 +155,60 @@ export function EcosystemHeader(props: {
client,
});

// ------------------- Image Upload Logic -------------------
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);

const storageUpload = useDashboardStorageUpload();
const router = useDashboardRouter();

const { mutateAsync: updateEcosystem, isPending: isUpdating } =
useUpdateEcosystem(
{
authToken: props.authToken,
teamId: props.teamId,
},
{
onSuccess: () => {
toast.success("Ecosystem image updated");
setIsDialogOpen(false);
router.refresh();
},
onError: (error) => {
const message =
error instanceof Error ? error.message : "Failed to update image";
toast.error(message);
},
},
);

const isUploading = storageUpload.isPending || isUpdating;

async function handleUpload() {
if (!selectedFile) {
toast.error("Please select an image to upload");
return;
}

// Validate file type
const validTypes = ["image/png", "image/jpeg", "image/webp"];
if (!validTypes.includes(selectedFile.type)) {
toast.error("Only PNG, JPG or WEBP images are allowed");
return;
}

try {
const [uri] = await storageUpload.mutateAsync([selectedFile]);
await updateEcosystem({
...ecosystem,
imageUrl: uri,
});
} catch (err) {
console.error(err);
toast.error("Failed to upload image");
}
}
Comment on lines +187 to +210
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding file size validation.

The file type validation is good, but there's no check for file size limits, which could lead to issues with large files.

function handleUpload() {
  if (!selectedFile) {
    toast.error("Please select an image to upload");
    return;
  }

  // Validate file type
  const validTypes = ["image/png", "image/jpeg", "image/webp"];
  if (!validTypes.includes(selectedFile.type)) {
    toast.error("Only PNG, JPG or WEBP images are allowed");
    return;
  }

+ // Validate file size (e.g., max 5MB)
+ const maxSizeInBytes = 5 * 1024 * 1024; // 5MB
+ if (selectedFile.size > maxSizeInBytes) {
+   toast.error("Image size should be less than 5MB");
+   return;
+ }

  try {
    // rest of the function...
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function handleUpload() {
if (!selectedFile) {
toast.error("Please select an image to upload");
return;
}
// Validate file type
const validTypes = ["image/png", "image/jpeg", "image/webp"];
if (!validTypes.includes(selectedFile.type)) {
toast.error("Only PNG, JPG or WEBP images are allowed");
return;
}
try {
const [uri] = await storageUpload.mutateAsync([selectedFile]);
await updateEcosystem({
...ecosystem,
imageUrl: uri,
});
} catch (err) {
console.error(err);
toast.error("Failed to upload image");
}
}
async function handleUpload() {
if (!selectedFile) {
toast.error("Please select an image to upload");
return;
}
// Validate file type
const validTypes = ["image/png", "image/jpeg", "image/webp"];
if (!validTypes.includes(selectedFile.type)) {
toast.error("Only PNG, JPG or WEBP images are allowed");
return;
}
// Validate file size (e.g., max 5MB)
const maxSizeInBytes = 5 * 1024 * 1024; // 5MB
if (selectedFile.size > maxSizeInBytes) {
toast.error("Image size should be less than 5MB");
return;
}
try {
const [uri] = await storageUpload.mutateAsync([selectedFile]);
await updateEcosystem({
...ecosystem,
imageUrl: uri,
});
} catch (err) {
console.error(err);
toast.error("Failed to upload image");
}
}
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx
around lines 187 to 210, the handleUpload function lacks validation for the file
size of the selected image. Add a check after the file type validation to ensure
the selectedFile size does not exceed a defined limit (e.g., 5MB). If the file
is too large, show an error toast message and return early to prevent uploading
oversized files.


return (
<div className="border-b py-8">
<div className="container flex flex-col gap-8">
Expand All @@ -146,11 +220,74 @@ export function EcosystemHeader(props: {
<Skeleton className="size-24" />
) : (
ecosystemImageLink && (
<Img
src={ecosystemImageLink}
alt={ecosystem.name}
className="size-24 rounded-full border object-contain object-center"
/>
<div className="relative">
<Img
src={ecosystemImageLink}
alt={ecosystem.name}
className={cn(
"size-24",
"border",
"rounded-full",
"object-contain object-center",
)}
/>

{/* Upload Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"absolute",
"right-0 bottom-0",
"h-6 w-6",
"p-1",
"rounded-full",
"bg-background",
"hover:bg-accent",
)}
aria-label="Change logo"
>
<PencilIcon className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-[480px]">
<DialogHeader>
<DialogTitle>Update Ecosystem Logo</DialogTitle>
</DialogHeader>

<div className="flex flex-col gap-4 py-2">
<ImageUpload
onUpload={(files) => {
if (files?.[0]) {
setSelectedFile(files[0]);
}
}}
accept="image/png,image/jpeg,image/webp"
/>
</div>

<DialogFooter className="mt-4">
<DialogClose asChild>
<Button variant="outline" disabled={isUploading}>
Cancel
</Button>
</DialogClose>
<Button
onClick={handleUpload}
disabled={isUploading || !selectedFile}
>
{isUploading ? (
<Spinner className="h-4 w-4" />
) : (
"Upload"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
)}
<div className="flex flex-col gap-2">
Expand Down
Loading