@sproutsocial/seeds-react-profile 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,186 @@
1
+ import styled from "styled-components";
2
+ import { PartnerLogo } from "@sproutsocial/seeds-react-partner-logo";
3
+ import { Avatar } from "@sproutsocial/seeds-react-avatar";
4
+ import {
5
+ Card,
6
+ CardHeader,
7
+ CardContent,
8
+ CardFooter,
9
+ } from "@sproutsocial/seeds-react-card";
10
+ import { Icon } from "@sproutsocial/seeds-react-icon";
11
+ import { Link } from "@sproutsocial/seeds-react-link";
12
+ import { Text } from "@sproutsocial/seeds-react-text";
13
+ import type { TypeProfileCardProps } from "./types";
14
+ import { VerifiedProfileIcon } from "./VerifiedProfileIcon";
15
+
16
+ // Styled Components
17
+ const StyledProfileCard = styled(Card)`
18
+ max-width: 300px;
19
+ `;
20
+
21
+ const StyledCardHeader = styled(CardHeader)<{
22
+ $bannerColor?: string;
23
+ $partnerName?: string;
24
+ }>`
25
+ position: relative;
26
+ height: 80px;
27
+ padding: ${(props) => props.theme.space[400]}
28
+ ${(props) => props.theme.space[400]} 0 ${(props) => props.theme.space[400]};
29
+ margin-bottom: 0;
30
+ color: ${(props) => props.theme.colors.text.inverse};
31
+ border-radius: ${(props) => props.theme.radii[500]}
32
+ ${(props) => props.theme.radii[500]} 0 0;
33
+ ${(props) => {
34
+ // Use explicit bannerColor if provided
35
+ if (props.$bannerColor) {
36
+ return `background: ${props.$bannerColor};`;
37
+ }
38
+ // Fall back to theme.colors.network[partnerName] if partnerName is available
39
+ if (props.$partnerName && props.theme.colors.network) {
40
+ const networkColor =
41
+ props.theme.colors.network[
42
+ props.$partnerName as keyof typeof props.theme.colors.network
43
+ ];
44
+ return networkColor ? `background: ${networkColor};` : "";
45
+ }
46
+ return "";
47
+ }}
48
+ `;
49
+
50
+ const BannerContent = styled.div`
51
+ display: flex;
52
+ justify-content: flex-end;
53
+ width: 100%;
54
+ `;
55
+
56
+ const ActionsSection = styled.div`
57
+ display: flex;
58
+ justify-content: flex-end;
59
+ gap: ${(props) => props.theme.space[100]};
60
+ padding: 0 ${(props) => props.theme.space[300]};
61
+
62
+ &:empty {
63
+ min-height: 44px;
64
+ }
65
+ `;
66
+
67
+ const StyledCardContent = styled(CardContent)`
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: ${(props) => props.theme.space[300]};
71
+ padding: 0 ${(props) => props.theme.space[400]}
72
+ ${(props) => props.theme.space[400]};
73
+ `;
74
+
75
+ const ProfileNameSection = styled.div`
76
+ display: flex;
77
+ align-items: center;
78
+ gap: ${(props) => props.theme.space[200]};
79
+ `;
80
+
81
+ const InfoRow = styled.div`
82
+ display: flex;
83
+ align-items: center;
84
+ gap: ${(props) => props.theme.space[300]};
85
+ `;
86
+
87
+ /**
88
+ * A flexible profile component that can display user profiles in various formats
89
+ * across different social networks with consistent styling and behavior.
90
+ */
91
+ export const ProfileCard = ({
92
+ partnerName,
93
+ name,
94
+ secondaryName,
95
+ avatarUrl,
96
+ subtext,
97
+ description,
98
+ verificationType,
99
+ profileUrl,
100
+ location,
101
+ bannerColor,
102
+ bannerContent,
103
+ metadata,
104
+ profileActions,
105
+ footer,
106
+ cardProps,
107
+ }: TypeProfileCardProps) => {
108
+ return (
109
+ <StyledProfileCard role="presentation" {...cardProps}>
110
+ <StyledCardHeader $bannerColor={bannerColor} $partnerName={partnerName}>
111
+ <Avatar src={avatarUrl} name={name} size="60px" mt={450} />
112
+ {bannerContent && <BannerContent>{bannerContent}</BannerContent>}
113
+ </StyledCardHeader>
114
+
115
+ <ActionsSection>{profileActions}</ActionsSection>
116
+
117
+ <StyledCardContent>
118
+ <ProfileNameSection>
119
+ {partnerName && (
120
+ <PartnerLogo
121
+ partnerName={partnerName}
122
+ aria-label={`${partnerName} profile`}
123
+ size="small"
124
+ />
125
+ )}
126
+ <Text.SubHeadline as="h2">{name}</Text.SubHeadline>
127
+ </ProfileNameSection>
128
+
129
+ {subtext && (
130
+ <Text fontSize={200} color="text.subtext">
131
+ {subtext}
132
+ </Text>
133
+ )}
134
+
135
+ {secondaryName && (
136
+ <InfoRow>
137
+ {profileUrl ? (
138
+ <Link href={profileUrl} external>
139
+ <Text fontSize={200}>{secondaryName}</Text>
140
+ <Icon
141
+ name="arrow-right-up-outline"
142
+ aria-hidden
143
+ ml={100}
144
+ size="small"
145
+ />
146
+ </Link>
147
+ ) : (
148
+ <Text.SmallByline color="text.subtext">
149
+ {secondaryName}
150
+ </Text.SmallByline>
151
+ )}
152
+ {verificationType && (
153
+ <VerifiedProfileIcon
154
+ partnerName={partnerName}
155
+ verificationType={verificationType}
156
+ />
157
+ )}
158
+ </InfoRow>
159
+ )}
160
+
161
+ {description && <Text fontSize={200}>{description}</Text>}
162
+
163
+ {location && (
164
+ <InfoRow>
165
+ <Icon name="location-pin-outline" aria-hidden size="small" />
166
+ <Text fontSize={200}>{location}</Text>
167
+ </InfoRow>
168
+ )}
169
+
170
+ {metadata && metadata.length > 0 && (
171
+ <InfoRow>
172
+ {metadata.map((info, index) => (
173
+ <Text fontSize={200} key={index}>
174
+ {info}
175
+ </Text>
176
+ ))}
177
+ </InfoRow>
178
+ )}
179
+ </StyledCardContent>
180
+
181
+ {footer && <CardFooter>{footer}</CardFooter>}
182
+ </StyledProfileCard>
183
+ );
184
+ };
185
+
186
+ export default ProfileCard;
@@ -1,15 +1,9 @@
1
- import React from "react";
2
1
  import type { Meta, StoryObj } from "@storybook/react";
