├── .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 | 
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 | 
20 |
21 |
22 | View a specific projects page providing you the options to request to join or leave a comment:
23 |
24 | 
25 |
26 | Create a new project:
27 |
28 | 
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 | You need to enable JavaScript to run this app.
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 |
props.closePopup(false)}>
130 | X
131 |
132 |
New Project
133 |
134 |
234 |
235 | );
236 | };
237 |
238 | export default AddProject;
239 |
--------------------------------------------------------------------------------
/src/components/AddProject/Popup.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 | overflow: auto;
12 | }
13 |
14 | .popup-inner {
15 | position: relative;
16 | padding: 50px;
17 | width: 100%;
18 | max-width: 640px;
19 | background-color: #45a293;
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/AddProject/Popup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AddProject from './AddProject';
3 | import './Popup.css';
4 |
5 | function Popup(props) {
6 | return props.trigger ? (
7 |
8 | {props.children}
9 |
10 |
11 | ) : (
12 | ''
13 | );
14 | }
15 |
16 | export default Popup;
17 |
--------------------------------------------------------------------------------
/src/components/AddProject/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/src/components/AddProject/index.js
--------------------------------------------------------------------------------
/src/components/Admin/AdminAdd/AdminAddCategory.css:
--------------------------------------------------------------------------------
1 | #submit-button {
2 | padding: 15px;
3 | background-color: white;
4 | border: none;
5 | height: 2px;
6 | border-radius: 5px;
7 | color: #45a29e;
8 | font-size: 10px;
9 | max-width: 100px;
10 | margin-bottom: 20px;
11 | align-items: center;
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/Admin/AdminAdd/AdminAddLanguage.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import supabase from "../../../client";
3 | import { toast } from "react-toastify";
4 | import "./AdminAddCategory.css";
5 |
6 | const AdminAddLanguages = (props) => {
7 | const [newCategory, setNewCategory] = useState("");
8 | const [submitted, setSubmitted] = useState(false);
9 |
10 | const createCategory = async (e) => {
11 | e.preventDefault();
12 | const { data, error } = await supabase.from("categories").insert([
13 | {
14 | name: newCategory,
15 | },
16 | ]);
17 |
18 | setNewCategory("");
19 | setSubmitted(true);
20 | toast("New Category has been added.");
21 | };
22 |
23 | const handleChange = (e) => {
24 | setNewCategory(e.target.value);
25 | };
26 |
27 | return (
28 |
29 |
34 | {submitted ? (
35 | Category Has Been Added
36 | ) : null}
37 |
38 |
46 |
51 | Submit
52 |
53 |
54 |
55 |
56 | );
57 | };
58 | export default AdminAddLanguages;
59 |
--------------------------------------------------------------------------------
/src/components/Admin/AdminAdd/AdminPopup.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 | position: relative;
15 | padding: 50px;
16 | width: 100%;
17 | max-width: 640px;
18 | background-color: #1f2833;
19 | /* background-color: #45a293; */
20 | }
21 | .close-button {
22 | position: absolute;
23 | top: 20px;
24 | right: 30px;
25 | background-color: #0b0c10;
26 | border: none;
27 | color: white;
28 | font-size: 24px;
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Admin/AdminAdd/AdminPopup.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import AdminAddLanguages from "./AdminAddLanguage";
3 | import "./AdminPopup.css";
4 |
5 | function AdminPopup(props) {
6 | return props.trigger ? (
7 |
8 |
9 | {props.children}
10 |
11 |
props.setTrigger(false)}
14 | >
15 | X
16 |
17 |
18 |
19 | ) : (
20 | ""
21 | );
22 | }
23 |
24 | export default AdminPopup;
25 |
--------------------------------------------------------------------------------
/src/components/Admin/AdminUsers/AdminUsers.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gitTogether-capstone/gittogether/bff7c228ca29ddeac6d6c2eafea5196b80f72823/src/components/Admin/AdminUsers/AdminUsers.css
--------------------------------------------------------------------------------
/src/components/Admin/AdminUsers/AdminUsers.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import supabase from "../../../client";
3 | import { toast } from "react-toastify";
4 | import { Link } from "react-router-dom";
5 | import { useSelector } from "react-redux";
6 |
7 | const AdminUsers = (props) => {
8 | const [user, setUser] = useState([]);
9 | const currentUser = supabase.auth.user();
10 | const [current, setCurrent] = useState([]);
11 | const [isBanned, setIsBanned] = useState("");
12 | //const isBanned = useSelector((state) => state.user.isBanned);
13 | useEffect(() => {
14 | fetchCurrent();
15 | }, [currentUser]);
16 |
17 | useEffect(() => {
18 | setIsBanned();
19 | }, []);
20 |
21 | useEffect(() => {
22 | async function fetchUser() {
23 | const { data } = await supabase.from("user").select("*");
24 | console.log("data", { data });
25 | setUser(data);
26 | }
27 | fetchUser();
28 | }, []);
29 |
30 | async function fetchCurrent() {
31 | if (currentUser) {
32 | const { data } = await supabase
33 | .from("user")
34 | .select("*")
35 | .eq("id", currentUser.id);
36 | setCurrent(data);
37 | }
38 | }
39 | async function banUser() {
40 | let { user } = await supabase.from("user").select("isBanned");
41 | // .update({ isBanned: !isBanned });
42 | // if (error) {
43 | // console.log("error", error);
44 | // }
45 | //setUser({ ...user, isBanned: !isBanned });
46 | setIsBanned(() => ({
47 | isBanned: !isBanned,
48 | }));
49 | toast("User has been banned!");
50 | }
51 |
52 | console.log("user", user);
53 | console.log("isBanned", isBanned);
54 | console.log("isBanned", isBanned);
55 | return current.length === 0 ? null : !current[0].isAdmin ? null : !user ? (
56 | Loading users...
57 | ) : (
58 |
59 |
60 |
61 |
62 |
63 | {user.map((use) => (
64 |
65 |
66 |
67 |
68 |
Username: {use.username}
69 |
70 |
71 |
72 | Ban USer
73 |
74 |
75 |
76 | ))}
77 |
78 |
79 |
80 | );
81 | };
82 |
83 | export default AdminUsers;
84 |
--------------------------------------------------------------------------------
/src/components/Chat/Chat.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import Conversations from './TeamConvo/TeamConvo';
4 | import './chat.scss';
5 | import Messages from './Messages/Messages';
6 | import Private from './PrivateConvo/Private';
7 | import supabase from '../../client';
8 |
9 | export default function Chat() {
10 | const currentUser = supabase.auth.user();
11 | const convoId = useSelector((state) => state.convoId);
12 | const [chatToggle, setChatToggle] = useState(false);
13 | const textAreaRef = useRef(null);
14 | const [newMessage, setNewMessage] = useState("");
15 | let receiverId = useSelector((state) => state.dmId);
16 |
17 | async function handleSend(e) {
18 | e.preventDefault()
19 | let message = textAreaRef.current.value;
20 | if (!chatToggle) {
21 | const { data } = await supabase.from('messages').insert([
22 | {
23 | content: message,
24 | sender_id: currentUser.id,
25 | conversation_id: convoId,
26 | },
27 | ]);
28 | } else {
29 | const { data } = await supabase.from('directMessages').insert([
30 | {
31 | content: message,
32 | sender_Id: currentUser.id,
33 | receiver_Id: receiverId,
34 | },
35 | ]);
36 | }
37 | setNewMessage("");
38 | }
39 |
40 | function handleChange(e) {
41 | setNewMessage(e.target.value);
42 | }
43 |
44 | return (
45 |
46 |
47 |
48 |
49 | setChatToggle(false)}>
50 | Teams
51 |
52 | |
53 | setChatToggle(true)}>
54 | Direct Messages
55 |
56 |
57 |
58 |
59 |
60 | {chatToggle === false ?
:
}
61 |
62 |
63 |
64 |
65 |
70 |
71 |
72 |
80 |
81 | Send
82 |
83 |
84 |
85 |
86 |
87 |
88 | );
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/Chat/Messages/Messages.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import supabase from '../../../client';
4 | import { fetchMessages, addMessage } from '../../../store/messages';
5 | import { fetchDMContent, addDM } from '../../../store/dmContent';
6 | import './messages.scss';
7 |
8 | export default function Messages({ dmState }) {
9 | const currentUser = supabase.auth.user();
10 | const dispatch = useDispatch();
11 | const convoId = useSelector((state) => state.convoId);
12 | let messages = useSelector((state) => state.messages);
13 | let directMessages = useSelector((state) => state.dmContent);
14 | let dmId = useSelector((state) => state.dmId);
15 |
16 | useEffect(() => {
17 | dispatch(fetchMessages(convoId));
18 | }, [convoId]);
19 |
20 | useEffect(() => {
21 | dispatch(fetchDMContent(currentUser.id, dmId));
22 | }, []);
23 |
24 | useEffect(() => {
25 | if (convoId) {
26 | const handleMessagesInsert = async (payload) => {
27 | if (payload.new.conversation_id === convoId) {
28 | let { data: messages } = await supabase
29 | .from('messages')
30 | .select(
31 | `
32 | *,
33 | user (
34 | id, imageUrl
35 | )
36 | `
37 | )
38 | .eq('id', payload.new.id);
39 | dispatch(addMessage(messages[0]));
40 | }
41 | };
42 | const messages = supabase
43 | .from('messages')
44 | .on('INSERT', handleMessagesInsert)
45 | .subscribe();
46 | }
47 | }, [convoId]);
48 |
49 | useEffect(() => {
50 | if (dmId) {
51 | const handleDirectMessagesInsert = async (payload) => {
52 | if (
53 | (payload.new.sender_Id === currentUser.id ||
54 | payload.new.receiver_Id === currentUser.id) &&
55 | (payload.new.sender_Id === dmId || payload.new.receiver_Id === dmId)
56 | ) {
57 | const { data: directMessages, error } = await supabase
58 | .from('directMessages')
59 | .select(
60 | `*,
61 | sender:user!directMessages_sender_Id_fkey(id, username, imageUrl),
62 | receiver: user!directMessages_receiver_Id_fkey(id, username, imageUrl)
63 | `
64 | )
65 | .eq('id', payload.new.id);
66 | dispatch(addDM(directMessages[0]));
67 | }
68 | };
69 | const directMessages = supabase
70 | .from('directMessages')
71 | .on('INSERT', handleDirectMessagesInsert)
72 | .subscribe();
73 | }
74 | }, [dmId]);
75 |
76 | return (
77 |
78 | {dmState === false ? (
79 |
80 | {messages.length < 1
81 | ? 'Start a chat!'
82 | : messages.map((message) => {
83 | return (
84 |
85 |
92 | {currentUser.id === message.sender_id ? null : (
93 |
98 | )}
99 |
106 | {message.content}
107 |
108 |
109 |
116 | {message.created_at}
117 |
118 |
119 | );
120 | })}
121 |
122 | ) : (
123 |
124 | {directMessages.length < 1
125 | ? 'Start a chat!'
126 | : directMessages.map((message) => {
127 | return dmId !== message.receiver_Id &&
128 | dmId !== message.sender_Id &&
129 | currentUser.id !== message.receiver_Id &&
130 | currentUser.id !== message.sender_Id ? null : (
131 |
132 |
139 | {currentUser.id === message.sender_Id ? null : (
140 |
145 | )}
146 |
153 | {message.content}
154 |
155 |
156 |
163 | {message.created_at}
164 |
165 |
166 | );
167 | })}
168 |
169 | )}
170 |
171 | );
172 | }
173 |
--------------------------------------------------------------------------------
/src/components/Chat/Messages/messages.scss:
--------------------------------------------------------------------------------
1 | .messages {
2 | display: flex;
3 | flex-direction: column;
4 | margin-bottom: 20px;
5 |
6 | .messagesTop {
7 | display: flex;
8 | align-items: center;
9 | text-align: start;
10 | }
11 |
12 | .messagesTop-Own {
13 | display: flex;
14 | align-items: center;
15 | text-align: start;
16 | justify-content: flex-end;
17 | }
18 |
19 | .messagesImg {
20 | height: 40px;
21 | width: 40px;
22 | border-radius: 50%;
23 | object-fit: cover;
24 | margin-right: 15px;
25 | }
26 |
27 | .messagesText {
28 | padding: 10px;
29 | border-radius: 20px;
30 | background-color: #0B0C10;
31 | color: #66FCF1;
32 | max-width: 300px;
33 | }
34 |
35 | .messagesText-Own {
36 | padding: 10px;
37 | border-radius: 20px;
38 | background-color: #45A29E;
39 | color: #FFF;
40 | max-width: 300px;
41 | }
42 |
43 | .messagesBottom {
44 | font-size: 12px;
45 | margin-top: 10px;
46 | text-align: start;
47 | }
48 |
49 | .messagesBottom-Own {
50 | font-size: 12px;
51 | margin-top: 10px;
52 | text-align: end;
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/Chat/PrivateConvo/Private.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import supabase from "../../../client";
4 | import "./privateConvo.scss";
5 | import { fetchDMUsers } from "../../../store/dmUsers";
6 | import { fetchSingleDM } from "../../../store/dmId";
7 | import { fetchDMContent } from "../../../store/dmContent";
8 |
9 | export default function Private() {
10 | const currentUser = supabase.auth.user();
11 | const dispatch = useDispatch();
12 | const dmUsers = useSelector((state) => state.dmUsers);
13 | const dmId = useSelector((state) => state.dmId);
14 |
15 | useEffect(() => {
16 | dispatch(fetchDMUsers(currentUser.id));
17 | }, []);
18 |
19 | return (
20 |
21 | {dmUsers.length < 1 ? (
22 |
No direct messages currently!
23 | ) : (
24 | dmUsers.map((user) => {
25 | return (
26 |
{dispatch(fetchDMContent(currentUser.id, user.id))
29 | dispatch(fetchSingleDM(user.id))}}
30 | >
31 |
32 |
33 |
38 |
39 |
40 |
{user.username}
41 |
42 |
43 | );
44 | })
45 | )}
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/Chat/PrivateConvo/privateConvo.scss:
--------------------------------------------------------------------------------
1 | .privateConvo-user {
2 | display: flex;
3 | align-items: center;
4 | font-weight: 500;
5 | cursor: pointer;
6 | justify-content: first baseline;
7 | padding: 20px;
8 | }
9 |
10 | .privateConvo-user:hover {
11 | background-color: #C5C6C7;
12 | color: #0b0c10;
13 | font-weight: 900;
14 | border-radius: 50px;
15 | height: 25%;
16 | }
17 |
18 | .privateConvo-current-user{
19 | background-color: #45A29E;
20 | color: #FFF;
21 | font-weight: 900;
22 | border-radius: 50px;
23 | display: flex;
24 | align-items: center;
25 | cursor: pointer;
26 | justify-content: first baseline;
27 | padding: 20px;
28 | height: 25%;
29 | }
30 |
31 | .privateConvo-img-container {
32 | position: relative;
33 | margin-right: 10px;
34 |
35 | img {
36 | width: 32px;
37 | height: 32px;
38 | border-radius: 50%;
39 | object-fit: cover;
40 | }
41 |
42 | }
43 |
44 | .onlineBadge {
45 | width: 10px;
46 | height:10px;
47 | border-radius: 50%;
48 | position: absolute;
49 | background-color: limegreen;
50 | top: 2px;
51 | right: 2px;
52 | }
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/components/Chat/TeamConvo/TeamConvo.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from "react";
2 | import { useSelector, useDispatch } from "react-redux";
3 | import "./teamConvo.scss";
4 | import GroupsIcon from "@mui/icons-material/GroupsOutlined";
5 | import supabase from "../../../client";
6 | import { fetchConversations } from "../../../store/conversations";
7 | import { fetchSingleConvo } from "../../../store/convoId";
8 |
9 | export default function Conversations() {
10 | const currentUser = supabase.auth.user();
11 | const dispatch = useDispatch();
12 | const conversations = useSelector((state) => state.conversations);
13 | let convoId = useSelector((state) => state.convoId);
14 |
15 | useEffect(() => {
16 | dispatch(fetchConversations(currentUser.id));
17 | }, []);
18 |
19 | return (
20 |
21 | {conversations.length < 1 ? (
22 |
23 | No conversations currently! Start or join a team to start a chat!
24 |
25 | ) : (
26 | conversations.map((convo) => {
27 | return (
28 |
32 | dispatch(fetchSingleConvo(convo.conversation.conversation_id))
33 | }
34 | >
35 |
36 |
37 | {convo.conversation.conversation_name}
38 |
39 |
40 |
41 | );
42 | })
43 | )}
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/Chat/TeamConvo/teamConvo.scss:
--------------------------------------------------------------------------------
1 | .conversation {
2 | display: flex;
3 | align-items: center;
4 | padding: 20px;
5 | cursor: pointer;
6 | justify-content: first baseline;
7 |
8 | span {
9 | margin-left: 20px;
10 | font-weight: 700;
11 | }
12 |
13 | }
14 |
15 | .conversation:hover{
16 | background-color: #C5C6C7;
17 | color: #0b0c10;
18 | font-weight: 900;
19 | border-radius: 50px;
20 | height: 25%;
21 | }
22 |
23 | .conversation-current-user{
24 | background-color: #45A29E;
25 | color: #FFF;
26 | font-weight: 900;
27 | border-radius: 50px;
28 | display: flex;
29 | align-items: center;
30 | cursor: pointer;
31 | justify-content: first baseline;
32 | padding: 20px;
33 | height: 25%;
34 |
35 | span {
36 | margin-left: 20px;
37 | font-weight: 700;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/Chat/chat.scss:
--------------------------------------------------------------------------------
1 | .chat {
2 |
3 | display: grid;
4 | gap: 7rem;
5 | grid-template-columns: repeat(5, 1fr);
6 | margin:auto;
7 | box-sizing: border-box;
8 | width: 90%;
9 | border: solid #0b0c10 80px;
10 | margin-top: -3%;
11 |
12 | .convo {
13 | grid-column: span 2;
14 | background-color: #1f2833;
15 | box-shadow: -20px 30px 63px -22px rgba(197,198,199,.45);
16 | border-radius: 25px;
17 | box-sizing: border-box;
18 | border: 100px;
19 | max-height: 95%;
20 | min-height: 95%;
21 | }
22 |
23 | .chat-box {
24 | grid-column: span 3;
25 | background-color: #1f2833;
26 | box-shadow: 20px 30px 63px -22px rgba(197,198,199,.45);
27 | border-radius: 25px;
28 | max-height: 95%;
29 | min-height: 95%;
30 | }
31 |
32 | .convo-header {
33 | display: flex;
34 | justify-content: center;
35 | margin-bottom: 1px;
36 | span {
37 | font-size: 20px;
38 | font-weight: 800;
39 | padding: 5px;
40 | }
41 | }
42 |
43 | .teams-header:hover {
44 | cursor: pointer;
45 | color: #FFF;
46 | }
47 |
48 | .dm-header:hover {
49 | cursor: pointer;
50 | color: #FFF;
51 | }
52 |
53 | .wrapper-chat-box {
54 | display: flex;
55 | flex-direction: column;
56 | padding: 25px;
57 | height: 98%;
58 | }
59 |
60 | .wrapper-convo {
61 | padding: 20px;
62 | height: 100%;
63 | padding: 40px;
64 | display: flex;
65 | flex-direction: column;
66 | overflow-y: scroll;
67 | mask-image: linear-gradient(to top, transparent, black),
68 | linear-gradient(to left, transparent 17px, black 17px);
69 | mask-size: 100% 20000px;
70 | mask-position: left bottom;
71 | -webkit-mask-image: linear-gradient(to top, transparent, black),
72 | linear-gradient(to left, transparent 17px, black 17px);
73 | -webkit-mask-size: 100% 20000px;
74 | -webkit-mask-position: left bottom;
75 | transition: mask-position 0.3s, -webkit-mask-position 0.3s;
76 | }
77 |
78 | .chatBoxTop {
79 | height: 100vh;
80 | padding: 10px;
81 | overflow-y: scroll;
82 | mask-image: linear-gradient(to top, transparent, black),
83 | linear-gradient(to left, transparent 17px, black 17px);
84 | mask-size: 100% 20000px;
85 | mask-position: left bottom;
86 | -webkit-mask-image: linear-gradient(to top, transparent, black),
87 | linear-gradient(to left, transparent 17px, black 17px);
88 | -webkit-mask-size: 100% 20000px;
89 | -webkit-mask-position: left bottom;
90 | transition: mask-position 0.3s, -webkit-mask-position 0.3s;
91 | }
92 |
93 | .chatBoxTop:hover {
94 | -webkit-mask-position: left top;
95 | }
96 |
97 | .chatBoxBottom {
98 | margin-top: 5px;
99 | display: flex;
100 | align-items: flex-end;
101 | justify-content: space-between;
102 | }
103 |
104 | .chatMessageInput {
105 | width: 80%;
106 | height: 90px;
107 | padding: 10px;
108 | margin-top: 40px;
109 | border-radius: 25px;
110 | }
111 |
112 | .chatSubmitButton {
113 | width: 80px;
114 | height: 40px;
115 | border: none;
116 | border-radius: 25px;
117 | cursor: pointer;
118 | background-color: #0b0c10;
119 | color: #66FCF1;
120 | font-weight: 900;
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/components/Footer/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./footer.scss";
3 | import TwitterIcon from "@mui/icons-material/Twitter";
4 | import YouTubeIcon from "@mui/icons-material/YouTube";
5 | import FacebookIcon from "@mui/icons-material/Facebook";
6 | import LinkedInIcon from "@mui/icons-material/LinkedIn";
7 | import CopyrightIcon from "@mui/icons-material/Copyright";
8 |
9 | const Footer = () => {
10 | return (
11 |
12 |
13 |
14 |
Why gitTogether?
15 |
16 | Our Mission
17 | Engagement
18 | Scale
19 | Watch Demo
20 |
21 |
22 |
23 |
Product
24 |
25 | Features
26 | Integrations
27 | Enteprise
28 | Solutions
29 |
30 |
31 |
32 |
Company
33 |
34 | About Us
35 | Our Services
36 | Privacy Policy
37 | Affiliate Program
38 |
39 |
40 |
41 |
Follow Us
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | gitTogether
54 |
55 | Legal
56 | Privacy Center
57 | Privacy Policy
58 | Cookies
59 | About Ads
60 | Additional Privacy Disclosures
61 |
62 |
63 | );
64 | };
65 |
66 | export default Footer;
67 |
--------------------------------------------------------------------------------
/src/components/Footer/footer.scss:
--------------------------------------------------------------------------------
1 | .footer-container{
2 | background-color: #0b0c10;
3 | padding-bottom: 2rem;
4 | width: 100%;
5 | display: flex;
6 | flex-direction: column;
7 |
8 | .footer {
9 | display: flex;
10 | justify-content: flex-start;
11 | margin-left: 15%;
12 | gap: 5rem;
13 | }
14 |
15 | .footer-heading {
16 | display: flex;
17 | flex-direction: column;
18 | margin-right: 4rem;
19 | color:#66FCF1;
20 |
21 |
22 | > * {
23 | display: flex;
24 | flex-direction: column;
25 |
26 |
27 | }
28 |
29 | ul {
30 | margin-top: auto;
31 | list-style: none;
32 | color: #45A29E;
33 | padding-left: 0;
34 | }
35 |
36 | li {
37 | padding: 3px;
38 |
39 | }
40 |
41 | li:hover {
42 | cursor: pointer;
43 | color: white;
44 | transition: 0.3s ease-out;
45 |
46 | }
47 |
48 | footer-4 {
49 | flex-direction: column;
50 | }
51 |
52 | .socials {
53 | display: inline-block;
54 |
55 | > * {
56 | margin-right: 10px;
57 | height: 30px;
58 | width: 30px;
59 | padding: 5px;
60 | background-color: rgba(255,255,255,0.2);
61 | border-radius: 50%;
62 |
63 | }
64 |
65 | > *:hover {
66 | color: #696969;
67 | background-color: #FFFFFF;
68 | cursor: pointer;
69 | transition: 0.3s ease-out;
70 | }
71 |
72 | }
73 |
74 | }
75 | .legal {
76 | display: flex;
77 | justify-content: flex-start;
78 | gap: 3rem;
79 | margin-left: 13%;
80 | padding: 25px 10px 5px 20px;
81 | color:#FFFFFF99;
82 | max-width: 72%;
83 |
84 |
85 | span {
86 | font-size: 15px;
87 | }
88 |
89 | span:hover {
90 | cursor: pointer;
91 | color: white;
92 | transition: 0.3s ease-out;
93 |
94 | }
95 |
96 | .copyright {
97 | height: 15px;
98 | }
99 | }
100 |
101 | }
102 |
103 |
--------------------------------------------------------------------------------
/src/components/GithubCollab/AddCollaborators.js:
--------------------------------------------------------------------------------
1 | import { Octokit } from '@octokit/core';
2 | import supabase from '../../client';
3 |
4 | export async function addCollaborator(user, project) {
5 | const userSession = supabase.auth.session();
6 | const currentUser = supabase.auth.user();
7 | const octokit = new Octokit({
8 | auth: userSession.provider_token,
9 | });
10 | if (project.repoLink) {
11 | let repourl = project.repoLink.split('/');
12 | let repo = repourl[repourl.length - 1];
13 | try {
14 | await octokit.request(
15 | `PUT /repos/{owner}/{repo}/collaborators/{username}`,
16 | {
17 | owner: currentUser.user_metadata.user_name,
18 | repo,
19 | username: user,
20 | permission: 'push',
21 | }
22 | );
23 | } catch (err) {
24 | console.log(err.message);
25 | }
26 | }
27 | }
28 |
29 | export async function addAllCollaborators(projectdata, owner) {
30 | const userSession = supabase.auth.session();
31 | const octokit = new Octokit({
32 | auth: userSession.provider_token,
33 | });
34 |
35 | const project = projectdata.data ? projectdata.data[0] : projectdata;
36 |
37 | if (project.repoLink) {
38 | let repourl = project.repoLink.split('/');
39 | let repo = repourl[repourl.length - 1];
40 |
41 | for (let i = 0; i < project.user.length; i++) {
42 | try {
43 | if (project.user[i].username !== owner) {
44 | await octokit.request(
45 | `GET /repos/{owner}/{repo}/collaborators/{username}`,
46 | {
47 | owner,
48 | repo,
49 | username: project.user[i].username,
50 | }
51 | );
52 | }
53 | } catch (err) {
54 | if (err.message === 'Not Found') {
55 | await octokit.request(
56 | `PUT /repos/{owner}/{repo}/collaborators/{username}`,
57 | {
58 | owner,
59 | repo,
60 | username: project.user[i].username,
61 | permission: 'push',
62 | }
63 | );
64 | }
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/GithubCollab/ProjectRepo.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import supabase from '../../client';
3 | import { Octokit } from '@octokit/core';
4 | import { useDispatch } from 'react-redux';
5 | import { updateRepo } from '../../store/project';
6 | import { addAllCollaborators } from './AddCollaborators';
7 |
8 | function ProjectRepo(props) {
9 | const user = supabase.auth.user();
10 | const userSession = supabase.auth.session();
11 | const octokit = new Octokit({
12 | auth: userSession.provider_token,
13 | });
14 | const [repoName, setRepoName] = useState('');
15 | const dispatch = useDispatch();
16 |
17 | const verifyRepo = async (evt) => {
18 | try {
19 | let repository = repoName.split('/');
20 | let newreponame = repository[repository.length - 1];
21 | await octokit.request(`GET /repos/{owner}/{repo}`, {
22 | owner: userSession.user.user_metadata.user_name,
23 | repo: newreponame,
24 | });
25 | dispatch(updateRepo(repoName));
26 | await supabase
27 | .from('projects')
28 | .update({ repoLink: repoName })
29 | .eq('id', props.project.id);
30 | } catch (err) {
31 | alert('You can not provide a repository you are not the owner of.');
32 | }
33 | };
34 |
35 | const unlistRepo = async (evt) => {
36 | await supabase
37 | .from('projects')
38 | .update({ repoLink: '' })
39 | .eq('id', props.project.id);
40 | dispatch(updateRepo(''));
41 | };
42 |
43 | const addProjectCollaborators = async (evt) => {
44 | let proj = await supabase
45 | .from('projects')
46 | .select('*, projectUser(*), user!projectUser(*)')
47 | .eq('id', props.project.id);
48 | addAllCollaborators(proj, user.user_metadata.user_name);
49 | };
50 |
51 | if (props.project.repoLink) {
52 | if (user.id === props.project.projectUser[0].user.id) {
53 | return (
54 |
65 |
72 |
73 | Github
74 |
75 |
addProjectCollaborators(e)}
78 | >
79 | Add Collaborators
80 |
81 |
82 | Unlist Repo
83 |
84 |
85 | );
86 | } else {
87 | return (
88 |
95 |
96 | Github
97 |
98 | );
99 | }
100 | } else if (
101 | props.project.id &&
102 | user.id === props.project.projectUser[0].user.id
103 | ) {
104 | return (
105 |
114 |
props.setShowRepoCreation(true)}
118 | >
119 | Create Repo
120 |
121 |
122 | setRepoName(e.target.value)}
129 | >
130 | verifyRepo(e)}>
131 | Add Repo
132 |
133 |
134 |
135 | );
136 | } else {
137 | return This project has no repo yet.
;
138 | }
139 | }
140 |
141 | export default ProjectRepo;
142 |
--------------------------------------------------------------------------------
/src/components/GithubCollab/RepoCreation.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Octokit } from '@octokit/core';
3 | import supabase from '../../client';
4 | import { updateRepo } from '../../store/project';
5 | import { useDispatch } from 'react-redux';
6 | import { addAllCollaborators } from '../GithubCollab/AddCollaborators';
7 |
8 | function CreateRepo(props) {
9 | const userSession = supabase.auth.session();
10 | const dispatch = useDispatch();
11 | const octokit = new Octokit({
12 | auth: userSession.provider_token,
13 | });
14 | const [repoName, setRepoName] = useState('');
15 | const [repoVisibility, setRepoVisibility] = useState(false);
16 |
17 | async function createRepo(evt) {
18 | evt.preventDefault();
19 | try {
20 | let newRepo = await octokit.request('POST /user/repos', {
21 | name: repoName,
22 | visibility: repoVisibility,
23 | });
24 | let { data, err } = await supabase
25 | .from('projects')
26 | .update([{ repoLink: newRepo.data['html_url'] }])
27 | .eq('id', props.project.id);
28 | dispatch(updateRepo(newRepo.data['html_url']));
29 | props.onClose();
30 | } catch (err) {
31 | let message = err.message.indexOf(`message":`) + 10;
32 | let error = err.message.slice(message, err.message.indexOf('}') - 1);
33 | alert(error);
34 | }
35 | }
36 |
37 | if (props.showRepoCreation) {
38 | return (
39 |
40 |
41 |
46 | X
47 |
48 |
49 |
e.stopPropagation()}>
50 |
51 | GitHub Repository Form
52 |
53 |
54 |
55 |
createRepo(evt)}>
56 |
57 | {' '}
58 |
59 |
60 | Name
61 |
62 |
63 |
setRepoName(evt.target.value)}
70 | >
71 |
72 |
73 |
74 |
75 | Repo Visibility
76 |
77 |
78 |
81 | setRepoVisibility(
82 | e.target.value === 'true' ? true : false
83 | )
84 | }
85 | >
86 |
87 | Public
88 |
89 |
90 | Private
91 |
92 |
93 |
94 |
95 | Create Repository
96 |
97 |
98 | Creating this repo will add any current project members as
99 | collaborators.
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | );
109 | } else {
110 | return null;
111 | }
112 | }
113 |
114 | export default CreateRepo;
115 |
--------------------------------------------------------------------------------
/src/components/LandingPage/Final/Final.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Footer from "../../Footer/Footer";
3 | import { Link } from "react-router-dom";
4 | import "./final.scss";
5 |
6 | const Final = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
Make your contribution
13 |
14 |
15 | The next developer of your dreams starts with you! Join millions
16 | of other developers and sign up for gitTogether today!{" "}
17 |
18 |
19 |
20 |
21 |
22 | Sign up for gitTogether
23 |
24 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default Final;
39 |
--------------------------------------------------------------------------------
/src/components/LandingPage/Final/final.scss:
--------------------------------------------------------------------------------
1 | .final {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | position: absolute;
6 | height: 120vh;
7 | width: 100%;
8 |
9 | .content {
10 | flex-grow: 1;
11 | display: flex;
12 | flex-direction: row;
13 | margin-top: 9%;
14 | margin-left: 8%;
15 | max-width: 85%;
16 | gap: 17em;
17 |
18 | .content-header {
19 | h1 {
20 | font-size: 56px;
21 | }
22 | }
23 |
24 | .content-description {
25 | font-size: 14px;
26 | color: #c5c6c7;
27 | line-height: 30px;
28 | }
29 |
30 | .final-signup {
31 | margin-top: 40px;
32 | button {
33 | font-family: "Raleway", sans-serif;
34 | font-size: 15px;
35 | font-weight: 800;
36 | text-align: center;
37 | background-color: #45a29e;
38 | color: #fff;
39 | border-radius: 10px;
40 | border: 2px solid;
41 | border-color: #439292;
42 | height: 60px;
43 | width: 200px;
44 | cursor: pointer;
45 | }
46 | button:hover {
47 | box-shadow: 2px 5px 6px rgba(255, 255, 255, 0.3);
48 | transform: translateY(-1px);
49 | transition: all 150ms ease-in-out 0s;
50 | }
51 | }
52 |
53 | .astro-final-img {
54 | img {
55 | height: 500px;
56 | width: 500px;
57 | }
58 | animation-name: float-y;
59 | animation-duration: 6s;
60 | animation-iteration-count: infinite;
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/LandingPage/Intro/Intro.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import "./intro.scss";
4 |
5 | const Intro = () => {
6 | return (
7 |
8 |
9 |
10 |
Where the world gits together
11 |
12 |
13 |
14 | Millions of developers are collaborating together on gitTogether-the
15 | largest and most advanced software development social platform in
16 | the world.
17 |
18 |
19 |
20 |
21 | Sign up for gitTogether
22 |
23 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default Intro;
39 |
--------------------------------------------------------------------------------
/src/components/LandingPage/Intro/intro.scss:
--------------------------------------------------------------------------------
1 | .intro {
2 | background: linear-gradient(328deg, rgba(197,198,199,1) 0%, rgba(31,40,51,1) 19%, rgba(11,12,16,1) 26%, rgba(11,12,16,1) 91%, rgba(67,146,146,1) 98%, rgba(197,198,199,1) 100%);
3 | color: #66FCF1;
4 | margin-top: -3%;
5 | height: 120vh;
6 |
7 | .intro-text {
8 | margin-top: 9%;
9 | margin-left: 8%;
10 | max-width: 33%;
11 | }
12 |
13 | .intro-header {
14 | h1 {
15 | font-size: 72px;
16 | margin-bottom: 0;
17 | }
18 | }
19 |
20 | .intro-description {
21 | font-size: 14px;
22 | color:#c5c6c7;
23 | line-height: 30px;
24 | }
25 |
26 | .intro-signup-button {
27 | margin-top: 40px;
28 |
29 | button {
30 | font-family: 'Raleway', sans-serif;
31 | font-size: 15px;
32 | font-weight: 800;
33 | text-align: center;
34 | background-color: #45A29E;
35 | color: #FFF;
36 | border-radius: 2em;
37 | border: 2px solid;
38 | border-color: #439292;
39 | height: 60px;
40 | width: 200px;
41 | cursor: pointer;
42 | }
43 | button:hover {
44 | box-shadow: 2px 5px 6px rgba(255,255,255,0.30);
45 | transform: translateY(-1px);
46 | transition: all 150ms ease-in-out 0s;
47 | }
48 | }
49 |
50 | .world-container {
51 | position: relative;
52 | display: flex;
53 | flex-direction: row;
54 | max-width: 77%;
55 |
56 | .world-img {
57 | height: 900px;
58 | width: 900px;
59 |
60 | }
61 |
62 | @keyframes float-y {
63 | 50% {transform: translateY(-120px)};
64 | }
65 |
66 | .astro-img {
67 | position: absolute;
68 | bottom: 10%;
69 | right: 20%;
70 | height: 400px;
71 | width: 200px;
72 | z-index: 2;
73 | object-fit: cover;
74 | animation-name: float-y;
75 | animation-duration: 5s;
76 | animation-iteration-count: infinite;
77 | }
78 |
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/LandingPage/LandingPage.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./landingPage.scss";
3 | import Intro from "./Intro/Intro";
4 | import StepOne from "./StepOne/StepOne";
5 | import StepTwo from "./StepTwo/StepTwo";
6 | import StepThree from "./StepThree/StepThree";
7 | import Final from "./Final/Final";
8 |
9 | const LandingPage = () => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default LandingPage;
24 |
--------------------------------------------------------------------------------
/src/components/LandingPage/StepOne/StepOne.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./stepOne.scss";
3 |
4 | const StepOne = () => {
5 | return (
6 |
7 |
8 |
Create or Join a Project!
9 |
10 |
11 |
12 |
13 |
Starting a new project is easy-
14 |
15 |
16 |
17 | gitTogether's GitHub integration allows you to see a potential
18 | team member's top languages and their GitHub link within a few
19 | clicks!
20 |
21 |
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
Run out of ideas?
38 |
39 |
40 |
41 | No worries! Our project feed will keep you up to date with
42 | projects posted from other developers who are currently looking
43 | for other collaborators. Joining a project is just a click away!
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default StepOne;
53 |
--------------------------------------------------------------------------------
/src/components/LandingPage/StepOne/stepOne.scss:
--------------------------------------------------------------------------------
1 | .stepOne {
2 | display: flex;
3 | flex-direction: column;
4 | margin-left: 20%;
5 | padding-bottom: 2rem;
6 |
7 | .header {
8 | margin-top: 2%;
9 | max-width: 50%;
10 | margin-bottom: 5%;
11 |
12 | h1 {
13 | font-size: 45px;
14 | color: #66fcf1;
15 | }
16 | }
17 | .create-container {
18 | display: flex;
19 | align-items: center;
20 |
21 | .start-text {
22 | max-width: 50%;
23 | }
24 |
25 | .start-header {
26 | color: #fff;
27 | }
28 |
29 | .start-content {
30 | color: #c5c6c7;
31 | }
32 |
33 | h3 {
34 | max-width: 70%;
35 | line-height: 30px;
36 | margin: 0;
37 | }
38 |
39 | .create-img {
40 |
41 | img {
42 | height: 400px;
43 | width: 300px;
44 | box-shadow: 0px 51px 95px -49px rgba(255,255,255,0.2);
45 | border-radius: 25px;
46 | }
47 | }
48 | }
49 |
50 | .join-container {
51 | display: flex;
52 | margin-top: 5%;
53 | .join {
54 |
55 | .join-img {
56 | height: 500px;
57 | width: 450px;
58 | box-shadow: 0px 51px 95px -44px rgba(255,255,255,0.2);
59 | border-radius: 25px;
60 | }
61 | }
62 |
63 | .join-text {
64 | max-width: 50%;
65 | margin-top: 5%;
66 | margin-left: 5%;
67 | }
68 |
69 | .join-header {
70 | color: #fff;
71 | }
72 |
73 | .join-content {
74 | color: #c5c6c7;
75 | }
76 |
77 | h3 {
78 | max-width: 70%;
79 | line-height: 30px;
80 | margin: 0;
81 | }
82 |
83 |
84 | }
85 |
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/src/components/LandingPage/StepThree/StepThree.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./stepThree.scss";
3 |
4 | const StepThree = () => {
5 | return (
6 |
7 |
8 |
Finalize and Create a Repo!
9 |
10 |
11 |
12 |
13 |
All set? Repo Time!
14 |
15 |
16 |
Simply go to a project page to be able to automatically create its GitHub repo. Does the project already have a repo? No problem! Just add the repo's GitHub link.
17 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default StepThree;
33 |
--------------------------------------------------------------------------------
/src/components/LandingPage/StepThree/stepThree.scss:
--------------------------------------------------------------------------------
1 | .stepThree {
2 | display: flex;
3 | flex-direction: column;
4 | padding-bottom: 2rem;
5 | margin-left: 20%;
6 |
7 | .stepThree-header {
8 | margin-top: 10%;
9 | h1 {
10 | font-size: 45px;
11 | color: #66fcf1;
12 | margin-bottom: 20%;
13 | }
14 | }
15 |
16 | .single-project-container {
17 | display: flex;
18 | align-items: center;
19 | .single-project-text{
20 | max-width: 50%;
21 | .single-project-header {
22 | color: #fff;
23 | h3 {
24 | max-width: 70%;
25 | line-height: 30px;
26 | margin: 0;
27 | }
28 | }
29 | .single-project-content {
30 | color: #c5c6c7;
31 | h3 {
32 | max-width: 70%;
33 | line-height: 30px;
34 | margin: 0;
35 | }
36 | }
37 | }
38 |
39 | .single-project-img {
40 | img {
41 | height: 350px;
42 | width: 400px;
43 | box-shadow: 0px 51px 95px -49px rgba(255,255,255,0.2);
44 | border-radius: 25px;
45 | }
46 | }
47 |
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/LandingPage/StepTwo/StepTwo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./stepTwo.scss";
3 |
4 | const StepTwo = () => {
5 | return (
6 |
7 |
8 |
Find Team Members!
9 |
10 |
11 |
12 |
13 |
GitHub Integration-
14 |
15 |
16 |
17 | gitTogether's GitHub integration allows you to see a potential
18 | team member's top languages and their GitHub link in a user's profile.
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
Time to Chat!
30 |
31 |
32 |
33 | Use gitTogether's chat feature to talk to your project team members.
34 | A direct message option also exists if you want to have private conversations.
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default StepTwo;
47 |
--------------------------------------------------------------------------------
/src/components/LandingPage/StepTwo/stepTwo.scss:
--------------------------------------------------------------------------------
1 | .stepTwo{
2 | display: flex;
3 | flex-direction: column;
4 | margin-left: 20%;
5 |
6 | .team-header {
7 | margin-top: 4%;
8 | max-width: 50%;
9 | margin-bottom: 30px;
10 |
11 | h1 {
12 | font-size: 45px;
13 | color: #66fcf1;
14 | }
15 | }
16 | .team-container {
17 | display: flex;
18 | align-items: center;
19 |
20 | .landing-profile-text {
21 | max-width: 45%;
22 | }
23 |
24 | .landing-profile-header {
25 | color: #fff;
26 | }
27 |
28 | .landing-profile-content {
29 | color: #c5c6c7;
30 | }
31 |
32 | h3 {
33 | max-width: 70%;
34 | line-height: 30px;
35 | margin: 0;
36 | }
37 |
38 | .landing-profile-img {
39 |
40 | img {
41 | height: 300px;
42 | width: 550px;
43 | box-shadow: 0px 51px 95px -49px rgba(255,255,255,0.2);
44 | border-radius: 25px;
45 | }
46 | }
47 | }
48 |
49 | .landing-chat-container {
50 | display: flex;
51 | margin-top: 5%;
52 |
53 | .landing-chat-img {
54 | img {
55 | height: 300px;
56 | width: 550px;
57 | box-shadow: 0px 51px 95px -44px rgba(255,255,255,0.2);
58 | border-radius: 25px;
59 | }
60 | }
61 |
62 | .landing-chat-text {
63 | max-width: 45%;
64 | margin-top: 5%;
65 | }
66 |
67 | .landing-chat-header {
68 | color: #fff;
69 | }
70 |
71 | .landing-chat-content {
72 | color: #c5c6c7;
73 | }
74 |
75 | h3 {
76 | max-width: 70%;
77 | line-height: 30px;
78 | margin: 0;
79 | }
80 |
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/LandingPage/landingPage.scss:
--------------------------------------------------------------------------------
1 | .landing-container{
2 |
3 | .sections{
4 | width: 100%;
5 | overflow-y: scroll;
6 | &::-webkit-scrollbar{
7 | display: none;
8 | }
9 |
10 | > * {
11 | display: flex;
12 | align-items: flex-start;
13 | justify-content: flex-start;
14 | color: #66FCF1;
15 | text-align: start;
16 |
17 | }
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/components/Login/Login.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { login, signOut, setUser } from '../../store/user';
4 | import supabase from '../../client';
5 | import { Link, useHistory } from 'react-router-dom';
6 | import './login.scss';
7 | import Footer from '../Footer/Footer';
8 |
9 | const Login = (props) => {
10 | const dispatch = useDispatch();
11 | const user = useSelector((state) => state.user);
12 | const history = useHistory();
13 |
14 | const signInWithGithub = () => {
15 | dispatch(login());
16 | };
17 |
18 | const logout = () => {
19 | dispatch(signOut(history));
20 | };
21 |
22 | return (
23 |
24 | {user && user.id ? (
25 |
26 |
27 |
{user.email}
28 |
Projects
29 |
Signout
30 |
31 | ) : (
32 | <>
33 |
34 |
35 | Sign In With GitHub
36 |
37 |
38 |
39 | >
40 | )}
41 |
42 | );
43 | };
44 |
45 | export default Login;
46 |
--------------------------------------------------------------------------------
/src/components/Login/login.scss:
--------------------------------------------------------------------------------
1 | .login {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: space-between;
5 | height: 100vh;
6 | align-items: center;
7 |
8 | .signIn-button {
9 | background-color: #1f2833;
10 | color: #66FCF1;
11 | padding: 0.3em 1.2em;
12 | margin: 0 0.3em 0.3em 0;
13 | border-radius: 2em;
14 | border: 2px solid;
15 | border-color: #66FCF1;
16 | box-sizing: border-box;
17 | text-decoration: none;
18 | text-align: center;
19 | font-family: 'Roboto', sans-serif;
20 | font-weight: 900;
21 | font-size: 50px;
22 |
23 | }
24 |
25 | .signIn-button:hover {
26 | color: #1f2833;
27 | background-color: #66FCF1;
28 | border-color: #1f2833;
29 | }
30 |
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/components/NotFound/NotFound.css:
--------------------------------------------------------------------------------
1 | #header {
2 | font-size: 150px;
3 | color: #66fcf1;
4 | }
5 |
6 | #sub-header {
7 | font-size: 50px;
8 | color: #66fcf1;
9 | }
10 |
11 | #back-home {
12 | height: 70px;
13 | width: 300px;
14 | font-size: 36px;
15 | border: none;
16 | background-color: #66fcf1;
17 | border-radius: 10px;
18 | }
19 |
20 | #back-home:hover {
21 | cursor: pointer;
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/NotFound/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './NotFound.css';
3 | import { Link } from 'react-router-dom';
4 |
5 | const NotFound = () => {
6 | return (
7 |
8 |
9 |
10 |
11 | Let's go Home
12 |
13 |
14 | );
15 | };
16 |
17 | export default NotFound;
18 |
--------------------------------------------------------------------------------
/src/components/ProjectFeed/ProjectFeed.css:
--------------------------------------------------------------------------------
1 | .project-feed {
2 | display: flex;
3 | flex-direction: row;
4 | justify-content: center;
5 | gap: 50px;
6 | margin-top: 50px;
7 | }
8 |
9 | .project-filters {
10 | position: sticky;
11 | top: 140px;
12 | display: flex;
13 | flex-direction: column;
14 | align-items: flex-start;
15 | background-color: #1f2833;
16 | padding: 30px;
17 | border-radius: 5px;
18 | gap: 5px;
19 | align-self: flex-start;
20 | }
21 |
22 | .project-list {
23 | display: flex;
24 | flex-direction: column;
25 | gap: 20px;
26 | align-items: flex-start;
27 | }
28 |
29 | .project-tile {
30 | display: flex;
31 | flex-direction: column;
32 | text-align: left;
33 | background-color: #1f2833;
34 | padding: 30px;
35 | border-radius: 5px;
36 | max-width: 700px;
37 | min-width: 700px;
38 | margin: 20px;
39 | }
40 |
41 | .project-tile a {
42 | text-decoration: none;
43 | color: white;
44 | }
45 |
46 | .project-tile img {
47 | height: 50px;
48 | width: 50px;
49 | border-radius: 25px;
50 | }
51 |
52 | .project-owner {
53 | display: flex;
54 | flex-direction: row;
55 | align-items: center;
56 | gap: 10px;
57 | }
58 |
59 | .project-owner a {
60 | text-decoration: underline;
61 | }
62 |
63 | .request-to-collab {
64 | background-color: #45a29e;
65 | border: none;
66 | height: 40px;
67 | border-radius: 5px;
68 | color: white;
69 | font-size: 16px;
70 | max-width: 300px;
71 | margin-bottom: 10px;
72 | }
73 |
74 | .delete-button {
75 | background-color: #1f2833;
76 | border: none;
77 | height: 40px;
78 | border-radius: 20px;
79 | color: white;
80 | font-size: 24px;
81 | max-width: 50px;
82 | min-width: 50px;
83 | margin-bottom: 10px;
84 | }
85 |
86 | .request-to-collab:disabled {
87 | background-color: gray;
88 | }
89 |
90 | .request-to-collab:hover {
91 | cursor: pointer;
92 | }
93 |
94 | .delete-button:hover {
95 | cursor: pointer;
96 | }
97 |
98 | .tile-header {
99 | display: flex;
100 | flex-direction: row;
101 | justify-content: space-between;
102 | }
103 |
104 | .radio-container input {
105 | background-color: #45a293;
106 | }
107 |
108 | .container {
109 | position: relative;
110 | padding-left: 35px;
111 | cursor: pointer;
112 | -webkit-user-select: none;
113 | -moz-user-select: none;
114 | -ms-user-select: none;
115 | user-select: none;
116 | }
117 |
118 | .container input {
119 | position: absolute;
120 | opacity: 0;
121 | cursor: pointer;
122 | height: 0;
123 | width: 0;
124 | }
125 |
126 | .radio-fill {
127 | position: absolute;
128 | top: 0;
129 | left: 0;
130 | height: 15px;
131 | width: 15px;
132 | background-color: #0b0c10;
133 | border-radius: 50%;
134 | }
135 |
136 | .container:hover input ~ .radio-fill {
137 | background-color: #ccc;
138 | }
139 |
140 | .container input:checked ~ .radio-fill {
141 | background-color: #45a29e;
142 | }
143 |
144 | .radio-fill:after {
145 | content: '';
146 | position: absolute;
147 | display: none;
148 | }
149 |
150 | .container input:checked ~ .radio-fill:after {
151 | display: block;
152 | }
153 |
154 | .checkmark {
155 | position: absolute;
156 | top: 0;
157 | left: 0;
158 | height: 15px;
159 | width: 15px;
160 | background-color: #0b0c10;
161 | }
162 |
163 | .container:hover input ~ .checkmark {
164 | background-color: rgb(126, 126, 126);
165 | }
166 |
167 | /* When the checkbox is checked, add a blue background */
168 | .container input:checked ~ .checkmark {
169 | background-color: #45a293;
170 | }
171 |
172 | /* Create the checkmark/indicator (hidden when not checked) */
173 | .checkmark:after {
174 | content: '';
175 | position: absolute;
176 | display: none;
177 | }
178 |
179 | /* Show the checkmark when checked */
180 | .container input:checked ~ .checkmark:after {
181 | display: block;
182 | }
183 |
184 | /* Style the checkmark/indicator */
185 | .container .checkmark:after {
186 | left: 4px;
187 | top: 0px;
188 | width: 3px;
189 | height: 10px;
190 | border: solid white;
191 | border-width: 0 3px 3px 0;
192 | -webkit-transform: rotate(45deg);
193 | -ms-transform: rotate(45deg);
194 | transform: rotate(45deg);
195 | }
196 |
197 | .request-message {
198 | color: #45a29e;
199 | }
200 |
201 | .header-container {
202 | display: flex;
203 | flex-direction: row;
204 | align-items: center;
205 | }
206 |
207 | .header-container h2 {
208 | margin: 5px;
209 | }
210 |
211 | .arrow {
212 | font-size: 300px;
213 | }
214 |
215 | .arrow:hover {
216 | cursor: pointer;
217 | color: #45a293;
218 | }
219 |
--------------------------------------------------------------------------------
/src/components/ProjectFeed/ProjectFeed.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { fetchProjects, setProjects } from '../../store/projects';
4 | import supabase from '../../client.js';
5 | import './ProjectFeed.css';
6 | import ProjectTile from './ProjectTile';
7 | import InfiniteScroll from 'react-infinite-scroll-component';
8 | import { toast } from 'react-toastify';
9 | import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
10 | import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
11 | import createUser from '../../CreateUser';
12 | import { fetchUserDMs } from '../../util';
13 | import { setUser } from '../../store/user';
14 | import AccountSetup from '../AccountSetup/AccountSetup';
15 |
16 | const ProjectFeed = () => {
17 | const [filters, setFilters] = useState({
18 | beginnerFriendly: false,
19 | category: 'all',
20 | languages: [],
21 | });
22 |
23 | const userId = useSelector((state) => state.user.id);
24 | const [currentUser, setCurrentUser] = useState({});
25 | const [categories, setCategories] = useState([]);
26 | const [languages, setLanguages] = useState([]);
27 | const [isLoading, setIsLoading] = useState(false);
28 | const [page, setPage] = useState(0);
29 | const [showCategories, setShowCategories] = useState(false);
30 | const [showLanguages, setShowLanguages] = useState(false);
31 | const [showBeginner, setShowBeginner] = useState(false);
32 | const [creatingUser, setCreatingUser] = useState(false);
33 |
34 | const hasMore = useSelector((state) => state.hasMore);
35 |
36 | const projects = useSelector((state) => state.projects);
37 | const dispatch = useDispatch();
38 |
39 | useEffect(() => {
40 | async function createUserInDB() {
41 | setCreatingUser(true);
42 | await createUser();
43 | let user = supabase.auth.user();
44 | dispatch(setUser(user));
45 | setCreatingUser(false);
46 | }
47 | createUserInDB();
48 | }, []);
49 |
50 | const grabMoreProjects = async () => {
51 | setIsLoading(true);
52 | dispatch(fetchProjects(filters, categories, languages, page, 'more'));
53 | setPage(page + 1);
54 | setIsLoading(false);
55 | };
56 | const fetchAll = async () => {
57 | if (userId) {
58 | setIsLoading(true);
59 | const currentUser = await supabase
60 | .from('user')
61 | .select(
62 | `
63 | *,
64 | languages (id, name)
65 | `
66 | )
67 | .eq('id', userId);
68 | const categories = await supabase.from('categories').select('*');
69 | const languages = await supabase.from('languages').select('*');
70 | setLanguages(languages.data);
71 | setCategories(categories.data);
72 | setCurrentUser(currentUser.data);
73 | dispatch(
74 | fetchProjects(filters, categories.data, languages.data, page, 'initial')
75 | );
76 |
77 | setPage(page + 1);
78 | setIsLoading(false);
79 | }
80 | };
81 |
82 | useEffect(() => {
83 | fetchAll();
84 | }, [filters, userId]);
85 |
86 | const showToastNotification = (message) => {
87 | toast(message);
88 | };
89 |
90 | const handleChange = (e) => {
91 | setPage(0);
92 | dispatch(setProjects([]));
93 | if (e.target.name === 'category') {
94 | setFilters({ ...filters, [e.target.name]: e.target.value });
95 | } else if (e.target.name === 'language') {
96 | if (e.target.checked) {
97 | setFilters({
98 | ...filters,
99 | languages: [...filters.languages, e.target.value],
100 | });
101 | } else {
102 | let newFilters = filters.languages.filter(
103 | (languageId) => languageId !== e.target.value
104 | );
105 | setFilters({ ...filters, languages: newFilters });
106 | }
107 | } else {
108 | setFilters({ ...filters, [e.target.name]: e.target.checked });
109 | }
110 | };
111 |
112 | if (creatingUser) {
113 | return ;
114 | } else {
115 | return (
116 |
117 |
118 |
Filters
119 |
120 |
Beginner Friendly
121 | {showBeginner ? (
122 | setShowBeginner(false)}
125 | />
126 | ) : (
127 | setShowBeginner(true)}
130 | />
131 | )}
132 |
133 |
134 |
135 |
140 |
141 | Beginner Friendly
142 |
143 |
144 |
145 |
Categories
146 | {showCategories ? (
147 | setShowCategories(false)}
150 | />
151 | ) : (
152 | setShowCategories(true)}
155 | />
156 | )}
157 |
158 |
159 |
160 |
161 |
168 |
169 | All
170 |
171 |
172 | {categories
173 | ? categories.map((category) => {
174 | return (
175 |
180 |
181 |
189 |
190 | {category.name}
191 |
192 |
193 | );
194 | })
195 | : ''}
196 |
197 |
Languages
198 | {showLanguages ? (
199 | setShowLanguages(false)}
202 | />
203 | ) : (
204 | setShowLanguages(true)}
207 | />
208 | )}
209 |
210 |
211 | {languages.length ? (
212 | languages.map((language) => {
213 | return (
214 |
219 |
220 |
226 |
227 | {language.name}
228 |
229 |
230 | );
231 | })
232 | ) : (
233 |
{isLoading ? '' : "Sorry, we couldn't find any projects"}
234 | )}
235 |
236 |
237 |
238 |
Loading feed...}
243 | endMessage={No more projects }
244 | >
245 | {(!!projects || projects.length) && !isLoading ? (
246 | projects.map((project) => (
247 |
254 | ))
255 | ) : isLoading ? (
256 | <>>
257 | ) : (
258 | We couldn't find any projects ¯\_(ツ)_/¯
259 | )}
260 |
261 |
262 |
263 | );
264 | }
265 | };
266 |
267 | export default ProjectFeed;
268 |
--------------------------------------------------------------------------------
/src/components/ProjectFeed/ProjectTile.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { compareLanguages } from '../../util';
4 | import supabase from '../../client';
5 | import { setProjects } from '../../store/projects';
6 | import { useDispatch } from 'react-redux';
7 |
8 | const ProjectTile = ({
9 | project,
10 | currentUser,
11 | allProjects,
12 | sendNotification,
13 | }) => {
14 | const [wasDeleted, setWasDeleted] = useState(false);
15 | const [requestMessage, setRequestMessage] = useState('');
16 | const dispatch = useDispatch();
17 |
18 | const handleClick = async () => {
19 | //check if this user has already requested to join this project
20 | const existingUser = await supabase
21 | .from('projectUser')
22 | .select('*')
23 | .eq('projectId', project.id)
24 | .eq('userId', currentUser[0].id);
25 |
26 | //if not, send the join request
27 |
28 | if (existingUser.data.length === 0) {
29 | const { data, error } = await supabase
30 | .from('projectUser')
31 | .insert([{ userId: currentUser[0].id, projectId: project.id }]);
32 | setRequestMessage(
33 | 'Success! Your request to join this project was sent, and the owner has been notified.'
34 | );
35 | } else {
36 | setRequestMessage(
37 | "You've already requested to join this project. The owner has been notified."
38 | );
39 | }
40 | };
41 |
42 | const handleDelete = async () => {
43 | await supabase
44 | .from('projectUser')
45 | .delete()
46 | .match({ projectId: project.id });
47 | const { data, error } = await supabase
48 | .from('projects')
49 | .delete()
50 | .match({ id: project.id });
51 | if (error) {
52 | console.log(error);
53 | } else {
54 | const newProjects = allProjects.filter(
55 | (element) => element.id !== project.id
56 | );
57 | setWasDeleted(true);
58 | sendNotification('Post was succesfully deleted.');
59 | dispatch(setProjects(newProjects));
60 | }
61 | };
62 |
63 | if (JSON.stringify(currentUser) === '{}') return
;
64 |
65 | return project.projectUser.length === 0 ? null : (
66 |
67 | {wasDeleted ? (
68 |
69 | This post has been deleted
70 |
71 | ) : (
72 |
73 |
74 |
75 |
76 |
77 |
@{project.projectUser[0].user.username}
78 |
79 |
80 | {project.projectUser[0].user.id === currentUser[0]?.id ? (
81 | !wasDeleted ? (
82 |
83 | X
84 |
85 | ) : (
86 | ''
87 | )
88 | ) : (
89 | ''
90 | )}
91 |
92 |
93 |
94 | {project.name}
95 |
96 |
{project.description}
97 |
98 |
99 |
100 | Language:
101 | {project.languages.name}
102 |
103 |
104 | Category:
105 | {project.categories.name}
106 |
107 |
108 | Beginner Friendly:
109 | {project.beginnerFriendly ? 'Yes' : 'No'}
110 |
111 |
112 |
113 | )}
114 |
115 | {project.projectUser[0].user.id === currentUser[0]?.id ? (
116 | ''
117 | ) : requestMessage ? (
118 |
119 |
120 | {requestMessage}
121 |
122 |
123 | ) : (
124 |
125 |
133 | Request to Collab
134 |
135 |
143 |
144 | You don't have the required languages on your profile. Spend some
145 | time learning them first, or look for a beginner friendly project.
146 |
147 |
148 |
149 | )}
150 |
151 | );
152 | };
153 |
154 | export default ProjectTile;
155 |
--------------------------------------------------------------------------------
/src/components/SingleProject/SingleProject.css:
--------------------------------------------------------------------------------
1 | .single-project {
2 | display: flex;
3 | flex-direction: column;
4 | margin-top: 50px;
5 | justify-content: center;
6 | align-items: center;
7 | }
8 | .project-info {
9 | display: flex;
10 | align-items: top;
11 | flex-direction: column;
12 | width: 50%;
13 | }
14 | .Project-messages {
15 | display: flex;
16 | flex-direction: column;
17 | text-align: left;
18 | justify-content: space-between;
19 | min-width: 700px;
20 | max-width: 700px;
21 | background-color: #1f2833;
22 | padding: 20px;
23 | border-radius: 25px;
24 | }
25 |
26 | #comment-input {
27 | display: flex;
28 | width: 70%;
29 | height: 50px;
30 | flex-direction: column;
31 |
32 | align-items: right;
33 | }
34 |
35 | #post {
36 | background-color: #45a29e;
37 | border: none;
38 | height: 50px;
39 | width: 100px;
40 | border-radius: 5px;
41 | color: white;
42 | font-size: 16px;
43 | margin-bottom: 10px;
44 | align-items: center;
45 | }
46 | a {
47 | color: #66fcf1;
48 | text-decoration: none;
49 | }
50 | a:visited {
51 | color: #45a293;
52 | }
53 | a:hover {
54 | color: white;
55 | }
56 |
57 | .profile-picture {
58 | height: 50px;
59 | width: 50px;
60 | border-radius: 25px;
61 | }
62 | .project-tile-wider {
63 | display: flex;
64 | flex-direction: column;
65 | text-align: left;
66 | background-color: #1f2833;
67 | padding: 30px;
68 | border-radius: 5px;
69 | min-width: 700px;
70 | max-width: 800px;
71 | /* margin: 60px; */
72 | }
73 | .project-tiles {
74 | display: flex;
75 | flex-direction: column;
76 | text-align: center;
77 | justify-content: center;
78 | background-color: #1f2833;
79 | padding: 30px;
80 | border-radius: 5px;
81 | min-width: 700px;
82 | max-width: 800px;
83 | /* margin: 20px; */
84 | }
85 | .display-flex {
86 | display: flex;
87 | flex-direction: column;
88 | gap: 1.5rem;
89 | }
90 | #create-repo-form {
91 | display: flex;
92 | flex-direction: column;
93 | align-items: center;
94 | justify-content: center;
95 | }
96 |
97 | .form-labels {
98 | font-size: 3rem;
99 | font-weight: bold;
100 | }
101 |
102 | .repo-form-input {
103 | height: 30px;
104 | width: 20rem;
105 | font-size: 2rem;
106 | color: #1f2833;
107 | margin-bottom: 40px;
108 | background-color: #45a29e;
109 | }
110 |
111 | #repo-privacy-field {
112 | height: 40px;
113 | width: 200px;
114 | font-size: 2rem;
115 | font-weight: bold;
116 | background-color: #45a29e;
117 | margin-top: 10px;
118 | }
119 |
120 | .create-repo-button {
121 | background-color: #45a29e;
122 | border: none;
123 | height: 40px;
124 | border-radius: 5px;
125 | color: white;
126 | font-size: 30px;
127 | max-width: 300px;
128 | margin-bottom: 10px;
129 | cursor: pointer;
130 | margin-top: 3rem;
131 | }
132 | .comment-picture {
133 | height: 30px;
134 | width: 30px;
135 | border-radius: 25px;
136 | margin-right: 20px;
137 | }
138 | .title {
139 | justify-content: center;
140 | }
141 | .display-flex-column {
142 | display: flex;
143 | flex-direction: column;
144 | gap: 1.5rem;
145 | }
146 | .row {
147 | display: flex;
148 | flex-direction: row;
149 | gap: 1.5rem;
150 | }
151 |
152 | .text-box {
153 | display: flex;
154 | justify-content: space-between;
155 | align-items: center;
156 | }
157 |
--------------------------------------------------------------------------------
/src/components/SingleProject/SingleProject.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { fetchProject } from "../../store/project";
4 | import { fetchComments } from "../../store/comments";
5 | import { compareLanguages } from "../../util";
6 | import { Link } from "react-router-dom";
7 | import "./SingleProject.css";
8 | import supabase from "../../client";
9 | import ProjectRepo from "../GithubCollab/ProjectRepo";
10 | import CreateRepo from "../GithubCollab/RepoCreation";
11 | import UserProfile from "../UserProfile/UserProfile";
12 | import { toast } from "react-toastify";
13 |
14 | const SingleProject = (props) => {
15 | const dispatch = useDispatch();
16 | const project = useSelector((state) => state.project);
17 | const [comments, setComments] = useState([]);
18 | const [comment, setComment] = useState({ body: "" });
19 | const [requestMessage, setRequestMessage] = useState("");
20 | const [wasDeleted, setWasDeleted] = useState("");
21 | const [projects, setProjects] = useState([]);
22 | const [user, setUser] = useState([]);
23 | const currentUser = supabase.auth.user();
24 | const [current, setCurrent] = useState([]);
25 | const isAdmin = false;
26 | //const currentUser = useSelector((state) => state.user);
27 | const { body } = comment;
28 | const [showRepoCreation, setShowRepoCreation] = useState(false);
29 |
30 | useEffect(() => {
31 | fetchCurrent();
32 | }, [currentUser]);
33 |
34 | useEffect(() => {
35 | dispatch(fetchProject(props.match.params.projectId));
36 | fetchComments(props.match.params.projectId);
37 | fetchUsers(props.match.params.projectId);
38 | }, []);
39 |
40 | async function fetchComments(projectId) {
41 | const { data } = await supabase
42 | .from("comments")
43 | .select("*, user(id , username, imageUrl)")
44 | .eq("projectId", projectId);
45 |
46 | setComments(data);
47 | }
48 | async function fetchCurrent() {
49 | if (currentUser) {
50 | const { data } = await supabase
51 | .from("user")
52 | .select("*")
53 | .eq("id", currentUser.id);
54 | setCurrent(data);
55 | }
56 | }
57 | async function createComment() {
58 | await supabase.from("comments").insert([
59 | {
60 | projectId: project.id,
61 | body: body,
62 | userId: currentUser.id,
63 | },
64 | ]);
65 | setComment({ body: "" });
66 | fetchComments(project.id);
67 | }
68 |
69 | async function createConversation() {
70 | const conversation = await supabase.from("conversation").insert([
71 | {
72 | conversation_name: project.name,
73 | projectId: project.id,
74 | },
75 | ]);
76 | if (conversation.error) {
77 | toast("Group conversation already exists");
78 | } else {
79 | user.map(async (member) => {
80 | const { error } = await supabase.from("conversation_member").insert([
81 | {
82 | user_id: member.user.id,
83 | conversation_id: conversation.data[0].conversation_id,
84 | },
85 | ]);
86 | if (error) {
87 | console.log("ERROR", error);
88 | }
89 | });
90 | }
91 | }
92 |
93 | async function fetchUsers(projectId) {
94 | let { data } = await supabase
95 | .from("projectUser")
96 | .select(`*, user(id , username, bio)"`)
97 | .eq("projectId", projectId);
98 |
99 | setUser(data);
100 | }
101 | const handleDelete = async () => {
102 | await supabase
103 | .from("projectUser")
104 | .delete()
105 | .match({ projectId: project.id });
106 | const { data, error } = await supabase
107 | .from("projects")
108 | .delete()
109 | .match({ id: project.id });
110 | setWasDeleted(true);
111 | setProjects();
112 | };
113 |
114 | const handleClick = async () => {
115 | const existingUser = await supabase
116 | .from("projectUser")
117 | .select("*")
118 | .eq("projectId", project.id)
119 | .eq("userId", currentUser.id);
120 |
121 | if (existingUser.data.length === 0) {
122 | const { data, error } = await supabase
123 | .from("projectUser")
124 | .insert([{ userId: currentUser.id, projectId: project.id }]);
125 | setRequestMessage(
126 | "Success! Your request to join this project was sent, and the owner has been notified."
127 | );
128 | } else {
129 | setRequestMessage(
130 | "You've already requested to join this project. The owner has been notified."
131 | );
132 | }
133 | };
134 | console.log("current", current);
135 | return !project ? (
136 | Loading project..
137 | ) : (
138 |
139 |
140 |
{project.name}
141 | {!project.projectUser ? null : (
142 |
143 |
147 |
{project.projectUser[0].user.username}
148 |
{project.projectUser[0].user.bio}
149 |
150 | )}
151 |
152 | {current.length === 0 ? null : !current[0].isAdmin ? null : (
153 |
154 |
155 | Delete Project
156 |
157 |
158 | )}
159 |
160 |
161 |
162 |
163 | {/*
164 | {!project.projectUser ?
loading project member...
: null}
165 |
*/}
166 | {/*
*/}
167 |
168 |
169 |
170 |
171 |
Project Description
172 | {project.description}
173 |
174 | Beginner Friendly:
175 | {project.beginnerFriendly ? "Yes" : "No"}
176 |
177 |
Language
178 | {project.languages ? project.languages.name : ""}
179 |
setShowRepoCreation(false)}
181 | project={project}
182 | setShowRepoCreation={setShowRepoCreation}
183 | />
184 |
185 | setShowRepoCreation(false)}
188 | project={project}
189 | />
190 |
191 |
192 |
Current Team Members of Project:
193 |
194 | {!user ? (
195 |
Loading group members...
196 | ) : (
197 |
198 |
199 | {user.map((use) => {
200 | if (use.isAccepted) {
201 | return (
202 |
203 |
204 |
{use.user.username}
205 |
206 | {use.user.bio ? `Bio: ${use.user.bio}` : null}{" "}
207 |
208 |
209 | );
210 | } else {
211 | return null;
212 | }
213 | })}
214 |
215 | {/*
216 | {project.projectUser[0].userId === currentUser?.id ? (
217 | ""
218 | ) : requestMessage ? (
219 |
220 |
221 | {requestMessage}
222 |
223 |
224 | ) : ( */}
225 | <>
226 |
230 | Create Conversation
231 |
232 |
233 |
241 | Request to Collab
242 |
243 |
251 |
252 | You don't have the required languages on your profile.
253 | Spend some time learning them first, or look for a
254 | beginner friendly project.
255 |
256 |
257 |
258 | >
259 | {/* )} */}
260 |
261 | )}
262 |
263 |
264 |
265 |
266 |
267 | {comments.map((comment) => (
268 |
269 |
270 |
274 | {comment.user.username}:
275 | {comment.body}
276 |
277 |
278 | ))}
279 |
280 |
281 |
282 |
283 |
284 | setComment({ ...comment, body: e.target.value })}
289 | />
290 |
291 |
292 | Post
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 | );
301 | };
302 |
303 | export default SingleProject;
304 |
--------------------------------------------------------------------------------
/src/components/UserProfile/BioModal.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import supabase from '../../client';
3 |
4 | const BioModal = (props) => {
5 | const user = supabase.auth.user();
6 | const [isUser, setIsUser] = useState(false);
7 |
8 | useEffect(() => {
9 | document.addEventListener('keydown', closePic, false);
10 |
11 | return function cleanup() {
12 | document.removeEventListener('keydown', closePic, false);
13 | };
14 | }, []);
15 |
16 | useEffect(() => {
17 | setIsUser(
18 | user.identities[0]['identity_data'].user_name === props.showBio.username
19 | );
20 | }, [props.showBio.username]);
21 |
22 | function closePic(e) {
23 | if (e.key === 'Escape') {
24 | props.onClose();
25 | }
26 | }
27 |
28 | if (!props.showBio.display) {
29 | return null;
30 | }
31 |
32 | return (
33 |
34 |
35 |
36 | X
37 |
38 |
39 |
e.stopPropagation()}>
40 |
41 | {user.identities[0]['identity_data'].user_name ===
42 | props.showBio.username ? (
43 | Your Bio
44 | ) : (
45 | {`${props.showBio.username}'s Bio`}
46 | )}
47 |
48 |
49 | {props.showBio.bio && !props.editingBio ? (
50 |
51 |
{props.showBio.bio}
52 |
53 | ) : props.editingBio ? (
54 |
55 |
56 | {
59 | let val = e.target.value;
60 | e.target.value = '';
61 | e.target.value = val;
62 | }}
63 | type="text"
64 | id="editing-bio-text"
65 | defaultValue={props.showBio.bio}
66 | onChange={(evt) => {
67 | props.setUserBio(evt.target.value);
68 | }}
69 | >
70 |
71 |
72 | ) : (
73 |
This user has no bio.
74 | )}
75 |
76 |
77 | {isUser ? (
78 |
79 | {props.editingBio ? (
80 | props.handleClick(e)}
84 | >
85 | Save
86 |
87 | ) : (
88 | props.setEditingBio(true)}
92 | >
93 | Edit
94 |
95 | )}
96 | {props.editingBio ? (
97 | props.setEditingBio(false)}
100 | >
101 | Cancel
102 |
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 |
46 | Send
47 |
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 | props.setTrigger(false)}
14 | >
15 | X
16 |
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 |
29 | X
30 |
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 |
55 | X
56 |
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 |
140 | {!loadingLanguages && isUser ? (
141 |
150 | Update Languages
151 |
152 | ) : null}
153 | {isUser ? null : (
154 |
155 |
156 | setMessageButtonPopup(true)}
165 | >
166 | Message
167 |
168 |
173 | Message
174 |
175 |
176 |
177 | )}
178 |
179 | {loadingLanguages ? (
180 |
187 | ) : null}
188 |
189 |
190 |
191 | Languages
192 |
193 |
194 | {user.id
195 | ? user.languages.map((language, i) => {
196 | return (
197 |
198 | {language.name}
199 |
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 |
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 |
70 | Accept
71 |
72 |
73 | Decline
74 |
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 |
setAdminButtonPopup(true)}>
134 | Add Language
135 |
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 |
183 | {' '}
184 |
185 | {current.length === 0 ? null : !current[0].isAdmin ? null : (
186 |
187 |
setAdminButtonPopup(true)}>
188 | Add Category
189 |
190 |
194 | Add Category
195 |
196 |
197 | )}
198 |
199 |
200 | Logout
201 |
202 |
203 |
204 | ) : (
205 |
206 |
Login
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 |
39 | Go
40 |
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 |
--------------------------------------------------------------------------------