Last active
June 9, 2020 14:12
-
-
Save artemartemov/db224a5c8f0c34725f2a6fe3e48f6f7b to your computer and use it in GitHub Desktop.
Fullpage Backgroud Video: including all components and schemas for gatsby frontend and sanity.io backend respecitvely -- demo can be found on: https://cameronmichael.io/ (WIP)
This file contains hidden or 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
| // loader component | |
| import React, { PureComponent } from 'react'; | |
| import PropTypes from 'prop-types'; | |
| import styled, { keyframes } from 'styled-components'; | |
| import LoadingIcon from 'react-feather/dist/icons/loader'; | |
| const spin = keyframes` | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| `; | |
| const Spinner = styled(LoadingIcon).attrs(({ size, color }) => ({ | |
| size: size || '48', | |
| color: color || 'red', | |
| }))` | |
| transform-origin: center center; | |
| animation: ${spin} 1.5s linear infinite; | |
| `; | |
| /** | |
| * Loading component used to display loading animation or an error message if the | |
| * content cannot be loaded. | |
| */ | |
| class Loader extends PureComponent { | |
| // Store timeout ID to later clear out on unmount | |
| delayTimeout = null; | |
| // eslint-disable-next-line react/state-in-constructor | |
| state = { | |
| // eslint-disable-next-line react/destructuring-assignment | |
| show: !this.props.delay, | |
| }; | |
| componentDidMount() { | |
| const { delay } = this.props; | |
| if (delay) { | |
| this.delayShow(delay); | |
| } | |
| } | |
| componentWillUnmount() { | |
| clearTimeout(this.delayTimeout); | |
| } | |
| /** | |
| * Delay rendering of spinner | |
| * | |
| * @param {number} delay Milliseconds to delay render | |
| */ | |
| delayShow(delay) { | |
| // Store timeout ID to later clear out on unmount | |
| this.delayTimeout = setTimeout(() => { | |
| this.setState({ | |
| show: true, | |
| }); | |
| }, delay); | |
| } | |
| render() { | |
| const { size, color } = this.props; | |
| const { show } = this.state; | |
| return <>{show && <Spinner size={size} color={color} />}</>; | |
| } | |
| } | |
| Loader.propTypes = { | |
| /** | |
| * Size of Spinner Icon | |
| */ | |
| size: PropTypes.string, | |
| /** | |
| * Color of Spinner Icon | |
| */ | |
| color: PropTypes.string, | |
| /** | |
| * How long to wait to render Spinner | |
| */ | |
| delay: PropTypes.number, | |
| }; | |
| Loader.defaultProps = { | |
| size: '48', | |
| color: 'red', | |
| delay: null, | |
| }; | |
| export default Loader; |
This file contains hidden or 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
| export default { | |
| type: 'object', | |
| name: 'homeVideoAssets', | |
| title: 'Home Video Assets', | |
| options: { | |
| collapsible: true, | |
| collapsed: false, | |
| }, | |
| fieldsets: [ | |
| { | |
| name: 'homeVideoAssetFields', | |
| title: 'Home video assets', | |
| }, | |
| ], | |
| fields: [ | |
| { | |
| name: 'homeVideo', | |
| type: 'array', | |
| title: 'Home Video', | |
| description: 'Please add Vimeo or Youtube URL', | |
| validation: Rule => Rule.length(1), | |
| options: { | |
| editModal: 'popover', | |
| sortable: false, | |
| }, | |
| of: [ | |
| { | |
| title: 'Video', | |
| type: 'videoEmbed', | |
| }, | |
| ], | |
| }, | |
| { | |
| name: 'homeVideoLoop', | |
| type: 'array', | |
| title: 'Home Video Loop', | |
| description: 'Please add a fallback loop for the homepage video. Only supporting .mp4 file type.', | |
| validation: Rule => Rule.length(1), | |
| options: { | |
| editModal: 'popover', | |
| sortable: false, | |
| }, | |
| of: [ | |
| { | |
| title: 'Video Loops', | |
| type: 'videoUpload', | |
| }, | |
| ], | |
| }, | |
| { | |
| name: 'homeVideoImage', | |
| type: 'figure', | |
| title: 'Home Video Fallback Image', | |
| validation: Rule => Rule.required(), | |
| description: | |
| 'Please provide image that will be used in case the video does not load. Also used in mobile devices.', | |
| }, | |
| ], | |
| }; |
This file contains hidden or 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 is my index.js file that includes a few other things, I removed some extra code below the video: | |
| import React from 'react'; | |
| import { graphql, Link } from 'gatsby'; | |
| import getVideoId from 'get-video-id'; | |
| import PropTypes from 'prop-types'; | |
| import styled, { keyframes } from 'styled-components'; | |
| import { | |
| Container, | |
| GraphQLErrorList, | |
| SEO, | |
| Layout, | |
| Video, | |
| VideoModal, | |
| useToggle, | |
| } from 'components'; | |
| import { getFixedGatsbyImage } from 'gatsby-source-sanity'; | |
| import imageUrlBuilder from '@sanity/image-url'; | |
| import { animated } from 'react-spring'; | |
| import { colors, mq } from 'utils'; | |
| import { Spring } from 'react-spring/renderprops'; | |
| import PlayButton from 'react-feather/dist/icons/play'; | |
| import clientConfig from '../../client-config'; | |
| import ArrowDown from '../components/icons/arrowdown.svg'; | |
| export const query = graphql` | |
| query IndexPageQuery { | |
| site: sanitySiteSettings(_id: { regex: "/(drafts.|)siteSettings/" }) { | |
| title | |
| description | |
| keywords | |
| } | |
| homepage: sanityIndexPage(_id: { eq: "indexPage" }) { | |
| _rawHomeVideoAssets | |
| _rawBody | |
| homeVideoAssets { | |
| homeVideoLoop { | |
| source { | |
| asset { | |
| url | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| `; | |
| const VideoWrapper = styled.div` | |
| overflow: hidden; | |
| position: relative; | |
| &:after { | |
| background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.64) 75%, rgba(0, 0, 0, 1) 100%); | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| } | |
| `; | |
| const PlayReelButton = styled(PlayButton).attrs({ | |
| size: '14', | |
| color: '#ffffff', | |
| })` | |
| transition: stroke 0.2s ease-in-out; | |
| `; | |
| const PlayReelContainer = styled(animated(Link))` | |
| display: none; | |
| visibility: hidden; | |
| ${mq.tabletWide` | |
| display: block; | |
| visibility: visible; | |
| text-decoration: none; | |
| color: ${colors.white}; | |
| position: fixed; | |
| z-index: 10; | |
| min-width: 150px; | |
| display: inline-flex; | |
| align-items: center; | |
| height: 50px; | |
| border: 1px solid ${colors.white}; | |
| padding: 0 2rem; | |
| background-color: transparent; | |
| transition: background-color 0.2s ease-in-out; | |
| cursor: pointer; | |
| & h5 { | |
| font-family: 'Poppins'; | |
| letter-spacing: 0.5em; | |
| text-transform: uppercase; | |
| font-weight: 300; | |
| margin: 0; | |
| padding-left: 10px; | |
| transition: color 0.2s ease-in-out; | |
| } | |
| `} | |
| ${mq.desktop` | |
| &:hover { | |
| background-color: ${colors.white}; | |
| & h5 { | |
| color: ${colors.black}; | |
| } | |
| ${PlayReelButton} { | |
| stroke: ${colors.black}; | |
| } | |
| } | |
| `} | |
| `; | |
| const pulsate = keyframes` | |
| 0% { | |
| transform: translateY(0); | |
| } | |
| 50% { | |
| transform: translateY(0.25rem); | |
| } | |
| 100% { | |
| transform: translateY(0); | |
| } | |
| `; | |
| const IndexPage = ({ data, errors }) => { | |
| if (errors) { | |
| return ( | |
| <Layout> | |
| <GraphQLErrorList errors={errors} /> | |
| </Layout> | |
| ); | |
| } | |
| const { site, homepage } = data || {}; | |
| const homeVideoUrl = homepage._rawHomeVideoAssets.homeVideo[0].url; | |
| const id = homeVideoUrl && getVideoId(homeVideoUrl).id; | |
| const vimeoUrl = `https://player.vimeo.com/video/${id}?title=0&byline=0&portrait=0`; | |
| const builder = imageUrlBuilder(clientConfig.sanity); | |
| const homeVideoImageAssetId = homepage._rawHomeVideoAssets.homeVideoImage.asset._ref; | |
| const homeVideoImage = homepage._rawHomeVideoAssets.homeVideoImage.asset; | |
| const homeVideoLoop = homepage && homepage.homeVideoAssets.homeVideoLoop[0].source.asset.url; | |
| function urlFor(source) { | |
| return builder.image(source); | |
| } | |
| const fluidProps = getFixedGatsbyImage(homeVideoImageAssetId, { maxWidth: 1800 }, clientConfig.sanity); | |
| const [isOpen, setIsOpen] = useToggle(false); | |
| function toggle() { | |
| setIsOpen(); | |
| } | |
| return ( | |
| <Layout id="top"> | |
| <SEO title={site.title} description={site.description} keywords={site.keywords} /> | |
| <VideoWrapper> | |
| {homeVideoLoop && ( | |
| <Video | |
| url={homeVideoLoop} | |
| poster={urlFor(homeVideoImage)} | |
| image={fluidProps} | |
| srcSet={fluidProps.srcSet} | |
| sizes={fluidProps.sizes} | |
| /> | |
| )} | |
| <ScrollMoreContainer> | |
| <h6>Scroll For More</h6> | |
| <ArrowDown width="100%" height="48px" /> | |
| </ScrollMoreContainer> | |
| <Spring | |
| from={{ opacity: 0, bottom: '-10rem', right: '3rem' }} | |
| to={{ opacity: 1, right: '3rem', bottom: '4rem' }} | |
| config={{ delay: 1000 }} | |
| > | |
| {playReelAnimate => ( | |
| <PlayReelContainer style={playReelAnimate} onClick={toggle} data-scroll to="/#top"> | |
| <PlayReelButton /> | |
| <h5>Play Reel</h5> | |
| </PlayReelContainer> | |
| )} | |
| </Spring> | |
| </VideoWrapper> | |
| {isOpen && <VideoModal onClose={toggle} role="dialog" videoSrc={vimeoUrl} />} | |
| </Layout> | |
| ); | |
| }; | |
| IndexPage.propTypes = { | |
| data: PropTypes.any, | |
| errors: PropTypes.any, | |
| }; | |
| IndexPage.defaultProps = { | |
| data: '', | |
| errors: '', | |
| }; | |
| export default IndexPage; |
This file contains hidden or 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 is how I call the video assets for the indexpage document | |
| export default { | |
| name: 'indexPage', | |
| type: 'document', | |
| title: 'Homepage', | |
| fields: [ | |
| { | |
| name: 'homeVideoAssets', | |
| type: 'homeVideoAssets', | |
| title: 'Home Video Assets', | |
| }, | |
| { | |
| name: 'body', | |
| title: 'Body', | |
| type: 'portableText', | |
| }, | |
| ], | |
| preview: { | |
| select: { | |
| title: 'title', | |
| media: 'heroImage', | |
| subtitle: '/', | |
| }, | |
| prepare({ title = 'No title', media, subtitle = '/' }) { | |
| return { | |
| title, | |
| media, | |
| subtitle, | |
| }; | |
| }, | |
| }, | |
| }; | |
| // VideoUpload.js | |
| import VideoPreviewComponent from './videoPreviewComponent'; | |
| export default { | |
| type: 'object', | |
| name: 'videoUpload', | |
| title: 'Video', | |
| fields: [ | |
| { | |
| type: 'file', | |
| name: 'source', | |
| title: 'Video source', | |
| options: { storeOriginalFilename: true }, | |
| validation: Rule => | |
| Rule.custom(props => { | |
| const assetUrl = props && props.asset && props.asset._ref && props.asset._ref; | |
| const urlValidation = RegExp(/^[^.]+-(mp4|MP4|webm|WEBM)$/).test(assetUrl && assetUrl); | |
| if (typeof props === 'undefined') { | |
| return true; // Allow undefined values | |
| } | |
| return urlValidation && urlValidation ? true : 'Only .mp4 and .webm file types allowed'; | |
| }), | |
| }, | |
| ], | |
| preview: { | |
| select: { | |
| sourceUrl: 'source.asset.url', | |
| }, | |
| component: VideoPreviewComponent, | |
| }, | |
| }; |
This file contains hidden or 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
| mport React from 'react'; | |
| import { createPortal } from 'react-dom'; | |
| import styled, { css } from 'styled-components'; | |
| import PropTypes from 'prop-types'; | |
| import { useKeyboardEvent } from 'components'; | |
| const AbsoluteCenter = css` | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| max-height: calc(100% - 100px); | |
| `; | |
| const ModalWrapper = styled.div` | |
| width: ${props => props.modalWidth}; | |
| max-width: ${props => props.maxModalWidth}; | |
| background: ${props => props.background}; | |
| max-height: ${props => props.maxHeight}; | |
| overflow: ${props => props.overflow || 'scroll'}; | |
| z-index: 999999; | |
| ${props => (props.centered ? AbsoluteCenter : null)}; | |
| `; | |
| const ModalHeader = styled.div` | |
| display: flex; | |
| font-size: 0.6875rem; | |
| line-height: 1.2; | |
| letter-spacing: 0.1rem; | |
| text-transform: uppercase; | |
| font-weight: bold; | |
| color: white; | |
| height: 3rem; | |
| background-color: transparent; | |
| justify-content: flex-end; | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| left: 0; | |
| `; | |
| const IconContainer = styled.div` | |
| height: 3rem; | |
| background: rgba(0, 0, 0, 0.5); | |
| color: white; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 0 20px; | |
| cursor: pointer; | |
| border-radius: 0 0 0 0.25rem; | |
| line-height: 0; | |
| `; | |
| const Modal = ({ isSidePanel, centered, background, modalWidth, maxModalWidth, onClose, children, className }) => { | |
| // useKeyboardEvent('Escape', () => { | |
| // onClose(); | |
| // }); | |
| const modalMarkup = ( | |
| <ModalWrapper | |
| isSidePanel={isSidePanel} | |
| className={className} | |
| modalWidth={modalWidth} | |
| maxModalWidth={maxModalWidth} | |
| background={background} | |
| centered={centered} | |
| onKeyPress={useKeyboardEvent('Escape', () => { | |
| onClose(); | |
| })} | |
| > | |
| <ModalHeader> | |
| <IconContainer onClick={onClose}>Close</IconContainer> | |
| </ModalHeader> | |
| {children} | |
| </ModalWrapper> | |
| ); | |
| return createPortal(modalMarkup, document.body); | |
| }; | |
| Modal.propTypes = { | |
| /** | |
| * Child nodes | |
| */ | |
| children: PropTypes.node.isRequired, | |
| /** | |
| * Close handler for modal button | |
| */ | |
| onClose: PropTypes.func, | |
| /** | |
| * Title text to show in header | |
| */ | |
| title: PropTypes.string.isRequired, | |
| /** | |
| * Whether to show a close icon | |
| */ | |
| showCloseIcon: PropTypes.bool, | |
| }; | |
| Modal.defaultProps = { | |
| onClose: null, | |
| showCloseIcon: true, | |
| }; | |
| export default Modal; |
This file contains hidden or 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 probably needs a bit of work, but including just in case | |
| import { useEffect } from 'react'; | |
| function useKeyboardEvent(key, callback) { | |
| useEffect(() => { | |
| const handler = function(event) { | |
| if (event.key === key) { | |
| callback(); | |
| } | |
| }; | |
| window.addEventListener('keydown', handler); | |
| return () => { | |
| window.removeEventListener('keydown', handler); | |
| }; | |
| }, []); | |
| } | |
| export default useKeyboardEvent; |
This file contains hidden or 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 is a toggle hook I like to use: | |
| import { useState, useCallback } from 'react'; | |
| const useToggle = initial => { | |
| const [open, setOpen] = useState(initial); | |
| return [open, useCallback(() => setOpen(status => !status))]; | |
| }; | |
| export default useToggle; |
This file contains hidden or 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 is the video component | |
| import React from 'react'; | |
| function Video({ url, poster, image, srcSet, sizes }) { | |
| // const url = value && value.sourceUrl && value.sourceUrl; | |
| return ( | |
| // eslint-disable-next-line jsx-a11y/media-has-caption | |
| <video | |
| style={{ width: '100%', height: '100vh', objectFit: 'cover' }} | |
| autoPlay | |
| loop | |
| muted | |
| playsInline | |
| poster={`${poster}?fit=fillmax&q=50&w=50`} | |
| > | |
| <source type="video/mp4" src={url} /> | |
| <source type="video/webm" src={url} /> | |
| <img srcSet={srcSet} sizes={sizes} src={`${poster}?fit=fillmax&q=80&w=1600`} alt="Alt Text" /> | |
| </video> | |
| ); | |
| } | |
| export default Video; |
This file contains hidden or 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
| import React from 'react'; | |
| import getVideoId from 'get-video-id'; | |
| import { AlertCircle } from 'react-feather'; | |
| import styled from 'styled-components'; | |
| const AlertWrapper = styled.div` | |
| height: 55px; | |
| border-left: solid 10px #e2093b; | |
| display: flex; | |
| align-items: center; | |
| padding-left: 0.75rem; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| `; | |
| export const GetVideo = ({ value }) => { | |
| const id = value.url && getVideoId(value.url).id; | |
| const service = id && getVideoId(value.url).service; | |
| const youTubeUrl = `https://www.youtube.com/embed/${id}?controls=0&showinfo=0&rel=0&theme=light&fs=0&color=white&disablekb=1`; | |
| const vimeoUrl = `https://player.vimeo.com/video/${id}?title=0&byline=0&portrait=0`; | |
| switch (true) { | |
| case !id: | |
| return ( | |
| <AlertWrapper> | |
| <AlertCircle color="#e66666" style={{ marginRight: '0.25rem' }} /> Please add a video URL | |
| </AlertWrapper> | |
| ); | |
| case id && service === 'youtube': | |
| return ( | |
| <iframe | |
| title="YouTube Preview" | |
| width="100%" | |
| height="360px" | |
| src={youTubeUrl} | |
| frameBorder="0" | |
| allow="accelerometer; encrypted-media; gyroscope;" | |
| /> | |
| ); | |
| case id && service === 'vimeo': | |
| return <iframe title="Vimeo Preview" src={vimeoUrl} width="100%" height="360" frameBorder="0" />; | |
| default: | |
| return null; | |
| } | |
| }; | |
| export default { | |
| name: 'videoEmbed', | |
| type: 'object', | |
| title: 'Video Embed', | |
| fields: [ | |
| { | |
| name: 'url', | |
| type: 'url', | |
| title: 'Enter Youtube or Vimeo URL', | |
| }, | |
| ], | |
| preview: { | |
| select: { | |
| url: 'url', | |
| }, | |
| component: GetVideo, | |
| }, | |
| }; |
This file contains hidden or 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
| import React, { useState } from 'react'; | |
| import styled from 'styled-components'; | |
| import PropTypes from 'prop-types'; | |
| import { Modal, useToggle, Loader } from 'components'; | |
| import ReactPlayer from 'react-player'; | |
| import { colors } from 'utils'; | |
| const VideoModalWrapper = styled(Modal)` | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| bottom: 0; | |
| height: 100%; | |
| overflow: hidden; | |
| width: 100vw; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| `; | |
| const LoaderWrapper = styled.span` | |
| width: 100vw; | |
| height: 100vw; | |
| display: flex; | |
| position: fixed; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 10; | |
| `; | |
| const LoaderIcon = styled(Loader).attrs({ | |
| size: '60', | |
| color: colors.white, | |
| })``; | |
| const VideoModal = ({ videoSrc, onClose }) => { | |
| const [, setIsOpen] = useToggle(false); | |
| const [loading, isLoading] = useState(true); | |
| return ( | |
| <> | |
| <VideoModalWrapper | |
| title="No soup for you" | |
| modalWidth="100%" | |
| maxModalWidth="none" | |
| background="rgba(0,0,0,0.8)" | |
| onClose={onClose} | |
| onSave={() => setIsOpen()} | |
| > | |
| <> | |
| {loading ? ( | |
| <LoaderWrapper> | |
| <LoaderIcon delay={100} /> | |
| </LoaderWrapper> | |
| ) : null} | |
| <ReactPlayer url={videoSrc} playing onReady={() => isLoading()} width="100vw" height="100%" /> | |
| </> | |
| </VideoModalWrapper> | |
| </> | |
| ); | |
| }; | |
| VideoModal.propTypes = { | |
| onClose: PropTypes.func, | |
| videoSrc: PropTypes.string, | |
| }; | |
| VideoModal.defaultProps = { | |
| onClose: () => {}, | |
| videoSrc: '', | |
| }; | |
| export default VideoModal; |
This file contains hidden or 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
| // VideoPreviewComponent.js | |
| import React from 'react'; | |
| function VideoPreviewComponent({ value }) { | |
| const url = value && value.sourceUrl && value.sourceUrl; | |
| const validationCheck = RegExp(/\w+\.(mp4|MP4|webm|WEBM)$/).test(url); | |
| const isMp4 = RegExp(/\w+\.(mp4|MP4)$/).test(url); | |
| if (!url) { | |
| return <pre>Please upload a video loop</pre>; | |
| } | |
| if (validationCheck === false) { | |
| return ( | |
| <pre> | |
| Unsupported filetype: <strong>.mp4</strong>, <strong>.webm</strong> only. | |
| </pre> | |
| ); | |
| } | |
| return ( | |
| // eslint-disable-next-line jsx-a11y/media-has-caption | |
| <video style={{ width: '100%', height: '100%' }} controls autoPlay loop muted> | |
| {isMp4 ? <source type="video/mp4" src={url} /> : <source type="video/webm" src={url} />} | |
| </video> | |
| ); | |
| } | |
| export default VideoPreviewComponent; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment