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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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' }}> | </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' }}> | </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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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)} />
</>
)
}