How to cluster polygons with react-leaflet? - reactjs

I'm looking for a way to cluster polygons using react-leaflet v4 and react-leaflet-markercluster. I have not found any up-to-date examples of how I can achieve this, so I'm hoping I might get some help here.
Any example code to get me started would be a great help!

This will probably not solve your problem directly but hopefully show that using markercluster is rather simple. The only thing you need is to have a createMarkerCluster function.
clusterProps has a field for polygonOptions:
/*
* Options to pass when creating the L.Polygon(points, options) to show the bounds of a cluster.
* Defaults to empty
*/
polygonOptions?: PolylineOptions | undefined;
Since you now use a plain leaflet plugin it opens up for mor information on the internet, these two might help how you should configure polygonOptions
How to make MarkerClusterGroup cluster polygons
https://gis.stackexchange.com/questions/197882/is-it-possible-to-cluster-polygons-in-leaflet
Below is my general code to make clustermarkers work with React:
import { createPathComponent } from "#react-leaflet/core";
import L, { LeafletMouseEventHandlerFn } from "leaflet";
import "leaflet.markercluster";
import { ReactElement, useMemo } from "react";
import { Building, BuildingStore, Circle } from "tabler-icons-react";
import { createLeafletIcon } from "./utils";
import styles from "./LeafletMarkerCluster.module.css";
import "leaflet.markercluster/dist/MarkerCluster.css";
type ClusterType = { [key in string]: any };
type ClusterEvents = {
onClick?: LeafletMouseEventHandlerFn;
onDblClick?: LeafletMouseEventHandlerFn;
onMouseDown?: LeafletMouseEventHandlerFn;
onMouseUp?: LeafletMouseEventHandlerFn;
onMouseOver?: LeafletMouseEventHandlerFn;
onMouseOut?: LeafletMouseEventHandlerFn;
onContextMenu?: LeafletMouseEventHandlerFn;
};
// Leaflet is badly typed, if more props needed add them to the interface.
// Look in this file to see what is available.
// node_modules/#types/leaflet.markercluster/index.d.ts
// MarkerClusterGroupOptions
export interface LeafletMarkerClusterProps {
spiderfyOnMaxZoom?: boolean;
children: React.ReactNode;
size?: number;
icon?: ReactElement;
}
const createMarkerCluster = (
{
children: _c,
size = 30,
icon = <Circle size={size} />,
...props
}: LeafletMarkerClusterProps,
context: any
) => {
const markerIcons = {
default: <Circle size={size} />,
property: <Building size={size} />,
business: <BuildingStore size={size} />,
} as { [key in string]: ReactElement };
const clusterProps: ClusterType = {
iconCreateFunction: (cluster: any) => {
const markers = cluster.getAllChildMarkers();
const types = markers.reduce(
(
acc: { [x: string]: number },
marker: {
key: string;
options: { icon: { options: { className: string } } };
}
) => {
const key = marker?.key || "";
const type =
marker.options.icon.options.className || key.split("-")[0];
const increment = (key.split("-")[1] as unknown as number) || 1;
if (type in markerIcons) {
return { ...acc, [type]: (acc[type] || 0) + increment };
}
return { ...acc, default: (acc.default || 0) + increment };
},
{}
) as { [key in string]: number };
const typeIcons = Object.entries(types).map(([type, count], index) => {
if (count > 0) {
const typeIcon = markerIcons[type];
return (
<div key={`${type}-${count}`} style={{ display: "flex" }}>
<span>{typeIcon}</span>
<span style={{ width: "max-content" }}>{count}</span>
</div>
);
}
});
const iconWidth = typeIcons.length * size;
return createLeafletIcon(
<div style={{ display: "flex" }} className={"cluster-marker"}>
{typeIcons}
</div>,
iconWidth,
undefined,
iconWidth,
30
);
},
showCoverageOnHover: false,
animate: true,
animateAddingMarkers: false,
removeOutsideVisibleBounds: false,
};
const clusterEvents: ClusterType = {};
// Splitting props and events to different objects
Object.entries(props).forEach(([propName, prop]) =>
propName.startsWith("on")
? (clusterEvents[propName] = prop)
: (clusterProps[propName] = prop)
);
const instance = new (L as any).MarkerClusterGroup(clusterProps);
instance.on("spiderfied", (e: any) => {
e.cluster._icon?.classList.add(styles.spiderfied);
});
instance.on("unspiderfied", (e: any) => {
e.cluster._icon?.classList.remove(styles.spiderfied);
});
// This is not used at the moment, but could be used to add events to the cluster.
// Initializing event listeners
Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`;
instance.on(clusterEvent, callback);
});
return {
instance,
context: {
...context,
layerContainer: instance,
},
};
};
const updateMarkerCluster = (instance: any, props: any, prevProps: any) => {};
const LeafletMarkerCluster = createPathComponent(
createMarkerCluster,
updateMarkerCluster
);
const LeafletMarkerClusterWrapper: React.FC<LeafletMarkerClusterProps> = ({
children,
...props
}) => {
const markerCluster = useMemo(() => {
return <LeafletMarkerCluster>{children}</LeafletMarkerCluster>;
}, [children]);
return <>{markerCluster}</>;
};
export default LeafletMarkerClusterWrapper;
Below is my function to create a marker icon from react elements:
import { divIcon } from "leaflet";
import { ReactElement } from "react";
import { renderToString } from "react-dom/server";
export const createLeafletIcon = (
icon: ReactElement,
size: number,
className?: string,
width: number = size,
height: number = size
) => {
return divIcon({
html: renderToString(icon),
iconSize: [width, height],
iconAnchor: [width / 2, height],
popupAnchor: [0, -height],
className: className ? className : "",
});
};

Related

How to open multiple mui DialogProvider dynamically?

I have a generic DialogProvider that written with MUI and I open all dialog modals with this generic provider.
Code is below.
import React from "react";
import { makeAutoObservable } from "mobx";
import {
BooleanValuePaths,
getValue,
setValue,
Paths,
PathValueType,
StringValuePaths,
} from "../utils/utils";
import { IAppState, useAppState } from "./app-state";
type BaseType = object;
export class DialogState<T extends BaseType> {
_data: T;
_resetFunction?: () => T;
_touched: BooleanValuePaths<Paths<T>>;
_error: StringValuePaths<Paths<T>>;
_formError?: string;
_initialData: T;
constructor(dataInitiator: () => T) {
this._data = dataInitiator();
this._initialData = dataInitiator();
this._resetFunction = dataInitiator;
this._touched = {};
this._error = {};
makeAutoObservable(this);
}
reset() {
this._resetFunction?.();
}
get data() {
return this._data;
}
get formTouched() {
const touched = Object.keys(this._touched).find(
(key) => this._touched[key as Paths<T>],
);
return touched;
}
setValue<P extends Paths<T>>(path: P, value: PathValueType<T, P>): void {
setValue(this._data, path, value as any); //TODO remove the "as any", but the typescript has problem
this._touched[path] = true;
}
getValue<P extends Paths<T>>(path: P): PathValueType<T, P> {
return getValue(this._data, path) as PathValueType<T, P>; //TODO remove the "as PathValueType<T, P>", but the typescript has problem
}
isTouched<P extends Paths<T>>(path: P) {
return this._touched[path];
}
errorOf<P extends Paths<T>>(path: P) {
return this._error[path];
}
setErrorOf<P extends Paths<T> | "">(path: P, error: string) {
this._error[path] = error;
}
resetErrors() {
this._error = {};
}
resetTouches() {
this._touched = {};
}
resetData() {
this._data = this._initialData;
}
setFormError(error: string) {
this._formError = error;
}
}
function processErrorFunction<T extends BaseType>(
state: DialogState<T>,
err: any,
appState: IAppState,
errorKeyMap?: { errorKey: string; dataKey: string }[],
) {
try {
const json = JSON.parse(err?.message);
if (Array.isArray(json?.message)) {
(json.message as any[]).forEach((m) => {
if (
typeof m?.property === "string" &&
typeof m?.constraints === "object"
) {
const msg = Object.keys(m.constraints)
.map((k) => m.constraints[k])
.join(", ");
const dataKey =
errorKeyMap?.find((erk) => erk.errorKey === m.property)?.dataKey ??
m.property;
state.setErrorOf(dataKey, msg);
}
});
} else {
appState.setSnackMessage({ message: json?.message || "unknown" });
}
} catch (_) {
appState.setSnackMessage({ message: err?.message || "unknown" });
}
}
export function useDialogStateProvider<T extends BaseType>(
stateInitiator: () => T,
saverFunction: (v: T, appState: IAppState) => Promise<any>,
closeFunction?: (value?: T) => void,
errorKeyMap?: { errorKey: string; dataKey: string }[],
) {
const appState = useAppState();
const [loading, setLoading] = React.useState(false);
const state = React.useMemo(
() => new DialogState(stateInitiator),
[stateInitiator],
);
const reset = () => {
state.reset();
};
const handleSave = async () => {
try {
setLoading(true);
state.resetErrors();
await saverFunction(state.data, appState);
state.resetTouches();
if (closeFunction) {
state.resetData();
closeFunction(state.data);
reset();
}
} catch (err: any) {
processErrorFunction(state, err, appState, errorKeyMap);
} finally {
setLoading(false);
}
};
const handleClose = () => {
try {
if (closeFunction) {
setLoading(false);
state.resetErrors();
state.resetData();
state.resetTouches();
closeFunction(state.data);
reset();
}
} catch (err: any) {
processErrorFunction(state, err, appState, errorKeyMap);
} finally {
setLoading(false);
}
};
return {
reset,
state,
loading,
handleSave,
handleClose,
};
}
interface useDialogStateProviderInterface<T extends BaseType> {
(stateProvider: () => T): {
reset: () => void;
state: DialogState<T>;
loading: boolean;
handleSave: () => Promise<void>;
handleClose?: () => void;
};
}
export type DialogStateProviderType<T extends BaseType> = ReturnType<
useDialogStateProviderInterface<T>
>;
export const DialogStateContext =
React.createContext<DialogStateProviderType<any> | null>(null);
export function DialogStateProvider<
K extends BaseType,
T extends DialogStateProviderType<K>,
>({ state, children }: { state: T; children?: React.ReactNode }) {
return (
<DialogStateContext.Provider value={state}>
{children}
</DialogStateContext.Provider>
);
}
export function useDialogState<T extends BaseType>() {
const store = React.useContext(DialogStateContext);
if (!store) {
throw new Error("useDialogState must be used within DialogStateProvider");
}
return store as DialogStateProviderType<T>;
}
Usage is like below:
<DialogStateProvider state={taskState}>
<BaseDialog open={open} handleClose={handleClose} title={title}>
<AssignedTaskEditor isSubsequent={isSubsequent} />
</BaseDialog>
</DialogStateProvider>
BaseDialog code is this:
import {
Dialog,
DialogContent,
DialogTitle,
IconButton,
SxProps,
Theme,
useMediaQuery,
useTheme,
} from "#mui/material";
import CloseIcon from "#mui/icons-material/Close";
export function BaseDialog({
open,
handleClose,
title,
children,
sx,
}: {
open: boolean;
handleClose: () => void;
title?: React.ReactNode;
children: React.ReactNode;
sx?: SxProps<Theme>;
}) {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down("md"));
return (
<Dialog
fullScreen={fullScreen}
open={open}
onClose={handleClose}
maxWidth="lg"
sx={{
...sx,
"& .MuiDialog-container > .MuiPaper-root": {
height: "100%",
backgroundColor: "background.default",
...(sx as any)?.["& .MuiDialog-container > .MuiPaper-root"],
},
"& .MuiDialogContent-root": {
flexGrow: 1,
backgroundColor: "background.default",
maxHeight: "80vh",
marginBlock: 2,
...(sx as any)?.["& .MuiDialogContent-root"],
},
"& .dialogBox-Icon": {
position: "absolute",
right: (theme) => theme.spacing(2.5),
top: (theme) => theme.spacing(1),
...(sx as any)?.["& .dialogBox-Icon"],
},
}}
>
{title && <DialogTitle>{title}</DialogTitle>}
<IconButton onClick={handleClose} className="dialogBox-Icon" size="large">
<CloseIcon />
</IconButton>
<DialogContent dividers>{children}</DialogContent>
</Dialog>
);
}
I want to open multiple dialog concurrently like that:
Save and add a subsequent task button should call an API endpoint to save data and it should close current modal and open another dialog and this should go forever. There should not be an end.
I have searched on internet but I couldn't find good example for this.
How can I do that with this current generic DialogProvider?

How to add button to every node by default using TreeItem in React TypeScript?

I am working with parent-child tree in react typescript using react mui library 'TreeItem' and 'TreeView' and I don't know how to add button to each and every node by default.
And I have done an parent-child dropdown as shown in the below image. And I want an button to each and every node as I marked with blue and red color.
Here is My code:
import React, { useEffect, useState } from "react";
import TreeView from "#mui/lab/TreeView";
import TreeItem from "#mui/lab/TreeItem";
import ExpandMoreIcon from "#material-ui/icons/ExpandMore";
import ChevronRightIcon from "#mui/icons-material/ChevronRight";
import { useForm } from "react-hook-form";
interface FileNode {
id: string;
parentId: number;
name: string;
children?: readonly FileNode[];
}
export const TreeViewFile = () => {
const [data, setData] = useState<FileNode[]>([]);
const [selectedNode, setSelectedNode] = useState({});
const [selectedRoot, setSelectedRoot] = useState({});
const [selectedOption, setSelectedOption] = useState(null as any);
const [dropDownData, setDropDownData] = useState([] as any);
useEffect(() => {
fetch("https://localhost:44306/api/user/Getlistofchildren")
.then((data) => data.json())
.then((data) => setData(data));
console.log("selectedNode", selectedNode);
console.log("selectedRoo", selectedRoot);
}, [selectedNode, selectedRoot]);
const handleChange = (event: any, nodeId: string) => {
data.forEach((treeRoot: any) => {
if (treeRoot.id === nodeId) {
setSelectedRoot(treeRoot);
setSelectedNode(treeRoot);
return;
}
handleSelectedNode(treeRoot.children, treeRoot, nodeId);
});
};
const handleSelectedNode = (
children: string | any[],
treeRoot: React.SetStateAction<{}>,
nodeId: any
) => {
if (!children) {
return;
}
for (let i = 0; i < children.length; i++) {
let childNode = children[i];
if (childNode.id === nodeId) {
setSelectedRoot(treeRoot);
setSelectedNode(childNode);
return;
}
handleSelectedNode(childNode.children || [], treeRoot, nodeId);
}
};
const displayTreeView = (treeViewArray: any[]) => {
if (!treeViewArray) {
return null;
}
return treeViewArray.map(
(treeViewItem: {
children: any[];
id: React.Key | null | undefined;
name:
| string
| number
| boolean
| React.ReactElement<any, string | React.JSXElementConstructor<any>>
| React.ReactFragment
| React.ReactPortal
| null
| undefined;
childNodes: any;
}) => {
return (
<TreeItem
key={treeViewItem.id}
nodeId={`${treeViewItem.id}`}
label={treeViewItem.name}
>
{displayTreeView(treeViewItem.children) }
</TreeItem>
);
}
);
};
return (
<>
<TreeView
onNodeSelect={handleChange}
sx={{ height: 240, flexGrow: 1, maxWidth: 800, overflowY: "auto" }}
defaultExpandIcon={<ChevronRightIcon />}
defaultCollapseIcon={<ExpandMoreIcon />}
>
<div>
{displayTreeView(data)}
</div>
</TreeView>
</>
);
};

Using DraftJS in a Functional Component

I am trying to implement DraftJS within an existing functional component and I am unable to figure out how to do this. It appears that all of the documentation and user-submitted content refers to class components.
I try to set it up using the following:
import { Editor } from "react-draft-wysiwyg";
import { EditorState } from 'draft-js'
export default function myFunctionalComponent() {
const [editorState, setEditorState] = useState(EditorState.createEmpty())
return(
<Editor
editorState={editorState}
onChange={setEditorState}
/>
)
}
However, unfortunately, I get this error in the console:
Warning: Can't call setState on a component that is not yet mounted.
This is a no-op, but it might indicate a bug in your application.
Instead, assign to this.state directly or define a state = {};
class property with the desired state in the r component.
Is there a way to make this work in a functional component?
As my very first answer in StackOverflow. :)
I took the example from https://github.com/facebook/draft-js/blob/main/examples/draft-0-10-0/rich/rich.html and converted it into a functional components 'RTEditor', .. .
Use the component with setContent as a prop. It takes the function to update parent elements state from useState
const [content, setContent] = useState<any>({})
...
<RTEditor setContent={setContent} />
RTEditor.tsx
import React, { useState, useRef } from 'react'
import {
Editor,
EditorState,
RichUtils,
getDefaultKeyBinding,
ContentBlock,
DraftHandleValue,
convertFromHTML,
convertFromRaw,
convertToRaw,
ContentState,
RawDraftContentState,
} from 'draft-js'
import 'draft-js/dist/Draft.css'
import BlockStyleControls from './BlockStyleControls'
import InlineStyleControls from './InlineStyleControls'
type Props = {
setContent: (state: RawDraftContentState) => void
}
const RTEditor = ({ setContent }: Props) => {
const editorRef = useRef(null)
const [editorState, setEditorState] = useState(EditorState.createEmpty())
const styleMap = {
CODE: {
backgroundColor: 'rgba(0, 0, 0, 0.05)',
fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace',
fontSize: 16,
padding: 2,
},
}
const getBlockStyle = (block: ContentBlock) => {
switch (block.getType()) {
case 'blockquote':
return 'RichEditor-blockquote'
default:
return ''
}
}
const onChange = (state: EditorState) => {
setEditorState(state)
setContent(convertToRaw(editorState.getCurrentContent()))
}
const mapKeyToEditorCommand = (e: any): string | null => {
if (e.keyCode === 9 /* TAB */) {
const newEditorState = RichUtils.onTab(e, editorState, 4 /* maxDepth */)
if (newEditorState !== editorState) {
onChange(newEditorState)
}
return null
}
return getDefaultKeyBinding(e)
}
const handleKeyCommand = (
command: string,
editorState: EditorState,
eventTimeStamp: number
): DraftHandleValue => {
const newState = RichUtils.handleKeyCommand(editorState, command)
if (newState) {
onChange(newState)
return 'handled'
}
return 'not-handled'
}
const toggleBlockType = (blockType: string) => {
onChange(RichUtils.toggleBlockType(editorState, blockType))
}
const toggleInlineStyle = (inlineStyle: string) => {
onChange(RichUtils.toggleInlineStyle(editorState, inlineStyle))
}
return (
<>
<BlockStyleControls
editorState={editorState}
onToggle={toggleBlockType}
/>
<InlineStyleControls
editorState={editorState}
onToggle={toggleInlineStyle}
/>
<Editor
ref={editorRef}
editorState={editorState}
placeholder='Tell a story...'
customStyleMap={styleMap}
blockStyleFn={(block: ContentBlock) => getBlockStyle(block)}
keyBindingFn={(e) => mapKeyToEditorCommand(e)}
onChange={onChange}
spellCheck={true}
handleKeyCommand={handleKeyCommand}
/>
</>
)
}
export default React.memo(RTEditor)
BlockStyleControls.tsx
import React from 'react'
import { EditorState } from 'draft-js'
import StyleButton from './StyleButton'
const BLOCK_TYPES = [
{ label: 'H1', style: 'header-one' },
{ label: 'H2', style: 'header-two' },
{ label: 'H3', style: 'header-three' },
{ label: 'H4', style: 'header-four' },
{ label: 'H5', style: 'header-five' },
{ label: 'H6', style: 'header-six' },
{ label: 'Blockquote', style: 'blockquote' },
{ label: 'UL', style: 'unordered-list-item' },
{ label: 'OL', style: 'ordered-list-item' },
{ label: 'Code Block', style: 'code-block' },
]
type Props = {
editorState: EditorState
onToggle: (bockType: string) => void
}
const BlockStyleControls = ({ editorState, onToggle }: Props) => {
const selection = editorState.getSelection()
const blockType = editorState
.getCurrentContent()
.getBlockForKey(selection.getStartKey())
.getType()
return (
<div className='RichEditor-controls'>
{BLOCK_TYPES.map((type) => (
<StyleButton
key={type.label}
active={type.style === blockType}
label={type.label}
onToggle={onToggle}
style={type.style}
/>
))}
</div>
)
}
export default React.memo(BlockStyleControls)
InlineStyleControls.tsx
import React from 'react'
import { EditorState } from 'draft-js'
import StyleButton from './StyleButton'
const INLINE_STYLES = [
{ label: 'Bold', style: 'BOLD' },
{ label: 'Italic', style: 'ITALIC' },
{ label: 'Underline', style: 'UNDERLINE' },
{ label: 'Monospace', style: 'CODE' },
]
type Props = {
editorState: EditorState
onToggle: (bockType: string) => void
}
const InlineStyleControls = ({ editorState, onToggle }: Props) => {
const currentStyle = editorState.getCurrentInlineStyle()
return (
<div className='RichEditor-controls'>
{INLINE_STYLES.map((type) => (
<StyleButton
key={type.label}
active={currentStyle.has(type.style)}
label={type.label}
onToggle={onToggle}
style={type.style}
/>
))}
</div>
)
}
export default React.memo(InlineStyleControls)
StyleButton.tsx
import React from 'react'
type Props = {
active: boolean
style: string
label: string
onToggle: (bockType: string) => void
}
const StyleButton = ({ active, style, label, onToggle }: Props) => {
const _onToggle = (e: any) => {
e.preventDefault()
onToggle(style)
}
const className = 'RichEditor-styleButton'
return (
<button
className={className + `${active ? ' RichEditor-activeButton' : ''}`}
onClick={_onToggle}
>
{label}
</button>
)
}
export default React.memo(StyleButton)
Sry for not covering all typings. Hope that helps.
I was able to solve this using React useCallback hook and it works for me.
import { EditorState } from 'draft-js'
export default function myFunctionalComponent() {
const [editorState, setEditorState] = useState(EditorState.createEmpty())
const onEditorStateChange = useCallback(
(rawcontent) => {
setEditorState(rawcontent.blocks[0].text);
},
[editorState]
);
return(
<Editor
placeholder="Tell a story..."
onChange={onEditorStateChange}
/>
)
}

Azure Media Player events now working for React Component

I tried to create a React component to play Azure Media Services content and it works to play, but not to capture events. This is the code of my component:
import React, { useEffect, useRef } from 'react';
import { Helmet } from 'react-helmet';
export interface AzureMediaPlayerProps {
videoUrl: string;
}
const AzureMediaPlayer = (props: AzureMediaPlayerProps): JSX.Element => {
const { videoUrl } = props;
const videoRef = useRef<HTMLVideoElement>(null);
const clearListener = (): void => {
videoRef.current?.removeEventListener('load', (): void => { });
videoRef.current?.removeEventListener('progress', (): void => { });
};
const addListener = (): void => {
videoRef.current?.addEventListener('load', (ev): void => { console.log(ev); });
videoRef.current?.addEventListener('progress', (ev): void => { console.log(ev); });
};
useEffect((): void => {
addListener();
return clearListener();
}, [videoRef]);
return (
<>
<Helmet>
<link href="//amp.azure.net/libs/amp/2.3.7/skins/amp-default/azuremediaplayer.min.css" rel="stylesheet" />
<script src="//amp.azure.net/libs/amp/2.3.7/azuremediaplayer.min.js" />
</Helmet>
<video
id="vid1"
className="azuremediaplayer amp-default-skin"
autoPlay
controls
width="100%"
data-setup='{"nativeControlsForTouch": false}'
ref={videoRef}
>
<source src={videoUrl} type="application/vnd.ms-sstr+xml" />
<p className="amp-no-js">
To view this video please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video
</p>
</video>
</>
);
};
AzureMediaPlayer.displayName = 'AzureMediaPlayer';
export default AzureMediaPlayer;
I also tried:
<video
id="vid1"
className="azuremediaplayer amp-default-skin"
autoPlay
controls
width="100%"
data-setup='{"nativeControlsForTouch": false}'
ref={videoRef}
onProgress={(ev): void => { console.log(ev); }}
>
<source src={videoUrl} type="application/vnd.ms-sstr+xml" />
<p className="amp-no-js">
To view this video please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video
</p>
</video>
But I was not able to get the events of the player. Can anybody please help me? How can I get the player events in React?
Thanks a lot.
Recommend that you don't try to use the AMP player in React component directly. It was not written to be componentized.
Try looking at a more modern player like Shaka or HLS.js that may already have a React wrapper for it.
For example -
https://www.npmjs.com/package/shaka-player-react
https://github.com/matvp91/shaka-player-react
Or a more commercial player that supports React like Theo Player or Bitmovin
https://docs.theoplayer.com/getting-started/02-frameworks/02-react/00-getting-started.md
https://github.com/bitmovin/bitmovin-player-web-samples
This is the final version of VideoJsPlayer.tsx:
import { Box } from 'grommet';
import React, { useEffect, useRef, useState } from 'react';
import videojs, { VideoJsPlayerOptions } from 'video.js';
import 'video.js/dist/video-js.css';
export interface VideoJsPlayerProps {
videoUrl?: string;
transcriptionUrl?: string;
type?: string;
saveProgress: (minute: number) => Promise<void>;
completeProgress: (progress: string) => Promise<void>;
startingTime?: number;
}
const VideoJsPlayer = (props: VideoJsPlayerProps): JSX.Element => {
const {
videoUrl, transcriptionUrl, type, saveProgress, completeProgress, startingTime = 0,
} = props;
let player;
const videoRef = useRef<HTMLVideoElement>(null);
const [minutes, setMinutes] = useState<number>(0);
const getMin = (sec: number): number => Math.floor(sec / 60);
const updateMinutes = (time: string): void => {
const secs = parseInt(time, 10);
const min = getMin(secs);
if (min > minutes) {
setMinutes(min);
}
};
const videoJsOptions = {
autoplay: true,
controls: true,
responsive: true,
fill: true,
nativeControlsForTouch: false,
playbackRates: [0.5, 1, 1.5, 2],
sources: [
{
src: videoUrl as string,
type: type as string,
},
],
} as VideoJsPlayerOptions;
useEffect((): any => {
const videoElement = videoRef.current;
// mount
if (videoElement) {
// #ts-ignore
player = videojs(
videoElement,
videoJsOptions,
() => {
player.addRemoteTextTrack({
kind: 'captions',
src: transcriptionUrl || '',
label: 'English',
language: 'en',
srcLang: 'en',
default: true,
}, false);
player.on('progress', (): void => {
updateMinutes(player.currentTime());
});
player.on('ended', (): void => {
completeProgress(player.currentTime());
});
},
);
// set starting time
player.currentTime(startingTime);
}
// unmount
return (): void => {
if (player) {
player.dispose();
}
};
}, []);
useEffect((): void => {
if (minutes > 0) {
saveProgress(minutes);
}
}, [minutes]);
return (
<Box
fill="horizontal"
align="center"
background="yellow"
style={{
height: 700,
}}
>
<video
className="video-js vjs-big-play-centered"
ref={videoRef}
/>
</Box>
);
};
VideoJsPlayer.displayName = 'VideoJsPlayer';
export default VideoJsPlayer;
The only details is the url to play: it must be /manifest(format=mpd-time-csf)

React with typescript func types

I've decided to refactor a project I've made with vanilla javascript and use typescript, I'm always in doubt about how to pass a function as a type on interface.
I took a look on the typescript documentation but I didn't understood how it works.
For instance, What type bringTransactions should have ? and the return type of the handleChange func ?
import React from 'react';
import moment from 'moment';
import { Select } from 'antd';
interface Props {
bringTransactions: any;
}
const PeriodPicker: React.FC<Props> = ({ bringTransactions }: Props) => {
const periodOptions = [
{ value: 0, label: 'Hoje' },
{ value: 3, label: 'Últimos 3 dias' },
{ value: 7, label: 'Últimos 7 dias' },
{ value: 15, label: 'Últimos 15 dias' },
{ value: 30, label: 'Últimos 30 dias' },
{ value: 5, label: 'Data específica' },
];
async function handleChange(value: number): Promise<void> {
const filter = [];
// setDataPickerStatus(false);
if (value === 5) {
// setDataPickerStatus(true);
} else {
const current = moment().format('YYYY-MM-DD HH:mm:ss');
const subtracted = moment(current).subtract(value, 'days');
const initialDate = moment(subtracted)
.utc()
.hours(0)
.minutes(0)
.seconds(0)
.milliseconds(0);
filter[0] = initialDate.format('YYYY-MM-DD HH:mm:ss');
const finatlDate = moment(current)
.utc()
.subtract(1, 'days')
.hours(23)
.minutes(59)
.seconds(59)
.milliseconds(0);
filter[1] = finatlDate.format('YYYY-MM-DD HH:mm:ss');
if (value === 0) {
const normalized = `&date=${filter[0]}`;
bringTransactions(normalized);
} else {
const normalized = `&period=${JSON.stringify(filter)}`;
bringTransactions(normalized);
}
}
}
return (
<Select
placeholder="Selecione um período"
onChange={handleChange}
style={{ width: 200 }}
>
{periodOptions.map(option => (
<Select.Option value={option.value} key={option.value}>
{option.label}
</Select.Option>
))}
</Select>
);
};
export default PeriodPicker;
It looks like bringTransactions is a function which takes a single string argument and returns nothing. That would be:
interface Props {
bringTransactions(value: string): void;
}
or you can write it with arrow syntax
interface Props {
bringTransactions: (value: string) => void;
}
Read more about functions in the typescript docs.
I usually write function types like this:
interface Props {
bringTransactions: (prop: propType) => ReturnType;
}
And the handleChange function will return void as events callbacks usually return void

Resources