Start with fitness-demo

This commit is contained in:
harshithpabbati 2021-12-28 21:41:18 +05:30
parent 5b5643e8a7
commit ec770a189a
32 changed files with 1170 additions and 0 deletions

View File

@ -0,0 +1,4 @@
{
"presets": ["next/babel"],
"plugins": ["inline-react-svg"]
}

35
custom/fitness-demo/.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,55 @@
# Basic call
![Basic Call](./image.png)
### Live example
**[See it in action here ➡️](https://custom-basic-call.vercel.app)**
---
## What does this demo do?
- Built on [NextJS](https://nextjs.org/)
- Create a Daily instance using call object mode
- Manage user media devices
- Render UI based on the call state
- Handle media and call errors
- Obtain call access token via Daily REST API
- Handle preauthentication, knock for access and auto join
Please note: this demo is not currently mobile optimised
### Getting started
```
# set both DAILY_API_KEY and DAILY_DOMAIN
mv env.example .env.local
# from project root...
yarn
yarn workspace @custom/basic-call dev
```
## How does this example work?
This demo puts to work the following [shared libraries](../shared):
**[MediaDeviceProvider.js](../shared/contexts/MediaDeviceProvider.js)**
Convenience context that provides an interface to media devices throughout app
**[useDevices.js](../shared/contexts/useDevices.js)**
Hook for managing the enumeration and status of client media devices)
**[CallProvider.js](../shared/contexts/CallProvider.js)**
Primary call context that manages Daily call state, participant state and call object interaction
**[useCallMachine.js](../shared/contexts/useCallMachine.js)**
Abstraction hook that manages Daily call state and error handling
**[ParticipantsProvider.js](../shared/contexts/ParticipantsProvider.js)**
Manages participant state and abstracts common selectors / derived data
## 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,51 @@
import React, { useMemo } from 'react';
import ExpiryTimer from '@custom/shared/components/ExpiryTimer';
import { useCallState } from '@custom/shared/contexts/CallProvider';
import { useCallUI } from '@custom/shared/hooks/useCallUI';
import PropTypes from 'prop-types';
import Room from '../Call/Room';
import { Asides } from './Asides';
import { Modals } from './Modals';
export const App = ({ customComponentForState }) => {
const { roomExp, state } = useCallState();
const componentForState = useCallUI({
state,
room: <Room />,
...customComponentForState,
});
// Memoize children to avoid unnecassary renders from HOC
return useMemo(
() => (
<>
{roomExp && <ExpiryTimer expiry={roomExp} />}
<div className="app">
{componentForState()}
<Modals />
<Asides />
<style jsx>{`
color: white;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
.loader {
margin: 0 auto;
}
`}</style>
</div>
</>
),
[componentForState, roomExp]
);
};
App.propTypes = {
customComponentForState: PropTypes.any,
};
export default App;

View File

@ -0,0 +1,20 @@
import React from 'react';
import { NetworkAside } from '@custom/shared/components/Aside';
import { PeopleAside } from '@custom/shared/components/Aside';
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
export const Asides = () => {
const { asides } = useUIState();
return (
<>
<PeopleAside />
<NetworkAside />
{asides.map((AsideComponent) => (
<AsideComponent key={AsideComponent.name} />
))}
</>
);
};
export default Asides;

View File

@ -0,0 +1,18 @@
import React from 'react';
import DeviceSelectModal from '@custom/shared/components/DeviceSelectModal/DeviceSelectModal';
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
export const Modals = () => {
const { modals } = useUIState();
return (
<>
<DeviceSelectModal />
{modals.map((ModalComponent) => (
<ModalComponent key={ModalComponent.name} />
))}
</>
);
};
export default Modals;

View File

@ -0,0 +1 @@
export { App as default } from './App';

View File

@ -0,0 +1,49 @@
import React, { useMemo } from 'react';
import { Audio } from '@custom/shared/components/Audio';
import { BasicTray } from '@custom/shared/components/Tray';
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
import useJoinSound from '@custom/shared/hooks/useJoinSound';
import PropTypes from 'prop-types';
import { WaitingRoom } from './WaitingRoom';
export const Container = ({ children }) => {
const { isOwner } = useParticipants();
useJoinSound();
const roomComponents = useMemo(
() => (
<>
{/* Show waiting room notification & modal if call owner */}
{isOwner && <WaitingRoom />}
{/* Tray buttons */}
<BasicTray />
{/* Audio tags */}
<Audio />
</>
),
[isOwner]
);
return (
<div className="room">
{children}
{roomComponents}
<style jsx>{`
.room {
flex-flow: column nowrap;
width: 100%;
height: 100%;
display: flex;
}
`}</style>
</div>
);
};
Container.propTypes = {
children: PropTypes.node,
};
export default Container;

View File

@ -0,0 +1,56 @@
import React, { useMemo } from 'react';
import HeaderCapsule from '@custom/shared/components/HeaderCapsule';
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
export const Header = () => {
const { participantCount } = useParticipants();
const { customCapsule } = useUIState();
return useMemo(
() => (
<header className="room-header">
<img
src="/assets/daily-logo.svg"
alt="Daily"
className="logo"
width="80"
height="32"
/>
<HeaderCapsule>{process.env.PROJECT_TITLE}</HeaderCapsule>
<HeaderCapsule>
{`${participantCount} ${
participantCount === 1 ? 'participant' : 'participants'
}`}
</HeaderCapsule>
{customCapsule && (
<HeaderCapsule variant={customCapsule.variant}>
{customCapsule.variant === 'recording' && <span />}
{customCapsule.label}
</HeaderCapsule>
)}
<style jsx>{`
.room-header {
display: flex;
flex: 0 0 auto;
column-gap: var(--spacing-xxs);
box-sizing: border-box;
padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-xxs)
var(--spacing-sm);
align-items: center;
width: 100%;
}
.logo {
margin-right: var(--spacing-xs);
}
`}</style>
</header>
),
[participantCount, customCapsule]
);
};
export default Header;

View File

@ -0,0 +1,17 @@
import React from 'react';
import VideoContainer from '@custom/shared/components/VideoContainer/VideoContainer';
import { Container } from './Container';
import { Header } from './Header';
import { VideoGrid } from './VideoGrid';
export function Room({ children }) {
return (
<Container>
<Header />
<VideoContainer>{children ? children : <VideoGrid />}</VideoContainer>
</Container>
);
}
export default Room;

View File

@ -0,0 +1,140 @@
import React, { useState, useMemo, useEffect, useRef } from 'react';
import Tile from '@custom/shared/components/Tile';
import { DEFAULT_ASPECT_RATIO } from '@custom/shared/constants';
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
import { useDeepCompareMemo } from 'use-deep-compare';
/**
* Basic unpaginated video tile grid, scaled by aspect ratio
*
* Note: this component is designed to work with automated track subscriptions
* and is only suitable for small call sizes as it will show all participants
* and not paginate.
*
* Note: this grid does not show screenshares (just participant cams)
*
* Note: this grid does not sort participants
*/
export const VideoGrid = React.memo(
() => {
const containerRef = useRef();
const { participants } = useParticipants();
const [dimensions, setDimensions] = useState({
width: 1,
height: 1,
});
// Keep a reference to the width and height of the page, so we can repack
useEffect(() => {
let frame;
const handleResize = () => {
if (frame) cancelAnimationFrame(frame);
frame = requestAnimationFrame(() =>
setDimensions({
width: containerRef.current?.clientWidth,
height: containerRef.current?.clientHeight,
})
);
};
handleResize();
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
};
}, []);
// Basic brute-force packing algo
const layout = useMemo(() => {
const aspectRatio = DEFAULT_ASPECT_RATIO;
const tileCount = participants.length || 0;
const w = dimensions.width;
const h = dimensions.height;
// brute-force search layout where video occupy the largest area of the container
let bestLayout = {
area: 0,
cols: 0,
rows: 0,
width: 0,
height: 0,
};
for (let cols = 0; cols <= tileCount; cols += 1) {
const rows = Math.ceil(tileCount / cols);
const hScale = w / (cols * aspectRatio);
const vScale = h / rows;
let width;
let height;
if (hScale <= vScale) {
width = Math.floor(w / cols);
height = Math.floor(width / aspectRatio);
} else {
height = Math.floor(h / rows);
width = Math.floor(height * aspectRatio);
}
const area = width * height;
if (area > bestLayout.area) {
bestLayout = {
area,
width,
height,
rows,
cols,
};
}
}
return bestLayout;
}, [dimensions, participants]);
// Memoize our tile list to avoid unnecassary re-renders
const tiles = useDeepCompareMemo(
() =>
participants.map((p) => (
<Tile
participant={p}
key={p.id}
mirrored
style={{ maxWidth: layout.width, maxHeight: layout.height }}
/>
)),
[layout, participants]
);
if (!participants.length) {
return null;
}
return (
<div className="video-grid" ref={containerRef}>
<div className="tiles">{tiles}</div>
<style jsx>{`
.video-grid {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
position: relative;
width: 100%;
}
.video-grid .tiles {
align-items: center;
display: flex;
flex-flow: row wrap;
max-height: 100%;
justify-content: center;
margin: auto;
overflow: hidden;
width: 100%;
}
`}</style>
</div>
);
},
() => true
);
export default VideoGrid;

