├── .gitignore ├── .vscode └── launch.json ├── README.md ├── package-lock.json ├── package.json ├── public ├── assets │ ├── Cute-astronaut-floating-with-balloon-cartoon-on-transparent-background-PNG.png │ ├── add-project-form.png │ ├── chat.png │ ├── cute-astronaut-floating-space-cartoon-character.png │ ├── cute-astronaut-floating-space-no-stars.png │ ├── gitHub_world.png │ ├── gitTogetherLogo.png │ ├── notifications-dropdown.png │ ├── project_feed.png │ ├── registered-trademark-png-white-png-download-registered-trademark.png │ ├── repo_form.png │ ├── respond-join-request.png │ ├── single-project-view.png │ ├── single_project.png │ └── user_profile.png ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── seed.js └── src ├── App.css ├── App.js ├── App.test.js ├── CreateUser.js ├── FetchLanguages.js ├── Routes.js ├── client.js ├── components ├── AccountSetup │ ├── AccountSetup.css │ └── AccountSetup.js ├── AddProject │ ├── AddProject.css │ ├── AddProject.js │ ├── Popup.css │ ├── Popup.js │ └── index.js ├── Admin │ ├── AdminAdd │ │ ├── AdminAddCategory.css │ │ ├── AdminAddLanguage.js │ │ ├── AdminPopup.css │ │ └── AdminPopup.js │ └── AdminUsers │ │ ├── AdminUsers.css │ │ └── AdminUsers.js ├── Chat │ ├── Chat.js │ ├── Messages │ │ ├── Messages.js │ │ └── messages.scss │ ├── PrivateConvo │ │ ├── Private.js │ │ └── privateConvo.scss │ ├── TeamConvo │ │ ├── TeamConvo.js │ │ └── teamConvo.scss │ └── chat.scss ├── Footer │ ├── Footer.js │ └── footer.scss ├── GithubCollab │ ├── AddCollaborators.js │ ├── ProjectRepo.js │ └── RepoCreation.js ├── LandingPage │ ├── Final │ │ ├── Final.js │ │ └── final.scss │ ├── Intro │ │ ├── Intro.js │ │ └── intro.scss │ ├── LandingPage.js │ ├── StepOne │ │ ├── StepOne.js │ │ └── stepOne.scss │ ├── StepThree │ │ ├── StepThree.js │ │ └── stepThree.scss │ ├── StepTwo │ │ ├── StepTwo.js │ │ └── stepTwo.scss │ └── landingPage.scss ├── Login │ ├── Login.js │ └── login.scss ├── NotFound │ ├── NotFound.css │ └── NotFound.js ├── ProjectFeed │ ├── ProjectFeed.css │ ├── ProjectFeed.js │ └── ProjectTile.js ├── SingleProject │ ├── SingleProject.css │ └── SingleProject.js ├── UserProfile │ ├── BioModal.js │ ├── FirstMessage │ │ ├── FirstMessage.css │ │ ├── FirstMessage.js │ │ └── MessagePopup.js │ ├── PictureModal.css │ ├── PictureModal.js │ ├── ProjectModal.css │ ├── ProjectModal.js │ ├── UserProfile.js │ └── style.css └── navbar │ ├── DropdownMenu │ ├── DropdownMenu.css │ ├── DropdownMenu.js │ ├── DropdownMenuItem.js │ └── NotificationOptions.js │ ├── Navbar.js │ ├── Notifications.js │ ├── SearchBox.js │ ├── SearchDropdown │ ├── SearchDropdown.css │ └── SearchDropdown.js │ └── navbar.scss ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js ├── setupTests.js ├── store ├── comments.js ├── conversations.js ├── convoId.js ├── dmContent.js ├── dmId.js ├── dmUsers.js ├── hasMore.js ├── index.js ├── messages.js ├── project.js ├── projects.js └── user.js └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [gitTogether](https://gittogether-fsa.herokuapp.com/) 2 | 3 | gitTogether is a web application to find new projects and developers to work with in one place. 4 | 5 | * **Declarative:** gitTogether makes it simple to find new projects and developers to work with. Log in with your GitHub account and immediately begin looking for projects to work on with people or list your own. 6 | * **GitHub-Oriented:** By using and authorizing GitHub accounts, a users top programming languages will be displayed on their profile making it easy to assess their capability in non-beginner friendly projects. By creating a project, you have the option to provide a repository you've already made, or you can create a GitHub repository directly from your project. You can invite current project members to be repository collaborators from your project, they will also get an invitation when you accept them into the project if a repository exists. 7 | * **User-Friendly:** gitTogether provides real-time notifications about your status in a project, or another user requesting to join your project. Users also have access to a project-specific chat, as well as team chats and direct messages. Users can directly create and edit a bio from their profile. 8 | 9 | 10 | ## Getting Started 11 | 12 | From the home page, you can authorize and [log in](https://gittogether-gokq.onrender.com/login) with your GitHub account: 13 | 14 | ![logging in](https://user-images.githubusercontent.com/86242483/161310151-6ccdbc0e-8d64-4a6e-a5c2-d7a46195ee26.png) 15 | 16 | 17 | Once logged in, you'll be redirected to a project feed where you can filter projects and request to collaborate on them: 18 | 19 | ![project feed](https://user-images.githubusercontent.com/86242483/161310225-ec907131-ddd9-46b0-a753-0a0cbdedf0cc.png) 20 | 21 | 22 | View a specific projects page providing you the options to request to join or leave a comment: 23 | 24 | ![project view](https://user-images.githubusercontent.com/86242483/161312067-3c1ecaff-c073-456c-be1d-baa6cd2067f9.png) 25 | 26 | Create a new project: 27 | 28 | ![create project](https://user-images.githubusercontent.com/86242483/161321059-7647842f-39c6-4b12-8947-c4722eab7f56.png) 29 | 30 | 31 | ## Finding a Project 32 | 33 | * Finding a project to work on with people can be as simple as logging in with your GitHub account and clicking Request to Collab! 34 | * As a user, you have the ability to see a scrollable feed of every project people have created and are looking for members on and directly request to join them. 35 | * The feed of projects may be filtered to beginner friendly or not, a specific category you're interested in or the programming language you are interested in. 36 | 37 | 38 | ## Joining a Project 39 | 40 | * If you are not fluent in the language that is provided in a non-beginner friendly project, you will not be able to request to join. 41 | * The project owner will receive a notification when you request to join their project, and you will receive a notification when they accept it. 42 | * A project will not show up on your user profile until you are accepted into it. 43 | * Projects will have their own comments section for all users to discuss about the project. 44 | * Users have access to a real-time messaging system to communicate with potential project members or teams. 45 | 46 | ## Leading a Project 47 | 48 | * A project creator has the ability to provide one of their user-owned GitHub repositories or create a brand new GitHub repository directly from the project. 49 | * If your project is not beginner friendly you may only create the project in a language you are fluent with. 50 | * Creating or providing a repository means you are the owner of both the project and the repository. 51 | * If a repository is provided at the time a user is accepted, they will automatically be invited to join as a collaborator. Otherwise, you will have to click the "Add Collaborators" button provided once you have created or provided the repository. 52 | * Project members will not be added to the repository until they accept their email invtation from GitHub. 53 | * You have access to other users profiles to view their languages and GitHub profiles as well as directly messaging them ahead of accepting or declining their request. 54 | * Deleting or changing a repository from GitHub will not affect the repository you have listed. If you are to change the name or create a different repository you must unlist your current repository and provide the new one. 55 | 56 | ## GitHub Data 57 | 58 | * The only data fetched and stored from your GitHub account is your profile specific repositories top languages displayed on the repository itself as well as your GitHub name and picture. 59 | * gitTogether can not make any changes to a repository if you are not the owner and do not provide it in a project. 60 | * This data is used to assess your experience in a programming language to determine qualifications for non-beginner friendly projects and allow other developers to easily see your experience as well as ensure a direct connection to GitHub. 61 | 62 | ## Feedback 63 | 64 | To leave feedback about this project or your experience, you can fill out this form: [gitTogether Feedback Form](https://forms.gle/7LTPzNgyff6gVi2T8) 65 | 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gittogether", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.8.2", 7 | "@emotion/styled": "^11.8.1", 8 | "@mui/icons-material": "^5.5.1", 9 | "@mui/material": "^5.5.1", 10 | "@octokit/core": "^3.6.0", 11 | "@supabase/supabase-js": "^1.31.1", 12 | "@testing-library/jest-dom": "^5.16.2", 13 | "@testing-library/react": "^12.1.4", 14 | "@testing-library/user-event": "^13.5.0", 15 | "axios": "^0.26.1", 16 | "node-sass": "^7.0.1", 17 | "react": "^17.0.2", 18 | "react-dom": "^17.0.2", 19 | "react-infinite-scroll-component": "^6.1.0", 20 | "react-redux": "^7.0.1", 21 | "react-router-dom": "^5.2.0", 22 | "react-scripts": "5.0.0", 23 | "react-toastify": "^8.2.0", 24 | "react-transition-group": "^4.4.2", 25 | "redux": "^4.0.1", 26 | "redux-logger": "^3.0.6", 27 | "redux-thunk": "^2.3.0", 28 | "style-components": "^0.1.0", 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 | "seed": "node seed.js" 37 | }, 38 | "eslintConfig": { 39 | "extends": [ 40 | "react-app", 41 | "react-app/jest" 42 | ] 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/assets/Cute-astronaut-floating-with-balloon-cartoon-on-transparent-background-PNG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/Cute-astronaut-floating-with-balloon-cartoon-on-transparent-background-PNG.png -------------------------------------------------------------------------------- /public/assets/add-project-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/add-project-form.png -------------------------------------------------------------------------------- /public/assets/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/chat.png -------------------------------------------------------------------------------- /public/assets/cute-astronaut-floating-space-cartoon-character.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/cute-astronaut-floating-space-cartoon-character.png -------------------------------------------------------------------------------- /public/assets/cute-astronaut-floating-space-no-stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/cute-astronaut-floating-space-no-stars.png -------------------------------------------------------------------------------- /public/assets/gitHub_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/gitHub_world.png -------------------------------------------------------------------------------- /public/assets/gitTogetherLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/gitTogetherLogo.png -------------------------------------------------------------------------------- /public/assets/notifications-dropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/notifications-dropdown.png -------------------------------------------------------------------------------- /public/assets/project_feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/project_feed.png -------------------------------------------------------------------------------- /public/assets/registered-trademark-png-white-png-download-registered-trademark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/registered-trademark-png-white-png-download-registered-trademark.png -------------------------------------------------------------------------------- /public/assets/repo_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/repo_form.png -------------------------------------------------------------------------------- /public/assets/respond-join-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/respond-join-request.png -------------------------------------------------------------------------------- /public/assets/single-project-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/single-project-view.png -------------------------------------------------------------------------------- /public/assets/single_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/single_project.png -------------------------------------------------------------------------------- /public/assets/user_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/assets/user_profile.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 13 | 14 | 15 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 32 | 33 | 37 | 38 | 47 | gitTogether 48 | 49 | 50 | 51 |
52 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/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 | -------------------------------------------------------------------------------- /seed.js: -------------------------------------------------------------------------------- 1 | import supabase from './src/client.js'; 2 | //ONLY RUN THIS SEED FILE IF THE DB IS EMPTY, otherwise we will get a lot of duplicate data 3 | 4 | //to run this seed file, add "type": "module" to the package.json file 5 | //after running, remove the "type":"module" from package.json 6 | 7 | //Generate categories: 8 | // const categories = [ 9 | // { 10 | // name: "Machine Learning", 11 | // }, 12 | // { 13 | // name: "Dev Tools", 14 | // }, 15 | // { 16 | // name: "Collab Tools", 17 | // }, 18 | // { 19 | // name: "Gaming", 20 | // }, 21 | // { 22 | // name: "Data Analytics", 23 | // }, 24 | // ]; 25 | // const seed = async () => { 26 | // const { data, error } = await supabase.from("categories").insert(categories); 27 | // if (error) { 28 | // console.log(error); 29 | // } 30 | // }; 31 | 32 | // Generate 100 random projects to populate the feed, it assigns each project a random owner, category and BeginnerFriendly value 33 | 34 | const seed = async () => { 35 | const projects = []; 36 | 37 | // const ownerIds = await supabase.from("user").select("id"); 38 | const ownerIds = [ 39 | '72b5c3db-d5fd-4f99-93ce-3ccf9a5d8ef5', 40 | '12a51642-ba58-4de0-a0e3-5189c65ade71', 41 | '179b3744-bfd3-49d2-8cfc-851fd52e3559', 42 | '581f4c5b-771a-48a3-897e-4db2deafc343', 43 | ]; 44 | 45 | const { data, error } = await supabase.from('categories').select('*'); 46 | 47 | console.log('seeding projects...'); 48 | for (let i = 1; i <= 100; i++) { 49 | const beginnerFriendly = Math.floor(Math.random() * 2) ? true : false; 50 | 51 | const randomCategory = data[Math.floor(Math.random() * data.length)].id; 52 | const languages = await supabase.from('languages').select('id'); 53 | let randIdx = Math.floor(Math.random() * languages.data.length); 54 | const randLanguage = languages.data[randIdx]; 55 | 56 | const newProject = { 57 | name: `Example Project #${i}`, 58 | description: 59 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Tempus urna et pharetra pharetra massa massa ultricies. Pulvinar sapien et ligula ullamcorper.', 60 | beginnerFriendly, 61 | repoLink: 'https://github.com/gitTogether-capstone/gittogether', 62 | categoryId: randomCategory, 63 | languageId: randLanguage.id, 64 | }; 65 | 66 | projects.push(newProject); 67 | console.log('seeding project ', i); 68 | await supabase.from('projects').insert([newProject]); 69 | } 70 | 71 | const resp = await supabase.from('projects').select('*'); 72 | const allProjects = resp.data; 73 | 74 | for (const project of allProjects) { 75 | const randomOwner = ownerIds[Math.floor(Math.random() * ownerIds.length)]; 76 | 77 | console.log('assigning owner to project ', project.id); 78 | 79 | await supabase.from('projectUser').insert({ 80 | projectId: project.id, 81 | userId: randomOwner, 82 | isOwner: true, 83 | isAccepted: true, 84 | }); 85 | } 86 | 87 | console.log(`seeded ${allProjects.length} projects`); 88 | }; 89 | 90 | seed(); 91 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #0b0c10; 3 | color: #c5c6c7; 4 | font-family: 'Roboto', sans-serif; 5 | } 6 | 7 | * { 8 | box-sizing: border-box; 9 | font-family: 'Roboto', sans-serif; 10 | } 11 | 12 | .App { 13 | text-align: center; 14 | } 15 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import Routes from './Routes'; 5 | import Navbar from './components/navbar/Navbar'; 6 | import supabase from './client'; 7 | import { setUser, signOut } from './store/user'; 8 | import { useHistory } from 'react-router-dom'; 9 | import { Octokit } from '@octokit/core'; 10 | 11 | function App() { 12 | const dispatch = useDispatch(); 13 | const history = useHistory(); 14 | const user = useSelector((state) => state.user); 15 | const [session, setSession] = useState(null); 16 | 17 | useEffect(() => { 18 | let user = supabase.auth.session(); 19 | setSession(user); 20 | }, []); 21 | 22 | useEffect(() => { 23 | checkUser(); 24 | window.addEventListener('hashchange', () => { 25 | checkUser(); 26 | history.push('/'); 27 | }); 28 | }, []); 29 | 30 | const checkUser = async () => { 31 | const user = supabase.auth.user(); 32 | dispatch(setUser(user)); 33 | }; 34 | 35 | return ( 36 |
37 | 38 | 39 |
40 | ); 41 | } 42 | 43 | export default App; 44 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/CreateUser.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import supabase from './client'; 3 | import { Octokit } from '@octokit/core'; 4 | import { setUser } from './store/user'; 5 | 6 | async function CreateUser() { 7 | const user = supabase.auth.user(); 8 | const userSession = supabase.auth.session(); 9 | 10 | if (user) { 11 | //see if user exists in DB yet 12 | let { data, err } = await supabase 13 | .from('user') 14 | .select('*') 15 | .eq('id', user.id); 16 | if (data.length === 0) { 17 | //if user doesn't exist yet, add them 18 | let { data, err } = await supabase.from('user').insert([ 19 | { 20 | id: user.id, 21 | username: user.identities[0]['identity_data'].preferred_username, 22 | imageUrl: user.identities[0]['identity_data'].avatar_url, 23 | }, 24 | ]); 25 | 26 | //octo kit needs to be authorized with users provider token 27 | const octokit = new Octokit({ 28 | auth: userSession.provider_token, 29 | }); 30 | let repoqueries = []; 31 | let page = 1; 32 | //grab first page of repos 33 | let langquery = await octokit.request( 34 | `GET /user/repos?per_page=100&page=${page}`, 35 | { 36 | sort: 'full_name', 37 | } 38 | ); 39 | 40 | //filter nodeids to avoid duplicates github API sends back 41 | repoqueries.push( 42 | ...langquery.data.filter( 43 | (repo) => repo['node_id'].includes('=') === false 44 | ) 45 | ); 46 | page = page + 1; 47 | //while you aren't on the last or only page 48 | if (langquery.headers.link) { 49 | while (langquery.headers.link.includes('next')) { 50 | //request again with incremented page count 51 | langquery = await octokit.request( 52 | `GET /user/repos?per_page=100&page=${page}`, 53 | { 54 | sort: 'full_name', 55 | } 56 | ); 57 | 58 | repoqueries.push( 59 | ...langquery.data.filter( 60 | (repo) => repo['node_id'].includes('=') === false 61 | ) 62 | ); 63 | page = page + 1; 64 | } 65 | } 66 | 67 | let languages = {}; 68 | //loop through repos, store the top language in an object 69 | for (let i = 0; i < repoqueries.length; i++) { 70 | if (languages[repoqueries[i].language]) { 71 | languages[repoqueries[i].language] = 72 | languages[repoqueries[i].language] + 1; 73 | } else { 74 | languages[repoqueries[i].language] = 1; 75 | } 76 | } 77 | let langkeys = Object.keys(languages); 78 | //loop through languages 79 | for (let i = 0; i < langkeys.length; i++) { 80 | //grab all languages in DB 81 | let { data, err } = await supabase.from('languages').select('*'); 82 | 83 | let languages = []; 84 | let langvalues = Object.values(data); 85 | //loop through languages and put name of them in an array 86 | for (let i = 0; i < langvalues.length; i++) { 87 | if (langvalues[i].name !== null) { 88 | languages.push(langvalues[i].name); 89 | } 90 | } 91 | 92 | //if language not in database and isn't null(comes out as a string) 93 | if ( 94 | !languages.includes(langkeys[i]) && 95 | langkeys[i] !== 'null' && 96 | langkeys[i] !== 'HTML' && 97 | langkeys[i] !== 'CSS' 98 | ) { 99 | //insert language into DB 100 | let { data, error } = await supabase 101 | .from('languages') 102 | .insert([{ name: `${langkeys[i]}` }]); 103 | if (error) { 104 | console.log(`LINE 104`, error); 105 | } 106 | 107 | //insert users language into userLanguages 108 | 109 | let { dataa, errr } = await supabase 110 | .from('userLanguages') 111 | .insert([{ languageId: data[0].id, userId: user.id }]); 112 | if (errr) { 113 | console.log(`LINE 113`, errr); 114 | } 115 | //if language exists in DB and isn't null 116 | } else if ( 117 | langkeys[i] !== 'null' && 118 | langkeys[i] !== 'HTML' && 119 | langkeys[i] !== 'CSS' 120 | ) { 121 | //filter current language out of list of languages fetched earlier 122 | let language = data.filter((lang) => lang.name === langkeys[i]); 123 | console.log(`LANGUAGE`, language); 124 | //insert users language into userLanguages 125 | let { newdata, err } = await supabase 126 | .from('userLanguages') 127 | .insert([{ languageId: language[0].id, userId: user.id }]); 128 | if (err) { 129 | console.log(`LINE 129`, err); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | export default CreateUser; 138 | -------------------------------------------------------------------------------- /src/FetchLanguages.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Octokit } from '@octokit/core'; 3 | import supabase from './client'; 4 | async function fetchLanguages() { 5 | const userSession = supabase.auth.session(); 6 | if (userSession.user) { 7 | //grab user from DB 8 | 9 | if (!userSession.provider_token) { 10 | alert('Your session has expired. Please log in again.'); 11 | return; 12 | } 13 | let { data, err } = await supabase 14 | .from('user') 15 | .select('*') 16 | .eq('id', userSession.user.id); 17 | 18 | //octo kit needs to be authorized with users provider token 19 | const octokit = new Octokit({ 20 | auth: userSession.provider_token, 21 | }); 22 | let repoqueries = []; 23 | let page = 1; 24 | //grab first page of repos 25 | let langquery = await octokit.request( 26 | `GET /user/repos?per_page=100&page=${page}`, 27 | { 28 | sort: 'full_name', 29 | } 30 | ); 31 | 32 | //filter nodeids to avoid duplicates github API sends back 33 | repoqueries.push( 34 | ...langquery.data.filter( 35 | (repo) => repo['node_id'].includes('=') === false 36 | ) 37 | ); 38 | page = page + 1; 39 | //while you aren't on the last or only page 40 | if (langquery.headers.link) { 41 | while (langquery.headers.link.includes('next')) { 42 | //request again with incremented page count 43 | langquery = await octokit.request( 44 | `GET /user/repos?per_page=100&page=${page}`, 45 | { 46 | sort: 'full_name', 47 | } 48 | ); 49 | 50 | repoqueries.push( 51 | ...langquery.data.filter( 52 | (repo) => repo['node_id'].includes('=') === false 53 | ) 54 | ); 55 | page = page + 1; 56 | } 57 | } 58 | 59 | let languages = {}; 60 | //loop through repos, store the top language in an object 61 | for (let i = 0; i < repoqueries.length; i++) { 62 | if (languages[repoqueries[i].language]) { 63 | languages[repoqueries[i].language] = 64 | languages[repoqueries[i].language] + 1; 65 | } else { 66 | languages[repoqueries[i].language] = 1; 67 | } 68 | } 69 | let langkeys = Object.keys(languages); 70 | //loop through languages 71 | for (let i = 0; i < langkeys.length; i++) { 72 | //grab all languages in DB 73 | let { data, err } = await supabase.from('languages').select('*'); 74 | 75 | let languages = []; 76 | let langvalues = Object.values(data); 77 | //loop through languages and put name of them in an array 78 | for (let i = 0; i < langvalues.length; i++) { 79 | if (langvalues[i].name !== null) { 80 | languages.push(langvalues[i].name); 81 | } 82 | } 83 | 84 | //if language not in database and isn't null(comes out as a string) 85 | if ( 86 | !languages.includes(langkeys[i]) && 87 | langkeys[i] !== 'null' && 88 | langkeys[i] !== 'HTML' && 89 | langkeys[i] !== 'CSS' 90 | ) { 91 | //insert language into DB 92 | let { data, error } = await supabase 93 | .from('languages') 94 | .insert([{ name: `${langkeys[i]}` }]); 95 | //grab language to get its ID 96 | let userLangs = await supabase 97 | .from('userLanguages') 98 | .select('*') 99 | .eq('userId', userSession.user.id); 100 | let usersLanguages; 101 | // if user has any languages, grab them and see if they already have this language 102 | if (userLangs.data) { 103 | usersLanguages = userLangs.data.reduce((accum, language) => { 104 | accum.push(language.languageId); 105 | return accum; 106 | }, []); 107 | let language = data.filter((lang) => lang.name === langkeys[i]); 108 | if (!usersLanguages.includes(language[0].id)) { 109 | //insert users language into userLanguages 110 | let { dataa, errr } = await supabase 111 | .from('userLanguages') 112 | .insert([ 113 | { languageId: data[0].id, userId: userSession.user.id }, 114 | ]); 115 | } 116 | } 117 | //if language exists in DB and isn't null 118 | } else if ( 119 | langkeys[i] !== 'null' && 120 | langkeys[i] !== 'HTML' && 121 | langkeys[i] !== 'CSS' 122 | ) { 123 | //filter current language out of list of languages fetched earlier 124 | let language = data.filter((lang) => lang.name === langkeys[i]); 125 | 126 | let usersLanguagesInDb = await supabase 127 | .from('userLanguages') 128 | .select('*') 129 | .eq('userId', userSession.user.id); 130 | let usersLanguages; 131 | if (usersLanguagesInDb.data) { 132 | usersLanguages = usersLanguagesInDb.data.reduce((accum, language) => { 133 | accum.push(language.languageId); 134 | return accum; 135 | }, []); 136 | if (!usersLanguages.includes(language[0].id)) { 137 | // insert users language into userLanguages 138 | let { newdata, err } = await supabase 139 | .from('userLanguages') 140 | .insert([ 141 | { languageId: language[0].id, userId: userSession.user.id }, 142 | ]); 143 | } 144 | } 145 | } 146 | } 147 | } 148 | } 149 | 150 | export default fetchLanguages; 151 | -------------------------------------------------------------------------------- /src/Routes.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { Route, Switch, Redirect } from "react-router-dom"; 4 | import SingleProject from "./components/SingleProject/SingleProject"; 5 | import Login from "./components/Login/Login"; 6 | import ProjectFeed from "./components/ProjectFeed/ProjectFeed.js"; 7 | import LandingPage from "./components/LandingPage/LandingPage"; 8 | import UserProfile from "./components/UserProfile/UserProfile"; 9 | import AddProject from "./components/AddProject/AddProject"; 10 | import Chat from "./components/Chat/Chat"; 11 | import supabase from "./client"; 12 | import NotFound from "./components/NotFound/NotFound"; 13 | import AccountSetup from "./components/AccountSetup/AccountSetup"; 14 | import AdminUsers from "./components/Admin/AdminUsers/AdminUsers"; 15 | 16 | function Routes(props) { 17 | const isLoggedIn = supabase.auth.user(); 18 | 19 | return ( 20 |
21 | {/* {isLoggedIn && isBanned ? null : 22 | ( */} 23 | {isLoggedIn ? ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ) : ( 42 | 43 | 44 | 45 | {!isLoggedIn ? : null} 46 | 47 | )} 48 |
49 | ); 50 | } 51 | 52 | export default Routes; 53 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | const { REACT_APP_SUPABASE_PUBLIC_KEY, REACT_APP_SUPABASE_URL } = process.env; 3 | 4 | const supabase = createClient( 5 | REACT_APP_SUPABASE_URL, 6 | REACT_APP_SUPABASE_PUBLIC_KEY 7 | ); 8 | 9 | export default supabase; 10 | -------------------------------------------------------------------------------- /src/components/AccountSetup/AccountSetup.css: -------------------------------------------------------------------------------- 1 | #account-setup { 2 | height: 100%; 3 | width: 1000px; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | color: #66fcf1; 9 | font-size: 30px; 10 | background-color: #1f2833; 11 | padding: 30px; 12 | border-radius: 10px; 13 | } 14 | 15 | #flex-container { 16 | display: flex; 17 | align-items: center; 18 | flex-direction: column; 19 | padding-top: 50px; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/AccountSetup/AccountSetup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './AccountSetup.css'; 3 | 4 | const AccountSetup = () => { 5 | return ( 6 |
7 |
8 |

