give Prototype its own page

This commit is contained in:
bedlam343 2024-05-07 20:11:51 -07:00
parent 9ea8956284
commit b881ef1d62
13 changed files with 243 additions and 87 deletions

View File

@ -3,16 +3,15 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Minimap from 'src/pages/Minimap';
import LeftScreen from 'src/pages/LeftScreen';
import RightScreen from 'src/pages/RightScreen';
import Root from 'src/pages/Root';
import useMoveShips from 'src/hooks/useMoveShips';
import Layout from 'src/pages/Layout';
import Prototype from 'src/pages/Prototype';
const App = () => {
useMoveShips();
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Root />}>
<Route path="/" element={<Layout />}>
<Route path="/prototype" element={<Prototype />} />
<Route path="minimap" element={<Minimap />} />
<Route path="left-screen" element={<LeftScreen />} />
<Route path="right-screen" element={<RightScreen />} />

60
src/components/Home.tsx Normal file
View File

@ -0,0 +1,60 @@
import { NavLink } from 'react-router-dom';
import Spinner from 'src/ui/Spinner';
const Home = () => {
return (
<div
className="h-screen flex items-center
justify-center flex-col gap-10"
>
<p className="text-5xl text-blue-700">Conversation Manager</p>
<div className="flex items-center justify-center gap-4">
<span className="text-xl">Running</span>
<Spinner />
</div>
<div className="mt-10">
<p className="text-2xl text-center">Pages:</p>
<div className="flex items-center justify-center gap-2 mt-4">
<div>
<NavLink
to="/left-screen"
target="_blank"
className="w-24 bg-transparent hover:bg-blue-500
text-blue-700 font-semibold hover:text-white border border-blue-500 hover:border-transparent
rounded text-sm px-2 py-1 text-center"
>
Left Screen
</NavLink>
</div>
<div>
<NavLink
to="/minimap"
target="_blank"
className="w-24 bg-transparent hover:bg-blue-500
text-blue-700 font-semibold hover:text-white border border-blue-500 hover:border-transparent
rounded text-sm px-2 py-1 text-center"
>
Minimap
</NavLink>
</div>
<div>
<NavLink
to="/right-screen"
target="_blank"
className="w-24 bg-transparent hover:bg-blue-500
text-blue-700 font-semibold hover:text-white border border-blue-500 hover:border-transparent
rounded text-sm px-2 py-1 text-center"
>
Right Screen
</NavLink>
</div>
</div>
</div>
</div>
);
};
export default Home;

View File

@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'src/redux/hooks';
import {
getDrones,
getOwnship,
updateWidget,
updateShipPosition,
} from 'src/redux/slices/minimapSlice';
import { OWNSHIP_TRAJECTORY } from 'src/utils/constants';
@ -12,6 +12,10 @@ const useMoveShips = () => {
const ownship = useAppSelector(getOwnship);
const drones = useAppSelector(getDrones);
useEffect(() => {
console.log('drones', drones);
}, [drones.length]);
useEffect(() => {
if (!ownship) return;
@ -23,11 +27,11 @@ const useMoveShips = () => {
) {
// only update ownship position if within bounds
dispatch(
updateWidget({
...ownship,
x: ownship.x + OWNSHIP_TRAJECTORY.xSpeed,
y: ownship.y - OWNSHIP_TRAJECTORY.ySpeed,
}),
updateShipPosition(
ownship.id,
ownship.x + OWNSHIP_TRAJECTORY.xSpeed,
ownship.y - OWNSHIP_TRAJECTORY.ySpeed,
),
);
}
}, 500);
@ -72,11 +76,11 @@ const useMoveShips = () => {
}
dispatch(
updateWidget({
...drone,
x: drone.x + droneMove.x,
y: drone.y + droneMove.y,
}),
updateShipPosition(
drone.id,
drone.x + droneMove.x,
drone.y + droneMove.y,
),
);
});
}, 1500);
@ -86,7 +90,7 @@ const useMoveShips = () => {
// dependencies omitted because drones array is changing too frequently
// some warning/issue of selector returning different values despite same parameters
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, []);
}, [drones.length]);
};
export default useMoveShips;