View File

@ -0,0 +1,18 @@
import React from 'react';
import {
WaitingRoomModal,
WaitingRoomNotification,
} from '@custom/shared/components/WaitingRoom';
import { useWaitingRoom } from '@custom/shared/contexts/WaitingRoomProvider';
export const WaitingRoom = () => {
const { setShowModal, showModal } = useWaitingRoom();
return (
<>
<WaitingRoomNotification />
{showModal && <WaitingRoomModal onClose={() => setShowModal(false)} />}
</>
);
};
export default WaitingRoom;

View File

@ -0,0 +1,100 @@
import React, { useState, useEffect } from 'react';
import { Card, CardHeader, CardBody } from '@custom/shared/components/Card';
import Loader from '@custom/shared/components/Loader';
import Well from '@custom/shared/components/Well';
import PropTypes from 'prop-types';
export const CreatingRoom = ({ onCreated }) => {
const [room, setRoom] = useState();
const [fetching, setFetching] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
if (room) return;
async function createRoom() {
setError(false);
setFetching(true);
console.log(`🚪 Creating new demo room...`);
// Create a room server side (using Next JS serverless)
const res = await fetch('/api/createRoom', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
const resJson = await res.json();
if (resJson.name) {
setFetching(false);
setRoom(resJson.name);
return;
}
setError(resJson.error || 'An unknown error occured');
setFetching(false);
}
createRoom();
}, [room]);
useEffect(() => {
if (!room || !onCreated) return;
console.log(`🚪 Room created: ${room}, joining now`);
onCreated(room, true);
}, [room, onCreated]);
return (
<div className="creating-room">
{fetching && (
<div className="creating">
<Loader /> Creating new demo room...
</div>
)}
{error && (
<Card error>
<CardHeader>An error occured</CardHeader>
<CardBody>
<Well variant="error">{error}</Well>
An error occured when trying to create a demo room. Please check
that your environmental variables are correct and try again.
</CardBody>
</Card>
)}
<style jsx>
{`
.creating-room {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
max-width: 420px;
margin: 0 auto;
}
.creating-room .creating {
display: flex;
}
.creating-room :global(.loader) {
margin-right: var(--spacing-xxxs);
}
`}
</style>
</div>
);
};
CreatingRoom.propTypes = {
onCreated: PropTypes.func.isRequired,
};
export default CreatingRoom;

