UserPicture.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import {
  2. type ReactNode,
  3. memo, forwardRef, useCallback, useRef,
  4. } from 'react';
  5. import type { Ref, IUser } from '@growi/core';
  6. import { pagePathUtils } from '@growi/core/dist/utils';
  7. import dynamic from 'next/dynamic';
  8. import { useRouter } from 'next/router';
  9. import type { UncontrolledTooltipProps } from 'reactstrap';
  10. const UncontrolledTooltip = dynamic<UncontrolledTooltipProps>(() => import('reactstrap').then(mod => mod.UncontrolledTooltip), { ssr: false });
  11. const DEFAULT_IMAGE = '/images/icons/user.svg';
  12. type UserPictureRootProps = {
  13. user: Partial<IUser>,
  14. className?: string,
  15. children?: ReactNode,
  16. }
  17. const UserPictureRootWithoutLink = forwardRef<HTMLSpanElement, UserPictureRootProps>((props, ref) => {
  18. return <span ref={ref} className={props.className}>{props.children}</span>;
  19. });
  20. const UserPictureRootWithLink = forwardRef<HTMLSpanElement, UserPictureRootProps>((props, ref) => {
  21. const router = useRouter();
  22. const { user } = props;
  23. const href = pagePathUtils.userHomepagePath(user);
  24. const clickHandler = useCallback(() => {
  25. router.push(href);
  26. }, [href, router]);
  27. // Using <span> tag here instead of <a> tag because UserPicture is used in SearchResultList which is essentially a anchor tag.
  28. // Nested anchor tags causes a warning.
  29. // https://stackoverflow.com/questions/13052598/creating-anchor-tag-inside-anchor-taga
  30. return <span ref={ref} className={props.className} onClick={clickHandler} style={{ cursor: 'pointer' }}>{props.children}</span>;
  31. });
  32. // wrapper with Tooltip
  33. const withTooltip = (UserPictureSpanElm: React.ForwardRefExoticComponent<UserPictureRootProps & React.RefAttributes<HTMLSpanElement>>) => {
  34. return (props: UserPictureRootProps) => {
  35. const { user } = props;
  36. const userPictureRef = useRef<HTMLSpanElement>(null);
  37. return (
  38. <>
  39. <UserPictureSpanElm ref={userPictureRef} user={user}>{props.children}</UserPictureSpanElm>
  40. <UncontrolledTooltip placement="bottom" target={userPictureRef} delay={0} fade={false}>
  41. @{user.username}<br />
  42. {user.name}
  43. </UncontrolledTooltip>
  44. </>
  45. );
  46. };
  47. };
  48. /**
  49. * type guard to determine whether the specified object is IUser
  50. */
  51. const isUserObj = (obj: Partial<IUser> | Ref<IUser>): obj is Partial<IUser> => {
  52. return typeof obj !== 'string' && 'username' in obj;
  53. };
  54. type Props = {
  55. user?: Partial<IUser> | Ref<IUser> | null,
  56. size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl',
  57. noLink?: boolean,
  58. noTooltip?: boolean,
  59. };
  60. export const UserPicture = memo((props: Props): JSX.Element => {
  61. const {
  62. user, size, noLink, noTooltip,
  63. } = props;
  64. const classNames = ['rounded-circle', 'picture'];
  65. if (size != null) {
  66. classNames.push(`picture-${size}`);
  67. }
  68. const className = classNames.join(' ');
  69. if (user == null || !isUserObj(user)) {
  70. return (
  71. <img
  72. src={DEFAULT_IMAGE}
  73. alt="someone"
  74. className={className}
  75. />
  76. );
  77. }
  78. // determine RootElm
  79. const UserPictureSpanElm = noLink ? UserPictureRootWithoutLink : UserPictureRootWithLink;
  80. const UserPictureRootElm = noTooltip
  81. ? UserPictureSpanElm
  82. : withTooltip(UserPictureSpanElm);
  83. const userPictureSrc = user.imageUrlCached ?? DEFAULT_IMAGE;
  84. return (
  85. <UserPictureRootElm user={user}>
  86. <img
  87. src={userPictureSrc}
  88. alt={user.username}
  89. className={className}
  90. />
  91. </UserPictureRootElm>
  92. );
  93. });
  94. UserPicture.displayName = 'UserPicture';