3
2
  import { ProfileToken } from "./ProfileToken";
4
- import { Icon } from "@sproutsocial/seeds-react-icon";
5
3
 
6
4
  const avatarUrl =
7
5
  "https://d672eyudr6aq1.cloudfront.net/avatar/cede1373e17c05542b1cc60f427067f2?s=30&amp;d=404";
8
6
 
9
- const VerificationBadge = () => (
10
- <Icon name="check-solid" style={{ color: "#1da1f2" }} />
11
- );
12
-
13
7
  const meta: Meta<typeof ProfileToken> = {
14
8
  title: "Components/Profile/ProfileToken",
15
9
  component: ProfileToken,
@@ -17,7 +11,25 @@ const meta: Meta<typeof ProfileToken> = {
17
11
  layout: "centered",
18
12
  },
19
13
  tags: ["autodocs"],
20
- argTypes: {},
14
+ argTypes: {
15
+ partnerName: {
16
+ control: { type: "select" },
17
+ options: [
18
+ "twitter",
19
+ "facebook",
20
+ "instagram",
21
+ "linkedin",
22
+ "youtube",
23
+ "tiktok",
24
+ "threads",
25
+ "bluesky",
26
+ ],
27
+ },
28
+ verificationType: {
29
+ control: { type: "select" },
30
+ options: ["verified", "blue_verified", "gray_verified", "not_verified"],
31
+ },
32
+ },
21
33
  };
