Componentize-ing, styling tweaks, README updates, typos

This commit is contained in:
Kimberlee Johnson 2021-09-15 19:01:31 -07:00
parent 770f95bd7c
commit b235b9a211
8 changed files with 286 additions and 348 deletions

View File

@ -4,7 +4,7 @@
## 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.
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 Daily rooms server-side.
## Requirements
@ -15,7 +15,8 @@ You can also paste an existing Daily room into the input. The room URL should be
# 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
3. yarn
4. yarn workspace @prebuilt-ui/basic-embed dev
Or...

View File

@ -0,0 +1,119 @@
import DailyIframe from '@daily-co/daily-js';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { writeText } from 'clipboard-polyfill';
import { Button } from '@dailyjs/shared/components/Button';
import {
Card,
CardBody,
CardHeader,
CardFooter,
} from '@dailyjs/shared/components/Card';
import { ExpiryTimer } from '@dailyjs/shared/components/ExpiryTimer';
import { TextInput } from '@dailyjs/shared/components/Input';
const CALL_OPTIONS = {
showLeaveButton: true,
iframeStyle: {
height: '100%',
width: '100%',
aspectRatio: 16 / 9,
minwidth: '400px',
maxWidth: '920px',
border: '0',
borderRadius: '12px',
},
};
export default function Call({
room,
setRoom,
callFrame,
setCallFrame,
expiry,
}) {
const callRef = useRef(null);
const [isLinkCopied, setIsLinkCopied] = useState(false);
const handleCopyClick = useCallback(() => {
writeText(room);
setIsLinkCopied(true);
setTimeout(() => setIsLinkCopied(false), 5000);
}, [room, isLinkCopied]);
const createAndJoinCall = useCallback(() => {
const newCallFrame = DailyIframe.createFrame(
callRef?.current,
CALL_OPTIONS
);
setCallFrame(newCallFrame);
newCallFrame.join({ url: room });
const leaveCall = () => {
setRoom(null);
setCallFrame(null);
callFrame.destroy();
};
newCallFrame.on('left-meeting', leaveCall);
});
/**
* Initiate Daily iframe creation on component render if it doesn't already exist
*/
useEffect(() => {
if (callFrame) return;
createAndJoinCall();
}, [callFrame, createAndJoinCall]);
return (
<div className="call-container">
{/* Daily iframe container */}
<div ref={callRef} className="call" />
<Card>
<CardHeader>Copy and share the URL to invite others</CardHeader>
<CardBody>
<label htmlFor="copy-url"></label>
<TextInput
type="text"
id="copy-url"
placeholder="Copy this room URL"
value={room}
pattern="^(https:\/\/)?[\w.-]+(\.(daily\.(co)))+[\/\/]+[\w.-]+$"
/>
<Button onClick={handleCopyClick}>
{isLinkCopied ? 'Copied!' : `Copy room URL`}
</Button>
</CardBody>
<CardFooter>{expiry && <ExpiryTimer expiry={expiry} />}</CardFooter>
</Card>
<style jsx>{`
.call-container {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.call-container :global(.call) {
width: 100%;
}
.call-container :global(.button) {
margin-top: var(--spacing-md);
}
.call-container :global(.card) {
max-width: 300px;
max-height: 300px;
}
@media only screen and (max-width: 750px) {
.call-container {
flex-direction: column;
}
}
`}</style>
</div>
);
}

View File

