From 1024e8aeb70db35768afc14f258a0a9f10611c0e Mon Sep 17 00:00:00 2001 From: Caner Akdas Date: Fri, 25 Oct 2024 23:49:22 +0300 Subject: [PATCH 01/19] feat: tooltip component and avatar tooltip --- .../AvatarGroup/Avatar/index.module.css | 19 ++- .../AvatarGroup/Avatar/index.stories.tsx | 13 +- .../Common/AvatarGroup/Avatar/index.tsx | 35 +++-- .../AvatarGroup/Overlay/index.module.css | 28 ++++ .../Common/AvatarGroup/Overlay/index.tsx | 36 +++++ .../AvatarGroup/__tests__/index.test.mjs | 4 +- .../Common/AvatarGroup/index.stories.tsx | 21 ++- .../components/Common/AvatarGroup/index.tsx | 37 ++++-- .../BlogPostCard/__tests__/index.test.mjs | 18 +-- .../Common/BlogPostCard/index.stories.tsx | 15 +-- .../components/Common/BlogPostCard/index.tsx | 13 +- .../Common/Tooltip/index.module.css | 42 ++++++ .../Common/Tooltip/index.stories.tsx | 74 +++++++++++ apps/site/components/Common/Tooltip/index.tsx | 43 ++++++ .../ChangelogModal/index.stories.tsx | 6 +- .../Downloads/ChangelogModal/index.tsx | 8 +- apps/site/components/withAvatarGroup.tsx | 35 +++++ apps/site/components/withBlogCategories.tsx | 2 +- apps/site/components/withChangelogModal.tsx | 3 +- apps/site/components/withMetaBar.tsx | 21 +-- apps/site/layouts/Post.tsx | 19 +-- apps/site/package.json | 3 +- apps/site/util/__tests__/authorUtils.test.mjs | 119 +++++++++++++++++ apps/site/util/__tests__/blogUtils.test.mjs | 82 ------------ apps/site/util/authorUtils.ts | 63 +++++++++ apps/site/util/blogUtils.ts | 18 --- package-lock.json | 125 ++++++++++++++++++ 27 files changed, 694 insertions(+), 208 deletions(-) create mode 100644 apps/site/components/Common/AvatarGroup/Overlay/index.module.css create mode 100644 apps/site/components/Common/AvatarGroup/Overlay/index.tsx create mode 100644 apps/site/components/Common/Tooltip/index.module.css create mode 100644 apps/site/components/Common/Tooltip/index.stories.tsx create mode 100644 apps/site/components/Common/Tooltip/index.tsx create mode 100644 apps/site/components/withAvatarGroup.tsx create mode 100644 apps/site/util/__tests__/authorUtils.test.mjs delete mode 100644 apps/site/util/__tests__/blogUtils.test.mjs create mode 100644 apps/site/util/authorUtils.ts diff --git a/apps/site/components/Common/AvatarGroup/Avatar/index.module.css b/apps/site/components/Common/AvatarGroup/Avatar/index.module.css index 616144b872ba2..a4ed07ca0d6ba 100644 --- a/apps/site/components/Common/AvatarGroup/Avatar/index.module.css +++ b/apps/site/components/Common/AvatarGroup/Avatar/index.module.css @@ -1,6 +1,9 @@ .avatar { @apply flex - size-8 + h-full + max-h-10 + w-full + max-w-10 items-center justify-center rounded-full @@ -15,9 +18,21 @@ dark:text-neutral-300; } -.avatarRoot { +.container { @apply -ml-2 size-8 flex-shrink-0 first:ml-0; + + &:hover { + @apply z-10; + } +} + +.small { + @apply size-8; +} + +.medium { + @apply size-10; } diff --git a/apps/site/components/Common/AvatarGroup/Avatar/index.stories.tsx b/apps/site/components/Common/AvatarGroup/Avatar/index.stories.tsx index 7f3a6430d87b8..8f72650102c0f 100644 --- a/apps/site/components/Common/AvatarGroup/Avatar/index.stories.tsx +++ b/apps/site/components/Common/AvatarGroup/Avatar/index.stories.tsx @@ -8,22 +8,23 @@ type Meta = MetaObj; export const Default: Story = { args: { - src: getGitHubAvatarUrl('ovflowd'), - alt: 'ovflowd', + image: getGitHubAvatarUrl('ovflowd'), + nickname: 'ovflowd', }, }; export const NoSquare: Story = { args: { - src: '/static/images/logos/nodejs.png', - alt: 'SD', + image: '/static/images/logo-hexagon-card.png', + nickname: 'SD', }, }; export const FallBack: Story = { args: { - src: 'https://avatars.githubusercontent.com/u/', - alt: 'UA', + image: 'https://avatars.githubusercontent.com/u/', + nickname: 'John Doe', + fallback: 'JD', }, }; diff --git a/apps/site/components/Common/AvatarGroup/Avatar/index.tsx b/apps/site/components/Common/AvatarGroup/Avatar/index.tsx index 4837f9b40b585..86b071c450a8d 100644 --- a/apps/site/components/Common/AvatarGroup/Avatar/index.tsx +++ b/apps/site/components/Common/AvatarGroup/Avatar/index.tsx @@ -1,27 +1,40 @@ import * as RadixAvatar from '@radix-ui/react-avatar'; -import type { FC } from 'react'; +import classNames from 'classnames'; +import type { ComponentPropsWithoutRef, ElementRef } from 'react'; +import { forwardRef } from 'react'; import styles from './index.module.css'; export type AvatarProps = { - src: string; - alt: string; - fallback: string; + image?: string; + name?: string; + nickname: string; + fallback?: string; + size?: 'small' | 'medium'; }; -const Avatar: FC = ({ src, alt, fallback }) => ( - +const Avatar = forwardRef< + ElementRef, + ComponentPropsWithoutRef & AvatarProps +>(({ image, name, fallback, size = 'small', ...props }, ref) => ( + - + {fallback} -); +)); export default Avatar; diff --git a/apps/site/components/Common/AvatarGroup/Overlay/index.module.css b/apps/site/components/Common/AvatarGroup/Overlay/index.module.css new file mode 100644 index 0000000000000..8f247bf1b6ec2 --- /dev/null +++ b/apps/site/components/Common/AvatarGroup/Overlay/index.module.css @@ -0,0 +1,28 @@ +.container { + @apply flex + min-w-56 + gap-2 + p-3; +} + +.user { + @apply grow; +} + +.name { + @apply font-semibold + text-neutral-900 + dark:text-neutral-300; +} + +.nickname { + @apply font-medium + text-neutral-700 + dark:text-neutral-500; +} + +.arrow { + @apply w-3 + fill-neutral-600 + dark:fill-white; +} diff --git a/apps/site/components/Common/AvatarGroup/Overlay/index.tsx b/apps/site/components/Common/AvatarGroup/Overlay/index.tsx new file mode 100644 index 0000000000000..54a44ace8dd44 --- /dev/null +++ b/apps/site/components/Common/AvatarGroup/Overlay/index.tsx @@ -0,0 +1,36 @@ +import { ArrowUpRightIcon } from '@heroicons/react/24/solid'; +import type { ComponentProps, FC } from 'react'; + +import Avatar from '@/components/Common/AvatarGroup/Avatar'; +import Link from '@/components/Link'; + +import styles from './index.module.css'; + +export type AvatarOverlayProps = ComponentProps & { + website?: string; +}; + +const AvatarOverlay: FC = ({ + image, + name, + nickname, + fallback, + website, +}) => ( + + +
+ {name &&
{name}
} + {nickname &&
{nickname}
} +
+ + +); + +export default AvatarOverlay; diff --git a/apps/site/components/Common/AvatarGroup/__tests__/index.test.mjs b/apps/site/components/Common/AvatarGroup/__tests__/index.test.mjs index f04c7c1dbeb26..3ba01e856f083 100644 --- a/apps/site/components/Common/AvatarGroup/__tests__/index.test.mjs +++ b/apps/site/components/Common/AvatarGroup/__tests__/index.test.mjs @@ -22,8 +22,8 @@ const names = [ ]; const avatars = names.map(name => ({ - src: getGitHubAvatarUrl(name), - alt: name, + image: getGitHubAvatarUrl(name), + nickname: name, })); describe('AvatarGroup', () => { diff --git a/apps/site/components/Common/AvatarGroup/index.stories.tsx b/apps/site/components/Common/AvatarGroup/index.stories.tsx index 6aef57bb904ed..f421febeab56c 100644 --- a/apps/site/components/Common/AvatarGroup/index.stories.tsx +++ b/apps/site/components/Common/AvatarGroup/index.stories.tsx @@ -24,14 +24,25 @@ const names = [ ]; const unknownAvatar = { - src: 'https://avatars.githubusercontent.com/u/', - alt: 'unknown-avatar', + image: 'https://avatars.githubusercontent.com/u/', + nickname: 'unknown-avatar', }; const defaultProps = { avatars: [ unknownAvatar, - ...names.map(name => ({ src: getGitHubAvatarUrl(name), alt: name })), + ...names.map(name => ({ image: getGitHubAvatarUrl(name), nickname: name })), + ], +}; + +const avatarOverlay = { + avatars: [ + { + image: getGitHubAvatarUrl('nodejs'), + name: 'Node.js', + nickname: 'nodejs', + website: 'https://nodejs.org', + }, ], }; @@ -46,6 +57,10 @@ export const WithCustomLimit: Story = { }, }; +export const WithOverlay: Story = { + args: avatarOverlay, +}; + export const InSmallContainer: Story = { decorators: [ Story => ( diff --git a/apps/site/components/Common/AvatarGroup/index.tsx b/apps/site/components/Common/AvatarGroup/index.tsx index 4dea98b700d5d..82696535e5ca6 100644 --- a/apps/site/components/Common/AvatarGroup/index.tsx +++ b/apps/site/components/Common/AvatarGroup/index.tsx @@ -1,25 +1,29 @@ 'use client'; import classNames from 'classnames'; -import type { ComponentProps, FC } from 'react'; -import { useState, useMemo } from 'react'; +import type { FC } from 'react'; +import { useState, useMemo, Fragment } from 'react'; +import type { AvatarProps } from '@/components/Common/AvatarGroup/Avatar'; import Avatar from '@/components/Common/AvatarGroup/Avatar'; import avatarstyles from '@/components/Common/AvatarGroup/Avatar/index.module.css'; -import { getAcronymFromString } from '@/util/stringUtils'; +import AvatarOverlay from '@/components/Common/AvatarGroup/Overlay'; +import Tooltip from '@/components/Common/Tooltip'; import styles from './index.module.css'; -type AvatarGroupProps = { - avatars: Array, 'fallback'>>; +export type AvatarGroupProps = { + avatars: Array; limit?: number; isExpandable?: boolean; + size?: AvatarProps['size']; }; const AvatarGroup: FC = ({ avatars, limit = 10, isExpandable = true, + size = 'small', }) => { const [showMore, setShowMore] = useState(false); @@ -30,19 +34,24 @@ const AvatarGroup: FC = ({ return (
- {renderAvatars.map((avatar, index) => ( - + {renderAvatars.map(({ website, ...avatar }) => ( + + {website ? ( + } + asChild + > + + + ) : ( + + )} + ))} - {avatars.length > limit && ( setShowMore(prev => !prev) : undefined} - className={classNames(avatarstyles.avatarRoot, 'cursor-pointer')} + className={classNames(avatarstyles.container, 'cursor-pointer')} > {`${showMore ? '-' : '+'}${avatars.length - limit}`} diff --git a/apps/site/components/Common/BlogPostCard/__tests__/index.test.mjs b/apps/site/components/Common/BlogPostCard/__tests__/index.test.mjs index f6a45912ae3a7..e4752d4b09dc9 100644 --- a/apps/site/components/Common/BlogPostCard/__tests__/index.test.mjs +++ b/apps/site/components/Common/BlogPostCard/__tests__/index.test.mjs @@ -65,38 +65,32 @@ describe('BlogPostCard', () => { ); it('Renders all passed authors fullName(s), comma-separated', () => { - const authors = [ - { fullName: 'Jane Doe', src: '' }, - { fullName: 'John Doe', src: '' }, - ]; + const authors = ['Jane Doe', 'John Doe']; renderBlogPostCard({ authors }); const fullNames = authors.reduce((prev, curr, index) => { if (index === 0) { - return curr.fullName; + return curr; } - return `${prev}, ${curr.fullName}`; + return `${prev}, ${curr}`; }, ''); expect(screen.getByText(fullNames)).toBeVisible(); }); it('Renders all passed authors fullName(s), comma-separated', () => { - const authors = [ - { fullName: 'Jane Doe', src: '' }, - { fullName: 'John Doe', src: '' }, - ]; + const authors = ['Jane Doe', 'John Doe']; renderBlogPostCard({ authors }); const fullNames = authors.reduce((prev, curr, index) => { if (index === 0) { - return curr.fullName; + return curr; } - return `${prev}, ${curr.fullName}`; + return `${prev}, ${curr}`; }, ''); expect(screen.getByText(fullNames)).toBeVisible(); diff --git a/apps/site/components/Common/BlogPostCard/index.stories.tsx b/apps/site/components/Common/BlogPostCard/index.stories.tsx index 0311c67453386..eae501859a985 100644 --- a/apps/site/components/Common/BlogPostCard/index.stories.tsx +++ b/apps/site/components/Common/BlogPostCard/index.stories.tsx @@ -11,12 +11,7 @@ export const Default: Story = { category: 'vulnerability', description: 'Starting on March 15th and going through to March 17th (with much of the issue being mitigated on the 16th), users were receiving intermittent 404 responses when trying to download Node.js from nodejs.org, or even accessing parts of the website.', - authors: [ - { - fullName: 'Hayden Bleasel', - src: 'https://avatars.githubusercontent.com/u/', - }, - ], + authors: ['Hayden Bleasel'], slug: '/blog/vulnerability/something', date: new Date('17 October 2023'), }, @@ -33,13 +28,7 @@ export const MoreThanOneAuthor: Story = { ...Default, args: { ...Default.args, - authors: [ - ...(Default.args?.authors ?? []), - { - fullName: 'Jane Doe', - src: 'https://avatars.githubusercontent.com/u/', - }, - ], + authors: [...(Default.args?.authors ?? []), 'Jane Doe'], }, }; diff --git a/apps/site/components/Common/BlogPostCard/index.tsx b/apps/site/components/Common/BlogPostCard/index.tsx index 2065f0b20d5d2..7adaba23d4327 100644 --- a/apps/site/components/Common/BlogPostCard/index.tsx +++ b/apps/site/components/Common/BlogPostCard/index.tsx @@ -1,22 +1,19 @@ import { useTranslations } from 'next-intl'; import type { FC } from 'react'; -import AvatarGroup from '@/components/Common/AvatarGroup'; import FormattedTime from '@/components/Common/FormattedTime'; import Preview from '@/components/Common/Preview'; import Link from '@/components/Link'; +import WithAvatarGroup from '@/components/withAvatarGroup'; import { mapBlogCategoryToPreviewType } from '@/util/blogUtils'; import styles from './index.module.css'; -// @todo: this should probably be a global type? -type Author = { fullName: string; src: string }; - type BlogPostCardProps = { title: string; category: string; description?: string; - authors?: Array; + authors?: Array; date?: Date; slug?: string; }; @@ -31,8 +28,6 @@ const BlogPostCard: FC = ({ }) => { const t = useTranslations(); - const avatars = authors.map(({ fullName, src }) => ({ alt: fullName, src })); - const type = mapBlogCategoryToPreviewType(category); return ( @@ -52,10 +47,10 @@ const BlogPostCard: FC = ({ {description &&

{description}

}