Extract some shite out into an options modal

This commit is contained in:
Marks Polakovs 2020-04-03 16:23:03 +02:00
parent 5763295b76
commit 502811f68a
14 changed files with 416 additions and 181 deletions

3
.env
View file

@ -1 +1,2 @@
HOST=local-development.ury.org.uk
HOST=local-development.ury.org.uk
REACT_APP_VERSION=$npm_package_version

View file

@ -1,6 +1,6 @@
{
"name": "showplanner2",
"version": "0.1.0",
"name": "webstudio",
"version": "0.0.9",
"private": true,
"dependencies": {
"@babel/core": "7.6.0",
@ -18,6 +18,7 @@
"@types/react-dom": "16.9.4",
"@types/react-modal": "^3.10.5",
"@types/react-redux": "^7.1.5",
"@types/reactstrap": "^8.4.2",
"@types/wavesurfer.js": "^3.2.0",
"@types/webpack-env": "^1.14.1",
"@typescript-eslint/eslint-plugin": "^2.2.0",
@ -72,6 +73,7 @@
"react-dom": "^0.0.0-experimental-38dd17ab9",
"react-modal": "^3.11.2",
"react-redux": "^7.1.3",
"reactstrap": "^8.4.1",
"redux": "^4.0.4",
"resolve": "1.12.0",
"resolve-url-loader": "3.1.0",

View file

@ -263,3 +263,7 @@ button{
0% { }
50% { background-color: rgb(199, 255, 199); }
}
.ReactModal__Overlay {
z-index: 10000;
}

View file

@ -1,83 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import Modal from "react-modal";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../rootReducer";
import * as MixerState from "./state";
import { VUMeter } from "./VUMeter";
export function MicCalibrationModal() {
const state = useSelector((state: RootState) => state.mixer.mic);
const rafRef = useRef<number | null>(null);
const [peak, setPeak] = useState(-Infinity);
const animate = () => {
if (state.calibration) {
const result = MixerState.getMicAnalysis();
setPeak(result);
rafRef.current = requestAnimationFrame(animate);
} else if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
};
useEffect(() => {
if (state.calibration) {
rafRef.current = requestAnimationFrame(animate);
} else if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
}, [state.calibration]);
const dispatch = useDispatch();
return (
<Modal
isOpen={state.calibration}
onRequestClose={() => dispatch(MixerState.stopMicCalibration())}
>
{state.calibration && (
<>
<b>
Speak into the microphone at a normal volume. Adjust the
gain slider until the bar below is green when you're
speaking.
</b>
<div>
<VUMeter
width={400}
height={40}
value={peak}
range={[-70, 0]}
greenRange={[-20, -7]}
/>
</div>
<div>
<input
type="range"
min={1 / 10}
max={3}
step={0.05}
value={state.gain}
onChange={e =>
dispatch(
MixerState.setMicBaseGain(
parseFloat(e.target.value)
)
)
}
/>
<b>{state.baseGain.toFixed(1)}</b>
</div>
<button
onClick={() =>
dispatch(MixerState.stopMicCalibration())
}
>
Stop
</button>
</>
)}
</Modal>
);
}

View file

@ -41,7 +41,7 @@ type PlayerStateEnum = "playing" | "paused" | "stopped";
type PlayerRepeatEnum = "none" | "one" | "all";
type VolumePresetEnum = "off" | "bed" | "full";
type MicVolumePresetEnum = "off" | "full";
type MicErrorEnum = "NO_PERMISSION" | "NOT_SECURE_CONTEXT" | "UNKNOWN";
export type MicErrorEnum = "NO_PERMISSION" | "NOT_SECURE_CONTEXT" | "UNKNOWN";
interface PlayerState {
loadedItem: PlanItem | Track | AuxItem | null;
@ -660,6 +660,11 @@ export const openMicrophone = (micID: string): AppThunk => async (
.connect(micCompressor)
.connect(finalCompressor);
dispatch(mixerState.actions.micOpen(micID));
const state2 = getState();
if (state2.optionsMenu.open && state2.optionsMenu.currentTab === "mic") {
dispatch(startMicCalibration());
}
};
export const setMicVolume = (

View file

@ -0,0 +1,13 @@
import React from "react";
import logo from "../assets/images/webstudio.svg";
export function AboutTab() {
return (
<>
<img src={logo} style={{ filter: "invert(1)" }} />
<div><b>WebStudio v{process.env.REACT_APP_VERSION}</b></div>
<div>Brought to you by URY Computing Team</div>
</>
);
}

159
src/optionsMenu/MicTab.tsx Normal file
View file

@ -0,0 +1,159 @@
import React, { useState, useEffect, useRef } from "react";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../rootReducer";
import * as MixerState from "../mixer/state";
import { VUMeter } from "./helpers/VUMeter";
type MicErrorEnum =
| "NO_PERMISSION"
| "NOT_SECURE_CONTEXT"
| "UNKNOWN"
| "UNKNOWN_ENUM";
function reduceToInputs(devices: MediaDeviceInfo[]) {
var temp: MediaDeviceInfo[] = [];
devices.forEach((device) => {
if (device.kind == "audioinput") {
temp.push(device);
}
});
return temp;
}
export function MicTab() {
const state = useSelector((state: RootState) => state.mixer.mic);
const [micList, setMicList] = useState<null | MediaDeviceInfo[]>(null);
const dispatch = useDispatch();
const [nextMicSource, setNextMicSource] = useState("default");
const [openError, setOpenError] = useState<null | MicErrorEnum>(null);
async function fetchMicNames() {
// Because Chrome, we have to call getUserMedia() before enumerateDevices()
try {
await navigator.mediaDevices.getUserMedia({ audio: true });
} catch (e) {
if (e instanceof DOMException) {
switch (e.message) {
case "Permission denied":
setOpenError("NO_PERMISSION");
break;
default:
setOpenError("UNKNOWN");
}
} else {
setOpenError("UNKNOWN");
}
return;
}
try {
const devices = await navigator.mediaDevices.enumerateDevices();
setMicList(reduceToInputs(devices));
} catch (e) {
setOpenError("UNKNOWN_ENUM");
}
}
function setMicSource(sourceId: string) {
setNextMicSource(sourceId);
dispatch(MixerState.openMicrophone(sourceId));
}
const rafRef = useRef<number | null>(null);
const [peak, setPeak] = useState(-Infinity);
const animate = () => {
if (state.calibration) {
const result = MixerState.getMicAnalysis();
setPeak(result);
rafRef.current = requestAnimationFrame(animate);
} else if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
};
useEffect(() => {
if (state.calibration) {
rafRef.current = requestAnimationFrame(animate);
} else if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
}, [state.calibration]);
return (
<>
<button onClick={fetchMicNames} disabled={micList !== null}>
Open
</button>
<select
className="form-control"
style={{ width: "100%" }}
value={nextMicSource}
onChange={(e) => setMicSource(e.target.value)}
>
<option
value={"None"}
disabled
label="Choose a microphone"
></option>
{(micList || []).map(function(e, i) {
return (
<option value={e.deviceId} key={i}>
{e.label !== "" ? e.label : e.deviceId}
</option>
);
})}
</select>
{state.openError !== null && (
<div className="sp-alert">
{state.openError === "NO_PERMISSION" ||
openError === "NO_PERMISSION"
? "Please grant this page permission to use your microphone and try again."
: state.openError === "NOT_SECURE_CONTEXT" ||
openError === "NOT_SECURE_CONTEXT"
? "We can't open the microphone. Please make sure the address bar has a https:// at the start and try again."
: openError === "UNKNOWN_ENUM"
? "An error occurred when enumerating input devices. Please try again."
: "An error occurred when opening the microphone. Please try again."}
</div>
)}
<div style={{ opacity: state.open ? 1 : 0.5 }}>
<h3>Calibration</h3>
<b>
Speak into the microphone at a normal volume. Adjust the
gain slider until the bar below is green when you're
speaking.
</b>
<div>
<VUMeter
width={400}
height={40}
value={peak}
range={[-70, 0]}
greenRange={[-20, -7]}
/>
</div>
<div>
<input
type="range"
min={1.0 / 10}
max={3}
step={0.05}
value={state.gain}
onChange={(e) =>
dispatch(
MixerState.setMicBaseGain(
parseFloat(e.target.value)
)
)
}
/>
<b>{state.baseGain.toFixed(1)}</b>
</div>
</div>
</>
);
}

46
src/optionsMenu/index.tsx Normal file
View file

@ -0,0 +1,46 @@
import React from "react";
import { Nav, TabContent, TabPane, NavItem, NavLink } from "reactstrap";
import Modal from "react-modal";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../rootReducer";
import * as OptionsState from "./state";
import { MicTab } from "./MicTab";
import { AboutTab } from "./AboutTab";
export function OptionsMenu() {
const state = useSelector((state: RootState) => state.optionsMenu);
const dispatch = useDispatch();
return (
<Modal
isOpen={state.open}
onRequestClose={() => dispatch(OptionsState.close())}
>
<Nav tabs>
<NavItem>
<NavLink
className={state.currentTab === "mic" ? "active" : ""}
onClick={() => dispatch(OptionsState.changeTab("mic"))}
>
Microphone
</NavLink>
</NavItem>
<NavItem>
<NavLink
className={state.currentTab === "about" ? "active" : ""}
onClick={() => dispatch(OptionsState.changeTab("about"))}
>
About
</NavLink>
</NavItem>
</Nav>
<TabContent activeTab={state.currentTab}>
<TabPane tabId="mic"><MicTab /></TabPane>
<TabPane tabId="about"><AboutTab /></TabPane>
</TabContent>
<footer>
<button onClick={() => dispatch(OptionsState.close())}>Exit</button>
</footer>
</Modal>
);
}

59
src/optionsMenu/state.ts Normal file
View file

@ -0,0 +1,59 @@
import {
createSlice,
PayloadAction,
Middleware,
Dispatch,
} from "@reduxjs/toolkit";
import { RootState } from "../rootReducer";
import * as MixerState from "../mixer/state";
export type OptionsTabIDsEnum = "mic" | "about";
const optionsMenuState = createSlice({
name: "optionsMenu",
initialState: {
open: false,
currentTab: "mic" as OptionsTabIDsEnum,
},
reducers: {
open(state) {
state.open = true;
},
openToTab(state, action: PayloadAction<OptionsTabIDsEnum>) {
state.open = true;
state.currentTab = action.payload;
},
close(state) {
state.open = false;
},
changeTab(state, action: PayloadAction<OptionsTabIDsEnum>) {
state.currentTab = action.payload;
},
},
});
export default optionsMenuState.reducer;
export const { open, openToTab, close, changeTab } = optionsMenuState.actions;
export const tabSyncMiddleware: Middleware<{}, RootState, Dispatch> = (
store
) => (next) => (action) => {
const oldState = store.getState();
const result = next(action);
const newState = store.getState();
if (
newState.optionsMenu.currentTab === "mic"
) {
if (oldState.optionsMenu.currentTab !== "mic" && newState.optionsMenu.open) {
store.dispatch(MixerState.startMicCalibration() as any);
}
} else if (
oldState.optionsMenu.currentTab === "mic" ||
oldState.optionsMenu.open !== newState.optionsMenu.open
) {
store.dispatch(MixerState.stopMicCalibration() as any);
}
return result;
};

View file

@ -5,13 +5,15 @@ import MixerReducer from "./mixer/state";
import BroadcastReducer from "./broadcast/state";
import sessionReducer from "./session/state";
import NavbarReducer from "./navbar/state";
import OptionsMenuReducer from "./optionsMenu/state";
const rootReducer = combineReducers({
showplan: ShowplanReducer,
mixer: MixerReducer,
broadcast: BroadcastReducer,
session: sessionReducer,
navbar: NavbarReducer
navbar: NavbarReducer,
optionsMenu: OptionsMenuReducer
});
export type RootState = ReturnType<typeof rootReducer>;

View file

@ -1,17 +1,15 @@
import React, { useState, useReducer, useEffect, memo } from "react";
import { ContextMenu, MenuItem } from "react-contextmenu";
import { useBeforeunload } from "react-beforeunload";
import { MYRADIO_NON_API_BASE } from "../api"
import { MYRADIO_NON_API_BASE } from "../api";
import {
TimeslotItem,
} from "../api";
import { TimeslotItem } from "../api";
import {
Droppable,
DragDropContext,
DropResult,
ResponderProvided
ResponderProvided,
} from "react-beautiful-dnd";
import { useSelector, useDispatch } from "react-redux";
@ -22,23 +20,24 @@ import {
itemId,
moveItem,
addItem,
removeItem
removeItem,
} from "./state";
import * as MixerState from "../mixer/state";
import * as BroadcastState from "../broadcast/state";
import * as OptionsMenuState from "../optionsMenu/state";
import { Item, TS_ITEM_MENU_ID } from "./Item";
import {
CentralMusicLibrary,
CML_CACHE,
AuxLibrary,
AUX_CACHE
AUX_CACHE,
} from "./libraries";
import { Player, USE_REAL_GAIN_VALUE } from "./Player";
import { MicCalibrationModal } from "../mixer/MicCalibrationModal";
import { timestampToDateTime } from "../lib/utils";
import { CombinedNavAlertBar } from "../navbar";
import { OptionsMenu } from "../optionsMenu";
function Column({ id, data }: { id: number; data: PlanItem[] }) {
return (
@ -54,7 +53,7 @@ function Column({ id, data }: { id: number; data: PlanItem[] }) {
{typeof data[id] === "undefined"
? null
: data
.filter(x => x.channel === id)
.filter((x) => x.channel === id)
.sort((a, b) => a.weight - b.weight)
.map((x, index) => (
<Item
@ -88,7 +87,7 @@ const AUX_LIBRARIES: { [key: string]: string } = {
"aux-12": "Roses 2018",
"aux-10": "Sound Effects",
"aux-8": "Speech",
"aux-9": "Teasers"
"aux-9": "Teasers",
};
function LibraryColumn() {
@ -99,14 +98,14 @@ function LibraryColumn() {
className="form-control"
style={{ width: "100%" }}
value={sauce}
onChange={e => setSauce(e.target.value)}
onChange={(e) => setSauce(e.target.value)}
>
<option value={"None"} disabled>
Choose a library
</option>
<option value={"CentralMusicLibrary"}>Central Music Library</option>
<option disabled>Resources</option>
{Object.keys(AUX_LIBRARIES).map(libId => (
{Object.keys(AUX_LIBRARIES).map((libId) => (
<option key={libId} value={libId}>
{AUX_LIBRARIES[libId]}
</option>
@ -128,74 +127,17 @@ function LibraryColumn() {
function MicControl() {
const state = useSelector((state: RootState) => state.mixer.mic);
const [micList, setMicList] = useState<MediaDeviceInfo[]>([]);
const dispatch = useDispatch();
const [nextMicSource, setNextMicSource] = useState("default") // next mic source
const [lock, setLock] = useState(false)
useEffect(()=>{
navigator.mediaDevices.enumerateDevices()
.then((devices)=>{
setMicList(reduceToInputs(devices))
})
.catch(() => {console.log("Could not fetch devices");})
}, [])
function reduceToInputs(devices:MediaDeviceInfo[]){
var temp: MediaDeviceInfo[] = []
devices.forEach((device)=>{
if (device.kind == "audioinput") {
temp.push(device)
}
})
return temp
}
function toggleCheck(){setLock(!lock)}
return (
<div className="sp-col" style={{ height: "48%", overflowY: "visible" }}>
<h2>Microphone</h2>
<button
disabled={state.id == nextMicSource || lock}
onClick={() => dispatch(MixerState.openMicrophone(nextMicSource))}
>
Open
</button>
<div className="custom-control custom-checkbox">
<input className="custom-control-input" type="checkbox" id="micLock" onChange={toggleCheck}></input>
<label className="custom-control-label" htmlFor="micLock" style={{marginLeft:"8px"}}> Lock Microphone</label>
</div>
<select
className="form-control"
style={{ width: "100%" }}
value={nextMicSource}
onChange={e => setNextMicSource(e.target.value)}
>
<option value={"None"} disabled label="Choose a microphone"></option>
{
micList.map(function(e,i) {
return <option value={e.deviceId} key={i}>{e.label !== "" ? e.label : e.deviceId}</option>;
})
}
</select>
<button disabled={!state.open} onClick={() => dispatch(MixerState.startMicCalibration())}>
Calibrate Trim
</button>
{state.openError !== null && (
<div className="sp-alert">
{state.openError === "NO_PERMISSION"
? "Please grant this page permission to use your microphone and try again."
: state.openError === "NOT_SECURE_CONTEXT"
? "We can't open the microphone. Please make sure the address bar has a https:// at the start and try again."
: "An error occurred when opening the microphone. Please try again."}
</div>
)}
<div className="sp-mixer-buttons">
<div
className="sp-mixer-buttons-backdrop"
style={{
width: (USE_REAL_GAIN_VALUE ? state.gain : state.volume) * 100 + "%"
width:
(USE_REAL_GAIN_VALUE ? state.gain : state.volume) * 100 + "%",
}}
></div>
<button onClick={() => dispatch(MixerState.setMicVolume("off"))}>
@ -205,6 +147,9 @@ function MicControl() {
Full
</button>
</div>
<div>
<button onClick={() => dispatch(OptionsMenuState.open())}>Options</button>
</div>
</div>
);
}
@ -219,25 +164,25 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
planLoadError,
planLoading,
planSaveError,
planSaving
planSaving,
} = useSelector((state: RootState) => state.showplan);
const dispatch = useDispatch();
useBeforeunload(event => event.preventDefault());
useBeforeunload((event) => event.preventDefault());
useEffect(() => {
dispatch(getShowplan(timeslotId));
}, [dispatch, timeslotId]);
function toggleSidebar() {
var element = document.getElementById("sidebar");
if (element) {
element.classList.toggle("active");
}
setTimeout(function () {dispatch(MixerState.redrawWavesurfers())}, 500);
setTimeout(function() {
dispatch(MixerState.redrawWavesurfers());
}, 500);
}
const [insertIndex, increment] = useReducer(incrReducer, 0);
@ -259,7 +204,7 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
timeslotitemid: "I" + insertIndex,
channel: parseInt(result.destination.droppableId, 10),
weight: result.destination.index,
...data
...data,
};
dispatch(addItem(timeslotId, newItem));
increment(null);
@ -273,7 +218,7 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
channel: parseInt(result.destination.droppableId, 10),
weight: result.destination.index,
clean: true,
...data
...data,
};
dispatch(addItem(timeslotId, newItem));
increment(null);
@ -282,7 +227,7 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
dispatch(
moveItem(timeslotId, result.draggableId, [
parseInt(result.destination.droppableId, 10),
result.destination.index
result.destination.index,
])
);
}
@ -344,7 +289,7 @@ const Showplanner: React.FC<{ timeslotId: number }> = function({ timeslotId }) {
<ContextMenu id={TS_ITEM_MENU_ID}>
<MenuItem onClick={onCtxRemoveClick}>Remove</MenuItem>
</ContextMenu>
<MicCalibrationModal />
<OptionsMenu />
</div>
);
};

View file

@ -1,18 +1,27 @@
import rootReducer, { RootState } from "./rootReducer";
import { configureStore, Action, getDefaultMiddleware } from "@reduxjs/toolkit";
import { ThunkAction } from "redux-thunk";
import { mixerMiddleware, mixerKeyboardShortcutsMiddleware } from "./mixer/state";
import {
mixerMiddleware,
mixerKeyboardShortcutsMiddleware,
} from "./mixer/state";
import { tabSyncMiddleware } from "./optionsMenu/state";
const store = configureStore({
reducer: rootReducer,
middleware: [...getDefaultMiddleware(), mixerMiddleware, mixerKeyboardShortcutsMiddleware]
reducer: rootReducer,
middleware: [
mixerMiddleware,
mixerKeyboardShortcutsMiddleware,
tabSyncMiddleware,
...getDefaultMiddleware(),
],
});
if (process.env.NODE_ENV === "development" && module.hot) {
module.hot.accept("./rootReducer", () => {
const newRootReducer = require("./rootReducer").default;
store.replaceReducer(newRootReducer);
});
module.hot.accept("./rootReducer", () => {
const newRootReducer = require("./rootReducer").default;
store.replaceReducer(newRootReducer);
});
}
export type AppDispatch = typeof store.dispatch;

View file

@ -914,7 +914,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
"@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.9.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
@ -1523,6 +1523,14 @@
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/reactstrap@^8.4.2":
version "8.4.2"
resolved "https://registry.yarnpkg.com/@types/reactstrap/-/reactstrap-8.4.2.tgz#e7066d0e67e2924dab0a52c6aedcf922f2be53b6"
integrity sha512-ag4hfFqBZaeoNSSTKjCtedvdcO68QqqlBrFd3obg94JSmhgNTmHz50BvNJkf9NjSzx1yGTW4l/OyP/khLPKqww==
dependencies:
"@types/react" "*"
popper.js "^1.14.1"
"@types/resolve@0.0.8":
version "0.0.8"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
@ -2809,7 +2817,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.5:
classnames@^2.2.3, classnames@^2.2.5:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
@ -3196,6 +3204,14 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
create-react-context@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.3.0.tgz#546dede9dc422def0d3fc2fe03afe0bc0f4f7d8c"
integrity sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==
dependencies:
gud "^1.0.0"
warning "^4.0.3"
cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -3535,7 +3551,7 @@ decode-uri-component@^0.2.0:
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
deep-equal@^1.0.1:
deep-equal@^1.0.1, deep-equal@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
@ -3734,6 +3750,13 @@ dom-converter@^0.2:
dependencies:
utila "~0.4"
dom-helpers@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
dependencies:
"@babel/runtime" "^7.1.2"
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@ -4883,6 +4906,11 @@ growly@^1.3.0:
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
gud@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
gzip-size@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274"
@ -7748,6 +7776,11 @@ pnp-webpack-plugin@1.5.0:
dependencies:
ts-pnp "^1.1.2"
popper.js@^1.14.1, popper.js@^1.14.4:
version "1.16.1"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
portfinder@^1.0.9:
version "1.0.25"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca"
@ -8509,7 +8542,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.4"
prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -8787,7 +8820,7 @@ react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-i
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-lifecycles-compat@^3.0.0:
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
@ -8802,6 +8835,19 @@ react-modal@^3.11.2:
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-popper@^1.3.6:
version "1.3.7"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324"
integrity sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==
dependencies:
"@babel/runtime" "^7.1.2"
create-react-context "^0.3.0"
deep-equal "^1.1.1"
popper.js "^1.14.4"
prop-types "^15.6.1"
typed-styles "^0.0.7"
warning "^4.0.2"
react-redux@^7.1.1, react-redux@^7.1.3:
version "7.2.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
@ -8813,6 +8859,16 @@ react-redux@^7.1.1, react-redux@^7.1.3:
prop-types "^15.7.2"
react-is "^16.9.0"
react-transition-group@^2.3.1:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
dependencies:
dom-helpers "^3.4.0"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react@^0.0.0-experimental-38dd17ab9:
version "0.0.0-fec00a869"
resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-fec00a869.tgz#1803f4f17cdd5adfdf614de2386c5fb5c84bbe91"
@ -8823,6 +8879,18 @@ react@^0.0.0-experimental-38dd17ab9:
prop-types "^15.6.2"
scheduler "0.0.0-fec00a869"
reactstrap@^8.4.1:
version "8.4.1"
resolved "https://registry.yarnpkg.com/reactstrap/-/reactstrap-8.4.1.tgz#c7f63b9057f58b52833061711ebe235b9ec4e3e5"
integrity sha512-oAjp9PYYUGKl7SLXwrQ1oRIrYw0MqfO2mUqYgGapFKHG2uwjEtLip5rYxtMujkGx3COjH5FX1WtcfNU4oqpH0Q==
dependencies:
"@babel/runtime" "^7.2.0"
classnames "^2.2.3"
prop-types "^15.5.8"
react-lifecycles-compat "^3.0.4"
react-popper "^1.3.6"
react-transition-group "^2.3.1"
read-pkg-up@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
@ -10463,6 +10531,11 @@ type@^2.0.0:
resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3"
integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==
typed-styles@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@ -10737,7 +10810,7 @@ walker@^1.0.7, walker@~1.0.5:
dependencies:
makeerror "1.0.x"
warning@^4.0.3:
warning@^4.0.2, warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==