@ -0,0 +1,42 @@
import React, { useState } from 'react';
import { Well } from '@dailyjs/shared/components/Well';
import { Button } from '@dailyjs/shared/components/Button';
export function CreateRoom({ isConfigured, isValidRoom, setRoom, setExpiry }) {
const [isError, setIsError] = useState(false);
/**
* Send a request to create a Daily room server-side via Next API routes, then set the returned url in local state to trigger Daily iframe creation in <Call />
*/
const createRoom = async () => {
try {
const res = await fetch('/api/room', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
const resJson = await res.json();
setExpiry(resJson.config?.exp);
setRoom(resJson.url);
} catch (e) {
setIsError(true);
}
};
return (
<>
{!isConfigured && (
<Well variant="error">
You must configure env variables to create rooms (see README
instructions).
</Well>
)}
{isError && (
<Well variant="error">Error creating the room. Please try again.</Well>
)}
<Button onClick={createRoom} disabled={isValidRoom}>
Create room and start
</Button>
</>
);
}

View File

@ -1,81 +0,0 @@
import React from 'react';
export const Header = () => (
<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>
<a href="https://github.com/daily-demos/examples/tree/main/prebuilt-ui/basic-embed">
<img src="github-logo.png" alt="Ocotocat" className="logo octocat" />
</a>
</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;
top: 0;
}
.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,66 @@
import React, { useCallback, useRef, useState } from 'react';
import { Button } from '@dailyjs/shared/components/Button';
import {
Card,
CardBody,
CardFooter,
CardHeader,
} from '@dailyjs/shared/components/Card';
import { CreateRoom } from '../components/CreateRoom';
import { Field } from '@dailyjs/shared/components/Field';
import { TextInput } from '@dailyjs/shared/components/Input';
export default function Home({ setRoom, setExpiry, isConfigured }) {
const roomRef = useRef(null);
const [isValidRoom, setIsValidRoom] = useState(false);
/**
* If the room is valid, setIsValidRoom and enable the join button
*/
const checkValidity = useCallback(
(e) => {
if (e?.target?.checkValidity()) {
setIsValidRoom(true);
}
},
[isValidRoom]
);
/**
* Set the roomUrl in local state to trigger Daily iframe creation in <Call />
*/
const joinCall = useCallback(() => {
const roomUrl = roomRef?.current?.value;
setRoom(roomUrl);
}, [roomRef]);
return (
<Card>
<CardHeader>
Start demo with a new unique room, or paste in your own room URL
</CardHeader>
<CardBody>
<CreateRoom
isConfigured={isConfigured}
isValidRoom={isValidRoom}
setRoom={setRoom}
setExpiry={setExpiry}
/>
<Field label="Or enter room to join">
<TextInput
ref={roomRef}
type="text"
placeholder="Enter room URL..."
pattern="^(https:\/\/)?[\w.-]+(\.(daily\.(co)))+[\/\/]+[\w.-]+$"
onChange={checkValidity}
/>
</Field>
<CardFooter>
<Button onClick={joinCall} disabled={!isValidRoom}>
Join room
</Button>
</CardFooter>
</CardBody>
</Card>
);
}

View File

@ -1,254 +0,0 @@
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';
import { Header } from '../components/Header';
export default function PrebuiltCall(props) {
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>
{!props.configured && (
<Well variant="error">
You must configure env variables to create rooms (see README
instructions).
</Well>
)}
{isError && (
<Well variant="error">
Error creating the room. Please try again.
</Well>
)}
<Button onClick={() => createRoom()} disabled={roomValidity}>
Create room and start
</Button>
<Field label="Or enter room to join" className="roomField">
<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">
<Header />
{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);
}
:global(.field) {
margin-top: var(--spacing-sm);
}
:global(.card) {
margin: 8px;
}
@media only screen and (max-width: 750px) {
.container {
flex-direction: column;
}
}
`}</style>
</div>
);
}

View File

@ -3,8 +3,6 @@
*/
export default async function handler(req, res) {
const { roomExp } = req.body.properties;
if (req.method === 'POST') {
const options = {
method: 'POST',
@ -19,7 +17,7 @@ export default async function handler(req, res) {
enable_network_ui: true,
enable_screenshare: true,
enable_chat: true,
exp: roomExp,
exp: Math.round(Date.now() / 1000) + 5 * 60,
eject_at_room_exp: true,
},
}),

View File

@ -1,17 +1,64 @@
import PrebuiltCall from '../components/PrebuiltCall';
// import PrebuiltCall from '../components/PrebuiltCall';
import React, { useState } from 'react';
import Call from '../components/Call';
import Home from '../components/Home';
import Header from '@dailyjs/shared/components/Header';
import getDemoProps from '@dailyjs/shared/lib/demoProps';
export default function Index({ isConfigured = false }) {
const [room, setRoom] = useState(null);
const [expiry, setExpiry] = useState(null);
const [callFrame, setCallFrame] = useState(null);
return (
<>
<PrebuiltCall configured={isConfigured} />
<div className="index-container">
<Header
demoTitle={'Daily Prebuilt demo'}
repoLink={
'https://github.com/daily-demos/examples/tree/main/prebuilt-ui/basic-embed'
}
/>
<main>
{room ? (
<Call
room={room}
expiry={expiry}
setRoom={setRoom}
setCallFrame={setCallFrame}
callFrame={callFrame}
/>
) : (
<Home
setRoom={setRoom}
setExpiry={setExpiry}
isConfigured={isConfigured}
/>
)}
</main>
<style jsx>{`
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
.index-container {
display: flex;
flex-direction: column;
min-height: 100vh;
overflow: auto;
max-width: 1200px;
margin: auto;
}
main {
padding: 2rem;
flex: 1;
}
:global(.field) {
margin-top: var(--spacing-sm);
}
:global(.card) {
margin: 8px;
}
`}</style>
</>
</div>
);
}