Hang tight while we fetch the latest information...

9 |

You will be redirected in a few seconds.

10 |
11 |
12 | ); 13 | }; 14 | 15 | export default AccountSetup; 16 | -------------------------------------------------------------------------------- /src/components/AddProject/AddProject.css: -------------------------------------------------------------------------------- 1 | .form-container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | max-height: calc(100vh - 50px); 6 | width: 500px; 7 | margin-left: 50px; 8 | border-radius: 10px; 9 | background-color: #0b0c10; 10 | padding-bottom: 20px; 11 | color: white; 12 | position: relative; 13 | overflow-y: scroll; 14 | overflow-x: visible; 15 | } 16 | 17 | .form-container::-webkit-scrollbar { 18 | background-color: #192029; 19 | } 20 | 21 | .form-container::-webkit-scrollbar-thumb { 22 | background-color: #66fcf1; 23 | } 24 | 25 | .form-field { 26 | height: 50px; 27 | width: 300px; 28 | background-color: #1f2833; 29 | border: none; 30 | border-radius: 10px; 31 | padding: 10px; 32 | color: white; 33 | } 34 | 35 | .form-field::placeholder { 36 | color: rgb(204, 204, 204); 37 | font-style: italic; 38 | } 39 | 40 | .close-button { 41 | position: relative; 42 | width: 20px; 43 | height: 20px; 44 | } 45 | 46 | .form-element { 47 | display: flex; 48 | flex-direction: column; 49 | align-items: flex-start; 50 | justify-content: center; 51 | gap: 5px; 52 | } 53 | 54 | .new-project-form { 55 | display: flex; 56 | flex-direction: column; 57 | gap: 30px; 58 | } 59 | 60 | .form-element select { 61 | align-self: center; 62 | background-color: #1f2833; 63 | border: none; 64 | color: white; 65 | height: 50px; 66 | width: 300px; 67 | border-radius: 10px; 68 | padding: 10px; 69 | } 70 | 71 | #description { 72 | height: 100px; 73 | } 74 | 75 | #post-project { 76 | width: 300px; 77 | height: 50px; 78 | border-radius: 10px; 79 | border: none; 80 | background-color: #45a293; 81 | color: white; 82 | font-size: 18px; 83 | align-self: center; 84 | } 85 | 86 | #post-project:disabled { 87 | background-color: gray; 88 | color: white; 89 | } 90 | 91 | #submit-button:hover { 92 | cursor: pointer; 93 | } 94 | 95 | #beginner-friendly { 96 | background-color: #1f2833; 97 | height: 20px; 98 | width: 20px; 99 | } 100 | 101 | .container input:checked ~ #beginner-friendly { 102 | background-color: #45a293; 103 | } 104 | 105 | .container #beginner-friendly:after { 106 | left: 7px; 107 | top: 3px; 108 | } 109 | 110 | .close-button { 111 | position: absolute; 112 | top: 20px; 113 | right: 30px; 114 | background-color: #0b0c10; 115 | border: none; 116 | color: white; 117 | font-size: 24px; 118 | } 119 | 120 | .close-button:hover { 121 | cursor: pointer; 122 | } 123 | 124 | #form-checkbox { 125 | align-self: center; 126 | } 127 | 128 | #info-icon { 129 | position: absolute; 130 | right: 130px; 131 | } 132 | 133 | #more-info { 134 | background-color: #45a293; 135 | position: absolute; 136 | top: 365px; 137 | border-radius: 10px; 138 | width: 300px; 139 | height: 150px; 140 | padding: 10px; 141 | } 142 | -------------------------------------------------------------------------------- /src/components/AddProject/AddProject.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import "./AddProject.css"; 4 | import supabase from "../../client"; 5 | import { addProjects } from "../../store/projects"; 6 | import { toast } from "react-toastify"; 7 | import HelpIcon from "@mui/icons-material/Help"; 8 | import { Octokit } from "@octokit/core"; 9 | 10 | const AddProject = (props) => { 11 | const dispatch = useDispatch(); 12 | const [newProject, setNewProject] = useState({ 13 | name: "", 14 | description: "", 15 | beginnerFriendly: false, 16 | repoLink: "", 17 | languageId: 0, 18 | categoryId: 0, 19 | }); 20 | const [submitted, setSubmitted] = useState(false); 21 | const user = useSelector((state) => state.user); 22 | const [allLanguages, setAllLanguages] = useState([]); 23 | const [userLanguages, setUserLanguages] = useState([]); 24 | const [categories, setCategories] = useState([]); 25 | const [showTooltip, setShowtooltip] = useState(false); 26 | 27 | useEffect(() => { 28 | fetchLanguages(); 29 | fetchCategories(); 30 | }, []); 31 | 32 | const fetchLanguages = async () => { 33 | const userLanguages = await supabase 34 | .from("userLanguages") 35 | .select( 36 | ` 37 | *, 38 | languages(id, name) 39 | ` 40 | ) 41 | .eq("userId", user.id); 42 | setUserLanguages(userLanguages.data.map((language) => language.languages)); 43 | const languages = await supabase.from("languages").select("*"); 44 | 45 | setAllLanguages(languages.data); 46 | }; 47 | 48 | const fetchCategories = async () => { 49 | const categories = await supabase.from("categories").select("*"); 50 | setCategories(categories.data); 51 | }; 52 | 53 | const handleChange = (e) => { 54 | if (e.target.name === "beginnerFriendly") { 55 | setNewProject((newProject) => ({ 56 | ...newProject, 57 | [e.target.name]: e.target.checked, 58 | })); 59 | } else { 60 | if (e.target.name === "languageId") { 61 | setNewProject((newProject) => ({ 62 | ...newProject, 63 | [e.target.name]: Number(e.target.value), 64 | })); 65 | } else { 66 | setNewProject((newProject) => ({ 67 | ...newProject, 68 | [e.target.name]: e.target.value, 69 | })); 70 | } 71 | } 72 | }; 73 | 74 | const verifyRepo = async (evt) => { 75 | evt.preventDefault(); 76 | if (newProject.repoLink !== "") { 77 | const userSession = supabase.auth.session(); 78 | const octokit = new Octokit({ 79 | auth: userSession.provider_token, 80 | }); 81 | try { 82 | let repository = newProject.repoLink.split("/"); 83 | let reponame = repository[repository.length - 1]; 84 | await octokit.request(`GET /repos/{owner}/{repo}`, { 85 | owner: userSession.user.user_metadata.user_name, 86 | repo: reponame, 87 | }); 88 | handleSubmit(evt); 89 | } catch (err) { 90 | alert("You can not provide a repository you are not the owner of."); 91 | } 92 | } else { 93 | handleSubmit(evt); 94 | } 95 | }; 96 | 97 | const handleSubmit = async (e) => { 98 | const { data, error } = await supabase 99 | .from("projects") 100 | .insert([newProject]); 101 | const projectUser = await supabase.from("projectUser").insert([ 102 | { 103 | projectId: data[0].id, 104 | userId: user.id, 105 | isAccepted: true, 106 | isOwner: true, 107 | }, 108 | ]); 109 | const addedProject = await supabase 110 | .from("projects") 111 | .select( 112 | ` 113 | *, languages (id, name), 114 | categories (id, name), 115 | projectUser(*, user(id, username, imageUrl)) 116 | ` 117 | ) 118 | .eq("id", data[0].id) 119 | .eq("projectUser.isOwner", true); 120 | dispatch(addProjects(addedProject.data)); 121 | if (!error && !projectUser.error && !addedProject.error) { 122 | props.closePopup(false); 123 | toast("Your project was succesfully posted!"); 124 | } 125 | }; 126 | 127 | return ( 128 |
129 | 132 |

