Merge pull request #31 from CPSC319-Winter-term-2/front-end-fixes

redux updates
This commit is contained in:
cth0604 2022-03-16 17:14:13 -07:00 committed by GitHub
commit 16587686d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 316 additions and 183 deletions

View File

@ -1,4 +1,4 @@
const people = [
const userLites = [
{
uuid: "0",
emailAddress: "cth0604@gmail.com",
@ -58,7 +58,7 @@ const meetings = [
meetingId: "1",
liveParticipantIds: [],
registrantIds: ["0", "2", "4"],
start: "2022-03-13T17:30:00",
start: "2022-03-16T17:30:00",
duration: 30,
timezone: "",
joinUrl: "",
@ -66,7 +66,7 @@ const meetings = [
},
{
meetingId: "2",
liveParticipantIds: [],
liveParticipantIds: ["3", "5"],
registrantIds: ["3", "5", "6"],
start: "2022-03-13T17:30:00",
duration: 30,
@ -83,4 +83,6 @@ const team = {
directReports: [],
};
export { people, meetings, team };
const favorites = ["2", "4"];
export { userLites, meetings, team, favorites };

10
src/api-bodies/User.tsx Normal file
View File

@ -0,0 +1,10 @@
interface User {
uuid: string;
emailAddress: string;
name: string;
role: string;
inMeeting: boolean;
meetingID: string | null;
}
export default User;

View File

@ -0,0 +1,7 @@
interface UserStatus {
uuid: string;
inMeeting: boolean;
meetingID: string | null;
}
export default UserStatus;

View File

@ -16,10 +16,22 @@ const returnStatusColor = (
}
};
const getMeetingDuration = (meeting: DetailedMeeting) => {
const getUpcomingMeetingTime = (meeting: DetailedMeeting) => {
const startDate = new Date(meeting.start);
const endDate = new Date(startDate.getTime() + meeting.duration * 60000);
return `${startDate.toTimeString()} - ${endDate.toTimeString()}`;
const startTime = startDate
.toTimeString()
.split(" ")[0]
.split(":")
.slice(0, 2)
.join(":");
const endTime = endDate
.toTimeString()
.split(" ")[0]
.split(":")
.slice(0, 2)
.join(":");
return `${startTime} - ${endTime}`;
};
export { returnStatusColor, getMeetingDuration };
export { returnStatusColor, getUpcomingMeetingTime };

View File

@ -16,6 +16,7 @@ const Body: React.FC<Props> = (props) => {
flexDirection: "column",
width: "100%",
height: "100%",
mt: 2,
}}
>
<Toolbar />

View File

@ -11,10 +11,7 @@ import React from "react";
import { ExpandLess, ExpandMore } from "@mui/icons-material";
import ContactItem from "./sidebar-components/ContactItem";
import { useAppSelector } from "../../../redux/hooks";
import {
selectFavoritesJSON,
selectTeamJSON,
} from "../../../redux/slices/peopleSlice";
import { selectUsers, selectTeam } from "../../../redux/slices/usersSlice";
import { selectFavorites } from "../../../redux/slices/favoritesSlice";
import UserLite from "../../../api-bodies/UserLite";
@ -28,11 +25,11 @@ const Sidebar: React.FC<Props> = (props) => {
const [favoritesOpen, setFavoritesOpen] = React.useState<boolean>(true);
const [teamOpen, setTeamOpen] = React.useState<boolean>(false);
const favorites = useAppSelector(selectFavorites);
const favoritesJSON = useAppSelector((state) =>
selectFavoritesJSON(state, favorites)
const favoritesUuids = useAppSelector(selectFavorites);
const favorites = useAppSelector((state) =>
selectUsers(state, favoritesUuids)
);
const teamJSON = useAppSelector(selectTeamJSON);
const team = useAppSelector(selectTeam);
return (
<Drawer
@ -54,13 +51,13 @@ const Sidebar: React.FC<Props> = (props) => {
{favoritesOpen ? <ExpandLess /> : <ExpandMore />}
<ListItemText primary="Favorites" sx={{ textAlign: "center" }} />
<ListItemText
primary={favoritesJSON.length}
primary={favorites.length}
sx={{ textAlign: "right" }}
/>
</ListItemButton>
<Collapse in={favoritesOpen} timeout="auto" unmountOnExit>
<List disablePadding>
{favoritesJSON.map((favorite, i) => (
{favorites.map((favorite, i) => (
<ContactItem
contactInfo={favorite}
key={i}
@ -72,11 +69,11 @@ const Sidebar: React.FC<Props> = (props) => {
<ListItemButton onClick={() => setTeamOpen(!teamOpen)}>
{teamOpen ? <ExpandLess /> : <ExpandMore />}
<ListItemText primary="Team" sx={{ textAlign: "center" }} />
<ListItemText primary={teamJSON.length} sx={{ textAlign: "right" }} />
<ListItemText primary={team.length} sx={{ textAlign: "right" }} />
</ListItemButton>
<Collapse in={teamOpen} timeout="auto" unmountOnExit>
<List disablePadding>
{teamJSON.map((member, i) => (
{team.map((member, i) => (
<ContactItem
contactInfo={member}
key={i}

View File

@ -3,8 +3,8 @@ import React from "react";
import UserLite from "../../../../api-bodies/UserLite";
import { useAppSelector, useAppDispatch } from "../../../../redux/hooks";
import { open } from "../../../../redux/slices/meetingDetailsOpenSlice";
import { selectUserUpcomingMeetings } from "../../../../redux/slices/meetingsSlice";
import { getMeetingDuration } from "../../Utils";
import { selectUserUpcomingMeetings } from "../../../../redux/slices/meetingsAndUserStatusSlice";
import { getUpcomingMeetingTime } from "../../Utils";
interface Props {
contactInfo: UserLite;
@ -53,7 +53,7 @@ const LowerBody: React.FC<Props> = (props) => {
{meeting.topic}
</Button>
<Typography sx={{ pt: 1.5 }}>
{getMeetingDuration(meeting)}
{getUpcomingMeetingTime(meeting)}
</Typography>
</Box>
))}

View File

@ -6,12 +6,34 @@ import GroupsIcon from "@mui/icons-material/Groups";
import AddIcon from "@mui/icons-material/Add";
import Status from "../../Status";
import UserLite from "../../../../api-bodies/UserLite";
import UserStatus from "../../../../api-bodies/UserStatus";
import { useAppSelector, useAppDispatch } from "../../../../redux/hooks";
import {
selectUserStatus,
selectMeeting,
} from "../../../../redux/slices/meetingsAndUserStatusSlice";
import {
selectFavorites,
addFavorite,
removeFavorite,
} from "../../../../redux/slices/favoritesSlice";
import RemoveIcon from "@mui/icons-material/Remove";
interface Props {
contactInfo: UserLite;
}
const UpperBody: React.FC<Props> = (props) => {
const dispatch = useAppDispatch();
const userStatus: UserStatus | null = useAppSelector((state) =>
selectUserStatus(state, props.contactInfo.uuid)
);
const favoritesUuids = useAppSelector(selectFavorites);
const detailedMeeting = useAppSelector((state) =>
selectMeeting(state, userStatus.meetingID)
);
const status: Status =
userStatus && userStatus.inMeeting ? Status.Online : Status.Offline;
return (
<Box
sx={{
@ -34,9 +56,25 @@ const UpperBody: React.FC<Props> = (props) => {
}}
>
<Typography variant="h3">{props.contactInfo.name}</Typography>
<Button variant="outlined" color="success" startIcon={<AddIcon />}>
{!favoritesUuids.includes(props.contactInfo.uuid) ? (
<Button
variant="outlined"
color="success"
startIcon={<AddIcon />}
onClick={() => dispatch(addFavorite(props.contactInfo.uuid))}
>
favorites
</Button>
) : (
<Button
variant="outlined"
color="secondary"
startIcon={<RemoveIcon />}
onClick={() => dispatch(removeFavorite(props.contactInfo.uuid))}
>
favorites
</Button>
)}
</Box>
<Box
sx={{
@ -64,8 +102,10 @@ const UpperBody: React.FC<Props> = (props) => {
</IconButton>
</Box>
<Box sx={{ display: "flex", flexDirection: "column" }}>
<Typography sx={{ textAlign: "right" }}>{Status.Online}</Typography>
<Typography>MeetingName-1372</Typography>
<Typography sx={{ textAlign: "right" }}>{status}</Typography>
{detailedMeeting ? (
<Typography>{detailedMeeting.topic}</Typography>
) : null}
</Box>
</Box>
</Box>

View File

@ -5,6 +5,9 @@ import CircleIcon from "@mui/icons-material/Circle";
import { returnStatusColor } from "../../Utils";
import UserLite from "../../../../api-bodies/UserLite";
import Status from "../../Status";
import { useAppSelector } from "../../../../redux/hooks";
import { selectUserStatus } from "../../../../redux/slices/meetingsAndUserStatusSlice";
import UserStatus from "../../../../api-bodies/UserStatus";
interface Props {
contactInfo: UserLite;
@ -12,6 +15,11 @@ interface Props {
}
const ContactItem: React.FC<Props> = (props) => {
const userStatus: UserStatus | null = useAppSelector((state) =>
selectUserStatus(state, props.contactInfo.uuid)
);
const status: Status =
userStatus && userStatus.inMeeting ? Status.Online : Status.Offline;
return (
<ListItemButton
onClick={() => {
@ -23,10 +31,10 @@ const ContactItem: React.FC<Props> = (props) => {
</ListItemIcon>
<ListItemText
primary={props.contactInfo.name}
secondary={Status.Online}
secondary={status}
sx={{ flexGrow: 1 }}
/>
<CircleIcon color={returnStatusColor(Status.Online)} />
<CircleIcon color={returnStatusColor(status)} />
</ListItemButton>
);
};

View File

@ -12,7 +12,7 @@ const MeetingDetails: React.FC = () => {
const meetingDetailsOpen = useAppSelector(selectMeetingDetailsOpen);
const dispatch = useAppDispatch();
return (
return meetingDetailsOpen.meeting ? (
<Dialog
fullScreen
open={meetingDetailsOpen.open}
@ -21,7 +21,7 @@ const MeetingDetails: React.FC = () => {
<Header meeting={meetingDetailsOpen.meeting} />
<Body meeting={meetingDetailsOpen.meeting} />
</Dialog>
);
) : null;
};
export default MeetingDetails;

View File

@ -0,0 +1,30 @@
import DetailedMeeting from "../../api-bodies/DetailedMeeting";
const getMeetingStatus = (meeting: DetailedMeeting) => {
const startDate = new Date(meeting.start);
const endDate = new Date(startDate.getTime() + meeting.duration * 60000);
const currentDate = new Date();
if (currentDate > startDate && currentDate < endDate) return "Live";
else if (currentDate < startDate) return "Scheduled";
else return "Finished";
};
const getUpcomingMeetingTime = (meeting: DetailedMeeting) => {
const startDate = new Date(meeting.start);
const endDate = new Date(startDate.getTime() + meeting.duration * 60000);
const startTime = startDate
.toTimeString()
.split(" ")[0]
.split(":")
.slice(0, 2)
.join(":");
const endTime = endDate
.toTimeString()
.split(" ")[0]
.split(":")
.slice(0, 2)
.join(":");
return `${startTime} - ${endTime}`;
};
export { getMeetingStatus, getUpcomingMeetingTime };

View File

@ -9,12 +9,20 @@ import {
import React from "react";
import PersonOutlineIcon from "@mui/icons-material/PersonOutline";
import DetailedMeeting from "../../../api-bodies/DetailedMeeting";
import { useAppSelector } from "../../../redux/hooks";
import { selectUsers } from "../../../redux/slices/usersSlice";
import { getMeetingStatus } from "../Utils";
import UserLite from "../../../api-bodies/UserLite";
interface Props {
meeting: DetailedMeeting | null;
meeting: DetailedMeeting;
}
const Body: React.FC<Props> = () => {
const Body: React.FC<Props> = (props) => {
const registrants: UserLite[] = useAppSelector((state) =>
selectUsers(state, props.meeting.registrantIds)
);
return (
<Box
sx={{
@ -34,7 +42,7 @@ const Body: React.FC<Props> = () => {
}}
>
<Typography>Feb 10, 2022 10:45 am - 11:00 am</Typography>
<Typography>Status: Live</Typography>
<Typography>Status: {getMeetingStatus(props.meeting)}</Typography>
<Typography>
Topic: Lorem Ipsum is simply dummy text of the printing and
typesetting industry. Lorem Ipsum has been the industrys standard
@ -56,20 +64,14 @@ const Body: React.FC<Props> = () => {
<ListItem sx={{ borderBottom: 1 }}>
<ListItemText primary="Invited" />
</ListItem>
<ListItem>
{registrants.map((registrant, i) => (
<ListItem key={i}>
<ListItemIcon>
<PersonOutlineIcon />
</ListItemIcon>
<ListItemText primary="Taehee Choi" />
</ListItem>
</List>
<List>
<ListItem>
<ListItemIcon>
<PersonOutlineIcon />
</ListItemIcon>
<ListItemText primary="Taehee Choi" />
<ListItemText primary={registrant.name} />
</ListItem>
))}
</List>
</Box>
</Box>

View File

@ -5,14 +5,12 @@ import { AuthProvider } from "./context/AuthProvider";
import { Provider } from "react-redux";
import { store } from "./redux/store";
import { fetchFavorites } from "./redux/slices/favoritesSlice";
import { fetchTeam } from "./redux/slices/teamSlice";
import { fetchMeetings } from "./redux/slices/meetingsSlice";
import { fetchPeople } from "./redux/slices/peopleSlice";
import { fetchMeetings } from "./redux/slices/meetingsAndUserStatusSlice";
import { fetchUsers } from "./redux/slices/usersSlice";
store.dispatch(fetchPeople(""));
store.dispatch(fetchFavorites(""));
store.dispatch(fetchTeam(""));
store.dispatch(fetchMeetings(""));
store.dispatch(fetchUsers(""));
store.dispatch(fetchFavorites(""));
const Index: React.FC = () => {
return (

View File

@ -1,5 +1,6 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { RootState } from "../store";
import { favorites } from "../../api-bodies/MockData";
interface FavoritesState {
favorites: string[];
@ -34,7 +35,7 @@ export const fetchFavorites = createAsyncThunk(
// const response = await client.post("/fakeApi/posts", initialPost);
// !!!
console.log(uuid);
return ["2", "4"];
return favorites;
}
);
export const addFavorite = createAsyncThunk(

View File

@ -0,0 +1,78 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import DetailedMeeting from "../../api-bodies/DetailedMeeting";
import { RootState } from "../store";
import { meetings } from "../../api-bodies/MockData";
import UserStatus from "../../api-bodies/UserStatus";
interface MeetingsAndUserStatusState {
meetings: DetailedMeeting[];
userStatuses: Record<string, UserStatus>;
}
const initialState: MeetingsAndUserStatusState = {
meetings: [],
userStatuses: {},
};
export const meetingsAndUserStatusSlice = createSlice({
name: "meetingsAndUserStatus",
initialState,
reducers: {},
extraReducers(builder) {
builder.addCase(fetchMeetings.fulfilled, (state, action) => {
state.meetings = action.payload.meetings;
state.userStatuses = action.payload.userStatuses;
});
},
});
export const fetchMeetings = createAsyncThunk(
"meetingsAndUserStatus/fetchMeetings",
async (uuid: string) => {
// const response = await client.post("/fakeApi/posts", initialPost);
// !!!
console.log(uuid);
const userStatuses: Record<string, UserStatus> = {};
meetings.forEach((meeting) => {
meeting.liveParticipantIds.forEach((uuid) => {
userStatuses[uuid] = {
uuid: uuid,
inMeeting: true,
meetingID: meeting.meetingId,
};
});
});
return { userStatuses: userStatuses, meetings: meetings };
}
);
export const selectMeetings = (state: RootState) =>
state.meetingsAndUserStatuses.meetings;
export const selectMeeting = (state: RootState, meetingID: string | null) => {
return meetingID !== null
? state.meetingsAndUserStatuses.meetings.find(
(meeting) => meeting.meetingId === meetingID
)
: null;
};
export const selectUserUpcomingMeetings = (state: RootState, uuid: string) => {
return state.meetingsAndUserStatuses.meetings.filter((meeting) =>
meeting.registrantIds.includes(uuid)
);
};
export const selectUserStatus = (
state: RootState,
uuid: string
): UserStatus => {
const userStatus = state.meetingsAndUserStatuses.userStatuses[uuid];
if (userStatus) {
return userStatus;
} else {
return {
uuid: uuid,
inMeeting: false,
meetingID: null,
};
}
};
export default meetingsAndUserStatusSlice.reducer;

View File

@ -1,41 +0,0 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import DetailedMeeting from "../../api-bodies/DetailedMeeting";
import { RootState } from "../store";
import { meetings } from "../../api-bodies/MockData";
interface MeetingsState {
meetings: DetailedMeeting[];
}
const initialState: MeetingsState = {
meetings: [],
};
export const meetingsSlice = createSlice({
name: "meetings",
initialState,
reducers: {},
extraReducers(builder) {
builder.addCase(fetchMeetings.fulfilled, (state, action) => {
return action.payload;
});
},
});
export const fetchMeetings = createAsyncThunk(
"meetings/fetchMeetings",
async (uuid: string) => {
// const response = await client.post("/fakeApi/posts", initialPost);
// !!!
console.log(uuid);
return { meetings: meetings };
}
);
export const selectMeetings = (state: RootState) => state.meetings.meetings;
export const selectUserUpcomingMeetings = (state: RootState, uuid: string) => {
return state.meetings.meetings.filter((meeting) =>
meeting.registrantIds.includes(uuid)
);
};
export default meetingsSlice.reducer;

View File

@ -1,40 +0,0 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import UserLite from "../../api-bodies/UserLite";
import { RootState } from "../store";
import { people } from "../../api-bodies/MockData";
interface PeopleState {
people: UserLite[];
}
const initialState: PeopleState = {
people: [],
};
export const peopleSlice = createSlice({
name: "people",
initialState,
reducers: {},
extraReducers(builder) {
builder.addCase(fetchPeople.fulfilled, (state, action) => {
console.log(action.payload);
state.people = action.payload;
});
},
});
export const fetchPeople = createAsyncThunk(
"people/fetchPeople",
async (uuid: string) => {
// const response = await client.post("/fakeApi/posts", initialPost);
// !!!
console.log(uuid);
return people;
}
);
export const selectFavoritesJSON = (state: RootState, favorites: string[]) => {
return state.people.people.filter((p) => favorites.includes(p.uuid));
};
export const selectTeamJSON = (state: RootState) => state.people.people;
export default peopleSlice.reducer;

View File

@ -1,41 +0,0 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { RootState } from "../store";
import { team } from "../../api-bodies/MockData";
interface TeamState {
user: string | null;
manager: string | null;
sameManager: string[];
directReports: string[];
}
const initialState: TeamState = {
user: null,
manager: null,
sameManager: [],
directReports: [],
};
export const teamSlice = createSlice({
name: "team",
initialState,
reducers: {},
extraReducers(builder) {
builder.addCase(fetchTeam.fulfilled, (state, action) => {
return action.payload;
});
},
});
export const fetchTeam = createAsyncThunk(
"team/fetchTeam",
async (uuid: string) => {
// const response = await client.post("/fakeApi/posts", initialPost);
// !!!
console.log(uuid);
return team;
}
);
export const selectTeam = (state: RootState) => state.team;
export default teamSlice.reducer;

View File

@ -0,0 +1,66 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { RootState } from "../store";
import { userLites, team } from "../../api-bodies/MockData";
import UserLite from "../../api-bodies/UserLite";
interface TeamState {
user: string | null;
manager: string | null;
sameManager: string[];
directReports: string[];
}
interface UsersState {
users: Record<string, UserLite>;
team: TeamState;
}
const initialState: UsersState = {
users: {},
team: { user: null, manager: null, sameManager: [], directReports: [] },
};
export const usersSlice = createSlice({
name: "users",
initialState,
reducers: {},
extraReducers(builder) {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.users = action.payload.users;
state.team = action.payload.team;
});
},
});
export const fetchUsers = createAsyncThunk(
"users/fetchUsers",
async (uuid: string) => {
// fetch userfull
// fetch managerfull
// now you have yourself, co-workers, manager, and direct reports (your team basically!)
// call setTeam reducer in teamSlice
console.log(uuid);
const users: Record<string, UserLite> = {};
userLites.forEach((userLite) => {
users[userLite.uuid] = userLite;
});
return { users: users, team: team };
}
);
export const selectUsers = (state: RootState, uuids: string[]): UserLite[] => {
const users: UserLite[] = [];
uuids.forEach((uuid) => users.push(state.users.users[uuid]));
return users;
};
export const selectTeam = (state: RootState) => {
const team: UserLite[] = [];
if (state.users.team.manager !== null)
team.push(state.users.users[state.users.team.manager]);
state.users.team.sameManager.forEach((u) => team.push(state.users.users[u]));
state.users.team.directReports.forEach((u) =>
team.push(state.users.users[u])
);
return team;
};
export default usersSlice.reducer;

View File

@ -1,17 +1,15 @@
import { configureStore } from "@reduxjs/toolkit";
import meetingDetailsOpenReducer from "./slices/meetingDetailsOpenSlice";
import favoritesReducer from "./slices/favoritesSlice";
import teamReducer from "./slices/teamSlice";
import meetingsReducer from "./slices/meetingsSlice";
import peopleReducer from "./slices/peopleSlice";
import meetingsAndUserStatusReducer from "./slices/meetingsAndUserStatusSlice";
import usersReducer from "./slices/usersSlice";
export const store = configureStore({
reducer: {
meetingDetailsOpen: meetingDetailsOpenReducer,
favorites: favoritesReducer,
team: teamReducer,
meetings: meetingsReducer,
people: peopleReducer,
meetingsAndUserStatuses: meetingsAndUserStatusReducer,
users: usersReducer,
},
});

View File

@ -5,3 +5,8 @@ body,
#app > div {
height: 100%;
}
html,
body {
overflow: hidden;
}