UserPicture.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import {
  2. forwardRef,
  3. type JSX,
  4. memo,
  5. type ReactNode,
  6. useCallback,
  7. useRef,
  8. } from 'react';
  9. import dynamic from 'next/dynamic';
  10. import { useRouter } from 'next/router';
  11. import type { IUser, Ref } from '@growi/core';
  12. import { pagePathUtils } from '@growi/core/dist/utils';
  13. import type { UncontrolledTooltipProps } from 'reactstrap';
  14. import styles from './UserPicture.module.scss';
  15. const moduleClass = styles['user-picture'];
  16. const moduleTooltipClass = styles['user-picture-tooltip'];
  17. const UncontrolledTooltip = dynamic<UncontrolledTooltipProps>(
  18. () => import('reactstrap').then((mod) => mod.UncontrolledTooltip),
  19. { ssr: false },
  20. );
  21. const DEFAULT_IMAGE = '/images/icons/user.svg';
  22. type UserPictureSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
  23. type BaseUserPictureRootProps = {
  24. displayName: string;
  25. children: ReactNode;
  26. size?: UserPictureSize;
  27. className?: string;
  28. };
  29. type UserPictureRootWithoutLinkProps = BaseUserPictureRootProps;
  30. type UserPictureRootWithLinkProps = BaseUserPictureRootProps & {
  31. username: string;
  32. };
  33. const UserPictureRootWithoutLink = forwardRef<
  34. HTMLSpanElement,
  35. UserPictureRootWithoutLinkProps
  36. >((props, ref) => {
  37. return (
  38. <span ref={ref} className={props.className}>
  39. {props.children}
  40. </span>
  41. );
  42. });
  43. const UserPictureRootWithLink = forwardRef<
  44. HTMLSpanElement,
  45. UserPictureRootWithLinkProps
  46. >((props, ref) => {
  47. const router = useRouter();
  48. const { username } = props;
  49. const clickHandler = useCallback(() => {
  50. const href = pagePathUtils.userHomepagePath({ username });
  51. router.push(href);
  52. }, [router, username]);
  53. // Using <span> tag here instead of <a> tag because UserPicture is used in SearchResultList which is essentially a anchor tag.
  54. // Nested anchor tags causes a warning.
  55. // https://stackoverflow.com/questions/13052598/creating-anchor-tag-inside-anchor-taga
  56. return (
  57. // biome-ignore lint/a11y/useSemanticElements: ignore
  58. <span
  59. ref={ref}
  60. className={props.className}
  61. onClick={clickHandler}
  62. onKeyDown={clickHandler}
  63. style={{ cursor: 'pointer' }}
  64. role="link"
  65. tabIndex={0}
  66. >
  67. {props.children}
  68. </span>
  69. );
  70. });
  71. // wrapper with Tooltip
  72. const withTooltip =
  73. <P extends BaseUserPictureRootProps>(
  74. UserPictureSpanElm: React.ForwardRefExoticComponent<
  75. P & React.RefAttributes<HTMLSpanElement>
  76. >,
  77. ) =>
  78. (props: P): JSX.Element => {
  79. const { displayName, size } = props;
  80. const username = 'username' in props ? props.username : undefined;
  81. const tooltipClassName = `${moduleTooltipClass} user-picture-tooltip-${size ?? 'md'}`;
  82. const userPictureRef = useRef<HTMLSpanElement>(null);
  83. return (
  84. <>
  85. <UserPictureSpanElm ref={userPictureRef} {...props} />
  86. <UncontrolledTooltip
  87. placement="bottom"
  88. target={userPictureRef}
  89. popperClassName={tooltipClassName}
  90. delay={0}
  91. fade={false}
  92. >
  93. {username ? (
  94. <>
  95. {`@${username}`}
  96. <br />
  97. </>
  98. ) : null}
  99. {displayName}
  100. </UncontrolledTooltip>
  101. </>
  102. );
  103. };
  104. /**
  105. * type guard to determine whether the specified object is IUser
  106. */
  107. const hasUsername = (
  108. obj: Partial<IUser> | Ref<IUser> | null | undefined,
  109. ): obj is { username: string } => {
  110. return obj != null && typeof obj !== 'string' && 'username' in obj;
  111. };
  112. /**
  113. * Type guard to determine whether tooltip should be shown
  114. */
  115. const hasName = (
  116. obj: Partial<IUser> | Ref<IUser> | null | undefined,
  117. ): obj is { name: string } => {
  118. return obj != null && typeof obj === 'object' && 'name' in obj;
  119. };
  120. /**
  121. * type guard to determine whether the specified object is IUser
  122. */
  123. const hasProfileImage = (
  124. obj: Partial<IUser> | Ref<IUser> | null | undefined,
  125. ): obj is { imageUrlCached: string } => {
  126. return obj != null && typeof obj === 'object' && 'imageUrlCached' in obj;
  127. };
  128. type Props = {
  129. user?: Partial<IUser> | Ref<IUser> | null;
  130. size?: UserPictureSize;
  131. noLink?: boolean;
  132. noTooltip?: boolean;
  133. className?: string;
  134. };
  135. export const UserPicture = memo((userProps: Props): JSX.Element => {
  136. const {
  137. user,
  138. size,
  139. noLink,
  140. noTooltip,
  141. className: additionalClassName,
  142. } = userProps;
  143. // Extract user information
  144. const username = hasUsername(user) ? user.username : undefined;
  145. const displayName = hasName(user) ? user.name : 'someone';
  146. const src = hasProfileImage(user)
  147. ? (user.imageUrlCached ?? DEFAULT_IMAGE)
  148. : DEFAULT_IMAGE;
  149. const showTooltip = !noTooltip && hasName(user);
  150. // Build className
  151. const className = [
  152. moduleClass,
  153. 'user-picture',
  154. 'rounded-circle',
  155. size && `user-picture-${size}`,
  156. additionalClassName,
  157. ]
  158. .filter(Boolean)
  159. .join(' ');
  160. // biome-ignore lint/performance/noImgElement: ignore
  161. const imgElement = <img src={src} alt={displayName} className={className} />;
  162. const baseProps = { displayName, size, children: imgElement };
  163. if (username == null || noLink) {
  164. const Component = showTooltip
  165. ? withTooltip(UserPictureRootWithoutLink)
  166. : UserPictureRootWithoutLink;
  167. return <Component {...baseProps} />;
  168. }
  169. const Component = showTooltip
  170. ? withTooltip(UserPictureRootWithLink)
  171. : UserPictureRootWithLink;
  172. return <Component {...baseProps} username={username} />;
  173. });
  174. UserPicture.displayName = 'UserPicture';