├── .babelrc
├── .eslintrc.json
├── .gitignore
├── README.md
├── __tests__
├── db.js
├── react.js
└── supertest.js
├── client
├── App.jsx
├── assets
│ ├── GitGoodDash.png
│ └── GitGoodLogo.png
├── components
│ ├── Card.jsx
│ ├── ExpandedCard.css
│ ├── ExpandedCard.jsx
│ ├── GithubLogin.jsx
│ └── Topic.jsx
├── containers
│ ├── CardContainer.jsx
│ ├── Dashboard.jsx
│ └── Nav.jsx
├── index.html
├── index.js
└── stylesheets
│ └── styles.scss
├── package.json
├── server
├── controllers
│ ├── OAuthController.js
│ ├── sessionController.js
│ ├── subtopicController.js
│ ├── topicController.js
│ └── userController.js
├── db
│ └── db.js
├── routes
│ ├── api.js
│ └── github.js
└── server.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"]
3 | }
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": ["**/test", "**/__tests__"],
4 | "env": {
5 | "node": true,
6 | "browser": true,
7 | "es2021": true
8 | },
9 | "plugins": ["react"],
10 | "extends": ["eslint:recommended", "plugin:react/recommended"],
11 | "parserOptions": {
12 | "sourceType": "module",
13 | "ecmaFeatures": {
14 | "jsx": true
15 | }
16 | },
17 | "rules": {
18 | "indent": ["warn", 2],
19 | "no-unused-vars": ["off", { "vars": "local" }],
20 | "no-case-declarations": "off",
21 | "prefer-const": "warn",
22 | "quotes": ["warn", "single"],
23 | "react/prop-types": "off",
24 | "semi": ["warn", "always"],
25 | "space-infix-ops": "warn"
26 | },
27 | "settings": {
28 | "react": { "version": "detect"}
29 | }
30 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | .env
4 | dist
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
6 | # GitGood
7 |
8 | An open source software engineering study tool that allows users to create topic categories and save useful resources as flashcards.
9 |
10 | ## Want to Contribute?
11 |
12 | 1. Clone the repo and make a new branch
13 | 1. Add a feature, fix a bug, or refactor some code :)
14 | 1. Make sure to lint your code!
15 | 1. Write/update tests for the changes you made, if necessary.
16 | 1. Run unit & integration tests and make sure all tests pass: npm test.
17 | 1. Open a Pull Request with a comprehensive description of changes to the dev branch
18 |
19 | ## Technologies
20 |
21 | - React
22 | - Node
23 | - Express
24 | - bCrypt
25 | - JSON Web Tokens
26 | - PostgreSQL
27 | - MaterialUI
28 | - Jest
29 | - React Testing Library
30 | - Webpack
31 |
32 | ## Screenshots
33 | 
34 |
--------------------------------------------------------------------------------
/__tests__/db.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const db = require('../server/db/db');
4 | const { query } = require('express');
5 | import regeneratorRuntime from 'regenerator-runtime';
6 |
7 | /**
8 | * Like many testing frameworks, in Jest we use the "describe" function to
9 | * separate our tests into sections. They make your test outputs readable.
10 | *
11 | * You can place "beforeAll", "beforeEach", "afterAll", and "afterEach"
12 | * functions inside of "describe" blocks and they will only run for tests
13 | * inside that describe block. You can even nest describes within describes!
14 | */
15 | describe('db unit tests', () => {
16 | /**
17 | * Jest runs the "beforeAll" function once, before any tests are executed.
18 | * Here, we write to the file and then reset our database model. Then, we
19 | * invoke the "done" callback to tell Jest our async operations have
20 | * completed. This way, the tests won't start until the "database" has been
21 | * reset to an empty Array!
22 | */
23 |
24 | beforeAll(done => {
25 | done()
26 | })
27 |
28 | // afterAll(done => {
29 | // // Closing the DB connection allows Jest to exit successfully.
30 | // db.connection.close()
31 | // done()
32 | // })
33 |
34 | describe('#sync', () => {
35 | it('db test is working', () => {
36 | const sqlQuery = `SELECT * FROM Users`;
37 |
38 | db.query(sqlQuery,[],(err, result) => {
39 | // console.log(result)
40 | return expect(typeof result).toEqual('object');
41 | });
42 |
43 | });
44 |
45 | // TODO: Finish unit testing the sync function
46 |
47 | // it('overwrites previously existing markets', () => {
48 | // const sqlQuery = `SELECT * FROM Users`;
49 |
50 | // // console.log(process.env.PG_URI);
51 | // return db.query(sqlQuery)
52 | // .then((data) => expect(typeof data).toBe('object'))
53 | // .finally((done)=>done());
54 | // });
55 |
56 | it('db test', () => {
57 |
58 | });
59 |
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/__tests__/react.js:
--------------------------------------------------------------------------------
1 | import React from 'React';
2 | import userEvent from '@testing-library/user-event';
3 | import { render, screen, waitFor, fireEvent } from '@testing-library/react';
4 | import "@testing-library/jest-dom"
5 | import regeneratorRuntime from 'regenerator-runtime';
6 |
7 | import App from '../client/App';
8 |
9 | describe('Unit testing React components', () => {
10 | describe('App', () => {
11 | let app;
12 |
13 | beforeAll(() => {
14 | app = render();
15 | // console.log(text);
16 | });
17 |
18 | test('App has a h1 with text "Test"', () => {
19 | // console.log((text.getByText('Mega:')));
20 | expect(app.getByRole('heading')).toHaveTextContent('Test');
21 | // expect(text.getByText('Mega:')).toHaveStyle('font-weight: bold');
22 | });
23 | });
24 |
25 | describe('App2', () => {
26 | let app;
27 |
28 | beforeEach(async () => {
29 | let app = await render();
30 | // console.log(text);
31 | });
32 |
33 | test('App has a h1 with text "Test"', () => {
34 | // console.log((text.getByText('Mega:')));
35 | expect(screen.getByRole('heading')).toHaveTextContent('Test');
36 | // expect(text.getByText('Mega:')).toHaveStyle('font-weight: bold');
37 | });
38 |
39 | })
40 |
41 | });
--------------------------------------------------------------------------------
/__tests__/supertest.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 |
3 | const server = 'http://localhost:3000';
4 |
5 | /**
6 | * Read the docs! https://www.npmjs.com/package/supertest
7 | */
8 | describe('Route integration', () => {
9 | describe('/', () => {
10 | describe('GET', () => {
11 | // Note that we return the evaluation of `request` here! It evaluates to
12 | // a promise, so Jest knows not to say this test passes until that
13 | // promise resolves. See https://jestjs.io/docs/en/asynchronous
14 | it('responds with 200 status and text/html content type', () => request(server)
15 | .get('/')
16 | .expect('Content-Type', /text\/html/)
17 | .expect(200));
18 | });
19 |
20 | });
21 |
22 | describe('/github', () => {
23 | describe('/auth', () => {
24 |
25 | it('GET: responds with a redirect to github sign in', () => request(server)
26 | .get('/github/auth')
27 | .expect('Location', 'https://github.com/login/oauth/authorize?scope=user,repo&redirect_uri=http://localhost:3000/github/callback&client_id=27337f49cf34f7d2d21b')
28 | .expect(302));
29 | });
30 | });
31 |
32 | describe('/api', () => {
33 | describe('/topic', () => {
34 | describe('GET', () => {
35 | it('responds with json', () => {
36 | return request(server)
37 | .get('/api/topic')
38 | .set('Cookie', ['ssid=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFyaXNob2hhbSIsImlhdCI6MTY0NTM3OTIzNn0.B4yB2wVJL7quFTHATAUPcOD3Gpb_t5hn-R-XvGnk6YY'])
39 | .expect('Content-Type', /application\/json/)
40 | .expect(200)
41 | });
42 |
43 | it('responds with an array of objects', () => {
44 | return request(server)
45 | .get('/api/topic')
46 | .set('Cookie', ['ssid=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFyaXNob2hhbSIsImlhdCI6MTY0NTM3OTIzNn0.B4yB2wVJL7quFTHATAUPcOD3Gpb_t5hn-R-XvGnk6YY'])
47 | .then((response) => {
48 | expect(Array.isArray(response.body)).toBe(true);
49 | expect(typeof request.body[0]).toBe('object');
50 | })
51 | })
52 | });
53 |
54 | describe('POST', () => {
55 | it('Creating a new topic responds with an id number', () => {
56 | const body = {name: 'topic_test'}
57 | return request(server)
58 | .put('/markets')
59 | .set('Cookie', ['ssid=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFyaXNob2hhbSIsImlhdCI6MTY0NTM3OTIzNn0.B4yB2wVJL7quFTHATAUPcOD3Gpb_t5hn-R-XvGnk6YY'])
60 | .send(body)
61 | .then(response => {
62 | expect(typeof response.body).toEqual('number');
63 | });
64 | })
65 | })
66 | });
67 |
68 | });
69 |
70 | });
--------------------------------------------------------------------------------
/client/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CssBaseline from '@mui/material/CssBaseline';
3 | import { createTheme, ThemeProvider, styled } from '@mui/material/styles';
4 | import Dashboard from './containers/Dashboard.jsx';
5 |
6 | function App() {
7 | return (
8 |
9 |
10 | {/*
Test
*/}
11 |
12 |
13 | );
14 | }
15 |
16 | export default App;
--------------------------------------------------------------------------------
/client/assets/GitGoodDash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GitGoodOrg/GitGood/3e01cd70b11d1cabaad9601af2b7d58462d1d223/client/assets/GitGoodDash.png
--------------------------------------------------------------------------------
/client/assets/GitGoodLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GitGoodOrg/GitGood/3e01cd70b11d1cabaad9601af2b7d58462d1d223/client/assets/GitGoodLogo.png
--------------------------------------------------------------------------------
/client/components/Card.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Button } from '@mui/material';
3 |
4 | function Card(props) {
5 | function handleClick () {
6 | props.setTrigger(true);
7 | props.setEmojiText(props.cards.emoji);
8 | props.setCardText(props.cards.title);
9 | props.setBodyText(props.cards.text);
10 | props.setCurrentCardId(props.cards._id);
11 | }
12 |
13 | return(
14 |
15 |
{ props.cards.title }
16 |
{ props.cards.emoji + ' ' + props.cards.text}
17 |
18 |
19 | );
20 | }
21 |
22 | export default Card;
--------------------------------------------------------------------------------
/client/components/ExpandedCard.css:
--------------------------------------------------------------------------------
1 | /* .ExpandedCard {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | width: 100%;
6 | height: 100vh;
7 | background-color: rgba(0, 0, 0, 0.2);
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | }
12 |
13 | .ExpandedCard-inner {
14 | position: relative;
15 | padding: 32px;
16 | width: 100%;
17 | max-width: 640px;
18 | background-color: white
19 | }
20 |
21 | .ExpandedCard-inner .close-btn {
22 | position: absolute;
23 | top: 16px;
24 | right: 16px;
25 | } */
--------------------------------------------------------------------------------
/client/components/ExpandedCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | // import './ExpandedCard.css';
3 | import { TextField } from '@mui/material';
4 | import { Button } from '@mui/material';
5 |
6 | function ExpandedCard(props) {
7 |
8 | const handleSubmit = (e) => {
9 | e.preventDefault();
10 | props.setTrigger(false);
11 | if(props.currentCardId) {
12 | props.updateCard(props.currentCardId);
13 | }
14 | else {
15 | props.addCard();
16 | }
17 | };
18 |
19 | return (props.trigger) ? (
20 |
21 |
22 |
23 |
29 |
30 |
31 | ) : '';
32 | }
33 | // sx={{ m: 0.5 }}
34 | export default ExpandedCard;
--------------------------------------------------------------------------------
/client/components/GithubLogin.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@mui/material';
2 | import GitHubIcon from '@mui/icons-material/GitHub';
3 | import React from 'react';
4 |
5 | export default function App() {
6 | return (
7 | }
10 | onClick={()=>location.href = '/github/auth'}
11 | >
12 | github
13 |
14 | );
15 | }
--------------------------------------------------------------------------------
/client/components/Topic.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Button } from '@mui/material';
3 |
4 | function Topic(props) {
5 | return (
6 |
7 |
8 | props.getCards(props.topic_id)}>
9 | {props.topics}
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | export default Topic;
18 |
--------------------------------------------------------------------------------
/client/containers/CardContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Card from '../components/Card.jsx';
3 | import ExpandedCard from '../components/ExpandedCard.jsx';
4 | import { Button } from '@mui/material';
5 |
6 | function CardContainer(props) {
7 | const [ buttonPopup, setButtonPopup ] = useState(false);
8 | const [ currentCardId, setCurrentCardId ] = useState(undefined);
9 |
10 |
11 | const cardsFeed = [];
12 | for (let i = 0; i < props.cards.length; i++) {
13 | cardsFeed.push();
14 | }
15 |
16 | return(
17 |
18 |
{props.currentTopicName}
19 |
24 |
25 |
26 | {cardsFeed}
27 |
45 |
46 |
47 | );
48 | }
49 |
50 | export default CardContainer;
--------------------------------------------------------------------------------
/client/containers/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Nav from './Nav.jsx';
3 | import CardContainer from './CardContainer.jsx';
4 | import GithubLogin from '../components/GithubLogin';
5 | import { Button } from '@mui/material';
6 |
7 | function Dashboard() {
8 | //All the topics in key value pairs {_id: name}
9 | const [ topics, setTopics ] = useState({});
10 | //All the cards for a topic in an array of objects [{card}, {card}]
11 | const [ cards, setCards ] = useState([]);
12 | const [ emojis, setEmojis ] = useState([]);
13 | const [ bodies, setBodies ] = useState([]);
14 | //the current topic id that the user is looking at
15 | const [ currentTopicId, setCurrentTopicId ] = useState();
16 |
17 | const [ topicText, setTopicText ] = useState('');
18 | const [ cardText, setCardText ] = useState('');
19 | const [ emojiText, setEmojiText ] = useState(''); //might be dropdown later
20 | const [ bodyText, setBodyText ] = useState('');
21 |
22 | useEffect(() => {
23 | getTopics();
24 | },[]);
25 |
26 | const getTopics = () => {
27 | const url = 'http://localhost:3000/api/topic';
28 | fetch(url)
29 | .then((data) => {
30 | return data.json();
31 | })
32 | .then((data) => {
33 | setTopics(data);
34 | })
35 | .catch((err) => console.log('err', err));
36 | };
37 |
38 | const getCards = (topic_id) => {
39 | const url = `http://localhost:3000/api/subtopic/${topic_id}`;
40 | fetch(url)
41 | .then(data => data.json())
42 | .then(data => {
43 | console.log(data);
44 | setCards(data);
45 | setCurrentTopicId(topic_id);
46 | });
47 | };
48 |
49 | const topicSubmit = (e) => {
50 | // const topicTitle = e.target[0].value;
51 | e.preventDefault();
52 | fetch('http://localhost:3000/api/topic', {
53 | method: 'Post',
54 | headers: {
55 | 'Content-Type': 'application/json'
56 | },
57 | body: JSON.stringify({
58 | topic_name: topicText,
59 | })
60 | })
61 | .then((data) => data.json())
62 | .then((data) => {
63 | console.log(data);
64 | const topicsCopy = {...topics};
65 | topicsCopy[data._id] = data.topic_name;
66 | setTopics(topicsCopy);
67 | setTopicText('');
68 | });
69 | };
70 |
71 | const deleteTopic = (topic_id) => {
72 | fetch(`http://localhost:3000/api/topic/${topic_id}`, {
73 | method: 'DELETE',
74 | headers: {
75 | 'Content-Type': 'application/json'
76 | }
77 | })
78 | .then(data => {
79 | console.log(data);
80 | const topicsCopy = {...topics};
81 | delete topicsCopy[topic_id];
82 | setTopics(topicsCopy);
83 | });
84 | };
85 |
86 | const addCard = () => {
87 | // const topicTitle = e.target[0].value;
88 | fetch('http://localhost:3000/api/subtopic/', {
89 | method: 'Post',
90 | headers: {
91 | 'Content-Type': 'application/json'
92 | },
93 | body: JSON.stringify({
94 | topic_id: currentTopicId,
95 | title: cardText,
96 | emoji: emojiText,
97 | text: bodyText,
98 | })
99 | })
100 | .then((data) => data.json())
101 | .then((data) => {
102 | // console.log(data);
103 | setCards([...cards, data]);
104 | setEmojiText('');
105 | setCardText('');
106 | setBodyText('');
107 | });
108 | };
109 |
110 |
111 | const deleteCard = (card_id) => {
112 | fetch(`http://localhost:3000/api/subtopic/${card_id}`, {
113 | method: 'DELETE',
114 | headers: {
115 | 'Content-Type': 'application/json'
116 | }
117 | })
118 | .then(data => data.json())
119 | .then(data => {
120 | console.log(data);
121 | const cardsCopy = [...cards];
122 | let index;
123 | cardsCopy.forEach((cur, i) =>{
124 | if (cur._id === card_id) index = i;
125 | });
126 | cardsCopy.splice(index, 1);
127 | setCards(cardsCopy);
128 | });
129 | };
130 |
131 | const updateCard = (card_id) => {
132 | fetch('http://localhost:3000/api/subtopic/', {
133 | method: 'PUT',
134 | headers: {
135 | 'Content-Type': 'application/json'
136 | },
137 | body: JSON.stringify({
138 | _id: card_id,
139 | title: cardText,
140 | emoji: emojiText,
141 | text: bodyText,
142 | })
143 | })
144 | .then((data) => data.json())
145 | .then(data => {
146 | console.log(data);
147 | const cardsCopy = [...cards];
148 | let index;
149 | cardsCopy.forEach((cur, i) =>{
150 | if (cur._id === card_id) index = i;
151 | });
152 | cardsCopy[index] = data;
153 | setCards(cardsCopy);
154 | setEmojiText('');
155 | setCardText('');
156 | setBodyText('');
157 | });
158 | };
159 |
160 | // fx cardSubmit will submit the card, the body, the emoji
161 | const cardSubmit = (e) => {
162 | e.preventDefault();
163 | const emojiValue = e.target[0].value;
164 | const cardTitleValue = e.target[1].value;
165 | const bodyValue = e.target[2].value;
166 | console.log(e.target[0].value);
167 | setCards([...cards, cardText]);
168 | setCardText('');
169 | // bodySubmit();
170 | // emojiSubmit();
171 | // fetch('http://localhost:3000/subtopic', {
172 | // method: 'Post',
173 | // headers: {
174 | // 'Content-Type': 'application/json'
175 | // },
176 | // body: {
177 | // Emoji: emojiValue,
178 | // SubTopic: cardTitleValue,
179 | // body: bodyValue
180 | // }
181 | // })
182 |
183 | };
184 |
185 | // cardSubmit inner functions
186 | const emojiSubmit = (e) => {
187 | e.preventDefault();
188 | setEmojis([...emojis, emojiText]);
189 | setEmojiText('');
190 | };
191 |
192 | const bodySubmit = (e) => {
193 | e.preventDefault();
194 | setBodies([...bodies, bodyText]);
195 | setBodyText('');
196 | };
197 |
198 | const topicTextEntry = (e) => {
199 | // console.log(e);
200 | setTopicText(e.target.value);
201 | };
202 |
203 | const bodyTextEntry = (e) => {
204 | setBodyText(e.target.value);
205 | };
206 |
207 | const emojiTextEntry = (e) => {
208 | setEmojiText(e.target.value);
209 | };
210 |
211 | const cardTextEntry = (e) => {
212 | setCardText(e.target.value);
213 | };
214 |
215 | return (
216 |
217 |
223 |
224 | {typeof topics === 'object' &&
225 |
233 | }
234 | {currentTopicId && typeof topics === 'object' &&
235 |
254 | }
255 |
256 |
257 | );
258 | }
259 |
260 | export default Dashboard;
--------------------------------------------------------------------------------
/client/containers/Nav.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Topic from '../components/Topic.jsx';
3 | import { Button } from '@mui/material';
4 | import { TextField } from '@mui/material';
5 |
6 |
7 | function Nav(props) {
8 | const topicsFeed = [];
9 | // iterate through props.topics
10 | for (const topic_id in props.topics) {
11 | topicsFeed.push();
12 | }
13 |
14 | return(
15 |
16 |
Topics
17 |
21 |
22 | {topicsFeed}
23 |
24 | );
25 | }
26 |
27 | export default Nav;
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Git Good
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from './App';
4 | import styles from './stylesheets/styles.scss';
5 | import '@fontsource/roboto';
6 |
7 | render(
8 | ,
9 | document.getElementById('app'),
10 | );
11 |
--------------------------------------------------------------------------------
/client/stylesheets/styles.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Cabin&family=Oswald:wght@200&family=Roboto+Mono:wght@500&display=swap');
2 |
3 | * {
4 | font-family: 'Roboto Mono', monospace;
5 | }
6 |
7 | .App {
8 | background-color: white;
9 | margin: 1rem;
10 | }
11 |
12 | .GitHub {
13 | margin-left: 10px;
14 | margin-top: 10px;
15 | }
16 |
17 | .Dashboard {
18 | text-align: center;
19 | }
20 |
21 | .GitGoodTitle{
22 | font-weight: bold;
23 | font-size: 75px;
24 | margin: 0;
25 | }
26 |
27 | .Tagline{
28 | margin-top: 5px;
29 | font-size:20px;
30 | }
31 |
32 | header{
33 | margin-bottom: 1rem;
34 | }
35 |
36 | .containers{
37 | display: flex;
38 | }
39 |
40 | .Nav {
41 | display: flex;
42 | flex-direction: column;
43 | outline: 2px solid #1976D2;
44 | border-radius: 10px;
45 | padding: 25px 25px;
46 | background-color: white;
47 | margin-left: 5px;
48 | }
49 |
50 | .CardContainer {
51 | outline: 2px solid #1976D2;
52 | border-radius: 10px;
53 | padding: 25px 50px;
54 | width: 65%;
55 | background-color: white;
56 | margin-left: 10px;
57 | }
58 |
59 | .Card {
60 | display: flex;
61 | flex-direction: column;
62 | outline: 1px solid #1976D2;
63 | margin: 5px;
64 | border-radius: 10px;
65 | background-color: white;
66 | position: relative;
67 | box-shadow: 5px 5px 5px 1px #DAE1E7;
68 | }
69 |
70 | .Topic {
71 | text-align: left;
72 | // outline: 2px solid black;
73 | padding-left: 20px;
74 | outline: 1px solid #1976D2;
75 | border-radius: 10px;
76 | margin-top: 10px;
77 | background-color: white;
78 | max-width: 300px ;
79 | box-shadow: 5px 5px 5px 1px #DAE1E7;
80 | }
81 |
82 | .Topic h3 {
83 | display: flex;
84 | justify-content: space-between;
85 | }
86 |
87 | .deleteButtons {
88 | border-radius: 15px;
89 | background-color: black;
90 | color: white;
91 | margin-right: 15px;
92 |
93 | }
94 |
95 | .entryForm {
96 | border-radius: 20px;
97 | height: 30px;
98 | width: 250px;
99 | }
100 |
101 | .submitButtons {
102 | border-radius: 10px;
103 | margin-left: 5px;
104 | }
105 |
106 | .subtopicsContainer{
107 | display: grid;
108 | grid-template-columns: 1fr 1fr 1fr 1fr;
109 | // flex-direction: row;
110 | // flex-wrap: wrap;
111 | margin: 4px;
112 | }
113 |
114 | .ExpandedCard {
115 | // display: flex;
116 | position: fixed;
117 | top: 0;
118 | left: 0;
119 | width: 100%;
120 | height: 100vh;
121 | background-color: rgba(0, 0, 0, 0.2);
122 | display: flex;
123 | justify-content: center;
124 | align-items: center;
125 | border-radius: 15px;
126 | }
127 |
128 | .ExpandedCard-inner {
129 | position: relative;
130 | padding: 32px;
131 | width: 100%;
132 | max-width: 640px;
133 | background-color: white;
134 | border-radius: 15px;
135 | }
136 |
137 | .ExpandedCard-inner .close-btn {
138 | position: absolute;
139 | top: 16px;
140 | right: 16px;
141 | }
142 |
143 | .subtopicBody {
144 | height: 300px;
145 | width: 100%;
146 | line-height: normal;
147 | justify-content: left;
148 | border-radius: 15px;
149 | float: right;
150 | border-color: lightgray;
151 | padding: 1em;
152 | }
153 |
154 | .addSubtopic {
155 | border-radius: 10px;
156 | }
157 |
158 | .titleBody {
159 | justify-content: left;
160 | border-radius: 15px;
161 | height:25px;
162 | width: 325px;
163 | margin-bottom: 4px;
164 | }
165 |
166 | .emojiBody {
167 | justify-content: left;
168 | border-radius: 15px;
169 | height: 25px;
170 | margin-bottom: 4px;
171 |
172 | }
173 |
174 | .cardButton {
175 | position: absolute;
176 | bottom: 0;
177 | align-self: center;
178 | // justify-self: flex-end;
179 | }
180 |
181 | .entryForm {
182 | border: 2px solid #1976D2;
183 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "study-tool",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "cross-env NODE_ENV=production webpack",
8 | "dev": "concurrently \"cross-env NODE_ENV=development webpack-dev-server --open /\" \"cross-env NODE_ENV=development nodemon ./server/server.js\"",
9 | "start": "nodemon ./server/server.js",
10 | "test": "jest --verbose"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/Goblin-Shark-31/scratch.git"
15 | },
16 | "author": "",
17 | "license": "ISC",
18 | "bugs": {
19 | "url": "https://github.com/arishoham/solo-project/issues"
20 | },
21 | "homepage": "https://github.com/arishoham/solo-project#readme",
22 | "jest": {
23 | "testEnvironment": "jest-environment-jsdom",
24 | "setupFilesAfterEnv": [
25 | "@testing-library/jest-dom/extend-expect"
26 | ]
27 | },
28 | "dependencies": {
29 | "@emotion/react": "^11.7.1",
30 | "@emotion/styled": "^11.6.0",
31 | "@fontsource/roboto": "^4.5.3",
32 | "@mui/icons-material": "^5.4.2",
33 | "@mui/material": "^5.4.2",
34 | "bcryptjs": "^2.4.3",
35 | "cookie-parser": "^1.4.6",
36 | "cors": "^2.8.5",
37 | "dotenv": "^16.0.0",
38 | "jsonwebtoken": "^8.5.1",
39 | "node-fetch": "^2.6.7",
40 | "pg": "^8.7.3",
41 | "react": "^17.0.2",
42 | "react-dom": "^17.0.2",
43 | "react-hot-loader": "^4.13.0"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "^7.17.2",
47 | "@babel/preset-env": "^7.16.11",
48 | "@babel/preset-react": "^7.16.7",
49 | "@testing-library/jest-dom": "^5.16.2",
50 | "@testing-library/react": "^12.1.3",
51 | "@testing-library/user-event": "^13.5.0",
52 | "babel-loader": "^8.2.3",
53 | "clean-webpack-plugin": "^4.0.0",
54 | "concurrently": "^7.0.0",
55 | "cross-env": "^7.0.3",
56 | "css-loader": "^6.6.0",
57 | "eslint": "^8.9.0",
58 | "eslint-plugin-react": "^7.28.0",
59 | "html-webpack-plugin": "^5.5.0",
60 | "jest": "^27.5.1",
61 | "jest-environment-jsdom": "^27.5.1",
62 | "nodemon": "^2.0.15",
63 | "react-test-renderer": "^17.0.2",
64 | "regenerator-runtime": "^0.13.9",
65 | "sass": "^1.49.7",
66 | "sass-loader": "^12.6.0",
67 | "style-loader": "^3.3.1",
68 | "supertest": "^6.2.2",
69 | "webpack": "^5.68.0",
70 | "webpack-cli": "^4.9.2",
71 | "webpack-dev-server": "^4.7.4",
72 | "webpack-hot-middleware": "^2.25.1"
73 | },
74 | "nodemonConfig": {
75 | "ignore": [
76 | "server/data/*",
77 | "client/*"
78 | ]
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/server/controllers/OAuthController.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch');
2 |
3 | const OAuthController = {};
4 |
5 |
6 | OAuthController.getToken = async (req, res, next) => {
7 | const requestToken = req.query.code;
8 | const url = `https://github.com/login/oauth/access_token?client_id=${process.env.CLIENT_ID}&client_secret=${process.env.CLIENT_SECRET}&code=${requestToken}&scope=user,repo`;
9 |
10 | try {
11 | const tokenJSON = await fetch(url, {
12 | method: 'post',
13 | headers: {'Accept': 'application/json'}
14 | });
15 | const token = await tokenJSON.json();
16 | res.locals.access_token = token.access_token;
17 |
18 | next();
19 | } catch(err) {
20 | return next({
21 | log: `Error in OAuthController.getToken Err: ${err.message}`,
22 | status: 500,
23 | message: { err: 'An error occurred' },
24 | });
25 | }
26 | // console.log(token)
27 | };
28 |
29 | OAuthController.getProfile = async (req, res, next) => {
30 | const url = 'https://api.github.com/user';
31 | try {
32 | const profileInfoJSON = await fetch(url,{
33 | method: 'get',
34 | headers: {
35 | Accept: 'application/json',
36 | Authorization: 'token ' + res.locals.access_token,
37 | },
38 | });
39 | const profileInfo = await profileInfoJSON.json();
40 | res.locals.profile = profileInfo;
41 | next();
42 | } catch(err) {
43 | return next({
44 | log: `Error in OAuthController.getProfile Err: ${err.message}`,
45 | status: 500,
46 | message: { err: 'An error occurred' },
47 | });
48 | }
49 | };
50 |
51 | // OAuthController.getRepo = async (req, res, next) => {
52 | // const url = res.locals.profile.data.repos_url
53 | // let repoInfo;
54 | // try {
55 | // repoInfo = await axios({
56 | // method: 'get',
57 | // url,
58 | // headers: {
59 | // accept: 'application/json',
60 | // Authorization: 'token ' + req.cookies.ssid,
61 | // },
62 | // });
63 | // res.locals.repo = repoInfo;
64 | // next();
65 | // } catch(err) {
66 | // return next(err);
67 | // }
68 | // }
69 |
70 | module.exports = OAuthController;
71 |
--------------------------------------------------------------------------------
/server/controllers/sessionController.js:
--------------------------------------------------------------------------------
1 | const { query } = require('express');
2 | const db = require('../db/db');
3 | var jwt = require('jsonwebtoken');
4 |
5 | const sessionController = {};
6 |
7 | //Check if user is already logged in
8 | sessionController.isLoggedIn = (req, res, next) => {
9 | try {
10 | if(req.cookies.ssid) {
11 | const decoded = jwt.verify(req.cookies.ssid, process.env.SECRET_KEY);
12 | if(decoded.username !== undefined) {
13 | res.locals.username = decoded.username;
14 | return next();
15 | }
16 | else {
17 | //JWT exists but is not verified
18 | return res
19 | .clearCookie('access_token')
20 | .json('not logged in');
21 | }
22 | //JWT does not exist
23 | } else {
24 | //HARD CODED USERNAME TO FIX CORS ISSUE
25 | if(process.env.NODE_ENV === 'development') {
26 | res.locals.username = 'nlakshman';
27 | return next();
28 | } else {
29 | return res
30 | .json('not logged in');
31 | }
32 | }
33 | } catch(err) {
34 | return next({
35 | log: `Cannot check if user is logged in (sessionController.isLoggedIn) Err: ${err.message}`,
36 | status: 500,
37 | message: { err: 'An error occurred' },
38 | });
39 | }
40 | };
41 |
42 | // add session JWT to cookies
43 | sessionController.startSession = (req, res, next) => {
44 | try {
45 | const username = res.locals.profile.login;
46 | const email = res.locals.profile.email;
47 | const token = jwt.sign({ username: username }, process.env.SECRET_KEY);
48 | res.cookie('ssid', token); //{httpOnly: true}
49 | return next();
50 | } catch(err) {
51 | return next({
52 | log: `Cannot start session. Error in sessionController.startSession Err: ${err.message}`,
53 | status: 500,
54 | message: { err: 'An error occurred' },
55 | });
56 | }
57 | };
58 |
59 | module.exports = sessionController;
60 |
--------------------------------------------------------------------------------
/server/controllers/subtopicController.js:
--------------------------------------------------------------------------------
1 | const db = require('../db/db');
2 |
3 | const subtopicController = {};
4 |
5 | subtopicController.getSubtopics = (req, res, next) => {
6 |
7 | const {topic_id} = req.params;
8 |
9 | const sqlQuery = 'SELECT * FROM subtopics WHERE topic_id=$1';
10 |
11 | db.query(sqlQuery, [topic_id])
12 | .then(payload => {
13 | res.locals.subtopics = payload.rows;
14 | next();
15 | }).catch(err => {
16 | return next({
17 | log: `subtopicController.getSubtopics: ERROR: ${typeof err === 'object' ? JSON.stringify(err) : err}`,
18 | message: { err: 'Error occurred in subtopicController.getSubtopics. Check server log for more details.'},
19 | });
20 | });
21 | };
22 |
23 | subtopicController.postSubtopic = (req, res, next) => {
24 | // const {topic_id} = req.params;
25 | let {topic_id, emoji, title, text, progress} = req.body;
26 | emoji = emoji || '';
27 | title = title || 'Title Holder'; //for dev only
28 | text = text || '';
29 | progress = progress || 0;
30 |
31 | const sqlQuery = `INSERT INTO subtopics (topic_id, emoji, title, text, progress)
32 | VALUES ($1, $2, $3, $4, $5) RETURNING *`;
33 | // console.log('topic_id',topic_id, emoji, title, text, progress);
34 | db.query(sqlQuery, [topic_id, emoji, title, text, progress])
35 | .then(payload => {
36 | res.locals.subtopic = payload.rows[0];
37 | next();
38 | }).catch(err => {
39 | return next({
40 | log: `subtopicController.postSubtopic: ERROR: ${typeof err === 'object' ? JSON.stringify(err) : err}`,
41 | message: { err: 'Error occurred in subtopicController.postSubtopic. Check server log for more details.'},
42 | });
43 | });
44 | };
45 |
46 | subtopicController.deleteSubtopic = (req, res, next) => {
47 | const {id} = req.params;
48 |
49 | const sqlQuery = 'DELETE FROM subtopics WHERE _id=$1 RETURNING *';
50 |
51 | db.query(sqlQuery, [id])
52 | .then(payload => {
53 | res.locals.subtopic = payload.rows[0];
54 | next();
55 | }).catch(err => {
56 | return next({
57 | log: `subtopicController.deleteSubtopic: ERROR: ${typeof err === 'object' ? JSON.stringify(err) : err}`,
58 | message: { err: 'Error occurred in subtopicController.deleteSubtopic. Check server log for more details.'},
59 | });
60 | });
61 | };
62 |
63 | subtopicController.putSubtopic = (req, res, next) => {
64 |
65 | let {emoji, title, text, progress, _id} = req.body;
66 | emoji = emoji || '';
67 | title = title || 'Title Holder'; //for dev only
68 | text = text || '';
69 | progress = progress || 0;
70 |
71 | const sqlQuery = 'UPDATE subtopics SET emoji=$1, title=$2, text=$3, progress=$4 WHERE _id=$5 RETURNING *';
72 |
73 | db.query(sqlQuery, [emoji, title, text, progress, _id])
74 | .then(payload => {
75 | res.locals.subtopic = payload.rows[0];
76 | next();
77 | }).catch(err => {
78 | return next({
79 | log: `subtopicController.putSubtopic: ERROR: ${typeof err === 'object' ? JSON.stringify(err) : err}`,
80 | message: { err: 'Error occurred in subtopicController.putSubtopic. Check server log for more details.'},
81 | });
82 | });
83 | };
84 |
85 | module.exports = subtopicController;
86 |
--------------------------------------------------------------------------------
/server/controllers/topicController.js:
--------------------------------------------------------------------------------
1 | const db = require('../db/db');
2 |
3 | const topicController = {};
4 |
5 | topicController.getTopics = (req, res, next) => {
6 |
7 | const username = res.locals.username;
8 |
9 | const sqlQuery = 'SELECT * FROM topics WHERE username=$1';
10 |
11 | db.query(sqlQuery,[username])
12 | .then(payload => {
13 | const result = {};
14 | payload.rows.forEach((curr) => {
15 | result[curr._id] = curr.topic_name;
16 | });
17 | res.locals.topics = result;
18 | next();
19 | }).catch(err => {
20 | return next({
21 | log: `topicController.getTopics: ERROR: ${typeof err === 'object' ? JSON.stringify(err) : err}`,
22 | message: { err: 'Error occurred in topicController.getTopics. Check server log for more details.'},
23 | });
24 | });
25 | };
26 |
27 | topicController.postTopic = (req, res, next) => {
28 | const username = res.locals.username;
29 | const {topic_name} = req.body;
30 | console.log(req.body);
31 |
32 | const sqlQuery = 'INSERT INTO topics (username, topic_name) VALUES ($1, $2) RETURNING *';
33 |
34 | db.query(sqlQuery,[username, topic_name])
35 | .then(payload => {
36 | res.locals.topic = payload.rows[0];
37 | next();
38 | }).catch(err => {
39 | return next({
40 | log: `topicController.postTopics: ERROR: ${typeof err === 'object' ? JSON.stringify(err) : err}`,
41 | message: { err: 'Error occurred in topicController.postTopics. Check server log for more details.'},
42 | });
43 | });
44 | };
45 |
46 | topicController.deleteTopic = (req, res, next) => {
47 | const {id} = req.params;
48 |
49 | const sqlQuery = 'DELETE FROM topics WHERE _id=$1 RETURNING *';
50 |
51 | db.query(sqlQuery,[id])
52 | .then(payload => {
53 | res.locals.topic = payload.rows[0];
54 | next();
55 | }).catch(err => {
56 | return next({
57 | log: `topicController.deleteTopics: ERROR: ${typeof err === 'object' ? JSON.stringify(err) : err}`,
58 | message: { err: 'Error occurred in topicController.deleteTopics. Check server log for more details.'},
59 | });
60 | });
61 | };
62 |
63 | module.exports = topicController;
64 |
--------------------------------------------------------------------------------
/server/controllers/userController.js:
--------------------------------------------------------------------------------
1 | const { query } = require('express');
2 | const db = require('../db/db');
3 | //bringing in the object we exported that has a .query method to query the pool
4 | // require('dotenv').config();
5 |
6 | // const bcrypt = require('bcryptjs');
7 |
8 | const userController = {};
9 |
10 | // add user to database
11 | userController.addUser = async (req, res, next) => {
12 | const username = res.locals.profile.login;
13 | const email = res.locals.profile.email;
14 | const token = res.locals.access_token;
15 | try {
16 | const sqlQuery = `
17 | INSERT INTO Users (username, email, token)
18 | VALUES($1,$2, $3)
19 | ON CONFLICT (username) DO UPDATE
20 | SET token = EXCLUDED.token;
21 | `;
22 | await db.query(sqlQuery, [username, email, token]);
23 | return next();
24 | } catch(err) {
25 | return next({
26 | log: `Cannot add user to database
27 | (userController.addUser) Err: ${err.message}`,
28 | status: 400,
29 | message: { err: 'An error occurred' },
30 | });
31 | }
32 | };
33 |
34 | // userController.findUser = async (req, res, next) => {
35 | // try {
36 | // const {username, password} = req.body;
37 | // const sqlQuery = `
38 | // SELECT *
39 | // FROM Users
40 | // WHERE username = $1;
41 | // `;
42 | // const user = await db.query(sqlQuery, [username]);
43 | // if(!user.rows[0]) return res.json({status: false});
44 | // const verify = await bcrypt.compare(password, user.rows[0].password);
45 | // if(verify) return next();
46 | // else return res.json({status: false});
47 | // } catch(err) {
48 | // return next({
49 | // log: `Cannot find user in database Err: ${err.message}`,
50 | // status: 400,
51 | // message: { err: 'An error occurred' },
52 | // });
53 | // }
54 | // };
55 |
56 | module.exports = userController;
57 |
--------------------------------------------------------------------------------
/server/db/db.js:
--------------------------------------------------------------------------------
1 | const { Pool } = require('pg');
2 | require('dotenv').config();
3 |
4 | // create a new pool here using the connection string above
5 | const pool = new Pool({
6 | connectionString: process.env.PG_URI
7 | });
8 |
9 | // Adding some notes about the database here will be helpful for future you or other developers.
10 | // Schema for the database can be found below:
11 | // https://github.com/CodesmithLLC/unit-10SB-databases/blob/master/docs/assets/images/schema.png
12 |
13 | // We export an object that contains a property called query,
14 | // which is a function that returns the invocation of pool.query() after logging the query
15 | // This will be required in the controllers to be the access point to the database
16 | module.exports = {
17 | query: (text, params, callback) => {
18 | console.log('executed query', text);
19 | return pool.query(text, params, callback);
20 | }
21 | };
22 |
23 |
24 | //Trying to figure out promise based query
25 | // module.exports = {
26 | // query: pool
27 | // .query('SELECT $1::text as name', ['brianc'])
28 | // .then(res => console.log(res.rows[0].name)) // brianc
29 | // .catch(err => console.error('Error executing query', err.stack))
30 | // }
--------------------------------------------------------------------------------
/server/routes/api.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 |
3 | const sessionController = require('../controllers/sessionController');
4 | const topicController = require('../controllers/topicController');
5 | const subtopicController = require('../controllers/subtopicController');
6 |
7 | const router = express.Router();
8 |
9 | // TOPIC - CRUD routes
10 | router.get('/topic',
11 | sessionController.isLoggedIn,
12 | topicController.getTopics,
13 | (req, res) => res.status(200).json(res.locals.topics)
14 | );
15 |
16 | router.post('/topic/',
17 | sessionController.isLoggedIn,
18 | topicController.postTopic,
19 | (req, res) => res.status(200).json(res.locals.topic)
20 | );
21 |
22 | router.delete('/topic/:id',
23 | sessionController.isLoggedIn,
24 | topicController.deleteTopic,
25 | (req, res) => res.status(200).json(res.locals.topic)
26 | );
27 |
28 | // SUBTOPIC - CRUD routes
29 | router.get('/subtopic/:topic_id',
30 | sessionController.isLoggedIn,
31 | subtopicController.getSubtopics,
32 | (req, res) => res.status(200).json(res.locals.subtopics)
33 | );
34 |
35 | router.post('/subtopic/',
36 | sessionController.isLoggedIn,
37 | subtopicController.postSubtopic,
38 | (req, res) => res.status(200).json(res.locals.subtopic)
39 | );
40 |
41 | router.delete('/subtopic/:id',
42 | sessionController.isLoggedIn,
43 | subtopicController.deleteSubtopic,
44 | (req, res) => res.status(200).json(res.locals.subtopic)
45 | );
46 |
47 | router.put('/subtopic/',
48 | sessionController.isLoggedIn,
49 | subtopicController.putSubtopic,
50 | (req, res) => res.status(200).json(res.locals.subtopic)
51 | );
52 |
53 | module.exports = router;
54 |
--------------------------------------------------------------------------------
/server/routes/github.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 |
3 | const sessionController = require('../controllers/sessionController');
4 | const userController = require('../controllers/userController');
5 | const OAuthController = require('../controllers/OAuthController');
6 |
7 | const router = express.Router();
8 |
9 | //test comment dev branch
10 | //sign up!
11 | router.get('/auth', (req, res) => {
12 | console.log('got auth request');
13 | const url = 'https://github.com/login/oauth/authorize?'
14 | + 'scope=user,repo&'
15 | + 'redirect_uri=http://localhost:3000/github/callback&'
16 | + 'client_id=' + process.env.CLIENT_ID;
17 | res.redirect(url);
18 |
19 | });
20 |
21 | router.get('/callback',
22 | OAuthController.getToken,
23 | OAuthController.getProfile,
24 | sessionController.startSession,
25 | userController.addUser,
26 | (req, res) => {
27 | // FOR DEV SERVER ONLY
28 | if(process.env.NODE_ENV === 'development') {
29 | res.redirect('http://localhost:8080/');
30 | } else {
31 | res.redirect('/');
32 | }
33 | });
34 |
35 |
36 | module.exports = router;
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const express = require('express');
3 | const app = express();
4 | const cors = require('cors');
5 | const apiRouter = require('./routes/api');
6 |
7 | app.use(cors());
8 | // const bodyParser = require('body-parser');
9 | // const loginRouter = require('./routes/login');
10 | // const signupRouter = require('./routes/signup');
11 | const githubRouter = require('./routes/github');
12 | const cookieParser = require('cookie-parser');
13 | require('dotenv').config();
14 |
15 | //New line from dev branch
16 |
17 | const PORT = 3000;
18 |
19 | //This is a line from ari/test2
20 |
21 | /**
22 | * handle parsing request body
23 | */
24 | app.use(express.json());
25 | app.use(express.urlencoded({ extended: true }));
26 | // app.use(bodyParser.urlencoded({ extended: true }));
27 | app.use(cookieParser());
28 |
29 | //Static Routes
30 | // app.use(express.static(path.resolve(__dirname, '../client')));
31 |
32 | /**
33 | * define route handlers
34 | */
35 | // const sessionController = require('./controllers/sessionController');
36 |
37 | // app.get('/',(req,res) => {
38 | // return res.sendFile(path.resolve('dist','index.html'));
39 | // });
40 |
41 | if(process.env.NODE_ENV !== 'development') {
42 | app.use('/', express.static(path.resolve(__dirname, '../dist')));
43 | }
44 | // app.get('/dist', (req, res) => {
45 | // console.log('dist')
46 | // return res.sendFile(path.resolve('dist','bundle.js'));
47 | // });
48 |
49 | app.use('/api', apiRouter);
50 | app.use('/github', githubRouter);
51 | // app.use('/login', loginRouter);
52 | // app.use('/signup', signupRouter);
53 | app.get('/logout',
54 | (req, res) => {
55 | return res
56 | .clearCookie('ssid')
57 | .redirect('/');
58 | }
59 | );
60 |
61 |
62 | // catch-all route handler for any requests to an unknown route
63 | // app.get('/*', (req, res) => res.redirect('/'));
64 |
65 | /**
66 | * express error handler
67 | * @see https://expressjs.com/en/guide/error-handling.html#writing-error-handlers
68 | */
69 |
70 | app.use((err, req, res, next) => {
71 | const defaultErr = {
72 | log: 'Express error handler caught unknown middleware error',
73 | status: 500,
74 | message: { err: 'An error occurred' },
75 | };
76 | const errorObj = Object.assign({}, defaultErr, err);
77 | console.log(errorObj.log);
78 | return res.status(errorObj.status).json(errorObj.message);
79 | });
80 |
81 | /**
82 | * start server
83 | */
84 | app.listen(PORT, () => {
85 | console.log(`Server listening on port: ${PORT}...`);
86 | });
87 |
88 | module.exports = app;
89 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | const webpack = require('webpack');
3 | const path = require('path');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
6 |
7 | module.exports = {
8 | entry: [
9 | // entry point of our app
10 | './client/index.js',
11 | ],
12 | output: {
13 | path: path.resolve(__dirname, 'dist'),
14 | publicPath: '/',
15 | filename: 'bundle.js',
16 | },
17 | devtool: 'eval-source-map',
18 | mode: process.env.NODE_ENV,
19 | devServer: {
20 | host: 'localhost',
21 | port: 8080,
22 | // match the output path
23 | static: {
24 | directory: path.resolve(__dirname, 'dist'),
25 | // match the output 'publicPath'
26 | publicPath: '/',
27 | },
28 | // enable HMR on the devServer
29 | hot: true,
30 | // match the output 'publicPath'
31 | // publicPath: '/',
32 | // fallback to root for other urls
33 | // historyApiFallback: true,
34 |
35 | // headers: { 'Access-Control-Allow-Origin': '*' },
36 | headers: {
37 | 'Access-Control-Allow-Origin': '*',
38 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
39 | 'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization'
40 | },
41 | proxy: {
42 | '/api/**': {
43 | target: 'http://localhost:3000/',
44 | secure: false,
45 | },
46 | // '/': {
47 | // target: 'http://localhost:3000/',
48 | // secure: false,
49 | // },
50 | '/assets/**': {
51 | target: 'http://localhost:3000/',
52 | secure: false,
53 | },
54 | '/github': {
55 | target: 'http://localhost:3000/',
56 | secure: false,
57 | },
58 | '/logout': {
59 | target: 'http://localhost:3000/',
60 | secure: false,
61 | },
62 | // '/signup': {
63 | // target: 'http://localhost:3000/',
64 | // secure: false,
65 | // },
66 | },
67 | },
68 | module: {
69 | rules: [
70 | {
71 | test: /\.jsx?/,
72 | exclude: /node_modules/,
73 | use: {
74 | loader: 'babel-loader',
75 | },
76 | },
77 | {
78 | test: /\.s?[ac]ss$/i,
79 | use: ['style-loader', 'css-loader', 'sass-loader'],
80 | },
81 | // Fonts and SVGs
82 | {
83 | test: /\.(woff(2)?|eot|ttf|otf|svg|)$/,
84 | type: 'asset/inline',
85 | },
86 | // Images
87 | {
88 | test: /\.(?:ico|gif|png|jpg|jpeg)$/i,
89 | type: 'asset/resource',
90 | },
91 | ],
92 | },
93 | plugins: [
94 | new HtmlWebpackPlugin({
95 | template: './client/index.html',
96 | }),
97 | new CleanWebpackPlugin(),
98 | ],
99 | resolve: {
100 | // Enable importing JS / JSX files without specifying their extension
101 | extensions: ['.js', '.jsx'],
102 | },
103 | };
104 |
--------------------------------------------------------------------------------