├── server ├── routes │ └── router.js ├── models │ ├── questionsModel.js │ └── userModel.js ├── controllers │ ├── authController.js │ ├── userController.js │ └── questionsController.js └── server.js ├── .gitignore ├── client ├── Styles │ ├── SignupStyles.css │ ├── QuestionCategory.css │ ├── WinCondition.css │ ├── LoginStyles.css │ ├── Question.css │ ├── QuestionCard.css │ ├── Quiz.css │ └── App.css ├── assets │ ├── jeopardy.mp3 │ ├── AlexTrebek.jpg │ ├── TriviaNights.png │ ├── background.jpeg │ └── CongratulationsSnip.mp3 ├── store.js ├── components │ ├── Scoreboard.jsx │ ├── QuestionCategory.jsx │ ├── Wincondition.jsx │ ├── Time.jsx │ ├── QuestionCards.jsx │ ├── Question.jsx │ ├── Signup.jsx │ └── Quiz.jsx ├── index.html ├── index.js ├── reducers │ └── gameSlice.js └── App.jsx ├── .env ├── README.md ├── package.json └── webpack.config.js /server/routes/router.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /client/Styles/SignupStyles.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | JWT_KEY=401c90ec39c660d27229550f783f5eb8dde509799a89903ed75a1a5ae2ea12bf -------------------------------------------------------------------------------- /client/assets/jeopardy.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goblin-Shark-4/trivia-game-night/HEAD/client/assets/jeopardy.mp3 -------------------------------------------------------------------------------- /client/assets/AlexTrebek.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goblin-Shark-4/trivia-game-night/HEAD/client/assets/AlexTrebek.jpg -------------------------------------------------------------------------------- /client/assets/TriviaNights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goblin-Shark-4/trivia-game-night/HEAD/client/assets/TriviaNights.png -------------------------------------------------------------------------------- /client/assets/background.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goblin-Shark-4/trivia-game-night/HEAD/client/assets/background.jpeg -------------------------------------------------------------------------------- /client/assets/CongratulationsSnip.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goblin-Shark-4/trivia-game-night/HEAD/client/assets/CongratulationsSnip.mp3 -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import gameSlice from './reducers/gameSlice'; 3 | 4 | export const store = configureStore({ 5 | reducer: { 6 | game: gameSlice 7 | }, 8 | }); -------------------------------------------------------------------------------- /client/components/Scoreboard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Scoreboard = ({score}) => { 4 | return ( 5 |
6 |

Score: {score}

7 |
8 | ); 9 | }; 10 | 11 | export default Scoreboard; -------------------------------------------------------------------------------- /client/Styles/QuestionCategory.css: -------------------------------------------------------------------------------- 1 | .question-category { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | gap: 10px; 6 | padding: 10px; 7 | background-color: #f0f0f0; 8 | } 9 | 10 | .question-cards { 11 | display: flex; 12 | flex-direction: column; 13 | gap: 10px; 14 | padding: 10px; 15 | } -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.jsx'; 4 | import { store } from './store'; 5 | import { Provider } from 'react-redux'; 6 | 7 | const rootElement = document.getElementById('root'); 8 | const root = ReactDOM.createRoot(rootElement); 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /client/reducers/gameSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | 3 | 4 | 5 | const initialState = { 6 | users: [] 7 | } 8 | 9 | export const gameSlice = createSlice({ 10 | name: 'game', 11 | initialState, 12 | add_user: (state, action) => { 13 | state.users.push({user: action.payload}) 14 | } 15 | 16 | }) 17 | 18 | 19 | export const { add_user } = gameSlice.actions; 20 | 21 | export default gameSlice.reducer; -------------------------------------------------------------------------------- /server/models/questionsModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | // sets a schema for the 'species' collection 5 | const topicSchema = new Schema({ 6 | category: { type: String, required: true }, 7 | type: { type: String, required: true }, 8 | difficulty: { type: String, required: true }, 9 | question: { type: String, required: true }, 10 | correct_answer: { type: String, required: true }, 11 | incorrect_answers: { type: [String], required: true }, 12 | }); 13 | 14 | const Topic = mongoose.model('topic', topicSchema); 15 | 16 | module.exports = Topic; 17 | -------------------------------------------------------------------------------- /client/components/QuestionCategory.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import QuestionCard from './QuestionCards'; 3 | 4 | const QuestionCategory = ({ category, questions }) => { 5 | return ( 6 |
7 |

{category}

8 |
9 | {questions.easy.map((question) => ( 10 | 11 | ))} 12 | {questions.medium.map((question) => ( 13 | 14 | ))} 15 | 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default QuestionCategory; 22 | -------------------------------------------------------------------------------- /server/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const SALT_WORK_FACTOR = 10; 6 | const bcrypt = require('bcryptjs'); 7 | 8 | 9 | 10 | const userSchema = new Schema({ 11 | username: { type: String, required: true, unique: true }, 12 | password: { type: String, required: true }, 13 | location: { type: String } 14 | }) 15 | 16 | userSchema.pre('save', function(next) { 17 | if (!this.isModified('password')) return next(); 18 | 19 | 20 | bcrypt.hash(this.password, SALT_WORK_FACTOR, (err, hash) => { 21 | if (err) return next(err); 22 | this.password = hash; 23 | return next(); 24 | }) 25 | }) 26 | 27 | const User = mongoose.model('user', userSchema); 28 | 29 | module.exports = User; -------------------------------------------------------------------------------- /client/components/Wincondition.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react' 2 | import useSound from 'use-sound'; 3 | import '../Styles/WinCondition.css' // this will be neon color 4 | import Alex from '../assets/AlexTrebek.jpg' 5 | import Music from '../assets/CongratulationsSnip.mp3' 6 | 7 | const WinCondition = ({score, resetGame, hasWon}) => { 8 | console.log(resetGame) 9 | const [play, {stop}] = useSound(Music); 10 | 11 | 12 | useEffect(() => { 13 | if (hasWon) { 14 | play(); 15 | return function cleanup() { 16 | stop(); 17 | } 18 | } 19 | }, [play, stop]) 20 | 21 | return ( 22 |
23 |

Congratulations! You've Won!

24 | Alex Trebek 25 | 26 |
27 | 28 | ); 29 | }; 30 | 31 | export default WinCondition 32 | -------------------------------------------------------------------------------- /client/components/Time.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | 4 | const Timer = ({points}) => { 5 | const [timer, setTimer ] = useState(20); 6 | const [pointsRemaining, setPoints] = useState(points); 7 | const navigate = useNavigate(); 8 | 9 | useEffect(() => { 10 | let interval; 11 | 12 | interval = setInterval(() => { 13 | setTimer((seconds) => { 14 | if (seconds === 0) { 15 | navigate('/') 16 | } 17 | setPoints((prev) => prev - 50); 18 | return seconds - 1; 19 | }) 20 | }, 1000) 21 | return () => clearInterval(interval) 22 | }, []) 23 | 24 | return ( 25 | <> 26 |
27 | TIME REMAINING: {timer} 28 |
29 | {/*
30 | POINTS POSSIBLE: {pointsRemaining} 31 |
*/} 32 | 33 | ) 34 | } 35 | 36 | export default Timer; -------------------------------------------------------------------------------- /client/Styles/WinCondition.css: -------------------------------------------------------------------------------- 1 | /* WinCondition.css */ 2 | 3 | .win-container { 4 | align-items: center; 5 | text-align: center; 6 | padding: 20px; 7 | } 8 | 9 | #alex { 10 | border: 2px solid #00ffcc; 11 | } 12 | .neon-text { 13 | font-size: 36px; 14 | font-weight: bold; 15 | color: #fff; 16 | text-shadow: 0 0 10px #00ffcc, 0 0 20px #00ffcc, 0 0 30px #00ffcc; 17 | } 18 | 19 | /* Customize the glowing effect */ 20 | .neon-text::after { 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | width: 100%; 25 | height: 100%; 26 | border-radius: 10px; 27 | box-shadow: 0 0 20px #00ffcc, 0 0 40px #00ffcc, 0 0 60px #00ffcc; 28 | opacity: 0; 29 | animation: glow 1s ease-in-out infinite; 30 | } 31 | 32 | @keyframes glow { 33 | 0% { 34 | opacity: 1; 35 | } 36 | 50% { 37 | opacity: 0.5; 38 | } 39 | 100% { 40 | opacity: 1; 41 | } 42 | } 43 | 44 | #playAgain { 45 | color: white 46 | } 47 | 48 | #playAgain:hover { 49 | background-color: #4800b3; 50 | } -------------------------------------------------------------------------------- /client/Styles/LoginStyles.css: -------------------------------------------------------------------------------- 1 | .login { 2 | max-width: 400px; 3 | margin: 20px auto; 4 | padding: 20px; 5 | border: 2px solid #4800b3; 6 | border-radius: 5px; 7 | background-color: rgba(200, 200, 200, 0); 8 | box-shadow: 0 0 10px #f200fffb, 0 0 20px #d400ff, 0 0 30px #9500ff; ; 9 | } 10 | 11 | .logTitle { 12 | font-size: 24px; 13 | font-weight: bold; 14 | text-align: center; 15 | margin-bottom: 20px; 16 | background-color: rgba(41, 52, 103, 0); /* Set the alpha value to 0 for full transparency */ 17 | color: #fff; 18 | text-shadow: 0 0 10px #00aeff, 0 0 20px #0055ff, 0 0 30px #0088ff; 19 | } 20 | 21 | .username, 22 | .password { 23 | width: 100%; 24 | padding: 10px; 25 | margin-bottom: 10px; 26 | border: 1px solid #ccc; 27 | border-radius: 5px; 28 | } 29 | 30 | button { 31 | display: block; 32 | width: 100%; 33 | padding: 10px; 34 | border: none; 35 | border-radius: 5px; 36 | background-color: #000ed9; 37 | color: #fff; 38 | font-size: 16px; 39 | font-weight: bold; 40 | cursor: pointer; 41 | } 42 | 43 | button:hover { 44 | background-color: #4800b3; 45 | } 46 | 47 | .createAcct { 48 | margin-top: 2em; 49 | } -------------------------------------------------------------------------------- /client/Styles/Question.css: -------------------------------------------------------------------------------- 1 | .question-container { 2 | display: flex; 3 | flex-direction: column; 4 | max-width: 600px; 5 | margin: 0 auto; 6 | padding: 20px; 7 | border: 5px solid #0074d9; 8 | border-radius: 10px; 9 | background-color: #f8f8f8; 10 | width: 100vw; 11 | 12 | 13 | } 14 | 15 | .question-title { 16 | font-size: 36px; 17 | font-weight: bold; 18 | margin-bottom: 20px; 19 | color: black; 20 | } 21 | 22 | .answer-container { 23 | display: grid; 24 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 25 | gap: 10px; 26 | justify-content: center; 27 | margin-top: 20px; 28 | 29 | } 30 | .timer { 31 | font-size: 36px; 32 | font-weight: bold; 33 | color: #fff; 34 | text-shadow: 0 0 10px #00ffcc, 0 0 20px #00ffcc, 0 0 30px #00ffcc; 35 | } 36 | 37 | .answer { 38 | cursor: pointer; 39 | border: 2px solid #0074d9; 40 | background-color: #fff; 41 | color: #0074d9; 42 | font-size: 24px; 43 | font-weight: bold; 44 | padding: 20px; 45 | border-radius: 5px; 46 | transition: background-color 0.3s ease, color 0.3s ease; 47 | } 48 | 49 | .answer:hover { 50 | background-color: #0074d9; 51 | color: #fff; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /client/components/QuestionCards.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styles from '../Styles/QuestionCard.css'; 3 | 4 | const shuffleArray = (array) => { 5 | 6 | const shuffledArray = [...array]; 7 | 8 | for (let i = shuffledArray.length - 1; i > 0; i--) { 9 | const j = Math.floor(Math.random() * (i + 1)); 10 | [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]]; 11 | } 12 | return shuffledArray; 13 | }; 14 | 15 | const QuestionCard = ({ question, handleQuestionClick, setQuestion }) => { 16 | const [timer, setTimer] = useState(20); 17 | const points = { easy: 1000, medium: 3000, hard: 5000 }; 18 | 19 | // Combine correct_answer and incorrect_answers into a single array 20 | const answers = [question.correct_answer, ...question.incorrect_answers]; 21 | 22 | // Shuffle the answers array to display them in random order 23 | const shuffledAnswers = shuffleArray(answers); 24 | {/* onClick={() => setShowQuestion(!showQuestion)} */} 25 | return ( 26 |
handleQuestionClick(question)} 29 | > 30 |
31 |

