React engine
Simple SDK for easy and flexible implementation of microsite featuring Webout player project.
This package contains:
- Licensing & Sharing of videos
- Platform & device checks for available browser APIs
- Routing over desired features
- Common features implementation
You can fire up your project by running:
pnpm add @webout1/react-engine
NOTE: You need to have .npmrc file in your project's root directory containing our package registry setup to be able to download the package.
How to get started
This package offers you features for two different scenarios of a microsite.
You might want to have an interactive version, where user first visits the page
and creates video, or shared version, which is read-only instance
with a video created by other user via the interactive version.
If you want to go straight for the shared version, you can follow this link to
setup video sharing.
Before we can dive into actual implementation of the project, you will have to have preapred following steps:
- Derived data factories
- State management
- Licensing
All of these are subchapters of this chapter How to get started.
Prepare for VAR
Perhaps one of the first things you might want to do during your setup of an interactive
microsite is to start with vars and args factories.
NOTE: If you are unsure what vars and args your project requires, reach out to us
Hence there will be more places using these factories, it might be a good idea to have them defined as factory functions rather defining them conversion with each place of use inside callbacks.
What these functions essentially do is that their take arbitary state model and from it's actual value derives by player understandable data which would become part of the video.
Example:
function createVars(state: State) {
return {
TEXT_FIRST_NAME: state.name,
TEXT_LAST_NAME: state.family,
IMAGE_PHOTO: state.image,
}
}
function createArgs(state: State) {
return {
FIRST_NAME: state.name,
GENDER: state.gender,
}
}
NOTE: State type is your state model, we will get into that in following section. For now you can imagine it as any type you define
State management
To produce experience unqiue to the user, we rely on the user to provide
us a unique information, hence we need an observable that would allows to
change player's state as the user input progress.
How you implement state in your project is completely up to you,
the only requirement we have, is that we must be able to read the changes and update the state.
This fairly simple API can be achieved using any state management library as Redux, Zustand, or
just React's useState API.
Example (Zustand):
import React from "react";
import { createStore, useStoreWithEqualityFn } from "zustand";
import { createContext } from "@webout1/react-engine";
// Define your state model
type State = { name: string, image: string, gender: "male" | "female" }
// Make your state factory
function createState() {
return createStore<State>()((set, get) => ({
name: '',
image: '',
gender: 'male',
update: <T extends keyof State>(property: T, value: State[T]) => (
set({ [property]: value })
)
}))
}
type StateStore = ReturnType<typeof createState>;
// Create context
const [Provider, useProvider] = createContext<StateStore>('StateContext');
// Add a context provider for your Sync API
function StateProvider({ children }: Children) {
const instance = React.useRef<StateStore>()
if (!instance.current) {
instance.current = createState()
}
return (
<Provider value={instance.current}>
{children}
</Provider>
)
}
// Create state selectors
function useStateStore<T>(selector: (state: State) => T) {
const store = useProvider()
return useStoreWithEqualityFn(store, selector)
}
function useStateProperty(property: keyof State) {
return useStateStore((s) => s[property])
}
NOTE: Whenever you use context, module singleton, or you bash everything into a single file, all of this is up to you
Licensing & Sharing (Dragonfly)
For user to be able to play video and share it, there has to be a server in the background. Luckily, we got one, so you don't
have to worry about that. Yet there is still some work to do, in this step you have to initiate Dragonfly instance
with link to the server given and a factory method to create shareable payload.
TIP: If you intend to build also shared version, you might design this feature as a component that would be
added into both versions as plain wrapper with just different props.
Example:
import { DragonflyProvider } from "@webout1/react-engine";
import { useStateProvider } from "../_providers/State.provider";
// Servers endpoints
const DRAGONFLY_URL = process.env.NEXT_PUBLIC_DRAGONFLY_URL;
const UPLOADER_URL = process.env.NEXT_PUBLIC_UPLOADER_URL;
const PROJECT_ID = +process.env.NEXT_PUBLIC_PROJECT_ID
// Factory for shareable payload
async function createPayload(state: State) {
const vars = createVars(state);
const args = createArgs(state);
const imageToShare = state.image
? (await uploadImage(UPLOADER_URL, state.image)).data.filePath
: null;
const thumbnail = state.thumbnail
? (await uploadImage(UPLOADER_URL, state.thumbnail)).data.filePath
: null;
const finalVars = {
...vars,
THUMBNAIL_PHOTO: thumbnail,
IMAGE_PHOTO: imageToShare,
}
return {
variables: finalVars,
arguments: args,
}
}
// Wrapper component
function DragonflyWrapper({ children }: ElementChildren) {
const provider = useStateProvider()
const share = async () => await createPayload(provider.getState())
return (
<DragonflyProvider createPayloadForShare={share} dragonflyUrl={DRAGONFLY_URL} projectId={PROJECT_ID}>
{children}
</DragonflyProvider>
)
}
export default DragonflyWrapper
The purpose of the factory here is that the instance of shared version player can easily load the user's vars and args
and play the exact same content the user itended to share.
With state management ready, dragonfly-wrapper wrapped over the application we are ready to dive into microsite's features
Screens & features
Now that everything is in place, we are ready to build some features for the user. Default and most common features among microsites are:
- Input form
- Video player
- Selfie taker
- Reels & Video generator
- Video share
Decide which ones you want to implement and examine their APIs in following sections.
For transitions between features, you may want to use built-in Router,
which is basically a simple, lightweight script using schema as roadmap.
Screens - Built-in routing
Built-in router offers easy implementation of in-app routing -- route changes does not affect nor are derived from the current URL as features rendered by the router could become unaccessible cause of data loss caused by the memory wipe out.
Interactive version routing can be setup as follows:
import { Router, sitemaps } from '@webout1/react-engine'
function Campaign() {
return (
<Router.Root scheme={sitemaps.common} initial='FORM_ROUTE'>
...
</Router.Root>
)
}
Shared version routing can be setup as follows:
...
<Router.Root scheme={sitemaps.shared} closeOrRedirect={redirectToForm} initial="PLAY_ROUTE">
...
</Router.Root>
...
The main differences are that shared version routing starts at play screen and does NOT have form screen.
Screens - Input form
Form screen is an entrypoint, the very first screen user will see entering the microsite.
The only one requirement for this screen is just a wrapper around your form:
function FormScreen() {
return (
<Form.Root>
<section>
...
</section>
</Form.Root>
)
}
The JSX passed in as children can be whatever you want them to be with respect to React boundaries.
There are some shorthands built-in to reduce time spent building the form:
- InputName
- InputLastname
- InputGender
- InputPicture
- FormActionContinue
Shorthands - InputName / InputLastname
Just a simple input component that you may use as an input for the user's names, all you have to do is add a className for styling and state API.
Usage:
function NameInput() {
const name = useStateStore((state) => state.name);
const setName = useStateProvider().getState().setName;
return <Form.InputName name={name} setName={setName} className="input-base w-full text-2xl font-SourceSans-regular" />
}
Shorthands - InputGender
A wrapper around accessible gender select buttons.
Usage:
import React from "react";
import { Form } from "@webout1/react-engine";
import { useStateProvider, useStateStore } from "../_providers/State.provider";
import Button from "./Button";
import clsx from "clsx";
function GenderInput() {
const gender = useStateStore((state) => state.gender);
const setGender = useStateProvider().getState().setGender;
return (
<Form.InputGender gender={gender} setGender={setGender} className="flex justify-center">
<Form.InputGenderItem
gender="male"
renderCustomButton={(checked) => {
const className = clsx(checked
? "bg-button-primary text-white p-4 text-2xl"
: "bg-gray-500 hover:bg-gray-600 text-white p-4 text-2xl",
"rounded-l"
)
return <Button className={className}>Muž</Button>;
}}
/>
<Form.InputGenderItem
gender="female"
renderCustomButton={(checked) => {
const className = clsx(checked
? "bg-button-primary text-white p-4 text-2xl"
: "bg-gray-500 hover:bg-gray-600 text-white p-4 text-2xl",
"rounded-r"
)
return <Button className={className}>Žena</Button>;
}}
/>
</Form.InputGender>
)
}
The input accepts gender and setGender props, where the value must be either male or female,
so it can detect which button is active.
For InputGenderItem components, you can use your own buttons as of example, or you can just stick to default ones providing them classic ButtonProps to alter their properties.
Example:
<Form.InputGenderItem gender="female" className="text-black" />
Shorthands - InputPicture
This input allows you to very quickly implement an input, where user can either click on image upload or take a selfie and with photo received replace input contents with the photo preview and remove button.
Usage:
import { Form } from '@webout1/react-engine';
import { useStateProvider, useStateStore } from "../_providers/State.provider";
import Button from './Button';
function PictureInput() {
const image = useStateStore((state) => state.image);
const setImage = useStateProvider().getState().setImage;
return (
<Form.InputPicture
image={image}
setImage={setImage}
className="flex justify-center space-x-2"
uploadImageBtn={(upload) => (
<Button onClick={upload} className="bg-button-primary rounded p-4 hover:bg-sky-500 text-text-secondary text-2xl">Nahrát fotku</Button>
)}
takeImageBtn={(take) => (
<Button onClick={take} className="bg-button-primary rounded p-4 hover:bg-sky-500 text-text-secondary text-2xl">Vyfotit fotku</Button>
)}
removeImageBtn={(remove) => (
<div className="flex justify-center items-center space-x-4 w-full">
<img
className="h-14 w-14 rounded-full object-cover shadow"
src={image!}
/>
<Button onClick={remove}>Odebrat fotku</Button>
</div>
)}
/>
)
}
Shorthands - FormActionContinue
A submit button shorthand, as with genderItems, here you may also use default UI, or provide your own:
Example:
import React from "react";
import { useStateStore } from "../_providers/State.provider";
import { Form } from '@webout1/react-engine';
import Button from "./Button";
function FormSubmitAction() {
const canSubmit = useStateStore((state) => Boolean(state.name?.length > 0 && state.family?.length > 0 && state.image));
return (
<Form.ActionContinue renderCustomAction={(submit) => (
<Button disabled={!canSubmit} onClick={submit} className="bg-button-primary rounded disabled:bg-opacity-40 text-text-secondary hover:bg-sky-500 text-2xl p-4">
Přehrát video
</Button>
)}
/>
)
}
Screens - Selfie taker
Screen intended enabling user to take a selfie, confirm with user that that's really the photo user wish to use, or repeat the process and then save result image into state.
Usage:
// Wrapper over image sections - Confirm / Take
function PictureRoot({ children }: Children) {
const image = useStateStore((state) => state.image);
const setImage = useStateProvider().getState().setImage;
return (
<Picture.Root picture={image} setPicture={setImage}>
{children}
</Picture.Root>
)
}
// Component rendered for picture confirmation
function PictureConfirm() {
const image = useStateStore((state) => state.image);
if (!image) {
return null;
}
return (
<Picture.Confirm className='video-frame' picture={image}>
<Picture.ConfirmTools>
<section className='flex justify-center mt-4 items-center space-x-4'>
<Picture.ConfirmAction className='bg-button-primary rounded p-4 hover:bg-sky-500 text-text-secondary text-2xl' label='Použít fotku' />
<Picture.ResetAction className='rounded p-4 hover:bg-gray-100 text-black text-2xl' label='Odstranit fotku' />
</section>
</Picture.ConfirmTools>
</Picture.Confirm>
)
}
// Component rendered for picture take
function PictureTake() {
const image = useStateStore((state) => state.image);
const setImage = useStateProvider().getState().setImage;
if (image) {
return null;
}
return (
<Picture.Take setPicture={setImage} className='video-frame'>
<section className='flex items-center justify-center w-full mt-4'>
<Picture.TakeAction className="bg-button-primary rounded p-4 hover:bg-sky-500 text-text-secondary text-2xl" label='Vyfotit fotku' />
</section>
</Picture.Take>
)
}
// Components composition
function Main() {
return (
<PictureRoot>
<PictureConfirm />
<PictureTake />
</PictureRoot>
)
}
Picture root makes sure that the screen related to user's picture is shown when routed to and then based on image state provided decides if render of confirm or take section should be in place.
Screens - Video player
The very main part of the microsite, video player. You can easily set it up as follows:
import React from 'react'
import { Play } from '@webout1/react-engine'
import { useStateProvider, useStateStore } from '../_providers/State.provider'
import createVars from '../_utils/createVars';
import createArgs from '../_utils/createArgs';
import useRedirectToSignature from '../_hooks/useRedirectToSignature';
const PROJECT_SLUG = process.env.NEXT_PUBLIC_PROJECT_SLUG;
function PlayRoot({ children }: ElementChildren) {
const state = useStateStore((state) => state);
const provider = useStateProvider();
// When thumbnail is ready, this fn will be invoked to add
// into your state the thumbnail base64
const setThumbnail = provider.getState().setThumbnail;
// Serves as reset if user clicks `New`
const clear = provider.getState().clear;
// Any redirect to the returned uuid -- best pick is the router of your lib / framework, so
// the user does not need to load all the js again.
const redirect = useRedirectToSignature();
const vars = createVars(state);
const args = createArgs(state);
return (
<Play.Root
slug={PROJECT_SLUG}
vars={vars}
args={args}
redirectToSharedLink={redirect}
clearState={clear}
onThumbnailReady={(event) => {
setThumbnail(event.detail.thumbnail)
}}
>
{children}
</Play.Root>
)
}
function Main() {
return (
<PlayRoot>
<Play.Tools>
<section className='space-y-4 sm:space-y-0 flex-col sm:flex-row sm:space-x-4 justify-center mt-4 flex items-center'>
<Play.ActionNew className="btn-common w-full sm:w-fit" label="Vytvořit nové video" />
<Play.ActionShare className="btn-common w-full sm:w-fit" label="Sdílet video" />
<Play.ActionReels className="btn-common w-full sm:w-fit" label="Vytvořit Reels" />
<Play.ActionDownload className="btn-common w-full sm:w-fit" label="Stáhnout video" />
</section>
<p className="text-center mt-5">* Kliknutím na tlačítko Sdílet video souhlasíte se <a href="https://cdn.webout.io/assets/webout_zpracovani_osobnich_udaju.pdf" target="_blank" className="underline">Zpracováním osobním údajů.</a></p>
</Play.Tools>
</PlayRoot>
)
None of the 'PlayAction' are required, if you don't want to allow downloads for example, you can just skip it.
Screens - Reels & Video generator
Reels & Download are composed of a common component called Generator, by props you decide which one
functionality you require.
Example usage:
const GENERATOR_SLUG = process.env.NEXT_PUBLIC_DOWNLOAD_SLUG;
// Generator wrapper with decided functionality
function GeneratorRoot({ children }: ElementChildren) {
const state = useStateStore((state) => state);
const vars = createVars(state);
const args = createArgs(state);
// origin decides which version it is use DOWNLOAD or REELS in *_ROUTE format to specify
return (
<Generator.Root origin='DOWNLOAD_ROUTE' slug={GENERATOR_SLUG} vars={vars} args={args}>
{children}
</Generator.Root>
)
}
function Main() {
return (
<GeneratorRoot>
<Generator.Video className='w-full h-full video-frame' loaderComponent={<Generator.Loader className='text-sky-500 text-3xl font-semibold text-center' />} />
<Generator.Tools>
<section className='flex items-center mt-4 justify-center space-y-4 sm:space-y-0 flex-col sm:flex-row sm:space-x-4 w-full'>
<Generator.ActionShare
deviceCheck
onShareForbidden={() =>
alert("Tento prohlížeč nepodporuje sdílení")
}
className="btn-common"
label="Sdílet video"
/>
<Generator.ActionDownload
fileName='cool_project.mp4'
platformCheck
className="btn-common"
label="Stáhnout video"
/>
</section>
</Generator.Tools>
</GeneratorRoot>
)
}
API overview - Dragonfly
Licensing & Video sharing module exports these components:
DragonflyProvider
Creates an instance of Dragonfly wrapper, which allows your project to create shared signatures.
import { DragonflyProvider } from "@webout1/react-engine"
...
<DragonflyProvider>
...
</DragonflyProvider>
...
- Name
projectId- Type
- number
- Description
Project ID
- Name
dragonflyUrl- Type
- string
- Description
DF server address
- Name
sharedSignature- Type
- string
- Description
Shared video signature -- used for shared version of microsite
- Name
createPayloadForShare- Type
- () => Promise<Record<string, unknown>>
- Description
Shared payload factory
API overview - Built-in Router
Built-in router is URL independent in-app memory features transitioning mechanism, which you may use, to move from screen to another.
Router.Root
Enables routing among features.
import { Router, sitemaps } from "@webout1/react-engine"
...
<Router.Root initial="FORM_ROUTE" scheme={sitemaps.common}>
....
<Router.Root>
...
- Name
initial- Type
- Route -- (FORM | PICTURE | PLAY | REELS | DOWNLOAD)_ROUTE
- Description
Initial rendered route
- Name
closeOrRedirect- Type
- Function
- Description
Callback invoked on close event
- Name
scheme- Type
- RouteMap -- Record<Route, Record<FORWARD | BACKWARD, Nullable<Route | CLOSE_APP>>>
- Description
Routing scheme
- Name
onRouteChange- Type
- (route: Route) => void
- Description
Function invoked on route change
Root also allows you an imperative access to the router methods:
type Imperative = {
forward: (origin: Route) => void;
backward: (origin: Route) => void;
redirect: (destination: Route) => void;
};
Router.Guard
Guards which children are rendered when Router state matches passed in origin
<Router.Root initial="PLAY_ROUTE" scheme={sitemaps.common}>
<Router.Guard origin="DOWNLOAD_ROUTE">
...
</Router.Guard>
<Router.Guard origin="PLAY_ROUTE">
...
</Router.Guard>
<Router.Root>
- Name
origin- Type
- Route -- (FORM | PICTURE | PLAY | REELS | DOWNLOAD)_ROUTE
- Description
Render children when router state matches the origin
API overview - Form module
Form module enables you to gather user's data required for the projects, it also exposes few components that are the most common among microsites to speed up your development process.
Form.Root
Convenient way to declare Form screen. While you may not need
using Form, you can use Router.Guard with FORM_ROUTE props,
it's easier to just declare Form.Root without any props.
<Form.Root>
...
</Form.Root>
FormActionContniue
Component allowing you to easily transition into another screen following the current scheme in active router instance.
<Form.Root>
...
<Form.ActionContinue label="Pokračovat" />
...
<Form.Root>
There are two variants of using this component. You can either just
pass in label along common button props, or you can pass in
renderCustomAction factory which allows you to handle html completely by you.
<Form.ActionContinue renderCustomAction={(confirm) => <button onClick={confirm}>Submit</button>} />
FormInputGender
Component that enables you to quickly implement gender input.
<Form.InputGender gender={gender} setGender={setGender} className="flex justify-center">
<Form.InputGenderItem gender="male" label="Male" className="checked:bg-blue-300 bg-white" />
<Form.InputGenderItem gender="female" label="Female" className="checked:bg-blue-300 bg-white" />
</Form.InputGender>
- Name
gender- Type
- 'male' | 'female'
- Description
Observable value of currently selected gender
- Name
setGender- Type
- (gender: 'male' | 'female') => void
- Description
Setter that updates selected gender
Component also supports all native properties React.ComponentPropsWithoutRef<"div">.
FormInputGenderItem
Button component respresenting a gender selection.
<Form.InputGenderItem gender="male" label="Male" className="checked:bg-blue-300 bg-white" />
<Form.InputGenderItem
gender="female"
renderCustomButton={(checked) => {
const className = clsx(checked
? "bg-button-primary text-white p-4 text-2xl"
: "bg-gray-500 hover:bg-gray-600 text-white p-4 text-2xl",
"rounded-r"
)
return <Button className={className}>Žena</Button>
}}
/>
- Name
gender- Type
- 'male' | 'female'
- Description
Value of the button
- Name
label- Type
- string
- Description
Button label
- Name
renderCustomAction- Type
- (checked: boolean) => void
- Description
Factory of a button
Component also supports pass in all native button properties.
FormInputLastname
FormInputName
FormInputPicture
API overview - Picture module
Picture module is a simple wrapper over a multi-step process obtaining user's taken selfie. There are also predefined actions and components which you can customize to your liking, so you can focus on design rather than application logic.
Picture.Root
PictureTake
PictureTakeTools
PictureConfirm
PictureConfirmTools
PictureActionTake
PictureActionReset
PictureActionConfirm
API overview - Play module
Play module delivers a simple way how to initiate Webout player in React project. Inputs insertion, actions, cleanups, events, are all handled out of the box for you.
Play.Root
PlayTools
PlayActionNew
PlayActionShare
PlayActionReels
PlayActionDownload
API overview - Generator module
Generator module allows you to abstract steps required for player to generate your video for specific use case into a simple component rendering API.
Generator.Root
GeneratorVideo
GeneratorLoader
GeneratorTools
GeneratorActionShare
GeneratorActionDownload
API overview - Components
Components that of which are pre-created actions composed of available to you, to define and create new features on top of default ones.
TextInput
ClipboardLink
ActionShareLinkedin
ActionShareTwitter
ActionShareFacebook
withDeviceGuard
API overview - Services
Simple static services allows you to quickly perform desired actions and checks.
ShareService
PlatformService
DevicesService
API overview - Utilities
Utilities that ease use of React in composition with Webout ecosystem.