View File

@ -0,0 +1,160 @@
import React, { useEffect, useState } from 'react';
import Button from '@custom/shared/components/Button';
import {
Card,
CardBody,
CardFooter,
CardHeader,
} from '@custom/shared/components/Card';
import Field from '@custom/shared/components/Field';
import { TextInput, BooleanInput, SelectInput } from '@custom/shared/components/Input';
import Well from '@custom/shared/components/Well';
import PropTypes from 'prop-types';
/**
* Intro
* ---
* Specify which room we would like to join
*/
export const Intro = ({
room,
error,
onJoin,
}) => {
const [rooms, setRooms] = useState({});
const [duration, setDuration] = useState("30");
const [roomName, setRoomName] = useState();
const [privacy, setPrivacy] = useState(false);
const fetchRooms = async () => {
const res = await fetch('/api/presence', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const resJson = await res.json();
setRooms(resJson);
}
useEffect(() => {
fetchRooms();
const i = setInterval(fetchRooms, 15000);
return () => clearInterval(i);
}, []);
useEffect(() => {
setRoomName(room);
}, [room]);
return (
<div className="intro">
<Card>
<div className="jc-card">
<CardHeader>Join a class</CardHeader>
<CardBody>
{Object.keys(rooms).length === 0 && (
<p>
Looks like there&apos;s no class going on right now,
start with creating one!
</p>
)}
{Object.keys(rooms).map(room => (
<div className="room" key={room}>
<div>
<div className="label">{room}</div>
<span>{`${rooms[room].length} people in class`}</span>
</div>
<div className="join-room">
<Button variant="dark" size="tiny" onClick={() => onJoin(room, 'join')}>
Join Room
</Button>
</div>
</div>
))}
</CardBody>
</div>
</Card>
<span className="or-text">OR</span>
<Card>
<div className="jc-card">
<CardHeader>Create a class</CardHeader>
<CardBody>
{error && (
<Well variant="error">
Failed to create room <p>{error}</p>
</Well>
)}
<Field label="Give you a class name">
<TextInput
type="text"
placeholder="Eg. super-stretch"
defaultValue={roomName}
onChange={(e) => setRoomName(e.target.value)}
required
/>
</Field>
<Field label="How long would you like to be?">
<SelectInput
onChange={(e) => setDuration(e.target.value)}
value={duration}>
<option value="15">15 minutes</option>
<option value="30">30 minutes</option>
<option value="60">60 minutes</option>
</SelectInput>
</Field>
<Field label="Public (anyone can join)">
<BooleanInput value={privacy} onChange={e => setPrivacy(e.target.checked)} />
</Field>
</CardBody>
<CardFooter divider>
<Button
fullWidth
onClick={() => onJoin(roomName, 'create', duration, privacy)}
>
Create class
</Button>
</CardFooter>
</div>
</Card>
<style jsx>{`
.intro {
display: flex;
gap: 15px;
margin: auto;
}
.or-text {
color: var(--reverse);
margin: auto;
}
.room {
display: flex;
width: 25vw;
border-bottom: 1px solid var(--gray-light);
padding-bottom: var(--spacing-xxs);
gap: 10px;
}
.room .label {
font-weight: var(--weight-medium);
color: var(--text-default);
}
.room .join-room {
margin-left: auto;
margin-bottom: auto;
margin-top: auto;
}
.jc-card {
width: 25vw;
}
`}
</style>
</div>
);
};
Intro.propTypes = {
room: PropTypes.string,
onJoin: PropTypes.func.isRequired,
};
export default Intro;

View File

@ -0,0 +1,36 @@
import React from 'react';
import { Card, CardBody, CardHeader } from '@custom/shared/components/Card';
export const NotConfigured = () => (
<Card>
<CardHeader>Environmental variables not set</CardHeader>
<CardBody>
<p>
Please ensure you have set both the <code>DAILY_API_KEY</code> and{' '}
<code>DAILY_DOMAIN</code> environmental variables. An example can be
found in the provided <code>env.example</code> file.
</p>
<p>
If you do not yet have a Daily developer account, please{' '}
<a
href="https://dashboard.daily.co/signup"
target="_blank"
rel="noreferrer"
>
create one now
</a>
. You can find your Daily API key on the{' '}
<a
href="https://dashboard.daily.co/developers"
target="_blank"
rel="noreferrer"
>
developer page
</a>{' '}
of the dashboard.
</p>
</CardBody>
</Card>
);
export default NotConfigured;

View File

@ -0,0 +1,11 @@
# 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
# Run in demo mode (will create a demo room for you to try)
DAILY_DEMO_MODE=0

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1 @@
// Note: I am here because next-transpile-modules requires a mainfile

View File

@ -0,0 +1,10 @@
const withPlugins = require('next-compose-plugins');
const withTM = require('next-transpile-modules')(['@custom/shared']);
const packageJson = require('./package.json');
module.exports = withPlugins([withTM], {
env: {
PROJECT_TITLE: packageJson.description,
},
});

View File

@ -0,0 +1,22 @@
{
"name": "@custom/fitness-demo",
"description": "Basic Call + Fitness Demo",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@custom/shared": "*",
"next": "^11.1.2",
"pluralize": "^8.0.0"
},
"devDependencies": {
"babel-plugin-module-resolver": "^4.1.0",
"next-compose-plugins": "^2.2.1",
"next-transpile-modules": "^8.0.0"
}
}

