├── README.md ├── .babelrc ├── .gitignore ├── src ├── index.js ├── components │ ├── Spinner.jsx │ ├── Landing.jsx │ ├── NavigateBar.jsx │ ├── App.jsx │ ├── Login.jsx │ ├── Profile.jsx │ ├── Signup.jsx │ ├── IdeaPage.jsx │ ├── Explore.jsx │ └── SubmitIdea.jsx └── styles │ ├── ideapage.scss │ ├── user-profile.scss │ └── login-signup.scss ├── public └── index.html ├── server ├── Routers │ ├── signupRouter.js │ ├── loginRouter.js │ ├── profileRouter.js │ ├── submitRouter.js │ └── exploreRouter.js ├── Models │ └── model.js ├── Controllers │ ├── techController.js │ ├── authController.js │ └── ideaController.js ├── passport.js ├── server.js └── database.sql ├── tsconfig.json ├── webpack.config.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # scratch-project 2 | 3 | 4 | Welcome to Goblin Shark territory. V2 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | modules: false 7 | } 8 | ], 9 | '@babel/preset-react' 10 | ], 11 | plugins: [ 12 | 'react-hot-loader/babel' 13 | ] 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | coverage/ 3 | dist/ 4 | !dist/index.html 5 | node_modules/ 6 | *.log 7 | .env 8 | 9 | # OS generated files 10 | .DS_Store 11 | .DS_Store? 12 | ._* 13 | .Spotlight-V100 14 | .Trashes 15 | ehthumbs.db 16 | Thumbs.db -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './components/App'; 4 | 5 | // uncomment so that webpack can bundle styles 6 | // import styles from './scss/application.scss'; 7 | 8 | render(, document.getElementById('root')); 9 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Scratch Project 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /server/Routers/signupRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const authController = require('../Controllers/authController.js'); 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/', authController.register, (req, res) => { 7 | res.status(200).send('register success'); 8 | // res.redirect('/') 9 | }); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "strict": true, 6 | "noImplicitReturns": true, 7 | "noImplicitAny": true, 8 | "module": "es6", 9 | "moduleResolution": "node", 10 | "target": "es5", 11 | "allowJs": true, 12 | "jsx": "react", 13 | }, 14 | "include": [ 15 | "./src/**/*" 16 | ] 17 | } -------------------------------------------------------------------------------- /server/Models/model.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | require('dotenv').config(); 3 | 4 | const PG_URI = process.env.elephantURI; 5 | 6 | const pool = new Pool({ 7 | connectionString: PG_URI, 8 | }); 9 | 10 | module.exports = { 11 | query: (text, params, callback) => { 12 | console.log('executed query', text); 13 | return pool.query(text, params, callback); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /server/Routers/loginRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const authController = require('../Controllers/authController.js'); 3 | const passport = require('passport'); 4 | 5 | const router = express.Router(); 6 | // when get a post request fine and compare user 7 | router.post('/', passport.authenticate('local'), (req, res) => { 8 | res.status(200).send('logIn success'); 9 | }); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /server/Routers/profileRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const authController = require('../Controllers/authController.js'); 3 | 4 | const router = express.Router(); 5 | 6 | // get router for explore page 7 | router.get('/:username', authController.getProfile, (req, res) => { 8 | // console.log('res.locals.ideas', res.locals.ideas); 9 | res.json(res.locals.userData); 10 | }); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /src/components/Spinner.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | // import spinner from '../../img/spinner.gif'; 3 | const spinner = 'http://static.onemansblog.com/wp-content/uploads/2016/05/Spinner-Loading.gif' 4 | 5 | export default () => 6 | ( 7 | 8 | Loading... 12 | 13 | ) -------------------------------------------------------------------------------- /src/styles/ideapage.scss: -------------------------------------------------------------------------------- 1 | #idea-wrapper { 2 | margin-top: 10vh; 3 | border: 1px solid black; 4 | border-radius: 8px; 5 | min-width: fit-content; 6 | width: 75%; 7 | } 8 | 9 | #idea-pic { 10 | height: 200px; 11 | align-self: center; 12 | } 13 | 14 | .prof-pic { 15 | height: 30px; 16 | } 17 | 18 | #button-container { 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | 23 | .unstyled-li { 24 | list-style: none; 25 | } 26 | 27 | ul { 28 | padding: 0; 29 | } 30 | -------------------------------------------------------------------------------- /server/Routers/submitRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const ideaController = require('../Controllers/ideaController.js'); 3 | const techController = require('../Controllers/techController.js'); 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/', techController.getTechs, (req, res) => { 8 | res.json(res.locals.techs); 9 | }); 10 | 11 | router.post('/', ideaController.submitIdea, (req, res) => { 12 | res.sendStatus(200); 13 | }); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /src/styles/user-profile.scss: -------------------------------------------------------------------------------- 1 | #userProfileContainer { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .cardHeader { 7 | display: flex; 8 | background-color: #2E7BFF; 9 | justify-content: center; 10 | align-items: center; 11 | height: 50px; 12 | width: 2rem; 13 | } 14 | 15 | #row1 { 16 | margin-top: 100px; 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | } 21 | 22 | #profilePic { 23 | height: 150px; 24 | width: 150px; 25 | margin: 20px; 26 | } -------------------------------------------------------------------------------- /server/Controllers/techController.js: -------------------------------------------------------------------------------- 1 | const model = require('../Models/model.js'); 2 | 3 | const techController = {}; 4 | 5 | techController.getTechs = (req, res, next) => { 6 | const queryText = 'SELECT * FROM Tech_stacks'; 7 | model.query(queryText, (err, results) => { 8 | if (err) { 9 | console.log(err); 10 | return next({ 11 | log: `error occurred at getTechs middleware. error message is: ${err}`, 12 | status: 400, 13 | message: { err: 'An error occurred' }, 14 | }); 15 | } 16 | 17 | res.locals.techs = results.rows; 18 | return next(); 19 | }); 20 | }; 21 | 22 | module.exports = techController; 23 | -------------------------------------------------------------------------------- /src/styles/login-signup.scss: -------------------------------------------------------------------------------- 1 | .login-container { 2 | height: 80vh; 3 | width: 100vw; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .login-box { 11 | border: 1px solid black; 12 | border-radius: 8px; 13 | padding: 1.2rem; 14 | margin-bottom: 10rem; 15 | min-width: 350px; 16 | } 17 | 18 | .error-msg { 19 | margin-top: 0.5rem; 20 | color: red; 21 | } 22 | 23 | .hidden { 24 | display: none; 25 | } 26 | 27 | input[type=checkbox] 28 | { 29 | /* Double-sized Checkboxes */ 30 | width: 22px; 31 | height: 22px; 32 | background-color: #ddd; 33 | position: relative; 34 | } 35 | 36 | .form-check-label { 37 | color: red; 38 | } -------------------------------------------------------------------------------- /server/Routers/exploreRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const ideaController = require('../Controllers/ideaController.js'); 3 | const techController = require('../Controllers/techController.js'); 4 | 5 | const router = express.Router(); 6 | 7 | // get router for explore page 8 | router.get( 9 | '/', 10 | ideaController.getIdeas, 11 | techController.getTechs, 12 | (req, res) => { 13 | // console.log('res.locals.ideas', res.locals.ideas); 14 | res.json([res.locals.ideas, res.locals.techs]); 15 | } 16 | ); 17 | 18 | router.get('/:ideaID', ideaController.getOneIdea, (req, res) => { 19 | // console.log('res.locals.idea', res.locals.idea); 20 | res.json(res.locals.idea); 21 | }); 22 | 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /src/components/Landing.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Button, Container, Row, Col } from 'react-bootstrap'; 3 | import { withRouter } from 'react-router-dom'; 4 | 5 | /* 6 | * HELPER FUNCTION: nextPath 7 | * Push `path` to Landing component's destructured `history` prop 8 | * (provided by invoking withRouter on Landing component before export) 9 | */ 10 | const redirectToPath = (history, path) => { 11 | history.push(path); 12 | }; 13 | 14 | const Landing = ({ history }) => { 15 | return ( 16 | 17 |
18 |

19 | {" "} 20 | Welcome to Scratch Project{" "} 21 |

22 |
23 |

24 | {" "} 25 | A place where developers make their dreams come true{" "} 26 |

27 |
28 |
29 |
30 | 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default withRouter(Landing); 45 | -------------------------------------------------------------------------------- /src/components/NavigateBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Navbar, 4 | Nav /* Form, FormControl, Button, Container */, 5 | } from 'react-bootstrap'; 6 | import { Link } from 'react-router-dom'; 7 | 8 | const NavigateBar = () => { 9 | return ( 10 | 11 | {/* Leftside Nav Logo/Link */} 12 | {/* TODO: Point this href to `/explore` if User is authenticated */} 13 | 14 | Scratch Project 15 | 16 | {/* Rightside Nav Links */} 17 | {/* Set class for Login and Signup button Nav item to `margin-left: auto;`*/} 18 | 38 | 39 | ); 40 | 41 | // // Search Bar Component 42 | // < Form inline > 43 | // 44 | // 45 | // 46 | }; 47 | 48 | export default NavigateBar; 49 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | const config = { 6 | entry: ['react-hot-loader/patch', './src/index.js'], 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'bundle.js', 10 | }, 11 | mode: process.env.NODE_ENV, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(js|jsx)$/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | options: { 19 | presets: ['@babel/preset-env', '@babel/preset-react'], 20 | plugins: ['@babel/transform-runtime'], 21 | }, 22 | }, 23 | { 24 | test: /\.scss?/, 25 | exclude: /(node_modules)/, 26 | use: ['style-loader', 'css-loader', 'sass-loader'], 27 | }, 28 | { 29 | test: /\.css$/, 30 | use: [ 31 | // 2. Inject CSS into the DOM 32 | { 33 | loader: 'style-loader', 34 | }, 35 | // 1. Convert CSS to CommonJS 36 | { 37 | loader: 'css-loader', 38 | }, 39 | ], 40 | }, 41 | { 42 | test: /\.ts(x)?$/, 43 | loader: 'ts-loader', 44 | exclude: /node_modules/, 45 | }, 46 | ], 47 | }, 48 | resolve: { 49 | extensions: ['.js', '.jsx', '.tsx', '.ts'], 50 | alias: { 51 | 'react-dom': '@hot-loader/react-dom', 52 | }, 53 | }, 54 | devServer: { 55 | contentBase: './dist', 56 | proxy: { 57 | '/api': 'http://localhost:3000', 58 | }, 59 | historyApiFallback: true, 60 | }, 61 | plugins: [ 62 | new HtmlWebpackPlugin({ 63 | template: `./public/index.html`, 64 | }), 65 | ], 66 | }; 67 | 68 | module.exports = config; 69 | -------------------------------------------------------------------------------- /server/passport.js: -------------------------------------------------------------------------------- 1 | const LocalStrategy = require('passport-local').Strategy; 2 | const model = require('./Models/model'); 3 | const bcrypt = require('bcrypt'); 4 | // to Authenticate with passport 5 | const initialize = (passport) => { 6 | const autheticateUser = (username, password, done) => { 7 | //find same username in database 8 | model.query( 9 | `SELECT * FROM User_credentials WHERE username = $1`, 10 | [username], 11 | (err, results) => { 12 | if (err) { 13 | console.log(err, 'user_credentials error'); 14 | return; 15 | } 16 | //find and compare same password 17 | if (results.rows.length > 0) { 18 | const user = results.rows[0]; 19 | 20 | bcrypt.compare(password, user.password, (err, isMatch) => { 21 | if (err) { 22 | console.log(err, 'bcrypt compare error'); 23 | return; 24 | } 25 | //if password matched send user body 26 | if (isMatch) { 27 | console.log('match'); 28 | return done(null, user); 29 | } else { 30 | console.log('password is not matched'); 31 | return done(null, false, { message: 'password is not matched' }); 32 | } 33 | }); 34 | } else { 35 | return done(null, false, { message: 'username is not registered' }); 36 | } 37 | } 38 | ); 39 | }; 40 | 41 | passport.use( 42 | new LocalStrategy( 43 | { 44 | usernameField: 'username', 45 | passwordField: 'password', 46 | }, 47 | autheticateUser 48 | ) 49 | ); 50 | // about session with cookie 51 | passport.serializeUser((user, done) => { 52 | done(null, user.username); 53 | }); 54 | passport.deserializeUser((username, done) => { 55 | model.query( 56 | `SELECT * FROM User_credentials WHERE username = $1`, 57 | [username], 58 | (err, results) => { 59 | if (err) { 60 | console.log(err, 'deserializeUser error'); 61 | return; 62 | } 63 | 64 | return done(null, results.rows[0]); 65 | } 66 | ); 67 | }); 68 | }; 69 | 70 | module.exports = initialize; 71 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | 4 | const app = express(); 5 | const cookieParser = require('cookie-parser'); 6 | const bcrypt = require('bcrypt'); 7 | const session = require('express-session'); 8 | const model = require('./Models/model'); 9 | const signUpRouter = require('./Routers/signupRouter'); 10 | const exploreRouter = require('./Routers/exploreRouter'); 11 | const submitRouter = require('./Routers/submitRouter'); 12 | const loginRouter = require('./Routers/loginRouter'); 13 | const profileRouter = require('./Routers/profileRouter'); 14 | const flash = require('express-flash'); 15 | const initializePassport = require('./passport'); 16 | const passport = require('passport'); 17 | initializePassport(passport); 18 | require('dotenv').config(); 19 | const PORT = 3000; 20 | 21 | /* 22 | * Handle parsing request body 23 | */ 24 | app.use(express.json()); 25 | app.use(express.urlencoded({ extended: true })); 26 | app.use(express.static(path.resolve(__dirname, 'public'))); 27 | app.use( 28 | session({ 29 | secret: 'secret', 30 | resave: false, 31 | saveUninitialized: false, 32 | }) 33 | ); 34 | app.use(passport.initialize()); 35 | app.use(passport.session()); 36 | app.use(flash()); 37 | app.use('/api/login', loginRouter); 38 | app.use('/api/signup', signUpRouter); 39 | app.use('/api/explore', exploreRouter); 40 | app.use('/api/submit', submitRouter); 41 | app.use('/api/profile', profileRouter); 42 | 43 | // globoal error handler 44 | app.use((err, req, res, next) => { 45 | const defaultErr = { 46 | log: 'Express error handler caught unknown middleware error', 47 | status: 400, 48 | message: { err: 'An error occurred' }, 49 | }; 50 | const errorObj = Object.assign({}, defaultErr, err); 51 | console.log(errorObj.log); 52 | return res.status(errorObj.status).json(errorObj.message); 53 | }); 54 | 55 | /* 56 | * Start server 57 | */ 58 | app.listen(PORT, () => { 59 | console.log(`Server listening on port: ${PORT}`); 60 | }); 61 | 62 | module.exports = app; 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bob", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./server/server.js", 8 | "build": "cross-env NODE_ENV=production webpack", 9 | "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot & nodemon ./server/server.js", 10 | "dew": "concurrently \"cross-env NODE_ENV=development webpack-dev-server --open --hot --inline --progress --colors --watch --content-base ./\" \"nodemon ./server/server.js\"", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/the-goblin-shark/scratch-project.git" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/the-goblin-shark/scratch-project/issues" 22 | }, 23 | "homepage": "https://github.com/the-goblin-shark/scratch-project#readme", 24 | "devDependencies": { 25 | "@babel/core": "^7.10.5", 26 | "@babel/plugin-transform-runtime": "^7.10.5", 27 | "@babel/preset-env": "^7.10.4", 28 | "@babel/preset-react": "^7.10.4", 29 | "@hot-loader/react-dom": "^16.13.0", 30 | "@types/react": "^16.9.43", 31 | "@types/react-dom": "^16.9.8", 32 | "babel-loader": "^8.1.0", 33 | "cross-env": "^7.0.2", 34 | "css-loader": "^3.6.0", 35 | "html-webpack-plugin": "^4.3.0", 36 | "node-sass": "^4.14.1", 37 | "nodemon": "^2.0.4", 38 | "sass-loader": "^9.0.2", 39 | "style-loader": "^1.2.1", 40 | "ts-loader": "^8.0.1", 41 | "typescript": "^3.9.7", 42 | "webpack": "^4.44.0", 43 | "webpack-cli": "^3.3.12", 44 | "webpack-dev-server": "^3.11.0" 45 | }, 46 | "dependencies": { 47 | "@babel/runtime": "^7.10.5", 48 | "axios": "^0.19.2", 49 | "bcrypt": "^5.0.0", 50 | "bootstrap": "^4.5.0", 51 | "cookie": "^0.4.1", 52 | "cookie-parser": "^1.4.5", 53 | "dotenv": "^8.2.0", 54 | "express": "^4.17.1", 55 | "express-flash": "0.0.2", 56 | "express-session": "^1.17.1", 57 | "passport": "^0.4.1", 58 | "passport-local": "^1.0.0", 59 | "pg": "^8.3.0", 60 | "react": "^16.13.1", 61 | "react-bootstrap": "^1.3.0", 62 | "react-bootstrap-typeahead": "^5.1.0", 63 | "react-dom": "^16.13.1", 64 | "react-hot-loader": "^4.12.21", 65 | "react-router-dom": "^5.2.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState } from 'react'; 2 | import Landing from './Landing.jsx'; 3 | import Explore from './Explore.jsx'; 4 | import Login from './Login.jsx'; 5 | import Signup from './Signup.jsx'; 6 | import Profile from './Profile.jsx'; 7 | import NavigateBar from './NavigateBar'; 8 | import IdeaPage from './IdeaPage'; 9 | import SubmitIdea from './SubmitIdea'; 10 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 11 | import '../../node_modules/bootstrap/dist/css/bootstrap.css'; 12 | 13 | const App = () => { 14 | const [authStatus, setAuthStatus] = useState({ 15 | isLoggedIn: false, 16 | username: '', 17 | }); 18 | 19 | return ( 20 | 21 | {/* Using Fragment rather than native div to avoid React warnings */} 22 | 23 | {/* Navigation Bar is ever-present */} 24 | 25 | {/* Use the first Route whose path matches current URL */} 26 | 27 | {/* Render given component if given path matches current URL */} 28 | {/* */} 29 | } 33 | /> 34 | ( 38 | 39 | )} 40 | /> 41 | ( 45 | 46 | )} 47 | /> 48 | } 52 | /> 53 | 54 | } 58 | /> 59 | } 63 | /> 64 | 65 | 66 | 67 | ); 68 | }; 69 | 70 | export default App; 71 | -------------------------------------------------------------------------------- /src/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | import '../styles/login-signup.scss'; 4 | import '../../node_modules/bootstrap/dist/css/bootstrap.css'; 5 | import { Form, Button } from 'react-bootstrap'; 6 | 7 | const Login = (props) => { 8 | const { authStatus, setAuthStatus } = props; 9 | 10 | const [loginInputs, setLoginInputs] = useState({ 11 | username: '', 12 | password: '', 13 | }); 14 | 15 | //used to toggle error message if auth fails 16 | //as well as redirect if auth succeeds 17 | const [loginStatus, setLoginStatus] = useState(null); 18 | 19 | const handleSubmit = async (e) => { 20 | e.preventDefault(); 21 | const { username, password } = loginInputs; 22 | const body = { 23 | username, 24 | password, 25 | }; 26 | let response = await fetch('/api/login', { 27 | method: 'POST', 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | }, 31 | body: JSON.stringify(body), 32 | }); 33 | 34 | if (response.status === 200) { 35 | setLoginStatus(true); 36 | setAuthStatus({ isLoggedIn: true, username }); 37 | } else setLoginStatus(false); 38 | }; 39 | 40 | const setInput = (e) => { 41 | setLoginInputs({ ...loginInputs, [e.target.id]: e.target.value }); 42 | }; 43 | 44 | return loginStatus || authStatus.isLoggedIn ? ( 45 | 46 | ) : ( 47 |
48 |
49 |
50 |

Welcome Back!

51 |
52 |
53 | 54 | Username 55 | 60 | 61 | 62 | 63 | Password 64 | 69 | 70 | 71 | 74 |
75 | Sorry, your username/password was invalid. 76 |
77 |
78 |
79 |
80 | ); 81 | }; 82 | 83 | export default Login; 84 | -------------------------------------------------------------------------------- /server/Controllers/authController.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const model = require('../Models/model.js'); 3 | 4 | const authController = {}; 5 | 6 | authController.register = async (req, res, next) => { 7 | const { username, password, email, firstname, lastname } = req.body; 8 | const queryText = `INSERT INTO User_credentials (username,password,email) VALUES ($1,$2,$3)`; 9 | const usersQueryText = `INSERT INTO Users (username, firstname, lastname) VALUES($1,$2,$3)`; 10 | const hashedPassWord = await bcrypt.hash(password, 10); 11 | try { 12 | await model.query(queryText, [username, hashedPassWord, email]); 13 | await model.query(usersQueryText, [username, firstname, lastname]); 14 | return next(); 15 | } catch (err) { 16 | console.log(err); 17 | return next({ 18 | log: `error occurred at register middleware. error message is: ${err}`, 19 | status: 400, 20 | message: { err: 'An error occurred' }, 21 | }); 22 | } 23 | }; 24 | 25 | // ToDO : new columns in the Users table, about, linkedin, personal url 26 | // new table to link users to tech stack(association table), 27 | // signup new fields for firstname and lastname 28 | 29 | // middleware for get profiles 30 | authController.getProfile = async (req, res, next) => { 31 | const { username } = req.params; 32 | const queryText = `SELECT * FROM Users WHERE username=$1`; 33 | try { 34 | const userData = await model.query(queryText, [username]); 35 | [res.locals.userData] = userData.rows; 36 | return next(); 37 | } catch (err) { 38 | console.log(err); 39 | return next({ 40 | log: `error occurred at getProfile middleware. error message is: ${err}`, 41 | status: 400, 42 | message: { err: 'An error occurred' }, 43 | }); 44 | } 45 | }; 46 | 47 | // middeware to edit profiles (INCOMPLETE) 48 | authController.editProfile = async (req, res, next) => { 49 | const { 50 | username, 51 | firstName, 52 | lastName, 53 | about, 54 | profilepic, 55 | githubHandle, 56 | linkedIn, 57 | personalPage, 58 | } = req.body; 59 | 60 | const queryText = `UPDATE Users 61 | SET firstname=$1, 62 | lastname=$2, 63 | about=$3 64 | profilepic=$4, 65 | githubhandle=$5, 66 | linkedin=$6, 67 | personalpage=$7 68 | WHERE username=$8`; 69 | 70 | const queryValue = [ 71 | firstName, 72 | lastName, 73 | about, 74 | profilepic, 75 | githubHandle, 76 | linkedIn, 77 | personalPage, 78 | username, 79 | ]; 80 | 81 | try { 82 | await model.query(queryText, queryValue); 83 | return next(); 84 | } catch (err) { 85 | console.log(err); 86 | return next({ 87 | log: `error occurred at getProfile middleware. error message is: ${err}`, 88 | status: 400, 89 | message: { err: 'An error occurred' }, 90 | }); 91 | } 92 | }; 93 | 94 | module.exports = authController; 95 | -------------------------------------------------------------------------------- /src/components/Profile.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useEffect } from 'react' 2 | import { Container, Col, Row, Button } from 'react-bootstrap'; 3 | import Spinner from './Spinner'; 4 | import '../styles/user-profile.scss'; 5 | 6 | const Profile = (props) => { 7 | /* 8 | * Possible Props: 9 | * creatorUsername (possibly) passed in from IdeaPage 10 | * authStatus always passed in from App 11 | */ 12 | let { ideaCreator, authStatus } = props; 13 | 14 | // Destructure currently authenticated user's username from authStatus 15 | let { username } = authStatus; 16 | 17 | // Initialize creator name-to-display to currently authenticated user 18 | let creatorName = username; 19 | 20 | // Accessing Profile from Idea Page? 21 | if (ideaCreator) { 22 | console.log('idea creator is : ', ideaCreator) 23 | // If logged-in user is _not_ clicking on their own profile picture, 24 | // RESET name-to-display to that of the User being clicked by logged-in User 25 | if (loggedInUsername !== ideaCreator) { 26 | creatorName = ideaCreator; 27 | } 28 | } 29 | // Set up user data to display on Profile 30 | const [userData, setUserData] = useState({}); 31 | 32 | // componentDidMount() functional equivalent 33 | useEffect(() => { 34 | getUser(); 35 | }, []); 36 | 37 | const getUser = async () => { 38 | // Get all existing user data, sending username as a parameter 39 | const res = await fetch(`/api/profile/${creatorName}`); 40 | // Expect in response an object with all User table column properties 41 | const userTableData = await res.json(); 42 | setUserData(userTableData); 43 | }; 44 | 45 | /* 46 | * PROFILE COMPONENT USER FLOW: 47 | 48 | * Case 1: Viewing your own profile (READ and WRITE) 49 | * On first render, display all data in this DB row (distinguished by `username`) 50 | * 51 | * If current User clicks edit, then submit: 52 | * 1a. Send all data stored from initial GET request 53 | * 1b. Except for the modified fields, whose values will be set to User input 54 | * 55 | * Case 2: Viewing another User's profile (whether or not you're a registered user) 56 | * Same page without edit button functionality (READ-ONLY) 57 | */ 58 | 59 | if (!Object.keys(userData).length) { 60 | return ; 61 | } 62 | else if (userData.err) { 63 | return Could not load user; 64 | } 65 | 66 | return ( 67 | 68 | 69 |

