-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Initial work * Further work on video selection * Handle ratio * Add playground
- Loading branch information
Showing
12 changed files
with
510 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
83 changes: 83 additions & 0 deletions
83
packages/atlas/src/components/_crt/VideoPicker/VideoPicker.styles.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
188
packages/atlas/src/components/_crt/VideoPicker/VideoPicker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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’t 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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './VideoPicker' |
59 changes: 59 additions & 0 deletions
59
packages/atlas/src/components/_video/VideoListItem/VideoListItem.styles.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
` | ||
: ''} | ||
` |
Oops, something went wrong.