Skip to content
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

[TOOL-3392] Dashboard: Add project Image field in project settings page #6379

Merged
merged 1 commit into from
Feb 28, 2025
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,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
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
Loading