Skip to content

Instantly share code, notes, and snippets.

@artemartemov
Last active June 9, 2020 14:12
Show Gist options
  • Save artemartemov/db224a5c8f0c34725f2a6fe3e48f6f7b to your computer and use it in GitHub Desktop.
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)
// 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;
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 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 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,
},
};
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 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 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 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;
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,
},
};
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;
// 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