32
src/pages/Layout.tsx Normal file
View File

@ -0,0 +1,32 @@
import { useEffect } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import Gaze from 'src/ui/Gaze';
import { useMousePosition } from 'src/hooks/useMousePosition';
const Layout = () => {
// ~~~~~ React Router ~~~~~~
const { pathname } = useLocation();
const navigate = useNavigate();
const mousePosition = useMousePosition();
// Redirect to /prototype if the user is on the root path
useEffect(() => {
if (pathname === '/') {
navigate('/prototype');
}
}, [pathname, navigate]);
return (
<div>
{/* {pathname !== '/prototype' && <Navigation />} */}
{pathname !== '/prototype' && <Gaze mousePosition={mousePosition} />}
<main>
<Outlet />
</main>
</div>
);
};
export default Layout;

View File

@ -1,11 +1,17 @@
import Widget from 'src/components/Widget/Widget';
import { useAppSelector } from 'src/redux/hooks';
import { getMinimapWidgets } from 'src/redux/slices/minimapSlice';
import useMoveShips from 'src/hooks/useMoveShips';
import { useEffect } from 'react';
const Minimap = () => {
const widgets = useAppSelector(getMinimapWidgets);
/* If this is here, then ships only move if this page is being rendered */
useMoveShips();
// console.log('minimap widgets:', widgets);
useEffect(() => {
console.log('Minimap widgets:', widgets);
}, [widgets.length]);
return (
<div className="absolute top-0 left-0 bg-stone-200 w-[1920px] h-[1080px] hover:cursor-pointer">

View File

@ -1,14 +1,14 @@
import { useEffect } from 'react';
// ~~~~~~~ Components ~~~~~~~
import Gaze from 'src/ui/Gaze';
// ~~~~~~~ Redux ~~~~~~~
import { useAppDispatch, useAppSelector } from 'src/redux/hooks';
import {
type InitialMinimapState,
addElementToWidget,
addWidget,
addWidgetToSection,
getSections,
getWidgets,
initializeState,
} from 'src/redux/slices/minimapSlice';
import {
addKeyDown,
@ -31,6 +31,11 @@ import assimilator from 'src/prototype/assimilator';
import selector from 'src/prototype/selector';
// ~~~~~~~ Constants ~~~~~~~
import { GAZE_RADIUS } from 'src/utils/constants';
import { ownship, drones, initialShips } from 'src/utils/initialShips';
import { initialSections } from 'src/utils/initialSections';
import Spinner from 'src/ui/Spinner';
import Home from 'src/components/Home';
const CIRCLE_PERCENTAGE_THRESH = 0.1;
const ELEMENT_PERCENTAGE_THRESH = 0.1;
@ -51,6 +56,21 @@ const Prototype = () => {
const dispatch = useAppDispatch();
useEffect(() => {
// Intiailize minimap state
const initialState: InitialMinimapState = {
visualComplexity: 0,
audioComplexity: 0,
ownship,
drones,
widgets: { ...initialShips },
messages: [],
sections: [...initialSections],
};
dispatch(initializeState(initialState));
}, [dispatch]);
// on mouse position move, check for elements in gaze
useEffect(() => {
const elementsInGaze = findElementsInGaze(
@ -63,13 +83,13 @@ const Prototype = () => {
);
dispatch(setElementsInGaze(elementsInGaze));
if (elementsInGaze.length > 0) {
console.log('elements in gaze: ', elemsInGaze);
// console.log('elements in gaze: ', elemsInGaze);
}
}, [mousePosition]);
// print out the gazes and keys
useEffect(() => {
console.log('gazesAndKeys', gazesAndKeys);
// console.log('gazesAndKeys', gazesAndKeys);
}, [gazesAndKeys]);
// on key or mouse press, log the press and what elements are in the gaze to state
@ -175,7 +195,7 @@ const Prototype = () => {
}
}, [messages]);
return <Gaze mousePosition={mousePosition} />;
return <Home />;
};
export default Prototype;

View File

@ -1,30 +0,0 @@
import { useEffect } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import Prototype from 'src/components/Prototype';
import Navigation from 'src/ui/Navigation';
const Root = () => {
// ~~~~~ React Router ~~~~~~
const { pathname } = useLocation();
const navigate = useNavigate();
// Redirect to /minimap if the user is on the root path
useEffect(() => {
if (pathname === '/') {
navigate('/minimap');
}
}, [pathname, navigate]);
return (
<div>
<Navigation />
<Prototype />
<main>
<Outlet />
</main>
</div>
);
};
export default Root;

View File

@ -1,32 +1,46 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { Widget, WidgetMap } from 'src/types/widget';
import type { Widget, VehicleWidget, WidgetMap } from 'src/types/widget';
import type { Message } from 'src/types/schema-types';
import type { Element } from 'src/types/element';
import type { LinkedSectionWidget, Section } from 'src/types/support-types';
import { initialSections } from 'src/redux/utils/initialSections';
import { ownship, drones, initialShips } from 'src/utils/initialShips';
type InitialState = {
export type InitialMinimapState = {
visualComplexity: number;
audioComplexity: number;
ownship: VehicleWidget | null;
drones: VehicleWidget[];
widgets: WidgetMap;
messages: Message[];
sections: Section[];
};
const initialState: InitialState = {
const initialState: InitialMinimapState = {
visualComplexity: 0,
audioComplexity: 0,
widgets: initialShips,
ownship: null,
drones: [],
widgets: {},
messages: [],
sections: initialSections,
sections: [],
};
export const minimapSlice = createSlice({
name: 'minimap',
initialState,
reducers: {
// This is needed for compatibility with redux-state-sync
initializeState: (state, action: PayloadAction<InitialMinimapState>) => {
console.log('Initializing minimap state with: ', action.payload);
state.visualComplexity = action.payload.visualComplexity;
state.audioComplexity = action.payload.audioComplexity;
state.ownship = action.payload.ownship;
state.drones = action.payload.drones;
state.widgets = action.payload.widgets;
state.messages = action.payload.messages;
state.sections = action.payload.sections;
},
addMapSection: (state, action) => {
state.sections.push(action.payload); //add it to our sections as well
},
@ -39,6 +53,38 @@ export const minimapSlice = createSlice({
state.widgets[action.payload.id] = action.payload;
},
updateShipPosition: {
prepare(shipId: string, x: number, y: number) {
return {
payload: { shipId, x, y },
};
},
reducer: (
state,
action: PayloadAction<{ shipId: string; x: number; y: number }>,
) => {
const { shipId, x, y } = action.payload;
const ship = state.widgets[shipId];
if (!ship) {
console.error(`Ship with id ${shipId} not found`);
return;
}
if (ship.type !== 'vehicle') {
console.error(`Widget with id ${shipId} is not a vehicle`);
return;
}
state.widgets[shipId] = {
...ship,
x,
y,
};
},
},
removeWidget: (state, action: PayloadAction<string>) => {
delete state.widgets[action.payload];
},
@ -155,8 +201,6 @@ export const minimapSlice = createSlice({
(id) => state.widgets[id].screen === 'left',
);
console.log('minimapWidgets:', minimapWidgets);
return minimapWidgets.map((id) => state.widgets[id]);
},
getMinimapWidgets: (state) => {
@ -181,27 +225,29 @@ export const minimapSlice = createSlice({
getMessages: (state) => state.messages,
// ~~~~~ selectors for ships ~~~~~
getOwnship: (state) =>
state.widgets[ownship.id] ? state.widgets[ownship.id] : null,
getDrones: (state) =>
drones.map((drone) =>
state.widgets[drone.id] ? state.widgets[drone.id] : null,
),
getOwnship: (state) => state.ownship,
getDrones: (state) => state.drones,
},
});
// action creators (automatically generated by createSlice for each reducer)
export const {
initializeState,
addMapSection,
addMessage,
addWidget,
updateWidget,
removeWidget,
addElementToWidget,
addWidgetToSection,
deleteElementFromWidget,
updateWidget,
updateShipPosition,
updateVisualComplexity,
updateAudioComplexity,
removeWidget,
deleteElementFromWidget,
toggleElementInteraction,
} = minimapSlice.actions;

View File

@ -1,7 +1,8 @@
import { combineSlices, configureStore } from '@reduxjs/toolkit';
import {
createStateSyncMiddleware,
initMessageListener,
initStateWithPrevTab,
withReduxStateSync,
type Config,
} from 'redux-state-sync';
import { minimapSlice } from './slices/minimapSlice';
@ -16,18 +17,20 @@ const rootReducer = combineSlices(minimapSlice, gazeSlice);
type RootState = ReturnType<typeof rootReducer>;
const reduxStateSyncConfig: Config = {
prepareState: (state: RootState) => state,
// blacklist actions that should not be synced
blacklist: [minimapSlice.actions.initializeState.type],
};
const stateSyncMiddleware = createStateSyncMiddleware(reduxStateSyncConfig);
const store = configureStore({
reducer: rootReducer,
reducer: withReduxStateSync(rootReducer),
// @ts-ignore (TODO: fix this type error)
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(stateSyncMiddleware),
});
initMessageListener(store);
// Initialize the state with the previous tab's state
initStateWithPrevTab(store);
// Infer the type of `store`
type AppStore = typeof store;

View File

@ -34,6 +34,8 @@ export type GridWidget = BaseWidget & {
export type VehicleWidget = BaseWidget & {
type: 'vehicle';
// this corresponds to the id in the schema-types defined by the world-sim team
vehicleId: number;
// additonal properties...
};

10
src/ui/Spinner.tsx Normal file
View File

@ -0,0 +1,10 @@
const Spinner = () => {
return (
<div
className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-e-transparent align-[-0.125em] text-primary motion-reduce:animate-[spin_1.5s_linear_infinite] text-red-600"
role="status"
/>
);
};
export default Spinner;

View File

@ -2,19 +2,21 @@ import { v4 as uuid } from 'uuid';
import type { IconElement } from 'src/types/element';
import OWNSHIP_LOGO from 'src/icons/currentPosition.svg';
import DRONE_LOGO from 'src/icons/drone.svg';
import type { Widget, VehicleWidget, WidgetMap } from 'src/types/widget';
import type { VehicleWidget, WidgetMap } from 'src/types/widget';
const createDroneWidget = (
x: number,
y: number,
w: number,
h: number,
vehicleId: number,
): VehicleWidget => ({
elements: [droneElement],
id: uuid(),
sectionType: 'free',
type: 'vehicle',
screen: 'minimap',
vehicleId,
x,
y,
@ -42,6 +44,7 @@ const ownshipElement: IconElement = {
export const ownship: VehicleWidget = {
id: uuid(),
vehicleId: 0,
x: 400,
y: 950,
@ -72,18 +75,19 @@ const droneElement: IconElement = {
yWidget: 0,
};
export const drones = [
createDroneWidget(500, 200, 50, 50),
createDroneWidget(1500, 550, 50, 50),
createDroneWidget(1500, 350, 50, 50),
createDroneWidget(200, 900, 50, 50),
createDroneWidget(1150, 750, 50, 50),
];
const drone1 = createDroneWidget(500, 200, 50, 50, 1);
const drone2 = createDroneWidget(1500, 550, 50, 50, 2);
const drone3 = createDroneWidget(1500, 350, 50, 50, 3);
const drone4 = createDroneWidget(200, 900, 50, 50, 4);
const drone5 = createDroneWidget(1150, 750, 50, 50, 5);
export const drones = [drone1, drone2, drone3, drone4, drone5];
export const initialShips: WidgetMap = {
[ownship.id]: ownship,
...drones.reduce((acc, drone) => {
acc[drone.id] = drone;
return acc;
}, {} as WidgetMap),
[drone1.id]: drone1,
[drone2.id]: drone2,
[drone3.id]: drone3,
[drone4.id]: drone4,
[drone5.id]: drone5,
};