22
34
 
23
35
  export default meta;
@@ -27,7 +39,7 @@ export const Default: Story = {
27
39
  args: {
28
40
  name: "John Doe",
29
41
  secondaryName: "@johndoe",
30
- img: avatarUrl,
42
+ avatarUrl: avatarUrl,
31
43
  partnerName: "twitter",
32
44
  },
33
45
  };
@@ -36,9 +48,9 @@ export const WithVerification: Story = {
36
48
  args: {
37
49
  name: "Elon Musk",
38
50
  secondaryName: "@elonmusk",
39
- img: avatarUrl,
51
+ avatarUrl: avatarUrl,
40
52
  partnerName: "twitter",
41
- children: <VerificationBadge />,
53
+ verificationType: "verified",
42
54
  },
43
55
  };
44
56
 
@@ -46,7 +58,7 @@ export const FacebookUser: Story = {
46
58
  args: {
47
59
  name: "Facebook User",
48
60
  secondaryName: "/facebookuser",
49
- img: avatarUrl,
61
+ avatarUrl: avatarUrl,
50
62
  partnerName: "facebook",
51
63
  },
52
64
  };
@@ -55,7 +67,7 @@ export const InstagramUser: Story = {
55
67
  args: {
56
68
  name: "Instagram User",
57
69
  secondaryName: "@instagramuser",
58
- img: avatarUrl,
70
+ avatarUrl: avatarUrl,
59
71
  partnerName: "instagram",
60
72
  },
61
73
  };
@@ -64,7 +76,7 @@ export const LinkedInUser: Story = {
64
76
  args: {
65
77
  name: "LinkedIn User",
66
78
  secondaryName: "linkedinuser",
67
- img: avatarUrl,
79
+ avatarUrl: avatarUrl,
68
80
  partnerName: "linkedin",
69
81
  },
70
82
  };
@@ -73,7 +85,7 @@ export const YouTubeUser: Story = {
73
85
  args: {
74
86
  name: "YouTube Creator",
75
87
  secondaryName: "@youtubecreator",
76
- img: avatarUrl,
88
+ avatarUrl: avatarUrl,
77
89
  partnerName: "youtube",
78
90
  },
79
91
  };
@@ -82,7 +94,7 @@ export const TikTokUser: Story = {
82
94
  args: {
83
95
  name: "TikTok Creator",
84
96
  secondaryName: "@tiktokcreator",
85
- img: avatarUrl,
97
+ avatarUrl: avatarUrl,
86
98
  partnerName: "tiktok",
87
99
  },
88
100
  };
@@ -91,7 +103,7 @@ export const ThreadsUser: Story = {
91
103
  args: {
92
104
  name: "Threads User",
93
105
  secondaryName: "@threadsuser",
94
- img: avatarUrl,
106
+ avatarUrl: avatarUrl,
95
107
  partnerName: "threads",
96
108
  },
97
109
  };
@@ -100,7 +112,7 @@ export const BlueskyUser: Story = {
100
112
  args: {
101
113
  name: "Bluesky User",
102
114
  secondaryName: "@blueskyuser",
103
- img: avatarUrl,
115
+ avatarUrl: avatarUrl,
104
116
  partnerName: "bluesky",
105
117
  },
106
118
  };
@@ -113,43 +125,53 @@ export const WithoutAvatar: Story = {
113
125
  },
114
126
  };
115
127
 
