├── .DS_Store ├── .gitignore ├── .sequelizerc ├── README.md ├── app.js ├── auth.js ├── bin └── www ├── config ├── config.json ├── database.js └── index.js ├── db ├── .DS_Store ├── migrations │ ├── .keep │ ├── 20210430153647-create-profile.js │ ├── 20210430155711-create-category.js │ ├── 20210430160023-create-question.js │ ├── 20210430161232-create-question-vote.js │ ├── 20210430161904-create-answer.js │ ├── 20210430162300-create-answer-vote.js │ ├── 20210430162740-create-comment.js │ └── 20210430163147-create-comment-vote.js ├── models │ ├── answer.js │ ├── answervote.js │ ├── category.js │ ├── comment.js │ ├── commentvote.js │ ├── index.js │ ├── profile.js │ ├── question.js │ └── questionvote.js └── seeders │ ├── .keep │ └── 20210504171600-onecategory.js ├── events.js ├── package-lock.json ├── package.json ├── public ├── .DS_Store ├── favicon.ico ├── images │ ├── 404-image.png │ ├── DO_black-transparent.png │ ├── DO_white-transparent.png │ └── corner-image.png ├── javascripts │ ├── index.js │ ├── layout.js │ └── question.js └── stylesheets │ ├── index.css │ └── reset.css ├── routes ├── answerquestion.js ├── answers.js ├── askquestions.js ├── index.js ├── questions.js ├── searchresults.js ├── users.js └── utils.js └── views ├── askQuestions.pug ├── editQuestions.pug ├── error.pug ├── index.pug ├── layout.pug ├── login.pug ├── question.pug ├── searchresults.pug ├── signup.pug └── utils.pug /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffyang/dark-overflow/16cd56a5da39fa7d6a350ea0c84ab0cf2c4e7d99/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | 'config': path.resolve('config', 'database.js'), 5 | 'models-path': path.resolve('db', 'models'), 6 | 'seeders-path': path.resolve('db', 'seeders'), 7 | 'migrations-path': path.resolve('db', 'migrations') 8 | }; 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Dark Overflow 2 | 3 | DarkOverflow is a community dedicated in helping your journey into Javascript. Ask questions when you are stuck. Look up topics by category or search for solutions to previously answered questions. Upvote the best questions and solutions. Downvote the answers that weren't helpful. Share your code with others. Easy on the eyes for your late night codding sessions. Dark Overflow turns you into a Javascrip Pro. 4 | 5 | 6 | 7 | Technologies 8 | 9 | 10 | * Javascript 11 | * Nodejs 12 | * Pug 13 | * Express 14 | * Sequelize 15 | * PostgreSQL 16 | * Heroku 17 | * Font Awesome 18 | 19 | 20 | Key features 21 | 22 | - Sign up and Log in to the website 23 | - Post and Delete questions 24 | - Post and Delete answers to the questions 25 | - Post and Delete comments 26 | - Upvote and Downvote questions/answers and comments 27 | 28 | 29 | Visit dark overflow 30 | 31 | https://dark-overflow.herokuapp.com/ 32 | 33 | 34 | Developers 35 | 36 | - Adam Lovett [GitHub](https://github.com/adamLovettApps) 37 | - Andrew Musta [GitHub](https://github.com/enomilan) 38 | - Geoffrey Yang [GitHub](https://github.com/geoffyang) 39 | - James Mayfield [GitHub](https://github.com/Jodm522) 40 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const createError = require('http-errors'); 2 | const express = require('express'); 3 | const path = require('path'); 4 | const cookieParser = require('cookie-parser'); 5 | const logger = require('morgan'); 6 | const cors = require('cors'); 7 | const { sequelize } = require('./db/models'); 8 | const session = require('express-session'); 9 | const SequelizeStore = require('connect-session-sequelize')(session.Store); 10 | const indexRouter = require('./routes/index'); 11 | const usersRouter = require('./routes/users'); 12 | const questionsRouter = require('./routes/questions'); 13 | const askQuestionsRouter = require('./routes/askquestions'); 14 | const answersRouter = require('./routes/answers'); 15 | const answerQuestionRouter = require('./routes/answerquestion'); 16 | const searchRouter = require('./routes/searchresults') 17 | 18 | const { restoreUser, requireAuth } = require('./auth.js') 19 | 20 | const app = express(); 21 | 22 | 23 | 24 | // view engine setup 25 | app.set('view engine', 'pug'); 26 | 27 | app.use(logger('dev')); 28 | app.use(express.json()); 29 | app.use(express.urlencoded({ extended: false })); 30 | app.use(cookieParser()); 31 | app.use(express.static(path.join(__dirname, 'public'))); 32 | 33 | // set up session middleware 34 | const store = new SequelizeStore({ db: sequelize }); 35 | 36 | app.use( 37 | session({ 38 | secret: 'superSecret', 39 | store, 40 | saveUninitialized: false, 41 | resave: false, 42 | }) 43 | ); 44 | 45 | // create Session table if it doesn't already exist 46 | store.sync(); 47 | app.use(function(req, res, next) { 48 | res.header("Access-Control-Allow-Origin", '*'); 49 | next(); 50 | }); 51 | app.use(restoreUser); 52 | app.use('/', indexRouter); 53 | app.use('/users', usersRouter); 54 | 55 | app.use('/questions', questionsRouter); 56 | app.use('/askQuestions', askQuestionsRouter); 57 | 58 | app.use('/answers', answersRouter); 59 | app.use('/answerquestion', answerQuestionRouter); 60 | app.use('/searchresults', searchRouter); 61 | 62 | // catch 404 and forward to error handler 63 | app.use(function (req, res, next) { 64 | next(createError(404)); 65 | }); 66 | 67 | // error handler 68 | app.use(function (err, req, res, next) { 69 | // set locals, only providing error in development 70 | res.locals.message = err.message; 71 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 72 | 73 | // render the error page 74 | res.status(err.status || 500); 75 | res.render('error'); 76 | }); 77 | 78 | //testing comment 79 | 80 | 81 | 82 | module.exports = app; 83 | -------------------------------------------------------------------------------- /auth.js: -------------------------------------------------------------------------------- 1 | const db = require("./db/models"); 2 | 3 | const loginUser = (req, res, user) => { 4 | req.session.auth = { 5 | userId: user.id 6 | } 7 | } 8 | 9 | const restoreUser = async (req, res, next) => { 10 | if (req.session.auth) { 11 | const {userId } = req.session.auth; 12 | 13 | try { 14 | const user = await db.Profile.findByPk(userId); 15 | 16 | if (user) { 17 | res.locals.authenticated = true; 18 | res.locals.user = user; 19 | next(); 20 | } 21 | 22 | }catch(err) { 23 | res.locals.authenticated = false; 24 | next(err); 25 | } 26 | } else { 27 | res.locals.authenticated = false; 28 | next(); 29 | } 30 | } 31 | 32 | 33 | const logoutUser = (req, res) => { 34 | 35 | delete req.session.auth; 36 | 37 | 38 | } 39 | 40 | const requireAuth = (req, res, next) => { 41 | if (!res.locals.authenticated) { 42 | res.redirect('/users/signup'); 43 | } else { 44 | next(); 45 | } 46 | } 47 | 48 | module.exports = { 49 | loginUser, 50 | restoreUser, 51 | logoutUser, 52 | requireAuth 53 | } 54 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const port = process.env.PORT || '8080'; 4 | 5 | const app = require('../app'); 6 | 7 | app.listen(port, () => console.log(`Listening on port ${port}...`)); 8 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | // "username": "dark_overflow_user", 4 | // "password": "password1", 5 | // "database": "dark_overflow_db", 6 | // "host": "localhost", 7 | // "dialect": "postgres", 8 | // "operatorsAliases": false, 9 | // "logging": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config/database.js: -------------------------------------------------------------------------------- 1 | const { 2 | db: { username, password, database, host }, 3 | } = require('./index'); 4 | 5 | module.exports = { 6 | development: { 7 | username, 8 | password, 9 | database, 10 | host, 11 | dialect: 'postgres', 12 | seederStorage: 'sequelize', 13 | logging:false, 14 | }, 15 | production: { 16 | use_env_variable: 'DATABASE_URL', 17 | dialect: 'postgres', 18 | seederStorage: 'sequelize', 19 | dialectOptions: { 20 | ssl: { 21 | require: true, 22 | rejectUnauthorized: false 23 | } 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | environment: process.env.NODE_ENV || 'development', 3 | port: process.env.PORT || 8080, 4 | db: { 5 | username: process.env.DB_USERNAME, 6 | password: process.env.DB_PASSWORD, 7 | database: process.env.DB_DATABASE, 8 | host: process.env.DB_HOST, 9 | logging:false 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /db/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffyang/dark-overflow/16cd56a5da39fa7d6a350ea0c84ab0cf2c4e7d99/db/.DS_Store -------------------------------------------------------------------------------- /db/migrations/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffyang/dark-overflow/16cd56a5da39fa7d6a350ea0c84ab0cf2c4e7d99/db/migrations/.keep -------------------------------------------------------------------------------- /db/migrations/20210430153647-create-profile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Profiles', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | userName: { 12 | allowNull: false, 13 | type: Sequelize.STRING, 14 | unique: true 15 | }, 16 | firstName: { 17 | allowNull: false, 18 | type: Sequelize.STRING 19 | }, 20 | lastName: { 21 | allowNull: false, 22 | type: Sequelize.STRING 23 | }, 24 | email: { 25 | allowNull: false, 26 | type: Sequelize.STRING, 27 | unique: true 28 | }, 29 | hashedPassword: { 30 | type: Sequelize.STRING.BINARY, 31 | allowNull: false 32 | }, 33 | needsJob: { 34 | // allowNull: false, 35 | type: Sequelize.BOOLEAN, 36 | defaultValue: false 37 | }, 38 | offeringJob: { 39 | // allowNull: false, 40 | type: Sequelize.BOOLEAN, 41 | defaultValue: false 42 | }, 43 | score: { 44 | allowNull: false, 45 | defaultValue: "0", 46 | type: Sequelize.INTEGER 47 | }, 48 | createdAt: { 49 | allowNull: false, 50 | type: Sequelize.DATE 51 | }, 52 | updatedAt: { 53 | allowNull: false, 54 | type: Sequelize.DATE 55 | } 56 | }); 57 | }, 58 | down: (queryInterface, Sequelize) => { 59 | return queryInterface.dropTable('Profiles'); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /db/migrations/20210430155711-create-category.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Categories', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | name: { 12 | allowNull: false, 13 | type: Sequelize.STRING 14 | }, 15 | createdAt: { 16 | allowNull: false, 17 | type: Sequelize.DATE 18 | }, 19 | updatedAt: { 20 | allowNull: false, 21 | type: Sequelize.DATE 22 | } 23 | }); 24 | }, 25 | down: (queryInterface, Sequelize) => { 26 | return queryInterface.dropTable('Categories'); 27 | } 28 | }; -------------------------------------------------------------------------------- /db/migrations/20210430160023-create-question.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Questions', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | title: { 12 | allowNull: false, 13 | type: Sequelize.STRING 14 | }, 15 | text: { 16 | allowNull: false, 17 | type: Sequelize.TEXT 18 | }, 19 | score: { 20 | allowNull: false, 21 | type: Sequelize.INTEGER, 22 | default: 0 23 | }, 24 | userId: { 25 | allowNull: false, 26 | type: Sequelize.INTEGER, 27 | references: { model: 'Profiles', key: 'id' } 28 | }, 29 | categoryId: { 30 | allowNull: false, 31 | type: Sequelize.INTEGER, 32 | references: { model: 'Categories', key: 'id' } 33 | }, 34 | createdAt: { 35 | allowNull: false, 36 | type: Sequelize.DATE 37 | }, 38 | updatedAt: { 39 | allowNull: false, 40 | type: Sequelize.DATE 41 | } 42 | }); 43 | }, 44 | down: (queryInterface, Sequelize) => { 45 | return queryInterface.dropTable('Questions'); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /db/migrations/20210430161232-create-question-vote.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('QuestionVotes', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | voteSum: { 12 | allowNull: false, 13 | type: Sequelize.INTEGER 14 | }, 15 | userId: { 16 | allowNull: false, 17 | type: Sequelize.INTEGER, 18 | references: { model: 'Profiles', key: 'id' } 19 | }, 20 | questionId: { 21 | allowNull: false, 22 | type: Sequelize.INTEGER, 23 | references: { model: 'Questions', key: 'id' } 24 | }, 25 | createdAt: { 26 | allowNull: false, 27 | type: Sequelize.DATE 28 | }, 29 | updatedAt: { 30 | allowNull: false, 31 | type: Sequelize.DATE 32 | } 33 | }); 34 | }, 35 | down: (queryInterface, Sequelize) => { 36 | return queryInterface.dropTable('QuestionVotes'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /db/migrations/20210430161904-create-answer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Answers', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | text: { 12 | allowNull: false, 13 | type: Sequelize.TEXT 14 | }, 15 | score: { 16 | allowNull: false, 17 | type: Sequelize.INTEGER 18 | }, 19 | questionId: { 20 | allowNull: false, 21 | type: Sequelize.INTEGER, 22 | references: { model: 'Questions', key: 'id' } 23 | }, 24 | userId: { 25 | allowNull: false, 26 | type: Sequelize.INTEGER, 27 | references: { model: 'Profiles', key: 'id' } 28 | }, 29 | createdAt: { 30 | allowNull: false, 31 | type: Sequelize.DATE 32 | }, 33 | updatedAt: { 34 | allowNull: false, 35 | type: Sequelize.DATE 36 | } 37 | }); 38 | }, 39 | down: (queryInterface, Sequelize) => { 40 | return queryInterface.dropTable('Answers'); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /db/migrations/20210430162300-create-answer-vote.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('AnswerVotes', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | voteSum: { 12 | allowNull: false, 13 | type: Sequelize.INTEGER 14 | }, 15 | userId: { 16 | allowNull: false, 17 | type: Sequelize.INTEGER, 18 | references: { model: 'Profiles', key: 'id' } 19 | }, 20 | answerId: { 21 | allowNull: false, 22 | type: Sequelize.INTEGER, 23 | references: { model: 'Answers', key: 'id' } 24 | }, 25 | createdAt: { 26 | allowNull: false, 27 | type: Sequelize.DATE 28 | }, 29 | updatedAt: { 30 | allowNull: false, 31 | type: Sequelize.DATE 32 | } 33 | }); 34 | }, 35 | down: (queryInterface, Sequelize) => { 36 | return queryInterface.dropTable('AnswerVotes'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /db/migrations/20210430162740-create-comment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Comments', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | text: { 12 | allowNull: false, 13 | type: Sequelize.TEXT 14 | }, 15 | score: { 16 | allowNull: false, 17 | type: Sequelize.INTEGER 18 | }, 19 | answerId: { 20 | allowNull: false, 21 | type: Sequelize.INTEGER, 22 | references: { model: 'Answers', key: 'id' } 23 | }, 24 | userId: { 25 | allowNull: false, 26 | type: Sequelize.INTEGER, 27 | references: { model: 'Profiles', key: 'id' } 28 | }, 29 | createdAt: { 30 | allowNull: false, 31 | type: Sequelize.DATE 32 | }, 33 | updatedAt: { 34 | allowNull: false, 35 | type: Sequelize.DATE 36 | } 37 | }); 38 | }, 39 | down: (queryInterface, Sequelize) => { 40 | return queryInterface.dropTable('Comments'); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /db/migrations/20210430163147-create-comment-vote.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('CommentVotes', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | voteSum: { 12 | allowNull: false, 13 | type: Sequelize.INTEGER 14 | }, 15 | userId: { 16 | allowNull: false, 17 | type: Sequelize.INTEGER, 18 | references: { model: 'Profiles', key: 'id' } 19 | }, 20 | commentId: { 21 | allowNull: false, 22 | type: Sequelize.INTEGER, 23 | references: { model: 'Comments', key: 'id' } 24 | }, 25 | createdAt: { 26 | allowNull: false, 27 | type: Sequelize.DATE 28 | }, 29 | updatedAt: { 30 | allowNull: false, 31 | type: Sequelize.DATE 32 | } 33 | }); 34 | }, 35 | down: (queryInterface, Sequelize) => { 36 | return queryInterface.dropTable('CommentVotes'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /db/models/answer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = (sequelize, DataTypes) => { 3 | const Answer = sequelize.define( 4 | "Answer", 5 | { 6 | text: DataTypes.TEXT, 7 | score: DataTypes.INTEGER, 8 | questionId: DataTypes.INTEGER, 9 | userId: DataTypes.INTEGER, 10 | }, 11 | {} 12 | ); 13 | Answer.associate = function (models) { 14 | Answer.belongsTo(models.Profile, { foreignKey: "userId" }); 15 | Answer.belongsTo(models.Answer, { foreignKey: "questionId" }); 16 | Answer.hasMany(models.AnswerVote, { 17 | foreignKey: "answerId", 18 | onDelete: "CASCADE", 19 | hooks: true, 20 | }); 21 | Answer.hasMany(models.Comment, { 22 | foreignKey: "answerId", 23 | onDelete: "CASCADE", 24 | hooks: true, 25 | }); 26 | }; 27 | return Answer; 28 | }; 29 | -------------------------------------------------------------------------------- /db/models/answervote.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = (sequelize, DataTypes) => { 3 | const AnswerVote = sequelize.define( 4 | "AnswerVote", 5 | { 6 | voteSum: DataTypes.INTEGER, 7 | userId: DataTypes.INTEGER, 8 | answerId: DataTypes.INTEGER, 9 | }, 10 | {} 11 | ); 12 | AnswerVote.associate = function (models) { 13 | AnswerVote.belongsTo(models.Profile, { foreignKey: "userId" }); 14 | AnswerVote.belongsTo(models.Answer, { foreignKey: "answerId" }); 15 | }; 16 | return AnswerVote; 17 | }; 18 | -------------------------------------------------------------------------------- /db/models/category.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = (sequelize, DataTypes) => { 3 | const Category = sequelize.define( 4 | "Category", 5 | { 6 | name: DataTypes.STRING, 7 | }, 8 | {} 9 | ); 10 | Category.associate = function (models) { 11 | Category.hasMany(models.Question, { 12 | foreignKey: "categoryId", 13 | onDelete: "CASCADE", 14 | hooks: true, 15 | }); 16 | }; 17 | return Category; 18 | }; 19 | -------------------------------------------------------------------------------- /db/models/comment.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = (sequelize, DataTypes) => { 3 | const Comment = sequelize.define( 4 | "Comment", 5 | { 6 | text: DataTypes.TEXT, 7 | score: DataTypes.INTEGER, 8 | answerId: DataTypes.INTEGER, 9 | userId: DataTypes.INTEGER, 10 | }, 11 | {} 12 | ); 13 | Comment.associate = function (models) { 14 | Comment.belongsTo(models.Profile, { foreignKey: "userId" }); 15 | Comment.hasMany(models.CommentVote, { 16 | foreignKey: "commentId", 17 | onDelete: "CASCADE", 18 | hooks: true, 19 | }); 20 | Comment.belongsTo(models.Answer, { foreignKey: "answerId" }); 21 | }; 22 | return Comment; 23 | }; 24 | -------------------------------------------------------------------------------- /db/models/commentvote.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = (sequelize, DataTypes) => { 3 | const CommentVote = sequelize.define( 4 | "CommentVote", 5 | { 6 | voteSum: DataTypes.INTEGER, 7 | userId: DataTypes.INTEGER, 8 | commentId: DataTypes.INTEGER, 9 | }, 10 | {} 11 | ); 12 | CommentVote.associate = function (models) { 13 | CommentVote.belongsTo(models.Profile, { foreignKey: "userId" }); 14 | CommentVote.belongsTo(models.Comment, { foreignKey: "commentId" }); 15 | }; 16 | return CommentVote; 17 | }; 18 | -------------------------------------------------------------------------------- /db/models/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const Sequelize = require("sequelize"); 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || "development"; 8 | const config = require(__dirname + "/../../config/database.js")[env]; 9 | const db = {}; 10 | 11 | let sequelize; 12 | if (config.use_env_variable) { 13 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 14 | } else { 15 | sequelize = new Sequelize( 16 | config.database, 17 | config.username, 18 | config.password, 19 | config 20 | ); 21 | } 22 | 23 | fs.readdirSync(__dirname) 24 | .filter((file) => { 25 | return ( 26 | file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js" 27 | ); 28 | }) 29 | .forEach((file) => { 30 | const model = sequelize["import"](path.join(__dirname, file)); 31 | db[model.name] = model; 32 | }); 33 | 34 | Object.keys(db).forEach((modelName) => { 35 | if (db[modelName].associate) { 36 | db[modelName].associate(db); 37 | } 38 | }); 39 | 40 | db.sequelize = sequelize; 41 | db.Sequelize = Sequelize; 42 | 43 | module.exports = db; 44 | -------------------------------------------------------------------------------- /db/models/profile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = (sequelize, DataTypes) => { 3 | const Profile = sequelize.define( 4 | "Profile", 5 | { 6 | userName: DataTypes.STRING, 7 | firstName: DataTypes.STRING, 8 | lastName: DataTypes.STRING, 9 | email: DataTypes.STRING, 10 | hashedPassword: DataTypes.STRING.BINARY, 11 | needsJob: DataTypes.BOOLEAN, 12 | offeringJob: DataTypes.BOOLEAN, 13 | score: DataTypes.INTEGER, 14 | }, 15 | {} 16 | ); 17 | Profile.associate = function (models) { 18 | Profile.hasMany(models.Question, { 19 | foreignKey: "userId", 20 | onDelete: "CASCADE", 21 | hooks: true, 22 | }); 23 | Profile.hasMany(models.Answer, { 24 | foreignKey: "userId", 25 | onDelete: "CASCADE", 26 | hooks: true, 27 | }); 28 | Profile.hasMany(models.QuestionVote, { 29 | foreignKey: "userId", 30 | onDelete: "CASCADE", 31 | hooks: true, 32 | }); 33 | Profile.hasMany(models.AnswerVote, { 34 | foreignKey: "userId", 35 | onDelete: "CASCADE", 36 | hooks: true, 37 | }); 38 | Profile.hasMany(models.CommentVote, { 39 | foreignKey: "userId", 40 | onDelete: "CASCADE", 41 | hooks: true, 42 | }); 43 | Profile.hasMany(models.Comment, { 44 | foreignKey: "userId", 45 | onDelete: "CASCADE", 46 | hooks: true, 47 | }); 48 | }; 49 | return Profile; 50 | }; 51 | -------------------------------------------------------------------------------- /db/models/question.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = (sequelize, DataTypes) => { 3 | const Question = sequelize.define( 4 | "Question", 5 | { 6 | title: DataTypes.STRING, 7 | text: DataTypes.TEXT, 8 | score: DataTypes.INTEGER, 9 | userId: DataTypes.INTEGER, 10 | categoryId: DataTypes.INTEGER, 11 | }, 12 | {} 13 | ); 14 | Question.associate = function (models) { 15 | Question.belongsTo(models.Profile, { foreignKey: "userId" }); 16 | Question.belongsTo(models.Category, { foreignKey: "categoryId" }); 17 | 18 | Question.hasMany(models.QuestionVote, { 19 | foreignKey: "questionId", 20 | onDelete: "CASCADE", 21 | hooks: true, 22 | }); 23 | Question.hasMany(models.Answer, { 24 | foreignKey: "questionId", 25 | onDelete: "CASCADE", 26 | hooks: true, 27 | }); 28 | }; 29 | return Question; 30 | }; 31 | -------------------------------------------------------------------------------- /db/models/questionvote.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = (sequelize, DataTypes) => { 3 | const QuestionVote = sequelize.define( 4 | "QuestionVote", 5 | { 6 | voteSum: DataTypes.INTEGER, 7 | userId: DataTypes.INTEGER, 8 | questionId: DataTypes.INTEGER, 9 | }, 10 | {} 11 | ); 12 | QuestionVote.associate = function (models) { 13 | QuestionVote.belongsTo(models.Profile, { foreignKey: "userId" }); 14 | QuestionVote.belongsTo(models.Question, { foreignKey: "questionId" }); 15 | }; 16 | return QuestionVote; 17 | }; 18 | -------------------------------------------------------------------------------- /db/seeders/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffyang/dark-overflow/16cd56a5da39fa7d6a350ea0c84ab0cf2c4e7d99/db/seeders/.keep -------------------------------------------------------------------------------- /db/seeders/20210504171600-onecategory.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | /* 6 | Add altering commands here. 7 | Return a promise to correctly handle asynchronicity. 8 | 9 | Example: 10 | return queryInterface.bulkInsert('People', [{ 11 | name: 'John Doe', 12 | isBetaMember: false 13 | }], {}); 14 | */ 15 | return queryInterface.bulkInsert( 16 | "Categories", 17 | [ 18 | { 19 | name: "Algorithms & Sorts", 20 | createdAt: new Date(), 21 | updatedAt: new Date(), 22 | }, 23 | { name: "Ajax", createdAt: new Date(), updatedAt: new Date() }, 24 | { name: "Async/Await", createdAt: new Date(), updatedAt: new Date() }, 25 | { name: "Arrays", createdAt: new Date(), updatedAt: new Date() }, 26 | 27 | { name: "Callbacks", createdAt: new Date(), updatedAt: new Date() }, 28 | { name: "Classes", createdAt: new Date(), updatedAt: new Date() }, 29 | { name: "Closures", createdAt: new Date(), updatedAt: new Date() }, 30 | { name: "CSS", createdAt: new Date(), updatedAt: new Date() }, 31 | { name: "Debugging", createdAt: new Date(), updatedAt: new Date() }, 32 | { name: "Express", createdAt: new Date(), updatedAt: new Date() }, 33 | { name: "HTTP", createdAt: new Date(), updatedAt: new Date() }, 34 | { name: "Networking", createdAt: new Date(), updatedAt: new Date() }, 35 | { name: "Node.js", createdAt: new Date(), updatedAt: new Date() }, 36 | { name: "NPM", createdAt: new Date(), updatedAt: new Date() }, 37 | { name: "Objects", createdAt: new Date(), updatedAt: new Date() }, 38 | { name: "Promises", createdAt: new Date(), updatedAt: new Date() }, 39 | { name: "Recursion", createdAt: new Date(), updatedAt: new Date() }, 40 | { name: "Regex", createdAt: new Date(), updatedAt: new Date() }, 41 | { 42 | name: "Scope & Hoisting", 43 | createdAt: new Date(), 44 | updatedAt: new Date(), 45 | }, 46 | { 47 | name: "Time Complexity", 48 | createdAt: new Date(), 49 | updatedAt: new Date(), 50 | }, 51 | { name: "Variables", createdAt: new Date(), updatedAt: new Date() }, 52 | { name: "Other", createdAt: new Date(), updatedAt: new Date() }, 53 | ], 54 | {} 55 | ); 56 | }, 57 | 58 | down: (queryInterface, Sequelize) => { 59 | /* 60 | Add reverting commands here. 61 | Return a promise to correctly handle asynchronicity. 62 | 63 | Example: 64 | return queryInterface.bulkDelete('People', null, {}); 65 | */ 66 | return queryInterface.bulkDelete("Categories", null, {}); 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /events.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffyang/dark-overflow/16cd56a5da39fa7d6a350ea0c84ab0cf2c4e7d99/events.js -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-project-starter", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "per-env", 7 | "dev": "nodemon -r dotenv/config ./bin/www", 8 | "start:production": "node ./bin/www" 9 | }, 10 | "dependencies": { 11 | "bcryptjs": "^2.4.3", 12 | "connect-session-sequelize": "^7.0.4", 13 | "cookie-parser": "~1.4.4", 14 | "cors": "^2.8.5", 15 | "csurf": "^1.11.0", 16 | "debug": "~2.6.9", 17 | "express": "~4.16.1", 18 | "express-session": "^1.17.1", 19 | "express-validator": "^6.10.1", 20 | "fetch": "^1.1.0", 21 | "http-errors": "~1.6.3", 22 | "morgan": "~1.9.1", 23 | "per-env": "^1.0.2", 24 | "pg": "^8.6.0", 25 | "pug": "2.0.4", 26 | "sequelize": "^5.22.3", 27 | "sequelize-cli": "^5.5.1" 28 | }, 29 | "devDependencies": { 30 | "dotenv": "^8.2.0", 31 | "dotenv-cli": "^4.0.0", 32 | "nodemon": "^2.0.6" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffyang/dark-overflow/16cd56a5da39fa7d6a350ea0c84ab0cf2c4e7d99/public/.DS_Store -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffyang/dark-overflow/16cd56a5da39fa7d6a350ea0c84ab0cf2c4e7d99/public/favicon.ico -------------------------------------------------------------------------------- /public/images/404-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffyang/dark-overflow/16cd56a5da39fa7d6a350ea0c84ab0cf2c4e7d99/public/images/404-image.png -------------------------------------------------------------------------------- /public/images/DO_black-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffyang/dark-overflow/16cd56a5da39fa7d6a350ea0c84ab0cf2c4e7d99/public/images/DO_black-transparent.png -------------------------------------------------------------------------------- /public/images/DO_white-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffyang/dark-overflow/16cd56a5da39fa7d6a350ea0c84ab0cf2c4e7d99/public/images/DO_white-transparent.png -------------------------------------------------------------------------------- /public/images/corner-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffyang/dark-overflow/16cd56a5da39fa7d6a350ea0c84ab0cf2c4e7d99/public/images/corner-image.png -------------------------------------------------------------------------------- /public/javascripts/index.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("DOMContentLoaded", async (event) => { 2 | console.log("hello from javascript!"); 3 | 4 | const upVote = document.querySelector("#fa-caret-square-up"); 5 | const downVote = document.querySelector("#fa-caret-square-down"); 6 | 7 | // upVote.addEventListener("click", await (event) => { 8 | // const voteType = 1 9 | // }) 10 | 11 | // downVote.addEventListener("click", await (event) => { 12 | // const voteType = 2 13 | // }) 14 | 15 | // QUESTION PAGE DOM MANIPULATION 16 | // delete question 17 | const deleteQuestion = document.querySelector(".delete-question-btn"); 18 | deleteQuestion.addEventListener("click", async (e) => { 19 | const target = e.target; 20 | const id = target.id; 21 | const res = await fetch(`/questions/${id}`, { 22 | method: "DELETE", 23 | }); 24 | }); 25 | 26 | 27 | 28 | // const answerButton = document.querySelector('#') 29 | }); 30 | -------------------------------------------------------------------------------- /public/javascripts/layout.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', event => { 2 | 3 | const categorySelect = document.querySelector('.search-category-selector'); 4 | categorySelect.addEventListener('change', (event) => { 5 | if (event.target.value) document.location.href = `/searchresults/${event.target.value}`; 6 | }); 7 | }) -------------------------------------------------------------------------------- /public/javascripts/question.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("DOMContentLoaded", async (event) => { 2 | 3 | const upVoteQ = document.querySelector(".question-page-upvote-question-icon"); 4 | const downVoteQ = document.querySelector(".question-page-downvote-question-icon"); 5 | const deleteQuestion = document.querySelector(".delete-question-btn"); 6 | const deleteAnswers = document.querySelectorAll(".delete-answer-btn"); 7 | const answerQuestionButton = document.querySelector( 8 | ".answer-question-button" 9 | ); 10 | const cancelAnswerButton = document.querySelector(".cancel-answer-button"); 11 | 12 | const openAnswerQuestion = document.querySelector(".open-answer-question"); 13 | 14 | const answersDiv = document.getElementById("answersDiv"); 15 | 16 | const answerTextBox = document.querySelector(".answer-text-box"); 17 | 18 | const editButton = document.querySelector(".edit-question-btn "); 19 | 20 | const submitEditButton = document.querySelector( 21 | ".submit-question-edit-button" 22 | ); 23 | const cancelEditButton = document.querySelector( 24 | ".cancel-edit-question-button" 25 | ); 26 | 27 | 28 | let originalTitle = ""; 29 | let originalText = ""; 30 | upVoteQ.addEventListener("click", (e) => questionVote(1, e.target.id)); 31 | downVoteQ.addEventListener("click", (e) => questionVote(2, e.target.id)); 32 | 33 | const upVoteA = document.querySelectorAll(".answer-upvote-arrow"); 34 | const downVoteA = document.querySelectorAll(".answer-downvote-arrow"); 35 | upVoteA.forEach(upVoteButton => { 36 | upVoteButton.addEventListener("click", (e) => answerVote(1, e.target.id)); 37 | }) 38 | 39 | downVoteA.forEach(downVoteButton => { 40 | downVoteButton.addEventListener("click", (e) => answerVote(2, e.target.id)); 41 | }) 42 | 43 | if (editButton) { 44 | editButton.addEventListener("click", (event) => { 45 | originalTitle = document.querySelector( 46 | ".question-page-question-box-title" 47 | ).innerText; 48 | originalText = document.querySelector( 49 | ".question-page-question-box-text-paragraph" 50 | ).innerText; 51 | document.querySelector(".editQuestionForm").style.display = "block"; 52 | }); 53 | } 54 | 55 | if (openAnswerQuestion) { 56 | openAnswerQuestion.addEventListener("click", (e) => { 57 | document.querySelector(".answerQuestionForm").style.display = "block"; 58 | }); 59 | } 60 | if (cancelAnswerButton) { 61 | cancelAnswerButton.addEventListener("click", (e) => { 62 | 63 | e.preventDefault(); 64 | document.querySelector(".answerQuestionForm").style.display = "none"; 65 | }); 66 | } 67 | 68 | if (cancelEditButton) { 69 | cancelEditButton.addEventListener("click", (event) => { 70 | event.preventDefault(); 71 | document.querySelector(".editQuestionForm").style.display = "none"; 72 | }); 73 | } 74 | if (submitEditButton) { 75 | submitEditButton.addEventListener("click", async (event) => { 76 | event.preventDefault(); 77 | let title = document.querySelector(".edit-question-title-field").value; 78 | let text = document.querySelector(".edit-question-text-field").value; 79 | let chosenCategory = document.querySelector( 80 | ".edit-question-category-field" 81 | ).value; 82 | let categoryText = document.querySelector( 83 | ".edit-question-category-field" 84 | ); 85 | let categoryTextValue = 86 | categoryText.options[categoryText.selectedIndex].text; 87 | let csrfvalue = document.querySelector(".csrfEdit").value; 88 | let questionId = document.querySelector(".questionToEditId").value; 89 | if (title.trim() && text.trim()) { 90 | try { 91 | const res = await fetch( 92 | `/askquestions/${questionId}`, 93 | { 94 | method: "POST", 95 | headers: { 96 | "Content-Type": "application/json", 97 | "X-CSRF-Token": csrfvalue, 98 | }, 99 | 100 | body: JSON.stringify({ text, title, chosenCategory }), 101 | } 102 | ); 103 | 104 | // const data = await res.json(); 105 | const pageTitle = document.querySelector( 106 | ".question-page-question-box-title" 107 | ); 108 | pageTitle.innerText = title; 109 | const pageText = document.querySelector( 110 | ".question-page-question-box-text-paragraph" 111 | ); 112 | pageText.innerText = text; 113 | document.querySelector(".editQuestionForm").style.display = "none"; 114 | const categoryText = document.querySelector( 115 | ".question-page-question-box-category-paragraph" 116 | ); 117 | categoryText.innerText = categoryTextValue; 118 | } catch (err) { 119 | console.log(err); 120 | } 121 | } 122 | 123 | if (!title.trim()) { 124 | document.querySelector( 125 | ".edit-question-title-field" 126 | ).value = originalTitle; 127 | } 128 | 129 | if (!text.trim()) { 130 | document.querySelector( 131 | ".edit-question-text-field" 132 | ).value = originalText; 133 | } 134 | }); 135 | } 136 | 137 | if (answerQuestionButton) { 138 | answerQuestionButton.addEventListener("click", async (e) => { 139 | e.preventDefault(); 140 | 141 | const id = e.target.id; 142 | 143 | const answerId = await postAnswer(`answerquestion/${id}`, answerTextBox); 144 | 145 | 146 | const newAnswerDiv = document.createElement("div"); 147 | 148 | newAnswerDiv.setAttribute("id", `answer-${id}-div`); 149 | 150 | newAnswerDiv.setAttribute("class", "question-page-answer-box-outer-wrapper"); 151 | const newAnswerButtons = document.createElement("div"); 152 | 153 | const newAnswerDelete = document.createElement("BUTTON"); 154 | newAnswerDelete.innerHTML = "Delete answer"; 155 | newAnswerDelete.setAttribute("class", "button"); 156 | 157 | newAnswerDelete.addEventListener("click", async (e) => { 158 | e.preventDefault(); 159 | removeDiv(id); 160 | 161 | await deleteItem("Answer", "answers", answerId); 162 | }); 163 | 164 | newAnswerButtons.appendChild(newAnswerDelete); 165 | 166 | const newAnswerText = document.createElement("div"); 167 | newAnswerText.innerHTML = answerTextBox.value; 168 | answerTextBox.value = ""; 169 | answersDiv.appendChild(newAnswerDiv); 170 | newAnswerDiv.appendChild(newAnswerText); 171 | const answerHr = document.createElement('hr'); 172 | answerHr.setAttribute('class', 'question-page-answer-box-divider') 173 | 174 | 175 | newAnswerDiv.appendChild(answerHr); 176 | const usernameBox = document.createElement('div'); 177 | let userForBox = document.getElementById('layout-greeter').innerText; 178 | userForBox = userForBox.split(' ')[1]; 179 | usernameBox.setAttribute('class', 'question-page-answer-box-username-answerer'); 180 | usernameBox.innerText = `Answered by: ${userForBox}`; 181 | newAnswerDiv.appendChild(usernameBox); 182 | //newAnswerDiv.appendChild(newAnswerButtons); 183 | const deleteAnswerButton = document.createElement('i'); 184 | deleteAnswerButton.setAttribute('class', 'fas'); 185 | deleteAnswerButton.classList.add('fa-trash'); 186 | deleteAnswerButton.classList.add('delete-answer-btn'); 187 | deleteAnswerButton.setAttribute('id', id); 188 | const editAnswerButton = document.createElement('i'); 189 | editAnswerButton.setAttribute('class', 'fas'); 190 | editAnswerButton.classList.add('fa-edit'); 191 | editAnswerButton.classList.add('edit-answer-btn'); 192 | editAnswerButton.setAttribute('id', id); 193 | newAnswerDiv.appendChild(deleteAnswerButton); 194 | deleteAnswerButton.addEventListener("click", async (e) => { 195 | newAnswerDiv.parentNode.removeChild(newAnswerDiv); 196 | await deleteItem("Answer", "answers", answerId); 197 | }); 198 | newAnswerDiv.appendChild(editAnswerButton); 199 | newAnswerDiv.style.paddingBottom = "30px"; 200 | const scoreBox = document.createElement('div'); 201 | scoreBox.setAttribute('class', 'answer-score-text-box'); 202 | const scorePara = document.createElement('p'); 203 | scorePara.innerHTML = `Score: 0` 204 | scoreBox.appendChild(scorePara); 205 | newAnswerDiv.appendChild(scoreBox); 206 | const upvoteAnswerButton = document.createElement('i'); 207 | upvoteAnswerButton.setAttribute('class', 'far'); 208 | upvoteAnswerButton.classList.add('fa-caret-square-up'); 209 | upvoteAnswerButton.classList.add('answer-upvote-arrow'); 210 | upvoteAnswerButton.setAttribute('id', `A${answerId}-up`); 211 | newAnswerDiv.appendChild(upvoteAnswerButton); 212 | const downvoteAnswerButton = document.createElement('i'); 213 | downvoteAnswerButton.setAttribute('class', 'far'); 214 | downvoteAnswerButton.classList.add('fa-caret-square-down'); 215 | downvoteAnswerButton.classList.add('answer-upvote-arrow'); 216 | downvoteAnswerButton.setAttribute('id', `A${answerId}-down`); 217 | newAnswerDiv.appendChild(downvoteAnswerButton); 218 | upvoteAnswerButton.style.marginRight = "8px"; 219 | upvoteAnswerButton.addEventListener("click", (e) => answerVote(1, e.target.id)); 220 | downvoteAnswerButton.addEventListener("click", (e) => answerVote(2, e.target.id)); 221 | document.querySelector(".answerQuestionForm").style.display = "none"; 222 | }); 223 | } 224 | 225 | if (deleteQuestion) { 226 | deleteQuestion.addEventListener("click", async (e) => { 227 | const target = e.target; 228 | const id = target.id; 229 | await deleteItem("Question", "questions", id, "/"); 230 | }); 231 | } 232 | 233 | if (deleteAnswers.length) { 234 | //console.log(deleteAnswers); 235 | // Use a for loop to add an event listener to each answer div 236 | for (let i = 0; i < deleteAnswers.length; i++) { 237 | deleteAnswers[i].addEventListener("click", async (e) => { 238 | const target = e.target; 239 | const id = target.id; 240 | //pass the appropriate vairables into the deleteItem function 241 | removeDiv(id); 242 | await deleteItem("Answer", "answers", id); 243 | }); 244 | } 245 | } 246 | 247 | 248 | 249 | }); 250 | 251 | const removeDiv = function (id) { 252 | var elemToDelete = document.getElementById(`answer-${id}-div`); 253 | elemToDelete.parentNode.removeChild(elemToDelete); 254 | }; 255 | 256 | const deleteItem = async function (type, route, id, reroute) { 257 | console.groupCollapsed(id); 258 | 259 | try { 260 | const res = await fetch(`/${route}/${id}`, { 261 | method: "DELETE", 262 | }); 263 | 264 | 265 | if (reroute) window.location.href = reroute; 266 | } catch (err) { 267 | window.alert("error: " + err); 268 | } 269 | }; 270 | 271 | async function postAnswer(route, answerTextBox) { 272 | textToSend = answerTextBox.value; 273 | 274 | try { 275 | const res = await fetch(`/${route}`, { 276 | method: "POST", 277 | headers: { "Content-Type": "application/json" }, 278 | 279 | body: JSON.stringify({ textToSend }), 280 | }); 281 | 282 | const data = await res.json(); 283 | const answerId = data.answerId; 284 | 285 | return answerId; 286 | } catch (err) { 287 | console.log(err); 288 | } 289 | } 290 | 291 | async function questionVote(upOrDownCode, questionId) { 292 | const upVoteQ = document.querySelector(".fa-caret-square-up"); 293 | const downVoteQ = document.querySelector(".fa-caret-square-down"); 294 | const score = document.querySelector(".question-page-question-score"); 295 | 296 | if (upOrDownCode === 1 && upVoteQ.classList.contains("upvoted-arrow")) { 297 | try { 298 | await fetch(`/questions/${questionId}/vote`, { 299 | method: `DELETE`, 300 | }); 301 | } catch (err) { 302 | console.log("question vote error", err); 303 | } 304 | upVoteQ.classList.remove("upvoted-arrow"); 305 | score.innerText--; 306 | return; 307 | } 308 | 309 | if (upOrDownCode === 2 && downVoteQ.classList.contains("downvoted-arrow")) { 310 | try { 311 | await fetch(`/questions/${questionId}/vote`, { 312 | method: `DELETE`, 313 | }); 314 | } catch (err) { 315 | console.log("question vote error", err); 316 | } 317 | downVoteQ.classList.remove("downvoted-arrow"); 318 | score.innerText++; 319 | return; 320 | } 321 | 322 | try { 323 | if ( 324 | downVoteQ.classList.contains("downvoted-arrow") || 325 | upVoteQ.classList.contains("upvoted-arrow") 326 | ) { 327 | await fetch(`/questions/${questionId}/vote`, { 328 | method: `DELETE`, 329 | }); 330 | } 331 | await fetch( 332 | `/questions/${questionId}/vote/${upOrDownCode}`, 333 | { method: `POST` } 334 | ); 335 | 336 | if (upOrDownCode === 1) { 337 | score.innerText++; 338 | if (downVoteQ.classList.contains("downvoted-arrow")) score.innerText++; 339 | upVoteQ.classList.add("upvoted-arrow"); 340 | downVoteQ.classList.remove("downvoted-arrow"); 341 | } else { 342 | score.innerText--; 343 | if (upVoteQ.classList.contains("upvoted-arrow")) score.innerText--; 344 | downVoteQ.classList.add("downvoted-arrow"); 345 | upVoteQ.classList.remove("upvoted-arrow"); 346 | } 347 | } catch (err) { 348 | console.log("question vote error", err); 349 | } 350 | } 351 | 352 | async function answerVote(upOrDownCode, answerId) { 353 | 354 | answerId = answerId.slice(1); 355 | answerId = answerId.split('-')[0]; 356 | const score = document.getElementById(`A${answerId}-score`) 357 | const upvoteButton = document.getElementById(`A${answerId}-up`); 358 | const downvoteButton = document.getElementById(`A${answerId}-down`); 359 | 360 | 361 | // previously voted 362 | if (upOrDownCode === 1 && upvoteButton.classList.contains("upvoted-arrow")) { 363 | try { 364 | await fetch(`/answers/${answerId}/vote`, { 365 | method: `DELETE`, 366 | }); 367 | } catch (err) { 368 | console.log("question vote error", err); 369 | } 370 | upvoteButton.classList.remove("upvoted-arrow"); 371 | score.innerText--; 372 | return; 373 | } 374 | 375 | // previously voted 376 | if (upOrDownCode === 2 && downvoteButton.classList.contains("downvoted-arrow")) { 377 | try { 378 | await fetch(`/answers/${answerId}/vote`, { 379 | method: `DELETE`, 380 | }); 381 | } catch (err) { 382 | console.log("question vote error", err); 383 | } 384 | downvoteButton.classList.remove("downvoted-arrow"); 385 | score.innerText++; 386 | return; 387 | } 388 | 389 | // fresh vote 390 | try { 391 | if (downvoteButton.classList.contains("downvoted-arrow") || upvoteButton.classList.contains("upvoted-arrow")) { 392 | await fetch(`/answers/${answerId}/vote`, { 393 | method: `DELETE`, 394 | }); 395 | } 396 | await fetch( 397 | `/answers/${answerId}/vote/${upOrDownCode}`, 398 | { method: `POST` } 399 | ); 400 | 401 | if (upOrDownCode === 1) { //upvote 402 | if (downvoteButton.classList.contains("downvoted-arrow")) score.innerText++; 403 | score.innerText++; 404 | upvoteButton.classList.add("upvoted-arrow"); 405 | downvoteButton.classList.remove("downvoted-arrow"); 406 | } else { //downvote 407 | if (upvoteButton.classList.contains("upvoted-arrow")) score.innerText--; 408 | score.innerText--; 409 | downvoteButton.classList.add("downvoted-arrow"); 410 | upvoteButton.classList.remove("upvoted-arrow"); 411 | } 412 | 413 | } catch (err) { 414 | console.log("question vote error", err); 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /public/stylesheets/index.css: -------------------------------------------------------------------------------- 1 | /*------------------root variable setup------------------------*/ 2 | :root { 3 | --main-background: #222831; 4 | --secondary-background: #2d4059; 5 | --accent-color: #ff5722; 6 | --text-color: #eeeeee; 7 | } 8 | 9 | /*----------------------Basic body setup----------------------*/ 10 | 11 | * { 12 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 13 | font-kerning: auto; 14 | font-size: 15px; 15 | font-stretch: normal; 16 | font-style: normal; 17 | font-variant: normal; 18 | font-variant-ligatures: normal; 19 | font-weight: normal; 20 | box-sizing: border-box; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | header { 25 | display: flex; 26 | justify-content: space-between; 27 | align-items: center; 28 | padding: 15px 5px; 29 | } 30 | body { 31 | text-align: center; 32 | background-color: var(--main-background); 33 | color: var(--text-color); 34 | overflow-x: hidden; 35 | } 36 | /* dropdown list setup */ 37 | option { 38 | background-color: #ff562285; 39 | } 40 | 41 | /* Scrollbar setup */ 42 | /* width */ 43 | ::-webkit-scrollbar { 44 | width: 8px; 45 | } 46 | 47 | /* Track */ 48 | ::-webkit-scrollbar-track { 49 | background: var(--secondary-background); 50 | border-radius: 10px; 51 | } 52 | 53 | /* Handle */ 54 | ::-webkit-scrollbar-thumb { 55 | background: var(--accent-color); 56 | border-radius: 10px; 57 | } 58 | /* scrollbar intersection */ 59 | ::-webkit-scrollbar-corner { 60 | background-image: url("../images/corner-image.png"); 61 | } 62 | .anchor-button { 63 | text-decoration: none; 64 | color: red; 65 | } 66 | /*------------------------Navbar setup-------------------------*/ 67 | 68 | .nav_bar { 69 | background-color: var(--secondary-background); 70 | } 71 | .logo { 72 | justify-self: stretch; 73 | cursor: pointer; 74 | height: 100%; 75 | width: 100%; 76 | object-fit: contain; 77 | } 78 | 79 | img.logo { 80 | max-width: 150px; 81 | max-height: 50px; 82 | } 83 | 84 | .nav_container { 85 | list-style: none; 86 | } 87 | 88 | .nav_container li { 89 | display: inline-block; 90 | padding: 0px 10px; 91 | } 92 | 93 | .nav_container li a { 94 | color: var(--text-color); 95 | text-decoration: none; 96 | transition: all 0.3s ease 0s; 97 | } 98 | 99 | .nav_container li a:hover { 100 | color: var(--accent-color); 101 | } 102 | .login_signup { 103 | padding: 0px 50px; 104 | } 105 | 106 | /* .login-button, 107 | .signup-button, 108 | .ask-question-button, 109 | .search-button, 110 | .logout-button, 111 | */ 112 | .search-category-selector, 113 | .ask-question-category-selector, 114 | .anchor-button, 115 | .edit-question-category-field, 116 | .button { 117 | color: var(--main-background); 118 | font-weight: 900px; 119 | padding: 5px 15px; 120 | margin: 5px; 121 | background-color: var(--accent-color); 122 | border: none; 123 | border-radius: 50px; 124 | cursor: pointer; 125 | transition: all 0.3s ease 0s; 126 | } 127 | 128 | /* .login-button:hover, 129 | .signup-button:hover, 130 | .ask-question-button:hover, 131 | .search-button:hover, 132 | .logout-button:hover, 133 | 134 | */ 135 | 136 | .ask-question-category-selector:hover, 137 | .search-category-selector:hover, 138 | .edit-question-category-field:hover, 139 | .button:hover, 140 | .anchor-button:hover { 141 | background-color: #ff562285; 142 | } 143 | 144 | input:-webkit-autofill { 145 | background-color: transparent !important; 146 | -webkit-box-shadow: 0 0 0 50px #222831 inset; 147 | -webkit-text-fill-color: var(--text-color); 148 | } 149 | 150 | input { 151 | background-color: #222831; 152 | color: var(--text-color); 153 | border: 1px solid #ff562285; 154 | border-radius: 10px; 155 | } 156 | 157 | textarea { 158 | background-color: #222831; 159 | color: var(--text-color); 160 | border: 1px solid #ff562285; 161 | border-radius: 10px; 162 | resize: none; 163 | } 164 | 165 | input { 166 | padding: 5px; 167 | } 168 | 169 | .bump-form-label { 170 | position: relative; 171 | top: 15px; 172 | } 173 | 174 | .logged-in-nav-buttons { 175 | display: flex; 176 | } 177 | 178 | /*--------------------------login/signup prompt setup------------------------*/ 179 | 180 | .login-or-signup-prompt { 181 | margin-top:10px; 182 | margin-bottom: -5px; 183 | font-size: 14px; 184 | color: var(--text-color); 185 | } 186 | 187 | .login-or-signup-prompt a { 188 | color: var(--text-color); 189 | } 190 | /*--------------------------signup setup------------------------*/ 191 | 192 | .signup_user { 193 | max-width: 500px; 194 | width: 100%; 195 | background: var(--secondary-background); 196 | padding: 25px 30px; 197 | border-radius: 10px; 198 | margin: 100px auto; 199 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15); 200 | border: 1px solid var(--accent-color); 201 | } 202 | .signup_user .title { 203 | font-size: 25px; 204 | font-weight: 500; 205 | color: var(--text-color); 206 | } 207 | 208 | .signup_user form .form-group { 209 | display: flex; 210 | flex-wrap: wrap; 211 | justify-content: space-between; 212 | } 213 | 214 | .signup_user .details { 215 | margin-top: 20px; 216 | } 217 | 218 | .signup_errors { 219 | background-color: red; 220 | max-width: 350px; 221 | margin: auto; 222 | margin-top: 20px; 223 | } 224 | 225 | .signup_errors ul { 226 | list-style-type: none; 227 | } 228 | .signup_user .form-control { 229 | margin: 0px 0 0px 0; 230 | width: calc(100% / 2- 20px); 231 | height: 45px; 232 | } 233 | 234 | .signup-to-togin { 235 | margin-top: -60px; 236 | } 237 | 238 | .signup-to-togin a { 239 | color: var(--text-color); 240 | } 241 | 242 | .form-field-div { 243 | margin-bottom: 10px; 244 | } 245 | 246 | 247 | 248 | 249 | 250 | 251 | /*--------------------------answer form setup------------------------*/ 252 | 253 | .add-answer-wrapper { 254 | margin-top:-80px; 255 | } 256 | 257 | .add-answer { 258 | max-width: 1200px; 259 | width: 100%; 260 | background: var(--secondary-background); 261 | padding: 25px 30px; 262 | border-radius: 2rem; 263 | margin: 100px auto; 264 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15); 265 | border: 1px solid var(--accent-color); 266 | } 267 | .add-answer .title { 268 | font-size: 25px; 269 | font-weight: 500; 270 | color: var(--text-color); 271 | } 272 | 273 | .add-answer form .form-group { 274 | display: flex; 275 | flex-wrap: wrap; 276 | justify-content: space-between; 277 | } 278 | 279 | .add-answer .details { 280 | margin-top: 20px; 281 | } 282 | 283 | .answer-text-box { 284 | height: 150px; 285 | width: 1150px; 286 | padding: 10px; 287 | } 288 | 289 | .add-answer .form-control { 290 | margin: 0px 0 0px 0; 291 | width: calc(100% / 2- 20px); 292 | height: 45px; 293 | } 294 | 295 | 296 | /*--------------------------login setup------------------------*/ 297 | .login_user { 298 | max-width: 500px; 299 | width: 100%; 300 | background: var(--secondary-background); 301 | padding: 25px 30px; 302 | border-radius: 10px; 303 | margin: 100px auto; 304 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15); 305 | border: 1px solid var(--accent-color); 306 | } 307 | .login_user .title { 308 | font-size: 25px; 309 | font-weight: 500; 310 | color: var(--text-color); 311 | } 312 | 313 | .login_user form .form-group { 314 | display: flex; 315 | flex-wrap: wrap; 316 | justify-content: space-between; 317 | } 318 | 319 | .login_user .details { 320 | margin-top: 20px; 321 | } 322 | 323 | .login_errors { 324 | background-color: red; 325 | max-width: 350px; 326 | margin: auto; 327 | margin-top: 20px; 328 | } 329 | 330 | .login_errors ul { 331 | list-style-type: none; 332 | } 333 | 334 | .login_user .form-control { 335 | margin: 0px 0 0px 0; 336 | width: calc(100% / 2- 20px); 337 | height: 45px; 338 | } 339 | 340 | .login-to-signup { 341 | margin-top: -60px; 342 | } 343 | 344 | .login-to-signup a { 345 | color: var(--text-color); 346 | } 347 | 348 | /*--------------------------askquestion page setup------------------------*/ 349 | .ask_question { 350 | max-width: 500px; 351 | width: 100%; 352 | background: var(--secondary-background); 353 | padding: 25px 30px; 354 | border-radius: 10px; 355 | margin: 100px auto; 356 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.15); 357 | border: 1px solid var(--accent-color); 358 | } 359 | .ask_question .title { 360 | font-size: 25px; 361 | font-weight: 500; 362 | color: var(--text-color); 363 | } 364 | 365 | .ask_question form .form-group { 366 | display: flex; 367 | flex-wrap: wrap; 368 | justify-content: space-between; 369 | } 370 | .ask-question-form-spacing { 371 | margin-top: 10px; 372 | margin-bottom: 10px; 373 | } 374 | 375 | .ask_question form .form-group input { 376 | width: 350px; 377 | padding: 10px; 378 | } 379 | 380 | .ask_question form .form-group textarea { 381 | height: 400px; 382 | width: 350px; 383 | padding: 10px; 384 | } 385 | 386 | .ask_question .details { 387 | margin-top: 20px; 388 | } 389 | 390 | .ask_question_errors { 391 | background-color: red; 392 | max-width: 350px; 393 | margin: auto; 394 | margin-top: 20px; 395 | } 396 | 397 | .ask_question_errors ul { 398 | list-style-type: none; 399 | } 400 | 401 | .ask_question .form-control { 402 | margin: 0px 0 0px 0; 403 | width: calc(100% / 2- 20px); 404 | height: 45px; 405 | } 406 | 407 | /*--------------------------landing page animation setup------------------------*/ 408 | 409 | .landing-page-title { 410 | font-size: 30px; 411 | padding: 10px 30px; 412 | margin: 20px; 413 | border: 3px solid #ff562285; 414 | border-radius: 1rem; 415 | display: inline-block; 416 | /* animation: animate 5s; */ 417 | /* white-space: nowrap; */ 418 | /* overflow: hidden; 419 | position: relative; */ 420 | } 421 | 422 | .content-slider { 423 | width: 100%; 424 | height: 90px; 425 | } 426 | 427 | .slider { 428 | height: 90px; 429 | width: 680px; 430 | margin: 40px auto 0; 431 | overflow: visible; 432 | position: relative; 433 | } 434 | 435 | .mask { 436 | overflow: hidden; 437 | height: 90px; 438 | } 439 | 440 | .slider ul { 441 | margin: 0; 442 | padding: 0; 443 | position: relative; 444 | } 445 | 446 | .slider li { 447 | width: 680px; 448 | height: 60px; 449 | position: absolute; 450 | top: -25px; 451 | list-style: none; 452 | } 453 | 454 | .slider .quote { 455 | font-size: 40px; 456 | font-style: italic; 457 | } 458 | 459 | .slider li.anim1 { 460 | animation: cycle 11s linear infinite; 461 | } 462 | 463 | .slider li.anim2 { 464 | animation: cycle2 11s linear infinite; 465 | } 466 | 467 | .slider li.anim3 { 468 | animation: cycle3 11s linear infinite; 469 | } 470 | 471 | .slider li.anim4 { 472 | animation: cycle4 11s linear infinite; 473 | } 474 | 475 | .slider li.anim5 { 476 | animation: cycle5 11s linear infinite; 477 | } 478 | 479 | .slider:hover li { 480 | animation-play-state: paused; 481 | } 482 | 483 | @keyframes cycle { 484 | 0% { 485 | top: 0px; 486 | } 487 | 4% { 488 | top: 0px; 489 | } 490 | 16% { 491 | top: 0px; 492 | opacity: 1; 493 | z-index: 0; 494 | } 495 | 20% { 496 | top: 35px; 497 | opacity: 0; 498 | z-index: 0; 499 | } 500 | 21% { 501 | top: -10px; 502 | opacity: 0; 503 | z-index: -1; 504 | } 505 | 50% { 506 | top: -10px; 507 | opacity: 0; 508 | z-index: -1; 509 | } 510 | 92% { 511 | top: -10px; 512 | opacity: 0; 513 | z-index: 0; 514 | } 515 | 96% { 516 | top: -10px; 517 | opacity: 0; 518 | } 519 | 100% { 520 | top: 0px; 521 | opacity: 1; 522 | } 523 | } 524 | 525 | @keyframes cycle2 { 526 | 0% { 527 | top: -25px; 528 | opacity: 0; 529 | } 530 | 16% { 531 | top: -25px; 532 | opacity: 0; 533 | } 534 | 20% { 535 | top: 0px; 536 | opacity: 1; 537 | } 538 | 24% { 539 | top: 0px; 540 | opacity: 1; 541 | } 542 | 36% { 543 | top: 0px; 544 | opacity: 1; 545 | z-index: 0; 546 | } 547 | 40% { 548 | top: 25px; 549 | opacity: 0; 550 | z-index: 0; 551 | } 552 | 41% { 553 | top: -25px; 554 | opacity: 0; 555 | z-index: -1; 556 | } 557 | 100% { 558 | top: -25px; 559 | opacity: 0; 560 | z-index: -1; 561 | } 562 | } 563 | 564 | @keyframes cycle3 { 565 | 0% { 566 | top: -25px; 567 | opacity: 0; 568 | } 569 | 36% { 570 | top: -25px; 571 | opacity: 0; 572 | } 573 | 40% { 574 | top: 0px; 575 | opacity: 1; 576 | } 577 | 44% { 578 | top: 0px; 579 | opacity: 1; 580 | } 581 | 56% { 582 | top: 0px; 583 | opacity: 1; 584 | z-index: 0; 585 | } 586 | 60% { 587 | top: 25px; 588 | opacity: 0; 589 | z-index: 0; 590 | } 591 | 61% { 592 | top: -25px; 593 | opacity: 0; 594 | z-index: -1; 595 | } 596 | 100% { 597 | top: -25px; 598 | opacity: 0; 599 | z-index: -1; 600 | } 601 | } 602 | 603 | @keyframes cycle4 { 604 | 0% { 605 | top: -25px; 606 | opacity: 0; 607 | } 608 | 56% { 609 | top: -25px; 610 | opacity: 0; 611 | } 612 | 60% { 613 | top: 0px; 614 | opacity: 1; 615 | } 616 | 64% { 617 | top: 0px; 618 | opacity: 1; 619 | } 620 | 76% { 621 | top: 0px; 622 | opacity: 1; 623 | z-index: 0; 624 | } 625 | 80% { 626 | top: 25px; 627 | opacity: 0; 628 | z-index: 0; 629 | } 630 | 81% { 631 | top: -25px; 632 | opacity: 0; 633 | z-index: -1; 634 | } 635 | 100% { 636 | top: -25px; 637 | opacity: 0; 638 | z-index: -1; 639 | } 640 | } 641 | 642 | @keyframes cycle5 { 643 | 0% { 644 | top: -25px; 645 | opacity: 0; 646 | } 647 | 76% { 648 | top: -25px; 649 | opacity: 0; 650 | } 651 | 80% { 652 | top: 0px; 653 | opacity: 1; 654 | } 655 | 84% { 656 | top: 0px; 657 | opacity: 1; 658 | } 659 | 96% { 660 | top: 0px; 661 | opacity: 1; 662 | z-index: 0; 663 | } 664 | 100% { 665 | top: 25px; 666 | opacity: 0; 667 | z-index: 0; 668 | } 669 | } 670 | 671 | 672 | 673 | 674 | @keyframes animate { 675 | 0% { 676 | width: 0ch; 677 | } 678 | 679 | 100% { 680 | width: 48ch; 681 | } 682 | } 683 | 684 | 685 | 686 | 687 | 688 | .landing-page-top-10 { 689 | margin-bottom: 20px; 690 | } 691 | 692 | .landing-page-top-10 p { 693 | font-size: 25px; 694 | color: var(--accent-color); 695 | } 696 | 697 | .landing-question-1 { 698 | grid-area: landing-question-1; 699 | } 700 | 701 | .landing-question-2 { 702 | grid-area: landing-question-2; 703 | } 704 | 705 | .landing-question-3 { 706 | grid-area: landing-question-3; 707 | } 708 | 709 | .landing-question-4 { 710 | grid-area: landing-question-4; 711 | } 712 | 713 | .landing-question-5 { 714 | grid-area: landing-question-5; 715 | } 716 | 717 | .landing-question-6 { 718 | grid-area: landing-question-6; 719 | } 720 | 721 | .landing-question-7 { 722 | grid-area: landing-question-7; 723 | } 724 | 725 | .landing-question-8 { 726 | grid-area: landing-question-8; 727 | } 728 | 729 | .landing-question-9 { 730 | grid-area: landing-question-9; 731 | } 732 | 733 | .landing-question-10 { 734 | grid-area: landing-question-10; 735 | } 736 | 737 | .landing-question-box-container { 738 | justify-content: center; 739 | display: grid; 740 | grid-template-columns: auto; 741 | grid-template-rows: auto; 742 | grid-template-areas: 743 | "landing-question-1 landing-question-2" 744 | "landing-question-3 landing-question-4" 745 | "landing-question-5 landing-question-6 " 746 | "landing-question-7 landing-question-8 " 747 | "landing-question-9 landing-question-10 "; 748 | gap: 40px 120px; 749 | } 750 | 751 | .landing-question-box { 752 | color: var(--text-color); 753 | background-color: var(--secondary-background); 754 | border: 1px solid var(--accent-color); 755 | border-radius: 2rem; 756 | padding: 30px; 757 | width: 400px; 758 | height: 400px; 759 | text-align: left; 760 | } 761 | 762 | .landing-question-title-text-wrapper { 763 | height: 290px; 764 | } 765 | 766 | .landing-question-box-title { 767 | font-size: 18px; 768 | font-weight: 700; 769 | color: var(--accent-color); 770 | text-decoration: none; 771 | padding-top: 3px; 772 | padding-bottom: 3px; 773 | margin-bottom: 5px; 774 | border-radius: 1rem; 775 | max-height: 80px; 776 | overflow-y: hidden; 777 | } 778 | 779 | .landing-question-box-top-divider { 780 | border: 1px solid #ff562285; 781 | border-radius: 5px; 782 | margin-bottom: 10px; 783 | } 784 | 785 | .landing-question-box-text { 786 | height: 180px; 787 | overflow-y: hidden; 788 | } 789 | 790 | .landing-question-box-category { 791 | position: relative; 792 | bottom: -10px; 793 | left: 10px; 794 | background-color: var(--main-background); 795 | padding: 7px 10px; 796 | display: inline-block; 797 | border-radius: 2rem; 798 | } 799 | 800 | .landing-question-box-userName { 801 | position: relative; 802 | left: 220px; 803 | bottom: 40px; 804 | overflow: hidden; 805 | } 806 | 807 | .landing-question-box-score { 808 | position: relative; 809 | left: 220px; 810 | bottom: -5px; 811 | } 812 | 813 | /*--------------------------question page setup------------------------*/ 814 | .question-page-question-box-container { 815 | display: grid; 816 | justify-content: center; 817 | margin-top: 20px; 818 | } 819 | 820 | .question-page-question-box { 821 | color: var(--text-color); 822 | background-color: var(--secondary-background); 823 | border: 1px solid var(--accent-color); 824 | border-radius: 2rem; 825 | padding: 30px; 826 | width: 1200px; 827 | margin-bottom: 20px; 828 | text-align: left; 829 | } 830 | .answer-div { 831 | background-color: var(--secondary-background); 832 | padding: 20px; 833 | margin: 5px auto; 834 | align-items: flex; 835 | border: 1px solid var(--accent-color); 836 | border-radius: 2rem; 837 | width: 700px; 838 | justify-content: space-around; 839 | align-items: center; 840 | } 841 | 842 | /* .question-page-question-title-text-wrapper { 843 | 844 | } */ 845 | 846 | .question-page-question-box-title { 847 | font-size: 18px; 848 | font-weight: 700; 849 | color: var(--accent-color); 850 | text-decoration: none; 851 | padding-top: 3px; 852 | padding-bottom: 3px; 853 | margin-bottom: 5px; 854 | border-radius: 1rem; 855 | overflow-y: hidden; 856 | } 857 | 858 | .question-page-question-box-top-divider { 859 | border: 1px solid #ff562285; 860 | border-radius: 5px; 861 | margin-bottom: 10px; 862 | } 863 | 864 | .question-page-question-box-bottom-divider { 865 | border: 1px solid #ff562285; 866 | border-radius: 5px; 867 | margin-bottom: -40px; 868 | } 869 | 870 | .question-page-answer-box-divider { 871 | border: 1px solid #ff562285; 872 | border-radius: 5px; 873 | margin-top: 10px; 874 | margin-bottom: 0px; 875 | } 876 | 877 | .question-page-question-box-text { 878 | margin-bottom: 8px; 879 | overflow-wrap: break-word; 880 | } 881 | 882 | .answer-question-box-title { 883 | font-size: large; 884 | color: var(--text-color); 885 | } 886 | .answerQuestionForm { 887 | justify-content: space-between; 888 | display: none; 889 | background-color: var(--secondary-background); 890 | border: 1px solid var(--accent-color); 891 | border-radius: 2rem; 892 | padding: 30px; 893 | width: 600px; 894 | height: 425px; 895 | z-index: 10; 896 | top: 50%; 897 | position: absolute; 898 | left: 650px; 899 | margin-top: -212px; 900 | } 901 | .answer-text-box { 902 | margin: 0px 0 0px 0; 903 | width: 100%; 904 | height: 300px; 905 | } 906 | .question-page-question-box-category { 907 | position: relative; 908 | bottom: -70px; 909 | left: 10px; 910 | background-color: var(--main-background); 911 | padding: 7px 10px; 912 | display: inline-block; 913 | border-radius: 2rem; 914 | } 915 | 916 | .question-page-question-box-score { 917 | position: relative; 918 | left: 1000px; 919 | bottom: -30px; 920 | } 921 | 922 | .question-page-question-box-userName { 923 | position: relative; 924 | left: 1000px; 925 | bottom: 15px; 926 | overflow: hidden; 927 | } 928 | 929 | /* .question-page-question-voting-icons { 930 | display:flex; 931 | flex-direction: row; 932 | position: relative; 933 | left: 1100px; 934 | bottom: -48px; 935 | } */ 936 | 937 | .question-page-upvote-question-icon { 938 | position: relative; 939 | left: 1100px; 940 | bottom: -48px; 941 | margin-right: 6px; 942 | z-index: 1; 943 | } 944 | 945 | .question-page-downvote-question-icon { 946 | position: relative; 947 | left: 1100px; 948 | bottom: -48px; 949 | margin-right: 6px; 950 | z-index: 1; 951 | } 952 | 953 | .question-page-delete-and-edit-question { 954 | position: relative; 955 | left: 950px; 956 | bottom: -63px; 957 | } 958 | 959 | .question-page-delete-question-icon { 960 | margin-right: 5px; 961 | } 962 | 963 | .fa-caret-square-up, 964 | .fa-caret-square-down { 965 | font-size: 16px; 966 | } 967 | 968 | .unclickable-vote-button { 969 | pointer-events: none; 970 | } 971 | 972 | .upvoted-arrow { 973 | color: var(--accent-color); 974 | } 975 | 976 | .downvoted-arrow { 977 | color: #3a99be; 978 | } 979 | 980 | .question-page-delete-and-edit-question { 981 | position: relative; 982 | left: 950px; 983 | bottom: -63px; 984 | } 985 | 986 | .question-page-delete-question-icon { 987 | margin-right: 8px; 988 | } 989 | 990 | .fa-caret-square-up, 991 | .fa-caret-square-down { 992 | font-size: 16px; 993 | } 994 | 995 | .editQuestionForm { 996 | display: none; 997 | position: fixed; 998 | z-index: 3; 999 | left: 700px; 1000 | top: 0px; 1001 | } 1002 | 1003 | .edit-question-form-button-wrapper { 1004 | position: relative; 1005 | left: 130px; 1006 | } 1007 | 1008 | .wrapper-for-all-answers { 1009 | top: 100px; 1010 | } 1011 | 1012 | .open-answer-question { 1013 | margin-top: 10px; 1014 | /* margin-bottom: 10px; */ 1015 | } 1016 | 1017 | .answer-upvote-arrow { 1018 | font-size: 16px; 1019 | position: relative; 1020 | left: 800px; 1021 | top: 10px; 1022 | } 1023 | 1024 | .answer-downvote-arrow { 1025 | font-size: 16px; 1026 | position: relative; 1027 | left: 808px; 1028 | top: 10px; 1029 | } 1030 | 1031 | .answer-score-text-box { 1032 | font-size: 16px; 1033 | position: relative; 1034 | left: 700px; 1035 | top: 25px; 1036 | } 1037 | 1038 | .delete-answer-btn { 1039 | position: relative; 1040 | left: 650px; 1041 | top: 42px; 1042 | } 1043 | 1044 | .edit-answer-btn { 1045 | 1046 | position: relative; 1047 | left: 655px; 1048 | top: 42px; 1049 | } 1050 | 1051 | .question-page-answer-box-username { 1052 | position: relative; 1053 | left: 700px; 1054 | top: 25px; 1055 | margin-bottom: 10px; 1056 | } 1057 | 1058 | .question-page-answer-box-username-answerer { 1059 | position: relative; 1060 | left: 700px; 1061 | top: 30px; 1062 | } 1063 | 1064 | .new-answer-padding { 1065 | padding-bottom:200px; 1066 | } 1067 | 1068 | .open-answer-question { 1069 | margin-top: 0px; 1070 | margin-bottom: 20px; 1071 | } 1072 | /*--------------------------answer box setup------------------------*/ 1073 | 1074 | .wrapper-for-all-answers { 1075 | margin: auto; 1076 | width:900px; 1077 | margin-top: 10px; 1078 | } 1079 | .question-page-answer-box-outer-wrapper { 1080 | color: var(--text-color); 1081 | background-color: var(--secondary-background); 1082 | border: 1px solid var(--accent-color); 1083 | border-radius: 2rem; 1084 | padding: 30px; 1085 | width: 900px; 1086 | text-align: left; 1087 | margin-bottom: 20px; 1088 | 1089 | left: 500px; 1090 | } 1091 | 1092 | 1093 | /*--------------------------search results page setup------------------------*/ 1094 | .no-question-found { 1095 | position: relative; 1096 | left: 50px; 1097 | align-self: center; 1098 | width: 400px; 1099 | color: var(--text-color); 1100 | display: flex; 1101 | flex-wrap: wrap; 1102 | justify-content: space-around; 1103 | 1104 | background-color: var(--secondary-background); 1105 | border: 1px solid var(--accent-color); 1106 | border-radius: 2rem; 1107 | padding: 30px; 1108 | height: 150px; 1109 | text-align: left; 1110 | } 1111 | 1112 | .single-search-result { 1113 | position: relative; 1114 | left: 55px; 1115 | } 1116 | 1117 | .no-question-found a { 1118 | color: #ff5722; 1119 | } 1120 | .search-page-title { 1121 | font-size: 30px; 1122 | padding: 10px 30px; 1123 | margin: 20px; 1124 | border: 3px solid #ff562285; 1125 | border-radius: 1rem; 1126 | display: inline-block; 1127 | } 1128 | 1129 | .search-page-top-10 { 1130 | margin-bottom: 20px; 1131 | } 1132 | 1133 | .search-page-top-10 p { 1134 | font-size: 25px; 1135 | color: var(--accent-color); 1136 | } 1137 | 1138 | .search-question-1 { 1139 | grid-area: search-question-1; 1140 | } 1141 | 1142 | .search-question-2 { 1143 | grid-area: search-question-2; 1144 | } 1145 | 1146 | .search-question-3 { 1147 | grid-area: search-question-3; 1148 | } 1149 | 1150 | .search-question-4 { 1151 | grid-area: search-question-4; 1152 | } 1153 | 1154 | .search-question-5 { 1155 | grid-area: search-question-5; 1156 | } 1157 | 1158 | .search-question-6 { 1159 | grid-area: search-question-6; 1160 | } 1161 | 1162 | .search-question-7 { 1163 | grid-area: search-question-7; 1164 | } 1165 | 1166 | .search-question-8 { 1167 | grid-area: search-question-8; 1168 | } 1169 | 1170 | .search-question-9 { 1171 | grid-area: search-question-9; 1172 | } 1173 | 1174 | .search-question-10 { 1175 | grid-area: search-question-10; 1176 | } 1177 | 1178 | .search-question-box-container { 1179 | justify-content: center; 1180 | display: grid; 1181 | grid-template-columns: auto; 1182 | grid-template-rows: auto; 1183 | grid-template-areas: 1184 | "search-question-1 search-question-2" 1185 | "search-question-3 search-question-4" 1186 | "search-question-5 search-question-6 " 1187 | "search-question-7 search-question-8 " 1188 | "search-question-9 search-question-10 "; 1189 | gap: 40px 120px; 1190 | } 1191 | 1192 | .search-question-box { 1193 | color: var(--text-color); 1194 | background-color: var(--secondary-background); 1195 | border: 1px solid var(--accent-color); 1196 | border-radius: 2rem; 1197 | padding: 30px; 1198 | width: 400px; 1199 | height: 400px; 1200 | text-align: left; 1201 | } 1202 | 1203 | .search-question-title-text-wrapper { 1204 | height: 290px; 1205 | } 1206 | 1207 | .search-question-box-title { 1208 | font-size: 18px; 1209 | font-weight: 700; 1210 | color: var(--accent-color); 1211 | text-decoration: none; 1212 | padding: 3px; 1213 | margin-bottom: 5px; 1214 | border-radius: 1rem; 1215 | max-height: 80px; 1216 | overflow-y: hidden; 1217 | } 1218 | 1219 | .search-question-box-top-divider { 1220 | border: 1px solid #ff562285; 1221 | border-radius: 5px; 1222 | margin-bottom: 10px; 1223 | } 1224 | 1225 | .search-question-box-text { 1226 | height: 180px; 1227 | overflow-y: hidden; 1228 | } 1229 | 1230 | .search-question-box-category { 1231 | position: relative; 1232 | bottom: -10px; 1233 | left: 10px; 1234 | background-color: var(--main-background); 1235 | padding: 7px 10px; 1236 | display: inline-block; 1237 | border-radius: 2rem; 1238 | } 1239 | 1240 | .search-question-box-userName { 1241 | position: relative; 1242 | left: 220px; 1243 | bottom: 40px; 1244 | overflow: hidden; 1245 | } 1246 | 1247 | .search-question-box-score { 1248 | position: relative; 1249 | left: 220px; 1250 | bottom: -5px; 1251 | } 1252 | 1253 | /*--------------------------footer setup------------------------*/ 1254 | 1255 | .contact-footer { 1256 | margin-top: 50px; 1257 | margin-bottom: 20px; 1258 | font-size: 16px; 1259 | border: 1px dashed #ff562285; 1260 | border-radius: 1rem; 1261 | 1262 | padding-top: 10px; 1263 | padding-bottom: 10px; 1264 | display: inline-block; 1265 | } 1266 | 1267 | .contact-container { 1268 | display: flex; 1269 | flex-direction: row; 1270 | justify-content: center; 1271 | } 1272 | 1273 | .contact { 1274 | margin-top: 20px; 1275 | margin-left: 40px; 1276 | margin-right: 40px; 1277 | background-image: radial-gradient(80px 20px at center, #2d4059, #222831); 1278 | } 1279 | 1280 | .contact-link { 1281 | margin-top: 5px; 1282 | } 1283 | 1284 | .contact-link a { 1285 | color: var(--accent-color); 1286 | text-decoration: none; 1287 | } 1288 | -------------------------------------------------------------------------------- /public/stylesheets/reset.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | 10 | * { 11 | box-sizing: border-box; 12 | } 13 | 14 | h1, h2, h3, h4, h5, h6, a, ol, li, ul, div, span, 15 | body, header, nav { 16 | font-size: 1rem; 17 | color: inherit; 18 | background-color: transparent; 19 | margin: 0; 20 | padding: 0; 21 | outline: 0; 22 | font-weight: normal; 23 | } 24 | 25 | h1 { 26 | font-size: 2rem; 27 | } -------------------------------------------------------------------------------- /routes/answerquestion.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var router = express.Router(); 3 | const { asyncHandler } = require("./utils"); 4 | const db = require("../db/models"); 5 | const { requireAuth } = require("../auth"); 6 | 7 | router.post( 8 | "/:id(\\d+)", 9 | asyncHandler(async (req, res) => { 10 | const text = req.body.textToSend; 11 | 12 | const questionId = parseInt(req.params.id, 10); 13 | const { userId } = req.session.auth; 14 | 15 | const score = 0; 16 | const answer = await db.Answer.create({ text, score, questionId, userId }); 17 | res.json({ answerId: answer.id }); 18 | }) 19 | ); 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /routes/answers.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var router = express.Router(); 3 | const { asyncHandler } = require("./utils"); 4 | const db = require("../db/models"); 5 | const { requireAuth } = require("../auth"); 6 | const sequelize = require('sequelize'); 7 | 8 | // post answer vote 9 | router.post( 10 | "/:id(\\d+)/vote/:votetype(\\d+)", 11 | requireAuth, 12 | asyncHandler(async (req, res) => { 13 | const answerId = parseInt(req.params.id, 10); 14 | 15 | let voteSum = parseInt(req.params.votetype, 10); 16 | 17 | if (voteSum === 2) voteSum = -1; 18 | 19 | const { userId } = req.session.auth; 20 | 21 | const vote = await db.AnswerVote.create({ userId, answerId, voteSum }); 22 | const answer = await db.Answer.findByPk(answerId); 23 | const score = await db.AnswerVote.findAll({ 24 | attributes: [[sequelize.fn("sum", sequelize.col("voteSum")), "total"]], 25 | where: { answerId: answer.id }, 26 | }); 27 | if (score[0].dataValues.total !== null) { 28 | answer.score = score[0].dataValues.total; 29 | } else { 30 | answer.score = 0; 31 | } 32 | await answer.save(); 33 | res.send(); 34 | 35 | }) 36 | ); 37 | 38 | // delete answer vote - may be wrong 39 | router.delete( 40 | "/:id(\\d+)/vote", 41 | requireAuth, 42 | asyncHandler(async (req, res) => { 43 | const answerId = parseInt(req.params.id, 10); 44 | const { userId } = req.session.auth; 45 | const vote = await db.AnswerVote.findOne({ 46 | where: { 47 | answerId, 48 | userId, 49 | }, 50 | }); 51 | 52 | vote.destroy(); 53 | 54 | const answer = await db.Answer.findByPk(answerId); 55 | const score = await db.AnswerVote.findAll({ 56 | attributes: [[sequelize.fn("sum", sequelize.col("voteSum")), "total"]], 57 | where: { answerId: answer.id }, 58 | }); 59 | if (score[0].dataValues.total !== null) { 60 | answer.score = score[0].dataValues.total; 61 | } else { 62 | answer.score = 0; 63 | } 64 | await answer.save(); 65 | res.send(); 66 | }) 67 | ); 68 | 69 | 70 | // delete answer 71 | router.delete( 72 | "/:id", 73 | requireAuth, 74 | asyncHandler(async (req, res) => { 75 | const answer = await db.Answer.findByPk(parseInt(req.params.id, 10)); 76 | // const { userId } = req.session.auth; 77 | answer.destroy(); 78 | res.send(); 79 | }) 80 | ); 81 | 82 | module.exports = router; 83 | -------------------------------------------------------------------------------- /routes/askquestions.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | const { csrfProtection, asyncHandler, } = require("./utils"); 4 | const { Question, Category } = require('../db/models'); 5 | const { check, validationResult } = require('express-validator'); 6 | const { requireAuth } = require('../auth') 7 | 8 | 9 | 10 | // GET /askQuestions: 11 | router.get('/', requireAuth, csrfProtection, async (req, res, next) => { 12 | 13 | const question = Question.build({}); 14 | const categoryList = await Category.findAll(); 15 | res.render('askQuestions', { 16 | title: "Ask a Question", 17 | csrfToken: req.csrfToken(), 18 | categoryList 19 | }) 20 | }); 21 | 22 | router.get('/:id(\\d+)', requireAuth, csrfProtection, async (req, res, next) => { 23 | const id = parseInt(req.params.id, 10); 24 | 25 | const question = await Question.findByPk(id); 26 | const { userId } = req.session.auth; 27 | if (question.userId !== userId) { 28 | res.redirect(`/questions/${id}`) 29 | } 30 | const categoryList = await Category.findAll(); 31 | 32 | res.render('editQuestions', { 33 | title: "Edit a Question", 34 | csrfToken: req.csrfToken(), 35 | categoryList, 36 | question 37 | }) 38 | }); 39 | 40 | 41 | const questionValidators = [ 42 | check('title') 43 | .exists({ checkFalsy: true }) 44 | .withMessage('Please provide a title for your question.'), 45 | check('text') 46 | .exists({ checkFalsy: true }) 47 | .withMessage('Please provide a question.') 48 | ]; 49 | 50 | router.post('/:id(\\d+)', requireAuth, csrfProtection, questionValidators, async (req, res, next) => { 51 | const id = parseInt(req.params.id, 10); 52 | const categoryList = await Category.findAll(); 53 | const { userId } = req.session.auth; 54 | const { title, text , chosenCategory } = req.body; 55 | const questionErrors = validationResult(req); 56 | const question = await Question.findByPk(id); 57 | 58 | 59 | let errors = []; 60 | 61 | if (questionErrors.isEmpty()) { 62 | question.title = title; 63 | question.text = text; 64 | question.categoryId = chosenCategory; 65 | await question.save(); 66 | //return res.redirect(`/questions/${question.id}`) 67 | res.json() 68 | 69 | } else { 70 | errors = questionErrors.array().map((error) => error.msg); 71 | } 72 | 73 | res.json(); 74 | 75 | // res.render('editQuestions', { 76 | // title: "Edit a Question", 77 | // csrfToken: req.csrfToken(), 78 | // categoryList, 79 | // question 80 | // }) 81 | }); 82 | 83 | 84 | 85 | // POST /askQuestions: 86 | router.post('/', requireAuth, csrfProtection, questionValidators, asyncHandler(async (req, res, next) => { 87 | 88 | const { userId } = req.session.auth; 89 | const { title, text , chosenCategory } = req.body; 90 | const questionErrors = validationResult(req); 91 | const categoryList = await Category.findAll(); 92 | const question = Question.build({ 93 | userId, 94 | title: title, 95 | text: text, 96 | categoryId: chosenCategory, 97 | score: 0 98 | }) 99 | 100 | let errors = []; 101 | 102 | if (questionErrors.isEmpty()) { 103 | 104 | await question.save(); 105 | return res.redirect(`/questions/${question.id}`) 106 | 107 | } else { 108 | errors = questionErrors.array().map((error) => error.msg); 109 | } 110 | 111 | res.render('askQuestions', { 112 | errors, 113 | csrfToken: req.csrfToken(), 114 | title: 'Ask a Question', 115 | categoryList 116 | }); 117 | })) 118 | 119 | 120 | module.exports = router; 121 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | const { asyncHandler } = require("./utils"); 4 | const { Profile, Question, Answer, QuestionVote, Category } = require('../db/models') 5 | const sequelize = require('sequelize'); 6 | 7 | /* GET home page. */ 8 | router.get('/', asyncHandler(async (req, res, next) => { 9 | res.set('Cache-Control', 'no-store') 10 | 11 | const orderedQuestions = await Question.findAll({ 12 | include: [Profile, Answer, QuestionVote, Category], 13 | order: [['score', 'DESC']], 14 | limit: 10 15 | }) 16 | 17 | 18 | const categoryList = await Category.findAll(); 19 | 20 | req.session.save(() => 21 | res.render('index', { 22 | title: 'Welcome to Dark Overflow, get answers to your javascript problems.', 23 | questions: orderedQuestions, 24 | categoryList 25 | })); 26 | 27 | })); 28 | 29 | module.exports = router; 30 | -------------------------------------------------------------------------------- /routes/questions.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var router = express.Router(); 3 | const { 4 | Question, 5 | Answer, 6 | QuestionVote, 7 | AnswerVote, 8 | Category, 9 | Profile, 10 | } = require("../db/models"); 11 | const { requireAuth } = require("../auth"); 12 | 13 | const { csrfProtection, asyncHandler, } = require("./utils"); 14 | const sequelize = require('sequelize'); 15 | const { check, validationResult } = require('express-validator'); 16 | 17 | 18 | const { log } = require("debug"); 19 | 20 | // GET /questions/:id 21 | router.get("/:id", csrfProtection, async (req, res, next) => { 22 | 23 | const id = req.params.id; 24 | // join question, answer, questionVote, answerVote 25 | const question = await Question.findByPk(parseInt(id, 10), { 26 | include: [ 27 | { 28 | model: Answer, 29 | include: [ 30 | { 31 | model: AnswerVote, 32 | }, 33 | { 34 | model: Profile, 35 | } 36 | ], 37 | }, 38 | { 39 | model: QuestionVote, 40 | }, 41 | { 42 | model: Category, 43 | }, 44 | { 45 | model: Profile, 46 | }, 47 | ], 48 | order: [[Answer, 'score', 'DESC']], 49 | }); 50 | 51 | 52 | if (!question) { 53 | console.log("making question error"); 54 | let error = { 55 | status: 404, 56 | typeOf: "question", 57 | message: "Not found", 58 | }; 59 | 60 | next(error); 61 | } 62 | //database handle questionVotes 63 | const questionScore = await QuestionVote.findAll({ 64 | attributes: [[sequelize.fn("sum", sequelize.col("voteSum")), "total"]], 65 | where: { questionId: id }, 66 | }); 67 | if (questionScore[0].dataValues.total !== null) { 68 | question.score = questionScore[0].dataValues.total; 69 | } else { 70 | question.score = 0; 71 | } 72 | await question.save(); 73 | 74 | // database handle answerVotes 75 | // let answersArrayForPug = []; 76 | // question.Answers.forEach(async (answerOnPage) => { 77 | // /* const answer = grab each answer on the page, 78 | // const answerScore = query for fresh aggregate voteSum 79 | // */ 80 | // const answer = await Answer.findByPk(parseInt(answerOnPage.id)) 81 | // const answerScore = await AnswerVote.findAll({ 82 | // attributes: [[sequelize.fn('sum', sequelize.col('voteSum')), 'total']], 83 | // where: {answerId: answerOnPage.id} 84 | // }) 85 | // console.log("answer#", answer.dataValues.id, "answersScore:", answerScore[0].dataValues.total); 86 | // // save fresh answerScore into the answer.score field 87 | // if (answerScore[0].dataValues.total !== null) { 88 | // answer.score = answerScore[0].dataValues.total; 89 | // } else answer.score = 0; 90 | // await answer.save(); 91 | // // console.log("*******ANSWER********", answer); 92 | // answersArrayForPug.push( {answer} ); 93 | // }); 94 | 95 | // const newAnswers = Answer.findAll({ 96 | // where: { 97 | // id: { 98 | // [Op.in]: 99 | // } 100 | // } 101 | // }) 102 | // question.toJSON().Answers.forEach(ansObj => console.log(`Here are your answer votes ${ansObj.AnswerVotes}`)) 103 | 104 | const answersArray = question.toJSON().Answers; 105 | const categoryList = await Category.findAll(); 106 | let isQuestionAsker = false; 107 | if (req.session.auth) { 108 | const { userId } = req.session.auth; 109 | if (userId === question.userId) isQuestionAsker = true; 110 | } 111 | 112 | 113 | res.render('question', { 114 | question, categoryList, isQuestionAsker, 115 | answers: answersArray, 116 | csrfToken: req.csrfToken() 117 | }) 118 | 119 | }) 120 | 121 | 122 | 123 | //DELETE /questions/:id 124 | router.delete( 125 | "/:id", 126 | requireAuth, 127 | asyncHandler(async (req, res) => { 128 | const question = await Question.findByPk(parseInt(req.params.id, 10), { 129 | include: [ 130 | { 131 | model: Answer, 132 | include: [ 133 | { 134 | model: AnswerVote, 135 | }, 136 | ], 137 | }, 138 | { 139 | model: QuestionVote, 140 | }, 141 | ], 142 | }); 143 | const { userId } = req.session.auth; 144 | question.destroy(); 145 | res.send(); 146 | }) 147 | ); 148 | 149 | // POST /questions/:id/vote/votetype 150 | router.post( 151 | "/:id(\\d+)/vote/:votetype(\\d+)", 152 | requireAuth, 153 | asyncHandler(async (req, res) => { 154 | const questionId = parseInt(req.params.id, 10); 155 | let voteSum = parseInt(req.params.votetype, 10); 156 | if (voteSum === 2) { 157 | voteSum = -1; 158 | } 159 | const { userId } = req.session.auth; 160 | const vote = await QuestionVote.create({ userId, questionId, voteSum }); 161 | 162 | const question = await Question.findByPk(questionId); 163 | const score = await QuestionVote.findAll({ 164 | attributes: [[sequelize.fn("sum", sequelize.col("voteSum")), "total"]], 165 | where: { questionId: question.id }, 166 | }); 167 | if (score[0].dataValues.total !== null) { 168 | question.score = score[0].dataValues.total; 169 | } else { 170 | question.score = 0; 171 | } 172 | await question.save(); 173 | 174 | res.end(); 175 | }) 176 | ); 177 | 178 | // DELETE /questions/:id/vote 179 | router.delete( 180 | "/:id(\\d+)/vote", 181 | requireAuth, 182 | asyncHandler(async (req, res) => { 183 | const questionId = parseInt(req.params.id, 10); 184 | const { userId } = req.session.auth; 185 | const vote = await QuestionVote.findOne({ 186 | where: { 187 | questionId, 188 | userId, 189 | }, 190 | }); 191 | vote.destroy(); 192 | const question = await Question.findByPk(questionId); 193 | const score = await QuestionVote.findAll({ 194 | attributes: [[sequelize.fn("sum", sequelize.col("voteSum")), "total"]], 195 | where: { questionId: question.id }, 196 | }); 197 | if (score[0].dataValues.total !== null) { 198 | question.score = score[0].dataValues.total; 199 | } else { 200 | question.score = 0; 201 | } 202 | await question.save(); 203 | 204 | res.send(); 205 | }) 206 | ); 207 | 208 | module.exports = router; 209 | -------------------------------------------------------------------------------- /routes/searchresults.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | const { csrfProtection, asyncHandler, } = require("./utils"); 4 | const db = require('../db/models'); 5 | const { Question, Profile, Answer, QuestionVote, Category } = require('../db/models') 6 | const { check, validationResult } = require('express-validator'); 7 | const { loginUser, logoutUser } = require('../auth') 8 | const sequelize = require('sequelize'); 9 | const op = sequelize.Op; 10 | 11 | router.post('/', asyncHandler(async (req, res) => { 12 | const { searchString } = req.body; 13 | 14 | const scoreQuestions = await Question.findAll() 15 | 16 | scoreQuestions.forEach(async question => { 17 | const score = await QuestionVote.findAll({ 18 | attributes: [[sequelize.fn('sum', sequelize.col('voteSum')), 'total']], 19 | where: { questionId: question.id } 20 | }) 21 | if (score[0].dataValues.total !== null) { 22 | question.score = score[0].dataValues.total; 23 | } else { 24 | question.score = 0; 25 | } 26 | await question.save(); 27 | }) 28 | const questions = await db.Question.findAll({ 29 | include: [Profile, Answer, QuestionVote, Category], 30 | order: [['score', 'DESC']], 31 | limit: 10, 32 | where: { 33 | [op.or]: [ 34 | { 35 | title: { 36 | [op.iLike]: '%' + searchString + '%' 37 | } 38 | }, 39 | { 40 | text: { 41 | [op.iLike]: '%' + searchString + '%' 42 | } 43 | } 44 | ] 45 | } 46 | }) 47 | 48 | const categoryList = await Category.findAll(); 49 | const title = 'Search Results' 50 | res.render('searchresults', { 51 | questions, 52 | title, 53 | categoryList 54 | }); 55 | })); 56 | 57 | router.get('/:id', asyncHandler(async (req, res) => { 58 | const categoryId = parseInt(req.params.id, 10); 59 | 60 | const scoreQuestions = await Question.findAll() 61 | 62 | scoreQuestions.forEach(async question => { 63 | const score = await QuestionVote.findAll({ 64 | attributes: [[sequelize.fn('sum', sequelize.col('voteSum')), 'total']], 65 | where: { questionId: question.id } 66 | }) 67 | if (score[0].dataValues.total !== null) { 68 | question.score = score[0].dataValues.total; 69 | } else { 70 | question.score = 0; 71 | } 72 | await question.save(); 73 | }) 74 | const questions = await db.Question.findAll({ 75 | include: [Profile, Answer, QuestionVote, Category], 76 | order: [['score', 'DESC']], 77 | limit: 10, 78 | where: { 79 | categoryId 80 | } 81 | }) 82 | 83 | const category = await db.Category.findByPk(categoryId); 84 | const title = `${category.dataValues.name} Questions`; 85 | const titleCaps = title.charAt(0).toUpperCase() + title.slice(1); 86 | const categoryList = await Category.findAll(); 87 | res.render('searchresults', { 88 | questions, 89 | title: titleCaps, 90 | categoryList 91 | }); 92 | })); 93 | 94 | module.exports = router; 95 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var router = express.Router(); 3 | const { csrfProtection, asyncHandler } = require("./utils"); 4 | const bcrypt = require("bcryptjs"); 5 | const db = require("../db/models"); 6 | const { check, validationResult } = require("express-validator"); 7 | const { loginUser, logoutUser } = require("../auth"); 8 | 9 | const userValidators = [ 10 | check("userName") 11 | .exists({ checkFalsy: true }) 12 | .withMessage("Please provide a value for Username") 13 | .isLength({ max: 50 }) 14 | .withMessage("Username must not be more than 50 characters long"), 15 | check("firstName") 16 | .exists({ checkFalsy: true }) 17 | .withMessage("Please provide a value for First Name") 18 | .isLength({ max: 50 }) 19 | .withMessage("First Name must not be more than 50 characters long"), 20 | check("lastName") 21 | .exists({ checkFalsy: true }) 22 | .withMessage("Please provide a value for Last Name") 23 | .isLength({ max: 50 }) 24 | .withMessage("Last Name must not be more than 50 characters long"), 25 | check("email") 26 | .exists({ checkFalsy: true }) 27 | .withMessage("Please provide a value for Email Address") 28 | .isLength({ max: 255 }) 29 | .withMessage("Email Address must not be more than 255 characters long") 30 | .isEmail() 31 | .withMessage("Email Address is not a valid email") 32 | .custom((value) => { 33 | return db.Profile.findOne({ where: { email: value } }).then((user) => { 34 | if (user) { 35 | return Promise.reject( 36 | "The provided Email Address is already in use by another account" 37 | ); 38 | } 39 | }); 40 | }), 41 | check("password") 42 | .exists({ checkFalsy: true }) 43 | .withMessage("Please provide a value for Password") 44 | .isLength({ max: 50 }) 45 | .withMessage("Password must not be more than 50 characters long"), 46 | // .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])/, 'g') 47 | // .withMessage('Password must contain at least 1 lowercase letter, uppercase letter, number, and special character (i.e. "!@#$%^&*")'), 48 | check("confirmPassword") 49 | .exists({ checkFalsy: true }) 50 | .withMessage("Please provide a value for Confirm Password") 51 | .isLength({ max: 50 }) 52 | .withMessage("Confirm Password must not be more than 50 characters long") 53 | .custom((value, { req }) => { 54 | if (value !== req.body.password) { 55 | return false; 56 | } 57 | return true; 58 | }) 59 | .withMessage("Passwords don't match."), 60 | ]; 61 | 62 | /* GET /users/signup */ 63 | router.get( 64 | "/signup", 65 | csrfProtection, 66 | asyncHandler(async (req, res, next) => { 67 | const user = db.Profile.build({}); 68 | const categoryList = await db.Category.findAll(); 69 | res.render("signup", { user, csrfToken: req.csrfToken(), categoryList }); 70 | }) 71 | ); 72 | // POST /users/signup 73 | router.post( 74 | "/signup", 75 | csrfProtection, 76 | userValidators, 77 | asyncHandler(async (req, res, next) => { 78 | const { 79 | userName, 80 | firstName, 81 | lastName, 82 | email, 83 | password, 84 | confirmPassword, 85 | } = req.body; 86 | const user = db.Profile.build({ userName, firstName, lastName, email }); 87 | const validatorErrors = validationResult(req); 88 | if (validatorErrors.isEmpty()) { 89 | const hashedPassword = await bcrypt.hash(password, 10); 90 | user.hashedPassword = hashedPassword; 91 | await user.save(); 92 | loginUser(req, res, user); 93 | res.redirect("/"); 94 | } else { 95 | const categoryList = await db.Category.findAll(); 96 | const errors = validatorErrors.array().map((error) => error.msg); 97 | res.render("signup", { 98 | title: "Register", 99 | errors, 100 | user, 101 | csrfToken: req.csrfToken(), 102 | categoryList, 103 | }); 104 | } 105 | }) 106 | ); 107 | 108 | const loginValidators = [ 109 | check("userName") 110 | .exists({ checkFalsy: true }) 111 | .withMessage("Please provide a value for Username"), 112 | check("password") 113 | .exists({ checkFalsy: true }) 114 | .withMessage("Please provide a value for Password"), 115 | ]; 116 | 117 | // GET /users/login 118 | router.get( 119 | "/login", 120 | csrfProtection, 121 | asyncHandler(async (req, res) => { 122 | const categoryList = await db.Category.findAll(); 123 | res.render("login", { 124 | csrfToken: req.csrfToken(), 125 | title: "Login", 126 | categoryList, 127 | }); 128 | }) 129 | ); 130 | 131 | // POST /users/login 132 | router.post( 133 | "/login", 134 | csrfProtection, 135 | loginValidators, 136 | asyncHandler(async (req, res) => { 137 | const { userName, password } = req.body; 138 | const loginErrors = validationResult(req); 139 | let errors = []; 140 | if (loginErrors.isEmpty()) { 141 | const user = await db.Profile.findOne({ where: { userName } }); 142 | 143 | if (user) { 144 | const authorized = await bcrypt.compare( 145 | password, 146 | user.hashedPassword.toString() 147 | ); 148 | if (authorized) { 149 | loginUser(req, res, user); 150 | return res.redirect("/"); 151 | } 152 | } 153 | errors.push("Login failed for that username and password"); 154 | } else { 155 | errors = loginErrors.array().map((error) => error.msg); 156 | } 157 | 158 | const categoryList = await db.Category.findAll(); 159 | res.render("login", { 160 | errors, 161 | csrfToken: req.csrfToken(), 162 | title: "Login", 163 | categoryList, 164 | }); 165 | }) 166 | ); 167 | 168 | //Demo User 169 | router.post( 170 | "/demo", 171 | csrfProtection, 172 | asyncHandler(async (req, res) => { 173 | const user = await db.Profile.findByPk(1); 174 | 175 | loginUser(req, res, user); 176 | return res.redirect("/"); 177 | }) 178 | ); 179 | 180 | router.post("/logout", (req, res) => { 181 | logoutUser(req, res); 182 | 183 | req.session.save(() => res.redirect("/")); 184 | }); 185 | 186 | module.exports = router; 187 | -------------------------------------------------------------------------------- /routes/utils.js: -------------------------------------------------------------------------------- 1 | const csrf = require('csurf'); 2 | 3 | const csrfProtection = csrf({ cookie: true }); 4 | 5 | const asyncHandler = (handler) => (req, res, next) => handler(req, res, next).catch(next); 6 | 7 | module.exports = { 8 | csrfProtection, 9 | asyncHandler, 10 | }; 11 | -------------------------------------------------------------------------------- /views/askQuestions.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | include utils.pug 3 | 4 | block content 5 | div(class = "ask_question_errors") 6 | +validationErrorSummary(errors) 7 | div(class = "ask_question") 8 | div(class = 'title') Ask a Question 9 | div(class = 'details') 10 | form(action="/askQuestions" method="post") 11 | input(type="hidden", name="_csrf", value=csrfToken) 12 | +field("Title", "title" ) 13 | div(class='ask-question-form-spacing') 14 | +field("Question", "text", "", "textarea") 15 | div(class='form-group') 16 | label(for='chosenCategory') 17 | select(name='chosenCategory', id="" class='ask-question-category-selector') 18 | each category in categoryList 19 | option(value=category.id)=category.name 20 | div() 21 | button(type="submit" class ="ask-question-button button") Submit 22 | -------------------------------------------------------------------------------- /views/editQuestions.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | include utils.pug 3 | 4 | block content 5 | div(class = "ask_question_errors") 6 | +validationErrorSummary(errors) 7 | div(class = "ask_question") 8 | div(class = 'title') Edit a Question 9 | div(class = 'details') 10 | form(action=`/askQuestions/${question.id}` method="post") 11 | input(type="hidden", name="_csrf", value=csrfToken) 12 | +field("Title", "title", question.title ) 13 | div(class='ask-question-form-spacing') 14 | +field("Question", "text", question.text, "textarea") 15 | div(class='form-group') 16 | label(for='chosenCategory') Choose a Category 17 | select(name='chosenCategory', id="") 18 | each category in categoryList 19 | option(value=category.id)=category.name 20 | div() 21 | button(type="submit" class ="ask-question-button button") Submit 22 | -------------------------------------------------------------------------------- /views/error.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | if error.status = 404 7 | div 8 | img(src="/images/404-image.png" alt="404IMG" class = "not-found-img" ) 9 | 10 | if error.typeOf === "question" 11 | div We couldn't find that question. Sorry about that. 12 | else 13 | div We couldn't find that page 14 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | if !locals.authenticated 5 | div(class='login-or-signup-prompt') 6 | span Please #[a(href="/users/login") log in] or #[a(href="/users/signup") sign up] to start asking questions. 7 | h1(class='landing-page-title') Welcome to Dark Overflow 8 | .content-slider 9 | .slider 10 | .mask 11 | ul 12 | li.anim1 13 | .quote Become a Javascript Pro 14 | li.anim2 15 | .quote Get answers to your questions 16 | li.anim3 17 | .quote Upvote the best solutions 18 | li.anim4 19 | .quote Search for your favorite topic 20 | li.anim5 21 | .quote Share your code solution 22 | div(class='landing-page-top-10') 23 | p TOP 10 QUESTIONS 24 | div(class='landing-question-box-container') 25 | - var n =1 26 | each question in questions 27 | div(class=`landing-question-${n++} landing-question-box` ) 28 | div(class='landing-question-title-text-wrapper') 29 | div(class='landing-question-box-title') 30 | a(href=`/questions/${question.dataValues.id}` class='landing-question-box-title')=question.dataValues.title 31 | hr(class='landing-question-box-top-divider') 32 | div(class='landing-question-box-text') 33 | p=question.dataValues.text 34 | hr(class='landing-question-box-top-divider') 35 | div(class='landing-question-box-category') 36 | p=`${question.Category.dataValues.name}` 37 | div(class='landing-question-box-score') 38 | p=`Score: ${question.dataValues.score}` 39 | div(class='landing-question-box-userName') 40 | p=`Asked by: ${question.Profile.dataValues.userName}` -------------------------------------------------------------------------------- /views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | block head 5 | link(rel="stylesheet" href="/stylesheets/index.css") 6 | script(src="/javascripts/layout.js") 7 | 8 | 9 | header(class = 'nav_bar') 10 | //1 far left -- logo which navigates to landing page 11 | a(href="/" ) 12 | img(src="/images/DO_black-transparent.png" class = "logo") 13 | //2 search bar 14 | form(action="/searchresults", method="post") 15 | input(type="text" name="searchString" placeholder="Search..") 16 | button(type="submit" class ="search-button button" ) Go 17 | nav(class = "nav_container") 18 | ul 19 | li(class = "categories_button") 20 | select(name='chosenCategory', id="", class='search-category-selector') 21 | option(value=null) Choose a question category 22 | each category in categoryList 23 | option(value=category.id)=category.name 24 | 25 | 26 | //5 if logged in, need logout button on far right 27 | if locals.authenticated 28 | div 29 | span(class='layout-greeter' id='layout-greeter') Welcome #{user.userName} 30 | div(class='logged-in-nav-buttons') 31 | a(href ='/askQuestions') 32 | button(class="ask-question-button button") Ask a Question 33 | form(action= "/users/logout" method= "post" ) 34 | button(type= "submit" class="logout-button button") Logout 35 | else 36 | div(class = "login_signup") 37 | a(href="/users/login" ) 38 | button(class="login-button button") Log in 39 | 40 | a(href="/users/signup" ) 41 | button(class = "signup-button button") Sign up 42 | 43 | block content 44 | 45 | 46 | 47 | footer(class='contact-footer') 48 | div(class='contact-footer-header') Developed By: 49 | div(class='contact-container') 50 | div(class='contact') Adam Lovett 51 | div(class='contact-link') 52 | a(href='https://github.com/adamLovettApps') Github 53 | span | 54 | a(href='#') LinkedIn 55 | span | 56 | a(href=`mailto:''`) Email 57 | div(class='contact') Andrew Musta 58 | div(class='contact-link') 59 | a(href='https://github.com/enomilan') Github 60 | span | 61 | a(href='#') LinkedIn 62 | span | 63 | a(href=`mailto:''`) Email 64 | div(class='contact') Geoffrey Yang 65 | div(class='contact-link') 66 | a(href='https://github.com/geoffyang') Github 67 | span | 68 | a(href='https://www.linkedin.com/in/geoffreyy/') LinkedIn 69 | span | 70 | a(href=`mailto:'geoffreyyang@gmail.com'`) Email 71 | div(class='contact') James Mayfield 72 | div(class='contact-link') 73 | a(href='https://github.com/Jodm522') Github 74 | span | 75 | a(href='#') LinkedIn 76 | span | 77 | a(href=`mailto:''`) Email 78 | -------------------------------------------------------------------------------- /views/login.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | include utils.pug 3 | 4 | 5 | block content 6 | div(class = "login_errors") 7 | +validationErrorSummary(errors) 8 | div(class = "login_user") 9 | div(class = 'title') Log In 10 | div(class = 'details') 11 | form(action="/users/login", method="post") 12 | input(type="hidden" name="_csrf" value=csrfToken) 13 | div(class='form-field-div') 14 | +field("Username", "userName", "") 15 | div(class='form-field-div') 16 | +field("Password", "password", "", "password" ) 17 | div() 18 | button(type="submit" class ="login-button button" ) Log in 19 | form(action="/users/demo", method="post") 20 | input(type="hidden" name="_csrf" value=csrfToken) 21 | button(type="submit" class ="login-button button" ) Demo User 22 | 23 | div(class='login-to-signup') 24 | span Don't have an account? #[a(href="/users/signup") Sign up] to start asking questions. 25 | -------------------------------------------------------------------------------- /views/question.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | include utils.pug 3 | append head 4 | script(src="https://kit.fontawesome.com/898a425ea4.js" crossorigin="anonymous") 5 | script(src="/javascripts/question.js") 6 | //shove each question into a separate DIV with an ID of its ID? 7 | // delete button to have the same id as the answer it's attached to 8 | 9 | block content 10 | div(class='question-page-question-box-container') 11 | div(class='question-page-question-box') 12 | div(class='question-page-question-title-text-wrapper') 13 | div(class='question-page-question-box-title')=question.title 14 | hr(class='question-page-question-box-top-divider') 15 | div(class='question-page-question-box-text') 16 | p(class='question-page-question-box-text-paragraph')=question.text 17 | hr(class='question-page-question-box-bottom-divider ') 18 | div(class='question-page-question-box-category') 19 | p(class='question-page-question-box-category-paragraph')=`${question.Category.dataValues.name}` 20 | if locals.authenticated 21 | div(class="answerQuestionForm") 22 | label(for='text' class="answer-question-box-title") Your Answer: 23 | form(action=`/answerquestion/${question.id}`, method="post" ) 24 | textarea(name="text" class="answer-text-box form-control") 25 | button( class = "answer-question-button button" id = question.id type="submit") Submit 26 | button(class= "cancel-answer-button button") Cancel 27 | if isQuestionAsker 28 | div(class='question-page-delete-and-edit-question') 29 | i(class="fas fa-trash delete-question-btn question-page-delete-question-icon" id = question.id) 30 | i(class="fas fa-edit edit-question-btn question-page-edit-question-icon" id = question.id) 31 | div(class = "ask_question editQuestionForm") 32 | div(class = 'title') Edit Your Question 33 | div(class = 'details') 34 | form(action=`/askQuestions/${question.id}` method="post") 35 | input(type="hidden" class="questionToEditId" value=question.id) 36 | input(type="hidden", name="_csrf", class='csrfEdit' value=csrfToken) 37 | div(class='form-field-div') 38 | div(class='form-group') 39 | label(for='title') Title 40 | input(type='text' name='title' class="edit-question-title-field" value=question.title) 41 | div(class='ask-question-form-spacing') 42 | div(class='form-field-div') 43 | div(class='form-group') 44 | label(for='text') Question 45 | textarea(name='text' class='form-control edit-question-text-field')=question.text 46 | div(class='form-field-div') 47 | div(class='form-group') 48 | label(for='chosenCategory') Choose a Category 49 | select(name='chosenCategory', id="" class='edit-question-category-field') 50 | each category in categoryList 51 | option(value=category.id)=category.name 52 | div(class='edit-question-form-button-wrapper') 53 | button(type="submit" class ="ask-question-button submit-question-edit-button button") Submit 54 | button(type="submit" class="ask-question-button cancel-edit-question-button button") Cancel 55 | div(class='.question-page-question-voting-icons') 56 | - var foundPositiveVote = false 57 | - var foundNegativeVote = false 58 | each vote in question.dataValues.QuestionVotes 59 | if vote.userId === user.id 60 | if vote.voteSum === 1 61 | - foundPositiveVote = true; 62 | if vote.voteSum === -1 63 | - foundNegativeVote = true; 64 | if foundPositiveVote === true 65 | i(class="far fa-caret-square-up question-page-upvote-question-icon upvoted-arrow", id=question.id) 66 | else 67 | i(class="far fa-caret-square-up question-page-upvote-question-icon", id=question.id) 68 | if foundNegativeVote 69 | i(class="far fa-caret-square-down question-page-downvote-question-icon downvoted-arrow" id=question.id) 70 | else 71 | i(class="far fa-caret-square-down question-page-downvote-question-icon" id=question.id) 72 | else 73 | div(class='.question-page-question-voting-icons') 74 | i(class="far fa-caret-square-up question-page-upvote-question-icon unclickable-vote-button", id=question.id) 75 | i(class="far fa-caret-square-down question-page-downvote-question-icon unclickable-vote-button" id=question.id) 76 | div(class='question-page-question-box-score') 77 | p=`Score: ` 78 | span(class='question-page-question-score')=question.dataValues.score 79 | div(class='question-page-question-box-userName') 80 | p=`Asked by: ${question.Profile.dataValues.userName}` 81 | if locals.authenticated 82 | button(class = 'open-answer-question button') Answer this question 83 | div(id='answersDiv' class='wrapper-for-all-answers') 84 | each answer in question.dataValues.Answers 85 | div(id=`answer-${answer.id}-div` class="question-page-answer-box-outer-wrapper") 86 | div()=answer.text 87 | hr(class='question-page-answer-box-divider') 88 | if !locals.authenticated 89 | div(class='question-page-answer-box-username') 90 | p=`Answered by: ` 91 | span= answer.dataValues.Profile.userName 92 | else 93 | if answer.userId !== user.id 94 | div(class='question-page-answer-box-username') 95 | p=`Answered by: ` 96 | span= answer.dataValues.Profile.userName 97 | else 98 | div(class='question-page-answer-box-username-answerer') 99 | p=`Answered by: ` 100 | span= answer.dataValues.Profile.userName 101 | if answer.userId === user.id 102 | i(class=`fas fa-trash delete-answer-btn` id = answer.id) 103 | i(class="fas fa-edit edit-answer-btn" id = answer.id) 104 | //- a(class = "delete-answer-btn" href="#", id = answer.id) Delete answer 105 | div(class='question-page-answer-box') 106 | div(class='answer-score-text-box') 107 | p=`Score: ` 108 | span(class = 'question-page-answer-score' id=`A${answer.id}-score`)=answer.score 109 | if locals.authenticated 110 | div(class='.question-page-answer-voting-icons') 111 | - var foundPositiveVote = false 112 | - var foundNegativeVote = false 113 | each vote in answer.dataValues.AnswerVotes 114 | if vote.userId === user.id 115 | if vote.voteSum === 1 116 | - foundPositiveVote = true; 117 | if vote.voteSum === -1 118 | - foundNegativeVote = true; 119 | if foundPositiveVote === true 120 | i(class="far fa-caret-square-up answer-upvote-arrow upvoted-arrow", id=`A${answer.id}-up`) 121 | else 122 | i(class="far fa-caret-square-up answer-upvote-arrow", id=`A${answer.id}-up`) 123 | if foundNegativeVote 124 | i(class="far fa-caret-square-down answer-downvote-arrow downvoted-arrow" id=`A${answer.id}-down`) 125 | else 126 | i(class="far fa-caret-square-down answer-downvote-arrow" id=`A${answer.id}-down`) 127 | else 128 | div(class='.question-page-answer-voting-icons') 129 | i(class="far fa-caret-square-up answer-upvote-arrow unclickable-vote-button", id=`A${answer.id}-up`) 130 | i(class="far fa-caret-square-down answer-downvote-arrow unclickable-vote-button" id=`A${answer.id}-down`) 131 | 132 | -------------------------------------------------------------------------------- /views/searchresults.pug: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1(class='search-page-title')=title 5 | 6 | //- if !questions.length 7 | //- div(class = "no-question-found") 8 | //- div We couldnt find any questions about that... 9 | //- a(href ='/askQuestions') Why not ask one here? 10 | //- else 11 | div(class='search-question-box-container') 12 | if !questions.length 13 | div(class = "no-question-found") 14 | div We couldnt find any questions about that... 15 | a(href ='/askQuestions') Why not ask one here? 16 | else 17 | - var n =1 18 | each question in questions 19 | if (questions.length === 1) 20 | div(class=`search-question-${n++} search-question-box single-search-result` ) 21 | div(class='search-question-title-text-wrapper') 22 | div(class='search-question-box-title') 23 | a(href=`/questions/${question.dataValues.id}` class='search-question-box-title')=question.dataValues.title 24 | hr(class='search-question-box-top-divider') 25 | div(class='search-question-box-text') 26 | p=question.dataValues.text 27 | hr(class='search-question-box-top-divider') 28 | div(class='search-question-box-category') 29 | p=`${question.Category.dataValues.name}` 30 | div(class='search-question-box-score') 31 | p=`Score: ${question.dataValues.score}` 32 | div(class='search-question-box-userName') 33 | p=`Asked by: ${question.Profile.dataValues.userName}` 34 | else 35 | div(class=`search-question-${n++} search-question-box` ) 36 | div(class='search-question-title-text-wrapper') 37 | div(class='search-question-box-title') 38 | a(href=`/questions/${question.dataValues.id}` class='search-question-box-title')=question.dataValues.title 39 | hr(class='search-question-box-top-divider') 40 | div(class='search-question-box-text') 41 | p=question.dataValues.text 42 | hr(class='search-question-box-top-divider') 43 | div(class='search-question-box-category') 44 | p=`${question.Category.dataValues.name}` 45 | div(class='search-question-box-score') 46 | p=`Score: ${question.dataValues.score}` 47 | div(class='search-question-box-userName') 48 | p=`Asked by: ${question.Profile.dataValues.userName}` 49 | 50 | -------------------------------------------------------------------------------- /views/signup.pug: -------------------------------------------------------------------------------- 1 | extends layout.pug 2 | include utils.pug 3 | 4 | block content 5 | div(class = "signup_errors") 6 | +validationErrorSummary(errors) 7 | div(class = "signup_user") 8 | div(class = 'title') Sign Up 9 | div(class = 'details') 10 | form(action="/users/signup", method="post") 11 | input(type="hidden" name ="_csrf" value = csrfToken) 12 | div(class='form-field-div') 13 | +field("Username", "userName", user.userName,"","Enter your username" ) 14 | div(class='form-field-div') 15 | +field("First Name", "firstName", user.firstName, "", "Enter your first name" ) 16 | div(class='form-field-div') 17 | +field("Last Name", "lastName", user.lastName, "", "Enter your last name" ) 18 | div(class='form-field-div') 19 | +field("Email", "email", user.email, "email", "Enter your email" ) 20 | div(class='form-field-div') 21 | +field("Password", "password", "", "password", "Enter your password" ) 22 | div(class='form-field-div') 23 | +field("Confirm your password", "confirmPassword", "", "password", "Confirm password" ) 24 | div() 25 | button(type="submit" class ="signup-button button" ) Submit 26 | form(action="/users/demo", method="post") 27 | input(type="hidden" name="_csrf" value=csrfToken) 28 | button(type="submit" class ="login-button button" ) Demo User 29 | div(class='signup-to-togin') 30 | span Already have an account? #[a(href="/users/login") Log in] to start asking questions. 31 | -------------------------------------------------------------------------------- /views/utils.pug: -------------------------------------------------------------------------------- 1 | mixin validationErrorSummary(errors) 2 | if errors 3 | div(class='alert alert-danger' role='alert') 4 | p The following error(s) occurred: 5 | ul 6 | each error in errors 7 | li= error 8 | mixin field(labelText, fieldName, fieldValue, fieldType = 'text', placeholder = null) 9 | div(class='form-group') 10 | label(for=fieldName class='bump-form-label')= labelText 11 | if fieldType === 'textarea' 12 | textarea(id=fieldName name=fieldName class='form-control' rows='5')= fieldValue 13 | else 14 | input(type=fieldType id=fieldName name=fieldName value=fieldValue placeholder=placeholder class='form-control') 15 | --------------------------------------------------------------------------------