diff --git a/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.stories.tsx b/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.stories.tsx
index 55f5897e4a1..66f509399e0 100644
--- a/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.stories.tsx
+++ b/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.stories.tsx
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer } from "../../../../stories/utils";
+import { getThirdwebClient } from "../../../constants/thirdweb.server";
import { Button } from "../../ui/button";
import { ProjectAvatar } from "./ProjectAvatar";
@@ -17,17 +18,19 @@ export const Desktop: Story = {
args: {},
};
+const client = getThirdwebClient();
+
function Story() {
return (
All images below are set with size-6 className
-
+
-
+
@@ -62,13 +65,14 @@ function ToggleTest() {
Src+Name is: {data ? "set" : "not set"}
-
+
diff --git a/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.tsx b/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.tsx
index 6018d82c9cf..73d917c4107 100644
--- a/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.tsx
+++ b/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.tsx
@@ -1,15 +1,23 @@
import { Img } from "@/components/blocks/Img";
import { BoxIcon } from "lucide-react";
+import type { ThirdwebClient } from "thirdweb";
+import { resolveSchemeWithErrorHandler } from "../../../lib/resolveSchemeWithErrorHandler";
import { cn } from "../../../lib/utils";
export function ProjectAvatar(props: {
src: string | undefined;
className: string | undefined;
+ client: ThirdwebClient;
}) {
return (
diff --git a/apps/dashboard/src/@/components/ui/button.tsx b/apps/dashboard/src/@/components/ui/button.tsx
index f2d72c6c9b8..201ea8d6361 100644
--- a/apps/dashboard/src/@/components/ui/button.tsx
+++ b/apps/dashboard/src/@/components/ui/button.tsx
@@ -47,13 +47,16 @@ const Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
const btnOnlyProps =
- Comp === "button" ? { type: "button" as const } : undefined;
+ Comp === "button"
+ ? { type: props.type || ("button" as const) }
+ : undefined;
+
return (
);
},
diff --git a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx
index 60a7edc66ca..2663ce8370a 100644
--- a/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx
+++ b/apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/layout.tsx
@@ -109,6 +109,7 @@ async function getTeamsAndProjectsIfLoggedIn() {
projects: (await getProjects(team.slug)).map((x) => ({
id: x.id,
name: x.name,
+ image: x.image,
})),
})),
);
diff --git a/apps/dashboard/src/app/(dashboard)/published-contract/components/uri-based-deploy.tsx b/apps/dashboard/src/app/(dashboard)/published-contract/components/uri-based-deploy.tsx
index 66401b1fbe1..c8c46795bda 100644
--- a/apps/dashboard/src/app/(dashboard)/published-contract/components/uri-based-deploy.tsx
+++ b/apps/dashboard/src/app/(dashboard)/published-contract/components/uri-based-deploy.tsx
@@ -36,6 +36,7 @@ export async function DeployFormForUri(props: DeployFormForUriProps) {
projects: (await getProjects(team.slug)).map((x) => ({
id: x.id,
name: x.name,
+ image: x.image,
})),
})),
);
diff --git a/apps/dashboard/src/app/account/components/AccountHeaderUI.tsx b/apps/dashboard/src/app/account/components/AccountHeaderUI.tsx
index 636ffdd9a7d..4ad6d399241 100644
--- a/apps/dashboard/src/app/account/components/AccountHeaderUI.tsx
+++ b/apps/dashboard/src/app/account/components/AccountHeaderUI.tsx
@@ -66,6 +66,7 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
focus="team-selection"
createProject={props.createProject}
account={props.account}
+ client={props.client}
/>
)}
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx
index bd69b372ad7..53481e7db65 100644
--- a/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx
@@ -1,8 +1,11 @@
import { getWalletConnections } from "@/api/analytics";
import { type Project, getProjects } from "@/api/projects";
import { getTeamBySlug } from "@/api/team";
+import { getThirdwebClient } from "@/constants/thirdweb.server";
import { subDays } from "date-fns";
import { redirect } from "next/navigation";
+import { getAuthToken } from "../../../api/lib/getAuthToken";
+import { loginRedirect } from "../../../login/loginRedirect";
import {
type ProjectWithAnalytics,
TeamProjectsPage,
@@ -12,7 +15,14 @@ export default async function Page(props: {
params: Promise<{ team_slug: string }>;
}) {
const params = await props.params;
- const team = await getTeamBySlug(params.team_slug);
+ const [team, authToken] = await Promise.all([
+ getTeamBySlug(params.team_slug),
+ getAuthToken(),
+ ]);
+
+ if (!authToken) {
+ loginRedirect(`/team/${params.team_slug}`);
+ }
if (!team) {
redirect("/team");
@@ -32,7 +42,11 @@ export default async function Page(props: {
-
+
);
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx
index e882d31af4c..8db877b1eac 100644
--- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/projects/TeamProjectsPage.tsx
@@ -29,6 +29,7 @@ import { formatDate } from "date-fns";
import { PlusIcon, SearchIcon } from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
+import type { ThirdwebClient } from "thirdweb";
type SortById = "name" | "createdAt" | "monthlyActiveUsers";
@@ -39,6 +40,7 @@ export type ProjectWithAnalytics = Project & {
export function TeamProjectsPage(props: {
projects: ProjectWithAnalytics[];
team: Team;
+ client: ThirdwebClient;
}) {
const { projects } = props;
const [searchTerm, setSearchTerm] = useState("");
@@ -175,6 +177,7 @@ export function TeamProjectsPage(props: {
{project.name}
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx
index a75e9cec2dd..b60adde94f2 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx
@@ -36,6 +36,10 @@ function Story(props: {
return (
{
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ console.log("updateProjectImage", file);
+ }}
isOwnerAccount={props.isOwnerAccount}
transferProject={async (newTeam) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.tsx
index 7b98d097775..f787cbe5bf7 100644
--- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.tsx
+++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.tsx
@@ -1,4 +1,5 @@
"use client";
+import { apiServerProxy } from "@/actions/proxies";
import type { Project } from "@/api/projects";
import type { Team } from "@/api/team";
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
@@ -31,6 +32,7 @@ import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useDashboardRouter } from "@/lib/DashboardRouter";
+import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
import { cn } from "@/lib/utils";
import type { RotateSecretKeyAPIReturnType } from "@3rdweb-sdk/react/hooks/useApi";
import {
@@ -46,6 +48,7 @@ import {
type ServiceName,
getServiceByName,
} from "@thirdweb-dev/service-utils";
+import { FileInput } from "components/shared/FileInput";
import { format } from "date-fns";
import { useTrack } from "hooks/analytics/useTrack";
import {
@@ -60,11 +63,11 @@ import { type UseFormReturn, useForm } from "react-hook-form";
import { type FieldArrayWithId, useFieldArray } from "react-hook-form";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
+import { upload } from "thirdweb/storage";
import { RE_BUNDLE_ID } from "utils/regex";
import { joinWithComma, toArrFromList } from "utils/string";
import { validStrList } from "utils/validations";
import { z } from "zod";
-import { apiServerProxy } from "../../../../../@/actions/proxies";
import {
HIDDEN_SERVICES,
projectDomainsSchema,
@@ -119,6 +122,29 @@ export function ProjectGeneralSettingsPage(props: {
client={props.client}
teamSlug={props.teamSlug}
project={props.project}
+ updateProjectImage={async (file) => {
+ let uri: string | undefined = undefined;
+
+ if (file) {
+ // upload to IPFS
+ uri = await upload({
+ client: props.client,
+ files: [file],
+ });
+ }
+
+ await updateProjectClient(
+ {
+ projectId: props.project.id,
+ teamId: props.project.teamId,
+ },
+ {
+ image: uri,
+ },
+ );
+
+ router.refresh();
+ }}
updateProject={async (projectValues) => {
return updateProjectClient(
{
@@ -184,6 +210,7 @@ export function ProjectGeneralSettingsPageUI(props: {
client: ThirdwebClient;
transferProject: (newTeam: Team) => Promise;
isOwnerAccount: boolean;
+ updateProjectImage: (file: File | undefined) => Promise;
}) {
const projectLayout = `/team/${props.teamSlug}/${props.project.slug}`;
@@ -320,6 +347,12 @@ export function ProjectGeneralSettingsPageUI(props: {
handleSubmit={handleSubmit}
/>
+
+
Promise;
+ avatar: string | null;
+ client: ThirdwebClient;
+}) {
+ const projectAvatarUrl = resolveSchemeWithErrorHandler({
+ client: props.client,
+ uri: props.avatar || undefined,
+ });
+
+ const [projectAvatar, setProjectAvatar] = useState();
+
+ const updateProjectAvatarMutation = useMutation({
+ mutationFn: async (_avatar: File | undefined) => {
+ await props.updateProjectImage(_avatar);
+ },
+ });
+
+ function handleSave() {
+ const promise = updateProjectAvatarMutation.mutateAsync(projectAvatar);
+ toast.promise(promise, {
+ success: "Project avatar updated successfully",
+ error: "Failed to update project avatar",
+ });
+ }
+
+ return (
+
+
+
+
+ Project Avatar
+
+
+ This is your project's avatar.
Click on the avatar to upload
+ a custom one
+
+
+
+
+
+ );
+}
+
function AllowedDomainsSetting(props: {
form: UpdateAPIForm;
isUpdatingProject: boolean;
diff --git a/apps/dashboard/src/app/team/components/TeamHeader/ProjectSelectorMobileMenuButton.tsx b/apps/dashboard/src/app/team/components/TeamHeader/ProjectSelectorMobileMenuButton.tsx
index bf130e3c10a..0e7ffe25aa5 100644
--- a/apps/dashboard/src/app/team/components/TeamHeader/ProjectSelectorMobileMenuButton.tsx
+++ b/apps/dashboard/src/app/team/components/TeamHeader/ProjectSelectorMobileMenuButton.tsx
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { ChevronsUpDownIcon } from "lucide-react";
import { useState } from "react";
+import type { ThirdwebClient } from "thirdweb";
import { ProjectSelectorUI } from "./ProjectSelectorUI";
type ProjectSelectorMobileMenuButtonProps = {
@@ -14,6 +15,7 @@ type ProjectSelectorMobileMenuButtonProps = {
projects: Project[];
team: Team;
createProject: (team: Team) => void;
+ client: ThirdwebClient;
};
export function ProjectSelectorMobileMenuButton(
@@ -46,6 +48,7 @@ export function ProjectSelectorMobileMenuButton(
{
diff --git a/apps/dashboard/src/app/team/components/TeamHeader/ProjectSelectorUI.tsx b/apps/dashboard/src/app/team/components/TeamHeader/ProjectSelectorUI.tsx
index a1e22613f33..42532fcc9c6 100644
--- a/apps/dashboard/src/app/team/components/TeamHeader/ProjectSelectorUI.tsx
+++ b/apps/dashboard/src/app/team/components/TeamHeader/ProjectSelectorUI.tsx
@@ -10,6 +10,7 @@ import { cn } from "@/lib/utils";
import { CheckIcon, CirclePlusIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
+import type { ThirdwebClient } from "thirdweb";
import { SearchInput } from "./SearchInput";
export function ProjectSelectorUI(props: {
@@ -17,6 +18,7 @@ export function ProjectSelectorUI(props: {
currentProject: Project | undefined;
team: Team;
createProject: () => void;
+ client: ThirdwebClient;
}) {
const { projects, currentProject, team } = props;
const [searchProjectTerm, setSearchProjectTerm] = useState("");
@@ -60,8 +62,11 @@ export function ProjectSelectorUI(props: {
>
- {/* TODO - set Image */}
-
+
{project.name}
{isSelected && (
diff --git a/apps/dashboard/src/app/team/components/TeamHeader/TeamAndProjectSelectorPopoverButton.tsx b/apps/dashboard/src/app/team/components/TeamHeader/TeamAndProjectSelectorPopoverButton.tsx
index 0916585dc12..c7f7ea3de42 100644
--- a/apps/dashboard/src/app/team/components/TeamHeader/TeamAndProjectSelectorPopoverButton.tsx
+++ b/apps/dashboard/src/app/team/components/TeamHeader/TeamAndProjectSelectorPopoverButton.tsx
@@ -9,10 +9,10 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
-import { useThirdwebClient } from "@/constants/thirdweb.client";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { ChevronsUpDownIcon } from "lucide-react";
import { useState } from "react";
+import type { ThirdwebClient } from "thirdweb";
import { ProjectSelectorUI } from "./ProjectSelectorUI";
import { TeamSelectionUI } from "./TeamSelectionUI";
@@ -23,6 +23,7 @@ type TeamSwitcherProps = {
focus: "project-selection" | "team-selection";
createProject: (team: Team) => void;
account: Pick | undefined;
+ client: ThirdwebClient;
};
export function TeamAndProjectSelectorPopoverButton(props: TeamSwitcherProps) {
@@ -31,7 +32,6 @@ export function TeamAndProjectSelectorPopoverButton(props: TeamSwitcherProps) {
const [hoveredTeam, setHoveredTeam] = useState();
const projectsToShowOfTeam =
hoveredTeam || currentTeam || teamsAndProjects[0]?.team;
- const client = useThirdwebClient();
// if we can't find a single team associated with this user - something is really wrong
if (!projectsToShowOfTeam) {
@@ -93,7 +93,7 @@ export function TeamAndProjectSelectorPopoverButton(props: TeamSwitcherProps) {
: undefined
}
account={props.account}
- client={client}
+ client={props.client}
/>
{/* Right */}
@@ -102,6 +102,7 @@ export function TeamAndProjectSelectorPopoverButton(props: TeamSwitcherProps) {
currentProject={props.currentProject}
projects={projectsToShow}
team={projectsToShowOfTeam}
+ client={props.client}
createProject={() => {
setOpen(false);
props.createProject(projectsToShowOfTeam);
diff --git a/apps/dashboard/src/app/team/components/TeamHeader/TeamHeaderUI.tsx b/apps/dashboard/src/app/team/components/TeamHeader/TeamHeaderUI.tsx
index 3fa2db91f32..57650caf0d4 100644
--- a/apps/dashboard/src/app/team/components/TeamHeader/TeamHeaderUI.tsx
+++ b/apps/dashboard/src/app/team/components/TeamHeader/TeamHeaderUI.tsx
@@ -75,6 +75,7 @@ export function TeamHeaderDesktopUI(props: TeamHeaderCompProps) {
focus="team-selection"
createProject={props.createProject}
account={props.account}
+ client={props.client}
/>
@@ -86,8 +87,11 @@ export function TeamHeaderDesktopUI(props: TeamHeaderCompProps) {
href={`/team/${props.currentTeam.slug}/${props.currentProject.slug}`}
className="flex flex-row items-center gap-2 font-semibold text-sm"
>
- {/* TODO - set project avatar image */}
-
+
{props.currentProject.name}
@@ -98,6 +102,7 @@ export function TeamHeaderDesktopUI(props: TeamHeaderCompProps) {
focus="project-selection"
createProject={props.createProject}
account={props.account}
+ client={props.client}
/>
>
@@ -181,6 +186,7 @@ export function TeamHeaderMobileUI(props: TeamHeaderCompProps) {
projects={projects}
team={props.currentTeam}
createProject={props.createProject}
+ client={props.client}
/>
>
diff --git a/apps/dashboard/src/components/contract-components/contract-deploy-form/add-to-project-card.stories.tsx b/apps/dashboard/src/components/contract-components/contract-deploy-form/add-to-project-card.stories.tsx
index ad08d7a1e2b..9039ba0d6f9 100644
--- a/apps/dashboard/src/components/contract-components/contract-deploy-form/add-to-project-card.stories.tsx
+++ b/apps/dashboard/src/components/contract-components/contract-deploy-form/add-to-project-card.stories.tsx
@@ -51,6 +51,7 @@ function teamsAndProjectsStub(teamCount: number, projectCount: number) {
projects.push({
id: `project_${i + 1}_${j + 1}`,
name: `Project ${i + 1}_${j + 1}`,
+ image: `https://picsum.photos/200?random=${i}`,
});
}
diff --git a/apps/dashboard/src/components/contract-components/contract-deploy-form/add-to-project-card.tsx b/apps/dashboard/src/components/contract-components/contract-deploy-form/add-to-project-card.tsx
index aab2b9134bf..cb70187d9ad 100644
--- a/apps/dashboard/src/components/contract-components/contract-deploy-form/add-to-project-card.tsx
+++ b/apps/dashboard/src/components/contract-components/contract-deploy-form/add-to-project-card.tsx
@@ -20,7 +20,7 @@ import type { ThirdwebClient } from "thirdweb";
import { Fieldset } from "./common";
export type MinimalTeam = Pick;
-export type MinimalProject = Pick; // TODO: add image when project has image
+export type MinimalProject = Pick;
export type TeamAndProjectSelection = {
team: MinimalTeam | undefined;
@@ -206,7 +206,11 @@ export function AddToProjectSelector(props: {
className="py-2.5"
>