From d7f449ad3f48bbd2fb0eabfcb5d6ca0e9bc98cbf Mon Sep 17 00:00:00 2001 From: WRadoslaw <92513933+WRadoslaw@users.noreply.github.com> Date: Sun, 13 Aug 2023 19:17:24 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=B9=20Video=20trailer=20picker=20(#462?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial work * Further work on video selection * Handle ratio * Add playground --- .../components/ListItem/ListItem.styles.ts | 3 + .../src/components/ListItem/ListItem.tsx | 9 +- .../_crt/VideoPicker/VideoPicker.styles.ts | 83 ++++++++ .../_crt/VideoPicker/VideoPicker.tsx | 188 ++++++++++++++++++ .../src/components/_crt/VideoPicker/index.ts | 1 + .../VideoListItem/VideoListItem.styles.ts | 59 ++++++ .../_video/VideoListItem/VideoListItem.tsx | 136 +++++++++++++ .../VideoListItem/VideoListItemLoader.tsx | 18 ++ .../components/_video/VideoListItem/index.ts | 2 + .../src/views/playground/PlaygroundLayout.tsx | 2 + .../Playgrounds/PlaygroundVideoPicker.tsx | 9 + .../src/views/playground/Playgrounds/index.ts | 1 + 12 files changed, 510 insertions(+), 1 deletion(-) create mode 100644 packages/atlas/src/components/_crt/VideoPicker/VideoPicker.styles.ts create mode 100644 packages/atlas/src/components/_crt/VideoPicker/VideoPicker.tsx create mode 100644 packages/atlas/src/components/_crt/VideoPicker/index.ts create mode 100644 packages/atlas/src/components/_video/VideoListItem/VideoListItem.styles.ts create mode 100644 packages/atlas/src/components/_video/VideoListItem/VideoListItem.tsx create mode 100644 packages/atlas/src/components/_video/VideoListItem/VideoListItemLoader.tsx create mode 100644 packages/atlas/src/components/_video/VideoListItem/index.ts create mode 100644 packages/atlas/src/views/playground/Playgrounds/PlaygroundVideoPicker.tsx diff --git a/packages/atlas/src/components/ListItem/ListItem.styles.ts b/packages/atlas/src/components/ListItem/ListItem.styles.ts index c16488f079..989f43261b 100644 --- a/packages/atlas/src/components/ListItem/ListItem.styles.ts +++ b/packages/atlas/src/components/ListItem/ListItem.styles.ts @@ -125,9 +125,12 @@ type NodeContainerProps = { destructive?: boolean isHovering?: boolean isSelected?: boolean + position?: 'top' | 'bottom' } export const NodeContainer = styled.div` ${iconStyles}; + + align-self: ${(props) => (props.position === 'top' ? 'flex-start' : 'unset')}; ` export const SeparatorWrapper = styled.div` diff --git a/packages/atlas/src/components/ListItem/ListItem.tsx b/packages/atlas/src/components/ListItem/ListItem.tsx index 950e4693f0..6652cbbfaf 100644 --- a/packages/atlas/src/components/ListItem/ListItem.tsx +++ b/packages/atlas/src/components/ListItem/ListItem.tsx @@ -44,6 +44,7 @@ export type ListItemProps = { title: string description: string } + nodeEndPosition?: 'top' | 'bottom' } export const ListItem = forwardRef( @@ -67,6 +68,7 @@ export const ListItem = forwardRef( externalLink, isSeparator, isInteractive = true, + nodeEndPosition, }, ref ) => { @@ -120,7 +122,12 @@ export const ListItem = forwardRef( {selected && } {!!nodeEnd && ( - + {nodeEnd} )} diff --git a/packages/atlas/src/components/_crt/VideoPicker/VideoPicker.styles.ts b/packages/atlas/src/components/_crt/VideoPicker/VideoPicker.styles.ts new file mode 100644 index 0000000000..0ee2884a7e --- /dev/null +++ b/packages/atlas/src/components/_crt/VideoPicker/VideoPicker.styles.ts @@ -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)}; +` diff --git a/packages/atlas/src/components/_crt/VideoPicker/VideoPicker.tsx b/packages/atlas/src/components/_crt/VideoPicker/VideoPicker.tsx new file mode 100644 index 0000000000..751c0cd2a3 --- /dev/null +++ b/packages/atlas/src/components/_crt/VideoPicker/VideoPicker.tsx @@ -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 ( + + setShowPicker(false)} + onVideoSelection={(id) => { + setSelectedVideo(id) + setShowPicker(false) + }} + memberId={memberId || undefined} + /> + {data?.videos[0] ? ( + + + setSelectedVideo(null)}> + + + Clear selection + + + + ) : ( + + + + + Token video trailer + + + Present yourself, your idea and your project. Tell people why they should invest in you. + + + + + )} + + ) +} + +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 ( + , + iconPlacement: 'right', + } + : undefined + } + > + {hasNoVideos ? ( + + + + + You don’t have any video uploaded yet + + + You need to upload a video first in order to select it as a video trailer for your token. + + + + ) : ( + <> + + setSearch(e.target.value)} + nodeStart={} + placeholder="Search for video" + /> + + + + {loading + ? Array.from({ length: 5 }, (_, idx) => ) + : data?.videos.map((video) => ( + onVideoSelection(video.id)} + variant="small" + id={video.id} + /> + ))} + + + )} + + ) +} diff --git a/packages/atlas/src/components/_crt/VideoPicker/index.ts b/packages/atlas/src/components/_crt/VideoPicker/index.ts new file mode 100644 index 0000000000..5a65e95cca --- /dev/null +++ b/packages/atlas/src/components/_crt/VideoPicker/index.ts @@ -0,0 +1 @@ +export * from './VideoPicker' diff --git a/packages/atlas/src/components/_video/VideoListItem/VideoListItem.styles.ts b/packages/atlas/src/components/_video/VideoListItem/VideoListItem.styles.ts new file mode 100644 index 0000000000..3aff6a036a --- /dev/null +++ b/packages/atlas/src/components/_video/VideoListItem/VideoListItem.styles.ts @@ -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; + } + } + ` + : ''} +` diff --git a/packages/atlas/src/components/_video/VideoListItem/VideoListItem.tsx b/packages/atlas/src/components/_video/VideoListItem/VideoListItem.tsx new file mode 100644 index 0000000000..d8a1aab83b --- /dev/null +++ b/packages/atlas/src/components/_video/VideoListItem/VideoListItem.tsx @@ -0,0 +1,136 @@ +import { FC } from 'react' +import { CSSTransition, SwitchTransition } from 'react-transition-group' + +import { useBasicVideo } from '@/api/hooks/video' +import { SvgActionMore } from '@/assets/icons' +import { ListItemProps } from '@/components/ListItem' +import { Pill } from '@/components/Pill' +import { Text } from '@/components/Text' +import { Button } from '@/components/_buttons/Button' +import { ContextMenu } from '@/components/_overlays/ContextMenu' +import { VideoListItemLoader } from '@/components/_video/VideoListItem/VideoListItemLoader' +import { VideoThumbnail } from '@/components/_video/VideoThumbnail' +import { Views } from '@/components/_video/VideoTileDetails/VideoTileDetails.styles' +import { cVar, transitions } from '@/styles' +import { SentryLogger } from '@/utils/logs' +import { formatDurationShort } from '@/utils/time' +import { formatVideoDate } from '@/utils/video' + +import { ContextMenuWrapper, DetailsWrapper, StyledListItem, ThumbnailContainer } from './VideoListItem.styles' + +type VideoListItemProps = { + id?: string + onClick?: () => void + isSelected?: boolean + variant?: 'small' | 'large' + className?: string + isInteractive?: boolean + menuItems?: ListItemProps[] +} + +export const VideoListItem: FC = ({ + id, + onClick, + isSelected, + className, + variant = 'small', + isInteractive = true, + menuItems, +}) => { + const { video, loading } = useBasicVideo(id ?? '', { + skip: !id, + onError: (error) => SentryLogger.error('Failed to fetch video', 'VideoListItem', error, { video: { id } }), + }) + + return ( + + + {loading ? ( + + ) : ( + + {menuItems && ( + + null} icon={} variant="tertiary" size="small" />} + /> + + )} + + } + nodeStart={ + + + ), + type: 'default', + }, + } + : undefined + } + /> + + } + label={ + + {video?.title} + + } + caption={ + video && ( + + <> + {formatVideoDate(video.createdAt)} •{' '} + +  views + {' '} + + ) + } + /> + )} + + + ) +} diff --git a/packages/atlas/src/components/_video/VideoListItem/VideoListItemLoader.tsx b/packages/atlas/src/components/_video/VideoListItem/VideoListItemLoader.tsx new file mode 100644 index 0000000000..66aa4cb00d --- /dev/null +++ b/packages/atlas/src/components/_video/VideoListItem/VideoListItemLoader.tsx @@ -0,0 +1,18 @@ +import { FC } from 'react' + +import { SkeletonLoader } from '@/components/_loaders/SkeletonLoader' +import { sizes } from '@/styles' + +import { StyledListItem } from './VideoListItem.styles' + +export const VideoListItemLoader: FC<{ variant: 'small' | 'large' }> = ({ variant }) => { + return ( + } + caption={} + nodeStart={} + ignoreRWD={variant === 'small'} + /> + ) +} diff --git a/packages/atlas/src/components/_video/VideoListItem/index.ts b/packages/atlas/src/components/_video/VideoListItem/index.ts new file mode 100644 index 0000000000..85d4b479a6 --- /dev/null +++ b/packages/atlas/src/components/_video/VideoListItem/index.ts @@ -0,0 +1,2 @@ +export * from './VideoListItem' +export * from './VideoListItemLoader' diff --git a/packages/atlas/src/views/playground/PlaygroundLayout.tsx b/packages/atlas/src/views/playground/PlaygroundLayout.tsx index 28191643a5..fbe0847f5b 100644 --- a/packages/atlas/src/views/playground/PlaygroundLayout.tsx +++ b/packages/atlas/src/views/playground/PlaygroundLayout.tsx @@ -18,6 +18,7 @@ import { ConnectionStatusManager } from '@/providers/connectionStatus' import { useUser } from '@/providers/user/user.hooks' import { UserProvider } from '@/providers/user/user.provider' import { cVar } from '@/styles' +import { PlaygroundVideoPicker } from '@/views/playground/Playgrounds/PlaygroundVideoPicker' import { PlaygroundCaptcha, @@ -49,6 +50,7 @@ const playgroundRoutes = [ { path: 'input-autocomplete', element: , name: 'Input autocomplete' }, { path: 'marketplace-carousel', element: , name: 'Marketplace carousel' }, { path: 'sign-up', element: , name: 'Sign up' }, + { path: 'video-picker', element: , name: 'Video picker' }, ] export const PlaygroundLayout = () => { diff --git a/packages/atlas/src/views/playground/Playgrounds/PlaygroundVideoPicker.tsx b/packages/atlas/src/views/playground/Playgrounds/PlaygroundVideoPicker.tsx new file mode 100644 index 0000000000..6e672a8693 --- /dev/null +++ b/packages/atlas/src/views/playground/Playgrounds/PlaygroundVideoPicker.tsx @@ -0,0 +1,9 @@ +import { useState } from 'react' + +import { VideoPicker } from '@/components/_crt/VideoPicker' + +export const PlaygroundVideoPicker = () => { + const [selectedVideo, setSelectedVideo] = useState(null) + + return +} diff --git a/packages/atlas/src/views/playground/Playgrounds/index.ts b/packages/atlas/src/views/playground/Playgrounds/index.ts index e472d652ab..1c53e617c4 100644 --- a/packages/atlas/src/views/playground/Playgrounds/index.ts +++ b/packages/atlas/src/views/playground/Playgrounds/index.ts @@ -10,3 +10,4 @@ export * from './PlaygroundCaptcha' export * from './PlaygroundGoogleAuthentication' export * from './PlaygroundInputAutocomplete' export * from './PlaygroundSignUp' +export * from './PlaygroundVideoPicker'