Skip to content

Commit

Permalink
馃摴 Video trailer picker (#4622)
Browse files Browse the repository at this point in the history
* Initial work

* Further work on video selection

* Handle ratio

* Add playground
  • Loading branch information
WRadoslaw committed Mar 14, 2024
1 parent 9ad8557 commit d7f449a
Show file tree
Hide file tree
Showing 12 changed files with 510 additions and 1 deletion.
3 changes: 3 additions & 0 deletions packages/atlas/src/components/ListItem/ListItem.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,12 @@ type NodeContainerProps = {
destructive?: boolean
isHovering?: boolean
isSelected?: boolean
position?: 'top' | 'bottom'
}
export const NodeContainer = styled.div<NodeContainerProps>`
${iconStyles};
align-self: ${(props) => (props.position === 'top' ? 'flex-start' : 'unset')};
`

export const SeparatorWrapper = styled.div`
Expand Down
9 changes: 8 additions & 1 deletion packages/atlas/src/components/ListItem/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type ListItemProps = {
title: string
description: string
}
nodeEndPosition?: 'top' | 'bottom'
}

export const ListItem = forwardRef<HTMLDivElement, ListItemProps>(
Expand All @@ -67,6 +68,7 @@ export const ListItem = forwardRef<HTMLDivElement, ListItemProps>(
externalLink,
isSeparator,
isInteractive = true,
nodeEndPosition,
},
ref
) => {
Expand Down Expand Up @@ -120,7 +122,12 @@ export const ListItem = forwardRef<HTMLDivElement, ListItemProps>(
</LabelCaptionContainer>
{selected && <SelectedIcon />}
{!!nodeEnd && (
<NodeContainer isSelected={selected} isHovering={isInteractive && isHovering} destructive={destructive}>
<NodeContainer
position={nodeEndPosition}
isSelected={selected}
isHovering={isInteractive && isHovering}
destructive={destructive}
>
{nodeEnd}
</NodeContainer>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import styled from '@emotion/styled'

import { cVar, media, sizes } from '@/styles'

export const MainWrapper = styled.div`
overflow: hidden;
margin-bottom: 20px;
position: relative;
min-height: 280px;
${media.sm} {
padding-top: 0;
aspect-ratio: 16/9;
}
`

export const PlaceholderBox = styled.div`
position: absolute;
display: flex;
flex-direction: column;
gap: ${sizes(6)};
align-items: center;
justify-content: center;
background-color: ${cVar('colorBackgroundMuted')};
padding: ${sizes(4)};
top: 0;
width: 100%;
height: 100%;
${media.sm} {
padding: ${sizes(6)};
}
`

export const TextBox = styled.div`
display: flex;
flex-direction: column;
gap: ${sizes(2)};
align-items: center;
justify-content: center;
max-width: 300px;
text-align: center;
`

export const DialogContent = styled.div`
display: grid;
padding: ${sizes(6)};
`

export const VideoBox = styled.div`
margin-bottom: ${sizes(6)};
`

export const ThumbnailContainer = styled.div`
position: absolute;
top: 0;
width: 100%;
height: 100%;
`

export const ThumbnailOverlay = styled.div`
display: grid;
place-items: center;
align-content: center;
gap: ${sizes(2)};
position: absolute;
inset: 0;
background-color: #101214bf;
opacity: 0;
cursor: pointer;
transition: all ${cVar('animationTransitionFast')};
:hover {
opacity: 1;
}
`

export const RowBox = styled.div<{ gap: number }>`
display: flex;
flex-direction: column;
width: 100%;
gap: ${(props) => sizes(props.gap)};
`
188 changes: 188 additions & 0 deletions packages/atlas/src/components/_crt/VideoPicker/VideoPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { useState } from 'react'

import { useGetBasicVideosQuery } from '@/api/queries/__generated__/videos.generated'
import {
SvgActionNewTab,
SvgActionPlus,
SvgActionSearch,
SvgActionTrash,
SvgAlertsInformative32,
SvgIllustrativeVideo,
} from '@/assets/icons'
import { Text } from '@/components/Text'
import { Button } from '@/components/_buttons/Button'
import { Input } from '@/components/_inputs/Input'
import { DialogModal } from '@/components/_overlays/DialogModal'
import { VideoListItem, VideoListItemLoader } from '@/components/_video/VideoListItem'
import { ThumbnailImage } from '@/components/_video/VideoThumbnail/VideoThumbnail.styles'
import { absoluteRoutes } from '@/config/routes'
import { useDebounceValue } from '@/hooks/useDebounceValue'
import { useMediaMatch } from '@/hooks/useMediaMatch'
import { useUser } from '@/providers/user/user.hooks'

import {
DialogContent,
MainWrapper,
PlaceholderBox,
RowBox,
TextBox,
ThumbnailContainer,
ThumbnailOverlay,
VideoBox,
} from './VideoPicker.styles'

type VideoPickerProps = {
selectedVideo: string | null
setSelectedVideo: (id: string | null) => void
className?: string
}

export const VideoPicker = ({ setSelectedVideo, selectedVideo, className }: VideoPickerProps) => {
const [showPicker, setShowPicker] = useState(false)
const xsMatch = useMediaMatch('xs')
const { memberId } = useUser()
const { data } = useGetBasicVideosQuery({
variables: {
where: {
id_eq: selectedVideo,
},
},
skip: !selectedVideo,
})

return (
<MainWrapper className={className}>
<SelectVideoDialog
show={showPicker}
onClose={() => setShowPicker(false)}
onVideoSelection={(id) => {
setSelectedVideo(id)
setShowPicker(false)
}}
memberId={memberId || undefined}
/>
{data?.videos[0] ? (
<ThumbnailContainer>
<ThumbnailImage resolvedUrls={data?.videos[0].thumbnailPhoto?.resolvedUrls} />
<ThumbnailOverlay onClick={() => setSelectedVideo(null)}>
<SvgActionTrash />
<Text variant="t300" as="p">
Clear selection
</Text>
</ThumbnailOverlay>
</ThumbnailContainer>
) : (
<PlaceholderBox>
<SvgIllustrativeVideo />
<TextBox>
<Text variant="h400" as="h4" color="colorTextStrong">
Token video trailer
</Text>
<Text variant={xsMatch ? 't200' : 't100'} as="p" color="colorText">
Present yourself, your idea and your project. Tell people why they should invest in you.
</Text>
</TextBox>
<Button variant="secondary" icon={<SvgActionPlus />} onClick={() => setShowPicker(true)} size="large">
Select video trailer
</Button>
</PlaceholderBox>
)}
</MainWrapper>
)
}

type SelectVideoDialogProps = {
memberId?: string
onVideoSelection: (id: string) => void
show: boolean
onClose: () => void
}

const SelectVideoDialog = ({ memberId, onVideoSelection, show, onClose }: SelectVideoDialogProps) => {
const [search, setSearch] = useState('')
const debouncedSearch = useDebounceValue(search)
const { data, loading } = useGetBasicVideosQuery({
notifyOnNetworkStatusChange: true,
variables: {
where: {
title_containsInsensitive: debouncedSearch,
channel: {
ownerMember: {
id_eq: memberId,
},
},
},
limit: 5,
},
skip: !memberId,
})

const hasNoVideos = !loading && !data?.videos?.length

return (
<DialogModal
title={!hasNoVideos ? 'Select video trailer' : undefined}
onExitClick={!hasNoVideos ? onClose : undefined}
show={show}
noContentPadding={!hasNoVideos}
secondaryButton={
hasNoVideos
? {
text: 'Cancel',
onClick: onClose,
}
: undefined
}
primaryButton={
hasNoVideos
? {
text: 'Upload a video',
to: absoluteRoutes.studio.videoWorkspace(),
icon: <SvgActionNewTab />,
iconPlacement: 'right',
}
: undefined
}
>
{hasNoVideos ? (
<RowBox gap={6}>
<SvgAlertsInformative32 />
<RowBox gap={2}>
<Text variant="h500" as="h5">
You don鈥檛 have any video uploaded yet
</Text>
<Text variant="t200" as="p" color="colorText">
You need to upload a video first in order to select it as a video trailer for your token.
</Text>
</RowBox>
</RowBox>
) : (
<>
<DialogContent>
<Input
size="large"
value={search}
onChange={(e) => setSearch(e.target.value)}
nodeStart={<SvgActionSearch />}
placeholder="Search for video"
/>
</DialogContent>

<VideoBox>
{loading
? Array.from({ length: 5 }, (_, idx) => <VideoListItemLoader key={idx} variant="small" />)
: data?.videos.map((video) => (
<VideoListItem
key={video.id}
isInteractive
onClick={() => onVideoSelection(video.id)}
variant="small"
id={video.id}
/>
))}
</VideoBox>
</>
)}
</DialogModal>
)
}
1 change: 1 addition & 0 deletions packages/atlas/src/components/_crt/VideoPicker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './VideoPicker'
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { css } from '@emotion/react'
import styled from '@emotion/styled'

import { ListItem } from '@/components/ListItem'
import { cVar, media, sizes } from '@/styles'

export const DetailsWrapper = styled.div<{ variant: 'small' | 'large' }>`
align-self: ${({ variant }) => (variant === 'small' ? 'center' : 'start')};
gap: ${({ variant }) => (variant === 'small' ? 'unset' : sizes(2))};
display: grid;
position: relative;
width: 100%;
`

export const ContextMenuWrapper = styled.div`
position: absolute;
top: 0;
right: 0;
opacity: 1;
transition: opacity ${cVar('animationTransitionFast')};
${media.sm} {
opacity: 0;
}
`

export const ThumbnailContainer = styled.div<{ variant: 'small' | 'large' }>`
> *:first-of-type {
min-width: ${({ variant }) => (variant === 'small' ? '80px' : '197px')};
}
`

export const StyledListItem = styled(ListItem)<{ ignoreRWD?: boolean }>`
:hover {
.video-list-item-kebab {
align-self: flex-start;
opacity: 1;
}
}
${(props) =>
!props.ignoreRWD
? css`
> *:first-of-type {
grid-column: 1/3;
width: 100%;
}
grid-template-columns: 1fr;
grid-template-rows: auto auto;
${media.sm} {
grid-template-columns: auto 1fr;
grid-template-rows: auto;
> *:first-of-type {
grid-column: unset;
}
}
`
: ''}
`
Loading

0 comments on commit d7f449a

Please sign in to comment.