New Project

133 | 134 |
139 | {submitted ? ( 140 |
Project Added
141 | ) : null} 142 |
143 | 144 | 152 |
153 |
154 | 155 | 70 |

71 |
72 | ) : ( 73 |

This user has no bio.

74 | )} 75 |
76 |
77 | {isUser ? ( 78 |
79 | {props.editingBio ? ( 80 | 87 | ) : ( 88 | 95 | )} 96 | {props.editingBio ? ( 97 | 103 | ) : null} 104 |
105 | ) : null} 106 |
107 | 108 | 109 | ); 110 | }; 111 | 112 | export default BioModal; 113 | -------------------------------------------------------------------------------- /src/components/UserProfile/FirstMessage/FirstMessage.css: -------------------------------------------------------------------------------- 1 | .popup { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | background-color: rgba(31, 28, 28, 0.7); 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | 13 | .popup-inner { 14 | display: flex; 15 | justify-content: space-between; 16 | align-items: center; 17 | } 18 | .close-button { 19 | position: absolute; 20 | top: 20px; 21 | right: 30px; 22 | background-color: #0b0c10; 23 | border: none; 24 | color: white; 25 | font-size: 24px; 26 | } 27 | .input { 28 | display: flex; 29 | width: 130%; 30 | height: 50px; 31 | flex-direction: column; 32 | 33 | /* align-items: right; */ 34 | justify-content: space-between; 35 | align-items: center; 36 | } 37 | .post-button { 38 | } 39 | -------------------------------------------------------------------------------- /src/components/UserProfile/FirstMessage/FirstMessage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import supabase from "../../../client"; 3 | import { useHistory } from "react-router-dom"; 4 | import { fetchSingleDM } from "../../../store/dmId"; 5 | import { useDispatch, useSelector } from "react-redux"; 6 | import "./FirstMessage.css"; 7 | 8 | function FirstMessage(props) { 9 | const [content, setContent] = useState({ body: "" }); 10 | const [directMessages, setDirectMessages] = useState([]); 11 | const [user, setUser] = useState([]); 12 | const { body } = content; 13 | const currentUser = supabase.auth.user(); 14 | const dispatch = useDispatch(); 15 | const history = useHistory(); 16 | //const user = props.match.params.userId; 17 | 18 | async function createDirectMessages(user) { 19 | const { data, error } = await supabase.from("directMessages").insert([ 20 | { 21 | sender_Id: currentUser.id, 22 | receiver_Id: props.userId, 23 | content: body, 24 | }, 25 | ]); 26 | if (error) console.log("ERROR", error); 27 | setContent({ content: "" }); 28 | dispatch(fetchSingleDM(props.userId)); 29 | history.push("/chat"); 30 | } 31 | 32 | return ( 33 |
34 |
35 |
36 | setContent({ ...content, body: e.target.value })} 42 | /> 43 |
44 |
45 | 48 |
49 |
50 | ); 51 | } 52 | 53 | export default FirstMessage; 54 | -------------------------------------------------------------------------------- /src/components/UserProfile/FirstMessage/MessagePopup.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import UserProfile from "../UserProfile"; 3 | import FirstMessage from "./FirstMessage"; 4 | 5 | function MessagePopup(props) { 6 | return props.trigger ? ( 7 |
8 |
9 | {props.children} 10 | 11 | 17 |
18 |
19 | ) : ( 20 | "" 21 | ); 22 | } 23 | 24 | export default MessagePopup; 25 | -------------------------------------------------------------------------------- /src/components/UserProfile/PictureModal.css: -------------------------------------------------------------------------------- 1 | .picture-modal { 2 | position: fixed; 3 | left: 40%; 4 | background-color: none; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | height: fit-content; 10 | width: fit-content; 11 | border-radius: 8px; 12 | } 13 | 14 | .picture-modal-content { 15 | background-color: none; 16 | height: 100%; 17 | width: 100%; 18 | border-radius: 4px; 19 | } 20 | 21 | .modal-header, 22 | .modal-footer { 23 | padding: 10px; 24 | } 25 | 26 | .modal-title { 27 | margin-top: 1rem; 28 | } 29 | 30 | .modal-body { 31 | padding: 5rem; 32 | border-top: 1px solid rgb(53, 53, 53); 33 | border-bottom: 1px solid rgb(53, 53, 53); 34 | overflow-y: scroll; 35 | } 36 | 37 | .modal-body::-webkit-scrollbar { 38 | display: none; 39 | } 40 | 41 | .button { 42 | position: absolute; 43 | background: rgb(92, 89, 89); 44 | color: white; 45 | top: -10px; 46 | right: -10px; 47 | border-radius: 4px; 48 | } 49 | 50 | #profile-pic { 51 | height: 400px; 52 | width: 500px; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/UserProfile/PictureModal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import './PictureModal.css'; 3 | 4 | const PictureModal = (props) => { 5 | useEffect(() => { 6 | document.addEventListener('keydown', closePic, false); 7 | return function cleanup() { 8 | document.removeEventListener('keydown', closePic, false); 9 | }; 10 | }, []); 11 | 12 | function closePic(e) { 13 | if (e.key === 'Escape') { 14 | props.onClose(); 15 | } 16 | } 17 | 18 | if (!props.showpic.display) { 19 | return null; 20 | } 21 | 22 | return ( 23 |
24 |
e.stopPropagation()} 27 | > 28 | 31 | 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default PictureModal; 38 | -------------------------------------------------------------------------------- /src/components/UserProfile/ProjectModal.css: -------------------------------------------------------------------------------- 1 | .project-modal { 2 | position: fixed; 3 | left: 15%; 4 | background-color: #192029; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | width: 70%; 10 | height: 70%; 11 | border-radius: 8px; 12 | border-style: groove; 13 | border-color: #66fcf1; 14 | } 15 | 16 | .project-modal-content { 17 | background-color: #192029; 18 | height: 100%; 19 | width: 100%; 20 | border-radius: 4px; 21 | } 22 | 23 | .modal-header, 24 | .modal-footer { 25 | padding: 10px; 26 | } 27 | 28 | .modal-title { 29 | color: #66fcf1; 30 | margin-top: 1rem; 31 | font-size: 2rem; 32 | } 33 | 34 | .project-date { 35 | margin-top: 0; 36 | display: flex; 37 | flex-direction: row; 38 | justify-content: space-evenly; 39 | align-items: flex-end; 40 | flex-shrink: 5; 41 | margin-bottom: 0; 42 | } 43 | 44 | .button { 45 | position: absolute; 46 | background: rgb(92, 89, 89); 47 | color: white; 48 | top: -10px; 49 | right: -10px; 50 | border-radius: 4px; 51 | } 52 | 53 | .proj-footer { 54 | color: #45a29e; 55 | } 56 | 57 | .user-links { 58 | text-decoration: none; 59 | color: #66fcf1; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/UserProfile/ProjectModal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import supabase from '../../client'; 3 | import './ProjectModal.css'; 4 | import { NavLink } from 'react-router-dom'; 5 | 6 | const Modal = (props) => { 7 | const [project, setProject] = useState({}); 8 | const currentUser = supabase.auth.user(); 9 | const [projectUsers, setProjectUsers] = useState([]); 10 | 11 | useEffect(() => { 12 | document.addEventListener('keydown', closeProject, false); 13 | return function cleanup() { 14 | document.removeEventListener('keydown', closeProject, false); 15 | }; 16 | }, []); 17 | 18 | function closeProject(e) { 19 | if (e.key === 'Escape') { 20 | props.onClose(); 21 | } 22 | } 23 | 24 | useEffect(() => { 25 | async function fetchProj() { 26 | if (props.show.project) { 27 | let proj = await supabase 28 | .from('projects') 29 | .select('*, user!projectUser(*), projectUser(*)') 30 | .eq('id', props.show.project.id); 31 | setProject(proj.data[0]); 32 | let projUsers = proj.data[0].user.reduce((accum, user) => { 33 | accum.push(user.username); 34 | return accum; 35 | }, []); 36 | setProjectUsers(projUsers); 37 | } 38 | } 39 | fetchProj(); 40 | }, [props.show.project]); 41 | 42 | if (!props.show.display) { 43 | return null; 44 | } 45 | 46 | if (project.user) { 47 | return ( 48 |
49 |
50 | 57 |
58 |
e.stopPropagation()}> 59 |

{project.name}

60 |
61 |
{project.description}
62 |
63 | Collaborators:{' '} 64 | {project.user.map((user, i) => { 65 | if (i !== project.user.length - 1) { 66 | return ( 67 | {`${user.username}, `} 73 | ); 74 | } 75 | return ( 76 | {`${user.username}`} 82 | ); 83 | })} 84 |
85 |
86 |
87 |
88 | Created{' '} 89 | {`${props.show.project.created_at.slice( 90 | 5, 91 | 7 92 | )}/${props.show.project.created_at.slice( 93 | 8, 94 | 10 95 | )}/${props.show.project.created_at.slice(0, 4)}`} 96 |
97 | {projectUsers.includes( 98 | currentUser.identities[0]['identity_data'].preferred_username 99 | ) ? ( 100 | 101 | Project 102 | 103 | ) : null} 104 | 108 | 109 | Repo 110 | 111 |
112 |
113 |
114 | ); 115 | } else { 116 | return null; 117 | } 118 | }; 119 | 120 | export default Modal; 121 | -------------------------------------------------------------------------------- /src/components/UserProfile/UserProfile.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import supabase from '../../client'; 3 | import './style.css'; 4 | import PictureModal from './PictureModal'; 5 | import fetchLanguages from '../../FetchLanguages'; 6 | import BioModal from './BioModal'; 7 | import { toast } from 'react-toastify'; 8 | import { useHistory } from 'react-router-dom'; 9 | import { useSelector } from 'react-redux'; 10 | import { NavLink } from 'react-router-dom'; 11 | import MessagePopup from './FirstMessage/MessagePopup'; 12 | 13 | function UserProfile(props) { 14 | const [user, setUser] = useState({}); 15 | const [editingBio, setEditingBio] = useState(false); 16 | const [userBio, setUserBio] = useState(''); 17 | const [stateError, setStateError] = useState(''); 18 | const [show, setShow] = useState({ display: false, project: null }); 19 | const [showpic, setShowPic] = useState({ display: false, pic: null }); 20 | const [loadingLanguages, setLoadingLanguages] = useState(false); 21 | const [showBio, setShowBio] = useState({ display: false, bio: null }); 22 | const [loading, setLoading] = useState(false); 23 | const [isUser, setIsUser] = useState(false); 24 | const [directMessages, setDirectMessages] = useState([]); 25 | const [current, setCurrent] = useState([]); 26 | const [MessageButtonPopup, setMessageButtonPopup] = useState(false); 27 | const currentUser = supabase.auth.user(); 28 | const history = useHistory(); 29 | 30 | useEffect(() => { 31 | setLoading(true); 32 | let username = props.match.params.user; 33 | async function fetchUser() { 34 | let newuser = await supabase 35 | .from('user') 36 | .select('*, userLanguages(*), languages(*), projects!projectUser(*)') 37 | .ilike('username', username); 38 | 39 | let projs = await supabase 40 | .from('projectUser') 41 | .select('*, projects(*)') 42 | .eq('userId', newuser.data[0].id); 43 | 44 | setUser({ ...newuser.data[0], projects: projs.data }); 45 | setUserBio(newuser.bio); 46 | setLoading(false); 47 | } 48 | fetchUser(); 49 | }, [props.location.pathname]); 50 | 51 | useEffect(() => { 52 | let currentUser = supabase.auth.user(); 53 | if (user.id) { 54 | setIsUser( 55 | currentUser.identities[0]['identity_data'].user_name === 56 | props.match.params.user 57 | ); 58 | } 59 | }, [user]); 60 | 61 | useEffect(() => { 62 | fetchCurrent(); 63 | }, [currentUser]); 64 | 65 | async function handleClick(evt) { 66 | evt.preventDefault(); 67 | setStateError(''); 68 | if (evt.target.id === 'edit-bio') { 69 | setEditingBio(true); 70 | } else if (evt.target.id === 'save-bio') { 71 | let { error } = await supabase 72 | .from('user') 73 | .update({ bio: userBio }) 74 | .eq('id', user.id); 75 | if (error) { 76 | alert('There was a problem updating your bio.'); 77 | return; 78 | } 79 | setUser({ ...user, bio: userBio }); 80 | setShowBio({ display: true, bio: userBio, username: user.username }); 81 | setEditingBio(false); 82 | } 83 | } 84 | 85 | async function updateLanguages(evt) { 86 | evt.preventDefault(); 87 | setLoadingLanguages(true); 88 | await fetchLanguages(); 89 | let username = props.match.params.user; 90 | let newuser = await supabase 91 | .from('user') 92 | .select('*, userLanguages(*), languages(*), projects!projectUser(*)') 93 | .ilike('username', username); 94 | setUser(newuser.data[0]); 95 | setUserBio(newuser.bio); 96 | setLoadingLanguages(false); 97 | } 98 | 99 | async function fetchCurrent() { 100 | if (currentUser) { 101 | const { data } = await supabase 102 | .from('user') 103 | .select('*') 104 | .eq('id', currentUser.id); 105 | setCurrent(data); 106 | } 107 | } 108 | 109 | if (!loading) { 110 | return ( 111 |
{ 114 | if (showpic.display) { 115 | setShowPic({ display: false, pic: null }); 116 | } 117 | }} 118 | > 119 |
120 | setShowPic({ display: true, pic: user.imageUrl })} 122 | alt={'profile-pic'} 123 | id="profile-img" 124 | src={user.imageUrl} 125 | /> 126 | 127 |
128 |

@{user.username}

129 | 136 | 137 |

Github

138 |
139 |
140 | {!loadingLanguages && isUser ? ( 141 | 152 | ) : null} 153 | {isUser ? null : ( 154 |
155 |
156 | 168 | 173 |

Message

174 |
175 |
176 |
177 | )} 178 | 179 | {loadingLanguages ? ( 180 | Loading... 187 | ) : null} 188 |
189 |
190 | 193 |
    194 | {user.id 195 | ? user.languages.map((language, i) => { 196 | return ( 197 |
  1. 198 | {language.name} 199 |
  2. 200 | ); 201 | }) 202 | : null} 203 |
204 |
205 |

206 |
207 | {`User bio`} 208 |

211 | setShowBio({ 212 | display: true, 213 | bio: user.bio, 214 | username: user.username, 215 | }) 216 | } 217 | > 218 | Click to view 219 |

