diff --git a/apps/site/components/Common/AvatarGroup/Avatar/index.module.css b/apps/site/components/Common/AvatarGroup/Avatar/index.module.css index 616144b872ba2..3e7bdb6bb98ef 100644 --- a/apps/site/components/Common/AvatarGroup/Avatar/index.module.css +++ b/apps/site/components/Common/AvatarGroup/Avatar/index.module.css @@ -1,6 +1,11 @@ -.avatar { - @apply flex - size-8 +.item { + @apply xs:max-h-10 + xs:max-w-10 + flex + h-full + max-h-12 + w-full + max-w-12 items-center justify-center rounded-full @@ -15,9 +20,26 @@ dark:text-neutral-300; } -.avatarRoot { - @apply -ml-2 - size-8 - flex-shrink-0 +.avatar { + @apply size-8 + flex-shrink-0; + + .wrapper { + @apply max-xs:block + max-xs:py-0; + } +} + +.small { + @apply xs:size-8 + xs:-ml-2 + ml-0.5 + size-10 + first:ml-0; +} + +.medium { + @apply -ml-2.5 + size-10 first:ml-0; } 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..c992fab126f5c 100644 --- a/apps/site/components/Common/AvatarGroup/Avatar/index.tsx +++ b/apps/site/components/Common/AvatarGroup/Avatar/index.tsx @@ -1,27 +1,53 @@ 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 Link from '@/components/Link'; 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'; + url?: string; }; -const Avatar: FC = ({ src, alt, fallback }) => ( - - - - {fallback} - - -); +const Avatar = forwardRef< + ElementRef, + ComponentPropsWithoutRef & AvatarProps +>(({ image, nickname, name, fallback, url, size = 'small', ...props }, ref) => { + const Wrapper = url ? Link : 'div'; + + return ( + + + + + {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..7c8188658b62c --- /dev/null +++ b/apps/site/components/Common/AvatarGroup/Overlay/index.module.css @@ -0,0 +1,29 @@ +.overlay { + @apply flex + min-w-56 + items-center + 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.stories.tsx b/apps/site/components/Common/AvatarGroup/Overlay/index.stories.tsx new file mode 100644 index 0000000000000..1c462999ccd77 --- /dev/null +++ b/apps/site/components/Common/AvatarGroup/Overlay/index.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +import AvatarOverlay from '@/components/Common/AvatarGroup/Overlay'; +import { getAuthorWithId, getAuthorWithName } from '@/util/authorUtils'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: getAuthorWithId(['nodejs'], true)[0], +}; + +export const FallBack: Story = { + args: getAuthorWithName(['Node.js'], true)[0], +}; + +export const WithoutName: Story = { + args: getAuthorWithId(['canerakdas'], true)[0], +}; + +export default { component: AvatarOverlay } as Meta; 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..a622c9ebe1c58 --- /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 & { + url?: string; +}; + +const AvatarOverlay: FC = ({ + image, + name, + nickname, + fallback, + url, +}) => ( + + +
+ {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..0065bf2f3f009 100644 --- a/apps/site/components/Common/AvatarGroup/index.stories.tsx +++ b/apps/site/components/Common/AvatarGroup/index.stories.tsx @@ -1,7 +1,7 @@ import type { Meta as MetaObj, StoryObj } from '@storybook/react'; import AvatarGroup from '@/components/Common/AvatarGroup'; -import { getGitHubAvatarUrl } from '@/util/gitHubUtils'; +import { getAuthorWithId } from '@/util/authorUtils'; type Story = StoryObj; type Meta = MetaObj; @@ -24,15 +24,13 @@ const names = [ ]; const unknownAvatar = { - src: 'https://avatars.githubusercontent.com/u/', - alt: 'unknown-avatar', + image: 'https://avatars.githubusercontent.com/u/', + nickname: 'unknown-avatar', + fallback: 'UA', }; const defaultProps = { - avatars: [ - unknownAvatar, - ...names.map(name => ({ src: getGitHubAvatarUrl(name), alt: name })), - ], + avatars: [unknownAvatar, ...getAuthorWithId(names, true)], }; export const Default: Story = { diff --git a/apps/site/components/Common/AvatarGroup/index.tsx b/apps/site/components/Common/AvatarGroup/index.tsx index 4dea98b700d5d..87f8765f62a65 100644 --- a/apps/site/components/Common/AvatarGroup/index.tsx +++ b/apps/site/components/Common/AvatarGroup/index.tsx @@ -1,25 +1,31 @@ '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'>>; + avatars: Array; limit?: number; isExpandable?: boolean; + size?: AvatarProps['size']; + container?: HTMLElement; }; const AvatarGroup: FC = ({ avatars, limit = 10, isExpandable = true, + size = 'small', + container, }) => { const [showMore, setShowMore] = useState(false); @@ -30,21 +36,34 @@ const AvatarGroup: FC = ({ return (
- {renderAvatars.map((avatar, index) => ( - + {renderAvatars.map(({ ...avatar }) => ( + + } + > + + + ))} - {avatars.length > limit && ( setShowMore(prev => !prev) : undefined} - className={classNames(avatarstyles.avatarRoot, 'cursor-pointer')} + className={classNames( + avatarstyles.avatar, + avatarstyles[size], + '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.module.css b/apps/site/components/Common/BlogPostCard/index.module.css index a2c84c21e5835..178d193cda6e7 100644 --- a/apps/site/components/Common/BlogPostCard/index.module.css +++ b/apps/site/components/Common/BlogPostCard/index.module.css @@ -33,6 +33,10 @@ .footer { @apply flex gap-x-3; + + div:first-child { + @apply overflow-visible; + } } .author { diff --git a/apps/site/components/Common/BlogPostCard/index.stories.tsx b/apps/site/components/Common/BlogPostCard/index.stories.tsx index 0311c67453386..dca5f00ccaa1c 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: ['Claudio Wunder'], 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 ?? []), 'Brian Muenzenmeyer'], }, }; diff --git a/apps/site/components/Common/BlogPostCard/index.tsx b/apps/site/components/Common/BlogPostCard/index.tsx index 2065f0b20d5d2..4f8696dd20565 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}

}