Skip to content

Commit b60d697

Browse files
authored
feat: new Badges and Awards widget (#4986)
1 parent 9d96a05 commit b60d697

5 files changed

Lines changed: 191 additions & 209 deletions

File tree

packages/shared/src/components/profile/ActivitySection.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@ import type { FeedData } from '../../graphql/feed';
88
import type { Post } from '../../graphql/posts';
99
import type { WithClassNameProps } from '../utilities';
1010

11-
export const ActivityContainer = classed('section', 'flex flex-col');
11+
export const ActivityContainer = classed(
12+
'section',
13+
'flex flex-col border border-border-subtlest-tertiary rounded-16 p-4 laptop:max-w-full max-w-[18.75rem]',
14+
);
1215

1316
export const ActivitySectionTitle = classed(
1417
'h2',
15-
'flex items-center mb-4 text-text-primary font-bold typo-body',
18+
'flex items-center text-text-primary font-bold typo-callout',
1619
);
1720

1821
export const ActivitySectionSubTitle = classed(
19-
'span',
20-
'mt-1 text-text-tertiary typo-callout font-normal',
22+
'a',
23+
'mt-1 text-text-link typo-caption2 font-normal',
2124
);
2225

2326
export const ActivitySectionTitleStat = classed(

packages/shared/src/components/profile/Awards.tsx

Lines changed: 0 additions & 75 deletions
This file was deleted.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import type { ReactElement } from 'react';
2+
import React from 'react';
3+
import classNames from 'classnames';
4+
import { useQuery } from '@tanstack/react-query';
5+
import { ActivityContainer, ActivitySectionTitle } from './ActivitySection';
6+
import { topReaderBadgeDocs } from '../../lib/constants';
7+
import {
8+
Typography,
9+
TypographyColor,
10+
TypographyTag,
11+
TypographyType,
12+
} from '../typography/Typography';
13+
import { formatDate, TimeFormatType } from '../../lib/dateFormat';
14+
import type { PublicProfile } from '../../lib/user';
15+
import { ClickableText } from '../buttons/ClickableText';
16+
import { Image } from '../image/Image';
17+
import { useTopReader } from '../../hooks/useTopReader';
18+
import Link from '../utilities/Link';
19+
import { getTagPageLink } from '../../lib';
20+
import { truncateTextClassNames } from '../utilities';
21+
import {
22+
userProductSummaryQueryOptions,
23+
ProductType,
24+
} from '../../graphql/njord';
25+
import { useHasAccessToCores } from '../../hooks/useCoresFeature';
26+
27+
type AwardProps = {
28+
image: string;
29+
amount: number;
30+
};
31+
32+
const Award = ({ image, amount }: AwardProps): ReactElement => {
33+
return (
34+
<div className="flex flex-col items-center justify-center">
35+
<Image src={image} alt="Award" className="size-8" />
36+
<Typography
37+
type={TypographyType.Caption2}
38+
color={TypographyColor.Primary}
39+
className="mt-1"
40+
>
41+
x{amount}
42+
</Typography>
43+
</div>
44+
);
45+
};
46+
47+
type SummaryCardProps = {
48+
count: number;
49+
label: string;
50+
};
51+
52+
const SummaryCard = ({ count, label }: SummaryCardProps): ReactElement => {
53+
return (
54+
<div className="flex-1 rounded-10 border border-border-subtlest-tertiary p-2 text-center">
55+
<Typography
56+
type={TypographyType.Body}
57+
color={TypographyColor.Primary}
58+
bold
59+
>
60+
x{count}
61+
</Typography>
62+
<Typography
63+
type={TypographyType.Footnote}
64+
color={TypographyColor.Tertiary}
65+
>
66+
{label}
67+
</Typography>
68+
</div>
69+
);
70+
};
71+
72+
type KeywordBadgeProps = {
73+
badge: {
74+
id: string;
75+
keyword: {
76+
value: string;
77+
flags?: { title?: string };
78+
};
79+
issuedAt: string | Date;
80+
};
81+
};
82+
83+
const KeywordBadge = ({ badge }: KeywordBadgeProps): ReactElement => {
84+
return (
85+
<div className="flex items-center justify-between">
86+
<Link
87+
href={getTagPageLink(badge.keyword.value)}
88+
passHref
89+
prefetch={false}
90+
>
91+
<Typography
92+
tag={TypographyTag.Link}
93+
type={TypographyType.Caption1}
94+
color={TypographyColor.Primary}
95+
className={classNames(
96+
'rounded-6 border border-border-subtlest-tertiary px-1 py-0.5 lowercase transition duration-200 hover:bg-background-popover',
97+
truncateTextClassNames,
98+
)}
99+
>
100+
{badge.keyword.flags?.title || badge.keyword.value}
101+
</Typography>
102+
</Link>
103+
<Typography
104+
tag={TypographyTag.Time}
105+
type={TypographyType.Caption2}
106+
color={TypographyColor.Quaternary}
107+
dateTime={new Date(badge.issuedAt).toISOString()}
108+
className="text-text-quaternary"
109+
>
110+
{formatDate({
111+
value: badge.issuedAt,
112+
type: TimeFormatType.TopReaderBadge,
113+
})}
114+
</Typography>
115+
</div>
116+
);
117+
};
118+
119+
export const BadgesAndAwards = ({
120+
user,
121+
}: {
122+
user: PublicProfile;
123+
}): ReactElement => {
124+
const { data: topReaders, isPending: isTopReaderLoading } = useTopReader({
125+
user,
126+
limit: 5,
127+
});
128+
129+
const hasCoresAccess = useHasAccessToCores();
130+
131+
const { data: awards, isPending: isAwardsLoading } = useQuery({
132+
...userProductSummaryQueryOptions({
133+
userId: user?.id,
134+
type: ProductType.Award,
135+
}),
136+
enabled: !!user?.id && hasCoresAccess,
137+
});
138+
139+
if (isTopReaderLoading || isAwardsLoading) {
140+
return null;
141+
}
142+
143+
if (!topReaders?.length && !awards?.length) {
144+
return null;
145+
}
146+
147+
const totalTopReaderBadges =
148+
topReaders?.reduce((sum, topReader) => sum + topReader.total, 0) ?? 0;
149+
const totalAwards = awards?.reduce((sum, award) => sum + award.count, 0) ?? 0;
150+
151+
return (
152+
<ActivityContainer>
153+
<ActivitySectionTitle>Badges & Awards</ActivitySectionTitle>
154+
<ClickableText tag="a" target="_blank" href={topReaderBadgeDocs}>
155+
Learn more
156+
</ClickableText>
157+
158+
<div className="my-3 flex gap-3">
159+
<SummaryCard count={totalTopReaderBadges} label="Top reader badge" />
160+
<SummaryCard count={totalAwards} label="Total Awards" />
161+
</div>
162+
163+
{topReaders && topReaders.length > 0 && (
164+
<div className="flex flex-col gap-2">
165+
{topReaders.map((badge) => (
166+
<KeywordBadge key={`badge-${badge.id}`} badge={badge} />
167+
))}
168+
</div>
169+
)}
170+
171+
{hasCoresAccess && awards && awards.length > 0 && (
172+
<div className="mt-4 grid grid-cols-5 gap-4 laptop:grid-cols-6">
173+
{awards.map((award) => (
174+
<Award key={award.id} image={award.image} amount={award.count} />
175+
))}
176+
</div>
177+
)}
178+
</ActivityContainer>
179+
);
180+
};

0 commit comments

Comments
 (0)