View File

@ -0,0 +1,39 @@
import React from 'react';
import GlobalStyle from '@custom/shared/components/GlobalStyle';
import Head from 'next/head';
import PropTypes from 'prop-types';
function App({ Component, pageProps }) {
return (
<>
<Head>
<title>Daily - {process.env.PROJECT_TITLE}</title>
</Head>
<GlobalStyle />
<Component
asides={App.asides}
modals={App.modals}
customTrayComponent={App.customTrayComponent}
customAppComponent={App.customAppComponent}
{...pageProps}
/>
</>
);
}
App.defaultProps = {
Component: null,
pageProps: {},
};
App.propTypes = {
Component: PropTypes.elementType,
pageProps: PropTypes.object,
};
App.asides = [];
App.modals = [];
App.customTrayComponent = null;
App.customAppComponent = null;
export default App;

View File

@ -0,0 +1,23 @@
import Document, { Html, Head, Main, NextScript } from 'next/document';
class MyDocument extends Document {
render() {
return (
<Html>
<Head>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600&display=optional"
rel="stylesheet"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;

View File

@ -0,0 +1,42 @@
export default async function handler(req, res) {
const { roomName, privacy, expiryMinutes, ...rest } = req.body;
if (req.method === 'POST') {
console.log(`Creating room on domain ${process.env.DAILY_DOMAIN}`);
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.DAILY_API_KEY}`,
},
body: JSON.stringify({
name: roomName,
privacy: privacy || 'public',
properties: {
exp: Math.round(Date.now() / 1000) + (expiryMinutes || 5) * 60, // expire in x minutes
eject_at_room_exp: true,
enable_knocking: privacy !== 'public',
...rest,
},
}),
};
const dailyRes = await fetch(
`${process.env.DAILY_REST_DOMAIN || 'https://api.daily.co/v1'}/rooms`,
options
);
const { name, url, error } = await dailyRes.json();
if (error) {
return res.status(500).json({ error });
}
return res
.status(200)
.json({ name, url, domain: process.env.DAILY_DOMAIN });
}
return res.status(500);
}

View File

@ -0,0 +1,28 @@
/*
* This is an example server-side function that generates a meeting token
* server-side. You could replace this on your own back-end to include
* custom user authentication, etc.
*/
export default async function handler(req, res) {
if (req.method === 'GET') {
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.DAILY_API_KEY}`,
},
};
const dailyRes = await fetch(
`${
process.env.DAILY_REST_DOMAIN || 'https://api.daily.co/v1'
}/presence`,
options
);
const response = await dailyRes.json();
return res.status(200).json(response);
}
return res.status(500);
}

