Initial commit to add Prebuilt embed demo
This commit is contained in:
parent
0cef22103e
commit
2903a72c8b
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, forwardRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import theme from '../../styles/defaultTheme';
|
||||
|
|
@ -136,15 +136,17 @@ InputContainer.propTypes = {
|
|||
prefix: PropTypes.string,
|
||||
};
|
||||
|
||||
export const TextInput = ({ onChange, prefix, variant, ...rest }) => {
|
||||
const cx = classNames('input-container', variant, { prefix });
|
||||
export const TextInput = forwardRef(
|
||||
({ onChange, prefix, variant, ...rest }, ref) => {
|
||||
const cx = classNames('input-container', variant, { prefix });
|
||||
|
||||
return (
|
||||
<InputContainer className={cx} prefix={prefix}>
|
||||
<input type="text" onChange={onChange} {...rest} />
|
||||
</InputContainer>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<InputContainer className={cx} prefix={prefix}>
|
||||
<input type="text" onChange={onChange} ref={ref} {...rest} />
|
||||
</InputContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TextInput.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain'
|
||||
DAILY_DOMAIN=
|
||||
|
||||
# Obtained from https://dashboard.daily.co/developers
|
||||
DAILY_API_KEY=
|
||||
|
||||
# Daily REST API endpoint
|
||||
DAILY_REST_DOMAIN=https://api.daily.co/v1
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
|
|
@ -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*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Daily Prebuilt: Next.js demo
|
||||
|
||||
[Clicking create room button starts a call](/public/basic-embed.gif)
|
||||
|
||||
## How the demo works
|
||||
|
||||
This demo embeds [Daily Prebuilt](https://www.daily.co/prebuilt), a ready-to-use video chat interface, into a Next.js site. It makes use of [Next API routes](https://nextjs.org/docs/api-routes/introduction) to create a Daily room server-side.
|
||||
|
||||
## Requirements
|
||||
|
||||
You'll need to create a [Daily account](https://dashboard.daily.co/signup) before using this demo. You'll need your Daily API key, which you can find in your Daily dashboard on the [Developers page](https://dashboard.daily.co/developers), if you want to create rooms through the demo UI.
|
||||
|
||||
You can also paste an existing Daily room into the input. The room URL should be in this format to be valid: https://domain-name.daily.co/room-name, with daily-domain changed to your domain, and room-name changed to the name of the existing room you would like to use.
|
||||
|
||||
# Running locally
|
||||
1. Copy .env.example and change it to an .env.local with your own DAILY_API_KEY and DAILY_DOMAIN
|
||||
2. `cd basic-embed`
|
||||
3. yarn dev
|
||||
|
||||
Or...
|
||||
|
||||
# Deploy your own on Vercel
|
||||
|
||||
[](https://vercel.com/new/daily-co/clone-flow?repository-url=https%3A%2F%2Fgithub.com%2Fdaily-demos%2Fexamples.git&env=DAILY_DOMAIN%2CDAILY_API_KEY&envDescription=Your%20Daily%20domain%20and%20API%20key%20can%20be%20found%20on%20your%20account%20dashboard&envLink=https%3A%2F%2Fdashboard.daily.co&project-name=daily-examples&repo-name=daily-examples)
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import React from 'react';
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<header className="header">
|
||||
<div className="row">
|
||||
<img src="daily-logo.svg" alt="Daily" className="logo" />
|
||||
<div className="capsule">Prebuilt demo</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="capsule">
|
||||
<a href="https://docs.daily.co/docs/">API docs</a>
|
||||
</div>
|
||||
<span className="divider"></span>
|
||||
<img
|
||||
src="github-logo.png"
|
||||
alt="Ocotocat"
|
||||
className="logo octocat"
|
||||
href="https://github.com/daily-demos/examples/tree/main/prebuilt-ui/basic-embed"
|
||||
/>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.header {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
padding: var(--spacing-sm);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.octocat {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: var(--spacing-xxs);
|
||||
}
|
||||
|
||||
.capsule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xxxs);
|
||||
background-color: var(--blue-dark);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--spacing-xxs) var(--spacing-xs);
|
||||
box-sizing: border-box;
|
||||
line-height: 1;
|
||||
font-weight: var(--weight-medium);
|
||||
user-select: none;
|
||||
margin-right: var(--spacing-xxs);
|
||||
color: var(--text-reverse);
|
||||
}
|
||||
|
||||
.capsule a {
|
||||
text-decoration: none;
|
||||
color: var(--text-reverse);
|
||||
}
|
||||
|
||||
.divider {
|
||||
background: var(--gray-light);
|
||||
margin: 0 var(--spacing-xxs);
|
||||
height: 32px;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 750px) {
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
import DailyIframe from '@daily-co/daily-js';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { writeText } from 'clipboard-polyfill';
|
||||
import { Button } from '@dailyjs/shared/components/Button';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from '@dailyjs/shared/components/Card';
|
||||
import Field from '@dailyjs/shared/components/Field';
|
||||
import { TextInput } from '@dailyjs/shared/components/Input';
|
||||
import { Well } from '@dailyjs/shared/components/Well';
|
||||
|
||||
export const PrebuiltCall = () => {
|
||||
const [demoState, setDemoState] = useState('home');
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [roomURL, setRoomURL] = useState('');
|
||||
const [exp, setExp] = useState();
|
||||
const [secs, setSecs] = useState();
|
||||
const [roomValidity, setRoomValidity] = useState(false);
|
||||
const roomURLRef = useRef(null);
|
||||
const iframeRef = useRef(null);
|
||||
const callFrame = useRef(null);
|
||||
const [linkCopied, setLinkCopied] = useState(false);
|
||||
|
||||
// Updates the time left that displays in the UI
|
||||
useEffect(() => {
|
||||
if (!exp) {
|
||||
return false;
|
||||
}
|
||||
const i = setInterval(() => {
|
||||
const timeLeft = Math.floor((new Date(exp * 1000) - Date.now()) / 1000);
|
||||
setSecs(`${Math.floor(timeLeft / 60)}:${`0${timeLeft % 60}`.slice(-2)}`);
|
||||
}, 1000);
|
||||
return () => clearInterval(i);
|
||||
}, [exp]);
|
||||
|
||||
// Listens for a "call" demo state, and creates then joins a callFrame as soon as that happens
|
||||
useEffect(() => {
|
||||
if (!iframeRef?.current || demoState !== 'call') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
callFrame.current = DailyIframe.createFrame(iframeRef.current, {
|
||||
showLeaveButton: true,
|
||||
iframeStyle: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
aspectRatio: 16 / 9,
|
||||
minwidth: '400px',
|
||||
maxWidth: '920px',
|
||||
border: '0',
|
||||
borderRadius: '12px',
|
||||
},
|
||||
});
|
||||
callFrame.current.join({
|
||||
url: roomURL,
|
||||
});
|
||||
} catch (e) {
|
||||
setDemoState('home');
|
||||
setIsError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const handleLeftMeeting = () => {
|
||||
setDemoState('home');
|
||||
setRoomValidity(false);
|
||||
callFrame.current.destroy();
|
||||
};
|
||||
|
||||
callFrame.current.on('left-meeting', handleLeftMeeting);
|
||||
}, [demoState, iframeRef, roomURL]);
|
||||
|
||||
const createRoom = useCallback(
|
||||
async (e) => {
|
||||
if (!roomURLRef?.current.value) {
|
||||
const roomExp = Math.round(Date.now() / 1000) + 60 * 5;
|
||||
try {
|
||||
const res = await fetch('/api/room', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
properties: {
|
||||
roomExp,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const roomJson = await res.json();
|
||||
const { url } = roomJson;
|
||||
setRoomURL(url);
|
||||
setDemoState('call');
|
||||
setExp(roomExp);
|
||||
setIsError(false);
|
||||
} catch (e) {
|
||||
setDemoState('home');
|
||||
setIsError(true);
|
||||
}
|
||||
}
|
||||
},
|
||||
[roomURL, exp]
|
||||
);
|
||||
|
||||
// Updates state if a room is provided
|
||||
const handleRoomInput = useCallback(
|
||||
(e) => {
|
||||
setRoomURL(e?.target?.value);
|
||||
if (e?.target?.checkValidity()) {
|
||||
setRoomValidity(true);
|
||||
console.log(roomValidity);
|
||||
}
|
||||
},
|
||||
[roomValidity]
|
||||
);
|
||||
|
||||
const submitJoinRoom = useCallback(() => {
|
||||
setDemoState('call');
|
||||
});
|
||||
|
||||
const handleCopyClick = useCallback(() => {
|
||||
console.log('click');
|
||||
writeText(roomURL);
|
||||
setLinkCopied(true);
|
||||
setTimeout(() => setLinkCopied(false), 5000);
|
||||
}, [roomURL, linkCopied]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
switch (demoState) {
|
||||
case 'home':
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
Start demo with a new unique room or paste in your own room URL.
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{isError && (
|
||||
<Well variant="error">
|
||||
Failed to obtain token <p>{error}</p>
|
||||
</Well>
|
||||
)}
|
||||
<Button onClick={() => createRoom()} disabled={roomValidity}>
|
||||
Create room and start
|
||||
</Button>
|
||||
<Field label="Or enter room to join">
|
||||
<TextInput
|
||||
ref={roomURLRef}
|
||||
type="text"
|
||||
placeholder="Enter room URL..."
|
||||
pattern="^(https:\/\/)?[\w.-]+(\.(daily\.(co)))+[\/\/]+[\w.-]+$"
|
||||
onChange={handleRoomInput}
|
||||
/>
|
||||
</Field>
|
||||
<CardFooter>
|
||||
<Button
|
||||
onClick={() => submitJoinRoom()}
|
||||
disabled={!roomValidity}
|
||||
>
|
||||
Join room
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
case 'call':
|
||||
return (
|
||||
<>
|
||||
<div ref={iframeRef} className="call" />
|
||||
<Card>
|
||||
<CardHeader>Copy and share the URL to invite others.</CardHeader>
|
||||
<CardBody>
|
||||
<label for="copy-url"></label>
|
||||
<TextInput
|
||||
type="text"
|
||||
class="url-input"
|
||||
id="copy-url"
|
||||
placeholder="Copy this room URL"
|
||||
value={roomURL}
|
||||
pattern="^(https:\/\/)?[\w.-]+(\.(daily\.(co)))+[\/\/]+[\w.-]+$"
|
||||
/>
|
||||
<Button onClick={handleCopyClick}>
|
||||
{linkCopied ? 'Copied!' : `Copy room URL`}
|
||||
</Button>
|
||||
{exp && (
|
||||
<h3>
|
||||
This room expires in:{' '}
|
||||
<span className="countdown">{secs}</span>
|
||||
</h3>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}, [demoState, roomValidity, roomURLRef, exp, secs]);
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
{content}
|
||||
<style jsx>{`
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.container :global(.call) {
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
.container :global(.countdown) {
|
||||
padding: 4px 0;
|
||||
font-size: 1rem;
|
||||
font-weight: var(--weight-medium);
|
||||
border-radius: 0 0 0 var(--radius-sm);
|
||||
color: var(--blue-dark);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 750px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrebuiltCall;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
const withPlugins = require('next-compose-plugins');
|
||||
const withTM = require('next-transpile-modules')(['@dailyjs/shared']);
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
module.exports = withPlugins([withTM], {
|
||||
env: {
|
||||
PROJECT_TITLE: packageJson.description,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "basic-embed",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "11.1.2",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"clipboard-polyfill": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-next": "11.1.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import GlobalHead from '@dailyjs/shared/components/GlobalHead';
|
||||
import GlobalStyle from '@dailyjs/shared/components/GlobalStyle';
|
||||
import Head from 'next/head';
|
||||
|
||||
function App({ Component, pageProps }) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Daily Prebuilt + Next.js demo</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Daily Prebuilt video chat interface embedded in a Next.js app."
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<GlobalHead />
|
||||
<GlobalStyle />
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Generates a demo room server-side
|
||||
*/
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { roomExp } = req.body.properties;
|
||||
|
||||
if (req.method === 'POST') {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${process.env.DAILY_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
properties: {
|
||||
enable_prejoin_ui: true,
|
||||
enable_network_ui: true,
|
||||
enable_screenshare: true,
|
||||
enable_chat: true,
|
||||
exp: roomExp,
|
||||
eject_at_room_exp: true,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const dailyRes = await fetch(
|
||||
`${process.env.DAILY_REST_DOMAIN}/rooms`,
|
||||
options
|
||||
);
|
||||
|
||||
const response = await dailyRes.json();
|
||||
|
||||
if (response.error) {
|
||||
return res.status(500).json(response.error);
|
||||
}
|
||||
|
||||
return res.status(200).json(response);
|
||||
}
|
||||
|
||||
return res.status(500);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import PrebuiltCall from '../components/PrebuiltCall';
|
||||
import Header from '../components/Header';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<PrebuiltCall />
|
||||
<style jsx>{`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.5 MiB |
|
|
@ -0,0 +1,14 @@
|
|||
<svg width="80" height="32" viewBox="0 0 80 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.5886 27.0149C23.8871 27.0149 25.7976 25.6716 26.6035 24.0896V26.6866H30.9021V4H26.6035V13.403C25.7379 11.8209 24.1856 10.7164 21.6782 10.7164C17.7677 10.7164 14.8125 13.7313 14.8125 18.8657V19.1045C14.8125 24.2985 17.7976 27.0149 21.5886 27.0149ZM22.8722 23.6418C20.7229 23.6418 19.2304 22.1194 19.2304 19.0149V18.7761C19.2304 15.6716 20.5737 14.0299 22.9916 14.0299C25.3498 14.0299 26.7229 15.6119 26.7229 18.7164V18.9552C26.7229 22.1194 25.1409 23.6418 22.8722 23.6418Z" fill="white"/>
|
||||
<path d="M37.9534 27.0149C40.4011 27.0149 41.7743 26.0597 42.6698 24.806V26.6866H46.8787V16.5075C46.8787 12.2687 44.1623 10.7164 40.3414 10.7164C36.5205 10.7164 33.5951 12.3582 33.3265 16.0597H37.416C37.5951 14.7164 38.3713 13.8507 40.0728 13.8507C42.0429 13.8507 42.6101 14.8657 42.6101 16.7164V17.3433H40.8489C36.0728 17.3433 32.7295 18.7164 32.7295 22.3582C32.7295 25.6418 35.1175 27.0149 37.9534 27.0149ZM39.2369 24C37.6548 24 36.9683 23.2537 36.9683 22.1194C36.9683 20.4478 38.431 19.9104 40.9384 19.9104H42.6101V21.2239C42.6101 22.9552 41.1474 24 39.2369 24Z" fill="white"/>
|
||||
<path d="M49.647 26.6866H53.9456V11.0746H49.647V26.6866ZM51.7665 8.95522C53.1694 8.95522 54.2441 7.9403 54.2441 6.59702C54.2441 5.25373 53.1694 4.23881 51.7665 4.23881C50.3933 4.23881 49.3187 5.25373 49.3187 6.59702C49.3187 7.9403 50.3933 8.95522 51.7665 8.95522Z" fill="white"/>
|
||||
<path d="M56.9765 26.6866H61.275V4H56.9765V26.6866Z" fill="white"/>
|
||||
<path d="M70.5634 32L78.9917 11.0746H74.8711L71.3499 20.4478L67.5589 11.0746H62.9021L69.1523 25.1953L66.3779 32H70.5634Z" fill="white"/>
|
||||
<path d="M0 32H4.1875L17.0766 0H12.9916L0 32Z" fill="url(#paint0_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="8.88727" y1="-0.222885" x2="8.88727" y2="30" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1BEBB9"/>
|
||||
<stop offset="1" stop-color="#FF9254"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue