├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── _redirects
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── app
│ ├── hooks.ts
│ ├── slices
│ │ ├── AuthSlice.ts
│ │ └── MeetingSlice.ts
│ └── store.ts
├── assets
│ ├── animation.gif
│ ├── dashboard1.png
│ ├── dashboard2.png
│ ├── dashboard3.png
│ ├── logo.png
│ ├── meeting1.png
│ └── meeting2.png
├── components
│ ├── EditFlyout.tsx
│ ├── FormComponents
│ │ ├── CreateMeetingButtons.tsx
│ │ ├── MeetingDateField.tsx
│ │ ├── MeetingMaximumUsersField.tsx
│ │ ├── MeetingNameFIeld.tsx
│ │ └── MeetingUserField.tsx
│ ├── Header.tsx
│ ├── ThemeSelector.tsx
│ └── Themes
│ │ ├── DarkTheme.tsx
│ │ └── LightTheme.tsx
├── hooks
│ ├── useAuth.tsx
│ ├── useFetchUsers.tsx
│ └── useToast.tsx
├── index.tsx
├── pages
│ ├── CreateMeeting.tsx
│ ├── Dashboard.tsx
│ ├── JoinMeeting.tsx
│ ├── Login.tsx
│ ├── Meeting.tsx
│ ├── MyMeetings.tsx
│ ├── OneOnOneMeeting.tsx
│ └── VideoConference.tsx
├── react-app-env.d.ts
└── utils
│ ├── breadcrumbs.ts
│ ├── firebaseConfig.ts
│ ├── generateMeetingId.ts
│ └── types.ts
├── tsconfig.json
├── yarn-error.log
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 |
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "zoom-clone",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@elastic/datemath": "^5.0.3",
7 | "@elastic/eui": "^70.2.0",
8 | "@emotion/css": "^11.10.5",
9 | "@emotion/react": "^11.10.5",
10 | "@reduxjs/toolkit": "^1.9.0",
11 | "@testing-library/jest-dom": "^5.16.5",
12 | "@testing-library/react": "^13.4.0",
13 | "@testing-library/user-event": "^13.5.0",
14 | "@types/jest": "^27.5.2",
15 | "@types/node": "^16.18.3",
16 | "@types/react": "^18.0.25",
17 | "@types/react-dom": "^18.0.8",
18 | "@zegocloud/zego-uikit-prebuilt": "1.3.16",
19 | "firebase": "^9.14.0",
20 | "moment": "^2.29.4",
21 | "netlify-cli": "^12.2.0",
22 | "prop-types": "^15.8.1",
23 | "react": "^17.0.2",
24 | "react-dom": "^17.0.2",
25 | "react-redux": "^8.0.5",
26 | "react-router-dom": "^6.4.3",
27 | "react-scripts": "5.0.1",
28 | "typescript": "^4.8.4",
29 | "web-vitals": "^2.1.4"
30 | },
31 | "scripts": {
32 | "start": "react-scripts start",
33 | "build": "react-scripts build",
34 | "test": "react-scripts test",
35 | "eject": "react-scripts eject"
36 | },
37 | "eslintConfig": {
38 | "extends": [
39 | "react-app",
40 | "react-app/jest"
41 | ]
42 | },
43 | "browserslist": {
44 | "production": [
45 | ">0.2%",
46 | "not dead",
47 | "not op_mini all"
48 | ],
49 | "development": [
50 | "last 1 chrome version",
51 | "last 1 firefox version",
52 | "last 1 safari version"
53 | ]
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koolkishan/react-zoom-clone/534872419aac6d04a6ab55ddc48fd2f9268c645c/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
22 |
31 | React App
32 |
33 |
34 |
35 |
36 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koolkishan/react-zoom-clone/534872419aac6d04a6ab55ddc48fd2f9268c645c/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koolkishan/react-zoom-clone/534872419aac6d04a6ab55ddc48fd2f9268c645c/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": "./",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | EuiGlobalToastList,
3 | EuiProvider,
4 | EuiThemeProvider,
5 | } from "@elastic/eui";
6 | import { EuiThemeColorMode } from "@elastic/eui/src/services/theme";
7 | import React, { useEffect, useState } from "react";
8 | import { useDispatch } from "react-redux";
9 | import { Route, Routes } from "react-router-dom";
10 | import { useAppSelector } from "./app/hooks";
11 | import { setToasts } from "./app/slices/MeetingSlice";
12 | import ThemeSelector from "./components/ThemeSelector";
13 | import CreateMeeting from "./pages/CreateMeeting";
14 | import Dashboard from "./pages/Dashboard";
15 | import JoinMeeting from "./pages/JoinMeeting";
16 | import Login from "./pages/Login";
17 | import Meeting from "./pages/Meeting";
18 | import MyMeetings from "./pages/MyMeetings";
19 | import OneOnOneMeeting from "./pages/OneOnOneMeeting";
20 | import VideoConference from "./pages/VideoConference";
21 |
22 | export default function App() {
23 | const dispatch = useDispatch();
24 | const isDarkTheme = useAppSelector((zoomApp) => zoomApp.auth.isDarkTheme);
25 | const [isInitialEffect, setIsInitialEffect] = useState(true);
26 | const toasts = useAppSelector((zoom) => zoom.meetings.toasts);
27 |
28 | const removeToast = (removedToast: { id: string }) => {
29 | dispatch(
30 | setToasts(
31 | toasts.filter((toast: { id: string }) => toast.id !== removedToast.id)
32 | )
33 | );
34 | };
35 | const [theme, setTheme] = useState("light");
36 | useEffect(() => {
37 | const theme = localStorage.getItem("zoom-theme");
38 | if (theme) {
39 | setTheme(theme as EuiThemeColorMode);
40 | } else {
41 | localStorage.setItem("zoom-theme", "light");
42 | }
43 | }, []);
44 |
45 | useEffect(() => {
46 | if (isInitialEffect) setIsInitialEffect(false);
47 | else {
48 | window.location.reload();
49 | }
50 | // eslint-disable-next-line react-hooks/exhaustive-deps
51 | }, [isDarkTheme]);
52 |
53 | const overrides = {
54 | colors: {
55 | LIGHT: { primary: "#0b5cff" },
56 | DARK: { primary: "#0b5cff" },
57 | },
58 | };
59 |
60 | return (
61 |
62 |
63 |
64 |
65 | } />
66 | } />
67 | } />
68 | } />
69 | } />
70 | } />
71 | } />
72 | } />
73 | } />
74 |
75 |
80 |
81 |
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/src/app/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { useSelector } from "react-redux";
3 | import { TypedUseSelectorHook } from "react-redux";
4 | import { AppDispatch, RootState } from "./store";
5 |
6 | export const useAppDispatch: () => AppDispatch = useDispatch;
7 | export const useAppSelector: TypedUseSelectorHook = useSelector;
8 |
--------------------------------------------------------------------------------
/src/app/slices/AuthSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit";
2 |
3 | interface authInitialState {
4 | userInfo:
5 | | undefined
6 | | {
7 | uid: string;
8 | email: string;
9 | name: string;
10 | };
11 | isDarkTheme: boolean;
12 | }
13 |
14 | const initialState: authInitialState = {
15 | userInfo: undefined,
16 | isDarkTheme: false,
17 | };
18 |
19 | export const authSlice = createSlice({
20 | name: "auth",
21 | initialState,
22 | reducers: {
23 | changeTheme: (state, action) => {
24 | state.isDarkTheme = action.payload.isDarkTheme;
25 | },
26 | setUser: (
27 | state,
28 | action: PayloadAction<{
29 | uid: string;
30 | email: string;
31 | name: string;
32 | }>
33 | ) => {
34 | state.userInfo = action.payload;
35 | },
36 | },
37 | });
38 |
39 | export const { setUser, changeTheme } = authSlice.actions;
40 |
--------------------------------------------------------------------------------
/src/app/slices/MeetingSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { ToastType } from "../../utils/types";
3 |
4 | interface meetingInitialState {
5 | toasts: Array;
6 | }
7 |
8 | const initialState: meetingInitialState = {
9 | toasts: [],
10 | };
11 |
12 | export const meetingsSlice = createSlice({
13 | name: "meetings",
14 | initialState,
15 | reducers: {
16 | setToasts: (state, action) => {
17 | state.toasts = action.payload;
18 | },
19 | },
20 | });
21 |
22 | export const { setToasts } = meetingsSlice.actions;
23 |
--------------------------------------------------------------------------------
/src/app/store.ts:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import { authSlice } from "./slices/AuthSlice";
3 | import { meetingsSlice } from "./slices/MeetingSlice";
4 |
5 | export const store = configureStore({
6 | reducer: {
7 | auth: authSlice.reducer,
8 | meetings: meetingsSlice.reducer,
9 | },
10 | });
11 |
12 | export type RootState = ReturnType;
13 | export type AppDispatch = typeof store.dispatch;
14 |
--------------------------------------------------------------------------------
/src/assets/animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koolkishan/react-zoom-clone/534872419aac6d04a6ab55ddc48fd2f9268c645c/src/assets/animation.gif
--------------------------------------------------------------------------------
/src/assets/dashboard1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koolkishan/react-zoom-clone/534872419aac6d04a6ab55ddc48fd2f9268c645c/src/assets/dashboard1.png
--------------------------------------------------------------------------------
/src/assets/dashboard2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koolkishan/react-zoom-clone/534872419aac6d04a6ab55ddc48fd2f9268c645c/src/assets/dashboard2.png
--------------------------------------------------------------------------------
/src/assets/dashboard3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koolkishan/react-zoom-clone/534872419aac6d04a6ab55ddc48fd2f9268c645c/src/assets/dashboard3.png
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koolkishan/react-zoom-clone/534872419aac6d04a6ab55ddc48fd2f9268c645c/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/meeting1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koolkishan/react-zoom-clone/534872419aac6d04a6ab55ddc48fd2f9268c645c/src/assets/meeting1.png
--------------------------------------------------------------------------------
/src/assets/meeting2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koolkishan/react-zoom-clone/534872419aac6d04a6ab55ddc48fd2f9268c645c/src/assets/meeting2.png
--------------------------------------------------------------------------------
/src/components/EditFlyout.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | EuiFlyout,
3 | EuiFlyoutBody,
4 | EuiFlyoutHeader,
5 | EuiForm,
6 | EuiFormRow,
7 | EuiSpacer,
8 | EuiSwitch,
9 | EuiTitle,
10 | } from "@elastic/eui";
11 | import { doc, updateDoc } from "firebase/firestore";
12 | import moment from "moment";
13 | import React, { useEffect, useState } from "react";
14 | import useFetchUsers from "../hooks/useFetchUsers";
15 | import useToast from "../hooks/useToast";
16 | import { firebaseDB } from "../utils/firebaseConfig";
17 | import { FieldErrorType, MeetingType, UserType } from "../utils/types";
18 | import CreateMeetingButtons from "./FormComponents/CreateMeetingButtons";
19 | import MeetingDateField from "./FormComponents/MeetingDateField";
20 | import MeetingMaximumUsersField from "./FormComponents/MeetingMaximumUsersField";
21 | import MeetingNameField from "./FormComponents/MeetingNameFIeld";
22 | import MeetingUserField from "./FormComponents/MeetingUserField";
23 |
24 | export default function EditFlyout({
25 | closeFlyout,
26 | meeting,
27 | }: {
28 | closeFlyout: any;
29 | meeting: MeetingType;
30 | }) {
31 | const [users] = useFetchUsers();
32 | const [createToast] = useToast();
33 | const [meetingName, setMeetingName] = useState(meeting.meetingName);
34 | const [meetingType] = useState(meeting.meetingType);
35 | const [selectedUser, setSelectedUser] = useState>([]);
36 | const [startDate, setStartDate] = useState(moment(meeting.meetingDate));
37 | const [size, setSize] = useState(1);
38 | const [status, setStatus] = useState(false);
39 | const onUserChange = (selectedOptions: Array) => {
40 | setSelectedUser(selectedOptions);
41 | };
42 |
43 | useEffect(() => {
44 | if (users) {
45 | const foundUsers: Array = [];
46 | meeting.invitedUsers.forEach((user: string) => {
47 | const findUser = users.find(
48 | (tempUser: UserType) => tempUser.uid === user
49 | );
50 | if (findUser) foundUsers.push(findUser);
51 | });
52 | setSelectedUser(foundUsers);
53 | }
54 | }, [users, meeting]);
55 |
56 | const [showErrors] = useState<{
57 | meetingName: FieldErrorType;
58 | meetingUsers: FieldErrorType;
59 | }>({
60 | meetingName: {
61 | show: false,
62 | message: [],
63 | },
64 | meetingUsers: {
65 | show: false,
66 | message: [],
67 | },
68 | });
69 |
70 | const editMeeting = async () => {
71 | const editedMeeting = {
72 | ...meeting,
73 | meetingName,
74 | meetingType,
75 | invitedUsers: selectedUser.map((user: UserType) => user.uid),
76 | maxUsers: size,
77 | meetingDate: startDate.format("L"),
78 | status: !status,
79 | };
80 | delete editedMeeting.docId;
81 | const docRef = doc(firebaseDB, "meetings", meeting.docId!);
82 | await updateDoc(docRef, editedMeeting);
83 | createToast({ title: "Meeting updated successfully.", type: "success" });
84 | closeFlyout(true);
85 | };
86 |
87 | return (
88 | closeFlyout()}>
89 |
90 |
91 | {meeting.meetingName}
92 |
93 |
94 |
95 |
96 |
104 | {meetingType === "anyone-can-join" ? (
105 |
106 | ) : (
107 |
120 | )}
121 |
122 |
123 | setStatus(e.target.checked)}
128 | />
129 |
130 |
131 |
136 |
137 |
138 |
139 | );
140 | }
141 |
--------------------------------------------------------------------------------
/src/components/FormComponents/CreateMeetingButtons.tsx:
--------------------------------------------------------------------------------
1 | import { EuiButton, EuiFlexGroup, EuiFlexItem } from "@elastic/eui";
2 | import React from "react";
3 | import { useNavigate } from "react-router-dom";
4 |
5 | function CreateMeetingButtons({
6 | createMeeting,
7 | isEdit = false,
8 | closeFlyout,
9 | }: {
10 | createMeeting: () => {};
11 | isEdit?: boolean;
12 | closeFlyout?: () => {};
13 | }) {
14 | const navigate = useNavigate();
15 | return (
16 |
17 |
18 | (isEdit ? closeFlyout!() : navigate("/"))}
21 | fill
22 | >
23 | Cancel
24 |
25 |
26 |
27 |
28 | {isEdit ? "Edit Meeting" : "Create Meeting"}
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | export default CreateMeetingButtons;
36 |
--------------------------------------------------------------------------------
/src/components/FormComponents/MeetingDateField.tsx:
--------------------------------------------------------------------------------
1 | import { EuiDatePicker, EuiFormRow } from "@elastic/eui";
2 | import moment from "moment";
3 | import React from "react";
4 |
5 | function MeetingDateField({
6 | selected,
7 | setStartDate,
8 | }: {
9 | selected: moment.Moment;
10 | setStartDate: React.Dispatch>;
11 | }) {
12 | return (
13 |
14 | setStartDate(date!)}
17 | />
18 |
19 | );
20 | }
21 |
22 | export default MeetingDateField;
23 |
--------------------------------------------------------------------------------
/src/components/FormComponents/MeetingMaximumUsersField.tsx:
--------------------------------------------------------------------------------
1 | import { EuiFieldNumber, EuiFormRow } from "@elastic/eui";
2 | import React from "react";
3 |
4 | function MeetingMaximumUsersField({
5 | value,
6 | setSize,
7 | }: {
8 | value: number;
9 | setSize: React.Dispatch>;
10 | }) {
11 | return (
12 |
13 | {
19 | if (!e.target.value.length || parseInt(e.target.value) === 0)
20 | setSize(1);
21 | else if (parseInt(e.target.value) > 50) setSize(50);
22 | else setSize(parseInt(e.target.value));
23 | }}
24 | />
25 |
26 | );
27 | }
28 |
29 | export default MeetingMaximumUsersField;
30 |
--------------------------------------------------------------------------------
/src/components/FormComponents/MeetingNameFIeld.tsx:
--------------------------------------------------------------------------------
1 | import { EuiFieldText, EuiFormRow } from "@elastic/eui";
2 | import React from "react";
3 | import ThemeSelector from "../ThemeSelector";
4 |
5 | function MeetingNameField({
6 | label,
7 | isInvalid,
8 | error,
9 | placeholder,
10 | value,
11 | setMeetingName,
12 | }: {
13 | label: string;
14 | isInvalid: boolean;
15 | error: Array;
16 | placeholder: string;
17 | value: string;
18 | setMeetingName: React.Dispatch>;
19 | }) {
20 | return (
21 |
22 |
23 | setMeetingName(e.target.value)}
27 | isInvalid={isInvalid}
28 | />
29 |
30 |
31 | );
32 | }
33 |
34 | export default MeetingNameField;
35 |
--------------------------------------------------------------------------------
/src/components/FormComponents/MeetingUserField.tsx:
--------------------------------------------------------------------------------
1 | import { EuiComboBox, EuiFormRow } from "@elastic/eui";
2 | import React from "react";
3 |
4 | function MeetingUserField({
5 | label,
6 | isInvalid,
7 | error,
8 | options,
9 | onChange,
10 | selectedOptions,
11 | singleSelection = false,
12 | isClearable,
13 | placeholder,
14 | }: {
15 | label: string;
16 | isInvalid: boolean;
17 | error: Array;
18 | options: any;
19 | onChange: any;
20 | selectedOptions: any;
21 | singleSelection?: { asPlainText: boolean } | boolean;
22 | isClearable: boolean;
23 | placeholder: string;
24 | }) {
25 | return (
26 |
27 |
36 |
37 | );
38 | }
39 |
40 | export default MeetingUserField;
41 |
--------------------------------------------------------------------------------
/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | EuiButtonIcon,
3 | EuiFlexGroup,
4 | EuiFlexItem,
5 | EuiHeader,
6 | EuiText,
7 | EuiTextColor,
8 | } from "@elastic/eui";
9 | import { signOut } from "firebase/auth";
10 | import { useEffect, useState } from "react";
11 | import { useDispatch } from "react-redux";
12 | import { Link, useLocation, useNavigate } from "react-router-dom";
13 | import { useAppSelector } from "../app/hooks";
14 | import { changeTheme } from "../app/slices/AuthSlice";
15 | import {
16 | getCreateMeetingBreadCrumbs,
17 | getDashboardBreadCrumbs,
18 | getMeetingsBreadCrumbs,
19 | getMyMeetingsBreadCrumbs,
20 | getOneOnOneMeetingBreadCrumbs,
21 | getVideoConferenceBreadCrumbs,
22 | } from "../utils/breadcrumbs";
23 | import { firebaseAuth } from "../utils/firebaseConfig";
24 | import { BreadCrumbsType } from "../utils/types";
25 |
26 | export default function Header() {
27 | const navigate = useNavigate();
28 | const location = useLocation();
29 | const userName = useAppSelector((zoomApp) => zoomApp.auth.userInfo?.name);
30 | const isDarkTheme = useAppSelector((zoomApp) => zoomApp.auth.isDarkTheme);
31 | const [breadCrumbs, setBreadCrumbs] = useState>([
32 | {
33 | text: "Dashboard",
34 | },
35 | ]);
36 | const dispatch = useDispatch();
37 | const [isResponsive, setIsResponsive] = useState(false);
38 |
39 | useEffect(() => {
40 | const { pathname } = location;
41 | if (pathname === "/") setBreadCrumbs(getDashboardBreadCrumbs(navigate));
42 | else if (pathname === "/create")
43 | setBreadCrumbs(getCreateMeetingBreadCrumbs(navigate));
44 | else if (pathname === "/create1on1")
45 | setBreadCrumbs(getOneOnOneMeetingBreadCrumbs(navigate));
46 | else if (pathname === "/videoconference")
47 | setBreadCrumbs(getVideoConferenceBreadCrumbs(navigate));
48 | else if (pathname === "/mymeetings")
49 | setBreadCrumbs(getMyMeetingsBreadCrumbs(navigate));
50 | else if (pathname === "/meetings") {
51 | setBreadCrumbs(getMeetingsBreadCrumbs(navigate));
52 | }
53 | }, [location, navigate]);
54 |
55 | const logout = () => {
56 | signOut(firebaseAuth);
57 | };
58 |
59 | const invertTheme = () => {
60 | const theme = localStorage.getItem("zoom-theme");
61 | localStorage.setItem("zoom-theme", theme === "light" ? "dark" : "light");
62 | dispatch(changeTheme({ isDarkTheme: !isDarkTheme }));
63 | };
64 |
65 | const section = [
66 | {
67 | items: [
68 |
69 |
70 |
71 | Zoom
72 |
73 |
74 | ,
75 | ],
76 | },
77 | {
78 | items: [
79 | <>
80 | {userName ? (
81 |
82 |
83 | Hello,
84 | {userName}
85 |
86 |
87 | ) : null}
88 | >,
89 | ],
90 | },
91 | {
92 | items: [
93 |
99 |
100 | {isDarkTheme ? (
101 |
109 | ) : (
110 |
118 | )}
119 |
120 |
121 |
128 |
129 | ,
130 | ],
131 | },
132 | ];
133 |
134 | const responsiveSection = [
135 | {
136 | items: [
137 |
138 |
139 |
140 | Zoom
141 |
142 |
143 | ,
144 | ],
145 | },
146 | {
147 | items: [
148 |
154 |
155 | {isDarkTheme ? (
156 |
164 | ) : (
165 |
173 | )}
174 |
175 |
176 |
183 |
184 | ,
185 | ],
186 | },
187 | ];
188 |
189 | useEffect(() => {
190 | if (window.innerWidth < 480) {
191 | // sectionSpliced.splice(1, 1);
192 | // setSection(sectionSpliced);
193 | setIsResponsive(true);
194 | }
195 | }, []);
196 |
197 | return (
198 | <>
199 |
204 |
212 | >
213 | );
214 | }
215 |
--------------------------------------------------------------------------------
/src/components/ThemeSelector.tsx:
--------------------------------------------------------------------------------
1 | import { EuiThemeColorMode } from "@elastic/eui";
2 | import React, { Suspense, useEffect, useState } from "react";
3 |
4 | const LightTheme = React.lazy(() => import("./Themes/LightTheme"));
5 | const DarkTheme = React.lazy(() => import("./Themes/DarkTheme"));
6 |
7 | export default function ThemeSelector({
8 | children,
9 | }: {
10 | children: React.ReactNode;
11 | }) {
12 | const [theme, setTheme] = useState("light");
13 | useEffect(() => {
14 | const theme = localStorage.getItem("zoom-theme");
15 | if (theme) {
16 | setTheme(theme as EuiThemeColorMode);
17 | }
18 | }, []);
19 |
20 | return (
21 | <>
22 | >}>
23 | {theme === "dark" ? : }
24 |
25 | {children}
26 | >
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Themes/DarkTheme.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "@elastic/eui/dist/eui_theme_dark.css";
3 |
4 | export default function DarkTheme() {
5 | return <>>;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/Themes/LightTheme.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "@elastic/eui/dist/eui_theme_light.css";
3 |
4 | export default function LightTheme() {
5 | return <>>;
6 | }
7 |
--------------------------------------------------------------------------------
/src/hooks/useAuth.tsx:
--------------------------------------------------------------------------------
1 | import { onAuthStateChanged } from "firebase/auth";
2 | import { useEffect } from "react";
3 | import { useDispatch } from "react-redux";
4 | import { useNavigate } from "react-router-dom";
5 | import { setUser } from "../app/slices/AuthSlice";
6 | import { firebaseAuth } from "../utils/firebaseConfig";
7 |
8 | export default function useAuth() {
9 | const navigate = useNavigate();
10 | const dispatch = useDispatch();
11 | useEffect(() => {
12 | const unsubscribe = onAuthStateChanged(firebaseAuth, (currentUser) => {
13 | if (!currentUser) navigate("/login");
14 | else {
15 | dispatch(
16 | setUser({
17 | uid: currentUser.uid,
18 | email: currentUser.email!,
19 | name: currentUser.displayName!,
20 | })
21 | );
22 | }
23 | });
24 | return () => unsubscribe();
25 | }, [dispatch, navigate]);
26 | }
27 |
--------------------------------------------------------------------------------
/src/hooks/useFetchUsers.tsx:
--------------------------------------------------------------------------------
1 | import { getDocs, query, where } from "firebase/firestore";
2 | import { useEffect, useState } from "react";
3 | import { useAppSelector } from "../app/hooks";
4 | import { usersRef } from "../utils/firebaseConfig";
5 | import { UserType } from "../utils/types";
6 |
7 | function useFetchUsers() {
8 | const [users, setUsers] = useState>([]);
9 | const uid = useAppSelector((zoomApp) => zoomApp.auth.userInfo?.uid);
10 |
11 | useEffect(() => {
12 | if (uid) {
13 | const getUser = async () => {
14 | const firestoreQuery = query(usersRef, where("uid", "!=", uid));
15 | const data = await getDocs(firestoreQuery);
16 | const firebaseUsers: Array = [];
17 |
18 | data.forEach((user) => {
19 | const userData: UserType = user.data() as UserType;
20 | firebaseUsers.push({
21 | ...userData,
22 | label: userData.name,
23 | });
24 | });
25 | setUsers(firebaseUsers);
26 | };
27 | getUser();
28 | }
29 | }, [uid]);
30 | return [users];
31 | }
32 |
33 | export default useFetchUsers;
34 |
--------------------------------------------------------------------------------
/src/hooks/useToast.tsx:
--------------------------------------------------------------------------------
1 | import { useAppDispatch, useAppSelector } from "../app/hooks";
2 | import { setToasts } from "../app/slices/MeetingSlice";
3 |
4 | function useToast() {
5 | const toasts = useAppSelector((zoom) => zoom.meetings.toasts);
6 | const dispatch = useAppDispatch();
7 | const createToast = ({
8 | title,
9 | type,
10 | }: {
11 | title: string;
12 | type: "success" | "primary" | "warning" | "danger" | undefined;
13 | }) => {
14 | dispatch(
15 | setToasts(
16 | toasts.concat({
17 | id: new Date().toISOString(),
18 | title,
19 | color: type,
20 | })
21 | )
22 | );
23 | };
24 | return [createToast];
25 | }
26 |
27 | export default useToast;
28 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | // import "@elastic/eui/dist/eui_theme_light.css";
5 | // import "@elastic/eui/dist/eui_theme_dark.css";
6 | import { Provider } from "react-redux";
7 | import { store } from "./app/store";
8 | import { BrowserRouter } from "react-router-dom";
9 |
10 | ReactDOM.render(
11 |
12 |
13 |
14 |
15 |
16 |
17 | ,
18 | document.getElementById("root")
19 | );
20 |
--------------------------------------------------------------------------------
/src/pages/CreateMeeting.tsx:
--------------------------------------------------------------------------------
1 | import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiImage } from "@elastic/eui";
2 |
3 | import React from "react";
4 | import { useNavigate } from "react-router-dom";
5 | import meeting1 from "../assets/meeting1.png";
6 | import meeting2 from "../assets/meeting2.png";
7 |
8 | import Header from "../components/Header";
9 | import useAuth from "../hooks/useAuth";
10 |
11 | export default function CreateMeeting() {
12 | useAuth();
13 | const navigate = useNavigate();
14 |
15 | return (
16 | <>
17 |
24 |
25 |
30 |
31 | }
33 | title={`Create 1 on 1 Meeting`}
34 | description="Create a personal single person meeting."
35 | onClick={() => navigate("/create1on1")}
36 | paddingSize="xl"
37 | />
38 |
39 |
40 | }
42 | title={`Create Video Conference`}
43 | description="Invite multiple persons to the meeting."
44 | onClick={() => navigate("/videoconference")}
45 | paddingSize="xl"
46 | />
47 |
48 |
49 |
50 | >
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/pages/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiImage } from "@elastic/eui";
3 | import { useNavigate } from "react-router-dom";
4 | import dashboard1 from "../assets/dashboard1.png";
5 | import dashboard2 from "../assets/dashboard2.png";
6 | import dashboard3 from "../assets/dashboard3.png";
7 | import Header from "../components/Header";
8 | import useAuth from "../hooks/useAuth";
9 |
10 | function Dashboard() {
11 | useAuth();
12 | const navigate = useNavigate();
13 |
14 | return (
15 | <>
16 |
23 |
24 |
29 |
30 | }
32 | title={`Create Meeting`}
33 | description="Create a new meeting and invite people."
34 | onClick={() => navigate("/create")}
35 | paddingSize="xl"
36 | />
37 |
38 |
39 | }
41 | title={`My Meetings`}
42 | description="View your created meetings."
43 | onClick={() => navigate("/mymeetings")}
44 | paddingSize="xl"
45 | />
46 |
47 |
48 | }
50 | title={`Meetings`}
51 | description="View the meetings that you are invited to."
52 | onClick={() => navigate("/meetings")}
53 | paddingSize="xl"
54 | />
55 |
56 |
57 |
58 | >
59 | );
60 | }
61 |
62 | export default Dashboard;
63 |
--------------------------------------------------------------------------------
/src/pages/JoinMeeting.tsx:
--------------------------------------------------------------------------------
1 | import { ZegoUIKitPrebuilt } from "@zegocloud/zego-uikit-prebuilt";
2 | import { onAuthStateChanged } from "firebase/auth";
3 | import { getDocs, query, where } from "firebase/firestore";
4 | import moment from "moment";
5 | import React, { useEffect, useState } from "react";
6 | import { useNavigate, useParams } from "react-router-dom";
7 | import useToast from "../hooks/useToast";
8 | import { firebaseAuth, meetingsRef } from "../utils/firebaseConfig";
9 | import { generateMeetingID } from "../utils/generateMeetingId";
10 |
11 | export default function JoinMeeting() {
12 | const params = useParams();
13 | const navigate = useNavigate();
14 | const [createToast] = useToast();
15 | const [isAllowed, setIsAllowed] = useState(false);
16 | const [user, setUser] = useState(undefined);
17 | const [userLoaded, setUserLoaded] = useState(false);
18 |
19 | onAuthStateChanged(firebaseAuth, (currentUser) => {
20 | if (currentUser) {
21 | setUser(currentUser);
22 | }
23 | setUserLoaded(true);
24 | });
25 | useEffect(() => {
26 | const getMeetingData = async () => {
27 | if (params.id && userLoaded) {
28 | const firestoreQuery = query(
29 | meetingsRef,
30 | where("meetingId", "==", params.id)
31 | );
32 | const fetchedMeetings = await getDocs(firestoreQuery);
33 |
34 | if (fetchedMeetings.docs.length) {
35 | const meeting = fetchedMeetings.docs[0].data();
36 | const isCreator = meeting.createdBy === user?.uid;
37 | if (meeting.meetingType === "1-on-1") {
38 | if (meeting.invitedUsers[0] === user?.uid || isCreator) {
39 | if (meeting.meetingDate === moment().format("L")) {
40 | setIsAllowed(true);
41 | } else if (
42 | moment(meeting.meetingDate).isBefore(moment().format("L"))
43 | ) {
44 | createToast({ title: "Meeting has ended.", type: "danger" });
45 | navigate(user ? "/" : "/login");
46 | } else if (moment(meeting.meetingDate).isAfter()) {
47 | createToast({
48 | title: `Meeting is on ${meeting.meetingDate}`,
49 | type: "warning",
50 | });
51 | navigate(user ? "/" : "/login");
52 | }
53 | } else navigate(user ? "/" : "/login");
54 | } else if (meeting.meetingType === "video-conference") {
55 | const index = meeting.invitedUsers.findIndex(
56 | (invitedUser: string) => invitedUser === user?.uid
57 | );
58 | if (index !== -1 || isCreator) {
59 | if (meeting.meetingDate === moment().format("L")) {
60 | setIsAllowed(true);
61 | } else if (
62 | moment(meeting.meetingDate).isBefore(moment().format("L"))
63 | ) {
64 | createToast({ title: "Meeting has ended.", type: "danger" });
65 | navigate(user ? "/" : "/login");
66 | } else if (moment(meeting.meetingDate).isAfter()) {
67 | createToast({
68 | title: `Meeting is on ${meeting.meetingDate}`,
69 | type: "warning",
70 | });
71 | }
72 | } else {
73 | createToast({
74 | title: `You are not invited to the meeting.`,
75 | type: "danger",
76 | });
77 | navigate(user ? "/" : "/login");
78 | }
79 | } else {
80 | setIsAllowed(true);
81 | }
82 | }
83 | }
84 | };
85 | getMeetingData();
86 | }, [params.id, user, userLoaded, createToast, navigate]);
87 | const myMeeting = async (element: any) => {
88 | const kitToken = ZegoUIKitPrebuilt.generateKitTokenForTest(
89 | parseInt(process.env.REACT_APP_ZEGOCLOUD_APP_ID!),
90 | process.env.REACT_APP_ZEGOCLOUD_SERVER_SECRET as string,
91 | params.id as string,
92 | user?.uid ? user.uid : generateMeetingID(),
93 | user?.displayName ? user.displayName : generateMeetingID()
94 | );
95 | const zp = ZegoUIKitPrebuilt.create(kitToken);
96 |
97 | zp?.joinRoom({
98 | container: element,
99 | maxUsers: 50,
100 | sharedLinks: [
101 | {
102 | name: "Personal link",
103 | url: window.location.origin,
104 | },
105 | ],
106 | scenario: {
107 | mode: ZegoUIKitPrebuilt.VideoConference,
108 | },
109 | });
110 | };
111 |
112 | return isAllowed ? (
113 |
126 | ) : (
127 | <>>
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/src/pages/Login.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | EuiButton,
3 | EuiFlexGroup,
4 | EuiFlexItem,
5 | EuiImage,
6 | EuiPanel,
7 | EuiProvider,
8 | EuiSpacer,
9 | EuiText,
10 | EuiTextColor,
11 | } from "@elastic/eui";
12 | import logo from "../assets/logo.png";
13 | import animation from "../assets/animation.gif";
14 |
15 | import React from "react";
16 | import {
17 | GoogleAuthProvider,
18 | onAuthStateChanged,
19 | signInWithPopup,
20 | } from "firebase/auth";
21 | import { firebaseAuth, firebaseDB, usersRef } from "../utils/firebaseConfig";
22 | import { useNavigate } from "react-router-dom";
23 | import { useAppDispatch } from "../app/hooks";
24 | import { setUser } from "../app/slices/AuthSlice";
25 | import { collection, query, where, addDoc, getDocs } from "firebase/firestore";
26 |
27 | function Login() {
28 | const navigate = useNavigate();
29 | const dispatch = useAppDispatch();
30 |
31 | onAuthStateChanged(firebaseAuth, (currentUser) => {
32 | if (currentUser) navigate("/");
33 | });
34 |
35 | const login = async () => {
36 | const provider = new GoogleAuthProvider();
37 | const {
38 | user: { displayName, email, uid },
39 | } = await signInWithPopup(firebaseAuth, provider);
40 |
41 | if (email) {
42 | const firestoreQuery = query(usersRef, where("uid", "==", uid));
43 | const fetchedUser = await getDocs(firestoreQuery);
44 | if (fetchedUser.docs.length === 0) {
45 | await addDoc(collection(firebaseDB, "users"), {
46 | uid,
47 | name: displayName,
48 | email,
49 | });
50 | }
51 | dispatch(setUser({ uid, email: email!, name: displayName! }));
52 | navigate("/");
53 | }
54 | };
55 | return (
56 |
57 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | One Platform to
74 | connect
75 |
76 |
77 |
78 |
79 | Login with Google
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | );
88 | }
89 |
90 | export default Login;
91 |
--------------------------------------------------------------------------------
/src/pages/Meeting.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | EuiBadge,
3 | EuiBasicTable,
4 | EuiButtonIcon,
5 | EuiCopy,
6 | EuiFlexGroup,
7 | EuiFlexItem,
8 | EuiPanel,
9 | } from "@elastic/eui";
10 |
11 | import { getDocs, query } from "firebase/firestore";
12 | import moment from "moment";
13 | import React, { useEffect, useState } from "react";
14 | import { Link } from "react-router-dom";
15 | import { useAppSelector } from "../app/hooks";
16 | import Header from "../components/Header";
17 | import useAuth from "../hooks/useAuth";
18 |
19 | import { meetingsRef } from "../utils/firebaseConfig";
20 | import { MeetingType } from "../utils/types";
21 |
22 | export default function Meeting() {
23 | useAuth();
24 | const userInfo = useAppSelector((zoom) => zoom.auth.userInfo);
25 | const [meetings, setMeetings] = useState>([]);
26 |
27 | useEffect(() => {
28 | const getMyMeetings = async () => {
29 | const firestoreQuery = query(meetingsRef);
30 | const fetchedMeetings = await getDocs(firestoreQuery);
31 | if (fetchedMeetings.docs.length) {
32 | const myMeetings: Array = [];
33 | fetchedMeetings.forEach((meeting) => {
34 | const data = meeting.data() as MeetingType;
35 | if (data.createdBy === userInfo?.uid)
36 | myMeetings.push(meeting.data() as MeetingType);
37 | else if (data.meetingType === "anyone-can-join")
38 | myMeetings.push(meeting.data() as MeetingType);
39 | else {
40 | const index = data.invitedUsers.findIndex(
41 | (user: string) => user === userInfo?.uid
42 | );
43 | if (index !== -1) {
44 | myMeetings.push(meeting.data() as MeetingType);
45 | }
46 | }
47 | });
48 |
49 | setMeetings(myMeetings);
50 | }
51 | };
52 | if (userInfo) getMyMeetings();
53 | }, [userInfo]);
54 |
55 | const meetingColumns = [
56 | {
57 | field: "meetingName",
58 | name: "Meeting Name",
59 | },
60 | {
61 | field: "meetingType",
62 | name: "Meeting Type",
63 | },
64 | {
65 | field: "meetingDate",
66 | name: "Meeting Date",
67 | },
68 | {
69 | field: "",
70 | name: "Status",
71 |
72 | render: (meeting: MeetingType) => {
73 | if (meeting.status) {
74 | if (meeting.meetingDate === moment().format("L")) {
75 | return (
76 |
77 |
81 | Join Now
82 |
83 |
84 | );
85 | } else if (
86 | moment(meeting.meetingDate).isBefore(moment().format("L"))
87 | ) {
88 | return Ended;
89 | } else if (moment(meeting.meetingDate).isAfter()) {
90 | return Upcoming;
91 | }
92 | } else return Cancelled;
93 | },
94 | },
95 | {
96 | field: "meetingId",
97 | name: "Copy Link",
98 | width: "10%",
99 | render: (meetingId: string) => {
100 | return (
101 |
104 | {(copy: any) => (
105 |
111 | )}
112 |
113 | );
114 | },
115 | },
116 | ];
117 |
118 | return (
119 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | );
136 | }
137 |
--------------------------------------------------------------------------------
/src/pages/MyMeetings.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | EuiBadge,
3 | EuiBasicTable,
4 | EuiButtonIcon,
5 | EuiCopy,
6 | EuiFlexGroup,
7 | EuiFlexItem,
8 | EuiPanel,
9 | } from "@elastic/eui";
10 | import { getDocs, query, where } from "firebase/firestore";
11 | import moment from "moment";
12 | import React, { useEffect, useState, useCallback } from "react";
13 | import { Link } from "react-router-dom";
14 | import { useAppSelector } from "../app/hooks";
15 | import EditFlyout from "../components/EditFlyout";
16 | import Header from "../components/Header";
17 | import useAuth from "../hooks/useAuth";
18 | import { meetingsRef } from "../utils/firebaseConfig";
19 | import { MeetingType } from "../utils/types";
20 |
21 | export default function MyMeetings() {
22 | useAuth();
23 | const userInfo = useAppSelector((zoom) => zoom.auth.userInfo);
24 | const [meetings, setMeetings] = useState>([]);
25 | const [showEditFlyout, setShowEditFlyout] = useState(false);
26 | const [editMeeting, setEditMeeting] = useState();
27 | const getMyMeetings = useCallback(async () => {
28 | const firestoreQuery = query(
29 | meetingsRef,
30 | where("createdBy", "==", userInfo?.uid)
31 | );
32 | const fetchedMeetings = await getDocs(firestoreQuery);
33 | if (fetchedMeetings.docs.length) {
34 | const myMeetings: Array = [];
35 | fetchedMeetings.forEach((meeting) => {
36 | myMeetings.push({
37 | docId: meeting.id,
38 | ...(meeting.data() as MeetingType),
39 | });
40 | });
41 | setMeetings(myMeetings);
42 | }
43 | }, [userInfo?.uid]);
44 | useEffect(() => {
45 | if (userInfo) getMyMeetings();
46 | }, [userInfo, getMyMeetings]);
47 |
48 | const openEditFlyout = (meeting: MeetingType) => {
49 | setShowEditFlyout(true);
50 | setEditMeeting(meeting);
51 | };
52 |
53 | const closeEditFlyout = (dataChanged = false) => {
54 | setShowEditFlyout(false);
55 | setEditMeeting(undefined);
56 | if (dataChanged) getMyMeetings();
57 | };
58 |
59 | const meetingColumns = [
60 | {
61 | field: "meetingName",
62 | name: "Meeting Name",
63 | },
64 | {
65 | field: "meetingType",
66 | name: "Meeting Type",
67 | },
68 | {
69 | field: "meetingDate",
70 | name: "Meeting Date",
71 | },
72 | {
73 | field: "",
74 | name: "Status",
75 | render: (meeting: MeetingType) => {
76 | if (meeting.status) {
77 | if (meeting.meetingDate === moment().format("L")) {
78 | return (
79 |
80 |
84 | Join Now
85 |
86 |
87 | );
88 | } else if (
89 | moment(meeting.meetingDate).isBefore(moment().format("L"))
90 | ) {
91 | return Ended;
92 | } else if (moment(meeting.meetingDate).isAfter()) {
93 | return Upcoming;
94 | }
95 | } else return Cancelled;
96 | },
97 | },
98 | {
99 | field: "",
100 | name: "Edit",
101 | width: "5%",
102 | render: (meeting: MeetingType) => {
103 | return (
104 | openEditFlyout(meeting)}
114 | />
115 | );
116 | },
117 | },
118 | {
119 | field: "meetingId",
120 | name: "Copy Link",
121 | width: "5%",
122 | render: (meetingId: string) => {
123 | return (
124 |
127 | {(copy: any) => (
128 |
134 | )}
135 |
136 | );
137 | },
138 | },
139 | ];
140 |
141 | return (
142 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 | {showEditFlyout && (
158 |
159 | )}
160 |
161 | );
162 | }
163 |
--------------------------------------------------------------------------------
/src/pages/OneOnOneMeeting.tsx:
--------------------------------------------------------------------------------
1 | import { EuiFlexGroup, EuiForm, EuiSpacer } from "@elastic/eui";
2 | import { addDoc } from "firebase/firestore";
3 | import moment from "moment";
4 | import React, { useState } from "react";
5 | import { useNavigate } from "react-router-dom";
6 | import { useAppSelector } from "../app/hooks";
7 | import CreateMeetingButtons from "../components/FormComponents/CreateMeetingButtons";
8 | import MeetingDateField from "../components/FormComponents/MeetingDateField";
9 | import MeetingNameField from "../components/FormComponents/MeetingNameFIeld";
10 | import MeetingUserField from "../components/FormComponents/MeetingUserField";
11 |
12 | import Header from "../components/Header";
13 | import useAuth from "../hooks/useAuth";
14 | import useFetchUsers from "../hooks/useFetchUsers";
15 | import useToast from "../hooks/useToast";
16 | import { meetingsRef } from "../utils/firebaseConfig";
17 | import { generateMeetingID } from "../utils/generateMeetingId";
18 | import { FieldErrorType, UserType } from "../utils/types";
19 |
20 | export default function OneOnOneMeeting() {
21 | useAuth();
22 | const [users] = useFetchUsers();
23 | const [createToast] = useToast();
24 | const uid = useAppSelector((zoomApp) => zoomApp.auth.userInfo?.uid);
25 | const navigate = useNavigate();
26 |
27 | const [meetingName, setMeetingName] = useState("");
28 | const [selectedUser, setSelectedUser] = useState>([]);
29 | const [startDate, setStartDate] = useState(moment());
30 | const [showErrors, setShowErrors] = useState<{
31 | meetingName: FieldErrorType;
32 | meetingUser: FieldErrorType;
33 | }>({
34 | meetingName: {
35 | show: false,
36 | message: [],
37 | },
38 | meetingUser: {
39 | show: false,
40 | message: [],
41 | },
42 | });
43 |
44 | const onUserChange = (selectedOptions: Array) => {
45 | setSelectedUser(selectedOptions);
46 | };
47 |
48 | const validateForm = () => {
49 | const showErrorsClone = { ...showErrors };
50 | let errors = false;
51 | if (!meetingName.length) {
52 | showErrorsClone.meetingName.show = true;
53 | showErrorsClone.meetingName.message = ["Please Enter Meeting Name"];
54 | errors = true;
55 | } else {
56 | showErrorsClone.meetingName.show = false;
57 | showErrorsClone.meetingName.message = [];
58 | }
59 | if (!selectedUser.length) {
60 | showErrorsClone.meetingUser.show = true;
61 | showErrorsClone.meetingUser.message = ["Please Select a User"];
62 | errors = true;
63 | } else {
64 | showErrorsClone.meetingUser.show = false;
65 | showErrorsClone.meetingUser.message = [];
66 | }
67 | setShowErrors(showErrorsClone);
68 | return errors;
69 | };
70 |
71 | const createMeeting = async () => {
72 | if (!validateForm()) {
73 | const meetingId = generateMeetingID();
74 | await addDoc(meetingsRef, {
75 | createdBy: uid,
76 | meetingId,
77 | meetingName,
78 | meetingType: "1-on-1",
79 | invitedUsers: [selectedUser[0].uid],
80 | meetingDate: startDate.format("L"),
81 | maxUsers: 1,
82 | status: true,
83 | });
84 | createToast({
85 | title: "One on One Meeting Created Successfully",
86 | type: "success",
87 | });
88 | navigate("/");
89 | }
90 | };
91 |
92 | return (
93 |
100 |
101 |
102 |
103 |
111 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/src/pages/VideoConference.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | EuiFlexGroup,
3 | EuiForm,
4 | EuiFormRow,
5 | EuiSpacer,
6 | EuiSwitch,
7 | } from "@elastic/eui";
8 | import { addDoc } from "firebase/firestore";
9 | import moment from "moment";
10 | import React, { useState } from "react";
11 | import { useNavigate } from "react-router-dom";
12 | import { useAppSelector } from "../app/hooks";
13 | import CreateMeetingButtons from "../components/FormComponents/CreateMeetingButtons";
14 | import MeetingDateField from "../components/FormComponents/MeetingDateField";
15 | import MeetingMaximumUsersField from "../components/FormComponents/MeetingMaximumUsersField";
16 | import MeetingNameField from "../components/FormComponents/MeetingNameFIeld";
17 | import MeetingUserField from "../components/FormComponents/MeetingUserField";
18 |
19 | import Header from "../components/Header";
20 | import useAuth from "../hooks/useAuth";
21 | import useFetchUsers from "../hooks/useFetchUsers";
22 | import useToast from "../hooks/useToast";
23 | import { meetingsRef } from "../utils/firebaseConfig";
24 | import { generateMeetingID } from "../utils/generateMeetingId";
25 | import { FieldErrorType, UserType } from "../utils/types";
26 |
27 | export default function VideoConference() {
28 | useAuth();
29 | const [users] = useFetchUsers();
30 | const [createToast] = useToast();
31 | const uid = useAppSelector((zoomApp) => zoomApp.auth.userInfo?.uid);
32 | const navigate = useNavigate();
33 |
34 | const [meetingName, setMeetingName] = useState("");
35 | const [selectedUser, setSelectedUser] = useState>([]);
36 | const [startDate, setStartDate] = useState(moment());
37 | const [size, setSize] = useState(1);
38 | const [showErrors, setShowErrors] = useState<{
39 | meetingName: FieldErrorType;
40 | meetingUsers: FieldErrorType;
41 | }>({
42 | meetingName: {
43 | show: false,
44 | message: [],
45 | },
46 | meetingUsers: {
47 | show: false,
48 | message: [],
49 | },
50 | });
51 | const [anyoneCanJoin, setAnyoneCanJoin] = useState(false);
52 |
53 | const onUserChange = (selectedOptions: Array) => {
54 | setSelectedUser(selectedOptions);
55 | };
56 |
57 | const validateForm = () => {
58 | const showErrorsClone = { ...showErrors };
59 | let errors = false;
60 | if (!meetingName.length) {
61 | showErrorsClone.meetingName.show = true;
62 | showErrorsClone.meetingName.message = ["Please Enter Meeting Name"];
63 | errors = true;
64 | } else {
65 | showErrorsClone.meetingName.show = false;
66 | showErrorsClone.meetingName.message = [];
67 | }
68 | if (!selectedUser.length && !anyoneCanJoin) {
69 | showErrorsClone.meetingUsers.show = true;
70 | showErrorsClone.meetingUsers.message = ["Please Select a User"];
71 | errors = true;
72 | } else {
73 | showErrorsClone.meetingUsers.show = false;
74 | showErrorsClone.meetingUsers.message = [];
75 | }
76 | setShowErrors(showErrorsClone);
77 | return errors;
78 | };
79 |
80 | const createMeeting = async () => {
81 | if (!validateForm()) {
82 | const meetingId = generateMeetingID();
83 | await addDoc(meetingsRef, {
84 | createdBy: uid,
85 | meetingId,
86 | meetingName,
87 | meetingType: anyoneCanJoin ? "anyone-can-join" : "video-conference",
88 | invitedUsers: anyoneCanJoin
89 | ? []
90 | : selectedUser.map((user: UserType) => user.uid),
91 | meetingDate: startDate.format("L"),
92 | maxUsers: anyoneCanJoin ? 100 : size,
93 | status: true,
94 | });
95 | createToast({
96 | title: anyoneCanJoin
97 | ? "Anyone can join meeting created successfully"
98 | : "Video Conference created successfully.",
99 | type: "success",
100 | });
101 | navigate("/");
102 | }
103 | };
104 |
105 | return (
106 |
113 |
114 |
115 |
116 |
117 | setAnyoneCanJoin(e.target.checked)}
122 | compressed
123 | />
124 |
125 |
126 |
134 |
135 | {anyoneCanJoin ? (
136 |
137 | ) : (
138 |
148 | )}
149 |
150 |
151 |
152 |
153 |
154 |
155 | );
156 | }
157 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/utils/breadcrumbs.ts:
--------------------------------------------------------------------------------
1 | import { NavigateFunction } from "react-router-dom";
2 | import { BreadCrumbsType } from "./types";
3 |
4 | export const getDashboardBreadCrumbs = (
5 | navigate: NavigateFunction
6 | ): Array => [
7 | {
8 | text: "Dashboard",
9 | },
10 | ];
11 |
12 | export const getCreateMeetingBreadCrumbs = (
13 | navigate: NavigateFunction
14 | ): Array => [
15 | {
16 | text: "Dashboard",
17 | href: "#",
18 | onClick: () => {
19 | navigate("/");
20 | },
21 | },
22 | {
23 | text: "Create Meeting",
24 | },
25 | ];
26 |
27 | export const getOneOnOneMeetingBreadCrumbs = (
28 | navigate: NavigateFunction
29 | ): Array => [
30 | {
31 | text: "Dashboard",
32 | href: "#",
33 | onClick: () => {
34 | navigate("/");
35 | },
36 | },
37 | {
38 | text: "Create Meeting",
39 | href: "#",
40 | onClick: () => {
41 | navigate("/create");
42 | },
43 | },
44 | {
45 | text: "Create 1 on 1 Meeting",
46 | },
47 | ];
48 |
49 | export const getVideoConferenceBreadCrumbs = (
50 | navigate: NavigateFunction
51 | ): Array => [
52 | {
53 | text: "Dashboard",
54 | href: "#",
55 | onClick: () => {
56 | navigate("/");
57 | },
58 | },
59 | {
60 | text: "Create Meeting",
61 | href: "#",
62 | onClick: () => {
63 | navigate("/create");
64 | },
65 | },
66 | {
67 | text: "Create Video Conference",
68 | },
69 | ];
70 |
71 | export const getMyMeetingsBreadCrumbs = (
72 | navigate: NavigateFunction
73 | ): Array => [
74 | {
75 | text: "Dashboard",
76 | href: "#",
77 | onClick: () => {
78 | navigate("/");
79 | },
80 | },
81 | {
82 | text: "My Meetings",
83 | },
84 | ];
85 |
86 | export const getMeetingsBreadCrumbs = (
87 | navigate: NavigateFunction
88 | ): Array => [
89 | {
90 | text: "Dashboard",
91 | href: "#",
92 | onClick: () => {
93 | navigate("/");
94 | },
95 | },
96 | {
97 | text: "Meetings",
98 | },
99 | ];
100 |
--------------------------------------------------------------------------------
/src/utils/firebaseConfig.ts:
--------------------------------------------------------------------------------
1 | import { getAuth } from "firebase/auth";
2 | import { initializeApp } from "firebase/app";
3 | import { collection, getFirestore } from "firebase/firestore";
4 | const firebaseConfig = {
5 | apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
6 | authDomain: "react-auth-6788d.firebaseapp.com",
7 | projectId: "react-auth-6788d",
8 | storageBucket: "react-auth-6788d.appspot.com",
9 | messagingSenderId: "131797845021",
10 | appId: "1:131797845021:web:3f7ff4766e2b89ca5d32f4",
11 | measurementId: "G-VWPBR1NSLL",
12 | };
13 |
14 | const app = initializeApp(firebaseConfig);
15 | export const firebaseAuth = getAuth(app);
16 | export const firebaseDB = getFirestore(app);
17 |
18 | export const usersRef = collection(firebaseDB, "users");
19 | export const meetingsRef = collection(firebaseDB, "meetings");
20 |
--------------------------------------------------------------------------------
/src/utils/generateMeetingId.ts:
--------------------------------------------------------------------------------
1 | export const generateMeetingID = () => {
2 | let meetingID = "";
3 | const chars =
4 | "12345qwertyuiopasdfgh67890jklmnbvcxzMNBVCZXASDQWERTYHGFUIOLKJP";
5 | const maxPos = chars.length;
6 |
7 | for (let i = 0; i < 8; i++) {
8 | meetingID += chars.charAt(Math.floor(Math.random() * maxPos));
9 | }
10 | return meetingID;
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/types.ts:
--------------------------------------------------------------------------------
1 | export interface ToastType {
2 | id: string;
3 | title: string;
4 | color: "success" | "primary" | "warning" | "danger" | undefined;
5 | }
6 |
7 | export interface BreadCrumbsType {
8 | text: string;
9 | href?: string;
10 | onClick?: () => void;
11 | }
12 |
13 | export type MeetingJoinType = "anyone-can-join" | "video-conference" | "1-on-1";
14 |
15 | export interface MeetingType {
16 | docId?: string;
17 | createdBy: string;
18 | invitedUsers: Array;
19 | maxUsers: number;
20 | meetingDate: string;
21 | meetingId: string;
22 | meetingName: string;
23 | meetingType: MeetingJoinType;
24 | status: boolean;
25 | }
26 |
27 | export interface UserType {
28 | email: string;
29 | name: string;
30 | uid: string;
31 | label?: string;
32 | }
33 |
34 | export interface FieldErrorType {
35 | show: boolean;
36 | message: Array;
37 | }
38 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------