Initial commit to add Prebuilt embed demo

This commit is contained in:
Kimberlee Johnson 2021-09-03 18:05:27 -07:00
parent 0cef22103e
commit 2903a72c8b
17 changed files with 3744 additions and 9 deletions

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, forwardRef } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import theme from '../../styles/defaultTheme'; import theme from '../../styles/defaultTheme';
@ -136,15 +136,17 @@ InputContainer.propTypes = {
prefix: PropTypes.string, prefix: PropTypes.string,
}; };
export const TextInput = ({ onChange, prefix, variant, ...rest }) => { export const TextInput = forwardRef(
const cx = classNames('input-container', variant, { prefix }); ({ onChange, prefix, variant, ...rest }, ref) => {
const cx = classNames('input-container', variant, { prefix });
return ( return (
<InputContainer className={cx} prefix={prefix}> <InputContainer className={cx} prefix={prefix}>
<input type="text" onChange={onChange} {...rest} /> <input type="text" onChange={onChange} ref={ref} {...rest} />
</InputContainer> </InputContainer>
); );
}; }
);
TextInput.propTypes = { TextInput.propTypes = {
onChange: PropTypes.func, onChange: PropTypes.func,

View File

@ -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

View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

35
prebuilt-ui/basic-embed/.gitignore vendored Normal file
View 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*
# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel

View File

@ -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
[![Deploy with Vercel](https://vercel.com/button)](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)

View File

@ -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;

View File

@ -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;

View File

@ -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,
},
});

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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

View File

@ -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