70 | {creatorName}'s Developer Profile 71 |

72 | 73 |
74 | 75 | 76 | Bio 77 | 78 | 79 | Where else can your future teammates contact you? 80 | 81 | 82 |
83 | ); 84 | } 85 | 86 | export default Profile; 87 | -------------------------------------------------------------------------------- /server/database.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE public.User_credentials 2 | ( 3 | username varchar(50) NOT NULL, 4 | password varchar(255) NOT NULL, 5 | email varchar(255) UNIQUE NOT NULL, 6 | CONSTRAINT PK_users PRIMARY KEY (username) 7 | ); 8 | 9 | CREATE TABLE Users 10 | ( 11 | user_id serial NOT NULL, 12 | firstname varchar(50) NOT NULL, 13 | lastname varchar(50) NOT NULL, 14 | about varchar(255) NULL, 15 | profilepic varchar(65535) NULL DEFAULT 'https://www.eguardtech.com/wp-content/uploads/2018/08/Network-Profile.png', 16 | githubhandle varchar(50) NULL, 17 | username varchar(50) NOT NULL, 18 | linkedin varchar(255) NULL, 19 | personalpage varchar(255) NULL, 20 | CONSTRAINT PK_user_profile PRIMARY KEY (user_id), 21 | CONSTRAINT FK_34 FOREIGN KEY (username) REFERENCES public.User_credentials (username) 22 | ); 23 | 24 | CREATE INDEX fkIdx_34 ON Users 25 | (z1 26 | username 27 | ); 28 | 29 | CREATE TABLE Ideas 30 | ( 31 | idea_id serial NOT NULL, 32 | name varchar(50) NOT NULL, 33 | description varchar(255) NOT NULL, 34 | why varchar(255) NOT NULL, 35 | when_start date NOT NULL, 36 | when_end date NULL, 37 | who int NOT NULL, 38 | image varchar(65535) NULL DEFAULT 'https://foroalfa.org/imagenes/ilustraciones/idea.png', 39 | creator_username varchar(50) NOT NULL, 40 | CONSTRAINT PK_ideas PRIMARY KEY (idea_id), 41 | CONSTRAINT FK_56 FOREIGN KEY (creator_username) REFERENCES public.User_credentials (username) 42 | ); 43 | 44 | CREATE INDEX fkIdx_56 ON Ideas 45 | ( 46 | creator_username 47 | ); 48 | 49 | CREATE TABLE Idea_Participants 50 | ( 51 | idea_id integer NOT NULL, 52 | participant_username varchar(50) NOT NULL, 53 | CONSTRAINT FK_59 FOREIGN KEY (idea_id) REFERENCES Ideas ( idea_id ), 54 | CONSTRAINT FK_62 FOREIGN KEY (participant_username) REFERENCES public.User_credentials (username) 55 | ); 56 | 57 | CREATE INDEX fkIdx_59 ON Idea_Participants 58 | ( 59 | idea_id 60 | ); 61 | 62 | CREATE INDEX fkIdx_62 ON Idea_Participants 63 | ( 64 | participant_username 65 | ); 66 | 67 | CREATE TABLE Favorites 68 | ( 69 | username varchar(50) NOT NULL, 70 | idea_id integer NOT NULL, 71 | CONSTRAINT FK_66 FOREIGN KEY (username) REFERENCES public.User_credentials (username), 72 | CONSTRAINT FK_69 FOREIGN KEY (idea_id) REFERENCES Ideas (idea_id) 73 | ); 74 | 75 | CREATE INDEX fkIdx_66 ON Favorites 76 | ( 77 | username 78 | ); 79 | 80 | CREATE INDEX fkIdx_69 ON Favorites 81 | ( 82 | idea_id 83 | ); 84 | 85 | CREATE TABLE Idea_Tech_Stacks 86 | ( 87 | idea_id integer NOT NULL, 88 | tech_id integer NOT NULL, 89 | CONSTRAINT FK_84 FOREIGN KEY ( idea_id ) REFERENCES Ideas (idea_id), 90 | CONSTRAINT FK_87 FOREIGN KEY ( tech_id ) REFERENCES Tech_Stacks ( tech_id ) 91 | ); 92 | 93 | CREATE INDEX fkIdx_84 ON Idea_Tech_Stacks 94 | ( 95 | idea_id 96 | ); 97 | 98 | CREATE INDEX fkIdx_87 ON Idea_Tech_Stacks 99 | ( 100 | tech_id 101 | ); 102 | 103 | CREATE TABLE Tech_Stacks 104 | ( 105 | tech_id serial NOT NULL, 106 | name varchar(50) NOT NULL, 107 | CONSTRAINT PK_tech_stacks PRIMARY KEY ( tech_id ) 108 | ); 109 | 110 | CREATE TABLE User_tech_stacks 111 | ( 112 | user_id integer NOT NULL, 113 | tech_id integer NOT NULL, 114 | CONSTRAINT FK_94 FOREIGN KEY (user_id) REFERENCES public.Users (user_id), 115 | CONSTRAINT FK_97 FOREIGN KEY (tech_id) REFERENCES Tech_Stacks 116 | (tech_id) 117 | ); 118 | 119 | CREATE INDEX fkIdx_94 ON User_tech_stacks 120 | ( 121 | user_id 122 | ); 123 | 124 | CREATE INDEX fkIdx_97 ON User_tech_stacks 125 | ( 126 | tech_id 127 | ); -------------------------------------------------------------------------------- /src/components/Signup.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | import '../styles/login-signup.scss'; 4 | import '../../node_modules/bootstrap/dist/css/bootstrap.css'; 5 | import { Form, Button } from 'react-bootstrap'; 6 | 7 | const Signup = (props) => { 8 | const { authStatus, setAuthStatus } = props; 9 | 10 | const [registrationInputs, setRegistrationInputs] = useState({ 11 | firstname: '', 12 | lastname: '', 13 | username: '', 14 | password: '', 15 | confirmPassword: '', 16 | email: '', 17 | }); 18 | 19 | const [errorMsg, setErrorMsg] = useState(''); 20 | const [registerStatus, setRegisterStatus] = useState(false); 21 | 22 | const handleSubmit = async (e) => { 23 | e.preventDefault(); 24 | const { 25 | username, 26 | password, 27 | email, 28 | confirmPassword, 29 | firstname, 30 | lastname, 31 | } = registrationInputs; 32 | if (password !== confirmPassword) 33 | return setErrorMsg(`Passwords don't match!`); 34 | 35 | const body = { 36 | username, 37 | password, 38 | email, 39 | firstname, 40 | lastname, 41 | }; 42 | 43 | let response = await fetch('/api/signup', { 44 | method: 'POST', 45 | headers: { 46 | 'Content-Type': 'application/json', 47 | }, 48 | body: JSON.stringify(body), 49 | }); 50 | 51 | if (response.status === 200) { 52 | setRegisterStatus(true); 53 | setAuthStatus({ isLoggedIn: true, username }); 54 | } else 55 | setErrorMsg('New user could not be created - duplicate username/email'); 56 | }; 57 | 58 | const setInput = (e) => { 59 | setRegistrationInputs({ 60 | ...registrationInputs, 61 | [e.target.id]: e.target.value, 62 | }); 63 | }; 64 | 65 | return registerStatus || authStatus.isLoggedIn ? ( 66 | 71 | ) : ( 72 |
73 |
74 |
75 |

Welcome!

76 |
77 |
78 | 79 | Username 80 | 86 | 87 | 88 | 89 | First Name 90 | 96 | 97 | 98 | 99 | Last Name 100 | 106 | 107 | 108 | 109 | Email 110 | 115 | 116 | 117 | 118 | Password 119 | 125 | 126 | 127 | 128 | Password 129 | 135 | 136 | 137 | 140 |
141 |
{errorMsg}
142 |
143 |
144 | ); 145 | }; 146 | 147 | export default Signup; 148 | -------------------------------------------------------------------------------- /src/components/IdeaPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import Spinner from './Spinner'; 4 | import '../styles/ideapage.scss'; 5 | import { Container, Col, Row, Button } from 'react-bootstrap'; 6 | 7 | const IdeaPage = (props) => { 8 | //passed in from Explore 9 | let { idea_id, authStatus } = props.location.state; 10 | const [ideaData, setIdeaData] = useState({}); 11 | const [interested, setInterested] = useState(false); 12 | 13 | useEffect(() => { 14 | getIdea(); 15 | }, []); 16 | 17 | const getIdea = async () => { 18 | const res = await fetch(`/api/explore/${idea_id}`); 19 | const parsed = await res.json(); 20 | setIdeaData(parsed); 21 | }; 22 | 23 | const handleInterestClick = async () => { 24 | setInterested(true); 25 | //TODO: actually build out functionality to email/notify creator 26 | }; 27 | 28 | if (!Object.keys(ideaData).length) return ; 29 | else if (ideaData.err) 30 | return Could not load idea; 31 | 32 | let { 33 | name, 34 | description, 35 | why, 36 | when_start, 37 | when_end = null, 38 | who, 39 | creator_username, 40 | image, 41 | participants, 42 | techStacks, 43 | profilepic, 44 | } = ideaData; 45 | 46 | return ( 47 | 48 | 49 | 50 |

WHY

51 | {why} 52 | 53 |

HOW

54 | 55 |
Tech Stack
56 |
57 | 58 |
    59 | {techStacks.map((stack, idx) => ( 60 |
  • {stack.name}
  • 61 | ))} 62 |
63 |
64 | 65 |

WHEN

66 | 67 |
68 | Start Date: {when_start ? when_start.substring(0, 10) : undefined} 69 |
70 | {when_end ? ( 71 |
72 | End Date: {when_end ? when_end.substring(0, 10) : undefined} 73 |
74 | ) : undefined} 75 |
76 | 77 |

WHO

78 | 79 |
Current Teammates - {who} Desired
80 | {/* TODO: MAKE EACH LI A LINK TO USER PROFILE */} 81 |
    82 |
  • 83 | {/* TODO:CONVERT THE PROFILE PIC IN SCHEMA TO STRING */} 84 | 93 | 94 | 95 | {creator_username} (creator) 96 |
  • 97 | {participants.map((user, idx) => ( 98 |
  • 99 | 100 | {user.username} 101 |
  • 102 | ))} 103 |
104 |
105 | 106 | 107 | 108 | 109 | 110 |

{name}

111 | 112 |
{description}
113 |
114 | 115 |
116 | 117 | 118 | 119 | 120 | 121 | 122 | {!interested ? ( 123 | 130 | ) : ( 131 | 134 | )} 135 | 136 | 137 | 138 | 141 | 142 | 143 | 144 | 145 |
146 |
147 | ); 148 | }; 149 | 150 | export default IdeaPage; 151 | -------------------------------------------------------------------------------- /src/components/Explore.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useState, useEffect } from 'react'; 2 | import { Container, Col, Row, Form, Card, Button } from 'react-bootstrap'; 3 | import axios from 'axios'; 4 | import { NavLink } from 'react-router-dom'; 5 | import Spinner from './Spinner'; 6 | 7 | const Explore = (props) => { 8 | const { authStatus } = props; 9 | const [response, setResponse] = useState([ 10 | { 11 | idea_id: '', 12 | name: '', 13 | description: '', 14 | why: '', 15 | when_start: '', 16 | when_end: '', 17 | who: '', 18 | creator_username: '', 19 | image: '', 20 | techstacks: [], 21 | }, 22 | ]); 23 | 24 | const [query, setQuery] = useState(''); 25 | const [techList, setTechList] = useState([{ tech_id: '', name: '' }]); 26 | //for sort by tech stack functionality, if user checks off a tech, it gets added to array 27 | const [techFilter, setTechFilter] = useState([]); 28 | 29 | useEffect(() => { 30 | const fetchData = async () => { 31 | const results = await axios.get('api/explore'); 32 | // console.log('results.data', results.data); 33 | setResponse(results.data[0]); 34 | setTechList(results.data[1]); 35 | // console.log(results.data[0]); 36 | }; 37 | 38 | fetchData(); 39 | }, []); 40 | 41 | const handleTechFilter = (e) => { 42 | let newTechFilter; 43 | if (techFilter.includes(e.target.value)) { 44 | newTechFilter = techFilter.filter((tech) => tech !== e.target.value); 45 | } else newTechFilter = [...techFilter, e.target.value]; 46 | setTechFilter(newTechFilter); 47 | }; 48 | 49 | // get technologies 50 | const techStack = techList.map((tech) => tech.name); 51 | // Generate checkbox component for each technology 52 | const generateTech = techStack.map((tech, idx) => { 53 | return ( 54 |
55 |
56 | 57 | 63 | 64 | {' '} 65 |

{tech}

{' '} 66 |
67 |
68 |
69 |
70 | ); 71 | }); 72 | 73 | const onChange = (q) => setQuery(q); 74 | 75 | const ideas = response; 76 | 77 | const sortedIdeas = ideas.filter((data) => { 78 | return data.name.toLowerCase().indexOf(query.toLowerCase()) !== -1; 79 | }); 80 | 81 | //check if user wants to filter for tech, otherwise just return sortedIdeas as-is 82 | const filteredIdeas = techFilter.length 83 | ? sortedIdeas.filter((idea) => { 84 | //if idea has tech that is inside techFilter, then include that idea 85 | for (let i = 0; i < techFilter.length; i++) { 86 | const selectedTech = techFilter[i]; 87 | if (!idea.techstacks.includes(selectedTech)) return false; 88 | } 89 | return true; 90 | }) 91 | : sortedIdeas; 92 | 93 | const generateBoxes = filteredIdeas.map((idea, idx) => { 94 | return ( 95 | 96 | 97 | 98 | {idea.name} 99 | {idea.description} 100 | 101 | 102 | Tech Stack:{' '} 103 | {' '} 104 |
105 | {idea.techstacks.join(', ')} 106 |
107 | 116 | 117 | 118 |
119 |
120 | ); 121 | }); 122 | 123 | // Search box component 124 | const searchIdea = ( 125 |
126 | 127 | 128 | {' '} 129 |

May your dream come true

{' '} 130 |
131 | onChange(e.target.value)} 136 | /> 137 | {/* */} 138 |
139 |
140 | ); 141 | 142 | const explorePage = ( 143 | 144 | 145 | 146 | 147 | {' '} 148 |

152 | {' '} 153 | Choose your tech stack:{' '} 154 |

155 |
156 |
{generateTech}
157 | 158 | 159 | 160 | {searchIdea} 161 | {generateBoxes} 162 | 163 |
164 |
165 | ); 166 | 167 | return response.length === 1 ? ( 168 | 169 | ) : ( 170 | {explorePage} 171 | ); 172 | }; 173 | 174 | export default Explore; 175 | -------------------------------------------------------------------------------- /server/Controllers/ideaController.js: -------------------------------------------------------------------------------- 1 | const model = require('../Models/model.js'); 2 | 3 | // controllers for explor page 4 | const ideaController = {}; 5 | 6 | // middleware to get all ideas data from database 7 | ideaController.getIdeas = (req, res, next) => { 8 | /* query text will join tables for ideas, idea_tech_stacks, and tech_stacks 9 | then aggregate the tech stack names into an array 10 | */ 11 | const queryText = `SELECT Ideas.*, array_agg(tech_stacks.name) AS techstacks FROM Ideas 12 | JOIN Idea_tech_stacks ON Idea_tech_stacks.idea_id = Ideas.idea_id 13 | JOIN tech_stacks ON tech_stacks.tech_id=Idea_tech_stacks.tech_id 14 | GROUP BY Ideas.idea_id`; 15 | 16 | model.query(queryText, (err, results) => { 17 | if (err) { 18 | console.log(err); 19 | return next({ 20 | log: `error occurred at getIdeas middleware. error message is: ${err}`, 21 | status: 400, 22 | message: { err: 'An error occurred' }, 23 | }); 24 | } 25 | // console.log('results', results.rows); 26 | res.locals.ideas = results.rows; 27 | return next(); 28 | }); 29 | }; 30 | 31 | // INSERT INTO Ideas (name, description, why, when_start, when_end, who, image, creator_username) VALUES ('scratch', 'scratch project', 'for fun', '2020-07-25', '2020-08-15', '3', 'image.png', 'hello1'); 32 | 33 | // we need to know who's submitting the idea 34 | ideaController.submitIdea = (req, res, next) => { 35 | const { 36 | name, 37 | description, 38 | why, 39 | techStack, 40 | whenStart, 41 | whenEnd, 42 | teamNumber, 43 | imageURL, 44 | username, 45 | } = req.body; 46 | 47 | const teamNumberInt = Number(teamNumber); 48 | 49 | // will need to get user's username (may need to modify database to grab user_id instead...) 50 | // front end to modify date to following format YYYY-MM-DD 51 | // if dateEnd is not given, front end to assign it as null 52 | 53 | // if imageurl or endDate is falsy, then we have to omit from query text/value so that it will default to default image or date(null) 54 | let queryText1; 55 | let queryValue1; 56 | if (!whenEnd && !imageURL) { 57 | queryText1 = `INSERT INTO Ideas (name, description, why, when_start, who, creator_username) VALUES ($1, $2, $3, $4, $5, $6) RETURNING idea_id`; 58 | queryValue1 = [name, description, why, whenStart, teamNumberInt, username]; 59 | } else if (!imageURL) { 60 | queryText1 = `INSERT INTO Ideas (name, description, why, when_start, when_end, who, creator_username) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING idea_id`; 61 | queryValue1 = [ 62 | name, 63 | description, 64 | why, 65 | whenStart, 66 | whenEnd, 67 | teamNumberInt, 68 | username, 69 | ]; 70 | } else if (!whenEnd) { 71 | queryText1 = `INSERT INTO Ideas (name, description, why, when_start, who, image, creator_username) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING idea_id`; 72 | queryValue1 = [ 73 | name, 74 | description, 75 | why, 76 | whenStart, 77 | teamNumberInt, 78 | imageURL, 79 | username, 80 | ]; 81 | } else { 82 | queryText1 = `INSERT INTO Ideas (name, description, why, when_start, when_end, who, image, creator_username) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING idea_id`; 83 | queryValue1 = [ 84 | name, 85 | description, 86 | why, 87 | whenStart, 88 | whenEnd, 89 | teamNumberInt, 90 | imageURL, 91 | username, 92 | ]; 93 | } 94 | let addedIdeaId; 95 | model.query(queryText1, queryValue1, async (err, result) => { 96 | if (err) { 97 | console.log(err); 98 | return next({ 99 | log: `error occurred at submitIdea middleware query1. error message is: ${err}`, 100 | status: 400, 101 | message: { err: 'An error occurred' }, 102 | }); 103 | } 104 | addedIdeaId = result.rows[0].idea_id; 105 | 106 | // separate query to insert tech stacks into idea_tech_stacks 107 | let queryText2; 108 | const quertValue2 = []; 109 | for (let i = 0; i < techStack.length; i += 1) { 110 | quertValue2.push([addedIdeaId, techStack[i]]); 111 | } 112 | // console.log(techStack); 113 | for (let i = 0; i < techStack.length; i += 1) { 114 | queryText2 = `INSERT INTO Idea_tech_stacks (idea_id, tech_id) VALUES ($1, $2)`; 115 | await model.query(queryText2, quertValue2[i], (err) => { 116 | if (err) { 117 | console.log(err); 118 | return next({ 119 | log: `error occurred at submitIdea middleware query2. error message is: ${err}`, 120 | status: 400, 121 | message: { err: 'An error occurred' }, 122 | }); 123 | } 124 | }); 125 | } 126 | return next(); 127 | }); 128 | }; 129 | 130 | // middleware to get one idea 131 | // need to set up route for this 132 | ideaController.getOneIdea = async (req, res, next) => { 133 | const id = req.params.ideaID; 134 | try { 135 | const ideasQueryText = `SELECT * FROM Ideas 136 | JOIN Users 137 | ON ideas.creator_username = users.username 138 | WHERE idea_id=${id}`; 139 | const ideaDetail = await model.query(ideasQueryText); 140 | // rows will only contain one. ok to destructure 141 | [res.locals.idea] = ideaDetail.rows; 142 | 143 | const participantQueryText = `SELECT * 144 | FROM idea_participants 145 | JOIN users 146 | ON idea_participants.participant_username = users.username 147 | WHERE idea_id = ${id}`; 148 | const participants = await model.query(participantQueryText); 149 | //will return array of objects 150 | res.locals.idea = { ...res.locals.idea, participants: participants.rows }; 151 | 152 | const stackQueryText = `SELECT * FROM idea_tech_stacks 153 | JOIN tech_stacks 154 | ON tech_stacks.tech_id = idea_tech_stacks.tech_id 155 | WHERE idea_id = ${id}`; 156 | const techStacks = await model.query(stackQueryText); 157 | res.locals.idea = { ...res.locals.idea, techStacks: techStacks.rows }; 158 | 159 | return next(); 160 | } catch (err) { 161 | return next({ 162 | log: `error occurred at getOneIdea middleware. error message is: ${err}`, 163 | status: 400, 164 | message: { err: 'An error occurred' }, 165 | }); 166 | } 167 | }; 168 | 169 | module.exports = ideaController; 170 | -------------------------------------------------------------------------------- /src/components/SubmitIdea.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Container, Form, Button, Row, Col } from 'react-bootstrap'; 3 | import { Typeahead } from 'react-bootstrap-typeahead'; 4 | import 'react-bootstrap-typeahead/css/Typeahead.css'; 5 | import axios from 'axios'; 6 | 7 | const SubmitIdea = (props) => { 8 | const { authStatus } = props; 9 | const { username } = authStatus; 10 | 11 | const [retrievedTechStacks, setRetrievedTechStacks] = useState([]); 12 | 13 | const [name, setName] = useState(''); 14 | const [description, setDescription] = useState(''); 15 | const [why, setWhy] = useState(''); 16 | const [techStack, setTechStack] = useState([]); 17 | const [teamNumber, setTeamNumber] = useState(''); 18 | const [imageURL, setImageURL] = useState(''); 19 | const [whenStart, setWhenStart] = useState(''); 20 | const [whenEnd, setWhenEnd] = useState(''); 21 | const [isSubmitted, setIsSubmitted] = useState(false); 22 | 23 | // get request to backend to fetch tech stack data 24 | useEffect(() => { 25 | const fetchTechs = async () => { 26 | const results = await axios.get('/api/submit'); 27 | // array of objects with id and name. need to filter for just names 28 | 29 | const techNamesList = results.data.map((el) => el.name); 30 | // console.log('techNamesList', techNamesList); 31 | setRetrievedTechStacks(techNamesList); 32 | // console.log('techStacks', techStacks); 33 | }; 34 | 35 | fetchTechs(); 36 | // console.log('techStacks', techStacks); 37 | }, []); 38 | 39 | const handleChange = (e) => { 40 | const { id, value } = e.target; 41 | switch (id) { 42 | case 'name': 43 | setName(value); 44 | break; 45 | case 'description': 46 | setDescription(value); 47 | break; 48 | case 'why': 49 | setWhy(value); 50 | break; 51 | case 'who': 52 | setTeamNumber(value); 53 | break; 54 | case 'uploadImage': 55 | setImageURL(value); 56 | break; 57 | default: 58 | console.log('not working'); 59 | } 60 | }; 61 | 62 | // helper function to convert tech stack into tech stack id 63 | const techIDConverter = (techs) => { 64 | const result = techs.map((el) => retrievedTechStacks.indexOf(el) + 1); 65 | return result; 66 | }; 67 | 68 | const handleSubmit = async (e) => { 69 | e.preventDefault(); 70 | // convert tech stack into tech stack id 71 | const techStackID = techIDConverter(techStack); 72 | 73 | const data = { 74 | name, 75 | description, 76 | why, 77 | techStack: techStackID, 78 | whenStart, 79 | whenEnd, 80 | teamNumber, 81 | imageURL, 82 | // hardcode, need a logic to pass username as prop 83 | username, 84 | }; 85 | // console.log('data', data); 86 | await axios.post('/api/submit', data); 87 | setIsSubmitted(true); 88 | }; 89 | 90 | return isSubmitted ? ( 91 |
100 |

