react-transition-group#SwitchTransition TypeScript Examples

The following examples show how to use react-transition-group#SwitchTransition. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: UploadProgressBar.tsx    From atlas with GNU General Public License v3.0 6 votes vote down vote up
UploadProgressBar: React.FC<UploadProgressBarProps> = ({
  progress = 0,
  lastStatus,
  className,
  withLoadingIndicator,
  withCompletedAnimation,
}) => {
  return (
    <UploadProgressBarContainer className={className}>
      <ProgressBar
        runCompletedAnimation={withCompletedAnimation}
        progress={progress}
        isProcessing={lastStatus === 'processing'}
        isCompleted={lastStatus === 'completed'}
      />
      {lastStatus !== 'completed' && <BottomProgressBar progress={progress} />}
      {withLoadingIndicator && (
        <SwitchTransition>
          <CSSTransition
            key={lastStatus === 'inProgress' || lastStatus === 'processing' ? 'progress' : 'completed'}
            classNames={transitions.names.fade}
            timeout={200}
          >
            <LoaderWrapper>
              {(lastStatus === 'inProgress' || lastStatus === 'processing') && <Loader variant="small" />}
              {lastStatus === 'completed' && <SvgAlertsSuccess24 />}
            </LoaderWrapper>
          </CSSTransition>
        </SwitchTransition>
      )}
    </UploadProgressBarContainer>
  )
}
Example #2
Source File: NewVideoTile.tsx    From atlas with GNU General Public License v3.0 6 votes vote down vote up
NewVideoTile: React.FC<NewVideoTileProps> = ({ loading, onClick }) => {
  return (
    <SwitchTransition>
      <CSSTransition
        key={loading ? 'cover-placeholder' : 'cover'}
        timeout={parseInt(transitions.timings.sharp)}
        classNames={transitions.names.fade}
      >
        <NewVideoTileWrapper>
          {loading ? (
            <NewVideoTileSkeleton />
          ) : (
            <NewVideoTileLink to={absoluteRoutes.studio.videoWorkspace()} onClick={onClick}>
              <TextAndIconWrapper>
                <StyledIcon />
                <StyledText variant="t200">Upload new video</StyledText>
              </TextAndIconWrapper>
            </NewVideoTileLink>
          )}
        </NewVideoTileWrapper>
      </CSSTransition>
    </SwitchTransition>
  )
}
Example #3
Source File: VideoHeroSlider.tsx    From atlas with GNU General Public License v3.0 6 votes vote down vote up
VideoSliderPreview: React.FC<VideoSliderPreviewProps> = ({
  progress,
  active,
  thumbnailUrl,
  isLoadingThumbnail,
  onClick,
}) => {
  const smMatch = useMediaMatch('sm')
  return (
    <SwitchTransition>
      <CSSTransition
        key={isLoadingThumbnail ? 'data' : 'placeholder'}
        classNames={transitions.names.fade}
        timeout={parseInt(transitions.timings.regular)}
      >
        <VideoSliderPreviewWrapper onClick={onClick}>
          {isLoadingThumbnail ? (
            <ThumbnailSkeletonLoader active={active} width={smMatch ? 80 : '100%'} height={smMatch ? 45 : 4} />
          ) : (
            <VideoSliderThumbnail src={thumbnailUrl || ''} active={active} />
          )}

          <VideoSliderProgressBar active={active}>
            <VideoSliderProgress style={{ transform: `scaleX(${progress ? progress / 100 : 0})` }} />
          </VideoSliderProgressBar>
        </VideoSliderPreviewWrapper>
      </CSSTransition>
    </SwitchTransition>
  )
}
Example #4
Source File: VideoHeroHeader.tsx    From atlas with GNU General Public License v3.0 6 votes vote down vote up
VideoHeroHeader: React.FC<VideoHeroHeaderProps> = ({ loading, icon, title }) => {
  return (
    <SwitchTransition>
      <CSSTransition
        key={loading ? 'data' : 'placeholder'}
        classNames={transitions.names.fade}
        timeout={parseInt(transitions.timings.regular)}
      >
        <StyledVideoHeroHeader>
          {!loading ? (
            <IconButton variant="tertiary" to={absoluteRoutes.viewer.discover()}>
              <SvgActionChevronL />
            </IconButton>
          ) : (
            <SkeletonLoader rounded height={40} width={40} />
          )}
          <Divider />
          {!loading ? (
            <>
              {icon}
              <VideoHeroHeaderTitle variant="h400">{title}</VideoHeroHeaderTitle>
            </>
          ) : (
            <SkeletonLoader height={24} width={160} />
          )}
        </StyledVideoHeroHeader>
      </CSSTransition>
    </SwitchTransition>
  )
}
Example #5
Source File: NftInfoItem.tsx    From atlas with GNU General Public License v3.0 6 votes vote down vote up
NftInfoItem: React.FC<NftInfoItemProps> = ({ size, label, content, secondaryText, loading }) => {
  if (loading) {
    return (
      <InfoItemContainer data-size={size}>
        <SkeletonLoader width="32%" height={16} />
        <SkeletonLoader width="64%" height={40} />
        {secondaryText && <SkeletonLoader width="32%" height={16} />}
      </InfoItemContainer>
    )
  }
  return (
    <InfoItemContainer data-size={size}>
      <Label variant="h100" secondary>
        {label}
      </Label>
      <InfoItemContent data-size={size}>{content}</InfoItemContent>
      <SwitchTransition>
        <CSSTransition
          key={secondaryText ? 'placeholder' : 'content'}
          timeout={parseInt(cVar('animationTransitionFast', true))}
          classNames={transitions.names.fade}
        >
          <SecondaryText data-size={size}>
            <Text as="div" variant="t100" secondary>
              {secondaryText ?? '‌'}
            </Text>
          </SecondaryText>
        </CSSTransition>
      </SwitchTransition>
    </InfoItemContainer>
  )
}
Example #6
Source File: AcceptBidList.tsx    From atlas with GNU General Public License v3.0 6 votes vote down vote up
BidRow: React.FC<BidRowProps> = ({ bidder, createdAt, amount, amountUSD, selectedValue, onSelect }) => {
  const xsMatch = useMediaMatch('xs')
  const selected = selectedValue?.id === bidder.id
  const { url, isLoadingAsset } = useMemberAvatar(bidder)
  return (
    <BidRowWrapper selected={selected} onClick={() => onSelect?.(bidder.id, amount)}>
      <RadioInput selectedValue={selectedValue?.id} value={bidder.id} onChange={() => onSelect?.(bidder.id, amount)} />
      {xsMatch && <Avatar assetUrl={url} loading={isLoadingAsset} size="small" />}
      <div>
        <Text variant="h300" secondary={!selected} margin={{ bottom: 1 }}>
          {bidder?.handle}
        </Text>
        <Text as="p" secondary variant="t100">
          {formatDateTime(new Date(createdAt))}
        </Text>
      </div>
      <Price>
        <TokenPrice>
          <JoyTokenIcon variant={selected ? 'regular' : 'gray'} />
          <Text variant="h300" margin={{ left: 1 }} secondary={!selected}>
            {amount}
          </Text>
        </TokenPrice>
        <SwitchTransition>
          <CSSTransition
            key={amountUSD ? 'placeholder' : 'content'}
            timeout={parseInt(cVar('animationTransitionFast', true))}
            classNames={transitions.names.fade}
          >
            <Text as="p" variant="t100" secondary>
              {amountUSD ?? '‌'}
            </Text>
          </CSSTransition>
        </SwitchTransition>
      </Price>
    </BidRowWrapper>
  )
}
Example #7
Source File: CategoryLink.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
CategoryLink: React.FC<CategoryLinkProps> = ({
  id,
  name,
  onClick,
  hideIcon,
  hideHandle,
  noLink,
  className,
  textVariant,
  textSecondary,
}) => {
  const _textVariant = textVariant || 't200-strong'
  return (
    <Container
      onClick={onClick}
      to={absoluteRoutes.viewer.category(id || '')}
      disabled={!id || noLink}
      className={className}
    >
      {!hideIcon && id ? (
        <IconWrapper withHandle={!hideHandle} color={videoCategories[id].color}>
          <CircleDefaultBackground color={videoCategories[id].color} />
          {videoCategories[id].icon}
        </IconWrapper>
      ) : (
        <StyledSkeletonLoader width={40} height={40} rounded withHandle={!hideHandle} />
      )}
      {!hideHandle && (
        <SwitchTransition>
          <CSSTransition
            key={id ? 'data' : 'placeholder'}
            classNames={transitions.names.fade}
            timeout={parseInt(transitions.timings.regular)}
          >
            {id ? (
              <Text variant={_textVariant} secondary={!!textSecondary}>
                More in {name}
              </Text>
            ) : (
              <SkeletonLoader height={16} width={150} />
            )}
          </CSSTransition>
        </SwitchTransition>
      )}
    </Container>
  )
}
Example #8
Source File: Inventory.tsx    From RPG-Inventory-UI with GNU General Public License v3.0 5 votes vote down vote up
render() {
        const { t } = this.props;

        return (
            <div id="inventory">
                <div className="inventory-list">
                    <div className="item-list-title">
                        <div className={"title" + (this.state.clothes ? "" : " selected")} onClick={() => this.setState({ clothes : false })}>{t("Inventaire")}</div>
                        <div className="title" style={{ pointerEvents: 'none' }}>&nbsp;&nbsp;|&nbsp;&nbsp;</div>
                        <div className={"title" + (!this.state.clothes ? "" : " selected")} onClick={() => this.setState({ clothes : true })} >{t("Vêtements")}</div>
                        <div className="infos">{inventoryStore.pocketsWeight} / 45</div>
                    </div>
                    <SwitchTransition>
                        <FadeTransition key={this.state.clothes ? "lol" : "no"}>
                            <ItemList items={this.state.clothes ? inventoryStore.clothes : inventoryStore.pockets} eventName="inventory" />
                        </FadeTransition>
                    </SwitchTransition>

                    <div className="gunInventory">
                        <div className="item-list-title">
                            <div className="title selected" style={{ pointerEvents: 'none' }}>{t("Armes")}</div>
                        </div>
                        <Item keyNumber="1" eventName="weaponOne" data={inventoryStore.weaponOne && { name : inventoryStore.weaponOne.name, base : inventoryStore.weaponOne.base }} />
                        <Item keyNumber="2" eventName="weaponTwo" data={inventoryStore.weaponTwo && { name : inventoryStore.weaponTwo.name, base : inventoryStore.weaponTwo.base }} />
                        <Item keyNumber="3" eventName="weaponThree" data={inventoryStore.weaponThree && { name : inventoryStore.weaponThree.name, base : inventoryStore.weaponThree.base }} />
                    </div>
                </div>

                <Options />

                <div className={"inventory-list " + (inventoryStore.targetMaxWeight <= 0 ? "hide" : "")}>
                    {inventoryStore.targetMaxWeight > 0 && (
                    <div className="item-list-title">
                        <div className={"title" + (this.state.targetClothes ? "" : " selected")} onClick={() => this.setState({ targetClothes : false })}>{t("Coffre")}</div>
                        <div className="title" style={{ pointerEvents: 'none' }}>&nbsp;&nbsp;|&nbsp;&nbsp;</div>
                        <div className={"title" + (!this.state.targetClothes ? "" : " selected")} onClick={() => this.setState({ targetClothes : true })} >{t("Vêtements")}</div>
                        <div className="infos">{inventoryStore.targetWeight} / {inventoryStore.targetMaxWeight}</div>
                    </div>
                    )}
                    
                    {inventoryStore.targetMaxWeight > 0 && (
                        <SwitchTransition>
                            <FadeTransition key={this.state.targetClothes ? "lol" : "no"}>
                                <ItemList IsTarget={true} items={this.state.targetClothes ? inventoryStore.targetClothes : inventoryStore.target} eventName="targetInventory" />
                            </FadeTransition>
                        </SwitchTransition>
                    )}
                </div>

                <div className={'item-icon drag ' + this.dragging} style={this.dragStyle} ref={draggable => (this.draggable = draggable)}>
                    <Item data={this.itemData} key={this.itemKey} />
                </div>
            </div>
        )
    }
