Add example: with-apivideo-upload (#36050)
* feat(app): Landing * feat(index): Add button to video view * Remove React stric mode * feat(index): Handle navigate with query params * feat(app): Create common style * feat(app): Video view * feat(video view): Add footer * feat(app): Add .env * feat(index): Hide upload button on progress * Update pages type * Change MyApp props type * feat(index): Add prod domain for fetching * update(app): CSS * update(index): Domain name * feat(index): Remove getSSProps * Update README * Update README * update(status): CSS * feat(index): Add link to GitHub repo * Remove static API key from .env * Lint * update(packages): next version * Lint + Prettier * Remove name & version from package.json * update: Rename .env.development -> .env.local.example * update: Remove esLint + updagrade React & ReactDOM version * Remove yarn.lock changes Co-authored-by: José Barcelon-Godfrey <jose.barcelon@gmail.com>
This commit is contained in:
parent
a37abc62ee
commit
27bfdec1ea
21 changed files with 937 additions and 0 deletions
1
examples/with-apivideo-upload/.env.local.example
Normal file
1
examples/with-apivideo-upload/.env.local.example
Normal file
|
@ -0,0 +1 @@
|
|||
API_KEY=
|
35
examples/with-apivideo-upload/.gitignore
vendored
Normal file
35
examples/with-apivideo-upload/.gitignore
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
47
examples/with-apivideo-upload/README.md
Normal file
47
examples/with-apivideo-upload/README.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
# api.video video uploader
|
||||
|
||||
This video uploader and playback app is built with Next.js and api.video, the video first API.
|
||||
|
||||
## Demo
|
||||
|
||||
[https://apivideo-uploader.vercel.app/](https://apivideo-uploader.vercel.app/)
|
||||
|
||||
## Deploy your own
|
||||
|
||||
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
|
||||
|
||||
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-apivideo-upload&project-name=with-apivideo-upload&repository-name=with-apivideo-upload)
|
||||
|
||||
## How to use
|
||||
|
||||
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
|
||||
|
||||
```bash
|
||||
npx create-next-app --example with-apivideo-upload with-apivideo-upload-app
|
||||
# or
|
||||
yarn create next-app --example with-apivideo-upload with-apivideo-upload-app
|
||||
# or
|
||||
pnpm create next-app -- --example with-apivideo-upload with-apivideo-upload-app
|
||||
```
|
||||
|
||||
Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
|
||||
|
||||
## Getting started
|
||||
|
||||
### 1. Create an api.video free account
|
||||
|
||||
Go to [dashboard.api.video](https://dashboard.api.video/), log in or create a free account.
|
||||
You can choose to stay in sandbox and have watermark over your videos, or enter in [production mode](https://api.video/pricing) and take advantage of all the features without limitations.
|
||||
|
||||
### 2. Get you API key
|
||||
|
||||
Once in the dashboard, find your API keys directly in the `/overview` or navigate to `/apikeys` with the "API Keys" button in the side navigation.
|
||||
Copy your API key and paste it in `.env.development` as value for `API_KEY`.
|
||||
You can now try the application locally by running `npm run dev` from the root directory.
|
||||
|
||||
### 3. Deployment
|
||||
|
||||
First, push your app to GitHub/GitLab or Bitbucket
|
||||
The, go to [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) and import your new repository.
|
||||
Add an environment variable with name `API_KEY` and your API key for value.
|
||||
Click on deploy 🎉
|
61
examples/with-apivideo-upload/components/Card/index.tsx
Normal file
61
examples/with-apivideo-upload/components/Card/index.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface ICardProps {
|
||||
content: string
|
||||
url: string
|
||||
method: 'get' | 'post'
|
||||
}
|
||||
|
||||
const Card: React.FC<ICardProps> = ({ content, url, method }): JSX.Element => (
|
||||
<Container target="_blank" href={url}>
|
||||
<Method $method={method}>{method.toUpperCase()}</Method>
|
||||
<Content>{content}</Content>
|
||||
<ImageContainer>
|
||||
<Image src="/arrow.png" alt="Sketch arrow" width={20} height={20} />
|
||||
<p>Try it out with our API!</p>
|
||||
</ImageContainer>
|
||||
</Container>
|
||||
)
|
||||
|
||||
export default Card
|
||||
|
||||
const Container = styled.a`
|
||||
border: 1px solid rgb(215, 219, 236);
|
||||
border-radius: 0.25rem;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.5rem;
|
||||
position: relative;
|
||||
box-shadow: rgb(0 0 0 / 5%) 0px 2px 4px;
|
||||
`
|
||||
|
||||
const Method = styled.div<{ $method: 'get' | 'post' }>`
|
||||
color: #ffffff;
|
||||
background-color: ${(p) => (p.$method === 'get' ? 'green' : 'blue')};
|
||||
padding: 0.3rem;
|
||||
border-radius: 2px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const Content = styled.p`
|
||||
letter-spacing: 0.05rem;
|
||||
`
|
||||
|
||||
const ImageContainer = styled.div`
|
||||
position: absolute;
|
||||
bottom: -25px;
|
||||
right: -105px;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: flex-end;
|
||||
p {
|
||||
margin-bottom: -3px;
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
`
|
28
examples/with-apivideo-upload/components/Loader/index.tsx
Normal file
28
examples/with-apivideo-upload/components/Loader/index.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import Image from 'next/image'
|
||||
import React from 'react'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
|
||||
interface ILoaderProps {
|
||||
done: boolean
|
||||
}
|
||||
const Loader: React.FC<ILoaderProps> = ({ done }): JSX.Element =>
|
||||
done ? <Image src="/check.png" width={30} height={30} /> : <Spinner />
|
||||
|
||||
export default Loader
|
||||
|
||||
const spin = keyframes`
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
`
|
||||
const Spinner = styled.div`
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid rgb(235, 137, 82);
|
||||
border-radius: 50%;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
animation: ${spin} 1s linear infinite;
|
||||
`
|
23
examples/with-apivideo-upload/components/Status/index.tsx
Normal file
23
examples/with-apivideo-upload/components/Status/index.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Loader from '../Loader'
|
||||
|
||||
interface IStatusProps {
|
||||
done: boolean
|
||||
title: string
|
||||
}
|
||||
const Status: React.FC<IStatusProps> = ({ done, title }): JSX.Element => (
|
||||
<Container>
|
||||
<p>{title}</p>
|
||||
<Loader done={done} />
|
||||
</Container>
|
||||
)
|
||||
|
||||
export default Status
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
`
|
5
examples/with-apivideo-upload/next-env.d.ts
vendored
Normal file
5
examples/with-apivideo-upload/next-env.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
9
examples/with-apivideo-upload/next.config.js
Normal file
9
examples/with-apivideo-upload/next.config.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: false,
|
||||
compiler: {
|
||||
styledComponents: true,
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
25
examples/with-apivideo-upload/package.json
Normal file
25
examples/with-apivideo-upload/package.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.video/nodejs-client": "2.2.5",
|
||||
"@api.video/player-sdk": "1.2.13",
|
||||
"@api.video/video-uploader": "1.0.4",
|
||||
"@types/styled-components": "5.1.24",
|
||||
"next": "latest",
|
||||
"react": "18.1.0",
|
||||
"react-dom": "18.1.0",
|
||||
"styled-components": "5.3.5",
|
||||
"swr": "1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "17.0.23",
|
||||
"@types/react": "17.0.43",
|
||||
"@types/react-dom": "17.0.14",
|
||||
"typescript": "4.6.3"
|
||||
}
|
||||
}
|
155
examples/with-apivideo-upload/pages/[videoId].tsx
Normal file
155
examples/with-apivideo-upload/pages/[videoId].tsx
Normal file
|
@ -0,0 +1,155 @@
|
|||
import { PlayerSdk, PlayerTheme } from '@api.video/player-sdk'
|
||||
import { GetServerSideProps, NextPage } from 'next'
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
Footer,
|
||||
GlobalContainer,
|
||||
Header,
|
||||
InputsContainer,
|
||||
PlayerSdkContainer,
|
||||
Text,
|
||||
TextsContainer,
|
||||
} from '../style/common'
|
||||
|
||||
interface IVideoViewProps {
|
||||
children: React.ReactNode
|
||||
videoId: string
|
||||
width: string
|
||||
height: string
|
||||
}
|
||||
const VideoView: NextPage<IVideoViewProps> = ({
|
||||
videoId,
|
||||
width,
|
||||
height,
|
||||
}): JSX.Element => {
|
||||
const [player, setPlayer] = useState<PlayerSdk | undefined>(undefined)
|
||||
const [playerSettings, setPlayerSettings] = useState<PlayerTheme>({
|
||||
link: 'rgb(235, 137, 82)',
|
||||
linkHover: 'rgb(240, 95, 12)',
|
||||
})
|
||||
const [hideControls, setHideControls] = useState<boolean>(false)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const player = new PlayerSdk('#player', {
|
||||
id: videoId,
|
||||
})
|
||||
player.setTheme({
|
||||
link: 'rgb(235, 137, 82)',
|
||||
linkHover: 'rgb(240, 95, 12)',
|
||||
})
|
||||
setPlayer(player)
|
||||
}, [videoId])
|
||||
useEffect(() => {
|
||||
player && player?.loadConfig({ id: videoId, hideControls: hideControls })
|
||||
}, [hideControls, player, videoId])
|
||||
|
||||
const handleChangeSetting = (
|
||||
e: ChangeEvent<HTMLInputElement>,
|
||||
prop: string
|
||||
) => {
|
||||
const newSettings = { ...playerSettings, [prop]: e.currentTarget.value }
|
||||
setPlayerSettings(newSettings)
|
||||
player?.setTheme(newSettings)
|
||||
}
|
||||
|
||||
return (
|
||||
<GlobalContainer>
|
||||
<Head>
|
||||
<title>Video view</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Generated by create next app & created by api.video"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Header>
|
||||
<span>Already there</span> 🎉
|
||||
</Header>
|
||||
|
||||
<main>
|
||||
<TextsContainer>
|
||||
<Text>
|
||||
This player is generated by the{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/apivideo/api.video-player-sdk"
|
||||
>
|
||||
api.video's Player SDK
|
||||
</a>
|
||||
.<br />
|
||||
It provides multiple properties to customize your video player.
|
||||
</Text>
|
||||
<Text>Try 3 of them just bellow 👇</Text>
|
||||
</TextsContainer>
|
||||
|
||||
<InputsContainer>
|
||||
<div>
|
||||
<label>Play button color</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Play button color"
|
||||
value={playerSettings.link}
|
||||
onChange={(e) => handleChangeSetting(e, 'link')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Buttons hover color</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Buttons hover color"
|
||||
value={playerSettings.linkHover}
|
||||
onChange={(e) => handleChangeSetting(e, 'linkHover')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hideControls}
|
||||
onChange={(e) => {
|
||||
setHideControls(e.currentTarget.checked)
|
||||
}}
|
||||
/>
|
||||
<label>Hide controls</label>
|
||||
</div>
|
||||
</InputsContainer>
|
||||
<PlayerSdkContainer
|
||||
id="player"
|
||||
$width={parseInt(width)}
|
||||
$height={parseInt(height)}
|
||||
/>
|
||||
|
||||
<Button onClick={() => router.push('/')}>Another video?</Button>
|
||||
</main>
|
||||
|
||||
<Footer>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Powered by{' '}
|
||||
<span>
|
||||
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
|
||||
</span>
|
||||
</a>
|
||||
<span>and</span>
|
||||
<a href="https://api.video" target="_blank" rel="noopener noreferrer">
|
||||
api.video
|
||||
</a>
|
||||
</Footer>
|
||||
</GlobalContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default VideoView
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const { videoId, w, h } = context.query
|
||||
return { props: { videoId, width: w ?? null, height: h ?? null } }
|
||||
}
|
7
examples/with-apivideo-upload/pages/_app.tsx
Normal file
7
examples/with-apivideo-upload/pages/_app.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import '../style/index.css'
|
||||
|
||||
function MyApp({ Component, pageProps }: any) {
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
|
||||
export default MyApp
|
15
examples/with-apivideo-upload/pages/api/[videoId].ts
Normal file
15
examples/with-apivideo-upload/pages/api/[videoId].ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import ApiVideoClient from '@api.video/nodejs-client'
|
||||
|
||||
const getVideoStatus = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
const { videoId } = req.query
|
||||
const client = new ApiVideoClient({ apiKey: process.env.API_KEY })
|
||||
const status = await client.videos.getStatus(videoId as string)
|
||||
res.status(200).json({ status })
|
||||
} catch (error) {
|
||||
res.status(400).end()
|
||||
}
|
||||
}
|
||||
|
||||
export default getVideoStatus
|
14
examples/with-apivideo-upload/pages/api/uploadToken.ts
Normal file
14
examples/with-apivideo-upload/pages/api/uploadToken.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import ApiVideoClient from '@api.video/nodejs-client'
|
||||
|
||||
const getUploadToken = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
const client = new ApiVideoClient({ apiKey: process.env.API_KEY })
|
||||
const uploadToken = await client.uploadTokens.createToken()
|
||||
res.status(200).json(uploadToken)
|
||||
} catch (error) {
|
||||
res.status(400).end()
|
||||
}
|
||||
}
|
||||
|
||||
export default getUploadToken
|
234
examples/with-apivideo-upload/pages/index.tsx
Normal file
234
examples/with-apivideo-upload/pages/index.tsx
Normal file
|
@ -0,0 +1,234 @@
|
|||
import type { NextPage } from 'next'
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image'
|
||||
import React, { ChangeEvent, useEffect, useRef, useState } from 'react'
|
||||
import Card from '../components/Card'
|
||||
import { VideoUploader, VideoUploadResponse } from '@api.video/video-uploader'
|
||||
import Status from '../components/Status'
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
Button,
|
||||
Footer,
|
||||
GlobalContainer,
|
||||
Header,
|
||||
StatusContainer,
|
||||
Text,
|
||||
TextsContainer,
|
||||
} from '../style/common'
|
||||
import useSWR from 'swr'
|
||||
|
||||
const fetcher = async (url: string): Promise<any> => {
|
||||
return fetch(url).then((res) => res.json())
|
||||
}
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const [uploadProgress, setUploadProgress] = useState<number | undefined>(
|
||||
undefined
|
||||
)
|
||||
const [video, setVideo] = useState<VideoUploadResponse | undefined>(undefined)
|
||||
const [ready, setReady] = useState<boolean>(false)
|
||||
const [status, setStatus] = useState<{ ingested: boolean; encoded: boolean }>(
|
||||
{ ingested: false, encoded: false }
|
||||
)
|
||||
const [interId, setInterId] = useState<number | undefined>(undefined)
|
||||
const [size, setSize] = useState<
|
||||
{ width: number; height: number } | undefined
|
||||
>(undefined)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const router = useRouter()
|
||||
|
||||
const { data: uploadToken } = useSWR<{ token: string }>(
|
||||
'/api/uploadToken',
|
||||
fetcher
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (video) {
|
||||
const intervalId = window.setInterval(() => {
|
||||
fetchVideoStatus(video.videoId)
|
||||
}, 1000)
|
||||
setInterId(intervalId)
|
||||
}
|
||||
}, [video, ready])
|
||||
useEffect(() => {
|
||||
ready && window.clearInterval(interId)
|
||||
}, [interId, ready])
|
||||
|
||||
const handleSelectFile = async (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
): Promise<void> => {
|
||||
e.preventDefault()
|
||||
clearState()
|
||||
if (!e.target.files || !uploadToken) return
|
||||
const file = e.target.files[0]
|
||||
const uploader = new VideoUploader({
|
||||
file,
|
||||
uploadToken: uploadToken.token,
|
||||
})
|
||||
uploader.onProgress((e) =>
|
||||
setUploadProgress(Math.round((e.uploadedBytes * 100) / e.totalBytes))
|
||||
)
|
||||
const video = await uploader.upload()
|
||||
setVideo(video)
|
||||
}
|
||||
|
||||
const fetchVideoStatus = async (videoId: string): Promise<void> => {
|
||||
const { status } = await fetcher(`/api/${videoId}`)
|
||||
const { encoding, ingest } = status
|
||||
setStatus({
|
||||
ingested: ingest.status === 'uploaded',
|
||||
encoded: encoding.playable,
|
||||
})
|
||||
if (ingest.status === 'uploaded' && encoding.playable) {
|
||||
setSize({
|
||||
width: encoding.metadata.width,
|
||||
height: encoding.metadata.height,
|
||||
})
|
||||
setReady(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNavigate = (): void => {
|
||||
if (!video) return
|
||||
router.push(`/${video.videoId}?w=${size?.width}&h=${size?.height}`)
|
||||
}
|
||||
|
||||
const clearState = (): void => {
|
||||
setReady(false)
|
||||
setStatus({ ingested: false, encoded: false })
|
||||
setVideo(undefined)
|
||||
setUploadProgress(undefined)
|
||||
setSize(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<GlobalContainer>
|
||||
<Head>
|
||||
<title>Video Uploader</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Generated by create next app & created by api.video"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<Header>
|
||||
<span>api.video uploader</span> 🚀
|
||||
</Header>
|
||||
|
||||
<main>
|
||||
<TextsContainer>
|
||||
<Text>
|
||||
Hey fellow dev! 👋 <br />
|
||||
Welcome to this basic example of video uploader provided by{' '}
|
||||
<a
|
||||
href="https://api.video"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
api.video
|
||||
</a>{' '}
|
||||
and powered by{' '}
|
||||
<a
|
||||
href="https://nextjs.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Vercel & Next.js
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
<Text>
|
||||
api.video provides APIs and clients to handle all your video needs.
|
||||
<br />
|
||||
This app is built with the{' '}
|
||||
<a
|
||||
href="https://github.com/apivideo/api.video-nodejs-client"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
api.video Node.js client
|
||||
</a>{' '}
|
||||
and the{' '}
|
||||
<a
|
||||
href="https://github.com/apivideo/api.video-typescript-uploader"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Typescript uploader
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
<Text>
|
||||
You can{' '}
|
||||
<a
|
||||
href="https://github.com/vercel/next.js/tree/canary/examples/with-apivideo-upload"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
check the source code on GitHub
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
<Text>
|
||||
Please add a video to upload and let the power of the API do the
|
||||
rest 🎩
|
||||
</Text>
|
||||
</TextsContainer>
|
||||
|
||||
{!uploadProgress ? (
|
||||
<>
|
||||
<Button $upload onClick={() => inputRef.current?.click()}>
|
||||
Select a file
|
||||
</Button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
hidden
|
||||
type="file"
|
||||
accept="mp4"
|
||||
onChange={handleSelectFile}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<StatusContainer>
|
||||
<Status title="Uploaded" done={uploadProgress >= 100} />
|
||||
<span />
|
||||
<Status title="Ingested" done={status.ingested} />
|
||||
<span />
|
||||
<Status title="Playable" done={status.encoded} />
|
||||
</StatusContainer>
|
||||
<Card
|
||||
content="https://ws.api.video/videos/{videoId}/source"
|
||||
url="https://docs.api.video/reference/post_videos-videoid-source"
|
||||
method="post"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{ready && video && (
|
||||
<Button onClick={handleNavigate}>Watch it 🍿</Button>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<Footer>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Powered by{' '}
|
||||
<span>
|
||||
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
|
||||
</span>
|
||||
</a>
|
||||
<span>and</span>
|
||||
<a href="https://api.video" target="_blank" rel="noopener noreferrer">
|
||||
api.video
|
||||
</a>
|
||||
</Footer>
|
||||
</GlobalContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
BIN
examples/with-apivideo-upload/public/arrow.png
Normal file
BIN
examples/with-apivideo-upload/public/arrow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
examples/with-apivideo-upload/public/check.png
Normal file
BIN
examples/with-apivideo-upload/public/check.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
examples/with-apivideo-upload/public/favicon.ico
Normal file
BIN
examples/with-apivideo-upload/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
4
examples/with-apivideo-upload/public/vercel.svg
Normal file
4
examples/with-apivideo-upload/public/vercel.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
124
examples/with-apivideo-upload/style/common.ts
Normal file
124
examples/with-apivideo-upload/style/common.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
import styled from 'styled-components'
|
||||
|
||||
export const GlobalContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
gap: 20px;
|
||||
main {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
export const Header = styled.header`
|
||||
font-size: 2.5rem;
|
||||
margin-top: 2rem;
|
||||
span {
|
||||
font-weight: 700;
|
||||
background: -webkit-linear-gradient(
|
||||
45deg,
|
||||
rgb(250, 91, 48) 0%,
|
||||
rgb(128, 54, 255) 26.88%,
|
||||
rgb(213, 63, 255) 50.44%,
|
||||
rgb(235, 137, 82) 73.83%,
|
||||
rgb(247, 181, 0) 100%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
export const TextsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 3rem 5rem;
|
||||
box-shadow: rgb(0 0 0 / 10%) 0px 2px 4px;
|
||||
border-radius: 5px;
|
||||
`
|
||||
|
||||
export const Text = styled.p`
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
letter-spacing: 0.03rem;
|
||||
a {
|
||||
font-weight: 700;
|
||||
}
|
||||
`
|
||||
|
||||
export const Button = styled.button<{ $upload?: boolean }>`
|
||||
background: ${(p) =>
|
||||
p.$upload
|
||||
? '-webkit-linear-gradient(45deg, rgb(250, 91, 48) 0%, rgb(235, 137, 82) 50%, rgb(247, 181, 0) 100%)'
|
||||
: '-webkit-linear-gradient(45deg, rgb(247, 181, 0) 0%, rgb(235, 137, 82) 50%, rgb(250, 91, 48) 100%)'};
|
||||
border: none;
|
||||
padding: 0.8rem 1.2rem;
|
||||
border-radius: 5px;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
export const StatusContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
span {
|
||||
width: 35px;
|
||||
height: 5px;
|
||||
border-radius: 5px;
|
||||
background-color: rgb(235, 137, 82);
|
||||
margin-top: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
export const Footer = styled.footer`
|
||||
margin-top: auto;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
a:nth-of-type(2) {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
`
|
||||
|
||||
export const PlayerSdkContainer = styled.div<{
|
||||
$width: number
|
||||
$height: number
|
||||
}>`
|
||||
width: ${(p) => (p.$width && p.$width <= 800 ? p.$width : '800')}px;
|
||||
height: ${(p) => (p.$height && p.$height <= 250 ? p.$height : '250')}px;
|
||||
iframe {
|
||||
height: ${(p) => (p.$height <= 250 ? p.$height : '250')}px !important;
|
||||
}
|
||||
`
|
||||
|
||||
export const InputsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
label {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
> div:last-child {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-self: flex-end;
|
||||
}
|
||||
`
|
130
examples/with-apivideo-upload/style/index.css
Normal file
130
examples/with-apivideo-upload/style/index.css
Normal file
|
@ -0,0 +1,130 @@
|
|||
* {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
div,
|
||||
span,
|
||||
applet,
|
||||
object,
|
||||
iframe,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote,
|
||||
pre,
|
||||
a,
|
||||
abbr,
|
||||
acronym,
|
||||
address,
|
||||
big,
|
||||
cite,
|
||||
code,
|
||||
del,
|
||||
dfn,
|
||||
em,
|
||||
img,
|
||||
ins,
|
||||
kbd,
|
||||
q,
|
||||
s,
|
||||
samp,
|
||||
small,
|
||||
strike,
|
||||
strong,
|
||||
sub,
|
||||
sup,
|
||||
tt,
|
||||
var,
|
||||
b,
|
||||
u,
|
||||
i,
|
||||
center,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ol,
|
||||
ul,
|
||||
li,
|
||||
fieldset,
|
||||
form,
|
||||
label,
|
||||
legend,
|
||||
table,
|
||||
caption,
|
||||
tbody,
|
||||
tfoot,
|
||||
thead,
|
||||
tr,
|
||||
th,
|
||||
td,
|
||||
article,
|
||||
aside,
|
||||
canvas,
|
||||
details,
|
||||
embed,
|
||||
figure,
|
||||
figcaption,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
output,
|
||||
ruby,
|
||||
section,
|
||||
summary,
|
||||
time,
|
||||
mark,
|
||||
audio,
|
||||
video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote,
|
||||
q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before,
|
||||
blockquote:after,
|
||||
q:before,
|
||||
q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: unset;
|
||||
}
|
20
examples/with-apivideo-upload/tsconfig.json
Normal file
20
examples/with-apivideo-upload/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
Loading…
Reference in a new issue