{points[question.difficulty]}

32 |
33 |
34 | ); 35 | }; 36 | 37 | export default QuestionCard; 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trivia-game-night 2 | This is a web-based trivia game created using React, JavaScript, and CSS. 3 | 4 | ## Table of Contents 5 | - [Overview](#overview) 6 | - [Features](#features) 7 | - [Installation](#installation) 8 | - [Usage](#usage) 9 | - [Technologies Used](#technologies-used) 10 | 11 | ## Overview 12 | 13 | Trivia Game Night was created to provide great entertainment to users, fetching questions from an API. 14 | 15 | ## Features 16 | 17 | - User creation using Oath, login and logout. 18 | - 5 categories of questions. 19 | - Interactive sounds as you play the game. 20 | - Scoreboard implementation with winning functionality. 21 | 22 | ## Installation 23 | 24 | 1. Clone the repository to your local machine: 25 | 26 | git clone https://github.com/raulclassico7/trivia-game-night.git 27 | 28 | css 29 | Copy code 30 | 31 | 2. Navigate to the project directory: 32 | 33 | cd trivia-game 34 | 35 | markdown 36 | Copy code 37 | 38 | 3. Install dependencies: 39 | 40 | npm install 41 | 42 | markdown 43 | Copy code 44 | 45 | ## Usage 46 | 47 | 1. Start the development server: 48 | 49 | npm start 50 | 51 | markdown 52 | Copy code 53 | 54 | 2. Open your web browser and visit [http://localhost:3000](http://localhost:3000) to play the trivia game. 55 | 56 | ## Technologies Used 57 | 58 | - React 59 | - JavaScript 60 | - CSS 61 | - Webpack 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /client/Styles/QuestionCard.css: -------------------------------------------------------------------------------- 1 | /* Update the question card style to have a background color and box-shadow */ 2 | .question-card { 3 | width: 10em; 4 | height: 8em; 5 | background-color: rgba(0, 124, 255, 0.8); /* Change the alpha value to adjust transparency */ 6 | border: 2px solid; 7 | border-radius: 10px; 8 | cursor: pointer; 9 | transition: transform 0.4s; 10 | perspective: 1000px; /* Add perspective for 3D effect */ 11 | } 12 | 13 | .question-card .front, 14 | .question-card .back { 15 | top: 0; 16 | left: 0; 17 | width: 100%; 18 | height: 100%; 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | text-align: center; 23 | padding: 10px; 24 | font-size: 34px; 25 | border-radius: 10px; 26 | backface-visibility: hidden; 27 | } 28 | 29 | /* Add a neon-style effect to the front side */ 30 | .question-card .front { 31 | /* Add more text-shadow layers to increase the glow effect */ 32 | background-color: rgba(41, 52, 103, 0); /* Set the alpha value to 0 for full transparency */ 33 | color: #fff; 34 | text-shadow: 0 0 10px #00ffcc, 0 0 20px #00ffcc, 0 0 30px #00ffcc; 35 | } 36 | 37 | /* Add a neon-style effect to the back side */ 38 | .question-card .back { 39 | background-color: #00ffcc; 40 | color: #000; 41 | text-shadow: 0 0 10px #007bff, 0 0 20px #007bff, 0 0 30px #007bff; 42 | /* Add more text-shadow layers to increase the glow effect */ 43 | } 44 | 45 | 46 | .question-card.flipped { 47 | transform: rotateY(180deg); 48 | } 49 | -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 3 | import SignUp from './components/Signup'; 4 | import Quiz from './components/Quiz'; 5 | import './Styles/App.css'; 6 | 7 | 8 | 9 | 10 | const App = (props) => { 11 | const [user, setUser] = useState({}); 12 | const [loggedIn, setLoggedIn] = useState(false); 13 | const [loading, setLoading] = useState(true); 14 | 15 | //check for JWT on page load, if user has JWT, send them to start page 16 | useEffect(() => { 17 | const jwtToken = localStorage.getItem('triviaJwtToken'); 18 | jwtToken ? fetchUserData(jwtToken) : setLoading(false); 19 | setLoggedIn(false); 20 | }, [loggedIn]); 21 | 22 | const fetchUserData = async (jwt) => { 23 | const user = await fetch('/verifyJwt', { 24 | headers: { 25 | 'Authorization': `Bearer ${jwt}` 26 | } 27 | }) 28 | if (user.ok) { 29 | const userData = await user.json(); 30 | setUser(userData) 31 | } else { 32 | localStorage.removeItem('triviaJwtToken'); 33 | } 34 | setLoading(false); 35 | } 36 | 37 | if (loading) return null; 38 | 39 | return ( 40 |
41 | 42 | 43 | {/* }/> */} 44 | 45 | : }/> 46 | 47 | 48 |
49 | ); 50 | }; 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /client/components/Question.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import useSound from 'use-sound'; 3 | import jeopardyMusic from '../assets/jeopardy.mp3' 4 | import '../Styles/Question.css' 5 | import Timer from './Time'; 6 | 7 | 8 | const shuffleArray = (array) => { 9 | 10 | const shuffledArray = [...array]; 11 | 12 | for (let i = shuffledArray.length - 1; i > 0; i--) { 13 | const j = Math.floor(Math.random() * (i + 1)); 14 | [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]]; 15 | } 16 | return shuffledArray; 17 | }; 18 | 19 | 20 | const Question = ({ question, handleAnswerClick, points }) => { 21 | const [play, {stop}] = useSound(jeopardyMusic); 22 | 23 | useEffect(() => { 24 | play(); 25 | return function cleanup() { 26 | stop(); 27 | } 28 | }, [play, stop]) 29 | 30 | // const handleBtnClick = () => { 31 | 32 | // } 33 | 34 | const answers = shuffleArray([question.correct_answer, ...question.incorrect_answers]); 35 | 36 | return ( 37 | <> 38 | 39 |
40 |

{question.question}

41 |
42 | {answers.map((answer, i) => { 43 | return 44 | })} 45 |
46 |
47 | 48 | ) 49 | } 50 | 51 | 52 | export default Question; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trivia_game_night", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "server": "nodemon server/server.js", 8 | "watch": "webpack --watch --mode=development", 9 | "build": "webpack --mode=production", 10 | "dev": "webpack-dev-server --mode development --open --hot", 11 | "gulp-prod": "node_modules/.bin/gulp prod", 12 | "gulp-dev": "node_modules/.bin/gulp dev" 13 | }, 14 | "nodemonConfig": { 15 | "ignore": [ 16 | "build", 17 | "client" 18 | ] 19 | }, 20 | "author": "CodesmithLLC https://github.com/CodesmithLLC ", 21 | "license": "ISC", 22 | "dependencies": { 23 | "@reduxjs/toolkit": "^1.9.5", 24 | "axios": "^1.4.0", 25 | "bcryptjs": "^2.4.3", 26 | "cors": "^2.8.5", 27 | "dotenv": "^16.3.1", 28 | "express": "^4.18.2", 29 | "file-loader": "^6.2.0", 30 | "jsonwebtoken": "^9.0.1", 31 | "mongoose": "^7.4.1", 32 | "react": "^18.2.0", 33 | "react-dom": "^18.2.0", 34 | "react-redux": "^8.1.2", 35 | "react-router-dom": "^6.14.2", 36 | "redux": "^4.2.1", 37 | "use-sound": "^4.0.1" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.22.9", 41 | "@babel/preset-env": "^7.22.9", 42 | "@babel/preset-react": "^7.22.5", 43 | "babel-loader": "^9.1.3", 44 | "css-loader": "^6.8.1", 45 | "html-webpack-plugin": "^5.5.3", 46 | "mini-css-extract-plugin": "^2.7.6", 47 | "node-sass": "^9.0.0", 48 | "nodemon": "^2.0.22", 49 | "sass-loader": "^13.3.2", 50 | "style-loader": "^3.3.3", 51 | "webpack": "^5.88.2", 52 | "webpack-cli": "^4.10.0", 53 | "webpack-dev-server": "^4.15.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/userModel'); 2 | const jwt = require('jsonwebtoken'); 3 | 4 | const authController = {}; 5 | 6 | 7 | authController.verifyJwt = (req, res, next) => { 8 | console.log('verifyAuth') 9 | try { 10 | const auth = req.headers['authorization']; 11 | if (!auth) return next({ 12 | log: `Express error in cookieController.verifyJWT: no JWT`, 13 | status: 401, 14 | message: { err: `An error occurred in cookieController.verifyJWT: no JWT` }, 15 | }) 16 | 17 | const jwtToken = auth.split(' ')[1]; 18 | jwt.verify(jwtToken, process.env.JWT_KEY, async (err, user) => { 19 | if (err) return res.sendStatus(401); 20 | const { username } = user; 21 | console.log('user', user); 22 | const userData = await User.findOne({ username }); 23 | console.log('userData', userData); 24 | res.locals.user = userData; 25 | return next(); 26 | }) 27 | } catch (err) { 28 | return next({}); 29 | } 30 | } 31 | 32 | authController.setJwtToken = async (req, res, next) => { 33 | try { 34 | const { _id, username } = res.locals.user 35 | const payload = { _id, username } 36 | const secret = process.env.JWT_KEY; 37 | console.log('secret', secret) 38 | const jwtToken = await jwt.sign(payload, secret, {expiresIn: '1h'}); 39 | console.log('im here', jwtToken) 40 | 41 | res.locals.secret = { 42 | jwtToken: jwtToken, 43 | user: { 44 | id: _id, 45 | username: username 46 | } 47 | } 48 | console.log('secret signed!', jwtToken) 49 | return next() 50 | } catch (err) { 51 | return next({ 52 | 53 | }) 54 | } 55 | } 56 | 57 | module.exports = authController; -------------------------------------------------------------------------------- /client/Styles/Quiz.css: -------------------------------------------------------------------------------- 1 | body { 2 | /* background-color: #007bff; */ 3 | background-size: cover; 4 | background-repeat: no-repeat; 5 | background-attachment: fixed; 6 | margin: 0; 7 | padding: 0; 8 | background-image: url('https://nextrestaurants.com/wp-content/uploads/2019/07/Restaurants-TriviaNights-Marketing.png'); 9 | font-family: 'Arial', sans-serif; 10 | } 11 | .jeopardy-board { 12 | display: grid; 13 | grid-template-columns: repeat(6, 1fr); /* 6 columns for categories */ 14 | gap: 10px; 15 | width: 100%; 16 | max-width: 1200px; 17 | margin-top: 20px; 18 | margin: auto; 19 | } 20 | 21 | .category { 22 | background-color: #007bff; 23 | color: #fff; 24 | padding: 10px; 25 | text-align: center; 26 | font-weight: bold; 27 | } 28 | 29 | .questions { 30 | display: flex; 31 | flex-direction: column; 32 | gap: 10px; 33 | padding: 10px; 34 | background-color: transparent; 35 | } 36 | 37 | .scoreboard { 38 | max-width: 400px; 39 | margin: 0 auto; 40 | padding: 20px; 41 | background-color: rgba(41, 52, 103, 0); /* Set the alpha value to 0 for full transparency */ 42 | color: #fff; 43 | text-shadow: 0 0 10px #00ffcc, 0 0 20px #00ffcc, 0 0 30px #00ffcc; 44 | } 45 | 46 | header { 47 | display: flex; 48 | justify-content: space-between; 49 | } 50 | 51 | header > div { 52 | display: flex; 53 | flex-direction: column; 54 | } 55 | 56 | #logOffBtn { 57 | border-radius: 5px; 58 | background-color: rgba(220,220,220,.8); 59 | } 60 | 61 | #logOffBtn:hover { 62 | background-color: rgba(20,20,20,.8); 63 | color: white; 64 | cursor: pointer; 65 | } 66 | 67 | #deleteAcctBtn { 68 | border-radius: 0 0 0 10px; 69 | background-color: rgba(220,220,220,.8); 70 | } 71 | 72 | #deleteAcctBtn:hover { 73 | background-color: rgb(255, 0, 0); 74 | width: 8em; 75 | height: 3em; 76 | color: white; 77 | cursor: pointer; 78 | } 79 | 80 | .hideCard { 81 | display: none; 82 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | 5 | module.exports = { 6 | entry: './client/index.js', 7 | output: { 8 | filename: 'bundle.js', 9 | path: path.resolve(__dirname, './build'), 10 | // publicPath: '' 11 | }, 12 | mode: process.env.NODE_ENV, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?$/, 17 | exclude: /node_modules/, //1)massive folder 2)doesn't need to be transformed 18 | use: { 19 | loader: 'babel-loader', 20 | options: { 21 | presets: ['@babel/preset-env', '@babel/preset-react'], 22 | }, 23 | }, 24 | }, 25 | // { 26 | // test: /\.s[ac]ss$/i, ///also: /\.s[ac]ss$/i 27 | // use: ['style-loader', 'css-loader', 'sass-loader'], 28 | // }, 29 | { 30 | test: /.(css|scss)$/, 31 | exclude: /node_modules/, 32 | use: ['style-loader', 'css-loader'], 33 | }, 34 | { 35 | test: /\.(jpe?g|png|gif|svg|mp3)$/i, 36 | loader: 'file-loader', 37 | // options: { 38 | // name: '/public/icons/[name].[ext]' 39 | // } 40 | } 41 | ], 42 | }, 43 | resolve: { 44 | extensions: ['.js', '.jsx'], 45 | }, 46 | devServer: { 47 | static: { 48 | directory: path.resolve(__dirname, './build'), 49 | publicPath: '/', 50 | }, 51 | compress: true, 52 | port: 8080, 53 | proxy: { 54 | '/questions': 'http://localhost:3000', 55 | '/verifyJwt': 'http://localhost:3000', 56 | '/log-in': 'http://localhost:3000', 57 | '/sign-up': 'http://localhost:3000', 58 | '/delete': 'http://localhost:3000' 59 | }, 60 | }, 61 | plugins: [ 62 | new HtmlWebpackPlugin({ 63 | filename: 'index.html', 64 | template: './client/index.html', 65 | }), 66 | ], 67 | devtool: 'eval-source-map', 68 | }; 69 | -------------------------------------------------------------------------------- /client/Styles/App.css: -------------------------------------------------------------------------------- 1 | /* Add your overall styling for the App component here */ 2 | 3 | 4 | 5 | 6 | * { 7 | box-sizing: border-box; 8 | 9 | } 10 | 11 | #quiz { 12 | display: flex; 13 | flex-direction: column; 14 | /* align-items: ; */ 15 | } 16 | 17 | body { 18 | font-family: Arial, sans-serif; 19 | margin: 0; 20 | padding: 0; 21 | } 22 | 23 | .container { 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | justify-content: center; 28 | padding: 20px; 29 | } 30 | 31 | .header { 32 | text-align: center; 33 | font-size: 24px; 34 | font-weight: bold; 35 | margin-bottom: 20px; 36 | } 37 | 38 | .game-board { 39 | display: grid; 40 | grid-template-columns: repeat(6, 1fr); /* 6 columns for categories */ 41 | gap: 10px; 42 | width: 100%; 43 | max-width: 1200px; 44 | margin-top: 20px; 45 | } 46 | 47 | .category { 48 | background-color: #007bff; 49 | color: #fff; 50 | padding: 10px;; 51 | text-align: center; 52 | font-weight: bold; 53 | font-size: 28px; 54 | border-radius: 10px; 55 | 56 | } 57 | 58 | .question-card { 59 | display: flex; 60 | justify-content: center; 61 | align-items: center; 62 | background-color: rgba(0, 123, 255, 0.7); /* Set the alpha value to 0 for full transparency */ 63 | color: #fff; 64 | text-shadow: 0 0 10px #00ffcc, 0 0 20px #00ffcc, 0 0 30px #00ffcc; 65 | /* padding: 20px; */ 66 | text-align: center; 67 | cursor: pointer; 68 | transition: transform 0.2s ease; 69 | width: 8vw; 70 | height: 8vw; 71 | } 72 | 73 | .question-card.flipped { 74 | transform: rotateY(180deg); 75 | } 76 | 77 | .front, .back { 78 | width: 100%; 79 | } 80 | 81 | .back { 82 | display: flex; 83 | flex-direction: column; 84 | 85 | } 86 | 87 | .back p { 88 | margin: 10px 0; 89 | } 90 | 91 | 92 | main { 93 | display: flex; 94 | 95 | } 96 | 97 | #scoreboard { 98 | padding: 1em; 99 | } 100 | 101 | h1, h2 { 102 | color: white; 103 | } 104 | 105 | h1 { 106 | padding-left: 1em; 107 | } 108 | 109 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/userModel'); 2 | const bcrypt = require('bcryptjs'); 3 | 4 | const userController = {}; 5 | 6 | userController.verifyUser = (req, res, next) => { 7 | console.log('hello') 8 | const { username, password } = req.body; 9 | 10 | User.findOne({username}) 11 | .then((user) => { 12 | if (user === null) return res.status(200).json({}) 13 | res.locals.user = user; 14 | bcrypt.compare(password, user.password) 15 | .then(match => { 16 | console.log('match', match); 17 | return match ? next() 18 | : next({ 19 | log: `Express error in userController.verifyUser: invalid password`, 20 | status: 400, 21 | message: { err: `An error occurred in userController.verifyUser: invalid password` } 22 | }) 23 | }) 24 | .catch(err => next({ 25 | log: `Express error in userController.verifyUser: ${err}`, 26 | status: 400, 27 | message: { err: 'An error occurred in userController.verifyUser' }, 28 | })) 29 | }) 30 | .catch(err => next({ 31 | log: `Express error in userController.verifyUser: ${err}`, 32 | status: 400, 33 | message: { err: 'An error occurred in userController.verifyUser' }, 34 | })); 35 | }; 36 | 37 | 38 | userController.addUser = (req, res, next) => { 39 | const { username, password } = req.body; 40 | 41 | User.create({ username, password }) 42 | .then((user) => { 43 | res.locals.user = user; 44 | console.log('afsdfsdf') 45 | return next(); 46 | }) 47 | .catch(err => next({ 48 | log: `Express middleware error in userController.createUser: ${err}`, 49 | status: 400, 50 | message: { err: 'An error occurred in userController.createUser' }, 51 | })); 52 | }; 53 | 54 | userController.deleteUser = (req, res, next) => { 55 | const { username } = req.body; 56 | console.log('username', username) 57 | 58 | User.deleteOne({username}) 59 | .then(res => { 60 | console.log('res', res); 61 | return next(); 62 | }) 63 | .catch(err => next({})) 64 | } 65 | 66 | module.exports = userController; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const path = require('path'); 4 | const port = process.env.PORT || 3000; 5 | const mongoose = require('mongoose'); 6 | const questionsController = require('./controllers/questionsController'); 7 | const authController = require('./controllers/authController'); 8 | const userController = require('./controllers/userController'); 9 | const cors = require('cors'); 10 | require('dotenv').config(); 11 | 12 | 13 | 14 | app.use(express.json()); 15 | app.use(cors()); 16 | app.use(cors({ origin: '/questions' })); 17 | mongoose 18 | .connect( 19 | 'mongodb+srv://dbUserDarius:naF0xDjsXhAaOaHE@cluster0.pjxxpw9.mongodb.net/', 20 | { 21 | useUnifiedTopology: true, 22 | 23 | dbName: 'TriviaGameNight', 24 | } 25 | ) 26 | .then(() => console.log('Connected to Mongo DB.')) 27 | .catch((err) => console.log(err)); 28 | 29 | app.use(express.static(path.resolve(__dirname, '../build'))); 30 | 31 | app.get('/verifyJwt', authController.verifyJwt, (req, res) => { 32 | return res.status(200).json(res.locals.user) 33 | }) 34 | 35 | app.post('/log-in', userController.verifyUser, authController.setJwtToken, (req, res) => { 36 | return res.status(200).json(res.locals.secret) 37 | }) 38 | 39 | app.post('/sign-up', userController.addUser, authController.setJwtToken, (req, res) => { 40 | return res.status(200).json(res.locals.secret) 41 | }) 42 | 43 | app.delete('/delete', userController.deleteUser, (req, res) => { 44 | console.log('slafjks;') 45 | return res.status(200).json({}) 46 | }) 47 | 48 | app.get('/questions', questionsController.getQuestions, (req, res) => { 49 | console.log('Change', res.locals); 50 | // console.log('get questions hi', res.locals.questions); 51 | 52 | // // return res.status(200).send(JSON.stringify(res.locals.questions)); 53 | // return res.status(200).json(JSON.stringify(res.locals.questions)); 54 | return res.status(200).json(res.locals.questions); 55 | }); 56 | 57 | // catch-all route handler for any requests to an unknown route 58 | app.use((req, res) => 59 | res.status(404).send("This is not the page you're looking for...") 60 | ); 61 | 62 | app.use((err, req, res, next) => { 63 | const defaultErr = { 64 | log: 'Express error handler caught unknown error', 65 | status: 500, 66 | message: { err: 'An error occurred' }, 67 | }; 68 | const errorObj = Object.assign({}, defaultErr, err); 69 | console.log(errorObj.log); 70 | return res.status(errorObj.status).json(errorObj.message); 71 | }); 72 | 73 | app.listen(port, () => { 74 | console.log('App listening on port: ' + port); 75 | }); 76 | -------------------------------------------------------------------------------- /server/controllers/questionsController.js: -------------------------------------------------------------------------------- 1 | const Topic = require('../models/questionsModel'); 2 | 3 | const questionsController = {}; 4 | 5 | questionsController.getQuestions = (req, res, next) => { 6 | console.log('IM HERE!'); 7 | 8 | const categories = ['Sports', 'Film', 'Television', 'Music', 'Geography'] 9 | const questions = {}; 10 | console.log('here too') 11 | Promise.all(categories.map(async (category) => { 12 | try { 13 | const easy = await Topic.aggregate([{ $match: {category: category, difficulty: 'easy'} }, { $sample: { size: 2 } }]) 14 | const medium = await Topic.aggregate([{ $match: {category: category, difficulty: 'medium'} }, { $sample: { size: 2 } }]) 15 | const high = await Topic.aggregate([{ $match: {category: category, difficulty: 'hard'} }, { $sample: { size: 1 } }]) 16 | questions[category] = [...easy, ...medium, ...high]; 17 | console.log(questions) 18 | } catch (err) { 19 | return next({}) 20 | } 21 | })) 22 | .then(() => { 23 | res.locals.questions = questions; 24 | console.log(questions, res.locals, 'zzzzz') 25 | return next(); 26 | }) 27 | .catch((err) => console.error(err)) 28 | } 29 | 30 | // questionsController.getQuestions = (req, res, next) => { 31 | // console.log('IM HERE!'); 32 | // Topic.find({}) 33 | // .then((response) => { 34 | // // console.log(response, 'response'); 35 | // const questions = { 36 | // SPORTS: [], 37 | // FILM: [], 38 | // MUSIC: [], 39 | // TELEVISION: [], 40 | // GEOGRAPHY: [], 41 | // }; 42 | 43 | // response.forEach((question) => { 44 | // // console.log('11111', question); 45 | // if (question.category === 'Sports') { 46 | // questions.SPORTS.push(question); 47 | // } else if (question.category === 'Film') { 48 | // questions.FILM.push(question); 49 | // } else if (question.category === 'Music') { 50 | // questions.MUSIC.push(question); 51 | // } else if (question.category === 'Television') { 52 | // questions.TELEVISION.push(question); 53 | // } else if (question.category === 'Geography') { 54 | // questions.GEOGRAPHY.push(question); 55 | // } 56 | // }); 57 | 58 | 59 | // res.locals.questions = questions; 60 | // console.log('questions', questions); 61 | 62 | // return next(); 63 | // }) 64 | // .catch((err) => { 65 | // return next({ 66 | // log: 'ERROR in questionsController', 67 | // status: 404, 68 | // message: `ERROR in questionsController: ${err}`, 69 | // }); 70 | // }); 71 | // }; 72 | 73 | module.exports = questionsController; 74 | -------------------------------------------------------------------------------- /client/components/Signup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import '../Styles/LoginStyles.css'; 5 | 6 | export default function SignUp({ setLoggedIn }) { 7 | const [username, setUsername] = useState(''); 8 | const [password, setPassword] = useState(''); 9 | const navigate = useNavigate(); 10 | 11 | const handleSignUp = () => { 12 | console.log('Type Username here', username); 13 | console.log('Type Password here', password); 14 | console.log('Button has been clicked to login'); 15 | fetch('/log-in', { 16 | method: 'POST', 17 | headers: { 18 | 'Content-Type': 19 | 'application/json', 20 | }, 21 | body: JSON.stringify({ 22 | username, password 23 | }) 24 | }) 25 | .then(response => { 26 | if (response.ok) { 27 | response.json() 28 | .then(data => { 29 | localStorage.setItem('triviaJwtToken', data.jwtToken); 30 | setLoggedIn(true); 31 | return navigate('/'); 32 | }) 33 | .catch(err => console.error(err)) 34 | } 35 | }) 36 | .catch(err => console.error(err)) 37 | 38 | } 39 | 40 | const handleCreateAccount = () => { 41 | fetch('/sign-up', { 42 | method: 'POST', 43 | headers: { 44 | 'Content-Type': 45 | 'application/json' 46 | }, 47 | body: JSON.stringify({ 48 | username, password, location 49 | }) 50 | }) 51 | .then(res => { 52 | if (res.ok) { 53 | res.json() 54 | .then(data => { 55 | localStorage.setItem('triviaJwtToken', data.jwtToken); 56 | setLoggedIn(true); 57 | return navigate('/'); 58 | }) 59 | .catch(err => console.error(err)) 60 | } 61 | }) 62 | .catch(err => console.error(err)) 63 | } 64 | 65 | return ( 66 |
67 |

