UserPicture.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  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: 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 clickHandler = useCallback(() => {
  24. const href = pagePathUtils.userHomepagePath(user);
  25. router.push(href);
  26. }, [router, user]);
  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 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. additionalClassName?: string
  60. };
  61. export const UserPicture = memo((props: Props): JSX.Element => {
  62. const {
  63. user, size, noLink, noTooltip, additionalClassName,
  64. } = props;
  65. const classNames = ['rounded-circle', 'picture'];
  66. if (size != null) {
  67. classNames.push(`picture-${size}`);
  68. }
  69. if (additionalClassName != null) {
  70. classNames.push(additionalClassName);
  71. }
  72. const className = classNames.join(' ');
  73. if (user == null || !isUserObj(user)) {
  74. return (
  75. <img
  76. src={DEFAULT_IMAGE}
  77. alt="someone"
  78. className={className}
  79. />
  80. );
  81. }
  82. // determine RootElm
  83. const UserPictureSpanElm = noLink ? UserPictureRootWithoutLink : UserPictureRootWithLink;
  84. const UserPictureRootElm = noTooltip
  85. ? UserPictureSpanElm
  86. : withTooltip(UserPictureSpanElm);
  87. const userPictureSrc = user.imageUrlCached ?? DEFAULT_IMAGE;
  88. return (
  89. <UserPictureRootElm user={user}>
  90. <img
  91. src={userPictureSrc}
  92. alt={user.username}
  93. className={className}
  94. />
  95. </UserPictureRootElm>
  96. );
  97. });
  98. UserPicture.displayName = 'UserPicture';