Not gonna work

This commit is contained in:
Fergal Moran
2022-04-09 15:30:26 +01:00
parent 02d2432419
commit f44fc3e5cc
16 changed files with 575 additions and 110 deletions

View File

@@ -12,6 +12,7 @@
"postcss": "^8.4.12",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-icons": "^4.3.1",
"react-router-dom": "6",
"react-scripts": "5.0.0",
@@ -24,7 +25,8 @@
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^17.0.20",
"@types/react-dom": "^17.0.9"
"@types/react-dom": "^17.0.9",
"@types/react-helmet": "^6.1.5"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -24,7 +24,7 @@
<script src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"></script>
<!-- <script src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"></script>
<script>
window['__onGCastApiAvailable'] = function (isAvailable) {
if (isAvailable) {
@@ -37,7 +37,7 @@
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
});
};
</script>
</script> -->
</body>
</html>

View File

@@ -37,7 +37,6 @@ const Header = () => {
return (
<header className="z-40 py-4 bg-white shadow-bottom dark:bg-gray-800">
<div className="container flex items-center justify-between h-full px-6 mx-auto text-purple-600 dark:text-purple-300">
{/* <!-- Mobile hamburger --> */}
<button
className="p-1 mr-5 -ml-1 rounded-md lg:hidden focus:outline-none focus:shadow-outline-purple"
onClick={toggleSidebar}
@@ -45,7 +44,6 @@ const Header = () => {
>
<MenuIcon className="w-6 h-6" aria-hidden="true" />
</button>
{/* <!-- Search input --> */}
<div className="flex justify-center flex-1 lg:mr-32">
<div className="relative w-full max-w-xl mr-6 focus-within:text-purple-500">
<div className="absolute inset-y-0 flex items-center pl-2">
@@ -60,7 +58,6 @@ const Header = () => {
</div>
</div>
<ul className="flex items-center flex-shrink-0 space-x-6">
{/* <!-- Theme toggler --> */}
<li className="flex">
<button
className="rounded-md focus:outline-none focus:shadow-outline-purple"

View File

@@ -1,4 +1,4 @@
import { SidebarContext } from "./sidebar.context";
import { ThemeContext } from "./theme.context";
import { SidebarContext, SidebarProvider } from "./sidebar.context";
import { ThemeContext, ThemeProvider } from "./theme.context";
export { SidebarContext, ThemeContext };
export { SidebarContext, ThemeContext, SidebarProvider, ThemeProvider };

View File

@@ -9,26 +9,26 @@ interface ISidebarProviderContext {
closeSidebar: () => void;
}
export const SidebarContext = React.createContext<ISidebarProviderContext>({
isSidebarOpen: false,
isSidebarOpen: true,
toggleSidebar: () => {},
closeSidebar: () => {},
});
export const SidebarProvider = ({ children }: ISidebarProvider) => {
const [isSidebarOpen, setIsSidebarOpen] = React.useState(false);
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
const toggleSidebar = () => {
const _toggleSidebar = () => {
setIsSidebarOpen(!isSidebarOpen);
};
const closeSidebar = () => {
const _closeSidebar = () => {
setIsSidebarOpen(false);
};
const value = React.useMemo(
() => ({
isSidebarOpen,
toggleSidebar,
closeSidebar,
toggleSidebar: _toggleSidebar,
closeSidebar: _closeSidebar,
}),
[isSidebarOpen]
);

View File

@@ -3,10 +3,16 @@ import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { SidebarProvider } from "./context";
import { Windmill } from "@windmill/react-ui";
ReactDOM.render(
<React.StrictMode>
<App />
<SidebarProvider>
<Windmill usePreferences>
<App />
</Windmill>
</SidebarProvider>
</React.StrictMode>,
document.getElementById("root")
);

View File

@@ -16,11 +16,21 @@ import { FaChromecast } from "react-icons/fa";
import { Stream } from "../models/stream";
import { convertEpochToSpecificTimezone } from "../utils/date-utils";
import { EPGComponent } from "../components";
import {
CastButton,
CastProvider,
useCast,
useMedia,
} from "../utils/chromecast";
const ChannelPage = () => {
let params = useParams();
const cast = useCast();
const media = useMedia();
const [streams, setStreams] = React.useState<Stream[]>([]);
const [currentVideoUrl, setCurrentVideoUrl] = React.useState("");
React.useEffect(() => {
const fetchChannels = async () => {
const res = await fetch(
@@ -33,10 +43,7 @@ const ChannelPage = () => {
fetchChannels().catch(console.error);
}, [params.channelId]);
const handleXHR = (...args: any[]) => {
console.log("channel.page", "handleXHR", args);
};
const playStream = async (streamId: number) => {
const _getStreamUrl = async (streamId: number) => {
const res = await fetch(
`${process.env.REACT_APP_API_URL}/live/stream/url/${streamId}`
);
@@ -45,7 +52,25 @@ const ChannelPage = () => {
return;
}
const data = await res.json();
if (data.url) {
return data?.url;
};
const _cast = React.useCallback(
async (streamId: number) => {
const streamUrl = await _getStreamUrl(streamId);
if (streamUrl) {
await media.playMedia(streamUrl);
}
},
[media]
);
const handleXHR = (...args: any[]) => {
console.log("channel.page", "handleXHR", args);
};
const playStream = async (streamId: number) => {
const url = await _getStreamUrl(streamId);
if (url) {
const mpv_args =
"--keep-open=yes\n--geometry=1024x768-0-0\n--ontop\n--screen=2\n--ytdl-format=bestvideo[ext=mp4][height<=?720]+bestaudio[ext=m4a]\n--border=no".split(
/\n/
@@ -53,7 +78,7 @@ const ChannelPage = () => {
const query =
`?play_url=` +
encodeURIComponent(data.url) +
encodeURIComponent(url) +
[""].concat(mpv_args.map(encodeURIComponent)).join("&mpv_args=");
const xhr = new XMLHttpRequest();
@@ -63,89 +88,64 @@ const ChannelPage = () => {
}
};
return (
<TableContainer className="mb-8">
<Table>
<TableHeader>
<tr>
<TableCell>Channel</TableCell>
<TableCell>Type</TableCell>
<TableCell></TableCell>
</tr>
</TableHeader>
<TableBody>
{streams.map((stream: Stream) => [
<TableRow key={stream.num}>
<TableCell>
<div className="flex items-center text-sm">
<Avatar
className="hidden w-10 h-10 ml-2 mr-3 md:block"
src={stream.stream_icon}
alt="Stream icon"
/>
<div>
<p className="font-semibold">{stream.name}</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
Added: {convertEpochToSpecificTimezone(stream.added)}
</p>
</div>
</div>
</TableCell>
<TableCell>
<Badge type={`primary`}>{stream.stream_type}</Badge>
</TableCell>
<TableCell>
<div className="flex items-center space-x-4">
<Button
layout="link"
size="small"
aria-label="Edit"
onClick={() => playStream(stream.stream_id)}
>
<AiOutlinePlayCircle
className="w-6 h-6"
aria-hidden="true"
<CastProvider>
<TableContainer className="mb-8">
<Table>
<TableHeader>
<tr>
<TableCell>Channel</TableCell>
<TableCell>Type</TableCell>
<TableCell></TableCell>
</tr>
</TableHeader>
<TableBody>
{streams.map((stream: Stream) => [
<TableRow key={stream.num}>
<TableCell>
<div className="flex items-center text-sm">
<Avatar
className="hidden w-10 h-10 ml-2 mr-3 md:block"
src={stream.stream_icon}
alt="Stream icon"
/>
</Button>
<Button
layout="link"
size="small"
aria-label="Delete"
onClick={() => {
var mediaInfo = new chrome.cast.media.MediaInfo(
currentMediaURL,
contentType
);
var request = new chrome.cast.media.LoadRequest(
mediaInfo
);
castSession.loadMedia(request).then(
function () {
console.log("Load succeed");
},
function (errorCode) {
console.log("Error code: " + errorCode);
}
);
}}
>
<FaChromecast className="w-5 h-5" aria-hidden="true" />
</Button>
</div>
</TableCell>
</TableRow>,
<tr key={`${stream.num}-epg`}>
{false && (
<td colSpan={3} className="px-4 py-2 mt-8 border-4 shadow-md">
<Suspense fallback={<h1>Loading epg</h1>}>
<EPGComponent channelId={stream.epg_channel_id} />
</Suspense>
</td>
)}
</tr>,
])}
</TableBody>
</Table>
</TableContainer>
<div>
<p className="font-semibold">{stream.name}</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
Added: {convertEpochToSpecificTimezone(stream.added)}
</p>
</div>
</div>
</TableCell>
<TableCell>
<Badge type={`primary`}>{stream.stream_type}</Badge>
</TableCell>
<TableCell>
<div className="flex items-center space-x-4">
<Button
icon={AiOutlinePlayCircle}
layout="link"
aria-label="Edit"
onClick={() => playStream(stream.stream_id)}
>
</Button>
<CastButton streamId={stream.stream_id} onPlay={_cast} />
</div>
</TableCell>
</TableRow>,
<tr key={`${stream.num}-epg`}>
{false && (
<td colSpan={3} className="px-4 py-2 mt-8 border-4 shadow-md">
<Suspense fallback={<h1>Loading epg</h1>}>
<EPGComponent channelId={stream.epg_channel_id} />
</Suspense>
</td>
)}
</tr>,
])}
</TableBody>
</Table>
</TableContainer>
</CastProvider>
);
};

View File

@@ -0,0 +1,33 @@
import React from "react";
import useCast from "./useCast";
import { Button } from "@windmill/react-ui";
import { FaChromecast } from "react-icons/fa";
interface ICastButtonProps {
streamId: number;
onPlay: (streamId: number) => void;
}
const CastButton = ({ streamId, onPlay }: ICastButtonProps) => {
const cast = useCast({
initialize_media_player: "DEFAULT_MEDIA_RECEIVER_APP_ID",
auto_initialize: true,
});
const handleClick = React.useCallback(async () => {
if (cast.castReceiver) {
await cast.handleConnection();
onPlay(streamId);
}
}, [cast.castReceiver, cast.handleConnection]);
return (
<Button
layout="link"
size="small"
aria-label="Delete"
onClick={handleClick}
>
<FaChromecast className="w-5 h-5" aria-hidden="true" />
</Button>
);
};
export default CastButton;

View File

@@ -0,0 +1,72 @@
import * as React from "react";
import { Helmet } from "react-helmet";
import castContext from "./castContext";
import CastReceiver from "./CastReceiver";
const { useState, useEffect } = React;
const wait = (time: number) =>
new Promise((res) => {
setTimeout(res, time);
});
function CastProvider({ children }: { children: any }) {
const [cast, setCast] = useState<{
castReceiver?: CastReceiver;
castSender?: any;
}>({});
const [session, setSession] = useState<any>({});
useEffect(() => {
(async () => {
let toBreak = false;
let tries = 15;
let castReceiver: CastReceiver;
let castSender: any;
while (true) {
try {
// @ts-ignore
castReceiver = window.chrome.cast as CastReceiver;
// @ts-ignore
castSender = window.cast.framework as any;
toBreak = true;
} catch (err) {
tries--;
if (!tries) {
toBreak = true;
}
} finally {
if (toBreak) break;
}
await wait(95);
}
// @ts-ignore
if (tries !== 0 && !!castReceiver) {
setCast({
castReceiver,
castSender,
});
} else {
throw new Error("Can't Load castReceiver and\\or castSender");
}
})();
}, []);
return (
<>
<Helmet>
<script src="//www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1" />
<script src="//www.gstatic.com/cast/sdk/libs/receiver/2.0.0/cast_receiver.js" />
</Helmet>
<castContext.Provider
value={{
...cast,
session,
setSession,
}}
>
{children}
</castContext.Provider>
</>
);
}
export default CastProvider;

View File

@@ -0,0 +1,21 @@
export default interface CastReceiver {
SessionRequest: new (...args: Array<any>) => any;
media: {
MediaInfo: new (p: string) => any;
LoadRequest: new (p: string) => any;
[key: string]: any;
};
Capability: {
[key: string]: string;
};
ApiConfig: new (...args: Array<any>) => any;
initialize: (
ApiConfig: any,
initSucess: (e: any) => void,
initError: (e: any) => void
) => void;
requestSession: (
initSucess: (e: any) => void,
initError: (e: any) => void
) => void;
}

View File

@@ -0,0 +1,11 @@
import { createContext } from "react";
import CastReceiver from "./CastReceiver";
const castContext = createContext<{
castReceiver?: CastReceiver;
castSender?: any;
setSession?: (p: any) => void;
session?: any;
}>({});
export default castContext;

View File

@@ -0,0 +1,6 @@
import CastButton from "./CastButton";
import CastProvider from "./CastProvider";
import useCast from "./useCast";
import useMedia from "./useMedia";
export { CastProvider, CastButton, useMedia, useCast };

View File

@@ -0,0 +1,143 @@
import { useEffect, useContext, useCallback, useState } from "react";
import castContext from "./castContext";
import CastReceiver from "./CastReceiver";
interface Props {
/**
* @param initialize_media_player - is media_receiver id
*
* if you pass auto_initialize as true you should pass this
*
* you should pass media_receiver id or 'DEFAULT_MEDIA_RECEIVER_APP_ID' to use default media receiver
*/
initialize_media_player?: string;
/**
* @param auto_initialize - you can use this to auto initialize cast media player when castReceiver was define
*
* you not need to pass nothing to this if you go initialize by yourself
*/
auto_initialize?: boolean;
}
interface Cast {
/**
* Function to initialize cast player before connect to chromecast
*
* This function should wait for castReceiver
*
* @param media_player - you should pass media_receiver id or 'DEFAULT_MEDIA_RECEIVER_APP_ID' to use default media receiver
*
* Example:
* ```jsx
* const cast = useCast()
* useEffect(() => {
* if(cast.castReceiver){
* cast.initializeCast('DEFAULT_MEDIA_RECEIVER_APP_ID')
* }
* }, [cast.castReceiver])
* ```
*/
initializeCast?: (media_player: string) => void;
/**
* Function to connect and disconnect client to chromecast
*/
handleConnection: () => Promise<any>;
/**
* castReceiver object, from cast_receiver google lib
*/
castReceiver?: CastReceiver;
/**
* castSender object, from cast_sender google lib
*/
castSender?: any;
/**
* this inidicate if client is connected with chromecast or not
*/
isConnect: boolean;
}
function useCast(props?: Props) {
const { initialize_media_player, auto_initialize } = props || {};
const { castReceiver, castSender, setSession } = useContext(castContext);
const [cast, setCast] = useState({});
const [isConnect, setIsConnect] = useState(false);
const initiliazeCast = useCallback(
(media_player: string) => {
if (!castReceiver) return;
const sessionRequest = new castReceiver.SessionRequest(
castReceiver.media[media_player]
);
const apiConfig = new castReceiver.ApiConfig(
// @ts-ignore
sessionRequest,
(e: any) => {
// console.log("ss listener", e);
if (setSession) setSession(e);
setIsConnect(true);
},
(e: any) => {
// console.log("rc listener", e);
}
);
castReceiver.initialize(
apiConfig,
(e: any) => {
// console.log("init success", e);
},
(e: any) => {
// console.log("init error", e);
}
);
setCast({
castReceiver,
});
},
[castReceiver, setSession]
);
const handleConnection = useCallback(
() =>
new Promise((res, rej) => {
if (castReceiver) {
// @ts-ignore
castReceiver.requestSession(
(e: any) => {
if (setSession) setSession(e);
setIsConnect(true);
res(e);
},
(e: any) => {
setIsConnect(false);
if (!isConnect) return rej(e);
return res(null);
}
);
}
}),
[castReceiver, setSession]
);
useEffect(() => {
if (castReceiver) {
setCast({ castReceiver });
if (auto_initialize && !initialize_media_player)
throw new Error(
"if you pass auto_initialize: true, you should pass initialize_media_player"
);
else if (auto_initialize && initialize_media_player)
initiliazeCast(initialize_media_player);
}
}, [castReceiver, castSender]);
const Cast = { ...cast, handleConnection, isConnect } as Cast;
if (!auto_initialize) {
Cast["initializeCast"] = initiliazeCast;
}
return Cast;
}
export default useCast;

View File

@@ -0,0 +1,134 @@
import { useCallback, useContext, useState, useEffect } from "react";
import castContext from "./castContext";
interface Media {
/**
* @function playMedia - function to add an media url to chromecast to play, you should use this to first media before add more with the add function, and before use play function
* @param src - this should be an media url acessible by chromecast
* @param autoplay - this inidicate if media will play after defined, default is true
*/
playMedia: (src: string, autoplay?: boolean) => Promise<any>;
/**
* @function addMedia - function to add an media url to chromecast queue
* @param src - this should be an media url acessible by chromecast
*/
addMedia: (src: string) => Promise<any>;
/**
* @function play - function to play media in chromecast
*/
play: () => Promise<any>;
/**
* @function pause - function to pause media in chromecast
*/
pause: () => Promise<any>;
/**
* this inidicate if is a media connected to chromecast
*/
isMedia: boolean;
/**
* @function next - function to jump to next video in chromecast queue
*/
next: () => Promise<any>;
/**
* @function prev - function to jump to prev video in chromecast queue
*/
prev: () => Promise<any>;
/**
* @function to - function to jump to the time passed in seconds in chromecast playing video
* @param seconds - time in seconds to jump to
*/
to: (seconds: number) => Promise<any>;
}
function useMedia() {
const { session, castReceiver } = useContext(castContext);
const [media, setMedia] = useState<any>(null);
const [isMedia, setIsMedia] = useState(false);
useEffect(() => {
if (!session && isMedia) setIsMedia(false);
}, [session, isMedia]);
const playMedia = useCallback(
(src: string, autoplay?: boolean) =>
new Promise((res, rej) => {
if (!castReceiver || !session)
return rej(new Error("An Error occurred"));
// @ts-ignore
const mediaInfo = new castReceiver.media.MediaInfo(src);
// @ts-ignore
const request = new castReceiver.media.LoadRequest(mediaInfo);
request.autoplay = autoplay || true;
session.loadMedia(
request,
(media: any) => {
setMedia(media);
setIsMedia(true);
res(media);
},
(err: any) => rej(err)
);
}),
[castReceiver, session]
);
const addMedia = useCallback(
async (src: string) => {
if (!castReceiver && !media) return;
// @ts-ignore
const mediaInfo = new castReceiver.media.MediaInfo(src);
// @ts-ignore
const queueItem = new castReceiver.media.QueueItem(mediaInfo);
await media.queueAppendItem(queueItem);
},
[media, castReceiver]
);
const play = useCallback(async () => {
if (!media) return;
await media.play();
}, [media]);
const pause = useCallback(async () => {
if (!media) return;
await media.pause();
}, [media]);
const prev = useCallback(async () => {
if (!media) return;
await media.queuePrev();
}, [media]);
const next = useCallback(async () => {
if (!media) return;
await media.queueNext();
}, [media]);
const to = useCallback(
async (seconds: number) => {
if (!media && !castReceiver) return;
// @ts-ignore
const seek = new castReceiver.media.SeekRequest();
seek.currentTime = seconds;
await media.seek(seek);
},
[media, castReceiver]
);
return {
playMedia,
addMedia,
play,
pause,
isMedia,
next,
prev,
to,
} as Media;
}
export default useMedia;

View File

@@ -1,18 +1,19 @@
const defaultTheme = require("tailwindcss/defaultTheme");
const windmill = require("@windmill/react-ui/config");
module.exports = {
const config = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {
fontFamily: {
Rampart: ["Raleway", "sans-serif"],
},
boxShadow: {
bottom:
"0 5px 6px -7px rgba(0, 0, 0, 0.6), 0 2px 4px -5px rgba(0, 0, 0, 0.06)",
},
},
},
plugins: [],
};
module.exports = {
...windmill(config),
...config,
};

View File

@@ -1760,6 +1760,13 @@
dependencies:
"@types/node" "*"
"@types/helmet@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/helmet/-/helmet-4.0.0.tgz#af7af46de26abe368b85360769ae9938bfb2318a"
integrity sha512-ONIn/nSNQA57yRge3oaMQESef/6QhoeX7llWeDli0UZIfz8TQMkfNPTXA8VnnyeA1WUjG2pGqdjEIueYonMdfQ==
dependencies:
helmet "*"
"@types/hls.js@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/hls.js/-/hls.js-1.0.0.tgz#47a03f97217461d279e4dc89481ddff86def5a36"
@@ -1868,6 +1875,13 @@
dependencies:
"@types/react" "*"
"@types/react-helmet@^6.1.5":
version "6.1.5"
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.5.tgz#35f89a6b1646ee2bc342a33a9a6c8777933f9083"
integrity sha512-/ICuy7OHZxR0YCAZLNg9r7I9aijWUWvxaPR6uTuyxe8tAj5RL4Sw1+R6NhXUtOsarkGYPmaHdBDvuXh2DIN/uA==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@^17.0.20":
version "17.0.43"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55"
@@ -4545,6 +4559,11 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
helmet@*:
version "5.0.2"
resolved "https://registry.yarnpkg.com/helmet/-/helmet-5.0.2.tgz#3264ec6bab96c82deaf65e3403c369424cb2366c"
integrity sha512-QWlwUZZ8BtlvwYVTSDTBChGf8EOcQ2LkGMnQJxSzD1mUu8CCjXJZq/BXP8eWw4kikRnzlhtYo3lCk0ucmYA3Vg==
history@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b"
@@ -6960,7 +6979,7 @@ prompts@^2.0.1, prompts@^2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.6.2, prop-types@^15.8.1:
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -7099,6 +7118,11 @@ react-error-overlay@^6.0.10:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
react-fast-compare@^3.1.1:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-focus-lock@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.4.1.tgz#e842cc93da736b5c5d331799012544295cbcee4f"
@@ -7111,6 +7135,16 @@ react-focus-lock@2.4.1:
use-callback-ref "^1.2.1"
use-sidecar "^1.0.1"
react-helmet@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726"
integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==
dependencies:
object-assign "^4.1.1"
prop-types "^15.7.2"
react-fast-compare "^3.1.1"
react-side-effect "^2.1.0"
react-icons@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca"
@@ -7201,6 +7235,11 @@ react-scripts@5.0.0:
optionalDependencies:
fsevents "^2.3.2"
react-side-effect@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3"
integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ==
react-transition-group@4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"