Skip to content

Commit

Permalink
[TOOL-3392] Dashboard: Add project Image field in project settings pa…
Browse files Browse the repository at this point in the history
…ge (#6379)
  • Loading branch information
MananTank committed Feb 28, 2025
1 parent e772581 commit 5a9a76b
Show file tree
Hide file tree
Showing 16 changed files with 171 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -17,17 +18,19 @@ export const Desktop: Story = {
args: {},
};

const client = getThirdwebClient();

function Story() {
return (
<div className="flex flex-col gap-10 p-10">
<p> All images below are set with size-6 className </p>

<BadgeContainer label="No Src - Skeleton">
<ProjectAvatar src={undefined} className="size-6" />
<ProjectAvatar src={undefined} className="size-6" client={client} />
</BadgeContainer>

<BadgeContainer label="Invalid/Empty Src - BoxIcon Fallback">
<ProjectAvatar src={""} className="size-6" />
<ProjectAvatar src={""} className="size-6" client={client} />
</BadgeContainer>

<ToggleTest />
Expand Down Expand Up @@ -62,13 +65,14 @@ function ToggleTest() {
<p> Src+Name is: {data ? "set" : "not set"} </p>

<BadgeContainer label="Valid Src">
<ProjectAvatar src={data?.src} className="size-6" />
<ProjectAvatar src={data?.src} className="size-6" client={client} />
</BadgeContainer>

<BadgeContainer label="invalid Src">
<ProjectAvatar
src={data ? "invalid-src" : undefined}
className="size-6"
client={client}
/>
</BadgeContainer>
</div>
Expand Down
12 changes: 10 additions & 2 deletions apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Img
src={props.src}
className={cn("rounded-lg border border-border", props.className)}
src={
resolveSchemeWithErrorHandler({
uri: props.src,
client: props.client,
}) || ""
}
className={cn("rounded-full border border-border", props.className)}
alt={""}
fallback={
<div className="flex items-center justify-center bg-card">
Expand Down
7 changes: 5 additions & 2 deletions apps/dashboard/src/@/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,16 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ 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 (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...btnOnlyProps}
{...props}
{...btnOnlyProps}
/>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ async function getTeamsAndProjectsIfLoggedIn() {
projects: (await getProjects(team.slug)).map((x) => ({
id: x.id,
name: x.name,
image: x.image,
})),
})),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})),
})),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
focus="team-selection"
createProject={props.createProject}
account={props.account}
client={props.client}
/>
)}
</div>
Expand Down
18 changes: 16 additions & 2 deletions apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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");
Expand All @@ -32,7 +42,11 @@ export default async function Page(props: {
</div>

<div className="container flex grow flex-col pt-8 pb-20">
<TeamProjectsPage projects={projectsWithTotalWallets} team={team} />
<TeamProjectsPage
projects={projectsWithTotalWallets}
team={team}
client={getThirdwebClient(authToken)}
/>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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("");
Expand Down Expand Up @@ -175,6 +177,7 @@ export function TeamProjectsPage(props: {
<ProjectAvatar
className="size-8 rounded-full"
src={project.image || ""}
client={props.client}
/>
<span className="font-medium text-sm">
{project.name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ function Story(props: {
return (
<div className="mx-auto w-full max-w-[1100px] px-4 py-6">
<ProjectGeneralSettingsPageUI
updateProjectImage={async (file) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("updateProjectImage", file);
}}
isOwnerAccount={props.isOwnerAccount}
transferProject={async (newTeam) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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(
{
Expand Down Expand Up @@ -184,6 +210,7 @@ export function ProjectGeneralSettingsPageUI(props: {
client: ThirdwebClient;
transferProject: (newTeam: Team) => Promise<void>;
isOwnerAccount: boolean;
updateProjectImage: (file: File | undefined) => Promise<void>;
}) {
const projectLayout = `/team/${props.teamSlug}/${props.project.slug}`;

Expand Down Expand Up @@ -320,6 +347,12 @@ export function ProjectGeneralSettingsPageUI(props: {
handleSubmit={handleSubmit}
/>

<ProjectImageSetting
updateProjectImage={props.updateProjectImage}
avatar={project.image || null}
client={props.client}
/>

<ProjectKeyDetails
project={project}
rotateSecretKey={props.rotateSecretKey}
Expand Down Expand Up @@ -402,6 +435,66 @@ function ProjectNameSetting(props: {
);
}

function ProjectImageSetting(props: {
updateProjectImage: (file: File | undefined) => Promise<void>;
avatar: string | null;
client: ThirdwebClient;
}) {
const projectAvatarUrl = resolveSchemeWithErrorHandler({
client: props.client,
uri: props.avatar || undefined,
});

const [projectAvatar, setProjectAvatar] = useState<File | undefined>();

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 (
<SettingsCard
bottomText="An avatar is optional but strongly recommended."
saveButton={{
onClick: handleSave,
disabled: false,
isPending: updateProjectAvatarMutation.isPending,
}}
noPermissionText={undefined}
errorText={undefined}
>
<div className="flex flex-row gap-4 md:justify-between">
<div>
<h3 className="font-semibold text-xl tracking-tight">
Project Avatar
</h3>
<p className="mt-1.5 mb-4 text-foreground text-sm leading-relaxed">
This is your project's avatar. <br /> Click on the avatar to upload
a custom one
</p>
</div>
<FileInput
accept={{ "image/*": [] }}
value={projectAvatar}
setValue={setProjectAvatar}
className="w-20 rounded-full lg:w-28"
disableHelperText
fileUrl={projectAvatarUrl}
/>
</div>
</SettingsCard>
);
}

function AllowedDomainsSetting(props: {
form: UpdateAPIForm;
isUpdatingProject: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ 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 = {
currentProject: Project;
projects: Project[];
team: Team;
createProject: (team: Team) => void;
client: ThirdwebClient;
};

export function ProjectSelectorMobileMenuButton(
Expand Down Expand Up @@ -46,6 +48,7 @@ export function ProjectSelectorMobileMenuButton(
<DynamicHeight>
<ProjectSelectorUI
currentProject={props.currentProject}
client={props.client}
projects={props.projects}
team={props.team}
createProject={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ 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: {
projects: Project[];
currentProject: Project | undefined;
team: Team;
createProject: () => void;
client: ThirdwebClient;
}) {
const { projects, currentProject, team } = props;
const [searchProjectTerm, setSearchProjectTerm] = useState("");
Expand Down Expand Up @@ -60,8 +62,11 @@ export function ProjectSelectorUI(props: {
>
<Link href={`/team/${team.slug}/${project.slug}`}>
<div className="flex items-center gap-2">
{/* TODO - set Image */}
<ProjectAvatar src="" className="size-6" />
<ProjectAvatar
src={project.image || ""}
className="size-6"
client={props.client}
/>
<span className="truncate"> {project.name}</span>
</div>
{isSelected && (
Expand Down
Loading

0 comments on commit 5a9a76b

Please sign in to comment.