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" >
- + {project.name}