View File

@ -0,0 +1,40 @@
/*
* This is an example server-side function that generates a meeting token
* server-side. You could replace this on your own back-end to include
* custom user authentication, etc.
*/
export default async function handler(req, res) {
const { roomName, isOwner } = req.body;
if (req.method === 'POST' && roomName) {
console.log(`Getting token for room '${roomName}' as owner: ${isOwner}`);
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.DAILY_API_KEY}`,
},
body: JSON.stringify({
properties: { room_name: roomName, is_owner: isOwner },
}),
};
const dailyRes = await fetch(
`${
process.env.DAILY_REST_DOMAIN || 'https://api.daily.co/v1'
}/meeting-tokens`,
options
);
const { token, error } = await dailyRes.json();
if (error) {
return res.status(500).json({ error });
}
return res.status(200).json({ token, domain: process.env.DAILY_DOMAIN });
}
return res.status(500);
}

View File

@ -0,0 +1,145 @@
import React, { useState, useCallback } from 'react';
import { CallProvider } from '@custom/shared/contexts/CallProvider';
import { MediaDeviceProvider } from '@custom/shared/contexts/MediaDeviceProvider';
import { ParticipantsProvider } from '@custom/shared/contexts/ParticipantsProvider';
import { TracksProvider } from '@custom/shared/contexts/TracksProvider';
import { UIStateProvider } from '@custom/shared/contexts/UIStateProvider';
import { WaitingRoomProvider } from '@custom/shared/contexts/WaitingRoomProvider';
import getDemoProps from '@custom/shared/lib/demoProps';
import PropTypes from 'prop-types';
import App from '../components/App';
import Intro from '../components/Prejoin/Intro';
import NotConfigured from '../components/Prejoin/NotConfigured';
/**
* Index page
* ---
* - Checks configuration variables are set in local env
* - Optionally obtain a meeting token from Daily REST API (./pages/api/token)
* - Set call owner status
* - Finally, renders the main application loop
*/
export default function Index({
domain,
isConfigured = false,
forceFetchToken = false,
forceOwner = false,
subscribeToTracksAutomatically = true,
demoMode = false,
asides,
modals,
customTrayComponent,
customAppComponent,
}) {
const [roomName, setRoomName] = useState();
const [fetching, setFetching] = useState(false);
const [token, setToken] = useState();
const [error, setError] = useState();
const createRoom = async (room, duration, privacy) => {
setError(false);
setFetching(true);
console.log(`🚪 Creating a new class...`);
// Create a room server side (using Next JS serverless)
const res = await fetch('/api/createRoom', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
roomName: room,
expiryMinutes: Number(duration),
privacy: !privacy ? 'private': 'public'
}),
});
const resJson = await res.json();
if (resJson.name) {
setFetching(false);
setRoomName(resJson.name);
return;
}
setError(resJson.error || 'An unknown error occured');
setFetching(false);
}
const isReady = !!(isConfigured && roomName);
if (!isReady) {
return (
<main>
{(() => {
if (!isConfigured) return <NotConfigured />;
return (
<Intro
room={roomName}
domain={domain}
onJoin={(room, type, duration = 60, privacy = 'public') =>
type === 'join' ? setRoomName(room): createRoom(room, duration, privacy)
}
/>
);
})()}
<style jsx>{`
height: 100vh;
display: grid;
align-items: center;
justify-content: center;
`}</style>
</main>
);
}
/**
* Main call UI
*/
return (
<UIStateProvider
asides={asides}
modals={modals}
customTrayComponent={customTrayComponent}
>
<CallProvider
domain={domain}
room={roomName}
token={token}
subscribeToTracksAutomatically={subscribeToTracksAutomatically}
>
<ParticipantsProvider>
<TracksProvider>
<MediaDeviceProvider>
<WaitingRoomProvider>
{customAppComponent || <App />}
</WaitingRoomProvider>
</MediaDeviceProvider>
</TracksProvider>
</ParticipantsProvider>
</CallProvider>
</UIStateProvider>
);
}
Index.propTypes = {
isConfigured: PropTypes.bool.isRequired,
domain: PropTypes.string,
asides: PropTypes.arrayOf(PropTypes.func),
modals: PropTypes.arrayOf(PropTypes.func),
customTrayComponent: PropTypes.node,
customAppComponent: PropTypes.node,
forceFetchToken: PropTypes.bool,
forceOwner: PropTypes.bool,
subscribeToTracksAutomatically: PropTypes.bool,
demoMode: PropTypes.bool,
};
export async function getStaticProps() {
const defaultProps = getDemoProps();
return {
props: defaultProps,
};
}

View File

@ -0,0 +1,21 @@
import React from 'react';
import MessageCard from '@custom/shared/components/MessageCard';
export default function RoomNotFound() {
return (
<div className="not-found">
<MessageCard error header="Room not found">
The room you are trying to join does not exist. Have you created the
room using the Daily REST API or the dashboard?
</MessageCard>
<style jsx>{`
display: grid;
align-items: center;
justify-content: center;
grid-template-columns: 620px;
width: 100%;
height: 100vh;
`}</style>
</div>
);
}

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="#121A24"/>
<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="#121A24"/>
<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="#121A24"/>
<path d="M56.9765 26.6866H61.275V4H56.9765V26.6866Z" fill="#121A24"/>
<path d="M70.5634 32L78.9917 11.0746H74.8711L71.3499 20.4478L67.5589 11.0746H62.9021L69.1523 25.1953L66.3779 32H70.5634Z" fill="#121A24"/>
<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

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.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB