Skip to content

Commit 09b1c28

Browse files
committed
Replace Avatar placeholders with Avatar components in teams and account layout (#4803)
## Problem solved Short description of the bug fixed or feature added <!-- start pr-codex --> --- ## PR-Codex overview This PR primarily focuses on updating the account handling in various components to use an `account` object instead of separate email fields, and introduces new avatar components for enhanced visual representation of users and projects. ### Detailed summary - Replaced `email` with `account` in multiple components. - Introduced `GradientAvatar` and `ProjectAvatar` components for displaying user and project images. - Updated story files to showcase new avatar components. - Added placeholder comments for future image integration. - Adjusted layout and styling for better alignment with new account structure. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 98f2b56 commit 09b1c28

25 files changed

+494
-132
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { useState } from "react";
3+
import { BadgeContainer } from "../../../../stories/utils";
4+
import { Button } from "../../ui/button";
5+
import { GradientAvatar } from "./GradientAvatar";
6+
7+
const meta = {
8+
title: "blocks/Avatars/GradientAvatar",
9+
component: Story,
10+
parameters: {},
11+
} satisfies Meta<typeof Story>;
12+
13+
export default meta;
14+
type Story = StoryObj<typeof meta>;
15+
16+
export const Desktop: Story = {
17+
args: {},
18+
};
19+
20+
function Story() {
21+
return (
22+
<div className="flex flex-col gap-10 p-10">
23+
<p> All images below are set with size-6 className </p>
24+
25+
<BadgeContainer label="No Src, No id - Skeleton">
26+
<GradientAvatar id={undefined} src={undefined} className="size-6" />
27+
</BadgeContainer>
28+
29+
<BadgeContainer label="No Src, id=foo - Skeleton">
30+
<GradientAvatar id={"foo"} src={undefined} className="size-6" />
31+
</BadgeContainer>
32+
33+
<BadgeContainer label="Invalid/Empty Src, id=foo - Gradient">
34+
<GradientAvatar id={"foo"} src={""} className="size-6" />
35+
</BadgeContainer>
36+
37+
<BadgeContainer label="Invalid/Empty Src, id=bar - Gradient">
38+
<GradientAvatar id={"bar"} src={""} className="size-6" />
39+
</BadgeContainer>
40+
41+
<BadgeContainer label="Empty/Invalid Src, No id - default fallback">
42+
<GradientAvatar src="invalid-src" id={undefined} className="size-6" />
43+
</BadgeContainer>
44+
45+
<ToggleTest />
46+
</div>
47+
);
48+
}
49+
50+
function ToggleTest() {
51+
const [data, setData] = useState<undefined | { src: string; id: string }>(
52+
undefined,
53+
);
54+
55+
return (
56+
<div className="relative flex flex-col gap-10 border p-6">
57+
<Button
58+
variant="outline"
59+
onClick={() => {
60+
if (data) {
61+
setData(undefined);
62+
} else {
63+
setData({
64+
id: "foo",
65+
src: "https://picsum.photos/400",
66+
});
67+
}
68+
}}
69+
className="absolute top-6 right-6 inline-flex"
70+
>
71+
Toggle Src
72+
</Button>
73+
74+
<p> Src+ID is: {data ? "set" : "not set"} </p>
75+
76+
<BadgeContainer label="Valid Src">
77+
<GradientAvatar src={data?.src} id={data?.id} className="size-6" />
78+
</BadgeContainer>
79+
80+
<BadgeContainer label="invalid Src">
81+
<GradientAvatar
82+
className="size-6"
83+
src={data ? "invalid-src" : undefined}
84+
id={undefined}
85+
/>
86+
</BadgeContainer>
87+
</div>
88+
);
89+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Img } from "@/components/blocks/Img";
2+
import { useMemo } from "react";
3+
import { cn } from "../../../lib/utils";
4+
5+
const gradients = [
6+
["#fca5a5", "#b91c1c"],
7+
["#fdba74", "#c2410c"],
8+
["#fcd34d", "#b45309"],
9+
["#fde047", "#a16207"],
10+
["#a3e635", "#4d7c0f"],
11+
["#86efac", "#15803d"],
12+
["#67e8f9", "#0e7490"],
13+
["#7dd3fc", "#0369a1"],
14+
["#93c5fd", "#1d4ed8"],
15+
["#a5b4fc", "#4338ca"],
16+
["#c4b5fd", "#6d28d9"],
17+
["#d8b4fe", "#7e22ce"],
18+
["#f0abfc", "#a21caf"],
19+
["#f9a8d4", "#be185d"],
20+
["#fda4af", "#be123c"],
21+
];
22+
23+
function getGradientForString(str: string) {
24+
const number = Math.abs(
25+
str.split("").reduce((acc, b, i) => acc + b.charCodeAt(0) * (i + 1), 0),
26+
);
27+
const index = number % gradients.length;
28+
return gradients[index];
29+
}
30+
31+
export function GradientAvatar(props: {
32+
src: string | undefined;
33+
id: string | undefined;
34+
className: string;
35+
}) {
36+
const gradient = useMemo(() => {
37+
if (!props.id) {
38+
return undefined;
39+
}
40+
return getGradientForString(props.id);
41+
}, [props.id]);
42+
43+
return (
44+
<Img
45+
src={props.src}
46+
className={cn("rounded-full", props.className)}
47+
fallback={
48+
gradient ? (
49+
<div
50+
style={{
51+
background: `linear-gradient(45deg, ${gradient[0]} 0%, ${gradient[1]} 100%)`,
52+
}}
53+
/>
54+
) : undefined
55+
}
56+
/>
57+
);
58+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { useState } from "react";
3+
import { BadgeContainer } from "../../../../stories/utils";
4+
import { Button } from "../../ui/button";
5+
import { ProjectAvatar } from "./ProjectAvatar";
6+
7+
const meta = {
8+
title: "blocks/Avatars/ProjectAvatar",
9+
component: Story,
10+
parameters: {},
11+
} satisfies Meta<typeof Story>;
12+
13+
export default meta;
14+
type Story = StoryObj<typeof meta>;
15+
16+
export const Desktop: Story = {
17+
args: {},
18+
};
19+
20+
function Story() {
21+
return (
22+
<div className="flex flex-col gap-10 p-10">
23+
<p> All images below are set with size-6 className </p>
24+
25+
<BadgeContainer label="No Src - Skeleton">
26+
<ProjectAvatar src={undefined} className="size-6" />
27+
</BadgeContainer>
28+
29+
<BadgeContainer label="Invalid/Empty Src - BoxIcon Fallback">
30+
<ProjectAvatar src={""} className="size-6" />
31+
</BadgeContainer>
32+
33+
<ToggleTest />
34+
</div>
35+
);
36+
}
37+
38+
function ToggleTest() {
39+
const [data, setData] = useState<undefined | { src: string; name: string }>(
40+
undefined,
41+
);
42+
43+
return (
44+
<div className="relative flex flex-col gap-10 border p-6">
45+
<Button
46+
variant="outline"
47+
onClick={() => {
48+
if (data) {
49+
setData(undefined);
50+
} else {
51+
setData({
52+
name: "foo",
53+
src: "https://picsum.photos/400",
54+
});
55+
}
56+
}}
57+
className="absolute top-6 right-6 inline-flex"
58+
>
59+
Toggle Src
60+
</Button>
61+
62+
<p> Src+Name is: {data ? "set" : "not set"} </p>
63+
64+
<BadgeContainer label="Valid Src">
65+
<ProjectAvatar src={data?.src} className="size-6" />
66+
</BadgeContainer>
67+
68+
<BadgeContainer label="invalid Src">
69+
<ProjectAvatar
70+
src={data ? "invalid-src" : undefined}
71+
className="size-6"
72+
/>
73+
</BadgeContainer>
74+
</div>
75+
);
76+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Img } from "@/components/blocks/Img";
2+
import { BoxIcon } from "lucide-react";
3+
import { cn } from "../../../lib/utils";
4+
5+
export function ProjectAvatar(props: {
6+
src: string | undefined;
7+
className: string | undefined;
8+
}) {
9+
return (
10+
<Img
11+
src={props.src}
12+
className={cn("rounded-lg border", props.className)}
13+
alt={""}
14+
fallback={
15+
<div className="flex items-center justify-center bg-muted/50">
16+
<BoxIcon className="size-[50%] text-muted-foreground" />
17+
</div>
18+
}
19+
/>
20+
);
21+
}

apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export type Account = {
6767
paymentFailureCode: string;
6868
serviceCutoffDate: string;
6969
}[];
70+
// TODO - add image URL
7071
};
7172

7273
interface UpdateAccountInput {

apps/dashboard/src/app/account/components/AccountHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ export function AccountHeader(props: {
3535

3636
const headerProps: AccountHeaderCompProps = {
3737
teamsAndProjects: props.teamsAndProjects,
38-
email: myAccountQuery.data?.email,
3938
logout: logout,
4039
connectButton: <CustomConnectWallet />,
4140
createProject: () => setIsCreateProjectDialogOpen(true),
41+
account: myAccountQuery.data,
4242
};
4343

4444
return (

apps/dashboard/src/app/account/components/AccountHeaderUI.stories.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,26 @@ function Variants(props: {
4545
return (
4646
<ThirdwebProvider>
4747
<div className="flex min-h-screen flex-col gap-6 bg-gray-700 px-4 py-10">
48-
<BadgeContainer label="Logged in">
48+
<BadgeContainer label="Account Loaded">
4949
<Comp
5050
teamsAndProjects={teamsAndProjectsStub}
51-
5251
logout={() => {}}
5352
connectButton={<ConnectButtonStub />}
5453
createProject={() => {}}
54+
account={{
55+
id: "foo",
56+
57+
}}
58+
/>
59+
</BadgeContainer>
60+
61+
<BadgeContainer label="Account Loading">
62+
<Comp
63+
teamsAndProjects={teamsAndProjectsStub}
64+
logout={() => {}}
65+
connectButton={<ConnectButtonStub />}
66+
createProject={() => {}}
67+
account={undefined}
5568
/>
5669
</BadgeContainer>
5770
</div>

apps/dashboard/src/app/account/components/AccountHeaderUI.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { Project } from "@/api/projects";
22
import type { Team } from "@/api/team";
3+
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
34
import { cn } from "@/lib/utils";
5+
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
46
import Link from "next/link";
57
import { SecondaryNav } from "../../components/Header/SecondaryNav/SecondaryNav";
68
import { MobileBurgerMenuButton } from "../../components/MobileBurgerMenuButton";
@@ -10,11 +12,11 @@ import { TeamSelectorMobileMenuButton } from "../../team/components/TeamHeader/T
1012

1113
export type AccountHeaderCompProps = {
1214
className?: string;
13-
email: string | undefined;
1415
logout: () => void;
1516
connectButton: React.ReactNode;
1617
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
1718
createProject: () => void;
19+
account: Pick<Account, "email" | "id"> | undefined;
1820
};
1921

2022
export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
@@ -37,8 +39,12 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
3739
href="/account"
3840
className="flex flex-row items-center gap-2 font-normal text-sm"
3941
>
40-
{/* TODO - replace with account image */}
41-
<div className="size-6 rounded-full border border-border bg-muted" />
42+
{/* TODO - set account Image */}
43+
<GradientAvatar
44+
id={props.account?.id}
45+
src={props.account ? "" : undefined}
46+
className="size-6"
47+
/>
4248
<span> My Account </span>
4349
</Link>
4450

@@ -48,12 +54,13 @@ export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
4854
teamsAndProjects={props.teamsAndProjects}
4955
focus="team-selection"
5056
createProject={props.createProject}
57+
account={props.account}
5158
/>
5259
</div>
5360
</div>
5461

5562
<SecondaryNav
56-
email={props.email}
63+
account={props.account}
5764
logout={props.logout}
5865
connectButton={props.connectButton}
5966
/>
@@ -77,21 +84,26 @@ export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
7784
"flex flex-row items-center gap-2 font-normal text-foreground text-sm",
7885
)}
7986
>
80-
{/* TODO - replace with account image */}
81-
<div className="size-7 rounded-full border border-border bg-muted" />
87+
{/* TODO - set account image */}
88+
<GradientAvatar
89+
id={props.account?.id}
90+
src={props.account ? "" : undefined}
91+
className="size-6"
92+
/>
8293
<span> My Account </span>
8394
</Link>
8495

8596
<TeamSelectorMobileMenuButton
8697
currentTeam={undefined}
8798
teamsAndProjects={props.teamsAndProjects}
8899
upgradeTeamLink={undefined}
100+
account={props.account}
89101
/>
90102
</div>
91103
</div>
92104

93105
<MobileBurgerMenuButton
94-
email={props.email}
106+
email={props.account?.email}
95107
logout={props.logout}
96108
connectButton={props.connectButton}
97109
/>

apps/dashboard/src/app/account/overview/AccountTeamsUI.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import type { Team } from "@/api/team";
44
import type { TeamAccountRole } from "@/api/team-members";
5+
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
56
import { Button } from "@/components/ui/button";
67
import {
78
DropdownMenu,
@@ -102,8 +103,8 @@ function TeamRow(props: {
102103
<div className="flex items-center justify-between gap-2">
103104
{/* start */}
104105
<div className="flex items-center gap-4">
105-
{/* TODO - render team avatar */}
106-
<div className="size-8 rounded-full border bg-muted" />
106+
{/* TODO - set image */}
107+
<GradientAvatar className="size-8" src={""} id={props.team.id} />
107108

108109
<div>
109110
<div className="flex items-center gap-3">

0 commit comments

Comments
 (0)