Goblin Sharks Trivia

68 |
69 | setUsername(e.target.value)} 75 | > 76 |
77 |
78 | setPassword(e.target.value)} 84 | > 85 |
86 | 87 | 88 |
89 |
90 |
91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /client/components/Quiz.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Routes, Route, useNavigate } from 'react-router-dom'; 3 | import QuestionCard from './QuestionCards'; 4 | import Question from './Question'; 5 | import '../Styles/Quiz.css'; // Import the CSS file for the Quiz component 6 | // import backgroundImage from '../assets/background.jpg' 7 | import Scoreboard from './Scoreboard' 8 | import WinCondition from './Wincondition' 9 | // import ResetQuiz from './ResetQuiz' 10 | 11 | const Quiz = ({user, setUser}) => { 12 | const [quizQuestions, setQuizQuestions] = useState([]); 13 | const [questionState, setQuestion] = useState({}); 14 | const [answeredQuestions, setAnsweredQuestions] = useState([]); 15 | const [score, setScore] = useState(0); 16 | const points = { easy: 1000, medium: 3000, hard: 5000 }; 17 | const navigate = useNavigate(); 18 | const [hasWon, setHasWon] = useState(false); 19 | const [newGame, setNewGame] = useState(false); 20 | 21 | const resetGame = () => { 22 | console.log('reset game') 23 | setQuizQuestions([]); 24 | setQuestion({}); 25 | setAnsweredQuestions([]); 26 | setScore(0); 27 | setHasWon(false) 28 | setNewGame(true) 29 | navigate('/') 30 | } 31 | 32 | useEffect(() => { 33 | if (score >= 10000) { 34 | setHasWon(true); 35 | navigate('/win') 36 | } 37 | }, [score]); 38 | 39 | // const { sports, film, geography, music, television} = quizQuestions; 40 | 41 | const handleQuestionClick = (question) => { 42 | setQuestion(question); 43 | setAnsweredQuestions((prev) => [...prev, question.question]) 44 | navigate('/card'); 45 | } 46 | 47 | 48 | const handleAnswerClick = (question, answer) => { 49 | 50 | if (question.correct_answer === answer) { 51 | setScore((prevScore) => prevScore + points[question.difficulty]); 52 | navigate('/'); 53 | } 54 | else { 55 | alert("Wrong answer!") 56 | navigate('/') 57 | } 58 | } 59 | 60 | const handleDeleteAccount = () => { 61 | fetch('/delete', { 62 | method: 'DELETE', 63 | headers: { 64 | 'Content-Type': 65 | 'application/json', 66 | }, 67 | body: JSON.stringify({ 68 | username: user.username 69 | }) 70 | }) 71 | .then(res => { 72 | console.log(`account for ${user.username} has been deleted`); 73 | localStorage.removeItem('triviaJwtToken'); 74 | setUser({}); 75 | navigate('/'); 76 | }) 77 | .catch(err => console.error(err)) 78 | } 79 | 80 | const handleLogOut = () => { 81 | localStorage.removeItem('triviaJwtToken'); 82 | setUser({}); 83 | navigate('/'); 84 | } 85 | 86 | useEffect(() => { 87 | fetch('/questions') 88 | .then(response => response.json()) 89 | .then(data => { 90 | setQuizQuestions(data) 91 | setNewGame(false) 92 | }) 93 | .catch(error => { 94 | console.error('Error fetching quiz questions:', error); 95 | }) 96 | }, [newGame]); 97 | 98 | if (!Object.keys(quizQuestions).length) return; 99 | 100 | return ( 101 |
102 |
103 |

WELCOME, {user.username.toUpperCase()}

104 |
105 | 106 | 107 |
108 |
109 |
110 | 114 | 115 | 116 | 119 | {Object.keys(quizQuestions).map((category, i) => ( 120 |
121 |
122 | {category} 123 |
124 | {quizQuestions[category].map((question,i) => ( 125 | (!answeredQuestions.includes(question.question) && ) ||
126 | )) 127 | } 128 |
129 | ))} 130 |
131 | } /> 132 | 133 | } /> 134 | } /> 135 | 136 | 137 | 138 | 139 | ); 140 | }; 141 | 142 | 143 | export default Quiz; 144 | --------------------------------------------------------------------------------