128
+ export const BlueVerifiedUser: Story = {
129
+ args: {
130
+ name: "Blue Verified User",
131
+ secondaryName: "@blueverified",
132
+ partnerName: "twitter",
133
+ avatarUrl: "https://via.placeholder.com/40",
134
+ verificationType: "blue_verified",
135
+ },
136
+ };
137
+
116
138
  export const DifferentNetworks: Story = {
117
139
  render: () => (
118
140
  <div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
119
141
  <ProfileToken
120
142
  name="Twitter User"
121
143
  secondaryName="@twitteruser"
122
- img={avatarUrl}
144
+ avatarUrl={avatarUrl}
123
145
  partnerName="twitter"
124
146
  />
125
147
  <ProfileToken
126
148
  name="Facebook User"
127
149
  secondaryName="/facebookuser"
128
- img={avatarUrl}
150
+ avatarUrl={avatarUrl}
129
151
  partnerName="facebook"
130
152
  />
131
153
  <ProfileToken
132
154
  name="Instagram User"
133
155
  secondaryName="@instagramuser"
134
- img={avatarUrl}
156
+ avatarUrl={avatarUrl}
135
157
  partnerName="instagram"
136
158
  />
137
159
  <ProfileToken
138
160
  name="LinkedIn User"
139
161
  secondaryName="linkedinuser"
140
- img={avatarUrl}
162
+ avatarUrl={avatarUrl}
141
163
  partnerName="linkedin"
142
164
  />
143
165
  <ProfileToken
144
166
  name="YouTube Creator"
145
167
  secondaryName="@youtubecreator"
146
- img={avatarUrl}
168
+ avatarUrl={avatarUrl}
147
169
  partnerName="youtube"
148
170
  />
149
171
  <ProfileToken
150
172
  name="TikTok Creator"
151
173
  secondaryName="@tiktokcreator"
152
- img={avatarUrl}
174
+ avatarUrl={avatarUrl}
153
175
  partnerName="tiktok"
154
176
  />
155
177
  </div>
@@ -162,41 +184,77 @@ export const TokenVariations: Story = {
162
184
  <ProfileToken
163
185
  name="Verified User"
164
186
  secondaryName="@verified"
165
- img={avatarUrl}
187
+ avatarUrl={avatarUrl}
166
188
  partnerName="twitter"
167
- >
168
- <VerificationBadge />
169
- </ProfileToken>
189
+ verificationType="verified"
190
+ />
170
191
  <ProfileToken
171
192
  name="Regular User"
172
193
  secondaryName="@regular"
173
- img={avatarUrl}
194
+ avatarUrl={avatarUrl}
174
195
  partnerName="twitter"
175
196
  />
176
197
  <ProfileToken
177
198
  name="Facebook User"
178
199
  secondaryName="/facebookuser"
179
- img={avatarUrl}
200
+ avatarUrl={avatarUrl}
180
201
  partnerName="facebook"
181
202
  />
182
203
  <ProfileToken
183
204
  name="Instagram User"
184
205
  secondaryName="@instagramuser"
185
- img={avatarUrl}
206
+ avatarUrl={avatarUrl}
186
207
  partnerName="instagram"
187
208
  />
188
209
  <ProfileToken
189
210
  name="LinkedIn User"
190
211
  secondaryName="linkedinuser"
191
- img={avatarUrl}
212
+ avatarUrl={avatarUrl}
192
213
  partnerName="linkedin"
193
214
  />
194
215
  <ProfileToken
195
216
  name="YouTube Creator"
196
217
  secondaryName="@youtubecreator"
197
- img={avatarUrl}
218
+ avatarUrl={avatarUrl}
198
219
  partnerName="youtube"
199
220
  />
200
221
  </div>
201
222
  ),
202
223
  };
224
+
225
+ export const LongNamesTruncation: Story = {
226
+ render: () => (
227
+ <div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
228
+ <div
229
+ style={{ maxWidth: "250px", border: "1px solid #ccc", padding: "8px" }}
230
+ >
231
+ <ProfileToken
232
+ name="A Very Long User Name That Should Truncate"
233
+ secondaryName="@verylonghandlethatshouldalsotruncate"
234
+ avatarUrl={avatarUrl}
235
+ partnerName="twitter"
236
+ />
237
+ </div>
238
+ <div
239
+ style={{ maxWidth: "180px", border: "1px solid #ccc", padding: "8px" }}
240
+ >
241
+ <ProfileToken
242
+ name="Another Long Name Token"
243
+ secondaryName="@anotherlonghandle"
244
+ avatarUrl={avatarUrl}
245
+ partnerName="facebook"
246
+ />
247
+ </div>
248
+ <div
249
+ style={{ maxWidth: "120px", border: "1px solid #ccc", padding: "8px" }}
250
+ >
251
+ <ProfileToken
252
+ name="Very Narrow Token"
253
+ secondaryName="@narrow"
254
+ avatarUrl={avatarUrl}
255
+ partnerName="instagram"
256
+ />
257
+ </div>
258
+ </div>
259
+ ),
260
+ };
@@ -1,20 +1,32 @@
1
- import React from "react";
1
+ import styled from "styled-components";
2
2
  import { Token } from "@sproutsocial/seeds-react-token";
3
3
  import { InlineProfile } from "./InlineProfile";
4
4
  import type { ProfileTokenProps } from "./types";
5
5
 
6
+ const StyledProfileToken = styled(Token)`
7
+ box-sizing: border-box;
8
+ display: inline-flex;
9
+ max-width: 100%;
10
+ `;
11
+
6
12
  /**
7
13
  * A ProfileToken component that wraps CompactProfile in a Seeds Token component.
8
14
  * This token is not closable and provides a compact way to display profile information inline.
9
15
  */
10
- export const ProfileToken: React.FC<ProfileTokenProps> = ({
11
- tokenProps,
16
+ export const ProfileToken = ({
12
17
  className,
18
+ onClick,
19
+ tokenProps,
13
20
  ...props
14
- }) => (
15
- <Token className={className} closeable={false} {...tokenProps}>
16
- <InlineProfile {...props} />
17
- </Token>
21
+ }: ProfileTokenProps) => (
22
+ <StyledProfileToken
23
+ className={className}
24
+ closeable={false}
25
+ onClick={onClick}
26
+ {...tokenProps}
27
+ >
28
+ <InlineProfile size="small" {...props} />
29
+ </StyledProfileToken>
18
30
  );
19
31
 
20
32
  export default ProfileToken;
@@ -0,0 +1,64 @@
1
+ import { Icon } from "@sproutsocial/seeds-react-icon";
2
+ import type { TypeProfileNetwork, TypeProfileVerification } from "./types";
3
+
4
+ // TODO: Move these to a more appropriate location
5
+ const VERIFICATION_COLORS = {
6
+ twitter: "#000000",
7
+ facebook: "#1877F2",
8
+ x: "#000000",
9
+ linkedin: "#0A66C2",
10
+ instagram: "#e4405f",
11
+ gray: "#515e5f",
12
+ } as const;
13
+
14
+ /**
15
+ * Gets the verification color based on partnerName and verification type
16
+ */
17
+ export const getVerificationColor = (
18
+ partnerName?: TypeProfileNetwork,
19
+ verificationType?: TypeProfileVerification
20
+ ): string => {
21
+ if (!verificationType || verificationType === "not_verified") return "";
22
+
23
+ switch (verificationType) {
24
+ case "blue_verified":
25
+ // Use partner-specific blue color
26
+ return partnerName === "facebook"
27
+ ? VERIFICATION_COLORS.facebook
28
+ : VERIFICATION_COLORS.twitter;
29
+ case "gray_verified":
30
+ return VERIFICATION_COLORS.gray;
31
+ case "verified":
32
+ default:
33
+ // Default to partner color or Twitter blue
34
+ if (partnerName && partnerName in VERIFICATION_COLORS) {
35
+ return VERIFICATION_COLORS[
36
+ partnerName as keyof typeof VERIFICATION_COLORS
37
+ ];
38
+ }
39
+ return VERIFICATION_COLORS.twitter;
40
+ }
41
+ };
42
+
43
+ export interface TypeVerifiedProfileIconProps {
44
+ partnerName?: TypeProfileNetwork;
45
+ verificationType: TypeProfileVerification;
46
+ }
47
+
48
+ export const VerifiedProfileIcon = ({
49
+ partnerName,
50
+ verificationType,
51
+ }: TypeVerifiedProfileIconProps) => {
52
+ if (!verificationType || verificationType === "not_verified") return;
53
+
54
+ const verificationColor = getVerificationColor(partnerName, verificationType);
55
+ return (
56
+ <Icon
57
+ title="Verified"
58
+ aria-label="Verified account"
59
+ color={verificationColor}
60
+ name="verified"
61
+ ml="150"
62
+ />
63
+ );
64
+ };
@@ -9,7 +9,7 @@ describe("InlineProfile", () => {
9
9
  name="John Doe"
10
10
  secondaryName="@johndoe"
11
11
  partnerName="twitter"
12
- img="https://example.com/avatar.jpg"
12
+ avatarUrl="https://example.com/avatar.jpg"
13
13
  />
14
14
  );
15
15
 
@@ -17,21 +17,6 @@ describe("InlineProfile", () => {
17
17
  expect(screen.getByText("@johndoe")).toBeInTheDocument();
18
18
  });
19
19
 
20
- it("renders with children", () => {
21
- render(
22
- <InlineProfile
23
- name="John Doe"
24
- secondaryName="@johndoe"
25
- partnerName="twitter"
26
- img="https://example.com/avatar.jpg"
27
- >
28
- Children
29
- </InlineProfile>
30
- );
31
-
32
- expect(screen.getByText("Children")).toBeInTheDocument();
33
- });
34
-
35
20
  it("renders network logo", () => {
36
21
  render(
37
22
  <InlineProfile
@@ -39,7 +24,7 @@ describe("InlineProfile", () => {
39
24
  secondaryName="@johndoe"
40
25
  partnerName="twitter"
41
26
  partnerLogoLabel="Twitter Logo"
42
- img="https://example.com/avatar.jpg"
27
+ avatarUrl="https://example.com/avatar.jpg"
43
28
  />
44
29
  );
45
30
 
@@ -52,7 +37,7 @@ describe("InlineProfile", () => {
52
37
  <InlineProfile
53
38
  name="John Doe"
54
39
  secondaryName="@johndoe"
55
- img="https://example.com/avatar.jpg"
40
+ avatarUrl="https://example.com/avatar.jpg"
56
41
  />
57
42
  );
58
43
 
@@ -66,7 +51,7 @@ describe("InlineProfile", () => {
66
51
  <InlineProfile
67
52
  name="John Doe"
68
53
  partnerName="twitter"
69
- img="https://example.com/avatar.jpg"
54
+ avatarUrl="https://example.com/avatar.jpg"
70
55
  />
71
56
  );
72
57
 
@@ -74,10 +59,24 @@ describe("InlineProfile", () => {
74
59
  expect(screen.queryByText(/@/)).not.toBeInTheDocument();
75
60
  });
76
61
 
77
- it("renders avatar with name fallback when img prop is not provided", () => {
62
+ it("renders avatar with name fallback when avatarUrl prop is not provided", () => {
78
63
  render(<InlineProfile name="John Doe" />);
79
64
 
80
65
  const avatar = screen.getByText("JD");
81
66
  expect(avatar).toBeInTheDocument();
82
67
  });
68
+
69
+ it("renders with verification badge when verified", () => {
70
+ render(
71
+ <InlineProfile
72
+ name="John Doe"
73
+ secondaryName="@johndoe"
74
+ partnerName="twitter"
75
+ avatarUrl="https://example.com/avatar.jpg"
76
+ verificationType="verified"
77
+ />
78
+ );
79
+
80
+ expect(screen.getByLabelText("Verified account")).toBeInTheDocument();
81
+ });
83
82
  });