Example #9
Source File: ViewerLayout.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
ViewerLayout: React.FC = () => {
  const location = useLocation()
  const locationState = location.state as RoutingState
  const { activeMemberId, isLoading } = useUser()

  const navigate = useNavigate()
  const mdMatch = useMediaMatch('md')
  const { searchOpen } = useSearchStore()

  const displayedLocation = locationState?.overlaidLocation || location

  return (
    <>
      <Modal show={isLoading} noBoxShadow>
        <Loader variant="xlarge" />
      </Modal>
      <TopbarViewer />
      <SidenavViewer />
      <MainContainer>
        <ErrorBoundary
          fallback={ViewErrorBoundary}
          onReset={() => {
            navigate(absoluteRoutes.viewer.index())
          }}
        >
          <SwitchTransition>
            <CSSTransition
              timeout={parseInt(transitions.timings.routing)}
              classNames={transitions.names.fadeAndSlide}
              key={displayedLocation.pathname}
            >
              <Routes location={displayedLocation}>
                {viewerRoutes.map((route) => (
                  <Route key={route.path} {...route} />
                ))}
                <Route
                  path={relativeRoutes.viewer.editMembership()}
                  element={
                    <PrivateRoute
                      isAuth={!!activeMemberId}
                      element={<EditMembershipView />}
                      redirectTo={ENTRY_POINT_ROUTE}
                    />
                  }
                />
                <Route
                  path={absoluteRoutes.viewer.notifications()}
                  element={
                    <PrivateRoute
                      isAuth={!!activeMemberId}
                      element={<NotificationsView />}
                      redirectTo={ENTRY_POINT_ROUTE}
                    />
                  }
                />
              </Routes>
            </CSSTransition>
          </SwitchTransition>
        </ErrorBoundary>
      </MainContainer>
      {!mdMatch && !searchOpen && <BottomNav />}
    </>
  )
}
Example #10
Source File: VideoOverlay.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
VideoOverlay: React.FC<VideoOverlayProps> = ({
  playerState,
  onPlay,
  channelId,
  currentThumbnailUrl,
  videoId,
  isFullScreen,
  isPlayNextDisabled,
  playRandomVideoOnEnded = true,
}) => {
  const [randomNextVideo, setRandomNextVideo] = useState<BasicVideoFieldsFragment | null>(null)
  const { videos } = useBasicVideos({
    where: {
      channel: {
        id_eq: channelId,
      },
      isPublic_eq: true,
      media: {
        isAccepted_eq: true,
      },
      thumbnailPhoto: {
        isAccepted_eq: true,
      },
    },
  })

  useEffect(() => {
    if (!videos?.length || videos.length <= 1) {
      return
    }
    const filteredVideos = videos.filter((video) => video.id !== videoId)
    const randomNumber = getRandomIntInclusive(0, filteredVideos.length - 1)

    setRandomNextVideo(filteredVideos[randomNumber])
  }, [videoId, videos])

  return (
    <SwitchTransition>
      <CSSTransition
        key={playerState}
        timeout={playerState !== 'error' ? parseInt(transitions.timings.sharp) : 0}
        classNames={transitions.names.fade}
        mountOnEnter
        unmountOnExit
        appear
      >
        <div>
          {playerState === 'pending' && <InactiveOverlay />}
          {playerState === 'loading' && <LoadingOverlay />}
          {playerState === 'ended' && (
            <EndingOverlay
              isFullScreen={isFullScreen}
              isEnded={true}
              isPlayNextDisabled={isPlayNextDisabled}
              onPlayAgain={onPlay}
              channelId={channelId}
              currentThumbnailUrl={currentThumbnailUrl}
              randomNextVideo={playRandomVideoOnEnded ? randomNextVideo : undefined}
            />
          )}
          {playerState === 'error' && <ErrorOverlay />}
        </div>
      </CSSTransition>
    </SwitchTransition>
  )
}
Example #11
Source File: VideoCategoryCard.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
VideoCategoryCard: React.FC<VideoCategoryCardProps> = ({
  variant = 'default',
  isLoading,
  title,
  categoryVideosCount,
  icon,
  videosTotalCount,
  coverImg,
  color,
  id,
}) => {
  // value from 1 to 100 percentage
  const pieChartValue = ((categoryVideosCount ?? 0) / (videosTotalCount ?? 1)) * 100

  const categoryUrl = id ? absoluteRoutes.viewer.category(id) : ''
  return (
    <SwitchTransition>
      <CSSTransition
        key={isLoading ? 'placeholder' : 'content'}
        timeout={parseInt(transitions.timings.sharp)}
        classNames={transitions.names.fade}
      >
        <GeneralContainer to={categoryUrl} isLoading={isLoading} variantCategory={variant} color={color}>
          <Content variantCategory={variant}>
            {isLoading ? (
              <SkeletonLoader bottomSpace={sizes(4)} width="40px" height="40px" rounded />
            ) : (
              <IconCircle color={color}>
                <CircleDefaultBackground color={color} />
                {icon}
              </IconCircle>
            )}

            {isLoading ? (
              <SkeletonLoader
                bottomSpace={variant === 'default' ? sizes(6) : sizes(4)}
                width="100%"
                height={variant === 'default' ? '32px' : '20px'}
              />
            ) : (
              <Title variantCategory={variant} variant={variant === 'default' ? 'h500' : 'h300'}>
                {title}
              </Title>
            )}

            <VideosNumberContainer>
              {isLoading ? (
                <SkeletonLoader width="80px" height={variant === 'default' ? '20px' : '16px'} />
              ) : (
                <>
                  <PieChart>
                    <CircleDefaultBackground />
                    <PieSegment value={pieChartValue} />
                  </PieChart>
                  <Text variant={variant === 'default' ? 't200' : 't100'} secondary>
                    {categoryVideosCount} videos
                  </Text>
                </>
              )}
            </VideosNumberContainer>
          </Content>

          {variant === 'default' && !isLoading && (
            <CoverImgContainer>
              <CoverImgOverlay />
              <CoverImg bgImgUrl={coverImg} />
            </CoverImgContainer>
          )}
        </GeneralContainer>
      </CSSTransition>
    </SwitchTransition>
  )
}
Example #12
Source File: NftHistory.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
HistoryItem: React.FC<HistoryItemProps> = ({ size, member, date, joyAmount, text }) => {
  const navigate = useNavigate()
  const { url, isLoadingAsset } = useMemberAvatar(member)
  const { convertToUSD } = useTokenPrice()

  const dollarValue = joyAmount ? convertToUSD(joyAmount) : null

  return (
    <HistoryItemContainer data-size={size}>
      <Avatar
        onClick={() => navigate(absoluteRoutes.viewer.member(member?.handle))}
        assetUrl={url}
        loading={isLoadingAsset}
        size={size === 'medium' ? 'small' : 'default'}
      />
      <TextContainer>
        <CopyContainer>
          <Text variant={size === 'medium' ? 'h300' : 'h200'} secondary>
            {text}
            {' by '}
            <OwnerHandle to={absoluteRoutes.viewer.member(member?.handle)} variant="secondary" textOnly>
              <Text variant={size === 'medium' ? 'h300' : 'h200'}>{member?.handle}</Text>
            </OwnerHandle>
          </Text>
        </CopyContainer>
        <Text variant="t100" secondary>
          {formatDateTime(date)}
        </Text>
      </TextContainer>
      {!!joyAmount && (
        <ValueContainer>
          <JoyPlusIcon>
            <JoyTokenIcon size={16} variant="silver" />
            <Text variant={size === 'medium' ? 'h300' : 'h200'}>{formatNumberShort(joyAmount)}</Text>
          </JoyPlusIcon>
          <SwitchTransition>
            <CSSTransition
              key={dollarValue ? 'placeholder' : 'content'}
              timeout={parseInt(cVar('animationTransitionFast', true))}
              classNames={transitions.names.fade}
            >
              <DollarValue variant="t100" secondary>
                {dollarValue ?? '‌'}
              </DollarValue>
            </CSSTransition>
          </SwitchTransition>
        </ValueContainer>
      )}
    </HistoryItemContainer>
  )
}
Example #13
Source File: FeaturedVideoCategoryCard.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
FeaturedVideoCategoryCard: React.FC<FeaturedVideoCategoryCardProps> = ({
  title,
  icon,
  videoUrl,
  videoTitle,
  color,
  variant = 'default',
  id,
}) => {
  const isTouchDevice = useTouchDevice()
  const [hoverRef, isVideoHovering] = useHover<HTMLAnchorElement>()
  const isLoading = !title && !videoUrl && !icon
  const isPlaying = isTouchDevice ? true : isVideoHovering && !isLoading
  const categoryUrl = id ? absoluteRoutes.viewer.category(id) : ''

  return (
    <SwitchTransition>
      <CSSTransition
        key={isLoading ? 'placeholder' : 'content'}
        timeout={parseInt(transitions.timings.sharp)}
        classNames={transitions.names.fade}
      >
        <FeaturedContainer
          to={categoryUrl}
          ref={hoverRef}
          isLoading={isLoading}
          variantCategory={variant}
          color={color}
        >
          <PlayerContainer>
            <BackgroundVideoPlayer src={isLoading ? undefined : videoUrl} loop muted playing={isPlaying} />
          </PlayerContainer>

          <FeaturedContent variantCategory={variant}>
            <div>
              {isLoading ? (
                <SkeletonLoader bottomSpace={sizes(4)} width="40px" height="40px" rounded />
              ) : (
                <FeaturedIconCircle color={color}>{icon}</FeaturedIconCircle>
              )}

              {isLoading ? (
                <SkeletonLoader width="312px" height={variant === 'default' ? '40px' : '32px'} />
              ) : (
                <Text variant={variant === 'default' ? 'h600' : 'h500'}>{title}</Text>
              )}
            </div>

            {!isLoading && (
              <FeaturedVideoTitleContainer variantCategory={variant}>
                <FeaturedVideoText variant="t100" secondary>
                  Featured video
                </FeaturedVideoText>
                <Text variant="h300">{videoTitle}</Text>
              </FeaturedVideoTitleContainer>
            )}
          </FeaturedContent>
        </FeaturedContainer>
      </CSSTransition>
    </SwitchTransition>
  )
}
Example #14
Source File: ChannelLink.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
ChannelLink: React.FC<ChannelLinkProps> = ({
  id,
  onClick,
  hideHandle,
  hideAvatar,
  noLink,
  overrideChannel,
  avatarSize = 'default',
  onNotFound,
  className,
  textVariant,
  textSecondary,
  customTitle,
  followButton = false,
}) => {
  const { channel } = useBasicChannel(id || '', {
    skip: !id,
    onCompleted: (data) => !data && onNotFound?.(),
    onError: (error) => SentryLogger.error('Failed to fetch channel', 'ChannelLink', error, { channel: { id } }),
  })
  const { toggleFollowing, isFollowing } = useHandleFollowChannel(channel?.id, channel?.title)
  const { url: avatarPhotoUrl } = useAsset(channel?.avatarPhoto)

  const displayedChannel = overrideChannel || channel

  const handleFollowButtonClick = (e: React.MouseEvent) => {
    e.preventDefault()
    toggleFollowing()
  }

  const _textVariant = textVariant || 't200-strong'
  return (
    <Container className={className}>
      {!hideAvatar && (
        <StyledLink onClick={onClick} to={absoluteRoutes.viewer.channel(id)} disabled={!id || noLink}>
          <StyledAvatar
            withHandle={!hideHandle}
            loading={!displayedChannel}
            size={avatarSize}
            assetUrl={avatarPhotoUrl}
          />
        </StyledLink>
      )}
      {!hideHandle && (
        <SwitchTransition>
          <CSSTransition
            key={displayedChannel ? 'data' : 'placeholder'}
            classNames={transitions.names.fade}
            timeout={parseInt(transitions.timings.regular)}
          >
            {displayedChannel ? (
              <TitleWrapper followButton={followButton}>
                <StyledLink onClick={onClick} to={absoluteRoutes.viewer.channel(id)} disabled={!id || noLink}>
                  <StyledText variant={_textVariant} isSecondary={!!textSecondary}>
                    {customTitle || displayedChannel?.title}
                  </StyledText>
                  {followButton && (
                    <Follows as="p" variant="t100" secondary>
                      {displayedChannel.follows} {displayedChannel.follows === 1 ? 'follower' : 'followers'}
                    </Follows>
                  )}
                </StyledLink>
                {followButton && (
                  <FollowButton variant="secondary" onClick={handleFollowButtonClick}>
                    {isFollowing ? 'Unfollow' : 'Follow'}
                  </FollowButton>
                )}
              </TitleWrapper>
            ) : (
              <SkeletonLoader height={16} width={150} />
            )}
          </CSSTransition>
        </SwitchTransition>
      )}
    </Container>
  )
}
Example #15
Source File: ChannelCard.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
ChannelCard: React.FC<ChannelCardProps> = ({
  className,
  onClick,
  withFollowButton = true,
  channel,
  loading,
}) => {
  const mdMatch = useMediaMatch('md')
  const [activeDisabled, setActiveDisabled] = useState(false)

  const { url, isLoadingAsset } = useAsset(channel?.avatarPhoto)

  const { toggleFollowing, isFollowing } = useHandleFollowChannel(channel?.id, channel?.title)

  const handleFollowButtonClick = (e: React.MouseEvent) => {
    e.preventDefault()
    toggleFollowing()
  }
  return (
    <ChannelCardArticle className={className} activeDisabled={activeDisabled}>
      <ChannelCardAnchor onClick={onClick} to={channel?.id ? absoluteRoutes.viewer.channel(channel.id) : ''}>
        <StyledAvatar size="channel-card" loading={isLoadingAsset || loading} assetUrl={url} />
        <SwitchTransition>
          <CSSTransition
            key={loading ? 'placeholder' : 'content'}
            timeout={parseInt(cVar('animationTransitionFast', true))}
            classNames={transitions.names.fade}
          >
            <InfoWrapper>
              {loading || !channel ? (
                <>
                  <SkeletonLoader width={100} height={mdMatch ? 24 : 20} bottomSpace={mdMatch ? 4 : 8} />
                  <SkeletonLoader width={70} height={mdMatch ? 20 : 16} bottomSpace={withFollowButton ? 16 : 0} />
                  {withFollowButton && <SkeletonLoader width={60} height={32} />}
                </>
              ) : (
                <>
                  <ChannelTitle variant={mdMatch ? 'h300' : 't200-strong'}>{channel.title}</ChannelTitle>
                  <ChannelFollows variant={mdMatch ? 't200' : 't100'} secondary>
                    {formatNumberShort(channel.follows || 0)} followers
                  </ChannelFollows>
                  {withFollowButton && (
                    <FollowButton
                      variant="secondary"
                      size="small"
                      onClick={handleFollowButtonClick}
                      onMouseOut={() => setActiveDisabled(false)}
                      onMouseMove={() => setActiveDisabled(true)}
                    >
                      {isFollowing ? 'Unfollow' : 'Follow'}
                    </FollowButton>
                  )}
                </>
              )}
            </InfoWrapper>
          </CSSTransition>
        </SwitchTransition>
      </ChannelCardAnchor>
    </ChannelCardArticle>
  )
}
Example #16
Source File: Avatar.tsx    From atlas with GNU General Public License v3.0 5 votes vote down vote up
Avatar: React.FC<AvatarProps> = ({
  assetUrl,
  hasAvatarUploadFailed,
  withoutOutline,
  loading = false,
  size = 'default',
  children,
  onClick,
  className,
  editable,
  newChannel,
  clickable,
  onError,
}) => {
  const isEditable = !loading && editable && size !== 'default' && size !== 'bid'

  return (
    <Container
      as={onClick ? 'button' : 'div'}
      type={onClick ? 'button' : undefined}
      onClick={onClick}
      size={size}
      className={className}
      isLoading={loading}
      withoutOutline={withoutOutline}
      isClickable={clickable || !!onClick}
    >
      {isEditable && (
        <EditOverlay size={size}>
          <SvgActionImage />
          <span>{assetUrl ? 'Edit avatar' : 'Add avatar'}</span>
        </EditOverlay>
      )}
      {!children &&
        (newChannel && !isEditable ? (
          <NewChannelAvatar>
            <SvgActionNewChannel />
          </NewChannelAvatar>
        ) : (
          <SwitchTransition>
            <CSSTransition
              key={loading ? 'placeholder' : 'content'}
              timeout={parseInt(transitions.timings.loading)}
              classNames={transitions.names.fade}
            >
              {loading ? (
                <StyledSkeletonLoader rounded />
              ) : assetUrl ? (
                <StyledImage src={assetUrl} onError={onError} />
              ) : hasAvatarUploadFailed ? (
                <NewChannelAvatar>
                  <StyledSvgIllustrativeFileFailed />
                </NewChannelAvatar>
              ) : (
                <SilhouetteAvatar />
              )}
            </CSSTransition>
          </SwitchTransition>
        ))}
      {children && (loading ? <StyledSkeletonLoader rounded /> : <ChildrenWrapper>{children}</ChildrenWrapper>)}
    </Container>
  )
}
Example #17
Source File: index.tsx    From metaplex with Apache License 2.0 4 votes vote down vote up
ClaimingStep: React.FC<ClaimingStepProps> = ({ onClose }) => {
  const [currentCardIndex, setCurrentCardIndex] = useState<number>(-1);

  const { pack, voucherMetadataKey, provingProcess, redeemModalMetadata } =
    usePack();
  const { whitelistedCreatorsByCreator } = useMeta();
  const ghostCards = useGhostCards(currentCardIndex);

  const { name = '', authority = '' } = pack?.info || {};
  const { cardsRedeemed = 0, isExhausted = false } = provingProcess?.info || {};

  const creator = useMemo(
    () => getCreator(whitelistedCreatorsByCreator, authority),
    [whitelistedCreatorsByCreator, authority],
  );

  const isClaiming = currentCardIndex < cardsRedeemed - 1 || !isExhausted;
  const currentMetadataToShow =
    currentCardIndex >= 0
      ? redeemModalMetadata[currentCardIndex]
      : voucherMetadataKey;

  const titleText = isClaiming ? name : 'You Opened the Pack!';
  const subtitleText = isClaiming
    ? `From ${creator.name || shortenAddress(creator.address || '')}`
    : `${cardsRedeemed} new Cards were added to your wallet`;
  const footerText = isClaiming
    ? `Retrieving ${currentCardIndex === -1 ? 'first' : 'next'} Card...`
    : 'Pack Opening Succesful!';
  const infoMessageText =
    currentCardIndex === -1 ? INFO_MESSAGES[0] : INFO_MESSAGES[1];

  useInterval(
    () => {
      // Checking if can proceed with showing the next card
      if (currentCardIndex + 1 < cardsRedeemed) {
        // Select the next card to show
        setCurrentCardIndex(currentCardIndex + 1);
      }
    },
    // Delay in milliseconds or null to stop it
    isClaiming ? DELAY_BETWEEN_CARDS_CHANGE : null,
  );

  return (
    <div className="claiming-step">
      <span className="claiming-step__title">{titleText}</span>
      <span className="claiming-step__subtitle">{subtitleText}</span>
      <div className="claiming-step__cards-container">
        <div
          style={{ height: `${CARDS_DISTANCE * ghostCards.length}px` }}
          className="claiming-step__ghost-cards"
        >
          {ghostCards.map((_, index) => (
            <GhostCard key={index} index={index} />
          ))}
        </div>
        <div className="current-card-container">
          <SwitchTransition>
            <CSSTransition
              classNames="fade"
              key={currentCardIndex}
              addEndListener={(node, done) =>
                node.addEventListener('transitionend', done, false)
              }
            >
              <ArtContent
                key={currentCardIndex}
                pubkey={currentMetadataToShow}
                preview={false}
              />
            </CSSTransition>
          </SwitchTransition>
        </div>
      </div>
      {isClaiming && (
        <div className="claiming-step__notes">
          <img src="wallet.svg" />
          <span>{infoMessageText}</span>
        </div>
      )}
      {!isClaiming && (
        <button className="claiming-step__btn" onClick={onClose}>
          <span>Close and view cards</span>
        </button>
      )}
      <div className="claiming-step__footer">
        {isClaiming && <SmallLoader />}
        {!isClaiming && <CheckOutlined className="claiming-step__check" />}

        {footerText}
      </div>
    </div>
  );
}
Example #18
Source File: Chat.tsx    From watchparty with MIT License 4 votes vote down vote up
ChatMessage = ({
  message,
  nameMap,
  pictureMap,
  formatMessage,
  user,
  socket,
  owner,
  isChatDisabled,
  setReactionMenu,
  handleReactionClick,
  className,
}: {
  message: ChatMessage;
  nameMap: StringDict;
  pictureMap: StringDict;
  formatMessage: (cmd: string, msg: string) => React.ReactNode;
  user: firebase.User | undefined;
  socket: Socket;
  owner: string | undefined;
  isChatDisabled: boolean | undefined;
  setReactionMenu: Function;
  handleReactionClick: Function;
  className: string;
}) => {
  const { id, timestamp, cmd, msg, system, isSub, reactions } = message;
  const spellFull = 5; // the number of people whose names should be written out in full in the reaction popup
  return (
    <Comment className={`${classes.comment} ${className}`}>
      {id ? (
        <Popup
          content="WatchParty Plus subscriber"
          disabled={!isSub}
          trigger={
            <Comment.Avatar
              className={isSub ? classes.subscriber : ''}
              src={
                pictureMap[id] ||
                getDefaultPicture(nameMap[id], getColorForStringHex(id))
              }
            />
          }
        />
      ) : null}
      <Comment.Content>
        <UserMenu
          displayName={nameMap[id] || id}
          user={user}
          timestamp={timestamp}
          socket={socket}
          userToManage={id}
          isChatMessage
          disabled={!Boolean(owner && owner === user?.uid)}
          trigger={
            <Comment.Author as="a" className="light">
              {Boolean(system) && 'System'}
              {nameMap[id] || id}
            </Comment.Author>
          }
        />
        <Comment.Metadata className="dark">
          <div>{new Date(timestamp).toLocaleTimeString()}</div>
        </Comment.Metadata>
        <Comment.Text className="light system">
          {cmd && formatMessage(cmd, msg)}
        </Comment.Text>
        <Linkify
          componentDecorator={(
            decoratedHref: string,
            decoratedText: string,
            key: string
          ) => (
            <SecureLink href={decoratedHref} key={key}>
              {decoratedText}
            </SecureLink>
          )}
        >
          <Comment.Text className="light">{!cmd && msg}</Comment.Text>
        </Linkify>
        <div className={classes.commentMenu}>
          <Icon
            onClick={(e: MouseEvent) => {
              const viewportOffset = (e.target as any).getBoundingClientRect();
              setReactionMenu(
                true,
                id,
                timestamp,
                viewportOffset.top,
                viewportOffset.right
              );
            }}
            name={'' as any}
            inverted
            link
            disabled={isChatDisabled}
            style={{
              opacity: 1,
              display: 'flex',
              justifyContent: 'center',
              alignItems: 'center',
              padding: 10,
              margin: 0,
            }}
          >
            <span
              role="img"
              aria-label="React"
              style={{ margin: 0, fontSize: 18 }}
            >
              ?
            </span>
          </Icon>
        </div>
        <TransitionGroup>
          {Object.keys(reactions ?? []).map((key) =>
            reactions?.[key].length ? (
              <CSSTransition
                key={key}
                timeout={200}
                classNames={{
                  enter: classes['reaction-enter'],
                  enterActive: classes['reaction-enter-active'],
                  exit: classes['reaction-exit'],
                  exitActive: classes['reaction-exit-active'],
                }}
                unmountOnExit
              >
                <Popup
                  content={`${reactions[key]
                    .slice(0, spellFull)
                    .map((id) => nameMap[id] || 'Unknown')
                    .concat(
                      reactions[key].length > spellFull
                        ? [`${reactions[key].length - spellFull} more`]
                        : []
                    )
                    .reduce(
                      (text, value, i, array) =>
                        text + (i < array.length - 1 ? ', ' : ' and ') + value
                    )} reacted.`}
                  offset={[0, 6]}
                  trigger={
                    <div
                      className={`${classes.reactionContainer} ${
                        reactions[key].includes(socket.id)
                          ? classes.highlighted
                          : ''
                      }`}
                      onClick={() =>
                        handleReactionClick(key, message.id, message.timestamp)
                      }
                    >
                      <span
                        style={{
                          fontSize: 17,
                          position: 'relative',
                          bottom: 1,
                        }}
                      >
                        {key}
                      </span>
                      <SwitchTransition>
                        <CSSTransition
                          key={key + '-' + reactions[key].length}
                          classNames={{
                            enter: classes['reactionCounter-enter'],
                            enterActive:
                              classes['reactionCounter-enter-active'],
                            exit: classes['reactionCounter-exit'],
                            exitActive: classes['reactionCounter-exit-active'],
                          }}
                          addEndListener={(node, done) =>
                            node.addEventListener('transitionend', done, false)
                          }
                          unmountOnExit
                        >
                          <span
                            className={classes.reactionCounter}
                            style={{
                              color: 'rgba(255, 255, 255, 0.85)',
                              marginLeft: 3,
                            }}
                          >
                            {reactions[key].length}
                          </span>
                        </CSSTransition>
                      </SwitchTransition>
                    </div>
                  }
                />
              </CSSTransition>
            ) : null
          )}
        </TransitionGroup>
      </Comment.Content>
    </Comment>
  );
}
Example #19
Source File: RankProgress.tsx    From apps with GNU Affero General Public License v3.0 4 votes vote down vote up
export function RankProgress({
  progress,
  rank = 0,
  nextRank,
  showRankAnimation = false,
  showCurrentRankSteps = false,
  className,
  onRankAnimationFinish,
  fillByDefault = false,
  smallVersion = false,
  showRadialProgress = true,
  showTextProgress = false,
  rankLastWeek,
}: RankProgressProps): ReactElement {
  const [prevProgress, setPrevProgress] = useState(progress);
  const [animatingProgress, setAnimatingProgress] = useState(false);
  const [forceColor, setForceColor] = useState(false);
  const [shownRank, setShownRank] = useState(
    showRankAnimation ? getRank(rank) : rank,
  );
  const attentionRef = useRef<HTMLDivElement>();
  const progressRef = useRef<HTMLDivElement>();
  const badgeRef = useRef<SVGSVGElement>();
  const plusRef = useRef<HTMLElement>();

  const rankIndex = getRank(rank);
  const finalRank = isFinalRank(rank);
  const levelUp = () =>
    rank > shownRank &&
    rank > 0 &&
    (rank !== rankLastWeek || progress === RANKS[rankIndex].steps);
  const getLevelText = levelUp() ? 'You made it ?' : '+1 Reading day';
  const shouldForceColor = animatingProgress || forceColor || fillByDefault;

  const steps = useMemo(() => {
    if (
      showRankAnimation ||
      showCurrentRankSteps ||
      (finalRank && progress < RANKS[rankIndex].steps)
    ) {
      return RANKS[rankIndex].steps;
    }

    if (!finalRank) {
      return RANKS[rank].steps;
    }
    return 0;
  }, [showRankAnimation, showCurrentRankSteps, shownRank, progress, rank]);

  const animateRank = () => {
    setForceColor(true);
    const animatedRef = badgeRef.current || plusRef.current;
    const firstAnimationDuration = 400;
    const maxScale = 1.666;

    const progressAnimation = showRadialProgress
      ? progressRef.current.animate(
          [
            {
              transform: 'scale(1) rotate(180deg)',
              '--radial-progress-completed-step': rankToColor(shownRank),
            },
            { transform: `scale(${maxScale}) rotate(180deg)` },
            {
              transform: 'scale(1) rotate(180deg)',
              '--radial-progress-completed-step': rankToColor(rank),
            },
          ],
          { duration: firstAnimationDuration, fill: 'forwards' },
        )
      : null;

    const firstBadgeAnimation = animatedRef.animate(
      [
        {
          transform: 'scale(1)',
          '--stop-color1': rankToGradientStopBottom(shownRank),
          '--stop-color2': rankToGradientStopTop(shownRank),
        },
        { transform: `scale(${maxScale})`, opacity: 1 },
        { transform: 'scale(1)', opacity: 0 },
      ],
      { duration: firstAnimationDuration, fill: 'forwards' },
    );
    firstBadgeAnimation.onfinish = () => {
      setShownRank(rank);
      // Let the new rank update
      setTimeout(() => {
        const attentionAnimation = showRankAnimation
          ? attentionRef.current.animate(
              [
                {
                  transform: 'scale(0.5)',
                  opacity: 1,
                },
                {
                  transform: 'scale(1.5)',
                  opacity: 0,
                },
              ],
              { duration: 600, fill: 'forwards' },
            )
          : null;

        const lastBadgeAnimation = animatedRef.animate(
          [
            {
              transform: `scale(${2 - maxScale})`,
              opacity: 0,
              '--stop-color1': rankToGradientStopBottom(rank),
              '--stop-color2': rankToGradientStopTop(rank),
            },
            {
              transform: 'scale(1)',
              opacity: 1,
              '--stop-color1': rankToGradientStopBottom(rank),
              '--stop-color2': rankToGradientStopTop(rank),
            },
          ],
          { duration: 100, fill: 'forwards' },
        );
        const cancelAnimations = () => {
          progressAnimation?.cancel();
          firstBadgeAnimation.cancel();
          lastBadgeAnimation?.cancel();
          attentionAnimation?.cancel();
          setForceColor(false);
          onRankAnimationFinish?.();
        };

        if (attentionAnimation) {
          attentionAnimation.onfinish = cancelAnimations;
        } else {
          cancelAnimations();
          setTimeout(() => setAnimatingProgress(false), 2000);
        }
      });
    };
  };

  const onProgressTransitionEnd = () => {
    if (showRankAnimation || levelUp()) {
      setAnimatingProgress(false);
      animateRank();
    } else {
      setTimeout(() => setAnimatingProgress(false), 2000);
    }
  };

  useEffect(() => {
    if (!showRankAnimation) {
      setShownRank(rank);
    }
  }, [rank]);

  useEffect(() => {
    if (
      progress > prevProgress &&
      (!rank ||
        showRankAnimation ||
        levelUp() ||
        RANKS[rankIndex].steps !== progress)
    ) {
      if (!showRadialProgress) animateRank();
      setAnimatingProgress(true);
    }
    setPrevProgress(progress);
  }, [progress]);

  return (
    <>
      <div
        className={classNames(
          className,
          'relative z-1 border-1',
          styles.rankProgress,
          {
            [styles.enableColors]: shownRank > 0,
            [styles.forceColor]: shouldForceColor,
            [styles.smallVersion]: smallVersion && showRadialProgress,
            [styles.smallVersionClosed]: smallVersion && !showRadialProgress,
          },
        )}
        style={
          shownRank > 0
            ? ({
                '--rank-color': rankToColor(shownRank),
                '--rank-stop-color1': rankToGradientStopBottom(shownRank),
                '--rank-stop-color2': rankToGradientStopTop(shownRank),
              } as CSSProperties)
            : {}
        }
      >
        {showRankAnimation && (
          <div
            className="absolute -z-1 w-full h-full bg-theme-active rounded-full opacity-0 l-0 t-0"
            ref={attentionRef}
          />
        )}
        {showRadialProgress && (
          <RadialProgress
            progress={progress}
            steps={steps}
            onTransitionEnd={onProgressTransitionEnd}
            ref={progressRef}
            className={styles.radialProgress}
          />
        )}

        <SwitchTransition mode="out-in">
          <CSSTransition
            timeout={notificationDuration}
            key={animatingProgress}
            classNames="rank-notification-slide-down"
            mountOnEnter
            unmountOnExit
          >
            {getRankElement({
              shownRank,
              rank,
              badgeRef,
              plusRef,
              smallVersion,
              showRankAnimation,
              animatingProgress,
            })}
          </CSSTransition>
        </SwitchTransition>
      </div>
      {showTextProgress && (
        <SwitchTransition mode="out-in">
          <CSSTransition
            timeout={notificationDuration}
            key={animatingProgress}
            classNames="rank-notification-slide-down"
            mountOnEnter
            unmountOnExit
          >
            <div className="flex flex-col items-start ml-3">
              <span className="font-bold text-theme-rank typo-callout">
                {animatingProgress ? getLevelText : getRankName(shownRank)}
              </span>
              <span className="typo-footnote text-theme-label-tertiary">
                {getNextRankText({
                  nextRank,
                  rank,
                  finalRank,
                  progress,
                  rankLastWeek,
                  showNextLevel: !finalRank,
                })}
              </span>
            </div>
          </CSSTransition>
        </SwitchTransition>
      )}
    </>
  );
}
Example #20
Source File: index.tsx    From midway with MIT License 4 votes vote down vote up
Layout = ({ children, location, pageContext }: { children: any, location: { pathname: string }, pageContext: { site?: {}, layout: string }}) => {

  const { site } = pageContext

  // Render documentation for CMS minus header/footer experience
  if (pageContext.layout === 'docs') {
    return (
      <div>
        {children}
      </div>
    )
  }

  if (pageContext.layout === 'accounts') {
    return (
      <React.Fragment>
        <Helmet title='Accounts' />
        <Header />
        <div>{children}</div>
        <Footer {...site} />
      </React.Fragment>
    )
  }

  useEffect(() => {
    tighpo('spaghetti', function () {
      const style = document.createElement('style')
      document.body.appendChild(style)
      style.sheet.insertRule('html, body { cursor: url(https://spaghet.now.sh), auto !important; }')
    })
  }, [0])

  return (
    <React.Fragment>
      <Helmet title='Midway'>
        <link href='https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap' rel='stylesheet' />
      </Helmet>
      <Analytics
        googleAnalyticsPropertyId={process.env.GATSBY_GA_ID} />
      <PasswordWrapper>
        <div>
          <a
            name='maincontent'
            className='pf top left z10 skip'
            href='#maincontent'
          >
            Skip to main content
          </a>
          <Header />
          <CartDrawer />
          {/* 
          
            Smooth transition credits to Ian Williams: https://github.com/dictions
          
          */}
          {!/account/.test(location.pathname) ? (
            <SwitchTransition>
              <Transition
                key={location.pathname}
                mountOnEnter={true}
                unmountOnExit={true}
                appear={true}
                timeout={TRANSITION_DURATION}>
                  {status => (
                    <main
                      className='site'
                      id='maincontent'
                      style={{
                        ...TRANSITION_STYLES.default,
                        ...TRANSITION_STYLES[status],
                      }}>
                      {children}
                      <Footer {...site} />
                    </main>
                  )}
              </Transition>
            </SwitchTransition>
          ) : (
            <div>
              {children}
              <Footer {...site} />
            </div>
          )}
        </div>
      </PasswordWrapper>
    </React.Fragment>
  )
}
Example #21
Source File: MembershipInfo.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
MembershipInfo: React.FC<MembershipInfoProps> = ({
  address,
  avatarUrl,
  avatarLoading,
  hasAvatarUploadFailed,
  onAvatarEditClick,
  handle,
  loading,
  isOwner,
  editable,
  className,
}) => {
  const { copyToClipboard } = useClipboard()
  const [copyButtonClicked, setCopyButtonClicked] = useState(false)
  const smMatch = useMediaMatch('sm')

  const handleCopyAddress = () => {
    if (!address) {
      return
    }
    copyToClipboard(address, 'Account address copied to clipboard')
    setCopyButtonClicked(true)
    setTimeout(() => {
      setCopyButtonClicked(false)
    }, 3000)
  }

  return (
    <SwitchTransition>
      <CSSTransition
        key={String(loading)}
        timeout={parseInt(cVar('animationTimingFast', true))}
        classNames={transitions.names.fade}
      >
        <MembershipHeader className={className}>
          <MembershipInfoContainer>
            <Avatar
              size={smMatch ? 'preview' : 'channel-card'}
              editable={editable}
              onClick={onAvatarEditClick}
              assetUrl={avatarUrl}
              loading={avatarLoading}
              hasAvatarUploadFailed={hasAvatarUploadFailed}
            />
            <MembershipDetails>
              {loading ? (
                <SkeletonLoader width={200} height={smMatch ? 56 : 40} bottomSpace={8} />
              ) : (
                <StyledHandle variant={smMatch ? 'h700' : 'h600'}>{handle || '\xa0'}</StyledHandle>
              )}
              {loading || !address ? (
                <SkeletonLoader width={140} height={24} />
              ) : (
                <StyledText variant="t300" secondary onClick={handleCopyAddress}>
                  {shortenAddress(address, 6, 4)}
                  <Tooltip text="Copy account address" arrowDisabled placement="top">
                    {copyButtonClicked ? <StyledSvgActionCheck /> : <StyledSvgActionCopy />}
                  </Tooltip>
                </StyledText>
              )}
            </MembershipDetails>
          </MembershipInfoContainer>
          {isOwner &&
            (loading ? (
              <SkeletonLoader width={smMatch ? 148 : '100%'} height={48} />
            ) : (
              <Button
                to={absoluteRoutes.viewer.editMembership()}
                icon={<SvgActionEdit />}
                size="large"
                variant="secondary"
                fullWidth={!smMatch}
              >
                Edit profile
              </Button>
            ))}
        </MembershipHeader>
      </CSSTransition>
    </SwitchTransition>
  )
}
Example #22
Source File: AccountStep.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
AccountStep: React.FC<AccountStepProps> = ({
  nextStepPath,
  setSelectedAccountAddress,
  selectedAccountAddress,
}) => {
  const navigate = useNavigate()
  const { accounts, memberships, membershipsLoading } = useUser()

  const membershipsControllerAccounts = memberships?.map((a) => a.controllerAccount)
  const accountsWithNoMembership = (accounts || []).filter((el) => !membershipsControllerAccounts?.includes(el.id))

  const handleSubmitSelectedAccount = async (e: FormEvent) => {
    e.preventDefault()
    if (!selectedAccountAddress) {
      return
    }
    navigate({ search: nextStepPath })
  }

  const handleSelect = (id: string) => {
    setSelectedAccountAddress(id)
  }
  if (membershipsLoading) {
    return <StyledSpinner />
  }
  return (
    <SwitchTransition>
      <CSSTransition
        key={!accountsWithNoMembership?.length ? 'no-accounts' : 'accounts'}
        classNames={transitions.names.fadeAndSlide}
        timeout={parseInt(transitions.timings.routing)}
      >
        {!accountsWithNoMembership?.length ? (
          <StyledStepWrapper>
            <AccountStepImg />
            <StepTitle variant="h500">Create blockchain account</StepTitle>
            <SubTitle variant="t200" secondary>
              Use the Polkadot extension to generate your personal keypair. Follow these instructions:
            </SubTitle>
            <OrderedSteps>
              <OrderedStep secondary variant="t100" as="li">
                Open the extension popup with the icon in your browser bar
              </OrderedStep>
              <OrderedStep secondary variant="t100" as="li">
                Click the plus icon
              </OrderedStep>
              <OrderedStep secondary variant="t100" as="li">
                Continue with instructions presented on the screen
              </OrderedStep>
            </OrderedSteps>
            <StepFooter>
              <BottomBarIcon />
              <Text variant="t200" secondary>
                Make sure to safely save your seed phrase!
              </Text>
            </StepFooter>
          </StyledStepWrapper>
        ) : (
          <form onSubmit={handleSubmitSelectedAccount}>
            <StepWrapper>
              <IconGroup>
                <StyledPolkadotLogo />
                <SvgControlsConnect />
                <StyledJoystreamLogo />
              </IconGroup>
              <StepTitle variant="h500">Connect account</StepTitle>
              <StepSubTitle variant="t200" secondary>
                Select Polkadot account which you want to use to manage your new Joystream membership:
              </StepSubTitle>
              <AccountsWrapper>
                {accountsWithNoMembership?.map(({ id, name }) => (
                  <AccountBar
                    key={id}
                    id={id}
                    name={name}
                    onSelect={() => handleSelect(id)}
                    selectedValue={selectedAccountAddress}
                  />
                ))}
              </AccountsWrapper>
              <StepFooter>
                <StyledButton type="submit" disabled={!selectedAccountAddress}>
                  Connect account
                </StyledButton>
              </StepFooter>
            </StepWrapper>
          </form>
        )}
      </CSSTransition>
    </SwitchTransition>
  )
}
Example #23
Source File: UploadStatusGroup.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
UploadStatusGroup: React.FC<UploadStatusGroupProps> = ({ uploads, size = 'compact' }) => {
  const [isAssetsDrawerActive, setAssetsDrawerActive] = useState(false)
  const [runCompletedAnimation, setRunCompletedAnimation] = useState(false)
  const [uploadGroupState, setUploadGroupState] = useState<UploadGroupState>(null)
  const drawer = useRef<HTMLDivElement>(null)
  const uploadsStatuses = useUploadsStore((state) => uploads.map((u) => state.uploadsStatus[u.id], shallow))
  const location = useLocation()

  const locationState = location.state as RoutingState

  const isChannelType = uploads[0].parentObject.type === 'channel'

  const { video, loading: videoLoading } = useVideo(uploads[0].parentObject.id, { skip: isChannelType })
  const { channel, loading: channelLoading } = useChannel(uploads[0].parentObject.id, { skip: !isChannelType })

  const isWaiting = uploadsStatuses.every((file) => file?.progress === 0 && file?.lastStatus === 'inProgress')
  const isCompleted = uploadsStatuses.every((file) => file?.lastStatus === 'completed')
  const uploadRetries = uploadsStatuses
    .filter((file) => file?.lastStatus === 'reconnecting')
    .map((file) => file?.retries)[0]
  const errorsCount = uploadsStatuses.filter((file) => file?.lastStatus === 'error').length
  const missingAssetsCount = uploadsStatuses.filter((file) => !file || !file.lastStatus).length

  const allAssetsSize = uploads.reduce((acc, file) => acc + Number(file.size), 0)
  const alreadyUploadedSize = uploads.reduce(
    (acc, file, idx) => acc + ((uploadsStatuses[idx]?.progress ?? 0) / 100) * Number(file.size),
    0
  )
  const masterProgress = Math.floor((alreadyUploadedSize / allAssetsSize) * 100)
  const isProcessing = uploadsStatuses.some((file) => masterProgress === 100 && file?.lastStatus === 'processing')
  const hasUploadingAsset = uploadsStatuses.some((file) => file?.lastStatus === 'inProgress')

  const assetsGroupTitleText = isChannelType ? 'Channel assets' : video?.title
  const assetsGroupNumberText = `${uploads.length} asset${uploads.length > 1 ? 's' : ''}`

  useEffect(() => {
    if (isCompleted) {
      setUploadGroupState('completed')
    }
    if (errorsCount || missingAssetsCount) {
      setUploadGroupState('error')
      setAssetsDrawerActive(!!locationState?.highlightFailed)
    }
    if (hasUploadingAsset) {
      setUploadGroupState('inProgress')
    }
    if (isProcessing) {
      setUploadGroupState('processing')
    }
  }, [errorsCount, hasUploadingAsset, locationState?.highlightFailed, isCompleted, isProcessing, missingAssetsCount])

  const renderAssetsGroupInfo = () => {
    if (isWaiting) {
      return 'Starting upload...'
    }
    if (isCompleted) {
      return 'Uploaded'
    }
    if (isProcessing) {
      return 'Processing...'
    }
    if (hasUploadingAsset) {
      return `Uploading... (${masterProgress}%)`
    }
    if (errorsCount) {
      return `${errorsCount} asset${errorsCount > 1 ? 's' : ''} upload failed`
    }
    if (missingAssetsCount) {
      return `${missingAssetsCount} asset${missingAssetsCount > 1 ? 's' : ''} lost connection`
    }
    if (uploadRetries) {
      return `Trying to reconnect...(${uploadRetries})`
    }
  }

  const enrichedUploadData =
    (isChannelType && (channelLoading || !channel)) || (!isChannelType && (videoLoading || !video))
      ? uploads
      : uploads.map((asset) => {
          const typeToAsset = {
            'video': video?.media,
            'thumbnail': video?.thumbnailPhoto,
            'avatar': channel?.avatarPhoto,
            'cover': channel?.coverPhoto,
          }
          const fetchedAsset = typeToAsset[asset.type]
          const enrichedAsset: AssetUpload = { ...asset, ipfsHash: fetchedAsset?.ipfsHash }
          return enrichedAsset
        })

  if (videoLoading || channelLoading) {
    return <UploadStatusGroupSkeletonLoader />
  }

  const renderThumbnailIcon = (uploadGroupState: UploadGroupState) => {
    switch (uploadGroupState) {
      case 'completed':
        return <SvgAlertsSuccess24 />
      case 'error':
        return <SvgAlertsError24 />
      case 'inProgress':
      case 'processing':
        return <Loader variant="small" />
      default:
        return <Loader variant="small" />
    }
  }

  return (
    <Container>
      <UploadStatusGroupContainer
        onClick={() => setAssetsDrawerActive(!isAssetsDrawerActive)}
        isActive={isAssetsDrawerActive}
      >
        <UploadProgressBar
          withCompletedAnimation={runCompletedAnimation}
          lastStatus={uploadGroupState || undefined}
          progress={masterProgress}
        />
        <Thumbnail size={size}>
          {uploadGroupState && (
            <SwitchTransition>
              <CSSTransition
                addEndListener={() => uploadGroupState === 'completed' && setRunCompletedAnimation(true)}
                key={uploadGroupState}
                classNames={transitions.names.fade}
                timeout={200}
              >
                {renderThumbnailIcon(uploadGroupState)}
              </CSSTransition>
            </SwitchTransition>
          )}
        </Thumbnail>
        <AssetsInfoContainer>
          <AssetGroupTitleText variant="t300-strong">{assetsGroupTitleText}</AssetGroupTitleText>
          <Text variant="t200" secondary>
            {assetsGroupNumberText}
          </Text>
        </AssetsInfoContainer>
        <UploadInfoContainer>
          {size === 'large' && (
            <Text variant="t200" secondary>
              {renderAssetsGroupInfo()}
            </Text>
          )}
          <StyledExpandButton
            expanded={isAssetsDrawerActive}
            onClick={() => setAssetsDrawerActive(!isAssetsDrawerActive)}
            size="large"
          />
        </UploadInfoContainer>
      </UploadStatusGroupContainer>
      <AssetsDrawerContainer isActive={isAssetsDrawerActive} ref={drawer} maxHeight={drawer?.current?.scrollHeight}>
        {enrichedUploadData.map((file, idx) => (
          <UploadStatus size={size} key={file.id} asset={file} isLast={uploads.length === idx + 1} />
        ))}
      </AssetsDrawerContainer>
    </Container>
  )
}
Example #24
Source File: UploadStatus.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
UploadStatus: React.FC<UploadStatusProps> = ({ isLast = false, asset, size }) => {
  const navigate = useNavigate()
  const startFileUpload = useStartFileUpload()
  const uploadStatus = useUploadsStore((state) => state.uploadsStatus[asset.id])
  const { setUploadStatus } = useUploadsStore((state) => state.actions)

  const thumbnailDialogRef = useRef<ImageCropModalImperativeHandle>(null)
  const avatarDialogRef = useRef<ImageCropModalImperativeHandle>(null)
  const coverDialogRef = useRef<ImageCropModalImperativeHandle>(null)

  const [openDifferentFileDialog, closeDifferentFileDialog] = useConfirmationModal({
    title: 'Different file was selected!',
    description: `We detected that you selected a different file than the one you uploaded previously. Select the same file to continue the upload or edit ${
      asset.parentObject.type === 'channel' ? 'your channel' : 'the video'
    } to use the new file.`,
    iconType: 'warning',
    primaryButton: {
      text: 'Reselect file',
      onClick: () => {
        reselectFile()
        closeDifferentFileDialog()
      },
    },
    secondaryButton: {
      text: `Edit ${asset.parentObject.type === 'channel' ? 'channel' : 'video'}`,
      onClick: () => {
        if (asset.parentObject.type === 'video') {
          navigate(absoluteRoutes.studio.videoWorkspace())
        }
        if (asset.parentObject.type === 'channel') {
          navigate(absoluteRoutes.studio.editChannel())
        }
        closeDifferentFileDialog()
      },
    },
  })
  const [openMissingCropDataDialog, closeMissingCropDataDialog] = useConfirmationModal({
    title: 'Missing asset details',
    description:
      "It seems you've published this asset from a different device or you've cleared your browser history. All image assets require crop data to reconstruct, otherwise they end up being different files. Please try re-uploading from the original device or overwrite this asset.",
    iconType: 'warning',
    secondaryButton: {
      text: 'Close',
      onClick: () => {
        closeMissingCropDataDialog()
      },
    },
  })

  const onDrop: DropzoneOptions['onDrop'] = useCallback(
    async (acceptedFiles) => {
      const [file] = acceptedFiles
      setUploadStatus(asset.id, { lastStatus: 'inProgress', progress: 0 })
      const fileHash = await computeFileHash(file)
      if (fileHash !== asset.ipfsHash) {
        setUploadStatus(asset.id, { lastStatus: undefined })
        openDifferentFileDialog()
      } else {
        startFileUpload(
          file,
          {
            id: asset.id,
            owner: asset.owner,
            parentObject: {
              type: asset.parentObject.type,
              id: asset.parentObject.id,
            },
            type: asset.type,
          },
          {
            isReUpload: true,
          }
        )
      }
    },
    [asset, openDifferentFileDialog, setUploadStatus, startFileUpload]
  )

  const isVideo = asset.type === 'video'
  const {
    getRootProps,
    getInputProps,
    open: openFileSelect,
  } = useDropzone({
    onDrop,
    maxFiles: 1,
    multiple: false,
    noClick: true,
    noKeyboard: true,
    accept: isVideo ? 'video/*' : 'image/*',
  })

  const fileTypeText = isVideo ? 'Video file' : `${asset.type.charAt(0).toUpperCase() + asset.type.slice(1)} image`

  const handleChangeHost = () => {
    startFileUpload(
      null,
      {
        id: asset.id,
        owner: asset.owner,
        parentObject: {
          type: asset.parentObject.type,
          id: asset.parentObject.id,
        },
        type: asset.type,
      },
      {
        changeHost: true,
      }
    )
  }

  const handleCropConfirm = async (croppedBlob: Blob) => {
    const fileHash = await computeFileHash(croppedBlob)
    if (fileHash !== asset.ipfsHash) {
      openDifferentFileDialog()
    } else {
      startFileUpload(
        croppedBlob,
        {
          id: asset.id,
          owner: asset.owner,
          parentObject: {
            type: asset.parentObject.type,
            id: asset.parentObject.id,
          },
          type: asset.type,
        },
        {
          isReUpload: true,
        }
      )
    }
  }

  const assetDimension =
    asset.dimensions?.width && asset.dimensions.height
      ? `${Math.floor(asset.dimensions.width)}x${Math.floor(asset.dimensions.height)}`
      : ''
  const assetSize = formatBytes(Number(asset.size))

  const assetsDialogs = {
    avatar: avatarDialogRef,
    cover: coverDialogRef,
    thumbnail: thumbnailDialogRef,
  }
  const reselectFile = () => {
    if (asset.type === 'video') {
      openFileSelect()
      return
    }
    if (!asset.imageCropData) {
      openMissingCropDataDialog()
      return
    }
    assetsDialogs[asset.type].current?.open(undefined, asset.imageCropData)
  }

  const renderStatusMessage = () => {
    const failedStatusText = size === 'compact' ? 'Upload failed' : 'Asset upload failed'
    if (uploadStatus?.lastStatus === 'error') {
      return (
        <FailedStatusWrapper>
          <StatusText variant="t200" secondary size={size}>
            {failedStatusText}
          </StatusText>
          <RetryButton size="small" variant="secondary" icon={<SvgActionUpload />} onClick={handleChangeHost}>
            Try again
          </RetryButton>
        </FailedStatusWrapper>
      )
    }
    if (!uploadStatus?.lastStatus) {
      return (
        <FailedStatusWrapper>
          <StatusText variant="t200" secondary size={size}>
            {failedStatusText}
          </StatusText>
          <div {...getRootProps()}>
            <input {...getInputProps()} />
            <RetryButton size="small" variant="secondary" icon={<SvgActionUpload />} onClick={reselectFile}>
              Reconnect file
            </RetryButton>
          </div>
        </FailedStatusWrapper>
      )
    }
  }

  const renderStatusIndicator = () => {
    if (uploadStatus?.lastStatus === 'completed') {
      return <SvgAlertsSuccess24 />
    }
    if (uploadStatus?.lastStatus === 'error' || !uploadStatus?.lastStatus) {
      return <SvgAlertsWarning24 />
    }
    if (uploadStatus?.lastStatus === 'processing') {
      return <Loader variant="small" />
    }
    return (
      <ProgressbarContainer>
        <CircularProgress strokeWidth={10} value={uploadStatus?.progress ?? 0} />
      </ProgressbarContainer>
    )
  }
  const isReconnecting = uploadStatus?.lastStatus === 'reconnecting'
  return (
    <>
      <FileLineContainer isLast={isLast} size={size}>
        <FileInfoContainer>
          {isLast ? <FileLineLastPoint size={size} /> : <FileLinePoint size={size} />}
          <FileStatusContainer>
            <SwitchTransition>
              <CSSTransition
                key={uploadStatus?.lastStatus || 'no-status'}
                classNames={transitions.names.fade}
                timeout={200}
              >
                {renderStatusIndicator()}
              </CSSTransition>
            </SwitchTransition>
          </FileStatusContainer>
          <FileInfo size={size}>
            <FileInfoType warning={isReconnecting && size === 'compact'}>
              {isVideo ? <SvgActionVideoFile /> : <SvgActionImageFile />}
              <Text variant="t200">{fileTypeText}</Text>
            </FileInfoType>
            {size === 'compact' && isReconnecting ? (
              <Text variant="t200" secondary>
                Trying to reconnect...({uploadStatus.retries})
              </Text>
            ) : (
              <FileInfoDetails size={size}>
                {assetDimension && (
                  <Text variant="t200" secondary>
                    {assetDimension}
                  </Text>
                )}
                {assetSize && (
                  <Text variant="t200" secondary>
                    {assetSize}
                  </Text>
                )}
              </FileInfoDetails>
            )}
          </FileInfo>
        </FileInfoContainer>
        {renderStatusMessage()}
      </FileLineContainer>
      <ImageCropModal ref={thumbnailDialogRef} imageType="videoThumbnail" onConfirm={handleCropConfirm} />
      <ImageCropModal ref={avatarDialogRef} imageType="avatar" onConfirm={handleCropConfirm} />
      <ImageCropModal ref={coverDialogRef} imageType="cover" onConfirm={handleCropConfirm} />
    </>
  )
}
Example #25
Source File: VideoTileDetails.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
VideoTileDetails: React.FC<VideoTileDetailsProps> = ({
  videoTitle,
  onVideoTitleClick,
  videoSubTitle,
  videoHref,
  views,
  createdAt,
  channelTitle,
  channelHref,
  onChannelAvatarClick,
  size = 'medium',
  channelAvatarUrl,
  loadingAvatar,
  loading,
  kebabMenuItems = [],
  variant = 'withChannelNameAndAvatar',
}) => {
  return (
    <VideoDetailsContainer>
      {variant === 'withChannelNameAndAvatar' && (
        <StyledAvatar
          assetUrl={channelAvatarUrl}
          loading={loadingAvatar}
          onClick={onChannelAvatarClick}
          smallGap={size === 'small'}
        />
      )}
      <SwitchTransition>
        <CSSTransition
          timeout={parseInt(cVar('animationTimingFast', true))}
          key={String(loading)}
          classNames={transitions.names.fade}
        >
          <VideoInfoContainer>
            {loading ? (
              <SkeletonLoader height={24} width="60%" />
            ) : (
              <LinkWrapper to={videoHref}>
                <VideoTitle onClick={onVideoTitleClick} variant={size === 'medium' ? 'h400' : 'h300'}>
                  {videoTitle}
                </VideoTitle>
              </LinkWrapper>
            )}
            <VideoMetaContainer>
              {variant !== 'withoutChannel' &&
                (loading ? (
                  <SkeletonLoader height={16} width="100%" bottomSpace={8} />
                ) : (
                  <LinkWrapper to={channelHref}>
                    <ChannelTitle variant="t200" secondary as="p">
                      {channelTitle}
                    </ChannelTitle>
                  </LinkWrapper>
                ))}
              {loading ? (
                <SkeletonLoader height={variant === 'withoutChannel' ? 20 : 16} width="100%" />
              ) : (
                <Text variant="t200" secondary as="p">
                  {videoSubTitle
                    ? videoSubTitle
                    : createdAt && (
                        <>
                          {formatVideoDate(createdAt)} • <Views>{formatVideoViews(views || 0)}</Views>
                        </>
                      )}
                </Text>
              )}
            </VideoMetaContainer>
          </VideoInfoContainer>
        </CSSTransition>
      </SwitchTransition>
      {kebabMenuItems.length > 0 && !loading && (
        <ContextMenu
          placement="bottom-end"
          items={kebabMenuItems}
          trigger={
            <KebabMenuButtonIcon onClick={() => null} variant="tertiary" size="small" smallGap={size === 'small'}>
              <SvgActionMore />
            </KebabMenuButtonIcon>
          }
        />
      )}
    </VideoDetailsContainer>
  )
}
Example #26
Source File: VideoThumbnail.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
VideoThumbnail = forwardRef<HTMLAnchorElement, VideoThumbnailProps>(
  (
    {
      loading,
      videoHref,
      linkState,
      slots,
      thumbnailUrl,
      thumbnailAlt,
      onClick,
      clickable = true,
      contentSlot,
      onMouseEnter,
      onMouseLeave,
    },
    ref
  ) => {
    const [activeDisabled, setActiveDisabled] = useState(false)
    const slotsArray = slots && Object.entries(slots)

    const handleClick = (e: React.MouseEvent) => {
      if (!videoHref) {
        e.preventDefault()
      }
      clickable && onClick?.()
    }

    return (
      <VideoThumbnailContainer
        ref={ref}
        onMouseEnter={onMouseEnter}
        onMouseLeave={onMouseLeave}
        onClick={handleClick}
        clickable={clickable}
        activeDisabled={activeDisabled}
        to={videoHref ? videoHref : ''}
        state={linkState}
      >
        <ContentOverlay>
          <SwitchTransition>
            <CSSTransition
              key={String(loading)}
              timeout={parseInt(cVar('animationTimingFast', true))}
              classNames={transitions.names.fade}
            >
              {loading ? (
                <ThumbnailSkeletonLoader />
              ) : (
                <ThumbnailBackground>
                  {thumbnailUrl && <ThumbnailImage src={thumbnailUrl || ''} alt={thumbnailAlt || ''} />}
                </ThumbnailBackground>
              )}
            </CSSTransition>
          </SwitchTransition>
          {contentSlot && (
            <CSSTransition
              in={!!contentSlot}
              timeout={parseInt(cVar('animationTimingFast', true))}
              classNames={transitions.names.fade}
            >
              <ContentContainer>{contentSlot}</ContentContainer>
            </CSSTransition>
          )}
        </ContentOverlay>
        <HoverOverlay loading={loading} />
        <SlotsOverlay>
          {slotsArray?.map(
            ([position, properties]) =>
              properties && (
                <SlotContainer
                  key={position}
                  type={properties.type}
                  halfWidth={properties.halfWidth}
                  position={position as keyof SlotsObject}
                  onMouseEnter={() => clickable && properties.clickable && setActiveDisabled(true)}
                  onMouseLeave={() => clickable && properties.clickable && setActiveDisabled(false)}
                >
                  {properties.element}
                </SlotContainer>
              )
          )}
        </SlotsOverlay>
      </VideoThumbnailContainer>
    )
  }
)
Example #27
Source File: VideoHero.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
VideoHero: React.FC<VideoHeroProps> = ({
  videoHeroData = null,
  headerNode,
  isCategory,
  sliderNode,
  withMuteButton,
  onTimeUpdate,
  onEnded,
}) => {
  const smMatch = useMediaMatch('sm')
  const xsMatch = useMediaMatch('xs')

  const [soundMuted, setSoundMuted] = useState(true)

  const handleSoundToggleClick = () => {
    setSoundMuted(!soundMuted)
  }

  const handleEnded = (e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
    onEnded?.(e)
  }

  return (
    <Container isCategory={isCategory}>
      <BackgroundContainer isCategory={isCategory}>
        {videoHeroData && (
          <BackgroundVideoPlayer
            preload="metadata"
            muted={soundMuted}
            autoPlay
            onTimeUpdate={onTimeUpdate}
            poster={videoHeroData.heroPosterUrl ?? undefined}
            onEnded={handleEnded}
            src={videoHeroData?.heroVideoCutUrl}
          />
        )}
        <GradientOverlay />
      </BackgroundContainer>
      {sliderNode && sliderNode}
      {headerNode && headerNode}
      <InfoContainer isCategory={isCategory}>
        <StyledLayoutGrid>
          <GridItem colSpan={{ xxs: 12, sm: 6 }}>
            <StyledChannelLink textSecondary id={videoHeroData?.video?.channel?.id} />
            <TitleContainer>
              <SwitchTransition>
                <CSSTransition
                  key={videoHeroData ? 'data' : 'placeholder'}
                  classNames={transitions.names.fade}
                  timeout={parseInt(transitions.timings.regular)}
                >
                  {videoHeroData ? (
                    <Link to={absoluteRoutes.viewer.video(videoHeroData.video?.id)}>
                      <TitleText isCategory={isCategory} variant={smMatch ? 'h700' : 'h500'}>
                        {videoHeroData.heroTitle}
                      </TitleText>
                    </Link>
                  ) : smMatch ? (
                    <SkeletonLoader height={48} width={360} />
                  ) : (
                    <div>
                      <SkeletonLoader height={30} width="100%" bottomSpace={4} />
                      <SkeletonLoader height={30} width={240} />
                    </div>
                  )}
                </CSSTransition>
              </SwitchTransition>
            </TitleContainer>
          </GridItem>
        </StyledLayoutGrid>
        <SwitchTransition>
          <CSSTransition
            key={videoHeroData ? 'data' : 'placeholder'}
            classNames={transitions.names.fade}
            timeout={parseInt(transitions.timings.regular)}
          >
            {videoHeroData ? (
              <ButtonsContainer>
                <Button
                  size={xsMatch ? 'large' : 'medium'}
                  fullWidth={!xsMatch}
                  to={absoluteRoutes.viewer.video(videoHeroData.video?.id)}
                  icon={<SvgActionPlayAlt />}
                >
                  Play
                </Button>
                {withMuteButton && (
                  <IconButton size={smMatch ? 'large' : 'medium'} variant="secondary" onClick={handleSoundToggleClick}>
                    {!soundMuted ? <SvgActionSoundOn /> : <SvgActionSoundOff />}
                  </IconButton>
                )}
              </ButtonsContainer>
            ) : (
              <ButtonsContainer>
                <SkeletonLoader width={xsMatch ? '96px' : '100%'} height={xsMatch ? '48px' : '40px'} />
                {withMuteButton && (
                  <SkeletonLoader width={smMatch ? '48px' : '40px'} height={smMatch ? '48px' : '40px'} />
                )}
              </ButtonsContainer>
            )}
          </CSSTransition>
        </SwitchTransition>
      </InfoContainer>
    </Container>
  )
}
Example #28
Source File: TopbarViewer.tsx    From atlas with GNU General Public License v3.0 4 votes vote down vote up
TopbarViewer: React.FC = () => {
  const { activeAccountId, extensionConnected, activeMemberId, activeMembership, signIn } = useUser()
  const [isMemberDropdownActive, setIsMemberDropdownActive] = useState(false)

  const isLoggedIn = activeAccountId && !!activeMemberId && !!extensionConnected

  const { url: memberAvatarUrl, isLoadingAsset: memberAvatarLoading } = useMemberAvatar(activeMembership)

  const { pathname, search } = useLocation()
  const mdMatch = useMediaMatch('md')
  const { incrementOverlaysOpenCount, decrementOverlaysOpenCount } = useOverlayManager()
  const {
    searchOpen,
    searchQuery,
    actions: { setSearchOpen, setSearchQuery },
  } = useSearchStore()

  useEffect(() => {
    if (searchOpen) {
      incrementOverlaysOpenCount()
    } else {
      decrementOverlaysOpenCount()
    }
  }, [searchOpen, incrementOverlaysOpenCount, decrementOverlaysOpenCount])

  // set input search query on results page
  useEffect(() => {
    if (pathname.includes(absoluteRoutes.viewer.search())) {
      if (search) {
        const params = new URLSearchParams(search)
        const query = params.get(QUERY_PARAMS.SEARCH)
        setSearchQuery(query || '')
      }
    }
  }, [pathname, search, setSearchQuery])

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setSearchOpen(true)
    setSearchQuery(event.currentTarget.value)
  }

  const onClose = useCallback(() => {
    setSearchOpen(false)
  }, [setSearchOpen])

  const handleFocus = () => {
    setSearchOpen(true)
  }

  const handleCancel = () => {
    setSearchQuery('')
  }

  const handleDrawerToggle = (e: React.MouseEvent<HTMLElement>) => {
    e.stopPropagation()
    setIsMemberDropdownActive(!isMemberDropdownActive)
  }

  const topbarButtonLoaded = extensionConnected !== null

  return (
    <>
      <StyledTopbarBase
        hasFocus={searchOpen}
        noLogo={!mdMatch && !!searchQuery}
        fullLogoNode={<SvgJoystreamLogoFull />}
        logoLinkUrl={absoluteRoutes.viewer.index()}
      >
        <SearchbarContainer>
          <CSSTransition classNames="searchbar" in={searchOpen} timeout={0}>
            <Searchbar
              placeholder="Search..."
              onChange={handleChange}
              onFocus={handleFocus}
              onCancel={handleCancel}
              showCancelButton={!!searchQuery}
              onClose={onClose}
              controlled
              onClick={handleFocus}
            />
          </CSSTransition>
        </SearchbarContainer>
        <SwitchTransition>
          <CSSTransition
            key={String(topbarButtonLoaded)}
            mountOnEnter
            classNames={transitions.names.fade}
            timeout={parseInt(cVar('animationTimingFast', true))}
          >
            <ButtonWrapper>
              {topbarButtonLoaded ? (
                isLoggedIn ? (
                  <SignedButtonsWrapper>
                    <NotificationsWidget trigger={<NotificationsButton />} />
                    {!mdMatch && !searchOpen && (
                      <StyledAvatar
                        size="small"
                        assetUrl={memberAvatarUrl}
                        loading={memberAvatarLoading}
                        onClick={handleDrawerToggle}
                      />
                    )}
                    {mdMatch && (
                      <StyledAvatar
                        size="small"
                        assetUrl={memberAvatarUrl}
                        onClick={handleDrawerToggle}
                        loading={memberAvatarLoading}
                      />
                    )}
                  </SignedButtonsWrapper>
                ) : (
                  mdMatch && (
                    <Button icon={<SvgActionMember />} iconPlacement="left" size="medium" onClick={signIn}>
                      Sign In
                    </Button>
                  )
                )
              ) : (
                <SignedButtonsWrapper>
                  <StyledButtonSkeletonLoader width={mdMatch ? 102 : 78} height={40} />
                </SignedButtonsWrapper>
              )}
              {!searchQuery && !mdMatch && !isLoggedIn && topbarButtonLoaded && (
                <StyledIconButton onClick={signIn}>Sign In</StyledIconButton>
              )}
            </ButtonWrapper>
          </CSSTransition>
        </SwitchTransition>
        <CSSTransition classNames="searchbar-overlay" in={searchOpen} timeout={0} unmountOnExit mountOnEnter>
          <Overlay onClick={onClose} />
        </CSSTransition>
      </StyledTopbarBase>
      <MemberDropdown isActive={isMemberDropdownActive} closeDropdown={() => setIsMemberDropdownActive(false)} />
    </>
  )
}