Skip to content

Commit

Permalink
Merge pull request #928 from ONEARMY/feat/#915-video-embed
Browse files Browse the repository at this point in the history
Feat/#915 video embed
  • Loading branch information
BenGamma authored Mar 31, 2020
2 parents 48febbb + 384d72c commit 0343edd
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 76 deletions.
33 changes: 12 additions & 21 deletions cypress/fixtures/seed/howtos.json
Original file line number Diff line number Diff line change
Expand Up @@ -2214,6 +2214,14 @@
"time": "< 1 week"
},
"gPpPDEvfNT9a6w5FWzaj": {
"moderation": "accepted",
"id": "me5Bq0wq5FdoJUY8gELN",
"_createdBy": "howto_creator",
"tags": {
"36hWyk3OckrLSH1ehdIE": true,
"Wk6RnHHFfKSiI71BlM8r": true
},
"title": "Make an interlocking brick",
"steps": [
{
"images": [
Expand All @@ -2234,19 +2242,9 @@
},
{
"title": "Idea and first drawing",
"images": [
{
"timeCreated": "2019-09-27T15:03:51.356Z",
"name": "howto-bope brick-2.jpg",
"fullPath": "uploads/v2_howtos/me5Bq0wq5FdoJUY8gELN/howto-bope brick-2.jpg",
"type": "image/jpeg",
"updated": "2019-09-27T15:03:51.356Z",
"size": 32393,
"downloadUrl": "https://firebasestorage.googleapis.com/v0/b/onearmyworld.appspot.com/o/uploads%2Fv2_howtos%2Fme5Bq0wq5FdoJUY8gELN%2Fhowto-bope%20brick-2.jpg?alt=media&token=79b1ef96-ceab-4fc1-a5ed-67edf15046b8",
"contentType": "image/jpeg"
}
],
"text": "We wanted to develop a product that can have many functions. So we decided to figure out a shape that can be adapted or compliment one another to get a variety of uses. Finally we decided to draw a curved shape. The idea of this shape is to be attached to each other like a Lego. You can use this design as a plant pot or connect it as a partition and build a wall. ",
"images": [],
"videoUrl": "https://www.youtube.com/watch?v=Os7dREQ00l4&t=5s",
"text": "We wanted to develop a product that can have many functions. So we decided to figure out a shape that can be adapted or compliment one another to get a variety of uses. Finally we decided to draw a curved shape. The idea of this shape is to be attached to each other like a Lego. You can use this design as a plant pot or connect it as a partition and build a wall. Why not check out this super cool video?",
"_animationKey": "unique2"
},
{
Expand Down Expand Up @@ -2470,14 +2468,7 @@
"_animationKey": "uniquec9kbu"
}
],
"moderation": "accepted",
"id": "me5Bq0wq5FdoJUY8gELN",
"_createdBy": "howto_creator",
"tags": {
"36hWyk3OckrLSH1ehdIE": true,
"Wk6RnHHFfKSiI71BlM8r": true
},
"title": "Make an interlocking brick",

"_modified": "2019-09-30T18:27:57.570Z",
"_created": "2019-10-01T20:49:43.452Z",
"slug": "make-an-interlocking-brick",
Expand Down
4 changes: 4 additions & 0 deletions cypress/integration/howto/read.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ describe('[How To]', () => {
.and('match', pic3Regex)
})

cy.step('Video embed exists')
cy.get('[data-cy="video-embed"]')
.find('iframe')
.should(iframe => expect(iframe.contents().find('video')).to.exist)
cy.step('Back button at top takes users to /how-to')
cy.get('[data-cy="go-back"]:eq(0)')
.as('topBackButton')
Expand Down
52 changes: 33 additions & 19 deletions cypress/integration/howto/write.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ describe('[How To]', () => {
title: string,
description: string,
images: string[],
videoUrl?: string,
) => {
const stepIndex = stepNumber - 1
console.log('stepIndex', stepIndex)
cy.step(`Filling step ${stepNumber}`)
cy.get(`[data-cy=step_${stepIndex}]:visible`).within($step => {
cy.get('[data-cy=step-title]')
Expand All @@ -41,21 +43,28 @@ describe('[How To]', () => {
cy.get('[data-cy=step-description]')
.clear()
.type(`Description for step ${stepNumber}`)
cy.step('Uploading pics')
const hasExistingPics =
Cypress.$($step).find('[data-cy=delete-step-img]').length > 0
if (hasExistingPics) {
cy.wrap($step)
.find('[data-cy=delete-image]')
.each($deleteButton => {
cy.wrap($deleteButton).click()
})
if (videoUrl) {
cy.step('Adding Video Url')
cy.get('[data-cy=step-videoUrl]')
.clear()
.type(videoUrl)
} else {
cy.step('Uploading pics')
const hasExistingPics =
Cypress.$($step).find('[data-cy=delete-step-img]').length > 0
if (hasExistingPics) {
cy.wrap($step)
.find('[data-cy=delete-image]')
.each($deleteButton => {
cy.wrap($deleteButton).click()
})
}
images.forEach((image, index) => {
cy.get(`[data-cy=step-image-${index}]`)
.find(':file')
.uploadFiles([image])
})
}
images.forEach((image, index) => {
cy.get(`[data-cy=step-image-${index}]`)
.find(':file')
.uploadFiles([image])
})
})
}

Expand Down Expand Up @@ -142,11 +151,16 @@ describe('[How To]', () => {
.find(':file')
.uploadFiles('images/howto-intro.jpg')

expected.steps.forEach((step, index) => {
fillStep(index + 1, step.title, step.text, [
'images/howto-step-pic1.jpg',
'images/howto-step-pic2.jpg',
])
expected.steps.forEach((step, i) => {
const videoUrl =
i === 1 ? 'https://www.youtube.com/watch?v=Os7dREQ00l4' : undefined
fillStep(
i + 1,
step.title,
step.text,
['images/howto-step-pic1.jpg', 'images/howto-step-pic2.jpg'],
videoUrl,
)
})
deleteStep(3)

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"react-leaflet": "^2.5.0",
"react-leaflet-markercluster": "^2.0.0-rc3",
"react-linkify": "^0.2.2",
"react-player": "^1.15.3",
"react-portal": "^4.2.0",
"react-pose": "^4.0.8",
"react-router": "^5.0.0",
Expand Down
39 changes: 14 additions & 25 deletions src/components/ImageInput/ImageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ interface ITitleProps {
hasUploadedImg: boolean
}

interface IUploadImageOverlayIProps {
isHovering: boolean
}

const AlignCenterWrapper = styled.div`
display: flex;
align-items: center;
Expand All @@ -36,27 +32,22 @@ const ImageInputWrapper = styled(AlignCenterWrapper)<ITitleProps>`
cursor: pointer;
`

const UploadImageOverlay = styled(AlignCenterWrapper)<
IUploadImageOverlayIProps
>`
const UploadImageOverlay = styled(AlignCenterWrapper)`
position: absolute;
top: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.2);
visibility: hidden;
opacity: 0;
opacity:0;
visibility:hidden
transition: opacity 300ms ease-in;
border-radius: ${theme.space[1]}px;
${(props: IUploadImageOverlayIProps) =>
props.isHovering &&
`
${ImageInputWrapper}:hover & {
visibility: visible;
opacity: 1;
`}
}
`

/*
Expand Down Expand Up @@ -89,18 +80,16 @@ interface IState {
inputFiles: File[]
lightboxImg?: IConvertedFileMeta
openLightbox?: boolean
isHovering: boolean
}

export class ImageInput extends React.Component<IProps, IState> {
export class ImageInput extends React.PureComponent<IProps, IState> {
private fileInputRef = React.createRef<HTMLInputElement>()

constructor(props: IProps) {
super(props)
this.state = {
inputFiles: [],
convertedFiles: [],
isHovering: false,
}
}

Expand Down Expand Up @@ -132,14 +121,10 @@ export class ImageInput extends React.Component<IProps, IState> {
if (!imgPreviewMode) {
return
}

this.setState((prevState: Readonly<IState>) => ({
isHovering: !prevState.isHovering,
}))
}

render() {
const { inputFiles, isHovering } = this.state
const { inputFiles } = this.state
// if at least one image present, hide the 'choose image' button and replace with smaller button
const imgPreviewMode = inputFiles.length > 0 || this.props.src
const useImageSrc = this.props.src && this.state.inputFiles.length === 0
Expand Down Expand Up @@ -183,7 +168,7 @@ export class ImageInput extends React.Component<IProps, IState> {
</Button>
)}

<UploadImageOverlay isHovering={isHovering}>
<UploadImageOverlay>
{this.props.canDelete && imgPreviewMode && (
<Button
data-cy="delete-image"
Expand All @@ -198,7 +183,6 @@ export class ImageInput extends React.Component<IProps, IState> {
this.setState({
inputFiles: [],
convertedFiles: [],
isHovering: false,
})
}}
>
Expand All @@ -207,7 +191,12 @@ export class ImageInput extends React.Component<IProps, IState> {
)}

{!this.props.canDelete && (
<Button small variant="outline" icon="image" data-cy="replace-image">
<Button
small
variant="outline"
icon="image"
data-cy="replace-image"
>
Replace image
</Button>
)}
Expand Down
1 change: 1 addition & 0 deletions src/models/howto.models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type IHowtoDB = IHowto & DBDoc
export interface IHowtoStep extends IHowToStepFormInput {
// *** NOTE - adding an '_animationKey' field to track when specific array element removed for
images: Array<IUploadedFileMeta | null>
videoUrl?: string
title: string
text: string
_animationKey?: string
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Howto/Content/Common/Howto.form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const Label = styled.label`

@inject('howtoStore')
@observer
export class HowtoForm extends React.Component<IProps, IState> {
export class HowtoForm extends React.PureComponent<IProps, IState> {
uploadRefs: { [key: string]: UploadedFile | null } = {}
constructor(props: any) {
super(props)
Expand Down
31 changes: 28 additions & 3 deletions src/pages/Howto/Content/Common/HowtoStep.form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const Label = styled.label`
margin-bottom: ${theme.space[2] + 'px'};
`

class HowtoStep extends Component<IProps, IState> {
class HowtoStep extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props)
this.state = {
Expand All @@ -51,6 +51,21 @@ class HowtoStep extends Component<IProps, IState> {
this.toggleDeleteModal()
this.props.onDelete(this.props.index)
}
/**
* Ensure either url or images included (not both), and any url formatted correctly
*/
validateMedia(videoUrl: string) {
const { images } = { ...this.props }
if (videoUrl) {
if (images[0]) {
return 'Do not include both images and video'
}
const ytRegex = new RegExp(/(youtu\.be\/|youtube\.com\/watch\?v=)/gi)
const urlValid = ytRegex.test(videoUrl)
return urlValid ? null : 'Please provide a valid YouTube Url'
}
return images[0] ? null : 'Include either images or a video'
}

render() {
const { step, index, images } = this.props
Expand Down Expand Up @@ -129,8 +144,7 @@ class HowtoStep extends Component<IProps, IState> {
/>
</Flex>
<Label htmlFor={`${step}.text`}>Upload image(s) for this step *</Label>

<Flex flexDirection={['column', 'row']} alignItems="center">
<Flex flexDirection={['column', 'row']} alignItems="center" mb={3}>
<ImageInputFieldWrapper data-cy="step-image-0">
<Field
canDelete
Expand Down Expand Up @@ -159,6 +173,17 @@ class HowtoStep extends Component<IProps, IState> {
/>
</ImageInputFieldWrapper>
</Flex>
<Flex flexDirection="column" mb={3}>
<Label htmlFor={`${step}.videoUrl`}>Or embed a YouTube video*</Label>
<Field
name={`${step}.videoUrl`}
data-cy="step-videoUrl"
component={InputField}
placeholder="https://youtube.com/watch?v="
validate={url => this.validateMedia(url)}
validateFields={[]}
/>
</Flex>
</Flex>
)
}
Expand Down
21 changes: 14 additions & 7 deletions src/pages/Howto/Content/Howto/Step/Step.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import Linkify from 'react-linkify'
import ReactPlayer from 'react-player'
import { IHowtoStep } from 'src/models/howto.models'
import { Box } from 'rebass'
import Flex from 'src/components/Flex'
Expand All @@ -20,7 +21,7 @@ const FlexStepNumber = styled(Flex)`

export default class Step extends React.PureComponent<IProps> {
render() {
const { stepindex } = this.props
const { stepindex, step } = this.props
return (
<>
<Flex
Expand All @@ -40,7 +41,7 @@ export default class Step extends React.PureComponent<IProps> {
width={1}
>
<Heading medium mb={0}>
{this.props.stepindex + 1}
{stepindex + 1}
</Heading>
</FlexStepNumber>
</Flex>
Expand All @@ -56,18 +57,24 @@ export default class Step extends React.PureComponent<IProps> {
>
<Flex width={[1, 1, 4 / 9]} py={4} px={4} flexDirection={'column'}>
<Heading medium mb={0}>
{this.props.step.title}
{step.title}
</Heading>
<Box>
<Text preLine paragraph mt={3} color={'grey'}>
<Linkify>{this.props.step.text}</Linkify>
<Linkify>{step.text}</Linkify>
</Text>
</Box>
</Flex>
<Flex width={[1, 1, 5 / 9]}>
<ImageGallery
images={this.props.step.images as IUploadedFileMeta[]}
/>
{step.videoUrl ? (
<ReactPlayer
data-cy="video-embed"
controls
url={step.videoUrl}
/>
) : (
<ImageGallery images={step.images as IUploadedFileMeta[]} />
)}
</Flex>
</Flex>
</Flex>
Expand Down
Loading

0 comments on commit 0343edd

Please sign in to comment.