101 | You submitted your idea successfully! 102 |

103 |
112 | 115 | 123 |
124 |
125 | ) : ( 126 | 127 |
128 | 129 | 130 | 131 | WHAT 132 | Name your idea 133 | 138 | 139 | 140 | 141 | Describe your idea 142 | 143 | 144 | 145 | 146 | WHY 147 | 148 | Why do feel passionate about this idea? 149 | 150 | 151 | 152 | 153 | 154 | HOW 155 | Choose your tech 156 | 164 | 165 | 166 | 167 | WHO 168 | 169 | Desired Number of Teammates 170 | 171 | 172 | 173 | 174 | 175 | 176 | Upload Image: 177 | 178 | for now, put a image source url 179 | 180 | 181 | 182 | 183 | 184 | WHEN 185 | 186 | What do you want to start? 187 |
188 | Note: This is also the date you will stop accepting new 189 | teammates. 190 |
191 | setWhenStart(e.target.value)} 193 | type="date" 194 | /> 195 |
196 | 197 | 198 | When is the expected due date? (optional) 199 | 200 | setWhenEnd(e.target.value)} 202 | type="date" 203 | /> 204 | 205 |
206 | 209 | {' '} 210 | 218 |
219 | 220 |
221 |
222 |
223 | ); 224 | }; 225 | 226 | export default SubmitIdea; 227 | --------------------------------------------------------------------------------