220 |
221 |

222 |
223 | {stateError ?
{stateError}
: null} 224 |
225 |
226 | {user.id 227 | ? user.projects.map((proj, i) => { 228 | let project = proj.projects; 229 | if (proj.isAccepted) { 230 | return ( 231 | 236 |

{project.name}

237 |

{project.description}

238 | 250 |
251 | ); 252 | } else { 253 | return null; 254 | } 255 | }) 256 | : null} 257 |
258 | setShowPic({ display: false, pic: null })} 262 | /> 263 | setShowBio({ display: false, bio: null })} 265 | showBio={showBio} 266 | setUserBio={setUserBio} 267 | setEditingBio={setEditingBio} 268 | editingBio={editingBio} 269 | handleClick={handleClick} 270 | /> 271 |
272 | ); 273 | } else { 274 | return ( 275 |
276 | Loading... 282 |
283 | ); 284 | } 285 | } 286 | 287 | export default UserProfile; 288 | -------------------------------------------------------------------------------- /src/components/UserProfile/style.css: -------------------------------------------------------------------------------- 1 | #user-profile { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: flex-start; 5 | align-items: flex-start; 6 | height: 100%; 7 | width: 100%; 8 | overflow: hidden; 9 | color: #c5c6c7; 10 | animation-name: zoom; 11 | animation-duration: 0.2s; 12 | margin-top: 2rem; 13 | padding: 20px; 14 | } 15 | 16 | #profile-img { 17 | height: 150px; 18 | width: 150px; 19 | object-fit: scale-down; 20 | align-items: center; 21 | justify-content: center; 22 | border-style: groove; 23 | border-color: gray; 24 | border-radius: 50%; 25 | cursor: pointer; 26 | } 27 | 28 | #profile-img:hover { 29 | border-color: white; 30 | } 31 | 32 | #user-img-name { 33 | display: flex; 34 | flex-direction: column; 35 | justify-content: center; 36 | align-items: center; 37 | background-color: #192029; 38 | border-radius: 8px; 39 | } 40 | 41 | #user-bio-languages { 42 | display: flex; 43 | flex-direction: row; 44 | justify-content: center; 45 | align-items: center; 46 | } 47 | 48 | .github-button { 49 | color: gray; 50 | } 51 | 52 | #user-bio { 53 | width: 10rem; 54 | } 55 | 56 | #show-bio { 57 | color: #45a29e; 58 | cursor: pointer; 59 | } 60 | 61 | .fa-github, 62 | .github-link { 63 | color: #45a29e; 64 | text-decoration: none; 65 | } 66 | 67 | #github-link { 68 | text-decoration: none; 69 | } 70 | 71 | #profile-username { 72 | color: #66fcf1; 73 | } 74 | 75 | .fa-github { 76 | font-size: 30px; 77 | } 78 | 79 | #loading-languages { 80 | width: 50px; 81 | height: 50px; 82 | } 83 | 84 | #editing-bio-text { 85 | text-align: start; 86 | height: 150px; 87 | width: 150px; 88 | background-color: #45a29e; 89 | border-style: none; 90 | color: #0b0c10; 91 | font-size: 2rem; 92 | width: 50rem; 93 | height: 20rem; 94 | display: flex; 95 | flex-direction: row; 96 | justify-content: center; 97 | align-items: center; 98 | border-radius: 8px; 99 | } 100 | 101 | textarea { 102 | resize: none; 103 | } 104 | 105 | #save-cancel-buttons { 106 | display: flex; 107 | flex-direction: column; 108 | justify-content: flex-start; 109 | align-items: center; 110 | } 111 | 112 | #user-projects { 113 | display: flex; 114 | flex-direction: row; 115 | flex-wrap: wrap; 116 | height: fit-content; 117 | color: #c5c6c7; 118 | flex-grow: 1; 119 | } 120 | 121 | #user-projects::-webkit-scrollbar, 122 | body::-webkit-scrollbar, 123 | .proj-modal-body::-webkit-scrollbar, 124 | .modal-content::-webkit-scrollbar, #project::-webkit-scrollbar { 125 | background-color: #192029; 126 | } 127 | 128 | #user-projects::-webkit-scrollbar-thumb, 129 | body::-webkit-scrollbar-thumb, 130 | .proj-modal-body::-webkit-scrollbar-thumb, 131 | .modal-content::-webkit-scrollbar-thumb, #project::-webkit-scrollbar-thumb { 132 | background-color: #66fcf1; 133 | } 134 | 135 | .proj-modal-body { 136 | padding: 5rem; 137 | border-top: 1px solid rgb(53, 53, 53); 138 | border-bottom: 1px solid rgb(53, 53, 53); 139 | overflow-y: scroll; 140 | display: flex; 141 | flex-direction: column; 142 | align-items: center; 143 | color: #c5c6c7; 144 | } 145 | 146 | #proj-desc { 147 | margin-bottom: 100px; 148 | line-height: 1.5; 149 | font-weight: 300; 150 | } 151 | 152 | #project { 153 | margin-bottom: 5px; 154 | margin-left: 5px; 155 | margin-right: 5px; 156 | height: 300px; 157 | width: 45%; 158 | background-color: #192029; 159 | border-radius: 4px; 160 | transition: all 0.2s; 161 | display: flex; 162 | flex-direction: column; 163 | justify-content: center; 164 | align-items: center; 165 | color: #c5c6c7; 166 | overflow-y: scroll; 167 | line-height: 1.5; 168 | font-weight: 300; 169 | cursor: pointer; 170 | } 171 | 172 | #loading-user-profile { 173 | width: 50px; 174 | height: 50px; 175 | } 176 | 177 | #project:hover { 178 | box-shadow: 1px 1px 30px 0px rgba(151, 151, 151, 0.2); 179 | } 180 | 181 | #user-languages { 182 | display: flex; 183 | flex-direction: column; 184 | justify-content: center; 185 | align-items: center; 186 | margin-right: 25px; 187 | } 188 | 189 | #label-for-languages { 190 | margin-top: 5px; 191 | font-size: 20px; 192 | font-weight: bold; 193 | } 194 | 195 | #language { 196 | text-align: left; 197 | font-size: 20px; 198 | } 199 | 200 | #user-bio { 201 | margin-top: 75px; 202 | } 203 | 204 | .modal { 205 | width: 80%; 206 | border-radius: 4px; 207 | border: 2px solid Black; 208 | padding: 15px 15px 15px 15px; 209 | margin: 20px 20px 20px 20px; 210 | background: #a4d3ee; 211 | overflow: visible; 212 | box-shadow: 3px 3px 2px #888888; 213 | position: relative; 214 | } 215 | 216 | /* Modal Content (Image) */ 217 | .modal-content { 218 | flex-grow: 1; 219 | width: 100%; 220 | height: 100%; 221 | font-size: 25px; 222 | overflow-y: scroll; 223 | display: flex; 224 | flex-direction: column; 225 | align-items: center; 226 | } 227 | 228 | .picture-modal-content { 229 | flex-grow: 1; 230 | width: 100%; 231 | height: 100%; 232 | font-size: 25px; 233 | } 234 | 235 | /* Caption of Modal Image (Image Text) - Same Width as the Image */ 236 | #caption { 237 | margin: auto; 238 | display: block; 239 | width: 80%; 240 | max-width: 700px; 241 | text-align: center; 242 | color: #ccc; 243 | padding: 10px 0; 244 | height: 150px; 245 | } 246 | 247 | /* Add Animation - Zoom in the Modal */ 248 | .modal-content, 249 | #caption { 250 | animation-name: zoom; 251 | animation-duration: 0.6s; 252 | } 253 | 254 | @keyframes zoom { 255 | from { 256 | transform: scale(0); 257 | } 258 | to { 259 | transform: scale(1); 260 | } 261 | } 262 | 263 | /* The Close Button */ 264 | .close { 265 | position: absolute; 266 | top: 15px; 267 | right: 35px; 268 | color: #f1f1f1; 269 | font-size: 40px; 270 | font-weight: bold; 271 | transition: 0.3s; 272 | } 273 | 274 | .close:hover, 275 | .close:focus { 276 | color: #bbb; 277 | text-decoration: none; 278 | cursor: pointer; 279 | } 280 | 281 | .refresh-icon { 282 | cursor: pointer; 283 | } 284 | 285 | .edit-bio-buttons { 286 | background-color: #45a29e; 287 | border: none; 288 | height: 40px; 289 | border-radius: 5px; 290 | color: white; 291 | font-size: 30px; 292 | max-width: 300px; 293 | width: 100px; 294 | } 295 | 296 | .bio-modal-body { 297 | border-top: 0; 298 | border-bottom: 0; 299 | padding: 0; 300 | margin: 0; 301 | } 302 | 303 | .button { 304 | cursor: pointer; 305 | } 306 | 307 | #create-repo-form { 308 | display: flex; 309 | flex-direction: column; 310 | align-items: center; 311 | } 312 | 313 | .form-labels { 314 | font-size: 3rem; 315 | font-weight: bold; 316 | } 317 | 318 | .repo-form-input { 319 | height: 30px; 320 | width: 20rem; 321 | font-size: 2rem; 322 | color: #1f2833; 323 | margin-bottom: 40px; 324 | background-color: #45a29e; 325 | } 326 | 327 | #repo-privacy-field { 328 | height: 40px; 329 | width: 200px; 330 | font-size: 2rem; 331 | font-weight: bold; 332 | background-color: #45a29e; 333 | margin-top: 10px; 334 | } 335 | 336 | .create-repo-button { 337 | background-color: #45a29e; 338 | border: none; 339 | height: 40px; 340 | border-radius: 5px; 341 | color: white; 342 | font-size: 30px; 343 | max-width: 300px; 344 | margin-bottom: 10px; 345 | cursor: pointer; 346 | margin-top: 3rem; 347 | } 348 | 349 | #create-repo-warning { 350 | font-style: italic; 351 | } 352 | 353 | .post-button { 354 | padding: 30px; 355 | background-color: #45a29e; 356 | border: none; 357 | height: 40px; 358 | border-radius: 5px; 359 | color: white; 360 | font-size: 16px; 361 | max-width: 300px; 362 | margin-bottom: 10px; 363 | align-items: center; 364 | } 365 | 366 | #project-footer { 367 | display: flex; 368 | flex-direction: row; 369 | justify-content: space-evenly; 370 | align-items: center; 371 | flex-grow: 3; 372 | width: 100%; 373 | } 374 | 375 | .project-modal { 376 | position: fixed; 377 | left: 15%; 378 | background-color: #192029; 379 | display: flex; 380 | flex-direction: column; 381 | align-items: center; 382 | justify-content: center; 383 | width: 70%; 384 | height: 70%; 385 | border-radius: 8px; 386 | border-style: groove; 387 | border-color: #66fcf1; 388 | } 389 | 390 | .create-repo-modal { 391 | position: absolute; 392 | left: 15%; 393 | bottom: 10%; 394 | background-color: #192029; 395 | display: flex; 396 | flex-direction: column; 397 | align-items: center; 398 | justify-content: center; 399 | width: 70%; 400 | height: 70%; 401 | border-radius: 8px; 402 | border-style: groove; 403 | border-color: #66fcf1; 404 | } 405 | -------------------------------------------------------------------------------- /src/components/navbar/DropdownMenu/DropdownMenu.css: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | position: absolute; 3 | width: 400px; 4 | top: 68px; 5 | transform: translateX(-45%); 6 | background-color: #1f2833; 7 | border: 1px solid #25303d; 8 | border-radius: 10px; 9 | overflow: hidden; 10 | padding: 0.5rem; 11 | transition: height 0.5s; 12 | color: white; 13 | transition: height 0.5s ease; 14 | } 15 | .notification-item { 16 | display: flex; 17 | flex-direction: row; 18 | align-items: center; 19 | text-align: left; 20 | gap: 10px; 21 | transition: background 0.5s; 22 | padding: 1rem; 23 | border-radius: 10px; 24 | } 25 | 26 | .notification-item:hover { 27 | background-color: #273241; 28 | } 29 | 30 | .notification-item img { 31 | height: 50px; 32 | width: 50px; 33 | border-radius: 50%; 34 | } 35 | 36 | .profile-pic { 37 | height: 100px; 38 | width: 100px; 39 | border-radius: 50%; 40 | } 41 | 42 | .buttons-container { 43 | display: flex; 44 | flex-direction: column; 45 | gap: 3px; 46 | } 47 | 48 | .notification-item button { 49 | height: 30px; 50 | background-color: #45a29e; 51 | color: white; 52 | border: none; 53 | width: 100px; 54 | border-radius: 5px; 55 | } 56 | 57 | .notification-item button:hover { 58 | cursor: pointer; 59 | } 60 | 61 | .response-message { 62 | color: #2fa851; 63 | } 64 | 65 | .menu-primary-enter { 66 | position: absolute; 67 | transform: translateX(-110%); 68 | } 69 | .menu-primary-enter-active { 70 | transform: translateX(0%); 71 | transition: all 0.5s ease; 72 | } 73 | .menu-primary-exit { 74 | position: absolute; 75 | } 76 | .menu-primary-exit-active { 77 | transform: translateX(-110%); 78 | transition: all 0.5s ease; 79 | } 80 | 81 | .menu-secondary-enter { 82 | transform: translateX(110%); 83 | } 84 | .menu-secondary-enter-active { 85 | transform: translateX(0%); 86 | transition: all 0.5s ease; 87 | } 88 | /* .menu-secondary-exit { 89 | position: absolute; 90 | } */ 91 | .menu-secondary-exit-active { 92 | transform: translateX(110%); 93 | transition: all 0.5s ease; 94 | } 95 | 96 | .user-info { 97 | display: flex; 98 | flex-direction: column; 99 | justify-content: center; 100 | align-items: center; 101 | } 102 | 103 | #back-button { 104 | display: block; 105 | position: absolute; 106 | margin: 20px; 107 | } 108 | 109 | .response-button { 110 | border: none; 111 | height: 30px; 112 | margin: 5px; 113 | border-radius: 5px; 114 | width: 100px; 115 | color: white; 116 | } 117 | 118 | .accept { 119 | background-color: #45a29e; 120 | } 121 | 122 | .decline { 123 | background-color: #b94945; 124 | } 125 | 126 | .user-info a { 127 | text-decoration: none; 128 | color: white; 129 | } 130 | 131 | .icon-link:hover { 132 | cursor: pointer; 133 | } 134 | -------------------------------------------------------------------------------- /src/components/navbar/DropdownMenu/DropdownMenu.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import './DropdownMenu.css'; 3 | import supabase from '../../../client'; 4 | import { fetchProjectRequests } from '../../../util'; 5 | import DropdownMenuItem from './DropdownMenuItem'; 6 | import { CSSTransition } from 'react-transition-group'; 7 | import NotificationOptions from './NotificationOptions'; 8 | 9 | const DropdownMenu = (props) => { 10 | const { user } = props; 11 | const [notifications, setNotifications] = useState([]); 12 | const [message, setMessage] = useState('No notifications'); 13 | const [activeMenu, setActiveMenu] = useState('main'); 14 | const [selectedNotification, setSelectedNotification] = useState({}); 15 | const [menuHeight, setMenuHeight] = useState(null); 16 | 17 | const calcHeight = (el) => { 18 | let height = notifications.length * 100 + 100; 19 | setMenuHeight(height); 20 | }; 21 | 22 | useEffect(() => { 23 | const getProjects = async () => { 24 | const requests = await fetchProjectRequests(user.id); 25 | if (requests.length === 0) { 26 | setMessage('No notifications'); 27 | } else { 28 | setMenuHeight(requests.length * 100 + 100); 29 | setNotifications(requests); 30 | } 31 | }; 32 | getProjects(); 33 | }, []); 34 | 35 | return ( 36 |
37 | 44 |
45 |

