diff --git a/custom/fitness-demo/.babelrc b/custom/fitness-demo/.babelrc
new file mode 100644
index 0000000..a6f4434
--- /dev/null
+++ b/custom/fitness-demo/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": ["next/babel"],
+ "plugins": ["inline-react-svg"]
+}
diff --git a/custom/fitness-demo/.gitignore b/custom/fitness-demo/.gitignore
new file mode 100644
index 0000000..058f0ec
--- /dev/null
+++ b/custom/fitness-demo/.gitignore
@@ -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
\ No newline at end of file
diff --git a/custom/fitness-demo/README.md b/custom/fitness-demo/README.md
new file mode 100644
index 0000000..648f221
--- /dev/null
+++ b/custom/fitness-demo/README.md
@@ -0,0 +1,55 @@
+# Basic call
+
+
+
+### 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
+
+[](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)
diff --git a/custom/fitness-demo/components/App/App.js b/custom/fitness-demo/components/App/App.js
new file mode 100644
index 0000000..81b43d9
--- /dev/null
+++ b/custom/fitness-demo/components/App/App.js
@@ -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: ,
+ ...customComponentForState,
+ });
+
+ // Memoize children to avoid unnecassary renders from HOC
+ return useMemo(
+ () => (
+ <>
+ {roomExp && }
+
+ )}
+ {error && (
+
+ An error occured
+
+ {error}
+ An error occured when trying to create a demo room. Please check
+ that your environmental variables are correct and try again.
+
+
+ )}
+
+
+
+ Please ensure you have set both the DAILY_API_KEY and{' '}
+ DAILY_DOMAIN environmental variables. An example can be
+ found in the provided env.example file.
+
+
+ If you do not yet have a Daily developer account, please{' '}
+
+ create one now
+
+ . You can find your Daily API key on the{' '}
+
+ developer page
+ {' '}
+ of the dashboard.
+
+
+
+);
+
+export default NotConfigured;
diff --git a/custom/fitness-demo/env.example b/custom/fitness-demo/env.example
new file mode 100644
index 0000000..b4eeffe
--- /dev/null
+++ b/custom/fitness-demo/env.example
@@ -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
\ No newline at end of file
diff --git a/custom/fitness-demo/image.png b/custom/fitness-demo/image.png
new file mode 100644
index 0000000..8623384
Binary files /dev/null and b/custom/fitness-demo/image.png differ
diff --git a/custom/fitness-demo/index.js b/custom/fitness-demo/index.js
new file mode 100644
index 0000000..9044efc
--- /dev/null
+++ b/custom/fitness-demo/index.js
@@ -0,0 +1 @@
+// Note: I am here because next-transpile-modules requires a mainfile
diff --git a/custom/fitness-demo/next.config.js b/custom/fitness-demo/next.config.js
new file mode 100644
index 0000000..81f29cd
--- /dev/null
+++ b/custom/fitness-demo/next.config.js
@@ -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,
+ },
+});
diff --git a/custom/fitness-demo/package.json b/custom/fitness-demo/package.json
new file mode 100644
index 0000000..839e51c
--- /dev/null
+++ b/custom/fitness-demo/package.json
@@ -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"
+ }
+}
diff --git a/custom/fitness-demo/pages/_app.js b/custom/fitness-demo/pages/_app.js
new file mode 100644
index 0000000..d8a7a82
--- /dev/null
+++ b/custom/fitness-demo/pages/_app.js
@@ -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 (
+ <>
+
+ Daily - {process.env.PROJECT_TITLE}
+
+
+
+ >
+ );
+}
+
+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;
diff --git a/custom/fitness-demo/pages/_document.js b/custom/fitness-demo/pages/_document.js
new file mode 100644
index 0000000..d57b011
--- /dev/null
+++ b/custom/fitness-demo/pages/_document.js
@@ -0,0 +1,23 @@
+import Document, { Html, Head, Main, NextScript } from 'next/document';
+
+class MyDocument extends Document {
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default MyDocument;
diff --git a/custom/fitness-demo/pages/api/createRoom.js b/custom/fitness-demo/pages/api/createRoom.js
new file mode 100644
index 0000000..533b237
--- /dev/null
+++ b/custom/fitness-demo/pages/api/createRoom.js
@@ -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);
+}
diff --git a/custom/fitness-demo/pages/api/presence.js b/custom/fitness-demo/pages/api/presence.js
new file mode 100644
index 0000000..b3ce9ee
--- /dev/null
+++ b/custom/fitness-demo/pages/api/presence.js
@@ -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);
+}
diff --git a/custom/fitness-demo/pages/api/token.js b/custom/fitness-demo/pages/api/token.js
new file mode 100644
index 0000000..37137da
--- /dev/null
+++ b/custom/fitness-demo/pages/api/token.js
@@ -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);
+}
diff --git a/custom/fitness-demo/pages/index.js b/custom/fitness-demo/pages/index.js
new file mode 100644
index 0000000..4eddf7f
--- /dev/null
+++ b/custom/fitness-demo/pages/index.js
@@ -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 (
+
+ {(() => {
+ if (!isConfigured) return ;
+ return (
+
+ type === 'join' ? setRoomName(room): createRoom(room, duration, privacy)
+ }
+ />
+ );
+ })()}
+
+
+
+ );
+ }
+
+ /**
+ * Main call UI
+ */
+ return (
+
+
+
+
+
+
+ {customAppComponent || }
+
+
+
+
+
+
+ );
+}
+
+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,
+ };
+}
diff --git a/custom/fitness-demo/pages/not-found.js b/custom/fitness-demo/pages/not-found.js
new file mode 100644
index 0000000..1d25fb7
--- /dev/null
+++ b/custom/fitness-demo/pages/not-found.js
@@ -0,0 +1,21 @@
+import React from 'react';
+import MessageCard from '@custom/shared/components/MessageCard';
+
+export default function RoomNotFound() {
+ return (
+
+
+ The room you are trying to join does not exist. Have you created the
+ room using the Daily REST API or the dashboard?
+
+
+
+ );
+}
diff --git a/custom/fitness-demo/public/assets/daily-logo-dark.svg b/custom/fitness-demo/public/assets/daily-logo-dark.svg
new file mode 100644
index 0000000..ef3a565
--- /dev/null
+++ b/custom/fitness-demo/public/assets/daily-logo-dark.svg
@@ -0,0 +1,14 @@
+
diff --git a/custom/fitness-demo/public/assets/daily-logo.svg b/custom/fitness-demo/public/assets/daily-logo.svg
new file mode 100644
index 0000000..534a18a
--- /dev/null
+++ b/custom/fitness-demo/public/assets/daily-logo.svg
@@ -0,0 +1,14 @@
+
diff --git a/custom/fitness-demo/public/assets/join.mp3 b/custom/fitness-demo/public/assets/join.mp3
new file mode 100644
index 0000000..7657915
Binary files /dev/null and b/custom/fitness-demo/public/assets/join.mp3 differ
diff --git a/custom/fitness-demo/public/assets/message.mp3 b/custom/fitness-demo/public/assets/message.mp3
new file mode 100644
index 0000000..a067315
Binary files /dev/null and b/custom/fitness-demo/public/assets/message.mp3 differ
diff --git a/custom/fitness-demo/public/assets/pattern-bg.png b/custom/fitness-demo/public/assets/pattern-bg.png
new file mode 100644
index 0000000..01e0d0d
Binary files /dev/null and b/custom/fitness-demo/public/assets/pattern-bg.png differ