diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..c8972cd16 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,38 @@ +# Description + +Please describe the issue of the pull request and the changes + + + +## Type of Change + +Please check the options that apply + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +# How Has the Changes Been Tested? + + + +# Checklist: + +- [ ] My code follows the style guidelines of this project +- [ ] Changes included in this pull request covers minimal topic +- [ ] I have performed a self-review of my code +- [ ] I have commented my code properly, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules diff --git a/CHANGE_LOG.md b/CHANGE_LOG.md index 14d239c84..ba6f1d328 100644 --- a/CHANGE_LOG.md +++ b/CHANGE_LOG.md @@ -1,17 +1,61 @@

- +

ReacType Change Log

-**Version 19.0.0 Changes** - -Changes:
- -- Developer Improvement: +## Version 20.0.0 Changes + +### Changes: + +- **Developer Improvement:** + - Migrated from Webpack to Vite, improving HMR times drastically + - Deployed app using Heroku instead of AWS decreasing time to deployment +- **User Features:** + - **Collaboration Room:** + - Implemented live video, audio, and text functionality using socket.IO + - Added authentication and error handling to joining existing rooms + - **UI updates to enhance user experience:** + - In addition to drag to add, users are now able to click to add + - Updated left panel to include user information and settings + - Added scroll and zoom buttons to canvas. Scroll now automatically scrolls to bottom once enough elements are added + - Updated UI design to reflect a more modern look +- **Bugs Fixed:** + - Canvas - All appropriate elements can now be nested - Nested Elements in the code preview now accurately reflect nested elements. They can also be dragged. + - Bottom Panel - Now opens by click instead of hover + - Users can now delete elements without first clicking it and then the X. This applies to the nested components as well. +- **Landing Page:** + - Revamped entire landing page for a more modern look + +### Recommendations for Future Enhancements: + +- Fix bottom panel to only close upon clicking the icon, and not anywhere else +- Populate settings tab in the left panel with additional functionality +- Allow users to modify code dynamically in the code preview and reflect visual componenets in real time +- Add zoom in and zoom out / scroll functionality to code preview and component tree +- Convert from 95% to 100% typescript +- Add more functionality to the nav bar +- List all active rooms to join +- Clean up unnecessary code / comments and deprecated libraries +- a tags which are nested do not display accurate code in code preview +- Eliminate all Webpack associated files/folders/dependencies/etc... now that we run on Vite +- Remove the many deprecated dependencies +- Add additional features to the live chat (Links, reactions, raise hand feature etc) +- Allow live chat to be a popup and draggable outside of the app +- Implement MUI/ShadcnUI in addition to standard html elements on left panel so that users are able to start off with pre styled elements +- Make the app mobile responsive. Right now it does not work/look good on mobile +- We had to deploy via Heroku due to time limitations and Vite. We would recommend going back to AWS with dockerized containers. +- Light/Dark mode in the left settings tab +- Update links in the footer of the landing page + +## Version 19.0.0 Changes + +### Changes: + +- **Developer Improvement:** - Typescript conversion continued and now sits at ~95% -- User Features: - - Collaboration Room: - - Bug Fixes: +- **User Features:** + - **Collaboration Room:** + - **Bug Fixes:** - Debug “Leave Room” functionality removing username from the users list - Debug “Join Room” functionality so the current canvas does not reset upon new user joining collaboration - Debug Code Preview button that sent error if toggled more than once and does not force toggled view to other users in the room @@ -23,15 +67,15 @@ Changes:
- Significantly reduces the amount of data being passed among users by passing only the payload for each individual action, triggering singular updates for other users in the collaboration environment - Added Event Emitters for each action that updates canvas - Created a websocket service layer to maintain a single socket instance throughout the app - - User List: + - **User List:** - Displays the username and mouse cursor of all connected users in a particular room with a specific color scheme - - UI updated to enhance user experience + - UI updated to enhance user experience: - Rendered MUI Icons in HTML Element Panel - Redesigned drag-n-drop to be more intuitive and professionalize application design. - Updated styling to overall style and theme to maintain consistency across the application - Removed Tailwind and CSS save buttons in Customization panel for cleaner UI and drying up repetitive functionality -Recommendations for Future Enhancements:
+### Recommendations for Future Enhancements: - Fix Undo & Redo functionality. Undo & Redo buttons on the customization page not functioning as expected. - Update Electron for desktop application use. Resolve electron app functionality to coincide with web app functionality. @@ -49,7 +93,7 @@ Recommendations for Future Enhancements:
- True real-time rendering so users can see components as they're being dragged onto the canvas, rather than only when they're placed. - List of active rooms so users can simply pick one to join. Will likely be paired with a password feature for security, so only users with the proper credentials can join a particular room. - Chat Feature in Collaboration Room - - Currently, the live tracking cursor is rendered based on the users username/nickname. If multiple users create the same username/nickname, the most recent username/nickname creator will override the former. Possible solution to this issue could be to store cursor with the socket id rather than username/nickname. " + - Currently, the live tracking cursor is rendered based on the users username/nickname. If multiple users create the same username/nickname, the most recent username/nickname creator will override the former. Possible solution to this issue could be to store cursor with the socket id rather than username/nickname. " **Version 18.0.0 Changes** diff --git a/Dockerfile b/Dockerfile index 7454cafeb..5eebc20b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Build -FROM node:19-alpine as build +FROM node:21.2.0-alpine as build # python: required dependency for node alpine, shrinks image size from 2.17GB to 1.67GB RUN apk add --no-cache --virtual .gyp \ @@ -16,7 +16,7 @@ RUN npm install --no-install-recommends --fetch-retry-maxtimeout 500000 COPY . . # Stage 2: Runtime -FROM node:19-alpine as runtime +FROM node:21.2.0-alpine as runtime WORKDIR /app @@ -27,10 +27,10 @@ RUN npm install --no-install-recommends --only=production --fetch-retry-maxtimeo # COPY --from=build /app/.env .env COPY --from=build /app/config.js ./config.js COPY --from=build /app/server ./server -COPY --from=build /app/app/dist /app +COPY --from=build /app/build /app EXPOSE 5656 ENV IS_DOCKER true -CMD [ "npm", "start" ] +CMD [ "npm", "start" ] \ No newline at end of file diff --git a/Procfile b/Procfile index 038261f7d..e9e2aa262 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: node server/server.js \ No newline at end of file +web: npx tsx server/server.ts \ No newline at end of file diff --git a/README.md b/README.md index 0a1c28f86..4c507c99e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ - -## File Structure of ReacType Version 19.0.0 +## File Structure of ReacType Version 20.0.0 Here is the main file structure:

-Given to us courtesy of our friends over at React Relay - -Please refer to the [Excalidraw](https://excalidraw.com/#json=JKwzVD5qx6lsfiHW1_pQ9,XJ6uDoehVu-1bsx0SMlC6w) provided by ReacType Version 14.0 for more details. +Given to us courtesy of our friends over at React Relay ## Run ReacType using CLI @@ -121,7 +119,7 @@ npm run test npm run dev ``` -- Note that a .env with DEV_PORT, and a NODE_ENV flag (=production or development) are needed. +- Note that DEV_PORT, NODE_ENV flag (=production or development) and VIDEOSDK token are needed in the .env file. - Please note that the development build is not connected to the production server. `npm run dev` should spin up the development server from the server folder of this repo. For additional information, the readme is [here](https://github.com/open-source-labs/ReacType/blob/master/server/README.md). Alternatively, you can select "Continue as guest" on the login page of the app, which will not use any features that rely on the server (authentication and saving project data.) - To run the development build of electron app @@ -146,10 +144,8 @@ npm install npm run build ``` - - Start an instance - ```bash npm run start ``` @@ -158,7 +154,7 @@ npm run start ## Stack -Typescript, React.js, Redux Toolkit, Javascript, ESM, Node.js (Express), HTML, CSS, MUI, GraphQL, Next.js, Gatsby.js, Electron, NoSQL, Webpack, TDD (Jest, React Testing Library, Playwright), OAuth 2.0, Websocket, SocketIO, Continuous Integration (Github Actions), Docker, AWS (ECR, Elastic Beanstalk), Ace Editor, Google Charts, React DnD +Typescript, React.js, Redux Toolkit, Javascript, ESM, Node.js (Express), HTML, CSS, MUI, GraphQL, Next.js, Gatsby.js, Electron, NoSQL, Webpack, TDD (Jest, React Testing Library, Playwright), OAuth 2.0, Websocket, SocketIO, Continuous Integration (Github Actions), Docker, AWS (ECR, Elastic Beanstalk), Ace Editor, Google Charts, React DnD, Vite ## Contributions diff --git a/__tests__/server.test.tsx b/__tests__/server.test.tsx index c64ca9f96..d1c339383 100644 --- a/__tests__/server.test.tsx +++ b/__tests__/server.test.tsx @@ -2,7 +2,7 @@ * @jest-environment node */ -import marketplaceController from '../server/controllers/marketplaceController'; +import marketplaceController from '../server/controllers/marketplaceController'; import sessionController from '../server/controllers/sessionController'; import app from '../server/server'; import mockData from '../mockData'; @@ -11,33 +11,34 @@ import { Projects, Users, Sessions } from '../server/models/reactypeModels'; const request = require('supertest'); const mongoose = require('mongoose'); const mockNext = jest.fn(); // Mock nextFunction -const MONGO_DB = process.env.MONGO_DB_TEST; -const { state, projectToSave, user } = mockData +const MONGO_DB = import.meta.env.MONGO_DB_TEST; +const { state, projectToSave, user } = mockData; const PORT = 8080; beforeAll(async () => { await mongoose.connect(MONGO_DB, { useNewUrlParser: true, - useUnifiedTopology: true, + useUnifiedTopology: true }); }); afterAll(async () => { - - const result = await Projects.deleteMany({});//clear the projects collection after tests are done - const result2 = await Users.deleteMany({_id: {$ne: '64f551e5b28d5292975e08c8'}});//clear the users collection after tests are done except for the mockdata user account - const result3 = await Sessions.deleteMany({cookieId: {$ne: '64f551e5b28d5292975e08c8'}}); + const result = await Projects.deleteMany({}); //clear the projects collection after tests are done + const result2 = await Users.deleteMany({ + _id: { $ne: '64f551e5b28d5292975e08c8' } + }); //clear the users collection after tests are done except for the mockdata user account + const result3 = await Sessions.deleteMany({ + cookieId: { $ne: '64f551e5b28d5292975e08c8' } + }); await mongoose.connection.close(); }); - describe('Server endpoint tests', () => { it('should pass this test request', async () => { const response = await request(app).get('/test'); expect(response.status).toBe(200); expect(response.text).toBe('test request is working'); }); - // // test saveProject endpoint // describe('/login', () => { @@ -45,7 +46,7 @@ describe('Server endpoint tests', () => { // it('responds with a status of 200 and json object equal to project sent', async () => { // return request(app) // .post('/login') - // .set('Cookie', [`ssid=${user.userId}`]) + // .set('Cookie', [`ssid=${user.userId}`]) // .set('Accept', 'application/json') // .send(projectToSave) // .expect(200) @@ -62,14 +63,14 @@ describe('Server endpoint tests', () => { it('responds with a status of 200 and json object equal to project sent', async () => { return request(app) .post('/saveProject') - .set('Cookie', [`ssid=${user.userId}`]) + .set('Cookie', [`ssid=${user.userId}`]) .set('Accept', 'application/json') .send(projectToSave) .expect(200) .expect('Content-Type', /application\/json/) .then((res) => expect(res.body.name).toBe(projectToSave.name)); }); - // }); + // }); }); }); // test getProjects endpoint @@ -79,7 +80,7 @@ describe('Server endpoint tests', () => { return request(app) .post('/getProjects') .set('Accept', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) + .set('Cookie', [`ssid=${user.userId}`]) .send({ userId: projectToSave.userId }) .expect(200) .expect('Content-Type', /json/) @@ -94,16 +95,20 @@ describe('Server endpoint tests', () => { describe('/deleteProject', () => { describe('DELETE', () => { it('responds with status of 200 and json object equal to deleted project', async () => { - const response: Response = await request(app).post('/getProjects').set('Accept', 'application/json').set('Cookie', [`ssid=${user.userId}`]).send({ userId: projectToSave.userId }); + const response: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); const _id: String = response.body[0]._id; const userId: String = user.userId; return request(app) .delete('/deleteProject') - .set('Cookie', [`ssid=${user.userId}`]) + .set('Cookie', [`ssid=${user.userId}`]) .set('Content-Type', 'application/json') .send({ _id, userId }) .expect(200) - .then((res) => expect(res.body._id).toBe(_id)); + .then((res) => expect(res.body._id).toBe(_id)); }); }); }); @@ -112,12 +117,11 @@ describe('Server endpoint tests', () => { describe('/publishProject', () => { describe('POST', () => { it('responds with status of 200 and json object equal to published project', async () => { - const projObj = await request(app) .post('/saveProject') - .set('Cookie', [`ssid=${user.userId}`]) + .set('Cookie', [`ssid=${user.userId}`]) .set('Accept', 'application/json') - .send(projectToSave) + .send(projectToSave); const _id: String = projObj.body._id; const project: String = projObj.body.project; const comments: String = projObj.body.comments; @@ -127,56 +131,65 @@ describe('Server endpoint tests', () => { return request(app) .post('/publishProject') .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, project, comments, userId, username, name })//_id, project, comments, userId, username, name + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, project, comments, userId, username, name }) //_id, project, comments, userId, username, name .expect(200) .then((res) => { - expect(res.body._id).toBe(_id) + expect(res.body._id).toBe(_id); expect(res.body.published).toBe(true); - }); + }); }); it('responds with status of 500 and error if userId and cookie ssid do not match', async () => { - const projObj: Response = await request(app).post('/getProjects').set('Accept', 'application/json').set('Cookie', [`ssid=${user.userId}`]).send({ userId: projectToSave.userId }); + const projObj: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); const _id: String = projObj.body[0]._id; const project: String = projObj.body[0].project; const comments: String = projObj.body[0].comments; const username: String = projObj.body[0].username; const name: String = projObj.body[0].name; - const userId: String = "ERROR"; + const userId: String = 'ERROR'; return request(app) .post('/publishProject') .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, project, comments, userId, username, name })//_id, project, comments, userId, username, name + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, project, comments, userId, username, name }) //_id, project, comments, userId, username, name .expect(500) .then((res) => { - expect(res.body.err).not.toBeNull() - }); + expect(res.body.err).not.toBeNull(); + }); }); it('responds with status of 500 and error if _id was not a valid mongo ObjectId', async () => { - const projObj: Response = await request(app).post('/getProjects').set('Accept', 'application/json').set('Cookie', [`ssid=${user.userId}`]).send({ userId: projectToSave.userId }); + const projObj: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); const _id: String = 'ERROR'; const project: String = projObj.body[0].project; const comments: String = projObj.body[0].comments; const username: String = user.username; const name: String = projObj.body[0].name; const userId: String = user.userId; - + return request(app) .post('/publishProject') .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, project, comments, userId, username, name })//_id, project, comments, userId, username, name + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, project, comments, userId, username, name }) //_id, project, comments, userId, username, name .expect(200) .then((res) => { - expect(res.body._id).not.toEqual(_id) - }); + expect(res.body._id).not.toEqual(_id); + }); }); }); }); //test getMarketplaceProjects endpoint - describe('/getMarketplaceProjects', () => {//most recent project should be the one from publishProject + describe('/getMarketplaceProjects', () => { + //most recent project should be the one from publishProject describe('GET', () => { it('responds with status of 200 and json object equal to unpublished project', async () => { @@ -187,7 +200,7 @@ describe('Server endpoint tests', () => { .then((res) => { expect(Array.isArray(res.body)).toBe(true); expect(res.body[0]._id).toBeTruthy; - }); + }); }); }); }); @@ -196,10 +209,9 @@ describe('Server endpoint tests', () => { describe('/cloneProject/:docId', () => { describe('GET', () => { it('responds with status of 200 and json object equal to cloned project', async () => { - const projObj = await request(app) .get('/getMarketplaceProjects') - .set('Content-Type', 'application/json') + .set('Content-Type', 'application/json'); return request(app) .get(`/cloneProject/${projObj.body[0]._id}`) @@ -209,13 +221,12 @@ describe('Server endpoint tests', () => { .then((res) => { expect(res.body.forked).toBeTruthy; expect(res.body.username).toBe(user.username); - }); + }); }); it('responds with status of 500 and error', async () => { - const projObj = await request(app) .get('/getMarketplaceProjects') - .set('Content-Type', 'application/json') + .set('Content-Type', 'application/json'); return request(app) .get(`/cloneProject/${projObj.body[0]._id}`) @@ -223,8 +234,8 @@ describe('Server endpoint tests', () => { .query({ username: [] }) .expect(500) .then((res) => { - expect(res.body.err).not.toBeNull() - }); + expect(res.body.err).not.toBeNull(); + }); }); }); }); @@ -233,7 +244,11 @@ describe('Server endpoint tests', () => { describe('/unpublishProject', () => { describe('PATCH', () => { it('responds with status of 200 and json object equal to unpublished project', async () => { - const response: Response = await request(app).post('/getProjects').set('Accept', 'application/json').set('Cookie', [`ssid=${user.userId}`]).send({ userId: projectToSave.userId }); //most recent project should be the one from publishProject + const response: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); //most recent project should be the one from publishProject const _id: String = response.body[0]._id; const userId: String = user.userId; return request(app) @@ -243,26 +258,29 @@ describe('Server endpoint tests', () => { .send({ _id, userId }) .expect(200) .then((res) => { - expect(res.body._id).toBe(_id) + expect(res.body._id).toBe(_id); expect(res.body.published).toBe(false); - }); + }); }); it('responds with status of 500 and error if userId and cookie ssid do not match', async () => { - const projObj: Response = await request(app).post('/getProjects').set('Accept', 'application/json').set('Cookie', [`ssid=${user.userId}`]).send({ userId: projectToSave.userId }); + const projObj: Response = await request(app) + .post('/getProjects') + .set('Accept', 'application/json') + .set('Cookie', [`ssid=${user.userId}`]) + .send({ userId: projectToSave.userId }); const _id: String = projObj.body[0]._id; const project: String = projObj.body[0].project; const comments: String = projObj.body[0].comments; const username: String = projObj.body[0].username; const name: String = projObj.body[0].name; let userId: String = user.userId; - await request(app)//publishing a project first + await request(app) //publishing a project first .post('/publishProject') .set('Content-Type', 'application/json') - .set('Cookie', [`ssid=${user.userId}`]) - .send({ _id, project, comments, userId, username, name }) - - - userId = "ERROR"; + .set('Cookie', [`ssid=${user.userId}`]) + .send({ _id, project, comments, userId, username, name }); + + userId = 'ERROR'; return request(app) .patch('/unpublishProject') .set('Content-Type', 'application/json') @@ -270,8 +288,8 @@ describe('Server endpoint tests', () => { .send({ _id, userId }) .expect(500) .then((res) => { - expect(res.body.err).not.toBeNull() - }); + expect(res.body.err).not.toBeNull(); + }); }); it('responds with status of 500 and error if _id was not a string', async () => { const userId: String = user.userId; @@ -280,32 +298,28 @@ describe('Server endpoint tests', () => { .patch('/unpublishProject') .set('Content-Type', 'application/json') .set('Cookie', [`ssid=${user.userId}`]) - .send({userId }) + .send({ userId }) .expect(500) .then((res) => { - expect(res.body.err).not.toBeNull() - }); + expect(res.body.err).not.toBeNull(); + }); }); }); }); }); describe('SessionController tests', () => { - - - - describe('isLoggedIn',() => { - + describe('isLoggedIn', () => { afterEach(() => { jest.resetAllMocks(); - }) - // Mock Express request and response objects and next function + }); + // Mock Express request and response objects and next function const mockReq: any = { - cookies: null,//trying to trigger if cookies was not assigned + cookies: null, //trying to trigger if cookies was not assigned body: { - userId: 'sampleUserId', // Set up a sample userId in the request body - }, - } + userId: 'sampleUserId' // Set up a sample userId in the request body + } + }; const mockRes: any = { json: jest.fn(), status: jest.fn(), @@ -313,79 +327,88 @@ describe('SessionController tests', () => { }; const next = jest.fn(); it('Assign userId from request body to cookieId', async () => { - // Call isLoggedIn + // Call isLoggedIn await sessionController.isLoggedIn(mockReq, mockRes, next); expect(mockRes.redirect).toHaveBeenCalledWith('/'); - // Ensure that next() was called + // Ensure that next() was called }); it('Trigger a database query error for findOne', async () => { - const mockFindOne = jest.spyOn(mongoose.model('Sessions'), 'findOne').mockImplementation(() => { - throw new Error('Database query error'); - }); - // Call isLoggedIn + const mockFindOne = jest + .spyOn(mongoose.model('Sessions'), 'findOne') + .mockImplementation(() => { + throw new Error('Database query error'); + }); + // Call isLoggedIn await sessionController.isLoggedIn(mockReq, mockRes, next); - // Ensure that next() was called with the error - expect(next).toHaveBeenCalledWith(expect.objectContaining({ - log: expect.stringMatching('Database query error'), // The 'i' flag makes it case-insensitive - })); + // Ensure that next() was called with the error + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + log: expect.stringMatching('Database query error') // The 'i' flag makes it case-insensitive + }) + ); mockFindOne.mockRestore(); }); }); - - - describe('startSession',() => { - + + describe('startSession', () => { afterEach(() => { jest.resetAllMocks(); - }) + }); it('Trigger a database query error for findOne', async () => { - const mockReq: any = { - cookies: projectToSave.userId,//trying to trigger if cookies was not assigned + cookies: projectToSave.userId, //trying to trigger if cookies was not assigned body: { - userId: 'sampleUserId', // Set up a sample userId in the request body - }, - } + userId: 'sampleUserId' // Set up a sample userId in the request body + } + }; const mockRes: any = { json: jest.fn(), status: jest.fn(), redirect: jest.fn(), - locals: {id: projectToSave.userId} + locals: { id: projectToSave.userId } }; - + const next = jest.fn(); - const findOneMock = jest.spyOn(mongoose.model('Sessions'), 'findOne') as jest.Mock; - findOneMock.mockImplementation((query: any, callback: (err: any, ses: any) => void) => { - callback(new Error('Database query error'), null); - }); + const findOneMock = jest.spyOn( + mongoose.model('Sessions'), + 'findOne' + ) as jest.Mock; + findOneMock.mockImplementation( + (query: any, callback: (err: any, ses: any) => void) => { + callback(new Error('Database query error'), null); + } + ); // Call startSession await sessionController.startSession(mockReq, mockRes, next); // Check that next() was called with the error - expect(next).toHaveBeenCalledWith(expect.objectContaining({ - log: expect.stringMatching('Database query error'), // The 'i' flag makes it case-insensitive - })); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + log: expect.stringMatching('Database query error') // The 'i' flag makes it case-insensitive + }) + ); findOneMock.mockRestore(); }); - xit('Check if a new Session is created', async () => {//not working for some reason cannot get mocknext() to be called in test? + xit('Check if a new Session is created', async () => { + //not working for some reason cannot get mocknext() to be called in test? const mockReq: any = { - cookies: projectToSave.userId,//trying to trigger if cookies was not assigned + cookies: projectToSave.userId, //trying to trigger if cookies was not assigned body: { - userId: 'sampleUserId', // Set up a sample userId in the request body - }, - } + userId: 'sampleUserId' // Set up a sample userId in the request body + } + }; const mockRes: any = { json: jest.fn(), status: jest.fn(), redirect: jest.fn(), - locals: {id: 'testID'}//a sesion id that doesnt exist + locals: { id: 'testID' } //a sesion id that doesnt exist }; - + const mockNext = jest.fn(); - + //Call startSession // Wrap your test logic in an async function await sessionController.startSession(mockReq, mockRes, mockNext); @@ -393,21 +416,10 @@ describe('SessionController tests', () => { //check if it reaches next() //await expect(mockRes.locals.ssid).toBe('testID'); expect(mockNext).toHaveBeenCalled(); - - - }); }); }); - - - - - - - - // describe('marketplaceController Middleware', () => { // describe('getProjects tests', () => { // it('should add the projects as an array to res.locals', () => { @@ -421,13 +433,12 @@ describe('SessionController tests', () => { // }); // }); - // it('should send an error response if there is an error in the middleware', () => { // const req = { user: { isAuthenticated: false } }; // const res = mockResponse(); - + // marketplaceController.authenticateMiddleware(req, res, mockNext); - + // expect(res.status).toHaveBeenCalledWith(500); // expect(res.json).toHaveBeenCalledWith({ err: 'Error in marketplaceController.getPublishedProjects, check server logs for details' }); // }); diff --git a/__tests__/userAuth.test.ts b/__tests__/userAuth.test.ts index 2f5137d97..4a3d8c907 100644 --- a/__tests__/userAuth.test.ts +++ b/__tests__/userAuth.test.ts @@ -2,17 +2,14 @@ * @jest-environment node */ - -import marketplaceController from '../server/controllers/marketplaceController'; import app from '../server/server'; import mockData from '../mockData'; -import { profileEnd } from 'console'; import { Sessions, Users } from '../server/models/reactypeModels'; const request = require('supertest'); const mongoose = require('mongoose'); const mockNext = jest.fn(); // Mock nextFunction -const MONGO_DB = process.env.MONGO_DB_TEST; -const { state, projectToSave, user } = mockData +const MONGO_DB = import.meta.env.MONGO_DB_TEST; +const { user } = mockData; const PORT = 8080; const num = Math.floor(Math.random() * 1000); @@ -20,15 +17,20 @@ const num = Math.floor(Math.random() * 1000); beforeAll(async () => { await mongoose.connect(MONGO_DB, { useNewUrlParser: true, - useUnifiedTopology: true, + useUnifiedTopology: true }); }); afterAll(async () => { - - const result = await Users.deleteMany({_id: {$ne: '64f551e5b28d5292975e08c8'}});//clear the users collection after tests are done except for the mockdata user account - const result2 = await Sessions.deleteMany({cookieId: {$ne: '64f551e5b28d5292975e08c8'}}); - console.log(`${result.deletedCount} and ${result2.deletedCount} documents deleted.`); + const result = await Users.deleteMany({ + _id: { $ne: '64f551e5b28d5292975e08c8' } + }); //clear the users collection after tests are done except for the mockdata user account + const result2 = await Sessions.deleteMany({ + cookieId: { $ne: '64f551e5b28d5292975e08c8' } + }); + console.log( + `${result.deletedCount} and ${result2.deletedCount} documents deleted.` + ); await mongoose.connection.close(); }); @@ -40,7 +42,7 @@ describe('User Authentication tests', () => { expect(response.text).toBe('test request is working'); }); }); - describe('/signup', ()=> { + describe('/signup', () => { describe('POST', () => { //testing new signup it('responds with status 200 and sessionId on valid new user signup', () => { @@ -55,7 +57,7 @@ describe('User Authentication tests', () => { .expect(200) .then((res) => expect(res.body.sessionId).not.toBeNull()); }); - + it('responds with status 400 and json string on invalid new user signup (Already taken)', () => { return request(app) .post('/signup') @@ -67,8 +69,9 @@ describe('User Authentication tests', () => { }); }); }); + describe('/login', () => { - // tests whether existing login information permits user to log in + // tests whether existing login information permits user to log in describe('POST', () => { it('responds with status 200 and json object on verified user login', () => { return request(app) @@ -80,7 +83,7 @@ describe('User Authentication tests', () => { .then((res) => expect(res.body.sessionId).toEqual(user.userId)); }); // if invalid username/password, should respond with status 400 - it('responds with status 400 and json string on invalid user login', () => { + it('responds with status 400 and json string on invalid user login', () => { return request(app) .post('/login') .send({ username: 'wrongusername', password: 'wrongpassword' }) @@ -88,175 +91,60 @@ describe('User Authentication tests', () => { .expect('Content-Type', /json/) .then((res) => expect(typeof res.body).toBe('string')); }); - }); - }); - -}); - - - -// import request from 'supertest'; -// import app from '../server/server'; -// import mockObj from '../mockData'; -// const user = mockObj.user; -// import mongoose from 'mongoose'; -// const URI = process.env.MONGO_DB; - -// beforeAll(() => { -// mongoose -// .connect(URI, { useNewUrlParser: true }, { useUnifiedTopology: true }) -// .then(() => console.log('connected to test database')); -// }); - -// afterAll(async () => { -// await mongoose.connection.close(); -// }); -// //for creating unqiue login credentials -// const num = Math.floor(Math.random() * 1000); - -// describe('User authentication tests', () => { -// //test connection to server -// describe('initial connection test', () => { -// it('should connect to the server', async () => { -// const response = await request(app).get('/test'); -// expect(response.text).toEqual('test request is working'); -// }); -// }); - -// xdescribe('POST', () => { -// it('responds with status 200 and json object on valid new user signup', () => { -// return request(app) -// .post('/signup') -// .set('Content-Type', 'application/json') -// .send({ -// username: `supertest${num}`, -// email: `test${num}@test.com`, -// password: `${num}` -// }) -// .expect(200) -// .then((res) => expect(typeof res.body).toBe('object')); -// }); - -// it('responds with status 400 and json string on invalid new user signup', () => { -// return request(app) -// .post('/signup') -// .send(user) -// .set('Accept', 'application/json') -// .expect('Content-Type', /json/) -// .expect(400) -// .then((res) => expect(typeof res.body).toBe('string')); -// }); -// }); -// }); -// describe('/login', () => { -// // tests whether existing login information permits user to log in -// xdescribe('POST', () => { -// it('responds with status 200 and json object on verified user login', () => { -// return request(app) -// .post('/login') -// .set('Accept', 'application/json') -// .send(user) -// .expect(200) -// .expect('Content-Type', /json/) -// .then((res) => expect(res.body.sessionId).toEqual(user.userId)); -// }); -// // if invalid username/password, should respond with status 400 -// it('responds with status 400 and json string on invalid user login', () => { -// return request(app) -// .post('/login') -// .send({ username: 'wrongusername', password: 'wrongpassword' }) -// .expect(400) -// .expect('Content-Type', /json/) -// .then((res) => expect(typeof res.body).toBe('string')); -// }); -// it('responds with status 400 and json string on invalid new user signup', () => { -// return request(app) -// .post('/signup') -// .send(user) -// .set('Accept', 'application/json') -// .expect('Content-Type', /json/) -// .expect(400) -// .then((res) => expect(typeof res.body).toBe('string')); -// }); -// }); -// }); - -// describe('sessionIsCreated', () => { -// it("returns the message 'No Username Input' when no username is entered", () => { -// return request(app) -// .post('/login') -// .send({ -// username: '', -// password: 'Reactype123!@#', -// isFbOauth: false -// }) -// .then((res) => expect(res.text).toBe('"No Username Input"')); -// }); - -// it("returns the message 'No Password Input' when no password is entered", () => { -// return request(app) -// .post('/login') -// .send({ -// username: 'reactype123', -// password: '', -// isFbOauth: false -// }) -// .then((res) => expect(res.text).toBe('"No Password Input"')); -// }); + it("returns the message 'No Username Input' when no username is entered", () => { + return request(app) + .post('/login') + .send({ + username: '', + password: 'Reactype123!@#', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Username Input"')); + }); -// it("returns the message 'Invalid Username' when username does not exist", () => { -// return request(app) -// .post('/login') -// .send({ -// username: 'l!b', -// password: 'test', -// isFbOauth: false -// }) -// .then((res) => expect(res.text).toBe('"Invalid Username"')); -// }); -// }); + it("returns the message 'No Username Input' when no username is entered", () => { + return request(app) + .post('/login') + .send({ + username: '', + password: 'Reactype123!@#', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Username Input"')); + }); -// it("returns the message 'Incorrect Password' when password does not match", () => { -// return request(app) -// .post('/login') -// .send({ -// username: 'test', -// password: 'test', -// isFbOauth: false -// }) -// .then((res) => expect(res.text).toBe('"Incorrect Password"')); -// }); -// // note that the username and password in this test are kept in the heroku database -// // DO NOT CHANGE unless you have access to the heroku database -// it("returns the message 'Success' when the user passes all auth checks", () => { -// return request(app) -// .post('/login') -// .send({ -// username: 'test', -// password: 'password1!', -// isFbOauth: false -// }) -// .then((res) => expect(res.body).toHaveProperty('sessionId')); -// }); + it("returns the message 'No Password Input' when no password is entered", () => { + return request(app) + .post('/login') + .send({ + username: 'reactype123', + password: '', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"No Password Input"')); + }); -// // // OAuth tests (currently inoperative) + it("returns the message 'Invalid Username' when username does not exist", () => { + return request(app) + .post('/login') + .send({ + username: 'l!b', + password: 'test', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"Invalid Username"')); + }); + }); -// // xdescribe('Github oauth tests', () => { -// // describe('/github/callback?code=', () => { -// // describe('GET', () => { -// // it('responds with status 400 and error message if no code received', () => { -// // return request(server) -// // .get('/github/callback?code=') -// // .expect(400) -// // .then((res) => { -// // return expect(res.text).toEqual( -// // '"Undefined or no code received from github.com"' -// // ); -// // }); -// // }); -// // it('responds with status 400 if invalid code received', () => { -// // return request(server).get('/github/callback?code=123456').expect(400); -// // }); -// // }); -// // }); -// // }); + it("returns the message 'Incorrect Password' when password does not match", () => { + return request(app) + .post('/login') + .send({ + username: 'test', + password: 'test', + isFbOauth: false + }) + .then((res) => expect(res.text).toBe('"Incorrect Password"')); + }); + }); +}); diff --git a/app/.electron/main.ts b/app/.electron/main.ts index a9209dfbe..6bf974c01 100644 --- a/app/.electron/main.ts +++ b/app/.electron/main.ts @@ -30,7 +30,8 @@ import MenuBuilder from './menu'; // mode that the app is running in const isDev = - process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; + import.meta.env.NODE_ENV === 'development' || + import.meta.env.NODE_ENV === 'test'; const port = 8080; const selfHost = `http://localhost:${port}`; @@ -388,8 +389,8 @@ ipcMain.on('github', (event) => { ? `http://localhost:${DEV_PORT}/auth/github` : `https://reactype-caret.herokuapp.com/auth/github`; const options = { - client_id: process.env.GITHUB_ID, - client_secret: process.env.GITHUB_SECRET, + client_id: import.meta.env.GITHUB_ID, + client_secret: import.meta.env.GITHUB_SECRET, scopes: ['user:email', 'notifications'] }; // create new browser window object with size, title, security options diff --git a/app/.electron/menu.ts b/app/.electron/menu.ts index dcdb2708a..c0814ebe2 100644 --- a/app/.electron/menu.ts +++ b/app/.electron/menu.ts @@ -47,7 +47,7 @@ var MenuBuilder = function (mainWindow, appName) { devTools: false } }); - if (process.env.NODE_ENV === 'development') { + if (import.meta.env.NODE_ENV === 'development') { tutorial.loadURL(`http://localhost:8080/#/tutorial`); } else { tutorial.loadURL(`${Protocol.scheme}://rse/index-prod.html#/tutorial`); diff --git a/app/src/Dashboard/NavbarDash.tsx b/app/src/Dashboard/NavbarDash.tsx index 019feab0d..328c0ec81 100644 --- a/app/src/Dashboard/NavbarDash.tsx +++ b/app/src/Dashboard/NavbarDash.tsx @@ -19,36 +19,38 @@ import SortIcon from '@mui/icons-material/Sort'; import StarBorderIcon from '@mui/icons-material/StarBorder'; import PersonIcon from '@mui/icons-material/Person'; import greenLogo from '../public/icons/png/512x512.png'; -import {setStyle} from '../redux/reducers/slice/styleSlice' -import { useSelector,useDispatch } from 'react-redux'; +import { setStyle } from '../redux/reducers/slice/styleSlice'; +import { useSelector, useDispatch } from 'react-redux'; // NavBar text and button styling -const useStyles = makeStyles((theme: Theme) => createStyles({ - root: { - flexGrow: 1, - width: '100%', - }, - menuButton: { - marginRight: theme.spacing(2), - color: 'white', - }, - title: { - flexGrow: 1, - color: 'white', - }, - manageProject: { - display: 'flex', - justifyContent: 'center', - }, -})); +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + flexGrow: 1, + width: '100%' + }, + menuButton: { + marginRight: theme.spacing(2), + color: 'white' + }, + title: { + flexGrow: 1, + color: 'white' + }, + manageProject: { + display: 'flex', + justifyContent: 'center' + } + }) +); // sorting options const sortMethods = ['RATING', 'DATE', 'USER']; // Drop down menu button for SORT PROJECTS const StyledMenu = withStyles({ paper: { - border: '1px solid #d3d4d5', + border: '1px solid #d3d4d5' } -})(props => ( +})((props) => ( )); -const StyledMenuItem = withStyles(theme => ({ +const StyledMenuItem = withStyles((theme) => ({ root: { '&:focus': { '& .MuiListItemIcon-root, & .MuiListItemText-primary': { @@ -76,14 +78,14 @@ const StyledMenuItem = withStyles(theme => ({ export default function NavBar(props) { // TO DO: import setStyle const classes = useStyles(); - const style = useSelector(store => store.styleSlice); + const style = useSelector((store) => store.styleSlice); const dispatch = useDispatch(); const toggling = () => setIsOpen(!isOpen); // toggle to open and close dropdown sorting menu const [isOpen, setIsOpen] = useState(false); // State for sort projects button const [anchorEl, setAnchorEl] = React.useState(null); - const handleClick = event => { + const handleClick = (event) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { @@ -91,80 +93,108 @@ export default function NavBar(props) { }; return (
- + - + - + ReacType - -
+ +
- - {sortMethods.map((option, index) => ( - { - props.optionClicked(option); - toggling(); - handleClose(); - }} - variant='contained' - color='primary' - style={{ minWidth: '137.69px' }} - className={classes.manageProject} - key={index} - > - - - ))} - -
- + + {sortMethods.map((option, index) => ( + { + props.optionClicked(option); + toggling(); + handleClose(); + }} + variant="contained" + color="primary" + style={{ minWidth: '137.69px' }} + className={classes.manageProject} + key={index} + > + + + ))} + +
+ +
- + diff --git a/app/src/Dashboard/Project.tsx b/app/src/Dashboard/Project.tsx index a28adb2f6..fb91843ec 100644 --- a/app/src/Dashboard/Project.tsx +++ b/app/src/Dashboard/Project.tsx @@ -1,11 +1,11 @@ import React, { useState } from 'react'; import { useMutation } from '@apollo/client'; -import { +import { ADD_LIKE, MAKE_COPY, DELETE_PROJECT, PUBLISH_PROJECT, - ADD_COMMENT, + ADD_COMMENT } from './gqlStrings'; import CloseIcon from '@mui/icons-material/Close'; import AddCommentIcon from '@mui/icons-material/AddComment'; @@ -19,13 +19,13 @@ import ListItemText from '@mui/material/ListItemText'; import createModal from '../components/right/createModal'; // Variable validation using typescript type props = { - name: string, - id: string, - userId: string, - username: string, - likes: number, - published: boolean, - comments: object[], + name: string; + id: string; + userId: string; + username: string; + likes: number; + published: boolean; + comments: object[]; }; // Use current user info to make a make copy of another user's project @@ -33,8 +33,13 @@ const currUserSSID = window.localStorage.getItem('ssid') || 'unavailable'; const currUsername = window.localStorage.getItem('username') || 'unavailable'; const Project = ({ - name, likes, id, username, published, comments, -}: props) : JSX.Element => { + name, + likes, + id, + username, + published, + comments +}: props): JSX.Element => { // IMPORTANT: // 1) schema change projId => id to allows Apollo Client cache auto-update. Only works with 'id' // 2) always request the 'id' in a mutation request @@ -46,16 +51,15 @@ const Project = ({ const [publishProject] = useMutation(PUBLISH_PROJECT); const [addComment] = useMutation(ADD_COMMENT); - const noPointer = {cursor: 'default'}; + const noPointer = { cursor: 'default' }; //Likes the project when the star icon is clicked function handleLike(e) { e.preventDefault(); const myVar = { - variables: - { + variables: { projId: id, - likes: likes + 1, - }, + likes: likes + 1 + } }; addLike(myVar); } @@ -63,12 +67,11 @@ const Project = ({ function handleDownload(e) { e.preventDefault(); const myVar = { - variables: - { + variables: { projId: id, userId: currUserSSID, - username: currUsername, - }, + username: currUsername + } }; makeCopy(myVar); } @@ -76,11 +79,10 @@ const Project = ({ function handlePublish(e) { e.preventDefault(); const myVar = { - variables: - { + variables: { projId: id, - published: !published, - }, + published: !published + } }; publishProject(myVar); } @@ -88,14 +90,13 @@ const Project = ({ function handleComment(e) { e.preventDefault(); const myVar = { - variables: - { - projId: id, - comment: commentVal, - username: currUsername, - }, + variables: { + projId: id, + comment: commentVal, + username: currUsername + } }; - addComment(myVar) + addComment(myVar); } //sets state of commentVal to what the user types in to comment function handleChange(e) { @@ -104,16 +105,17 @@ const Project = ({ setCommentVal(commentValue); } const recentComments = []; - if (comments?.length > 0) { + if (comments?.length > 0) { const reversedCommentArray = comments.slice(0).reverse(); - const min = Math.min(6, reversedCommentArray.length) - for (let i = 0; i < min ; i++) { - recentComments.push( -

- { reversedCommentArray[i].username }: - { reversedCommentArray[i].text } -

- )} + const min = Math.min(6, reversedCommentArray.length); + for (let i = 0; i < min; i++) { + recentComments.push( +

+ {reversedCommentArray[i].username}: + {reversedCommentArray[i].text} +

+ ); + } } // Closes out the open modal const closeModal = () => setModal(''); @@ -123,13 +125,12 @@ const Project = ({ const handleDelete = (e) => { e.preventDefault(); const myVar = { - variables: - { - projId: id, - }, + variables: { + projId: id + } }; deleteProject(myVar); - } + }; // Set modal options const children = ( @@ -138,7 +139,7 @@ const Project = ({ button onClick={handleDelete} style={{ - border: '1px solid #1b544b', + border: '1px solid #3c59ba', marginBottom: '2%', marginTop: '5%' }} @@ -157,7 +158,7 @@ const Project = ({ createModal({ closeModal, children, - message: 'Are you sure want to delete this project?', + message: 'Are you sure you want to delete this project?', primBtnLabel: null, primBtnAction: null, secBtnAction: null, @@ -168,61 +169,79 @@ const Project = ({ }; return ( -
-
- { currUsername === username ? - - - - : '' } -
+
+
+ {currUsername === username ? ( + + + + ) : ( + '' + )} +
-

Project: { name }

-

Author: { username }

-

Likes: { likes }

+

Project: {name}

+

Author: {username}

+

Likes: {likes}

-
+
+ + + + {currUsername !== username ? ( - - - { currUsername !== username ? - - - - : '' } - { currUsername === username ? + onClick={handleDownload} + size="large" + > + + + ) : ( + '' + )} + {currUsername === username ? ( - + onClick={handlePublish} + size="large" + > + - : '' } + ) : ( + '' + )}
-
- {recentComments} -
-
- - -
- {modal} +
{recentComments}
+
+ + +
+ {modal}
); }; diff --git a/app/src/Dashboard/styles.css b/app/src/Dashboard/styles.css index 9e90c528c..89500f729 100644 --- a/app/src/Dashboard/styles.css +++ b/app/src/Dashboard/styles.css @@ -1,12 +1,11 @@ - .project { display: flex; flex-direction: column; align-items: stretch; margin: 5px; - border: 1px solid #f0f0f0; + border: 1px solid #f0f0f0; border-radius: 5px; - box-shadow: 0 0 5px rgba(0,0,0,0.3); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); height: 500px; width: 400px; justify-content: space-between; @@ -14,7 +13,7 @@ .dashboardContainer { height: 100%; - width: 100% + width: 100%; } .userDashboard { @@ -26,12 +25,12 @@ width: 100%; } - .projectContainer{ +.projectContainer { display: flex; flex-direction: column-reverse; flex-flow: row wrap; flex-grow: 1; - overflow-y: scroll; + overflow-y: scroll; } .projectInfo { @@ -76,14 +75,14 @@ } .header { - background-color: #29a38a; + background-color: #0671e3; color: rgba(255, 255, 255, 0.897); width: 100%; position: relative; } .commentField { - border: 1px solid #f0f0f0; + border: 1px solid #f0f0f0; padding-left: 2%; } diff --git a/app/src/components/App.tsx b/app/src/components/App.tsx index f21d7aaab..f97e98a27 100644 --- a/app/src/components/App.tsx +++ b/app/src/components/App.tsx @@ -1,102 +1,19 @@ import '../public/styles/style.css'; -import React, { useEffect, useState } from 'react'; -import { - setInitialState, - toggleLoggedIn -} from '../redux/reducers/slice/appStateSlice'; +import React, { useEffect } from 'react'; +import { toggleLoggedIn } from '../redux/reducers/slice/appStateSlice'; import { useDispatch } from 'react-redux'; import AppContainer from '../containers/AppContainer'; -import Cookies from 'js-cookie'; -import { DndProvider } from 'react-dnd'; -import { HTML5Backend } from 'react-dnd-html5-backend'; -import { RootState } from '../redux/store'; -import localforage from 'localforage'; -import { saveProject } from '../helperFunctions/projectGetSaveDel'; -// Intermediary component to wrap main App component with higher order provider components export const App: React.FC = (): JSX.Element => { - // const state = useSelector((store: RootState) => store.appState); - const dispatch = useDispatch(); - // checks if user is signed in as guest or actual user and changes loggedIn boolean accordingly useEffect(() => { if (window.localStorage.getItem('ssid') !== 'guest') { dispatch(toggleLoggedIn(true)); } }, []); - // FOR LOCAL FORAGE: still trying to get this to work - - // // following useEffect runs on first mount - // useEffect(() => { - // console.log('cookies.get in App', Cookies.get()); - // // if user is a guest, see if a project exists in localforage and retrieve it - // // v17: May not currently work yet - // if (!state.isLoggedIn) { - // localforage.getItem('guestProject').then((project) => { - // // if project exists, use dispatch to set initial state to that project - // console.log('local forage get project', project); - // if (project) { - // dispatch(setInitialState(project)); - // } - // }); - // } else { - // // otherwise if a user is logged in, use a fetch request to load user's projects from DB - // let userId; - // if (Cookies.get('ssid')) { - // userId = Cookies.get('ssid'); - // } else { - // userId = window.localStorage.getItem('ssid'); - // } - // //also load user's last project, which was saved in localforage on logout - // localforage.getItem(userId).then((project) => { - // if (project) { - // dispatch(setInitialState(project)); - // } else { - // console.log( - // 'No user project found in localforage, setting initial state blank' - // ); - // } - // }); - // } - // }, []); - - // // New project save configuration to optimize server load and minimize Ajax requests - // useEffect(() => { - // // provide config properties to legacy projects so new edits can be auto saved - // if (state.config === undefined) { - // state.config = { saveFlag: true, saveTimer: false }; - // } - - // if (state.config.saveFlag) { - // state.config.saveFlag = false; - // state.config.saveTimer = true; - // //dispatch(configToggle()); - - // let userId; - // if (Cookies.get('ssid')) { - // userId = Cookies.get('ssid'); - // } else { - // userId = window.localStorage.getItem('ssid'); - // } - - // if (!state.isLoggedIn) { - // localforage.setItem('guestProject', state); - // } else if (state.name !== '') { - // saveProject(state.name, state); - // localforage.setItem(userId, state); - // } - // } - // if (!state.config.saveTimer) { - // state.config.saveTimer = false; - // setTimeout(() => { - // state.config.saveFlag = true; - // }, 15000); - // } - // }, [state]); - return (
diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx index 5d1575491..b06802471 100644 --- a/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx +++ b/app/src/components/ContextAPIManager/AssignTab/components/ComponentDropDrown.tsx @@ -63,14 +63,14 @@ const ComponentDropDown = ({ }; const renderOption = (props, option) => ( -
  • +
  • {option.name}
  • ); return ( - + ( )} diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ComponentTable.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ComponentTable.tsx index fb2363c59..76397b480 100644 --- a/app/src/components/ContextAPIManager/AssignTab/components/ComponentTable.tsx +++ b/app/src/components/ContextAPIManager/AssignTab/components/ComponentTable.tsx @@ -34,7 +34,7 @@ export default function DataTable({ target }) { - Contexts Consumed + Contexts Consumed diff --git a/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx b/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx index 861a7d8cd..f1591ebe4 100644 --- a/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx +++ b/app/src/components/ContextAPIManager/AssignTab/components/ContextDropDown.tsx @@ -61,14 +61,14 @@ const ContextDropDown = ({ }; const renderOption = (props, option) => ( -
  • +
  • {option.name}
  • ); return ( - + ( )} /> diff --git a/app/src/components/ContextAPIManager/ContextManager.tsx b/app/src/components/ContextAPIManager/ContextManager.tsx index 6a52847b3..8927f9dc0 100644 --- a/app/src/components/ContextAPIManager/ContextManager.tsx +++ b/app/src/components/ContextAPIManager/ContextManager.tsx @@ -14,7 +14,6 @@ import { RootState } from '../../redux/store'; const useStyles = makeStyles({ contextContainer: { - backgroundColor: '#191919', height: 'fit-content' } }); diff --git a/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx b/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx index 866b362e3..beae68878 100644 --- a/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx +++ b/app/src/components/ContextAPIManager/CreateTab/components/AddContextForm.tsx @@ -75,7 +75,7 @@ const AddContextForm = ({ return ( - + Create Context @@ -85,14 +85,13 @@ const AddContextForm = ({ }} onChange={handleChange} sx={{ - width: 425, - border: `1px solid ${color}` + width: 425 }} - label="Create Context" + label="context" value={contextInput} helperText={errorStatus ? errorMsg : null} error={errorStatus} - variant="filled" + variant="outlined" /> Create @@ -120,13 +125,13 @@ const AddContextForm = ({ Select Context - - - Select Context + + + select context { onChange={(event) => setInputType(event.target.value)} MenuProps={{ disablePortal: true }} style={{ - backgroundColor: 'gray', - color: '#fff', - border: '1px solid white', - height: '28px', - width: '200px' + height: '100%', + width: '100%', + margin: '0 auto' }} > - + Types - String + string - Number + number - Boolean + boolean - Array + array - Object + object - Undefined + undefined - Any + any { ? `${classes.addComponentButton} ${classes.lightThemeFontColor}` : `${classes.addComponentButton} ${classes.darkThemeFontColor}` } + sx={{ textTransform: 'capitalize' }} > Save @@ -428,7 +427,7 @@ const StatePropsPanel = ({ isThemeLight, data }): JSX.Element => { display: 'flex', flexDirection: 'column', width: `${40}px`, - color: 'black', + color: '#0671E3', justifyContent: 'center' }} > @@ -494,11 +493,11 @@ const useStyles = makeStyles((theme: Theme) => ({ width: '100%' }, rootCheckBox: { - borderColor: '#46C0A5', + borderColor: '#0671e3', padding: '0px' }, rootCheckBoxLabel: { - borderColor: '#46C0A5' + borderColor: '#0671e3' }, panelWrapper: { width: '100%', @@ -522,8 +521,8 @@ const useStyles = makeStyles((theme: Theme) => ({ alignItems: 'center', textAlign: 'center', width: '500px', - backgroundColor: '#46C0A5', - border: '5px solid #46C0A5' + backgroundColor: '#0671e3', + border: '5px solid #0671e3' }, panelSubheader: { textAlign: 'center', @@ -531,7 +530,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, input: {}, newComponent: { - color: '#1b544b', + color: '#3c59ba', fontSize: '95%', marginBottom: '20px' }, @@ -544,14 +543,14 @@ const useStyles = makeStyles((theme: Theme) => ({ flexDirection: 'column' }, addComponentButton: { - backgroundColor: 'transparent', - height: '40px', + height: '42px', width: '100px', fontFamily: 'Roboto, Raleway, sans-serif', - fontSize: '90%', + fontSize: '14.5px', textAlign: 'center', margin: '-20px 0px 5px 150px', - border: ' 1px solid #46C0A5', + border: ' 1px solid #0671E3', + borderRadius: '8px', transition: '0.3s' }, rootToggle: { @@ -580,15 +579,13 @@ const useStyles = makeStyles((theme: Theme) => ({ rootLight: { '& .MuiFormLabel-root': { color: 'white' - } + }, + margin: '5px' }, rootDark: { - '& .MuiFormLabel-root': { - color: '#fff' - }, - '& .MuiOutlinedInput-notchedOutline': { - borderColor: '#fff' - } + '& .MuiFormLabel-root': {}, + '& .MuiOutlinedInput-notchedOutline': {}, + margin: '5px' }, underlineDark: { borderBottom: '1px solid white' diff --git a/app/src/components/StateManagement/DisplayTab/DataTable.tsx b/app/src/components/StateManagement/DisplayTab/DataTable.tsx index 4714ab37e..6d066e906 100644 --- a/app/src/components/StateManagement/DisplayTab/DataTable.tsx +++ b/app/src/components/StateManagement/DisplayTab/DataTable.tsx @@ -86,22 +86,22 @@ export default function DataTable(props) { {/* The below table will contain the state initialized within the clicked component */} - + State Initialized in Current Component: - - Key - Type - Initial Value + + Key + Type + Initial Value {currComponentState ? currComponentState.map((data, index) => ( - - {data.key} - {data.type} - {data.value} + + {data.key} + {data.type} + {data.value} )) : ''} diff --git a/app/src/components/StateManagement/DisplayTab/DisplayContainer.tsx b/app/src/components/StateManagement/DisplayTab/DisplayContainer.tsx index d6307a906..22a7d8ff1 100644 --- a/app/src/components/StateManagement/DisplayTab/DisplayContainer.tsx +++ b/app/src/components/StateManagement/DisplayTab/DisplayContainer.tsx @@ -41,14 +41,14 @@ function DisplayContainer({ data, props }) { setClickedComp={setClickedComp} /> - + - Click on a component in the graph to see its state! + Click on a component in the graph to see its state node.x - 20) .attr('text-anchor', 'middle') .attr('font-size', 18) - .style('fill', textAndBorderColor) + .style('fill', 'white') .text((node) => node.data.name) .attr('opacity', 1) .attr('transform', `translate(${xPosition}, 0)`); @@ -185,13 +185,12 @@ function Tree({ }; const wrapperStyles = { - border: `2px solid ${textAndBorderColor}`, - borderRadius: '8px', + borderRadius: '10px', width: '100%', height: '90%', display: 'flex', justifyContent: 'center', - backgroundColor: '#42464C', + backgroundColor: '#1E2024' }; return ( diff --git a/app/src/components/StateManagement/StateManagement.tsx b/app/src/components/StateManagement/StateManagement.tsx index a2255aa13..c86aaad2f 100644 --- a/app/src/components/StateManagement/StateManagement.tsx +++ b/app/src/components/StateManagement/StateManagement.tsx @@ -33,14 +33,11 @@ const StateManager = (props): JSX.Element => { // add hook here to access which component has been clicked // then this will re-render the dataTable - const background_Color = '#21262b'; - const color = 'white'; - return (
    diff --git a/app/src/components/bottom/BottomPanel.tsx b/app/src/components/bottom/BottomPanel.tsx index 5004e07cc..753ff3a7f 100644 --- a/app/src/components/bottom/BottomPanel.tsx +++ b/app/src/components/bottom/BottomPanel.tsx @@ -1,39 +1,39 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import BottomTabs from './BottomTabs'; +import { ExpandLess, ExpandMore } from '@mui/icons-material'; const BottomPanel = (props): JSX.Element => { let y: number = 0; let h: number = 0; const node = useRef() as React.MutableRefObject; + const [isDragging, setIsDragging] = useState(false); + const mouseDownHandler = (e): void => { y = e.clientY; const styles = window.getComputedStyle(node.current); h = parseInt(styles.height, 10); - //Start listeners when the user clicks the bottom panel tab document.addEventListener('mousemove', mouseMoveHandler); document.addEventListener('mouseup', mouseUpHandler); - window.addEventListener('message', handleIframeMessage);//listens for messages from the iframe when the mouse is over it + window.addEventListener('message', handleIframeMessage); //listens for messages from the iframe when the mouse is over it }; - + //Interpret the messages from the iframe const handleIframeMessage = (e) => { if (e.data === 'iframeMouseUp') { mouseUpHandler(); - }else if(e.data.type === 'iframeMouseMove'){ - mouseMoveHandler(e.data) + } else if (e.data.type === 'iframeMouseMove') { + mouseMoveHandler(e.data); } - } - + }; const mouseMoveHandler = function (e: MouseEvent): void { - // How far the mouse has been moved + if (!props.bottomShow) return; // prevent drag calculation to occur when bottom menu is not showing const dy = y - e.clientY; - // Adjust the dimension of element const newVal = h + dy; const styles = window.getComputedStyle(node.current); const min = parseInt(styles.minHeight, 10); @@ -41,7 +41,8 @@ const BottomPanel = (props): JSX.Element => { }; const mouseUpHandler = function () { - // Remove the handlers of `mousemove` and `mouseup` + // puts false in callback queue after OnDragStart sets to true (b/c react 17 doesn't have onDragEnd) + setTimeout(() => setIsDragging(false), 0); document.removeEventListener('mousemove', mouseMoveHandler); document.removeEventListener('mouseup', mouseUpHandler); window.removeEventListener('message', handleIframeMessage); @@ -54,12 +55,23 @@ const BottomPanel = (props): JSX.Element => { return ( <> -
    -
    - ...... +
    +
    setIsDragging(true)} + onClick={() => !isDragging && props.setBottomShow(!props.bottomShow)} + tabIndex={0} + > + {props.bottomShow ? : } +
    +
    - -
    + ); }; diff --git a/app/src/components/bottom/BottomTabs.tsx b/app/src/components/bottom/BottomTabs.tsx index 04e66f3bc..ecdb6d954 100644 --- a/app/src/components/bottom/BottomTabs.tsx +++ b/app/src/components/bottom/BottomTabs.tsx @@ -2,12 +2,12 @@ import React, { useState } from 'react'; import makeStyles from '@mui/styles/makeStyles'; import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; -import CodePreview from './CodePreview'; import StylesEditor from './StylesEditor'; import CustomizationPanel from '../../containers/CustomizationPanel'; import CreationPanel from './CreationPanel'; import ContextManager from '../ContextAPIManager/ContextManager'; import StateManager from '../StateManagement/StateManagement'; +import Chatroom from './ChatRoom'; import Box from '@mui/material/Box'; import Tree from '../../tree/TreeChart'; import FormControl from '@mui/material/FormControl'; @@ -17,134 +17,160 @@ import arrow from '../main/Arrow'; import { useDispatch, useSelector } from 'react-redux'; import { changeProjectType } from '../../redux/reducers/slice/appStateSlice'; import { RootState } from '../../redux/store'; +import { MeetingProvider } from '@videosdk.live/react-sdk'; +const videoSDKToken = `${import.meta.env.VITE_VIDEOSDK_TOKEN}`; const BottomTabs = (props): JSX.Element => { - // state that controls which tab the user is on + const { setBottomShow, isThemeLight } = props; const dispatch = useDispatch(); - // const { state, contextParam, style } = useSelector((store: RootState) => ({ - // state: store.appState, - // contextParam: store.contextSlice, - // style: store.styleSlice - // })); - const state = useSelector((store: RootState) => store.appState); const contextParam = useSelector((store: RootState) => store.contextSlice); + const collaborationRoom = useSelector((store: RootState) => store.roomSlice); const [tab, setTab] = useState(0); const classes = useStyles(); const [theme, setTheme] = useState('solarized_light'); - // breaks if handleChange is commented out const handleChange = (event: React.ChangeEvent, value: number) => { setTab(value); }; - // Allows users to toggle project between "next.js" and "Classic React" - // When a user changes the project type, the code of all components is rerendered + const handleProjectChange = (event) => { const projectType = event.target.value; dispatch(changeProjectType({ projectType, contextParam })); }; const { components } = state; - // Render's the highliting arrow feature that draws an arrow from the Canvas to the DemoRender arrow.renderArrow(state.canvasFocus?.childId); + const showBottomPanel = () => { + setBottomShow(true); + }; + return ( -
    { - props.setBottomShow(true); + - - - - - - - - - -
    - - - + + + + + + + + + + + +
    + + + +
    +
    +
    + {tab === 0 && } + {tab === 1 && } + {tab === 2 && } + {tab === 3 && } + {tab === 4 && } + {tab === 5 && } + {tab === 6 && ( + + )}
    - -
    - {tab === 0 && } - {tab === 1 && } - {tab === 2 && } - {tab === 3 && } - {tab === 4 && } - {tab === 5 && ( - - )}
    -
    +
    ); }; @@ -152,11 +178,10 @@ const useStyles = makeStyles((theme) => ({ root: { flexGrow: 1, height: '100%', - color: '#E8E8E8', - boxShadow: '0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)' + color: '#E8E8E8' }, rootLight: { - backgroundColor: '#1e8370' + backgroundColor: '#0671e3' }, bottomHeader: { flex: 1, @@ -168,13 +193,12 @@ const useStyles = makeStyles((theme) => ({ minHeight: '50%' }, tabsIndicator: { - backgroundColor: 'white' + backgroundColor: '#0671E3' }, tabRoot: { textTransform: 'initial', - minWidth: 40, - margin: '0 16px', - + minWidth: 170, + height: 60, fontFamily: [ '-apple-system', 'BlinkMacSystemFont', @@ -191,8 +215,10 @@ const useStyles = makeStyles((theme) => ({ color: 'white', opacity: 1 }, + fontWeight: 300, '&$tabSelected': { - color: 'white' + color: 'white', + backgroundColor: '#2D313A' }, '&:focus': { color: 'white' @@ -215,8 +241,9 @@ const useStyles = makeStyles((theme) => ({ marginLeft: '10px' }, projectSelector: { - backgroundColor: '#29a38a', - color: 'white' + backgroundColor: '#131416', + color: 'white', + margin: '0 10px 10px 0' } })); diff --git a/app/src/components/bottom/ChatRoom.tsx b/app/src/components/bottom/ChatRoom.tsx new file mode 100644 index 000000000..128b2845b --- /dev/null +++ b/app/src/components/bottom/ChatRoom.tsx @@ -0,0 +1,237 @@ +import { useState, useRef, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../redux/store'; +import { emitEvent } from '../../helperFunctions/socket'; +import Videomeeting from './VideoMeeting'; +import { Send } from '@mui/icons-material'; + +const Chatroom = (props): JSX.Element => { + const { userName, roomCode, messages, userJoinCollabRoom } = useSelector( + (store: RootState) => store.roomSlice + ); + + const [inputContent, setInputContent] = useState(''); + + const wrapperStyles = { + border: '1px solid #31343A', + borderRadius: '15px', + width: '70%', + height: '100%', + display: 'flex', + flexDirection: 'column', + alignSelf: 'center', + padding: '12px 20px', + backgroundColor: '#1B1B1B', + overflow: 'auto' + }; + + const inputContainerStyles = { + width: '100%', + paddingLeft: '20px', + paddingTop: '10px', + display: 'flex', + justifyContent: 'center' + }; + + const inputStyles = { + width: '72%', + padding: '10px 12px', + borderRadius: '50px', + backgroundColor: '#1B1B1B', + color: 'white', + border: '1px solid #31343A', + marginLeft: '28px' + }; + + const buttonStyles = { + padding: '5px 7px', + marginLeft: '10px', + backgroundColor: '#0671E3', + color: 'white', + border: 'none', + borderRadius: '50%', + cursor: 'pointer' + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (inputContent !== '') { + emitEvent('send-chat-message', roomCode, { + userName, + message: inputContent + }); + setInputContent(''); + } + }; + + const handleMessageContainerStyle = (message: object) => { + if (message['type'] === 'activity') { + return { + color: '#E8E9EB', + fontSize: '12px', + alignSelf: 'center', + margin: '3px 0' + }; + } else { + if (message['userName'] === userName) + return { + alignSelf: 'end', + padding: '8px 15px', + backgroundColor: '#0671E3', + borderRadius: '15.5px', + margin: '3px 0', + maxWidth: '300px' + }; + return { + color: 'white', + padding: '8px 15px', + backgroundColor: '#333333', + borderRadius: '15.5px', + margin: '3px 0', + maxWidth: '300px' + }; + } + }; + + const renderMessages = () => { + return messages.map((message, index) => { + if (message.type === 'activity') { + return ( +
    + {message.message} +
    + ); + } else if (message.type === 'chat') { + if (message.userName === userName) { + return ( +
    + {message.message} +
    + ); + } else + return ( +
    +
    + {message.userName} +
    +
    + {message.message} +
    +
    + ); + } + return null; + }); + }; + + const containerRef = useRef(null); + + // Scroll to the bottom of the container whenever new messages are added + useEffect(() => { + if (containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + }, [messages]); + + return ( +
    +
    + + {userJoinCollabRoom && ( +
    +
    +
    + {renderMessages()} +
    +
    +
    + setInputContent(e.target.value)} + value={inputContent} + style={inputStyles} + /> + + +
    +
    +
    + )} +
    + {!userJoinCollabRoom && ( +
    +

    + Please join a collaboration room to enable this function +

    +
    + )} +
    + ); +}; +export default Chatroom; diff --git a/app/src/components/bottom/CodePreview.tsx b/app/src/components/bottom/CodePreview.tsx index 46730153c..74edf7200 100644 --- a/app/src/components/bottom/CodePreview.tsx +++ b/app/src/components/bottom/CodePreview.tsx @@ -2,7 +2,7 @@ import 'ace-builds/src-noconflict/ace'; import 'ace-builds/src-min-noconflict/ext-searchbox'; import 'ace-builds/src-noconflict/mode-javascript'; import 'ace-builds/src-noconflict/theme-dracula'; -import 'ace-builds/src-noconflict/theme-terminal'; +import 'ace-builds/src-noconflict/theme-clouds_midnight'; import React, { useContext, useEffect, useRef, useState } from 'react'; import { @@ -22,7 +22,9 @@ import { initializeEsbuild } from '../../helperFunctions/esbuildService'; const CodePreview: React.FC<{ theme: string | null; setTheme: any | null; -}> = ({ theme, setTheme }) => { + // zoom: number; // This is added if you want the Code Editor to zoom in/out + containerRef: any; +}> = ({ theme, setTheme, zoom, containerRef }) => { const ref = useRef(); const dispatch = useDispatch(); @@ -70,7 +72,7 @@ const CodePreview: React.FC<{ minify: true, plugins: [unpkgPathPlugin(), fetchPlugin(data)], define: { - 'process.env.NODE_ENV': '"production"', + 'import.meta.env.NODE_ENV': '"production"', global: 'window' } }); @@ -84,12 +86,14 @@ const CodePreview: React.FC<{ top: '1vw', height: '100%', maxWidth: '100%', - justifyContent: 'center' + justifyContent: 'center', + transform: `scale(${zoom})` }} > +
    {body} diff --git a/app/src/components/bottom/VideoMeeting.tsx b/app/src/components/bottom/VideoMeeting.tsx new file mode 100644 index 000000000..097941f93 --- /dev/null +++ b/app/src/components/bottom/VideoMeeting.tsx @@ -0,0 +1,257 @@ +import { useState, useRef, useEffect, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { RootState } from '../../redux/store'; +import { + setUserJoinMeetingStatus, + setMeetingParticipants, + setUseMic, + setUseWebcam +} from '../../redux/reducers/slice/roomSlice'; +import { + MeetingConsumer, + useMeeting, + useParticipant +} from '@videosdk.live/react-sdk'; +import ReactPlayer from 'react-player'; +import Button from '@mui/material/Button'; +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import MicOffIcon from '@mui/icons-material/MicOff'; +import MicIcon from '@mui/icons-material/Mic'; +import VideocamIcon from '@mui/icons-material/Videocam'; +import VideocamOffIcon from '@mui/icons-material/VideocamOff'; +import VideoMeetingControl from './VideoMeetingControl'; + +const Videomeeting = (props): JSX.Element => { + const dispatch = useDispatch(); + const { + meetingId, + userJoinCollabRoom, + userJoinMeetingStatus, + meetingParticipants, + useMic, + useWebcam + } = useSelector((store: RootState) => store.roomSlice); + + const micRef = useRef(null); + + const TurnOffCameraDisplay = () => { + return ( +
    + +
    + ); + }; + + const onMeetingLeave = () => { + dispatch(setUserJoinMeetingStatus(null)); + dispatch(setUseWebcam(false)); + dispatch(setUseMic(false)); + }; + + const handleUserInfoStyle = (isLocalParticipant: boolean) => { + if (isLocalParticipant) return { color: '#0671E3', alignItems: 'center' }; + else return { color: 'white', alignItems: 'center' }; + }; + + const ParticipantView = ({ participantId, isLocalParticipant }) => { + const { webcamStream, micStream, webcamOn, micOn, isLocal, displayName } = + useParticipant(participantId); + + const videoStream = useMemo(() => { + if (webcamOn && webcamStream) { + const mediaStream = new MediaStream(); + mediaStream.addTrack(webcamStream.track); + return mediaStream; + } + }, [webcamStream, webcamOn]); + + useEffect(() => { + if (micRef.current) { + if (micOn && micStream) { + const mediaStream = new MediaStream(); + mediaStream.addTrack(micStream.track); + + micRef.current.srcObject = mediaStream; + + micRef.current + .play() + .catch((error) => + console.error('videoElem.current.play() failed', error) + ); + } else { + micRef.current.srcObject = null; + } + } + }, [micStream, micOn]); + + return ( + <> + {userJoinMeetingStatus === 'JOINED' && ( + <> +
    +
    + + )} + + ); + }; + + const MeetingView = ({ onMeetingLeave }) => { + const { join, localParticipant, leave } = useMeeting(); + + const { participants } = useMeeting({ + onMeetingJoined: () => { + dispatch(setUserJoinMeetingStatus('JOINED')); + }, + onMeetingLeft: () => { + onMeetingLeave(); + } + }); + + const meetingParticipantsId = [...participants.keys()]; + + if ( + JSON.stringify(meetingParticipantsId) !== + JSON.stringify(meetingParticipants) && + meetingParticipantsId.length > 0 + ) { + dispatch(setMeetingParticipants(meetingParticipantsId)); + } + + const joinMeeting = () => { + dispatch(setUserJoinMeetingStatus('JOINING')); + join(); + }; + + if (!userJoinCollabRoom && userJoinMeetingStatus !== null) { + leave(); + onMeetingLeave(); + dispatch(setUserJoinMeetingStatus(null)); + } + + return ( +
    +
    + +
    + {[...meetingParticipantsId].map((participantId, idx) => ( + + ))} +
    +
    + {userJoinMeetingStatus === 'JOINING' &&

    Joining the meeting...

    } + {userJoinCollabRoom && userJoinMeetingStatus === null && ( + + )} +
    + ); + }; + + return ( + meetingId && ( + + {() => } + + ) + ); +}; + +export default Videomeeting; diff --git a/app/src/components/bottom/VideoMeetingControl.tsx b/app/src/components/bottom/VideoMeetingControl.tsx new file mode 100644 index 000000000..b8de16245 --- /dev/null +++ b/app/src/components/bottom/VideoMeetingControl.tsx @@ -0,0 +1,176 @@ +import React, { useState, useCallback } from 'react'; +import { useMeeting } from '@videosdk.live/react-sdk'; +import { useSelector, useDispatch } from 'react-redux'; +import CallEndIcon from '@mui/icons-material/CallEnd'; +import MicOffIcon from '@mui/icons-material/MicOff'; +import MicIcon from '@mui/icons-material/Mic'; +import VideocamIcon from '@mui/icons-material/Videocam'; +import VideocamOffIcon from '@mui/icons-material/VideocamOff'; + +import { setUseMic, setUseWebcam } from '../../redux/reducers/slice/roomSlice'; +import { RootState } from '../../redux/store'; + +interface VideoMeetingControlProps { + userJoinMeetingStatus: string; + useWebcam: boolean; + useMic: boolean; +} + +enum ButtonType { + CALL_END = 'Call End', + MIC = 'Mic', + WEBCAM = 'Webcam' +} + +const VideoMeetingControl: React.FC = () => { + const { leave, toggleMic, toggleWebcam } = useMeeting(); + + const [callEndHovered, setCallEndHovered] = useState(false); + const [micHovered, setMicHovered] = useState(false); + const [webcamHovered, setWebcamHovered] = useState(false); + + const dispatch = useDispatch(); + const { userJoinMeetingStatus, useMic, useWebcam } = useSelector( + (store: RootState) => store.roomSlice + ); + + const handleButtonHover = useCallback((button: string, hovered: boolean) => { + switch (button) { + case ButtonType.CALL_END: + setCallEndHovered(hovered); + break; + case ButtonType.MIC: + setMicHovered(hovered); + break; + default: + setWebcamHovered(hovered); + } + }, []); + + return ( + userJoinMeetingStatus === 'JOINED' && ( +
    + {/* Mic Button */} +
    handleButtonHover(ButtonType.MIC, true)} + onMouseLeave={() => handleButtonHover(ButtonType.MIC, false)} + > + +
    + {/* Webcam Button */} +
    handleButtonHover(ButtonType.WEBCAM, true)} + onMouseLeave={() => handleButtonHover(ButtonType.WEBCAM, false)} + > + +
    + {/* Call End Button */} +
    handleButtonHover(ButtonType.CALL_END, true)} + onMouseLeave={() => handleButtonHover(ButtonType.CALL_END, false)} + > + +
    +
    + ) + ); +}; + +export default VideoMeetingControl; diff --git a/app/src/components/form/Selector.tsx b/app/src/components/form/Selector.tsx index 2a7e490fd..f82d18267 100644 --- a/app/src/components/form/Selector.tsx +++ b/app/src/components/form/Selector.tsx @@ -4,45 +4,59 @@ import Select from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; type Props = { - items: [], + items: []; classes: { - configRow: any, - configType: any, - lightThemeFontColor: {color: String}, - formControl: any, - select: any, - selectInput: any, - darkThemeFontColor: {color: String}, - }, - isThemeLight: Boolean, - title: String, - selectValue: any, - handleChange: any, - name: String, -} + configRow: any; + configType: any; + lightThemeFontColor: { color: String }; + formControl: any; + select: any; + selectInput: any; + darkThemeFontColor: { color: String }; + }; + isThemeLight: Boolean; + title: String; + selectValue: any; + handleChange: any; + name: String; +}; const FormSelector = (props): JSX.Element => { const items = []; let key = 1; - props.items.forEach(el => { - items.push({el.text}); - key++ - }) + props.items.forEach((el) => { + items.push( + + {el.text} + + ); + key++; + }); return (
    -
    +

    {props.title}