Notifications

46 | {notifications.length ? ( 47 | notifications.map((notification) => { 48 | return ( 49 | { 52 | setSelectedNotification(notification); 53 | setActiveMenu('secondary'); 54 | }} 55 | key={notification.id} 56 | /> 57 | ); 58 | }) 59 | ) : ( 60 |

61 | {message} 62 |

63 | )} 64 |
65 |
66 | setMenuHeight(300)} 72 | > 73 | setActiveMenu('main')} 76 | allNotifications={notifications} 77 | setNotifications={setNotifications} 78 | /> 79 | 80 |
81 | ); 82 | }; 83 | 84 | export default DropdownMenu; 85 | -------------------------------------------------------------------------------- /src/components/navbar/DropdownMenu/DropdownMenuItem.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './DropdownMenu.css'; 3 | import supabase from '../../../client'; 4 | import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; 5 | 6 | const DropdownMenuItem = ({ notification, handleClick }) => { 7 | const [didAccept, setDidAccept] = useState(false); 8 | 9 | return ( 10 |
11 | 12 |

13 | @{notification.user.username} wants to join{' '} 14 | {notification.projects.name} 15 |

16 | 17 |
18 | ); 19 | }; 20 | 21 | export default DropdownMenuItem; 22 | -------------------------------------------------------------------------------- /src/components/navbar/DropdownMenu/NotificationOptions.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; 3 | import supabase from '../../../client'; 4 | import { Link } from 'react-router-dom'; 5 | import { addCollaborator } from '../../GithubCollab/AddCollaborators'; 6 | 7 | const NotificationOptions = (props) => { 8 | const { notification, handleClick, allNotifications, setNotifications } = 9 | props; 10 | const [didRespond, setDidRespond] = useState(false); 11 | 12 | const handleAccept = async () => { 13 | const { data, error } = await supabase 14 | .from('projectUser') 15 | .update({ isAccepted: true }) 16 | .eq('projectId', notification.projects.id) 17 | .eq('userId', notification.user.id); 18 | const { data: project } = await supabase 19 | .from('projects') 20 | .select('*, user!projectUser(*)') 21 | .eq('id', notification.projects.id); 22 | if (project[0]) { 23 | addCollaborator(notification.user.username, project[0]); 24 | } 25 | if (error) { 26 | console.log(error); 27 | } else { 28 | const newNotifs = allNotifications.filter( 29 | (notif) => notif.id !== notification.id 30 | ); 31 | setNotifications(newNotifs); 32 | setDidRespond(true); 33 | } 34 | }; 35 | 36 | const handleDecline = async () => { 37 | const { data, error } = await supabase 38 | .from('projectUser') 39 | .delete() 40 | .eq('projectId', notification.projects.id) 41 | .eq('userId', notification.user.id); 42 | if (error) { 43 | console.log(error); 44 | } else { 45 | const newNotifs = allNotifications.filter( 46 | (notif) => notif.id !== notification.id 47 | ); 48 | setNotifications(newNotifs); 49 | setDidRespond(true); 50 | } 51 | }; 52 | return ( 53 |
54 | 59 |
60 | 61 | 62 |

@{notification.user.username}

63 | 64 |

wants to join {notification.projects.name}

65 | {didRespond ? ( 66 |

Success!

67 | ) : ( 68 |
69 | 72 | 75 |
76 | )} 77 |
78 |
79 | ); 80 | }; 81 | 82 | export default NotificationOptions; 83 | -------------------------------------------------------------------------------- /src/components/navbar/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { signOut } from '../../store/user'; 4 | import { Link } from 'react-router-dom'; 5 | import './navbar.scss'; 6 | import AddIcon from '@mui/icons-material/Add'; 7 | import Notifications from './Notifications'; 8 | import DropdownMenu from './DropdownMenu/DropdownMenu.js'; 9 | import Popup from '../AddProject/Popup'; 10 | import supabase from '../../client'; 11 | import { ToastContainer, toast } from 'react-toastify'; 12 | import { fetchMyProjects } from '../../util'; 13 | import 'react-toastify/dist/ReactToastify.css'; 14 | import { useHistory } from 'react-router-dom'; 15 | import SearchBox from './SearchBox'; 16 | import SearchDropdown from './SearchDropdown/SearchDropdown'; 17 | import AdminUsers from '../Admin/AdminUsers/AdminUsers'; 18 | 19 | import AdminPopup from '../Admin/AdminAdd/AdminPopup'; 20 | 21 | const Navbar = () => { 22 | const dispatch = useDispatch(); 23 | const user = useSelector((state) => state.user); 24 | const [projectIds, setProjectIds] = useState([]); 25 | const [buttonPopup, setButtonPopup] = useState(false); 26 | const [AdminbuttonPopup, setAdminButtonPopup] = useState(false); 27 | const currentUser = supabase.auth.user(); 28 | const [current, setCurrent] = useState([]); 29 | const [openNotifications, setOpenNotifications] = useState(false); 30 | const [openSearch, setOpenSearch] = useState(false); 31 | const history = useHistory(); 32 | 33 | const logout = () => { 34 | dispatch(signOut()); 35 | history.push('/'); 36 | }; 37 | 38 | useEffect(() => { 39 | fetchCurrent(); 40 | }, [currentUser]); 41 | 42 | useEffect(() => { 43 | if (!!user && user.id) { 44 | const getAllProjects = async () => { 45 | const myProjects = await fetchMyProjects(user.id); 46 | setProjectIds(myProjects); 47 | }; 48 | getAllProjects(); 49 | } 50 | }, [user]); 51 | 52 | useEffect(() => { 53 | const handleInserts = (payload) => { 54 | const callback = async () => { 55 | if (projectIds.includes(payload.new.projectId)) { 56 | const { data, error } = await supabase 57 | .from('user') 58 | .select('id, username') 59 | .eq('id', payload.new.userId); 60 | if (error) console.log(error); 61 | toast(`@${data[0].username} wants to join your project`); 62 | } 63 | }; 64 | callback(); 65 | }; 66 | 67 | const handleUpdates = (payload) => { 68 | const callback = async () => { 69 | const { data } = await supabase 70 | .from('projects') 71 | .select('id, name') 72 | .eq('id', payload.new.projectId); 73 | toast(`Your request to join ${data[0].name} has been accepted!`); 74 | }; 75 | callback(); 76 | }; 77 | 78 | if (!!user && user.id) { 79 | const projectUser = supabase 80 | .from('projectUser') 81 | .on('INSERT', handleInserts) 82 | .subscribe(); 83 | const projectUserUpdates = supabase 84 | .from(`projectUser:userId=eq.${user.id}`) 85 | .on('UPDATE', handleUpdates) 86 | .subscribe(); 87 | } 88 | }, [projectIds]); 89 | 90 | async function fetchCurrent() { 91 | if (currentUser) { 92 | const { data, error } = await supabase 93 | .from('user') 94 | .select('*') 95 | .eq('id', currentUser.id); 96 | if (error && error?.message === 'JWSError JWSInvalidSignature') { 97 | supabase.auth.signOut(); 98 | history.push('/login'); 99 | return; 100 | } 101 | setCurrent(data); 102 | } 103 | } 104 | 105 | return ( 106 |
107 |
108 | 109 | 110 | gitTogether 111 | 112 | 130 | {!current.isAdmin ? null : ( 131 |
132 |
133 | 136 | 140 |

Add Language

141 |
142 |
143 | Ban Users 144 |
145 | )} 146 |
147 | {user?.id ? ( 148 |
149 |
150 | 151 | Chat 152 | 153 |
154 |
155 | setButtonPopup(true)} className="icon" /> 156 | 157 |
158 |
159 | 164 | 165 | 166 |
167 |
168 | 173 | 174 | 175 |
176 |
177 | 178 | profile 183 | {' '} 184 |
185 | {current.length === 0 ? null : !current[0].isAdmin ? null : ( 186 |
187 | 190 | 194 |

Add Category

195 |
196 |
197 | )} 198 |
199 | 202 |
203 |
204 | ) : ( 205 | 206 | 207 | 208 | )} 209 |
210 | ); 211 | }; 212 | 213 | export default Navbar; 214 | -------------------------------------------------------------------------------- /src/components/navbar/Notifications.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import NotificationsIcon from '@mui/icons-material/NotificationsOutlined'; 3 | 4 | const Notifications = (props) => { 5 | const { open, openNotifications, openSearch } = props; 6 | const handleClick = () => { 7 | openNotifications(!open); 8 | openSearch(false); 9 | }; 10 | 11 | return ( 12 |
13 | 14 | 15 | 16 | {open && props.children} 17 |
18 | ); 19 | }; 20 | 21 | export default Notifications; 22 | -------------------------------------------------------------------------------- /src/components/navbar/SearchBox.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import SearchIcon from '@mui/icons-material/Search'; 3 | 4 | const SearchBox = (props) => { 5 | const { open, openSearch, openNotifications } = props; 6 | 7 | const handleClick = () => { 8 | openSearch(!open); 9 | openNotifications(false); 10 | }; 11 | 12 | return ( 13 |
14 | 15 | {open && props.children} 16 |
17 | ); 18 | }; 19 | 20 | export default SearchBox; 21 | -------------------------------------------------------------------------------- /src/components/navbar/SearchDropdown/SearchDropdown.css: -------------------------------------------------------------------------------- 1 | .search-box { 2 | width: 300px; 3 | background-color: #1f2833; 4 | border: 1px solid #25303d; 5 | position: fixed; 6 | border-radius: 10px; 7 | overflow: hidden; 8 | right: 120px; 9 | top: 68px; 10 | padding: 20px; 11 | } 12 | 13 | .search-box input { 14 | height: 50px; 15 | width: 200px; 16 | background-color: #25303d; 17 | border-radius: 10px; 18 | border: none; 19 | padding: 10px; 20 | color: white; 21 | margin-bottom: 20px; 22 | } 23 | 24 | .user-image { 25 | height: 50px; 26 | width: 50px; 27 | border-radius: 50%; 28 | } 29 | 30 | .result { 31 | display: flex; 32 | flex-direction: row; 33 | align-items: center; 34 | gap: 20px; 35 | padding: 10px; 36 | border-radius: 10px; 37 | } 38 | 39 | .result:hover { 40 | background-color: #273241; 41 | } 42 | .search-results { 43 | display: flex; 44 | flex-direction: column; 45 | padding: 5px; 46 | } 47 | 48 | #search-button { 49 | border: none; 50 | background-color: #45a29e; 51 | color: white; 52 | height: 50px; 53 | border-radius: 10px; 54 | width: 50px; 55 | font-size: 16px; 56 | } 57 | -------------------------------------------------------------------------------- /src/components/navbar/SearchDropdown/SearchDropdown.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './SearchDropdown.css'; 3 | import supabase from '../../../client'; 4 | import { Link } from 'react-router-dom'; 5 | 6 | const SearchDropdown = () => { 7 | const [searchTerm, setSearchTerm] = useState(''); 8 | const [results, setResults] = useState([]); 9 | const [message, setMessage] = useState(''); 10 | const handleChange = (e) => { 11 | setMessage(''); 12 | setSearchTerm(e.target.value); 13 | }; 14 | 15 | const handleSubmit = async (e) => { 16 | e.preventDefault(); 17 | const { data, error } = await supabase.rpc('search_all_users', { 18 | search_term: searchTerm, 19 | }); 20 | if (!error) { 21 | if (data.length === 0) { 22 | setMessage('No users found'); 23 | } else { 24 | setResults(data); 25 | } 26 | setSearchTerm(''); 27 | } 28 | }; 29 | 30 | return ( 31 |
32 | 33 | 38 | 41 | 42 |
43 | {results.length 44 | ? results.map((user) => { 45 | return ( 46 |
47 | 48 | 49 |

@{user.username}

50 | 51 |
52 | ); 53 | }) 54 | : null} 55 |
56 |

57 | {message} 58 |

59 |
60 | ); 61 | }; 62 | 63 | export default SearchDropdown; 64 | -------------------------------------------------------------------------------- /src/components/navbar/navbar.scss: -------------------------------------------------------------------------------- 1 | .navBar { 2 | background-color: #1f2833; 3 | color: #66fcf1; 4 | width: 100%; 5 | height: 70px; 6 | position: sticky; 7 | top: 0; 8 | z-index: 50; 9 | padding: 10px 30px; 10 | display: flex; 11 | flex-direction: row; 12 | align-items: center; 13 | justify-content: space-between; 14 | 15 | .leftNav { 16 | display: flex; 17 | flex-direction: row; 18 | align-items: center; 19 | } 20 | 21 | .rightNav { 22 | display: flex; 23 | justify-content: flex-end; 24 | flex-direction: row; 25 | align-items: center; 26 | gap: 25px; 27 | } 28 | 29 | .logo { 30 | font-family: 'Raleway', sans-serif; 31 | display: flex; 32 | font-size: 30px; 33 | font-weight: 800; 34 | text-decoration: none; 35 | color: inherit; 36 | margin-right: 40px; 37 | align-items: center; 38 | 39 | img { 40 | height: 50px; 41 | width: 50px; 42 | margin-right: 10px; 43 | } 44 | } 45 | 46 | .icon { 47 | font-size: 25px; 48 | margin-right: 10px; 49 | color: #66fcf1; 50 | } 51 | 52 | .icon:hover { 53 | color: white; 54 | cursor: pointer; 55 | } 56 | 57 | span { 58 | font-size: 18px; 59 | font-weight: 900; 60 | text-decoration: none; 61 | } 62 | 63 | .messages-link { 64 | text-decoration: none; 65 | color: #66fcf1; 66 | } 67 | 68 | .messages-link:hover { 69 | color: white; 70 | } 71 | 72 | .img-div { 73 | width: 20%; 74 | } 75 | 76 | .profilePic { 77 | height: 50px; 78 | width: 50px; 79 | border-radius: 50%; 80 | } 81 | 82 | .profilePic:hover { 83 | border: 1px solid; 84 | border-color: #66fcf1; 85 | } 86 | 87 | .button-div { 88 | padding-left: 40px; 89 | } 90 | 91 | .logoutButton { 92 | width: 70px; 93 | } 94 | 95 | .logButton { 96 | background-color: #1f2833; 97 | color: #66fcf1; 98 | padding: 0.3em 1.2em; 99 | margin: 0 0.3em 0.3em 0; 100 | border-radius: 2em; 101 | border: 2px solid; 102 | border-color: #66fcf1; 103 | box-sizing: border-box; 104 | text-decoration: none; 105 | text-align: center; 106 | font-family: "Roboto", sans-serif; 107 | font-weight: 900; 108 | } 109 | 110 | .logButton:hover { 111 | color: #1f2833; 112 | background-color: #66fcf1; 113 | border-color: #1f2833; 114 | } 115 | 116 | .icon-link { 117 | text-decoration: none; 118 | color: white; 119 | } 120 | .Toastify__toast-container--top-right { 121 | top: 5em; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | import { Provider } from "react-redux"; 7 | import store from "./store"; 8 | import { BrowserRouter as Router } from "react-router-dom"; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById("root") 19 | ); 20 | 21 | // If you want to start measuring performance in your app, pass a function 22 | // to log results (for example: reportWebVitals(console.log)) 23 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 24 | reportWebVitals(); 25 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/store/comments.js: -------------------------------------------------------------------------------- 1 | import supabase from '../client'; 2 | 3 | const GET_COMMENTS = 'GET_COMMENTS'; 4 | 5 | export const getComments = (comments) => { 6 | return { 7 | type: GET_COMMENTS, 8 | comments, 9 | }; 10 | }; 11 | 12 | export const fetchComments = (projectId) => { 13 | return async (dispatch) => { 14 | let { data: comments, error } = await supabase 15 | .from('comments') 16 | .select('*') 17 | .eq('projectId', projectId); 18 | dispatch(getComments(comments)); 19 | }; 20 | }; 21 | 22 | const initialState = {}; 23 | 24 | export default (state = initialState, action) => { 25 | switch (action.type) { 26 | case GET_COMMENTS: 27 | return action.comments; 28 | default: 29 | return state; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/store/conversations.js: -------------------------------------------------------------------------------- 1 | import supabase from "../client"; 2 | const SET_CONVERSATIONS = "SET_CONVERSATIONS"; 3 | 4 | 5 | export const setConversations = (conversations) => { 6 | return { 7 | type: SET_CONVERSATIONS, 8 | conversations, 9 | }; 10 | }; 11 | 12 | export const fetchConversations = (userId) => { 13 | return async (dispatch) => { 14 | let { data: conversations, error } = await supabase 15 | .from("conversation_member") 16 | .select(` 17 | *, 18 | conversation ( 19 | * 20 | ) 21 | `) 22 | .eq("user_id", `${userId}`) 23 | if (error) { 24 | console.log(error); 25 | } else { 26 | dispatch(setConversations(conversations)); 27 | } 28 | }; 29 | }; 30 | 31 | export default (state = [], action) => { 32 | switch (action.type) { 33 | case SET_CONVERSATIONS: 34 | return action.conversations; 35 | default: 36 | return state; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/store/convoId.js: -------------------------------------------------------------------------------- 1 | const SET_SINGLE_CONVO = "SET_SINGLE_CONVO"; 2 | 3 | export const setSingleConvo = (convoId) => { 4 | return { 5 | type: SET_SINGLE_CONVO, 6 | convoId, 7 | }; 8 | }; 9 | 10 | export const fetchSingleConvo = (convoId) => { 11 | return (dispatch) => dispatch(setSingleConvo(convoId)); 12 | } 13 | 14 | export default (state = [], action) => { 15 | switch (action.type) { 16 | case SET_SINGLE_CONVO: 17 | return action.convoId; 18 | default: 19 | return state; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/store/dmContent.js: -------------------------------------------------------------------------------- 1 | import supabase from '../client'; 2 | 3 | const SET_DM = 'SET_DM'; 4 | const ADD_DM = 'ADD_DM'; 5 | 6 | export const setDirectMessages = (directMessages) => { 7 | return { 8 | type: SET_DM, 9 | directMessages, 10 | }; 11 | }; 12 | 13 | export const _addDM = (directMessage) => { 14 | return { 15 | type: ADD_DM, 16 | directMessage, 17 | }; 18 | }; 19 | 20 | export const fetchDMContent = (currentUserId, otherUserId) => { 21 | return async (dispatch) => { 22 | const { data: directMessages, error } = await supabase 23 | .from('directMessages') 24 | .select( 25 | `*, 26 | sender:user!directMessages_sender_Id_fkey(id, username, imageUrl), 27 | receiver: user!directMessages_receiver_Id_fkey(id, username, imageUrl) 28 | ` 29 | ) 30 | .or(`receiver_Id.eq.${currentUserId}, receiver_Id.eq.${otherUserId}`) 31 | .or(`sender_Id.eq.${currentUserId}, sender_Id.eq.${otherUserId}`); 32 | if (error) { 33 | console.log(error); 34 | } else { 35 | dispatch(setDirectMessages(directMessages)); 36 | } 37 | }; 38 | }; 39 | 40 | // sender_Id.eq.(${currentUserId},${otherUserId}) 41 | 42 | // export const fetchDMContent = (currentUserId) => { 43 | // return async (dispatch) => { 44 | // const { data: directMessages, error } = await supabase 45 | // .from("directMessages") 46 | // .select( 47 | // ` 48 | // *, 49 | // sender:user!directMessages_sender_Id_fkey(id,username,imageUrl), 50 | // receiver:user!directMessages_receiver_Id_fkey(id,username,imageUrl) 51 | // ` 52 | // ) 53 | // .in("sender_Id", [currentUserId]) 54 | // .in("receiver_Id", [currentUserId]); 55 | // if (error) { 56 | // console.log(error); 57 | // } else { 58 | // dispatch(setDirectMessages(directMessages)); 59 | // } 60 | // }; 61 | // }; 62 | 63 | export const addDM = (directMessage) => { 64 | return async (dispatch) => { 65 | dispatch(_addDM(directMessage)); 66 | }; 67 | }; 68 | 69 | export default (state = [], action) => { 70 | switch (action.type) { 71 | case SET_DM: 72 | return action.directMessages; 73 | case ADD_DM: 74 | return [...state, action.directMessage]; 75 | default: 76 | return state; 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /src/store/dmId.js: -------------------------------------------------------------------------------- 1 | const SET_SINGLE_DM = "SET_SINGLE_DM"; 2 | 3 | export const setSingleDM = (userId) => { 4 | return { 5 | type: SET_SINGLE_DM, 6 | userId, 7 | }; 8 | }; 9 | 10 | export const fetchSingleDM = (userId) => { 11 | return (dispatch) => dispatch(setSingleDM(userId)); 12 | } 13 | 14 | export default (state = [], action) => { 15 | switch (action.type) { 16 | case SET_SINGLE_DM: 17 | return action.userId; 18 | default: 19 | return state; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/store/dmUsers.js: -------------------------------------------------------------------------------- 1 | import supabase from "../client"; 2 | // import { fetchUserDMs } from "../util"; 3 | 4 | const SET_DM_USERS = "SET_DM_USERS"; 5 | 6 | export const setDMUsers = (dmUsers) => { 7 | return { 8 | type: SET_DM_USERS, 9 | dmUsers, 10 | }; 11 | }; 12 | 13 | // export const fetchDirectMessages = (currentUserId) => { 14 | // return (dispatch) => { 15 | // let directMessages = fetchUserDMs(currentUserId); 16 | // dispatch(setDirectMessages(directMessages)); 17 | // }; 18 | // }; 19 | 20 | export const fetchDMUsers = (currentUserId) => { 21 | return async (dispatch) => { 22 | const { data } = await supabase 23 | .from("directMessages") 24 | .select( 25 | `*, 26 | sender: user!directMessages_sender_Id_fkey(id, username, imageUrl), 27 | receiver: user!directMessages_receiver_Id_fkey(id, username, imageUrl) 28 | ` 29 | ) 30 | .or(`receiver_Id.eq.${currentUserId}, sender_Id.eq.${currentUserId}`); 31 | 32 | let seen = {}; 33 | let users = []; 34 | 35 | for (const element of data) { 36 | if (element.receiver.id !== currentUserId && !seen[element.receiver.id]) { 37 | users.push(element.receiver); 38 | seen[element.receiver.id] = true; 39 | } 40 | if (element.sender.id !== currentUserId && !seen[element.sender.id]) { 41 | users.push(element.sender); 42 | seen[element.sender.id] = true; 43 | } 44 | } 45 | dispatch(setDMUsers(users)); 46 | }; 47 | }; 48 | 49 | export default (state = [], action) => { 50 | switch (action.type) { 51 | case SET_DM_USERS: 52 | return action.dmUsers; 53 | default: 54 | return state; 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/store/hasMore.js: -------------------------------------------------------------------------------- 1 | import { END_PROJECTS, SET_PROJECTS } from './projects'; 2 | 3 | const initState = true; 4 | 5 | export default (state = initState, action) => { 6 | switch (action.type) { 7 | case END_PROJECTS: 8 | return false; 9 | case SET_PROJECTS: 10 | return true; 11 | default: 12 | return state; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import { createLogger } from 'redux-logger'; 4 | import user from './user'; 5 | import projects from './projects'; 6 | import project from './project'; 7 | import comments from './comments'; 8 | import hasMore from './hasMore'; 9 | import messages from './messages'; 10 | import conversations from './conversations'; 11 | import convoId from './convoId'; 12 | import dmUsers from './dmUsers'; 13 | import dmContent from './dmContent'; 14 | import dmId from './dmId'; 15 | 16 | 17 | const reducer = combineReducers({ 18 | user, 19 | projects, 20 | project, 21 | comments, 22 | hasMore, 23 | messages, 24 | conversations, 25 | convoId, 26 | dmUsers, 27 | dmContent, 28 | dmId, 29 | }); 30 | 31 | const middleware = applyMiddleware( 32 | thunkMiddleware, 33 | createLogger({ collapsed: true }) 34 | ); 35 | 36 | const store = createStore(reducer, middleware); 37 | 38 | export default store; 39 | -------------------------------------------------------------------------------- /src/store/messages.js: -------------------------------------------------------------------------------- 1 | import supabase from "../client"; 2 | const SET_MESSAGES = "SET_MESSAGES"; 3 | const ADD_MESSAGE = "ADD_MESSAGE"; 4 | 5 | export const setMessages = (messages) => { 6 | return { 7 | type: SET_MESSAGES, 8 | messages, 9 | }; 10 | }; 11 | 12 | export const _addMessage = (message) => { 13 | return { 14 | type: ADD_MESSAGE, 15 | message, 16 | }; 17 | }; 18 | 19 | export const fetchMessages = (convoId) => { 20 | return async (dispatch) => { 21 | let { data: messages, error } = await supabase 22 | .from("messages") 23 | .select(` 24 | *, 25 | user ( 26 | id, imageUrl 27 | ) 28 | `) 29 | .eq("conversation_id", `${convoId}`) 30 | if (error) { 31 | console.log(error); 32 | } else { 33 | dispatch(setMessages(messages)); 34 | } 35 | }; 36 | }; 37 | 38 | export const addMessage = (message) => { 39 | return async (dispatch) => { 40 | dispatch(_addMessage(message)); 41 | } 42 | } 43 | 44 | export default (state = [], action) => { 45 | switch (action.type) { 46 | case SET_MESSAGES: 47 | return action.messages; 48 | case ADD_MESSAGE: 49 | return [...state, action.message]; 50 | default: 51 | return state; 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/store/project.js: -------------------------------------------------------------------------------- 1 | import supabase from '../client'; 2 | const GET_PROJECT = 'GET_PROJECT'; 3 | const UPDATE_REPO = 'UPDATE_REPO'; 4 | 5 | export const getProject = (project) => { 6 | return { 7 | type: GET_PROJECT, 8 | project, 9 | }; 10 | }; 11 | 12 | export const updateRepo = (repo) => { 13 | return { 14 | type: UPDATE_REPO, 15 | repo, 16 | }; 17 | }; 18 | 19 | export const fetchProject = (id) => { 20 | return async (dispatch) => { 21 | let { data: project, error } = await supabase 22 | .from('projects') 23 | .select( 24 | `*, 25 | languages (id, name), 26 | categories (id, name), 27 | projectUser(*, user(id, username, imageUrl)) 28 | ` 29 | ) 30 | .eq('id', id) 31 | .eq('projectUser.isOwner', true) 32 | .single(); 33 | dispatch(getProject(project)); 34 | }; 35 | }; 36 | 37 | const initialState = {}; 38 | 39 | export default (state = initialState, action) => { 40 | switch (action.type) { 41 | case GET_PROJECT: 42 | return action.project; 43 | case UPDATE_REPO: 44 | return { ...state, repoLink: action.repo }; 45 | default: 46 | return state; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/store/projects.js: -------------------------------------------------------------------------------- 1 | import supabase from '../client'; 2 | export const SET_PROJECTS = 'SET_PROJECTS'; 3 | const ADD_PROJECTS = 'ADD_PROJECTS'; 4 | export const END_PROJECTS = 'END_PROJECTS'; 5 | 6 | export const setProjects = (projects) => ({ type: SET_PROJECTS, projects }); 7 | export const addProjects = (projects) => ({ type: ADD_PROJECTS, projects }); 8 | const endProjects = () => ({ type: END_PROJECTS }); 9 | 10 | export const fetchProjects = (filters, categories, languages, page, type) => { 11 | return async (dispatch) => { 12 | categories = categories.map((category) => category.id); 13 | languages = languages.map((language) => language.id); 14 | const startingRange = 20 * page; 15 | let { data: projects, error } = await supabase 16 | .from('projects') 17 | .select( 18 | ` 19 | *, 20 | languages (id, name), 21 | categories (id, name), 22 | projectUser(*, user(id, username, imageUrl)) 23 | ` 24 | ) 25 | .eq('projectUser.isOwner', true) 26 | .in( 27 | 'categoryId', 28 | filters.category === 'all' ? categories : [filters.category] 29 | ) 30 | .in( 31 | 'languageId', 32 | filters.languages.length ? filters.languages : languages 33 | ) 34 | .in('beginnerFriendly', filters.beginnerFriendly ? [true] : [true, false]) 35 | .range(startingRange, startingRange + 19); 36 | 37 | if (error) { 38 | console.log(error); 39 | } 40 | if (projects.length === 0) { 41 | dispatch(endProjects()); 42 | } else { 43 | if (type === 'initial') { 44 | dispatch(setProjects(projects)); 45 | } else if (type === 'more') { 46 | dispatch(addProjects(projects)); 47 | } 48 | } 49 | }; 50 | }; 51 | 52 | const initState = []; 53 | 54 | export default (state = initState, action) => { 55 | switch (action.type) { 56 | case SET_PROJECTS: 57 | return action.projects; 58 | case ADD_PROJECTS: 59 | return [...state, ...action.projects]; 60 | default: 61 | return state; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/store/user.js: -------------------------------------------------------------------------------- 1 | //import axios from "axios"; 2 | import supabase from '../client'; 3 | 4 | const SET_USER = 'SET_USER'; 5 | 6 | export const setUser = (user) => { 7 | return { 8 | type: SET_USER, 9 | user, 10 | }; 11 | }; 12 | 13 | export const login = () => { 14 | return async (dispatch) => { 15 | const { user, isAdmin, session, error } = await supabase.auth.signIn( 16 | { 17 | provider: 'github', 18 | }, 19 | { 20 | scopes: 'repo notifications', 21 | } 22 | ); 23 | 24 | dispatch(setUser(user)); 25 | }; 26 | }; 27 | 28 | export const signOut = () => { 29 | return async (dispatch) => { 30 | const { user, session, error } = await supabase.auth.signOut(); 31 | dispatch(setUser({})); 32 | }; 33 | }; 34 | 35 | export default function (state = {}, action) { 36 | switch (action.type) { 37 | case SET_USER: { 38 | return action.user; 39 | } 40 | default: 41 | return state; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import supabase from './client'; 2 | 3 | //check if current user has the language required for the project 4 | 5 | export const compareLanguages = (user, project) => { 6 | if (user.length) { 7 | const userLanguages = user[0].languages.map((language) => language.id); 8 | return !userLanguages.includes(project.languages.id); 9 | } 10 | return false; 11 | }; 12 | 13 | //fetch all projects belonging to a user 14 | //returns an array of preoject IDs where the owner is the userId passed in 15 | export const fetchMyProjects = async (userId) => { 16 | const { data, error } = await supabase 17 | .from('projectUser') 18 | .select( 19 | ` 20 | * 21 | ` 22 | ) 23 | .eq('userId', userId) 24 | .eq('isOwner', true); 25 | 26 | return data.map((item) => item.projectId); 27 | }; 28 | 29 | export const fetchProjectRequests = async (userId) => { 30 | const projectIds = await fetchMyProjects(userId); 31 | const { data, error } = await supabase 32 | .from('projectUser') 33 | .select( 34 | ` 35 | *, 36 | user(id, username, imageUrl), 37 | projects(id, name) 38 | ` 39 | ) 40 | .in('projectId', projectIds) 41 | .eq('isAccepted', false); 42 | return data; 43 | }; 44 | 45 | export const fetchUserDMs = async (currentUserId) => { 46 | const { data } = await supabase 47 | .from('directMessages') 48 | .select( 49 | ` 50 | sender:user!directMessages_sender_Id_fkey(id, username, imageUrl), 51 | receiver: user!directMessages_receiver_Id_fkey(id, username, imageUrl) 52 | ` 53 | ) 54 | .or(`receiver_Id.eq.${currentUserId}, sender_Id.eq.${currentUserId}`); 55 | 56 | let seen = {}; 57 | let users = []; 58 | 59 | for (const element of data) { 60 | if (element.receiver.id !== currentUserId && !seen[element.receiver.id]) { 61 | users.push(element.receiver); 62 | seen[element.receiver.id] = true; 63 | } 64 | if (element.sender.id !== currentUserId && !seen[element.sender.id]) { 65 | users.push(element.sender); 66 | seen[element.sender.id] = true; 67 | } 68 | } 69 | return users; 70 | }; 71 | --------------------------------------------------------------------------------