Merge pull request #32 from CPSC319-Winter-term-2/sidebar-calendar-updates

sidebar changes
This commit is contained in:
cth0604 2022-03-20 23:57:00 -07:00 committed by GitHub
commit 226d403733
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 283 additions and 224 deletions

View File

@ -22,7 +22,8 @@ const App: React.FC = () => {
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route element={<ProtectedRoute />}> <Route element={<ProtectedRoute />}>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/contacts" element={<Contacts />} /> <Route path="/contacts/" element={<Contacts />} />
<Route path="/contacts/:uuid" element={<Contacts />} />
<Route path="/calendar" element={<CalendarPage />} /> <Route path="/calendar" element={<CalendarPage />} />
</Route> </Route>
</Routes> </Routes>

View File

@ -1,42 +1,14 @@
import {useLocation, Outlet, Navigate} from "react-router-dom"; import { useLocation, Outlet, Navigate } from "react-router-dom";
import useAuth from "./hooks/useAuth"; import useAuth from "./hooks/useAuth";
import Navbar from "./components/navbar/Navbar"; import Navbar from "./components/navbar/Navbar";
import Sidebar from "./components/sidebar/Sidebar"; import Sidebar from "./components/sidebar/Sidebar";
import React, { useState } from "react";
import {
MeetingStatus,
SidebarUserObj,
} from "./components/sidebar/SidebarUser";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
const ProtectedRoute = () => { const ProtectedRoute = () => {
const auth= useAuth(); const auth = useAuth();
const location = useLocation(); const location = useLocation();
/* Temporary data */ /* Temporary data */
const [sidebarUsers] = useState<SidebarUserObj[]>([
{ id: 0, name: "Jincheng L.", meetingStatus: MeetingStatus.ONLINE },
{ id: 1, name: "Matt B.", meetingStatus: MeetingStatus.IN_MEETING },
{ id: 2, name: "Taehee C.", meetingStatus: MeetingStatus.OFFLINE },
{ id: 3, name: "Bob A.", meetingStatus: MeetingStatus.AWAY },
{ id: 4, name: "Bob B.", meetingStatus: MeetingStatus.IN_MEETING },
{ id: 5, name: "Bob C.", meetingStatus: MeetingStatus.OFFLINE },
{ id: 6, name: "Bob D.", meetingStatus: MeetingStatus.ONLINE },
{ id: 7, name: "Bob E.", meetingStatus: MeetingStatus.AWAY },
{ id: 8, name: "Bob F.", meetingStatus: MeetingStatus.OFFLINE },
{ id: 9, name: "Bob G.", meetingStatus: MeetingStatus.ONLINE },
{ id: 10, name: "Bob H.", meetingStatus: MeetingStatus.AWAY },
{ id: 11, name: "Bob I.", meetingStatus: MeetingStatus.OFFLINE },
{ id: 12, name: "Bob J.", meetingStatus: MeetingStatus.AWAY },
{ id: 13, name: "Bob K.", meetingStatus: MeetingStatus.IN_MEETING },
{ id: 14, name: "Bob L.", meetingStatus: MeetingStatus.IN_MEETING },
{ id: 15, name: "Bob M.", meetingStatus: MeetingStatus.OFFLINE },
{ id: 16, name: "Bob N.", meetingStatus: MeetingStatus.OFFLINE },
{ id: 17, name: "Bob O.", meetingStatus: MeetingStatus.AWAY },
{ id: 18, name: "Bob P.", meetingStatus: MeetingStatus.AWAY },
{ id: 19, name: "Bob Q.", meetingStatus: MeetingStatus.ONLINE },
{ id: 20, name: "Bob R.", meetingStatus: MeetingStatus.ONLINE },
]);
return auth?.isLoggedIn ? ( return auth?.isLoggedIn ? (
<> <>
<Navbar /> <Navbar />
@ -45,7 +17,7 @@ const ProtectedRoute = () => {
<Outlet /> <Outlet />
</Box> </Box>
<Box> <Box>
<Sidebar users={sidebarUsers} /> <Sidebar />
</Box> </Box>
</Box> </Box>
</> </>

View File

@ -4,18 +4,20 @@ import Body from "./contacts-components/Body";
import Sidebar from "./contacts-components/Sidebar"; import Sidebar from "./contacts-components/Sidebar";
import UserLite from "../../api-bodies/UserLite"; import UserLite from "../../api-bodies/UserLite";
import { useParams } from "react-router-dom";
import { selectUser } from "../../redux/slices/usersSlice";
import { useAppSelector } from "../../redux/hooks";
const Contacts: React.FC = () => { const Contacts: React.FC = () => {
const [contactSelected, setContactSelected] = React.useState<UserLite | null>( const { uuid } = useParams();
null const uriContact: UserLite | undefined = useAppSelector((state) =>
selectUser(state, uuid)
); );
return ( return (
<Box sx={{ display: "flex", height: "100%" }}> <Box sx={{ display: "flex", height: "100%" }}>
<Sidebar setContactSelected={setContactSelected} /> <Sidebar />
{contactSelected !== null ? ( {uriContact ? <Body contactSelected={uriContact} /> : null}
<Body contactSelected={contactSelected} />
) : null}
</Box> </Box>
); );
}; };

View File

@ -1,8 +0,0 @@
enum Status {
Online = "Online",
Offline = "Offline",
Away = "Away",
InMeeting = "In Meeting",
}
export default Status;

View File

@ -1,20 +1,4 @@
import DetailedMeeting from "../../api-bodies/DetailedMeeting"; import DetailedMeeting from "../../api-bodies/DetailedMeeting";
import Status from "./Status";
const returnStatusColor = (
status: Status
): "success" | "disabled" | "warning" | "error" => {
switch (status) {
case Status.Online:
return "success";
case Status.Offline:
return "disabled";
case Status.Away:
return "warning";
case Status.InMeeting:
return "error";
}
};
const getUpcomingMeetingTime = (meeting: DetailedMeeting) => { const getUpcomingMeetingTime = (meeting: DetailedMeeting) => {
const startDate = new Date(meeting.start); const startDate = new Date(meeting.start);
@ -34,4 +18,4 @@ const getUpcomingMeetingTime = (meeting: DetailedMeeting) => {
return `${startTime} - ${endTime}`; return `${startTime} - ${endTime}`;
}; };
export { returnStatusColor, getUpcomingMeetingTime }; export { getUpcomingMeetingTime };

View File

@ -13,15 +13,10 @@ import ContactItem from "./sidebar-components/ContactItem";
import { useAppSelector } from "../../../redux/hooks"; import { useAppSelector } from "../../../redux/hooks";
import { selectUsers, selectTeam } from "../../../redux/slices/usersSlice"; import { selectUsers, selectTeam } from "../../../redux/slices/usersSlice";
import { selectFavorites } from "../../../redux/slices/favoritesSlice"; import { selectFavorites } from "../../../redux/slices/favoritesSlice";
import UserLite from "../../../api-bodies/UserLite";
interface Props {
setContactSelected: (contactInfo: UserLite) => void;
}
const sidebarWidth = 240; const sidebarWidth = 240;
const Sidebar: React.FC<Props> = (props) => { const Sidebar: React.FC = () => {
const [favoritesOpen, setFavoritesOpen] = React.useState<boolean>(true); const [favoritesOpen, setFavoritesOpen] = React.useState<boolean>(true);
const [teamOpen, setTeamOpen] = React.useState<boolean>(false); const [teamOpen, setTeamOpen] = React.useState<boolean>(false);
@ -29,7 +24,8 @@ const Sidebar: React.FC<Props> = (props) => {
const favorites = useAppSelector((state) => const favorites = useAppSelector((state) =>
selectUsers(state, favoritesUuids) selectUsers(state, favoritesUuids)
); );
const team = useAppSelector(selectTeam); const teamUuids = useAppSelector(selectTeam);
const team = useAppSelector((state) => selectUsers(state, teamUuids));
return ( return (
<Drawer <Drawer
@ -58,11 +54,7 @@ const Sidebar: React.FC<Props> = (props) => {
<Collapse in={favoritesOpen} timeout="auto" unmountOnExit> <Collapse in={favoritesOpen} timeout="auto" unmountOnExit>
<List disablePadding> <List disablePadding>
{favorites.map((favorite, i) => ( {favorites.map((favorite, i) => (
<ContactItem <ContactItem contactInfo={favorite} key={i} />
contactInfo={favorite}
key={i}
setContactSelected={props.setContactSelected}
/>
))} ))}
</List> </List>
</Collapse> </Collapse>
@ -74,11 +66,7 @@ const Sidebar: React.FC<Props> = (props) => {
<Collapse in={teamOpen} timeout="auto" unmountOnExit> <Collapse in={teamOpen} timeout="auto" unmountOnExit>
<List disablePadding> <List disablePadding>
{team.map((member, i) => ( {team.map((member, i) => (
<ContactItem <ContactItem contactInfo={member} key={i} />
contactInfo={member}
key={i}
setContactSelected={props.setContactSelected}
/>
))} ))}
</List> </List>
</Collapse> </Collapse>

View File

@ -4,7 +4,6 @@ import PhoneIcon from "@mui/icons-material/Phone";
import VideocamIcon from "@mui/icons-material/Videocam"; import VideocamIcon from "@mui/icons-material/Videocam";
import GroupsIcon from "@mui/icons-material/Groups"; import GroupsIcon from "@mui/icons-material/Groups";
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import Status from "../../Status";
import UserLite from "../../../../api-bodies/UserLite"; import UserLite from "../../../../api-bodies/UserLite";
import UserStatus from "../../../../api-bodies/UserStatus"; import UserStatus from "../../../../api-bodies/UserStatus";
import { useAppSelector, useAppDispatch } from "../../../../redux/hooks"; import { useAppSelector, useAppDispatch } from "../../../../redux/hooks";
@ -18,6 +17,7 @@ import {
removeFavorite, removeFavorite,
} from "../../../../redux/slices/favoritesSlice"; } from "../../../../redux/slices/favoritesSlice";
import RemoveIcon from "@mui/icons-material/Remove"; import RemoveIcon from "@mui/icons-material/Remove";
import { MeetingStatus } from "../../../../utils";
interface Props { interface Props {
contactInfo: UserLite; contactInfo: UserLite;
@ -25,15 +25,17 @@ interface Props {
const UpperBody: React.FC<Props> = (props) => { const UpperBody: React.FC<Props> = (props) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const userStatus: UserStatus | null = useAppSelector((state) => const userStatus: UserStatus = useAppSelector((state) =>
selectUserStatus(state, props.contactInfo.uuid) selectUserStatus(state, props.contactInfo.uuid)
); );
const favoritesUuids = useAppSelector(selectFavorites); const favoritesUuids = useAppSelector(selectFavorites);
const detailedMeeting = useAppSelector((state) => const detailedMeeting = useAppSelector((state) =>
selectMeeting(state, userStatus.meetingID) selectMeeting(state, userStatus.meetingID)
); );
const status: Status = const meetingStatus: MeetingStatus =
userStatus && userStatus.inMeeting ? Status.Online : Status.Offline; userStatus && userStatus.inMeeting
? MeetingStatus.IN_MEETING
: MeetingStatus.NOT_IN_MEETING;
return ( return (
<Box <Box
sx={{ sx={{
@ -102,7 +104,7 @@ const UpperBody: React.FC<Props> = (props) => {
</IconButton> </IconButton>
</Box> </Box>
<Box sx={{ display: "flex", flexDirection: "column" }}> <Box sx={{ display: "flex", flexDirection: "column" }}>
<Typography sx={{ textAlign: "right" }}>{status}</Typography> <Typography sx={{ textAlign: "right" }}>{meetingStatus}</Typography>
{detailedMeeting ? ( {detailedMeeting ? (
<Typography>{detailedMeeting.topic}</Typography> <Typography>{detailedMeeting.topic}</Typography>
) : null} ) : null}

View File

@ -2,28 +2,30 @@ import { ListItemButton, ListItemIcon, ListItemText } from "@mui/material";
import React from "react"; import React from "react";
import PersonOutlineIcon from "@mui/icons-material/PersonOutline"; import PersonOutlineIcon from "@mui/icons-material/PersonOutline";
import CircleIcon from "@mui/icons-material/Circle"; import CircleIcon from "@mui/icons-material/Circle";
import { returnStatusColor } from "../../Utils";
import UserLite from "../../../../api-bodies/UserLite"; import UserLite from "../../../../api-bodies/UserLite";
import Status from "../../Status"; import { MeetingStatus, getStatusColor } from "../../../../utils";
import { useAppSelector } from "../../../../redux/hooks"; import { useAppSelector } from "../../../../redux/hooks";
import { selectUserStatus } from "../../../../redux/slices/meetingsAndUserStatusSlice"; import { selectUserStatus } from "../../../../redux/slices/meetingsAndUserStatusSlice";
import UserStatus from "../../../../api-bodies/UserStatus"; import UserStatus from "../../../../api-bodies/UserStatus";
import { useNavigate } from "react-router-dom";
interface Props { interface Props {
contactInfo: UserLite; contactInfo: UserLite;
setContactSelected: (contactInfo: UserLite) => void;
} }
const ContactItem: React.FC<Props> = (props) => { const ContactItem: React.FC<Props> = (props) => {
const navigate = useNavigate();
const userStatus: UserStatus | null = useAppSelector((state) => const userStatus: UserStatus | null = useAppSelector((state) =>
selectUserStatus(state, props.contactInfo.uuid) selectUserStatus(state, props.contactInfo.uuid)
); );
const status: Status = const status: MeetingStatus =
userStatus && userStatus.inMeeting ? Status.Online : Status.Offline; userStatus && userStatus.inMeeting
? MeetingStatus.IN_MEETING
: MeetingStatus.NOT_IN_MEETING;
return ( return (
<ListItemButton <ListItemButton
onClick={() => { onClick={() => {
props.setContactSelected(props.contactInfo); navigate(`/contacts/${props.contactInfo.uuid}`);
}} }}
> >
<ListItemIcon> <ListItemIcon>
@ -34,7 +36,7 @@ const ContactItem: React.FC<Props> = (props) => {
secondary={status} secondary={status}
sx={{ flexGrow: 1 }} sx={{ flexGrow: 1 }}
/> />
<CircleIcon color={returnStatusColor(status)} /> <CircleIcon sx={{ color: getStatusColor(status) }} />
</ListItemButton> </ListItemButton>
); );
}; };

View File

@ -1,44 +1,63 @@
import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, FormGroup, Typography } from "@mui/material"; import {
import { SidebarUserObj } from "../sidebar/SidebarUser"; Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControlLabel,
FormGroup,
Typography,
} from "@mui/material";
import { SidebarUserObj } from "./Home";
interface Props { interface Props {
open: boolean; open: boolean;
selectedValue: SidebarUserObj; selectedValue: SidebarUserObj;
onClose: (value: SidebarUserObj) => void; onClose: (value: SidebarUserObj) => void;
users: SidebarUserObj[]; users: SidebarUserObj[];
} }
const CallFavouritesDialog: React.FC<Props> = ({ open, selectedValue, onClose, users }: Props) => { const CallFavouritesDialog: React.FC<Props> = ({
open,
selectedValue,
onClose,
users,
}: Props) => {
const handleClose = () => { const handleClose = () => {
onClose(selectedValue); onClose(selectedValue);
}; };
return ( return (
<Dialog <Dialog onClose={handleClose} open={open} fullWidth maxWidth="sm">
onClose={handleClose}
open={open}
fullWidth
maxWidth="sm"
>
<DialogTitle>Select who to call:</DialogTitle> <DialogTitle>Select who to call:</DialogTitle>
<DialogContent> <DialogContent>
<FormControlLabel control={<Checkbox color="success" />} label="Select everyone" /> <FormControlLabel
control={<Checkbox color="success" />}
label="Select everyone"
/>
</DialogContent> </DialogContent>
<DialogContent sx={{ height: "40vh" }} dividers> <DialogContent sx={{ height: "40vh" }} dividers>
<FormGroup> <FormGroup>
{users.map((user) => ( {users.map((user) => (
<FormControlLabel key={user.id} control={<Checkbox color="success" />} label={user.name} /> <FormControlLabel
key={user.id}
control={<Checkbox color="success" />}
label={user.name}
/>
))} ))}
</FormGroup> </FormGroup>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button color="success" onClick={handleClose}> <Button color="success" onClick={handleClose}>
<Typography variant="button" color="black">Call</Typography> <Typography variant="button" color="black">
Call
</Typography>
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );
}; };
export default CallFavouritesDialog; export default CallFavouritesDialog;

View File

@ -2,22 +2,26 @@ import MeetingsPanel from "./MeetingsPanel";
import ShortCuts from "./ShortCuts"; import ShortCuts from "./ShortCuts";
import Container from "@mui/material/Container"; import Container from "@mui/material/Container";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import { MeetingStatus, SidebarUserObj } from "../sidebar/SidebarUser"; import { MeetingStatus } from "../../utils";
import { useState } from "react"; import { useState } from "react";
const Home: React.FC = () => { export interface SidebarUserObj {
id: number;
name: string;
meetingStatus: MeetingStatus;
}
const Home: React.FC = () => {
/* Temporary data - this is the same as what's in ProtectedRoute.tsx so it should not /* Temporary data - this is the same as what's in ProtectedRoute.tsx so it should not
be duplicated like this in the future (Route components were being weird when I tried be duplicated like this in the future (Route components were being weird when I tried
to pass it down from App.tsx) */ to pass it down from App.tsx) */
const [mockUsers] = useState<SidebarUserObj[]>([ const [mockUsers] = useState<SidebarUserObj[]>([
{ id: 0, name: "Jincheng L.", meetingStatus: MeetingStatus.ONLINE }, { id: 0, name: "Jincheng L.", meetingStatus: MeetingStatus.NOT_IN_MEETING },
{ id: 1, name: "Matt B.", meetingStatus: MeetingStatus.IN_MEETING }, { id: 1, name: "Matt B.", meetingStatus: MeetingStatus.IN_MEETING },
{ id: 2, name: "Taehee C.", meetingStatus: MeetingStatus.OFFLINE }, { id: 2, name: "Taehee C.", meetingStatus: MeetingStatus.IN_MEETING },
{ id: 3, name: "Bob A.", meetingStatus: MeetingStatus.AWAY } { id: 3, name: "Bob A.", meetingStatus: MeetingStatus.NOT_IN_MEETING },
]); ]);
return ( return (
<Container className="main-home"> <Container className="main-home">
<Grid container> <Grid container>
@ -25,7 +29,7 @@ const Home: React.FC = () => {
<MeetingsPanel /> <MeetingsPanel />
</Grid> </Grid>
<Grid item sm={4}> <Grid item sm={4}>
<ShortCuts users={mockUsers}/> <ShortCuts users={mockUsers} />
</Grid> </Grid>
</Grid> </Grid>
</Container> </Container>

View File

@ -5,7 +5,7 @@ import CircleIcon from "@mui/icons-material/Circle";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { SidebarUserObj } from "../sidebar/SidebarUser"; import { SidebarUserObj } from "./Home";
import CallFavouritesDialog from "./CallFavouritesDialog"; import CallFavouritesDialog from "./CallFavouritesDialog";
import { useState } from "react"; import { useState } from "react";
@ -27,19 +27,23 @@ const ShortCuts: React.FC<Props> = ({ users }: Props) => {
}; };
return ( return (
<div className="short-cuts" > <div className="short-cuts">
<Grid className="row-1" container spacing={1}> <Grid className="row-1" container spacing={1}>
<Grid item sm={6}> <Grid item sm={6}>
<Button className="tile"> <Button className="tile">
<PeopleIcon className="icon" /> <PeopleIcon className="icon" />
</Button> </Button>
<Typography className="mylabel" sx={{ ml: 1 }}>New Meeting</Typography> <Typography className="mylabel" sx={{ ml: 1 }}>
New Meeting
</Typography>
</Grid> </Grid>
<Grid item sm={6}> <Grid item sm={6}>
<Button className="tile"> <Button className="tile">
<AddIcon className="icon" /> <AddIcon className="icon" />
</Button> </Button>
<Typography className="mylabel" sx={{ ml: 1 }}>Join</Typography> <Typography className="mylabel" sx={{ ml: 1 }}>
Join
</Typography>
</Grid> </Grid>
</Grid> </Grid>
<Grid className="row-2" container spacing={1}> <Grid className="row-2" container spacing={1}>
@ -47,22 +51,27 @@ const ShortCuts: React.FC<Props> = ({ users }: Props) => {
<Button className="tile" onClick={handleClickOpen}> <Button className="tile" onClick={handleClickOpen}>
<PhoneEnabledIcon className="icon" /> <PhoneEnabledIcon className="icon" />
</Button> </Button>
<CallFavouritesDialog <CallFavouritesDialog
selectedValue={selectedValue} selectedValue={selectedValue}
open={open} open={open}
onClose={handleClose} onClose={handleClose}
users={users}/> users={users}
<Typography className="mylabel" sx={{ ml: 1 }}>Call Favourites</Typography> />
<Typography className="mylabel" sx={{ ml: 1 }}>
Call Favourites
</Typography>
</Grid> </Grid>
<Grid item sm={6}> <Grid item sm={6}>
<Button className="tile"> <Button className="tile">
<CircleIcon className="icon" /> <CircleIcon className="icon" />
</Button> </Button>
<Typography className="mylabel" sx={{ ml: 1 }}>Recordings</Typography> <Typography className="mylabel" sx={{ ml: 1 }}>
Recordings
</Typography>
</Grid> </Grid>
</Grid> </Grid>
</div> </div>
); );
}; };
export default ShortCuts; export default ShortCuts;

View File

@ -1,14 +1,25 @@
import { FormControl, InputLabel, MenuItem, Select, SelectChangeEvent, Typography } from "@mui/material"; import {
import { useState } from "react"; FormControl,
InputLabel,
MenuItem,
Select,
SelectChangeEvent,
Typography,
} from "@mui/material";
import React from "react";
// TODO: change MenuItem values to something meaningful // TODO: change MenuItem values to something meaningful
const GroupSelect: React.FC = () => { interface Props {
group: string;
setGroup: (group: string) => void;
}
const [group, setGroup] = useState(""); const groups = ["Favorites", "Team"];
const handleChange = (event: SelectChangeEvent) => { const GroupSelect: React.FC<Props> = ({ group, setGroup }) => {
setGroup(event.target.value); const handleChange = (e: SelectChangeEvent) => {
setGroup(e.target.value);
}; };
return ( return (
@ -23,18 +34,14 @@ const GroupSelect: React.FC = () => {
label="Group" label="Group"
onChange={handleChange} onChange={handleChange}
> >
<MenuItem value={0}> {groups.map((group, i) => (
<Typography color="black">Favourites</Typography> <MenuItem value={group} key={i}>
</MenuItem> <Typography color="black">{group}</Typography>
<MenuItem value={1}> </MenuItem>
<Typography color="black">Contacts</Typography> ))}
</MenuItem>
<MenuItem value={2}>
<Typography color="black">Office</Typography>
</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
); );
}; };
export default GroupSelect; export default GroupSelect;

View File

@ -1,12 +1,31 @@
import { Settings } from "@mui/icons-material"; import { Settings } from "@mui/icons-material";
import { IconButton, Menu, MenuItem } from "@mui/material"; import { IconButton, Menu, MenuItem } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import UserLite from "../../api-bodies/UserLite";
import UserStatus from "../../api-bodies/UserStatus";
import { useAppDispatch, useAppSelector } from "../../redux/hooks";
import {
selectFavorites,
addFavorite,
removeFavorite,
} from "../../redux/slices/favoritesSlice";
import { open as openMeetingDetails } from "../../redux/slices/meetingDetailsOpenSlice";
import { selectMeeting } from "../../redux/slices/meetingsAndUserStatusSlice";
import { useNavigate } from "react-router-dom";
interface Props {
user: UserLite;
status: UserStatus;
}
const SettingsButton: React.FC<Props> = ({ user, status }: Props) => {
const SettingsButton: React.FC = () => { const navigate = useNavigate();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const favoritesUuids = useAppSelector(selectFavorites);
const meeting = useAppSelector((state) =>
selectMeeting(state, status.meetingID)
);
const dispatch = useAppDispatch();
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@ -14,7 +33,7 @@ const SettingsButton: React.FC = () => {
const handleClose = () => { const handleClose = () => {
setAnchorEl(null); setAnchorEl(null);
}; };
return ( return (
<div> <div>
<IconButton <IconButton
@ -22,7 +41,8 @@ const SettingsButton: React.FC = () => {
aria-controls={open ? "context-menu" : undefined} aria-controls={open ? "context-menu" : undefined}
aria-haspopup="true" aria-haspopup="true"
aria-expanded={open ? "true" : undefined} aria-expanded={open ? "true" : undefined}
onClick={handleClick}> onClick={handleClick}
>
<Settings /> <Settings />
</IconButton> </IconButton>
<Menu <Menu
@ -32,13 +52,47 @@ const SettingsButton: React.FC = () => {
onClose={handleClose} onClose={handleClose}
MenuListProps={{ "aria-labelledby": "basic-button" }} MenuListProps={{ "aria-labelledby": "basic-button" }}
> >
<MenuItem onClick={handleClose}>Remove from Favourites</MenuItem> {favoritesUuids.includes(user.uuid) ? (
<MenuItem onClick={handleClose}>View current meeting</MenuItem> <MenuItem
<MenuItem onClick={handleClose}>View upcoming meetings</MenuItem> onClick={() => {
handleClose();
dispatch(removeFavorite(user.uuid));
}}
>
Remove from Favorites
</MenuItem>
) : (
<MenuItem
onClick={() => {
handleClose();
dispatch(addFavorite(user.uuid));
}}
>
Add to Favorites
</MenuItem>
)}
{status.inMeeting ? (
<MenuItem
onClick={() => {
handleClose();
dispatch(openMeetingDetails(meeting));
}}
>
View current meeting
</MenuItem>
) : null}
<MenuItem
onClick={() => {
handleClose();
navigate(`../contacts/${user.uuid}`);
}}
>
View upcoming meetings
</MenuItem>
<MenuItem onClick={handleClose}>Create meeting</MenuItem> <MenuItem onClick={handleClose}>Create meeting</MenuItem>
</Menu> </Menu>
</div> </div>
); );
}; };
export default SettingsButton; export default SettingsButton;

View File

@ -6,18 +6,35 @@ import {
ListSubheader, ListSubheader,
Toolbar, Toolbar,
} from "@mui/material"; } from "@mui/material";
import { useState } from "react";
import { useAppSelector } from "../../redux/hooks";
import { selectFavorites } from "../../redux/slices/favoritesSlice";
import { selectUserStatuses } from "../../redux/slices/meetingsAndUserStatusSlice";
import { selectTeam, selectUsers } from "../../redux/slices/usersSlice";
import GroupSelect from "./GroupSelect"; import GroupSelect from "./GroupSelect";
import SidebarUser, { SidebarUserObj } from "./SidebarUser"; import SidebarUser from "./SidebarUser";
// TODO: toolbar on top of sidebar since it goes under the navbar // TODO: toolbar on top of sidebar since it goes under the navbar
const drawerWidth = 200; const drawerWidth = 200;
interface Props { const Sidebar: React.FC = () => {
users: SidebarUserObj[]; const [group, setGroup] = useState<string>("Favorites");
}
const favoritesUuids = useAppSelector(selectFavorites);
const teamUuids = useAppSelector(selectTeam);
const groupMembersUuids: string[] =
group === "Favorites" ? favoritesUuids : teamUuids;
const groupMembers = useAppSelector((state) =>
selectUsers(state, groupMembersUuids)
);
const groupMemberStatuses = useAppSelector((state) =>
selectUserStatuses(state, groupMembersUuids)
);
console.log(group);
const Sidebar: React.FC<Props> = ({ users }: Props) => {
return ( return (
<Drawer <Drawer
sx={{ sx={{
@ -32,11 +49,11 @@ const Sidebar: React.FC<Props> = ({ users }: Props) => {
<Toolbar /> <Toolbar />
<List disablePadding sx={{ height: "80%", overflow: "auto" }}> <List disablePadding sx={{ height: "80%", overflow: "auto" }}>
<ListSubheader disableGutters> <ListSubheader disableGutters>
<GroupSelect /> <GroupSelect group={group} setGroup={setGroup} />
</ListSubheader> </ListSubheader>
{users.map((user) => ( {groupMembers.map((user, i) => (
<ListItem disablePadding key={user.id}> <ListItem disablePadding key={user.uuid}>
<SidebarUser user={user} /> <SidebarUser user={user} status={groupMemberStatuses[i]} />
</ListItem> </ListItem>
))} ))}
</List> </List>

View File

@ -1,70 +1,36 @@
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import UserLite from "../../api-bodies/UserLite";
import UserStatus from "../../api-bodies/UserStatus";
import SettingsButton from "./SettingsButton"; import SettingsButton from "./SettingsButton";
import { MeetingStatus, getStatusColor } from "../../utils";
export interface SidebarUserObj {
id: number,
name: string,
meetingStatus: MeetingStatus;
}
export const enum MeetingStatus {
OFFLINE = 0,
ONLINE = 1,
AWAY = 2,
IN_MEETING = 3
}
const getMeetingStatusText = (ms: MeetingStatus): string => {
switch (ms) {
case MeetingStatus.OFFLINE: {
return "Offline";
}
case MeetingStatus.ONLINE: {
return "Online";
}
case MeetingStatus.AWAY: {
return "Away";
}
case MeetingStatus.IN_MEETING: {
return "In meeting";
}
}
};
const getStatusColor = (ms: MeetingStatus): string => {
switch (ms) {
case MeetingStatus.OFFLINE: {
return "#b8b8b8";
}
case MeetingStatus.ONLINE: {
return "#70ff70";
}
case MeetingStatus.AWAY: {
return "#ff7070";
}
case MeetingStatus.IN_MEETING: {
return "#e8da46";
}
}
};
interface Props { interface Props {
user: SidebarUserObj; user: UserLite;
status: UserStatus;
} }
const SidebarUser: React.FC<Props> = ({ user }: Props) => { const SidebarUser: React.FC<Props> = ({ user, status }: Props) => {
return ( return (
<Box sx={{ display: "flex", flexDirection: "row", p: 1 }}> <Box sx={{ display: "flex", flexDirection: "row", p: 1 }}>
<Box <Box
bgcolor={getStatusColor(user.meetingStatus)} bgcolor={getStatusColor(
sx={{ p: 1 }} /> status.inMeeting
? MeetingStatus.IN_MEETING
: MeetingStatus.NOT_IN_MEETING
)}
sx={{ p: 1 }}
/>
<Box sx={{ display: "flex", flexDirection: "column", ml: 1 }}> <Box sx={{ display: "flex", flexDirection: "column", ml: 1 }}>
<Typography variant="h6">{user.name}</Typography> <Typography variant="h6">{user.name}</Typography>
<Typography variant="caption">{getMeetingStatusText(user.meetingStatus)}</Typography> <Typography variant="caption">
{status.inMeeting
? MeetingStatus.IN_MEETING
: MeetingStatus.NOT_IN_MEETING}
</Typography>
</Box> </Box>
<SettingsButton /> <SettingsButton user={user} status={status} />
</Box> </Box>
); );
}; };
export default SidebarUser; export default SidebarUser;

View File

@ -19,6 +19,7 @@ export const meetingDetailsOpenSlice = createSlice({
open: (state, action) => { open: (state, action) => {
state.open = true; state.open = true;
state.meeting = action.payload; state.meeting = action.payload;
console.log(action.payload);
}, },
close: (state) => { close: (state) => {
state.open = false; state.open = false;

View File

@ -75,4 +75,23 @@ export const selectUserStatus = (
}; };
} }
}; };
export const selectUserStatuses = (
state: RootState,
uuids: string[]
): UserStatus[] => {
const userStatuses: UserStatus[] = [];
uuids.forEach((uuid) => {
const userStatus = state.meetingsAndUserStatuses.userStatuses[uuid];
if (userStatus) {
userStatuses.push(userStatus);
} else {
userStatuses.push({
uuid: uuid,
inMeeting: false,
meetingID: null,
});
}
});
return userStatuses;
};
export default meetingsAndUserStatusSlice.reducer; export default meetingsAndUserStatusSlice.reducer;

View File

@ -48,19 +48,22 @@ export const fetchUsers = createAsyncThunk(
} }
); );
export const selectUser = (
state: RootState,
uuid: string | undefined
): UserLite | undefined => {
return uuid ? state.users.users[uuid] : undefined;
};
export const selectUsers = (state: RootState, uuids: string[]): UserLite[] => { export const selectUsers = (state: RootState, uuids: string[]): UserLite[] => {
const users: UserLite[] = []; const users: UserLite[] = [];
uuids.forEach((uuid) => users.push(state.users.users[uuid])); uuids.forEach((uuid) => users.push(state.users.users[uuid]));
return users; return users;
}; };
export const selectTeam = (state: RootState) => { export const selectTeam = (state: RootState) => {
const team: UserLite[] = []; const team: string[] = [];
if (state.users.team.manager !== null) if (state.users.team.manager !== null) team.push(state.users.team.manager);
team.push(state.users.users[state.users.team.manager]); state.users.team.sameManager.forEach((u) => team.push(u));
state.users.team.sameManager.forEach((u) => team.push(state.users.users[u])); state.users.team.directReports.forEach((u) => team.push(u));
state.users.team.directReports.forEach((u) =>
team.push(state.users.users[u])
);
return team; return team;
}; };
export default usersSlice.reducer; export default usersSlice.reducer;

17
src/utils.tsx Normal file
View File

@ -0,0 +1,17 @@
const enum MeetingStatus {
NOT_IN_MEETING = "Not in meeting",
IN_MEETING = "In meeting",
}
const getStatusColor = (ms: MeetingStatus): string => {
switch (ms) {
case MeetingStatus.NOT_IN_MEETING: {
return "#70ff70";
}
case MeetingStatus.IN_MEETING: {
return "#ff7070";
}
}
};
export { MeetingStatus, getStatusColor };