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 @webout-spark/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 "@webout-spark/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 "@webout-spark/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 '@webout-spark/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 "@webout-spark/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 '@webout-spark/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 '@webout-spark/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 '@webout-spark/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>
    )
}