From 59c5904df1bc6e9228295e785bb672ee799704fe Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Mon, 5 Feb 2024 10:44:50 -0500 Subject: [PATCH 001/221] Add sync for clear canvas --- app/src/components/left/RoomsContainer.tsx | 6 ++++++ app/src/components/top/NavBarButtons.tsx | 7 ++++++- app/src/redux/store.ts | 8 -------- server/server.ts | 12 ++++++++++++ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/src/components/left/RoomsContainer.tsx b/app/src/components/left/RoomsContainer.tsx index 0df5bac42..cc7ee3bda 100644 --- a/app/src/components/left/RoomsContainer.tsx +++ b/app/src/components/left/RoomsContainer.tsx @@ -28,6 +28,7 @@ import { addPassedInProps, deletePassedInProps, deleteElement, + resetAllState, updateStylesheet } from '../../redux/reducers/slice/appStateSlice'; import { @@ -132,6 +133,11 @@ const RoomsContainer = () => { } ); + // dispatch clear canvas action to local state when the host of the room has clear canvas + socket.on('clear canvas from server', () => { + store.dispatch(resetAllState()); + }); + // dispatch all updates to local state when another user has saved from Bottom Panel socket.on('update data from server', (updateData: BottomPanelObj) => { // console.log('update data received from server', updateData); diff --git a/app/src/components/top/NavBarButtons.tsx b/app/src/components/top/NavBarButtons.tsx index e7483b19f..333b39a47 100644 --- a/app/src/components/top/NavBarButtons.tsx +++ b/app/src/components/top/NavBarButtons.tsx @@ -21,6 +21,7 @@ import { resetAllState } from '../../redux/reducers/slice/appStateSlice'; import { setStyle } from '../../redux/reducers/slice/styleSlice'; import store from '../../redux/store'; import withStyles from '@mui/styles/withStyles'; +import { emitEvent } from '../../helperFunctions/socket'; const { API_BASE_URL } = config; @@ -97,6 +98,8 @@ function navbarDropDown(props) { // state: store.appState // })); const state = useSelector((store: RootState) => store.appState); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); + const userName = useSelector((store: RootState) => store.roomSlice.userName); const closeModal = () => setModal(''); const handleClick = (event) => { @@ -106,7 +109,8 @@ function navbarDropDown(props) { const clearWorkspace = () => { // Reset state for project to initial state const resetState = () => { - dispatch(resetAllState()); + if (roomCode) emitEvent('clearCanvasAction', roomCode, userName); + else dispatch(resetAllState()); }; // Set modal options const children = ( @@ -129,6 +133,7 @@ function navbarDropDown(props) { ); + // Create modal setModal( createModal({ diff --git a/app/src/redux/store.ts b/app/src/redux/store.ts index 5f1eb38ce..1181e2410 100644 --- a/app/src/redux/store.ts +++ b/app/src/redux/store.ts @@ -3,14 +3,6 @@ import { configureStore } from '@reduxjs/toolkit'; // Import of combined reducers in rootReducer import rootReducer from './reducers/rootReducer'; -/* -// Define the root state type based on the rootReducer -export type RootState = ReturnType; - -// Define the type of the Redux store -export type AppStore = Store; -*/ - const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => { diff --git a/server/server.ts b/server/server.ts index febcf5026..c6a6783ab 100644 --- a/server/server.ts +++ b/server/server.ts @@ -191,6 +191,18 @@ io.on('connection', (client) => { } }); + client.on('clearCanvasAction', async (roomCode: string, userName: string) => { + if (roomCode) { + // server send clear canvas to everyone in the room if action is from the host + if (userName === Object.values(roomLists[roomCode])[0]) { + io.to(roomCode).emit( + 'clear canvas from server', + Object.values(roomLists[roomCode]) + ); + } + } + }); + client.on( 'deleteElementAction', (roomCode: string, deleteElementData: object) => { From 3aaa3b0b978205393c3a5058c8796253f5588e6e Mon Sep 17 00:00:00 2001 From: John Wage Date: Tue, 6 Feb 2024 10:43:27 -0500 Subject: [PATCH 002/221] prop drilled bottomShow piece of state from MainContainer parent component to BottomPanel child component to change expand and collapse material ui icons. And changed onMouseOver event handler to onClick event handler on BottomTabs component. Install more Material UI dependencies also --- app/src/components/bottom/BottomPanel.tsx | 31 ++-- app/src/components/bottom/BottomTabs.tsx | 2 +- app/src/containers/MainContainer.tsx | 1 + package-lock.json | 191 +++++++++++++--------- package.json | 6 +- 5 files changed, 136 insertions(+), 95 deletions(-) diff --git a/app/src/components/bottom/BottomPanel.tsx b/app/src/components/bottom/BottomPanel.tsx index 5004e07cc..83f4e54ed 100644 --- a/app/src/components/bottom/BottomPanel.tsx +++ b/app/src/components/bottom/BottomPanel.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useRef } from 'react'; import BottomTabs from './BottomTabs'; +import { ExpandLess, ExpandMore } from '@mui/icons-material'; const BottomPanel = (props): JSX.Element => { let y: number = 0; @@ -15,18 +16,17 @@ const BottomPanel = (props): JSX.Element => { //Start listeners when the user clicks the bottom panel tab document.addEventListener('mousemove', mouseMoveHandler); document.addEventListener('mouseup', mouseUpHandler); - window.addEventListener('message', handleIframeMessage);//listens for messages from the iframe when the mouse is over it + window.addEventListener('message', handleIframeMessage); //listens for messages from the iframe when the mouse is over it }; - + //Interpret the messages from the iframe const handleIframeMessage = (e) => { if (e.data === 'iframeMouseUp') { mouseUpHandler(); - }else if(e.data.type === 'iframeMouseMove'){ - mouseMoveHandler(e.data) + } else if (e.data.type === 'iframeMouseMove') { + mouseMoveHandler(e.data); } - } - + }; const mouseMoveHandler = function (e: MouseEvent): void { // How far the mouse has been moved @@ -54,12 +54,21 @@ const BottomPanel = (props): JSX.Element => { return ( <> -
-
- ...... +
+
props.setBottomShow(true)} + tabIndex={0} + > + {props.bottomShow ? : } +
+
- -
+ ); }; diff --git a/app/src/components/bottom/BottomTabs.tsx b/app/src/components/bottom/BottomTabs.tsx index 04e66f3bc..b3f17482b 100644 --- a/app/src/components/bottom/BottomTabs.tsx +++ b/app/src/components/bottom/BottomTabs.tsx @@ -57,7 +57,7 @@ const BottomTabs = (props): JSX.Element => { zIndex: 1, borderTop: '2px solid grey' }} - onMouseOver={() => { + onClick={() => { props.setBottomShow(true); }} > diff --git a/app/src/containers/MainContainer.tsx b/app/src/containers/MainContainer.tsx index 578c503c7..84e8f16ef 100644 --- a/app/src/containers/MainContainer.tsx +++ b/app/src/containers/MainContainer.tsx @@ -119,6 +119,7 @@ const MainContainer = (props): JSX.Element => {
diff --git a/package-lock.json b/package-lock.json index 05516e35c..6789abe7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@babel/cli": "^7.23.4", "@babel/register": "^7.22.15", "@graphql-tools/schema": "^9.0.19", - "@mui/icons-material": "^5.15.0", + "@mui/icons-material": "^5.15.7", "@mui/lab": "^5.0.0-alpha.93", "@mui/styles": "^5.15.0", "@reduxjs/toolkit": "^1.9.7", @@ -90,10 +90,10 @@ "@babel/preset-env": "^7.23.6", "@babel/preset-react": "^7.23.3", "@babel/preset-typescript": "^7.23.3", - "@emotion/react": "^11.11.1", + "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@material-ui/icons": "^4.11.3", - "@mui/material": "^5.15.0", + "@mui/material": "^5.15.7", "@mui/styled-engine-sc": "^5.14.12", "@mui/x-data-grid": "^5.17.26", "@playwright/test": "^1.40.1", @@ -9050,9 +9050,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz", - "integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -9492,15 +9492,15 @@ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "node_modules/@emotion/react": { - "version": "11.11.1", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", - "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz", + "integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==", "devOptional": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.11.0", "@emotion/cache": "^11.11.0", - "@emotion/serialize": "^1.1.2", + "@emotion/serialize": "^1.1.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", "@emotion/utils": "^1.2.1", "@emotion/weak-memoize": "^0.3.1", @@ -9516,9 +9516,9 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", - "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", + "integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", "devOptional": true, "dependencies": { "@emotion/hash": "^0.9.1", @@ -9772,28 +9772,28 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz", - "integrity": "sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", "dependencies": { - "@floating-ui/utils": "^0.1.3" + "@floating-ui/utils": "^0.2.1" } }, "node_modules/@floating-ui/dom": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", - "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz", + "integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==", "dependencies": { - "@floating-ui/core": "^1.4.2", - "@floating-ui/utils": "^0.1.3" + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.1" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz", - "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", "dependencies": { - "@floating-ui/dom": "^1.5.1" + "@floating-ui/dom": "^1.6.1" }, "peerDependencies": { "react": ">=16.8.0", @@ -9801,9 +9801,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", - "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" }, "node_modules/@graphql-tools/merge": { "version": "8.4.2", @@ -10915,20 +10915,20 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.0.tgz", - "integrity": "sha512-NpGtlHwuyLfJtdrlERXb8qRqd279O0VnuGaZAor1ehdNhUJOD1bSxHDeXKZkbqNpvi50hasFj7lsbTpluworTQ==", + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.7.tgz", + "integrity": "sha512-AuF+Wo2Mp/edaO6vJnWjg+gj4tzEz5ChMZnAQpc22DXpSvM8ddgGcZvM7D7F99pIBoSv8ub+Iz0viL+yuGVmhg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.0.tgz", - "integrity": "sha512-zHY6fOkaK7VfhWeyxO8MjO3IAjEYpYMXuqUhX7TkUZJ9+TSH/9dn4ClG4K2j6hdgBU5Yrq2Z/89Bo6BHHp7AdQ==", + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.7.tgz", + "integrity": "sha512-EDAc8TVJGIA/imAvR3u4nANl2W5h3QeHieu2gK7Ypez/nIA55p08tHjf8UrMXEpxCAvfZO6piY9S9uaxETdicA==", "dependencies": { - "@babel/runtime": "^7.23.5" + "@babel/runtime": "^7.23.9" }, "engines": { "node": ">=12.0.0" @@ -10997,18 +10997,18 @@ } }, "node_modules/@mui/material": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.0.tgz", - "integrity": "sha512-60CDI/hQNwJv9a3vEZtFG7zz0USdQhVwpBd3fZqrzhuXSdiMdYMaZcCXeX/KMuNq0ZxQEAZd74Pv+gOb408QVA==", - "dependencies": { - "@babel/runtime": "^7.23.5", - "@mui/base": "5.0.0-beta.27", - "@mui/core-downloads-tracker": "^5.15.0", - "@mui/system": "^5.15.0", - "@mui/types": "^7.2.11", - "@mui/utils": "^5.15.0", - "@types/react-transition-group": "^4.4.9", - "clsx": "^2.0.0", + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.7.tgz", + "integrity": "sha512-l6+AiKZH3iOJmZCnlpel8ghYQe9Lq0BEuKP8fGj3g5xz4arO9GydqYAtLPMvuHKtArj8lJGNuT2yHYxmejincA==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.34", + "@mui/core-downloads-tracker": "^5.15.7", + "@mui/system": "^5.15.7", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.7", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", "csstype": "^3.1.2", "prop-types": "^15.8.1", "react-is": "^18.2.0", @@ -11040,21 +11040,52 @@ } } }, + "node_modules/@mui/material/node_modules/@mui/base": { + "version": "5.0.0-beta.34", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.34.tgz", + "integrity": "sha512-e2mbTGTtReD/y5RFwnhkl1Tgl3XwgJhY040IlfkTVaU9f5LWrVhEnpRsYXu3B1CtLrwiWs4cu7aMHV9yRd4jpw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@floating-ui/react-dom": "^2.0.8", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.7", + "@popperjs/core": "^2.11.8", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material/node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", "engines": { "node": ">=6" } }, "node_modules/@mui/private-theming": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.0.tgz", - "integrity": "sha512-7WxtIhXxNek0JjtsYy+ut2LtFSLpsUW5JSDehQO+jF7itJ8ehy7Bd9bSt2yIllbwGjCFowLfYpPk2Ykgvqm1tA==", + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.7.tgz", + "integrity": "sha512-bcEeeXm7GyQCQvN9dwo8htGv8/6tP05p0i02Z7GXm5EoDPlBcqTNGugsjNLoGq6B0SsdyanjJGw0Jw00o1yAOA==", "dependencies": { - "@babel/runtime": "^7.23.5", - "@mui/utils": "^5.15.0", + "@babel/runtime": "^7.23.9", + "@mui/utils": "^5.15.7", "prop-types": "^15.8.1" }, "engines": { @@ -11075,11 +11106,11 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.0.tgz", - "integrity": "sha512-6NysIsHkuUS2lF+Lzv1jiK3UjBJk854/vKVcJQVGKlPiqNEVZJNlwaSpsaU5xYXxWEZYfbVFSAomLOS/LV/ovQ==", + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.7.tgz", + "integrity": "sha512-ixSdslOjK1kzdGcxqj7O3d14By/LPQ7EWknsViQ8RaeT863EAQemS+zvUJDTcOpkfJh6q6gPnYMIb2TJCs9eWA==", "dependencies": { - "@babel/runtime": "^7.23.5", + "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.11.0", "csstype": "^3.1.2", "prop-types": "^15.8.1" @@ -11181,16 +11212,16 @@ } }, "node_modules/@mui/system": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.0.tgz", - "integrity": "sha512-8TPjfTlYBNB7/zBJRL4QOD9kImwdZObbiYNh0+hxvhXr2koezGx8USwPXj8y/JynbzGCkIybkUztCdWlMZe6OQ==", - "dependencies": { - "@babel/runtime": "^7.23.5", - "@mui/private-theming": "^5.15.0", - "@mui/styled-engine": "^5.15.0", - "@mui/types": "^7.2.11", - "@mui/utils": "^5.15.0", - "clsx": "^2.0.0", + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.7.tgz", + "integrity": "sha512-9alZ4/dLxsTwUOdqakgzxiL5YW6ntqj0CfzWImgWnBMTZhgGcPsbYpBLniNkkk7/jptma4/bykWXHwju/ls/pg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/private-theming": "^5.15.7", + "@mui/styled-engine": "^5.15.7", + "@mui/types": "^7.2.13", + "@mui/utils": "^5.15.7", + "clsx": "^2.1.0", "csstype": "^3.1.2", "prop-types": "^15.8.1" }, @@ -11220,17 +11251,17 @@ } }, "node_modules/@mui/system/node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", "engines": { "node": ">=6" } }, "node_modules/@mui/types": { - "version": "7.2.11", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.11.tgz", - "integrity": "sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w==", + "version": "7.2.13", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", + "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0" }, @@ -11241,11 +11272,11 @@ } }, "node_modules/@mui/utils": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.0.tgz", - "integrity": "sha512-XSmTKStpKYamewxyJ256+srwEnsT3/6eNo6G7+WC1tj2Iq9GfUJ/6yUoB7YXjOD2jTZ3XobToZm4pVz1LBt6GA==", + "version": "5.15.7", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.7.tgz", + "integrity": "sha512-8qhsxQRNV6aEOjjSk6YQIYJxkF5klhj8oG1FEEU4z6HV78TjNqRxMP08QGcdsibEbez+nihAaz6vu83b4XqbAg==", "dependencies": { - "@babel/runtime": "^7.23.5", + "@babel/runtime": "^7.23.9", "@types/prop-types": "^15.7.11", "prop-types": "^15.8.1", "react-is": "^18.2.0" diff --git a/package.json b/package.json index dbd568fa1..e97fce18e 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "@babel/cli": "^7.23.4", "@babel/register": "^7.22.15", "@graphql-tools/schema": "^9.0.19", - "@mui/icons-material": "^5.15.0", + "@mui/icons-material": "^5.15.7", "@mui/lab": "^5.0.0-alpha.93", "@mui/styles": "^5.15.0", "@reduxjs/toolkit": "^1.9.7", @@ -195,10 +195,10 @@ "@babel/preset-env": "^7.23.6", "@babel/preset-react": "^7.23.3", "@babel/preset-typescript": "^7.23.3", - "@emotion/react": "^11.11.1", + "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@material-ui/icons": "^4.11.3", - "@mui/material": "^5.15.0", + "@mui/material": "^5.15.7", "@mui/styled-engine-sc": "^5.14.12", "@mui/x-data-grid": "^5.17.26", "@playwright/test": "^1.40.1", From e1eeeb0afad092a7d2e4a59a281b6c2df5d6c7c0 Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Tue, 6 Feb 2024 15:17:47 -0500 Subject: [PATCH 003/221] Add pull request template --- .github/pull_request_template.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..54d7c24a0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,27 @@ +# Description + + + +## Type of change + +Please check the options that applied + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +# How Has the Changes Been Tested? + + + +# Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my code +- [ ] I have commented my code properly, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules From b00685b1b725a6e32f3ff41824bc86f00848b3df Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Tue, 6 Feb 2024 15:19:51 -0500 Subject: [PATCH 004/221] Add pull request template --- .github/pull_request_template.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 54d7c24a0..c3b7097b4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,8 +1,12 @@ # Description - + -## Type of change +## Type of Change Please check the options that applied @@ -13,7 +17,11 @@ Please check the options that applied # How Has the Changes Been Tested? - + # Checklist: From bf6c529926425f24b69c19c4db1e8c616552c9cc Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Tue, 6 Feb 2024 16:15:30 -0500 Subject: [PATCH 005/221] Update string --- .github/pull_request_template.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c3b7097b4..d76f6f911 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,6 @@ # Description -Vawb!dv2OLn_VW zN^%Di6a?i=O6NcWy^+*w!s@E2Ue|Pa^gH?Ypu~aJJ^jmk8ruP$7v@Ok%SZ}>eipwY zn!$Uae+9VPnVd5CR?`@VHgR(VFF`|E#R6*`n=?atV{7?OE)jTlZbsa#Q@Qr|&6;!p z=!E5$m)RZ14IDPBX^8|uCO}apQ#T#=>7FgfurJv!kdzw_ zk-thrO$8>KYx;=I%&dw6XT3G~K~g8)0IhC{^NL7*XXVhR-zdbIP4WkDX5*6v=8Ugr zoZLU1-ipD@N%+M6Jg+LNfI2%b+GR8kY*sUb57a^jO`QQ^zfv9K+%h(@S{nHO^i zKXG&gb#8264b_VZsq$0EQ5-$nGK3?CL$LB|nbLHRVEchf(oEkO=uL`@(gb0qJ^oW@ zmx_FnAyP-1wW!2exr(!2E`*&Uun=AAb6Y9LKZ`B!ROr~m78PxfaHT3!<)){R>b#09 zZP=ebU($+}2r^~YV*+%E*-3=4P42}JXl`DYw5su>Ewzha9V-o;>fJ<|J%c41X7MY6 z>y)0?b-%OA;v$_cB-lQnops*LZyX7wGvg1(fzdCNMhc={UW|7VAtrX{Q34!?M7%qF z=oMmS<3y7-llIPY`Osf0#Td(V;z!W>ZBl!`^|^Vu!8M20oe~%#Y||KiTUmFt)ed`p z8%gXXeELUF7WfZA`R`fcuG!HOsIZXwy89cJY+F6~M0%)MuwLr&hEVLDW+FQLmtn7Gg~>0>l8nwNv4b)a3IP2V z6}h&B0%+*DiZ^Q$zeDyNh=ZTDjwOeBde{fX&jq{|2@m49`CBhjdZr64YjbN_@EZ4F ze%qSs@1_b4bLtsN#JS}j0q%M zKOUjpmC;x=TWBJ{D?8oJC^Ph2{%%FZ@|5iqx&dq1TchULLtSw1bor~YTJ@xQKo=PN zx%rvkq~aR_=7h8#= z4#m?pG$EorP+<_ykf$N6<)utK9ePEuW=`utKw_I6LH`gn)Nj8G%U#*j4VsZ3yx>I% zXfSxq_E)Wr<9M(R^6rN1#?@t}|H=Z`HP4ond}DqIPc4aQ;Fc&?PY5+W6IX~Xm^BE0 zLJ~dH2J3s0MMv7%ZCK6lpsbJ@>-T#0fTe=WO~Roffu)EC#Gh?;)j>6+Hn&UUwCkl* zi$!n0U6Z*6!nGKepA){4)8=Auxni_!3dZkqGT#JguW4??)XQs3ANIOD*ID{qfdo5T z%{Kf+1((LB-$sxYNZD5u8ZWV4{NgJ_WG8k3GZ(uk1GB|z*yl$rQqhEmHak*jERNe@ z&im5#84}MX@eEd2m;w^BN{4>?SKyIV-hWKzpe|K+M&<>$uIIC(KUOgLiP)B{X~_<&>K*oWXgVkj8$+ zwsr+&Dlfk8tUGAZ4>mR2Km9;8ur>rud0AnB|Fiq0bykEr{S>!~4crQ5-GD!vSY83X zM6XG-^3UY3qo2&0c9M7%Bj+b$BkMh`!K_0a``b~a%^eDb=sSKV0`7|>-Kzm&Hca9q zBQkN*sxyiU)M85elZV=aU(+7ouzsQa^M(_~^E{sncRGfA=g}K;q4KBt+gEmCU}OTt zFyJSGHyXJTTWJZy|K_YSm{MYvNRt6adap@NM0K{(c@90e1AAS=AX`e>9SM{qn_aF32^%wMp~RDm3SjIp zvX}R>6HVo0)db@*xKHJ$OTDKR{2K(iWe^o_H>ZtxKcYWptIrMDWx`{3ocnwbB| zM7u`A(1!(S5Ps6(#6JucZ~6ZS5&m0=R(3CYO}?r+uOeRH`SED}BBau&V~-@dWHYIV z?b<#1&Xx`+J4O(`IC`UUv)NbS2AjPf+)Jrh|ngz3PXog0!9`I88=89cm;B zK5tBsni#xovZ=sai#qU)R-bGh@r={DTnExRw<_Kwvh;L*h8z6tHBGb)h;c_fBg>D= z??ipjDr#^qaDL-RX}%UERVRX%Z7$sF1p6IYlTQTG?_#;sf0&TH6AyZQFbXwV)<3uG zMfynh%hF#uGN6gtiX~-BLp`MYATZbwtm^s@wxf1OKLmJuv6Ufr{9i7)aT1};JJiZ4 z;R5l-ju|Uq4=s_r?vSp`lHM4Aq{d)rwI{?Q*kZ=y6m_yrhym6g62V_A#Iu{4GR6pT zu&IDI1-|hEPl64-fnLB@OnzI5J}A0R#Y+a$7``VFqxGFtcx=BF+AynF9@N2C|1M+S zOI#Tqbn=MtQIF}4t~vh`oe@gQyR_IIg(On&<%_NhO68~mraUm4*>g71uwd9`p@srBD`oa#OY}iB%EjGjej+Qw`s~uBuKb-lkJ+pS7oVN&e%4O7 z5V@V7`(4JdpPC_B3K+WSq2FVA4IN^{AQcf-NGD}!+Q=~ddw>3G z*c5U@?Bxms<51w|d0yKhreG!(T;iTOKXd{!mw(zP_42h}WFAN1Q&F-9e6HpyxF=RZ zywit|g3y)s9+nO4M6PKV;s-w*G^($|~C zzJplBFDr#vkr~#Zfi=#kbbfPzR$d#)FrVHo?y3Vg9h+V(q+)Jt1yEocXE&HDw+no| zfiE^+8+l8ozMAfx?1^?Bv%F9!}-I6(=)-K zaD^mEIE-L$Uk=1|ZS_FegxbAU>}eH^X#6Kj_2@a{#v)Kg#jB_$N3}D^_m`R7yEFY{>{1P-8(Ykf4Jx?hOMTi^OQNATXhT(5fs#G z2XV)g5lJ2iSu5IdQNgDhGDNtu2Jd?by!g($$MwOOqVXSojf!Yr35{HX`+TSNP%8Ew zK(7|lrDmgBziva{gK>sU|mt5NEHtENI<>KtcdKr;CE zO5Jcnnt}MXFx&LB!>UI7Jubry7cMyIP8Q-(9}=}9DVxLr*vIO=gW+L2!@`jamlx7&vHqynW`?jH`1>+kgT``{e8cC3E15bWK zf5`t5DEKNg?Siv`#$CBLcHt-X zYh0b(9^BI?AwnYC7tGdbz-2yqe{b#BS+pMP@YS3g`i2AQn@M6KpFGibut%chONr1V z61aGr=QvD`^Z7idhFNu+?6b7>F+%W`YK1D`8avCUb@S>6(ZRV!ha=bS=k~sd2tDfz z#!~?E)vrK918s`C%?@@Z*1rVkl+2@-MF*__>h}&sXMwSn5}<^z+sAu9R7R-5s;{uV zL?mWcoYckYRBpAq4DgeSYnrlZuuohwhV7FOW@3~wM~du%$ZI|1%20dZK`)hR)z|b&I^cJXA3W^t4 zDH@j>KwiDxzGziP0a0_>yIXjp^sR!{zfKw$DWAA5UZKac;P0L|5rneT(hA}}za$vL z--&Pay53;jhp^S&s3itNVl3i>{X~6F3!~Wgd3sfwjyf$YNakwta=eL@Pgt`=-_u#i z8p;SN_}a=*tJtpfS>b@}G|Tw$In?Kd&Vr;GNhte>;e)?eE6Q*$a?G-qO@N=dyPKg0hFddyo#NsxLJ8d0FQ&=EFr5>cO! zdD22y2L59r_W)D<^p-N#<|)0@%=(!p7E1tQ4$teh*YCNk;!e?3Q2W8TbWy2sv`i`i zc77{CI5=&5fuxMgXiUHziWK{GMR>NCt~FTujpW^dPWIe&JnY}!OLK2WAO9egu15vD zRCWvcm&E`{`aB*y1~cHe26WJPeCF~-T=pNfU%toBQkH&I z;BJoKiB6DGLVl2vj)(Q%ph1*yc%kVC_x7qrGZes7vZmf6Il737IK1-}RL&&PziEcC zZ$d_7u1-aTZ5-C;j-DRPzx?7am38)yTO6b6b@{(8Ogfu1!}#>d#nAdZGiohOZ+k@<&&b4*>8 zc!1~eXhLH7BePQJ?{gk6?BP{kpQyUEH2-d|M9gr5DOG`3rpsTnqLj*ntA(`ZWUXX@ zZ>S9`C5X26*m&(;gEOpETjht8^)-@1LR3>j&4`+3-;;59mLg(X9HEN7jQU8A#Apw; zrTO7-*}+s?EOx|GAFQP@`Bc^6rzS%a_GGT(Fdh>|8@nm}?pl}nt&R7?L7j_p=V4U@ zrt7!uZC4Au#el+4Dwh8#GT&E^S*jbz>tgri` z(2s}lAtv7=Y>)>&zSqr^@KIrW{PAq&PX3;$f>_w%wn*1LVM8pM_6=SQ&Gncur8Q?v zK!x9${-M3Jay*Ub=*E~ZNyXex!&2gr z7eRf%I%=7XEB=pX^|P|wEw7IZ)#zKt!rm_SFR(S6DR)u1{3rtibytu^AZCjQcrdR{LqgfY zJ6xJn^U+b`G7RkxB z{{_B)N;vvx?WuMnFRj~SX40~(vk@|N3zUST@X6>N*QnD^QX=jDs5a6*aS}*I)Z2hw zgsnaPso5X-QVf;p>|&C%1>nDhgWObM3%@e;m=W!XmCAg2MF5YG*vjcJm4cP68YyIi z@HCqhtBUT>*4&_>TkE6#$o2@lPIVVtNj%~NbKsE&crCVM-7Q;Lr76R#o?hUC_|f3( z+tCFC;Cx3!*_Zsbd~vRlG}YsO*|Fy#Y0R)gueUPt+XPa`k=9nLQs<>>rx@SL0VP}b z_AbbIkgNT5;$YS+RspU^mDDAOeC6%x-yk$bD$Z@ceW=h=CQB#Pqr0zrQhg`Oje$T*awt&HSsO}FD(E%^7Ri6j3w}DMvzuv-dU~^0-nW3GxQzQG{#V73QC(_gt3se+IO%8a5~cET z_`YD<418ur6u`Mr2gBIY>lY(Ddal=(;k44f%UPwcis1 zWl6>~Q_ep<)II!tw@Wb3pyb#umRXW5$J-mz?!IiFU09_$-_Y#vt!?p;u{n2c_-gI_gFKYKR8)8_qAx$%u5>RVcfa*u`Tg}^u-<)+!6L_0TRRzt#!Nu>jzG3+um zn;XzK!+*eVD{IbzfyawI$jXEs(LayU-4SL<4<12MWKrNV^jQP?cI)K8Eb{W$RvPxK^3RmfxU=c4%Y|WMdlT@{`UOz> z==9YrZoyYAE#!g%K&xrYl)2~F0%zb`sMBMn&V6R;;nVeVYDbOYh*^Uy`ng2Od$`u@ z>wJoy(}p$iSZFrVTXrvRVPE$OH|- z%DsEk8hI1ZgUIKLiw%p_m911H*Tl z4TmS{jdMZWV3u?eVs^I)TREE9lnT&vtXW zv!@ZvJtd7eed6tbW#XvC<5b@<5ja@xn78)>H4&^zUEIFELUDLd&gLX}Ozz<21ajyOeAGgSvVB-g5nL8KUsBD zUN_iLw#FTDk2wD`cx}%~X86U%GFc(s`!Mb2CvA0eU<8n6H?tL$mC_`_HBT;DuZ(J-(*kw3uq!R zp5F!5o+wwBDx#Wi#Y+8fJ@oYWXxgDUP|@JH#mApdz>|7v;4dy*IX09Y7WSK`@XS_9 z)g@2-ScLPl7^C0-B9{lhuoXdN8)CK;xk&}j`IkoC12ICMLNh#ym7YTaD4KQjKB-!G z`-B_~7-QLc4W|PjRy!6=HdQxm>2~|)UA^{28JKIy(kxt@c9xi<6|D+DSL_31fkQIL z$Y|@(*cAH2!II)q?3ovfP@xy&Jp0v{P>DNUGUP#KAy*qP0AMI(i7i1-5w__k6==@$aPc#`)lsX>`GOZA*{ zkX*M^Kq|959z%z}+%On}C`D3E;=-5X=sdtP0+&F(eM7&rrMg@iQIW>YSywPRS#+pu}v%eU*sQI)jp9|RyUw%b(?+e-{&TOSOAY~qvx-2taXDj`J z?X)Gmw~TOhZhY2nE&tO;W=%@hhYe48$g-Hks7yAe8 zF?k=1pPIz^wrYm_*Mw{P9~3&^--IctiLeVd-xqky9z(qhyJl}}%!Cgajh`2okMdEn z2#0DVF@`E{zp_~Zq-DIW)#a#=1UfDlJD2EE3`6HMsRl&>8YV1eZLUL!?YZUaKQ+vi z*k1>9Q-m8{=IxXn{*}NloR3e`;Da5}t1;AmtJ+bVU1M|)(cW%I7btlxS7=B1tljkx zSTXx&tS4M6rK$$!igw(pxkRX;Jx&RGRJY*XS*krO z5$j!oS$mf_C2%Bo4b-*r?p&dyOQOW+yKFx|3n zF9xjDaUA6jNnbFsy16ZX1i?s_T$jJ)$?NiTRQ#>iEXecOi0U!f^(XG`U0<5msK{nSUq8w|}4a3^g*cTDlwO*iuhz<6fpS8IOEiaze@ zu^IZwb@&}YatQOa*t_bEu%M7wA4wm+*#maTq~9ycj>QeNn6C=atF5zXG;V9JB@qL$ z20jToBO+U$dlf5oL{`1%*5svBI7KK&$@eFC?!@K2c)*UJcF;eHPr=1HU;1P+< zOf2f`(B3&fF0~tg-Q<6lCyDWGlm5^YqIz!k*sV@lim|BPROy zhMHA{F;JrwP)J9ffed2Pz~g75E~=LzUD$wR#m61u{sHR!f3#Gm6|s;1`0dTagjivT zn^k0u2C^BSOLq;c)Y!6YOUUIQ1_FkT=PX8fK)|2g%m%#KGU!9NXM$pw?N|jG@GrC5 zaNa#W*|R%a+}vlxRRI>4-2mTm$xC^DKCD%_)bKJw%h=G1y}lT!FZ^J}Oas$rQ488cFsy;^*o1|+7gz(y?`k1KS8COOd_hBd zYej?vHQsIG8Tr#tEvY(yLY-P<4=K}$GhXlb36x9Ogt@pd7CRhe zEKSzM-#-LLTvtBr(`C@cLChy>o#^tN1cy5b*#^-+D4hvEvaHzIP16W~S82)PwN3TX zk5D$PQhjUekTjB+0vbJ6DdrmgL>O^yM}G3W*p8v>h127{wYYZhpH$$tY{<4d(oY!4 z0lw^teOED>{kC7Y=*)R%W^Dee!jSd@8_sCY@k@Nv>{m~95?1#I>AW1T&ti7XkIblZ z5!_ucLA`skU^^^q>zIZ6;xRK|Vja(hzpBihsvGb6QUjf?ilJRPx3{8yg?*EVGif#5*6B0IRQUt*{>C zZ!`R=Q_)e$ohac_8=p_>@s>~Z)k}Yf?B_0?*HIX@DdQGz;Nu_GMn4Unc<8b|I-rra zAuHrLQs%U7e%|RDDViq?gmL2#HHkA%d{b_rK+q@4m+fSlQYt0&Z6c3~ZI?n2sDCM_ zMLeYlm4vA}X_BiTIkk33p<&`4Kz9HFiX#V8{T2*rV>>SiJH{jgQEmcC%AE2Jc|!ay z^DF95kU}S{&JRNQb8rd4d~1678}Z@EefnWZ-H%bPCN3>_LH`0MtrN!KmrY(9f~=oP zwzXQIRL5>0OJ`K^^&(D-2E!T)6H-CD6~xvUyCV}B4gA*(pbSuWlFS{kQGYvI|J&M}%SyTUj|;>OpM0RK z%{0(JDKE-0W-})oOuIk^`u2zcWBClj9#>4ug6HZby|z%Q^fY`e$w0XkToNY(SE8C- z(%+PH4oTfySL+wFJ!QKW-#IM<%k}`~;r#qF4@Qb&3f0pTz2R-H+2FcmP12vX)=ZxJ z)_JV(H~*{SVu)v@1*pxp<>Ev)avAC`L9n3${zeG;TRRtc;`lU673PI)a>&v-OU z0zMQn9mb!E;<)%F#q=L0gl|p8mXrg@U;5U9vhZH0WcV98Yyq*jZmK`TPPgDM*F9c5 zKpn^RW!bcH69n)kroaqo5yxt%iel4}TyJ0Bm4CYeoBtb2jc%N`f_m~MH_-VdfeW`#VRFs&42@^5WCAuBAlP25RHnl`@I zTZO(s-zne%Oj`%Y1|;%f=0Bfw<@*1eW(Kx0xgS{+q_zJhC77dGyuL5O9~h>e=Kkq+ zW(k`pP>l|lw4CW>L=SIFD&{V@m{}yBpsMv25YG;OW+xp9sh&i45S~~c>-b{OaYe%? zt`6_t8q%|%rH08y0Tvg9gb&+B+h*m*#o>!HUM$T{#HfJ;K^T;c1K zVek&UL@*8RBgTb`Z1PQK_DKwvsywxOix;*H9?2~!K;CgjS*CFD zZ3Px#Lk-wA41pkty7#hr1|AT`t|_KyjoVWNwRX7!0V8bfN5(RIgdtNE;8+a2QLKuR zGo_88fM0Vd2n;)$*~pDGioT%NI1PRAFv~j)?|NA5AHYnSsvm86=%a?%l- z+&T{6$yQvQ)}3xxD6?24TFoC%K<2f{QhFZeZy^Iq8X+keQ7R&nz-nlf=KX6Z-2bF7}GnI71BANHmps_?oD)RmFM9`T(cZ;}UQwsK zdZ!xg?mZ}oS6-5RT$FbA-)8y$airY8^j}BHSTdJhl(=?Dz00h~B-UECBXZ0N6AsF{ z^f7_YEpVEblQdhB5!v`yjn=1QYi|*oUe--Mf37JsgD~GlqzHNgl$%;EQankdjn8Vq zX-djGwcB3d+gw63ETa`0kcxL>rj@eeLX|lPI$f4cuzuq#tF*AWNMIWynJoqd(iIP65W@HU@X@8rxDwg{7iCR=FEj zwooL5!oywsjDt?+>9wdAO^SXYYQ0ykT8pM^91X53jwStE1FSldxmKNv*A4X%MoT@u zgJW#Tt`XpKD*JQoJ(%I`Fp02D()Jz-1WOFnA9LN%p)sSwk>zupqI0|bSfZg zy}i(`ChRxRgD}2N$`h)#w53K9QDhx!|9uDyKA>9Oy0T?B`Btohe(w64s}5cfiHrAq z*n@YF7YD8qoIMIwgq?+JVWq72PV3l%D=5y_uD%a(RB%sVdu{YzrUCy3jNCH$PfVu4 z-)%}|#kcpA?6+=f+3XD6!s^{wf(q2_93Cg*vnyAgeH!>HNBCYcBea2@UH=9&vS6@q zk!JQrst;MZm{(@xn2mNiob;>!=>MGLC6NyNJPl1lh0eqqF4lJ_@r}0pw2Z@L@n21B zk?d_2Db}iEro9VrXOC=^-acIN!L<#==L0d}PYHv6U^M^OZ)z%>+iB&(e3%glkCEYZ zo(FRITJhBIj@K!9cTw;1bY)h7g0nU1Jv<5qKaG;q{!T)VvUMh>@isG|pF_7-zLM5Q z1ZppJ5JFm;V?%d;QjQ9(>>P@#9e?21D>i%)j`HBH#HsaTtYG;PxrBx|RHGcl&*>Y1llC_GwkwG}mk`ovs1*AXpN zkDtT(Vh}YIcTm z;I711Nk}R14>*&1hx8kve9(p4_$M zi$#81Ay+3Z*O$3=L7FM;{OMZx;n#gGcao6ZTFGht1MmDWz93;R`^7^YwU8hk-KW!0 zX;er`=%MLFDe;o(L!33c6y|Dc@s(c1KrzRd<=ABYvADtT82+UUD=V@yF0ZXGyKbx1 znv4c#nOTFBdYMzL0gww3)!FJrch?URcXs}9)x>%H&k^PCTf%AO7P?nxIK;c0ghzw#nXp8QJCcGihgXRdrz)1kzGu{wQav!med;rmn7 z3!-WY+T-xHZ}f+)8~G>AeYGvLXPe{ZO>Ul>`VgmqaAs!VIYNw30%;Bm) zz}q-UQHJ3i;@dcx3bFC?QfIorytr5Mb1J7T<(QcpALFLV%o8G|#rjSazVVQ^fk~%_7V)^S423+=s*~u(% z_y+v={uE9%D2cW%k9c2Va_M%)HBC>u8VDzL90LxiXN)jnF^1oOTUA|gDWoHy47;wj zi876L$(jkjo!tn}m&hU9jr66mHn?kTBjnM$!)USs&pI+$tun>9{q5+rbak<;E&~8r zR`1fzfi_Cwigm2}^(ZRMRa?VsvmJM!;}RMP$XqsA8k|_O*eI1;sR{{@{)1`Vg;$n= zqlY}F12hg_Mj9#`4a1De8e7)1ie{^$=cnI5Hxu3oC4~7p*?;u-79LrkIF=FQ8K@RK ziSHfm9NWG882U3l3E zU9{pkXmzhOC7%3wXpznm2K(b&x%?jEx&?=7dNSi#Lp$Hu^Zc2wn~hHeBM z0Q_X9GBUtp7QcqE;Z_me2Ye6@3`ThG?4Ar&S+l{wO z%799<99Y$*zzQHAQ$lN4ZmS7Z-4}B!td8$z+?k++3rDmmi=*t*3?A={*MpN|w ze>i`B;q2grgYm_Dy!W)hFN2t?D1`X=j?#pLZV82ENizNET=f;RyxHNiIrr?ncw0cy z&<0*?n67yYLg|QjB5s;*_}lK}%0|}aG}cvpO`k7QM&pG^9bN7#jgLTk*g8rtQyMUh z42>yF=|Y|oyrLMz4&#e08ytowpD<|-LrBKS3Q-f%i>QPpdxQZ7Z`BOr#eTR}fnc5vNJUI}ll4|8XEU3wvW~+9^gfZvN zGNUadUSb}a=UU_;xxod-k-9y4j#NkX9Ui^mM=BOOik?2oa|;0mq1VL%>j~<=ZG3-? z-)D3OMN)TGd+it3V#iJ*I$Y%N4-Nr4UNc*9VRku&LFho07KTZKA8JTccNJM^}dGc*wO5#qsY4d43I#2z?S%;6p>rk)eiQn>#y@^N*{BpQv!2gh~Dv@ zhRT)lPak$YE+VQQNK~ z@eZswB+*+xsA{}*R>RgT2(x>%_-$Lui zw|m|lfHPt&&MUpgft{S=1bcPBKnVln1?sAO=3D+n;%8&# zpNeNi#mr6~c@ZQ~X84pnRSnNxFy*&;BHUai77#-p;YH@TAr}Sfa6_J5BmOmtR?pLs zvT5P>=(9&^AkUv!9fv2TOz5g-`4$151-g2@tY{X2I8Yj)q&?hI3od`OiYv zvVsjHIfN#|X`ngqPo}Pb4?hcG-1+8eB)GxEWb&irQ$)c;!tv$2AFa0`rXe9J!;|=j zDh!M6ycX;ZQf|%Br1F*=hq56shJeQM1T94$AbwGj*}F4~CeAEocg&kX^oD`gG=z~H z^5U8PYWfedCx0_DgUTWAP9JWO&Hv-)8&~+ZiwyLM&ddLo#_AWWj_nfBb6(=padGfk zs47-HiS@Jjz%m5`3zpQ)#Z*Hr)_fS`k0zQ(01*+Qa~|YBICcBCc_}uqA8t+8iYS!Th>U?a2ah6Cpd+ z@q(-IojZcO6N21YM&`0Wo!Xj&-J12r(BlJd?VM&YjZenUNcimKs3&7bnYOd1hr8dG z4qYc&pUP^c--vU?4g@k?7eg$+iB0X z{zOa_JZ7xiMJRD>`#DM10c`@0Vuo7#zI|FV7i?rEgh>wD-o$K+>`Wi)@^G7eCRx$U zAUv8Hu(G9Vg87S?oMt9&6pPdS1_b3n_@=kg3ht=c@@*mQc)`m2Xr1$qyk);3TLL~T zu9Mzf;z;s0h9TCzw3eu`K2tKW8*!4m9g37EV)E6pxi-uaS$4_)F<(___yIfWKkheD_iFS`39`oenIj!HS zb-O(4MwNQM|8|&YYxS#qfz3G9(v-Z;u)!4W$I zR`ibS)A{ODVE-`HO?`k0$z3#MCbd`w3(w?0odk+Gn$Dp0eNdN#P=1-2qe5+>D4|J- z+{0^?y)4B_Sk9j8%o~N6rnB!o*6aPf#5SJ>SeY}0$rRbFBY*eR(@0;OWV?8CpZvP@ zZzNwUoT4#brxI?R)M)_=Hx2Mmye{!1RCu$>7TVnjL+Qlc(VLsChd2o}6Pf7Ff_X;!Yp$~O8;S-Wb=Bdj=uFWm5bod=Mzc|G}g%}|E z6~GhVDcTMjG@SD9iy9Kc%$+?S*me6+E!}Vi8TRBs^ML}xx>?7(9%R4nVO0^_9ehwP zF)WDPJ0J6geEP90(hEPQ z?|k$lIDd?%U!|ipS6&SPvx%B9dMc9MwcLANyD?UvwyDEV#3bHXrunhxAEfE=5YaB{ ztd%Y0=N+YpF!!#=nlU5gAq}iGwel@UNQ38hM~zyd8}de8{YwmwIXDq_-|y%T8i~Cv z)pgT3JPbr+Lp4@9?3h+!s+aBeM3zE>w1lt)G}**?n{`E?r}5|NFzIWgGg=*s##}^{ zKd6-+98i#5c_6(a=vG)$i> z+B3pk;>i;0F-yDH2Z+t_B`;~)Osf@Gw)uxRs06IftkWg+@k(3 zM;IdHzuXwV8x9}-JyB)eU1d(_B!IEc0~OfP4=X3?7e&8?jnV-q1s%fchTDi{t%S{q zeaf`Zq;-Hk>Lb&B>DR75htN5ig-CZRZ_GMJI7@j4cB+Eh_MDLamuF%l0~6BLw|sEV zy1DJUi(W|%q!mTYp7+c8iKOQ>xBoccouv4A5#nRPx*8~Nz}!W3W~1FldqZQ}X@*LV zYh764Z$l=>w7=(*S!Y{1tn75o zTFcg!s)lK$JE&#DlOm%~fs&C8clX4KwsROK2wB(iWG+CC>^4cc!J1?B@2yOjLYx~I zH`wOZE!ToTzi-j97x`r=cj;1-rP-@;W7=RdMbPhqENQDtpx9`(xU#*979YevTdv*&hE%ws%_rd*+GCE@9;p2F-HeQ2T=dil1ASg^m^vXOjz zb6m0a;8G92J>Zv6KooDjSckX!JxPrRf29%}WPu<)wx2V+(-nLCpeP5Au|RkWlqBu* ztloah(&UW`4a?hWb)yu*^$05_;~x}Uz9gs_K6v>B$Ij4rIA86*2Y`f)J(#_X5L>cR zTYERH4_xzbA4u;=Z9Py+DqQaS>`_J9 zS>Ol!w+gBI&P0H{-Z-UKEW`KX7g=A-M1=PS1F-{bsR_Ziu_5TJb5-_5vlHoB$DuDe zq;zG6iywkxYprMrQiB=1?2XqpwrkehLKc!584ywQ@^k>>4~Rnr&?NK^tg0^_(Lm@= znl%*yr+?%#gz5)lVtHBJdBn^nax*KzgZ0X)BY7`IaPZYud-BGe{x8MT!%IfNX}|6* z$p@hOeI;43T2k0Os7dL24+nv<0GQRxS2h%{dCpx6Y-)id0&vjP2?pwSik z`rvx!;hsSXMLn{Z`=;y*)LNB0jGiTnGw+hPmB|0JL;bfLxwBwz$kn{{KEOWmbIf+< zjSh2N_bS^u87vQ z>1Cap)|#rCQ8#Pr0q7uQ^CdOrg7)3i;u|n;qef+z>sys1dHufJEN&aODJZ( zpXNK-10_+J_~7{+hJ%zoB>je5*R0}oiq%$TkYps^#@&>{6`6Y0Q4`S;4qLgrxM~hV zAZ2gQ^~)tp%1BXer+*N=iQXW=*L-)wS;g%QrL6Obg^X0rCf9-W<>&$ztvJK(nS&zu zqq2kUwZuLHG(Gx1WPNu$8~*xzVz+j+W>7kG*jmJ{QT=qF)uOg)tF;MYh16`Tg0_kp zq1sv%RP9Y|5^BVXAP9mWh$P0({oMP#uiw4*ul$|7-+4dJbIy6r@zgn}w;oiAkNs9# z7^_kT0_@Kgbz~Y;2m5D9v+o1p><5y~A+XS4bUjkhda^W2{}um}N}bMB!P@(^fj0?( zfy>|_k3+8y*3S_Z<#oZiOX|(_sR{mFc_uu!!j>0e#4*d`jD1hkG?N~jgtjUhb>tag=qo_VN&eh6eshf&AibZykW@i?D;dc zZKFkzlaa7j!&hEmVt-o80&uXp$g#$TOMNx25o`Pxl8;^8)Wc6??-m1n!$yO~N};92 zonze*(cB?*k_mBvs_wC&qs@ezzFe~vtx@jqh)=1Cn_48H zd6Ge3i_+nJCDG!Fh?~SL26G~05);l=OGtXeIp9)w;4akggfH#xg^zj!kG;nIM}ma2 zLmnX&3}(=Na`b6zn+MfV_7LPY&OIsw{&ny#0iN!e|2Q(P+2h&aBFv(P3Ya+|wtmpL z-C(d$9Q~JN_C0N-yLJ60KBCB+MPAA5+!qmOr=qaeC|5+-Qi9~ciwDS-D49C z6bzgYIt9<^U^jCKB8$2N-NiO@<(>EdtM-ez;_{k)nOs$&IRNz1&Fj2ySt1J?JEXH# zH66I-lH_~NW%uSx@9uWsJMviwUpjgpcT+R;REiFfe!!}xV!Xr^giffG&eqmXr|boz zt(qz-cV;n7+M&9qYbj}w_PRnq;j`FBoR-tlzEk&wO5EEX$Dabckd)BwQIq&~%PEat z{JB>6UKMArHZX79PK}hVB-z+XX@>u{kG^S1Oo@4z7a8}*d!61^%}%$rFeqnU zNrWgqK3f_iMfQAoSq*qB8;ISCGy;L9rw@vcql@0xQ|%B*Oaf63wPP~3V1AdFu4`!w zv?A}M<^LdiT@!4O*nw#uL4>_xOiLI8cHMxBGL!}fl>qpXXiS5+dNu?Qe~(e>>uBuD z=#MvQ%QjtKvly)s5-&Me?W8H0-LO-DW%abQtd;P7y02E9B^hJM4##<5sXyF0_kRSR zmN#B#xT94_iuq(db+12}lab^xed0h$er;q-!UqOHRu&ytjf~ihM*VsQe;GYzY`nwu zb0la@oG_jC1HGPXGo|_NTrsF8>5dI)gfiEbEwCDF$rwNOSGR>kC6MLeW`Q4w9xokJ zc@IaXO4<^Rqln(L?=Puiw>*QybL|~!U+ZPgJ2&1GZ`747k?P*5KtBeDLYa-5i+Q}l zju^i#J^ZK|{dOx6OA|gA^w7c792Vy;yxqnh!Sb*(>x;Jyb{&k`0XMaW^uPJo{%xmp zWyk!{|L@SJ!H|3qosV((u^;oCZAr2MTxmCg*)iy(Y3}~0Eq8;N zP$)oXTSLR_xwwWbryh*?9`1~b1tq@faBB5f7H{@8=X4vlT0v1|RdO+z(wu=HKll-S z3vaA>wq6cbqekRo54bwQ>UnJ1&1#U$Hd-e+b^d%U--*^|^DYcJH+3!i?n&th|6|0`^j&Ovl^w zGQ%B86|5(A=wlkfYU~M1!d0Uww@$voDNZI|OBtb3+jT}O63js;*g$|Lh^Kk45TA5s zDy7L9lCxH_TRbs+-);^{`{BXF^xi|SUz`~c%)Z+rcN{h3{f=p~vZ>2M7ESv(GD47At=EG;vufzgMiHJ;(} z&fy21k^`Z`x-&FQ75CCZTuCiEtZ{r#!g2$3fF%bVskK`SPu}Jv6gqWiCaNy3d3M^AI7VR1bvAk2Ijj{Fbx>5I*|(Bx-Dwvx;gsz3#Q}FuzU;l*z0p@Y9MI{RU+P~6g{0q+ zUZe?3*_nK{tRK@sixKO^Ev1IN-%&|9;D%TH55kyByzcVZ2!RKj@XB$$`w1cqYWcaG zYVcihqJ>%~PiA}2s9J}G@5pY$ zc`7L{Sf+9Vw7>Ei zeeeA)yVfvUw!$ACyeVS6KrYM_4n^s$DaQ|f4cB0Qfmf-xM^*eih4nF6(YE~R&Q3W9 z3I`o^d4#zef7`GTpA7SPP+Ya10GdAxKKmlUpwLZ|qQ(EVDLdM@)B8=rEs3=kLD$!- zW-CVr7FNbA^0hN235?8zrKR0L*Wo0zYIU`z;m6AaFJ3XCT`p#IEvNuiM4rU+&uFXb z=QVAEZxnfDIK*p|QtGY+ngBXofQq{3EIMC)mAJZ7_gmq2+NgK()?xU;zXe3)hz?mZQ4~@!ecQ3yhgB0kxQaWL^wSE^&qP@Nmxh*-?4RWu5MkNbh zf#)@kZN0Va!?}n>g$7N#xqB!_D=Bm4QPIK-TC?Vl(;UC4?46a|fXBRPPBS-HVpMgG z)K(81&~lT$xHm?971VN;MDi0}zmROk0~XGkH{2tJ*&avqSe$qiKx=sv1H3T;9|CZG za%#i4GNos;ZHR_mpr(j~r9uqfz0OD@O{S#GgBico_tveiLKgQ^!bclPYQ`)*%u%{E zn1&{c8O>yFcLA2SZJCgUw?2-y0Eb$|7dboRsMNP=iVy=+p}jlyUru>$N;${4D9j!X zb|j%nhU}oyBI{Y($hy{NvVlK(#T#wk7A-6i4ab#HEM4pxLvheWqtpkqp21Uc7>`@?c*gFF!O~Ms086xo=N_N-|T%`&5R)vFC=; zo34|Q5{TV4Kx_%|^>Kjih9UQ08~8INQa{D{) zc)Zz*YW4FKV!S!@`lgV+&)e=d*o&Oc3!N23dknv-tmk^Q_&Fr#Q7{;FT-t*oHYPXL zig0(j{6){LoPhca^E-cc-kD&dCP_8Dsdct7dbX6T$}^;2pZ_*G$JJJQ~X zK?^&QPEiT)Hh~=a~Bz$IlzDU3E z;2FA3jl?ZY>Fjp9PBO}O2`ERkYjvF?tu3f8U$$kR3at$Q7HVy=VOI-Z%f`yph0LNm zhB!O3d1Xd|g2%7xh{|NOYZ*WMfRsVABg)_W#10E`^|d{cz_aRqUKnl?*lL`TM=E@1 z!MFIvpCa6M8ojaV>GGmKs3tZl*XA1WbONO6khin;&2xInjOCIPqc&Qc96W)~e}>?%_f>#nk@F z(WCQIcdolE#`Z}zj~gMBSJ!Lj>n`T6>B_UqRIi2eM91rk=;uw%)v`}(4%k=a1y}-k zW#T1g;>6_Rjg|Y@G79)Uv}h&7X3nP2zSQ_Yt692cH@QT*$GKZZ)P*kpK1?&6+E>@x zaIR53CsRRpQEzt`mT}l?=1a?F^KvF$58F(Tq%&KDq-DN-&h$1UYm*5q{5cOZzdl{* z?_z4tm&u?zsf1V2r=lV?<9Uf*{a^=E1=c;00X&O zz40(l)h&&@jO>EhRAfP6`Htb1RPqoax%YzRedg1ajf~9&K{;2R&v181 zB6b1EK?q~&&rHWYLDQ0+m3JrsMV z_&8nWer3cC&I)?4NQ%i^s+O;rt}lJM?nMmx$GdSTHQZ!e`LCl6L%XsE7md6xDLi7T zY321zi?*>fygP3IOSfCE}WEdkP4R>63VGw=gG*2UkNCA?zdO z;5?161dme-BG(DJd3RCltcWHdb*I@JelukWpFiE((lw_1as8- zBKiGe{H-0dXt`Sk^7?jz_3>)u3#IaAAD>fFJQIgPtkI0ez)cCbb~ttRp>=3 z=HxCqm)XPwdv^*epou2>M}vRdXMK)~9e+D>Y9>TSbF0b6Bxy%HvaFk~7WM5ghg?+r z-!;Z4leuBUwOg$`+~?F&E%#_s89UowH)o$=!waUESf9A1m$!L4Iq0L9e-0(k8}Z7v zYTb#A8Xe+j5>rOeGz%6uw<&V)O|rG@=xtDTyAbocH64@)z7Zrd}3vCZ@AFCPxJ{cZNeHQ0KfCLBmSz?&eQ& z0VriwXSlR&tdWG#%s&`TnOB$F$fUed=~C?lHgV?r%NdkWw*nVlXe}y2d*i*mu=G7)PlKHm0$0j))p^^g%fG;&sl8Q! z8*v=))_1m(Q8V=>!3&Mvb(+z~;7CJ}sgR}m&RTlL)!$?*NWjG^HOz(BJC;KpRy$=q z&UFPr_Be3z(4)6>Maix^jgSk|;4kEik^H!-wVrhZZaGAJ{1KkC*a%4{_l&}z9_u|a zj&8sTU>@zvLOCZ`5(6vQNNb-R+)M)o7ZaFzUdn=PX%$|=0tu#fXpjB?OL2X=tudV$ zwg&HAIz85lZt3u1YieTjOgZ^(BPr@&7#sbTgZ!|4*n%=G zd_MigAoW~vx2Nv0#=E{i-X~w9d60b=L-vuiC9N_M!wB6CVw{@2B2UOY8#^ttEkPj- zDNsyYo{`dC-};*9bp0+ftnO+r_OOnKGgsfK_r8e(OT=0EIvDzP`AHBiCkb9KPOPza z+o>Nh+nP$z(6tRS%AqYg91I>+&Hl4=wERcuSm9egZV((+Rhe$7{CK_xyHVp79x(== zzYWOBuEOT{*z!b%aJSe?X{_AJ8N1N7{lau#$n8vWO8u#+ES?$>`;e}UT3{`ICBycp zA6A($NfagS`vh~e!it!!;66!UlSBT{!x`IgA)G_16)(6DA9SEUM zbYrEc!98tm9Dn+Rg)*_<78|t`PSsSV&}}+N5DBS?!KPnU2^}lWANQ)_uQWO{ZCNHd zNNG&G_1+<63H~KT;%5un$^GX&`gfl>8Y!B6+;xUaDc`u)e*t|{m&a}W4`A$sb&2x_ zF}pc-+~;!*t)9OK-WlO-qkk;db6q2*IKGATTvmX!?=Hx)T{G74-sWzWoC%dbZw5Z& zxv{KBt=zW72}{4-SOSI^20FpnHcU`%bsB9$wk5uzK`lU(z;FPQi;|#kFjk{~NqBoi z{2(J?D%2qZ30MZS7#Qk&`oK_3PU-SPXl|Ui@bTrXi=Nc-djBk!B!JYGaVg1v^ zJ=l2a(A&~p(yeluMyaAchrJE}4@LjR04Rl;SgM^3GMb`U;XclFBr{TYHF1DcYL{mGW<4R3@FFD1<5!esH+SLx5KSYM%#+R7;6y)TmP!*mVXt}P2U4){QxTJZKS zCy6zS^NCX;oXhO;xgvcy{5xpE-8a_a^trHOpm&8d7d8|&Bns^V#&CJ?1~%W!P|oltC~jh?K}5rbuU<_ zQ+CKa>S!WGZXMY7Mwjw%!Its>QWZB}zxZ8c?e5*Fo?Z#8eyoSrzX#lZOe4X$JhRiT z2WROp{pw>5Vc+YpCV!u3x92x1?l`ltJn-`38|dq)RzLJB^vnfcLv{`Q7nIB~aWoSA zmFlUvqO}z3Uj6%^nQ8$iH<5!gKy;^%UQCWu)knkJ$PS(nnE%N7k)u* zb)#%@ISRbSZ(nGYcd#q-hC#Zv15Fm_#eLoWJj4%5vC-^+xY%b=*NjnXwwBzFG5+KU z!XYKKCi7SIQ=3lzA6mSjch|kvg}i#7wT<+zla3d*ERf(;u)%N57u~Z%Indcbn$<@l zAL;rvjUF_)gzKT(-mV)BR_&8Sdh$tA*d7V|jJ{vLRfY}OtKFOBf#ajqN?^r2yLKFV z6D~Zku#av+v;9P(=*!s}ouSb9Q6pm^Jq7Gf;gnlZHchg6KW~wKhM*L;@dx&dK02sEeOV17`P&y{I)W?*VG{LIw%4Wc0WFTfbRP5Edkj4ze# zK_~BIY({V9bxx$cZiX$9=|FhtvycOyUbb+l_Q4i{DmpXa6dd%7FS-nMVEMv}r_p7t zjq&K!VHt?(dR6h4$=-Iqki-v&qsl7n&Tkj|-DfXmD%!rEQ(9~ut>>owJgb3?1}z9Y zeRpQjTKn$xJCAT9eIWpOlf`$x?4CxG<(ry73kN)(1_~xMYRbIw#7ie?@}aemKr;Nn znggy8QsIohap1%ej~Nj`H^bQK>OZW21!A}~Q!*DP1oUR=Q-9!qEvk4g|P4(U_ zB>T87^}^fSLF;Ehd{(QzaXIg{z#qI5&z-Irtk(Kf^`6iG*c4xSekq>JBxtZ=PQY^r zw}~fG8R=6*6+`VCNB*wDTDfj#&WAX>;M%d@Y4ptMv_>oGmIrZ7wsT&}on=u=J3zI>-k0(1x1m1fI#i@6*3QupJRx+E44^(GtBY zbRwz$6~28O zU=!V|^|So*YtV+5GEQLoTn}?aC|^{-V?CD`*XJA+jquvfM@p5Oy1p5Y#;wnrow$r} z)?YE>cD7r~)WFT+EgW<$8e*40DW5^guV_wJBu_XRIdZ}i&IChuxzSr- zQhAm_A}!nVuS5vY(h9a%8RrV%s6H~ z6XPRI9yZ6+I8p0g|Jk>De?2fnU2sMVTVkI;4>jrX&@f0LSZ{y_z`I_8l<4+bvHi|B z$I)-ih$~W#jnM)m;ygf!$WX_QbHm(WOh zW6j=+^%6o+_M`I-)KCwf@!)edP7^PA%R|5s$A%=UGy7+UByJcKx{6sL1Z`ZRc!9)(TjM^bPs1PQ_+2i@zoI z^xW{og5#n^C>jP01~H*YmZ4-6wOWGW4>s?gQFm`6U3MXA{b!A#AOE0#4=U6c?<0H| zHSfsQ@K7aoS@5xLJ}GJ$Y8 ze?z4F>U)mUkAGK6Vdda=gOe$>yq&&wMPb|bUdj)~;u_nCQkW07)Rm~)-{6?^ z2-Yr`ruR5uj$a6D=@QqM8aeyaW|+h`iI|^L7TiY-oa|_)&RNII$@rcau~0RIacu`_ z?Y+Go75HiY?7PiMt3^3O%$LxF0sLw!A|L>Duremd4r2_g?Cj?@QvdX(ZcJZmO@O@K z&Qy%8Qx0T0&VS3x4^09T)@jYkHm`OuhjAC6bB1WuC2sdIxF!eE-T?B%9mwmg1OITO zLjBv4_B@xr4zA3Zt(Z#n2T2k+h*(m&(+wQAVF`xAy-hlu)DYB9?uX4%^NCxw(%JnB z>#gm1UDD}BLZ(gxa))K+WSwul?IV**!m2HQ^(Non!(w_ z!2l-59VEoaBJcsbg@K9DE#Jr2auPM)(1BTboLP>)@>7{+ZC}0+*1h*I7VvT0RTKUw z%DV&&S&zk%Of0qn3irwiuSj0p`eQ!kull4`w7WHIARWDbzz{2ZRN ztfzaz^vm=KR80Mg+e|+cs?VN_4!a4~+YrC>M6WTj&Ggt0BADE~9tnW&!+UtrCTDM& z9wY4RG8H7Yj3mNPlE4JIu0P0)?l{TC^%I1lJ^Vk zkcD+p(0RJ51Mm<3U+1zOzegB|Y|@KY1^5dlWzpErF7JGJ_pdDRoOQeXszO`>ZKOHt zhj^x438!lAi8oIP%C?tL_9ZS#2lq=M>SqReF)3g}m)JeOuj!HrTX14Ik2TBFB$vrt zw;!9KiFZV?{fQjk`jaHbP8=369I)5Vv4oVx!&PHnbSh0j2eoZ=)fS#0ykSNSkjBN< zDmrMJZ$%JWA6_*Sn(g&`?j^fPyvGAN*<&W?5g=quS8yQ*$@K%46h9N>A3c=URhvR< zYy&|;IImqbzEQmhB1T`0olo4{0jwGPL71cc zGfRG{k!U+Njy&6%Uhd(2Yw5YtPh7b%Hvhr0gz^6UHdq#>tRN)?rF4xJd8=!u2rB1S z=uXx|)0WsYkT1H)f3+-eA}Vq-vh4Ni)RX#|=X6MYLmJ9!*PhO69|V5fB&4XT`<$HQ z5vsmvCxH@QbJE(A55ZOaKY zM8YEP+STm_BRg*d^Xq!|mi&}cP1i~SA-Mkd@ zAUGNv3)~mJbuJ2=f#YI|wPKopq|@EFVe2(r*nL2KIg?c!uh&m9m7n-u@J_j(8EfsXZ4w-fhcNgt{747^4ID2Jcsqbcx;>hz zVYQ1?ESw&) zZR+YVFd+mMyq!o)xw+&QYLR{^qb)x#==zilzcTE*;yChyxrtIbwmc|}M$Y2}FF3Dt ze%Q2iW>^J+_#+-j2J%ba+q<1Mv{C>6F?Gja3H2fKwgt&D|7fpH_rN7~TsOc>YR+um zaLkmcM|PgEh`E?&K=!+PfxovRsGFTfEzd9&f>LSmqF--_F{iE-p|GNDHwZTn8@wv9 z_y~^(+lfkBJZE5$#@=D}7U+REkMH)T?U9qH#dB}kmlenCRtVf1)YkN&DN-XB?)>o? zhh2*=>i`mX(-ihZ_)*SqX2?D&K6v#Q-mU!5BeQWzuYR$t{n#~{w_h@a-D>NQ?}t+} zp*ds1r`n?|Ve$h`Ct^$MJNyqz>+)Do#&_pj+4ZaLNXHaPJSKq2la*`ngYZu2ImT&! zs}4>+Ma=guafvwpj~zd6)1=``y)4+IaP0)VTW=Bv{kb`SOg;yE=)Vtpi zqRn*P6C4r%Ee!CiTI)vxuCnn0?G}l}AW!DwP;;)L8uT)~f~9S<2jQ*$`|-ZWOs2tC zNSOCFBr7)wuKvl&)^_w~I9us@D{Kp5@-l1eRbS535H+T?L9){;XSpBp{zeVT=*5z0 z|7H8S1QN@^$d(q-?ToXY1BJpy9S|kDYHBOIv=7{TWw3Pz&tywTJ-J5Gj^JCQg>_S! zF+HSEmSLjNUxA)E3dh|@qGlvH?Of)g38ofxg>+c)hRVa@1(17!_~6Y zS0Wd+O_Qz;pVu!V1R=t8U#HB%q~8n9^#`A8=lQdcA)QJ2ulwPZ!?twX$(o1D>+?MU zH{-;4S}x0hp;c`j0X{ZS2F7-OamFxbSO(sotT_%Lf#<>i8wS6{348nT8_%QulGIsD z`FxZJlOJ2Bjawcec0_W_KM>NV-g&Tt%sK2Gs z(#eK~`!ykOxN%YpuZ?;7>jTe7$c?(>*|N z{%CGJCn|H5LGNF7B`( z(%TajxK00e`&mNF-bcW-djy?lw{#e3q(AqX77vAmsOE;JhEcG{59ygR-ofYGHFeUB zZb99V*nwN*@e4Xlmd-wG2u)eXYorBgaLmG(S`(f*6|(NFHB{<&R*prP;wE|Y9Pp4g zJD&Kun0(6QuShcAJ%1S?+6rZ1ybv6#G4ZAB*ZLW{qNyCfUGS<+}FfqiFMG7 zh_3RTC`)57^wp`a_8xYhjcL5;;2>SYd1s){2lK{a-6X)AbQ14W3;%(-+$mkHrr~+c z6}xA;Pic}L%2ir(kiFkx4q=aVdze4xW-Rw{BXVZ7ceP(R^So(X`15C$U#;O}Z+W8kBJdN(6LT0> z*kIL4yX#&jAt=2k#?@`4QVEQvfR$L3cQAEG(cN`)^7qBg+2 zXgY~zi>5T0S{-T&we?YE? zeQvS!Co1#v^2oZ;_EOD^5}G=w*O7Z6!1>p`W8*9|>|Ghabu+*u3NXf5bq&MZp=eQa zlHrVCO2w80=pd&Ov=*zY0@tCG`;+sOZl)zcCO>~It@ZpWw2O|te{Cc14Y07O@X!7t zEe0_{R#28w*Z*{GA3PCNUU#L{$~Yv$!8R%?s?QVYF%>4)=3nr9sg86I_V~YTtIjc~6J=V+fpz=SCS8kSq3avPYaQGdwju%RqsoUFgn6patfiL>Wow+oAT)f3*oakkORzC7xf_#Z3+c@9=K{?}4^*AeSrO^In^D zR-z*sy<#4uZ0BBsaFw_g-pm=bSO%9W@MOzn9o zoyUW3j3n9$fe3l4vz2#&sE6x%_(`_>Dv%W=Q9S7F|(E{RB!V^0Uy%jH1F1mE(m&ee(wWy^;@Oh?zJ%{0|(OD&|f! zVg+tK23QBl`=-0y?SYt~Uuk0RcdmR)2$642V*PsWo({*YCca8*qB640TVf=$9rP!3 zv_{NyDb6t8S8sd#=JwMt+v?SH)bj>!aF>fzNom(DR&V$hrh}b05N=__li_MI zl_&=`Vx`1;hxrA3`;QYPN>E^hvkr5jWg^NgX|fH_-7o%TJnEUv zkwptFsXo~7X?w91dV>W6tjyKrDmeZQKUA}K3OJ`r$Ut*}4@;4^_r(^6yWHe85?VIZ z1K59GeOTf&`B-ELDgEEG%3-!E{1_|m*>>6`e!pnYeWEe!rfSl5+xDG)+$?b3;+A;? zNaZ($ubp>9aMEbeYqCv>x*_>PZ^BA2rpg?15gSz%h=(G zlIbkB>s@Pp6uniqtHZn~@bH`&@Kzt6C?C83d6^2fE+^>jw3Ak}Z+$#w=P&ZO63@+E z#JYJd#@}7hS_RJb?g8+fA)`Do)}C$Nnte?_4AaQ^^;GQ3n*>UlfNdG1(K6L5>U2s_ z$)%dR%%C_0)wDWk_OjsctyDt!_W+{m9pcC2sjW~irYZi1oWhb$gFcvS&&F`KIEq$m zWC-7w_EVtGnFv!AYu36_rN-4uyjJ}?=ZNZ&r3yt3t2~m*QVdI`F$e#wq6UV=`|=D* zqKbOcO~HZm5UH&}=Pa82^G;uXeuBif-1n9+fcp@ji>oY0CRJj69*%=kQbEJIoqX6o z11Gv+J|{Adn?@UAm>S+x@%YrIyxJjGYM1%a#qqbC&77cb5oHn=t3CU7O}vuo`Oebi zcv?y78~Z-S>NoZyOOPWnjL3i;$-Yl#3K3)10%6(^CWiI-5F37IMaoK8`B1&PvE zo6vpQ)fq9&fkb>z{S-2pIVHN*X4>y90{&fe$EJaIeqJbwi1GA536P0zdOUo-f|B4^ zP4=MRl26a^qlI#^=e0&2geP10DLIySLM4h;>Wc=Batyvx_K*H&DB;cj7rAnmgZlSi zVx0G`=~OrBhk`OC&S0O}GwRc^s<5w=QD&;vEW~);z&JkR-q`~?o1KF?RWkiCF0)Q! zy5iuPDS7cJl_vi%7$<+~>`TkYZgN`%X5A^w-`4DxbAuBdGi0YGs=)$YYf@lDvz673?^sg?bP@^{$ZdP#LhT2 zoZ=BdI76L&L$m1xpv4Fx22wQC&S}~Lv2P|0zi++xVYV!p zGEcay2s6X2XD!n12SgHe9$W@r=m4|KB+%bdp~Sc^=QnJd(=r&^!221@$p-wHXXz1C zPP=0k`g8xJUZBVJF?tI|VUyZFi=ibLE1Bgn5kjkO7FSu?r^&eq|TtfG&YRdOpYBi2U2CPaa%bc zpViT|r@$4RRMB0E{~68@U)1&$Xd7u~pXE{(gO`)2g)pk)D)Tu)E2}qEW=R1tI2yu; zxhQKE8}Uu&OXD5XuCna5K&i+xd7hwlh}#dJeryTdn$Qsr!QUAnKHXioJ8*iPEahC2 zdF+$rwg>4ueQcGBARI{go~$b}nwxMAl!z0YvwGKkcSv=30aj&ows{R;N!g=l#am5T z-ca9(6#{n1zGK-RME$WMDwR*wSR-MYchYCgN{*h-d65i8tD=3zpmcdb*#{56Jylwb zFQa;{R47nv|EFoxd=Tq>Ot~2akMBYF`IVAV=8uIGz1ENV?tO*_&XYN-PMv|~_hZ#Q%MnK+*Z{AyYuF=12GL+UqJvwpl= zb_})Y+;AIkA!cGa>j)!Co+x=JRM*Y_cJ!`RRIN8Hnn@VW{bpbR&?XIBF-ep92#n%4 z2{ObQ8h(NV%IWY?XaHfg@-DYE=gw$Rfg&C1tm~N^iq+yr%ws)nxoi)1Xi%dI!+U`3 zcRD-^w{33$94XP;j-1(6vl*Dc;T9QyCMhrT@}l(2sr%ERR)CLrt!r{R*!q0tXR{Au z_q1bQ>dt@Gte4=l1PeO6{tadfaOCo+`TEP^<@}r+-A}=0yeIW{=F*hO0+Tx70EiLO zcdQRYch8@wftj&N=X1tajJ{$7gv! z$%tHKi3QXo8QY%M&#|o9H3413<@`i#;J@(rkq$H_c}g?3;(M981)XNxUTg>6e9B&f zH&3-}17+|3=y$(ZW|m3KiM4U?P}xK5FXVBFfAK4M<4}@R0s`Y|uQXR+^2DCj1ONw+H3>BwP#|d4%{Wy~lQXu=Z?ohv^YRcfEA&KY&l`aLd2DiMLO$ zG@Yr@cNA<6WW8;BmQ>O~`q}air{yOo|jIH4gPa9wV5{*PJFhKDwW4zQmJb>>2uBi|mp~^+cf$6>n2tPLZdw^A_N!%I5L#AGKjq=RX!*^J2 zt%OWgswwL7kXIj+5KpQlrT8xSFm;iVaZ_fj1oidHtKe8imrW&M6*?Cm=0JF=goznb zaW;E5HbiW}SNF+g>u1fRpms+dpmst3ufXpB9KDu#nfU$ak|uc>4mr~WvE#y+YOpcJ zFT^uwMjMU#;p?0TpgX!*i)OU5|x>K9Yd|y&qzG+(^w-+ z=PlovbFsIO;0|VGjH~PGt<{X(41!zerh7c+7{zO-6##GxKq7-tF1oPpjfcf+tItco ztu?Y!o8K2_B&j0JM;QX?;JF5FbhjtVSXt>+D1yj@Bz~}gNj&_16-4V0|H28A`J?}8 zOpxd?`X&s!zopU>5l&_P+8bIqd)OkKouPcg&@hZ{`9JNY$q0DBrdZOD?0dY?jXxOI z{FZFDLt zF+DZr1qv*`&5VoH>e>j&lvtuxOPxw^kn(IHUU#j>zc;RFZv1CqcF0?ydE5cxv&x6zrWDs&lv_h!qxeE&(B-Xe zht4!1;!-dIRXXzR@byi{8Gm80umu*zAg5Nd%m7r74;*swjYpOzC=$>a}j zEOwcx`ftH^xYysWe(*L?TEwaxkhYRg4R_kNq$*_>GLW_zJF(9XK>*0x92Gr3RufxZ zgE7GU`v~zlVcqQD10}Joym&X1i1hM6`kmU8~}S}s?km;pm2kNC_KIM zlN7i|iQ}OzduZQEG)8wZ_4;iQyZ=YldpNTF_HF;MXM+x-Vz;HGM%5;?R+rHl(b`p` zRWmjrT188Z4x?gKTa*S>GsJ4eifD@%F=EAx5fQ}r<@(;&^ZcIsy8ebw&YbV_eH_Q@ zIQ*&3{wF{;!Exb$_@RGIeINc2$qQs@^{kUS zsAFR%Acm=}DrUDvmMosSs_cy})nZ{-0DunFEhvC-cOvcy9}SArOT;wZ(% z|;#b^O+=_@)`^|Jg0s zrut4^=%Rz_tI8FR<%(iNzzSI0#dm%bxl5zsM$j{IJl+{N-CFl<#RaQJdI_lzom>JYVLn(NC_nl zhLSg}oopqpqQqFzu!jgg1U+-1eY!pw4GaQb^M$KWXpPa6koldA@qSxh&#xRXEl7u* zuHk1OYx5Mx5I%bS`IB1#e$wc zL?$ejg9E~#>_biUhUfAwcb#%Ftb|Vbl*Dfcw(T`#pj&?7-fbIJ zO4S&@w#4edj<(wMw-y@TcGb_IGd==!HH?`~7M%cWJui|Q2%l(!0(|>qa2F+JXYs-n z**%tV7>|#d&gib&DMPmjR&qKMLtD0n@g$;&sNIIEH|i$9>8`?umzolQ@1_0qqP7!E zWqKs4b_^&)cdLk`JiE%A`s?QAe2~zCT*Ex)p7Pn1o$A513So`}P0wrw{b1|j1E%-) zH^$bva`4$mC5slt$KWQx?Yi&!dP|5}=M!?@bL3r{J$4k^pclIFq0n}20648T z`9lSBYJ4SN=xHJ>D(Xy6Wm&j};2ou2Rp4q z;0t?Tmrb6eD_1kIX|D_Ne^oVxe}lDO=Y-R=k93M7efX5gTxTJ6m){ggtP1iB&XN~g zLf60RM2WwJno&af>DX2faQ=gHQ${J-e^L-f=-P9nyj*W*xU)U+KI?&SXl{)H?RoFy z{K7)*EZv7`_Q&+%S*aP-*~Bj9bd6?l%*>ILMC4jNr<9}LtizOvOfwBC!-Nxollk_sqyHTyi?*Y#r~jV^Lw@yGEMxJ={pdnV%{(HoL{;A@=0+x4)YSxm zinRNTvTv<=(8&ti^>KHYlZlzWZ3!%4p0#%Lb-#X%vx|_JGgC{#IVTqX%4(5J77SaS zNYFkXg}YwOvA5fOO>v#~BCz$2%H4KeQlQr>nq0yo4@}8dM;8(JkMC+Wam!hls=oxs zxyOR%#3V?%hA3{ikB8Lo(S7TVyN7Mc_s^s9MQY2sA+6U{LY=CoTSMduXLx`k#=t^8 zhM<4Ug1siReDPAnyn(5xT*u+)wUEjF@U9^vur+I8ocuZN%*`efK;-lhct|l_L^vhM z2nHaX3QU*z(BlBFaGDsQX5~Iqxw*HQwD2KTO{c}m{0Y_<3h*Qt`r9P&f3o*U4vzhh zpA><0A|*Ob)GL{7)wo{@L^c9@mjKgao#C4e8uyXj5w=K*TxXJVpkr{qFajzUl&6}^ z=P!YZPv(;K(PZ@~)+{LSG8ll*;i0^B^`pFXK+~}JX{L8pUBPTdCo+TkiXM)vG@Z0m z>VMMo-F|$qHdIyVnRa)2N;LgIKp5i*2KIX%D~No@|6*^@Hym8BVKf(d$Hv~$Y;8CS zwnnxLH}4nA;Lcr z%$fFH_>P!Cy^uZfOQG`qY}laJw?f%T_sI64uPX3G4i}ZA!q9p6*!}R9231fcf%GAh z*N%PWs9ACWR=I2^q-|nstuRJh&;Bn@C*PNUq)&sdTcU6*x0tYI(oeqOTkD>jHM`a|>?0%qE5`o3LUj&R}z%bHQL8gZY2nYiL3sw6q zX5vkY*(Iau!s3LK`@F-PA|I>7vkry`=Ph%U^tq1wmT=l`pZ|TFlrzL^a)qxZeyYhSztIz1Gsuia<+3o20RhvQ!_ zY0Z{4{WiY6g92_TmH-5IxckWVb^=iYY}|C?;BPx$uDuN z^Ujh%*y=<7S#Cm9Z-daZ&U25yfQZn2x?lFP zd!6fksfeU_jEMVd=St+GI0ZoE=SU!o3ScrZF$S9LMa99i+li@HHlgWxzK0ztXLnMG#BZ|UZd)b^dATjaoP z?NubrqM`T=)Is3|{lvQlxLWL3*KAa?DH9{_x$YalRfqEpc5w4I&AFU^$z|l(;>R}? zrs7E#Y+HP)cf2!A#i*|7y`?|we%;bL%Rg0QH60<7g-zn!qTIZv-_(*ZF)fFo==Qcz zzo~3owFNfoMIGNY?@f+yU4p{C75)*qgwx79_2{H4`mOv~+$iBk2a&7OO}{lXFtvi1 zdv+tou%NmlQoX@}e73-8bPt`PRy9b9QryrB*e6`k-AP+)y#9{i9&fE12xTG(lEB|j z<+NX^*)@6~6qW-ggCSsfP-2%f0!$*hza|DU;|Mv*u2;BZ> z1d;XFa)yqEF_KKtycP6~Y{x+zp$Kg5Ww@mfASjp>sPB7NsU)y)hDt_^Eac032{s&P zOXzJJc0X2v6zh)ll6|Re;pQuzH{BYqYrLzuvK}ldk$(-QS;w>^&=ZwO5q*dUbp){w zJ6K@P1BIg@8e?d0ikw>s3<;|b{iM`YyUbJYmg0sd49fLFH=4td-g+pv2EIke>$uj# z7v#>iLsHWiO2wvb59u6A_O#8l_vKJDo9Mgm!-p$jtyof>hJFg{B6f!+S?|z#Y(j$p z5zvMQe6^eP^~VTVg5l>=O~}GOp88v0zI34GvfCVbourj?y{63qi*qnNw28XYR~_N3Jo%wccVU)$LJP9z7!BRN{4u3# z$06xPVCu@V`7+gZ2Kfg2U?q9&Ihv?YLNB5=NQJoa2%Rj2j_HxT} z|KVZmQ%hn}pZ+^Dbch)GXOPSs?TZoDe}(~vCUdp9v4JFAOa{7R?4|$#!ImB;`6G^| z^TdXh3!R7x;PWnFm+G{q_uAD6Ez%4Mh|VKmX@~)@`z(HyK?>M1^GpacSPsAQf^NAUDb{3@yPW3P#AnWD2SRTcfm-+4L+Ygk&f1+r5|{rV zN!=BH1A!NptDN&ciViptM%9{jK^E~C>6yX-n=7Xo@^tF=zd7EeE_tr^UmOm>(9jZ0 z(~XcOD;*~dtZJv@#F#@u_Bq-5`*Rng-vw68-<>iDjGS(k=V-Q`H@p-7A&S${>x~r2!MYN8$9IiF*Pwq~LFqw`bp{%<;BQ)V&C6|Uf0qM!`D=}^tKLik9>c*@G9<&s%y?bB%e+}_V( zP@RR)R7ul@pVI7_9hvlI?e|O_6+E)mMpgXMhy^vH#sL@$$9~YkN(9-Y5Wh?Uxe==C zPve5j5@Sxd<27M*JPj>sk~>lvfzdfXYf_+V<9m_U&bMmkk>ImubgfVyM=z2Y^37*s z>qY3rzi{Yq z(ir<`CP2ITffPjJ`pYENF!dbQ{*@ltR`}V*D6%TY^zN!_4^}bwj%h&sK-qJUf!MrE zOc-G3en{U~Q2WhltuGB?fIG&Ix^5?AId&w-JDT#M<>NvU<0x*QQ(miOr{c)HQ40I7 zLN>Aswd-DMiJf%!Pu< zCQ9}6EZV5)dCS|-AeQccRn{*Z6CKfOR^bE^B_cJv!DN=RZYM2$|0-R4Km>NLqgrkH zM>!4>7R}DtVy`C!ANx)wX0H$!iW%?z(BSx{`sM5Ks#&y+?RFX|oSu3wSt%mML^xce z+AIe>18t^7_wJ(u_(2q(+AFy^NWbga>tB9Y956#(Ld85?VLP6NPer%hR3Y^)(=XirQ;4}sO*wakvFQ>{!T_5BU4n#FuS$oO_n@@<&6#0iPpC4YC~SRCm%564<$PVK{g#U1ywhiQKjvf#Y!= zbZqZlS_7`Yhw`9=c2~udG!C*~$a^>aB(Y_JU}kfi3BKvZGqI^NA59h4&V)p=G+1f+ zo>+t_NGhB}MrppZh#?#At~=WK|NYy`jp&zW<}l?ekrbn3UXLgae+&P8<+(X-bAQst z??>UvbwF#1Ru6FgY-iNR$q(13xrxol!RG{sAW6O@MX9|>M!zvep9@I}i@@&XPMJ_Z zFLk!~20shzN9D!*922gJ`f9|sVYv@}<0b05p=GJi`GMMU02bbhOw4mVdTNK8IbClc zGjkKGDuF9XBaJ(0?B8CfjzQP4mOkb$%L{kB(q$&~7q98e03pL`*cC!|%PG}5s0K9k zaAu?F2U~NGJV^sXn%k+HzXf65mV0}=ncmr-5`;dgI{YRN!buISsCc*FUz5BKIr_d% z$@p*VT@$kst7X79{+~GPiswI%BR(biw;S-s0A&tu=@rZZ+hwM{cf@SLE=+jW;5dyvM7Dxc&(zEUMPs z7y2pZ=yv7}1;IPtI%?2Hp$LVaAHcc}IiKdb#^)XJFs&tph*i2Mv|hHMj0YND2@Aa+{G{h~7DndbWEJx)Y#JG4pkbjyK4( z;d4t8QmpeiQ@J+( zcy$ExSoY12ZJ(jp4Mb03qEeKh(X)mD&l)M|;%!X0IQJ;*FOf6IuI*MSea*E5^B0Jntmm2>8c0~7-bZ!`NlFv!qb6(iJoLS2KH z0ix}SL0uQ?#2FJ+{%x^KTmIDXk9F~duzVNHHDs%3cG)0<5f+q&OkC?xrmY_*{jQZBs!DfI@ znvVSOn6~wFZa);Ze+TtB+F+A-TWz@JA+&1hvE8;Q{^ZX4t!nYcVULD???0St{yQW6 z^w7lUq`wD0xgodvChSF^h%K&i+W�lWS5Z$!lA=ZaKIiq$4Os z*`-)pw@;T^${a!#LNyy-I7q6*UUbrmg@pwnA2Z@BapxCdBlwE zav-BHNC^1o{TU!3D8*&gRw$kIxv_;dZ8W_(RM6QqB7R!r0Y)nupQs(=FnnCL5ElEI{ zdPKGkFLMhKJtZAYw-2;>4^<-1;);F@mD3pPiGL)+BPX~Dh`QIEi+ z-kRqmW$6$6QQ%8+o$~#^LzoEVhP@%|6wTG?iYXgewERjn#_(6~nLJAM6PK#fF%YO| z4a#DpiHwRJq3-s!=V3R6G8_DxnOcha6Zrxj`1m~pPHOMCp7n>v-oJpf`0X>A~{m&#Z9!Z7-_W%yYBq3ZGrBQ&WQz^viVju~lX zC8w&jjcBaIuFX)aDq#*V17KV2VK4CT)Z9ulG#s3m8>*3z8ij^?k*<*3#W~Do%s&MKma<4{K=_8wK4IfMOfnOh+8!ylwOkS1L+!+d=J_^vv zp!Ev2J|MlP2k)1K=hT?XOh?{=zu}ApSKN-15A`zNh;qvRzTl~(h;z%r{4{amZy$I5 zU?p0zpe;>MGtA;jCEn5RyXV~C*CpY570gIkoZSkuA3L^jzG7tT({+0`xNBqWiQ5EE z*0Fp+2t4;Lb>31RGTPB+_%5f=xeUn$sWLV@R7wa-)!G?Y zTG1Sel2CnTn;U#WMEngXzlcmM2p>@QjpUev;vV`AD1s(=<2=V_rp0G}f}mmB6JY}n z;hXncz6BP4Kpg!Pt-A5jTlbqT!!9Z>fDRq7)V}`SpecLb8b9%1n#X`FE}jDiaM2^y0!aINOLQ!NYP*>vu{l< za-izoq(m7ga$7{brKB8%UfUOw31s`F`ZRL8BR}VDiKpG24TK{(ft5Ap&kb7oXIkdQ z@_ciGO`VN}4;lFN?~72Oc0I*A_l07Sq|w{#=-YQZQSq?NXbv1H41ywPNbF=gEY#=E z2or99HXndM( zqW*31`5(Z?{}uD7)exP3IfU$6+RVUAT?g}L*NM{%0!&VoPF8zq#g!>gtwk27u&4Nf zZt4mDXp3p#*3g~vJ&uJT2CP3`>~}uPP)iNRs-+O7`JZQu9tFjBhyiJH4U-nT`HnMZ z$OJC)I_`X9=96yr-%=f`g-_|Ui{wG@SC6I-I_2^ArOvdf>$F&KbtR+stKe@-ahrJ) z^HAPECT-t98zU8i>@0l%O4ZSV_<*#R?K>O=3{x?jz`&r&&T~n|+`PNl6t)3BzmI$@i6lvtdb3__bg81d_L39CoxEVy5LbSY-Y-zAb}!OA`ps?{{1 zWiHi)l_rD(dtrZqweN|-SK%uoB;w!9B9L#$pwCrL{}TLBQS@M2unK54z?hF|AFqmZ~3Rz~T9v^0aV zzI`}5|1{%y&bVyGk)~QA^Evy$T+rf&d`O9evmtOnXk=<$*8;gA8b*G|Z#HkYzh&6$`tiPqU=eY z|DY=UUG8uwZh$q<<{sTb5*;tM@SR%kcpMsDJ^EWOR;#1342me+aCEN9g+8q=%BjOQ zYJR=8CV`@iu3@RF$tDerEyIqahZ>&Jch~Rs%j=9x5Pf9Y%<5Y_^{+f|<>!_Sc~Qjs zLrX{vkE2Ft60c?uH}JW=+Yf?YOx^W^2i+`);=nRCthYL19QEmTxApb^#_QCwxrkCq ziPx#HtP2111aH`jJDwvJl{#CAqzJd0nB~+*GD|(ssqw|^~13!HC(66dt8?| zulbHrJG@DaoRyO$fBwC`{KiCXXS@7cK@DIXwVv@!0kNwCp^*<9_br#pBoFQ#lOKMD z8CLk5Fw)@E@7ys|Is}zB8?ybM>i)z*y3uX-;7*>AU_`d6kzRu1$Z?s@ItDt`_=CXdR#06Dr zq)Ev(>ig~i_Gw!L77(eGYzBbIAZou!P`|-nalhjyj~l*MJ(z@l+K5lYx{GEA!5u^|M)`k@j{#39nQv}p2STW|w93jsw88OA%A=bHkCOWD0reiKe zB*G05z-K}d5iYwC2atta#h0xh$YURf&n^#@UCrPwy9->q*;spJx^{5kz&TF_u%rc+ z$MfGi?b81-=J5finv<`GV$z@ZB)ggWF9Hw|U%)o9jW;#%Vvnq{C>>&x6m6ZR&z#-9 z?YiFqj6%{9JiY8fMa?75*ijqyiQeU!W%dkh#M|#M9-Mb;2((Tk{eZ_3w$vviF4bk@ zWZfUTtvz=>;P}cQnER)@LG-8n-iHbwTtcga>aiG+JlT=d=s#hWwF|7Y?BvHd`f8)n zH6P~X8%kAdgQ64dArbxS*Q+DxsE*R+Q#g;;&y_4c6l^{wDq8^kFT)=&YvXV~e*PT) za%H?~{a4cSCtlmo*GhF_xHICXK+{XvRxLV<5w@>MqX*HPoIt(HJ!6+ApZwV#_dXch}z0r;$CD}!enleG& z)Q+I_>5#R$KVe#s8qRCW%sZ8QL_E~)A$330k}zt|CF^8Zf*)8^8`QaNCf%pG=H_|z zdZJs(KtAjCW%%U+@H0&c-+?OPhc0*Uh6JwT=HY@eNiM;EQy9`bJTm`UJM>{m3OHaf z2>3k3asdR^YhT4pFI5Rk*Fp3pJec;h6hgyu@jI+)5&)I5gNsa^OhqEM7{w19!NIAh z=P;IyD*e|TTQ&AHJpYnSUe2W0cKJ2xn_d*CV_-pW^477-IJPSWBC z(;YrDOM*|MXhV+EF5LoPXHSyHs7+lyrN#_jM#IE61(mP(mdk+*;{)oqGf~aqx*(t) z5<7yUX%t`58o>X z1MF1ao^YHUj4#)=6bBHb4$rnED9cJNyS1uhV1i=_)YRVlec4?Rg6|tpCw!vu=I?)( z`Sw^zm|L_}T`!z1OvGK!Kkt-)d(1u#9F*VaRC{9GiL^ZU@i!l4QzArVisD;+E81l~ zkVjKJmw${D&Xb;g7pA5U32N~|7w@9pMdGHT#8nlQ#W3qDC)4}G{{z#MWc|-nV4cwI zMTWb*f!3^U2b5YkmGwIB5wx4qL#OJ6? zA^({O;#0y9t(ozno9e?WkZ<6J&Vz7@55-Sqae#1~+5MOTG_*~tFR6p7mRWH{G4zl9 z(WY};ZnFvpksuWM!Gbi&eNBuzL*{o=qFE0xj8b-YeyUE%+vkj=K}wrs)yU2cA94T&r@E< znCOqZ#=^v|%nkadk#K*YRbNABJqBPlvw zDX_$w^(~_!PD0@&QlXLWW>=yo*O|nPGpa}O-3l2nOH71#l0r#2(|ZfuaXiWYWe$l9 ze#BDWJNSFAwIctiGGj!5PcyInyjOc$=rE2LDBBcZ?%IxY}y{z z1H4bvU4i6@;paYQr!2Y;9nrUyHXeTziuP&Fo z4yi0)!SIYxv=|FE22$Z7VSjlAl>ZBy{0kHBgkYUd8b)LzKAzp{V52nlH<$lnsaEt&}B^)?*t zrwQ_lmhvC$NsHvMctDEx+A0Jw1y1Y5=9*-9H1)Dp!)w74UdfNIZ;a%P4Zm4I^dzf! zcGvVJ1>E9JX}TnxdJ38y7`a{>py%g8J0GM|`x*L0#9B27N&Pl^vZ?k^h1$$1ecPdV zO2@L_AKbkdF*O*^&{fa_#XEnGVes{Qht#`=obSO!YOJ49)Ju|Bn=MB3&T20=Ma5Jh zd47W-DdWE$=3)u|Lw5SVPm7^_wlc$d#xJA9nrd-@HAQ(zyzv2a*T=)^$#$`U9nlx* zS_iS`n)}LT_bWhJ;5$Ju=nkNvRiM64KwP__M~Gk{HE<$L>nB}hVP2_u&V47~LG0DZ z3-3bKuM2eDD9 zh>mM1-rVl-3@lb=i|OL_X?0iw+*>XP_);7i^F`fFg&Elk$`I0O3fDK;kV;P4gB?bh z+ubSrMRZ`-oKc?d%Fi_{PpD4`Kj7!Xn$w?%c@S@y!WKDFhsN*UJr&pU@$f9WUW1*x zLyrt0?1Vh7Vv%0;gy}#%yZ?#vZUGM@=S!K_PH=rMNbgTgSSzoJ7&6&!r169G;gh#E zsXaI7Y{~?6MvWkyXTYIm%hR!ymJ`=I-tqbGK}>m|gm$m3S5hixgJRj8XTxQ>4wEZz zw%#Hn3)Q~dQUFIw8q|pk#8O=gzUlV-pkN z<#(zu-`%B_8CW`ASw9+&xgdRfeywizw}`(kOWg>DY6H>G zX}%xo`ZpBjzv$Srdi^V;PFr%AoaN{hFsXhnwWzVbsyT)3t6LL;3SbUYB}6X;BQD-S z8AWdt=@zZj+o^q0NEKUSuSeI4QcpwbD^*ACX+13?ujc{=wYIe#7Ih#{=AI{+Yg+js ztq+0DXy^;P&ybine0&kiN!mvqoxP&h8F)g?d3+`!>}eFQUy{1#ODOSD9_Fliv!XRz zHHXei_M(1-^eR*len!9UGD=9<`7Mq|Ehin~MUy6o6sS>C;)+_&Pv(p=zN^S=yBp7> zc(eWOQ0ah1}?qBYpU(QmpkN|FxH z{LfEgDt9AtyTh>?kv=Hhiu&fUPOF@zik&_3Zcf#z;PaQO6GU`Hs~>y+s-%T);McdQ zWqA3z`_S0pty+Djr|ardR0-EV>uTN$Y<6c-_DM zb=siPfj{nWI>b9`Czq#55m*wup#`Wvlvd(n0yFI}XA3GfJv;Z*QxeG<#N;Rv9|;WL zT8!z&eY+rWvT~HTb|MV3d31><>v2dLe(QT+oT-!d)l;p`BF~hjg-826r}(dn6PpFT z!Z=9H4%wzpHLwjBJ)7%}yq1C|{TG9s{S7Em4<~UVTM_-m0-pq5BNiqr_V}|3ai^Le zxZCFME`%7Q@b`L))}OnDo>Qw^=50lmjZNo;h0XoC8i2d?ER~?|e52?TYrm>zFPErm zn_lJ))-O;=V9!JAs#cOvvjZhus1i9ksz;k$RdB)5wAup6@@C&@ce!0W;5c9EHJ78H z_C#3WTSuH}^Qob`t&N5><`Ku*v5$?4k`kCHL>ZLjhtibLcbp2k^zvHS@3VF_ioTQww3|te z%wP0j#Y53(7E-9&xR`$GOLtD&1rL0*7piZjIEHR|ToKc-?)v}s)cmVU}Y=ulK)*E6X9si1BM8_ZrGZm>BSgM~YuYv<6o; zmuD@V;Y7))K`H;>6iV<}*DlI4_?WDIhM-8_EHr#w0<~M!1sCKjm}kLgvgQkw{*h*O z=5)D~kdfRv6ppSnmfKzZZXs~d=+TIs8J<9ZcTOc6ODw2BxC+2z5Qv5bH0>x)kq@5aPYlAX#O5(&9_@j6Ti{~5$*@BG#I$V zx;KBk&F7BZX`qa^?KDoQ`BhBo#F(_R85JbfHZgMeL%nCuDubYHn7xAy%H59#QBKHp zfw7do*nz00t?T5-t;W}mM2cSKH|koD^YLUritHQ`&3zo|OPyej6&soIX&GiIhB?@v zHVG2z|MeDRDP>}~9^95SfL%Po9t#|@+Rq;y{~imh4?2MB8X383SG4JaDwPVF=mFz} zU!K>`-^c{{3d4dj_rG$Ohr};;y*#neKApGXs!!`6xei2 znc?Rs_w?73(vQWyIPkRBI^8&yro^)!eIZEu`MQ-VLiu^GpldZtTFk%!Ed8?OhJC{d}wo7ss!pPi@<{4yBu)Is&hM|D=R4dOFb zy;^4l1#ySJIP(Xuf66P9(3N&j-=f`oSV+pyU)En-oq;1zg8;ZQpN=myA63gWBOVc) z;CC%3+^p3eo61z_QLIItIQ^vgM>=Bp>hKSE>%-(O`{}A&kfy{stK`;kjs$qzDiL!Y7?9yWoV9SL^C?D5+=d<5Y2vH-;l7|7c(a&WDnPC z#FL3{{yG#ndvd+#rz#6Wo{&_DKlNFX)~B^v`VS8r1HQm44oiI3^cMSFj=Jw3U3CJl z4Pj?0 z!E~tFgO8LcKI4d0;G;X}u3p#L#IApNkJ1;$CuToxxl3sP)B71Y2@cQt1VasYe*n|M zft)AD?tQT8DmMJ1=@>BVg*U)bEPv?gYf)l%Lh|W9QI^<|1k3rbRPUF}jdZZYI=6E_ ztdxj5)I)4-JDG{4$dI%%C62TekvkLWHC%iRC+vY+;pon1Np2=O%4e*w3V{u+HYBaV zV5ea9G_8^kT@Q?6eGHu_F~Lsuj6T>0kh}qTWakZ8c)(Jl^Hzv@GoO@Y3h9}d!9n5X zlaIhxK0e1D(wt?ASL$;8omfHyZ>(+*h9v`9a?X!j&X7{`6Y8dqnw^+F{HUKu$%nW! z^{1pV^$MNTYf>ZT|cE6(!@)aFmjJy|7D-eybZDX;FNi%$J*_)%pHX ztB2?B;`=T8LVPTn_m%XVL02%jsXy2mQFVegYF4CX-}=kk47X{n^#b9Yerc)$yN{U3 z{O~(KyF}2p>|aNOKZDT4?U7@ogN@G;5Kf^NH_C6cj5|BdM3Q!ge9+KL%%+=aJTbhy$pXvScRBQPo0&XNWW)?SdaC2;YX@}Tv0=;$0)TuV!ngwJkQ$iL zw5kGi_HTmvKV=5BTB(R?_~;Pz^_twQOTOcRd%glkHHime@FAQT929X@;4JLfoK`8w zcj6J#dncApkE3d7;Vk5Z1-?19;5*=jd{aH%o#-%Go zY(}}{tIlpZLRmtN$Rp-m@5NxgoVmDxbu(p7`{vMrgE4(3Vv|!;z^e~RPqSy=>E3t=B6gi8PwVit z5?8CpRhd~?CQh5nocdQC=0UggNc@?>d9&bP)7@t~#dd)&$6u9CW88F5ouN2^2cs@4 z#JLoIfzl)|MXi=3!jj{{UyLE`x}#C$8LHpPz2ZY5Srn_L#cYQ+@)5z`%xn1wTv z-Yk<}C`<#(Lh7YpKWTX)$4 zwvq{0rY{x9mF0rYEKg3FKT76LZ>|*#B0IU>6Jf-KQnHFw{iEs7g38B}O`OAAN5r2% zvR^iBKRC8334I#*K6(P{sFnI(wm8euwdg}{hL)|_CU($rL~c1WmEM9#pMC`a*s{P` z+J_lj{>OA6fFOn$(`pEuBFVQN1wY-)!@Qe@1;4V_thw*j@XUs#Yvz61&Z_4ehi6!? z)L?`8(=x4}?|b}mqvk%Dko}@e4xxP{^{1Nn4!W?y#LMvo?TwzjPLi4+ls`=eF9dBR zj;cIU=nqZ_RHKEzSf(@n*JJi{Jtj3S#49%N0($jAo22~qZ%V?-aVO^&KKYR1Dta|D znHq1Mxf+^D4i`~$rWq@BQ=6AJm1m{7;LHjv3l}tM-&-zTKy!YHzY%KC?4pTk_x6HT z@nSyL&U#*w+_2=HQgpwQv4RFHig_XPr>=QNJ7R_{S8LEXa*J-=lNj-R$aoehp-pvy z{sShS!xpNMpr-Y!IC7CVGsy2MTJv|WWQ-s;f&{Mtfv zCAEH^K#-_19ym4%^U?i%Md3(B091#JNUuEhU44T<8&U6$PaY zcADw^`xEWk|L@WLU#4ozUq2(}%1;u}V%8kG6)wnj|2cp3363%*G;uXvlNN@gECYnn zd2~ZDBY_`P`q@jUw?$<@|F!272(YAE}m|Ode)fv0SLH4LEKq3U`qU zUw`9St3rYUkF`7;eG90nn6YmwMKfO&PThHE9^yLTDZZ+f5P?1B;h;Qm+3DTpo^nrI zUczI_i+9eGZdjxQW0RPq6`}@Gv}oQffP(Cj{#tjeY(c{%a8B3qEXcxuI{gVc9v4zk z6P#CQ_1H29Cf(Oy2+h?qMDKA;JyygYcG8@iISy@}eHefBv2}WHeEE-5y~IbU5HHna zW8tmkqt1FpcD*NVMAX3NYde-(p>#_>Eq^N$(;$<_oO4O0lfn}70|>y>~(Pfo#5&^M9#y?e9#OZNP?>0eR%VqUOkjZ-X-Ly^Ae!ut4xfREpz z?wq)KOqjGv+(dyzUR<0?jTz7y(`gM|TXUE~f!Aw?jM*IUBiqDk5}V|aDy+5r%X9KC zrGs@T?24axdj1jLf1OA#wSToUD4$@|1bB#>i-rIAo4Tl7sL+n&%T5EqS@qwwEK0^* ztGHrMEI7pYvww%W)mIDcGXD?^k+-<?UEID*xWa%QHQNCFd$ao6O-cSQhjxm->8+6)bCA#$FZlHGK07OhN-RLM7rW&t z3v2e+xVg4Jbz}dLpJNN&rfSoFK_7K@uZ#;hT=UxI?ehO-*1(ceAzFl65f@Mkw97#u z&ukzbr*v)t8<|QVqeZRW#E8YHV#}42z>VdTv??XLC`*amGd=)Cs3Npq^z~Yeex~*c z^h?G!=xz73lgz#mO%~KMK89a1BYsbcAuJkNA)}|a2=s)lhS#ilJaOOBKDdUBp~KC! zwle_uuG{@IkmBe?@)p?FZYwv72j;WJ8AB zkIpZPeNGKMOhNw&6G5+c*8U9!S6HHkX=n?2EqSNyHTV&soqRiICkVXdN!B1Nq0opc zI5FwKr&b!DdPpAO|*&kwPz$pg&c&)8zD87}Z)!up#|3y}a8lDnF2v#z`2;2WqA=rYjTfZ7aCj39@wYmR6b_6dcmX8AuO zshA0kn9;QcwRGBksc+nn;f**kl%hLx#+oB66EL~`Aa@GBfce^Mm{GCUF_Wu#@eN?F z1Zp)t^0N2o0-w7u+K;K>9^Dwq%Ve6KPge+b_&whk$>~qAyQG^y`QSLYWcRKXsy!g` z9V!+$k%5?c&{^RA)Vh=Sc@C#_>xGMB4@i3G_%!%nIS(EBaO&Dt=z6|nRDFXtN|t(j z>680NO@~&i{;7L;`uLsgx8_ffvs_O4Shh@5&a7_^%KA@}F&bmhi!}>EeXCK`{KAVc?OEW;=<4I~c8= zk~2W`;8Zb_YCLbam}?)EQTS#h)4QJqd~G9|;BT4qDD5@O-xw;v{|D8+6I%GYIzB5; zC{uk|=RIMPm2~;q=GgGfn%ifE7NiB`SnVd)@^drBzcY%S2Ly_B8fB~_3{=lsra(o) zf1cvZe_OeHGJpMpY~D3m(}jfRvI=1aiQ8F)T~a4XPFA{|G@j$Y9DS(EuK4V?sO6#- zqLB4jj%BwTZ#f6(Bab=p-yghpM0UGoZ*T>-^lfy0Exhw?imOI*KGG%W%6#B0u_KkrUFh@qe5vAEXcA8dY-Fh#d2n_i z3S#k%rGLFR;{PM-T>P2t|Nrk8IZKh8bwl0!KgW^6+_RC2mfh>=v} znB+J%tjuwf(;Q|ahhc0OW_Iw~=lXnqzu)b9UH`(~d%a%I!~OAi&==)jR<=6YDWCP| z-+Cthn)O8;%Kb~lJTHI!Bq@eY%r|ChB`_-g8Oq8n85H3AEwqMaao?#?v=t&FUHygX zKOS+Ld7C>W9_FfbG9;0nJGXRz4`OW;3P8lV_gV3?YEpgTeo7KK*cS;y;| z=t_oSejk{w2}-yYXXtj4M76011~Pp2EJF|0)28|l3olA!gQt2X;+tItl!^>qB1UK; zPETDzBG#4K{rB=*gt9bjzU8VI91zne4d#d*clze1{2~y0oANAWLsVnK**XZV6yP%V zUAEPA3ickH%*||?aZQXgSrf@qHf2~Ic1|`>9RH0tH(SD2l6V%arD<_EuQx9ovJKxw z6zNyP`G&ueT&7So+dd(?Ojv84VgNH#e#;61lXK_X24Xz>T=-yEFCG?O?Igxl3ngtB z5+JT)#g#5Q&86H$naX8!4q7J&2ji&wz|`nrt^jO=1+9XN`Lo}}4N6(NuMh7f*H8n> zJuSZyzVMvBM(bE_%F0K}SgG-?ZT=CI$nE;?$Bj!{G?Hq_Ce7)4Yc-i>g379_kHpXu zy}%7uTb|qmB|eTblI80szaI%&T6`)rMG#J#irC-~eqFh~{H^+c|0JmQM9Q>&yZCfU z-?VaL=}p(=ewj)1bH8U>lwF^KoAE-P4#~p{I(6Ab;PrI6D+uTQ=ya{d^4+ zi20P$;T%BAO@%f#-80%_6Ow>`5;SsZ58+(}>!(c`GH9YHQ0$E_3!E|sZuIqa!Yjm4 zoZ!)7-zz8K`L-~yR9!(%?iekDq>APKVZe-Q&9F=}0tB`bASg`DP}YVyxv5hwQsHHq zoCP=e2o!;EZ7BSdNcX!9aeA(+{4@>}^?tz)(>-gZ)PYPUqCzRj9iZLGtJa`W7Bez6+7BiyX1 zF|$5z5L4CY>I6qSx05lv$mr1I47G(LP7d~`VM?foGPzm9_4-Ha`d(rK4X*G?k%v$=;@R7l;iq4!AV8o0JeZ2o+eJu6cw)MO^tU-Xz-5*{#I0Iisu9!ZS7V^Av{!`pdf{$6e zh2H78!`h6KS?Eaed9}gm+0P;wUs0E^b>*wH?bRmI;oGq$dhZ|tOC}nfcLMO(E$UdE zbg#@q)L0C0p_gyV=@UaO0d6r47y!@f;?ED4$4>?Q? zNml~D+3E#v6qs+<_}Yi%?`=8M8l0c06)crUDEzOm^#2QQ(D(ma6=#oGDZbQ5kyQ{I z5t#mXIG5FyFBQ!)av9FiC_HxFtgV!RDmd}lj7Tn3vj!zckit}Xd6;@?S2beteAhj7 zoc%QiZpOkZeAq6chRo`lBp~Fv;L?S-4>d=!QkOw?UMcHQ{g$MIQ9dYgNTPm)xsJ=`QNo{V6voTS2}h77+YyQz z|0jfU;@y4TzFIzdX!E&bEt=iEGhwwS9bLjg1L`6sum!!|qXNrDGc2QZR#!IgT-Xyj zZQnAvnF0@v_RR`U@6{F@!+~1DH1}HM&NsSF5phdFfM1>%ZO6X?4|<7bnkdR=p)37o zUw0lW4TuXWx;uIUH?BN*Cr#d4jIlRJB!;Eg7xc*^ll|VZa6;)VsIQHyy}wt%sctp+ z7}HoVD@g?^oDNexfHm+FyNv#17$GzDU+^P%?!gtbEiA43)sd;*DA6wOn&%EZ@H|hZ)f=)Zx32v7n#oUsWx7^knZvpWerN#`=hJF9&0Q=b zENcxOeu#7`bbk6lJq|Xv*GWFv{7(nfoc%P8KHTuexn97e;)r}?&L&LmvX{e>!@W(d z5ZM^_>f9(~%ae8L1uSb)@ZthiE)e_OOl@kI`~HDugip^I>qu*O9H@>My+~=zZ-{Pl zCoG$yB+BW|NnPsBAlSTc^N|%`Ya_M{x4g#b37QT+_8b-D8bsDnb}$rR691Ua{1~uB z*ncdiz^; zUCys>L_ZA~3$qyzEii!6xzDY;Ex+kGwZ47O>(wiSLX__N_xR5nVbk)qx1y+7cV$Va$oI@!%eHk#Q^i52_IDI#oPvfLs?qB{kzzK1B@R=MeA5-L%5jSD;VqKg(7jXsvT8s!{(KxuN*c^SBQ zDp^f*dijh@eMi-Vr~cgfy{@zS8d^Nx>w!y(B1Fs%+;1!P%@CCnn?2whQMJj65*1ZZ zsxu}>J>SmN+`51H4-;_ic#OYnbihp~iI;XUJ-#)6_({*oAI)1>o!D|y*~rwhxFG`Q z&OMAV{Y)UWD!y^|Yf?*%ggrH?qE6`7c3$_e+&p-ym;QrFQ20@~Ge52&{7@Bd0k?`} zLCf8Te9?=nk%lf`E866iqeasT!OCLcf<<4*kKXYhwKnBbdsdjA44S0z;Fc#wQ2fn_ zNDU!G_X_y@0X_f9ij`3&^<(ONjWLSc;s`jSiVY%py-K!a@KVEehNv3>zc12&u4yg5 z@2-IN^DJWMBOGHiwL5~a+wt(j3PQ;K>VEL)um7li?p{0p_i*25TR|q+aw1{is|ChvBHm-QJw7sQ_uK#!mN*((>~jS#$`gs^@O@&M2;0CCY(sYtmUm2!g?)IoWxqB z9!_~3*?Qmh@!HH3X?C+Bg?7(k&TBK?_Pt@;W)s=h%Is%@MfhtR?>tlR`w3!&$4W56 zJLER#Xh?tahh`s3Yn;t1OlnzmzYBq+=_`JhY|aftqCYfg^8QsX2%QH z;E@3A=qANUa13+t)!faAVNMsew-z7fKR11TPN;~mZ3pSsmFP$;^QL~8ShhJC?V5T! z-)*6uw}A5W;@ny{Rum7)TS59tX)QQvIA(LNNIg_zgICpWZv^vd86XvdLvye2RA}BO z4^zLW|GlX19{Nv{8GGhD@4R>#-hm3}frP+y>nmdV;nxKsw4Ank_NkxVvz$v?%cId1 z8c>;Ls_46H`yrpe1jOnc$QrWe* zzbG313|(4Lu;^j3<^0rpGf?=)sktvU3?SdGFggHNqrnsz8dk-$(!)qUC~3gXbhxqJ zTju^YBDNN3K3+8E@RNj!PDxupt`4P9&7TD&Q%o&XBb60|Y272mcWKxwv-`i$+dk)0 z)kA$6VvE39-&hPU#ks0lJ*04@OdDGGexV*RcNod(9!UuSYjyLxj1MEW&j#K!wyl8u zd6uWQ1JohJeSgs{ZJRV7i*2p4CRo@_%rMg#5^%i^EbiVl3Y0E>O^JvfymSndt%3|7 zWF%QUUy=Al|2akay-9OMXD%APnp69RyvS~OrjS<{Gi!K7V$LbdkMUQMPwupllG%K+ z!?JywP%VF^V0kdk+v?YGaeLUl26&=}%D+zIiLZZ?a#^q8`($*s7gAPKX(Q&LY`#*M zj!#X1oNW8@m*ML+1nB7PeOL0;QUYzhB=u+vB?^Y6DvtpZn(lTyeOt9z$+uuaKhN5b za1bcEC;b)&`6XyJBow4k+xEV48eQI>7&m~Q+AM1a4s;l&EEmgaHNuAXLkIS%p&m4o z7T^C|HOhpXk?gwpp>fs{WCGT(f93|I6_fa3%RY?Sez%0a%+u7tLEsCALwo(|&IoR6Q<3qdT6ASMWY9gGER*$X(D`arFqp8Z}=^SrOL4`l}!F zMLrMPg)2?YHX~}gG``5fo%KLMAaP!oU~;)6ndu=#ky?SJqf&h-Jh=#Tec9n?RQ(TM zgs<`BmjC6@?4&cdkW_BkjiX_2#H(=Ji4AgWU<6oMw@|h-7U*8>XOruw(z#qdM7Y8} zglzV9!*yHARHM`gWj7P@c1Ijkf-IAv=vyZrUKfdW#03cr?@JkWnA<1gRAjOJ(!8hS z$L-GoE8CE=X_`&+j-}QwUn}qwj=D;3f&IYXe;N|e;k}}~(Y`tUH=fY1+_T2P{cEvlM{Q* zE{Dwws(p3|c~n~FyTd!s67^b{>*<55WD?w(&xSk)Mdckv%ie0(3aF@9V$pYPgm}%3 z+vzJKIn%S|sIZTY%CgtcV`B}S%L*(3UC5NtHa_ef{-0}>`w#wZC^ZoVaH={eVUDWg z`6o;AT@og2Nr|kd&94v@q>6WfEU)bp(d0R>8!t!+;S)qd|D6Puds zyLB{le&x?x%&5C`^Cft&Y%*$b@8`bRaA^9U6c5+ABSIwmNB7Gl5h7J3x-ISN`gn*aqSJR&?I(iS z|5~w?(wTYVL(@-~<}=@gF+CJXpy$NNqJF`7F!S6fQ|7keQMfT>-+R!+3#muG`8z6) z)IdrHEc~apjk$BRwQd@vYBB`Z)`(uW=&u+GUh<#P*ryXE|KXT~J7=!v;@=IZ3U;R( zW@tsU+?B8C&^IGK$&8NiJ}p^Pvh%?eZbEMOWu-VN-FH&vI3VBHg>d|08h|gJK1?|K zHukB?vB#!It_bh(e356H1Dfb?%4^x)er-NS3|Gq>glF(3hq7?KEx}XY2QTd+8A#@PU9^enB!_1!{CGHMdg}KB<@ZAe!QZS_dbyBEZDP@bn#|?wKi2 zv>#5g>*Y@&wP^dd3z3$mPoseyYe9L((=HwcgIJS|sFwM~ZFoo)eUY5Qi#O@L!i^^546TG*<&faam{62@kz!ytU|2{hi6OI*9EJ z&jfHCYkK4B5sewS^_Uf@$4Y}+a`N|dpGNeIyhYeR^o&EEwTQl`7tU?(TFHquF>81; zyejqE|K6VlU4dxX<&=hgi0Vw1zVTXYlOp=K1hGNU9zc6KVn``-^dDu;g=G-!%t~q8tb$AM4PT)g<7$u zyqs)gt6*umF>y))<8VQI971em_ae>C)$iJZyB{shMYb5NrR96clvo%8HpbbvG7Ns{ zioa?YI~PP}FY1Ln&wq4Ggmro5#MrJZ%k1MevfMH(xvPt6sz~9{wgx&?Zk`wjbK&#` z_ewD-LN`=XMi_~KWBW~S`S1$%lFZL7rd#G;tXM~)#*Zgb=iT4;ebIPcP{~aE^>L5P zKx&9WJ&@WA3ZnBTElw+z0nCY-RD&An>Ji@FT?qE=0)y>P!d*2gZwt}BiT5izy})3& zQyUmBn}}tLo20i>A>i={8G0AJ)~eTV%{v;eA&KXEnB2c)M#?v}3U_-KzgPL_&fcrd zn@{rU2F6RLFOE7MIYbGbauqSB6z^u_&iy)VM7B7AMPwP*+%q2^9L_H(M%|s+cVmUv zQ#;jp&z=F?ph1hT-0ul@(#b5HZFbumz%?wHbXgv&O$;X-WhkqWoJyjf`2+mgyH`1D zD(*XF7~P~IbF+em>1Y6BXQS55Z21+j8&Tn-S) zIk7)DtLllQ08&Ub9Bx0a{5JBQ>8_TXaZ;;R#x(EKH;TZ--LX6O3i>J=>-&{&d$46B zeBf%Id7^1l>uRCJY_gh%La6LZE4-r~1Q|H6^bG}7>{6%z?Nzl?^VHVZy4)Ao zR%+O~_J#!V0mog;yiK+{xNRHlEZOpq%x+srQ9S!X%>ChWf zk@r$Ul(yyZps_)#ALU zoqxijm>Ry1`+2}$x;UBK>o$04#uV&bM{Ef_R=Kr`7FyW=2Sc1zIN3~>z=}{hQQm; zwZrfz#nRzkA#=1L@`wZPliF=*fqmnZML`<4oGE>}qp53W`B4B zIn3O){rSOMw^cZShZ96meMK2zO&hPgG{F>8%Cyx%OPLkAZC>jZ--vhIA`s+cwJ8`= z-Es@==9ccki5jA53+JyJ<-tWqQNEjga-__MNVD>TS>bc&cLr#IZ@eX_+lsRO>P z!K?q*fnxmkKt*VcBBvX z&Woc}Aut)c4|f}W$lG`=F={`kB-LGnBAQy&9xh;HGBC{ua>lW6&2H-?C5t`^_#srx z4H9)KO390Jj30jV%y6=Nxd%2qTmUbRs+5SqVl0~il*B*j8_wfaRDx8;z2=t~w+PI6 zoG7oN$5A|#)J~b)oi)8_>Uohk+1Iy)2~U{rJL)tAtY5!2znpX7)Svt*O|3Je|?LuEd_ ze_7~;asB7z5&>!hD*Jf+whe6d7^dnu$;lbl2Rqd%=L&T%S^kG9%2QY!s!6Y(&om!< zZ@03wp)!fFEia9z2-ugzsrH(>h&^%qUK-@t-kh)}yYo?T8tr-y8?9rv1c8-yex2B$ zfZ)V+7jFxheN94cLoVTj+9ww_Ab$CR9Rs`JYD^g#T9}-OSi9e+;pryIqp??-nsNG; zjItvXwwLSEW^HBN#W9{1L&M;*Hw>#nUkhi13k;^#07vhkt-p8f3rB+v6_VaNZrlC= zu(M4Cs_UKzkz5!X(4n*u*WNj}P{v??^eV_l@ZE;#T$3AD)uI6dW@o5Uko>sFezq}F zI5Reiql-6=t~HQVn&*AvZ=lACxMvU1l*I;k&>`o^ zLm`OUKqX`vhl))anef~}&+qnN|=0lT* z6*#$B)iw68GR|`m)q56~gbR}znk`8Qn)d)4(gR>%8qcW<4W+ulZ6oX7nk~bX9c1?@ zhh7hRQT<>%H8W> zibMs1D%ZU0A6{77NRZ`xK$~l=Ei0ghqkf*d_I?N|@3FBr>q{b*8pl|A*AqmbR)F-9}?W74ZV#ak8 zYGHuxFqnn5*r|8rvkwdNWPhJ^(GC5C?Ip{e8E1Sw=DMFCg=J~`#C6hPVhpPHGGkpe z6e>zi*HPns0(?7~3LoTwK1t13UxK-W*A~i9Zg@Wyg6>S3g>VbZic4ARMhP`TLTx1JikEydTj=$Rt;=^mIeZ;`(7S@ zqbY=bb$$NDn*CJAh5hejZuS=RtdiV&6rp5@3v-#KI$@N%ZWpDqMOlX~S-ieckkZ`{ zU9$7F!gpjN=dAH8SY{I$o+zc~HZcdrCMd8n2+}{9X}wYgr-(#Vu3*5bnKvz$;8P^j zqm;?8M7zm$EVtf3jq4b=Gt+_>$hwGlw_}kL@6_7TW7?K794|FS_X?FTVB7srH-2!N zPnMQPSOQx+qqhkU*h$I3j_2Ulbe!Pv-C#+%<9jLo_62=lk5RzSdqr9EbFDQe+}L;k zVVdacGJYGUG}a=TI(;}VKjY0q6A-tm zb@&Xx7A+y*e{KP&IinUwe(Qc`rF=$c=Q=lMt!P+dmcoN-+#t%Uj4I-7)|6J5f&*%<8UumEL@9lTf|gIop?xtj4BG~)n!u0 zh3CP`p5o^Cb?+%aX+lILwuw(6`8N~+n#^{&4^*MyaO~+)Ne=C8H(ixu4~i5_F}avP zul%rjTHipC4-Zbt$yke0$jXt6?9kd!e;&4Yu z%XEnamK$ZPWcE>!6#>()_nqkKkxq&DbpR zpjXr(PVk(+IUmODo;1FCaE};~V^kBr+hTwPajVtvlIaFJsGQDWQ^_jt#wk&I2&AgE z^1@t95Uk?_0BtYX z0=&~IFeak8l3RHdD;$zkMw}9vs9SCcuG2JIt_)aL{R?61=&f`t5EH6}ry?AqigwrH zA4G^*gOyOofJ#j{PY3&!WnbJ9^7NlpV@nEj4h60J{yezs+Zf-M1FSXBV2?~&$zNd7 zvKF`1^S2IEXf@>LE@lD<;sZ^Rsd+a1{8aD;*?J_?wQgv--!w1hd*%;?+wVZJNdi7( zrqUD%plX_UMND(nK${gAb<9n=sn31ugv)gG`IAHkw+(}xEl|soQ?r|qN+HDV*5H8P z9;8QI%K8lp`Zgcu;b-8+PuB1bD58d|x@}$AAr5L#u7(#X70(nrTEQAk{5H1{B@*_2 zHtGd43eY_+-v)KCYC!KMWj3Zss!r##els2wEc{fbvhOE)`$m_aWy3swbv$4Y@a|^Y$ zoq|#i{s@410&AD8%_P%^A;s&9?VG1(ejK*R!bXM9LmFyeeeAJXX5jc{js1``Wm&w1 z1p&~AKk7gdubRMyZn={=O#9h_=7A{V_Wh2%-rZsPKIo_&*Xr^KeW_>RkQ}fl8St~W zec61_Bb*UoOa`+p3&E82sO>PPTALc3!`?MOB>=}iOi4&JD91L}3eel|`m#%*{Va7W zpH`sMx(O!q9CibZT3UIpAVC-K9>+o>mGQ{cWtZWiCa@n_4LweGC)`0JdHCZOB%J+e z#7TQbL~@8+1;5;X8TAbF83V8w$1!_#b6GSVS~4Cbc`YUS5@>!^?RsL!2oL5qrHWnE zy94$mSa#h2BH70(yyGt9CpQJ)thS%kvQ$5)=O!XOz|)l;@-G=kq+&RzlYtuip9aS* z>wkG_%&9 z%j)>{(NKRu;yzr+*5;t=lSKC7Y1{qpW*clK_U&$ZPSK9i!b(|B8iFc2^TX)2Av&{_i;jiPPr}v>l^|WfJ!L6GsUaQZ}`HM_&snT36e{1V$d2x}VO0-Lg$9 zEI;AkM!8Y(*=9lC1w)5mFtJd) zk&Q0;L=@!<%BBe>vuYM>QA1Ocq7WLJyQq7F^o*?mB-xu5PsZz8md(d})zGPixtKfx zz-lR@>I297AruCq9I52m=9GAx!@pHw_h4+f!o@qwm^9%O5>DGqUP&M=I)SrD^;3}0<{<}Wq| zZ(f`oIh7GTK)EmDDHf8WC(%s{%YA`xRN45J6uD^}?RCl6N=4v?!i!T(dr`9OV7}YF zk>PJtn|xp8nhuQ=nDaw-kp(9q$#bqD3AFS*T5Gx0&)GGareB1}H;yz&({$$MKZz=g zs@VrXI}iA-QBbh@%?YJlZj~QzTD1#{*X@d=8}Y`;3iK9$vlWf3cV@k-w_;*a=p zthnrdYVgXfzVW^V10R$RTV=I(Y3Y6e7}ufAg!k@0CwVvx_JBEtH}4F`=8k_?z@qD-2E>#DfBuUo~AHl1oKV{F!?Jzzu)%2&|L zE&WPQ`#MD@Odo8vv)1L2<}JJ^cP1&wVkmf$6Cl_!@kk^i>Ph$ytFJgEw3r{pNYUXn zsS++QAN`^OSRXnQNjkTr(*miZZwO%MV~Q^uOV~}%emPbEFjN;!H^AI%KdrYxA={}t zX%eE!*i)c7wUD}IObz*I*~+*Uu$%+%n9L1HJhAr1{)PAl939N6qi?r7JdmH?c6WF? zVSc=6&b%GDYB%|nhtmD4g=HC`ANwk+L{&s@lx1FEtQkG%sN|XZX!mPS6v=8xWOBf! z_(zMDuya6Zf?uk~$ym0_t_mMi?bDR)4T}dMSq#`SQ@8gBqV`mZQxYh(Dufg~h_&;} z6IMk=jSWUAvamNvoV=QAKI_}2siR~M6EYHzN>^ntYwVwz>k_>|JH}|jdCQ+ahq*X^ zfI0o25xnJS;hO)X?46s@t%=H;d1YYRzOB5yN1|C^Wzi(DFxO2v&oHBs1b(w6sOsDn zd+ym2Gpp@8X4cIhS=lkv_zq5*i%WK4-(og{jQI`?q}^~beFUy?9Oguyh!_R(h$PC~ z(vzKv)O?_LE7r-|Z?7}<>h345ieV9}Vm`o8rs1A z9#x&X*t!ljRln7tTWeT3LF4;_!Qd`u!D4yEi5_!5qmuTK;t@{&!krQ%T6$J`EcLb( zN6Ua-tyVok4z8gT(8MyscEPZ$+KJE2!E{V^G73MPP}fSaed4!IQTSnXQZBnS#kc1$ z@*|)p3V2fcJtzwlC%9|1B;7j~znl5sF)1*UzJ$b=;;M4#9tg?7C$sJ?%(_?g5AqI* zP9C^2wRX5d_&o76LUP;gfZ3cNa}SEC{NqG#rrDYi4!dakuiK5B{WtZJc3^66P3eH{ z@YFJ=;x-=X_;Eo z^EMX^l2e3hx1hRZE4>jWIRPV|SJr7jj~-T+S#6j&iEPWCD@mz*S{FzOiaszcLs~~!wA4TTn%#Pbvbdl`l}+TvD@dddXuM{?nbTm!m(y7FREib_gw zcmkt}bSdfdBIy#$GMb@UoICUL7dhLd@sC~b5Mke1=9{QjqCbxqQxS5gY>Y`xc5d7N zeeEwdTr7;1Yx#sCo`3p|)gAA!5BYytAst;dLYk9^WZ@t+{I6X%=@W+c4!I0OSe^6I z#V}8jz4s^ehYw;l=)?|=`J}_5{G!T=^XzAjUVoS9#Hnr7kP3sZHxOa1`ao<`RJ>VE zL1V{;ndDUWP*+x$`OBGKq7liEJV@uznq_*^{G@YAKzPYPD0{+cUO04o78=J_5=E&p zljkJKbe@G?a5JoLbMdEBFY|G4Aub)LeHBxq+P?E1H1w~zD7fT5E)=pKwM1VF=UNGT zeeQgu(I<8LeC_D`?BXntyueTBNm~|v zzVkOXdpfV(`B%(%M=RfZx?=-yr;@8yJhQ2<5tAkr3ziotp+<_;gYn>#mc=_+B7O0a zrWH3fd}RyzZiE4+LFCXJvHcow7lFr)D5X$I&rxOR=G%4TQ(?rQyVbT27LcM??POt1 z{)e}6Jl1O+o-sb@36tPZxcgo6ucpwma&-R1WxpjK)be>JsPV5-Vws?3aKwaqL0l=4O zXE$eCCZLNk0JrGHdX@9$(?IY>ciXun{JW=l)Y~s~AA-ulE?~BSR8&k8CKN&=Yed(&Yz*DM!7`d}B zB%sryX6E`FvG)N59NRv9B;%L2;t_$PYqF13Um>WCzTSjNd;(CGQrT=lvpoqM*!Smq zlJRqF%($S0|MJ1K3f}EiQ+Y4d519p8?dApk@hIh6AXw0Or{b=dX46*`mV-VfOp)Lo zmfS6Yz=cWQ1Sjcaf&~W?v!mP1)v4PvzH#^gnY`%;95#7XYAlf=l&Tg$?|!sfyfi9-FZD2Z7`XhHMpi%dX!^BQR zF9EAyPB}Xulpn&A>1GNO*R>7RI-BxBMBY`ox^3@C|NXBEi}^*5p2VMOy1fr?VMVDf z@Wm~BqF;>Sn`5s_+5N$gQ-Rw8%Y+2zFi#weM_@P=6%q|rR39`*68&H>sLpv37+rGQ z1Ze`kbA;rWE?cNd^v&rQx)RX6Pw%z{hU)JeD#UxrOkp6IWn&t=rsoUVpq*nkX_Kt} zDdi&nSQyW|701mj43L-Ms`_WWgx-0ZYCx)d&k&b|eC{DEO-=x4S_uR`=mAl`ZD)8h zTngQg-=y~I*R5K#;K)~5V`It8(G73oIRpsiG@O6L^wM$#e-;J@Yv@(zLvGuIew`kX*V40O#MU(q5yII3|S6;r`jcN7{wl^0+Cy z-Eta`cdpc5Rfo}Npr2lInJ=t!1gW%91QGW=jN~C#!u_d>nUfLk2L_dLxaA&34;c5i zEl=XrDkRed#!d1$z@M}=%;xVsmY;Hc>AojhCLuAzQ+lj|lwb@j1;%+zp-NBNNq);u=mw_yzNfEdi#RwY(&a;1ML1;V}9F1-W}k;OgPx(!WU3f2D*0OrTwr*r)odG_awtW0SYJrM zcUR;V^rRB=*l6oZ)&%mLq2Jhq7*xuI9N}rK%d;D+REf~l?rcB~j93TcF*?%y>QWdY zKlfT2#B2k{^MiRw*SEsOn!vD;;VZb!#k{eT?bVp^m* z&s>Dvn-+v`gLDV@zV=XhsuJMm0*;85o-Q|V6z!n~Sdz7`3g%8%S*0%100Mkp!=ePO(k+oekl=LD&gkFTIOh}n$fp|0vU%JH@KgIzy~ zVwol_#=3H&5N*nZ7H9ikp3BRE;*2k3$|a0b_0aLG3UUc(=$x!;BMMDHB}X zaKpze&leLEgN$-co72ZrS3eLb=Cxy$^jI=|{hk9nn&Ye$xoT;>;_fYEVU}qew~XCXeXL{{ z4PSZ?W|S!^kKFb(xwGp$-ggnCGruQN4HPte;8HUkXC?5-t*&Xm4G3V46nZ!Q zYm^Eb;wn5-%93*9$kBq*eTx?(lZU?F1g{sVjS16&E8#8Cb-1is4V#D z6qUewDJso?>nB#*U|nLXSR89{CH%uo?5k!97RWfIwqw zE8=8O9HUJXIPMEGdMaX5hy@vwJkn*DfVpRi%9CcbMA@*a^W2olrp>S%d@2&pFlxcu zoP-xM)ofQmu`7;Vm)A2<=Kbv}?<=jL%<^vLXt!g?OsFCf67`lY>pF92k7R+5+rP?c zs~3NRJjFc*5o3Tr@GRL%@%v74YPP>ifiZE?%zH~0Y$7SZ`FxPuhk8CFu9s4^Gzlq3 zqN0!IW1)9K5;9r%n7QjQ=i+RmWkV$ASX)vVUdDh-F{#mnd(j8OrAdPK)ye#X*URb$ zg>UV;qa790NjWH-HIkRQdtQ8{o!sZf7DG+E z z`{($L!Io%e8M;uBk@U&wKi@mi$Zkiq6_DtiIfK&X49vKlY<5&8V|y4D z;QeeFnLA^2o$W{p5wo2ix4E9la8mT_@fDNQz8IoSQ{@@C1P_)!BaQ^cd|?#RD+`OD zV@Md%{LhoZTS^$l6XTq(2U^3$O1*lwSan)li@4STx zEXG6T1C{qn_KMmrX#KvV{-%Fb#{v|gzW0q=d$0dp3gpMv1;zv;12Kt;PIkHjd-Ceu z%o7R#<4wzVKm$$!o(CAEr``dyGs=66@$|G&%e8O#CWauEep`udaz3+O@3fT;Z0?sh z)DMOSO){DnTP+eOOpba)?*;j1QBz|S-{Sjd(&+%HNM^%!{zhz{>7Q@#QZmS?&)5i&J=p-gWl^ii>{LKwp@uq z-v%>S`>KU57gb3&T#i8Bj>8lt3G}2SIc@^8Lepmz(ZOLFnUGY2CRt}efj=ifJgulZ zI)=#M-*>}~Ra2vcl9q(9?84XaO0MRBlXpDI9A>stR_4WJPvvl?R>86*Ao3D21R{Kv zJnZzrU`%dz=Aw5TgK@8pRRmlxhLPh#62or_^x&HTal|{xoPO;LvoV81#D>+;Q!bdh zj7Q>NZQorfQqa+wq_jyQIYLoX3TJInQF<;~M_c}AJk>2p$aQwi18mB)y6u|?_r7->%RJwm4h+WgX1K_;|Y&AVu?Nl>J% zxKXFtKI27i>j6CbHq;9z0Cf;aKK%pVgf%bLm?N91M!&eHC5rNtYG&a>P|6lNr=J{n zo9xC0lWK5*|N2Z;!haJxN0ZSVD^ao-t%N^8+uup?*DtA+?L9t9>$VuGDHVoY@B!|M zZSrR-ZR`;lvG(#)YM_+{@pW8}6&o6bmEWU_f$KWNDCI*^rU#GxtaP>g$nY+}Kau+7 zE>Ywe7UP7CAZ5DO-H*Fg86A^pw3k($#O}QybSa=GpJcOYCboT8YW>EWC2eMSsRHK< zI;bJ~ES}>CY;bMX-TWXbAECF0l1Asu)Vi*K3E0rrbLRb0Mq>WgaC0H%3Iu`TpTQ%^lC?L^L6*wy-qAPo}Ti9X@oi1lk1Am4Ki5*lrwE zBC&`~)SczRf>nwb%vF=0tnG(E z7VdX{VC2I6#xPvrtsDfBpUpnE0Rcq1-4R^C>07uBO|%L8so%VKKSq#1+qX`4S-gr* zzF%wlHlAb%orO#Rz8XmCGswD#Qlg$A8slR&i^qB0|U?JbL8>k1FmZu%^8dB*F! z%H@o}@GrmIv zB24Tz{S5GJ1E=TB`#QlT69IH^qDHuBS+@K5wAhATNc_7_!HR>iQzb8lV-OXxFWJWu zp}SO6wrok7%5H3949dQZx+|1rER_l~Np^;8V;>S@83w})27|$1EWdf~&*%95p6B!Y zGk?r6#~gFK=6YT4^SZ9{I!_^mZpO3<_ima}1o;ojqe)OqjsH++GQsc+$0t7qhn3#R z&60q%#Ws+r!){%f{QQzRjwS-wtlhYjPY+b#+-z%$06QmCLTnT(xZQ3&r zUL1$Rk3k(}voD^S&7#Ep6!j%Y;VGt`=&|GVSjcx_H~{v2B$R!YNz=xeEPs31W^^9) z4mKqEwig!&xOYL~j|Cg=xHlo7@w#y|@23mtrdu53xt#ZrH+eL#e2RMLcqGG`N7+u) zdA_*kS_@0q-j;s7P17-Gf1)9RC%agsr`$#T75egm^?u)^qUXy#V~U6HtiB4 zM`YCczcaj-7#_AqstcSw70-=+|6jxHQLObpiU~Fi(u?Q1=4fzMdoeVFM?Ea0f|az9 zv9#P0Kgs%~3&CucX)i}Cjf^*iF{T-2fd>OJ`Mjdc|`Y;F(c|sA!6%&2fK3OTyMwWIjiVZhjgKsi2N9I^yhBE)gbhMH`>0 z3fs=@KA*^Epn2lqo_gf^XEDPD4nTN)m+1}HGJUCA3u2Kc;ndcmV)pIeHz$l^ z#R?|g09oxSOu`q=qCCGK&Y6LmS-wFcN6xu%ZV96qMK8`~5UsRPArG?9ha{ z-P?zjxD9vFtbO-;CbL^4>i0Z}M!46gD2DYZ)5B<*X3T8N<^}zhZ3@Uc*@6LfmERsh z9FhO8+)P!}lmF%Wt{f+@?cdGO2+6SM(t?cwF+h^!g|$(u1J zUVS$ER=JYHu=;@MNmhLP&D6hom8u#oC2JIaEBaPf=U_!EfkW(wvxw))^w;{IrEP}q z%2{%L6};0Nfly+?O$JIl*Nmak+~+gE5=@nfmdl&n;PLSq-k&g!Yfj;JC5NO`r#7o% zu=NFnZNwnA(1%Ucc3^)n!f2&FC3}f`sxe0Q#fGPT+nx7b{EAI*E0x@ha6cBWQ4RHV zc1MeGeKq|w<7H2d&?epS_wb|pB{0{<7B7}cjpJwVrxIi?QLa}mUj;UrjrPs`?fA1Ib}}Fu=NmTnW2NtjhlJYSF(jB!K@&-yY)lNPX}R(EL_pkK zGEBQnzy17vaU`;!J`+RE=s(Ky&YkRlCd?ghll9Gav(WQGuBlhIHfzR@hMDmENcz9w zgW>-We!s$-AL3Rj0R6m(Yin02uVa3lIPDLgjhtr59$wmy1ng@~eWDgZmP;(%S23YReF$5)5OeYxB=Bd4Wf~ z>f!wkuJB7;sn`(J&Q81LlXr6!XV$%M4~MAzU3kgx-3Ekwza069#UUYMOt&Yn+u}A7 ztoM3|EQZmxGAvlztl~B+#=SadXkM}&NcDQGHLwWgqpck(@&tr_TcdG>YhCPY<SLMB3ZiUc%sf6eCQvp_>2gm>hCA$-RDgn>!Juw&3> z5zg})zv=@vvfDxdEqPV=ZRM`P3{G4Qi0?dCe?nDj&?~&|B({O(Bw4c3RgSsfQjdMH z{PM-QC{LMF0HUlz7L{)T8~C;1bE#Io9k|xhmc5~*F6uRiuG@eWkjp2RjKhC<2KJS zu^hFzG1H*8o@5|BzOCR)a^x(H7MxNF-(Eq1R76-Go-upn5Z>RDdWzJ*0GZpzQyK}% zHQ0wAX6p!0M@y=({dKK~*7vju{_|lWG0`J3 z?ibNyO4R58E1WuZ=KbFM#?eQr2~bx};Hn>*MvxWkJQ1sJ<-&6o7UbzhR z-f<1Lt8;H2t&CrlJgmGQDxbkW6GN4{9?kvq-utFlCp8FK|2{(%w0rMLj5ta1+j{;^Q z0n2P+cRlEY4Hly1&jW5-)%?DtUq((Bx!-st$kzcE7;(xmS1DiOOBBgM311>3U=h=n zt<~OGdXJ6X8p*7aU^2W$&H)6^v=Zw@2T{JhP|^#)=Y4!5*=9{Ug=I$Pyq;P;%RzJ8 z9UQP``$IW#i>Qq=VuoV3J~aHM{33#OZ4x{vV^x(T!rhO*1Gm{*0h(Z^M_Bec0}v^CSN@J<7D27 z1J0>6k&Zv*wZ`zyPc zy_Hno3(sxF{~NY9{V!~9G`JWn*(LaFRBVF_DP~Ey9jvhR{)R_c)T((&<*;#yW>V)eof|*Dot?+hN+C}@ zQd7+mc_NUe27CHnY0_d*Rpn7B)AD6meXmqrjNfJ>I8nw_GOuh&7c`{7sczaD^KxxWPDPCuUv^QTr z`1yTua@WmQp0Mf{2Cs<^>=`#8*ix3Q3YHV%-lp2tb${i0zIjUF{{RxG*mkm8$Tp(y zkw9D%(%wgIXUV*9NbyFCB=);h$ofIh`-*3&7^Y_fv7~EuzHNEo zVE0)`*Dd1!C-9XX?O$G0?81oT0Pz)&EIPwW zl|1rq3B%K3+#zt5y|H(yKU$fJb21dCf*+a5Ws$RNJrK9CcmqarbN<}9E59Vwp3hjE z0Aeu{13XGLsG&-b!)H}f)8>k&rWA z_Pv8h!tyLrpQIWQhbC$r3M@Lzsxyg4xTdCC)N-pIT0Kk7Gf7+t*7Z+ zPY;?xBDt@%^}7OAc=N&-gpuPWVqo~4nAPqOI`SxhwNo!#;G|(qA@_&9?B#?^rgNmn zzIqa`Cz1|BA}N07yH3X#J$5P53n1|S)qX<~4_b3p zx!3mL*f+$1AYGp)9@;eOJ`0?VCNo0jQ89yxQBnG{q~;6dXu$mM|+64PCkC zDxpC5-?8kd{eS3Lnc&qvFb@wTQ{{E$6MpvKTy$5-{#$3~Yj>mO3}s3-J9j8r7)OCf~WY*v`!Ur5=F-oyvoH2>wg40G;T%vVX z(rTFJmq#41CKC<1ndhwMj~t*{*B5f2ZjBl0wP+-D=G}ci5}R(DwjrDT$BNT?A1yk2 z7L4-7?Gp3&eu5mgfvUCjLk^*#1Qi?px-*5$y%%_}cqBYAt?Jb*UR5s}H;CWAO0|$P zMUo>NJ{z=$`EB?cOEV7bq;nz^fmUdmp=1sti2_`B>zRkUxAnKu!6!9BCzX0Xx^^mF z4~1Z@4uf=*PBOH7*M9cOp!?0;X7=)yYtdv53igkCuj+r#U1k5lZ5vzedu{ZWzSEs1 zePCGC?U;q$9SQZrF$CKc?Ra0pyYcP1%<&Q13?iL1Q2T{d*kPtVBxw7@ialr}R7&L` zzQC7-FwzfY?RIG%1&&0#zmPVrx-GM^8#jfS(CL{=&Ei88FP2{7?~>_*Qy|$rKN=#C(8$_f1GHcV` zxvl;n7yzAX()-!^u#x9IT8aMK+t85nC>}n07}g5&k9w)2kJ1kKy3glONUJQ~dI?v9LX@$_x75AF1-Q~!-dQPr zXPJM(l1nN?&59$D!7WEzc1TEz#oU8U{BL6diC-*a)s)Dd?`(|TNhY-Q_al8@*BZNh z<0G6z5BRQ3qhMvetAH++zL+BEzp>H{nBl$8kl8P+Ul5{B0qD(oleL_uv5b_%n7L;M zowp~NPKwV?Lu^k>#L}Q${FO>?ha|tQB?C5Hv`SRzK)fhU4w4yN%?b zDgmOOv4mez_h&~sl;}SM8dqu^_+JeA40hJ$px;)TYNMbGs@Hr+UGoBpam40v2t@$} z>GTBB#x=QF5IFPD6i;Vi6WVJ7&k}h@URGnR02F2lChyv_^u%`DTU zXcz!{!T?RJ-|;&LL+%H;{MX7z^V1Q636uM)=7Ko=?kKxBCx+gzFZPqL+VH1WwKhld za-RX9WBx%o9|0`#Dz^GofZlAcBjl}}-F?j%8t}Iw+|Z}V!5Ou3{xdpn;T_@3H>*)V z-h-IZf#tqy36{|(KID99z-YCXo4&2_w0^KtD9KcT|E#)TF3JZIx2^;g zNWYYq^%Npay<8SgA_RE_JZHTx<_c)mSa`ky&BsG|98olJLvX~^;*d3@MOMlIAE#TOV z2BB3-ZqL##%Mm>)gwSDV!4*HuhHA8422oW!(rAZo|BS6hx8GV=479-}M&x0W62p(# z`2KULK?B%tNB%GFh{8WJJK|FFijLo}D|OCFf1(sAAs;?Ps4;Y#M=IM^)o<>#%C!8| z2mc4~(6Y^3!!MsX*?^=xB_RRU zg4l_#e9*M_CKyQgU+9Q7b~ruX;o#KVrVG}a`Y|fb6R*R!T0iJPR%||5@w7A(^hFBi z8XAMqPqA5ToVq{HZ2mXD-rL7{TYn=CtLI$3(om=WID#qpMr8hh24R!mB{9s}Uafys z6Bxa8^OZC@;d-J(qaWewZ}C$HOyFcp;>&i((h@N?dUhR_zcRHzAi6b!e-h8o>4sTh zlDL8Kev}KGTxqsyfHXMHh;gAGj^|0(HRaXFi$?z!l+7smhd5g1yBl%}9C5PF|KV)RZM^P8WKlIJ-HrfBNiO8i>u53t)) zXOAQSxvAinfqbcarhH;icN+ryCGc+ZMjeb#!~@A>srUR!RlWZFCIt_D#$Tmgb4654J!Sa$@gEq?z_t$kpSl1@2Yg-Bnc&m?Cs|+| zKQ>Yhrdh_+nldf2x0)uQ1WW^dUhCpfWF||LG+NBU2)OUzcts%Kt&Dox>^J#>Gi1dl?6IM@U`x(vAWg_@|dTQ|*Yy08G)8k4SmWK~5f4mpM z+Trr2Bbo*=`tOO4*gsovnBiyvE(iY+kolC}7oyUu$>{%WMPA>xnHpqd|Pd>df5AI)O+9l;RPWEmb z$poe!9FH@aT_^vhGztPV$hW6H4}Rqsl+a_G-aTbs==1CB^*4$qUp|O2o?*+V0RzNZGJcZfs2Ou}4}wNMfLGJ3Gc|gof0na`|fGu|uD^?|AWn z;dHIZr$u!1*8cK+TqsXj#~9%5wy{p&^D5GK=~)`2HUxS;GF$3_JLoEWmCHMG#NiJMJ$6sMPkay!Jw>M`G^Bir##r6NqJ zI%UxyI)xwU`;Bq1!q6AIslm1zj>7GO!zqT*vloqpM;zvszx4}i#O*c2oj0LGpF4F3 zr-5MA{hHeectlOp0}QmS+xz$&z03pJtbzi~GJ8>fwdR-{%{Si#%E$4WamxJjf;wd| z$OZ9wxV!zJlt*4F-Z?VEqeja--yjV2Yy7jvZ!1o}07d7?kj9$z9)9k|QyypGwgrDe zKb&`%0t8gHutt07nW-qN9O;X?UVamk?D4~sb4><#=IRN+gvV{roP0t&uG-cy`F3mV zi>nq+2W{HJ-xnPn%u|VnKJ>>TX~P<^T9dhJqnXA^dOCTXm(sQ?8oJfQQuKhai;dM9 zyP#{Xv*Aor>^SNptYS)!!4siFPYL4qw{GxaE@}B)HOS^;DrV*r(n4}x&df@;HXTb4 zkW3xzU`T@#Yc}@wmo;%aLq>34SE%C-HxpBaye$oPJ0GT3-?|;CvI>m4?O#}kB?9cd zEI#O^7a6!U*#Bkz6|t(^9;y`@5bdcD9Grv!L&vA+UFVcaXfkeMcA(vZsaf++pn~wI zj5mzSe*OUI9bs~%xWfwdE6mf#r84{$CZ6!@2CtK}?lyidv-G$OpPtE}-}&n}I2OM4 z^9MG!SsLUyu=XOUanIzKJCFST{~7Zkw)qU=1(|uT7bcO%-IMO6vT~=2cpNR7#nxQR z9~3+Pv=>aND61~Ut>YE(5us6OMsaJm^Jr~CxrQ%?B%@BxS@%VkZ>vA{1Goq0fXtMu zhX+JO<&2$c9NjSz@mxXyT6A@|NfFrrNz~Gi?kzSqOzD1KDupeiNhf>Pr05OqV-XUO zI#s##VB$Wp>5svN6Nc<>WECBUJydLsnJV!-TUqSsf=%x8TPq0~Evb&iY89N&Pnle9 zu`CG*_NqZ&Uz+|Ao(5?_%*9yxCBM1zjBGCf@+_~~ zAs<1Aj#Za8gUJWqjC}4*b_9*t>)=WZ`YSc*^)poABVsL8J`*)u7g=V%X5UN!p zAce03{IBQ*N*qc3^-mPLX$BVvZgPLMi;{a;*x#fSNhOawlVtLim{PRp#jOzn-c}6W z8YXwU=%%CVjEY(W*lTp=EXwZVA5uXopSOIg{G;-LkkyxJB9_jPU3-~^;b(brwWi1z z)d9NDz?3KMt36g1dNcj z!68=MwT(HI&X!%Y#q$`l5lJa){==w+JR{(5bB4zAUuw%T&0i9MFH?;T+Uy)HH(VgW zhm6P>>36;Ab~|vjl4!Ap!m-JKm=zKeGZ?W59~%DSDMQ3!mN!Fb%QiI8%#TRtJB%YSQo`X_FKjsp|y8|kuc zX_z2!S=SCa#yCp2S^I57aEz6);v|2$gG`S@$QGjKbcEr^qa6x&6SaMUr$m)LaJ{qx1+5%)>c1zi7e&Xv@OiYC2d zANaVQ<(UA2O=Kb!l=AOc#m<~H?-j1wB%EUga2If{uxwU$+cSx+Us&~SeK>C5bVXyT z_~K?iU~pdwGaHoR0FLt=#0cC~Au%CSV-*G_rkdZz>e z^*aOGe#y8gkDfm9fPNO~UeL5C6o&MDY67;v1nevd;UF`#AN2GSpoq8n*P8f|MYoo` zKN4HffwW(vQ_VQVyOdFjrxGymDgU*xQNKg1QzU%n2iLM^VBUQ_9Al)1(MZrRGnK&T zv=&pcgQwIZ4N?{Y0S1|v87WNUZ1W;+D?+HlGk@(?^~ON~5!%5%jq1r8!6IRQaO%u! z^3dd3@gtu%N`UzFojo zr|6pfA%jvhq8vy$8Bcgmx=yFec9XQKt^33V&9PGuqne9aiw!NPzr}yU` z(2Yt#-c|e^t_a`QoUl)HDn&Q?pga20e_Rdif3C(`#p9U5UOQg3Do$=9Nz9*`5v6?B zly}F^ftQe9SC}^`mRmiPt{lJ=7_2CNvv+Huh|_q4%M1!tnK3|`4_*t@EKL7c)svfB~~OPpK70 zKMPVs)a~$XXs_ig-TF`Z2}bC>?55Q@6$BGZw8!N^yzZ+Y*J5|*@IjSkBrQzmju>$uW9gDfL75w}8 z8gnZA>k*)T&DcQ$F!j@IrfTz3_E{i#nfe z4&0F_KD(H3Lg2XUr-NVUMr?G)joNW(PgvrPY)MUb1I(5FLj20!J5*Vdihf?f%W zE$agZTNjGAyKN*c{ru2gI(1vjVpBL{I{?zOp~pR{@Z97Sy_y^Eq#D39gm9PIu26+@ z(UPL?SKnXn1kP?u+%ta)dNkW4Gvc2(siXO8+j`fvit*OCO7f;f1)tZ^+av1busdk5 zFZD95?oyA{TK`|hJbMtmElp#NQrZ!r#(G^E*Q&l2__t?6sgA?Tdbk%4LNL`#-TzCA z#y2hCQSxpy&sHS zTEoG<1>_b!(n)b9{WP)E3o+r~ahIZa>~3+tgcyRnMl}5RAXGlwj?~p%Rj)TnNi+MA zi?9JUAD~;HJzrZnsx=3ML)uxQgMgy8x^-vc*Q^0*SA`skYSZ~Zz-YBnmudP>u$&n| z2}n5G@9^l`fMLoT1tHK}Wbiuzw?-7o6jlnDMpf$gl^enhQ|3V1H=P#Pt!53C-tY`9 zUqO)5J%8xf%aAQdw@#Jb5~)HWO2eagJiQ5*Y9#la39n@TZs^!({M_7fMNA=^I{W@! z1t5V%+)4AL2QE+?^8049}|(yG5>clF>;#nb3;0DG+RPyNvm?+PTml1o8Pl-lVK za4=@ocQ1-EAv4|N+Mu33S5aTP(Uh5yqxfew!?&aA*XL-WWl%Bg|mz^6mElFYUlgI8MGgca^6;tMTSFG=mWD4ZUKUA0r@jWQP20( zQnQ`+MA|w|6Zo?jEq4{}a_o4OwMCGR2 z4E6OtuzCKHIfsD^>aF>_^p967p=EscFC^T;0J$dOV@{KWy z_b+$#xzlV|Tp&g1*b*uYt*ju_=f_)!3S|9$B>WtPCij<@_b<`CgHv=xtMd_)&hhQ2 zsdK(LsU%4I(^CSc{i8Bkq6lXyD!mO;$h96fbW5#Y-%o+*{NTKxvtxNcCq-;3qXzNw z;&4gVB^BS^6-O*)0aIw}y7hozT=XB|YyGbTeB*0X)LKTrwu)xr6^Dx2w{x4XFz2Uj z*E^)Ast^i)QmD!nNRQR`2RC0wGnN`bMG* zv4IUY`woAhZXzOyYe)&+F_m6)g?o}-9qap6Vtd=kwXWNagayW9y)LFG*%n1O zdt+)(&{nLwouE}%)n8m+Um8mF7&(&BY>GSO9LI7Ls~ zHeL()60G*NhHmU%oB0fj`$jmB!oAUOT3pvtE3Z8J$~HW!10=Ka3DW9mxNEJD8n7^| z4a<3l!&=I@fCe^|{r8;04|#=pH8eS*oSnY0Ho9R-Kbt+HDHUD5QE-rBQurTdYxB?9 zj^EsKVUz6KiG_WY>#_8R6#NSe%_**3;1I0<&MACwH=j~?{72x4h-*$1FR4>UL!R^( z{5Cz*Ex{ZxH6_wIeeJ{xZvi2ZVB%G0j9escPIB%R|J27VZ@Fw|)wMJ+;fHonm&8jQ zU9ZmD$?GFC>IdmkI55{nY+=vwVhjrt?VMf=Bf2|vgD88yW=|K10YzVa`C_6Z=1pk%CMPFC686!e$&mXzJ~ zR2ZJjcDt!DYu;=+VEs;v1oRd)|79hrSN~HT^pxi55 zk;iWpY&d(CHnTmMUf8$#K^eagT88_Xn-YC%xB3Up2E^f@OA^K~$_w<4c>*QC- z$q^>G>$!SS4tz6<-_V!3o}8fHm_f-Gxs5tp{uGUvr)&w8(2Ztsh~+=&u=2Q-u`pg- ze$|_FjcOwGbsZWs%ly$|x0~P!Y@>HvVi~%2Wy|eJEaOnBkh=-AG|GE3=?iBXMPIh? z;v)z{s-Ili`txW>|Mgzo&^oySCa`o_Xfs{K$uF?1^meeTd`P6X{xNj|pXr9H>hy$` zBM+RqLQ^l^+i%YMR5JMeI{SwY?K=MN{(;B;8R1X=iE#aEM}^Iy97H{nBWC#0r=tt} zUqY2qsTQW{D7P6)j$A3wnw|hS!y9Uh*)?cyG{9)(siw>(?jFqM_)NWk4+Q2Fhxrrr z`{`3b5z^*kaK6C)rKax4Bkt`XS3lVvxepR9ZI`|Eb15>Rl9LFtFIm1xvtwtt=eg^f zY;#9(i8dmbqHBrznXbuG8wXNrOKo#7Kw<(Wqwd$pY1KK@Qe7zeHWeY5dGI+r&lpvy zCqY0|jT=YTHR%7%)$*R=%sx^1;|9sP+3I@7zJe-&h&JhkqQyPyAs#ZJhs-m}5Toty)_61}_P*W4uH9o*Kff6KHwn#b?OUOTf0`IIDx zsl;j&-CoVDAVMa0S6gDj$-M>33!TJH>R)<5l0| z6k&O$+3a3MkIpm&Q9m$gl&@<%(_ANOwvqe)+JABU0+*>59xKYsm&yy89__A1JLCl5 z;Tu*A80Wn?K*dm&Nie*(_n!}ZFX}J19%#wmmKSRRT!==pgQ9bzKC`%vz5o!}Gx$F08I=1`9 zd^-95mDCW6i@r*N_O{^-+6g|unxOc9ebg9E)5kf$roTb&IMcVlr7Pvy;mcxk6d7UJ zyVpQFP1wRu3AJIWrJ(1=ZW$*hlYJjQ+8Bo%*06wsh|Om}A;c=jJJ@%B`$r~0TCkl} z;*-lf_NEPtpzJf8!eAw-a1(2c!8qCqYtLe;;fr|NjJ7`txyKWF5w zabWcKKZn)$c+pgTK7{t(fIRp*jp-ot%VwqSdI8n>{oV(jDmNw0VLN5{>ND@A0WnY$ zBI@hSJ77p@OD@-xZGnyL$*FNM@xz$VcXPRObIa|w*v6#cOcRb!b6SFQ;o60*;=(20 z^wELUq?F&#XN3c5vp|D6x@Mw%2ApQc=<+mg#w@?^#DqtblgVv`P#U{t~UgzT0w0 zH`8df&+>g+wbP_Ta8(Kfo1t%mz*jeP!0+42GQm~?dI?@Q(i}MX-Y@S((&*=+O6aA0 zjJItt4lH)owVj9jUJU6mSivg=s>5z;-!y!*79enL?gxrOeoS;TO5KpSXTEAhu8~Xy zZsD4QE{~Y~pmq>$rVYOI{CzNAZsJbjPd|U*f=cSAbp=n~mB@40fXNxt{|L#r6HG{# z?Ol#Yy^e)V(9#`xFCic4CS1?BFZyUSZaq^FALoC%#I;8?`=(2B;WPTR0QlZ7jjY=E z8#`W}4MW?B`rSE6I;rasj|<1j;nn1}L0u0i^G`SR{J=g`trrhyU_`vB`^Z&wt8m^h zDapBtsf>rNelH%?a?efXJ=*2em2CfJo!jY97Tx(^zNe$xxv8UK8El~*93hdD z(kgByshcpa2i&IuQdS?_HZQ9QnlmfP`gWq0Yfj;B>fv}L_HX20P!xEgv-0^Pi18gf zug%nx(jD}OLj}<9WvOu9jyQYEs3~RZi{`(Vbevo^%y<^A%-V!p_KLHs38=r7G|@0t z?ibK64c}|mefDyxYF{nwZsR=(cCt5rW^qG9LL@$?Zw^A*vaGo3N*qh%VheS{BoP}& z&7*9Y_*O1Lv`g|RHKI#YlV?7Kd-VPJyyS(U)v3{G~l&+8uBH9%# z=5ft#P|yOBz4#kWG@EtKsH8v8&39gq&4A57d^x}{X&Y5=IxzvM@Y(w#t8k-k1*l?vPtshCc&YXOH=*A`XQvy0!1YZd7w< zhjAc!SB(yIYsi`%kDx{Gy*s|~?}7b_w`T6JcJ60NK-wW9sx>rAa@h`{k>+Ddu5 zJwtCZ{bV4}>1SN!cZSlyW@2$mBX@k1W$-(9nIKb+m_aIX1&!(8SZyWP$?W9Oh}Ghv;~j0$@E1vEoH#?vo8=`O_$okV z<^`on%xgV7a*66n`jnVRr`@$`;GU~q^D28sx}5?3GO~87k*wHB0e+A`ZwIRT82-OL z1*go-o~)(n3vrRKvbbiGCpfbfOut{lRblz4%SXCXZv(eFi!kildZH=n8S6==Rn>eC>gn)U?$r%}B3 zVfH@@>RfLl22*1GBl4?#@P^|46V&_X(Ly(EdetZ20XK$$PtX*nKlC@_fo%z%VCGVjx%#><6wX38v-8S0(xU9&Q3;wDXzw{=!-Yxw zO?T0QmGQchxf8zqDj$Ifw?r!fD78}ICKK7)yxcp+2Mtj>ThmXC!&AE+wqrT#f8V@3 zlYmJ&Q7`u&dAW}LUr9p$lMiCmOYm@5nz22W_-fmoOCce(PuAjc4sdkOrO$~n>4Q>q zG#R}R;uU>g`A_5D3!DxDhV@-H!!j`j4~+AhM{rdsf(_bgjoh<}e|ze)cl>#`F5t8P zaT{@pKhLx%@Z$XV>Fsdis1u{t40^P-Dy+02-qE=N(E$B8lU2T;4Xh8%*lYDXE6_@MF zlzID244usNwc6G!x7H*vkPe>SPmn&;HILsofz3(kGCxCTrc)1jg$qK;^o~yM`yVdv zg=R=I=p$A3UvG=~5h{gAC`1+v*gzWVOlLji*QhWIfuV33*NnL*79BGn#8oZjH5X`J z@%b4`{^s`u3wLVzpR7(?0$vD|EMJ{{RPygtcbDY9o{;Yq9k zlSJD#A}mE(0aIgH0ckJk7LBi@D3XP^%60NkJW{_;`f+HY^wePzq;U|#f>B|gun?-a7}(25NA+J`vDhe+Y$rV|tZ$N-C`O3FklGp5q;5-` zUVVuQDm?E9Gd1cuBLVbWKH>0!IU(Q(c+DVpTnjjd^u@2+w1LF(p#d+Q-*OTw3|j-! zx1@-2;wI8B$oKK#sF^WKTc?8DI#S4KUrF{+VcqO!`5}!sg=z0-!?fP^kkXQzN6!ge zkG)@$-_^WYjT%m>F1}g3WUo%I&!=Jsa!meXR?_AA&%uL}BJEowcbR}f?fgM_aGqyW zfbi#HLy6?pnxr#dkIqB<0)=ZT4HRNhvpJ>PDk-J)&BYRtA-RDhHizfsB`;O$QCby@ z%%n0ed%w*QM{1x*X>CX^YqR)p`43xlI1MNmP<2}l-^ssNvrKexpe4WbmCLl6^^@G3 zX6u$KV>+b^c32q+#s~7&ExJJ`OOMDQEs%X^7D`%m$Ow6`C}i~-B0SDQ{`OLfdk%E? z<(1#SoPHTZsKT>Z&r(@*a_FXQ0b#01V2A;c@-=V!=rF`jux(5)YUV~S$`T5tL-b3< zkSK*Xlb`tyF1}dSt-9_XedID0Y58>n@)WnN53JwyCfezSgX6H?t0rn1&tsyWG=beO z+;}|jTzH!ZqhsrnmnzW6(gAFHFt~y!7eBsDC4ck_Xd;U~I}ZJt?RrC~`LHrsLfQt8 z1F>2JLfxmx4#NW3UyRoT|DAOZ2Yj*VWw663<1P(q#hoD>(WAr4H@~JhFZb97x^x6b zA__xPW$*slPA6CRHj22;$yS10+RYH4y&}ZfAc?D@ekzoMDeS6zX)M{V{}Rc0U_$>R zd~kxZJ{X&{F!vw{FLHC-hO_}&j?tL4>eu2A6(P3tSr2Lz{v zn$a~kP2Dg-0~$z(eFf?|V}O3pG15PgvbwxsL~9TpH5yRjsVO%ry)O8$3eM|M^Kn7l zKRg~X+w>EfLH~Wtmb1wSISUQ>NU06r^^iM@(BSol6IvCn7xy}2q}s|HYEAUWL*U)0 zu1+xqE(#tn6$d)xtAT>(UTINphw3mD7VB;u)P)O48{?o>9n-fPku~Won*9=p*Ln$J zciqj#_aN=$Hqtbe=YKc|jn{Xf7yp4DknX`3x4RO5k)hA#LciXsRducHR9^zk?S<5O zdj%y%vytCnBJ}8C!7pmmmvRjKS|iu&qmbc9-oT3E=-?@9e%JAu+ySSC@i9%^z|q8% z7$q6zTTUhlmLQoyE5ne`wBk46!pxNnO^i1P?3#{CFVBXW_+3e20tW|lyol&C_(XZ) z6+evJ`OGCX@NLlHlfpgX-s9kBlj1`UFD<4=E_c_L8rrv6dDlTCf$wh`?j*NX{m9zZ zipaz=%A!Nwc~33g#n2ajPt9r-k|V=?0;v_N`X%8F$IBwP09-`$lf-bz5b<4JN6sx@ z&T_9l@Ecj_5KOH>n@T;)&b?O9-N~AueW|v(th<(QpFZ3aY1wrc_W-&W+$=Q#kALmX z)o7rLIEi<(DzPc^1(b|5*h`DMQg*Gza_2O(U*;Rm9L*h6V8t%AkP;k{Q;g*QMY#Zp-6CQ)B-V7{UK-NN4IK~$Yrk*+P}3kUV+S*v+gr7n25+H zvjec&E#=0Dgt`aje(Q>ByRs@D_uY0H%u|(ecGdBBWO&l0*xlskW7=reWf> zY(W$VqnJ>TkX%OTG%_cxI0Jj$4;}+`i>(|7;4{y_Lc^Vtf*%V8E*xez6~JHm zi7YcmmMDo(e;d6~D#z(qq|DHE{_~>%zy)M8cc0>Bsj~#`zOMd^=1jH*LNCDvw>Kll z9!v%eiETZAR{N-|?omr^Qvl}tgiffDALR#Kn}Cnt*^-g%gZvxyqf4tW=5`l{>E%g~ ztpeet#KEX^{oI3`OEA}XvRlR~9#Z~zwLG_5t}#C&Y@yn?R~M!;{jMO5JixX)g8zXY zmGxWW&DbWoY&V4^4S3g!;|^7~#t&V42*dvo4jIEFm>T= zcTlXAhalJ8 zkGe_VkC}O}*r;w0KWzv@4#A{#y%r;fw&8V4fsyt4C+=~N?pPWg=hHx$JKkrCE2%e$peVReaXei2eLtW7)Bi0mInVR= zJHGQ+`&7L9hpi*gNSN~PfD)t{vMJxP=NqS~*j9l-1>Z2$ja&V-dWWRJenlAb3?;Ev z5qpT${OW~_D$B`F{Wq8^YilVa&GI7STUbe5E5U{-Yhf&QXC*#=?p=?G128Ue_2wIE zBc4+Mcc#Ol9m zo3O@O8wn48s2(<3786gO=IolfhMmMB>?OkTU#23S>LlknPE~)NyMdva_GxMaA(XIx zqfuHi5mwQ$l#QwYlQrWNY4PQ}Oh7t$Y`gR~rw5I!U!XbI7v~Vt@&H*xHKalU>-`2` z45eart(0|6hd`KF@Vzed za?ZMQ>x!#aM}2<M8NyO#WVohxlLHq{wv`TTz27c^4K2>BN#a?m6va`2cBcsSC|&7!kFoQ2A< zk?W$KBK?;1>qIC{7c{2sv7TF<{30{5t(013SgIvrH0XSMwgI7{0yI4R<4Jh%`(&_0 zfDEP(cHcBm%)sp-QTYxg>rDOFnbyeyO~DUWJIg%Z>(6O>MqV(mHofaEzjU%XuT%Ey zMW+Z6x>jIwvIh=3=^Gb03bz#ofezV*y$;#Sm zORfa@m*rsM%wo9csu9rlTzDB^TWp-vsN5v>0IMTZMM=N~o}x_XQCQu35uoN2o8rOl zp=EGeW!6clH~3;~XKOM1oAH{ze-+*k99%||NSbfia_jZ=o1tTD2c7Zog9*5tc{zsg zd-!>jQn~!JrU4+Pl{Uer?hVY)uL2aW=zj@XbQR~2I*~x9b#<<3l{c;=O2HGOKg{EnWNCP1iZ{~#*!QC0mhrvHQNy40nd$WXf~!->6fc;aWGUd_g_KTg7xRgF z(|4t)sD-P%f#+`XqM~P#4H{x-q0B|GrvX9P1OD%w^m|vpWoL&6c(w>$L~#Buoh(aT z0jKLRrbr@xCsO$Tx^@!4dwLeBWkVJ&7~+A3+jxX77q&z)dt;_pEb+WWQ!#V7iaPk?p(} z0ga_=&A(5+O6xd)_Z~LDDBrGh3RAMJmYK?pY_7ytrJ~jIe=ux@xNh8ROM`U2dx$gRLrC3~reiwcl@U<3F(s&)d z6*0snueGA+`-8|R|5DvCeT$w5X~Q2IaN~n!zv&{GWoP4^HE@H?y-*TVt4K#IU$_XT zGc#^6J5c(#kFu#>47)NMYOwJ z`FDhPE%K@Ui_xqM?`V-~yNlLdvY51)t($)tgNyM5i*>mx*~7L!0-DiIYCpa(QT~gO_matHofqpJ6P+B%dJtCg15Ttu;DDm2&`P8ryKl=qm zLQ%#%g6`54L{t`I56ziqHX%R6cyY{&GZj}Qn;!%H)>;x-QW~-tlFs4Warg$nDK^m^_lO&GUyIft8DvpK`VX?^}GEsqNZ`u4k7~&qdhicc6yK=Gt-G;C8I952VsGhlV zIBZ_9Sw1GTJ~N9~S;sfu{O0E=?SS}(m{=q+v`-UJ8ge)rKgM%zb-)rblyy=$6A-t# z=QYy?{oD?%h8H_}HS>c=l$L8xckdX?{^9oO&Efs`gi$l;yVDarzOr#aV|H;8DF|8F zyWfR#B|9ZaVs~6lv4BDbZ|ZGnD(RHchB;AekEDxn;lZ9h*6aSY$=BjgtkcHpQ;`dA zpdG#Zo`Gs`U)O~tIt^eRtrRq{US^o^jJF;1L0?LklZ=-Zv*u^m$}aXh}!;;-pT`JFwxy`-hXCqKbmZ@d#Cl5-a~o^Hq=E>>I^3N;7nd z=+sD!4eOeD3!Q4!y{^tYpzj7I;FO$_~-@ER7hD8?#U#0^?B1xxv%W|c_Rlt1b-01xK zhlf?b#jdVS@>2*zbl%jwuUUBibPUW^UZ3X^hgVL_nZeGxitbcvgJ&?TZsRszg<(tE zi>||yJ5Of$bia8{O>cSVK3(jVDIbo2E7a?L!%i$jT=p8pek1(GX>fUVC4xw=!akOz zmLniwW4vB{VS|bCX8s{W@eR#%vVLU(wkA{C%(pq2yLvP@P8F$OW_mQX`&l?*D66Vk z>@6pluv7Ig@z9Z&5fu*0$67A>()VMvyG9;o9uUwk-3xKtehB{w0~RM@x#V-RA9wG$d&H z#a82JZ9}ru4-b5nVpLVx8oC{Y`4GsvRoXNNu^FvFu-5Mlln|r52LI!G|DSeC6rTI6 zao4Q3Z-usv84%bNUTiCZyU{&qaeOW{eMtz!(Xt5`yi&IeZ25%ZiiO-_# z2x3n=YTxKji3FkL6cwq!!c%l-$&mNyTSMgG?^4uH{0~H1u)z_&kXrV({X0NQhl?V} zy-Y+L>q7@6+ujYq2mP2@P-}MGKAgC{}Z!Oi441<@oyy^8 zlLoD2bi>UuuP=bX5Qxu0gnaPCvFk!@o2uC|_$xW!BxE`pztMQ~lb%7R=+w}=y)AYn zSmjspN$GqmlP?7f7b|>BB1@Scp~L&ioBr$wh;MFjS82cT>X4W0HENVn?Hk3?5<(f* z0$!aF3rr1(NeUVRROy(4=%YPz=9QJ?T|w~o3|ie{-`H|QJ5{)|97Bx^y-YTEHQy5X zfpd=Z)^o`1K288=dD8vILM@7U1Z-b+%War^+lhr8jy6tyiJ>YaJK&h|YsGl0^M&9G zqgfwi6>Mw`+Dd3NUh&!AsL60Q!&~kd<$_E7DhI zkmKS5roa5dfbfF#pR?R3_HCiX^nLT};$(n84rLifw4bNzuGDPG3~Uhx>v2T``p}0` zcZ~+k-&Us`X=?FgrlX8{=#UPU%Iv}z0W+UwIT0du%wTk^R;pGB$U`Y{mOMpSX7wco4P?m8z@M)&H3GfxV3+wnRo=pfJC zAd&roO+f^c=C8en^*?IvxvASoIB4Cik)hIRbr0L>p4@U{`j#3b7b3~DT`R500@|7j znXo~emau#@%hQ&*`6Imh;!2Jk(xB#k_dUqb>O$t$gx_M?WjAmw&{eq=pA2)ji+8tt zoZ9$(k2=sr+{8uR%d=1N5E-cU%0g+VRckwk(*y!6oTH=4OeRBt#GPUmqNZz4(v z9o6|?qT6@Dx5dU9(}BJT|LDl6O1qkSnNU=8738`ZHi*4~^x2(>Us>GZg7dFaKym(S zhg&^*_G&kp+V>{J4o>)YvoJk7e*M zXH%g{%ARFSq_&bM!X{VkX`>D?qCiXL3Ps+k<|aKqLkjGR4G2DfWm_H|0;&k+U*>S- zq+7z@K~B(P;+S=K2BYQjB33+If;qsHfzPK)OTO6u2wh>@8ykE4?z&`1{?e<5Y^ z4!wg!=D-X}t>g1?%45bnhY7-Wqj&R6)G{UhSL%&bY1{F6p~`58B#pJp!D<9?ZfA*m~_$^h)^q^l@p z;qi(9v7xgdFp^ImWcb`&S`l?+dsi_f+OEvtt<_27|7kCKpH zZ3Z!rjF>%QnCw-2YsO`I(Vw8_V9Xxc=ZjejG~L$xLoC|ne#im19;fb}D%-aXJeVM* z^?p*+I=>bnt>qXwPRy67L|Y?jW)!vZDinx*7Wc-uuo&N z`t1fdD8vuIS+*)LXnYhSIQ28vSEeuO+hhjWX>jr_Gfu<|((F%+&0H88qu!yac)3SM zNaE@PLri_a#;&HX_(t8Uu@7a;L-Og3Qah}GMee2Uo%|;224Zd$`vqW@f{eAX9iS{Q zOOlmY0BFgQ7Bft2()&&6)4XQJmsksgB&N|p6P&Pq>idv->z!h5Q z+A;(;6a4&qMEj@RY2Ol>&_ThV{xm1D<7bKGP!=1n;Yovz#XwU5_U)v#zcIc%4?RB^S^ zEDRogw^F#t{;zKKN3Rie8IH@?IG+;lpg`={bL`cIhM#imD}Pw+$YMkzZK<&dDdKGZ z%SQ+890j|B5B4RVa;>v_bol*BOT#bt%$|mIMUzxKDrqsP5nu2P0^ J#{B$ORi{5 zq_hWeq~TZzXpoS|Hl#@G&QzdMZ>cqnMK!-}SV|QT3bQGu>K%-R?H?~<-8#7Q4J=T? zP?{Skj$JMO|2?}-K}KTp>7s?P{W8?SF4J<>k-S~^f-Y$%2=gKq8z54-f3p(rP#FqR$9=eHp|`m<24>IjmL{Tot5;rk>?>?Nd)(n59vlBCzm~t zpsG7TlG7u5x%jAjPx$C5D)OU*1is@v-KjqUNaepEsMC^Wrc!jaj>o+Ro!2y__ZuB> zy&VEWJQmILiEIVPJv~5gYzk!GSo9Cr(!q2lHC3-V#iT|8n^{!#*SSi6J?pY z(&Jn%z#J2(<};qa*lhFGa{Jc5*Tg;_vHtSiVL<|Oc*%08TV3%*j$4u!^B5*_K^QYc z7dT#wJuCXXMs0uFF0I_K89rwM1xP?#J%vA~=(&e1p4hC%DyTCM&atEN}2NL4byxwp29=y^_1Z*|K4U~rbM7Z&A%@E2|v zU6KN)VQLD6l6d_!)1jOg)Nx7SOE!lde0$GX@3t)!Ac^twqWdIT;|N4`SOc$}kq5pj zF-qjkhJ^ViU$f{XpgX%6c;U=eg@oJO&x6}~n;mk`-1g-~`c>W0%2Qp7;Y{1#KXtVF zbvf}YU(>rxq?M3F=L9Bh`Rm?RrtA1K;k^7ajI}ayv;E&aAlhWVB7wJv{jH3AMsY}!Hy2+y-@wJjt_ZUM7{a2XJzIDUo+$Q6X_;I zM*Y(n)(MQB)qyj?DrP_&v6TC6iRnNHn%0H&Z#lDr<|5s}VZhYL!t(*|)3&=(B;3{1 z^wS5F9jre^^=W$$>Qh$obalf-x?$b!LMum_g~ytlis#M-#<(DYL_1*5%o<`pJ>_3< zI6<9}r7Ml+pMjHJ=D1)$6f5iJuALP?c{pQHk8qb;i9qd5($}1`$DF}#aD)Z z@~M#jol3$g#yn)GLLPZrqQmGkwm8Wh(>h{PkmV(+!K$(mIcXWKysi9m-fk9ibKcwiFVQ^%aDq+yQJKLEW?3Qs0i!xZMJ+u`RQ zhb&^y5wtj7a3*9@>Zzx=Q=K0_(n{;ECUJ!SlJ_{oy=kLI*zHqQaf3!BT2G^p?yUDD zJ0?-oQGYG>G0E4#-U213e&l&|bxC)<+rDHi5+D(D9A~Zz;WQH{HF&#PKhgETK;HK4 zgqErj$HuC6jWC_=zYHp)C!NDV1!bwp=9=cWSzet~TV~}&X65SSNu+dV4%=uZA`dbi zvD;5LV8To=lXqQHV&N`w%eR-IOXE~9{|AmO6c8SA3gXbu;@qwSlyS-fcaN>Fe5z{{ z(tkT8dQTqt6IOrOej0j7Saj$VqCkH{;Hl!)k5f?Z6WnW);tuz%Kfb$q+U>W*q~wQ- zkSo55>gjz)?mP^UM6LM8D-16+WIcU(spBJga$D0$-YWXFFR`hjg?8`OVQQ7Www93r z;$?dj$$eg0oI*P+^<&ELi*~d&#oAGbz$i=~q@8v=iRQ-Ui;PFxrW_JKdwOJnSv`1* zWaT;Q<$@ZaML`#sCF zlbjxkD=7gN?4Nbph-y6vulzK#t+W4Y!R1b%@FZOHr5#>oprz%SWQYveV4=!I9&Q@V z`0@(+qsvve^>WyT(~^r-%hAcGsKTblWAvz?@sJrzLd5!X>L|$8bVc z=aExwn=$VcTN){Svy0Gxl`$x6;qPMEkQXqWIphoI(n4`S(9m@mgFpz(^O5ZHcDoL$ z^nt%LD{1CGcgX*;`Hrk3#fGMcUNPubPslf*VeBt6 znX((>Z_1()_zY5{HGfbxnVxyJvUiR&r#y8yh`j@ zy3ZFL*0@=d-g~IHY&nnA!KyQuu-Ss98G? zFfdBC#+v(AI?Z;vu&UDkJMY=*hqv#w7S=$6Gf;3!-reU2NBkEVy1@TQKGyQT+f zUx8w+yP^1%sL2!?SbmTnd~Jy8JFmmZeM+F+*-lwDRO1b@EJ4F9vY9ejVSn)X^o2`V zh5-d=?VPH;Do<1SsjIgCokV9765Fg#{|oe9Lv6+^ti8iQr*wO#RXo>P`iYfJ10XBw!Z1mGk#GmiV4zxDgw4oymInu(nr*bdE)En z)P{&>y({`7ElIb>=o1Z>6+$#=KUw|@Zo`rYC?Mov8Bd(p8&~*PZ4H5{sFt_Y2F$DI4bPnJ7Hj;b(MKjV$&n&h-3cGfCJsuw zHyIUjF075+@c3|d%YB!^nWcz;e8Zqv4?R<>@}T0Gb_Q?2%$JMgnSIy7c^VW~e`%Q5 z^*CeX_Y@N&ahE_*pZ4V2D`$>a!dr&n_B`N=zV0q(R}tgLnin+v5f|< z@fGkrJ2{SIVTb`}3gsV6DW)v?67Gyu{G=%J`@d4@@JyVDWAbU5D|~U&dBQ zhMsfpy_j$Zgnup4g+%ihtHGJT8j8?|J5qRI-blEsOmX9HJmE@z93xtHMF5Y=l28t0 zo)HPW_e0~2aMPTKqjep{9W`(Qe!E{^){?0of)XIZ2SeM^9^I1NoG|@Plfj)fc-(Iv zP!CfGVIE(5p8rl-SL18_1@az$=@ls1fJ1^lYUH9E4a6v?dhGbVH_nJ)JmGVN?gf0< z-1n2&%1fJRfM=)Sc2Y`k+R>3GC~}UzQ6hcXw;R&u@)v_g{JAWd@wDaYa? zB&j+V?nL*j1X@}K2)2IWx)75!DvHQYAW#mseQR&qS#y%pJ@mT{!PGw2uAq2C`ULXc zBh|TqgAK?1%YTf-9%p!`@ARrpH!2V;E%vGZLP4GEyLB&SiPsoO_QE&o19?*drU}$9 z=TQ#UNzThHIk&^5)a0qR8{zn#`|^xCh1W<`aOuX6Bh0<*00 z9N`w&lPu%kqUQS*DQh0ZHkymA($O$XnEp{CD}xy|rqgDL1fxBb9VB^t#_8g1w7}8U z6MgRJAT`DNasj-B#KhS*;?X=Mbz5s-pt*znrGI~u0AUnFu zf#Tu+__SUK$ox^4WcL2Sc6O3wU;z53>`U4i-Knz8pngiaKjW#$>XK;NleYMq*b@WVP^xms`v2P2& z7_kau%bTB5-Fvs~Szy!6Zy%Og8smoNjWSwIUVTaXC3PZFIJ>N?_4Z%-os-WXL*Ha& z%WT>i(|wr9)ZDFEcebd7wrH!s(~?*91C##C^+t zS6N+A3)I|pfQrab`@bhKF5$SZ^5DKK6RM#uv?z|i>F-?}|8n@9tTm>-cahR#Um4@x zoJzDp&i`fu`;K~89ZcqgMbh)e?g-CNC6_Fd3^YS@S!^9f;~2@z6ghuX_vyp;$ND`r z6fN(cexNj9l~Z83=IiVuT*Vr9BjHuqFaDss$52cX2x_k zpT}(PF2n-9smz#%aAH)`sMPK1Dc`c|VZJy8i@@b6z}|Z3$nlocb-}_By-g=MT#R1V zEtz6;Avw8B!{Q{4Wmlmp{5?3YNek9{#3vP3)~1%8$|nBk9IWms!O2Cn?87gQnf-1G zx3Ub7KR#zLSrcUlYx*EHKAo3);Qo}%E;l_GvkDiBVF&Inoj;KN^T^ZVG?kgn!Z zsi_4qb+(kX*lYPsYAWXPL{28<$!004F^l?pl)gWDP3_L1wYmOte$-9xfCr+jmgz=g zD&j(RpxH^;*KgVyWdR=a(P&7&sLW@T(`Cz*{HyvQb%ElA+mVBlD*Nmfj;=g%`i=R+{m@8j%Je4!#!HvgUVBj zlQ=myih)PJV?bkz?)f4*_uB?K6G6Fz(ds5Fqm>j*LYm=kvBCjBfCi}l!oKC{8A0oG zfKawjys@}Adogkgl3msqaqSQWlDB@nZWd?cBS46)-1BZ&fE-h6f2Pmx7ZsDyubvWFDZZ8tB%y~fI(fZM%soNPCX<%M zuq~OHdyg7=k0)*q>*#%{F+CsO>1O9tZ1%p>Wg9nM)VK~&uC(FSAud=@tjFJlh~6+c zdC6kU{4F^ur#0KFUpLtjgG=US^jgmpPQS$)??2V|4|d7tR#*jPX&pKI#{+B4=425P zl}$%=Po@f~Jo0Yf6nCn72Rx4Ri(i5yW(VZ{U4G>2DFwST$bx!4$OVjTugV#L5~bT! z&&mN>&v{AoXa|La>OCp$`xR+jUnpB1k+P7#=q@s4t!c{!Vg%ly9-lqm?b$Nb7Jdbp zg9p6YT5cqIglL%d`-XL7&`~q`(GP+CxMPnEeM^xZHNKy@0(^(S%JG$W7knF<+LzI- z^1km0L>f5dMmU-HiRCKuOg(;BtbaQbt7Xs`b&*0uB;O@Hdg)Rj03%v%X2_hq^xU8m zVt$133n@C{q0_gdcBz0Q1b>-i)YfhVQ{zR42u*c-Nk|m{A`~V;F+pkqA9!Xu`3OZ3 zd>I;y=Co-EvAw@xBtbZvdO@Jj`^)-u z0spKX%+>V8Qt6jn#>~7eI34$6QetVsO33ZXm2}}A>NXZw!UTak9-#Um`MLc3A+GZO zZ{1m9bb@mI82$s7f@YCK@SgteFy8Zk4TQHIPEGN%@E=?llBv^n-4ZrZQ0O3~0Ml^^3DpaldUSHuP-}xdx zgT+l=r_UFTO*nBBn119%$~M1h@u$cHa7L>4#zTL|519 z)Hfr`7*9wa(^^*QMAH<0sCRUsTbezDZ;+)vU(Y(iX#mk5(Y~VlCT;I&BiwKTu9kUO z=rd?h@seB5V7776tDL7r&Q9xoe03Y$n?)TJcV*d!0iMF0v-pPM4+=?y3TCK(6C3%xnGs7DAo=Bk{m>jp(#bIl*15 zIMa~?izc3KyFD^sbRa(?RD^oA6~vdPaW*6}I_~;$aiqBEtvvUnNVjj2pP80_HRRP4 zxo6BYB7r5};Cz0LoqCnf8Ck-DJ@Nf3cZ*Q{cN?J|Q?CZ*{4ikGg%Jl-y+9VsY(;Cp z{vATUO80yy;QES0t82}Q45Ag`DY@kA1o7)+Pxy=s4;+eG(FmQB@F8D*&L6-`_77MV zlXGY9mpXj@@I!o*K&Jg@eGnx&AMdAqz9F4zZHb9KJJ!7c)Nfys{IzPW4RU3tgQ1FN z4KSc{18E2-Q&Hf9rCh@U$oG76bYoh6zES#^hf%e8khSgl7+|k|>pI`QDEkY94xP3T z9|gNG${X(!g5&eoBs)I>HCD93L}I_aj=9Ye;F!W3Wx-lu5n>uKr%5S5eQ`W_a-p8j zZF>Ao^-&V)-ly#ZL-at?t22Vtu|r*PN83+%aJz}S!hTca6-Lav5;q%jWq;QM!s)Uz z;1*hX2o{;yQV_ivp3Q4j|reIq0`OiTxq zj7!7LUWt&b-ie)EtB-A;b4zY0j8UjxCc<0FF?5L_HCJ(FJxuPlo3ALLN9$Pi-wCc5 zzZv6nt*Dyj-%<}@D!IUkv6St4n-QIwCTR00?JTlP`MRpoU-D(z{lB2#dPklZ zXufQSn`oDt2xWL~Y9a9Kdee5+dy$VT4f;p%krGR*apmbZ7;FmDZa?{&Ll z3x24W0l6R|Rb}lF6 zoUr&4#}DQr`FOK?k*HB@aqnyjXpYP*F?o!DrEg#D!;w zpgWycR8^!Zha&k7Fbzq1fj<4`G`s@>L6EnCnfy+Y5W8}W9HPFrUQGYdOLHXSo(>6> ze9KY1k^8ejn>oL@nEQu0{emr>F-uIk*YDBqBx2?Lofr4~)U`7W5Ysou>p@s3gs&Tc z3*jACL=W1F;Jsug$KkRv88f;fn7~!b)a!o((Ce4Pxso&#Yef$VWP*D?j6E<4 z4ERO6l{0l2!mzs67b?S4LC0Ag}<~X%lgLccGDi;;LJRw_M{@ZmL3&RIz z(j|8FmWeJ`4rd(7Zo~{n1UhX+pp?9&Jwy0XQu$fu&^rSoA-z%y#mBn%ojK`<`nku2z%w zcStywID7XVMWuJSQH`e*l@Y14jJ300<>?PyFS+Bp+(?o31$B+_zwjrI<=BNKrGHj# zCvWj^4bf`H_Q^63HQxX_?UMDhtF;`)Z_mj%q!goH7*$ah3QE*FO$uP_BDFl9$!>Q# zLO_EP67H=ayXoa-8-!FC2a;AvZ-4%)9FM95ZkSslKP4`X$e9a$k_P(^@{YU)~sKeO|x+|=(p)#z!8 zX&UdlIk*5RY*s~Gq{+C=W?T~xz(GjG=fjErJLUE`Ya$7vGI z3EXQ(g(l-k6c5)%n`5n>ei|)tsa$^#b6-4&UR)~|6uQcc(N5U~gfrx z4WpGt(y|a+ehj)TjdKjG#k>-}_S^-3Ehp?vUCeCBNkq)j*X9kcwYQ(oyuxp|a~lcl ztvTN=_%lXM|2H=9$XjzRD|L6#z(RtjKH$z0JmaPD^`MCluVe=^dIi(3B<=`x6qf0_ zet-xM13YOD#e~G=+iA2Ij>!B`;#c~lKrp={mYm7s@43|YC5j7o)MOXc5bS#4hI`xm)w7T-tfl8s z-VDv3x~CqtL5!r8&g6iI7T&@yTJi~FL&qK;7;SBgK#JYzt?q@N_9h^r<9ji+p}0lF zfJ(R~S#JeW1df7E7RPzsii-QJ==cXNXjb8n#b!$Tvo8-;!P!fnq9zKj$M!0*1;3Ae zTaq0~=dM?d$GNN&N#w4sZ z=s)B?A7Iw_x~CF(*ejtX-L>VGX2#6cajPiFJNrsOp3hz%4Nj{(B-@#V86rzDj0U}u zlI^fV+QTo%4a3)D=kDhQ1tBWv6FTmTFNc+eH;FKPCA}j|7l98k z0sdExbc`2dXI@BMjP%Ptvk*SEVH7K;X3B924|XUABz}>ZTtb5{C9v@_t1(B96quLn z9`bF23!~fXvW|M_;nrZ`uxndwBMVT0a0dqsX4zY^LBO$VDO=6ihmvTtzc9Pg$`)+w zolV~D8hb|#iXdw*iWNoVv<7}Tk+d=sjL7HmxF(_*{CcJ}r^-VF54vs73DmUx(RkWh z8UOE>(Jf$U>xR_KoY*kVnBH-`DGSN3#JkP6s!LW6Zy=_}%WUIP^;@+FjhoNO1apa? zemex9CfG!zY2z`|t_mgAoi;_|a&**7c5llZN^|AFH!IInc`sX52zO{FZaOT+6r{wPp&E(4LZLE_rYA#kP#e#W|J3Kti4^@D z{IgiR@NzbR6lUrxI@O{qqkAz#K??VNjB&;q6BJZ;(zq^Zkd#bQS0smX;?r4oUxxX@ zRz`N6zt(R|kNaQSQ{uv>mH=&gD&I^^Zv*8k8}vIeTCL$^&7U%}PL8-IcMKnn3?l|Q zGbkpnB1Bfoeh0)XM3KT))jq~Irv^OxPKgwaBjs}u$J{st4nTlDxqik{b9Sjs^cY*+>YvDexE>|tqNB}}Si%(6Tb&F8E=L_!9 zsyOqMj0&yW0Mf~Y?JDS!a+FkOZrG(yYM<8Ja7`_R_nUydD`ynT{A* zu>03w7#-MWJ|pDzU4^NW+fp`UDOT695b+MMYZ&9cBDSQx zCI@!AJ{Gzx4SZ46)^=BKQ;8cpJ+$InHEmC5jHnvlUx?_X2x05VeW^P_eP1s!935LX z;0MxT2YdMLQogOoh`pba8|n{ne0l%B}t$I%5hYtVL~k9iiNvcHkTn!d=$s*4Y^4Rh_}C4bAW+48}32{@bMk z->Rto31*OLj)DtD-tf{J5>8nyIpSTzhAT|@YvkfvU|u0*cOHkhWf=M*=A6Gm8aoOT zqF$!_+@EY-j%jr!`^hSy_tPUf)%~JPJ=2XkAcYy+_*ji_Kl+m4I1YSCpuw^xp3 z^27Yu)T5o5ftCeeS*;bBLgJEhGn(trmaJk)m3&$8OQh#<*GE#TxsJ8vEe81xKe*TbG=`9?|KOcy}GRdz{`+@rXAl`+2;>|vLgjrBk%AA1yY7xinqpe)!-9Vy^O%Dz;okPxYZvZQP1Ak$_kGS=UtU&5(Z zP5L*Czt113tKo1k5}fb#i(*Qz1S}aKF2r@e6%{@GfyVtqDGEE)%<-R)-uHe;&m@e- zd#k5DIzQ+bk_AWwL;M510Y*H9MRVt&jXf(8cnCS0#v1?)G9F`{#fDyr?dODKOh8o{ zFg?od*zQl-3E1A>Gy{J#WuI4=p(Vh8nNu*t8`EfdLVS1kQ%C8hkStZZp$MLdJ+DFb zXmXq1ivjNj1`TqU8V|RWD}j1dC($_ntPO#Jm2c{Z;`hLBz88euc^dYXBEAL9F&eLT zgHO#h)0!(XeEfO48#^GG`z-^2F#mE|pa$Lf1&U?&1ZO*|hoCnQdvf)^0k2tyANOsT z^X7z`aF2;h29p@I0!)tRR-LsEB;U7g7>tvK^eeRTQnnb2yN@nxBKwbe~YjaS#wv#g|=~laeM#zYPFc*^j{Xf;7`?S zF(`7qZ;&Wa2K?MLHc<0fMyjgtTC!^!kcx(0vq(Nlp!m*As`y3#qXf2lT6!B!%Wk~o z9vJ3>ZhYBOJGliX%sR&*9}{0gfstc9LWu)caO<*z%dQBJUu9#%fPFq3varSPtrzfD z>YdOkR6&@i>xCdY0x^aX9|+X=maKf=dg>~q6*lpa_FMrIirlJwdgXQroy#t zblt|!OOwUdi!gYL+4@eSGJ|gs#XaF3x?wKVh%VUEsj;O9Ok*0ExB)i{lZDOk7lWBf zDCAdxO2>~6Bd zQhr!XpFWNcALt2)FK;Bf8jIAS-l3cWSZEP5K%9yRfU(>s*rXmEx(kf)3h;-3*s-Yj z{q60H-k|5?`jxj0DJdD!UvIiyE+`hpelVz+(*Izcx9_*!+Arxb)x2Tf)Mzk$rC%~2 zK|I0dNa6IrHXk-2wx|N}d;=nYDmppEB}A=S>(TyFFPIC+c3Cef(Q_5gKR#(tei6PA z64U=-30GGL+`uXP!f#iG=b*4SkItIWUs(cC66T9Xi?Mah)Yx4eT?jlXtf6H=CPcXX z!x)1XWxGHrh0VlBZrrL_amQ$@o>jb8(Q7{OVMOVND|8ud_$7>b8fP%D0gub)NeCzp zCn6?vza+)5G*-@T|60S3UwqdQ1y>1|2l!Xe<=q#F&WM-{nV3SgO|Cyw7!xD8R>onD z<=G_P&r~$KIIUOH@S1CN!Y}C$AV`pB1#hX&!aMJMS~p99iah$EVf5rXf~oG)@CUb28%pko6u8N$>6Z_&JSp zG|jY}xzfr^O;gJ~%8{M6l@mv5jzV!DPQa!!AuCr7QX7_vnIhuem38 z;`q^XdVc3QpYMMlzTU6becji6-Pg4Mn5e8EUo5*!zA*db@~)@sF$3vdD}VZx{Pt$` zn>vrkS&W0s!mgLor%}##@eZf1awksN9@tN<H?{H4Tatn=aIlJ*Xibu9UUUgu09 zF&ejgd3+_h!HM~pke+JS;V%PwHGJf1ZEZVA0?`uZ?`-Mujc73|l!oxhQ6$@6!&`*x zyuSiDDT6zomay0xZCHC=1c?MBe%oZZe`mdnOwEZMXNYmB-)Qb3FO)g5e6!c4xRwsG zAL%jcI>MqUOS~O(Y@Ib|v(Do8tJM|I{6 zZwg45*l$%rP{k$gy1&0;A^l8l-(^KIJ$9u~VHI{$t`I6owBL{p(A31XP8VMp{?Utr ztF3m%_#1yB44daX-*><=;g2|{dEqzOs$>ZdmQuJqx3rN8J@hOI|7R8EAhL<=G1Gui z;;nsx^tK{V3BFv0-=KvEY2NxnNsoz0T0Zg^Dp)kF@ul?@fzWevX07~(bK!6EJ-1{z z%dz2GfrK4k(T5ZY&)hYk{I1vs|CCOFUe9XY`#H-_b*WM-!Khm?^8+Yc zy#KigfKVoI9nA2l$Gq_>Ic>IXJ$f!{MIidNX}dt2_=8wa{?b!etYrNjzcAD9mC7hd z+yj`=82&h1Bdz|0iAlIVzNc9Kp!IF!z(NPbGgp5oh4GU*O^t3O)s$$KdwqG6wbMl8 zm@-p#kQ_L@q4t~nRngD$%e69wk$d+dh=ZrK=P_5|La#$h<>(Jq3%>N!2&_Y`N8g$; zYY`(6xtgDh8J`lL5YAY7&Sm4Iw*v*BLn;jGP3S1&(OY$sJJc>IW4a*%IsgXY)@mXd zw_jlS*WSdV2ls*?y}sti!FAIQ?5m{a5Oydu8D!8b{zH#al1Nqc_zrYWWSBtr9u1~p z8hgT#8jo!7mV1*tViLmz1EWKrVA|EkQ<~5iK~~%1I2|3`_J)`~fI#>C0W5le8~Pd+ z-5;jo$6g3-p_G^0Do3zk_SiC=ew-q}d37d{3#>?q2V=nrRa!y`0UC(1Cd==!&f5ZE z_5W@;THO4}1zrWyeiFU;K%%xoyn6J_z5J4(-3d_=jJ3`% zO*|$kOZA|%$$?;pxpNZc`*()`?K;;kL5U`MkJvjAi<`TxEs2YsKMpFKeT|$B5iM#c zq&=*z^%)J`uYMy(7ipAS-Y+3G`No8%tFuOPejdTns1de0asZw)GDZZw)JuM|d{hee z_4L8z>-KIaQ_0YV5zB;W!!${7HTH(D(1a1A=C3ovsS0e+;zxe!P?B;NLj5na!~ zEO}RZ)&Z%q`#TtARTkBE0xO@_r*lDI^LK@nk&v98@dP`Wq)D(sIbtuPUP=l_f@&e7 zelMtRMp2%S71C@CVcg2PULf5zXOL z`r+EJTQZ6hYT-o}0k)8y3MG7aGw$+T+e|F36=T`p0i5gMCb5A&!limu3Q9N*b(6N7%d{Yk}3%GzOkilqN`1_?wH6ww^^p0-= zq3H3-f%n>rgMM)-1>~aUl!-6O$;Mbb{>HU9C>(8ztd8Y%PqlT!C^rz^)I*oU@z&*aS0PTnNHSGSzj5lzDA*_?-|*^3f!BygHB zwWy9X-0T6FuT6S=uQ*6-VFY2R>^@U@Nc-!ieVPJM%<#asg@d2n*-Y^tC9!P2!C;*U z7n{`n232Vp6ql)I5HeFczd-3uYB!g~+;!$%zHqt%@R(gRYks38nq97P(9V3SIKigA zCO7on_aG1u{D6rWJ&N98Ve;AGUNsQB7C9~&@|cV9MlAJC2l)*1fVgTje&p1v83@cF zvZgFGX*{9$H;T;y5cB#Fvgirnv(k-(kpN^UWvfnji+)CCki%vP4#MPrl757EuLMS3 zLbx2u%_+-GsOHjPb4P!pcwIcPcd*RcPqO>8-J12HfN&LCa>6*p@a5eGK;8<_w3j|^=|pG1xkWw>$p3^sPo5{xC}{Bi9!;>Q_T zJw-*@^2=-=GUI&vWnpRWdP#u3C=B!<=75Lt1v#&;k%ASsIiEJWls)$l0pX3P(DNsx z4K}Y|UT`FyuI*@$1;Kte>s+~o5DS7jyc1Y%yvpC#0OACit+QUSFOFJ)!l>mzXBH5v zuE>cs_}q>twI`l$gePJGsZDATz$O>JRiMKU(w6+gs+^Pa?1I{UTbotyg73f}24DB@ z#nb;f7zxz9;479g&vbN>IZfExevx?f{@&jf46WLoz=*bX z+uIRm>Ubu{KH;XxK$&Km5z9@o`^3%KQJ%W{;D9P5c!@Y*d{caqz9O=kXCwOTg}3ASwS%SJtN<>z08TTPP@`M+WWntzkwX zZ-mk^K71~3bU8y*5;wG2rZRY0+)yU!z%m4$;5IwB+x0-LeREJN=E*w!kmVs_O=`mx z?MM+Rl<-{TPV@bS@>v1%yF=0hVTqVqBP$jJ@%y*0a0|S?hMEB)j=4R$cO4o95uH$C zygN!-UxVmnu21+mth-$p{g=UsxT>lk3Pk1Vl%O_TwUoJ|y;4LkG>orDOmx}C$VS*p zIK-ckX?smGRyJrT>c2H(*KUfEfN;bl_S;;Ov^E?OzXbviUgWqurWIN>8o#crl{Qh_ zMZI2o=!yJqT9+*7K#$amHi}h7g6?FlLi@X@%yCuBljVbYiqROO0$tkaHP~67qUipy5tk1d`{^q>4 zWUW+NL|QegGKrZZFkQ2oq4&9J)xjZwKG(5)t-@fRi}yEO?t!&7g`C6!fXvp2seNR zywTnzYt>~a0*+{m+gecG?!fPR1Jzb-(oj7dZekKFApLrfTpTsc92%;h zwY$yI!dH($A6%>}Wmd1tVRuo+6&u22m8EQ5!kviPs?(bjF=KpQJTlGyap?%dVFYNqr`6()2ixS+K?j<0+6~YFfN1JhS-^ zr54G(Quz;6Rnz}o`spJ6_V0<|RXRby2XAkP^9pJcU`7G#KV)?M^^+<7xGy=TBnpru zaG6gj+3DfV_>gwnxJ7qmEu@PWtw0KSP;&U|`ij?=hei4|qveN2dW}ooWJ*MCX;3}~ z@mhULCx*9+n0juslZY&x@gUI<@BM{2;=9HdCR^VEu7rYsB={pF9vbO+gr+oPIgGlac!QQxaHk-D^%fFp8%>h*^Q2 zb`W#2YhMZz_;~#Opkg1=0~r30$%o3P*eQ0czf9L{@$h#pkxA_1Sk%#!FK9myTy~9y zzsUFyka7qbvtWDr{it8^o4#q}2|fjI2i#uL1DCx&b{@Lb9qSN!fQoNqfEI!=ho9Ja44$lYoFcpiWzMp$& zLDasTW0qsSPct)ie#0a6n8^eoml0YIr>IhJo(Us>)-3(ZH51ns7s-b+A5NbgY-i0- zIECe9(Y+EDrme`BnlSpFVBbrxobJn$hjK*9Sx9`q6@D-n2=NB^*fvj?WhQ9{vquc< zw#pXIM^5?soJ3W3cVRv}KWKOV3Z!RWzB|@9`xYy2w43f1&)V~(Lt#3oq3Q(N5i4B= zS5j#<{p@pYdB^etHhT)k3Hrf(m-`JlJ*Pi9Jl?qNQ>VH_Cs^`4e+|T;m;dGBAifl6 zaNJUS(Hosu10@1naF6A0NOv%$0!fL=wkkW@VnCeogxLb6QQh zgJBKA_xa^j*Qvv!FJD=97vC7qotlkN0`Jg0&!|a~aizJwUnb|igw(svM}vzkA|#y1 z!|)rwnQUlo2-lPc`$RWg>^gvs;x^#k75(NhzWfQvrzuX&u6M#uUO$^dIjt7jA z)?@H6+|jM>kVl5^sstt|7kcC<2tba984+xxR&QXZQA{oio?j54Lz$Y|j*GgHYu2MJ zP;Ex5aj`ce5KZ z0mhvj?!ho%T-~iJEaR#54-Kw^yBp8y2<%8mF#+fhWk2W|+x9iCZcrC(*C$M#eI?_$ z9jhb%LO+z$9>qtdDr_>6L*oa3G3rQ~#0r(97VZz45vbNXfEfU@jAis}>1EM4M0AVOG+W*Y z4v)z8;i=&~+_E{)lUSqL4u3{e1X}n9vLLQ~eAuPQ@venG2XOfX!Pls>%$FTV$ekHA zDP2mVT?%Qa$PKEm$S*&fP!mKSWS0xv;lANgUP&~5EvM<Lz9Dn+IGiE}nqDZcWy)5^z9_$Z zWYxHIar;)zjhLxYrPjSQ%_$AUyEFS`((KWMV|nigNF_ohnw+IMZraiSa3e2&!6$=$ z03~4>wIv;SNHI{}Lz8;EY}?yMKI;XA@sR#U^DdXTn<}uzrw1qsw$iq?fd;U3c7#beLR4_YYWQac2%yz%(DVGF?eJ#Ebu zkP~SAh*ZYomL7XHw;bP=tQr5s|YJXI&HGfNkW?F%Ph0EaJO?Mj_QYL1NG8Z>9 z1GnS~1zKl~sn`U8wrk9fMa;W8p2s2a>izksYG5z<*AX~#1CHWwkLbJ^q01b zNK+47Ap_H0Ll8#yhs*qa=#HhSjN;_zm{0iqlAFh$11(Vxt66!A!;Vl;xKHyO-NU%K zXo1yJ1^upjyS@nxD_$9KIy>+grufNMR5qw}8B>>Q{!$*ALqUEVM$^L z2p?#ia&c16pu$hc@XZlV??d~!pPY1$t*JOD@u?~&5-`T?fX$wezhb$Il)Ty4xN>w5 z7|QgFY?m=T_pTm*yQz=Wo6TKiz>cEy&5{|7?mGGMy7lV@#+7}G)NaPDE1aU)5kQ_R zv~l5Z{oVP6VVjbNlhI`?JL!6ll9hUDOn-Y@1j61T`TBF86T(zTx+ z?C{roNIbp&Ol<8XGbh2{lo|5~`d!|wa(n_aa+=kXJFb?9EK};BFimXkEU6?ig3NtQ z3_scHw6zetab@W|Tu~GPv|v4V(C>Go7qNlqN+!W?v>2e3uU*C03fDHLU+;Bq#6gU# zST)-AX84%JI2+E|L5?^~gJWt-Ez)$ec|ClY{*h)736wKTziDKd(gnZlowH@y)tR6l z&%V2b{KW4d;%h^Ix$@L(ts#2qTFnDf{$DmqM_?W2v~ zj_jw8_L-YjGXkU=MdMG}(U&Nf;qz8l_Fv)Ihb@BZ&Ur4dIx22yhXQlYhka zHi+dmMau3q?@8R_spVt!639W1qvpzq^#dyd1J1w$S#T5*~AEm<30$0rA2%QSz&@MYL^gqdA)_; zAZ)oqV5p#R3aA~r0WJKkOtpe4P9rT{fdEqW1Z90p{UI(kQ~n?dowIAsv_`kCamY45 zDEw0eZ@FXI*vZhi-D!raD?<(n>Q+yX_-lnAYy9nBQg^HTpUd0=7y~rSZX3Uv?;*52Z-&s8=gb#prM5E}Jn*z}@wJ+0UU7VSLV4 zcFcBe@x`yX#W7F3vDIbZCQ*T@u&cwG$MkPV$}ME1n=(c?HX6O?6DT{9jO$=jS5Le) zAi@uH58_h!UDxK2#-E0!5!<&5jnG7gXBdMpf0O)W^sStPRDTQ{@X4g?Nn|`gI43jd ztJ*W=Pu(S;9azMqY*3caUB;SX7vBFp6J-sd1xlI$LLRPbJ1 zv^N?Va;txOTt&$;VRH7k(^_D=0fXR6aqQJZGsP`S64oT8opq}3(C*R}(DMfQSw3dT zw9&Omrrh9JMD*9nT>4-VLSaCy-@J79uK}ox=szu?fP|{w@A2TCK!$6%MGM|wId)^A z=fl#}&*uomsKxuhl>N+Q>dHU|@IEfW)3^7pJ54Zr_(nRci@T#0N|NWt(6z0V+*%Kf z)t57X-QfG>yCOxB=6f8DN#`eVzK{kTI3tN{;IXSw-r)4j{ZJeq98qe1t4n= zR3>#ApR*^mR>ux1FtBaw9Sid%*Othd7Qzi03oq5@ExEXv6B932(;n}VF($owP|(E~ zU{Rdd_$2VMS9gw2eJlN*YUZKed`7~daTi}BthP1gcrE}<3I1$jLOaLDswR){1@bCj zo*y8;^X{Tg|0B+bhk@U(j!j4TA3!G9XcoK#qZfE*`+Lwi@NQJkg7CZ#na=SF?{(8AoobLPWM2;7B`J^!6>Q1;2 ze73@+hINMilqh$2_?kA8Gvju#E-bN&Ci{8VIyt8%C=MJSAQ7HMTBF4Q-J6tc@R0Lm zc=hK=j2!1zoci5z(<>K_lZ5sL3v5cV$9j~`xyLt`T2(ww(ec__@%Hf!; z@(&S9c8BkVM7dShqI6gTjH|F9F#nn=D+4+l!!{xJQv?7kQRQo;_~PY25Dil?nFU)5 z69J+jRV>Zky=^E$kWjCjV?{9ywxx86So_$uX@xgak{^t^A4M^^-*I`LDi2vwso6lK z{>wK*dt8#96{cSFU>^Xobq zVo7(S@J5A?K=7Eagp=wYB-?OIDTqk0g)VfX9L5X9G+~q9^aZ zQyFo{MLoV7r=_B>$;ut`xQYBUV=&e~>#EHj$s|Z9G5dBjU^0nm?c~P&okwv>J-2-l zuR}6T>z9-ifg>iHovjqbg7QaOj2%X+P^2@fLGzALA2Eu4&! zKU*A$dEBR(J;E|9`F3c(-F*}KV)f#B#Pd37n*gRMLWe*l7dbrA^B6y24YgA-4c13z z`iq&fiQ@tjCOe&WrduUv;3Gp8VG$oMBOwz5Ganx1q)t2W1}ekYhv29OoQ3>0|KwjR znxW7?*ZyaA^wIhwzY7N90=A~}r$(V)dm-J3^|hC)iI~6D}@nAj;+UOVgscRiM;?%$@i; z7J!2HRWyx1R@}WH>+TJ@YULv5(-Q5J<&?j3)9qVlbb4dx#1})EfIt zZE$hc{fjz?6n$X$^2rpIv`{eooAuo!^nB2xzk*q=&Jdl~{#O>X0!>!aMd~VNYVX&W zrwn8s(z6?sV@y(uy38|g+$Skag8-)yy##F{dQ(6?tS_ zT|(5AS#ucU_6{~-``29`yT|Pby`mW!dlXTgOH1=lq=zM*f4@ZE-qQTF2-A4yAL1(d zisWT0A>8xzooVZpYJQQw7mExzytB0#XQoROKnI2YNy@ zW{JJ^j}QN%pW~(xQg?xzT8MEG$>X5Dw}0ac7k`C|RE(8qc5A3j~fv&H3Ne zt=O0e3uSqA1xc3aNcEjqi;g9~#?{+aZyogaJN)rrV=hLi!{kK*=0z%y3G^=9(jB&b z{W!$()+v(nV|Sb6Ia{aO&sR_Vz`?$CSs_Wm7o)nZN1(Q5o`u%<3 zp#=N|Gt}4LWY_09whLF2q~31pw&m~b6)ZuqTMngPsScCw z2DIDE$kG+~+$$Wp1~jk##B0;!IY8t`faz@4aL6?A`>WMu0`Z7R9jVUP4Od$a38D{K zyj|+jFwz5sMV$IB&Tbh#)PugFjU2 zjUJqgs^qV`<aeO8STQSF=aG!)ij;CRF2M{UBwKW!@ayk;oq+qeh#DesZS2o` zmDp!Ru^`A&@KgO3p8GCAfG&~&5@wA;n&_!>fGfv0D_TEtY8QW8`K8Q+w*8Z;`qN$T zDYsvn#8tY$bLp{17ksT7SIqq)_y>S*>YzZj53dT$)*Safho3197ctZ>Ecl95`4S*- zQrNjpijP8n`=HVfaM}9?{QxR^$H?FJ!eYT-&c~qT;3nl_Nz7T;eI~t>SZlX^^+b*t z;xJ5Sv!OZAHslHZl!vKuTOQlix(E;zY)P=j9fBb ziTSn~(ne7E(h)AgZfwaC$a35@^A4l4EhG+}Yj}(bCJz3UFZ`W(l7Wk|z`JC%p@Xl# z{-#Pw-Fsyb+csqIQg%PNGph-Sw^t{EOZ1$TEqfBWoCRYaD-#c-&j(-Wo`1K59(x3i z8-j21^G_mUdr>)VnGYC+-}Vnnb9tRKKIqke1P;b!8$S)k;SH0#zEz;RbaMw>?efTu zg5V96W*Vk9#~4X;n@n(PIn4qpU5gd*zj@2xPb__YRX-QsAOn70QV9i5S2ST_%_w#v ztogP#{`1k>ly+CP9*9wONqYbI@S%ON7AYuQ%o%A7a_cLRl{>H>_R`PX$BhW%4aBLj zxo5-6^asQU+uJtRu0=+Mci^i(8UJv*JG%#kuMQ*PV+%({W@Sr2yGCAXktu^JN_a^! z_uYfFZ}g3_rJH}Q%~B?L?}lMASl)>$f_47f)|SbyCu`}#|8Q&q!(^w{i!wja?{^k< zJ3@vh9e=sWiCiA;>Fwn9hXUysHsq12Ig{qlnLaQB*Vc(gR#JR>z|wsums@HaF& zwDPP-rDxtDP}S1z`OUIwTCBu@2qJEX$fdi>f`C!|{Wc2>x(pKLT~~A5d&tQkL!cJi zr%B|W$k%9Wdgy%h@7V1rssF(tD&xW`o+@~*&6K~!*Q|jjUj4hr>AKLEjqb5$;6HN- z)34NAvCMtGUIxlN8!_FjFS7wU}9r2eGh&CKN})r9OKL_OHkv& z#&q|LmgVwVLND3Xs=_IauwE%`nAl(?f(b1P$~klVgqBHsK%=AEqAc1J9OQI%QMw%9 z94wh#mfBFYnxc8b9RNR}bvri4A${+x3h|(IBDVH~R!yINeDx}<-RY`up}t9h!z+tc zs1|j7ZUE0~zcy)LM}3=7V!pwj=su;=WG)jlZ>NmSFn=vv;Q3^|ArP}#UKE&4o8nen zPF5JI8#O#`ZN>#Jf-9km%#(9n+2@~lb?HLRsyUaBeeS0ozJw>7{o&QWsQ(tIgtFX- z)yB?hH#z02$5R0wsLYI*hyOB)oGN+vAHR9(uKzGB^_#0)TZHm^h02tBG*Uuf(=Kd8 zyJ;(9;d|vn@_I?f*z)yQB@eUsli1wxFV?TH%f#E-QvH3O>M;o_gd%IDJZ3hVWLlta zX#9YqD`}Gax$=Bn*lD|W)tvo#6OHKFkC@UYp}nkAP7xN4;X!tGF`{kS?rVLmP&=7Z z2+Mt4Y$gE_-9CI_n>;iyZPa-p=$J_#L!9pKHQZ+mS5i_W4T9~`?Hh+RFu8TD7!NHl z^5dpI+t;L`gTm)yS`0*d;g4cUOe$YxCz#7f z5a|l53T+7dyJq6@j!NNX4>Qk%=2hCMG?>%{vwnO2=hjT00A95F26VYk1n%=&b;BMN zq;JUFD3B#DaZ?m&jMuLcQzAZJ!a66>60!nP7`N@WO39TL{k!HXfxGkHMTW*4roI}d z9k7tXZ+NuR-<8*Ojl8J%Z~*b^1+BmS&kz17`JG~ZC!)1q)XDTXm{`;rL0Nd!gH;L& z-^Ke!9%0*rgIy1lzpUJpovs%l&k{AXt9Tv;L^?((tgLc{H2YEu?ORx1KJndf^jz0m z_5QkIPhy2Q4KtiC1pWXK7JI*aU^?iw>#$X}$O|sQOWE6vN>rk|b$B!JHmo_W z_}*ib&hH&l)*sw}b$%0HPMFo?ms>Zl?5Ip;NSk;V%8eX6C4uX^xZ9U}?^Xe6Gp67o z{JFsRM}3%iTFYk*SG`n&u8})K^nqKpzK%Qwt8TPEglfTC7OWn8q|Drv&j~>jBU<5l<&#jF#TY8?j);I zjDfYV%wsszM^V&+XuBQ-1_w$1d!Apu`)@-1xx$R9YSZ>AY5S%PTtk)Y5LnP{d#&o! zpVf)NeONMm?_QKZ-~g71q{FIj{m{g(Lr=tCMNaeyBr4}dnNdy^kC9~$*3 ziQpnnfkr~xDCg0$?TVdi{=0nmV z_dSG{uwuvZ4*Qfwdk3uK6#JMVz|#k*Z@L_2S2Q+XDg#-J_0Bw{(N~$UWws?x{@0H$ zm;FCIGW6@Np96&`KI^CI_P$b40KF{_tG`awc*GBx&30ofKjuTf=NuTATR%@R@)z({sHi`P7zjJL5-CcZFaLpy_Hz-63}t*&1YiW5iCuxgfd!we zFrEE^WV7n`Ex8;MhL&<&6A64J%#Aw&S2HOOYw%a0IBlX^-{YAjtE${bYL9ARkGi;DWEP zVvx=N=%j%N9t|%4XkoH14X<=gk%4$nZBk|!aCi^n;1{Ua4Ultj^Z`8NaF-wq@*W{a zB!pz6Mi*$?MSUp49_NI@H|kb6jVt2I2ggj~tq8gEf~&wu@)d9g#&eq{)9hgTk|);| zHPg)K5k%_81+3QghL7tb+}s`%`WA>QE>P2RcsKhW{diaN*~!nxf}LPQf7e+SR-Xjca(Os#+AY22;Oh$|?KUBh{#Ry>goC3035EfO^y*m$gVEx8_deYem(s)VFZZ-QN07X`hu{8X~`#_C- ze>EgblUv*5gG(pzaRoz7y#sUH+B*GXQFJxrax$+&#cs{pY=(}uK0`nG}n?D^pOYON~GZ))hLA`N~54}KH0?^jq zR_^~6JS~oFtvZ6&^p@pRnq;4B)Id#(5yDb@#1^$))DGbZhxbhDxlJTrP4q%d@Nl_g zdK5f8|IqgQ`{1p~=iC2Pn*F)!|BMwHKmY6z*-e|PR}GF+4`eI$s4K+cjo@+9D!u84 zCBiW8$AX(KFOHxm*HeyodVNX0>EKk1l`Q3;4>3%q^xrxIb9Rxjo(;~-`BF;|8&+L&%HkTb?l4Vca0~W9SkwzvK?mGMPB1I zeuE0ASYBuBUfWR!2@RUN{`%$tOeJ(;tuZ})e``qoY4E}i zZrZ$RGo>%V^EH)k+m!=2U~AW-Nf}Wa9E~FujJ&{A*+)6zaTpn9A()&1Ce(-}aOM=S zi)C#e?CcE=e6Bn_aM~(zwp%Xdy@OJ#emX`T`~qb>Mm>6ay?X+-uVSMv-ZbtCYJ|(5 zgqbuqX`LK5Ryv#|W0rxN-a&nn^aC^HU6WdL1wb>Q5+UEXB}o6OXakyX#KMwEC}H2|@lNDAtVIT5n|_VWmws>iKa3~w z(BHS14UIey4OV<$WTMy*m8#v99-v^|i2pceze^^2_plbRHor;Abu**6l&#+a0pSyF zG_67@a-sQA&aQ6- zy{#4aPuU}7o4v<1V^TFh4+m#sRlmh9Xa0)zUOM~#{3unykAiLs+DA7V_)}rKN}q9? zr5Q=5MSc1uqEC;L(&OkLtneibx<8Isq`6^c$90>}dQ=4w*cXmnnCYaAM5q-PrG!fh zbbA`yo5UKrtH%&-D1YngTLizd!j>_2F(Rx9S&^=^Wmc`M_?N`#q%0w9(HN>l!Goht#D^mp2)G5BKdWO&aSE-4D6&VShpbl>zkl-oo4iz+?s?VQ3CQ3?mbFZHf_bgJ)sKqV1 zISwO_MWX2{qtqaouVp70a)Xx<69qmat1}nkh$mT^EynR2_}#1}u2k42j@I4%12;3( z$<1ivTfE~o5E)BqT6Pl-pRJ0Dz-|Pukrihdrs0DA1k$!QH7{=X=1kBOsvOr>swk2U zS}r&5eiUpOZ|zCmn7M`j_yeBmgkE!yObN`t;3(E&l5OgSB(zRY!KljWg&67!0m%3V zir@af^E{&Zue;o&sTzFpQfzOZy!CNZ(zX5Fvpj$o%17?-zP(LWwtlF6nt3!=s+pVD3qW310r0|bz*}j3VQLmtt6#awVt3euQ-oWSHe$|q?0Bw zJ9dP6Hkt2j$%i@hQq3!x02ALU!MZnNBDD0V_@TJ~5asJ#10^tnSg!X%MWBv-!shp8E=CYNWy zeuEN@4{FY5^pJvIblLhI=%sbarK!_tBZW^b331ZZL3_jcmr_tcT@n%uQ9^r=M0Wf9 zMXJ#YcT=Dy!I;g^P2q-GTaVX^8(OdJ-IpNrYa{5$<9~VyLEx^3aJkFR&#?hZ-3rcm z;R$$bfOv2)<1FR0D6j4*CeQdwdSvL^rhWc3$t{bVo=fK!*dde=n37NEt3QV8RKoYX!|kKYC`+mNW73B>r)qZ z@;k`KMzU{rKlR|Y`EvJI*Vs3w)Mo|l{4zE$J?)@sZ6ii2j1^@0=<@!Oy;2r7FhCvr zXgv|QxN*V8&hM;wbU$Td_bj8y(Z8zg&{N zunHsUA+&82a+|tkyaS@v%ftaZJMGp=Fq<6$gtET&44%1wp2Bp&uEte}R%7mRHPoP< zZB+YBOj&PGfvJLZ8GAv-{(e~Im+iTFlTIHwsOJY66MdPFeSg{vc+82XT`XG~@gymv zBuYjrsQRV3jsfdD$o{vOzTW4<%!3IRA#4mfxmciO%Zh@Z-+k5vZ!AJ7iYTMiyx762 zswKL*n|`4ZP%OR9ByjJp*e12D++m!f&bhcFIf1k-fMwl_i9-VS3XksaxT5AVtEjq6 zoW2`@w(KF2Up@i8=N+NRJRE3KQJ-$Pemag&CYsE^7bmciX2AEEBmZ{pv}&yQTe5&L z6_B=#+pDI^DP5L{T{_N!wT=%dpwTxdN*BHp|A)Ce!+8!{pCq`}a zjR`RH>}Yx(N5XwB)B?NYU4C8tJ4W-7Iubp9{u1$H*mL~THJ7Yq2knLP8CT&BR7=mn zO;*v4(p)aLp1`)x#n&TrGokb~LjD`sPV?UX=FH&!$E5B*`XY;6KZoT46uA5o1^!U2 z?TVHSPm`tae&i-idSYa^BQxS8r4UrX>T@nNs}TBA(xSlavSt{sr)p7-qu!d_eXYbWrNLG0YHA2T7GE*V z)EAi~y82#SDlMNkk2k@8ih)507;U;V(PXruDOe8p`#}bqcuOkGT%N9baz;+ zns?N8eb50$aN_zeaAYeAOJij}S9K1p#xJA+Y!MQL8Q zOo$B4R8FbYG>oFlI;m`wp7?7_KNKuo;`{A0_zW{tvX29u^HDQt_P_50h83yfq%Cdr zCiD+Hv4!R@M9k$)XcE~Ms)z>H2DDGZuSQLn9cVs^tKQ$l8Nb{1m!l)0yfrJO6x?0z zY;IW5dm3lC7ZRFcqZ=FXg9a^J=I1j6IjNBRu+m7imv)hL>Dj0u+38zHC+++S8WLD> z%bCA=o2RD#%&dN9EIPF&9QnIWL^cZQ#3WBki?6~~(>Q$fj$IFYROK66k)(BvQ1CT9 zeMAv=taQD*45qnqdUE#-VDPjNiWzM$Ve6fiesSCsv>ftXDwi}lYZp4Si#h(6o61dN z&{m?)5Zr0mRhW_ekgrr=j=rH)xkpn+_3*JZ4KsK|`KN2ME}KKqabyD%_3Q)VaA_) z*Jk~ZoUOawtPxyu>A|wXg}NawXc?#M_D<_{v?R-k)$47FI2zeTGnVWM)zX}hXSRn$ zDKOF>n_2eoVD$Y0A0X`IlM&@k-VrPUGS#rQDU`KPzp@NAf=ED^J%!^JKAr;w!JXZ2 z+$VSbFw`2UuQyWS_YqZOK9@lwpHZ`jr<8v!>vlc=e~#&AIp3g=*c*am2a1zQMx30P z?FxL4`ix&RK3!jCO{15SeZ%c@m@NSjDIl|QCp6;e52_|RnlFia;_llgA`!u!TaM0b zqt$5bMf2K1eVHiDuQnvJ_E_x@)U*Q9SKki1AN8kJWkf8)cpB{ZAWUr~f0O-8eS;6K z>AD9#|*Pu`kOX{`XL{{+~k; z)a=VPGHGm>T#$K(abr>!O%VcC;k#XG(zzKU7u|&Y6p>hNb3+rwP$1;eMz)Sl?ZQL5 zvk&DPmpNStbH6BMy!?^w=y}c1)W6eo$cgjm%-YhK$7cH)QLcsl$^PV@Iz z2fw3V@IXNnDBMY2bn-NnO2_8b^F;o8BKrT&iD>=zM9LDOOv+Np##D9yj6Ty@-M^0G zZrrd5tfPY;`P;l(urCShAc!cA=+@)Thg%kvI1+qS-w86nAb~byLF;eNp8(iNcFoPKcl=WYvh3Qji53k<4rx128K>Hfv7o>_d z{O5dnn=%CbgW+>eO$Q6&PRZJz5I%F#v;0|IyMvoHge4?U6_bEqA-gna=Cfpm>y`#2 zF-ILS8y&tEpcpzGdF&O2>m(69XEj!I^ZlDHjA!iJ^h=>=?Uy+p-PUz$+nwf#UzkPC zuC_MIPBQitD^IKCUTr_@YZ09)I&eXJ{ zIiaD|Da)2C6;7E^DUjkupyfzQTdq`STd9zeARF-Gp$Iwa79MACog$2;q(P)fV-cc_=-Iq0zj)ELH}L( z`}u?tICudXiZQC}A~$>mtn;yu*8Ij>#avM=^VWLdTRk03{cB{W*`4*B9mj)pMs=uqPH!fz~)jPwKi1M?cPoi=evnz-f?V?E3$%AH#>qy z&I)`*X^p`~@D`Q{Htt~RAp3bUbe?>;->kc1AyvShT&j9c1g{=ALdTG_+HDYghpHdrRjtM|bafsw48}BJ@UlhC%k#C$7_SRWp83Km-zE0S~*&6hMSIci9st&&ykH z5wOd4W9-BGSp4vlQOl}enw%?HyCpa96W8V)^7jK%iq+AprF^+vqhC9(OIFh*PbaP8 z+!f{;0weV+uH_WnYeZz|Kitr$D(?m0q9&g}>P;0^{@JdIa!moq6TS2OuX;}t@UDM7 zn&_@+S+QgcA#*RjbWrRrOH_v}cpDU-zH7IZr&!U zelQi|Ou%fD;9V{NXJw_AFLWkzqp4h<`)qbVO|C!~Znz7z_A_K3hU6)I6PFfbv_O+J z{0Z>JiyDKwE*uOM?@5IQ)l8mWo+&*$U(zab`@rzM^iMMjJ?wul>~F)re-3IWl8ff| zpQweH%39W6RPG*&UK)-a9E5#0DY&>POW>s2e!BCit1@~w{e-3NWeRO??0}*L54uu9mPAmv!xu#U6!_|xlKDtRw8fhoRkUA zTja5zH81W_J`o-A(JzNFZI_MC8~qwDd62v0UoVA~QngIFC~T#3Qq$=jtW+=4;zS3s z$2}%u$47iuN7h>-jz=%r1MHI1p1XK>8sNa#`^U$adV33NM>?xZ9d)7-E3kJ;=_`v& zNUondZB8cE?4JAbW$a+1JRHW~o?1WmI%%YWxN2lCWcqBJC>*!uAYVWUM~4PmCOceD z-8^FiDf1`VNMBm_s=Bsf6LzuigZ4}|SHyWCQ$XtY8|6%C=1e;aU-z9*)}byfG7o04 z3YUz*XMa`GE{b;lFAx6aT;)?9N@WRL`wn;ai909DH)X(nyNsqc){(pW(Ot6H1>_n% zXX;vkSE*WisPHq0}Au&w8$~IF*S1|6)OmZ@rU37mbR9W%>WShi`FkhY_s>5b{ZQFk-cb}hOpEMc? z-&50zv-oB~DM_Wg|p=Z%n8Wp9F5EzI4 zV-dt_{woW5YCn+wLka^{DuN$NMrwE7bNOsrMk#5``W@f(9#i)mdv_|~`@?-R^{w|| zu$5UZh=IO}zIaB+~E&+yIfoxD(T6i@$kC+VNYW0zDm*gNwK{08hCtLJTAc)e76=fPCw!X`m;M4xb_BW z@Gy>iq&{3^qF)8q7PXZIbhp2e$t-SRjP%e(E!(t(5J1J^fAA0VUu0+Qf=SL>ef0{( zxl0ikf&HM-1&&Kq$zLhZ0;S`UI6T`+Z#>Fb{~IxsZfTj>_`H$j*D_I-_Oz3;L z1LldPAvvHZ=8Eb36}`kgFubU~5QXhWMi6+qCOC4h{~+Hq#vtK7$ML&YMJd5pyNs)= z#|jr^bg~{z3j1s4VAzKj{6pQm{@2>tzrE8P&v-}ln+J)9M)Tx%To|{^9Fqpk5~kHA z4eT#nJ)K;iO)<;163@TpL%`H>?WkLY+j?9bK2Ad6`Ye*hoN1JkM^$!Ld0W1e^yKoV zK&-2|Qbz%Qj7z{nvgb(b>nWdIPbJx$Mh@zxRw&LdJv@mBc~v1)H}dy>j*7SjsTe;t z4Z1r7c_BF~=+#o)fe2@qPomy*Yx_jku{HbsL+fpDfIN~WQuqh5*2Q~7!E0ZTI z*|$ArZJ*Po%FYJ5sJ~pSQcBse-eDLNOyKs+@@`Kd(=^Mm%SAi70sFceL6)sd@o!R2 z_z@(b5WeKppmbQYvNNRICu(YMuVYFB{^hgSJH=*m&MT)6&5mzE)MPUM5|cli>{C`+ z)x5}&J!}BGxD5XqMhxEhFKnCr11%OtCM2TWYbJM~2^FshUHtgvr?HFK`Ne`2+DWi? zzpNP4=17=m$N|NQhi5o@hy3Ls1wmebq+Wzbm}f`wLKLH4+(>yiuhC!_;WC#xwC|8h zh?9QHUMr`|qiZ8=h|0}kAb6s}0^&x-4#)E6Sm zLbV%9eDZFgw#74T)z=0uu*s7SBwQRA?lm(l3i+y<;zKUp7H@d?n8nU!ypZPn-69-Y z^>1Kn`4PrbPw$dx(_eq+)4igZr}x97io)~`pb48dx?XWym&z~iiO=9l{MygI@my@r zKq+=~@r}4+lsdnje01n&D5{86;O}FgqBlx&aS5rsOJZ?yYp+((YGG{QZZlqmkQRT#c zc*2(Q{Sax8wqlq_W9}K7RaKc@_CB6};&;;yBJIy;X$Ed);jhHWfLX$S#VA)d;S%l+ z54N~Hz*b!fGz2*#z(Kw~cCsCb4ZUp>>fHyAd^U=M14UiUs8{NGRNJoNbzwyOeC>8d zvg_pD=T7NV%W@Q#VVM0p{;!^3#|%X>*=6SA^-=G-MxS>+OWQnYJ%+AIw*s&~P@lP-gtb>;rV)!xKAmWi zsK1|ooxVPvCb#wLfbObUxg*l&Ndq2IEIEJ9 zRXm<`D%+T&@R?7~6FK)b5;t_~-%`6>N)2R_m2i#(tzr>C$y zZKCh|VJ@R6^f`-3RzBUgYvACA#q1qiydm3%J6_+NORa=_p5AFnl|6h&C5F5~sPg6R zSN8>N8tgA|RA1Ew**fG7^um21vbNV-p44{t(hJrpP`l#NjyqwdUfLNgm^?GNH@|s# zAKytkfNfhUa{_@V;6VO~4V3(wUyB>XbhU_?@SSD56Nj(JH_KlkOj=h`P#BIC68nAD z%N2Ey1d=X$v2rh}q|Jw=GNTpo?9vbOhmC$9*E4_22A$t}=4$@x?*3*93^rAM-zUtrnD29G zE$O&-bK2wBwN)qfFiwQME9qg|x~UUe=W{ z!(Iqpgv@1yxP*jl@Sz~PyK8n$Mo-odT*c+feUAq@>oUpJL9#}J?$aMZcZJQ(hRn78 z8fj+vQ4C;VKJlVqMR~2koVk!cl7|k{?;fs$jwot;Vs0UU3AS&7N=e5b=6@dy;y0v@ z{K8_(`TwFV8p(97QKSE>*lQ5TL$eHT@mCve>i$XWS4lRf#3u?UK$zdl-cu^MOmG?W z{)U_QHk_63kW@jg%;l{LFB&_4FV$L8E2OQeMOaPGbnaf!@4!OTR`c);lxm@NuaIJ~mA~(8axN zhiC41ve2rl^>bNKEatU#HTDlD9JKo1reyG2$$fwH)M*`x(BC&D~XJ7<;opQT>`7G+XA9OJ?ad_K)uD+f+-BE^} z=`Re{{{q=Q>9DZWs_toiu@5$6#JNj1)k35!Z0hlJh`-3{tbsgC9?05*G7CcvJs?jt z?weN+^?^%&yDiJ{4CFiH8w_LzhrTN@^$mj>ax=)GvZB?rX2Cy-9NbAfg}_z8AV1U+V_57I28y#bzsKK%R{-<58!& z!NNdK3X^5fD|j#(J<<_~HrmxK4;#AW<(Um|vA#gL&jwaneJRVklV`#Ez1Up4=-|n# zSdwB3);GB!U!FD3;H$R8$DiNM^|L4@%t^wzfoTb(XMgFL#lxGmNHzMd%j ziYyp#+K+{}$+U00MNM2+=esCOu+UHdc|nIq0z-U}r;&O-^rWa2Rg!k2;TpBgMKw6Pv(4Zs!(FqtlTksgjRnQJ+zjQCvJzuN7DJv#gE-5vvL2p6PEv7RC#JY zhA|`J@RZ&I({2Y(hFQibiBq6~dRKH|q1#T#En(I?M}49fY$(_*f2vsS4RnmGvp1^z zqOAZI$%X3(aN5K!kxoA$30V;}4nHK?L_Lu*56F(Hoc{ zl`j8LN~eD-<*cH+*M$d=&8eX|Q6s>~ImxUTk^S75rCIn(7n+G^qOF%nD?g;I(^lu1 zv6FtfI&o&S!K6mLzKD(k8z>wWPVXX=GSKutF9=ViZrZtG^=DVlljHoJIMFH*%m}9T z+p=$>?1`xmv+a9JjQJLYd59w7?PO~Sa{k!f1j>DBc`mXO$A6IU5=>DBY`}nB=C`dE zKzgO8kBZ9`R{vx#;X405dFwb8juG?+*fonAzM|G{G^{JA68o*D5AFcle`R!XR%lE=i#KqM&_sWqu$V}sZyte!QYoPz%DXY~@3{0>8H)8UyYojp|!3iRF>^84geyLn?3TPLmHlWc39NO*M?VY#-0c z_Dwxq3X_2Zg7G)VPw+WJvhd*4V!+0Kt%tuU_QFJ5;S zx1-+7;!)gFoIfx6;w}4uvWEV2M~!{T^eIsT7$ph#MK6sP&6ubD&7mL2m;7_w^KLF+ z$7V0RVFZl7OfGKhv|A2I>Hio(`;dEAm=%_2I=w4ab!<0_Fo+K^zU5EouAOjpEDHzr)OXV}*PbTCbX;3X&84tiFR!HC?L{v&CHuFzZ)N?7O>hAwxxproe#80w!As9y z-g=5qk=tKFJfVlcPEcmHX@?m-JnqaYqCX8FVZGPZT>-9iGOphLPxbF@#^A~yTj^Iw z(0>;pXhRrYEOEKIFm>xPKFqW<<0c}~wv1Z%-TC=fz8L}}UO$)HiOuDlreoeu<5K&B zLD^#$&Y2Rg+~*f_FM=kVoG1_DAgr#TXzy`~c<%&S#R$Z5`ZT#_6J<&9PHIW2i& zTV*rh)a#db%E-q znBEpi0};?hXOZ+S$5|W9yz2KlaaiL=f%NY`yzLfn@`OHll(+dCZzJbgxi`E7mWiVo z|ItS!7Wr6iaS^}pU-b-%$+zL&JJq89!A<*K7_R*Eb7-ABm8%ZZ$|ir2*_5aq|x zju*B#RF_2W;P+7y-At!PBbiY^8@WA5|7t~9$^}qr^!@N$HF2AW_%_IdhRsV%j72BT z{BCUjDOO%vS^9aXu_5l4UWz zdo`J4%z^O(6z9Jm2mz!*7kxI;e^@x{gM~jV*<>Jizc)4mK~|g%z07;ek6DxC&tH!~ z5+reiP$~giusarg&-%fAdAcLA%DJ}>p398z=tT#kXKSo$>rvC z#iTd0fOQYyYVC5cap}jof1g?Ld(Zzo;jcu-to5`|>$^ui77`B)w5TKx@;Aq>b=VyA zxgo)t;oekTW)pp1JtPcp1Z6hV#e6PrO@97O+SR@75^hQTUir}bYU`{@ds5Nyhc8)! z>Ej$pK69r3uu&sAZF?*_t*NaH<}0<+5NEW@hGC0Y5cXbJ8f_mBY1_Dlyvn4fQ>f-% z_;@GyalA+il9z@!dm|Bm;bj&3zmOtHh1seg9)_!zd?lv(Z3r52-Hpw?{N5m1j2L-1_<%?=3W@zEX%B=obZUlBzJ0V992U%ULc8KvR zO*|^o5yQWvOj{Val z#b%C^oqpZ21%zG$G&C~LoOpKZ3CvqN@~onYb#n^s zUUGc5MD{Wp765pvu^lWCc(w3CEKcGm z551tg?B%MiW>`hv`Rk3+JIP=6bXqUQd#;uGuzE=-wciTN4p)nC>|PM^y|;ES1jA!_ zUG;rEUD6sR?!HN%VGAM!9%%j-KYRT#)@F<&Fv=6`@@&}%6GGwPiWLV0fTU0HdK*mb z4)5M`q3a-}U$(F=5a9o~nNE!aK2DeB{F0TB|Krvv)7M4mJijkr_6~3#bU@~Y?a<7urw&T7qpOxo0--)pTPc%{f+-0$L*LgTcPEP5ELQVcMHo4&)+Lr!y`&Z zL8(GQ#bsXTPKfE1rGwq$CPe`t0nsK|8f9$KiaYN$py(ap+ivwI)~E4m3+_yKMhd@8 z1)>_q?G2PZ^ywOhlAHFeHVJklpkrx%&rqt{j*ROk>zqFgEieZ8ciGcYjlZs~U=k2r zknB}6(Nxl(3T`S;A<#qxeEq3@3w1Gdn}2$8>VrQ@f3#t+3>G6s+qPKhyK(x8xq1HO+xfuF7C{-*<|y#meK28O${G69jp_(tCS= zjZry+uV5qAl^U_+Th_fI+Nv+d`NT3wZIp@ycVJt2;}57UC|KAUMn8hUUS@VSX>x^f z)t69SLRn2A42etbGu##)?a%%Xl;mP>Q~eQx+~J6`Z`oBZBb z%Lmr75vP|P@w_-?zS3$54HiyY5}6_NF=67Kg8d2EH%9rftWIqt&Y4sHU~#$Eiln_( zzU=KuZ5u%M@Q215D>4J9J75s}IPt`?Z4oM~%ciI>RPq(>xV=k^MMU-o0M8iABxmpf zfT_dtfyf9bzAR$27>M|eg9ayF-6(0;eJon~*nL(>{`M2*7LEK{C^SPWDk;eYp$}Mp z&5oKcLS?NO_WwY9P+IFquRT7yGO;&HAK<8s(E*ZbJlVZpU$)G+4b%A>r*bKbiP&u) zASPum^}%nfxgq!ECa^P^g1eXIMuf#`D})u!RS|ACSYM>A>x~nRbNGu{p*Q*2n=XPv z5NOCr>5gZD_8~|+q0Yowf)jK_EV44G8Se$9Ce>t%p|d)NxQ6MNVK2h3`(<5!ux{$RZz-$P$Y?23XXk5zr~wERFWae6k}teIbHa3;_a z;9g1sA6qI&*|gw1#o^={s{{0!mz$75y!qqSqhqHCg_P%gr3^3Y*5(M?osyMKaO1Dr z80Ty|>e|?BWn2Hewgte2?DGruZ5|dUldkR;2Vo9hL!LC=sxtXj?MVH~k(v2b@-^Q$ z_ctvq5ZTEoM-`sm{(q)JSpTE`USc!e?~ay8UnUPH)K@5edy#wku_Y7%%wBZ$we}yM z`e?5I1@mq?{nR2ha1Gh`SO~ihS;)A3YUFTx+<0Nk96we0F+)_3_oOojY|nHp``s^J z|I6}e>UZ5+A2bVB__Gr(MSoEjCE6W9M07jWp-xV$-PlDqt6yGuqSzwbt=mOEzj9oFs>WiTc;TmWk8M_ZwH1l7I zHu@f-=1tjO4T@Z2TnkD~ZGLkfDMqwLcbw_zDs+l}c4Wf0hq(FHNtNeGX%W-<6U$`) z2~-RaFcv>)@81x(;vhX@eZ>zjIaG?-tMGzr5>l7H!$HpgP$$*M$}BB$Q(=pjSGh=h zFYt*?!yPh;q477TB?pZ5t-BQ6nYLaO8^;PjnQcKEyp-M4o7{YL35TWn*fdnTYbFwl z>u%eHO*LVLILiQIo5!YevQcS3r{f!LWUuZ!|8!bd$uZt@@@CPCD8tH5Rf$A8NuWcK z=^T4!(%VIVNu|R_C%{>>n_a5?m}-Zrkf9i0YK(t^%D~BKU!$L2#p|?>9{KrCaoH_jCharGhX}m9cPYUFKe{%Cjw*2XbO2E+w6wQza!;moEN7@aT#2QlWvprK_#f@xW2R1rO4Go z;;UzKD?#3}lY{0@q!t2&z3cXUx9qDRZ7mf zx2}Chix1fDEVfy_AZfi`>DvPV8<)&ocIoD@Ds$go!2K)l<%`}YUgsL&p%iXpT}vh#yuxS?6os{y4k_1zsX6oM@HZ z?Q!UUea+Rs0OiC}VPNuK#?bpa183&GUG_E7hdd8XpstxK7pvBqA&yU2dpKJjYPWsM zy#`^lI-u2xsotQqpT!RA0VuGqYLxFWZ$fP~1}3Mn`Vj2^`MU=(wmhk)uOytK9OO~x z*XWviD#lCS=hIU*8JM*K4WmFDVy~ISX0SQBH5s@{iFmd^`zIkxH35yD-KVkE(HY-5 zZLwTO`xDlRbi|1adyTf-&#&up>6koMw<0Lhi-VDyf8)qY{HKJgHuRbKgee-nWXmcnegg}-h?N9#_|(s zqy9Ub&~K!Eob(aY>rkGp+9x79-G6Fl#|f4!+?sp}E@!hs*yW&EZxbdf2t35B8?Q!;GUxP5JBnzro9tM8q1h*SH!092M%~3>F?Ls5 z__kZ#2d>{JagD#MS7~LyXlS>kkxnyCK%axehz>f6ev(>#pkyf{gTp5Y$=whqUY!Kt zCZTTOE!O$*5pP*DJnr&#KgMA;_Pq=5b@|)YYG_TF$3uV4qg3(Zvul`iQ*ha(nwk&^OA;t!^IncW}o$(<4jKxuVCz&j9RdH zgu^1=S}#HRxU{HFC3$c@#`72~egbe@>hRV0k>AYB41BgFcimi_8;-x8 zS2yfNWIZEySM%fEPyzBGx^NWjyJ?z~*4WX`h533unEWi zmYse4{L*RX-1<4gWvz0DaoD^r`oY*}%K(|m!e+iXb5!LJ0q(?@r;89!mJhiBVZVjR zgq5}V$v|(A2BEo~i~owH!|?3*Zb1nx;bZF1m za>45r)%}9h$LYXbJCcJ+a1DT~faHk;9o1N9_9M=d)Hwdnm$d!$k}Lz~j-n zJhqYhqrtLv^~RnAQKoETzi(Qrx~vZoI1PYaDJ=-*zOVN8N|dKnMiyG-R?$JepZ&#= z1t3v=ns$79<)l?Jv&Up@FhpZr zP@x9c(TFN*V`=rY1&C?dI!C8Rk6t%V=LU1@{)FuZZ_NIou7YUP(#q8@wnOX23vP{< zF`zuUxZ+!3#DIEZOJ3*2R1WDp1WsPYJ0lmr$%4gA3K7Mybvy!@o4|{N64E5>{7)Eq zYM>5%CgjsM&sItCvFPr(C;)zWF&p^%aHaIs@)UAsJQ>lg+fGjLZ^Q(RN!0I(gp>?< z_$b5Xn9>-W6W*(mh})FXcl>vsQJT#OE#0fz4otVj2!zgQk4T29*f;m>EisGs_|0l} zv0`E`nrac}d~C)-9i7zUOCoPXw)5PCU6DI8`qM*~e&tV%^{cipr+U7wS9a?KBu@%< zp28R6m!Ck2P(&J-O@VsQg2A+v!B6VI2S zSx;5ehVcZ>fMfphB|oI}M&u*iuqe}W&$K(k(7}8_`IUTs&apn*reL!`m9E70Rrj^$ zT+$!Bm~JnMF{||>8*s^v4s=t&5_&H!P%Co+O2lPrRM&(1X{}SLbIA&lLo~)o?vLyB zl*xvaRtiqS05(Pa51Us=*s%xVWCzRp&>RF5#PihXZLyiY4IqOLL*I1`{ln$xS(gn*B$qt3dAqvxqDCo7DR2LXH+o^hSZ4H za9?NaRgLgNa5iA6v}}ZY_5&K_ME88o5_8^9U#e+1RmrAw%57~vg5vX_vRFr!l^ucE zfNNmi zYZ}yL?TENAM{ULnStyI?<1n!xvvOl{c>Y96wRwBu85_cpP76*CM$z3yg4|lXfF(0 z_|z(c@j+y-)x%Gb(ORV}J@;&0`H@+ng6C&w?YH!hg@9CfISTNHM<2pfaUtgWxVmgf zHQ%P9ZXzdP+l-K{Fk(w#x4XZ^mDG{C>cZ(69$l)|qPqPNhDNEJA>9evSJ+e^I@t4x zaZ0N|N?9IA0Yw>OSsqrTzBB3lD$FX*@d>tk&WYu|&K^jV@SX&xG!u3i54hYXQ zp+Q2(o+kqiU$4&)EgJ%6RUMJKR|q~(&5`d9Ud@=aIcs4@M<>ei-46s^piCIF1gPq+ zo|r5@8t(_8(Pg2z{Ifxh+fl6u>YFfdckYm^?J3VUC>1h6C+k%O*-SJvc^!m%`S31N zY-65IsL%5~7`CxgnloLyLB*dETULyw#B0mwe2p2ih%8n!1{rfCqR!loL%>vEDFnf>W{Y2=U+qhMFfGbYP>7audp-aWRg}z{K*U_zd#~ck(@8 zH!M~?FHU{8r___o`aC^iJJnAi0kp+sgVSZWfNmb?B!Lgpz;bl^$y|8JDL7Q!6kr$M zAy_;4=ca4?OXDU1W2hB}HJaI6FqoN;9{C2)EjQ`#7y z`U{wRJ;sjIL5CjvHt5hPco2&XqE#0^LKReF#frDt!q9W;Y@{JCQz~6ggA` zk5@vT^rPB5C)f@AknB(c=^%rqX+E6Q zSTd_QqW>Oj6yHnTZt+9%bUq>XFVH^Qt|p+LMlIGz6nhHHn#J#J>4aOQ7@k*rDnG}p zk?%Pos2!Y*^+uccs*1%gxCR ztuvAsVQD9+9LVEC1*h15za}X^xpjI9r@7MJ&VwVn zgX&vFh}WtJ{%vh~e7Z{EKHC!Hw_ZmFlr5HMA1dVC{)JI2HNjXtIciHOJz){FAZ*z| zLBPiXYmI5i+;W5bIR3HTUT-TWua&7oy9B-3jyzR%Ojpb&zXh~lem5OVdJgvVX30wm z@Psv!A1s_$UAuxgFF9bEDarz<6_^Y7&FC0aimJs7utzM&S z%6C)gdn77xQ|EkxL*x72JXh zDjX}U9f{^dxiL=4Lxq!tHbn_l4jkWpQgy0FdtO_g(IIZ3?x@;yk;sh99~CHw>&Y39 z`fWHRVenEgJ}c}?8OotXtUnQQ!**TxD%d~Yg3yCp3_ z>3JI>NB0`>SW=K@G0#Ty9YW2Xy%!nj2S-{6Gvn&OIhuh~ckb^xI^_p`GQZbz4SViR zE3^>3m2xz`&Q_;jO~34TIx1b@!|Fc|MNU$V#o)`ZzBpFiwcCoolF-6Qe9(PMw#2C0 zh$#-;d!MnPdTNB6#nv`=zYd5jj3RpaFm3o@xX^+9oLb^rO^K C6xxg_)T{Qrhtk6Lvs98fcz)o}IxH)# zo%-l9Z6ZPw#TD8P_xmw}o&?lzjTlaRgLHN@i@^36hrD*fFgUv=2g^niO#Guj#b(q$ zXe-WW(5>u*e$uykc*YjY(FmRoYU$rvQ=W0|S*8B-xw)h6KvgpT&*-$JmYqq0=v?h3 zK4c5Omj4!=B4!7SmYxPfofp>m20fRP+JeP$9wS}VW+ly}Cm|if#7uS($`y@qE1o^@ zNtxqY{Xq3y*^LO4y#v!qROzi%srDqmr7e^^ui&z9TF6nvKMsn=5V-!Ztd-=e>I zs;2C_@IjARe9-LrzlJNM5zZ3(NgUJWbL3vWRU(X&iPL?;RPReR)}1tOJc8IHvF&Lz zodg-FP7OwNm90`gk)`U9dOIO&Ow?e==Pvc_K72_Vg@j8P*5}g z8bw!iR*2uQhQ|h?nfs|~1NV{$aCg-oqH{~Znk8QAsJReN%qbN=`s0NlNE*Z76g4c+L|d&AOLs@<3`A$ylnPFg?t zmb3W;XPN?4mZPA05uCMLStm43?hiEG*@0g{V7KeN<_ln6aO2L2hKYoRXN=@G?&ofc zYa@at&#z>_vFsEKH2YG0KkDevQ7W9!i13hB4wFPblHaaOKoU0Cn%Km1Lu@e#=0<)i~W}OxAPskRX~(8dP`k^KQ#Rf{95{9lGzu*DBDN2gQft z&v<^8KVMP_kB_cDHKfAL8+ThH71j<>`&GrwyU^Xu0eY>=?PKK`eC^iT?6ZHH#2#8+ zoTJmq^QF=iKa3*4T{1e=uAM^Fx;8dnEnlB!!NCyZ1QzEoxvu}QjB=7DDk*FvlW-0Z z_!VH1I}inqBzu5)@NeX2#$xG%jcrrS6UFAyJY_k_jZnxFL*sh8g)%IY}e1*QGhd9nV0#F|P?#5tFCxTm&^Qo=f+6~y}ePo8A z8q}&p)mQyFI-h)d{v%s}-+@5QNGmk-;gdSsPuPHpz$j-gBQmr$p+5wbCYOMB)s4sE zi(!O)VXVS$A5Vi}Kzui?1i2C`_ZSY>SP#R^K6Cnq)(QpVu`|I#OuGpDQlOi;guixD z=1_Nd#E0hSL2xFcf$`F0Rp$FDuOlTYK|m}+a2OWH7fvLI!d0dILM z`^)z&6{?LrO`?%Z)Oz}H-vwyXGEqu*vFu%nw9}Bimk^-WP!7xUdi|v9q_nKTg#2~l z^f5(;B?}`N$$k9yp?hfC_b;5*QTNSk_xke(4-sA)w><}Aj~V5ETb?{|HeXZ~BD7aY zp&h!P@=9u`LC}T}{5fEWMHF>v`kj?cbuK8a$b5ReopFjP{B8WfAKQWOyASTZ*c z1fj%FUw8`)Zg*h%^`l2a_Lexp=Sj{S)4vRlQ|HYhp2L>_VfoOe$QD6}IEF6VIG8Xr z{i^T}I6w*$N<4RNO|Y0WGEQ(^kM1tFcs<~dFh;C{>M_%&_X!@%F5g!Jk zho4eX#iWX{&c1SzJx^D)eAAjib!(?Oa0k;^ue?!t?{z)!;QIIrYi@wuiPVPmq2vS6 zC`KRBc8fddt9T~%|CvDPC2<&5L>jYoT|L$7WGymA~ItC zj&7DbMJ|1n5tNzALba=7h7cc>B_ww^AqZp1ykrB5j&*zk{l_>Iq4 z(k}DrToM&BP)h!dD@%O7^z2`#(Xl#Y$B8-Uot3+~u$;F%=Q@&F5i{e&+W1Or9nUL7 zMeGc0e?JAfM}ej|nhn~~d2Bk*U3!JZ`>nrKLM-rK?P-CDp6v$$?6syWs_s|VZ-K+7 zg)or5b4Gs||7#3QVy#{h(2<1lim>lqh4Qxo`F^s5dclkdykOu`4ig6V@w{hzTL_!% zdV%usp}nsJ;Gg@;ExpF@gdm&Au+}&B3ag8_S+d6KI;L9a$WaytZy2>KN#hG0`k~3P z2>)@a7X&^}+FfKA1La@?5^0X;-^iPAfy;;qtvG(D+p;eLrDR{_hqtDfboHp(r-MyB zIixF@5QF6X_orSK^eQCGqg($3(EI28EjT@QPv6vYzoQ#Qf3LlAO|SP|uIIVb^pa+E zEgLEf18)VHk%M-iyqu7JS7SY{qBT}^6w-xpl*gDf`ecwF4MUPwbL= z-LT|KuM0~?ftEuG^L$D^@r?D-*YC#gf>c&ocinZzZ0L8@wTVLY@~p_>g4JTV?Al)- zCfp;7x2?myA4Wx~p5WAynIjmqWUdxbY{vf+iQBS)?ZP^D7sSz4KCJDj^Fkf%PH2&y z@0G3;5Zm^i?3I64QW<^evpI;Zk()ZiDJ9({)-gKmx*x~0YMsktY*0ZS`i zyr6;~J8hUZHWed}6XrFv3Wa4oGOo%-YiLX=PrV_H3Ot8p(N32)V0l9yu22Ewql62S zFxk|Uxb&h~E!chC446_vjEm^NH@O;kcr#>RT(Q^oi{c<(O*{ZIQ7Lar4G-@CPb z^DUZ*>ALO&QPH`AchT(dNVkdre&9-(U+k8$2!Xkf{gx$eYhzO0C8BO%?Us~by{g3eKEP-x~@wY^( zEt5$iq(O`4J)8}2xcx*>fEOny!PwKI_bYw_QSTPVzTxKBg?{FiyjzOs6BB0-XydT4 ze*4Oe9vgUq{bU!ElAOBH@gh^@1I|^$iNYhBu;kV*nc81DUhYh%;|1w#x$tQpbgMd@ zv_`4i3nnG;Uc2>1`gl~PUQmTJ3I0WH1zIuDZIT;UEEZ3#=v0i z_Mce&=b4|f%KaF)HKy3Ze)dD>FnU0u_KZS-1Ew%IJFbAbsvP<2##g_P{?blrbr!a zPqN4P<}LS+0*K(8H3>{qDdiX0FbWa%g&pjm7d#Iu z3_IL`fJV*sV-z1Y%+vYAuKZ76_&T9zmGinkEd&gQYGde_Cy@IVJoNqAWQ4zjWL<{p za=Uq@@Cg=Y{Ouqr$9+>6?!5sIl0hs6-?ukYEj^Tl4f2Tk3ZRZz^S~{V*5xL}KbfIulCvVOBWtKD&tc4WDgc6n8~E^59Eb@9CwX-OdLEM68AG$ZC7*(lBKc|j0<_O<6({dv>R9S^LZ9F!6Oe9LXu6n;#+ciL6&}KFtQ*N0jH5hv!wE*%e6j;>| zm#)*5jd!XHWT&UZs`HxSnQr#u(%Y`&uu>-9n@G|-{UOv+h-CXJFZ+1iG-xN`q6c{$Mbq!vN}8N*nysVEWtpEL>&|#GQ)&EAPA6d=2b61+V4-OMJLw528QL6_-lW8)>}2W>BG$Fk?y!tXK8{0n;8zhxswnXi;RD;h1kP%=8AiDr&p+mg zYh1WHKT?U~B@xi9qy-nbU!=`$%J%=nGrI%rpsMsYqOo1=S{}E--d=%zRV$R`#B1HH z8tfN7%Dfkut{UiW&Kr{d7972Rq!Q}AB>E1}%PDe*TM=gQ7p4xO@G;fyP%9W!ZMbxh ze|Sq}f@Vp~YO5Wk!1TPB3bKUzJc5|dk`4O*D zjI(WzShQPS87CfJ10IghV<57jFLfwN0jH+laHfV7vV-Wa(Ld_ ziwE1$rQ4eA?qg05;TjW1@5fe$qWwRn4X#h>ZcgtmVJ`HR9&4QUNv5Y&EjonsLO7O| zAt)0!@cQ=7s4A7-^}z5xB`ov`G_B*Vp%jlBOw^ei5kX47k?vKotEkjPO5wZ?Q6~y) z<}H3rzP(Ty!ZR%q)u>gg%GijBIWXtOl(9VHh@m!6f9dS!OVSe4j8fiV=(3+}y;!?< zMoHuy0|@kY-d2mUGmpiSSXP&5ou|1+>6fBxlNaFCOE593CG2Y#(iu5Q*8y=_uYR!d z_|r7&=W)+8nxJ3S^i;P_?WLD2(~Upi8Ac1Jg9U1Sb;eJo9X82}syXG*%V0(6BkqyR zk6H>k6`5-OhrkU4j^Nh+0Jgp|0M8K2#i~~2<3d6qw@R7JvDC#; z51cg_eFUB>Aue>>#O~QYClo8vrJ`ea-CmEX7}GmLywZl)t<}{{X`x!bC3v(t6MN54 zR3zH4CnSR-M5^%~82Y9}X5dVHL>`E~v3E>|z z{cP@PO;Bem-+S4 zqt37c8h<)>8dwLG7?LRd-+ zCGX?c$&@K9cPb|!lxxHt(BC?lx<^H?K0wUVgtX0fpyqGIdy%ViNG`NB9N@neHH|;0 zcy1`~CI}?e_ozAY5sI6asxFzEY!1w?Zt1Ptce0s2RIMEi_X3kC%TkZU?hYUDNGO=1 zZauK)AonAIYl(+Sp?W~l2jnRT96s7fW!h5Yvap)y!}2I0&Ca}DFH*UK0k&U6dODMo zo_6H4$y>6nPWCV~;AzpwmVGa%Hw0x>n4G)PhDTHa(Ifhra)2kFjr}fDE0WtAWv3Fr z^gI9E1j&gVaw6zuujeH`C~E&QJa8tvcc}bY>Xzv@acjn8ZZ~xq`IyVyk1lI8Rh|@f z-moi(5EbEwYLb>?L>~#|4rg?u3`=^ocSjWZvU*zk%IbCQn4IApOy(tdYT%lTO^0MG z)BM?fQKoLOxZ#nrQFVn?UX783b2qN*78D)hC7o5LH7ZO4b#;h!7<=mX#siOnMjmDw z4WO$y@S)K&M0BNkFGsP8;-~GVv>97Qq$FeVIS-tPdyK1%<0OSJtI3kZL>dphb3vFo zbP|=_wwT5Mox+p5B6Lo8^R4sUWohBf%tu0~o5F^4;rI4}g=^CP2nIlitPgnoPi`T~ zauEdg-8tVgx#a7iIUT`V{J}Onp*gscFPbt!Z~si1Gn~zzJ4n;V%@Ghq7rd6bOLH(p ziXKHLy-4sjux`==@M;bZoqgh%KCNjyX*MnacTPJ{H6WL{G!t3t&L*I6dqwP<)_*i8 zSi3o6k=Z{~?IqBwGp4tBK#oX5Iyz&LD+==rlC_41FROgqbTn1)3an8ojj#BY5^+47 znK7dA!w#vLdzE_cKlF#M6s5Yy7-#&fSejdln3AawjOV) z!jK;oNpW7Md19RIV^MU}H@zOD_q}Ee%b6G>>ITbxiq!Z~n!s7QtK)W-urzp-`^1zK znQm--_DC9qaMd##bRzs~pc5HdUH8m$V0`}#Cc7Km>g%6Kqy8{L!t!y2q*yimx4cAQ zRWJGz&)`COR#WuZk{w>VGq1o?HZ@1B+6pS3&Rx^STRoP+ zlzq-Iii>I&K4IteR(ycZH29uB7IL+nNKv;^uY}W17kLrOA4}ZHi@AJE;7RCVrn=RJ zf>>@;GTfmAfDUCx;O+j`hp#xavVB}Usf@kbb>8PGC#CJFo$UjgYg(ppTqGjJ3J^Rgzr z>|qRTO~`Y#|45x7h|9)_Jaz5k%Iqmh)5rt=jys9=IcsY7v9#`C3H+6a$UXOlYi}=$eVke&c z%$Mv+zP{bYrn9dzebtI>hU|MA1T+%HL4(P)>cfWt5mBJFH$&YMpg+-EdzmDm&0nh@ zk?Tw^I|q%O-NStDjdb#*!c%I(;H&|vIC((|O-SSoDl78i^33rO5 zeMwn5OG;g)y<5BSy&{KKyvrJFy8O`m*ECxZE}d7xWacN46s#94ba7!k&gMXM0y|uK zn{*f>z+7Lkll9lU%K|HJ){((9j|vP}Qwv`1U(A=>l}zT_l&;>=8>-IK8>>?@Hd#?F zG7ba;=HJDsH-3$o!Xs=35B#c~lM-~Ui*oh)hf*-S=7wMMR+@BTJZ+G`n>s*n(zTq{ zg+!Q+#;ZMZJRC$${c%5q)?zAry&Nk1^;IKGYYwlECl(8jQ#U{=_8sx87SsG?%jg#u zW+@j2>zC5MO?=!}9g&dhzCfzCflRmTO~T~gW&_jd@b}#E2@0oLKSZIZ>YfFy%14W& z-^M<6TR5eMI$jydqm#kP*KD;dx3u#H&6W*T6$G+CgeMcw5kDrI*6Fko~gi)gwx?BYxet=V^1euojhpw z_;Pu?Ls%6mZGK~OCyIKW6uU2+gJbOK*kjufsp+24Ls-d*|BqK-I-7S{^pnk=JKfF1 ztP#a6mxaDbw;5fJ!5TR5dbRY689mp>L|{qL()^0osra>LTkuC# z-_r*SVDG!qrrMtpxQrz@JlW)bR*>{mv;H2tyEB%=vK^(O*>QRJ@`D0;_-BmiQZz0t zy@@)KP7uglc?6^G{79S*jb#Pj?l!HzVfyJihn4mvE^7ZC zoIqG}|Hv(ttq(2sPH4DS+#h<+(G2Q)<@vU%zRv!>!q5+sq*Xoi#cj}>V!~b;wBD*R z!83Q=#u)=n-(kXY`m*xXU9Jd+j<7RWYJg4NPO#FXGM@Llw6uPV<&n+_jG}E#UNtZ; zf%`z2I<&fpH!_;GdjXXj$>Ubk(*tUAit#Sw0nb9r7OEpgc%c#9O8cXS%1kA6gHXGZ z_(kr9BMCJc);OM9!IV1unS6U;jUxK?aeeK_qU?w(s){v3wOp=W3!(eBTZI%fDJ-+= ze(3ya21H6fQcF_{?&X-WmljP$xC}c5#&XHclx9c=g1%B>MC_`81I$nw#y^;=d?|XB z(Jf0lXkC~i%1n+pUcELwv#|IQ{?~qa#9#XcBX!gv+(GM%xb4Y=%qziM+LBcJZGqWK zR;ZT+b@^F8ES#5e$7Che zp+7>H{iNiL9 z&aJ1#ox(Jc5-`GV9qM`P@9>SJmvL_3Z`Y~_yB4I;IDKNL+6{d=h2Th)&7zmU%672(62rTOAsu7kii|FLBm3GXRbK4N^uZ3c%T;fKNn52oxoweP zx`JJwuz||o1svGJA^w@M2QxG28v||%mRIZh4cXA^Ni;CqDP8o=KXN0HzNAFNkWvyH za`d^T*7QRVo+ zdScCb#vNYg$AS=2r$|M8mP#aj;bpbcy0&d0?>t?6KE_+(M{49@U*32;y#>2KaWTnS zmS)3t?)+UXUvO%)JSf8R(y{RL^VB>8dZZ$sG(?kRg1;67(~W_{aVAr$(LKA+ScA5%41v9hDrP$HJ8TDXbr> ze2ovC8CD_!bJ#-MtA_Rb+Tyz_DQYh5Ckk793$v-(|5!Ee&OT|)ScZglwMGs?V%T_Y zL^z+K5+n77PlX*v2iU+a_Ey++QsXYOMbSCMiimmVBT}rhpwxNe>`U!*XKls?gCa zPN#tdf*sWaK5h+1{{Zcul5X@X+EI-W)xKvlQ&E65N+5l!%_F#qcK)M^@;$G1R!V&; z?@f~6z?!6D+_id1@YYbUE#}b<4Da{oZ*0S8=Q?MNc=jalo>4-|))A?^fMKzGE>X+w z^iiallGdoG6{&(OCTX!$TqQp*Dy7N83MrH%iki^vFHxgawDU0zrtFROJk!czrldV* z7CE~&_e0urq$+Ga{bEKBQ(31C4_}nk`+ZSjCFTcd=W@dQ3(*gt1wrHx7G|-S>fD1a z0s+F|zGD^sc>O_M$qISr2=*s0d!DD7l3x0pbCg+>obE+0PnaPnBoo6736?&v6H7J*5NdS6qu)?kWW0S~pn%jV{rg%eBk&CyVw-1xGYU{#X6@Y)>b zt2Gi*z%bLZpYXMQti9!tYvzaIm-ZVaFIglJjntp3eHcSzh^WQC#BX;S@tqjN*OSafb3t#h>$tV2%~*fAF+``GX03O#=( z+jdu!(MdNcQZRmDNH@mQzNv2NTF$b<`lopKTGwENBV)hE z-plA(y^yshb-^~ZxV{MeXdR_jgT~_1Ss7j4U}u8RPd}2{`STdh>g)zWN;tZy*qYgJ z-|8uCF?w2Rh({QAiy$u)%U8KJJdj7z+{t#3qYcG50SQy1!p?py8T;}`@9bNk1$cR~ zUp9k6`zWH5e{4fP)3#sndzvJ0HR6KrVirAgh}ytaJ)IUUzU+MO5pBFy!RCt9Mig&XKHwIdxOx1X%FVFiry?Ut4NII(Ap7^>786qVMHK37`}%-snzePW zwi%N;so|qV6t}M2+g#`2KAcVMtg$qT-}}lwQO$iTw%)kYy2Ce|5{4z>(&rYg=(Aal z2)f7NLohz<2mO8xkpPF3r#}cOR^B~Pl{IWs#d7yo(1Z!z;~8RE0G+J!Y!Fo}(cs!X zB?zYW&|^Gg54NWTReV8ticMcvx=nwK+@L0$RiqVa`LoZ;NTG9|=Y*o3|17IHdL>vO zJx>u`F>Tn5y~E~4Rf6>yWOd#1{bn92W&(%pqNfYg%MpKclRKv}JkwA$vyx{-uLsY; zOZ_qUHlLiDQFcv7y*nMlolYrEv91y6J^#Xhb^pXCyfRsots#LEV0_u7nqMPUR!Ng- zL6lx4E0v5LabV~6rKHoXNL>7KaU|K z5Np;!D#Hxuaq0#{4k>$JrGHelT&G-raILzrINAOet{S)~`1pKJBBz&BMNVEYnYN}O zguCaglIQ3^)3*;2{_3WBud7KRr{Q_CGghLN>V+@4@^*qatsi)5qTUO|S2LMO(iZ&3 zA(4BHC%#))?YSeJ-2J+%!AQTULw1{-*u3M!>?gUK$!EgWCPifiO&MqTr%`PFpeZ5} z+*qU+d{>v;V>gaREoKNi?UXb z*Zp8mdggf(R-$6MGUpq)Ni&jfL%lQ^+1=N`d>U7+7a50uDG3wj^qb!w|D+S8ql4|WA1%^U#pu( zri9#D(SRoG($E)XRgTcZh`6#37)9y4#mvPI9q1cY3bxdCJlE1z|7la(4&uZz;%aR< znUQHtVKr_=igfg6;B?v41H<{))havkr?9N5+1dkOaot%piJQMwonSk)v?HvRwx({g z_u$&!SuPudF0WXTf>3e4@1w8mYtfByTMliG7eLDUi++;s#V==4gPwcJb5P`hIJDEt zSJC-|q||`twF(pGOaqeRlYF?7Iwh~u+K|cFHP6tA9pt%T;hBXs4>qPbSb1`OHsENK z;9^1BVWu^`mO7lhU9ZtUisX$*?M@VxeSl#bNBMOf&T7|wdQmr<8EPbgHCA%o+uc^Ic$j)I&?eSpp^60zCOZ$7(9?(-x zasQ`vQ5&bzN>^9<2K?$PI*mCea2;HK2-_`lr3ReDRD3Drek84tfG`huqIVi4L)d;^ zP&S>y>mz-V{FVjDVcAt|RZFC?Z8Uwh6J~Pio)@&JAO2PR7x^{_g{m z^0On{R#p=|z-kM8!|+)8{2$e%*SuJ1)u4J6yUIsFop;;j>8>E!R~X)HL3H3{+kpxn zJnfq(KJ_Olua~Qjr(vdzg`9IDXD{LQ?Fr+%MIbeTv{}y-u=C^;vF7OGzs~GawVduM zEvwOsDUrlP28*?LcI^ea_0w9aSX3tSoI~;Ff&WyMtMo4)IB5jd4K4b8?Y4cKpmuoI zZT&~TaqmMxs|svorJhA~#Pk*B$PZ=biP@<6-U=SvKiu1zvfw8gwH>V9D{Op+AZPu7-Wj(^vU%wC&?(afbb? zSo5$RR%g{w*TT!KUZ{hnk1gP);&c*oY@}gTP5WlSlk`&e8y7!%EXXu=Kng>WRdo1L ztecWb0&7(hFB>2%P-ya5$quuM*p)us66$cm=x;{yl09F}dK zpLqFV%|}D(K7C|S-Z&i^YgI$f3W~t~9ftm$QhaU##xPJnx>nT@N)=^eDtrXi^vD!e zTt-b|V4Nb9CfYZ60v|^h+pyF=B5-%Rbn&kxwKRmaV0#{Umvr$^bh$V z^4HaivkvC>FRyUIyRb~|eJY)%G~zMd+YcGqo|pz=7{@_=6yRNq)`O`R`o`>jr#W7Z z!NeSlu6>C9ZGh->POog^cM04XURez__k(8`$j?74UES#G;xE2_I9=LtX6EQ7f!fQu z=2MjK<%pDPLf!nu*X*Y)%+jT@gJ(7>mD2Y6Y0uHTdNl|j>9 z4~MBmnJcjKLhBc9^KyuK5SG>*MiUocIq2l5ezDUFCd(lOQzK70*d0dvqhWt9e|qoj zUqrXVeQlMSgG4mWz={zSX`gBRFCtP=Q$g5umf3sJjNgN&2%K7aj~T)@`8T~-*wk{|AC!tUok<9 zCYWjSj3=*H!?oF?)O0+;IJi^5BTpo>tl{hk)4k+n)gg<_VF?VF1V>3^$-+#Wzt39W z!f%{U{43XGw*)ID?4@kwCtz|YTh)6?K9)sQex(8Rv%!$Sog>JLdX{8;gOa_39{NGOM^bdT;M?>y<@`QB`ra#Y?{WI`WID6k*$SJNydXFS-60J)HA1yB?7ttz7R(s-4`6S9dytB5id&hnvQ8L8K9|vbZ$+55#Ce|A%hfXScgE$|yd*&^lRLeWGw<7V z91%CJBCdcY<&s|KKUF`@#Jf8zqet+=>KV+crtuxqL!N1k%k+H(W%@wTEpuyc0#gJ} z;xtX#@K6tjf!FJQS(3)MEWff)O4 z|K6ywS%$Oon@ zf?LIjTK}vM;~q_x<|me^H;k<`v5Smsu+eB}Zt3$!CsV@3g#6H}+(p%*R~r=k0B?P4 zQB)LuBZbMY1_SF2N?c}P(s{8#m;NfRWHQS!q?xv+$ELa&cihe8lmODjY9f{KfwYhpM5M||;#}oKe4||6e zE_Y?r;?2iJCESWvlu~igX7#>sn)qcpxMrmrQei0m={WMObu)DS*@)JH~K^U0xu`dY_al5bB9S*mt z0sE}lJNxT4K3Ys{&5mhOjNhQjjmo4ptOqN!$O!Cg26ulUS4d5S>DJRx# zhjVMqGz{qj**=wDKlf4i?fO8nDZ9t%3lI?$Ezy=n9GLc+5U0|0QFr<#S}}5?=$haT zkHFe>Id}Vbv14gso>gs6dP9)P`DODdkpNuvX6QGFJ)K`qbz^op!$hv6@U*h*Tzj)+ zVN8m8+_x*W$0%#L-1?+XNAc8WdJ6Iwu2!k737~r>mmZ~4H%W=LHhvcfZHvG|^K9d3 z{sUr{i&5_@NL<`nT>7eIkh|5{`%LCn^Pi*Ai)57^EPFrkrtH`Hmng!4Ux-3p-k~Dd zZ~DFpp`M>7Gk|BLurgaGtnd zQB|gScHPS?FE_q=O;e!U0Yhg$aS3Z zI{J3}Bd_qR;fiuP8poeLfWDpFoiEXZKJ-kp=4VH42|sW9TKRxI?}C>Dy^q>`tT9DQ z{ho=g<_Fy3#`!x9t*dc|NcPiSV)v>Q*+@=LJ_Ez|C!Z1XhO#3b$DisC{w9pORO(PJ zqI!S0g{J3GB}xM6D=F_*PF`>1PhdJSCsOzw-6J!m^FFsy(Tk@SrY-Wza`^<~JwEU% z(eR%y?C;aKS5%?qkl5m@ppN)IR2OYibiZt7dd7YdS~KGL=h~Wi$MYlZ&GE8|wAGM9 zd>N#Ln2={&VYJ^_jg}!&4HK57ZVbG@jSNQ53#$R`Qq36>rwuFdAb#U7!|isqa4^!8x_UH_R%E`BiGU)$KUCvU4?)^fjd~ zdt)}ws8P9By!HqR+uX!5b)m1Y;?9e~2H#Q|mxy(_1O`+yUPrgd_sFZ1YN0@G1D;Co z^}7vZJ!^DLS~c+cmR81x&~nWPf# zWL=?VZm{ap4f>r`rd`KeiXkau!incS3c|QQE0WT!IW{#HFqgii8GDx$IUp*4_?H?x zgAo*Kw)a}qo@Hs)ZP}7zSN<5;*obb+RbK0@h&7yDbt!>ZZ4+hngm-xqjBL2L6Yiqt zFO5zu3ALlKdR63U{e_2VP5#ePgGPyZo)~>w7cp-nb)D3(-s~h;!>$jvUs?+0#IHKE z+U%K@O28LxxWQT*xh2e7CfZm{u07=H8yaU=U-7lnd;C!4;?zYvw+4o6IWKOFBx|`M z;SYuLPlCLQHS2eaJA|?FV|?#T35C0?J1dSNn$UwrvKZPd1lIm)JD5(9ZJ4vJ0mUE< zC3uT~gF2XTu-vL%|3zA0=LI}VrO7x_){w7Hib~zR2s@8D_<4Vk$iqpUn%iGQ*DBRV zZ=E)Gk(e_;i&=+@_Ab#C_m4a7S*E>DUh3s`?0j(L^~1ZX52vN$-Kf-15Tyxp8Y35L z$TOc*UwKYll5$Oq>ngsq{WM`?;8kuaRs1?fTHCkQx68bnfbk97InBVQnV7`x^#M0& zOOmX?(`A-v1DsOBWB75Wh95fCDD5&bk6Ar#kQwLJBpzgbWlg6A*KkD@TMcu;5|1I? z2Kq&OTT@&ow%FQtb+*^(Nc#-eXN37#URZQq=gde zt6%+9V&nHJl-)sz;ZyMxF0t?V{dBA9>J69f{=n$>irXer$<7r!W=w6#9<6wds3wX( z9atrXckE9Yxew4_-ARH{+!nWDJPv}WUC*Mn>>>1xuIyoddPd5EFs?c>Pc!ew6%EeA zgfGHqIa`ez{iP0Vw8^^AHa-*W5gzi4E$+?E^&f}ai=4ATGy|s9BZfUtx1qLGU$Q^C zXb&%v|tq=xJm^7{(xZFR9D@!5{Pf`^B9dz4U5O6>{hn!dE* zDD7t_9$spilfm#J@eqAY(IXPnDGcd>(W~Z0ugZ7&$Ufvw&dlS$EBw5cx>X%?4aJBP z@u&Ay1T&`h;Pe4o_<@xemw?}sxp58yv~Av-$wq~(IL52`8F>FJ{Z z{)Ro9!3qoTT3i5AnF6i%zbROn@ARE#!@P~_R@3RR;+p#zOn(mH$m&)-f&QU?zxY_i zbwlwzwQ?_-Vk=UnZk}PveC275@bK?#w5qS_S+#O|PNZfIUjHzsXhkY6mvP0RBKx=7 zza8{rbao07h@>|CvE=K?A`P9K7yk%@>3l)!8+ql+4%f{9vBTZ(M(>Orp78U)4mNMa zx&&znxxELDv21^kW+^v>^mRJDOb@XYuW;aOCUbM{iimOG*(2cfgtz>Q87I_@rNVWT zq-AsI5mv2wWuwj6%aQ^9dfooVtoJwtN9m!_%&_Fm_jt7{8gPv~A2|uQs;)y7dFIy5 zSOYgh8>8yhgAWLs`^yD^Q-fw|bvYxW4DY{)1-mqtrSMHUWMEwtl48smcB{_BaKEJRl5U3v;txF9 zh3O7dcGC`cU5c8fy^Cw16%uRiFKXms2|&-P329%M6C(cD%re+6Co(UmRz57JQ$7WA zcS?@%yNvWvLM&Zn$5#tmpTFr_;n9che)@7)HS1EXn8{+9HH2SO6~|K zsX{i+sk#6ck>1)Ox{J{9_73=s)EReg>>z=DpGrTR#>-%`Fd>wp+m7v@ONlMH;-oQjR9g9LzW)ug ze5kN|rucb-6-MZ1npMWC0i<eL2UtoF}C4^vYNI9^?KwW<}503dCW512p}xMLuhj zYPy<@Ez`LE_DM9|K4v$c8X@I9D#Aq)?HI|@YMj&>F~d(5z-`OsCB`e5iUqp!i+~gC z5kyU*B$Ovi5zrDSJew%yTHP`I!sbj-M6!7p7Jd#GIM46UZfd*Z>)PGyvpCfDJz0aw zJ6QwjiYyHArSECEP0WUW*i_G>PG44EUERuiqH`>56V+T8^@(|&WGm_p9yW^F%cdFD zKhm>swFq2GR4Bj7nT*mUdt&^+lz`;bnVAgyH&BHkx~ zU~UL?(UDilMQ29@&n3)a=_{JRc41#PNePczZPpeVES`k~PzSM_;T7m5wUU#f3z%sF zqH1)3=o*lWenjb|HP#9;UYVzydK!T5j(Wf112EnOn~0`jLa6GK+Jw&*R`2BM2#@JH zMqo${Qcq7JVWb0W@ws!D)=f6uuEONgeYoA-?dO?5L6V^3qi5G)!IUC+jk$uJ?@0+# zPg%$ID7x>@26Mx@uE0Wdr_Z>bzp%9V*h=j!?ltpFD}E+Wo}S+`w`8W`R!06*=;#er zJ1%dimwQh23ZQJx8xHXw4&e+pQRjxs0+0-P!r&BptBps3KO|h?eu?2aQEZ1p8($6s=#DSA#a^#*5@2wAeJssn>;^|zLAEd*!H6@gbBcUB|#^F%dCz$AGfHg;$>-^ zxtzCv$wJwT^Ku50B%9~AiS2%;fuV!lFsW0=4Bf;$?-4TFgN2!*gXPE;baf;AJh=wc zCFQ;#Loq(GymHH~%29O%XON&*9SWPmw0_}xjuiWampEOvSJ%XlPaDKB#ARD-YPVh*<>WbebK2*0eT5LExc_WSgJ ze0AJSZbb```YjXaz}?zq+FO`Rl`K>C)teauYdDKM&K%?V{zRf}I<9Pwe$Qq95mHrF z+Vc$p-OqkVgylpotSbwUKH_2j>*ba4G&QzREDQl}@5JVk ztfB0Sz!v&1Q0rKYc|!DAH3(Y2KIdzPfR@9WaSk|L1Du4vo+TUzxEUx0iqu&JbKxPn8+fe;FKr8vS2w8EBfWFLrz0hb_-BrgCX zcv1F?d-<$;`>RvlfAgSC-jjSDclKnN%4+gHLww!Bc$2sd ztkb&tE;#ul{VwH8-{-3p3*HuYC)l}jX5Y!#_&cV4wY`_XIu*BxbcVG6I;J5BGJc=6 z2-Ilp-+L`l(h5-SfSX0j@}qao_D-C)zu4Tsb>`=CEz-2WP9hkLwdb-cpJ}%%9wQKCJs|;mR83daM8j!SBroE_`tI3*PjdZumG(ZS3;pzPX*^N`DdE65IAIGH{mLw3N3FnAB{bV` zy6+n`dF4zxW2#H|EBR3(kQ71q_w!^$tHuxwgQu^*28ATcliL?MnJ?cVHN-Xq6}jYhCLezqfQn3GQW<2PxC-H6?vWT0Wmn_^~ExC@EC=<@xj~R z$-bE&K!m*ROnn<~*>7Ir1Fo(<`PPq~AU7eq|8EtgYfB)~J%aN=p`n1sJCz=$b_V^ZGS2ft7= zk75I&60tFu|Cco3?e70Y6K>T1qWS)zC~Rb6%+ce3>i+Eq;7x|+a!gN*iopKs8ftq& zbV5`D4h>MqIrw@%5`#YKjY*8!u@9E&f1(m1zK@Uces}Q=e!BSvfXp}?6>%Jw7zIvk z{~Nvk2*~sh_Wvox2mi><4|nt^Ch_=NxS3%~q2DkoaJ;9~@ zPjpm5RAR(&Orl@Z@#8TG(O8R>22bR?7nK6%x%K}ZVP!`6{!^qqsC3k)sKY1>DiMW7 zMWBv@U#{T2549Jy1AIGzHx?BM?!=>_kb4`DYw(o>R1)~aq5@Gds6!y-5#&C2`V+v$ z{}0sHAbkuf8lcR1{1X=gXdMUl6H!M2C-Dd$G2ko#wI7uVu92sBb}Owjul-2f5(DJ-B31Ano_y^AO4#pyI(h5tKX; zd>;lmy)E%SQ^W#pqEX)f3a~_CK>n!+)y*g;gyK=av!(l+P#cl^F#cV6z63eJ(&3PGwC3z$qp zz^7+QYODEL?f-f(Vfz_Iqt+x*LX1?C1m zTLVxq4!-yHzWI9{Y7=T5(7BD^djoR527GS-IQWVSYCXyW^*K2E99;82xue`rUf`+| zY76QMaJ~~cqCsN}NBN?50V5L#boU5o2!5!8K#vcgfE|yG=BFtpI(6aW`J@AGy?!lA7C1M!DkdO1OcE~g#x2?5d8ign1Do( zUnpqifrv?f4M`|yO{oAIj@k}T$3fn4D1VTDB;mQU2edOm`#k{YCW1Ue0M*^#UM%1(4B+>G zw=2pMP;~-tSZeM-YU_bq;ll%*IRlxl12trpH(V1N5LvHBq`wx_l)28}dUFIg^EdqY zwl?82SOeZ3W{p@0bZ;f1f#$PMff4%mvE+>w!dkTywFJ@5w;J~8e`)7GH5WdIHTvzh zIUh$*md$|o39B?Afe8ux@07qqzckS=P4r9uJKN?2R}&KWKUD$~{nA9gG|?~ppW6B- z$~_^0|D6)p2x2NL!0|TP0Y^zb15p$>M)LOaT})-koA`?P8~*&~mAB_G{vE=c+8jYy z^>-xiZFKHkoX#A-`*$2~HHZTJ8}sgpnTq-IZAvqpcb{+3a1Z?MFyHqEu_{*(({V-) zClJBU6> zo*clVj-jrhO2E4SeAa=q<*3`>tPph>T)PVBoCC)l@O=@Li^@Zlf|Pi0q=F+2&`by4 z$;f#c_zmAn1G&LmoCRFmMV&{TMrEN+p?(7Ce+3jvQFM?4_)~P$6>y#cLi@?!{WFLX z9tA=EV<3Wv1!sxi>^Qgs)Bh3m3!rrY@N*S#mIKn=0{o1E_@W1LxC42?(NT9WMX>`! z3wMF|;Q(M`T?44f1n)>#aIlA{&e_?GlYcw3oTMjgRCCGIRsL9VK;&}h3OW-7A>9}q2>Pcwg zx>;Hia+r|AKikcOK1}Gtgg*RJ0u%c1PiaiphY5X{(1(9YU_u}MDUAvHFrg0<`tVN) zOz6Wur7>Y2CiG!KAO0zU{|)-^o>8ZiNhMO5T&H+%d`u#d%GG+)wf_-2gIn*9&}6M# zI#n{RHW=m5dnS-{Od?YmuUkZCL(>!zsnTddnMNSUG*u^;jOlJzgr0?_8x<0T&SWw{ zgD}>pk&bC^S;U@$rm7|5Mw1SD4NWr{HQ>C&A}|M5R{e!U z;GV=V05t)uLjKqyrWnq{&<{1j7+Ix7%uRri82g}lXp&M^Wf5`6(Bv@Mvub`*UB)O)>B7}@&#DyV7iDd`{(Ybc$2{cKi_|qbl`v`;s zLrhZ3kOv6lB~%Sf(u_Z|NTnQsv;hdHB-3+?n1=|zFCZ37#xmp)LZ%gln532=6$qpS zVuDIi)LP7l3LsJ=8-^GqmLW8RN;AX&>Z|?BB9+GoqzQ)Tq?RF-2&55)n8qwast`y6 zLZnV2EkVGK2{_5UKGms0w7HtGAdD3#MYKg&{hbWeBK0 zt;F~oh8QH4Ay43pOwRxWuIdJhIXy*k`V*=IOGh~saXK=|(Phg};sux0&$A?tI7Lm^pn$^%_xQz>;DTaQ5X2c>A zm?R(y7C=JEL1>EXC0`@52n6;G0GTjJjcfp#(%;;p8MTP4Ly!y@sT~`DrjE5X_iDu! zq4fxq4nuX)0ce`0tC`)akyykwAlNDdD;WST!9a5}yGJXv2nJmP5LG1%)=T=K>7y+G z+^ZS02nKx(DB9z<;7Q{gQ}bkYuU2Le-3*E)q5ch4bs7=yxJ5AN4gu+RXkCv1Mr-62 z(Je6PN9GdtgO=Z~2jCv9!Xmg820t`|`ykY?4k4_xh$a2E`|1KTyD zv=>5+NSmj?SWA9OH-Jh?VW?gL=$Zy42osi|e3)tp4Am$ARR3DsJc-?l(*E!-!mv9e zfXUv5u?pZfP15j3n^EjZz1mY2ustx{-(hoal=OlwszcgrY?|EMqy5nW7y$IrTQFEF z1Lv|fQPY$rfIV#i3o|LX31byHfEBStO%Osj%K{kGtzKFT_ihLm#{Q8e69Ue*00vD? zFTDYSHRFJ?wtJ+>%<@kbzyqLrkrn|kN(PE5=ZrKN0O4Nk84K7!7+VN;=6Wdrk2a4q z=@IbH7QjLnd>sZW;qn##xu}U>MKJs-3|FdQ zxwPvVQB7lwDuksR3kgQy1Xp0+%^(o~GbU`*H@?@<*x1m}2spZ6A%z%Dkq@Vk>p&K2 z-cTc|VRA!bgPE&eEhLe^NiM@~rdA4+c(hs52sMm1%8^oEw2(jwC&+`nTN9i>$CrWx zP@|z?ykT-P=G6U`Lw`fSJ(-DV#ui*+K$2O#N3lfm{oi(eR8Q!AOGy$sylD0tKAl z0z9-(kAobB8~Yo?2=yx#z)E0q1oB#6DQy-TMZYjrEEKx5KN!6mX#WF$hD{84r>ObQyzco%J{fZ&x-f|F_2O}H z1i``Ry+G3+szH@^moxtrn(i!Q6EW+dZ4J@A;*h@`F1y$%Oc&AN{Q%LA_^dHq>BP1F z9e32KAFP!!Nh3OZ;94tc1-EDw{G!d(P#R6{qP_5ctTdAMw66b8?x<(hYkTX9g-kLY z9Yp-OsmOAF-+v}!h(-3H+k)uOn%rr7;r~QwWc&Z_+n(mS&a`a}2mh=hN{fbTu@e6e*B=>=CSa&&7wi08+Yd5@o#BIbEl zxXkMdS3%leineaoZ}_iy{j0;Za+cfS&yngqU3niGHg=#I7M|q`sF*JdAoTOxDEzd! zyu7~NpWL*4;r~kM-0@rfv+l+Iav@`|!8w;e;Wn2Y8APV$B6H@wP0BS`*?Cd6Efk!* zC@z+R`E$GOF;OaIHp${4KWcJoHF@#30$Tovm8*TaS;`pmbYxx8YICO#9kA3O=DURvg!TMtem0lS z<+iQK&FUBaZR7{{X;oKphKtNyuFoRpW696#S~-u5KGUajEA#SJ z9=nkA=`5$`PlCK0H84|~{?y=XT`$Dt&XjA9>(j`^nEbyn{e@iqjDM`;wValta}e+6 z^tJxve*X*q_qVfW*1OrOq#`{CV_d6|`ojpwBoxc6K;(%9a$^0}oL)E!68=FEcKn&) z$x1frMMTZ-%S4QJ?-*+(rgQFCv59K^TqUP(3?~m7Uih4gsQYueby!K*kwvJ!h%_Qz zbi5AJUz;9s4?^PS*rP=~hu_;mSh@@na)<(9&!4{8F2qJGZQ9%mG&JuBYY@tP!06`T z@^jDfj5mLVOu2e)Bd28tIem{!t;a7h@;34(m`!)U`@rQ!R-RRE zY3aV7U&&o4GA-MpFULWJIS68lb|i!=sS!kWYLW& zG0qF0(;|9q@zz&gOEx2)M;4>a+g@5VMIw( zxCu)Sj&`ok-#+U2xpr3Hb|%lCUichO(KlZFdxwR~$adtzs2l6V^c8t+qjQ_IZzv8s zJA|dhb(Yna0w-B7SqvWj3?=mK_aD|F`{D*z4t7xV~^4aUj}`4IbFuQsqwCQRV-^a@ z_mNdCaD0*ZDtn#X&*<4&u;u&ythBsZ+>air-b7=XVa4n_C8~u_vLhvWHN&D33t067 zMKC%_??um}AJe$62%I|&YJ~-wUuCUKKBI3rL4ptc88qmu??kQW3))`B9K$tq^?heo z3uRg}Z!=o9of(QjGy2*n$nudtW3~M2(r(m`{TY^S%r~@H&+v?~CG1*ZIXj!lX8CiR zS+xCMj=cCzF#?KVfdc_R3{hYSVlk_0w zM-O8wu_+^Fc(JeV70B-*{vgvy=hJ$*5p4bi%^Q1(l~^^V8?lBljr3U3(BhskwL0Pr zo=)VA%%!vs6aOXcuhxy_*rgi7zZlaimd~EDG+W4QOS7;Z%TZ{3vBwR$7jsoh7rV8% zK0@|D)c5=&#p0+AEDBTD5Xgz?X?YF%GvAFL8v2^$`c+!i@_X2fR$;?0ho~p{BRc$* z;m0jxn#Oe8h#Q?a1P&k7(g94L_A+0lSJG3dbb6H1mwG`9aynsEGZqSrxVUGS1emf< zU%G|?`=v1zHw-VX88`8%#F}OM$BKQJc|X0IzDaTS1X<;L_AoVs(G|8fq}RqedXBiR zVI3Jqu&vsbe9mD)g;$+rqQ(>+KI&~4q=GG37$^C#Yd&^MNq zdLzEIrtG8KU(>yGEydhT>AN9Ii5&gzCD0^xWSpZ@gUeHwQ2F$>!BvgV@?naaLpB>k z&6jB_RY>XSYH)Rn!MLAbEMiOOCid!a^|y zNM|rJca!mg!K{k+j2YF_4vYz9x4zG*tW(SFz;a4QUAnrE(n~i%ra0S&+sm-@4Trxi z;flxU-?aP?C2bTO#r?#9Ysl|byqgepb}5KHzVD zOE8FuK9|_U#q9C;bwZ1us!p`oHIkup(7ciUJXJ>qgOD6@`Kf+V&zuFNu@T?Oo+1s? z8wm1^%lMfPCz^`hgT9oQO0799{|5S67AF35T9R^=qIVw0y zZlLB(d=x*2^9Y57o9T&-8a^L@PBrr`+{4r`xruEseDONS#2G>_=U{`PQWAic*koBZx5O?$}!x;Bzp;sy~@$(2D+0`3j&8j6l*OYe3#{dgZ4USgP- zjX%KpX4Axce%WTUbqfQ%(LZQ zG?8e<>~&S2))6!|oD8wWbuI>^!4&fL68gFw zl!Gsax5o;!LT3hv<-}Axk!&WIc!|rT%+NX;I&wp>rgNXUNsY=)JC|dZPS%qZs5Ksl^@NyjjTYSDN;O|~#6;Gbec^L9#Nc?yYN#22y;3{(UFato zw5%rdrEaLK?PN631#u?!K~=h;CBy=#%F-ZHUb=Ly&XZ~xZaU~w{C|rM%+u&FxR1a| zP$#|12plI^gw&{MiIng-XFkVEZY?Xzj|{TLtu@1Pms0m_Tgg?!&3MyGKQS%i-b2A& z;tIyogkEWdO594GVx|M+If;=>m@xw@qYNd(t|eNCl;vRxab{9aF~u|nXezyd$D|x% z)!5&au^+;+joJP9ZbI7+Zs~|uWNt3uB))--`6U$gi5zA@A?w9}j&-5JK@da9c(IOQ zV?S9-EF|>#Rw(BoH-nF3q%d6-D`r%~bt5lVk+r~#%<~+Q9HSafJVaP zZ;!i~MJBGN&w~4#^kS9OK*n6!DuE34He$SF{ceb5m`7YkOgM|5w)Bg$Zx1i1T3UAW zon#DAhg>LvNO^wR2=x=!aec!M3a&5%vK`hwEwPpCjHht22e(`+(zio)F~w{+oF(6f zR4D14y0nBE79QA`lu3k-5;JoIN6M4jLa-Sh`gcb@x)pjNf=sQC(jH$Sq+~2 z%=_AWk2gjpE7wLU%e$DCvLWRIbQ-HCMd8g)`47zI>I73K?aA|oN3aY6TFW3LcH_2s753x0frT~=9!9|v z#RXrho+*8~N7L<8c5Im`rp>PZae{<;8QxF<);sqqW)%o)W`0C?_!Fr{f+!bB>bSyO{1229t|B z1)cDG=iAwjuN(4>dSmc~+X)_5!`96)OzCRsIQOQK_Zq*`vpPH44w-g`5!-$1MVFVarh7`J-n942Ln=o|C2WUng5)M1Zf zF?Jf8PHbHF-w0#a`#SArY8Y?lUUsr&6qIZ|W+YjElA(^8@uJb=uB*>-Ru1+ z%oHa#=|ZNS*+2`m%P(5^>@)p5b`_mT?u`1q*Tb6Chv`_Rmzm9GGW=P7=tJ9>Vj6pF ziu=avu~zgtI+H)~`@KJgu{4(1imYd**p?mAhth)*L$+{*;zjI&v!mJAg6Vg=e+c2w zJNZPWpY79p{;v5!g1udX4NT+X*gEG$v+13I-|7B7ta-n@iVj-lESq|0E`vCT<(UMn z*gQu(&tm0s$8UFk7sg;O?PYf42qWV@FkHmqv2?T@eU{qj__X|Ope2j+CSfS5+sB+e zSX>Q`SM=@iI3&*5eDygP7_p}vi+VV&yb>**uov3y{%085VL!73TRRI=W)C(m+rMSA z4iaavE7hG(qWHvee9?=%lGJVj zgW}YxeqoJ7ouD%?-wf@7#c8bQ;d4;9`fZMVZi_$RL}5o}qUqK1(NJiDZphHMt4&3s zW!G~jkhF5MauU5GHwYWMdr>F)8;JA>N7>I}nY9aC;`unGb3rHQvag+qbser!?v#UL z@e*j|@4I#uor**`Vf-4#As4sF+$EPAnTcgLFD(~p6b<@f^Kmw+$hmJOXQSjW_llfw z%O6+C7~%k6<@Fs|nbF#eUMm?}k=baxw0pyFsBsN?gqL{KMU|6>%SpeMyWx_b7MFcb z826%kkvxi)BeSv8!cmJ8b5|K5=#S0^eY=z#Q$kYX>SBShX7L`k<5}c)s?X?_-#8oe zeU4wT=MMw-6ja{k3TAX~-B84fV=q4~jvelUa+?wMT;~j~KI)~H4jXnCP7E3J!)HIm zEZ}6IG&>$sd3QhVwcLngVB6=C5qcJhCCl3vgBD+g==uGCo{)MQGTf{nxH35WZ04~b zeoNxuqT#nBzuyf{d_9tdc2ChiizQ2&ryV?X#0K9Ba2JSLDlyQGGJ#Nwvxoc?PaHQt zBc2(zBHg==h{Um55B_8YT(|Cfc8<_yeBPhlRnqYU zxljVN+%>&%@^$>D?t3Hy(8RONpIhQ$&7FHCYGmfOPFv0u#%XEYe+{9Y?azz$$-F&Q z+{xp$DGFQPX70l0MYa_CtI=4Zu)J6A5d&CiNs zocQsH?T1KmxT+7*Oy#h`(LRF!y>P23Q8p*=aR_(g)oe&kUrd;vz`IhRImfu5|yk}ec#v(o_e)KgXnX9bsoiv6X$BU4Q2kp22d4UswevEUV zkctDC_{NPL!8FEr|ABHTqR%OBn-!S96N{${3#+>)R~-k5P`eYvVkxMdO=hXmQ_asp z{v2hm3kUbS@%czH^3QY8Xv~Ob3iHcbhi8qRv#mIt5Y99K=S#}I((V1i;R918Qbe_X zj_`h+$(QDrH}-1hH(h&8aoHhUDMA=3E;RYV5qHVhcJTuSrWx79#p-`Y(8@@pGPz=9 zVR>z9zjk)j?AzX=VzfdSIa)d?rp2)n?^V567!~h^Bz}nNlN+=6dP?vi!$~9igzV|? ziS;i$m-B@ZfhG$}D{GtEdk59iiz_l5I1Gu{b`&njJ4_dPNYVe|M}*7o*}y{GMahmFJkdvI8-)vDFwlatf4^NafB)lH+>?)HZx+vUg= zslyPd9QoNh6?}8?EiLK=T&~4gS}U|sK(=(O{kXV#xsm_FotnxPD~qceI|sGX`gNlP zx8<5BoCqGUv~GvACQ5FwT`KmA8UYMylwPG} zB$iu08y0BC%HIlUCV_)n<>Ga5x>Ve;_x)FBWE8dMomCzmvAt zA#I7TM`#aK$1y2qM9%1|J(&`x^(a22w7Uvv_c?hkwo7(#tJEy%i*4}E1-^2B5d38b z{xjrkmx=x<9+z4rT)0KTC5MrJlos`n7N4pX7MVq%*H>AOFNCRBehTFvZmG;a1vOZrw@cqu`{+qx%wGcS@E8yQP z4~QJw_70i(&ChU_guEJpepmWj*(26WtGH#{TWR1B-Tam`I0|X-j%u)9_GsWpe^I|_ zeacgn28SUH-d6e?l!s+@g<5w$JrA2 z4Y7L~(Ty}&?$D%ZeeAy@P4+^X;OsfrnLnuz5LvO1u}gzq>m&bNX|Nm8;BO*hp4dqu zC{1ayNQ^3pS_o!&PulE+w0YBil{ONiSuWS5%?8f}y~C#X`_g1Pq{$mfuQN0vz*Ap1 z2k#Jq{ej@OLh#J5;m%3LAw+Yv2e{q;!2eLdn<3!8$XeSc($uSTiNn~aW}MR2ft zCD_#g*iQdl{|mt`gh5Nxllg5ulVQV8}}f^7?S!vWjwzs+|jyu}dgF9oamw&j5} zows@O16v5e{<~ni@M^n2-Ns9Hfq~{jpuZ4kPoR51N5thD0?ma$e=gAef={&m0oY#q zCHwSXvmw}@33hOgZS`Jq*a$Wgg8es=uj~%*vF7L{2isuNA=sY^??}|$M}>y_k`qUu zsSxN-gx0z>f<_VnJgC|nQGWZYNIqVE4C$T?Qs45(YhkL-nKmc7LFkW!)Dh?gegy*M zi>=K$V-Wg7fw}@+8v<>~7b{RB1o{JkdIDW-kD>VGv;&U8qAQ`9{oMtDE?)y}zu=`8 z)@TUp_waV#739+C7|H%cn{Vu(ssxvce8zPQl)atqTnLcx)xIXkP3uH;i4CiE_nQ#p zbIw!IbEOTg`GSytaFgCN(jRa>z@;;ASd;z7_J5J5%O{iB?!p#0#*(evc6ZVvWX$Jb zp`C^HsjMt7iAe-F>25Iwy4YuDM{v$d5{-fTBD5nC5V3EbNx1b(f{npV`W=k<*bD8| zar*)AlE`8P@Uu`pwiSm1?*f~*UXmz{xAMtz{4pP|0g@+UO$aAX-R}`{bFT|x@<@A( z;MJ+gNrj1Jf4ZG7#BCJFILf&40zMYgM&{jbDR z6U^j^)0k#~|7Q{#iNhtB$-|^EMv^`>`(J~>e3Pd{V@L&v+5bkI9O?Ju3D6ir0xhrA zn-d2{eoh|mj4?%$igmoGI1@T%fAZ{RjKO=f-^87PmD6ejveJw(?4OMq_*Sr5dp&dV z1Z0fK;csGgaW7-(cUzswLya*Am*;NNPU4F;?G4S*=Yx?XTYag6=Uf7Nc8W_MsYUW0_K_ORcUl&rzsxH0}?{~tZ8W6F-=$aY-U z@&3VeNSn~-zcu@Aa5rn*>;wNHi>%#!ts`k#nM2%7Zizu(y;_z)cbpQr!+ zWQV&sB0rzz!_$0tnh%dH@H8JDTjObcc$yDS^Wm`tp60`2YdozFPxIkvK0LO-(|mYr zji>eDX+Av7hsPFpnh%ey@w7fX&4;J?@Yn)R^Wm{Ip4Nw_`S3I!9$VmPK0LO@)B5l< zAD-sJV+%aZhsV}%-H0c$yE7E$}oS9$VvSeR!G=|NqQ~ z2M1iM`>OcgDi7gse|`d$Mjdme(H=N%2d`k9h?|X8-wh8l_@O`B6`x^G^DZgDF=KTa zO)EU|;iotxcN<;H^E@)VgLQ{ya}*wr@iTvB&}h4!MbyTu=MMROGG*S_$MF~bOxGN^ z_&6WpLhZC0PIxfNFa7C`>3BWDQD!{1*C1r%@$k# z>@%q=>305$@GuJV5L0JRWa?X}HG}aU_U2EC%j5 zq2JI@j+l75UZD8RnB^@(_f5Ad!Fk<4KK(I-NNZS*fJ}KElFWtKkKk=+4$0F(Tn7mw z!!SgH!*d|fg&z6#!XQML+a}pth*h*3VX=zO3Dy=#dy>dyKtk0p7Q*0x?5a3QvNVT5 z-UrffcLd^*$RP|uus-(PmblFksjI)Lx%h?2#Ey{v?&mmLIYutkJX&p zkhs%+Xx!;@BC2Itt_#E?e_j|1DHedNNu=wL9Bp9`@-_fj1;QYaG$RZ{E(r8pk>J`P zrH{fOq?v$bSs;SP^;Sk1;8eutsIE`>fLIhEOhn_LiVd3A;nUW$YBN5_V3I~Vv zI;CjR0vUC}AtXP7B_$Bc42KY?gfdAWaz_!FXLpAeq4KiQGs2!gdL|L7Q|*g>Jrqfu z132R<6n=P4aMH=)@G=xB@m40uS-+-O6bZCiYX`%tP$26Yz?e`DTc_mbUN{K@$RaTT~7 zst1ols2|YQkt66~ClpQe;HdG%xQ8d)`6o8uqfuCLqURFAJZgMC1{3dHw-j+O1WR-# zjCo{!CfJdAOb)F@0UwRRQZYT3SZ0evCu;ZrHG~ z#T}MK?70L%TjUT?w;Z_w(4?!lgHd$CO^mPAcptQGHAz>{TvR{eB3s zDA{;Zuw4u6)#BB>uZVvT0%mM&^9^~rDJ|TSOOlX?7L)Oyq6N2{ZT?lj!#2#LeaV_4 z;2(uB6Nv4Ye*vtu=xS&ElEf`jEOb8(!IE^qG5;(NbDI=7`t`!)ysf}L2?3LNz%lLI?YO7 zsWcgaEDZ?lcFf<&yPMNEWEndr%g?K%ARPx9GOhw>xE&(FSjcl)$7R^Y-wOhgWt&8ttSnv1Cs_md7auMkE&Xe zk=@dyDfi{CrAe<(lg3iJ(kr*hV%N!7ae=o)!hgsswl$V;T5iI9WKG7N3%D)Zzmm6l z&*X-^UT{$6x@<~=(SHJX{Esx~2>UOkL1%8w-nm_%QmqMwZ9mll-jp~Wxc3&?3m?k%TUBej-a z5_=wvCN~tM2@&b8>HS=NSeYa^qt|sBIAz|($X@1`rYeWlWby%NNSHdZekLDnO^0^f zjcVC1Uy=3f$ePS&Ao!6I^l$PV*c^ek4@l2fVlYbOvRfjNTiK##`r&ADBSRV!%Z{+0 z%6Ddybd>rufm{ zi>Ovp$P??F6!%rTcLKu56+|cb&j+NCyxc4#qNLXpfxOmNN}I(7c`m1lg+iD12cvfb z?SCMt0$h2gs8I~Pd%lrRQrlh^1`)nqJT8tPI2gSbX!=8CUwL;q^IxIq&O$a3vmV;k z5ZxoEs3h8?u6&V%z(sU;KS1;&K5I-@I&tlP#~rom2WzDab?b)@Tx+E1bX5XSn*^W@ zrP1Urnk0FW5tsM0uK!Q&sAtw|d+UpZOfpV3e1C2#A{E%5$rxghedx9zI%dt0lE{I?cxQ<;B@?_2}SWZ+FMr(l*?6eRcV2 zMO(HPwFR%@7pDqRd)m*uLQSqj9Ptgr+qYHV5#LrKj|&?Gy>cFu_-|q%XT)9nFLu9u zbAEidySciwP$?C1nGBQ^eTq|y#wC%w7ICBP$lhJgj>}}QAPFm51;v}fdO@GRp4@zV z;r~q8ZOF|3?<9%1JUQ4}oiF6F7%Co%l8AOLjkem{!JWRxrG>%{DT>_!S+OSbAH9&= zvW(||x&4pB=EaeaWmCy`OlpvSNv-p{qUq)N0^1Y$HrJ54?%Tu39nTk%UY7l)J}Gkk z!DEIUZ7x6+XR1pj^TT_lQbn@;<}bKN{H&5Flf`Zj!!`M6>MqNR`dk_(t7Z zE9X-2=mMk{T&>8_t@WtuK0l_+lWLco*yHTXKADB4zXs}5NFr0#v8nb(iJ$-G)G*!7l9 zw0R+THo7`kE~Jbq{F0b^H8SLHv3=*EN75S^#eQK{^{3F!w;;bJQ~4;b&))a1rT ztrn6Vj24WG{FHHpRo&?}FExczWdKOYv+w2V{M5*AFQo#C=Rt0^?%B4Zd^V4f=Fw6v z-c^SF1FUEFx|T?Ce4FoGhO&tf8OwLMK8u`>B|o!kW%6btPqN&PWnSLOV;7P>o#piWNsyPL1_qhL zNQ1L=y%3i>Q?5O(Pa_v&^8d#47jpSC{;@(%cPRiM#QQmYjZ{UGBk}v&*)!|i>{U|K zAJOJwT&pB4zaIg)i^MW35Rs`}N+>|Mn$rtsLBc;M!j3;PJXy(7(L~hzzD&eu_l~hv zVmjxJ6`QEm&ruU&V>o%x@Pcf;6H3;QCV080v`MGCO*@8bqrd&O@q3T{x zas8Ws9sdNgtfAA+lzoqw*+xugxb418xe)Awn=vhi~)55=++vjcc{6fES)NJaPk=E1-XQ!=N4~$^|fS^6#9$N z<}HaeCXq*}tB7oVvvT#UK5qtzI8tKk+2%0qdLz|{#3N~H{M>0%D$`WTdAPY7e$#T) zZO`?nta1`0;@pW1U*7PW+nMXgLgb66P1z`YRbCzJT5%JW9vp4zy6}ug9jO_kZ#$Fu z9AC)s6n*2xzjs);jBH0fBpH1trmx6r8=c#veM52BR7&!5Ev~byz7#mgdO^x!*@Sdy zb~=9%sYUc?=}I~th&Z~L#X*UPzZec?FbK(rkjJlILJ(Q^502?Bj3kYV!$36aOmySpiBJ+Nx@ zDb>@WOxYIYrpD$*pYd?*Q%C6vB0sy59c1)MBdE>0JPOgqy{Pme*U?3)H4USBEEair-bAS$X2t9~C8~u_vLrR5C#xA2l~};4A1H#+QFK6T(HFG6q}-lsP>5cpT(wZ9HS?B&Y3hsNBA==UFvXYu^t8Y#+I;ah2`vQCQH4r>~Ur>GfMO52-19vGA+BB zx{R8!zf#LglbULG%G3%Q+0D#Orje$0VzBKeTK0@|D)b}LOC&C@o zfkj~o8v;2oJuRFY!EeHrma*VrKhXG z)h!0&euA-xEuov(OX|Z?>=L_aiOcv@a?Kw2oRB)z+&A>qRsC%VUG6+cCz%;{lktMV ztcv%H8P(Gcj0t78zT``FYPlU)PU$EmyweLQy>t^~inD#Vy$nm=aQNF2u6UgOP0J5a z(ni4%by6w%nHciB74K54e$AE-Ow`S0(;et9QYX~lJx?(sgFb7?H;is2Un@C{9)~%of4!_+hb4R0Ss>YD3 z`ObMV`bXe*;x!?6I+EWNoe$D|7)wg$YgnnHounMs#%26Wh^cNZ^c2sgWHgoJofbz5Qnel@6}s_)O1vV#PxUCw-A(G1W{{k1`(dSb zXBUmNKn<`IjMgaFD;-m1sHI4*6pSwt7SUjHk1AeLH!iVMUt}JcIq0b zQm5TbGAu!<8HQzpn$fxVwa_Qx6#r(NwRlDK!;}oS^kYtG?x?k@8gkd^1Gun?Ij23O1X5Y{LSdEP&s9CXmM|F*HCmkTY9&{ zDHX<9R1(9)G&O|d?@<_?O4X@#=UVddQjq*kAd^+5Xew1rULm8I>?E{<;8zN#8|TSu zgqZQ3BIapBg?TL=rE2;tb?VdW_Lb!EiaMnyog{_q6SEYxr^SGh z=+w7CFTHRblt{XGt7sz8Qboe|6WqoNbOTQ=YP{=7>fTFv?y2;;b1BJz>C-xb#)gw2 zwz$s4fHasw-d=)w>_IvBa(H{JKr3`+kXV*--pOWyiI=!c$_%Zm6P{X@HJ$rR<_-QS zsRjPO6ipZ63j|yy^z>=)JC|dZPS%qZs5Ksl^@NyjjTYSDN;QgO^Sg0F&B)iT_4 z(5LwS79E(U(P3~Ofs>$4dYAcsoL~`BqoyTN!sDFD@seB1%JL(FtZ{42u-v88ecM)Y z)o?T3^wLjE%eePYu$Pd+@}~(3zK2TON}gh-1LQf0kxZB|11qBpCBv>IT8Nb8VG40( zQctN8^DecYH}II0W2_qcyE67eShg{{AKy)A`@t<8@rumNC7i@Jura@c!akA1EGT5X z7|=0_EF^Fc#85I`tYg^NPu3C(34Oj5%6Z5*X&|17^7NZgOvz}vj4U@JQr7X!;Nuu6 zOjpH<8P#z4%yKP`1dPt?QulP5pEL4Oa2VWb`}XjHs-%HE#-umzLn@T?PF-3;4GRx!Osbw4ddUoKLsxJU;|${lAuQ&p zt9#>_bi{om%sLqJh2u>5{8!7$umPwR)Z%$)H$B-@y5tx<=RMPc^A`CHl%!jPGj|i+`T-j zZkbV5z2!>RI*^O<@&^2x`jU`$?D!GA+IE7X7AQNCWl>FDA!#|aN7cn%>R_z?j9W`3 z634uR`iY2o(;64JAHNEB(Epk*yJ7HP49C4IK2fw}CRLL%j=Tu-1(fQmmrB(MrcTZ5lj`f+vcU@2;LHeYr=|?NoMbnJK55b`t~M3db+} za?dcHqCe(PFQ>>do~e%xgHJ#oXVxKmneU7#sy3c7jq!F-U%L_!)B%rSuaz3@cOC4 z^ylNQ3ga+-!MCv=zu^+y((Ec}>ic=TLokX>B{w?3gZxFD@2*qDbSyp0=$p6d4fvAr z`4p9o${2ozQm$e1_lyx^r^H1?i_4BprMClBOT>uU=}_sJU?;-MXz9)aIdcYbx$D`s zQo6onXht`L&uouD!p*elIR#8sg_%@6Iv~v_udSd zliL0lX(w%rTRn3Qld?qgjd@zKR~2LGu*b0&JB>{zHm>_`gfZ-Wopv)djJI*pHu1?V0_c&kTwm7ScDFHFhOCnE~6)YH$yeO=HnuKgUSM&STTL<90vj zqa&SOW#5iFpUcfGuq`-lZyzR2qk#&H9URwmA{o1gO_#5F{h)ggJhgL7S7Ton6|_u) z-9?Wh0<~{?m~0rAY(e_Q-`FkhMQ375?OyLsVWv2_Nf$Ep%m!MhU4GHRXP@ckv8(7z za%a@1X>ipTBEO@>o2utBf6s1ndX;V7sdSJyh2*2?C5{2pvGSSiNx_(3hxAPE7B-oWwd_Uql^j)& z<;IfgbTu^vS1T}11VQDi!r-R-(;!~|;LsM=rd{NL$KhtneMUKu#fV{Y_I zdZR^`B35KNv3%tyj$Z{7;+14OlAVzwqFhbX$Z@z4={f?lF< zU7Vs$lVce|grFI6NT+2t9!zb2>3l2XuEPwYjV=x-pR z=sg9OW0|!JT;lmSrE@DO{MT=J7@@pu&65yrjfUL=pA<;ZL-wQ$tp#N1Uz z2>PS*LEkPV$CQxNxVl(itXaIr?RXaXo$52XS(ePW6-|wxt9?3$xr|6%> zlBLbl4xTztm&x~7b^%Z08KpG z{JAAA*4(*QqDE$Z>$K%uVVqX55Y-EORc8D1qJ1)Nj}>?Fcx{Tp*0-4{F^LPwmSTUE z;_!v#y?T!zV6oV@zbC}cG3RS1az9+*uT#&zCX;3SXHd}cCfD$Ry@SVxc!r%tPWfBB z#ZDqfNah!}jvE$HXh@i_$ykJLuQk=6YK0+kWU$@*tT@Jv-S_a7L6XB&eUN4U~3 z+uAo4@j>yUuNg@S)$g4&h91X@kc$WHxBq#;(Yt<(bD)rlGnV+qjh(VI#(4jMaw($E zDQ}wO-6U1UEsGUt_snS!;&qDqjWv>ed_q_4>NHX%zbJ1wb zh-V7(%Ug$Mjh?fuIGqsAGy&&J%D&R={leh`QzcSFwSSKAex1pe=9f42YUejydrfiK zAzUe(`716o`N9!*$=G)B0|urU*~G=_e@D>DNTf2kVr5}@ZEL@FcGc|L-lAf(LKrz( zIw_{bu@moAy;v9(?}j9Pi0hLZv-o;S@FBxVBm0Ex>F|m5FFcp?g%W`#3rj0&o7;N_ z)zgbBG8{MziP&}&F3CGg7kWt23atk*;&;`-41Vl?8p(7vUn~jj^2+Mk#>Uq6_Kv-$ z?R$rf!~c75SgqBn)#H5UCvb**g_{bMY-L>IGb` z#aUV_v{68|bgccjxO%yf|HGY{$`&h&s~bB9wbS~w6kQuy4oe-bwy+OE*g3TvZY>r1 z#S19Cai3jo|Ha8yppo>H<|JiS-b z7)4%-CAd3wPj33|mU!1f)(atv;c&QBY~xif-sM{1ipwdU74==|u)S~CkyngtW&P-~ z@2&c{)4~(BY!J>h*cuj=i{}qY@K#}D7kBD?rWC%iv% z^>+|GHy2IM?_3clf(IRz6 zQB`}mwXYfxyo0J;~Db;+&KFF@I8H!seQX3)7@yr-(mDW|5MyW{CMoHhsLNoT^ z;sx8IzZJPRAq~Go?sh{NM&$Z5?U!h*HWKaXE}K`sleX6(ZHcc(Xb)AHVq9lL&giQ> znG&bF11RyaEpXX z4kQ04E$SgHK2lIX;$wfB7UdBUMkawjB?6&r0tn63* zUj%;|g8vX}6Pip&thol2V!s!M6GGCW|a55Dl< zkOs#g4RBQ(ZWHfU+AsIZ`h5Ms_dB2aZvv-E`?sH(@^B-61^m0^0g+?d-XSx;`5DfV zkXJ*{?@FI5d&HV)6}OCgD-9f?o8OWKMY5#>+Ic4bu2=MRBPK=j)J-w%P~Yzh2^*gcKtMw%>lXwtMk z#%qx#dm&A5_8jcYpHv8ltXRm{r9rRt5t%_~up83gZz5x!*hwNNO=+@7jH*-;{s>dk zW+$Z0oBpe`kr>T#xh`!scrNH2HpSnUCfgxR-cWj-p%DR|`ocMQhY;)!1iux6XMPQL zPAU!|nyWp)?fwV;hXURV0slqT+CGt{UZqPMuD-CR=->B0k`@~wEx1XBJ67dJ&`TGf zJH7Y)j|IIRg64i4?3@dBrC;UMngH)l1iKc3&rxc6UX|l|G8k7L$EA@gWW5^t`5L<`tSN*2zDt1 z%R)QYxfbk>1GeG4>$5z!yNe-M7WBdH4H4A(!V%aZ(db_Zb|D1&pTg_RcU0y2bEU;Q zJ`bFBcRmEoBWkc~3U=iNtm(ZYSpwSKN(lDXf^7mPvawO_JN4>lWu{h43~_t;kN zC5Me*Ga=Z2Bl*hi@E&W9UUIMvHXVZfsql_O-F;MOxGyfzfvydKw&aTy zs1XAFfj~WhuC~We{Bqg>$6$FD5t`ZGT@dK|eC` z#ty1VaH+^=T*pA!+v!d^L5Xz2SNobEH?0%ZB{rD62i#UDyK0SaP!3?oN7yjQKn)w6oAYm6heCoD{YL?iOR9 zi+y%>1n0aY(HOWdLTsZVp?mx0nS@)vlpHAS?xf$rn2){CULCg|052u~NxSn|C?DI3 z!-02!&08-e^+~Hcd5%Bk<2AtIeq0m6$y4`xgxuWgf|xwg9wT^lYO+$0*`(c_JfFTt zbXa#b1u=QTe2?h2-j)m|?e63O@jb#}P23j5S3x3*-->EbHns;uCSv1@rq*40p|-DG;X9 z??MsWdF|e(;u*^TOrEKYby?;AnRxKXrfu+~Y>Zi9E2QClF3zeb^P5mnEE)q69pZ!v z&96fttUuk(7veSwNukFD{vK0M8br}^;M0#Ebdu{EC7ho|}QG#?&Y;AuWQw#L)? z@H8Ku=EGwPJk5v4)_7VUp60{Te0Xeur}^;M8c*xP(|mZE505SIG#?&Y<7s_(nh#I& z;jsmt=EGxaJgpB;^WkYeJhs5oe0Xe)r}g1!KK%bPA08ZVt?sMhf2%x%!~OXQR2p^6 znMQlyP(kWd4=3Vgqt$oA!wi1t&vwOU*wegAN^s0rokr6Nk9_zk4$0j{*YZ4%4DVpw zq1haT$7B4=pBXgTu4fUoG3&WQexIzuH}-M-g+J3Z2QEI&hqzEX?S>N`jPgr=x??(C zk8qS3&+Rn`8F@VX^1Cbv|KU$}P21}bRx<7OOeZ|T<~IaCHEgd%6wZ{}ZH5QxPz$Qj zC+)%}p6j;R3y%o;9bV~@2xcA7py7pw9{nDsk#|jW(v;Kehler!kuWBctk48Ncs-&U z!aDalWh1*}a9I+^efP^y43Vzp=$t?;{QpPcAzf7O@f!A-pNVh)Redrs&#G?XJIo~{=teluoyi_m@3 ztx9lSH;_+%3?b4QmLnijUWX)eVfG_<8=6D%v=G-p!pJZT(cthLNOYk`zP&IA5$3i@ z_7-9l?M7Iv;&XzvMbe%mav6|NHH?KYcp$qfj*=|RVUYKMG~6A5cqDQNgAlBbeYYiU zb3|(FFvy28TbwP40_~6?KMX=*0R(Ld#HG+c802F$=Qbqnv>zIG`kaVrnU?DU@yMSS z#zKk(AZrroIwVJ17=*kHKvsb;NF>b&!;lLCeODy7c1Y=?FbHWTpjj4(XNE(*RFEZ! z<{gAa^FAj!T532g3dE&ESePIwO44u_Bz(9>4xli|*9tN(5R2N3VUTYWq#_WH3VUIY zIRz<8Omm;Is9_ND&!9_5AhsC}i7H4@AYL;Z5|ipq6ce07kQEka{T!gG40(2{~i z1WJia(s}GHELMkMDPx|KT3u-86t;27E5Yf)!r~E?qQZuTM+2t%M!4{k$HNLtB_jgQqp(y= z&n1@GBGHK&KEREokuZ1>gY z18~O%e6!eEn8DP+pg&jiC5ua3SOf+a19WxgfLz1tu$ zM{nC)aObBMjz*IaltK)^HvcBzQH$SOt5@^W^I*RpLM%!)-V|)t0(-T1HSa6pAB2Dz zTibj?o^DDD_vDf!B%;L+L%`f}w)s~958E)0_9bhIfPWMMCJ@^({{mQR(bdlSC5c<6 zSm=Hnf+gvIWByql<~AvEs7HS}Z!7RmLcnAmaLhjmxF`K{YV~D;#_qOU7!zT63(P%OTSKxJl|54z*KJc5R#yoG)^IBynO|1XN-g^edRUPlc=k7{! z!5uqbn__wi5R+ekP!dWYkPrxI5WpsoN=P712*gx345r&)a5uKG+$HzAy=t|q-g{Yf zt+c(j`+w%Fns+aJct5=#jJ=kS=FB{2PM96jI7;nzwuH-J3?!TR?=r%B;BAuMerg$;YxnO4^}P&I76-b zP#|u@ZVb`OM60Z*HJToLfC}Q7I%IuJhclRe zVW2XIFoH54P_h^&g|Kv%+u9I}PArU4kw|)U2P%T+@Hid5Pg|8lAC>Xd*jlXtl=1O0 z>4p<3gV*sWvx<(}(N4k^YG}4PtWks`Y>jLO!Pq}kAW4cyyhmqvPZJ^lquV!GWe5rw zgB;Wlq4NSL+!9HTb3uiX^99o0#kNGAXrIIrOQ zHeQ^_$Nu4QaN>wQv;upL>_CG_RgN&y+3`+Wp_LcxEo|ilcb!#?Ag`d&!V=4|k;w7X z8r2FanFY~SYeQiRFO(j*Bv*ue3>$n3Z!$jGU9MLtcqtj=k#;%@n|ZlC8Nv`N2$jT- zTM$CBzt*BvBD^LE#J0Z3T3hHuoJ(1u#o|Z#mdM_W^6yfpfCTL+>_~?0@2M%!A-BCi z97uR|VMk#E3|b=lGK#*(^-Ft~ZgCunHk6pOs_1B0J46Q%Q1M6)7MgEH6m|)c$8Na!gMBh0MHnMfI2y%bSqk}_&!HO`!v5eIS~oC z`s_{Br52-3gJ}5T8JU7Iu;)-4LL<#!R|iI|(R52&uJ{2*6hj=*Sdt+mfgJG&LbC!s zk87~6+uqt#S6OPam<>9uTBW2Atsx)u<}?bD@hiKg)$4}o)(Sc zkdsTRfp{mx$A+@NL%f%Z?6A~Wgrc5Ii64=M;t`DE*|DH^u&1N7v9_|Tq{wP98;xLz zq)!^;qDdf-JX0i;UPaSz%{pv|V1Xd4FclOITBn)6)KOI3L`a4_d zDvK><6BMOUDG`YFi~(rbnU-O^9o7;{140xhEQpGg9{(tpV#}g)htRhth26bv#LT4E zX;hSh{5W!*52e!XuoYWio+$9*6GmS5`f$3%Gnc~4!rl}_h@A71=wWTOC19mNSEo$o zts|VQMHKC~pcfwm-R41*5yd(c2il%YHO zAzOp&jN*)IL?<>oxWe+nb!$Pma4gik0#STL-g}F?Kxj**$JNS()tf5e#fBg!;%F(i z=+*7uPl35B6=D2Ky7!INh-yQv7_?;b`dESm9xxw3r1D6CP&}9^U@aHm!5W>D^mf%+ zjp|}lq?)s3XSzl!5T#J$1qFUf1PuEMDhskH;C?;gji!&tFSu^pSW>f-uC_{xE(wlN za4f}FYFBXOBRz&qjirk#089~QKWXm6SBCiZlq#UGCzCf!_b}TbeKwX6cUzfR zx>L;Zv{#tb6tTP!pCgK%l;oV4-G;c?kVjGOQ!@L9Ei<{-(ZEAf`Jd03gQAW}yn9iqczR!%*+ZP7XE|>5+eg zol)UaiJ;{pEkL7)G*baaNuE4`v|ni!EZvz5H*$tPF)Q3zVM0a|g1M49qS8Oi(QZX| zHg|At1TAPrPK=sx`b9%7qV=Z6r`*$8q>U*`!Rna8i5TOqwDqc`w?&ZvQK9z!wOoq4! zf{8Xl3@h{zK#VDNDB6&gXGpj9LfGF6bY>segz*_bDjdWrOuxp-MJ!>ec@(?8zABv? zA@oa??ja`86cbuX6P!^D-$l)RCZX7s$p}|UFg?d`nKn?PcPcar19JQfqp{jhl+t-5 z%!BxjnUS~M96)B3&P+zQcY?teJA8M&aX?X`IHrstZIn<+TZfpvuoheb;NeAH7p(Ds zLpfuF`gnRg$6UIeLfx?O542kP74?cd1f%znk-@8mpU$dn|SJ@|B+gtCmA ztXzaFCL?45#kz`n6?TQ7R2)+JRDQrBW`KoOL!KegBtt@+cFxpqDl`d&eVMRu--QZ- znX^jMGs-M5gbd6)V9;wuJW*uCK@Oz0Odhxo10WnXf-UAQ1$r!!tmG>@keG@i$^sLU z$pJPwl%NNHYfNBgtVJ@Vm@=dL?-s|w){A@LzLE|_9E|E!p;0{%8fGpqS;0W4$cV6+ zi-5?f;{HZGay?+y2qIOF2WiUckZvlaZs18ZS2OY`IlxFXRhU9Xp~#tuW-pdPph@%= z1r!6yQe+JlS(5d6;*~- z4{!#NHbaxLM|lv1gPcKp7=hhVg5nNS)L4LQaq&zBd14k2_^KO}QDrX5_9NY1LXAZ9 z)aBYOHk3xcMZz?%F-!_I3RRIzF8PRu<`D<1jmVF%6$YsVTr`j+WyQRidYiuw&4al9CS@sa(ws{cG zU)iZT4(VZ4om!?&RRc|lDquPos!Ex$&Dd)YN{}oreX%=CTs&pfs!G&i0DLH|8&fp` z1fA!h5tqq^40?W}%xFM5xGIBC?9OC!gxEMNRh*3<$u3wW8!8_%;eOPlH;TqWgHYtk zEPE7X-JJ$xK~uIOlNeItAuHRd5dZ~^5Z_|-A&a13b7mGlhT_g9ZH1~`C8$+SwH(q_ zI^?%Zs%4cC?BG#}0=2Q9%1enqj`EIbb-Ajax8YZ6c?D9Jwen&m#(F9yRb#pe?JEsn zKsKomJPaI1_(DF$h*-Mrm^+z!ZUNl5MbEaZf)0haU*OLw|0 zx)88cBdh2*4NtzR)_A~BRF`vj5uCMTu{QHILja7>clAP}#8g68zfD>oQMIXifXRsu z5@zBV0L)Q0hW zRO3go`l=WmFag(OGWfv0SKo;oyghpK$V?$*D{rjeSv^y^YmqusT}Sz8@S*gxUP_|w z;B5UAr4?F$cZEKL5AwG9@I^F~>1!bYEmj@Plz_|cL_7KpL()`+Cg=Sf_sLL+0{r6| zJ28*@D84H&w-|!pXyup(z!iD4bx6mR-lJfC@6)voHM*$wf?Glp!oT=Vi$+9NXvTGt zfZ8?4mCTU!oOI)b)rcw)2Xd$3sJ;&}&hA3$PmzfLG}XEKM`K3o(S)(b;t z;SEd)qZtzRQpI>0YD$vKkeD1D9iJA6tB$!TSS`8}+-3|XASkHEbfH7q?wEc6QYlY+ zLWgF_lxjq?)F5ZHSu;TFwHhS04Cn(p3sq#BICx}jeEg6tPksIp&KZ^a3GTt?hpdzICN0R zL4w5~K85)75UbcAMbHJSN?!L=OS6G~!tzUu6Dd|lM_l86jfTns& zV5m7M!#E}tZCEQu4q?qcBt}Q3YJGKlfa38YA^EUCbXMx3dVRaj0U2(cPs>^|Pbs+F z=+O;8h)WaTjIr7v!#vX{ku^OVdFl<-F$cx*#R!OjWfY(Gp|9vj$Q9Lvn)q}`WUtLe zQhSsLAtgHU+W?nf8OW4~aPiSX7bHe`lJI6NhH(k10Z%UEco%fYdr#@PWrpf_KLrOy zovI-yr8sKhC_X);KopQcUXvF2*fZt8mm}GP8K?r432DnIowv@dMaR?j>!^qJ@f#ky z33EEe8TA|Zr|3)YdrF#4jC&!VUn>~8GS9iVcJY~#Gy{234Nhu<_%122PS*uBwLo568=(%9;((cv zni9>u40=|Ki@E}fv~DdrQ%{agc&NxH_e!@f&x9|o6ts9n@FYFTrpNr${5&1bZ6 z?P+jB{~{K#PP)tj3z{!Ns2GwgXyG6TMNxZU9)pfWon2d^6^cEQJX_ID3=}y89`!~m z>ZvsoXju$KNU4Tz1`fxNg6_(>LXEt+MD%i|fdnWWy^D`&FTQ5crl2*`)P~Emn6(e$ zdH{+>1*K;z=j}~no2Qz$^Lp9o_6$9&50WB$EQuNmI7hYh7z09SMqYtjx|!?4;2Eli zwo8Xj1m-KPYy9z63tX)j-%8;t4A`HV+)B(1x6at)D7yeE1r0s5vVCc@YZ$eNd zW&>(-D$50;vFjlwsp}hVVq~4uMeu1uheL{ZEEH%j+7*#1ggRAExEa zf(08YCw`vENW{sFd<1Y37TC% zM$=pzA2_`7IYWnGLCPTIpVmW(%?JT5wCjt&a{wRA4SVz$t!`bV@T3^gC&?60jQ}Fw2hh=-wKNwK@APn zs<9Yl-Y6pBc6&F+i!o~O>P;!l$l~pWgf@g-VQRxqV+QR>_@NHIIfX2B4nK6LaRd}0 ztxk=df9CBcMAQ|dEggD4P9jWSZ{U3hxOH)jX-LPkSx4`{bf9if%XF2 zIduvg2jbv!3Vf&$0TsAt+86{g98vh0DycE6J8*i7p|vVA@_z7)=OLRGiaeaEaZbEV zhp`LQb%BEmAB+@Wg6BKV&Z4GTO=r|WHBPv`%_yf2qzg4M^UOMwc!zfN~mM_H%qO8 zr6d7}QG8-5nV~)-YY8clb%i-jEi4g9BQy9$nWN8uIgNUltvJ@5YNl>i$xKZd*-bdB zj$^PFG{G=OwjoqCpkBk36j0e!aU>_ZPSuH0r9FWi5`B_2A2Y-a>Qt$mc&m=4 zh@iR{C1I;lq3giLQL5@v$+a~D!R=BUnob!KMmv<(Fd|#$QCqldL8aEJV3~tGUaoI) z1v7nS0KC;=7&KPFD(N$ZU~cA(+p2S`OVzOFsCB9ymE7Fn4QBf2P^Bwjx5LQCCt)ms zSuj;T)~a)vKww5Etcyup5pcU{tInzJ zhY3Pt^9;Sgq`UVl|z}OYls8`o282Ng$*iW zz?f|^8u7P$v<_ai4Ba>hqqwNvtnw%alvxFxVlc2tii@)xrpP{{3})Fd>R=rO5rzqu zSmdgDad(uhDshSaj07=!PVp6JjX_fo#fyiFw`gJ8Rl|U(KB%h3eNncdAtd@Ta>wxE zL0cs-Fk`n#KU$m`F{CO$C-A6>amACPvh~Ep%-k}ZL#(XH*g#hp#>6OxUX?~=K)(7M zeT`2<$#ogk$0S{gXDGbd9k_dl;^V~iN-D2>tX10suT9lv%Q#?w89S6m@iTRXyW!jtJG3SPZkfaAus z44w#qO&FJS%v3X~YM07~yP*Qy+R9Z0&HflM%Jfjh4lH2tf>ST&(ruOj7jdc`=G3eU z!~4dpR4TF+N>d$OJc3iEcPuz1D*fQ(q~;4Hs0GjEGjl)@_i0*xj<`_VR&Ex2XoBk%a3`$!cEf5-LW`;T1r5xZg z>J-^ZjkR$Q&B5C>(<9s}vSuZB@<`yOpP2{op|8jm508RjhMJ_ZNl^fxHbu5dU()8m zjd_AAA=4in2YRnnmtqMfc6|KMKnj^AfZ^D!SkLPWYy~ylp&$;Aq|f&<^a)nU+6t&K zG316qUK~vYa&qiQjDo9Gz~*{JjZYBp3}vm(m>)c$$V@+c*i&2N@uO@^1)Mwbns3FZwp>6S<%pOt^#6B&K~rYJaG~QS>%~9Br-8v5t0ax zGPr-i70CkEt&y7bK&V@B0RNhpQ-)|yUwy~~o&*NP1B1){u>ONS>&FBizn1E@{B zpb!32BQ>H1aKp1rJX3}b=G-&mlxkyfU6&{BfO0&UiKuentCAfoCiJ?3SPFQ&0A8B} zIC?f`q?m*YNgd}t9?9V?Tk%x3S9Vv zN4fjp@j;$p-HI;!OWtCg3P{iul-6}Pqj-e|328b>i4bB{Zr-TO5{4XV>_kB}ImV^j z_uwl7NpXDIN2R&A!wRnUp?PwO(xOHI#RR2VXDO?*4+N8)c-Rer5)xs__7qWfN=Zs} ztzbb8cf~6x@bGG}f+h%~eRi~2T?!u*Q8_`aL!$boPG>lY>qRgp58AW;Z&@+f;z_M3u&1DXDDiboka-6(> zS_*mqcEHjy!dFwQ&?%;#Q7TnxjnPtEUf0^~48-d>&$t9n10I~0as5)Yn=P%QoQf2M zfHqZu{k5^cT3lY!WbYaD$ErB5m;^69ocW6qU{hde!zfAVPSb*haVu)!V)Y#bP^qEh*V*umm89Sv;d+$nVMr zHHfO2YMsGUP-rE#fE8=$0X;b+n8f>}U@o`zVxK;7i_E*a!D|()C1$K66HCC~k1zod`T&QQ9kk zAFt#>_13~*A;SX(-b&+9QlTBk7!59j+Qb=`56o+RY<^mSZir7%%kf_?w~}$Bh?$p)x>SqN`6SZlH1wiE>8~ z=c3*kE3C7+twO06^zmN!%55b2ehK;`kW+8N>z~37tH%l#Zj#WC%g9YsqEDj4VO}DN z$~0NMR-veaWDMf^Z8MeVl_+tDFxhIX%oewXY(jk!J~VM>{FJzbO7uvSI4H_d!WO{` zBNrb2Sn&`lhKE;C8@!%Ug-?oGsc^SM;e0O6VI8#jZKx3F@mBVP_!ZH2Nzms(SY44d z00BOH0=9rnC~9uTW)5!E+o)8hL@7ju2sadY@r0_M3RHw!(A7i7#O+j|L!tm&)x!07 z?`Lhc1#LocUyCUEj*2@#jx6nGubQ;DA$|q;H`+pYIaZ%+MbCF1fwLr-w@Xm(r8*Zi z;jI}fO4#7uiUM)GZr(`++9V3>;RTv)Nfd}veF5JcJq*Dp&?-@2H^k#Zl3xT6Q*wTL23 z5=G$b8EPo*EW(4xq9|lEqCmitkH{b>&?r&hYm!lnx0855>7pW~c%w?0g!7?llxdJC zvqKz@GW15%X6vIeHCPt}AB^JrsYtyX8sjn4&_lEp!FFt|@xc2K(N;;&VzlE)Dri|f*am$2#KS~dDM5;FCum*qec}t69F(UwclViF8BI*7J11X^EkuXv1T z%Oq$q(S{lZh_)dPT4!>vi0RquFO{IhgdS=f#0zSnqz$xTyhcAkv?UU>UlQMVv5#l2 z@8Kx+h*)sE{$dGgEK#9G7tvM>g4UJXLs0^}{vrw5wM6SC+Dc3icnrRq9?*OJHVIlR zv!O;0(N>2+8}RZ+@m{}Gg7!0_^%8AO9JF4fQ^yX#i)VzHlh7KX<#nq|g4Pw^jh!E8 zEfTb=iPjIktP!M?>0L#n1rns65@~=)n?M>t+ITgQnk7g-A<|%ph}ZfppbdC;!A=ia zlLYO@L>n5R^#pd|vJtdK3EGbke5ElwLhFj`!o@ad4HC2;65j~HZ3YBqVi#^4L8_M^ z{eaj;hm^n&L;xOCUR+UTs`h;_ZHj?Xs7oTQg^M3&ytvWLBwa;J<3w5mzXBq~iOq|9 z#!S-ph%`Z@Rbh~N=)?+AwFK$AM4BYhN^go3emUL@$6#8E@H8{nSVE-b10eP0;s64! zN(rv-z}vl%D9gH1l(1iTamLPsipQlSAMKhVB-@TZ-3f}MFBe&m5oK+(lUE4_E3ZG@ z-G?Zb;nWYD6<*M}$sP#0bZ?p=IuV3r-JpXuVSn^~L!2(8)ebe5)PW9diJMigKiwlF z#V-jBHI#U}XlB`kOD+(m`*NfR6VOjX1JT8IAY-+YbnKQT!9H?}#Knm=(jJWG{5?*+rpGJpS~1{1iWI1ELs@Rm3p;)_sJ6 z26r`4q+e;LD6n;MW2Qi46R$u0etLw$hk2)#DAI43M<{}x-4wyZ>rcNR9-)Y1POK-2 z^sC<#MH6r&_E4x2PsVlb2!+qT2Tzs3BmIhYgd*zPLqSPAAk6H=^v+puCH9J_w8x)* ziJIckD2m_?&3ZB}M288mKiW&)-8h0!H$M9h{REY8BPJi&S-k$8X#^=QAPKqmk)t-a zq+eI2NT4iNVn2CXgG>6|&P0Jar(Yrt z6VT=>kGz+`WUHhg^pAiykKB?$l79P_VuMcRisqA3F-SH`asf#g5q97p43hL~yc8Ge z%)x`?<_nUIl3c3!G2#%p=z?a0Bn?JQ$W0{ZPU|HpF!H>C!{ix@21vhCOJS+R?_BWU zA){^PO<9Vj0;UjW@+dj00?ZeZqA+QM5a{Jk_7XQ-*=4MMnUqqBnhT6PvAJY ze}d$*Zy-VFqTmVg)C5iXjZ+FRF65Ffyp0TSn)J)06b%J^a0gF;2I@<{B}$PLfy5O& zO-_!e_a~BY5I`&G37jDZMtn`b-bwK(L{KqbQc2E)aaS1Qv8E~cFfTU>5@HcV!;a-NRKjHDEUuvX?;PUKu$8_*TlPANjjIcc^ zA_Kil3BbQYF2+5cm6EmBfsqR@o>)Q_aE9NJq|pMA+k;2&GRVrbo~MM-V*#fxRt&3g z*zHN4JtNWo(X6Dzvn8iDlB|G-UdZi9WLlR~bZ00wgF6_n1-Gc*<;%3Zrsz(I*$JQ1 z7fv*ZS@EF98H#39AVqi_!4EtNKDKaADJx%uH-e>m44 z#%kbPe;BKcbMwQw{&228jMc!o{xDV>=jMlV{o!1H7^{JE{b8&&&dm?!`op>YFjfQS z`omal{C}7q*fNPfxe`Akx^q8IY3rOG&go(7xH;D!&h>|L{b8&I&h>|}+Bi2qoa+zg z`omZaoa+x`wQ+8KIM*M}^@p(YUAAeaIQa`>knf!@c)PYFmcAUzg@bgprOa* z3nYUR1Kt69m15nCcVC>dF~fdFBlsOAyZMjX~eaNzAEYSFpalEmR$J#%+>1#={kD2VQ{exk=_>jk{U!Af`lIT+= zd$J-H#KRGJ(zSW+9!a7#O!i`H(vMdtvhcyH_DRxw&ScNECB1kmBMa@Bc|elp3nsg} z5?a8M3*UQrk|gVxOtwGj#Je6@vh9}#C27_(VL{&jo)5{Q`^F!ZB-y}(y$IZbw@RHq zJt9f5iOFthP4?5Xr|{~dk|dj%@RwjO9#F{=hAGD-NwzRyQ@n>BZMEKfLXzSuCYz{E zcGLSW*OMnDNwzWBl;UI;Jtg!1M$qm!z~Jx{Ba_L?ubFiwT$3w9t#d-s{zp6uXH> zGoBZ+3Tc8pOt`GNiQXS34{0P>>;=&3MvBc6eO)U_u@4lr4fHb7b&F1tVn2XZ)#H^V zE0HERz=XL~b@U7~zDh62B994IRM*m*PWLSaNs4>|T7ws&OfOAvkO@~+RnrsF$U>te zi$f%&3J+3QlQh9$BB-PntMUJuBv~ARkje^r|JspbmZUfeWmJ|EMf_E1iepT;wz3S* zZCR@{!Eu67O7C%FPZmfDIRO?GC3w%lilhlnk}`_v^>1*#MUurSCR|-vL=?%*(iEo& zr;T14x62A8d7L30R-%a9D@~zb!u6GfM3MYjnnFn$wII3{vr7}Gm~c~h0SJW1wN^SeN7cU;*kiN*xHRT`pexvYoD?pyr5K1n(=DNjvw z&f9vJeA-8Quj!X0EFkbIA`Jhoo5>$+$-BZKNoXNLB@xD7>tgax#T}OqND>yJDhLh* z!o=z>mi5S>xOLv3B&C& zTSPiK#c9=@Eay1{#*A#e+$~9448G+jIjy0S3HQ|_4qa^PJdY%C2^d$N;KVu*e@~64 zVv)_4c_oQUNe#zGi0AIW<1@hUNfMVqxmCwF@fmPF??b#7j%~ZlFG*ZZFpd&&BCmt7 zSMc&Jvh|99By9z?+7V8>rGp87Le?D_6c>~vu0&U_K1{^%_dxuF5AW{6n=cDV5?295 z`5_{Xz5wD!9eBtW+j?18lDL{UA0%S`PeJ_iF1!qkY`H8VNn8WY75PNmdjp6cYDY?j z=+?`llEk&dIgf}dXM^~`HauNK1I8qY>o5+h4qzP0l1H)#M<*UbMz&lYm!z%->#F@U zY7_6;nea#-URQ#3LXx-vQ)10NB98qM#J_RjwPs|~yrd*?BZ$lQ60!SI5dR_U!Na?) z^Oz)Y6Nsz#fSA>_GvOs{{^Zdu^CU@|iIm5bNallY&N4h;1>f}jKkcboF#T5VqPa8v z97t!b8wBz#?2_eajES}t>rz!N(fTHV_KLg#kwt~gm#mPWZKE}8Cums-(B5LkTiw`} zOWu*7#IG-_`AA`QZKFALA3rWK>HWOR6;rcFn(8p7!7-I z^>z?5D`>~B>Zjn6(ajgXCqawBc5(GKS`Fj>B|!W8>HTMH^Tn$qh`X3DyXGq*9*}|f z?&^MeQ5xNJ@%s|Q-9)^V7W3pDDCN0WKjLo^@dpybJ+!ghLd2nK!FlT1KK`C{)7%dw zhSn>P?bmK*zN)QhKa_xHBqa{zZGPbCP zUKmF<%v~)(J4m$ah}LyA7+-Uuhn_4)*3VfZLF@#I`Y&n!m{^N$y3p4{Z=j>==X@qX z?1Gl-z5p?60qvaJZhBH3{&Mc;60~k6OsQE*Ti4LTApV`Bo1SP#H_TZpLF|Fv>OZHg za^h3HEsH%{|4t55rzYqq|h(cOArT0ai8LlAxmtNf%WYr9rR{Cx^d11 z3F;uV$tQFqF=T;w$`^KegdbivH~qj)(?tjbbsy6Sr+Pea{J~?VNBz-_vo}d_4pW!= zhz?Bl*&x2v(oR7F!t3U2mLQHWVOs5nbpGnR0mQSmwNcQ4=*GF}#sGp5r7`sZ9p}1k z!o>J;tc`*gKzCauILBz1yicdY_8UO_8+$9IeS_}4k|2(QbNwnhY}Q=?;yZ0E6k`E> zBi-0QFcQ=%@6qA4_62u}-SG6lC zipj}~K|J-7MhYqc&gsSsf+1rwYTm&?TsU}9GY-ouBaIZK0+@G7a0cHMbga*x3)=JE z4mD8B3uulQjs26$mNRw%t65HFf;@nbO?o>}FCvK|G`H(4Qu2XXfg05=qp&8N_PnUU|*rx>V+II(|-cQsjNFw^bJpni|qkHVV5DheU5cT9G!Ai2|L^jMxx9H@u_o0w#3Q1ZI>UqR_S)c+%Wsxstr5ks0*$2Q~Loe+c zhN+hX$^lCjSUE921IdSU_NI`bjiRuRcu!k~h+8lU4K5PWjY7HXBT!e<%lszj8(oj6 zOG%yfCo_pZX0R?$WYk#%(i}J1w509@MDft;xQbln$5qXN&a>fc@Aq}^WvyKfB;k0vw6_Hi~ch`*+ z`%$J;pB6h6QP$L8!fe#_HGn-vr`oIP^t5mdkbhD$eT56 zW5XHfszLi%rM`e-swl_-FmT1pdS}_k_IBWndVJ&AB*+%_=0&TiSOw_O{gYJ0_1rfVf%W$Ks#X+1^qV|hWU2cEb}#I`Zz=(6 z?oH(sd@~3kw0_K7<~dILoxJJ82IBOpz`T1yB?aINVU|o4aUY{11>gIoA~5*QuWUk* zFltXl&R^y^N=2G~{!K+-0luM%LU2Y<)pz7D5NPK7q0*xJ{B6}x)Q;|GWzluU} zCQujJ;9$bZry{53sFv#Wp|J*}tlKj&9{`lIOJt|vmr#loACkNc_wp!ECZ6Ck!$NR3x zNwCk^g!6y6aLI;awub&l0;h6!U)#7^{rS$&Ytn$;=OfII|BN2nl}GSxcB^xENkYY@15*% zq205(POSXZ1&t$6RV+K_wtw%gK+4VRc=@L1ZxH&}v0=7-LrgsY2Ali$>uGx?Lf^DW zoUzP%fPmS4pki?*m*4$yZ7Z^{8D5x)#dgLsdmT;;WqI|Q`?B>cOMd@HMM*>+6SKNc zzJ33BUo-i6zxb?CHa&iR>j)kl%Mxz-pC$xWeY+RV7+@z;%<%ujDgY#~?f+jR z1wNIDd8DBkOFjDuNZB1KmSnQ;uj$3+EBw{XMNB^RPpX6iQ=Qzo=^j}DuJ+dVSD{u3}Q~QzvlFsKVBVS9oTX_xOD&7TaE&jBi!`&jk+d( zLd~8`48*u>_+cHzUlsCa6X4~D$qK+# zKUOhW&O$48MyC79nC#A6v@A|!E&rQU#AI0ui_7USUbpOugHZ27dn0rblqZ!7a=MsO zlr^uM6KAmB<~)8fwXa$J1EHA7uK%Oj#G5=0fYQgn{!HxUIn1T>pD) za%3}JMHE(?3j3GL>|tlHW&31ulEIcO`wMNCI=_5y9E_3QJ=71|kxcR8r8Xw}>5&A2 ziPiiy$ImpJHNua9aU6hfn{r1@xrpGC`QHJa=}WzP2v5_`bkOh9ZM5!gnN|;l{k^?| z&Vcc4m&1BFeP1yh={sMYbc)I5t`4@+S<=69avwV-${Vj^4Azj#EZF~ee~h}wmm=M3 zQe-VzQEajsU*GYKfD+8rgyauFdj^Dczlj9P3rfvJB$j|%CMw90ne z-UKEKTif}u@qG+`;r}{{$OyayJ|h5hWLD05uRU~9Q;koI*T%{kOf zK(sIe|DqZ(#(Msq9cBj35Mjw8?@s}Y<4ksoHCaYMJ`c}gu*qS}`F0VG${!n1nZ=lb zu%7Z?54P!gI zI0iJ*%_|+aUO~0Oq>yuyh*N|4C_q*6Jku8JpqSOwUJ=?)64_kKj`)HSaQdsvU)mctq@>FoQuQMGKS{{}Y+uX8eXb!sojPNjsSt6W-eOq93W!WfP!SC*Zj#dL+RA5)lcFA^>jl)^yKcmh;v&pxP)E}w z4@3*%r7p%^Alb05=OFqmb$on#N8b^Q{-+tRKHxk}YyUdo08=nw-p)etBgoF18DU*q z>Dx&5m)Su1Xd+0r;PWw<{*kDq!|N6SS0Xh9bX;-ViYe*`-4!@YvW+R(zlH1_DcS5+ zI~NVZ{nO!tB7;=`SpShYMLU%(LLS3(EW3DzMf@1%(nHh3m|bO4mm*3gwmZ=!kp2B& zfMmakUM#F2`vF_i$=FOY4KU}8xTJYBSdOM(w|aTC--Kv>I;clyPl;}1?4<*u@KqMZ zy|Cg0c2#T*$ra{pE1>)su>;dX47Yq!miV@kSm&RioS7Yd5_@bAW!8})g1y|u*n?&o zSlyRl5_~#VMq*E;#IA?fo}n7QmJ-{<*y8=@)kktL)`fM)u@PV|=QA~UZ#7dsjp)H? zL7KTIzv&!di0STTpkJQj?vCD@$f-R{;)NhDEUS-Z(0D8G-`}=fRJp#y3H1O zNw5Q5_R|WolsGTxVeG$Vj3t(j$@A$1m%At>_YmYJhU&XIB^RUcZ+p?!!T~u@etMMl z$j(a+F)e0?kBpS7Bbqnej|rJfJDW&s{!Da*D}!9F8w~$jPWBvwte3I>nK6vm3m7dA zMT@Dvdq?C#>FCpJNNOvL#)-WcL+o=*uKztkn|XumFfA|RUP%}!(MKe2l8-sK(QG5c zvJX+&Qy!}9fSa-3mch2e7D46pCK@TzX03ETTu9N!;vlh8Vtv2TKn2!<~2>^{aGHq)>* z2)KZn*={4T_LSI7>0&Y0%xd3FV>a;Goi*efV&OQRk!Y!S8aV_>>hbDX2Ak2wx z1=o|{iXWFSL0-cppYg(c*-{GY6x$BL7n*6v{u#5k?6j5Sx>IuBPM2#3g=QBG*uhJH za%#&VoXgnuBUn3yIfXh(Q4&5d)yi;ToitQqH==u8=qAzNatyMC>pC>ZjUo%@O&w$&h<-D$ zmPDVr45CkvXtoR0%`1X&kiCoQe#u3>*3Q_<0uuTT31yYev~G-f^x3azm=?-GnwN(& zJ$v^U?FYu^B{ayYf@YmGz`WQjyb<^u64|>@@TEyE@g6^8KdTnAn>PIG;cG5QPGfVj z=0Ec9o!EK^-E!1@UYHaubkPOy)e#QkCmhC_bQoC5Cw6^}fy-XNiZ+&K_8iWVE}Ovg zIFR8ISB>;7XoaZ`=0ksb6JdCL+?RIzxFR;+3Pmo8bgFz?p?@8S@SNwOQ>AH=e8 zHx%>UfH2X@e0^f8E2%rO&y^#CDTbDUYQf&Cz+LT(yaWdGDY7T`l?HEr|#F96z{0h*Te zq`(P1u+$GeC-`sx7VboP7FL5P@+U!2?%1;6B5X^iK5I$PP9QW9a)n|Ojq1p3j_u3A zw2p=a>_6LSt_sZt@i1$2k~{kb30e(76Xtg-kpo8-IXJZ!A?{@pUJHCm$h6-DvOf&a ztP;E$T3ZB5`^gJDbpLf_CveRIu9Z+byh|Gu(?yRj$skPdH6$BB)dymdQGZtGDzC5SeAolYEB*{l=TF3Mz^VoGG& zp&7mh7zpMkWC@Kg#SFiF06W2K&!IOSy?J(W+PI6a{_*c$*=7r2TrI-XFs={!S}Av< zcEZg#ZL^ z^cB@(N;leAHsOCr{)u{&+>UzG4`4ShkM4RP+l#~QU2hel#~%`){6C^AzUV~9VXp%J zt39w}91%PJoK?Zt9n67W)&b1}Rie!P`h`AH&*e)xk@4_RH$nfe6!c@&qO5H36+Rp| zfA}d7i5BI8Fde+g5x;r@n`N17OH6?jM6&3C$z3>Ckm^4IWcEG~Pj%2OY<&GC z4ko|kKZYb0$@;0#xlo01_3|JP3*(USg>ADi5$zM!z(Y%-SXFKM^!*UMvV>_Z6`v*eCskEIGpJoDMY4Lr1=krF=VP$EtB0;$whlJIq9^ zuYmXl=GS*1v-e)UNR4|F=|%<1L_SMB-8fB_Es4^CIdD}rW`&Cjx(TEaAX6fM5*sF{ z(slx;71^}90Q)yuby5KHP<+0FRCqJ+RS-TQ zrvXD|H#JiWE2S*8(>!4b^g<0(PK(T>V1U4Vgnh(Qt#{Pt# zm7&r<1TghP2Ve+)gBesPoI1eAxg@aXF$ea{Z^M$;OSqN{4DrTBX+B5Q%uyYQpGHhz zoaxSQBP$)c-+P2=$8@05I8ahJAplk$R2G`eaorDGWrRzZ;KcxU0@%k;f(2b1b-w{O zw8BA{GiU;m1a=fNoZ~7e=cPUZRtaD!?m%`wM^<)%;h1w>-zntFk|pzJw4!}v<6lJr zSzG{C-fLwH7ftBfp-ZvXm_W1SmOj$hD?nyA4CS(XNT1}uBn2gOUC@WEefE@-Vq&?_ z{|&g%?pesRB?NB4ne3x}YK?20!_4ucS2!>O2li6*6f$$k68Y!1Fgcs}8gkI$QZw*! zA!D6{i><=Y{V5Q9pQF+M)oG6EU-%jN2fG}oqCf1Vb571j_$sX`YH&bJ0BJa&(8R^9_u9h?aZ9h&eR0<`^n0CGF{9`Pa|`UrZe>!n%lsw5!o z^lJIZ&+29_OOy~#_}r0?=DyeE^rKxc8&rI=ZM|@RY?wJAAu3B=s!tG$PUav zb50Nz2UgGxbKz~4AuQ}@5o0BQ@*b_=!bCH^{g^}fE}%RNC_B+V>c@R6mi?GuT<7Cl z^*CV>{8=~|a-KM-Of>W5;A0Nv@#slPaCTr8{bDimEvLKjjwcEjnYh_s0QbB&0NmG0 zTZL4FBXf*T+v%QZ?Rw;}-|H`-Bh9>l`LE-j6Z< zViS95Nt?MUjHbA!mStc3^Z)HEMbfW&kv%)e*ag62>H0=I7!EUN+nR$z@SPmb^Elh4 z7-O>rwu98L_nZI+nXDvvl5)1?!3Hg#u$UYHumr}}P9{v){Pdy~{{zjH_s}JS?7nR< zXR_%{ENlEvmYjAs5~RNa(gUQTZ-sO>RJ5#$Ax9)>Xe$tLw)PZL-tDIoB3z=MprmkQ zgw7iOYJhx9YGC)e*pvcv3V^l(P%icxEbFG{_YVR>4F>4N|2z$U75vX4;8}zr-YR@6 zpnpKqi8(;DkR!4I5$D^@vN&5ln_@Q#NWY1kpwx4uBN?Uva%92sd-A)+6r}ZlM0Te4 z27vgQM_=9~SVGkzTML8fjK7=77lmWmgw|Zv`EAX%226~wCr!lO3`+6=(h%Pq&esy^ zq?vRr2lZCs1ZB75HyRTc<3@ygz$oS$QlQ=dD6$5<5uY}7#=Lpon}5%}k3I9!+n;=O zs&X)np~@bHN%MPKVK5tlrv|*MzZKq5uzLl&yTdMQ1lYx%Zx+x@FdybHbCV}1t=@jR zQ^0nD9Cfg|>F?C1VEzq#eyV{+t8*N7r9U`cGK{AQ9j3G6+th_OJ#0Sk{FUSR5jG@K zchYj<9Cw`If?<*lq7PwGpi3$=8+rY}iAaC1d`R}QkbMKmrY*j3@$Mon8moa2f$W{% zDjTUIg%E9ZZ{otAg76ziI9q>=WM5Vf*@uU&5GTGKBp)IiqV)I5bg6p(4zhp7WzUD% zPk6~^8xqcDq8*<9X4wObO+vj3VC|F#+|&?@A^T@s_I#2p%pW{XSp@UxQWn+_a!7(- zmhq>8HYNHoh<@5Y=8#j^!_GQYI4t=pNWSk|C0_%{+siNqbHQZId3q;Ju&0ia;QQjo zDJ3Dlot*eufP9ELXi&PBfl(Ktx$OEq(VW^T9)R{lXyj*4DwAS-I* zc8I=q&@@(b`?rc-1ksN{^kObr3(>20lBFYZ`(di}isW(1! zktUiw3CWXdjKh)_pwe~UD*3;Vd_N>FI$JV(@(>AL3Bjj^1fTa7&eGibtL4ShXUWBc z_IK>qw;9HYZT?oVONT1_A1+pxs&E*54wBf8iB_S8Syh&Wapp8lH6C-Urq-fYV!%yau;I$BZ29qZ#8XcS5)^O+0|LVz6 z!7B}9dFi|Z9eaH1S(2rC*R~PKpFr|oAbHh@qF3%Dvu@`dc_euiBr7199BwAR|=O< z3(4<5vJ!1flBc}wC-*trEV*AiJ}UX&2C^_Yo`yYUf@4JV?U+R_Pm}yR80ys{lGj7> zPDnO#$$KFA-#h7+!twMzlKdt{lnRzPl05Zop79zNYQ7hbjY@vbfO`R1_`|6%%=}$D zTws<`2tA(N1Ein;66a`j}KVkYAs|X(7n@F$=4egyR3b zf<`iZHLtxt8>d*jh#i-x`0A~ve=>hwblSM-^X6TD+fN?*bJiQ{jy94eXxnKlK1F&o>t zwj(K~#GYWykFpOlWkGv}3AUW`K{wR`@My&r??Z;RCaxPp7evkcC&4O{gI4iKW zAs}yKl4uy-ivNZz1(16oPPUh|jdT zcyq$tE(iaRHwxubBW96t(N@FPOBdcY1v>r|+n(SeS@&2pzs5o3;h}TEvjm2D&~^&} z=dH1Dgury%b50F#Kt%xbsb-Y53z%>!_z;jxL){n7SmX%``&avRG#Y|o*44pJ_;R$qZpxcK-ZH|2u9;)yz z&UVZA*^W+9Lt>_mib+Z7O#yv2euA!~Qo36P10Z)L9ay-36zGj;fnUX@vB_@SF|l4u z;@af1w~jOB>U zDVOb~NJz`QR2>WUjiABC7PZFZm6@^00QM{y`BMD}OXeBKuumB>8|>joYCyCE7kblh z?Ve06tlMx*-4DaTwgEax{SXK86jN&o#DoTNAx)_SZAY%Muz(HxVeb$KY9Cb3p4Cqv zuEV$GaIqMJVk!84F$AKCgD{~z-QR&7?yHs&5O5XvJxxn=ycL2uuwJCN+JFK!iMV>FzzfI3wMp+5adn(*)I$bkiTMbvA2yV1169B29V!Krb*fO*D&T;2#wY=v4Pa` z5)}0+PM6Okd*U!4?28Mh;&yf1@jkMKd>_ZYApyl4k!-vk#x~qJ@#X3KWiWwqhvNPP zxkHd(p2TAzGPnW|?U3yn-3?^rjv0$(*~W!Zo%b>EJm8T5Lcy+e@(!R#<+hB)Q>=e0jA-4TyoAW;I{9z z7=~O_NebBy;r5NI4Bn5=Uy5pSr)bB*uSbvx5LKxClN-mv223L))^>T2b!!>~=Y>S#Mm?Ups;j*X-Uu;1qvv zFFi3NQ|GWqc>3JBd^Z#*8)r4U+SAggX0D~s@Qa8ADmS0*&7$d2jT>Hf0dZyIkD=Q zd#}UAWA@w|AO2TvYt;yBm`qPyi8IplO`SCKn*{Xa6a<)oetb#>1bgUC;a|zqWUbHR z!(%DzZrmZf|EcYx$Zv+%ZSd{!UUFZWwWuZ`vOU1`EH=sm*gOqSU63Vf#x+~$i~sY& z7Al|kyMqzj67L1+QB>XeB*xFom+RxOzh@-~|3B7^fQ2h-Z2hJ1?3lLH-9fgFx6o=s zV(VBIgo^l&?IFanx!t&aJEQ=NyFExhxJ+0&+7Fz<&?{BL&?O?}gnxXoa7>qpdIEKZ4N z-dT9-yz{4>Kkt^Om!0yrknLqRnr;|QX9`S&%539xxzlxq?D7X_--LOUeEbtsm#rgp z!8Lc4DDVC1$;+a+P0W^m@2{Jy;Lt9_(J$FtOrpJ0M^`?`T5803j6H*2K73kat2xSL zqbN5KrdwfH8euyy%rppCV7m`GmSrDKD9GxUQt~nny(GYI9Zzi;0#<+{Qkj_JP_BRF ziTiIB=6xqIZT$IH-umEQmgknifX%gyZGw;w^UZMQhSZrJ6CD@3aC^q)p2p4jHH_4F z^;m$g-Y790gZ=m+_GfeVkPF$ieRMJY(~yEZc=+p<^Ov~kVF3DxW5MPTltNaM71k!W z=RhOnm`!($8{N}k27A+lvlCm2Uv`}qWycx%I#X>JY1B-z=t6RdWIOTOzd>d-Hcx_3vvEvaA@mk3BZBn+ji~4)2S}`JyOO`%12G2T z<736bohTPe+#Lrn1F`i7$SdbMco}0osjGtg-W?0p56Lt#wxNjZDv{X`T33Sv z#^pPfKJ(x$S9~WnZQOU}T{ZunhadlY*3xx(6;X`%Mc7+hAF+^8$c+67vz-#Ln3BOy zux=Vt{V=vlbFn?!kVjKtWOg1_YT>Pfnp`{iBii$qc(~WQV*wk5B24WrCO4MVfbw?g z2#kjNpM|_Je-CN@LX}rzFL8Tv^I5Xk%h=vb?an7B*VSCqbiEpkhF&Wz!Ndg5c8D60 zWx$vwQMdsH-)o0u336j0xly-aUm(oFrKk|Q1}a`ZRvgwqiyYVV50f~Q&nI8Cd-^q` zME-6GqlbI4I~F8IzaV*na|To@Ke%m-M9g)rt6?&Dn%s2RyNBSVE4&pX^O`XHs)!umAulo_Pt7+1 zbvS2DgTO0&DFZJKeiOANXbSdg)No3df~N1!uomPT**-=fU_|bQH}cCz$&&4!#{Gh` zzl4nkT|1Azs9NG7zjufXkBC&k2wsUFT4ZmOsADQ^dH4?jXz&-5<)1Vt2H<8&m zpFh@Gg4>TzNM>+ECOrQCn5d!akvANej&oBN1?cZNTgz!kdA+Tq}EX6kS;dL}_XGbqkYGzrl{p5Ux@H*;dGfvXzT~zQc%? zUL(>N*>WWEad$RsBFyGX6<2AWT6+g0{krQ87CmeD+7li&x-6I0Z{BzmVNHq_P`=i= zyFX5Rlu=E5 zjr)nsnz!xNcmy%bkmP);O#8GMCq0QKk0Q}=RNOS?EQ;|^Zz#k8wn7o`9P56vN8f8t z64E|Dv{%6`*n(v~RPXjld7;gqVr0|5)tBS0%bLSqzy|Sl6RPRVVFAT>39X22rY5VI zUybYlG{a`ni*}QkAYi{_pYbRHe4vt<2tW@7x#Vk55Q~y8c66F>odV8@0QOKCh_llKY^B(tkr!1zeA9+A$IfdC)Pen%3V&A?(bc zRQAh=5))_j=)MxXXaE3?>ui=i>97nyzQKou8@BSnZX7?PCOwsJ9?wdBtfOPR$fx>fy^DSo=dm*+v zBe&4X7Ofdz!SeA{Ojx}Bx2U|b2Zm0;>TP^jk#};-(ibUScdUj^=x(~z(TWS|L6u=T zFs&WIf9b@2P72{~#-p-1@(3yz9Gz+@A}r@6ThEuDyR!k-$Lw*vM#dsOrMmb4dxX6H z@ISq-G4g?{E(&(w3ZBigLXEZHmkSry3`dH8b~bc11kdQfhRPs_&fFk-m{Ob#Hgf>T zekBLXxN+0qc94+h0@N5oFd{SX<}n%O8x)nkiGDon$1; z%fH>9QD_)>z}k!siXtn7(i2}yk>@ZTp0hTl^A(snZ&UT9$X#pRpD_f#Rm|COT_CWM zhd$+-nI0H{*Zh-wHJl*ntMP4Q;9{GBzc>s;)aL08XpbQ8%3L|rfR$)dRqjyK4HL2O z0y4}c7_&^j3?>eZM@%4;+>M$)hQO4W-^G*}p;S?yduY{5erb$i6gn*5XsZK{Z%4(I zq{Bb1Su}G5o5FYucymiiEsuM#qB-Yx)w43#ET6tUp`i&S-#1UMenD;|BTw0kR6C3` z0=v8-q5|KgGjHcSUzBhDskGjql2Ri81&=vvx=HcE?wzo6cm@JRM`$XkndNVcPrqqP zHXTeUDCf+$GngMU=qihH5)W@(`OcJn(Tv~V>F=-n_kpO4{JT{;BW{`|i$V6FYCutb zPV$+s13z#4^5c0gKR?3vG~+XHrYh)gTETsiBkwTO7JFWZKd?dhD%zj;44J)R%hB{( zRlLz+_36-;=cgUs@ZmIXjyXjTS=+FtIEJNTya&Iwa(Cp#+YKnp_inE*)*jio=sDbg z6P%6pGmCBIF;?t5qtBXe(O5IQa0eTnbrv-ONZB=DsE(gg2zlZ3vi5j6G}d6@R4}LU z&q@f1rw?noO~JNmI@E?~fqy%OgIxKbSH>0?&RoUC*LoSR<}h0J)bp2`1F)k~&37n- zWLyenzj!3bj*|)XB4((16Bl*sFV4(opjm4wrDNKx!G?Xvk_rGj6xWG(!*LHPB0F%r3CACh>%sN~kEO>Z|t&XuNzDF0r)-S=k9BnF2f>&K5 z;?)2I--Ro59-bni5l4eJ%~VDQx;fJe2jZ-nBp|P$xLU*sgc_ZmNp@18Y5mOY54z?r$zKrB>rHD5QZ$-4= zU`p_u5g8qU&y;4WAl`=Q(5q@m0Kgb}D@452jC^X=A|K@O=5csjZ*&%+vCUZ}z4m&- zAYyb;JfOfB_I)Q2o@k0{JoTO*t_M$?!pqX~l(U#{%s?8tph3*Tmmwxth$qafURmxW zhPiLXshGZYx*YZOPybGV`Xp3?ipO-C_Ht7-J+4hjkn}DWM}Swos!Svau09QoD2%D! zzcr)^aLz?>J$Om{Ln7)F#+GibAyNAGtMIB#K0}yPe#t#&Q9D|!ikigjs_FfkuwhMI zo6v%|@hzD|+DS6ZTub*Fp}-5`R&<;pKqEy@=Hpb6_;Wy=!Qv|Ls9RZ*+?(h zj-6E~8;l|hA-|+p#Ea|4MCe^$lIT5*t9mAN@sx23VDpr+D1rgs%X4A)187}tE{WjGG zVs3aH`Lt&WBM6?xtHq73!MzEqy62sC3HLs{BA(AO68HVF;C@$UYhTN&#O=DlT|$fa zwKfSi{$c|Ad}a%A-yR6=bv+S;6XVtSBFka!IB?&4wpGHtm$za@X;|$gtGqMjEu`R`hp>w*NH@{9 z&sSjgyd6$}9zHNfBms2=0iPcH17-elL(F2oT8W6A5HTssOd@U}R=`w-697l2J}(kc z$|$^MtPCw^y>0bq&kC9)uaXGaK|-_^5>h!9Lh8c_9HUdy70>Zg%y>Te#_GM=YN5ue z;4bd}Y^p~&iSe!UXT0rq!JkM>wm@PU-hh~9TVob}nWJA8i7H_FDMr5X@wYod6E75% zKQN#bxNloy#dWP_&o>{;eB6GVgrg4zdM;_LBucjoqIBVeP0^_x4Za*!(G_$%USAnR z*|yV8iSUB1w|APDavN@RS=bp(u`(POkO`Ovr*^b|6H{(wm`O|qWEMXGN(|%=lIiGS zC#KxOC?;iDt&MX*hj}X?iH=r$5;|s5hP8bg=x|B`wV+cwngcr1Ss9NB=zm8~TlBlW zPKq7<3Y`fBM*hM%>s-)X3a2DK7wBlA=uBr}*vYx)L3SpbGUht9qpPDc8G;Sdk{2TM z6>Qc}nwnEP+D$ssp$Kx?+M$Sqt?STGQdqal(bCeH4i%lF#w!red%HeNYU$9D(QwK8dvJs$EMt!|aWqgi5nY+QijCAX|ps7-dkR zk~W3yr5JZJl0vSWTH0jRP62>pXe33UIhC|!t%E=o*d9fZQx0e$S0h1T-RQz78)R5T z-@y5YeuGfeZXud-9h_R)o!71f6muU&Q+9%2EzT0`6u|qQil$rwrA;5 zVzcT*A0lMlNih_wZ&rr6q(GQh*%ph8GOi8B`=aoSkEI+qr#j}60(ENh`7;!x=F~@B zQXo7QD#TF)nKIq31`CRrm2ni<=FoIn_az6nJ>8?M$Sopf2!09~7*wMo6NV92Xekk_0_@##~LJFd3)b?~($MxYz5F zDPY9~?sG{2l#$YyLh&L_Ep2XY|NKagZ0Ja(=mn?V>rw_Fx1PVE)C{o+PAzS{ZWn?$ zrf&#p9T9=x0)t&jL7k}IK_qDrD&SOmT#_J!=eMsbgCYXWDru*AyCQ@lJ$H0!>Fr*> zP};cOPLhu=Y|NxrcG+BMAACCnvH|mIvhdJkEtkeh`|8_CE@OYaYM~qt+jWAKHtM(2 zAap+LSjTyK6g^zE!{vw&$5z$~l&fIPSEl{slI9{FW{I7YMG&~{Ke!}8ryymy8SUV@ za%XOLNs+_I2k+qooX@6mCUC1unsbbNOq4+=KnT8iqo;KF*vFK`$j8U&5i3@hs)PKQ zN95S4(8}(>oIouiE9trdn|&`W%Us(<}PDp z_U?I)8~b31WqkXOpO3#tSJt3!qrXVkhQCNxl6i2=Ya_h%eegUwk@iV)|K2?h4j*{* zc(?x__wU{F;P4Upx7#1w{^0fp@eH{AK|CAo{lV=IZhsKZfZHF$v*F$!-2UM92k{KJ k{Xsk%?)|~-4{m=D&w$$>#IxbvAKd=n_6P9{{J;M2U!dqO5C8xG literal 0 HcmV?d00001 diff --git a/app/src/public/styles/globalDefaultStyles.ts b/app/src/public/styles/globalDefaultStyles.ts index 6776bc80a..db79ebc1b 100644 --- a/app/src/public/styles/globalDefaultStyles.ts +++ b/app/src/public/styles/globalDefaultStyles.ts @@ -10,11 +10,11 @@ const globalDefaultStyle: Object = { borderRadius: '10px', border: '10px Solid grey', fontFamily: 'Roboto', - color: '#f2fbf8', + color: '#f1efea', maxWidth: 'fit-content', minWidth: '250px', cursor: 'grab', - backgroundColor: 'rgba(0, 0, 0, 0.2)', // experiment to see what is inherited + backgroundColor: 'rgba(0, 0, 0, 0.2)', }; export default globalDefaultStyle; diff --git a/app/src/public/styles/style.css b/app/src/public/styles/style.css index f5e434c89..1f83ceb0d 100644 --- a/app/src/public/styles/style.css +++ b/app/src/public/styles/style.css @@ -398,10 +398,10 @@ h1 { } #export-modal:hover { - background-color: #29a38a; + background-color: #66c4eb; cursor: pointer; } - +/* ///CHANGED COLOR COLOR */ #export-modal:hover span { color: white; } @@ -460,7 +460,7 @@ RIGHT COLUMN } #addComponentButton:hover { - background-color: #29a38a; + background-color: #99d7f2; color: white; border: none; cursor: pointer; @@ -483,7 +483,7 @@ RIGHT COLUMN display: flex; justify-content: center; align-content: center; - border-color: #46c0a5; + border-color: #99d7f2; } .compPanelItem h3 { @@ -552,7 +552,7 @@ BOTTOM PANEL .tab-content::-webkit-scrollbar-thumb { transition: .3s ease all; border-color: transparent; - background-color: #1e83714b; + background-color: #99d7f2; border-radius: 8px; z-index: 40; } @@ -585,7 +585,7 @@ BOTTOM PANEL .bottom-panel { transition: width 250ms ease-in-out; - background-color: #1e8370; + background-color: #99d7f2; width: 100%; height: 100%; display: flex; @@ -628,7 +628,7 @@ BOTTOM PANEL padding-left: 10px; border: rgb(68, 68, 68) solid 1px; border-radius: 5px; - background-color: 252526; + background-color: #99d7f2; } .event-table { @@ -835,7 +835,7 @@ a.nav_link:hover { .useState-btn { color: rgb(241, 240, 240); background-color: rgba(0, 0, 0, 0.54); - border: 1px solid #46c0a5; + border: 1px solid #99d7f2; border-radius: 3px; box-shadow: '2px 2px 2px #1a1a1a'; font-family: Arial, Helvetica, sans-serif; @@ -854,12 +854,12 @@ a.nav_link:hover { .useState-header { font-size: 35px; - background-color: #1e8370; + background-color: #99d7f2; color: rgb(241, 240, 240); width: 600px; border: 3px; border-style: solid; - border-color: #1e8370; + border-color: #99d7f2; font-family: 'Open Sans', sans-serif; border-radius: 15px 15px 0px 0px; } diff --git a/app/src/redux/reducers/rootReducer.ts b/app/src/redux/reducers/rootReducer.ts index 487e04f82..e936d9265 100644 --- a/app/src/redux/reducers/rootReducer.ts +++ b/app/src/redux/reducers/rootReducer.ts @@ -8,7 +8,6 @@ import styleReducer from './slice/styleSlice'; import roomReducer from './slice/roomSlice'; const rootReducer = combineReducers({ - // Add desired slices here codePreviewSlice: codePreviewReducer, contextSlice: contextReducer, appState: appStateReducer, diff --git a/app/src/redux/reducers/slice/appStateSlice.ts b/app/src/redux/reducers/slice/appStateSlice.ts index 825b5d385..1b1b0c121 100644 --- a/app/src/redux/reducers/slice/appStateSlice.ts +++ b/app/src/redux/reducers/slice/appStateSlice.ts @@ -17,7 +17,6 @@ export const initialState: State = { forked: false, published: false, isLoggedIn: false, - //config: { saveFlag: true, saveTimer: false }, components: [ { id: 1, diff --git a/app/src/redux/reducers/slice/roomSlice.ts b/app/src/redux/reducers/slice/roomSlice.ts index 5b0413c0e..29755f8da 100644 --- a/app/src/redux/reducers/slice/roomSlice.ts +++ b/app/src/redux/reducers/slice/roomSlice.ts @@ -1,14 +1,13 @@ -// With redux toolkit you can combine all the actions, action types, and reducers into one 'slice' import { createSlice } from '@reduxjs/toolkit'; -// Sets initial state to false + const initialState = { roomCode: '', userName: '', userList: [], - userJoined: false + userJoined: false, + password: '' }; -// Creates new slice with the name , initial state, and reducer function const roomSlice = createSlice({ name: 'room', initialState, @@ -24,12 +23,19 @@ const roomSlice = createSlice({ }, setUserJoined: (state, action) => { state.userJoined = action.payload; + }, + setPassword: (state, action) => { + state.password = action.payload; } } }); -// Exports the action creator function to be used with useDispatch -export const { setRoomCode, setUserName, setUserList, setUserJoined } = - roomSlice.actions; -// Exports so we can combine in rootReducer +export const { + setRoomCode, + setUserName, + setUserList, + setUserJoined, + setPassword +} = roomSlice.actions; + export default roomSlice.reducer; diff --git a/app/src/redux/reducers/slice/styleSlice.ts b/app/src/redux/reducers/slice/styleSlice.ts index 932ec8032..49474d396 100644 --- a/app/src/redux/reducers/slice/styleSlice.ts +++ b/app/src/redux/reducers/slice/styleSlice.ts @@ -15,7 +15,6 @@ const styleSlice = createSlice({ initialState, reducers: { setStyle: (state, action: PayloadAction) => { - // Replace any with your specific type state.style = action.payload; }, cooperativeStyle: (state, action: PayloadAction>) => { diff --git a/app/src/redux/store.ts b/app/src/redux/store.ts index 5f1eb38ce..35d3c477f 100644 --- a/app/src/redux/store.ts +++ b/app/src/redux/store.ts @@ -1,16 +1,6 @@ -// Migration to redux toolkit with configureStore import { configureStore } from '@reduxjs/toolkit'; -// Import of combined reducers in rootReducer import rootReducer from './reducers/rootReducer'; -/* -// Define the root state type based on the rootReducer -export type RootState = ReturnType; - -// Define the type of the Redux store -export type AppStore = Store; -*/ - const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => { @@ -23,18 +13,14 @@ const store = configureStore({ return getDefaultMiddleware({ serializableCheck: { - // Ignore these paths in the state ignoredPaths } }); } }); -// Define the root state type based on the rootReducer -//export type RootState = ReturnType; export type RootState = ReturnType; -// Define the type of the Redux store export type AppStore = typeof store; export default store; From 2bb913d207ebe0aa1e669368225720ea6b32728d Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Wed, 7 Feb 2024 16:18:00 -0500 Subject: [PATCH 008/221] Cleaning the auth test and apply prettier --- __tests__/userAuth.test.ts | 217 ++++++++++++------------------------- server/routers/auth.ts | 11 +- 2 files changed, 74 insertions(+), 154 deletions(-) diff --git a/__tests__/userAuth.test.ts b/__tests__/userAuth.test.ts index 2f5137d97..cf6bdd108 100644 --- a/__tests__/userAuth.test.ts +++ b/__tests__/userAuth.test.ts @@ -2,33 +2,33 @@ * @jest-environment node */ - -import marketplaceController from '../server/controllers/marketplaceController'; import app from '../server/server'; import mockData from '../mockData'; -import { profileEnd } from 'console'; import { Sessions, Users } from '../server/models/reactypeModels'; const request = require('supertest'); const mongoose = require('mongoose'); -const mockNext = jest.fn(); // Mock nextFunction const MONGO_DB = process.env.MONGO_DB_TEST; -const { state, projectToSave, user } = mockData -const PORT = 8080; +const { user } = mockData; const num = Math.floor(Math.random() * 1000); beforeAll(async () => { await mongoose.connect(MONGO_DB, { useNewUrlParser: true, - useUnifiedTopology: true, + useUnifiedTopology: true }); }); afterAll(async () => { - - const result = await Users.deleteMany({_id: {$ne: '64f551e5b28d5292975e08c8'}});//clear the users collection after tests are done except for the mockdata user account - const result2 = await Sessions.deleteMany({cookieId: {$ne: '64f551e5b28d5292975e08c8'}}); - console.log(`${result.deletedCount} and ${result2.deletedCount} documents deleted.`); + const result = await Users.deleteMany({ + _id: { $ne: '64f551e5b28d5292975e08c8' } + }); //clear the users collection after tests are done except for the mockdata user account + const result2 = await Sessions.deleteMany({ + cookieId: { $ne: '64f551e5b28d5292975e08c8' } + }); + console.log( + `${result.deletedCount} and ${result2.deletedCount} documents deleted.` + ); await mongoose.connection.close(); }); @@ -40,7 +40,8 @@ describe('User Authentication tests', () => { expect(response.text).toBe('test request is working'); }); }); - describe('/signup', ()=> { + + describe('/signup', () => { describe('POST', () => { //testing new signup it('responds with status 200 and sessionId on valid new user signup', () => { @@ -55,7 +56,7 @@ describe('User Authentication tests', () => { .expect(200) .then((res) => expect(res.body.sessionId).not.toBeNull()); }); - + it('responds with status 400 and json string on invalid new user signup (Already taken)', () => { return request(app) .post('/signup') @@ -67,8 +68,9 @@ describe('User Authentication tests', () => { }); }); }); + describe('/login', () => { - // tests whether existing login information permits user to log in + // tests whether existing login information permits user to log in describe('POST', () => { it('responds with status 200 and json object on verified user login', () => { return request(app) @@ -80,7 +82,7 @@ describe('User Authentication tests', () => { .then((res) => expect(res.body.sessionId).toEqual(user.userId)); }); // if invalid username/password, should respond with status 400 - it('responds with status 400 and json string on invalid user login', () => { + it('responds with status 400 and json string on invalid user login', () => { return request(app) .post('/login') .send({ username: 'wrongusername', password: 'wrongpassword' }) @@ -88,144 +90,66 @@ describe('User Authentication tests', () => { .expect('Content-Type', /json/) .then((res) => expect(typeof res.body).toBe('string')); }); - }); - }); - -}); - - - -// import request from 'supertest'; -// import app from '../server/server'; -// import mockObj from '../mockData'; -// const user = mockObj.user; -// import mongoose from 'mongoose'; -// const URI = process.env.MONGO_DB; - -// beforeAll(() => { -// mongoose -// .connect(URI, { useNewUrlParser: true }, { useUnifiedTopology: true }) -// .then(() => console.log('connected to test database')); -// }); + it("returns the message 'No Username Input' when no username is entered", () => { + return request(app) + .post('/login') + .send({ + username: '', + password: 'Reactype123!@#', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Username Input"')); + }); -// afterAll(async () => { -// await mongoose.connection.close(); -// }); -// //for creating unqiue login credentials -// const num = Math.floor(Math.random() * 1000); + it("returns the message 'No Username Input' when no username is entered", () => { + return request(app) + .post('/login') + .send({ + username: '', + password: 'Reactype123!@#', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Username Input"')); + }); -// describe('User authentication tests', () => { -// //test connection to server -// describe('initial connection test', () => { -// it('should connect to the server', async () => { -// const response = await request(app).get('/test'); -// expect(response.text).toEqual('test request is working'); -// }); -// }); + it("returns the message 'No Password Input' when no password is entered", () => { + return request(app) + .post('/login') + .send({ + username: 'reactype123', + password: '', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Password Input"')); + }); -// xdescribe('POST', () => { -// it('responds with status 200 and json object on valid new user signup', () => { -// return request(app) -// .post('/signup') -// .set('Content-Type', 'application/json') -// .send({ -// username: `supertest${num}`, -// email: `test${num}@test.com`, -// password: `${num}` -// }) -// .expect(200) -// .then((res) => expect(typeof res.body).toBe('object')); -// }); + it("returns the message 'Invalid Username' when username does not exist", () => { + return request(app) + .post('/login') + .send({ + username: 'l!b', + password: 'test', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"Invalid Username"')); + }); + }); -// it('responds with status 400 and json string on invalid new user signup', () => { -// return request(app) -// .post('/signup') -// .send(user) -// .set('Accept', 'application/json') -// .expect('Content-Type', /json/) -// .expect(400) -// .then((res) => expect(typeof res.body).toBe('string')); -// }); -// }); -// }); -// describe('/login', () => { -// // tests whether existing login information permits user to log in -// xdescribe('POST', () => { -// it('responds with status 200 and json object on verified user login', () => { -// return request(app) -// .post('/login') -// .set('Accept', 'application/json') -// .send(user) -// .expect(200) -// .expect('Content-Type', /json/) -// .then((res) => expect(res.body.sessionId).toEqual(user.userId)); -// }); -// // if invalid username/password, should respond with status 400 -// it('responds with status 400 and json string on invalid user login', () => { -// return request(app) -// .post('/login') -// .send({ username: 'wrongusername', password: 'wrongpassword' }) -// .expect(400) -// .expect('Content-Type', /json/) -// .then((res) => expect(typeof res.body).toBe('string')); -// }); -// it('responds with status 400 and json string on invalid new user signup', () => { -// return request(app) -// .post('/signup') -// .send(user) -// .set('Accept', 'application/json') -// .expect('Content-Type', /json/) -// .expect(400) -// .then((res) => expect(typeof res.body).toBe('string')); -// }); -// }); -// }); + it("returns the message 'Incorrect Password' when password does not match", () => { + return request(app) + .post('/login') + .send({ + username: 'test', + password: 'test', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"Incorrect Password"')); + }); + }); +}); // describe('sessionIsCreated', () => { -// it("returns the message 'No Username Input' when no username is entered", () => { -// return request(app) -// .post('/login') -// .send({ -// username: '', -// password: 'Reactype123!@#', -// isFbOauth: false -// }) -// .then((res) => expect(res.text).toBe('"No Username Input"')); -// }); -// it("returns the message 'No Password Input' when no password is entered", () => { -// return request(app) -// .post('/login') -// .send({ -// username: 'reactype123', -// password: '', -// isFbOauth: false -// }) -// .then((res) => expect(res.text).toBe('"No Password Input"')); -// }); - -// it("returns the message 'Invalid Username' when username does not exist", () => { -// return request(app) -// .post('/login') -// .send({ -// username: 'l!b', -// password: 'test', -// isFbOauth: false -// }) -// .then((res) => expect(res.text).toBe('"Invalid Username"')); -// }); -// }); - -// it("returns the message 'Incorrect Password' when password does not match", () => { -// return request(app) -// .post('/login') -// .send({ -// username: 'test', -// password: 'test', -// isFbOauth: false -// }) -// .then((res) => expect(res.text).toBe('"Incorrect Password"')); -// }); // // note that the username and password in this test are kept in the heroku database // // DO NOT CHANGE unless you have access to the heroku database // it("returns the message 'Success' when the user passes all auth checks", () => { @@ -240,7 +164,6 @@ describe('User Authentication tests', () => { // }); // // // OAuth tests (currently inoperative) - // // xdescribe('Github oauth tests', () => { // // describe('/github/callback?code=', () => { // // describe('GET', () => { diff --git a/server/routers/auth.ts b/server/routers/auth.ts index 4e17ff196..79dd281a8 100644 --- a/server/routers/auth.ts +++ b/server/routers/auth.ts @@ -33,13 +33,13 @@ router.get( res.cookie('ssid', req.user.id, { httpOnly: true, sameSite: 'none', - secure: true, + secure: true }); res.cookie('username', req.user.username, { httpOnly: true, sameSite: 'none', - secure: true, + secure: true }); return res.redirect(API_BASE_URL); } @@ -50,7 +50,6 @@ router.get( passport.authenticate('google', { scope: ['profile'] }) - ); router.get( @@ -62,15 +61,13 @@ router.get( res.cookie('ssid', req.user.id, { httpOnly: true, sameSite: 'none', - secure: true, + secure: true }); - - res.cookie('username', req.user.username, { httpOnly: true, sameSite: 'none', - secure: true, + secure: true }); return res.redirect(API_BASE_URL); } From b1d259bcbc606d61f6d1ce153ab4d1b34d85cd37 Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Wed, 7 Feb 2024 19:55:55 -0500 Subject: [PATCH 009/221] add bottom tabs change --- app/src/components/bottom/BottomTabs.tsx | 27 +++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/app/src/components/bottom/BottomTabs.tsx b/app/src/components/bottom/BottomTabs.tsx index b3f17482b..1a5ec60d5 100644 --- a/app/src/components/bottom/BottomTabs.tsx +++ b/app/src/components/bottom/BottomTabs.tsx @@ -8,6 +8,7 @@ import CustomizationPanel from '../../containers/CustomizationPanel'; import CreationPanel from './CreationPanel'; import ContextManager from '../ContextAPIManager/ContextManager'; import StateManager from '../StateManagement/StateManagement'; +import Chatroom from './chatRoom'; import Box from '@mui/material/Box'; import Tree from '../../tree/TreeChart'; import FormControl from '@mui/material/FormControl'; @@ -29,6 +30,8 @@ const BottomTabs = (props): JSX.Element => { const state = useSelector((store: RootState) => store.appState); const contextParam = useSelector((store: RootState) => store.contextSlice); + const collaborationRoom = useSelector((store: RootState) => store.roomSlice); + // {roomCode: '', userName: '', userList: Array(0), userJoined: false} const [tab, setTab] = useState(0); const classes = useStyles(); @@ -57,7 +60,7 @@ const BottomTabs = (props): JSX.Element => { zIndex: 1, borderTop: '2px solid grey' }} - onClick={() => { + onMouseOver={() => { props.setBottomShow(true); }} > @@ -105,6 +108,11 @@ const BottomTabs = (props): JSX.Element => { classes={{ root: classes.tabRoot, selected: classes.tabSelected }} label="State Manager" /> +
@@ -143,6 +151,23 @@ const BottomTabs = (props): JSX.Element => { isThemeLight={props.isThemeLight} /> )} + {tab === 6 && + (collaborationRoom.userJoined ? ( + + ) : ( +
+

+ Please join a collaboration room to enable this function +

+
+ ))}
); From 58001b286aebb5e532ba5d45c89a0edd0be50a07 Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Wed, 7 Feb 2024 20:24:42 -0500 Subject: [PATCH 010/221] add server side --- server/server.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/server.ts b/server/server.ts index c6a6783ab..9e3c7527f 100644 --- a/server/server.ts +++ b/server/server.ts @@ -167,6 +167,11 @@ io.on('connection', (client) => { }); //-------Socket events for state synchronization in collab room------------------ + client.on('send-chat-message', (roomCode: string, messageData: object) => { + if (roomCode) { + io.to(roomCode).emit('new chat message', messageData); + } + }); client.on('addChildAction', (roomCode: string, childData: object) => { // console.log('child data received on server:', childData); if (roomCode) { From 2697a8e9baa0585345ec401b45a40218ea3a1840 Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Wed, 7 Feb 2024 20:25:03 -0500 Subject: [PATCH 011/221] Multiple messages received --- app/src/components/bottom/chatRoom.tsx | 108 +++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 app/src/components/bottom/chatRoom.tsx diff --git a/app/src/components/bottom/chatRoom.tsx b/app/src/components/bottom/chatRoom.tsx new file mode 100644 index 000000000..c792272ce --- /dev/null +++ b/app/src/components/bottom/chatRoom.tsx @@ -0,0 +1,108 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; +import Box from '@mui/material/Box'; +import { + initializeSocket, + getSocket, + emitEvent, + disconnectSocket +} from '../../helperFunctions/socket'; + +const Chatroom = (props): JSX.Element => { + const collaborationRoom = useSelector((store: RootState) => store.roomSlice); + // const newMessage = useSelector( + // (store: RootState) => store.roomSlice.newMessage + // ); + const socket = getSocket(); + + const nickName = collaborationRoom.userName; + const roomCode = collaborationRoom.roomCode; + // const newMessage = collaborationRoom.newMessage; + + const wrapperStyles = { + border: `2px solid #f2fbf8`, + borderRadius: '8px', + width: '70%', + height: '90%', + display: 'column', + padding: '20px', + // justifyContent: 'center', + backgroundColor: '#42464C', + overflow: 'auto' + // scrollTop: 'scrollHeight' + }; + + const inputContainerStyles = { + width: '100%', + padding: '10px', + display: 'flex', + justifyContent: 'center' + }; + + const inputStyles = { + width: '70%', + padding: '10px', + border: 'none', + borderRadius: '5px', + backgroundColor: '#333333', + color: 'white' + }; + + const buttonStyles = { + padding: '10px', + marginLeft: '10px', + backgroundColor: '#4CAF50', + color: 'white', + border: 'none', + borderRadius: '5px', + cursor: 'pointer' + }; + + const handleSubmit = (e) => { + e.preventDefault(); + const messageInput = document.getElementById( + 'message-input' + ) as HTMLInputElement; + const message = messageInput.value; + emitEvent('send-chat-message', roomCode, { message, nickName }); + messageInput.value = ''; + }; + + socket.on('new chat message', (remoteData) => { + const messageContainer = document.getElementById('message-container'); + const messageElement = document.createElement('div'); + messageElement.innerText = + nickName === remoteData.nickName + ? `You: ${remoteData.message}` + : `${remoteData.nickName}: ${remoteData.message}`; + messageContainer.append(messageElement); + }); + + return ( +
+
+

Current room: {roomCode}

+

Your Nickname: {nickName}

+
+
+
+
+
+ + +
+
+ ); +}; + +export default Chatroom; From d00b60fe201544c02e7666b01dd3f93581b90331 Mon Sep 17 00:00:00 2001 From: Brian Date: Thu, 8 Feb 2024 13:13:51 -0500 Subject: [PATCH 012/221] click to add functionality added to collab room --- app/src/components/left/HTMLItem.tsx | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/app/src/components/left/HTMLItem.tsx b/app/src/components/left/HTMLItem.tsx index 65d7bdce5..ee34b7e1e 100644 --- a/app/src/components/left/HTMLItem.tsx +++ b/app/src/components/left/HTMLItem.tsx @@ -12,8 +12,9 @@ import { useDrag } from 'react-dnd'; import { IconProps } from '@mui/material'; import CodeIcon from '@mui/icons-material/Code'; import * as Icons from '@mui/icons-material'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { addChild } from '../../redux/reducers/slice/appStateSlice'; +import { emitEvent } from '../../helperFunctions/socket'; const useStyles = makeStyles({ HTMLPanelItem: { @@ -43,6 +44,8 @@ const HTMLItem: React.FC<{ //load mui icons base on string parameter const IconComponent = Icons[icon]; + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); // current roomCode + const classes = useStyles(); const [modal, setModal] = useState(null); @@ -123,7 +126,7 @@ const HTMLItem: React.FC<{ const dispatch = useDispatch(); - const handleClick = () => { + /* const handleClick = () => { console.log('Component clicked:', name); console.log('id', id); // Dispatch action to add child @@ -137,6 +140,25 @@ const HTMLItem: React.FC<{ } }) ); + };*/ + + const handleClick = () => { + console.log('Component clicked:', name); + console.log('id', id); + const childData = { + type: 'HTML Element', + typeId: id, + childId: null, + contextParam: { + allContext: [] + } + }; + + dispatch(addChild(childData)); + if (roomCode) { + // Emit 'addChildAction' event to the server + emitEvent('addChildAction', roomCode, childData); + } }; // updated the id's to reflect the new element types input and label From 2b708f57e6549a8e34a0d6f8204b859bbe00957d Mon Sep 17 00:00:00 2001 From: cyburns <72774499+cyburns@users.noreply.github.com> Date: Thu, 8 Feb 2024 13:20:38 -0500 Subject: [PATCH 013/221] with vite working some fetch requests need to be midded --- __tests__/server.test.tsx | 255 ++-- __tests__/userAuth.test.ts | 35 +- app/.electron/main.ts | 7 +- app/.electron/menu.ts | 2 +- app/src/components/bottom/CodePreview.tsx | 2 +- app/src/components/left/Sidebar.tsx | 1 + app/src/components/login/SignIn.tsx | 6 +- app/src/components/right/LoginButton.tsx | 14 +- app/src/components/top/NavBarButtons.tsx | 5 +- app/src/helperFunctions/auth.ts | 13 +- app/src/helperFunctions/generateCode.ts | 17 +- app/src/helperFunctions/projectGetSaveDel.ts | 72 +- app/src/helperFunctions/socket.ts | 5 +- app/src/helperFunctions/zipFiles.ts | 2 +- app/src/index.tsx | 6 +- app/src/public/styles/style.css | 34 +- app/src/serverConfig.js | 14 + app/src/utils/createApplication.util.ts | 2 +- config.js | 2 + index.html | 14 + package-lock.json | 1085 +++++++++++++++++- package.json | 14 +- playwright.config.ts | 8 +- src/custom-aws-exports.js | 60 +- tsconfig.json | 21 +- vite.config.ts | 23 + webpack.config.js | 2 +- webpack.development.js | 32 +- webpack.production.js | 2 +- 29 files changed, 1481 insertions(+), 274 deletions(-) create mode 100644 app/src/serverConfig.js create mode 100644 index.html create mode 100644 vite.config.ts diff --git a/__tests__/server.test.tsx b/__tests__/server.test.tsx index c64ca9f96..d1c339383 100644 --- a/__tests__/server.test.tsx +++ b/__tests__/server.test.tsx @@ -2,7 +2,7 @@ * @jest-environment node */ -import marketplaceController from '../server/controllers/marketplaceController'; +import marketplaceController from '../server/controllers/marketplaceController'; import sessionController from '../server/controllers/sessionController'; import app from '../server/server'; import mockData from '../mockData'; @@ -11,33 +11,34 @@ import { Projects, Users, Sessions } from '../server/models/reactypeModels'; const request = require('supertest'); const mongoose = require('mongoose'); const mockNext = jest.fn(); // Mock nextFunction -const MONGO_DB = process.env.MONGO_DB_TEST; -const { state, projectToSave, user } = mockData +const MONGO_DB = import.meta.env.MONGO_DB_TEST; +const { state, projectToSave, user } = mockData; const PORT = 8080; beforeAll(async () => { await mongoose.connect(MONGO_DB, { useNewUrlParser: true, - useUnifiedTopology: true, + useUnifiedTopology: true }); }); afterAll(async () => { - - const result = await Projects.deleteMany({});//clear the projects collection after tests are done - const result2 = await Users.deleteMany({_id: {$ne: '64f551e5b28d5292975e08c8'}});//clear the users collection after tests are done except for the mockdata user account - const result3 = await Sessions.deleteMany({cookieId: {$ne: '64f551e5b28d5292975e08c8'}}); + const result = await Projects.deleteMany({}); //clear the projects collection after tests are done + const result2 = await Users.deleteMany({ + _id: { $ne: '64f551e5b28d5292975e08c8' } + }); //clear the users collection after tests are done except for the mockdata user account + const result3 = await Sessions.deleteMany({ + cookieId: { $ne: '64f551e5b28d5292975e08c8' } + }); await mongoose.connection.close(); }); - describe('Server endpoint tests', () => { it('should pass this test request', async () => { const response = await request(app).get('/test'); expect(response.status).toBe(200); expect(response.text).toBe('test request is working'); }); - // // test saveProject endpoint // describe('/login', () => { @@ -45,7 +46,7 @@ describe('Server endpoint tests', () => { // it('responds with a status of 200 and json object equal to project sent', async () => { // return request(app) // .post('/login') - // .set('Cookie', [`ssid=${user.userId}`]) + // .set('Cookie', [`ssid=${user.userId}`]) // .set('Accept', 'application/json') // .send(projectToSave) // .expect(200) @@ -62,14 +63,14 @@ describe('Server endpoint tests', () => { it('responds with a status of 200 and json object equal to project sent', async () => { return request(app) .post('/saveProject') - .set('Cookie', [`ssid=${user.userId}`]) + .set('Cookie', [`ssid=${user.userId}`]) .set('Accept', 'application/json') .send(projectToSave) .expect(200) .expect('Content-Type', /application\/json/) .then((res) => expect(res.body.name).toBe(projectToSave.name)); }); - // }); + // }); }); }); // test getProjects endpoint @@ -79,7 +80,7 @@ describe('Server endpoint tests', () => { return request(app) .post('/getProjects') .set('Accept', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) + .set('Cookie', [`ssid=${user.userId}`]) .send({ userId: projectToSave.userId }) .expect(200) .expect('Content-Type', /json/) @@ -94,16 +95,20 @@ describe('Server endpoint tests', () => { describe('/deleteProject', () => { describe('DELETE', () => { it('responds with status of 200 and json object equal to deleted project', async () => { - const response: Response = await request(app).post('/getProjects').set('Accept', 'application/json').set('Cookie', [`ssid=${user.userId}`]).send({ userId: projectToSave.userId }); + const response: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); const _id: String = response.body[0]._id; const userId: String = user.userId; return request(app) .delete('/deleteProject') - .set('Cookie', [`ssid=${user.userId}`]) + .set('Cookie', [`ssid=${user.userId}`]) .set('Content-Type', 'application/json') .send({ _id, userId }) .expect(200) - .then((res) => expect(res.body._id).toBe(_id)); + .then((res) => expect(res.body._id).toBe(_id)); }); }); }); @@ -112,12 +117,11 @@ describe('Server endpoint tests', () => { describe('/publishProject', () => { describe('POST', () => { it('responds with status of 200 and json object equal to published project', async () => { - const projObj = await request(app) .post('/saveProject') - .set('Cookie', [`ssid=${user.userId}`]) + .set('Cookie', [`ssid=${user.userId}`]) .set('Accept', 'application/json') - .send(projectToSave) + .send(projectToSave); const _id: String = projObj.body._id; const project: String = projObj.body.project; const comments: String = projObj.body.comments; @@ -127,56 +131,65 @@ describe('Server endpoint tests', () => { return request(app) .post('/publishProject') .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, project, comments, userId, username, name })//_id, project, comments, userId, username, name + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, project, comments, userId, username, name }) //_id, project, comments, userId, username, name .expect(200) .then((res) => { - expect(res.body._id).toBe(_id) + expect(res.body._id).toBe(_id); expect(res.body.published).toBe(true); - }); + }); }); it('responds with status of 500 and error if userId and cookie ssid do not match', async () => { - const projObj: Response = await request(app).post('/getProjects').set('Accept', 'application/json').set('Cookie', [`ssid=${user.userId}`]).send({ userId: projectToSave.userId }); + const projObj: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); const _id: String = projObj.body[0]._id; const project: String = projObj.body[0].project; const comments: String = projObj.body[0].comments; const username: String = projObj.body[0].username; const name: String = projObj.body[0].name; - const userId: String = "ERROR"; + const userId: String = 'ERROR'; return request(app) .post('/publishProject') .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, project, comments, userId, username, name })//_id, project, comments, userId, username, name + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, project, comments, userId, username, name }) //_id, project, comments, userId, username, name .expect(500) .then((res) => { - expect(res.body.err).not.toBeNull() - }); + expect(res.body.err).not.toBeNull(); + }); }); it('responds with status of 500 and error if _id was not a valid mongo ObjectId', async () => { - const projObj: Response = await request(app).post('/getProjects').set('Accept', 'application/json').set('Cookie', [`ssid=${user.userId}`]).send({ userId: projectToSave.userId }); + const projObj: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); const _id: String = 'ERROR'; const project: String = projObj.body[0].project; const comments: String = projObj.body[0].comments; const username: String = user.username; const name: String = projObj.body[0].name; const userId: String = user.userId; - + return request(app) .post('/publishProject') .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, project, comments, userId, username, name })//_id, project, comments, userId, username, name + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, project, comments, userId, username, name }) //_id, project, comments, userId, username, name .expect(200) .then((res) => { - expect(res.body._id).not.toEqual(_id) - }); + expect(res.body._id).not.toEqual(_id); + }); }); }); }); //test getMarketplaceProjects endpoint - describe('/getMarketplaceProjects', () => {//most recent project should be the one from publishProject + describe('/getMarketplaceProjects', () => { + //most recent project should be the one from publishProject describe('GET', () => { it('responds with status of 200 and json object equal to unpublished project', async () => { @@ -187,7 +200,7 @@ describe('Server endpoint tests', () => { .then((res) => { expect(Array.isArray(res.body)).toBe(true); expect(res.body[0]._id).toBeTruthy; - }); + }); }); }); }); @@ -196,10 +209,9 @@ describe('Server endpoint tests', () => { describe('/cloneProject/:docId', () => { describe('GET', () => { it('responds with status of 200 and json object equal to cloned project', async () => { - const projObj = await request(app) .get('/getMarketplaceProjects') - .set('Content-Type', 'application/json') + .set('Content-Type', 'application/json'); return request(app) .get(`/cloneProject/${projObj.body[0]._id}`) @@ -209,13 +221,12 @@ describe('Server endpoint tests', () => { .then((res) => { expect(res.body.forked).toBeTruthy; expect(res.body.username).toBe(user.username); - }); + }); }); it('responds with status of 500 and error', async () => { - const projObj = await request(app) .get('/getMarketplaceProjects') - .set('Content-Type', 'application/json') + .set('Content-Type', 'application/json'); return request(app) .get(`/cloneProject/${projObj.body[0]._id}`) @@ -223,8 +234,8 @@ describe('Server endpoint tests', () => { .query({ username: [] }) .expect(500) .then((res) => { - expect(res.body.err).not.toBeNull() - }); + expect(res.body.err).not.toBeNull(); + }); }); }); }); @@ -233,7 +244,11 @@ describe('Server endpoint tests', () => { describe('/unpublishProject', () => { describe('PATCH', () => { it('responds with status of 200 and json object equal to unpublished project', async () => { - const response: Response = await request(app).post('/getProjects').set('Accept', 'application/json').set('Cookie', [`ssid=${user.userId}`]).send({ userId: projectToSave.userId }); //most recent project should be the one from publishProject + const response: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); //most recent project should be the one from publishProject const _id: String = response.body[0]._id; const userId: String = user.userId; return request(app) @@ -243,26 +258,29 @@ describe('Server endpoint tests', () => { .send({ _id, userId }) .expect(200) .then((res) => { - expect(res.body._id).toBe(_id) + expect(res.body._id).toBe(_id); expect(res.body.published).toBe(false); - }); + }); }); it('responds with status of 500 and error if userId and cookie ssid do not match', async () => { - const projObj: Response = await request(app).post('/getProjects').set('Accept', 'application/json').set('Cookie', [`ssid=${user.userId}`]).send({ userId: projectToSave.userId }); + const projObj: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); const _id: String = projObj.body[0]._id; const project: String = projObj.body[0].project; const comments: String = projObj.body[0].comments; const username: String = projObj.body[0].username; const name: String = projObj.body[0].name; let userId: String = user.userId; - await request(app)//publishing a project first + await request(app) //publishing a project first .post('/publishProject') .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, project, comments, userId, username, name }) - - - userId = "ERROR"; + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, project, comments, userId, username, name }); + + userId = 'ERROR'; return request(app) .patch('/unpublishProject') .set('Content-Type', 'application/json') @@ -270,8 +288,8 @@ describe('Server endpoint tests', () => { .send({ _id, userId }) .expect(500) .then((res) => { - expect(res.body.err).not.toBeNull() - }); + expect(res.body.err).not.toBeNull(); + }); }); it('responds with status of 500 and error if _id was not a string', async () => { const userId: String = user.userId; @@ -280,32 +298,28 @@ describe('Server endpoint tests', () => { .patch('/unpublishProject') .set('Content-Type', 'application/json') .set('Cookie', [`ssid=${user.userId}`]) - .send({userId }) + .send({ userId }) .expect(500) .then((res) => { - expect(res.body.err).not.toBeNull() - }); + expect(res.body.err).not.toBeNull(); + }); }); }); }); }); describe('SessionController tests', () => { - - - - describe('isLoggedIn',() => { - + describe('isLoggedIn', () => { afterEach(() => { jest.resetAllMocks(); - }) - // Mock Express request and response objects and next function + }); + // Mock Express request and response objects and next function const mockReq: any = { - cookies: null,//trying to trigger if cookies was not assigned + cookies: null, //trying to trigger if cookies was not assigned body: { - userId: 'sampleUserId', // Set up a sample userId in the request body - }, - } + userId: 'sampleUserId' // Set up a sample userId in the request body + } + }; const mockRes: any = { json: jest.fn(), status: jest.fn(), @@ -313,79 +327,88 @@ describe('SessionController tests', () => { }; const next = jest.fn(); it('Assign userId from request body to cookieId', async () => { - // Call isLoggedIn + // Call isLoggedIn await sessionController.isLoggedIn(mockReq, mockRes, next); expect(mockRes.redirect).toHaveBeenCalledWith('/'); - // Ensure that next() was called + // Ensure that next() was called }); it('Trigger a database query error for findOne', async () => { - const mockFindOne = jest.spyOn(mongoose.model('Sessions'), 'findOne').mockImplementation(() => { - throw new Error('Database query error'); - }); - // Call isLoggedIn + const mockFindOne = jest + .spyOn(mongoose.model('Sessions'), 'findOne') + .mockImplementation(() => { + throw new Error('Database query error'); + }); + // Call isLoggedIn await sessionController.isLoggedIn(mockReq, mockRes, next); - // Ensure that next() was called with the error - expect(next).toHaveBeenCalledWith(expect.objectContaining({ - log: expect.stringMatching('Database query error'), // The 'i' flag makes it case-insensitive - })); + // Ensure that next() was called with the error + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + log: expect.stringMatching('Database query error') // The 'i' flag makes it case-insensitive + }) + ); mockFindOne.mockRestore(); }); }); - - - describe('startSession',() => { - + + describe('startSession', () => { afterEach(() => { jest.resetAllMocks(); - }) + }); it('Trigger a database query error for findOne', async () => { - const mockReq: any = { - cookies: projectToSave.userId,//trying to trigger if cookies was not assigned + cookies: projectToSave.userId, //trying to trigger if cookies was not assigned body: { - userId: 'sampleUserId', // Set up a sample userId in the request body - }, - } + userId: 'sampleUserId' // Set up a sample userId in the request body + } + }; const mockRes: any = { json: jest.fn(), status: jest.fn(), redirect: jest.fn(), - locals: {id: projectToSave.userId} + locals: { id: projectToSave.userId } }; - + const next = jest.fn(); - const findOneMock = jest.spyOn(mongoose.model('Sessions'), 'findOne') as jest.Mock; - findOneMock.mockImplementation((query: any, callback: (err: any, ses: any) => void) => { - callback(new Error('Database query error'), null); - }); + const findOneMock = jest.spyOn( + mongoose.model('Sessions'), + 'findOne' + ) as jest.Mock; + findOneMock.mockImplementation( + (query: any, callback: (err: any, ses: any) => void) => { + callback(new Error('Database query error'), null); + } + ); // Call startSession await sessionController.startSession(mockReq, mockRes, next); // Check that next() was called with the error - expect(next).toHaveBeenCalledWith(expect.objectContaining({ - log: expect.stringMatching('Database query error'), // The 'i' flag makes it case-insensitive - })); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + log: expect.stringMatching('Database query error') // The 'i' flag makes it case-insensitive + }) + ); findOneMock.mockRestore(); }); - xit('Check if a new Session is created', async () => {//not working for some reason cannot get mocknext() to be called in test? + xit('Check if a new Session is created', async () => { + //not working for some reason cannot get mocknext() to be called in test? const mockReq: any = { - cookies: projectToSave.userId,//trying to trigger if cookies was not assigned + cookies: projectToSave.userId, //trying to trigger if cookies was not assigned body: { - userId: 'sampleUserId', // Set up a sample userId in the request body - }, - } + userId: 'sampleUserId' // Set up a sample userId in the request body + } + }; const mockRes: any = { json: jest.fn(), status: jest.fn(), redirect: jest.fn(), - locals: {id: 'testID'}//a sesion id that doesnt exist + locals: { id: 'testID' } //a sesion id that doesnt exist }; - + const mockNext = jest.fn(); - + //Call startSession // Wrap your test logic in an async function await sessionController.startSession(mockReq, mockRes, mockNext); @@ -393,21 +416,10 @@ describe('SessionController tests', () => { //check if it reaches next() //await expect(mockRes.locals.ssid).toBe('testID'); expect(mockNext).toHaveBeenCalled(); - - - }); }); }); - - - - - - - - // describe('marketplaceController Middleware', () => { // describe('getProjects tests', () => { // it('should add the projects as an array to res.locals', () => { @@ -421,13 +433,12 @@ describe('SessionController tests', () => { // }); // }); - // it('should send an error response if there is an error in the middleware', () => { // const req = { user: { isAuthenticated: false } }; // const res = mockResponse(); - + // marketplaceController.authenticateMiddleware(req, res, mockNext); - + // expect(res.status).toHaveBeenCalledWith(500); // expect(res.json).toHaveBeenCalledWith({ err: 'Error in marketplaceController.getPublishedProjects, check server logs for details' }); // }); diff --git a/__tests__/userAuth.test.ts b/__tests__/userAuth.test.ts index 2f5137d97..2722ece56 100644 --- a/__tests__/userAuth.test.ts +++ b/__tests__/userAuth.test.ts @@ -2,8 +2,7 @@ * @jest-environment node */ - -import marketplaceController from '../server/controllers/marketplaceController'; +import marketplaceController from '../server/controllers/marketplaceController'; import app from '../server/server'; import mockData from '../mockData'; import { profileEnd } from 'console'; @@ -11,8 +10,8 @@ import { Sessions, Users } from '../server/models/reactypeModels'; const request = require('supertest'); const mongoose = require('mongoose'); const mockNext = jest.fn(); // Mock nextFunction -const MONGO_DB = process.env.MONGO_DB_TEST; -const { state, projectToSave, user } = mockData +const MONGO_DB = import.meta.env.MONGO_DB_TEST; +const { state, projectToSave, user } = mockData; const PORT = 8080; const num = Math.floor(Math.random() * 1000); @@ -20,15 +19,20 @@ const num = Math.floor(Math.random() * 1000); beforeAll(async () => { await mongoose.connect(MONGO_DB, { useNewUrlParser: true, - useUnifiedTopology: true, + useUnifiedTopology: true }); }); afterAll(async () => { - - const result = await Users.deleteMany({_id: {$ne: '64f551e5b28d5292975e08c8'}});//clear the users collection after tests are done except for the mockdata user account - const result2 = await Sessions.deleteMany({cookieId: {$ne: '64f551e5b28d5292975e08c8'}}); - console.log(`${result.deletedCount} and ${result2.deletedCount} documents deleted.`); + const result = await Users.deleteMany({ + _id: { $ne: '64f551e5b28d5292975e08c8' } + }); //clear the users collection after tests are done except for the mockdata user account + const result2 = await Sessions.deleteMany({ + cookieId: { $ne: '64f551e5b28d5292975e08c8' } + }); + console.log( + `${result.deletedCount} and ${result2.deletedCount} documents deleted.` + ); await mongoose.connection.close(); }); @@ -40,7 +44,7 @@ describe('User Authentication tests', () => { expect(response.text).toBe('test request is working'); }); }); - describe('/signup', ()=> { + describe('/signup', () => { describe('POST', () => { //testing new signup it('responds with status 200 and sessionId on valid new user signup', () => { @@ -55,7 +59,7 @@ describe('User Authentication tests', () => { .expect(200) .then((res) => expect(res.body.sessionId).not.toBeNull()); }); - + it('responds with status 400 and json string on invalid new user signup (Already taken)', () => { return request(app) .post('/signup') @@ -68,7 +72,7 @@ describe('User Authentication tests', () => { }); }); describe('/login', () => { - // tests whether existing login information permits user to log in + // tests whether existing login information permits user to log in describe('POST', () => { it('responds with status 200 and json object on verified user login', () => { return request(app) @@ -80,7 +84,7 @@ describe('User Authentication tests', () => { .then((res) => expect(res.body.sessionId).toEqual(user.userId)); }); // if invalid username/password, should respond with status 400 - it('responds with status 400 and json string on invalid user login', () => { + it('responds with status 400 and json string on invalid user login', () => { return request(app) .post('/login') .send({ username: 'wrongusername', password: 'wrongpassword' }) @@ -90,17 +94,14 @@ describe('User Authentication tests', () => { }); }); }); - }); - - // import request from 'supertest'; // import app from '../server/server'; // import mockObj from '../mockData'; // const user = mockObj.user; // import mongoose from 'mongoose'; -// const URI = process.env.MONGO_DB; +// const URI = import.meta.env.MONGO_DB; // beforeAll(() => { // mongoose diff --git a/app/.electron/main.ts b/app/.electron/main.ts index a9209dfbe..6bf974c01 100644 --- a/app/.electron/main.ts +++ b/app/.electron/main.ts @@ -30,7 +30,8 @@ import MenuBuilder from './menu'; // mode that the app is running in const isDev = - process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; + import.meta.env.NODE_ENV === 'development' || + import.meta.env.NODE_ENV === 'test'; const port = 8080; const selfHost = `http://localhost:${port}`; @@ -388,8 +389,8 @@ ipcMain.on('github', (event) => { ? `http://localhost:${DEV_PORT}/auth/github` : `https://reactype-caret.herokuapp.com/auth/github`; const options = { - client_id: process.env.GITHUB_ID, - client_secret: process.env.GITHUB_SECRET, + client_id: import.meta.env.GITHUB_ID, + client_secret: import.meta.env.GITHUB_SECRET, scopes: ['user:email', 'notifications'] }; // create new browser window object with size, title, security options diff --git a/app/.electron/menu.ts b/app/.electron/menu.ts index dcdb2708a..c0814ebe2 100644 --- a/app/.electron/menu.ts +++ b/app/.electron/menu.ts @@ -47,7 +47,7 @@ var MenuBuilder = function (mainWindow, appName) { devTools: false } }); - if (process.env.NODE_ENV === 'development') { + if (import.meta.env.NODE_ENV === 'development') { tutorial.loadURL(`http://localhost:8080/#/tutorial`); } else { tutorial.loadURL(`${Protocol.scheme}://rse/index-prod.html#/tutorial`); diff --git a/app/src/components/bottom/CodePreview.tsx b/app/src/components/bottom/CodePreview.tsx index 46730153c..ccf7d15d0 100644 --- a/app/src/components/bottom/CodePreview.tsx +++ b/app/src/components/bottom/CodePreview.tsx @@ -70,7 +70,7 @@ const CodePreview: React.FC<{ minify: true, plugins: [unpkgPathPlugin(), fetchPlugin(data)], define: { - 'process.env.NODE_ENV': '"production"', + 'import.meta.env.NODE_ENV': '"production"', global: 'window' } }); diff --git a/app/src/components/left/Sidebar.tsx b/app/src/components/left/Sidebar.tsx index 37cea676d..6674b790d 100644 --- a/app/src/components/left/Sidebar.tsx +++ b/app/src/components/left/Sidebar.tsx @@ -101,6 +101,7 @@ const Sidebar: React.FC = ({ icon={} value={4} /> + ); }; diff --git a/app/src/components/login/SignIn.tsx b/app/src/components/login/SignIn.tsx index a815da90a..c8b7c5990 100644 --- a/app/src/components/login/SignIn.tsx +++ b/app/src/components/login/SignIn.tsx @@ -24,13 +24,15 @@ import { LoginInt } from '../../interfaces/Interfaces'; import { RootState } from '../../redux/store'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; -import config from '../../../../config.js'; +import serverConfig from "../../serverConfig.js"; + +// const config = require('../../../../config.js'); import makeStyles from '@mui/styles/makeStyles'; import { newUserIsCreated } from '../../helperFunctions/auth'; import randomPassword from '../../helperFunctions/randomPassword'; import { sessionIsCreated } from '../../helperFunctions/auth'; -const { API_BASE_URL } = config; +const { API_BASE_URL } = serverConfig; declare module '@mui/styles/defaultTheme' { interface DefaultTheme extends Theme {} diff --git a/app/src/components/right/LoginButton.tsx b/app/src/components/right/LoginButton.tsx index b793d4603..61ba0bb4e 100644 --- a/app/src/components/right/LoginButton.tsx +++ b/app/src/components/right/LoginButton.tsx @@ -1,12 +1,14 @@ import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { toggleLoggedIn } from '../../redux/reducers/slice/appStateSlice'; -import config from '../../../../config.js'; +import serverConfig from '../../serverConfig.js'; +const { DEV_PORT, API_BASE_URL, API_BASE_URL2 } = serverConfig; +// const config = require('../../../../config.js'); import { RootState } from '../../redux/store'; import Cookies from 'js-cookie'; // note that API_BASE_URL is assigned to different pages on dev mode vs prod mode -const { DEV_PORT, API_BASE_URL, API_BASE_URL2 } = config; -const isDev = process.env.NODE_ENV === 'development'; +// const { DEV_PORT, API_BASE_URL, API_BASE_URL2 } = config; +const isDev = import.meta.env.NODE_ENV === 'development'; let serverURL = API_BASE_URL; //check if we're in dev mode @@ -14,9 +16,8 @@ if (isDev) { serverURL = `http://localhost:${DEV_PORT}`; } - export default function LoginButton() { - const state = useSelector((store:RootState) => store.appState); + const state = useSelector((store: RootState) => store.appState); const dispatch = useDispatch(); const handleLogout = () => { @@ -33,8 +34,7 @@ export default function LoginButton() { return data; }) .catch((err) => console.log(`Error getting project ${err}`)); - - + window.localStorage.clear(); if (state.isLoggedIn) { diff --git a/app/src/components/top/NavBarButtons.tsx b/app/src/components/top/NavBarButtons.tsx index fc4739ee2..f0ab8aaf7 100644 --- a/app/src/components/top/NavBarButtons.tsx +++ b/app/src/components/top/NavBarButtons.tsx @@ -13,7 +13,8 @@ import MenuItem from '@mui/material/MenuItem'; import ProjectsFolder from '../right/OpenProjects'; import { RootState } from '../../redux/store'; import SaveProjectButton from '../right/SaveProjectButton'; -import config from '../../../../config'; +import serverConfig from '../../serverConfig.js'; + import createModal from '../right/createModal'; import createStyles from '@mui/styles/createStyles'; import makeStyles from '@mui/styles/makeStyles'; @@ -22,7 +23,7 @@ import { setStyle } from '../../redux/reducers/slice/styleSlice'; import store from '../../redux/store'; import withStyles from '@mui/styles/withStyles'; -const { API_BASE_URL } = config; +const { API_BASE_URL } = serverConfig; const useStyles = makeStyles((theme) => createStyles({ diff --git a/app/src/helperFunctions/auth.ts b/app/src/helperFunctions/auth.ts index b7092fb14..7ee24c8db 100644 --- a/app/src/helperFunctions/auth.ts +++ b/app/src/helperFunctions/auth.ts @@ -1,6 +1,9 @@ -const fetch = require('node-fetch'); -const isDev = process.env.NODE_ENV === 'development'; -const { DEV_PORT, API_BASE_URL } = require('../../../config'); +// const fetch = require('node-fetch'); +const isDev = import.meta.env.NODE_ENV === 'development'; +// const fetch = require('node-fetch'); +// import fetch from 'node-fetch'; +import serverConfig from '../serverConfig.js'; +const { DEV_PORT, API_BASE_URL } = serverConfig; let serverURL = API_BASE_URL; if (isDev) { @@ -17,7 +20,7 @@ export const sessionIsCreated = ( password, isFbOauth }); - const result = fetch(`/login`, { + const result = fetch(`${serverURL}/login`, { method: 'POST', credentials: 'include', headers: { @@ -48,7 +51,7 @@ export const newUserIsCreated = ( email, password }); - const result = fetch(`/signup`, { + const result = fetch(`${serverURL}/signup`, { method: 'POST', credentials: 'include', headers: { diff --git a/app/src/helperFunctions/generateCode.ts b/app/src/helperFunctions/generateCode.ts index 382ddff7d..c92794cb9 100644 --- a/app/src/helperFunctions/generateCode.ts +++ b/app/src/helperFunctions/generateCode.ts @@ -173,7 +173,14 @@ const generateUnformattedCode = ( width, justifyContent } = childElement.style; - let w:String, h:String, items:String, bg:String, d:String, flexDir:String, justCon:String, cssClasses:String; + let w: String, + h: String, + items: String, + bg: String, + d: String, + flexDir: String, + justCon: String, + cssClasses: String; if (childElement.style.alignItems) { if (alignItems === 'center') items = 'items-center '; else if (alignItems === 'flex-start') items = 'items-start '; @@ -370,7 +377,7 @@ const generateUnformattedCode = ( }; // function to properly incorporate the user created state that is stored in the application state const writeStateProps = (stateArray: String[]) => { - let stateToRender:String = ''; + let stateToRender: String = ''; for (const element of stateArray) { stateToRender += levelSpacer(2, 2) + element + ';'; } @@ -392,7 +399,7 @@ const generateUnformattedCode = ( return `import ${comp} from './${comp}'`; }) .join('\n'); - const createState = (stateProps:StateProp[]) => { + const createState = (stateProps: StateProp[]) => { let state = '{'; stateProps.forEach((ele) => { state += ele.key + ':' + JSON.stringify(ele.value) + ', '; @@ -468,7 +475,7 @@ const generateUnformattedCode = ( return importStr; }; - const createEventHandler = (children:ChildElement[]) => { + const createEventHandler = (children: ChildElement[]) => { let importStr = ''; children.map((child) => { if (child.type === 'HTML Element') { @@ -567,7 +574,7 @@ const generateUnformattedCode = ( }; // formats code with prettier linter const formatCode = (code: string) => { - if (process.env.NODE_ENV === 'test') { + if (import.meta.env.NODE_ENV === 'test') { const { format } = require('prettier'); return format(code, { singleQuote: true, diff --git a/app/src/helperFunctions/projectGetSaveDel.ts b/app/src/helperFunctions/projectGetSaveDel.ts index 8c301750f..fcfd4aed2 100644 --- a/app/src/helperFunctions/projectGetSaveDel.ts +++ b/app/src/helperFunctions/projectGetSaveDel.ts @@ -1,7 +1,11 @@ -import { State } from "../interfaces/Interfaces"; +import { State } from '../interfaces/Interfaces'; -const isDev = process.env.NODE_ENV === 'development'; -const { DEV_PORT, API_BASE_URL } = require('../../../config.js'); +const isDev = import.meta.env.NODE_ENV === 'development'; +import serverConfig from '../serverConfig.js'; + +const { DEV_PORT, API_BASE_URL } = serverConfig; +// import config from '../../../config.js'; +// const { DEV_PORT, API_BASE_URL } = config; let serverURL = API_BASE_URL; //check if we're in dev mode @@ -23,19 +27,19 @@ export const getProjects = (): Promise => { return data; }) .catch((err) => console.log(`Error getting project ${err}`)); - return projects;//returns an array of projects with _id, name, project + return projects; //returns an array of projects with _id, name, project }; export const saveProject = ( name: string, workspace: State ): Promise => { - const newProject = { ...workspace} + const newProject = { ...workspace }; delete newProject._id; delete newProject.name; //deleting the _id from the current state slice. We don't actually want it in the project object in the mongo db document const body = JSON.stringify({ name, - project: { ...newProject}, + project: { ...newProject }, comments: [] }); const project = fetch(`${serverURL}/saveProject`, { @@ -48,29 +52,34 @@ export const saveProject = ( }) .then((res) => res.json()) .then((data) => { - return {_id: data._id, name: data.name, published: data.published, ...data.project}; //passing up what is needed for the global appstateslice + return { + _id: data._id, + name: data.name, + published: data.published, + ...data.project + }; //passing up what is needed for the global appstateslice }) .catch((err) => console.log(`Error saving project ${err}`)); - return project;//returns _id in addition to the project object from the document + return project; //returns _id in addition to the project object from the document }; export const publishProject = ( name: string, workspace: State ): Promise => { - const newProject = { ...workspace} - delete newProject.name; + const newProject = { ...workspace }; + delete newProject.name; const body = JSON.stringify({ - _id: workspace._id, + _id: workspace._id, name: name, - project: { ...newProject}, - comments: [], + project: { ...newProject }, + comments: [] }); const response = fetch(`${serverURL}/publishProject`, { method: 'POST', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, credentials: 'include', body @@ -79,7 +88,12 @@ export const publishProject = ( const publishedProject = response .then((res) => res.json()) .then((data) => { - return {_id: data._id, name: data.name, published:data.published, ...data.project}; + return { + _id: data._id, + name: data.name, + published: data.published, + ...data.project + }; }) .catch((err) => { console.log(`Error publishing project ${err}`); @@ -89,26 +103,29 @@ export const publishProject = ( return publishedProject; }; -export const unpublishProject = ( - projectData: State -): Promise => { +export const unpublishProject = (projectData: State): Promise => { const body = JSON.stringify({ - _id: projectData._id, + _id: projectData._id }); const response = fetch(`${serverURL}/unpublishProject`, { method: 'PATCH', headers: { - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, credentials: 'include', - body, + body }); const unpublishedProject = response .then((res) => res.json()) .then((data) => { - return {_id: data._id, name: data.name, published:data.published, ...data.project}; + return { + _id: data._id, + name: data.name, + published: data.published, + ...data.project + }; }) .catch((err) => { console.log(`Error unpublishing project ${err}`); @@ -120,7 +137,7 @@ export const unpublishProject = ( export const deleteProject = (project: any): Promise => { const body = JSON.stringify({ - _id: project._id, + _id: project._id // userId: window.localStorage.getItem('ssid') }); const deletedProject = fetch(`${serverURL}/deleteProject`, { @@ -132,8 +149,13 @@ export const deleteProject = (project: any): Promise => { body }) .then((res) => res.json()) - .then((data) => { - return {_id: data._id, name: data.name, published:data.published, ...data.project}; + .then((data) => { + return { + _id: data._id, + name: data.name, + published: data.published, + ...data.project + }; }) .catch((err) => console.log(`Error deleting project ${err}`)); return deletedProject; diff --git a/app/src/helperFunctions/socket.ts b/app/src/helperFunctions/socket.ts index 978c6deaa..d1fb25021 100644 --- a/app/src/helperFunctions/socket.ts +++ b/app/src/helperFunctions/socket.ts @@ -1,10 +1,9 @@ import { io } from 'socket.io-client'; -import config from '../../../config'; - +// import config from '../../../config.js'; let socket = null; export const initializeSocket = () => { - socket = io(config.API_BASE_URL, { + socket = io('http://localhost:5656', { transports: ['websocket'], // will force new socket connection if re-joining to prevent double emits forceNew: true diff --git a/app/src/helperFunctions/zipFiles.ts b/app/src/helperFunctions/zipFiles.ts index 4903945b0..7b15eb9e4 100644 --- a/app/src/helperFunctions/zipFiles.ts +++ b/app/src/helperFunctions/zipFiles.ts @@ -1,5 +1,5 @@ import { saveAs } from 'file-saver'; -const JSZip = require('jszip'); +import JSZip from'jszip'; import { State } from '../interfaces/Interfaces'; //function to create a zip file for export in web app diff --git a/app/src/index.tsx b/app/src/index.tsx index 883a470d8..8210a8049 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -27,8 +27,10 @@ const client = new ApolloClient({ cache: new InMemoryCache() }); -const isDev = process.env.NODE_ENV === 'development'; -const { DEV_PORT, API_BASE_URL } = require('../../config.js'); +const isDev = import.meta.env.NODE_ENV === 'development'; +import serverConfig from './serverConfig.js'; + +const { DEV_PORT, API_BASE_URL } = serverConfig; let serverURL = API_BASE_URL; if (isDev) { diff --git a/app/src/public/styles/style.css b/app/src/public/styles/style.css index 1f83ceb0d..05878628e 100644 --- a/app/src/public/styles/style.css +++ b/app/src/public/styles/style.css @@ -1,15 +1,15 @@ +/* @import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Raleway&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0'); +@import url('https: //fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap'); */ + *, *::before, *::after { box-sizing: inherit; } -@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Raleway&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0'); -@import url('https: //fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap'); - html { box-sizing: border-box; /* height: 100vh; @@ -398,7 +398,7 @@ h1 { } #export-modal:hover { - background-color: #66c4eb; + background-color: #66c4eb; cursor: pointer; } /* ///CHANGED COLOR COLOR */ @@ -532,12 +532,12 @@ BOTTOM PANEL } .tab-content::-webkit-scrollbar { - width: .8rem; - height: .5rem; - } - + width: 0.8rem; + height: 0.5rem; +} + .tab-content::-webkit-scrollbar-thumb { - transition: .3s ease all; + transition: 0.3s ease all; border-color: transparent; background-color: #1e83714b; border-radius: 8px; @@ -545,12 +545,12 @@ BOTTOM PANEL } .tab-content::-webkit-scrollbar { - width: .8rem; - height: .5rem; - } - + width: 0.8rem; + height: 0.5rem; +} + .tab-content::-webkit-scrollbar-thumb { - transition: .3s ease all; + transition: 0.3s ease all; border-color: transparent; background-color: #99d7f2; border-radius: 8px; diff --git a/app/src/serverConfig.js b/app/src/serverConfig.js new file mode 100644 index 000000000..9158634a5 --- /dev/null +++ b/app/src/serverConfig.js @@ -0,0 +1,14 @@ +const isProduction = process.env.NODE_ENV === 'production'; +const serverConfig = { + DEV_PORT: 5656, + API_BASE_URL: isProduction + ? 'https://app.reactype.dev' + : 'http://localhost:5656', + // : 'http://localhost:8080', + API_BASE_URL2: isProduction + ? 'https://app.reactype.dev' + : 'http://localhost:8080' +}; +// module.exports = config; + +export default serverConfig; diff --git a/app/src/utils/createApplication.util.ts b/app/src/utils/createApplication.util.ts index 401fa7282..6aa7a03b0 100644 --- a/app/src/utils/createApplication.util.ts +++ b/app/src/utils/createApplication.util.ts @@ -182,7 +182,7 @@ export const createPackage = (path, appName, test) => { }; export const createWebpack = (path, appName) => { const filePath = `${path}/${appName}/webpack.config.js`; - const data = `var status = process.env.NODE_ENV; //taken from script so we don't have to flip mode when using development/production + const data = `var status = import.meta.env.NODE_ENV; //taken from script so we don't have to flip mode when using development/production var path = require('path'); module.exports = { entry: './src/index.tsx', diff --git a/config.js b/config.js index a7adc4227..463d1814b 100644 --- a/config.js +++ b/config.js @@ -10,3 +10,5 @@ const config = { : 'http://localhost:8080' }; module.exports = config; + +// export default config; diff --git a/index.html b/index.html new file mode 100644 index 000000000..eaa00caa8 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + Document + + + +
+ + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 05516e35c..794586cbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,7 +82,8 @@ "typescript-coverage-report": "^0.7.0", "uniqid": "^5.4.0", "use-debounce": "^8.0.4", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "vite-plugin-env-compatible": "^2.0.1" }, "devDependencies": { "@babel/core": "^7.23.6", @@ -103,6 +104,8 @@ "@types/jest": "^28.1.8", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", + "@vitejs/plugin-react": "^4.2.1", + "@vitejs/plugin-react-refresh": "^1.3.6", "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0", "apollo": "^2.34.0", "axios": "^1.6.2", @@ -145,6 +148,8 @@ "ts-jest": "^28.0.8", "typescript": "^4.9.5", "url-loader": "^4.1.1", + "vite": "^5.1.0", + "vite-plugin-svgr": "^4.2.0", "webpack": "^5.89.0", "webpack-bundle-analyzer": "^4.10.1", "webpack-cli": "^5.1.4", @@ -8627,7 +8632,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -8642,7 +8646,6 @@ "version": "7.23.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", - "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -9631,6 +9634,182 @@ "typescript": ">=2.7" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/linux-loong64": { "version": "0.14.54", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", @@ -9647,6 +9826,182 @@ "node": ">=12" } }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -13240,6 +13595,188 @@ } } }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", + "integrity": "sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.6.tgz", + "integrity": "sha512-T14aNLpqJ5wzKNf5jEDpv5zgyIqcpn1MlwCrUXLrwoADr2RkWA0vOWP4XxbO9aiO3dvMCQICZdKeDrFl7UMClw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.6.tgz", + "integrity": "sha512-CqNNAyhRkTbo8VVZ5R85X73H3R5NX9ONnKbXuHisGWC0qRbTTxnF1U4V9NafzJbgGM0sHZpdO83pLPzq8uOZFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.6.tgz", + "integrity": "sha512-zRDtdJuRvA1dc9Mp6BWYqAsU5oeLixdfUvkTHuiYOHwqYuQ4YgSmi6+/lPvSsqc/I0Omw3DdICx4Tfacdzmhog==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.6.tgz", + "integrity": "sha512-oNk8YXDDnNyG4qlNb6is1ojTOGL/tRhbbKeE/YuccItzerEZT68Z9gHrY3ROh7axDc974+zYAPxK5SH0j/G+QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.6.tgz", + "integrity": "sha512-Z3O60yxPtuCYobrtzjo0wlmvDdx2qZfeAWTyfOjEDqd08kthDKexLpV97KfAeUXPosENKd8uyJMRDfFMxcYkDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.6.tgz", + "integrity": "sha512-gpiG0qQJNdYEVad+1iAsGAbgAnZ8j07FapmnIAQgODKcOTjLEWM9sRb+MbQyVsYCnA0Im6M6QIq6ax7liws6eQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.6.tgz", + "integrity": "sha512-+uCOcvVmFUYvVDr27aiyun9WgZk0tXe7ThuzoUTAukZJOwS5MrGbmSlNOhx1j80GdpqbOty05XqSl5w4dQvcOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.6.tgz", + "integrity": "sha512-HUNqM32dGzfBKuaDUBqFB7tP6VMN74eLZ33Q9Y1TBqRDn+qDonkAUyKWwF9BR9unV7QUzffLnz9GrnKvMqC/fw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.6.tgz", + "integrity": "sha512-ch7M+9Tr5R4FK40FHQk8VnML0Szi2KRujUgHXd/HjuH9ifH72GUmw6lStZBo3c3GB82vHa0ZoUfjfcM7JiiMrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.6.tgz", + "integrity": "sha512-VD6qnR99dhmTQ1mJhIzXsRcTBvTjbfbGGwKAHcu+52cVl15AC/kplkhxzW/uT0Xl62Y/meBKDZvoJSJN+vTeGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.6.tgz", + "integrity": "sha512-J9AFDq/xiRI58eR2NIDfyVmTYGyIZmRcvcAoJ48oDld/NTR8wyiPUu2X/v1navJ+N/FGg68LEbX3Ejd6l8B7MQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.6.tgz", + "integrity": "sha512-jqzNLhNDvIZOrt69Ce4UjGRpXJBzhUBzawMwnaDAwyHriki3XollsewxWzOzz+4yOFDkuJHtTsZFwMxhYJWmLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@samverschueren/stream-to-observable": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", @@ -13346,6 +13883,287 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@svgr/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@svgr/core/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@svgr/core/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -14526,6 +15344,51 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/@vitejs/plugin-react": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", + "integrity": "sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.5", + "@babel/plugin-transform-react-jsx-self": "^7.23.3", + "@babel/plugin-transform-react-jsx-source": "^7.23.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/@vitejs/plugin-react-refresh": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-refresh/-/plugin-react-refresh-1.3.6.tgz", + "integrity": "sha512-iNR/UqhUOmFFxiezt0em9CgmiJBdWR+5jGxB2FihaoJfqGt76kiwaKoVOJVU5NYcDWMdN06LbyN2VIGIoYdsEA==", + "deprecated": "This package has been deprecated in favor of @vitejs/plugin-react", + "dev": true, + "dependencies": { + "@babel/core": "^7.14.8", + "@babel/plugin-transform-react-jsx-self": "^7.14.5", + "@babel/plugin-transform-react-jsx-source": "^7.14.5", + "@rollup/pluginutils": "^4.1.1", + "react-refresh": "^0.10.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@vitejs/plugin-react-refresh/node_modules/react-refresh": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.10.0.tgz", + "integrity": "sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -20012,8 +20875,7 @@ "node_modules/dotenv-expand": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", - "dev": true + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" }, "node_modules/dotenv-webpack": { "version": "8.0.1", @@ -21887,6 +22749,12 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -30049,9 +30917,9 @@ "peer": true }, "node_modules/postcss": { - "version": "8.4.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", - "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "funding": [ { "type": "opencollective", @@ -30742,7 +31610,6 @@ "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -31483,6 +32350,38 @@ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" }, + "node_modules/rollup": { + "version": "4.9.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", + "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.9.6", + "@rollup/rollup-android-arm64": "4.9.6", + "@rollup/rollup-darwin-arm64": "4.9.6", + "@rollup/rollup-darwin-x64": "4.9.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.9.6", + "@rollup/rollup-linux-arm64-gnu": "4.9.6", + "@rollup/rollup-linux-arm64-musl": "4.9.6", + "@rollup/rollup-linux-riscv64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-gnu": "4.9.6", + "@rollup/rollup-linux-x64-musl": "4.9.6", + "@rollup/rollup-win32-arm64-msvc": "4.9.6", + "@rollup/rollup-win32-ia32-msvc": "4.9.6", + "@rollup/rollup-win32-x64-msvc": "4.9.6", + "fsevents": "~2.3.2" + } + }, "node_modules/rst-selector-parser": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", @@ -32785,6 +33684,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -34210,6 +35115,168 @@ "dev": true, "optional": true }, + "node_modules/vite": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.0.tgz", + "integrity": "sha512-STmSFzhY4ljuhz14bg9LkMTk3d98IO6DIArnTY6MeBwiD1Za2StcQtz7fzOUnRCqrHSD5+OS2reg4HOz1eoLnw==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-env-compatible": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vite-plugin-env-compatible/-/vite-plugin-env-compatible-2.0.1.tgz", + "integrity": "sha512-DRrOZTg/W44ojVQQfGSMPEgYQGzp5TeIpt9cpaK35hTOC/b2D7Ffl8/RIgK8vQ0mlnDIUgETcA173bnMEkyzdw==", + "dependencies": { + "dotenv": "8.2.0", + "dotenv-expand": "5.1.0" + } + }, + "node_modules/vite-plugin-env-compatible/node_modules/dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/vite-plugin-svgr": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.2.0.tgz", + "integrity": "sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.5", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0" + }, + "peerDependencies": { + "vite": "^2.6.0 || 3 || 4 || 5" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, "node_modules/vlq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", diff --git a/package.json b/package.json index dbd568fa1..74b75865c 100644 --- a/package.json +++ b/package.json @@ -82,9 +82,12 @@ "Nam Ha" ], "scripts": { + "start": "vite", + "build": "tsc && vite build", + "serve": "vite preview", "postinstall": "set ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true electron-builder install-app-deps", "dev-server": "cross-env NODE_ENV=development webpack-dev-server --config ./webpack.development.js", - "dev": "concurrently -k \"cross-env NODE_ENV=development webpack-dev-server --config ./webpack.development.js\" \"cross-env NODE_ENV=development nodemon server/server.ts --open\"", + "dev": "cross-env NODE_ENV=development nodemon server/server.ts --open", "electron-dev": "cross-env NODE_ENV=development electron .", "prod-build": "cross-env NODE_ENV=production npx webpack --mode=production --config ./webpack.production.js", "prod": "npm run prod-build && electron . --no-sandbox", @@ -96,7 +99,7 @@ "dist-all": "npm run prod-build && electron-builder --mac --linux --windows", "test": "NODE_ENV=test jest --verbose --coverage", "server": "cross-env NODE_ENV=development nodemon server/server.ts", - "start": "ts-node server/server.ts", + "start-server": "ts-node server/server.ts", "ts-coverage": "typescript-coverage-report" }, "repository": { @@ -187,7 +190,8 @@ "typescript-coverage-report": "^0.7.0", "uniqid": "^5.4.0", "use-debounce": "^8.0.4", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "vite-plugin-env-compatible": "^2.0.1" }, "devDependencies": { "@babel/core": "^7.23.6", @@ -208,6 +212,8 @@ "@types/jest": "^28.1.8", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", + "@vitejs/plugin-react": "^4.2.1", + "@vitejs/plugin-react-refresh": "^1.3.6", "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0", "apollo": "^2.34.0", "axios": "^1.6.2", @@ -250,6 +256,8 @@ "ts-jest": "^28.0.8", "typescript": "^4.9.5", "url-loader": "^4.1.1", + "vite": "^5.1.0", + "vite-plugin-svgr": "^4.2.0", "webpack": "^5.89.0", "webpack-bundle-analyzer": "^4.10.1", "webpack-cli": "^5.1.4", diff --git a/playwright.config.ts b/playwright.config.ts index 1eb6135e3..e87e2e4db 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -14,11 +14,11 @@ // /* Run tests in files in parallel */ // fullyParallel: true, // /* Fail the build on CI if you accidentally left test.only in the source code. */ -// forbidOnly: !!process.env.CI, +// forbidOnly: !!import.meta.env.CI, // /* Retry on CI only */ -// retries: process.env.CI ? 2 : 0, +// retries: import.meta.env.CI ? 2 : 0, // /* Opt out of parallel tests on CI. */ -// workers: process.env.CI ? 1 : undefined, +// workers: import.meta.env.CI ? 1 : undefined, // /* Reporter to use. See https://playwright.dev/docs/test-reporters */ // reporter: 'html', // /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -72,6 +72,6 @@ // // webServer: { // // command: 'npm run start', // // url: 'http://127.0.0.1:3000', -// // reuseExistingServer: !process.env.CI, +// // reuseExistingServer: !import.meta.env.CI, // // }, // }); diff --git a/src/custom-aws-exports.js b/src/custom-aws-exports.js index 363db6d89..dfd06d4ba 100644 --- a/src/custom-aws-exports.js +++ b/src/custom-aws-exports.js @@ -1,24 +1,48 @@ /* eslint-disable */ const awsmobile = { - "aws_project_region": process.env.REACT_APP_AWS_PROJECT_REGION, - "aws_cognito_identity_pool_id": process.env.REACT_APP_AWS_COGNITO_IDENTITY_POOL_ID, - "aws_cognito_region": process.env.REACT_APP_AWS_COGNITO_REGION, - "aws_user_pools_id": process.env.REACT_APP_AWS_USER_POOLS_ID, - "aws_user_pools_web_client_id": process.env.REACT_APP_AWS_USER_POOLS_WEB_CLIENT_ID, - "oauth": {}, - "aws_cognito_username_attributes": process.env.REACT_APP_AWS_COGNITO_USERNAME_ATTRIBUTES ? process.env.REACT_APP_AWS_COGNITO_USERNAME_ATTRIBUTES.split(',') : [], - "aws_cognito_social_providers": process.env.REACT_APP_AWS_COGNITO_SOCIAL_PROVIDERS ? process.env.REACT_APP_AWS_COGNITO_SOCIAL_PROVIDERS.split(',') : [], - "aws_cognito_signup_attributes": process.env.REACT_APP_AWS_COGNITO_SIGNUP_ATTRIBUTES ? process.env.REACT_APP_AWS_COGNITO_SIGNUP_ATTRIBUTES.split(',') : [], - "aws_cognito_mfa_configuration": process.env.REACT_APP_AWS_COGNITO_MFA_CONFIGURATION, - "aws_cognito_mfa_types": process.env.REACT_APP_AWS_COGNITO_MFA_TYPES ? process.env.REACT_APP_AWS_COGNITO_MFA_TYPES.split(',') : [], - "aws_cognito_password_protection_settings": { - "passwordPolicyMinLength": process.env.REACT_APP_AWS_COGNITO_PASSWORD_PROTECTION_SETTINGS_PASSWORDPOLICYMINLENGTH, - "passwordPolicyCharacters": process.env.REACT_APP_AWS_COGNITO_PASSWORD_PROTECTION_SETTINGS_PASSWORDPOLICYCHARACTERS ? process.env.REACT_APP_AWS_COGNITO_PASSWORD_PROTECTION_SETTINGS_PASSWORDPOLICYCHARACTERS.split(',') : [] - }, - "aws_cognito_verification_mechanisms": process.env.REACT_APP_AWS_COGNITO_VERIFICATION_MECHANISMS ? process.env.REACT_APP_AWS_COGNITO_VERIFICATION_MECHANISMS.split(',') : [], - "aws_user_files_s3_bucket": process.env.REACT_APP_AWS_USER_FILES_S3_BUCKET, - "aws_user_files_s3_bucket_region": process.env.REACT_APP_AWS_USER_FILES_S3_BUCKET_REGION + aws_project_region: import.meta.env.REACT_APP_AWS_PROJECT_REGION, + aws_cognito_identity_pool_id: import.meta.env + .REACT_APP_AWS_COGNITO_IDENTITY_POOL_ID, + aws_cognito_region: import.meta.env.REACT_APP_AWS_COGNITO_REGION, + aws_user_pools_id: import.meta.env.REACT_APP_AWS_USER_POOLS_ID, + aws_user_pools_web_client_id: import.meta.env + .REACT_APP_AWS_USER_POOLS_WEB_CLIENT_ID, + oauth: {}, + aws_cognito_username_attributes: import.meta.env + .REACT_APP_AWS_COGNITO_USERNAME_ATTRIBUTES + ? import.meta.env.REACT_APP_AWS_COGNITO_USERNAME_ATTRIBUTES.split(',') + : [], + aws_cognito_social_providers: import.meta.env + .REACT_APP_AWS_COGNITO_SOCIAL_PROVIDERS + ? import.meta.env.REACT_APP_AWS_COGNITO_SOCIAL_PROVIDERS.split(',') + : [], + aws_cognito_signup_attributes: import.meta.env + .REACT_APP_AWS_COGNITO_SIGNUP_ATTRIBUTES + ? import.meta.env.REACT_APP_AWS_COGNITO_SIGNUP_ATTRIBUTES.split(',') + : [], + aws_cognito_mfa_configuration: import.meta.env + .REACT_APP_AWS_COGNITO_MFA_CONFIGURATION, + aws_cognito_mfa_types: import.meta.env.REACT_APP_AWS_COGNITO_MFA_TYPES + ? import.meta.env.REACT_APP_AWS_COGNITO_MFA_TYPES.split(',') + : [], + aws_cognito_password_protection_settings: { + passwordPolicyMinLength: import.meta.env + .REACT_APP_AWS_COGNITO_PASSWORD_PROTECTION_SETTINGS_PASSWORDPOLICYMINLENGTH, + passwordPolicyCharacters: import.meta.env + .REACT_APP_AWS_COGNITO_PASSWORD_PROTECTION_SETTINGS_PASSWORDPOLICYCHARACTERS + ? import.meta.env.REACT_APP_AWS_COGNITO_PASSWORD_PROTECTION_SETTINGS_PASSWORDPOLICYCHARACTERS.split( + ',' + ) + : [] + }, + aws_cognito_verification_mechanisms: import.meta.env + .REACT_APP_AWS_COGNITO_VERIFICATION_MECHANISMS + ? import.meta.env.REACT_APP_AWS_COGNITO_VERIFICATION_MECHANISMS.split(',') + : [], + aws_user_files_s3_bucket: import.meta.env.REACT_APP_AWS_USER_FILES_S3_BUCKET, + aws_user_files_s3_bucket_region: import.meta.env + .REACT_APP_AWS_USER_FILES_S3_BUCKET_REGION }; export default awsmobile; diff --git a/tsconfig.json b/tsconfig.json index f93324f44..a9416b059 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,27 @@ { "compilerOptions": { + "target": "ESNext", + "lib": ["dom", "dom.iterable", "esnext"], + "types": ["vite/client", "vite-plugin-svgr/client"], + "allowJs": false, + "skipLibCheck": false, + "esModuleInterop": false, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", "outDir": "./app/dist/", "sourceMap": true, "noImplicitAny": false, "noImplicitThis": false, "strictNullChecks": false, - "module": "commonjs", - "target": "es6", - "jsx": "react", - "strict": true, - "lib": ["es6", "dom", "es2017"], "allowSyntheticDefaultImports": true, "strictFunctionTypes": false, - "esModuleInterop": true, "typeRoots": [ "node_modules/@types", "node_modules/@types/node", diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 000000000..e26eb78d2 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import svgr from 'vite-plugin-svgr'; + +// https://vitejs.dev/config/ +export default defineConfig({ + // This changes the out put dir from dist to build + // comment this out if that isn't relevant for your project + build: { + outDir: 'build' + }, + assetsInclude: ['**/*.PNG'], + server: { port: 8080 }, + plugins: [ + react(), + svgr({ + svgrOptions: { + icon: true + // ...svgr options (https://react-svgr.com/docs/options/) + } + }) + ] +}); diff --git a/webpack.config.js b/webpack.config.js index 92af87015..843d00f17 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -29,7 +29,7 @@ module.exports = { plugins: [ new Dotenv(), new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify('development') + 'import.meta.env.NODE_ENV': JSON.stringify('development') }) // new BundleAnalyzerPlugin(), ], diff --git a/webpack.development.js b/webpack.development.js index 15c74e20a..4b6bd645c 100644 --- a/webpack.development.js +++ b/webpack.development.js @@ -1,14 +1,14 @@ -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const merge = require('webpack-merge'); -const base = require('./webpack.config'); -const path = require('path'); -const nonce = require('./app/src/utils/createNonce.ts')(); -const { DEV_PORT } = require('./config'); +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; +import merge from 'webpack-merge'; +import baseConfig from './webpack.config.js'; +import path from 'path'; +import createNonce from './app/src/utils/createNonce.ts'; +import { DEV_PORT } from './config.js'; // merges webpack.config.js with development specific configs -module.exports = merge(base, { - // sets process.env.NODE_ENV to 'development; +const config = merge(baseConfig, { + // sets import.meta.env.NODE_ENV to 'development; mode: 'development', devtool: 'inline-source-map', devServer: { @@ -18,12 +18,6 @@ module.exports = merge(base, { port: '8080', hot: true, // Hot-reload this server if changes are detected compress: true, // Compress (gzip) files that are served - // contentBase: path.resolve(__dirname, 'app/dist') - // Where we serve the local dev server's files from, not valid devserver - // watchContentBase: true, // Watch the content base for changes , also not valid for dev server - // watchOptions: { - // ignored: /node_modules/, - // }, //watchOptions not valid dev server config proxy: { '/demoRender': { target: `http://localhost:${DEV_PORT}/` @@ -32,10 +26,10 @@ module.exports = merge(base, { target: `http://localhost:${DEV_PORT}/` }, '/auth/**': { - target: `http://localhost:5656/` + target: 'http://localhost:5656/' }, '/**': { - target: `http://localhost:5656/` + target: 'http://localhost:5656/' } } }, @@ -47,7 +41,9 @@ module.exports = merge(base, { new HtmlWebpackPlugin({ template: path.resolve(__dirname, 'app/src/public/index.ejs'), filename: 'index.html', - nonce: nonce + nonce: createNonce() }) ] }); + +export default config; diff --git a/webpack.production.js b/webpack.production.js index 0deffe7e1..fa23ecbe5 100644 --- a/webpack.production.js +++ b/webpack.production.js @@ -7,7 +7,7 @@ const nonce = require('./app/src/utils/createNonce.ts')(); // merges webpack.config.js with production specific configs module.exports = merge(base, { - // sets process.env.NODE_ENV to 'production' + // sets import.meta.env.NODE_ENV to 'production' mode: 'production', plugins: [ // miniCssExtractPlugin is included here because it's used as a loader in wepack.config.js From f8167bddce770a07244a3da6e949fedffb87fac5 Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Thu, 8 Feb 2024 14:02:50 -0500 Subject: [PATCH 014/221] Update with chatroom --- app/src/components/bottom/chatRoom.tsx | 71 ++++++++++++---------- app/src/components/left/RoomsContainer.tsx | 12 +++- app/src/redux/reducers/slice/roomSlice.ts | 13 +++- 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/app/src/components/bottom/chatRoom.tsx b/app/src/components/bottom/chatRoom.tsx index c792272ce..95a36bdc4 100644 --- a/app/src/components/bottom/chatRoom.tsx +++ b/app/src/components/bottom/chatRoom.tsx @@ -1,41 +1,33 @@ -import React, { useState, useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import React, { useEffect } from 'react'; +import { useSelector } from 'react-redux'; import { RootState } from '../../redux/store'; -import Box from '@mui/material/Box'; -import { - initializeSocket, - getSocket, - emitEvent, - disconnectSocket -} from '../../helperFunctions/socket'; +import { emitEvent } from '../../helperFunctions/socket'; const Chatroom = (props): JSX.Element => { const collaborationRoom = useSelector((store: RootState) => store.roomSlice); - // const newMessage = useSelector( - // (store: RootState) => store.roomSlice.newMessage - // ); - const socket = getSocket(); + + const newMessage = useSelector( + (store: RootState) => store.roomSlice.newMessage + ); const nickName = collaborationRoom.userName; const roomCode = collaborationRoom.roomCode; - // const newMessage = collaborationRoom.newMessage; const wrapperStyles = { border: `2px solid #f2fbf8`, borderRadius: '8px', width: '70%', - height: '90%', + height: '100%', display: 'column', padding: '20px', - // justifyContent: 'center', backgroundColor: '#42464C', overflow: 'auto' - // scrollTop: 'scrollHeight' }; const inputContainerStyles = { width: '100%', - padding: '10px', + paddingLeft: '30px', + paddingTop: '10px', display: 'flex', justifyContent: 'center' }; @@ -52,7 +44,7 @@ const Chatroom = (props): JSX.Element => { const buttonStyles = { padding: '10px', marginLeft: '10px', - backgroundColor: '#4CAF50', + backgroundColor: '#0070BA', color: 'white', border: 'none', borderRadius: '5px', @@ -64,31 +56,44 @@ const Chatroom = (props): JSX.Element => { const messageInput = document.getElementById( 'message-input' ) as HTMLInputElement; - const message = messageInput.value; - emitEvent('send-chat-message', roomCode, { message, nickName }); - messageInput.value = ''; + const message = messageInput.value.trim(); + if (message !== '') { + emitEvent('send-chat-message', roomCode, { message, nickName }); + messageInput.value = ''; + } + }; + + const handleMessageStyle = (messageNickName: string) => { + return { + color: messageNickName === nickName ? '#66C4EB' : 'white' + }; }; - socket.on('new chat message', (remoteData) => { - const messageContainer = document.getElementById('message-container'); - const messageElement = document.createElement('div'); - messageElement.innerText = - nickName === remoteData.nickName - ? `You: ${remoteData.message}` - : `${remoteData.nickName}: ${remoteData.message}`; - messageContainer.append(messageElement); - }); + useEffect(() => { + if (newMessage.nickName !== '' && newMessage.message !== '') { + const messageContainer = document.getElementById('message-container'); + const messageElement = document.createElement('div'); + messageElement.innerText = + nickName === newMessage.nickName + ? `You: ${newMessage.message}` + : `${newMessage.nickName}: ${newMessage.message}`; + messageElement.style.color = handleMessageStyle( + newMessage.nickName + ).color; + messageContainer.append(messageElement); + } + }, [newMessage]); return (
-
+

Current room: {roomCode}

Your Nickname: {nickName}

-
+
{ const userJoined = useSelector( (store: RootState) => store.roomSlice.userJoined ); + const newMessage = useSelector( + (store: RootState) => store.roomSlice.newMessage + ); // for websockets - initialize socket connection passing in roomCode function initSocketConnection(roomCode: string) { @@ -106,6 +110,12 @@ const RoomsContainer = () => { dispatch(setUserList(newUserList)); }); + socket.on('new chat message', (messageData) => { + if (JSON.stringify(messageData) !== JSON.stringify(newMessage)) { + dispatch(setNewMessage(messageData)); + } + }); + // dispatch add child to local state when element has been added by another user socket.on('child data from server', (childData: object) => { // console.log('child data received by users', childData); diff --git a/app/src/redux/reducers/slice/roomSlice.ts b/app/src/redux/reducers/slice/roomSlice.ts index 5b0413c0e..8a8a8d9cc 100644 --- a/app/src/redux/reducers/slice/roomSlice.ts +++ b/app/src/redux/reducers/slice/roomSlice.ts @@ -4,6 +4,7 @@ import { createSlice } from '@reduxjs/toolkit'; const initialState = { roomCode: '', userName: '', + newMessage: { nickName: '', message: '' }, userList: [], userJoined: false }; @@ -24,12 +25,20 @@ const roomSlice = createSlice({ }, setUserJoined: (state, action) => { state.userJoined = action.payload; + }, + setNewMessage: (state, action) => { + state.newMessage = action.payload; } } }); // Exports the action creator function to be used with useDispatch -export const { setRoomCode, setUserName, setUserList, setUserJoined } = - roomSlice.actions; +export const { + setRoomCode, + setUserName, + setUserList, + setUserJoined, + setNewMessage +} = roomSlice.actions; // Exports so we can combine in rootReducer export default roomSlice.reducer; From 4f36823ddaba6c6794d4bcd03aa27fa6cd2b55de Mon Sep 17 00:00:00 2001 From: cyburns <72774499+cyburns@users.noreply.github.com> Date: Thu, 8 Feb 2024 16:04:47 -0500 Subject: [PATCH 015/221] second w vite working --- app/src/components/left/Sidebar.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/components/left/Sidebar.tsx b/app/src/components/left/Sidebar.tsx index 6674b790d..37cea676d 100644 --- a/app/src/components/left/Sidebar.tsx +++ b/app/src/components/left/Sidebar.tsx @@ -101,7 +101,6 @@ const Sidebar: React.FC = ({ icon={} value={4} /> - ); }; From b174c03188a39db9605515c785d0a27973c05636 Mon Sep 17 00:00:00 2001 From: cyburns <72774499+cyburns@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:04:49 -0500 Subject: [PATCH 016/221] second try after pull --- app/src/components/bottom/BottomPanel.tsx | 6 ------ app/src/components/left/RoomsContainer.tsx | 3 --- 2 files changed, 9 deletions(-) diff --git a/app/src/components/bottom/BottomPanel.tsx b/app/src/components/bottom/BottomPanel.tsx index 438a014fd..526196e48 100644 --- a/app/src/components/bottom/BottomPanel.tsx +++ b/app/src/components/bottom/BottomPanel.tsx @@ -15,16 +15,10 @@ const BottomPanel = (props): JSX.Element => { document.addEventListener('mousemove', mouseMoveHandler); document.addEventListener('mouseup', mouseUpHandler); -<<<<<<< HEAD - window.addEventListener('message', handleIframeMessage); - }; - -======= window.addEventListener('message', handleIframeMessage); //listens for messages from the iframe when the mouse is over it }; //Interpret the messages from the iframe ->>>>>>> d96f85e661980df9abf2838c313aebd3698e2464 const handleIframeMessage = (e) => { if (e.data === 'iframeMouseUp') { mouseUpHandler(); diff --git a/app/src/components/left/RoomsContainer.tsx b/app/src/components/left/RoomsContainer.tsx index b53125e86..8aeddc8a0 100644 --- a/app/src/components/left/RoomsContainer.tsx +++ b/app/src/components/left/RoomsContainer.tsx @@ -110,15 +110,12 @@ const RoomsContainer = () => { } ); -<<<<<<< HEAD -======= // dispatch clear canvas action to local state when the host of the room has clear canvas socket.on('clear canvas from server', () => { store.dispatch(resetAllState()); }); // dispatch all updates to local state when another user has saved from Bottom Panel ->>>>>>> d96f85e661980df9abf2838c313aebd3698e2464 socket.on('update data from server', (updateData: BottomPanelObj) => { store.dispatch( updateStateUsed({ From 17d20b56811d37181f32ebee08a624c8b6501014 Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Thu, 8 Feb 2024 17:44:20 -0500 Subject: [PATCH 017/221] clean up chatroom function --- app/src/components/bottom/chatRoom.tsx | 15 +++++++++++++++ app/src/components/left/RoomsContainer.tsx | 16 +++++++++++++--- app/src/redux/reducers/slice/roomSlice.ts | 9 +++++++-- server/server.ts | 14 +++++++++----- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/app/src/components/bottom/chatRoom.tsx b/app/src/components/bottom/chatRoom.tsx index 95a36bdc4..8739a771f 100644 --- a/app/src/components/bottom/chatRoom.tsx +++ b/app/src/components/bottom/chatRoom.tsx @@ -10,6 +10,10 @@ const Chatroom = (props): JSX.Element => { (store: RootState) => store.roomSlice.newMessage ); + const userChange = useSelector( + (store: RootState) => store.roomSlice.userChange + ); + const nickName = collaborationRoom.userName; const roomCode = collaborationRoom.roomCode; @@ -84,6 +88,17 @@ const Chatroom = (props): JSX.Element => { } }, [newMessage]); + useEffect(() => { + const messageContainer = document.getElementById('message-container'); + const messageElement = document.createElement('div'); + messageElement.innerText = + userChange.status === 'JOIN' + ? `${userChange.nickName} joined the chatroom` + : `${userChange.nickName} left the chatroom`; + messageElement.style.color = 'yellow'; + messageContainer.append(messageElement); + }, [userChange]); + return (
{ (store: RootState) => store.roomSlice.newMessage ); + const userChange = useSelector( + (store: RootState) => store.roomSlice.userChange + ); + // for websockets - initialize socket connection passing in roomCode function initSocketConnection(roomCode: string) { // helper function to create socket connection @@ -105,9 +110,14 @@ const RoomsContainer = () => { }); // update user list when there's a change: new join or leave the room - socket.on('updateUserList', (newUserList) => { + socket.on('updateUserList', (messageData) => { //console.log('user list received from server'); - dispatch(setUserList(newUserList)); + dispatch(setUserList(messageData.userList)); + if ( + JSON.stringify(messageData.activity) !== JSON.stringify(userChange) + ) { + dispatch(setUserChange(messageData.activity)); + } }); socket.on('new chat message', (messageData) => { diff --git a/app/src/redux/reducers/slice/roomSlice.ts b/app/src/redux/reducers/slice/roomSlice.ts index 8a8a8d9cc..27aa8a756 100644 --- a/app/src/redux/reducers/slice/roomSlice.ts +++ b/app/src/redux/reducers/slice/roomSlice.ts @@ -6,7 +6,8 @@ const initialState = { userName: '', newMessage: { nickName: '', message: '' }, userList: [], - userJoined: false + userJoined: false, + userChange: { nickName: '', status: '' } }; // Creates new slice with the name , initial state, and reducer function @@ -28,6 +29,9 @@ const roomSlice = createSlice({ }, setNewMessage: (state, action) => { state.newMessage = action.payload; + }, + setUserChange: (state, action) => { + state.userChange = action.payload; } } }); @@ -38,7 +42,8 @@ export const { setUserName, setUserList, setUserJoined, - setNewMessage + setNewMessage, + setUserChange } = roomSlice.actions; // Exports so we can combine in rootReducer export default roomSlice.reducer; diff --git a/server/server.ts b/server/server.ts index 9e3c7527f..8828cb784 100644 --- a/server/server.ts +++ b/server/server.ts @@ -132,7 +132,10 @@ io.on('connection', (client) => { //send the message to all clients in room but the sender io.to(roomCode).emit( 'updateUserList', - Object.values(roomLists[roomCode]) // send updated userList to all users in room + { + userList: Object.values(roomLists[roomCode]), + activity: { nickName: userName, status: 'JOIN' } + } // send updated userList to all users in room ); } } catch (error) { @@ -153,16 +156,17 @@ io.on('connection', (client) => { //disconnecting functionality client.on('disconnecting', () => { const roomCode = Array.from(client.rooms)[1]; //grabbing current room client was in when disconnecting + const nickName = roomLists[roomCode][client.id]; delete roomLists[roomCode][client.id]; //if room empty, delete room from room list if (!Object.keys(roomLists[roomCode]).length) { delete roomLists[roomCode]; } else { //else emit updated user list - io.to(roomCode).emit( - 'updateUserList', - Object.values(roomLists[roomCode]) - ); + io.to(roomCode).emit('updateUserList', { + userList: Object.values(roomLists[roomCode]), + activity: { nickName, status: 'LEAVE' } + }); } }); From d452a077470aac25a99bf63dfa0c2ccd30d1789b Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Thu, 8 Feb 2024 17:59:51 -0500 Subject: [PATCH 018/221] chatroom function clean up and improvement --- app/src/components/bottom/BottomTabs.tsx | 3 +-- app/src/components/bottom/chatRoom.tsx | 14 ++++++-------- app/src/redux/reducers/slice/roomSlice.ts | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/app/src/components/bottom/BottomTabs.tsx b/app/src/components/bottom/BottomTabs.tsx index 1a5ec60d5..ead40ed78 100644 --- a/app/src/components/bottom/BottomTabs.tsx +++ b/app/src/components/bottom/BottomTabs.tsx @@ -31,7 +31,6 @@ const BottomTabs = (props): JSX.Element => { const state = useSelector((store: RootState) => store.appState); const contextParam = useSelector((store: RootState) => store.contextSlice); const collaborationRoom = useSelector((store: RootState) => store.roomSlice); - // {roomCode: '', userName: '', userList: Array(0), userJoined: false} const [tab, setTab] = useState(0); const classes = useStyles(); @@ -60,7 +59,7 @@ const BottomTabs = (props): JSX.Element => { zIndex: 1, borderTop: '2px solid grey' }} - onMouseOver={() => { + onClick={() => { props.setBottomShow(true); }} > diff --git a/app/src/components/bottom/chatRoom.tsx b/app/src/components/bottom/chatRoom.tsx index 8739a771f..2c1bb39a7 100644 --- a/app/src/components/bottom/chatRoom.tsx +++ b/app/src/components/bottom/chatRoom.tsx @@ -4,7 +4,8 @@ import { RootState } from '../../redux/store'; import { emitEvent } from '../../helperFunctions/socket'; const Chatroom = (props): JSX.Element => { - const collaborationRoom = useSelector((store: RootState) => store.roomSlice); + const nickName = useSelector((store: RootState) => store.roomSlice.userName); + const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); const newMessage = useSelector( (store: RootState) => store.roomSlice.newMessage @@ -14,9 +15,6 @@ const Chatroom = (props): JSX.Element => { (store: RootState) => store.roomSlice.userChange ); - const nickName = collaborationRoom.userName; - const roomCode = collaborationRoom.roomCode; - const wrapperStyles = { border: `2px solid #f2fbf8`, borderRadius: '8px', @@ -93,8 +91,8 @@ const Chatroom = (props): JSX.Element => { const messageElement = document.createElement('div'); messageElement.innerText = userChange.status === 'JOIN' - ? `${userChange.nickName} joined the chatroom` - : `${userChange.nickName} left the chatroom`; + ? `${userChange.nickName} joined the chat room` + : `${userChange.nickName} left the chat room`; messageElement.style.color = 'yellow'; messageContainer.append(messageElement); }, [userChange]); @@ -106,10 +104,10 @@ const Chatroom = (props): JSX.Element => { >

Current room: {roomCode}

-

Your Nickname: {nickName}

+

Your nickname: {nickName}

-
+
Date: Thu, 8 Feb 2024 18:30:49 -0500 Subject: [PATCH 019/221] Bug fix in member change message display --- app/src/components/bottom/chatRoom.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/components/bottom/chatRoom.tsx b/app/src/components/bottom/chatRoom.tsx index 2c1bb39a7..13193589b 100644 --- a/app/src/components/bottom/chatRoom.tsx +++ b/app/src/components/bottom/chatRoom.tsx @@ -87,14 +87,16 @@ const Chatroom = (props): JSX.Element => { }, [newMessage]); useEffect(() => { - const messageContainer = document.getElementById('message-container'); - const messageElement = document.createElement('div'); - messageElement.innerText = - userChange.status === 'JOIN' - ? `${userChange.nickName} joined the chat room` - : `${userChange.nickName} left the chat room`; - messageElement.style.color = 'yellow'; - messageContainer.append(messageElement); + if (userChange.nickName !== '' && userChange.status !== '') { + const messageContainer = document.getElementById('message-container'); + const messageElement = document.createElement('div'); + messageElement.innerText = + userChange.status === 'JOIN' + ? `${userChange.nickName} joined the chat room` + : `${userChange.nickName} left the chat room`; + messageElement.style.color = 'yellow'; + messageContainer.append(messageElement); + } }, [userChange]); return ( From 34f4604de10a52c288cbe33c165558fb2373d861 Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Thu, 8 Feb 2024 20:41:54 -0500 Subject: [PATCH 020/221] bug fix that messages missing when leave the tab --- app/src/components/bottom/chatRoom.tsx | 77 ++++++++++------------ app/src/components/left/RoomsContainer.tsx | 23 ++----- app/src/redux/reducers/slice/roomSlice.ts | 13 ++-- server/server.ts | 20 ++++-- 4 files changed, 61 insertions(+), 72 deletions(-) diff --git a/app/src/components/bottom/chatRoom.tsx b/app/src/components/bottom/chatRoom.tsx index 13193589b..ee7e2d30b 100644 --- a/app/src/components/bottom/chatRoom.tsx +++ b/app/src/components/bottom/chatRoom.tsx @@ -1,19 +1,13 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { useSelector } from 'react-redux'; import { RootState } from '../../redux/store'; import { emitEvent } from '../../helperFunctions/socket'; const Chatroom = (props): JSX.Element => { - const nickName = useSelector((store: RootState) => store.roomSlice.userName); + const userName = useSelector((store: RootState) => store.roomSlice.userName); const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - const newMessage = useSelector( - (store: RootState) => store.roomSlice.newMessage - ); - - const userChange = useSelector( - (store: RootState) => store.roomSlice.userChange - ); + const messages = useSelector((store: RootState) => store.roomSlice.messages); const wrapperStyles = { border: `2px solid #f2fbf8`, @@ -60,44 +54,39 @@ const Chatroom = (props): JSX.Element => { ) as HTMLInputElement; const message = messageInput.value.trim(); if (message !== '') { - emitEvent('send-chat-message', roomCode, { message, nickName }); + emitEvent('send-chat-message', roomCode, { userName, message }); messageInput.value = ''; } }; - const handleMessageStyle = (messageNickName: string) => { - return { - color: messageNickName === nickName ? '#66C4EB' : 'white' - }; - }; - - useEffect(() => { - if (newMessage.nickName !== '' && newMessage.message !== '') { - const messageContainer = document.getElementById('message-container'); - const messageElement = document.createElement('div'); - messageElement.innerText = - nickName === newMessage.nickName - ? `You: ${newMessage.message}` - : `${newMessage.nickName}: ${newMessage.message}`; - messageElement.style.color = handleMessageStyle( - newMessage.nickName - ).color; - messageContainer.append(messageElement); + const handleMessageContainerStyle = (message: object) => { + if (message['type'] === 'activity') { + return { color: 'yellow' }; + } else { + if (message['userName'] === userName) return { color: '#66C4EB' }; + return { color: 'white' }; } - }, [newMessage]); + }; - useEffect(() => { - if (userChange.nickName !== '' && userChange.status !== '') { - const messageContainer = document.getElementById('message-container'); - const messageElement = document.createElement('div'); - messageElement.innerText = - userChange.status === 'JOIN' - ? `${userChange.nickName} joined the chat room` - : `${userChange.nickName} left the chat room`; - messageElement.style.color = 'yellow'; - messageContainer.append(messageElement); - } - }, [userChange]); + const renderMessages = () => { + return messages.map((message, index) => { + if (message.type === 'activity') { + return ( +
+ {message.message} +
+ ); + } else if (message.type === 'chat') { + return ( +
+ {message.userName === userName ? 'You' : message.userName}:{' '} + {message.message} +
+ ); + } + return null; + }); + }; return (
{ >

Current room: {roomCode}

-

Your nickname: {nickName}

+

Your nickname: {userName}

-
+
+ {renderMessages()} +
{ const userJoined = useSelector( (store: RootState) => store.roomSlice.userJoined ); - const newMessage = useSelector( - (store: RootState) => store.roomSlice.newMessage - ); - - const userChange = useSelector( - (store: RootState) => store.roomSlice.userChange - ); + const messages = useSelector((store: RootState) => store.roomSlice.messages); // for websockets - initialize socket connection passing in roomCode function initSocketConnection(roomCode: string) { @@ -113,16 +106,14 @@ const RoomsContainer = () => { socket.on('updateUserList', (messageData) => { //console.log('user list received from server'); dispatch(setUserList(messageData.userList)); - if ( - JSON.stringify(messageData.activity) !== JSON.stringify(userChange) - ) { - dispatch(setUserChange(messageData.activity)); - } }); socket.on('new chat message', (messageData) => { - if (JSON.stringify(messageData) !== JSON.stringify(newMessage)) { - dispatch(setNewMessage(messageData)); + if ( + messages.length === 0 || + JSON.stringify(messageData) !== JSON.stringify(messages[-1]) + ) { + dispatch(setMessages(messageData)); } }); diff --git a/app/src/redux/reducers/slice/roomSlice.ts b/app/src/redux/reducers/slice/roomSlice.ts index f03d0acac..f3039827d 100644 --- a/app/src/redux/reducers/slice/roomSlice.ts +++ b/app/src/redux/reducers/slice/roomSlice.ts @@ -6,8 +6,7 @@ const initialState = { userName: '', userList: [], userJoined: false, - newMessage: { nickName: '', message: '' }, - userChange: { nickName: '', status: '' } + messages: [] }; // Creates new slice with the name , initial state, and reducer function @@ -27,11 +26,8 @@ const roomSlice = createSlice({ setUserJoined: (state, action) => { state.userJoined = action.payload; }, - setNewMessage: (state, action) => { - state.newMessage = action.payload; - }, - setUserChange: (state, action) => { - state.userChange = action.payload; + setMessages: (state, action) => { + state.messages = [...state.messages, action.payload]; } } }); @@ -42,8 +38,7 @@ export const { setUserName, setUserList, setUserJoined, - setNewMessage, - setUserChange + setMessages } = roomSlice.actions; // Exports so we can combine in rootReducer export default roomSlice.reducer; diff --git a/server/server.ts b/server/server.ts index 8828cb784..f6102d93e 100644 --- a/server/server.ts +++ b/server/server.ts @@ -137,6 +137,11 @@ io.on('connection', (client) => { activity: { nickName: userName, status: 'JOIN' } } // send updated userList to all users in room ); + io.to(roomCode).emit('new chat message', { + userName, + message: `${userName} joined chat room`, + type: 'activity' + }); } } catch (error) { //if joining event is having an error and time out @@ -156,7 +161,7 @@ io.on('connection', (client) => { //disconnecting functionality client.on('disconnecting', () => { const roomCode = Array.from(client.rooms)[1]; //grabbing current room client was in when disconnecting - const nickName = roomLists[roomCode][client.id]; + const userName = roomLists[roomCode][client.id]; delete roomLists[roomCode][client.id]; //if room empty, delete room from room list if (!Object.keys(roomLists[roomCode]).length) { @@ -164,8 +169,12 @@ io.on('connection', (client) => { } else { //else emit updated user list io.to(roomCode).emit('updateUserList', { - userList: Object.values(roomLists[roomCode]), - activity: { nickName, status: 'LEAVE' } + userList: Object.values(roomLists[roomCode]) + }); + io.to(roomCode).emit('new chat message', { + userName, + message: `${userName} left chat room`, + type: 'activity' }); } }); @@ -173,7 +182,10 @@ io.on('connection', (client) => { //-------Socket events for state synchronization in collab room------------------ client.on('send-chat-message', (roomCode: string, messageData: object) => { if (roomCode) { - io.to(roomCode).emit('new chat message', messageData); + io.to(roomCode).emit('new chat message', { + ...messageData, + type: 'chat' + }); } }); client.on('addChildAction', (roomCode: string, childData: object) => { From 80abbc75bba6de5056fc7d969012da3b7f84bf9f Mon Sep 17 00:00:00 2001 From: cyburns <72774499+cyburns@users.noreply.github.com> Date: Fri, 9 Feb 2024 17:38:19 -0500 Subject: [PATCH 021/221] ts day --- app/src/components/left/RoomsContainer.tsx | 321 +++++++++++---------- app/src/components/login/FBPassWord.tsx | 15 +- app/src/components/socketUtils/socket.tsx | 198 +++++++++++++ 3 files changed, 367 insertions(+), 167 deletions(-) create mode 100644 app/src/components/socketUtils/socket.tsx diff --git a/app/src/components/left/RoomsContainer.tsx b/app/src/components/left/RoomsContainer.tsx index 8aeddc8a0..46110eef6 100644 --- a/app/src/components/left/RoomsContainer.tsx +++ b/app/src/components/left/RoomsContainer.tsx @@ -53,6 +53,7 @@ import { DeleteContextPayload, addComponentToContext } from '../../../src/redux/reducers/slice/contextReducer'; +import { initSocketConnection } from '../socketUtils/socket'; const RoomsContainer = () => { const dispatch = useDispatch(); @@ -67,166 +68,166 @@ const RoomsContainer = () => { (store: RootState) => store.roomSlice.userJoined ); - const initSocketConnection = (roomCode: string) => { - initializeSocket(); - const socket = getSocket(); - if (socket) { - socket.on('connect', () => { - socket.emit('joining', userName, roomCode); - }); - - socket.on('requesting state from host', (callback) => { - const newState = store.getState(); - callback(newState); - }); - - socket.on('server emitting state from host', (state, callback) => { - store.dispatch(allCooperativeState(state.appState)); - store.dispatch(codePreviewCooperative(state.codePreviewCooperative)); - store.dispatch(cooperativeStyle(state.styleSlice)); - callback({ status: 'confirmed' }); - }); - - socket.on('updateUserList', (newUserList) => { - dispatch(setUserList(newUserList)); - }); - - socket.on('child data from server', (childData: object) => { - store.dispatch(addChild(childData)); - }); - - socket.on('focus data from server', (focusData: object) => { - store.dispatch(changeFocus(focusData)); - }); - - socket.on('delete data from server', (deleteData: object) => { - store.dispatch(deleteChild(deleteData)); - }); - - socket.on( - 'delete element data from server', - (deleteElementData: object) => { - store.dispatch(deleteElement(deleteElementData)); - } - ); - - // dispatch clear canvas action to local state when the host of the room has clear canvas - socket.on('clear canvas from server', () => { - store.dispatch(resetAllState()); - }); - - // dispatch all updates to local state when another user has saved from Bottom Panel - socket.on('update data from server', (updateData: BottomPanelObj) => { - store.dispatch( - updateStateUsed({ - stateUsedObj: updateData.stateUsedObj, - contextParam: updateData.contextParam - }) - ); - store.dispatch( - updateUseContext({ - useContextObj: updateData.useContextObj, - contextParam: updateData.contextParam - }) - ); - store.dispatch( - updateCss({ - style: updateData.style, - contextParam: updateData.contextParam - }) - ); - store.dispatch( - updateAttributes({ - attributes: updateData.attributes, - contextParam: updateData.contextParam - }) - ); - store.dispatch( - updateEvents({ - events: updateData.events, - contextParam: updateData.contextParam - }) - ); - }); - - socket.on('update css data from server', (cssData: object) => { - store.dispatch(updateStylesheet(cssData)); - }); - - socket.on( - 'item position data from server', - (itemPositionData: object) => { - store.dispatch(changePosition(itemPositionData)); - } - ); - - socket.on('new component data from server', (newComponent: object) => { - store.dispatch(addComponent(newComponent)); - }); - - socket.on('new element data from server', (newElement: object) => { - store.dispatch(addElement(newElement)); - }); - - socket.on( - 'new component state data from server', - (componentState: object) => { - store.dispatch(addState(componentState)); - } - ); - - socket.on( - 'delete component state data from server', - (componentStateDelete: object) => { - store.dispatch(deleteState(componentStateDelete)); - } - ); - - socket.on( - 'new PassedInProps data from server', - (passedInProps: object) => { - store.dispatch(addPassedInProps(passedInProps)); - } - ); - - socket.on( - 'PassedInProps delete data from server', - (passedInProps: object) => { - store.dispatch(deletePassedInProps(passedInProps)); - } - ); - - socket.on('new context from server', (context: AddContextPayload) => { - store.dispatch(addContext(context)); - }); - - socket.on( - 'new context value from server', - (contextVal: AddContextValuesPayload) => { - store.dispatch(addContextValues(contextVal)); - } - ); - - socket.on( - 'delete context data from server', - (context: DeleteContextPayload) => { - store.dispatch(deleteContext(context)); - } - ); - - socket.on('assign context data from server', (data) => { - store.dispatch( - addComponentToContext({ - context: data.context, - component: data.component - }) - ); - store.dispatch( - deleteElement({ id: 'FAKE_ID', contextParam: data.contextParam }) - ); - }); - } - }; + // const initSocketConnection = (roomCode: string) => { + // initializeSocket(); + // const socket = getSocket(); + // if (socket) { + // socket.on('connect', () => { + // socket.emit('joining', userName, roomCode); + // }); + + // socket.on('requesting state from host', (callback) => { + // const newState = store.getState(); + // callback(newState); + // }); + + // socket.on('server emitting state from host', (state, callback) => { + // store.dispatch(allCooperativeState(state.appState)); + // store.dispatch(codePreviewCooperative(state.codePreviewCooperative)); + // store.dispatch(cooperativeStyle(state.styleSlice)); + // callback({ status: 'confirmed' }); + // }); + + // socket.on('updateUserList', (newUserList) => { + // dispatch(setUserList(newUserList)); + // }); + + // socket.on('child data from server', (childData: object) => { + // store.dispatch(addChild(childData)); + // }); + + // socket.on('focus data from server', (focusData: object) => { + // store.dispatch(changeFocus(focusData)); + // }); + + // socket.on('delete data from server', (deleteData: object) => { + // store.dispatch(deleteChild(deleteData)); + // }); + + // socket.on( + // 'delete element data from server', + // (deleteElementData: object) => { + // store.dispatch(deleteElement(deleteElementData)); + // } + // ); + + // // dispatch clear canvas action to local state when the host of the room has clear canvas + // socket.on('clear canvas from server', () => { + // store.dispatch(resetAllState()); + // }); + + // // dispatch all updates to local state when another user has saved from Bottom Panel + // socket.on('update data from server', (updateData: BottomPanelObj) => { + // store.dispatch( + // updateStateUsed({ + // stateUsedObj: updateData.stateUsedObj, + // contextParam: updateData.contextParam + // }) + // ); + // store.dispatch( + // updateUseContext({ + // useContextObj: updateData.useContextObj, + // contextParam: updateData.contextParam + // }) + // ); + // store.dispatch( + // updateCss({ + // style: updateData.style, + // contextParam: updateData.contextParam + // }) + // ); + // store.dispatch( + // updateAttributes({ + // attributes: updateData.attributes, + // contextParam: updateData.contextParam + // }) + // ); + // store.dispatch( + // updateEvents({ + // events: updateData.events, + // contextParam: updateData.contextParam + // }) + // ); + // }); + + // socket.on('update css data from server', (cssData: object) => { + // store.dispatch(updateStylesheet(cssData)); + // }); + + // socket.on( + // 'item position data from server', + // (itemPositionData: object) => { + // store.dispatch(changePosition(itemPositionData)); + // } + // ); + + // socket.on('new component data from server', (newComponent: object) => { + // store.dispatch(addComponent(newComponent)); + // }); + + // socket.on('new element data from server', (newElement: object) => { + // store.dispatch(addElement(newElement)); + // }); + + // socket.on( + // 'new component state data from server', + // (componentState: object) => { + // store.dispatch(addState(componentState)); + // } + // ); + + // socket.on( + // 'delete component state data from server', + // (componentStateDelete: object) => { + // store.dispatch(deleteState(componentStateDelete)); + // } + // ); + + // socket.on( + // 'new PassedInProps data from server', + // (passedInProps: object) => { + // store.dispatch(addPassedInProps(passedInProps)); + // } + // ); + + // socket.on( + // 'PassedInProps delete data from server', + // (passedInProps: object) => { + // store.dispatch(deletePassedInProps(passedInProps)); + // } + // ); + + // socket.on('new context from server', (context: AddContextPayload) => { + // store.dispatch(addContext(context)); + // }); + + // socket.on( + // 'new context value from server', + // (contextVal: AddContextValuesPayload) => { + // store.dispatch(addContextValues(contextVal)); + // } + // ); + + // socket.on( + // 'delete context data from server', + // (context: DeleteContextPayload) => { + // store.dispatch(deleteContext(context)); + // } + // ); + + // socket.on('assign context data from server', (data) => { + // store.dispatch( + // addComponentToContext({ + // context: data.context, + // component: data.component + // }) + // ); + // store.dispatch( + // deleteElement({ id: 'FAKE_ID', contextParam: data.contextParam }) + // ); + // }); + // } + // }; const handleUserEnteredRoom = (roomCode) => { initSocketConnection(roomCode); diff --git a/app/src/components/login/FBPassWord.tsx b/app/src/components/login/FBPassWord.tsx index 2672f584c..717b855d2 100644 --- a/app/src/components/login/FBPassWord.tsx +++ b/app/src/components/login/FBPassWord.tsx @@ -1,4 +1,4 @@ -import React, { useState, MouseEvent} from 'react'; +import React, { useState, MouseEvent } from 'react'; import { LoginInt } from '../../interfaces/Interfaces'; import { Link as RouteLink, @@ -26,7 +26,7 @@ function Copyright() { ); } -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ paper: { marginTop: theme.spacing(8), display: 'flex', @@ -51,7 +51,7 @@ const useStyles = makeStyles(theme => ({ } })); -const SignUp: React.FC = props => { +const SignUp: React.FC = (props) => { const classes = useStyles(); const [password, setPassword] = useState(''); const [passwordVerify, setPasswordVerify] = useState(''); @@ -122,11 +122,10 @@ const SignUp: React.FC = props => { } // get username and email from FB - newUserIsCreated(email, email, password).then(userCreated => { + newUserIsCreated(email, email, password).then((userCreated) => { if (userCreated === 'Success') { props.history.push('/'); } else { - } }); }; @@ -183,9 +182,11 @@ const SignUp: React.FC = props => { fullWidth variant="contained" className={classes.submit} - onClick={e => handleSignUp(e)}> + onClick={(e) => handleSignUp(e)} + > Sign Up - + + + + diff --git a/app/src/components/socketUtils/socket.tsx b/app/src/components/socketUtils/socket.tsx new file mode 100644 index 000000000..d8376cfe7 --- /dev/null +++ b/app/src/components/socketUtils/socket.tsx @@ -0,0 +1,198 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; +import { BottomPanelObj } from '../../interfaces/Interfaces'; +import { + allCooperativeState, + addChild, + changeFocus, + deleteChild, + changePosition, + updateStateUsed, + updateUseContext, + updateCss, + updateAttributes, + updateEvents, + addComponent, + addElement, + addState, + deleteState, + addPassedInProps, + deletePassedInProps, + deleteElement, + resetAllState, + updateStylesheet +} from '../../redux/reducers/slice/appStateSlice'; +import { + addContext, + deleteContext, + addContextValues +} from '../../redux/reducers/slice/contextReducer'; +import { setUserList } from '../../redux/reducers/slice/roomSlice'; +import { codePreviewCooperative } from '../../redux/reducers/slice/codePreviewSlice'; +import { cooperativeStyle } from '../../redux/reducers/slice/styleSlice'; +import store from '../../redux/store'; +import { initializeSocket, getSocket } from '../../helperFunctions/socket'; +import { + AddContextPayload, + AddContextValuesPayload, + DeleteContextPayload, + addComponentToContext +} from '../../redux/reducers/slice/contextReducer'; + +export const initSocketConnection = (roomCode: string) => { + const userName = useSelector((store: RootState) => store.roomSlice.userName); + const dispatch = useDispatch(); + + initializeSocket(); + const socket = getSocket(); + if (socket) { + socket.on('connect', () => { + socket.emit('joining', userName, roomCode); + }); + + socket.on('requesting state from host', (callback) => { + const newState = store.getState(); + callback(newState); + }); + + socket.on('server emitting state from host', (state, callback) => { + store.dispatch(allCooperativeState(state.appState)); + store.dispatch(codePreviewCooperative(state.codePreviewCooperative)); + store.dispatch(cooperativeStyle(state.styleSlice)); + callback({ status: 'confirmed' }); + }); + + socket.on('updateUserList', (newUserList) => { + dispatch(setUserList(newUserList)); + }); + + socket.on('child data from server', (childData: object) => { + store.dispatch(addChild(childData)); + }); + + socket.on('focus data from server', (focusData: object) => { + store.dispatch(changeFocus(focusData)); + }); + + socket.on('delete data from server', (deleteData: object) => { + store.dispatch(deleteChild(deleteData)); + }); + + socket.on( + 'delete element data from server', + (deleteElementData: object) => { + store.dispatch(deleteElement(deleteElementData)); + } + ); + + // dispatch clear canvas action to local state when the host of the room has clear canvas + socket.on('clear canvas from server', () => { + store.dispatch(resetAllState()); + }); + + // dispatch all updates to local state when another user has saved from Bottom Panel + socket.on('update data from server', (updateData: BottomPanelObj) => { + store.dispatch( + updateStateUsed({ + stateUsedObj: updateData.stateUsedObj, + contextParam: updateData.contextParam + }) + ); + store.dispatch( + updateUseContext({ + useContextObj: updateData.useContextObj, + contextParam: updateData.contextParam + }) + ); + store.dispatch( + updateCss({ + style: updateData.style, + contextParam: updateData.contextParam + }) + ); + store.dispatch( + updateAttributes({ + attributes: updateData.attributes, + contextParam: updateData.contextParam + }) + ); + store.dispatch( + updateEvents({ + events: updateData.events, + contextParam: updateData.contextParam + }) + ); + }); + + socket.on('update css data from server', (cssData: object) => { + store.dispatch(updateStylesheet(cssData)); + }); + + socket.on('item position data from server', (itemPositionData: object) => { + store.dispatch(changePosition(itemPositionData)); + }); + + socket.on('new component data from server', (newComponent: object) => { + store.dispatch(addComponent(newComponent)); + }); + + socket.on('new element data from server', (newElement: object) => { + store.dispatch(addElement(newElement)); + }); + + socket.on( + 'new component state data from server', + (componentState: object) => { + store.dispatch(addState(componentState)); + } + ); + + socket.on( + 'delete component state data from server', + (componentStateDelete: object) => { + store.dispatch(deleteState(componentStateDelete)); + } + ); + + socket.on('new PassedInProps data from server', (passedInProps: object) => { + store.dispatch(addPassedInProps(passedInProps)); + }); + + socket.on( + 'PassedInProps delete data from server', + (passedInProps: object) => { + store.dispatch(deletePassedInProps(passedInProps)); + } + ); + + socket.on('new context from server', (context: AddContextPayload) => { + store.dispatch(addContext(context)); + }); + + socket.on( + 'new context value from server', + (contextVal: AddContextValuesPayload) => { + store.dispatch(addContextValues(contextVal)); + } + ); + + socket.on( + 'delete context data from server', + (context: DeleteContextPayload) => { + store.dispatch(deleteContext(context)); + } + ); + + socket.on('assign context data from server', (data) => { + store.dispatch( + addComponentToContext({ + context: data.context, + component: data.component + }) + ); + store.dispatch( + deleteElement({ id: 'FAKE_ID', contextParam: data.contextParam }) + ); + }); + } +}; From f77bbca83daf02c2639e88bbf0de022fb2ece8fe Mon Sep 17 00:00:00 2001 From: Brian Date: Sat, 10 Feb 2024 09:34:06 -0500 Subject: [PATCH 022/221] removed console logs and commented out code as per Jon/Eliza's review --- app/src/components/left/HTMLItem.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/app/src/components/left/HTMLItem.tsx b/app/src/components/left/HTMLItem.tsx index ee34b7e1e..bdca50065 100644 --- a/app/src/components/left/HTMLItem.tsx +++ b/app/src/components/left/HTMLItem.tsx @@ -126,25 +126,7 @@ const HTMLItem: React.FC<{ const dispatch = useDispatch(); - /* const handleClick = () => { - console.log('Component clicked:', name); - console.log('id', id); - // Dispatch action to add child - dispatch( - addChild({ - type: 'HTML Element', - typeId: id, - childId: null, - contextParam: { - allContext: [] - } - }) - ); - };*/ - const handleClick = () => { - console.log('Component clicked:', name); - console.log('id', id); const childData = { type: 'HTML Element', typeId: id, From f6213f9787f0f996585dc6a69349bee39b52bf1a Mon Sep 17 00:00:00 2001 From: cyburns <72774499+cyburns@users.noreply.github.com> Date: Sat, 10 Feb 2024 10:51:03 -0500 Subject: [PATCH 023/221] with comments added back --- app/src/Dashboard/NavbarDash.tsx | 206 +++++++----- app/src/components/left/RoomsContainer.tsx | 355 +++++++++++---------- app/src/components/main/Canvas.tsx | 56 +++- app/src/components/socketUtils/socket.tsx | 69 +++- 4 files changed, 415 insertions(+), 271 deletions(-) diff --git a/app/src/Dashboard/NavbarDash.tsx b/app/src/Dashboard/NavbarDash.tsx index 0c4faa373..328c0ec81 100644 --- a/app/src/Dashboard/NavbarDash.tsx +++ b/app/src/Dashboard/NavbarDash.tsx @@ -19,36 +19,41 @@ import SortIcon from '@mui/icons-material/Sort'; import StarBorderIcon from '@mui/icons-material/StarBorder'; import PersonIcon from '@mui/icons-material/Person'; import greenLogo from '../public/icons/png/512x512.png'; -import {setStyle} from '../redux/reducers/slice/styleSlice' -import { useSelector,useDispatch } from 'react-redux'; - -const useStyles = makeStyles((theme: Theme) => createStyles({ - root: { - flexGrow: 1, - width: '100%', - }, - menuButton: { - marginRight: theme.spacing(2), - color: 'white', - }, - title: { - flexGrow: 1, - color: 'white', - }, - manageProject: { - display: 'flex', - justifyContent: 'center', - }, -})); +import { setStyle } from '../redux/reducers/slice/styleSlice'; +import { useSelector, useDispatch } from 'react-redux'; +// NavBar text and button styling +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + flexGrow: 1, + width: '100%' + }, + menuButton: { + marginRight: theme.spacing(2), + color: 'white' + }, + title: { + flexGrow: 1, + color: 'white' + }, + manageProject: { + display: 'flex', + justifyContent: 'center' + } + }) +); +// sorting options const sortMethods = ['RATING', 'DATE', 'USER']; +// Drop down menu button for SORT PROJECTS const StyledMenu = withStyles({ paper: { - border: '1px solid #d3d4d5', + border: '1px solid #d3d4d5' } -})(props => ( +})((props) => ( )); -const StyledMenuItem = withStyles(theme => ({ +const StyledMenuItem = withStyles((theme) => ({ root: { '&:focus': { '& .MuiListItemIcon-root, & .MuiListItemText-primary': { @@ -73,94 +78,123 @@ const StyledMenuItem = withStyles(theme => ({ export default function NavBar(props) { // TO DO: import setStyle const classes = useStyles(); - const style = useSelector(store => store.styleSlice); + const style = useSelector((store) => store.styleSlice); const dispatch = useDispatch(); const toggling = () => setIsOpen(!isOpen); + // toggle to open and close dropdown sorting menu const [isOpen, setIsOpen] = useState(false); + // State for sort projects button const [anchorEl, setAnchorEl] = React.useState(null); - const handleClick = event => { + const handleClick = (event) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; - return (
- + - + - + ReacType - -
+ +
- - {sortMethods.map((option, index) => ( - { - props.optionClicked(option); - toggling(); - handleClose(); - }} - variant='contained' - color='primary' - style={{ minWidth: '137.69px' }} - className={classes.manageProject} - key={index} - > - - - ))} - -
- + + {sortMethods.map((option, index) => ( + { + props.optionClicked(option); + toggling(); + handleClose(); + }} + variant="contained" + color="primary" + style={{ minWidth: '137.69px' }} + className={classes.manageProject} + key={index} + > + + + ))} + +
+ +
- + diff --git a/app/src/components/left/RoomsContainer.tsx b/app/src/components/left/RoomsContainer.tsx index 46110eef6..bc4b2fc6b 100644 --- a/app/src/components/left/RoomsContainer.tsx +++ b/app/src/components/left/RoomsContainer.tsx @@ -5,7 +5,6 @@ import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import Button from '@mui/material/Button'; -import React from 'react'; import { RootState } from '../../redux/store'; import TextField from '@mui/material/TextField'; import { BottomPanelObj } from '../../interfaces/Interfaces'; @@ -53,7 +52,7 @@ import { DeleteContextPayload, addComponentToContext } from '../../../src/redux/reducers/slice/contextReducer'; -import { initSocketConnection } from '../socketUtils/socket'; +// import { initSocketConnection } from '../socketUtils/socket'; const RoomsContainer = () => { const dispatch = useDispatch(); @@ -68,166 +67,198 @@ const RoomsContainer = () => { (store: RootState) => store.roomSlice.userJoined ); - // const initSocketConnection = (roomCode: string) => { - // initializeSocket(); - // const socket = getSocket(); - // if (socket) { - // socket.on('connect', () => { - // socket.emit('joining', userName, roomCode); - // }); - - // socket.on('requesting state from host', (callback) => { - // const newState = store.getState(); - // callback(newState); - // }); - - // socket.on('server emitting state from host', (state, callback) => { - // store.dispatch(allCooperativeState(state.appState)); - // store.dispatch(codePreviewCooperative(state.codePreviewCooperative)); - // store.dispatch(cooperativeStyle(state.styleSlice)); - // callback({ status: 'confirmed' }); - // }); - - // socket.on('updateUserList', (newUserList) => { - // dispatch(setUserList(newUserList)); - // }); - - // socket.on('child data from server', (childData: object) => { - // store.dispatch(addChild(childData)); - // }); - - // socket.on('focus data from server', (focusData: object) => { - // store.dispatch(changeFocus(focusData)); - // }); - - // socket.on('delete data from server', (deleteData: object) => { - // store.dispatch(deleteChild(deleteData)); - // }); - - // socket.on( - // 'delete element data from server', - // (deleteElementData: object) => { - // store.dispatch(deleteElement(deleteElementData)); - // } - // ); - - // // dispatch clear canvas action to local state when the host of the room has clear canvas - // socket.on('clear canvas from server', () => { - // store.dispatch(resetAllState()); - // }); - - // // dispatch all updates to local state when another user has saved from Bottom Panel - // socket.on('update data from server', (updateData: BottomPanelObj) => { - // store.dispatch( - // updateStateUsed({ - // stateUsedObj: updateData.stateUsedObj, - // contextParam: updateData.contextParam - // }) - // ); - // store.dispatch( - // updateUseContext({ - // useContextObj: updateData.useContextObj, - // contextParam: updateData.contextParam - // }) - // ); - // store.dispatch( - // updateCss({ - // style: updateData.style, - // contextParam: updateData.contextParam - // }) - // ); - // store.dispatch( - // updateAttributes({ - // attributes: updateData.attributes, - // contextParam: updateData.contextParam - // }) - // ); - // store.dispatch( - // updateEvents({ - // events: updateData.events, - // contextParam: updateData.contextParam - // }) - // ); - // }); - - // socket.on('update css data from server', (cssData: object) => { - // store.dispatch(updateStylesheet(cssData)); - // }); - - // socket.on( - // 'item position data from server', - // (itemPositionData: object) => { - // store.dispatch(changePosition(itemPositionData)); - // } - // ); - - // socket.on('new component data from server', (newComponent: object) => { - // store.dispatch(addComponent(newComponent)); - // }); - - // socket.on('new element data from server', (newElement: object) => { - // store.dispatch(addElement(newElement)); - // }); - - // socket.on( - // 'new component state data from server', - // (componentState: object) => { - // store.dispatch(addState(componentState)); - // } - // ); - - // socket.on( - // 'delete component state data from server', - // (componentStateDelete: object) => { - // store.dispatch(deleteState(componentStateDelete)); - // } - // ); - - // socket.on( - // 'new PassedInProps data from server', - // (passedInProps: object) => { - // store.dispatch(addPassedInProps(passedInProps)); - // } - // ); - - // socket.on( - // 'PassedInProps delete data from server', - // (passedInProps: object) => { - // store.dispatch(deletePassedInProps(passedInProps)); - // } - // ); - - // socket.on('new context from server', (context: AddContextPayload) => { - // store.dispatch(addContext(context)); - // }); - - // socket.on( - // 'new context value from server', - // (contextVal: AddContextValuesPayload) => { - // store.dispatch(addContextValues(contextVal)); - // } - // ); - - // socket.on( - // 'delete context data from server', - // (context: DeleteContextPayload) => { - // store.dispatch(deleteContext(context)); - // } - // ); - - // socket.on('assign context data from server', (data) => { - // store.dispatch( - // addComponentToContext({ - // context: data.context, - // component: data.component - // }) - // ); - // store.dispatch( - // deleteElement({ id: 'FAKE_ID', contextParam: data.contextParam }) - // ); - // }); - // } - // }; + const initSocketConnection = (roomCode: string) => { + // helper function to create socket connection + initializeSocket(); + // assign socket to result of helper function to return socket created + const socket = getSocket(); + // if socket was created correctly and exists + if (socket) { + //run everytime when a client connects to server + socket.on('connect', () => { + socket.emit('joining', userName, roomCode); + // console.log(`${userName} Joined room ${roomCode} from RoomsContainer`); + }); + + //If you are the host: send current state to server when a new user joins + socket.on('requesting state from host', (callback) => { + const newState = store.getState(); //pull the current state + callback(newState); //send it to backend server + }); + + //If you are the new user: receive the state from the host + socket.on('server emitting state from host', (state, callback) => { + //dispatching new state to change user current state + // console.log('state received by new join:', state); + store.dispatch(allCooperativeState(state.appState)); + store.dispatch(codePreviewCooperative(state.codePreviewCooperative)); + store.dispatch(cooperativeStyle(state.styleSlice)); + callback({ status: 'confirmed' }); + }); + + // update user list when there's a change: new join or leave the room + socket.on('updateUserList', (newUserList) => { + //console.log('user list received from server'); + dispatch(setUserList(newUserList)); + }); + + // dispatch add child to local state when element has been added by another user + socket.on('child data from server', (childData: object) => { + // console.log('child data received by users', childData); + store.dispatch(addChild(childData)); + }); + + // dispatch changeFocus to local state when another user has changed focus by selecting element on canvas + socket.on('focus data from server', (focusData: object) => { + // console.log('focus data received from server', focusData); + store.dispatch(changeFocus(focusData)); + }); + + // dispatch deleteChild to local state when another user has deleted an element + socket.on('delete data from server', (deleteData: object) => { + // console.log('delete data received from server', deleteData); + store.dispatch(deleteChild(deleteData)); + }); + + // dispatch delete element to local state when another user has deleted an element + socket.on( + 'delete element data from server', + (deleteElementData: object) => { + // console.log('delete element data received from server', deleteElementData); + store.dispatch(deleteElement(deleteElementData)); + } + ); + + // dispatch all updates to local state when another user has saved from Bottom Panel + socket.on('update data from server', (updateData: BottomPanelObj) => { + // console.log('update data received from server', updateData); + store.dispatch( + updateStateUsed({ + stateUsedObj: updateData.stateUsedObj, + contextParam: updateData.contextParam + }) + ); + store.dispatch( + updateUseContext({ + useContextObj: updateData.useContextObj, + contextParam: updateData.contextParam + }) + ); + store.dispatch( + updateCss({ + style: updateData.style, + contextParam: updateData.contextParam + }) + ); + store.dispatch( + updateAttributes({ + attributes: updateData.attributes, + contextParam: updateData.contextParam + }) + ); + store.dispatch( + updateEvents({ + events: updateData.events, + contextParam: updateData.contextParam + }) + ); + }); + + // dispatch update style in local state when CSS panel is updated on their side + socket.on('update css data from server', (cssData: object) => { + // console.log('CSS data received from server', cssData); + store.dispatch(updateStylesheet(cssData)); + }); + + // dispatch new item position in local state when item position is changed by another user + socket.on( + 'item position data from server', + (itemPositionData: object) => { + // console.log( + // 'item position data received from server', + // itemPositionData + // ); + store.dispatch(changePosition(itemPositionData)); + } + ); + + // dispatch addComponent to local state when new component is created by another user + socket.on('new component data from server', (newComponent: object) => { + store.dispatch(addComponent(newComponent)); + }); + + // dispatch addElement to local state when new element is created by another user + socket.on('new element data from server', (newElement: object) => { + store.dispatch(addElement(newElement)); + }); + + // dispatch addState to local state when component state has been changed by another user + socket.on( + 'new component state data from server', + (componentState: object) => { + store.dispatch(addState(componentState)); + } + ); + + // dispatch deleteState to local state when component state has been deleted by another user + socket.on( + 'delete component state data from server', + (componentStateDelete: object) => { + store.dispatch(deleteState(componentStateDelete)); + } + ); + + // dispatch addPassedInProps to local state when p.I.P have been added by another user + socket.on( + 'new PassedInProps data from server', + (passedInProps: object) => { + store.dispatch(addPassedInProps(passedInProps)); + } + ); + + // dispatch deletePassedInProps to local state when p.I.P have been deleted by another user + socket.on( + 'PassedInProps delete data from server', + (passedInProps: object) => { + store.dispatch(deletePassedInProps(passedInProps)); + } + ); + + // dispatch addContext to local state when context has been changed by another user + socket.on('new context from server', (context: AddContextPayload) => { + store.dispatch(addContext(context)); + }); + + // dispatch addContextValues to local state when context values are added by another user + socket.on( + 'new context value from server', + (contextVal: AddContextValuesPayload) => { + store.dispatch(addContextValues(contextVal)); + } + ); + + // dispatch deleteContext to local state when context is deleted by another user + socket.on( + 'delete context data from server', + (context: DeleteContextPayload) => { + store.dispatch(deleteContext(context)); + } + ); + + // dispatch addComponentToContext to local state when context is assigned to component by another user + socket.on('assign context data from server', (data) => { + store.dispatch( + addComponentToContext({ + context: data.context, + component: data.component + }) + ); + store.dispatch( + deleteElement({ id: 'FAKE_ID', contextParam: data.contextParam }) + ); + }); + } + }; const handleUserEnteredRoom = (roomCode) => { initSocketConnection(roomCode); diff --git a/app/src/components/main/Canvas.tsx b/app/src/components/main/Canvas.tsx index 0b690994f..85380079c 100644 --- a/app/src/components/main/Canvas.tsx +++ b/app/src/components/main/Canvas.tsx @@ -24,14 +24,21 @@ const Canvas = (props: {}): JSX.Element => { const userName = useSelector((store: RootState) => store.roomSlice.userName); const userList = useSelector((store: RootState) => store.roomSlice.userList); + //remote cursor data const [remoteCursors, setRemoteCursors] = useState([]); + + // Toggle switch for live cursor tracking const [toggleSwitch, setToggleSwitch] = useState(true); + + // Toggle button text for live cursor tracking button - on/off const [toggleText, setToggleText] = useState('off'); const toggleButton = () => { setToggleText(toggleText === 'on' ? 'off' : 'on'); }; + // Prevents lagging and provides smoother user experience got live cursor tracking (milliseconds can be adjusted but 500ms is most optimal) const debounceSetPosition = debounce((newX, newY) => { + //emit socket event every 300ms when cursor moves if (userList.length > 1) emitEvent('cursorData', roomCode, { x: newX, y: newY, userName }); }, 500); @@ -42,14 +49,18 @@ const Canvas = (props: {}): JSX.Element => { const handleCursorDataFromServer = (remoteData) => { setRemoteCursors((prevState) => { + //check if received cursor data is from an existing user in the room const cursorIdx = prevState.findIndex( (cursor) => cursor.remoteUserName === remoteData.userName ); + //existing user if (cursorIdx >= 0) { + //check if cursor position has changed if ( prevState[cursorIdx].x !== remoteData.x || prevState[cursorIdx].y !== remoteData.y ) { + //update existing user's cursor position const updatedCursors = [...prevState]; updatedCursors[cursorIdx] = { ...prevState[cursorIdx], @@ -58,9 +69,11 @@ const Canvas = (props: {}): JSX.Element => { }; return updatedCursors; } else { + //return previous state if no change return prevState; } } else { + //new user: add new user's cursor return [ ...prevState, { @@ -74,18 +87,23 @@ const Canvas = (props: {}): JSX.Element => { }); }; + // Removes the mouse cursor of the user that leaves the collaboration room. const handleCursorDeleteFromServer = () => { setRemoteCursors((prevRemoteCursors) => + // filter cursors to include only those in the userList prevRemoteCursors.filter((cursor) => userList.includes(cursor.remoteUserName) ) ); }; + // Function that will turn on/off socket depending on toggle Switch. const handleToggleSwitch = () => { setToggleSwitch(!toggleSwitch); + //checks the state before it's updated so need to check the opposite condition if (toggleSwitch) { socket.off('remote cursor data from server'); + //make remote cursor invisible setRemoteCursors((prevState) => { const newState = prevState.map((cursor) => ({ ...cursor, @@ -97,6 +115,7 @@ const Canvas = (props: {}): JSX.Element => { socket.on('remote cursor data from server', (remoteData) => handleCursorDataFromServer(remoteData) ); + //make remote cursor visible setRemoteCursors((prevState) => prevState.map((cursor) => ({ ...cursor, @@ -106,15 +125,15 @@ const Canvas = (props: {}): JSX.Element => { } }; + //Function to handle the multiple click events of the toggle button. const multipleClicks = () => { handleToggleSwitch(); toggleButton(); }; const socket = getSocket(); - + //wrap the socket event listener in useEffect with dependency array as [socket], so the the effect will run only when: 1. After the initial rendering of the component 2. Every time the socket instance changes(connect, disconnect) useEffect(() => { - if (socket) { socket.on('remote cursor data from server', (remoteData) => handleCursorDataFromServer(remoteData) @@ -130,6 +149,7 @@ const Canvas = (props: {}): JSX.Element => { handleCursorDeleteFromServer(); }, [userList]); + // find the current component based on the canvasFocus component ID in the state const currentComponent: Component = state.components.find( (elem: Component) => elem.id === state.canvasFocus.componentId ); @@ -137,11 +157,13 @@ const Canvas = (props: {}): JSX.Element => { Arrow.deleteLines(); const dispatch = useDispatch(); + // changes focus of the canvas to a new component / child const changeFocusFunction = ( componentId?: number, childId?: number | null ) => { dispatch(changeFocus({ componentId, childId })); + //if room exists, send focus dispatch to all users if (roomCode) { emitEvent('changeFocusAction', roomCode, { componentId: componentId, @@ -150,12 +172,15 @@ const Canvas = (props: {}): JSX.Element => { } }; + // onClickHandler is responsible for changing the focused component and child component function onClickHandler(event: React.MouseEvent) { event.stopPropagation(); changeFocusFunction(state.canvasFocus.componentId, null); } + // stores a snapshot of state into the past array for UNDO. snapShotFunc is also invoked for nestable elements in DirectChildHTMLNestable.tsx const snapShotFunc = () => { + // make a deep clone of state const deepCopiedState = JSON.parse(JSON.stringify(state)); const focusIndex = state.canvasFocus.componentId - 1; dispatch( @@ -166,17 +191,21 @@ const Canvas = (props: {}): JSX.Element => { ); }; + // This hook will allow the user to drag items from the left panel on to the canvas const [{ isOver }, drop] = useDrop({ accept: ItemTypes.INSTANCE, drop: (item: DragItem, monitor: DropTargetMonitor) => { const didDrop = monitor.didDrop(); + // takes a snapshot of state to be used in UNDO and REDO cases snapShotFunc(); + // returns false for direct drop target if (didDrop) { return; } - + // if item dropped is going to be a new instance (i.e. it came from the left panel), then create a new child component if (item.newInstance && item.instanceType !== 'Component') { dispatch( + //update state addChild({ type: item.instanceType, typeId: item.instanceTypeId, @@ -185,6 +214,7 @@ const Canvas = (props: {}): JSX.Element => { }) ); + //emit the socket event if (roomCode) { emitEvent('addChildAction', roomCode, { type: item.instanceType, @@ -197,16 +227,24 @@ const Canvas = (props: {}): JSX.Element => { let hasDiffParent = false; const components = state.components; let newChildName = ''; + // loop over components array for (let i = 0; i < components.length; i++) { const comp = components[i]; + //loop over each componenets child for (let j = 0; j < comp.children.length; j++) { const child = comp.children[j]; if (child.name === 'separator') continue; + // check if the item.instanceTypeId matches and child ID if (item.instanceTypeId === child.typeId) { + // check if the name of the parent matches the canvas focus name + // comp is the parent component + // currentComponent is the canvas.focus component if (comp.name === currentComponent.name) { i = components.length; break; } else { + // if false + // setCopiedComp(child); hasDiffParent = true; newChildName = child.name; i = components.length; @@ -215,7 +253,7 @@ const Canvas = (props: {}): JSX.Element => { } } } - + // if (!hasDiffParent) { dispatch( addChild({ type: item.instanceType, @@ -231,7 +269,6 @@ const Canvas = (props: {}): JSX.Element => { childId: null, contextParam: contextParam }); - } } }, @@ -240,6 +277,7 @@ const Canvas = (props: {}): JSX.Element => { }) }); + // Styling for Canvas const defaultCanvasStyle: React.CSSProperties = { width: '100%', minHeight: '100%', @@ -249,11 +287,16 @@ const Canvas = (props: {}): JSX.Element => { boxSizing: 'border-box' }; + // Combine the default styles of the canvas with the custom styles set by the user for that component + // The renderChildren function renders all direct children of a given component + // Direct children are draggable/clickable + const canvasStyle: React.CSSProperties = combineStyles( defaultCanvasStyle, currentComponent.style ); + // Array of colors that color code users as they join the room (In a set order) const userColors = [ '#FC00BD', '#D0FC00', @@ -284,6 +327,7 @@ const Canvas = (props: {}): JSX.Element => { position: 'absolute', left: cursor.x + 'px', top: cursor.y - 68 + 'px', + //cursor style fontSize: '1em', color: userColors[userList.indexOf(cursor.remoteUserName)] }} @@ -321,4 +365,4 @@ const Canvas = (props: {}): JSX.Element => { ); } -export default Canvas; +export default Canvas; \ No newline at end of file diff --git a/app/src/components/socketUtils/socket.tsx b/app/src/components/socketUtils/socket.tsx index d8376cfe7..3858d7e27 100644 --- a/app/src/components/socketUtils/socket.tsx +++ b/app/src/components/socketUtils/socket.tsx @@ -40,58 +40,70 @@ import { } from '../../redux/reducers/slice/contextReducer'; export const initSocketConnection = (roomCode: string) => { - const userName = useSelector((store: RootState) => store.roomSlice.userName); - const dispatch = useDispatch(); - + // helper function to create socket connection initializeSocket(); + // assign socket to result of helper function to return socket created const socket = getSocket(); + // if socket was created correctly and exists if (socket) { + //run everytime when a client connects to server socket.on('connect', () => { socket.emit('joining', userName, roomCode); + // console.log(`${userName} Joined room ${roomCode} from RoomsContainer`); }); + //If you are the host: send current state to server when a new user joins socket.on('requesting state from host', (callback) => { - const newState = store.getState(); - callback(newState); + const newState = store.getState(); //pull the current state + callback(newState); //send it to backend server }); + //If you are the new user: receive the state from the host socket.on('server emitting state from host', (state, callback) => { + //dispatching new state to change user current state + // console.log('state received by new join:', state); store.dispatch(allCooperativeState(state.appState)); store.dispatch(codePreviewCooperative(state.codePreviewCooperative)); store.dispatch(cooperativeStyle(state.styleSlice)); callback({ status: 'confirmed' }); }); + // update user list when there's a change: new join or leave the room socket.on('updateUserList', (newUserList) => { + //console.log('user list received from server'); dispatch(setUserList(newUserList)); }); + // dispatch add child to local state when element has been added by another user socket.on('child data from server', (childData: object) => { + // console.log('child data received by users', childData); store.dispatch(addChild(childData)); }); + // dispatch changeFocus to local state when another user has changed focus by selecting element on canvas socket.on('focus data from server', (focusData: object) => { + // console.log('focus data received from server', focusData); store.dispatch(changeFocus(focusData)); }); + // dispatch deleteChild to local state when another user has deleted an element socket.on('delete data from server', (deleteData: object) => { + // console.log('delete data received from server', deleteData); store.dispatch(deleteChild(deleteData)); }); + // dispatch delete element to local state when another user has deleted an element socket.on( 'delete element data from server', (deleteElementData: object) => { + // console.log('delete element data received from server', deleteElementData); store.dispatch(deleteElement(deleteElementData)); } ); - // dispatch clear canvas action to local state when the host of the room has clear canvas - socket.on('clear canvas from server', () => { - store.dispatch(resetAllState()); - }); - // dispatch all updates to local state when another user has saved from Bottom Panel socket.on('update data from server', (updateData: BottomPanelObj) => { + // console.log('update data received from server', updateData); store.dispatch( updateStateUsed({ stateUsedObj: updateData.stateUsedObj, @@ -124,22 +136,35 @@ export const initSocketConnection = (roomCode: string) => { ); }); + // dispatch update style in local state when CSS panel is updated on their side socket.on('update css data from server', (cssData: object) => { + // console.log('CSS data received from server', cssData); store.dispatch(updateStylesheet(cssData)); }); - socket.on('item position data from server', (itemPositionData: object) => { - store.dispatch(changePosition(itemPositionData)); - }); + // dispatch new item position in local state when item position is changed by another user + socket.on( + 'item position data from server', + (itemPositionData: object) => { + // console.log( + // 'item position data received from server', + // itemPositionData + // ); + store.dispatch(changePosition(itemPositionData)); + } + ); + // dispatch addComponent to local state when new component is created by another user socket.on('new component data from server', (newComponent: object) => { store.dispatch(addComponent(newComponent)); }); + // dispatch addElement to local state when new element is created by another user socket.on('new element data from server', (newElement: object) => { store.dispatch(addElement(newElement)); }); + // dispatch addState to local state when component state has been changed by another user socket.on( 'new component state data from server', (componentState: object) => { @@ -147,6 +172,7 @@ export const initSocketConnection = (roomCode: string) => { } ); + // dispatch deleteState to local state when component state has been deleted by another user socket.on( 'delete component state data from server', (componentStateDelete: object) => { @@ -154,10 +180,15 @@ export const initSocketConnection = (roomCode: string) => { } ); - socket.on('new PassedInProps data from server', (passedInProps: object) => { - store.dispatch(addPassedInProps(passedInProps)); - }); + // dispatch addPassedInProps to local state when p.I.P have been added by another user + socket.on( + 'new PassedInProps data from server', + (passedInProps: object) => { + store.dispatch(addPassedInProps(passedInProps)); + } + ); + // dispatch deletePassedInProps to local state when p.I.P have been deleted by another user socket.on( 'PassedInProps delete data from server', (passedInProps: object) => { @@ -165,10 +196,12 @@ export const initSocketConnection = (roomCode: string) => { } ); + // dispatch addContext to local state when context has been changed by another user socket.on('new context from server', (context: AddContextPayload) => { store.dispatch(addContext(context)); }); + // dispatch addContextValues to local state when context values are added by another user socket.on( 'new context value from server', (contextVal: AddContextValuesPayload) => { @@ -176,6 +209,7 @@ export const initSocketConnection = (roomCode: string) => { } ); + // dispatch deleteContext to local state when context is deleted by another user socket.on( 'delete context data from server', (context: DeleteContextPayload) => { @@ -183,6 +217,7 @@ export const initSocketConnection = (roomCode: string) => { } ); + // dispatch addComponentToContext to local state when context is assigned to component by another user socket.on('assign context data from server', (data) => { store.dispatch( addComponentToContext({ @@ -195,4 +230,4 @@ export const initSocketConnection = (roomCode: string) => { ); }); } -}; +} From 6b0a44bd7723b47392340d0551939df55cb43efc Mon Sep 17 00:00:00 2001 From: cyburns <72774499+cyburns@users.noreply.github.com> Date: Sat, 10 Feb 2024 11:04:46 -0500 Subject: [PATCH 024/221] comments v2 --- app/src/components/left/RoomsContainer.tsx | 2 +- app/src/helperFunctions/auth.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/components/left/RoomsContainer.tsx b/app/src/components/left/RoomsContainer.tsx index bc4b2fc6b..fd137be29 100644 --- a/app/src/components/left/RoomsContainer.tsx +++ b/app/src/components/left/RoomsContainer.tsx @@ -280,7 +280,7 @@ const RoomsContainer = () => { dispatch(setRoomCode('')); dispatch(setUserName('')); dispatch(setUserList([])); - dispatch(setUserJoined(false)); + dispatch(setUserJoined(false)); //false: join room UI appear dispatch(resetState('')); dispatch(setPassword('')); }; diff --git a/app/src/helperFunctions/auth.ts b/app/src/helperFunctions/auth.ts index 7ee24c8db..23d295554 100644 --- a/app/src/helperFunctions/auth.ts +++ b/app/src/helperFunctions/auth.ts @@ -67,7 +67,7 @@ export const newUserIsCreated = ( window.localStorage.setItem('email', email); return 'Success'; } - return data; + return data;// response is either Email Taken or Username Taken, refer to userController.createUser }) .catch((err) => 'Error'); return result; From 3238ef223f1b3fb61496fae3cb3583c7ad1741dc Mon Sep 17 00:00:00 2001 From: cyburns <72774499+cyburns@users.noreply.github.com> Date: Sat, 10 Feb 2024 11:09:44 -0500 Subject: [PATCH 025/221] comments v3 css fonts working --- app/src/public/styles/style.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/public/styles/style.css b/app/src/public/styles/style.css index 05878628e..1aae82075 100644 --- a/app/src/public/styles/style.css +++ b/app/src/public/styles/style.css @@ -1,8 +1,8 @@ -/* @import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap'); +@import url('https://fonts.googleapis.com/css?family=Open+Sans&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Raleway&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0'); -@import url('https: //fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap'); */ +@import url('https: //fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap'); *, *::before, From dd75339814c47df82b365b46ee9dfd48168399df Mon Sep 17 00:00:00 2001 From: cyburns <72774499+cyburns@users.noreply.github.com> Date: Sat, 10 Feb 2024 11:20:40 -0500 Subject: [PATCH 026/221] comments v3 css fonts working and fixed aPI_BASE_URL --- app/src/helperFunctions/socket.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/helperFunctions/socket.ts b/app/src/helperFunctions/socket.ts index d1fb25021..f03029425 100644 --- a/app/src/helperFunctions/socket.ts +++ b/app/src/helperFunctions/socket.ts @@ -1,16 +1,15 @@ import { io } from 'socket.io-client'; // import config from '../../../config.js'; +import serverConfig from '../serverConfig'; +const { API_BASE_URL } = serverConfig; let socket = null; export const initializeSocket = () => { - socket = io('http://localhost:5656', { + socket = io(API_BASE_URL, { transports: ['websocket'], // will force new socket connection if re-joining to prevent double emits forceNew: true }); - // console.log('A user connected'); - // console.log('socket:', socket); - // } }; // export socket to ensure a single socket instance across the entire app From f061174576490bfed42dacdccd53ba9b86be3d9f Mon Sep 17 00:00:00 2001 From: John Wage Date: Sat, 10 Feb 2024 13:07:44 -0500 Subject: [PATCH 027/221] Change chatroom input field to be controlled component and update local state. Change chatRoom.tsx to ChatRoom.tsx --- app/src/components/bottom/BottomTabs.tsx | 2 +- app/src/components/bottom/chatRoom.tsx | 25 +++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/src/components/bottom/BottomTabs.tsx b/app/src/components/bottom/BottomTabs.tsx index ead40ed78..8b0160fb4 100644 --- a/app/src/components/bottom/BottomTabs.tsx +++ b/app/src/components/bottom/BottomTabs.tsx @@ -8,7 +8,7 @@ import CustomizationPanel from '../../containers/CustomizationPanel'; import CreationPanel from './CreationPanel'; import ContextManager from '../ContextAPIManager/ContextManager'; import StateManager from '../StateManagement/StateManagement'; -import Chatroom from './chatRoom'; +import Chatroom from './ChatRoom'; import Box from '@mui/material/Box'; import Tree from '../../tree/TreeChart'; import FormControl from '@mui/material/FormControl'; diff --git a/app/src/components/bottom/chatRoom.tsx b/app/src/components/bottom/chatRoom.tsx index ee7e2d30b..b30853e3d 100644 --- a/app/src/components/bottom/chatRoom.tsx +++ b/app/src/components/bottom/chatRoom.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useState } from 'react'; import { useSelector } from 'react-redux'; import { RootState } from '../../redux/store'; import { emitEvent } from '../../helperFunctions/socket'; @@ -6,9 +7,10 @@ import { emitEvent } from '../../helperFunctions/socket'; const Chatroom = (props): JSX.Element => { const userName = useSelector((store: RootState) => store.roomSlice.userName); const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); - const messages = useSelector((store: RootState) => store.roomSlice.messages); + const [inputContent, setInputContent] = useState(''); + const wrapperStyles = { border: `2px solid #f2fbf8`, borderRadius: '8px', @@ -49,13 +51,12 @@ const Chatroom = (props): JSX.Element => { const handleSubmit = (e) => { e.preventDefault(); - const messageInput = document.getElementById( - 'message-input' - ) as HTMLInputElement; - const message = messageInput.value.trim(); - if (message !== '') { - emitEvent('send-chat-message', roomCode, { userName, message }); - messageInput.value = ''; + if (inputContent !== '') { + emitEvent('send-chat-message', roomCode, { + userName, + message: inputContent + }); + setInputContent(''); } }; @@ -107,7 +108,13 @@ const Chatroom = (props): JSX.Element => { style={inputContainerStyles} onSubmit={handleSubmit} > - + setInputContent(e.target.value)} + value={inputContent} + style={inputStyles} + /> From 530323139c397ad821bfe65f9c8b4bf41fa67036 Mon Sep 17 00:00:00 2001 From: cyburns <72774499+cyburns@users.noreply.github.com> Date: Sat, 10 Feb 2024 14:36:23 -0500 Subject: [PATCH 028/221] ver 4 --- __tests__/userAuth.test.ts | 13 ------------- app/src/components/left/RoomsContainer.tsx | 5 +++++ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/__tests__/userAuth.test.ts b/__tests__/userAuth.test.ts index 140b8de26..0fbf0bdf9 100644 --- a/__tests__/userAuth.test.ts +++ b/__tests__/userAuth.test.ts @@ -2,24 +2,15 @@ * @jest-environment node */ -<<<<<<< HEAD -import marketplaceController from '../server/controllers/marketplaceController'; -======= ->>>>>>> d96f85e661980df9abf2838c313aebd3698e2464 import app from '../server/server'; import mockData from '../mockData'; import { Sessions, Users } from '../server/models/reactypeModels'; const request = require('supertest'); const mongoose = require('mongoose'); -<<<<<<< HEAD const mockNext = jest.fn(); // Mock nextFunction const MONGO_DB = import.meta.env.MONGO_DB_TEST; const { state, projectToSave, user } = mockData; const PORT = 8080; -======= -const MONGO_DB = process.env.MONGO_DB_TEST; -const { user } = mockData; ->>>>>>> d96f85e661980df9abf2838c313aebd3698e2464 const num = Math.floor(Math.random() * 1000); @@ -51,10 +42,6 @@ describe('User Authentication tests', () => { expect(response.text).toBe('test request is working'); }); }); -<<<<<<< HEAD -======= - ->>>>>>> d96f85e661980df9abf2838c313aebd3698e2464 describe('/signup', () => { describe('POST', () => { //testing new signup diff --git a/app/src/components/left/RoomsContainer.tsx b/app/src/components/left/RoomsContainer.tsx index fd137be29..19cb37b6e 100644 --- a/app/src/components/left/RoomsContainer.tsx +++ b/app/src/components/left/RoomsContainer.tsx @@ -132,6 +132,11 @@ const RoomsContainer = () => { // dispatch all updates to local state when another user has saved from Bottom Panel socket.on('update data from server', (updateData: BottomPanelObj) => { // console.log('update data received from server', updateData); + + socket.on('clear canvas from server', () => { + store.dispatch(resetAllState()); + }); + store.dispatch( updateStateUsed({ stateUsedObj: updateData.stateUsedObj, From bdde31992143e76b86368236607e181a755169fa Mon Sep 17 00:00:00 2001 From: cyburns <72774499+cyburns@users.noreply.github.com> Date: Sat, 10 Feb 2024 14:48:43 -0500 Subject: [PATCH 029/221] two more fixes --- __tests__/userAuth.test.ts | 127 +-------------------- app/src/components/left/RoomsContainer.tsx | 10 +- 2 files changed, 6 insertions(+), 131 deletions(-) diff --git a/__tests__/userAuth.test.ts b/__tests__/userAuth.test.ts index 0fbf0bdf9..4a3d8c907 100644 --- a/__tests__/userAuth.test.ts +++ b/__tests__/userAuth.test.ts @@ -9,7 +9,7 @@ const request = require('supertest'); const mongoose = require('mongoose'); const mockNext = jest.fn(); // Mock nextFunction const MONGO_DB = import.meta.env.MONGO_DB_TEST; -const { state, projectToSave, user } = mockData; +const { user } = mockData; const PORT = 8080; const num = Math.floor(Math.random() * 1000); @@ -148,128 +148,3 @@ describe('User Authentication tests', () => { }); }); }); - -<<<<<<< HEAD -// import request from 'supertest'; -// import app from '../server/server'; -// import mockObj from '../mockData'; -// const user = mockObj.user; -// import mongoose from 'mongoose'; -// const URI = import.meta.env.MONGO_DB; - -// beforeAll(() => { -// mongoose -// .connect(URI, { useNewUrlParser: true }, { useUnifiedTopology: true }) -// .then(() => console.log('connected to test database')); -// }); - -// afterAll(async () => { -// await mongoose.connection.close(); -// }); -// //for creating unqiue login credentials -// const num = Math.floor(Math.random() * 1000); - -// describe('User authentication tests', () => { -// //test connection to server -// describe('initial connection test', () => { -// it('should connect to the server', async () => { -// const response = await request(app).get('/test'); -// expect(response.text).toEqual('test request is working'); -// }); -// }); - -// xdescribe('POST', () => { -// it('responds with status 200 and json object on valid new user signup', () => { -// return request(app) -// .post('/signup') -// .set('Content-Type', 'application/json') -// .send({ -// username: `supertest${num}`, -// email: `test${num}@test.com`, -// password: `${num}` -// }) -// .expect(200) -// .then((res) => expect(typeof res.body).toBe('object')); -// }); - -// it('responds with status 400 and json string on invalid new user signup', () => { -// return request(app) -// .post('/signup') -// .send(user) -// .set('Accept', 'application/json') -// .expect('Content-Type', /json/) -// .expect(400) -// .then((res) => expect(typeof res.body).toBe('string')); -// }); -// }); -// }); -// describe('/login', () => { -// // tests whether existing login information permits user to log in -// xdescribe('POST', () => { -// it('responds with status 200 and json object on verified user login', () => { -// return request(app) -// .post('/login') -// .set('Accept', 'application/json') -// .send(user) -// .expect(200) -// .expect('Content-Type', /json/) -// .then((res) => expect(res.body.sessionId).toEqual(user.userId)); -// }); -// // if invalid username/password, should respond with status 400 -// it('responds with status 400 and json string on invalid user login', () => { -// return request(app) -// .post('/login') -// .send({ username: 'wrongusername', password: 'wrongpassword' }) -// .expect(400) -// .expect('Content-Type', /json/) -// .then((res) => expect(typeof res.body).toBe('string')); -// }); -// it('responds with status 400 and json string on invalid new user signup', () => { -// return request(app) -// .post('/signup') -// .send(user) -// .set('Accept', 'application/json') -// .expect('Content-Type', /json/) -// .expect(400) -// .then((res) => expect(typeof res.body).toBe('string')); -// }); -// }); -// }); - -======= ->>>>>>> d96f85e661980df9abf2838c313aebd3698e2464 -// describe('sessionIsCreated', () => { - -// // note that the username and password in this test are kept in the heroku database -// // DO NOT CHANGE unless you have access to the heroku database -// it("returns the message 'Success' when the user passes all auth checks", () => { -// return request(app) -// .post('/login') -// .send({ -// username: 'test', -// password: 'password1!', -// isFbOauth: false -// }) -// .then((res) => expect(res.body).toHaveProperty('sessionId')); -// }); - -// // // OAuth tests (currently inoperative) -// // xdescribe('Github oauth tests', () => { -// // describe('/github/callback?code=', () => { -// // describe('GET', () => { -// // it('responds with status 400 and error message if no code received', () => { -// // return request(server) -// // .get('/github/callback?code=') -// // .expect(400) -// // .then((res) => { -// // return expect(res.text).toEqual( -// // '"Undefined or no code received from github.com"' -// // ); -// // }); -// // }); -// // it('responds with status 400 if invalid code received', () => { -// // return request(server).get('/github/callback?code=123456').expect(400); -// // }); -// // }); -// // }); -// // }); diff --git a/app/src/components/left/RoomsContainer.tsx b/app/src/components/left/RoomsContainer.tsx index 19cb37b6e..4c64b8a3d 100644 --- a/app/src/components/left/RoomsContainer.tsx +++ b/app/src/components/left/RoomsContainer.tsx @@ -129,14 +129,14 @@ const RoomsContainer = () => { } ); + // dispatch clear canvas action to local state when the host of the room has clear canvas + socket.on('clear canvas from server', () => { + store.dispatch(resetAllState()); + }); + // dispatch all updates to local state when another user has saved from Bottom Panel socket.on('update data from server', (updateData: BottomPanelObj) => { // console.log('update data received from server', updateData); - - socket.on('clear canvas from server', () => { - store.dispatch(resetAllState()); - }); - store.dispatch( updateStateUsed({ stateUsedObj: updateData.stateUsedObj, From 89ea2fc0fc8322b24913dc3b3c8df06126d72957 Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Sat, 10 Feb 2024 15:12:39 -0500 Subject: [PATCH 030/221] Update pull_request_template.md --- .github/pull_request_template.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d76f6f911..c8972cd16 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,7 @@ # Description +Please describe the issue of the pull request and the changes + export only components // GenOption = 1 --> export an entire project w/ webpack, server, etc. @@ -24,7 +24,7 @@ const ProjectManager = () => { 'Export components', 'Export components with application files' ]; - let genOption:number = 0; + let genOption: number = 0; // Closes out the open modal const closeModal = () => setModal(''); @@ -45,7 +45,7 @@ const ProjectManager = () => { button onClick={resetStates} style={{ - border: '1px solid #1b544b', + border: '1px solid #3c59ba', marginBottom: '2%', marginTop: '5%' }} @@ -85,7 +85,7 @@ const ProjectManager = () => { button onClick={() => chooseGenOptions(i)} style={{ - border: '1px solid #1b544b', + border: '1px solid #3c59ba', marginBottom: '2%', marginTop: '5%' }} @@ -112,7 +112,7 @@ const ProjectManager = () => { // add listener for when an app directory is chosen // when a directory is chosen, the callback will export the project to the chosen folder // Note: this listener is imported from the main process via preload.js - window.api.addAppDirChosenListener(path => { + window.api.addAppDirChosenListener((path) => { exportProject( path, state.name @@ -121,7 +121,7 @@ const ProjectManager = () => { genOption, state.projectType, state.components, - state.rootComponents, + state.rootComponents ); }); @@ -139,18 +139,14 @@ const ProjectManager = () => { ); }; - - return ( -
{modal}
- ); + return
{modal}
; }; const useStyles = makeStyles({ - logoutButton: { position: 'absolute', bottom: '50px', - right: '150px', + right: '150px' }, btnGroup: { display: 'flex', @@ -169,7 +165,7 @@ const useStyles = makeStyles({ minWidth: '300px', marginTop: '10px', marginBotton: '10px' - }, + } }); export default withRouter(ProjectManager); diff --git a/app/src/components/top/NavBarButtons.tsx b/app/src/components/top/NavBarButtons.tsx index 5b945808c..d6a9b9c16 100644 --- a/app/src/components/top/NavBarButtons.tsx +++ b/app/src/components/top/NavBarButtons.tsx @@ -121,7 +121,7 @@ const navbarDropDown = (props) => { button onClick={resetState} style={{ - border: '1px solid #1b544b', + border: '1px solid #3c59ba', marginBottom: '2%', marginTop: '5%' }} diff --git a/app/src/components/top/NewExportButton.tsx b/app/src/components/top/NewExportButton.tsx index 22a5540a6..08adfa607 100644 --- a/app/src/components/top/NewExportButton.tsx +++ b/app/src/components/top/NewExportButton.tsx @@ -35,7 +35,7 @@ export default function NewExportButton(): React.JSX.Element { key={i} onClick={() => chooseGenOptions()} style={{ - border: '1px solid #1b544b', + border: '1px solid #3c59ba', marginBottom: '2%', marginTop: '5%' }} diff --git a/app/src/constants/Styling.ts b/app/src/constants/Styling.ts index dee553e47..780947160 100644 --- a/app/src/constants/Styling.ts +++ b/app/src/constants/Styling.ts @@ -1,6 +1,6 @@ export default { neonGreen: '#189bd7', - darkBlue: '#253b80', + darkBlue: '#354e9c', darkGray: '#252526', tutorialGray: '#f2f0f0' }; diff --git a/app/src/containers/CustomizationPanel.tsx b/app/src/containers/CustomizationPanel.tsx index ce3510b36..6f6e99098 100644 --- a/app/src/containers/CustomizationPanel.tsx +++ b/app/src/containers/CustomizationPanel.tsx @@ -463,7 +463,7 @@ const CustomizationPanel = ({ isThemeLight }): JSX.Element => { button onClick={handleDeleteReusableComponent} style={{ - border: '1px solid #1b544b', + border: '1px solid #3c59ba', marginBottom: '2%', marginTop: '5%' }} @@ -475,7 +475,7 @@ const CustomizationPanel = ({ isThemeLight }): JSX.Element => { button onClick={closeModal} style={{ - border: '1px solid #1b544b', + border: '1px solid #3c59ba', marginBottom: '2%', marginTop: '5%' }} @@ -585,7 +585,7 @@ const CustomizationPanel = ({ isThemeLight }): JSX.Element => { ? ' component' : ' element'}{' '}
- {configTarget.child.name} + {configTarget.child.name}
@@ -1049,11 +1049,11 @@ const useStyles = makeStyles({ paddingRight: '20px' }, saveButtonLight: { - border: '1px solid #46C0A5', + border: '1px solid #354e9c', backgroundColor: 'rgba(0, 0, 0, 0.2)' }, saveButtonDark: { - border: '1px solid #1b544b' + border: '1px solid #3c59ba' }, compName: { fontSize: '1rem' diff --git a/app/src/public/styles/style.css b/app/src/public/styles/style.css index 1aae82075..8dcab2184 100644 --- a/app/src/public/styles/style.css +++ b/app/src/public/styles/style.css @@ -151,7 +151,7 @@ span.material-symbols-outlined { .column.left::-webkit-scrollbar-thumb { transition: 0.3s ease all; border-color: transparent; - background-color: #1e83714b; + background-color: #354e9c; border-radius: 8px; z-index: 40; } @@ -220,7 +220,7 @@ span.material-symbols-outlined { #HTMLItemsTopHalf::-webkit-scrollbar-thumb { transition: 0.3s ease all; border-color: transparent; - background-color: #1e83714b; + background-color: #354e9c; border-radius: 8px; z-index: 40; } @@ -250,7 +250,7 @@ span.material-symbols-outlined { #HTMLItem { transition: 0.3s; - border: 1px solid #d2f5eb; + border: 1px solid #354e9c; color: #f2fbf8; padding: 10%; border-radius: 5px; @@ -258,7 +258,7 @@ span.material-symbols-outlined { #HTMLItem:hover { background-color: #474545a8; - color: #a5ead6; + color: #8ca8ff; } #submitButton { @@ -266,7 +266,7 @@ span.material-symbols-outlined { } #submitButton:hover { - background-color: #29a38a; + background-color: #354e9c; color: white; border: none; cursor: pointer; @@ -455,12 +455,12 @@ RIGHT COLUMN } #addComponentButton { - border: 2px solid #46c0a5; + border: 2px solid #354e9c; border-radius: 4px; } #addComponentButton:hover { - background-color: #99d7f2; + background-color: #354e9c; color: white; border: none; cursor: pointer; @@ -483,7 +483,7 @@ RIGHT COLUMN display: flex; justify-content: center; align-content: center; - border-color: #99d7f2; + border-color: #354e9c; } .compPanelItem h3 { @@ -539,7 +539,7 @@ BOTTOM PANEL .tab-content::-webkit-scrollbar-thumb { transition: 0.3s ease all; border-color: transparent; - background-color: #1e83714b; + background-color: #354e9c; border-radius: 8px; z-index: 40; } @@ -552,7 +552,7 @@ BOTTOM PANEL .tab-content::-webkit-scrollbar-thumb { transition: 0.3s ease all; border-color: transparent; - background-color: #99d7f2; + background-color: #354e9c; border-radius: 8px; z-index: 40; } @@ -585,7 +585,7 @@ BOTTOM PANEL .bottom-panel { transition: width 250ms ease-in-out; - background-color: #99d7f2; + background-color: #354e9c; width: 100%; height: 100%; display: flex; @@ -628,7 +628,7 @@ BOTTOM PANEL padding-left: 10px; border: rgb(68, 68, 68) solid 1px; border-radius: 5px; - background-color: #99d7f2; + background-color: #354e9c; } .event-table { @@ -835,7 +835,7 @@ a.nav_link:hover { .useState-btn { color: rgb(241, 240, 240); background-color: rgba(0, 0, 0, 0.54); - border: 1px solid #99d7f2; + border: 1px solid #354e9c; border-radius: 3px; box-shadow: '2px 2px 2px #1a1a1a'; font-family: Arial, Helvetica, sans-serif; @@ -854,12 +854,12 @@ a.nav_link:hover { .useState-header { font-size: 35px; - background-color: #99d7f2; + background-color: #354e9c; color: rgb(241, 240, 240); width: 600px; border: 3px; border-style: solid; - border-color: #99d7f2; + border-color: #354e9c; font-family: 'Open Sans', sans-serif; border-radius: 15px 15px 0px 0px; } diff --git a/app/src/public/styles/theme.ts b/app/src/public/styles/theme.ts index 55536b124..a2aae9608 100644 --- a/app/src/public/styles/theme.ts +++ b/app/src/public/styles/theme.ts @@ -5,13 +5,13 @@ export const theme1 = createTheme({ palette: { mode: 'dark', primary: { - main: '#1e8370' // navy blue + main: '#354e9c' // navy blue }, secondary: { - main: '#46C0A5' // light blue + main: '#354e9c' // light blue }, background: { - paper: '#d2f5eb' + paper: '#354e9c' } } }); @@ -20,13 +20,13 @@ export const theme2 = createTheme({ palette: { mode: 'light', primary: { - main: '#1e8370' + main: '#354e9c' }, secondary: { - main: '#46C0A5' + main: '#354e9c' }, background: { - paper: '#d2f5eb' + paper: '#354e9c' } } }); @@ -35,13 +35,13 @@ export const SigninDark = createTheme({ palette: { mode: 'dark', primary: { - main: '#1b544b' + main: '#3c59ba' }, secondary: { main: '#17a2b8' }, background: { - paper: '#d2f5eb' + paper: '#354e9c' } } }); @@ -50,13 +50,13 @@ export const SigninLight = createTheme({ palette: { mode: 'light', primary: { - main: '#1b544b' + main: '#3c59ba' }, secondary: { main: '#17a2b8' }, background: { - paper: '#d2f5eb' + paper: '#354e9c' } } }); diff --git a/server/assets/renderDemo.css b/server/assets/renderDemo.css index 3dc083970..1350967d2 100644 --- a/server/assets/renderDemo.css +++ b/server/assets/renderDemo.css @@ -1,6 +1,6 @@ @import url(https://fonts.googleapis.com/css?family=Roboto:300); -.tst-text-color { +.tst-text-color { color: #cc0707; } .tst-btn { @@ -17,7 +17,7 @@ .tst-form { position: relative; z-index: 1; - background: #29a38a; + background: #354e9c; max-width: 360px; margin: 0 auto 100px; padding: 45px; @@ -25,7 +25,7 @@ box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24); } .tst-form input { - font-family: "Roboto", sans-serif; + font-family: 'Roboto', sans-serif; outline: 0; background: #f2f2f2; width: 100%; @@ -36,21 +36,23 @@ font-size: 14px; } .tst-form button { - font-family: "Roboto", sans-serif; + font-family: 'Roboto', sans-serif; text-transform: uppercase; outline: 0; - background: #4CAF50; + background: #4caf50; width: 100%; border: 0; padding: 15px; - color: #FFFFFF; + color: #ffffff; font-size: 14px; -webkit-transition: all 0.3 ease; transition: all 0.3 ease; cursor: pointer; } -.tst-form button:hover,.form button:active,.form button:focus { - background: #43A047; +.tst-form button:hover, +.form button:active, +.form button:focus { + background: #354e9c; } .tst-form .message { margin: 15px 0 0; @@ -58,7 +60,7 @@ font-size: 12px; } .tst-form .message-a { - color: #4CAF50; + color: #313fbd; text-decoration: none; } .tst-form .register-form { @@ -70,8 +72,9 @@ max-width: 300px; margin: 0 auto; } -.tst-container:before, .container:after { - content: ""; +.tst-container:before, +.container:after { + content: ''; display: block; clear: both; } @@ -95,85 +98,84 @@ text-decoration: none; } .tst-container .info span .fa { - color: #EF3B3A; + color: #ef3b3a; } .tst-container-body { height: 100vh; background: #76b852; /* fallback for old browsers */ - background: -webkit-linear-gradient(right, #76b852, #8DC26F); - background: -moz-linear-gradient(right, #76b852, #8DC26F); - background: -o-linear-gradient(right, #76b852, #8DC26F); - background: linear-gradient(to left, #76b852, #8DC26F); + background: -webkit-linear-gradient(right, #5278b8, #2a66ce); + background: -moz-linear-gradient(right, #5278b8, #2a66ce); + background: -o-linear-gradient(right, #5278b8, #2a66ce); + background: linear-gradient(to left, #5278b8, #2a66ce); font-family: Roboto, Raleway, sans-serif; -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + -moz-osx-font-smoothing: grayscale; } .tst-list-example .body-container { - margin: 0; - height: 100vh; - display: flex; - align-items: center; - justify-content: center; - background-color: #333; + margin: 0; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background-color: #333; } .tst-list-example ul { - padding: 0; - list-style-type: none; + padding: 0; + list-style-type: none; } .tst-list-example li { - font-size: 25px; - width: 8em; - height: 2em; - color: orange; - border-left: 0.08em solid; - position: relative; - margin-top: 0.8em; - cursor: pointer; + font-size: 25px; + width: 8em; + height: 2em; + color: orange; + border-left: 0.08em solid; + position: relative; + margin-top: 0.8em; + cursor: pointer; } .tst-list-example li::before, -.tst-list-example li::after - { - content: ''; - position: absolute; - width: inherit; - border-left: inherit; - z-index: -1; +.tst-list-example li::after { + content: ''; + position: absolute; + width: inherit; + border-left: inherit; + z-index: -1; } .tst-list-example li::before { - height: 80%; - top: 10%; - left: calc(-0.15em - 0.08em * 2); - filter: brightness(0.8); + height: 80%; + top: 10%; + left: calc(-0.15em - 0.08em * 2); + filter: brightness(0.8); } .tst-list-example li::after { - height: 60%; - top: 20%; - left: calc(-0.15em * 2 - 0.08em * 3); - filter: brightness(0.6); + height: 60%; + top: 20%; + left: calc(-0.15em * 2 - 0.08em * 3); + filter: brightness(0.6); } .tst-list-example li span { - position: relative; - height: 120%; - top: -10%; - box-sizing: border-box; - border: 0.08em solid; - background-color: #333; - display: flex; - align-items: center; - justify-content: center; - font-family: sans-serif; - text-transform: capitalize; - transform: translateX(calc(-0.15em * 3 - 0.08em * 2)); - transition: 0.3s; + position: relative; + height: 120%; + top: -10%; + box-sizing: border-box; + border: 0.08em solid; + background-color: #333; + display: flex; + align-items: center; + justify-content: center; + font-family: sans-serif; + text-transform: capitalize; + transform: translateX(calc(-0.15em * 3 - 0.08em * 2)); + transition: 0.3s; } .tst-list-example li:hover span { - transform: translateX(0.15em); -} \ No newline at end of file + transform: translateX(0.15em); +} From 62e49a35a8c710fe76f5cb3079dae63e7e22d2e7 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 12 Feb 2024 15:14:27 -0500 Subject: [PATCH 046/221] deleted unncesssary comment in lines 337-337 from canvas.tsx from Jon's review --- app/src/components/main/Canvas.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/components/main/Canvas.tsx b/app/src/components/main/Canvas.tsx index 6e206aa9e..4b8ab67e2 100644 --- a/app/src/components/main/Canvas.tsx +++ b/app/src/components/main/Canvas.tsx @@ -330,11 +330,6 @@ const Canvas = (props: {}): JSX.Element => { }; const buttonStyle: React.CSSProperties = { - // position: 'fixed', - // width: '10px', - // height: '10px', - // bottom: '150px', - // right: '45vw', textAlign: 'center', color: '#ffffff', backgroundColor: '#151515', From c99896b0f9823b27485aff35cef9ff79fc69da79 Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Mon, 12 Feb 2024 16:45:52 -0500 Subject: [PATCH 047/221] add changes to send meeting id --- app/src/components/bottom/ChatRoom.tsx | 80 ++++++++++++++++++++++ app/src/components/left/RoomsContainer.tsx | 4 +- app/src/redux/reducers/slice/roomSlice.ts | 5 ++ server/server.ts | 45 ++++++++---- 4 files changed, 121 insertions(+), 13 deletions(-) diff --git a/app/src/components/bottom/ChatRoom.tsx b/app/src/components/bottom/ChatRoom.tsx index d1fe0d2ec..9dfddfdc2 100644 --- a/app/src/components/bottom/ChatRoom.tsx +++ b/app/src/components/bottom/ChatRoom.tsx @@ -3,13 +3,53 @@ import { useState } from 'react'; import { useSelector } from 'react-redux'; import { RootState } from '../../redux/store'; import { emitEvent } from '../../helperFunctions/socket'; +import { + MeetingProvider, + MeetingConsumer, + Constants, + useMeeting, + useParticipant +} from '@videosdk.live/react-sdk'; const Chatroom = (props): JSX.Element => { const userName = useSelector((store: RootState) => store.roomSlice.userName); const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); const messages = useSelector((store: RootState) => store.roomSlice.messages); + const [meetingId, setMeetingId] = useState(null); const [inputContent, setInputContent] = useState(''); + const token = 'token'; + + const createMeeting = async ({ token }: { token: string }) => { + const res = await fetch(`https://api.videosdk.live/v2/rooms`, { + method: 'POST', + headers: { + authorization: `${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ customRoomId: 'aaa-bbb-ccc' }) + }); + //Destructuring the roomId from the response + console.log('res: ', await res.json()); + const { roomId }: { roomId: string } = await res.json(); + console.log('Here room id: ', roomId); + + return roomId; + }; + createMeeting({ token }); + const getMeetingAndToken = async (id?: string) => { + const meetingId = id == null ? await createMeeting({ token }) : id; + console.log('Checking meeting id: ', meetingId); + setMeetingId(meetingId); + }; + + function JoinScreen({ + getMeetingAndToken + }: { + getMeetingAndToken: (meeting?: string) => void; + }) { + return null; + } const wrapperStyles = { border: `2px solid #f2fbf8`, @@ -89,6 +129,28 @@ const Chatroom = (props): JSX.Element => { }); }; + // const getMeetingId = async (token) => { + // try { + // const VIDEOSDK_API_ENDPOINT = `${LOCAL_SERVER_URL}/create-meeting`; + // const options = { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json' + // }, + // body: JSON.stringify({ token }) + // }; + // const response = await fetch(VIDEOSDK_API_ENDPOINT, options) + // .then(async (result) => { + // const { meetingId } = await result.json(); + // return meetingId; + // }) + // .catch((error) => console.log('error', error)); + // return response; + // } catch (e) { + // console.log(e); + // } + // }; + return (
{ {renderMessages()}
+ {/* {token && meetingId ? ( + +

{meetingId}

+
+ ) : ( + + )} */} { }); // update user list when there's a change: new join or leave the room - socket.on('updateUserList', (messageData) => { + socket.on('update room info', (messageData) => { //console.log('user list received from server'); dispatch(setUserList(messageData.userList)); + dispatch(setMeetingId(messageData.meetingId)); }); socket.on('new chat message', (messageData) => { diff --git a/app/src/redux/reducers/slice/roomSlice.ts b/app/src/redux/reducers/slice/roomSlice.ts index 10bb72fd7..50be9ab3c 100644 --- a/app/src/redux/reducers/slice/roomSlice.ts +++ b/app/src/redux/reducers/slice/roomSlice.ts @@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit'; const initialState = { roomCode: '', + meetingId: '', userName: '', userList: [], userJoined: false, @@ -16,6 +17,9 @@ const roomSlice = createSlice({ setRoomCode: (state, action) => { state.roomCode = action.payload; }, + setMeetingId: (state, action) => { + state.meetingId = action.payload; + }, setUserName: (state, action) => { state.userName = action.payload; }, @@ -37,6 +41,7 @@ const roomSlice = createSlice({ // Exports the action creator function to be used with useDispatch export const { setRoomCode, + setMeetingId, setUserName, setUserList, setUserJoined, diff --git a/server/server.ts b/server/server.ts index f6102d93e..d163dfc42 100644 --- a/server/server.ts +++ b/server/server.ts @@ -106,10 +106,30 @@ io.on('connection', (client) => { try { //if no room exists, add room to list if (!roomLists[roomCode]) { + const createMeeting = async () => { + const VITE_VIDEOSDK_TOKEN = 'token'; + + const res = await fetch(`https://api.videosdk.live/v2/rooms`, { + method: 'POST', + headers: { + // authorization: `${import.meta.env.VITE_VIDEOSDK_TOKEN}`, + authorization: VITE_VIDEOSDK_TOKEN, + 'Content-Type': 'application/json' + }, + // body: JSON.stringify({ customRoomId: roomCode }) + body: JSON.stringify({ customRoomId: 'aaabbb' }) + }); + // //Destructuring the roomId from the response + const { roomId }: { roomId: string } = await res.json(); + return roomId; + }; roomLists[roomCode] = {}; + roomLists[roomCode].userList = {}; + roomLists[roomCode].meetingId = await createMeeting(); } - roomLists[roomCode][client.id] = userName; // adding user into the room list with id: userName on server side - const userList = Object.keys(roomLists[roomCode]); //userList for roomCode + + roomLists[roomCode]['userList'][client.id] = userName; // adding user into the room list with id: userName on server side + const userList = Object.keys(roomLists[roomCode]['userList']); //userList for roomCode const hostID = userList[0]; // host is always assigned to user at index zero const newClientID = userList[userList.length - 1]; // new client id is always the last index in userList @@ -118,6 +138,7 @@ io.on('connection', (client) => { .timeout(5000) .to(hostID) // sends only to host .emitWithAck('requesting state from host'); //sending request + console.log('Checking hostState: ', hostState); //share host's state with the latest user const newClientResponse = await io //send the requested host state to the new client awaiting for the host state to come back before doing other task @@ -131,11 +152,11 @@ io.on('connection', (client) => { // console.log('a user joined the room'); //send the message to all clients in room but the sender io.to(roomCode).emit( - 'updateUserList', + 'update room info', { - userList: Object.values(roomLists[roomCode]), - activity: { nickName: userName, status: 'JOIN' } - } // send updated userList to all users in room + userList: Object.values(roomLists[roomCode]['userList']), + meetingId: roomLists[roomCode].meetingId + } // send updated room info to all users in the chat room ); io.to(roomCode).emit('new chat message', { userName, @@ -161,15 +182,15 @@ io.on('connection', (client) => { //disconnecting functionality client.on('disconnecting', () => { const roomCode = Array.from(client.rooms)[1]; //grabbing current room client was in when disconnecting - const userName = roomLists[roomCode][client.id]; - delete roomLists[roomCode][client.id]; + const userName = roomLists[roomCode]['userList'][client.id]; + delete roomLists[roomCode]['userList'][client.id]; //if room empty, delete room from room list - if (!Object.keys(roomLists[roomCode]).length) { + if (!Object.keys(roomLists[roomCode]['userList']).length) { delete roomLists[roomCode]; } else { //else emit updated user list io.to(roomCode).emit('updateUserList', { - userList: Object.values(roomLists[roomCode]) + userList: Object.values(roomLists[roomCode]['userList']) }); io.to(roomCode).emit('new chat message', { userName, @@ -215,10 +236,10 @@ io.on('connection', (client) => { client.on('clearCanvasAction', async (roomCode: string, userName: string) => { if (roomCode) { // server send clear canvas to everyone in the room if action is from the host - if (userName === Object.values(roomLists[roomCode])[0]) { + if (userName === Object.values(roomLists[roomCode]['userList'])[0]) { io.to(roomCode).emit( 'clear canvas from server', - Object.values(roomLists[roomCode]) + Object.values(roomLists[roomCode]['userList']) ); } } From fc33dcb466e7bb15ec4f4622f4ae1777be43f7c3 Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Mon, 12 Feb 2024 16:47:17 -0500 Subject: [PATCH 048/221] remove unused data --- server/server.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/server.ts b/server/server.ts index f6102d93e..867be6720 100644 --- a/server/server.ts +++ b/server/server.ts @@ -133,8 +133,7 @@ io.on('connection', (client) => { io.to(roomCode).emit( 'updateUserList', { - userList: Object.values(roomLists[roomCode]), - activity: { nickName: userName, status: 'JOIN' } + userList: Object.values(roomLists[roomCode]) } // send updated userList to all users in room ); io.to(roomCode).emit('new chat message', { From 61e85e8390968522b10be9db7f46224ebd7ed1e2 Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Mon, 12 Feb 2024 16:48:47 -0500 Subject: [PATCH 049/221] remove unused folder from dev --- app/src/components/left/RoomsContainer.tsx | 1 - app/src/components/socketUtils/socket.tsx | 233 --------------------- 2 files changed, 234 deletions(-) delete mode 100644 app/src/components/socketUtils/socket.tsx diff --git a/app/src/components/left/RoomsContainer.tsx b/app/src/components/left/RoomsContainer.tsx index 4c64b8a3d..39a3eb1cd 100644 --- a/app/src/components/left/RoomsContainer.tsx +++ b/app/src/components/left/RoomsContainer.tsx @@ -52,7 +52,6 @@ import { DeleteContextPayload, addComponentToContext } from '../../../src/redux/reducers/slice/contextReducer'; -// import { initSocketConnection } from '../socketUtils/socket'; const RoomsContainer = () => { const dispatch = useDispatch(); diff --git a/app/src/components/socketUtils/socket.tsx b/app/src/components/socketUtils/socket.tsx deleted file mode 100644 index 3858d7e27..000000000 --- a/app/src/components/socketUtils/socket.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { useDispatch, useSelector } from 'react-redux'; -import { RootState } from '../../redux/store'; -import { BottomPanelObj } from '../../interfaces/Interfaces'; -import { - allCooperativeState, - addChild, - changeFocus, - deleteChild, - changePosition, - updateStateUsed, - updateUseContext, - updateCss, - updateAttributes, - updateEvents, - addComponent, - addElement, - addState, - deleteState, - addPassedInProps, - deletePassedInProps, - deleteElement, - resetAllState, - updateStylesheet -} from '../../redux/reducers/slice/appStateSlice'; -import { - addContext, - deleteContext, - addContextValues -} from '../../redux/reducers/slice/contextReducer'; -import { setUserList } from '../../redux/reducers/slice/roomSlice'; -import { codePreviewCooperative } from '../../redux/reducers/slice/codePreviewSlice'; -import { cooperativeStyle } from '../../redux/reducers/slice/styleSlice'; -import store from '../../redux/store'; -import { initializeSocket, getSocket } from '../../helperFunctions/socket'; -import { - AddContextPayload, - AddContextValuesPayload, - DeleteContextPayload, - addComponentToContext -} from '../../redux/reducers/slice/contextReducer'; - -export const initSocketConnection = (roomCode: string) => { - // helper function to create socket connection - initializeSocket(); - // assign socket to result of helper function to return socket created - const socket = getSocket(); - // if socket was created correctly and exists - if (socket) { - //run everytime when a client connects to server - socket.on('connect', () => { - socket.emit('joining', userName, roomCode); - // console.log(`${userName} Joined room ${roomCode} from RoomsContainer`); - }); - - //If you are the host: send current state to server when a new user joins - socket.on('requesting state from host', (callback) => { - const newState = store.getState(); //pull the current state - callback(newState); //send it to backend server - }); - - //If you are the new user: receive the state from the host - socket.on('server emitting state from host', (state, callback) => { - //dispatching new state to change user current state - // console.log('state received by new join:', state); - store.dispatch(allCooperativeState(state.appState)); - store.dispatch(codePreviewCooperative(state.codePreviewCooperative)); - store.dispatch(cooperativeStyle(state.styleSlice)); - callback({ status: 'confirmed' }); - }); - - // update user list when there's a change: new join or leave the room - socket.on('updateUserList', (newUserList) => { - //console.log('user list received from server'); - dispatch(setUserList(newUserList)); - }); - - // dispatch add child to local state when element has been added by another user - socket.on('child data from server', (childData: object) => { - // console.log('child data received by users', childData); - store.dispatch(addChild(childData)); - }); - - // dispatch changeFocus to local state when another user has changed focus by selecting element on canvas - socket.on('focus data from server', (focusData: object) => { - // console.log('focus data received from server', focusData); - store.dispatch(changeFocus(focusData)); - }); - - // dispatch deleteChild to local state when another user has deleted an element - socket.on('delete data from server', (deleteData: object) => { - // console.log('delete data received from server', deleteData); - store.dispatch(deleteChild(deleteData)); - }); - - // dispatch delete element to local state when another user has deleted an element - socket.on( - 'delete element data from server', - (deleteElementData: object) => { - // console.log('delete element data received from server', deleteElementData); - store.dispatch(deleteElement(deleteElementData)); - } - ); - - // dispatch all updates to local state when another user has saved from Bottom Panel - socket.on('update data from server', (updateData: BottomPanelObj) => { - // console.log('update data received from server', updateData); - store.dispatch( - updateStateUsed({ - stateUsedObj: updateData.stateUsedObj, - contextParam: updateData.contextParam - }) - ); - store.dispatch( - updateUseContext({ - useContextObj: updateData.useContextObj, - contextParam: updateData.contextParam - }) - ); - store.dispatch( - updateCss({ - style: updateData.style, - contextParam: updateData.contextParam - }) - ); - store.dispatch( - updateAttributes({ - attributes: updateData.attributes, - contextParam: updateData.contextParam - }) - ); - store.dispatch( - updateEvents({ - events: updateData.events, - contextParam: updateData.contextParam - }) - ); - }); - - // dispatch update style in local state when CSS panel is updated on their side - socket.on('update css data from server', (cssData: object) => { - // console.log('CSS data received from server', cssData); - store.dispatch(updateStylesheet(cssData)); - }); - - // dispatch new item position in local state when item position is changed by another user - socket.on( - 'item position data from server', - (itemPositionData: object) => { - // console.log( - // 'item position data received from server', - // itemPositionData - // ); - store.dispatch(changePosition(itemPositionData)); - } - ); - - // dispatch addComponent to local state when new component is created by another user - socket.on('new component data from server', (newComponent: object) => { - store.dispatch(addComponent(newComponent)); - }); - - // dispatch addElement to local state when new element is created by another user - socket.on('new element data from server', (newElement: object) => { - store.dispatch(addElement(newElement)); - }); - - // dispatch addState to local state when component state has been changed by another user - socket.on( - 'new component state data from server', - (componentState: object) => { - store.dispatch(addState(componentState)); - } - ); - - // dispatch deleteState to local state when component state has been deleted by another user - socket.on( - 'delete component state data from server', - (componentStateDelete: object) => { - store.dispatch(deleteState(componentStateDelete)); - } - ); - - // dispatch addPassedInProps to local state when p.I.P have been added by another user - socket.on( - 'new PassedInProps data from server', - (passedInProps: object) => { - store.dispatch(addPassedInProps(passedInProps)); - } - ); - - // dispatch deletePassedInProps to local state when p.I.P have been deleted by another user - socket.on( - 'PassedInProps delete data from server', - (passedInProps: object) => { - store.dispatch(deletePassedInProps(passedInProps)); - } - ); - - // dispatch addContext to local state when context has been changed by another user - socket.on('new context from server', (context: AddContextPayload) => { - store.dispatch(addContext(context)); - }); - - // dispatch addContextValues to local state when context values are added by another user - socket.on( - 'new context value from server', - (contextVal: AddContextValuesPayload) => { - store.dispatch(addContextValues(contextVal)); - } - ); - - // dispatch deleteContext to local state when context is deleted by another user - socket.on( - 'delete context data from server', - (context: DeleteContextPayload) => { - store.dispatch(deleteContext(context)); - } - ); - - // dispatch addComponentToContext to local state when context is assigned to component by another user - socket.on('assign context data from server', (data) => { - store.dispatch( - addComponentToContext({ - context: data.context, - component: data.component - }) - ); - store.dispatch( - deleteElement({ id: 'FAKE_ID', contextParam: data.contextParam }) - ); - }); - } -} From 01a11457ab419260a693afa8baf462f25ddba901 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 12 Feb 2024 18:06:37 -0500 Subject: [PATCH 050/221] fixed netable properties of certain elements --- app/src/helperFunctions/generateCode.ts | 4 +++ app/src/helperFunctions/renderChildren.tsx | 29 +++++++++++----------- app/src/redux/HTMLTypes.ts | 11 ++++---- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/app/src/helperFunctions/generateCode.ts b/app/src/helperFunctions/generateCode.ts index c92794cb9..92aa26d8e 100644 --- a/app/src/helperFunctions/generateCode.ts +++ b/app/src/helperFunctions/generateCode.ts @@ -270,7 +270,11 @@ const generateUnformattedCode = ( activeLink = '"' + childElement.attributes.compLink + '"'; } } + const nestable = + childElement.tag === 'h1' || + childElement.tag === 'h2' || + childElement.tag === 'p' || childElement.tag === 'div' || childElement.tag === 'form' || childElement.tag === 'ol' || diff --git a/app/src/helperFunctions/renderChildren.tsx b/app/src/helperFunctions/renderChildren.tsx index 20c06c6ff..a0badbb4f 100644 --- a/app/src/helperFunctions/renderChildren.tsx +++ b/app/src/helperFunctions/renderChildren.tsx @@ -30,8 +30,7 @@ const renderChildren = (children: ChildElement[]) => { type={type} typeId={typeId} key={'DirChildComp' + childId.toString() + name} - name={name} - /> + name={name} style={undefined} attributes={undefined} events={undefined} stateProps={[]} passedInProps={[]} /> ); } // child is a non-nestable type of HTML element (aka NOT divs, forms, OrderedLists, UnorderedLists, menus) @@ -41,6 +40,10 @@ const renderChildren = (children: ChildElement[]) => { typeId !== 1000 && typeId !== 2 && typeId !== 3 && + typeId !== 4 && + typeId !== 6 && + typeId !== 8 && + typeId !== 9 && typeId !== 14 && typeId !== 15 && typeId !== 16 && @@ -55,8 +58,7 @@ const renderChildren = (children: ChildElement[]) => { type={type} typeId={typeId} key={'DirChildHTML' + childId.toString() + name} - name={name} - /> + name={name} style={undefined} attributes={undefined} events={undefined} stateProps={[]} passedInProps={[]} /> ); } // child is a nestable type of HTML element (divs, forms, OrderedLists, UnorderedLists, menus) @@ -65,6 +67,10 @@ const renderChildren = (children: ChildElement[]) => { (typeId === 11 || typeId === 2 || typeId === 3 || + typeId === 4 || + typeId === 6 || + typeId === 8 || + typeId === 9 || typeId === 14 || typeId === 15 || typeId === 16 || @@ -90,8 +96,7 @@ const renderChildren = (children: ChildElement[]) => { children={children} key={'DirChildHTMLNest' + childId.toString() + name} name={name} - attributes={attributes} - /> + attributes={attributes} style={undefined} events={undefined} stateProps={[]} passedInProps={[]} /> ); } else if (type === 'HTML Element' && typeId === 1000) { return ( @@ -100,14 +105,11 @@ const renderChildren = (children: ChildElement[]) => { type={type} typeId={typeId} children={children} - key={ - 'SeparatorChild' + + key={'SeparatorChild' + childId.toString() + name + - (Math.random() * 1000).toString() - } - name={name} - /> + (Math.random() * 1000).toString()} + name={name} style={undefined} attributes={undefined} events={undefined} stateProps={[]} passedInProps={[]} /> ); } // A route link is a next.js or gatsby.js navigation link @@ -120,8 +122,7 @@ const renderChildren = (children: ChildElement[]) => { typeId={typeId} children={children} key={'RouteLink' + childId.toString() + name} - name={name} - /> + name={name} style={undefined} attributes={undefined} events={undefined} stateProps={[]} passedInProps={[]} /> ); } }); diff --git a/app/src/redux/HTMLTypes.ts b/app/src/redux/HTMLTypes.ts index 3bc27011c..e47a744b6 100644 --- a/app/src/redux/HTMLTypes.ts +++ b/app/src/redux/HTMLTypes.ts @@ -1,3 +1,4 @@ +import { TroubleshootSharp } from '@mui/icons-material'; import { HTMLType } from '../interfaces/Interfaces'; //properties for all HTML components @@ -47,7 +48,7 @@ const HTMLTypes: HTMLType[] = [ placeHolderLong: '', icon: 'TextFormat', framework: 'reactClassic', - nestable: false + nestable: true }, { id: 4, @@ -58,7 +59,7 @@ const HTMLTypes: HTMLType[] = [ placeHolderLong: '', icon: 'TextFormat', framework: 'reactClassic', - nestable: false + nestable: TroubleshootSharp }, { id: 6, @@ -69,7 +70,7 @@ const HTMLTypes: HTMLType[] = [ placeHolderLong: '', icon: 'ShortTextOutlined', framework: 'reactClassic', - nestable: false + nestable: true }, { id: 8, @@ -80,7 +81,7 @@ const HTMLTypes: HTMLType[] = [ placeHolderLong: '', icon: 'NotesOutlined', framework: 'reactClassic', - nestable: false + nestable: true }, { id: 9, @@ -102,7 +103,7 @@ const HTMLTypes: HTMLType[] = [ placeHolderLong: '', icon: 'EditOutlined', framework: 'reactClassic', - nestable: false + nestable: true }, { id: 5, From 825a375da01c93d5a36bf3716b02033af1a06217 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 12 Feb 2024 18:21:24 -0500 Subject: [PATCH 051/221] added button and form to be nestable --- app/src/helperFunctions/generateCode.ts | 5 +++ app/src/helperFunctions/renderChildren.tsx | 47 ++++++++++++++++++---- app/src/redux/HTMLTypes.ts | 4 +- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/app/src/helperFunctions/generateCode.ts b/app/src/helperFunctions/generateCode.ts index 92aa26d8e..d71983400 100644 --- a/app/src/helperFunctions/generateCode.ts +++ b/app/src/helperFunctions/generateCode.ts @@ -76,6 +76,10 @@ const generateUnformattedCode = ( ); child['tag'] = referencedHTML.tag; if ( + referencedHTML.tag === 'h1' || + referencedHTML.tag === 'h2' || + referencedHTML.tag === 'button' || + referencedHTML.tag === 'span' || referencedHTML.tag === 'div' || referencedHTML.tag === 'separator' || referencedHTML.tag === 'form' || @@ -274,6 +278,7 @@ const generateUnformattedCode = ( const nestable = childElement.tag === 'h1' || childElement.tag === 'h2' || + childElement.tag === 'button' || childElement.tag === 'p' || childElement.tag === 'div' || childElement.tag === 'form' || diff --git a/app/src/helperFunctions/renderChildren.tsx b/app/src/helperFunctions/renderChildren.tsx index a0badbb4f..f2890a9ec 100644 --- a/app/src/helperFunctions/renderChildren.tsx +++ b/app/src/helperFunctions/renderChildren.tsx @@ -30,7 +30,13 @@ const renderChildren = (children: ChildElement[]) => { type={type} typeId={typeId} key={'DirChildComp' + childId.toString() + name} - name={name} style={undefined} attributes={undefined} events={undefined} stateProps={[]} passedInProps={[]} /> + name={name} + style={undefined} + attributes={undefined} + events={undefined} + stateProps={[]} + passedInProps={[]} + /> ); } // child is a non-nestable type of HTML element (aka NOT divs, forms, OrderedLists, UnorderedLists, menus) @@ -41,6 +47,7 @@ const renderChildren = (children: ChildElement[]) => { typeId !== 2 && typeId !== 3 && typeId !== 4 && + typeId !== 5 && typeId !== 6 && typeId !== 8 && typeId !== 9 && @@ -58,7 +65,13 @@ const renderChildren = (children: ChildElement[]) => { type={type} typeId={typeId} key={'DirChildHTML' + childId.toString() + name} - name={name} style={undefined} attributes={undefined} events={undefined} stateProps={[]} passedInProps={[]} /> + name={name} + style={undefined} + attributes={undefined} + events={undefined} + stateProps={[]} + passedInProps={[]} + /> ); } // child is a nestable type of HTML element (divs, forms, OrderedLists, UnorderedLists, menus) @@ -68,6 +81,7 @@ const renderChildren = (children: ChildElement[]) => { typeId === 2 || typeId === 3 || typeId === 4 || + typeId === 5 || typeId === 6 || typeId === 8 || typeId === 9 || @@ -96,7 +110,12 @@ const renderChildren = (children: ChildElement[]) => { children={children} key={'DirChildHTMLNest' + childId.toString() + name} name={name} - attributes={attributes} style={undefined} events={undefined} stateProps={[]} passedInProps={[]} /> + attributes={attributes} + style={undefined} + events={undefined} + stateProps={[]} + passedInProps={[]} + /> ); } else if (type === 'HTML Element' && typeId === 1000) { return ( @@ -105,11 +124,19 @@ const renderChildren = (children: ChildElement[]) => { type={type} typeId={typeId} children={children} - key={'SeparatorChild' + + key={ + 'SeparatorChild' + childId.toString() + name + - (Math.random() * 1000).toString()} - name={name} style={undefined} attributes={undefined} events={undefined} stateProps={[]} passedInProps={[]} /> + (Math.random() * 1000).toString() + } + name={name} + style={undefined} + attributes={undefined} + events={undefined} + stateProps={[]} + passedInProps={[]} + /> ); } // A route link is a next.js or gatsby.js navigation link @@ -122,7 +149,13 @@ const renderChildren = (children: ChildElement[]) => { typeId={typeId} children={children} key={'RouteLink' + childId.toString() + name} - name={name} style={undefined} attributes={undefined} events={undefined} stateProps={[]} passedInProps={[]} /> + name={name} + style={undefined} + attributes={undefined} + events={undefined} + stateProps={[]} + passedInProps={[]} + /> ); } }); diff --git a/app/src/redux/HTMLTypes.ts b/app/src/redux/HTMLTypes.ts index e47a744b6..02c6c6cf2 100644 --- a/app/src/redux/HTMLTypes.ts +++ b/app/src/redux/HTMLTypes.ts @@ -59,7 +59,7 @@ const HTMLTypes: HTMLType[] = [ placeHolderLong: '', icon: 'TextFormat', framework: 'reactClassic', - nestable: TroubleshootSharp + nestable: true }, { id: 6, @@ -114,7 +114,7 @@ const HTMLTypes: HTMLType[] = [ placeHolderLong: '', icon: 'EditAttributes', framework: 'reactClassic', - nestable: false + nestable: true }, { From dc44a4e5e87d917b08c2dd8db316b0b0d5f0f45c Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 12 Feb 2024 18:28:10 -0500 Subject: [PATCH 052/221] added span to be nestable as well --- app/src/helperFunctions/generateCode.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/helperFunctions/generateCode.ts b/app/src/helperFunctions/generateCode.ts index d71983400..2184614fd 100644 --- a/app/src/helperFunctions/generateCode.ts +++ b/app/src/helperFunctions/generateCode.ts @@ -278,6 +278,7 @@ const generateUnformattedCode = ( const nestable = childElement.tag === 'h1' || childElement.tag === 'h2' || + childElement.tag === 'span' || childElement.tag === 'button' || childElement.tag === 'p' || childElement.tag === 'div' || From a41cc744594bd065a6cd6dbed274814cb45d6b1f Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Mon, 12 Feb 2024 18:42:35 -0500 Subject: [PATCH 053/221] save progres --- app/src/components/bottom/ChatRoom.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/components/bottom/ChatRoom.tsx b/app/src/components/bottom/ChatRoom.tsx index 9dfddfdc2..8ee6258ba 100644 --- a/app/src/components/bottom/ChatRoom.tsx +++ b/app/src/components/bottom/ChatRoom.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import { useSelector } from 'react-redux'; import { RootState } from '../../redux/store'; import { emitEvent } from '../../helperFunctions/socket'; +import { setMeetingId } from '../../redux/reducers/slice/roomSlice'; import { MeetingProvider, MeetingConsumer, @@ -15,9 +16,12 @@ const Chatroom = (props): JSX.Element => { const userName = useSelector((store: RootState) => store.roomSlice.userName); const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); const messages = useSelector((store: RootState) => store.roomSlice.messages); - const [meetingId, setMeetingId] = useState(null); + const meetingId = useSelector( + (store: RootState) => store.roomSlice.meetingId + ); const [inputContent, setInputContent] = useState(''); + const [joinMeeting, setJoinMeeting] = useState(false); const token = 'token'; const createMeeting = async ({ token }: { token: string }) => { @@ -30,7 +34,6 @@ const Chatroom = (props): JSX.Element => { body: JSON.stringify({ customRoomId: 'aaa-bbb-ccc' }) }); //Destructuring the roomId from the response - console.log('res: ', await res.json()); const { roomId }: { roomId: string } = await res.json(); console.log('Here room id: ', roomId); @@ -157,10 +160,16 @@ const Chatroom = (props): JSX.Element => { style={{ paddingLeft: '10px', width: '100%', height: '100%' }} >
+ {token && meetingId && joinMeeting ? ( +

Start meeting

+ ) : ( + + )}
{renderMessages()}
+ {/* {token && meetingId ? ( Date: Mon, 12 Feb 2024 18:55:35 -0500 Subject: [PATCH 054/221] Add div for input --- app/src/components/bottom/ChatRoom.tsx | 34 ++++++++++++++------------ 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/app/src/components/bottom/ChatRoom.tsx b/app/src/components/bottom/ChatRoom.tsx index 24e22294f..191606a8b 100644 --- a/app/src/components/bottom/ChatRoom.tsx +++ b/app/src/components/bottom/ChatRoom.tsx @@ -108,22 +108,24 @@ const Chatroom = (props): JSX.Element => { {renderMessages()}
- - setInputContent(e.target.value)} - value={inputContent} - style={inputStyles} - /> - - +
+
+ setInputContent(e.target.value)} + value={inputContent} + style={inputStyles} + /> + +
+
); }; From 84d068c1df3ba89e214f3769d000af1321bf9dc2 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 12 Feb 2024 18:59:53 -0500 Subject: [PATCH 055/221] added all but label, img and image to be nestable. A tag code preview is not working as intended --- app/src/helperFunctions/generateCode.ts | 6 ++++-- app/src/helperFunctions/renderChildren.tsx | 2 ++ app/src/redux/HTMLTypes.ts | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/helperFunctions/generateCode.ts b/app/src/helperFunctions/generateCode.ts index 2184614fd..bd9b1e9ec 100644 --- a/app/src/helperFunctions/generateCode.ts +++ b/app/src/helperFunctions/generateCode.ts @@ -78,6 +78,8 @@ const generateUnformattedCode = ( if ( referencedHTML.tag === 'h1' || referencedHTML.tag === 'h2' || + referencedHTML.tag === 'a' || + referencedHTML.tag === 'p' || referencedHTML.tag === 'button' || referencedHTML.tag === 'span' || referencedHTML.tag === 'div' || @@ -89,8 +91,7 @@ const generateUnformattedCode = ( referencedHTML.tag === 'li' || referencedHTML.tag === 'Link' || referencedHTML.tag === 'Switch' || - referencedHTML.tag === 'Route' || - referencedHTML.tag === 'Image' + referencedHTML.tag === 'Route' ) { child.children = getEnrichedChildren(child); } @@ -278,6 +279,7 @@ const generateUnformattedCode = ( const nestable = childElement.tag === 'h1' || childElement.tag === 'h2' || + childElement.tag === 'a' || childElement.tag === 'span' || childElement.tag === 'button' || childElement.tag === 'p' || diff --git a/app/src/helperFunctions/renderChildren.tsx b/app/src/helperFunctions/renderChildren.tsx index f2890a9ec..bbae5960d 100644 --- a/app/src/helperFunctions/renderChildren.tsx +++ b/app/src/helperFunctions/renderChildren.tsx @@ -44,6 +44,7 @@ const renderChildren = (children: ChildElement[]) => { type === 'HTML Element' && typeId !== 11 && typeId !== 1000 && + typeId !== 1 && typeId !== 2 && typeId !== 3 && typeId !== 4 && @@ -78,6 +79,7 @@ const renderChildren = (children: ChildElement[]) => { else if ( type === 'HTML Element' && (typeId === 11 || + typeId === 1 || typeId === 2 || typeId === 3 || typeId === 4 || diff --git a/app/src/redux/HTMLTypes.ts b/app/src/redux/HTMLTypes.ts index 02c6c6cf2..c0d3fcf39 100644 --- a/app/src/redux/HTMLTypes.ts +++ b/app/src/redux/HTMLTypes.ts @@ -37,7 +37,7 @@ const HTMLTypes: HTMLType[] = [ placeHolderLong: '', icon: 'Link', framework: 'reactClassic', - nestable: false + nestable: true }, { id: 2, @@ -103,7 +103,7 @@ const HTMLTypes: HTMLType[] = [ placeHolderLong: '', icon: 'EditOutlined', framework: 'reactClassic', - nestable: true + nestable: false }, { id: 5, @@ -137,7 +137,7 @@ const HTMLTypes: HTMLType[] = [ placeHolderLong: '', icon: 'MoreOutlined', framework: 'reactClassic', - nestable: false + nestable: true }, { id: 14, From 252f6db697a16ac7cb3c5c24fa5fb3711fb8674a Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Mon, 12 Feb 2024 19:16:13 -0500 Subject: [PATCH 056/221] Remove comments --- app/src/redux/reducers/slice/roomSlice.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/redux/reducers/slice/roomSlice.ts b/app/src/redux/reducers/slice/roomSlice.ts index 10bb72fd7..07a0f03fb 100644 --- a/app/src/redux/reducers/slice/roomSlice.ts +++ b/app/src/redux/reducers/slice/roomSlice.ts @@ -34,7 +34,6 @@ const roomSlice = createSlice({ } }); -// Exports the action creator function to be used with useDispatch export const { setRoomCode, setUserName, @@ -43,6 +42,5 @@ export const { setMessages, setPassword } = roomSlice.actions; -// Exports so we can combine in rootReducer export default roomSlice.reducer; From 2ac671de94fa854987c5d547512a23a7dcab440a Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 12 Feb 2024 19:30:49 -0500 Subject: [PATCH 057/221] changed sperator div color when dragging in hovered elements --- app/src/components/main/SeparatorChild.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/main/SeparatorChild.tsx b/app/src/components/main/SeparatorChild.tsx index 1934dbf95..991491272 100644 --- a/app/src/components/main/SeparatorChild.tsx +++ b/app/src/components/main/SeparatorChild.tsx @@ -149,7 +149,7 @@ function DirectChildHTMLNestable({ }; defaultNestableStyle['backgroundColor'] = isOver - ? '#70d8be' + ? 'rgb(53, 78, 156)' : 'rgba(0, 0, 255, 0.0)'; const combinedStyle = combineStyles( From cf97ab41787501efa08300ec8f667ba7d049703d Mon Sep 17 00:00:00 2001 From: John Wage Date: Mon, 12 Feb 2024 20:35:47 -0500 Subject: [PATCH 058/221] Revise click functionality on bottom menu. Remove vanilla JS and replaced with react state and dynamic CSS styling. Need to fix drag functionality --- app/src/components/bottom/BottomPanel.tsx | 3 +-- app/src/components/bottom/BottomTabs.tsx | 13 ++++++++++--- app/src/components/main/Canvas.tsx | 4 ++-- app/src/containers/MainContainer.tsx | 16 ++++++++++++++-- app/src/public/styles/style.css | 19 ------------------- 5 files changed, 27 insertions(+), 28 deletions(-) diff --git a/app/src/components/bottom/BottomPanel.tsx b/app/src/components/bottom/BottomPanel.tsx index 526196e48..070a2e80c 100644 --- a/app/src/components/bottom/BottomPanel.tsx +++ b/app/src/components/bottom/BottomPanel.tsx @@ -28,7 +28,6 @@ const BottomPanel = (props): JSX.Element => { }; const mouseMoveHandler = function (e: MouseEvent): void { - const dy = y - e.clientY; const newVal = h + dy; @@ -54,7 +53,7 @@ const BottomPanel = (props): JSX.Element => {
props.setBottomShow(true)} + onClick={() => props.setBottomShow(!props.bottomShow)} tabIndex={0} > {props.bottomShow ? : } diff --git a/app/src/components/bottom/BottomTabs.tsx b/app/src/components/bottom/BottomTabs.tsx index cd5497ff9..01b5a5d4a 100644 --- a/app/src/components/bottom/BottomTabs.tsx +++ b/app/src/components/bottom/BottomTabs.tsx @@ -39,6 +39,10 @@ const BottomTabs = (props): JSX.Element => { arrow.renderArrow(state.canvasFocus?.childId); + const showBottomPanel = () => { + props.setBottomShow(true); + }; + return (
{ zIndex: 1, borderTop: '2px solid grey' }} - onClick={() => { - props.setBottomShow(true); - }} > { disableRipple classes={{ root: classes.tabRoot, selected: classes.tabSelected }} label="Creation Panel" + onClick={showBottomPanel} />
diff --git a/app/src/components/main/Canvas.tsx b/app/src/components/main/Canvas.tsx index 85380079c..1b10499e8 100644 --- a/app/src/components/main/Canvas.tsx +++ b/app/src/components/main/Canvas.tsx @@ -363,6 +363,6 @@ const Canvas = (props: {}): JSX.Element => {
); -} +}; -export default Canvas; \ No newline at end of file +export default Canvas; diff --git a/app/src/containers/MainContainer.tsx b/app/src/containers/MainContainer.tsx index 84e8f16ef..8c4dc742d 100644 --- a/app/src/containers/MainContainer.tsx +++ b/app/src/containers/MainContainer.tsx @@ -109,7 +109,16 @@ const MainContainer = (props): JSX.Element => { setBottomShow(false); }; - let showPanel = bottomShow ? 'bottom-show' : 'bottom-hide'; + const hideBottomPanelStyles = { + maxHeight: '64px', + boxSizing: 'border-box', + transition: 'all 0.5s ease-in-out' + }; + + const showBottomPanelStyles = { + maxHeight: '100%', + transition: 'all 0.5s ease-in-out' + }; return (
@@ -117,7 +126,10 @@ const MainContainer = (props): JSX.Element => {
-
+
Date: Mon, 12 Feb 2024 21:09:34 -0500 Subject: [PATCH 059/221] merge dev --- app/src/components/bottom/ChatRoom.tsx | 35 +++++++++++++------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/app/src/components/bottom/ChatRoom.tsx b/app/src/components/bottom/ChatRoom.tsx index 18ec6c0c1..0b7df2e98 100644 --- a/app/src/components/bottom/ChatRoom.tsx +++ b/app/src/components/bottom/ChatRoom.tsx @@ -196,24 +196,23 @@ const Chatroom = (props): JSX.Element => { ) : ( )} */} -
- setInputContent(e.target.value)} - value={inputContent} - style={inputStyles} - /> - -
-
- {renderMessages()} +
+
+ setInputContent(e.target.value)} + value={inputContent} + style={inputStyles} + /> + +
//
From 4a9a26ad1b36a0e3d939a75161b702e2a56527f8 Mon Sep 17 00:00:00 2001 From: John Wage Date: Mon, 12 Feb 2024 22:06:37 -0500 Subject: [PATCH 060/221] Fix dragging of bottom panel, but there is a small bug when mouse is dragged beyond the bottom panel or arrow button --- app/src/components/bottom/BottomPanel.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/components/bottom/BottomPanel.tsx b/app/src/components/bottom/BottomPanel.tsx index 070a2e80c..7d30f2586 100644 --- a/app/src/components/bottom/BottomPanel.tsx +++ b/app/src/components/bottom/BottomPanel.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import BottomTabs from './BottomTabs'; import { ExpandLess, ExpandMore } from '@mui/icons-material'; @@ -7,6 +7,8 @@ const BottomPanel = (props): JSX.Element => { let h: number = 0; const node = useRef() as React.MutableRefObject; + const [isDragging, setIsDragging] = useState(false); + const mouseDownHandler = (e): void => { y = e.clientY; @@ -28,6 +30,8 @@ const BottomPanel = (props): JSX.Element => { }; const mouseMoveHandler = function (e: MouseEvent): void { + if (!props.bottomShow) return; // prevent drag calc to occur when bottom menu is not showing + const dy = y - e.clientY; const newVal = h + dy; @@ -37,6 +41,8 @@ const BottomPanel = (props): JSX.Element => { }; const mouseUpHandler = function () { + // puts false in callback queue after OnDragStart sets to true (b/c react 17 doesn't have onDragEnd) + setTimeout(() => setIsDragging(false), 0); document.removeEventListener('mousemove', mouseMoveHandler); document.removeEventListener('mouseup', mouseUpHandler); window.removeEventListener('message', handleIframeMessage); @@ -53,7 +59,9 @@ const BottomPanel = (props): JSX.Element => {
props.setBottomShow(!props.bottomShow)} + draggable + onDragStart={() => setIsDragging(true)} + onClick={() => !isDragging && props.setBottomShow(!props.bottomShow)} tabIndex={0} > {props.bottomShow ? : } From 952a0d4fff7b9e626bbd22776053348821064c4d Mon Sep 17 00:00:00 2001 From: John Wage Date: Tue, 13 Feb 2024 12:04:29 -0500 Subject: [PATCH 061/221] components and nested components on canvas can now be deleted on click without component first needing to be selected or focused into --- app/src/components/main/DeleteButton.tsx | 3 ++- app/src/components/main/DirectChildHTML.tsx | 1 + app/src/components/main/DirectChildHTMLNestable.tsx | 6 +++++- app/src/interfaces/Interfaces.ts | 1 + 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/components/main/DeleteButton.tsx b/app/src/components/main/DeleteButton.tsx index 42cae1c79..3f252ada7 100644 --- a/app/src/components/main/DeleteButton.tsx +++ b/app/src/components/main/DeleteButton.tsx @@ -5,7 +5,7 @@ import { deleteChild } from '../../redux/reducers/slice/appStateSlice'; import { RootState } from '../../redux/store'; import { emitEvent } from '../../helperFunctions/socket'; -function DeleteButton({ id, name }: DeleteButtons) { +function DeleteButton({ id, name, onClickHandler }: DeleteButtons) { const contextParam = useSelector((store: RootState) => store.contextSlice); const roomCode = useSelector((store: RootState) => store.roomSlice.roomCode); @@ -33,6 +33,7 @@ function DeleteButton({ id, name }: DeleteButtons) { id={'btn' + id} onClick={(event) => { event.stopPropagation(); + onClickHandler(event); deleteHTMLtype(id); }} > diff --git a/app/src/components/main/DirectChildHTML.tsx b/app/src/components/main/DirectChildHTML.tsx index 611213035..e6156ea7a 100644 --- a/app/src/components/main/DirectChildHTML.tsx +++ b/app/src/components/main/DirectChildHTML.tsx @@ -80,6 +80,7 @@ function DirectChildHTML({ childId, name, type, typeId, style }: ChildElement) {
diff --git a/app/src/components/main/DirectChildHTMLNestable.tsx b/app/src/components/main/DirectChildHTMLNestable.tsx index 2cbdaca1c..3011ecc0a 100644 --- a/app/src/components/main/DirectChildHTMLNestable.tsx +++ b/app/src/components/main/DirectChildHTMLNestable.tsx @@ -225,7 +225,11 @@ function DirectChildHTMLNestable({ {attributes && attributes.compLink ? ` ${attributes.compLink}` : ''} {routeButton} - + {renderChildren(children)}
diff --git a/app/src/interfaces/Interfaces.ts b/app/src/interfaces/Interfaces.ts index 53a92dc24..ee9e04ed4 100644 --- a/app/src/interfaces/Interfaces.ts +++ b/app/src/interfaces/Interfaces.ts @@ -116,6 +116,7 @@ export interface LoginInt { export interface DeleteButtons { id: number; name: string; + onClickHandler: (event: any) => void; } export interface StatePropsPanelProps { selectHandler: (table: any) => void; From da49eebc57c0200b2b84981f5d2d8c2950a58366 Mon Sep 17 00:00:00 2001 From: John Wage Date: Tue, 13 Feb 2024 12:26:37 -0500 Subject: [PATCH 062/221] Add CSS styling and Material UI delete icon --- app/src/components/main/DeleteButton.tsx | 5 +++-- app/src/public/styles/style.css | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/components/main/DeleteButton.tsx b/app/src/components/main/DeleteButton.tsx index 3f252ada7..a27967860 100644 --- a/app/src/components/main/DeleteButton.tsx +++ b/app/src/components/main/DeleteButton.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useState } from 'react'; import { DeleteButtons } from '../../interfaces/Interfaces'; import { useDispatch, useSelector } from 'react-redux'; import { deleteChild } from '../../redux/reducers/slice/appStateSlice'; import { RootState } from '../../redux/store'; import { emitEvent } from '../../helperFunctions/socket'; +import { Delete } from '@mui/icons-material'; function DeleteButton({ id, name, onClickHandler }: DeleteButtons) { const contextParam = useSelector((store: RootState) => store.contextSlice); @@ -37,7 +38,7 @@ function DeleteButton({ id, name, onClickHandler }: DeleteButtons) { deleteHTMLtype(id); }} > - x +
); diff --git a/app/src/public/styles/style.css b/app/src/public/styles/style.css index 8dcab2184..9648422be 100644 --- a/app/src/public/styles/style.css +++ b/app/src/public/styles/style.css @@ -746,9 +746,14 @@ CANVAS WINDOW 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); } -.delete-button-empty:hover { +.deleteIcon { + font-size: 18px; + color: #4a4a4a; + cursor: pointer; +} + +.deleteIcon:hover { color: white; - background-color: rgba(0, 100, 0, 0.4); } .componentContainer { From 0f4b093c03a109cf82dff4fb71bb78db0d69ba93 Mon Sep 17 00:00:00 2001 From: John Wage Date: Tue, 13 Feb 2024 12:38:59 -0500 Subject: [PATCH 063/221] delete unused useState hook in DeleteButton.tsx --- app/src/components/main/DeleteButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/main/DeleteButton.tsx b/app/src/components/main/DeleteButton.tsx index a27967860..903f89fa0 100644 --- a/app/src/components/main/DeleteButton.tsx +++ b/app/src/components/main/DeleteButton.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { DeleteButtons } from '../../interfaces/Interfaces'; import { useDispatch, useSelector } from 'react-redux'; import { deleteChild } from '../../redux/reducers/slice/appStateSlice'; From 51c9056eb325a58576df7af35ccc9364d21fac81 Mon Sep 17 00:00:00 2001 From: Eliza612 Date: Tue, 13 Feb 2024 13:01:27 -0500 Subject: [PATCH 064/221] Room function work but status changed --- app/src/components/bottom/ChatRoom.tsx | 474 +++++++++++++++++++------ server/server.ts | 24 +- 2 files changed, 375 insertions(+), 123 deletions(-) diff --git a/app/src/components/bottom/ChatRoom.tsx b/app/src/components/bottom/ChatRoom.tsx index 0b7df2e98..66c42e48c 100644 --- a/app/src/components/bottom/ChatRoom.tsx +++ b/app/src/components/bottom/ChatRoom.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, useMemo } from 'react'; import { useSelector } from 'react-redux'; import { RootState } from '../../redux/store'; import { emitEvent } from '../../helperFunctions/socket'; @@ -7,10 +7,10 @@ import { setMeetingId } from '../../redux/reducers/slice/roomSlice'; import { MeetingProvider, MeetingConsumer, - Constants, useMeeting, useParticipant } from '@videosdk.live/react-sdk'; +import ReactPlayer from 'react-player'; const Chatroom = (props): JSX.Element => { const userName = useSelector((store: RootState) => store.roomSlice.userName); @@ -21,38 +21,10 @@ const Chatroom = (props): JSX.Element => { ); const [inputContent, setInputContent] = useState(''); - const [joinMeeting, setJoinMeeting] = useState(false); - const token = 'token'; - - const createMeeting = async ({ token }: { token: string }) => { - const res = await fetch(`https://api.videosdk.live/v2/rooms`, { - method: 'POST', - headers: { - authorization: `${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ customRoomId: 'aaa-bbb-ccc' }) - }); - //Destructuring the roomId from the response - const { roomId }: { roomId: string } = await res.json(); - console.log('Here room id: ', roomId); - - return roomId; - }; - createMeeting({ token }); - const getMeetingAndToken = async (id?: string) => { - const meetingId = id == null ? await createMeeting({ token }) : id; - console.log('Checking meeting id: ', meetingId); - setMeetingId(meetingId); - }; - - function JoinScreen({ - getMeetingAndToken - }: { - getMeetingAndToken: (meeting?: string) => void; - }) { - return null; - } + // const [joinMeeting, setJoinMeeting] = useState(false); + const [videoSDKToken, setVideoSDKToken] = useState( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcGlrZXkiOiJjYzg4NGJjNi0xOTE5LTQ0OTMtOWE5ZS0wYTdlZDcxMjJiODgiLCJwZXJtaXNzaW9ucyI6WyJhbGxvd19qb2luIl0sImlhdCI6MTcwNzQxMTE3NSwiZXhwIjoxNzM4OTQ3MTc1fQ.W6d992rzBjtcb6sCRz5c1Cr2dIy7d8m80mvgwirz4Ts' + ); const wrapperStyles = { border: `2px solid #f2fbf8`, @@ -132,27 +104,6 @@ const Chatroom = (props): JSX.Element => { }); }; - // const getMeetingId = async (token) => { - // try { - // const VIDEOSDK_API_ENDPOINT = `${LOCAL_SERVER_URL}/create-meeting`; - // const options = { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/json' - // }, - // body: JSON.stringify({ token }) - // }; - // const response = await fetch(VIDEOSDK_API_ENDPOINT, options) - // .then(async (result) => { - // const { meetingId } = await result.json(); - // return meetingId; - // }) - // .catch((error) => console.log('error', error)); - // return response; - // } catch (e) { - // console.log(e); - // } - // }; const containerRef = useRef(null); // Scroll to the bottom of the container whenever new messages are added @@ -162,78 +113,369 @@ const Chatroom = (props): JSX.Element => { } }, [messages]); + function Controls() { + const { leave, toggleMic, toggleWebcam } = useMeeting(); + return ( +
+ + + +
+ ); + } + + const onMeetingLeave = () => { + setMeetingId(null); + }; + + function OldParticipantView({ participantId }: { participantId: string }) { + const micRef = useRef(null); + const { webcamStream, micStream, webcamOn, micOn, isLocal, displayName } = + useParticipant(participantId); + + const videoStream = useMemo(() => { + if (webcamOn && webcamStream) { + const mediaStream = new MediaStream(); + mediaStream.addTrack(webcamStream.track); + return mediaStream; + } + }, [webcamStream, webcamOn]); + + useEffect(() => { + if (micRef.current) { + if (micOn && micStream) { + const mediaStream = new MediaStream(); + mediaStream.addTrack(micStream.track); + + micRef.current.srcObject = mediaStream; + micRef.current + .play() + .catch((error) => + console.error('videoElem.current.play() failed', error) + ); + } else { + micRef.current.srcObject = null; + } + } + }, [micStream, micOn]); + + return ( +
+

+ Participant: {displayName} | Webcam: {webcamOn ? 'ON' : 'OFF'} | Mic:{' '} + {micOn ? 'ON' : 'OFF'} +

+
+ ); + } + + function ParticipantView(props) { + const micRef = useRef(null); + const { webcamStream, micStream, webcamOn, micOn, isLocal, displayName } = + useParticipant(props.participantId); + + const videoStream = useMemo(() => { + if (webcamOn && webcamStream) { + const mediaStream = new MediaStream(); + mediaStream.addTrack(webcamStream.track); + return mediaStream; + } + }, [webcamStream, webcamOn]); + + useEffect(() => { + if (micRef.current) { + if (micOn && micStream) { + const mediaStream = new MediaStream(); + mediaStream.addTrack(micStream.track); + + micRef.current.srcObject = mediaStream; + micRef.current + .play() + .catch((error) => + console.error('videoElem.current.play() failed', error) + ); + } else { + micRef.current.srcObject = null; + } + } + }, [micStream, micOn]); + + return ( +
+

+ Participant: {displayName} | Webcam: {webcamOn ? 'ON' : 'OFF'} | Mic:{' '} + {micOn ? 'ON' : 'OFF'} +

+
+ ); + } + + // function MeetingView({ + // onMeetingLeave, + // meetingId + // }: { + // onMeetingLeave: () => void; + // meetingId: string; + // }) { + // const [joined, setJoined] = useState(null); + // //Get the method which will be used to join the meeting. + // //We will also get the participants list to display all participants + // // const { join, participants } = useMeeting({ + // // //callback for when meeting is joined successfully + // // onMeetingJoined: () => { + // // setJoined('JOINED'); + // // }, + // // //callback for when meeting is left + // // onMeetingLeft: () => { + // // onMeetingLeave(); + // // } + // // }); + // const { join } = useMeeting(); + // const { participants } = useMeeting({ + // onMeetingJoined: () => { + // setJoined('JOINED'); + // }, + // onMeetingLeft: () => { + // props.onMeetingLeave(); + // } + // }); + // const joinMeeting = () => { + // setJoined('JOINING'); + // join(); + // }; + + // console.log('Here check joined and meeting id: ', joined, meetingId); + + // return ( + //
+ //

Meeting Id: {meetingId}

+ // {joined && joined === 'JOINED' ? ( + //
+ // + // { + // //For rendering all the participants in the meeting + // [...participants.keys()].map((participantId) => ( + // + // )) + // } + //
+ // ) : joined && joined === 'JOINING' ? ( + //

Joining the meeting...

+ // ) : ( + // + // )} + //
+ // ); + // } + + function OldMeetingView(props) { + const [joined, setJoined] = useState(null); + const { join } = useMeeting(); + console.log('Check joined: ', joined); + const { participants } = useMeeting({ + onMeetingJoined: () => { + setJoined('JOINED'); + }, + onMeetingLeft: () => { + props.onMeetingLeave(); + } + }); + const joinMeeting = () => { + setJoined('JOINING'); + join(); + }; + + return ( +
+

Meeting Id: {props.meetingId}

+ {joined && joined === 'JOINED' ? ( +
+ + {[...participants.keys()].map((participantId) => ( + + ))} +
+ ) : joined && joined === 'JOINING' ? ( +

Joining the meeting...

+ ) : ( + + )} +
+ ); + } + + function MeetingView(props) { + const [joined, setJoined] = useState(null); + const { join } = useMeeting(); + console.log('Checking status of join: ', joined); + const { participants } = useMeeting({ + onMeetingJoined: () => { + setJoined('JOINED'); + }, + onMeetingLeft: () => { + props.onMeetingLeave(); + } + }); + const joinMeeting = () => { + setJoined('JOINING'); + join(); + }; + + return ( +
+

Meeting Id: {props.meetingId}

+ {joined && joined === 'JOINED' ? ( +
+ + {[...participants.keys()].map((participantId) => ( + + ))} +
+ ) : joined && joined === 'JOINING' ? ( +

Joining the meeting...

+ ) : ( + + )} +
+ ); + } + return (
-
- {token && meetingId && joinMeeting ? ( -

Start meeting

+
+ {videoSDKToken && meetingId ? ( +
+ {/* + + {() => ( + + )} + + */} + + + {() => ( + + )} + + +
) : ( - +
+ {/* */} + +
)} -
- {renderMessages()} -
- - {/* {token && meetingId ? ( - -

{meetingId}

-
- ) : ( - - )} */} -
-
+
- setInputContent(e.target.value)} - value={inputContent} - style={inputStyles} - /> - - +
+ {renderMessages()} +
+
+
+
+ setInputContent(e.target.value)} + value={inputContent} + style={inputStyles} + /> + +
+
- //
- //
- // setInputContent(e.target.value)} - // value={inputContent} - // style={inputStyles} - // /> - // - //
- //
- //
); }; diff --git a/server/server.ts b/server/server.ts index d163dfc42..4b470b33e 100644 --- a/server/server.ts +++ b/server/server.ts @@ -107,20 +107,18 @@ io.on('connection', (client) => { //if no room exists, add room to list if (!roomLists[roomCode]) { const createMeeting = async () => { - const VITE_VIDEOSDK_TOKEN = 'token'; - const res = await fetch(`https://api.videosdk.live/v2/rooms`, { method: 'POST', headers: { - // authorization: `${import.meta.env.VITE_VIDEOSDK_TOKEN}`, - authorization: VITE_VIDEOSDK_TOKEN, + authorization: process.env.VIDEOSDK_TOKEN, 'Content-Type': 'application/json' }, // body: JSON.stringify({ customRoomId: roomCode }) - body: JSON.stringify({ customRoomId: 'aaabbb' }) + body: JSON.stringify({ customRoomId: 'aaa-bbb' }) }); // //Destructuring the roomId from the response const { roomId }: { roomId: string } = await res.json(); + console.log('Here in server roomId: ', roomId); return roomId; }; roomLists[roomCode] = {}; @@ -138,7 +136,6 @@ io.on('connection', (client) => { .timeout(5000) .to(hostID) // sends only to host .emitWithAck('requesting state from host'); //sending request - console.log('Checking hostState: ', hostState); //share host's state with the latest user const newClientResponse = await io //send the requested host state to the new client awaiting for the host state to come back before doing other task @@ -180,12 +177,25 @@ io.on('connection', (client) => { }); //disconnecting functionality - client.on('disconnecting', () => { + client.on('disconnecting', async () => { const roomCode = Array.from(client.rooms)[1]; //grabbing current room client was in when disconnecting const userName = roomLists[roomCode]['userList'][client.id]; delete roomLists[roomCode]['userList'][client.id]; //if room empty, delete room from room list if (!Object.keys(roomLists[roomCode]['userList']).length) { + // const options = { + // method: 'POST', + // headers: { + // Authorization: process.env.VIDEOSDK_TOKEN, + // 'Content-Type': 'application/json' + // }, + // body: JSON.stringify({ roomId: roomLists[roomCode].meetingId }) + // }; + // const url = `https://api.videosdk.live/v2/rooms/deactivate`; + // const response = await fetch(url, options); + // const data = await response.json(); + // if (data.disabled) + // console.log(`Successfully deactivate room with id ${data.roomId}`); delete roomLists[roomCode]; } else { //else emit updated user list From c9c209b10cea319942c5a2075487807d33153fb0 Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 13 Feb 2024 16:29:49 -0500 Subject: [PATCH 065/221] disabled cursor to click on navbar logo and title --- app/src/components/top/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/top/NavBar.tsx b/app/src/components/top/NavBar.tsx index 4da7adf12..8c2c1065a 100644 --- a/app/src/components/top/NavBar.tsx +++ b/app/src/components/top/NavBar.tsx @@ -145,7 +145,7 @@ const NavBar: React.FC = () => { return (