├── .vscode ├── settings.json └── launch.json ├── .gitignore ├── .DS_Store ├── uploads ├── 1 │ └── dogsex.jpg ├── 2 │ ├── img.png │ └── dogsex.jpg └── 8 │ ├── Aryeh.jpg │ ├── Screen Shot 2020-08-15 at 9.04.09 PM.png │ └── Screen Shot 2020-08-15 at 9.04.03 PM (2).png ├── client ├── App.jsx ├── components │ ├── AddSearchEvent.jsx │ ├── Profile.jsx │ ├── Navbar.jsx │ ├── EventAttendees.jsx │ ├── EventsFeed.jsx │ ├── Event.jsx │ ├── SearchEvent.jsx │ ├── MainContainer.jsx │ ├── CreateEvent.jsx │ └── Content.jsx ├── index.html ├── index.js └── stylesheets │ └── styles.scss ├── .babelrc ├── .env ├── server ├── models │ └── models.js ├── controllers │ ├── cookieController.js │ ├── loginController.js │ ├── contentController.js │ ├── fileController.js │ └── eventController.js ├── routers │ ├── events.js │ ├── content.js │ └── api.js ├── server.js └── utils │ └── queries.js ├── webpack.config.js ├── scratch-project_postgres_create.sql ├── README.md └── package.json /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | 123,125 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | package-lock.json 3 | node_modules 4 | .env 5 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Team-Velocirabbits/storyboard-cache/HEAD/.DS_Store -------------------------------------------------------------------------------- /uploads/2/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Team-Velocirabbits/storyboard-cache/HEAD/uploads/2/img.png -------------------------------------------------------------------------------- /uploads/1/dogsex.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Team-Velocirabbits/storyboard-cache/HEAD/uploads/1/dogsex.jpg -------------------------------------------------------------------------------- /uploads/2/dogsex.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Team-Velocirabbits/storyboard-cache/HEAD/uploads/2/dogsex.jpg -------------------------------------------------------------------------------- /uploads/8/Aryeh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Team-Velocirabbits/storyboard-cache/HEAD/uploads/8/Aryeh.jpg -------------------------------------------------------------------------------- /uploads/8/Screen Shot 2020-08-15 at 9.04.09 PM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Team-Velocirabbits/storyboard-cache/HEAD/uploads/8/Screen Shot 2020-08-15 at 9.04.09 PM.png -------------------------------------------------------------------------------- /uploads/8/Screen Shot 2020-08-15 at 9.04.03 PM (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Team-Velocirabbits/storyboard-cache/HEAD/uploads/8/Screen Shot 2020-08-15 at 9.04.03 PM (2).png -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import MainContainer from "./components/MainContainer.jsx" 3 | 4 | export default function App () { 5 | return < MainContainer /> 6 | }; -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react"], 3 | "plugins": [ 4 | [ 5 | "@babel/plugin-proposal-class-properties", 6 | { 7 | "loose": true 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PG_URI=postgres://qhmwxatz:ybXHqCQGylKRB0K-2LgKbCAj1dL2LRDO@raja.db.elephantsql.com:5432/qhmwxatz 2 | CLIENT_ID=942116613855-ctqekeujhe5j7t1pu7rbmm8sv2kl2t3i.apps.googleusercontent.com 3 | CLIENT_SECRET=QREJbz1YLUVaOsaCJuTOt6r7 4 | REACT_APP_PLACES_API=AIzaSyBocV_s8PP94rcQYj51LXNbP957tHl9kxo -------------------------------------------------------------------------------- /server/models/models.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | const pool = new Pool({ 4 | connectionString: process.env.PG_URI, 5 | }); 6 | 7 | module.exports = { 8 | query: (text, params, callback) => { 9 | console.log('executed query', text); 10 | return pool.query(text, params, callback); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /client/components/AddSearchEvent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CreateEvent from './CreateEvent.jsx'; 3 | import SearchEvent from './SearchEvent.jsx'; 4 | 5 | export default function AddSearchEvent(props) { 6 | return ( 7 |
8 | 9 | 10 |
11 | ) 12 | } 13 | 14 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Storyboard Cache 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.css'; 2 | 3 | import React from 'react'; 4 | import { render } from 'react-dom'; 5 | import { BrowserRouter } from 'react-router-dom'; 6 | 7 | import App from './App.jsx'; 8 | import styles from './stylesheets/styles.scss'; 9 | 10 | // import Sample from './Sample.jsx'; 11 | 12 | render( 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/server/routers/api.js", 15 | "outFiles": [ 16 | "${workspaceFolder}/**/*.js" 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /client/components/Profile.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Card} from 'react-bootstrap'; 3 | 4 | export default function Profile(props) { 5 | return ( 6 |
7 | {/*

Profile

*/} 8 | {/* */} 9 | 10 | 11 | 12 | {props.username} 13 | 14 | Hi, my name is {props.firstname} {props.lastname}! 15 | 16 | 17 | {/* */} 18 |
19 | ); 20 | } -------------------------------------------------------------------------------- /client/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { Navbar, Nav, Button } from 'react-bootstrap'; 2 | import React from 'react'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 4 | import { faGoogle } from '@fortawesome/free-brands-svg-icons' 5 | import { faFeatherAlt } from '@fortawesome/free-solid-svg-icons' 6 | 7 | export default function Notnav() { 8 | return ( 9 | 10 | Social Scrapbook 11 | 14 | 15 | ) 16 | } -------------------------------------------------------------------------------- /server/controllers/cookieController.js: -------------------------------------------------------------------------------- 1 | const cookieController = {}; 2 | //set new cookie from JWT 3 | cookieController.setSSIDCookie = (req, res, next) => { 4 | res.cookie('user', res.locals.token, { httpOnly: true }); 5 | return next(); 6 | }; 7 | //check if user is logged in (has cookie) - if not return 401 error 8 | cookieController.isLoggedIn = (req, res, next) => { 9 | if (req.cookies.user) { 10 | return next(); 11 | } else { 12 | return next({ 13 | log: `User is not logged in`, 14 | code: 401, 15 | message: { err: 'User is not logged in.' }, 16 | }); 17 | } 18 | }; 19 | //cookie removal 20 | cookieController.removeCookie = (req, res, next) => { 21 | res.clearCookie('user'); 22 | return next(); 23 | }; 24 | 25 | module.exports = cookieController; 26 | -------------------------------------------------------------------------------- /server/routers/events.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | // const path = require('path'); 4 | const fileController = require('../controllers/fileController'); 5 | const cookieController = require('../controllers/cookieController'); 6 | const eventController = require('../controllers/eventController'); 7 | 8 | router.get('/', 9 | eventController.allEvents, 10 | (req, res) => res.status(200).json(res.locals.allEventsInfo)); 11 | 12 | router.delete('/:eventid', 13 | cookieController.isLoggedIn, 14 | fileController.userCanModifyEvent, 15 | eventController.deleteEvent, 16 | (req, res) => res.status(200).json({})); 17 | 18 | router.put('/:eventid', 19 | cookieController.isLoggedIn, 20 | fileController.userCanModifyEvent, 21 | eventController.updateEvent, 22 | (req, res) => res.status(200).json({})); 23 | 24 | module.exports = router; 25 | -------------------------------------------------------------------------------- /client/components/EventAttendees.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Image, Col, Row } from 'react-bootstrap'; 3 | 4 | export default function EventAttendees({ attendees, userUpdate }) { 5 | //creates attendee component for each attendee in list 6 | let attendeesList = []; 7 | if (attendees) { 8 | attendeesList = attendees.map((attendee, index) => { 9 | return ( 10 |
11 |
12 | { userUpdate(attendee.username) }} /> 13 |
14 |

{attendee.firstname} {attendee.lastname}

15 |
16 | ) 17 | }); 18 | } 19 | 20 | return ( 21 |
22 |
Attendees:
23 |
24 | {attendeesList} 25 |
26 |
27 | ); 28 | } -------------------------------------------------------------------------------- /client/components/EventsFeed.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Event from './Event.jsx'; 3 | import { faImages } from '@fortawesome/free-solid-svg-icons'; 4 | 5 | export default function EventsFeed(props) { 6 | let events = []; 7 | //creates events for each event in feed 8 | if (props.events && Object.keys(props.events).length > 0) { 9 | events = props.events.map((event, index) => { 10 | const images = event.content.filter(cont => cont.content[0] === '/' || cont.content.includes('http')).map(cont => cont.content); 11 | return ( 12 | 24 | ); 25 | }); 26 | } 27 | return
{events}
; 28 | } 29 | -------------------------------------------------------------------------------- /server/routers/content.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const router = express.Router(); 4 | const path = require('path'); 5 | const fileController = require('../controllers/fileController'); 6 | const contentController = require('../controllers/contentController'); 7 | const cookieController = require('../controllers/cookieController'); 8 | 9 | router.post( 10 | '/', 11 | fileController.verifyUser, 12 | fileController.getUser, 13 | contentController.createContent, 14 | (req, res) => { 15 | return res.status(200).json(res.locals.createdContent); 16 | } 17 | ); 18 | 19 | router.put( 20 | '/:contentid', 21 | cookieController.isLoggedIn, 22 | fileController.userCanModifyContent, 23 | contentController.updateContent, 24 | (req, res) => res.status(200).json('Post succcessfully updated.') 25 | ); 26 | 27 | router.delete( 28 | '/:contentid', 29 | cookieController.isLoggedIn, 30 | fileController.userCanModifyContent, 31 | contentController.deleteContent, 32 | (req, res) => res.status(200).json('Post succcessfully deleted.') 33 | ); 34 | 35 | module.exports = router; 36 | -------------------------------------------------------------------------------- /server/controllers/loginController.js: -------------------------------------------------------------------------------- 1 | const { google } = require('googleapis'); 2 | 3 | const loginController = {}; 4 | 5 | loginController.oAuth = async (req, res, next) => { 6 | const oauth2Client = new google.auth.OAuth2( 7 | process.env.CLIENT_ID, 8 | process.env.CLIENT_SECRET, 9 | 'http://localhost:3000/api/login/google' 10 | ); 11 | 12 | const scopes = [ 13 | 'https://www.googleapis.com/auth/userinfo.profile', 14 | 'https://www.googleapis.com/auth/classroom.profile.photos', 15 | 'https://www.googleapis.com/auth/userinfo.email', 16 | ]; 17 | 18 | const url = oauth2Client.generateAuthUrl({ 19 | access_type: 'offline', 20 | scope: scopes, 21 | response_type: 'code', 22 | prompt: 'consent', 23 | }); 24 | 25 | res.locals.url = url; 26 | return next(); 27 | }; 28 | //creates Oauth token 29 | loginController.afterConsent = (req, res, next) => { 30 | const oauth2Client = new google.auth.OAuth2( 31 | process.env.CLIENT_ID, 32 | process.env.CLIENT_SECRET, 33 | 'http://localhost:3000/api/login/google' 34 | ); 35 | 36 | oauth2Client 37 | .getToken(req.query.code) 38 | .then((data) => { 39 | const { tokens } = data; 40 | oauth2Client.setCredentials(tokens); 41 | res.locals.token = tokens.id_token; 42 | return next(); 43 | }) 44 | .catch((err) => { 45 | if (err) console.log('afterConsent .catch block: ', err); 46 | }); 47 | }; 48 | 49 | module.exports = loginController; 50 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const cookieParser = require('cookie-parser'); 4 | 5 | // added for multimedia handling 6 | const fileUpload = require('express-fileupload'); 7 | const cors = require('cors'); 8 | const bodyParser = require('body-parser'); 9 | const morgan = require('morgan'); 10 | const _ = require('lodash'); 11 | // multimedia handling above 12 | 13 | const app = express(); 14 | const apiRouter = require('./routers/api'); 15 | const contentRouter = require('./routers/content'); 16 | 17 | // enable files upload 18 | app.use(fileUpload({ 19 | createParentPath: true, 20 | })); 21 | 22 | // PARSERS AND MULTIMEDIA HANDLERS 23 | app.use(cors()); 24 | // app.use(bodyParser.json()); 25 | // app.use(bodyParser.urlencoded({extended: true})); 26 | app.use(morgan('dev')); 27 | 28 | // BODY PARSERS & COOKIE PARSER 29 | app.use(express.json()); 30 | app.use(express.urlencoded({ extended: true })); 31 | app.use(cookieParser()); 32 | 33 | // SERVE UP STATIC FILES 34 | app.use('/', express.static(path.join(__dirname, '../dist'))); 35 | app.use('/uploads', express.static(path.join(__dirname, '../uploads'))); 36 | 37 | // SERVE INDEX.HTML ON THE ROUTE '/' 38 | app.get('/', (req, res) => { 39 | res.sendFile(path.join(__dirname, '../dist/index.html')); 40 | }); 41 | 42 | // API ROUTER 43 | app.use('/api', apiRouter); 44 | 45 | // CONTENT ROUTER 46 | app.use('/content', contentRouter); 47 | 48 | // HANDLING UNKNOWN URLS 49 | app.use('*', (req, res) => { 50 | res.status(404).send('URL path not found'); 51 | }); 52 | 53 | // ERROR HANDLER 54 | app.use((err, req, res, next) => { 55 | res.status(401).send(err.message); // WHAT IS FRONT-END EXPECTING? JSON OR STRING? 56 | }); 57 | 58 | // app.listen(3000); //listens on port 3000 -> http://localhost:3000/ 59 | app.listen(process.env.PORT || 3000); 60 | module.exports = app; 61 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const Dotenv = require('dotenv-webpack'); 5 | 6 | module.exports = { 7 | entry: './client/index.js', 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: 'bundle.js', 11 | }, 12 | devtool: 'eval-source-map', 13 | mode: process.env.NODE_ENV, 14 | module: { 15 | rules: [ 16 | { 17 | test: /.(css|scss)$/, 18 | // include: [path.resolve(__dirname, '/node_modules/react-datepicker/'), path.resolve(__dirname, '/node_modules/bootstrap/')], 19 | // exclude: /node_modules/, 20 | use: ['style-loader', 'css-loader', 'sass-loader'], 21 | }, 22 | { 23 | test: /.jsx?$/, 24 | exclude: /node_modules/, 25 | use: { 26 | loader: 'babel-loader', 27 | options: { 28 | presets: ['@babel/preset-env', '@babel/preset-react'], 29 | }, 30 | }, 31 | }, 32 | { 33 | test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/, 34 | use: [ 35 | { 36 | // loads files as base64 encoded data url if image file is less than set limit 37 | loader: 'url-loader', 38 | options: { 39 | // if file is greater than the limit (bytes), file-loader is used as fallback 40 | limit: 8192, 41 | }, 42 | }, 43 | ], 44 | }, 45 | ], 46 | }, 47 | devServer: { 48 | port: 8080, 49 | // contentBase: path.resolve(__dirname, '/dist'), 50 | // publicPath: '/dist/', 51 | proxy: { 52 | '/': { 53 | target: 'http://localhost:3000', 54 | secure: false, 55 | }, 56 | }, 57 | hot: true, 58 | }, 59 | plugins: [ 60 | new HtmlWebpackPlugin({ 61 | template: './client/index.html', 62 | }), 63 | new MiniCssExtractPlugin(), 64 | new Dotenv(), 65 | ], 66 | node: { 67 | fs: 'empty', 68 | http2: 'empty', 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /scratch-project_postgres_create.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users 2 | ( 3 | "userid" serial PRIMARY KEY, 4 | "username" varchar NOT NULL CHECK ( username <> ''), 5 | "firstname" varchar NOT NULL CHECK ( firstname <> ''), 6 | "lastname" varchar NOT NULL CHECK ( lastname <> ''), 7 | "profilephoto" varchar NOT NULL, 8 | UNIQUE ( username ) 9 | ); 10 | 11 | SELECT setval('users_userid_seq', 1, false); 12 | 13 | CREATE TABLE events 14 | ( 15 | "eventid" SERIAL PRIMARY KEY, 16 | "eventtitle" varchar NOT NULL CHECK ( eventtitle <> ''), 17 | "eventdate" date NOT NULL, 18 | "eventstarttime" time NOT NULL, 19 | "eventendtime" time NOT NULL, 20 | "eventlocation" varchar NOT NULL CHECK ( eventlocation <> ''), 21 | "eventdetails" varchar NOT NULL CHECK ( eventdetails <> ''), 22 | "eventownerid" bigint NOT NULL, 23 | "eventownerusername" varchar NOT NULL, 24 | "eventmessages" varchar ARRAY, 25 | UNIQUE 26 | ( eventtitle ), 27 | FOREIGN KEY 28 | (eventownerid) REFERENCES users 29 | (userid), 30 | FOREIGN KEY 31 | (eventownerusername) REFERENCES users 32 | (username) 33 | ); 34 | 35 | SELECT setval('events_eventid_seq', 1, false); 36 | 37 | CREATE TABLE usersandevents 38 | ( 39 | "uselessid" serial PRIMARY KEY, 40 | "userid" bigint NOT NULL, 41 | "username" varchar NOT NULL, 42 | "eventid" bigint NOT NULL, 43 | "eventtitle" varchar NOT NULL, 44 | "eventdate" varchar NOT NULL, 45 | "eventstarttime" varchar NOT NULL, 46 | "eventendtime" varchar NOT NULL, 47 | "eventdetails" varchar NOT NULL, 48 | "eventlocation" varchar NOT NULL, 49 | UNIQUE (username, eventtitle), 50 | FOREIGN KEY ( userid ) REFERENCES users ( userid ), 51 | FOREIGN KEY ( eventid ) REFERENCES events ( eventid ) 52 | ); 53 | 54 | SELECT setval('usersandevents_uselessid_seq', 1, false); 55 | 56 | CREATE TABLE content 57 | ( 58 | "contentid" serial PRIMARY KEY, 59 | "userid" bigint NOT NULL, 60 | "eventid" bigint NOT NULL, 61 | "content" varchar NOT NULL, 62 | "contentdate" varchar NOT NULL, 63 | "contenttime" varchar NOT NULL, 64 | FOREIGN KEY ( userid ) REFERENCES users ( userid ), 65 | FOREIGN KEY ( eventid ) REFERENCES events ( eventid ) 66 | ); 67 | 68 | SELECT setval('content_contentid_seq', 1, false); -------------------------------------------------------------------------------- /server/routers/api.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const path = require('path'); 4 | const fileController = require('../controllers/fileController'); 5 | const cookieController = require('../controllers/cookieController'); 6 | const eventController = require('../controllers/eventController'); 7 | const loginController = require('../controllers/loginController'); 8 | const eventsRouter = require('./events'); 9 | 10 | // EXISING USER LOGIN 11 | 12 | router.get('/login', 13 | loginController.oAuth, 14 | (req, res) => { 15 | // res.send('ok'); 16 | return res.redirect(res.locals.url) 17 | }); 18 | 19 | router.get('/login/google', 20 | loginController.afterConsent, 21 | cookieController.setSSIDCookie, 22 | fileController.createUser, // if username already exists, return next() => getUser // if not, create user in SQL database 23 | (req, res) => { 24 | return res.redirect('/') //WAS "http://localhost:8080/" 25 | }); 26 | 27 | // REVISIT WEBSITE AFTER LEAVING, OR VISITING SOMEONE ELSE'S PROFILE PAGE 28 | router.get('/info', 29 | cookieController.isLoggedIn, // this is really only is applicable for the same user 30 | fileController.getUser, 31 | eventController.allEvents, 32 | eventController.filterForUser, 33 | (req, res) => { 34 | const responseObj = { 35 | users: res.locals.allUserInfo, 36 | events: res.locals.allEventsInfo, 37 | }; 38 | return res.status(200).json(responseObj); 39 | }); 40 | 41 | // LOGGING OUT 42 | router.use('/logout', // SWITCH THIS TO POST REQUEST!! 43 | cookieController.removeCookie, 44 | (req, res) => { 45 | return res.status(200).json('Successful logout.'); 46 | }); 47 | 48 | // CREATE A NEW EVENT 49 | router.post('/create', 50 | fileController.verifyUser, 51 | fileController.getUser, 52 | eventController.createEvent, 53 | eventController.addNewEventToJoinTable, 54 | (req, res) => { 55 | return res.status(200).json('Event succcessfully created.'); 56 | }); 57 | 58 | // ADD USER TO AN EXISTING EVENT 59 | router.post('/add', 60 | fileController.getUser, 61 | eventController.verifyAttendee, 62 | eventController.addAttendee, 63 | (req, res) => { 64 | return res.status(200).json('User successfully added as attendee.'); 65 | }); 66 | 67 | router.use('/events', eventsRouter); 68 | 69 | module.exports = router; 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SOCIAL SCRAPBOOK 2 | 3 | *** SETUP / INSTALLATION *** 4 | 1. Install dependencies 5 | [ ] Install your npm dependencies: run npm install in your terminal 6 | Start application (see note below) 7 | [ ] To start your node server and compile the boilerplate React application, run the following: npm run dev-mac OR 8 | npm run dev-win 9 | [ ] To 'access' your React application in the browser, visit: http://localhost:8080/ 10 | 11 | 2. Note: while the React app runs on http://localhost:8080, our server is going to be running on http://localhost:3000 so if you are planning to test with Postman instead of (or in addition to) using the React app, send your Postman requests to http://localhost:3000. 12 | 13 | 3. *** PLEASE NOTE *** THE DEV-SERVER MUST RUN ON LOCALHOST:8080. This is hard-coded into our webpack-config on line 47 if you'd like to change that and also hard-coded into api.js on line 29. Because the dev-server is forced to run on localhost:8080 at the moment, if you have a Live Share session open or any other app that runs on localhost:8080 before you run the dev-server, IT WILL GLITCH OUT. 14 | 15 | *** SETTING UP OATH *** 16 | To start the project, you will have to set up your google cloud platform. 17 | 18 | 1. Please go to https://console.cloud.google.com/ 19 | 2. On the left tap, please click APIs & Services 20 | 3. Click Credentials 21 | 4. You will see + Create Credentials 22 | 5. Click OAuth ClientID 23 | 6. Click Web Application 24 | 7. Name w/e you want 25 | 8. Authorized Javascript Origins: http://localhost:3000 26 | 9. Authorized Redirect URIs: http://localhost:3000/api/login/google 27 | 10. Save it! 28 | 29 | *** SAVE IT! *** 30 | 31 | 1. Go to OAuth consent screen 32 | 2. Set up your name 33 | 3. Scopes for Google APIs should have 34 | - email 35 | - profile 36 | - openid 37 | 4. save it! 38 | *** SAVE IT! *** 39 | 40 | *** Now go to server/controllers/loginController.js *** 41 | and change your client id and client secret in two places in line 7 and 32 42 | const oauth2Client = new google.auth.OAuth2( 43 | 'Client_ID', 44 | 'Client_Secret', 45 | 'http://localhost:3000/api/login/google' 46 | ); 47 | 48 | *** SETTING UP THE DATABASE *** 49 | 1. Set up an ElephantSQL database in the cloud and create a new instance. Copy 50 | 51 | 2. Set up your database connection 52 | Add your connection URL to the ElephantSQL database into PG_URI in models.js (line 3) - you will be using this connection string to connect to the database via a pool. 53 | 54 | 3. Run the following command in your terminal to create empty tables in the database (omit the carrots). This will create 3 empty tables, as specified in the file: 55 | psql -d -f scratch-project_postgres_create.sql 56 | 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scratch-project", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production node -r dotenv/config server/server.js --watch", 8 | "build": "NODE_ENV=production webpack", 9 | "dev-win": "concurrently \"cross-env NODE_ENV=development webpack-dev-server --open ./\" \"nodemon ./server/server.js\"", 10 | "dev-mac": "NODE_ENV=development webpack-dev-server --open --hot & nodemon -r dotenv/config server/server.js --watch", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Tasseled-Wobbegong/Scratch-project.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/Tasseled-Wobbegong/Scratch-project/issues" 21 | }, 22 | "homepage": "https://github.com/Tasseled-Wobbegong/Scratch-project#readme", 23 | "devDependencies": { 24 | "@babel/core": "^7.11.1", 25 | "@babel/preset-env": "^7.11.0", 26 | "@babel/preset-react": "^7.10.4", 27 | "babel-loader": "^8.1.0", 28 | "concurrently": "^5.3.0", 29 | "cross-env": "^7.0.2", 30 | "css-loader": "^4.2.1", 31 | "file-loader": "^6.0.0", 32 | "file-system": "^2.2.2", 33 | "html-webpack-plugin": "^4.3.0", 34 | "node-sass": "^4.14.1", 35 | "nodemon": "^2.0.4", 36 | "sass": "^1.26.10", 37 | "sass-loader": "^9.0.3", 38 | "style-loader": "^1.2.1", 39 | "url-loader": "^4.1.0", 40 | "webpack": "^4.44.1", 41 | "webpack-cli": "^3.3.12", 42 | "webpack-dev-server": "^3.11.0", 43 | "webpack-hot-middleware": "^2.25.0" 44 | }, 45 | "dependencies": { 46 | "@fortawesome/fontawesome-svg-core": "^1.2.30", 47 | "@fortawesome/free-brands-svg-icons": "^5.14.0", 48 | "@fortawesome/free-solid-svg-icons": "^5.14.0", 49 | "@fortawesome/react-fontawesome": "^0.1.11", 50 | "axios": "^0.19.2", 51 | "bcrypt": "^5.0.0", 52 | "bcryptjs": "^2.4.3", 53 | "body-parser": "^1.19.0", 54 | "bootstrap": "^4.5.2", 55 | "cookie-parser": "^1.4.5", 56 | "cors": "^2.8.5", 57 | "dotenv": "^8.2.0", 58 | "dotenv-webpack": "^2.0.0", 59 | "express": "^4.17.1", 60 | "express-fileupload": "^1.2.0", 61 | "form-data": "^3.0.0", 62 | "googleapis": "^59.0.0", 63 | "jsonwebtoken": "^8.5.1", 64 | "jwt-decode": "^2.2.0", 65 | "lodash": "^4.17.20", 66 | "mini-css-extract-plugin": "^0.10.0", 67 | "morgan": "^1.10.0", 68 | "node-fetch": "^2.6.0", 69 | "pg": "^8.3.0", 70 | "react": "^16.13.1", 71 | "react-bootstrap": "^1.3.0", 72 | "react-datetime-picker": "^3.0.2", 73 | "react-dom": "^16.13.1", 74 | "react-google-location": "^1.2.2", 75 | "react-hot-loader": "^4.12.21", 76 | "react-router": "^5.2.0", 77 | "react-router-dom": "^5.2.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /client/components/Event.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import EventAttendees from './EventAttendees.jsx'; 3 | import Content from './Content.jsx'; 4 | import { ListGroup, Container, Row, Jumbotron, Button, Carousel } from 'react-bootstrap'; 5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 6 | import { faLocationArrow } from '@fortawesome/free-solid-svg-icons'; 7 | import CreateEvent from './CreateEvent.jsx'; 8 | import axios from 'axios'; 9 | 10 | export default function Event(props) { 11 | let eventOwner = props.events[props.id].eventownerusername; 12 | let currentUser = props.user.username; 13 | 14 | if (props.images.length === 0) props.images = ["https://images.unsplash.com/photo-1594284487150-54d64729129c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80"] //default image if there are none in the feed 15 | 16 | const slides = props.images.map(img => { 17 | return ( 18 | 22 | 23 |

{props.eventtitle}

24 |

25 | {props.eventdate} - {props.starttime} 26 |

27 |

28 | Location : 29 | {props.eventlocation} 30 |

31 |

{props.eventdetails}

32 |
33 |
) 34 | }) 35 | 36 | 37 | return ( 38 | <> 39 | 40 |
41 | 42 | {eventOwner === currentUser && ( 43 |
44 | 49 | 50 | 68 |
69 | )} 70 | 71 | {slides} 72 | 73 | 74 | 75 | 76 | 77 |
78 |
79 | 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /client/components/SearchEvent.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | Link 7 | } from "react-router-dom"; 8 | 9 | import DateTimePicker from 'react-datetime-picker'; 10 | 11 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 12 | import { faSearchPlus } from '@fortawesome/free-solid-svg-icons' 13 | import { Modal, Button, Form, Card } from 'react-bootstrap'; 14 | import axios from 'axios'; 15 | 16 | export default function SearchEvent({ searchEvent, events }) { 17 | /* Form data */ 18 | const initialFormData = Object.freeze({ 19 | title: "", 20 | description: "" 21 | }); 22 | 23 | const [formData, updateFormData] = React.useState(initialFormData); 24 | const [results, updateResults] = useState([]); 25 | const [show, setShow] = useState(false); 26 | 27 | let exampleEventData; 28 | 29 | //pulls list of all events from DB 30 | useEffect(() => { 31 | axios.get('/api/events') 32 | .then(res => { 33 | exampleEventData = res.data; 34 | }) 35 | }); 36 | //filters list of events as the user types in 37 | const handleChange = (e) => { 38 | const regex = new RegExp(e.target.value.trim(), "gi"); 39 | const eventTitles = events.map(event => event.eventtitle) 40 | updateResults(exampleEventData.filter((event) => event.eventtitle.match(regex) && !eventTitles.includes(event.eventtitle))) 41 | }; 42 | //pass the added search event back to the main container 43 | const handleSubmit = (e, event) => { 44 | e.preventDefault() 45 | searchEvent(event) 46 | handleClose(); 47 | }; 48 | 49 | const handleClose = () => setShow(false); 50 | const handleShow = () => setShow(true); 51 | 52 | //generates a list of events on load using fetch 53 | const btnResults = results.map(event => { 54 | return ( 55 | 56 | ); 57 | }) 58 | 59 | 60 | 61 | return ( 62 |
63 |
64 | 65 |

Search Events

66 |
67 | 68 | 69 | 70 | Search for an Event 71 | 72 | 73 | 74 |
75 | 76 | Please enter the event name below. 77 | 78 | 79 |
80 | {btnResults} 81 | 82 |
83 | 84 |
85 |
86 |
87 |
88 | ); 89 | } -------------------------------------------------------------------------------- /server/controllers/contentController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/models'); 2 | const queries = require('../utils/queries'); 3 | const path = require('path'); 4 | const contentController = {}; 5 | 6 | const express = require('express'); 7 | const app = express(); 8 | 9 | // added for multimedia handling 10 | const fileUpload = require('express-fileupload'); 11 | const cors = require('cors'); 12 | const morgan = require('morgan'); 13 | const _ = require('lodash'); 14 | 15 | // enable files upload 16 | app.use( 17 | fileUpload({ 18 | createParentPath: true, 19 | }) 20 | ); 21 | 22 | //add other middleware 23 | app.use(cors()); 24 | app.use(morgan('dev')); 25 | 26 | contentController.createContent = (req, res, next) => { 27 | const { userid } = res.locals.allUserInfo; 28 | const { eventid } = req.body; 29 | let content; 30 | 31 | // define new date/time 32 | const now = new Date(); 33 | const contentdate = now.toDateString(); 34 | const contenttime = now.toTimeString().split(' ')[0]; 35 | 36 | try { 37 | if (!req.files) { 38 | content = req.body.content; 39 | } else { 40 | //Use the name of the input field (i.e. "avatar") to retrieve the uploaded file 41 | const file = req.files.image; 42 | 43 | //Use the mv() method to place the file in upload directory (i.e. "uploads") 44 | file.mv(`./uploads/${userid}/${file.name}`); 45 | 46 | //save file url to content 47 | content = path.resolve('/uploads/' + userid + '/' + file.name); 48 | } 49 | const queryString = queries.createContent; 50 | const queryValues = [userid, eventid, content, contentdate, contenttime]; 51 | console.log('QueryValues:', queryValues); 52 | db.query(queryString, queryValues) 53 | .then((data) => { 54 | res.locals.createdContent = data.rows[0]; 55 | return next(); 56 | }) 57 | .catch((err) => { 58 | return next({ 59 | log: `Error occurred with queries.createContent OR contentController.createCotent middleware: ${err}`, 60 | message: { 61 | err: 'An error occured with SQL when creating content.', 62 | }, 63 | }); 64 | }); 65 | } catch (err) { 66 | res.status(500).send(err); 67 | } 68 | }; 69 | 70 | contentController.updateContent = (req, res, next) => { 71 | const { contentid } = req.params; 72 | const { content } = req.body; 73 | const queryValues = [contentid, content]; 74 | 75 | db.query(queries.updateContent, queryValues) 76 | .then((resp) => { 77 | return next(); 78 | }) 79 | .catch((err) => 80 | next({ 81 | log: `Error occurred with contentController.deleteContent middleware: ${err}`, 82 | message: { 83 | err: 'An error occured with SQL when deleting content information.', 84 | }, 85 | }) 86 | ); 87 | }; 88 | 89 | contentController.deleteContent = (req, res, next) => { 90 | const { contentid } = req.params; 91 | 92 | db.query(queries.deleteContent, [contentid]) 93 | .then((resp) => { 94 | return next(); 95 | }) 96 | .catch((err) => 97 | next({ 98 | log: `Error occurred with contentController.deleteContent middleware: ${err}`, 99 | message: { 100 | err: 'An error occured with SQL when deleting content information.', 101 | }, 102 | }) 103 | ); 104 | }; 105 | 106 | module.exports = contentController; 107 | -------------------------------------------------------------------------------- /server/utils/queries.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/models.js'); // remove after testing 2 | 3 | const queries = {}; 4 | 5 | // GET ALL EVENTS 6 | queries.getAllEvents = ` 7 | SELECT * FROM events 8 | ORDER BY eventdate, eventstarttime 9 | `; 10 | 11 | queries.getAttendeeEvents = ` 12 | SELECT u.*, ue.eventid, ue.eventdate, ue.eventstarttime 13 | FROM usersandevents ue 14 | JOIN users u 15 | ON u.userid = ue.userid 16 | ORDER BY ue.eventdate 17 | `; 18 | 19 | // GET USER'S EVENTS 20 | queries.userEvents = ` 21 | SELECT * FROM usersandevents WHERE userid=$1 ORDER BY usersandevents.uselessid 22 | `; 23 | 24 | // GET ALL USER'S PERSONAL INFO 25 | queries.userInfo = 'SELECT * FROM users WHERE username=$1'; // const values = [req.query.id] 26 | 27 | // QUERY TO ADD USER 28 | queries.addUser = ` 29 | INSERT INTO users 30 | (username, firstname, lastname, profilephoto) 31 | VALUES($1, $2, $3, $4) 32 | RETURNING username 33 | ; 34 | `; 35 | 36 | // QUERY FOR WHEN USER CREATES EVENT 37 | queries.createEvent = ` 38 | INSERT INTO events 39 | (eventtitle, eventdate, eventstarttime, eventendtime, eventlocation, eventdetails, eventownerid, eventownerusername, eventmessages) 40 | VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9) 41 | RETURNING eventid 42 | ; 43 | `; 44 | 45 | // query for when creater deletes event 46 | queries.deleteEvent = ` 47 | DELETE FROM events 48 | WHERE eventid=$1 49 | `; 50 | 51 | // QUERY FOR DELETING EVENT ON USER EVENT TABLE 52 | queries.deleteUserEvents = ` 53 | DELETE FROM usersandevents 54 | WHERE eventid=$1 55 | `; 56 | 57 | // QUERY FOR WHEN USER UPDATES EVENTS 58 | queries.updateEvent = ` 59 | UPDATE events 60 | SET eventtitle=$2,eventdate= $3, eventstarttime=$4, eventendtime= $5, eventlocation= $6, eventdetails=$7 61 | WHERE eventid=$1 62 | `; 63 | 64 | // QUERY FOR WHEN USER TRIES TO MODIFY OR DELETE EVENT 65 | queries.checkEventOwner = ` 66 | SELECT eventownerusername 67 | FROM events 68 | WHERE eventid=$1 69 | `; 70 | 71 | // ADDS ALL CURRENT EVENTS TO USERSANDEVENTS 72 | queries.addNewEventToJoinTable = ` 73 | INSERT INTO usersandevents (userid, username, eventid, eventtitle, eventdate, eventstarttime, eventendtime, eventdetails, eventlocation) 74 | SELECT eventownerid, eventownerusername, eventid, eventtitle, eventdate, eventstarttime, eventendtime, eventdetails, eventlocation FROM events 75 | WHERE eventid=$1 76 | RETURNING usersandevents; 77 | `; 78 | 79 | // USERS ADDS THEMSELVES TO OTHER PEOPLE'S EVENTS 80 | queries.addUserToEvent = `INSERT INTO usersandevents 81 | (userid, username, eventid, eventtitle, eventdate, eventstarttime, eventendtime, eventdetails, eventlocation) 82 | VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9) 83 | RETURNING eventid 84 | ; 85 | `; 86 | 87 | // GRAB EVENT'S ATTENDEES 88 | queries.selectEventAttendees = 89 | 'SELECT * FROM usersandevents WHERE eventtitle=$1'; 90 | 91 | // GRAB CONTENT OWNER 92 | queries.checkCommentOwner = ` 93 | SELECT username FROM content JOIN users ON users.userid = content.userid WHERE contentid = $1`; 94 | 95 | // CREATING CONTENT 96 | queries.createContent = ` 97 | INSERT INTO content (userid, eventid, content, contentdate, contenttime) VALUES ($1, $2, $3, $4, $5) RETURNING *`; 98 | 99 | // UPDATING CONTENT 100 | queries.updateContent = ` 101 | UPDATE content SET content = $2 WHERE contentid=$1`; 102 | 103 | // DELETING CONTENT 104 | queries.deleteContent = ` 105 | DELETE FROM content WHERE contentid=$1`; 106 | 107 | // QUERY FOR DELETING ALL CONTENT RELATED TO AN EVENT 108 | queries.deleteEventContents = ` 109 | DELETE FROM content 110 | WHERE eventid=$1 111 | `; 112 | 113 | //QUERY CONTENT TABLE AND RETURN LIST OF CONTENT JOINED WITH USER DATA 114 | queries.getContentEvents = ` 115 | SELECT * 116 | FROM content 117 | LEFT JOIN users 118 | ON content.userid = users.userid 119 | ORDER BY contentid ASC 120 | `; 121 | 122 | // CLEAR ALL TABLES & DATA 123 | queries.clearAll = ` 124 | DROP TABLE usersandevents; 125 | DROP TABLE events; 126 | DROP TABLE users; 127 | `; 128 | 129 | module.exports = queries; 130 | -------------------------------------------------------------------------------- /client/components/MainContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Profile from './Profile.jsx'; 3 | import EventsFeed from './EventsFeed.jsx'; 4 | import Notnav from './Navbar.jsx'; 5 | import axios from 'axios'; 6 | import { Card, Button, Col, Row, Container } from 'react-bootstrap'; 7 | import AddSearchEvent from './AddSearchEvent.jsx'; 8 | 9 | // Implemented with hooks throughout 10 | export default function MainContainer() { 11 | const [userName, setUserName] = useState(''); 12 | const [user, setUser] = useState({}); 13 | const [events, setEvents] = useState([]); 14 | //pull user data after OAuth login - all variables are named from SQL DB columns 15 | useEffect(() => { 16 | axios.get(`/api/info?userName=${userName}`).then((res) => { 17 | let userInfo = { 18 | username: res.data.users.username, 19 | firstname: res.data.users.firstname, 20 | lastname: res.data.users.lastname, 21 | profilephoto: res.data.users.profilephoto, 22 | }; 23 | let eventsInfo = res.data.events; 24 | setUser(userInfo); 25 | setEvents(eventsInfo); 26 | 27 | setUserName(res.data.users.username); 28 | }); 29 | }, []); 30 | //updates username when a different user is selected 31 | function handleUserPageChange(username) { 32 | setUserName(username); 33 | } 34 | //handles the state change and posts to database on event creation 35 | function handleCreateEvent(event, newEvent, eventIndex) { 36 | let { 37 | eventtitle, 38 | eventlocation, 39 | eventdate, 40 | eventstarttime, 41 | eventdetails, 42 | } = event; 43 | if (!newEvent) { 44 | axios 45 | .put(`/api/events/${events[eventIndex].eventid}`, { 46 | eventtitle, 47 | eventlocation, 48 | eventdate, 49 | eventstarttime, 50 | eventdetails, 51 | }) 52 | .then((res) => { 53 | // Update events state variable 54 | const updatedEvents = events; 55 | updatedEvents[eventIndex] = { 56 | ...updatedEvents[eventIndex], 57 | ...event, 58 | }; 59 | 60 | return setEvents(updatedEvents); 61 | }); 62 | } else { 63 | axios 64 | .post(`/api/create?userName=${userName}`, { 65 | eventtitle, 66 | eventlocation, 67 | eventdate, 68 | eventstarttime, 69 | eventdetails, 70 | }) 71 | .then((res) => {}); 72 | event.attendees = [ 73 | { 74 | username: user.username, 75 | profilephoto: user.profilephoto, 76 | }, 77 | ]; 78 | event.eventownerusername = userName; 79 | const newEvents = [event].concat(events); 80 | return setEvents(newEvents); 81 | } 82 | } 83 | 84 | //handles the state change and posts to database on search event add 85 | function handleSearchEvent(event) { 86 | // ADD 87 | axios.post(`/api/add?eventtitle=${event.eventtitle}`).then((res) => { 88 | event.attendees.push({ 89 | username: user.username, 90 | firstname: user.firstname, 91 | lastname: user.lastname, 92 | profilephoto: user.profilephoto, 93 | }); 94 | 95 | const newEvents = [event].concat(events); 96 | setEvents(newEvents); 97 | }); 98 | } 99 | 100 | return ( 101 |
102 | 103 |
104 | 105 | 106 | 111 | 112 | 119 |
120 |
121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /client/components/CreateEvent.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { GoogleComponent } from 'react-google-location'; 3 | import DateTimePicker from 'react-datetime-picker'; 4 | 5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 6 | import { faPlus, faSearchPlus } from '@fortawesome/free-solid-svg-icons'; 7 | import { Modal, Button, Form, Card } from 'react-bootstrap'; 8 | 9 | export default function CreateEvent({ addEvent, updatingEvent, eventIndex }) { 10 | /* Form data */ 11 | const initialFormData = Object.freeze({ 12 | eventtitle: '', 13 | eventlocation: '', 14 | eventdetails: '', 15 | }); 16 | 17 | const [formData, updateFormData] = React.useState(initialFormData); 18 | const [dateTime, onChange] = useState(new Date()); 19 | const [show, setShow] = useState(false); 20 | //handles any change tot he form and updates the state 21 | const handleChange = (e) => { 22 | if (e.place) { 23 | return updateFormData({ 24 | ...formData, 25 | eventlocation: e.place, 26 | }); 27 | } else 28 | return updateFormData({ 29 | ...formData, 30 | // Trimming any whitespace 31 | [e.target.name]: e.target.value.trim(), 32 | }); 33 | }; 34 | //handles submit event - create date and time and append to the event object 35 | const handleSubmit = (e, newEvent) => { 36 | console.log(formData); 37 | const eventdate = dateTime.toDateString(); 38 | let time = dateTime.toTimeString(); 39 | let eventstarttime = time.split(' ')[0]; 40 | // ... submit to API or something 41 | addEvent({ ...formData, eventdate, eventstarttime }, newEvent, eventIndex); 42 | 43 | handleClose(); 44 | }; 45 | const handleClose = () => setShow(false); 46 | const handleShow = () => setShow(true); 47 | 48 | let newEvent = true; 49 | let buttonTitle = 'Add Event'; 50 | let formTitle = 'Create New Event'; 51 | let cardClass = 'cardContainer'; 52 | if (updatingEvent) { 53 | newEvent = false; 54 | buttonTitle = ''; 55 | formTitle = 'Update Event'; 56 | cardClass = 'cardContainer-small'; 57 | } 58 | 59 | return ( 60 |
61 |
62 | 63 |

{buttonTitle}

64 |
65 | 66 | 67 | 68 | {formTitle} 69 | 70 | 71 | 72 |
73 | 74 | Event Title 75 | 82 | 83 | 84 | 85 | Location 86 | 87 | 95 | 96 | 97 | 98 | Event Description 99 | 106 | 107 | 108 | 109 | Start Date & Time 110 | 111 | 112 | 113 | 122 |
123 |
124 |
125 |
126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /client/components/Content.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Media, Form, Button, Image } from 'react-bootstrap'; 3 | import axios from 'axios'; 4 | import fileUpload from 'express-fileupload'; 5 | import FormData from 'form-data'; 6 | 7 | export default function Content({ user, content, eventid }) { 8 | const [cont, setCont] = useState(content); 9 | const [comment, setComment] = useState(''); 10 | const [fileSelected, setFileSelected] = useState({}); 11 | 12 | const deleteContent = (e, id, index) => { 13 | e.preventDefault(); 14 | axios.delete(`/content/${id}`); 15 | let start = cont.slice(0, index); 16 | let end = cont.slice(index + 1); 17 | setCont([...start, ...end]); 18 | }; 19 | let messages = []; 20 | if (cont) { 21 | messages = cont.map((message, index) => { 22 | return ( 23 |
24 |
25 | 26 |
27 |
32 |

33 | {message.firstname} {message.lastname} 34 |

35 | {message.content[0] === '/' || message.content.includes('http') ? ( 36 | 37 | ) : ( 38 |

{message.content}

39 | )} 40 |

{message.time}

41 | {user.username !== message.username || ( 42 | 43 | 51 | 52 | )} 53 |
54 |
55 | ); 56 | }); 57 | } 58 | //handles change to comment - updates the state 59 | const handleChange = (e) => { 60 | setComment(e.target.value); 61 | }; 62 | //handles submit event - creates time stamp - does not submit to database....yet 63 | function handleCommentSubmit(e) { 64 | e.preventDefault(); 65 | 66 | //Add message to back end 67 | axios 68 | .post(`/content?userName=${user.username}`, { 69 | eventid: eventid, 70 | content: comment, 71 | }) 72 | .then((resp) => { 73 | const newMessage = resp.data; 74 | console.log('NEW MESSAGE: ',newMessage) 75 | newMessage.profilephoto = user.profilephoto; 76 | newMessage.firstname = user.firstname; 77 | newMessage.lastname = user.lastname; 78 | newMessage.username = user.username; 79 | setCont([...cont, newMessage]); 80 | document.getElementsByName('comment-form')[0].reset(); 81 | }); 82 | setComment(''); 83 | } 84 | 85 | const fileUploadHandler = (e) => { 86 | e.preventDefault(); 87 | console.log(fileSelected, fileSelected.name); 88 | const formData = new FormData(); 89 | formData.append('image', fileSelected, fileSelected.name); 90 | formData.append('eventid', eventid); 91 | const URL = `/content?userName=${user.username}`; 92 | axios 93 | .post(URL, formData, { 94 | headers: { 95 | accept: 'application/json', 96 | 'Accept-Language': 'en-US,en;q=0.8', 97 | 'Content-Type': `multipart/form-data; boundary=${formData._boundary}`, 98 | }, 99 | withCredentials: true, 100 | }) 101 | .then((response) => { 102 | //handle success 103 | const newMessage = { 104 | content: response.data.content, 105 | profilephoto: user.profilephoto, 106 | firstname: user.firstname, 107 | lastname: user.lastname, 108 | }; 109 | setCont([...cont, newMessage]); 110 | }) 111 | .catch((error) => { 112 | //handle error 113 | }); 114 | }; 115 | 116 | return ( 117 |
118 |

Comments

119 |
{messages}
120 |
121 | 122 | Add a Comment: 123 | 124 | 125 |
126 | 127 | 128 | 129 | setFileSelected(e.target.files[0])} 132 | /> 133 | 134 | 143 |
144 |
145 |
146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /server/controllers/fileController.js: -------------------------------------------------------------------------------- 1 | const jwtDecode = require('jwt-decode'); 2 | const { serviceusage } = require('googleapis/build/src/apis/serviceusage'); 3 | const e = require('express'); 4 | const queries = require('../utils/queries'); 5 | const db = require('../models/models.js'); 6 | 7 | const fileController = {}; 8 | 9 | fileController.createUser = (req, res, next) => { 10 | // ADD BACK ASYNC IF YOU TURN ON THE TRY / CATCH / AWAIT 11 | const decoded = jwtDecode(res.locals.token); 12 | 13 | const { email, given_name, family_name, picture } = decoded; 14 | 15 | if (!family_name) family_name = ' '; 16 | const queryString1 = queries.userInfo; 17 | const queryValues1 = [email]; 18 | 19 | const queryString2 = queries.addUser; 20 | const queryValues2 = [email, given_name, family_name, picture]; 21 | 22 | db.query(queryString1, queryValues1) 23 | .then((data) => { 24 | if (!data.rows.length) { 25 | db.query(queryString2, queryValues2) 26 | .then((data) => { 27 | res.locals.username = data.rows[0].username; // is this superfluous? 28 | return next(); 29 | }) 30 | .catch((err) => 31 | next({ 32 | log: `Error occurred with queries.addUser OR fileController.createUser middleware: ${err}`, 33 | message: { 34 | err: 'An error occurred with adding new user to the database.', 35 | }, 36 | }) 37 | ); 38 | } else { 39 | return next(); 40 | } 41 | }) 42 | .catch((err) => 43 | next({ 44 | log: `Error occurred with queries.userInfo OR fileController.createUser middleware: ${err}`, 45 | message: { 46 | err: 47 | 'An error occurred when checking user information from database.', 48 | }, 49 | }) 50 | ); 51 | }; 52 | 53 | fileController.getUser = (req, res, next) => { 54 | let decoded; 55 | if (!res.locals.token) { 56 | decoded = jwtDecode(req.cookies.user); 57 | } else { 58 | decoded = jwtDecode(res.locals.token); 59 | } 60 | const { email } = decoded; 61 | 62 | let targetUser; 63 | if (req.query.userName) { 64 | targetUser = req.query.userName; // this is in the event that user visits someone else' profile page 65 | } else { 66 | targetUser = email; 67 | } 68 | 69 | const queryString = queries.userInfo; 70 | const queryValues = [targetUser]; // user will have to be verified Jen / Minchan 71 | db.query(queryString, queryValues) 72 | .then((data) => { 73 | res.locals.allUserInfo = data.rows[0]; 74 | return next(); 75 | }) 76 | .catch((err) => 77 | next({ 78 | log: `Error occurred with queries.userInfo OR fileController.getUser middleware: ${err}`, 79 | message: { 80 | err: 81 | 'An error occured with SQL or server when retrieving user information.', 82 | }, 83 | }) 84 | ); 85 | }; 86 | 87 | fileController.verifyUser = (req, res, next) => { 88 | const decoded = jwtDecode(req.cookies.user); 89 | 90 | const { email } = decoded; 91 | console.log('username:', email); 92 | if (email == req.query.userName) { 93 | return next(); 94 | } 95 | return next({ 96 | log: 'Error occurred with fileController.verifyUser', 97 | code: 401, 98 | message: { err: 'Unauthorized Access.' }, 99 | }); 100 | }; 101 | 102 | // middleware to check if the logged in user is also the event owner 103 | // input - jwt with username, req.params with eventid 104 | fileController.userCanModifyEvent = (req, res, next) => { 105 | // retrieve username from jwt 106 | const decoded = jwtDecode(req.cookies.user); 107 | const { email } = decoded; 108 | 109 | // retrieve eventid from params 110 | const { eventid } = req.params; 111 | // query the SQL DB for the eventid in the events table 112 | db.query(queries.checkEventOwner, [eventid]) 113 | // check that the eventowner matches the userid 114 | .then((ownerUsername) => { 115 | if (ownerUsername.rows[0].eventownerusername === email) return next(); 116 | return next({ 117 | log: 'Error occurred with fileController.userCanModifyEvent', 118 | code: 401, 119 | message: { err: 'Unauthorized Access.' }, 120 | }); 121 | }); 122 | }; 123 | 124 | // middleware to check if the logged in user is also the content owner 125 | // input - jwt with username, req.params with commentid 126 | fileController.userCanModifyContent = (req, res, next) => { 127 | // retrieve username from jwt 128 | const decoded = jwtDecode(req.cookies.user); 129 | const { email } = decoded; 130 | 131 | // retrieve eventid from params 132 | const { contentid } = req.params; 133 | // query the SQL DB for the eventid in the events table 134 | db.query(queries.checkCommentOwner, [contentid]) 135 | // check that the eventowner matches the userid 136 | .then((ownerUsername) => { 137 | if (ownerUsername.rows[0].username === email) return next(); 138 | return next({ 139 | log: 'Error occurred with fileController.userCanModifyContent', 140 | code: 401, 141 | message: { err: 'Unauthorized Access.' }, 142 | }); 143 | }); 144 | }; 145 | 146 | module.exports = fileController; 147 | -------------------------------------------------------------------------------- /server/controllers/eventController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/models'); 2 | const queries = require('../utils/queries'); 3 | 4 | const eventController = {}; 5 | 6 | 7 | eventController.createEvent = (req, res, next) => { 8 | const { userid, username } = res.locals.allUserInfo; 9 | 10 | const queryString = queries.createEvent; 11 | 12 | const { 13 | eventtitle, eventlocation, eventdate, eventstarttime, eventdetails, 14 | } = req.body; 15 | 16 | const queryValues = [eventtitle, eventdate, eventstarttime, eventstarttime, eventlocation, eventdetails, userid, username, '{}']; 17 | db.query(queryString, queryValues) 18 | .then((data) => { 19 | res.locals.eventID = data.rows[0]; 20 | return next(); 21 | }) 22 | .catch((err) => { 23 | return next({ 24 | log: `Error occurred with queries.createEvent OR eventController.createEvent middleware: ${err}`, 25 | message: { err: 'An error occured with SQL when creating event.' }, 26 | }); 27 | }); 28 | }; 29 | 30 | eventController.addNewEventToJoinTable = (req, res, next) => { 31 | const queryString = queries.addNewEventToJoinTable; 32 | const queryValues = [res.locals.eventID.eventid]; 33 | db.query(queryString, queryValues) 34 | .then((data) => { 35 | res.locals.usersandevents = data.rows[0]; 36 | return next(); 37 | }) 38 | .catch((err) => { 39 | return next({ 40 | log: `Error occurred with queries.addtoUsersAndEvents OR eventController.addNewEventToJoinTable middleware: ${err}`, 41 | message: { err: 'An error occured with SQL when adding to addtoUsersAndEvents table.' }, 42 | }); 43 | }); 44 | }; 45 | 46 | eventController.verifyAttendee = (req, res, next) => { 47 | const title = req.query.eventtitle; // verify with frontend 48 | 49 | const { username } = res.locals.allUserInfo; 50 | 51 | const queryString = queries.selectEventAttendees; 52 | const queryValues = [title]; 53 | 54 | db.query(queryString, queryValues) 55 | .then((data) => { 56 | const attendees = []; 57 | for (const attendeeObj of data.rows) { 58 | attendees.push(attendeeObj.username); 59 | } 60 | if (attendees.includes(username)) { 61 | return next({ 62 | log: 'Error: User is already an attendee', 63 | message: { err: 'User is already an attendee' }, 64 | }); 65 | } 66 | res.locals.eventID = data.rows[0].eventid; 67 | res.locals.eventTitle = data.rows[0].eventtitle; 68 | res.locals.eventDate = data.rows[0].eventdate; 69 | res.locals.eventStartTime = data.rows[0].eventstarttime; 70 | res.locals.eventEndTime = data.rows[0].eventendtime; 71 | res.locals.eventDetails = data.rows[0].eventdetails; 72 | res.locals.eventLocation = data.rows[0].eventlocation; 73 | return next(); 74 | }) 75 | .catch((err) => next({ 76 | log: `Error occurred with queries.selectEventAttendees OR eventController.verifyAttendee middleware: ${err}`, 77 | message: { err: 'An error occured with SQL when verifying if user attended said event.' }, 78 | })); 79 | }; 80 | 81 | // (userid, username, eventid, eventtitle, eventdate, eventstarttime, eventendtime, eventdetails, eventlocation) 82 | eventController.addAttendee = (req, res, next) => { 83 | const title = req.query.eventtitle; 84 | 85 | const { userid, username } = res.locals.allUserInfo; 86 | // eventsID is saved in res.locals.eventID 87 | 88 | const queryString = queries.addUserToEvent; 89 | const queryValues = [ 90 | userid, 91 | username, 92 | res.locals.eventID, 93 | title, 94 | res.locals.eventDate, 95 | res.locals.eventStartTime, 96 | res.locals.eventEndTime, 97 | res.locals.eventDetails, 98 | res.locals.eventLocation, 99 | ]; 100 | 101 | db.query(queryString, queryValues) 102 | .then((data) => { 103 | return next(); 104 | }) 105 | .catch((err) => next({ 106 | log: `Error occurred with queries.addUserToEvent OR eventController.addAttendee middleware: ${err}`, 107 | message: { err: 'An error occured with SQL adding a user to an existing event as an attendee.' }, 108 | })); 109 | }; 110 | // extracts all events and then pulls the user and events DB and appends all attendees to each event 111 | eventController.allEvents = (req, res, next) => { 112 | const queryString = queries.getAllEvents; 113 | // pulls all events 114 | db.query(queryString) 115 | .then((data) => { 116 | if (!data.rows) { 117 | res.locals.allEventsInfo = []; 118 | } else { 119 | // then grabs all the attendees fromt he user and events table joined with the user table 120 | db.query(queries.getAttendeeEvents).then((eventAndUserData) => { 121 | db.query(queries.getContentEvents).then((eventAndContentData) => { 122 | // goes through the table and creates an attendees array with the list of user data 123 | const mergedTable = data.rows.map((e) => { 124 | e.attendees = eventAndUserData.rows.filter((entry) => entry.eventid == e.eventid); 125 | e.content = eventAndContentData.rows.filter((entry) => entry.eventid == e.eventid); 126 | return e; 127 | }); 128 | console.log(mergedTable); 129 | res.locals.allEventsInfo = mergedTable; 130 | return next(); 131 | }); 132 | }); 133 | } 134 | }) 135 | .catch((err) => next({ 136 | log: `Error occurred with queries.getAllEvents OR eventController.allEvents middleware: ${err}`, 137 | message: { err: 'An error occured with SQL when retrieving all events information.' }, 138 | })); 139 | }; 140 | 141 | // filters out all events to only return the ones that the current user is attending 142 | eventController.filterForUser = (req, res, next) => { 143 | const { userid } = res.locals.allUserInfo; 144 | 145 | const filtered = res.locals.allEventsInfo.filter((event) => event.attendees.some((attendee) => attendee.userid === userid)); 146 | res.locals.allEventsInfo = filtered; 147 | return next(); 148 | }; 149 | 150 | eventController.updateEvent = (req, res, next) => { 151 | const { eventid } = req.params; 152 | const { 153 | eventtitle, 154 | eventdate, 155 | eventstarttime, 156 | eventlocation, 157 | eventdetails, 158 | } = req.body; 159 | 160 | const eventendtime = eventstarttime; 161 | 162 | const queryValues = [ 163 | eventid, 164 | eventtitle, 165 | eventdate, 166 | eventstarttime, 167 | eventendtime, 168 | eventlocation, 169 | eventdetails, 170 | ]; 171 | 172 | db.query(queries.updateEvent, queryValues) 173 | .then((resp) => { 174 | return next(); 175 | }) 176 | .catch((err) => next({ 177 | log: `Error occurred with eventController.updateEvent middleware: ${err}`, 178 | message: { err: 'An error occured with SQL when updating event information.' }, 179 | })); 180 | }; 181 | 182 | eventController.deleteEvent = (req, res, next) => { 183 | const { eventid } = req.params; 184 | 185 | db.query(queries.deleteUserEvents, [eventid]) //Disassociates event from users 186 | .then((resp) => { 187 | db.query(queries.deleteEventContents, [eventid]) //Deletes all associated content with event 188 | .then((resp) => { 189 | db.query(queries.deleteEvent, [eventid]) //Deletes events itself 190 | .then((resp) => { 191 | return next(); 192 | }); 193 | }); 194 | }) 195 | .catch((err) => next({ 196 | log: `Error occurred with eventController.deleteEvent middleware: ${err}`, 197 | message: { err: 'An error occured with SQL when deleting event information.' }, 198 | })); 199 | }; 200 | 201 | module.exports = eventController; 202 | -------------------------------------------------------------------------------- /client/stylesheets/styles.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Abril+Fatface&family=Dancing+Script&family=Montserrat&family=Julius+Sans+One&family=Baloo+Tamma+2&family=Poiret+One&display=swap'); 2 | 3 | $colorOld: rgb(129, 216, 208); // original color for navbar 4 | // New Color SCheme 5 | $color1: #040F57; 6 | $color2: #12348D; 7 | $color3: #9043C3; // updated navbar color 8 | $color4: #D75CA2; 9 | $color5: #BD2B48; 10 | $color6: #E79556; 11 | 12 | 13 | body { 14 | color: rgb(77, 77, 77); 15 | } 16 | 17 | .myNavbar { 18 | // width: 90%; 19 | background-color: $color3; 20 | margin-bottom: 30px; 21 | color: white; 22 | } 23 | 24 | .navButton { 25 | color: white; 26 | border: 1px solid white; 27 | } 28 | 29 | .brand { 30 | font-family: 'Dancing Script', cursive; 31 | font-size: 40px; 32 | margin-bottom: 10px; 33 | color: white; 34 | } 35 | 36 | .col { 37 | display: flex; 38 | align-content: center; 39 | } 40 | .myCol { 41 | display: flex; 42 | justify-content: center; 43 | } 44 | 45 | .card { 46 | border: 0px; 47 | } 48 | 49 | .profile { 50 | display: flex; 51 | align-items: center; 52 | border: 1px rgb(157, 189, 211) solid; 53 | padding: 2em; 54 | padding-left: 80px; 55 | border-radius: 5px; 56 | box-shadow: 1px 1px 2px 1px $color3; 57 | background-color: rgb(250, 250, 250); 58 | font-family: 'Poiret One', cursive; 59 | } 60 | 61 | .profile img { 62 | border-radius: 10px; 63 | border: 3px solid $color2; 64 | width: 150px; 65 | } 66 | 67 | .cardContainer { 68 | border: 1px gainsboro solid; 69 | width: 150px; 70 | height: 150px; 71 | border-radius: 5px; 72 | box-shadow: 1px 1px 1px 1px gainsboro; 73 | text-align: center; 74 | padding-top: 30px; 75 | margin-top: 40px; 76 | margin-right: 25px; 77 | font-family: 'Baloo Tamma 2'; 78 | } 79 | .cardContainer:hover, 80 | .cardContainer-small:hover{ 81 | background-color: $color2; 82 | color: rgb(255, 255, 255); 83 | cursor: pointer; 84 | } 85 | 86 | .cardContainerAdd { 87 | border: 1px gainsboro solid; 88 | width: 150px; 89 | height: 150px; 90 | border-radius: 5px; 91 | box-shadow: 1px 1px 1px 1px gainsboro; 92 | text-align: center; 93 | padding-top: 50px; 94 | margin-top: 40px; 95 | margin-right: 25px; 96 | font-family: 'Baloo Tamma 2'; 97 | } 98 | 99 | .cardContainer-small { 100 | border: 1px gainsboro solid; 101 | width: 40px; 102 | height: 40px; 103 | border-radius: 2px; 104 | box-shadow: 1px 1px 1px 1px gainsboro; 105 | text-align: center; 106 | padding-top: 10px; 107 | margin-top: 10px; 108 | margin-bottom: 5px; 109 | margin-right: 10px; 110 | font-family: 'Baloo Tamma 2'; 111 | } 112 | 113 | .cardContainer p { 114 | padding-top: 20px; 115 | } 116 | 117 | /* Event.jsx */ 118 | 119 | .events { 120 | display: flex; 121 | flex-direction: row; 122 | flex-wrap: wrap; 123 | } 124 | 125 | .event { 126 | border: 1px solid gainsboro; 127 | flex: 1 0 33%; 128 | min-width: 33%; 129 | margin: 1em; 130 | padding: 1em; 131 | box-shadow: 1px 1px 1px 1px gainsboro; 132 | border-radius: 5px; 133 | background-color: rgb(250, 250, 250); 134 | } 135 | 136 | .jumbotron { 137 | border: 1px solid $color3; 138 | box-shadow: 1px 1px 1px 1px gainsboro; 139 | } 140 | 141 | .eventJumbotron { 142 | text-align: center; 143 | font-family: 'Montserrat'; 144 | } 145 | 146 | .eventJumbotron h1 { 147 | font-family: 'Abril Fatface'; 148 | letter-spacing: 3px; 149 | font-size: 60px; 150 | } 151 | 152 | .event-owner-buttons { 153 | display: flex; 154 | justify-content: flex-end; 155 | } 156 | 157 | 158 | 159 | 160 | /* EventAttendees.jsx */ 161 | 162 | .circular { 163 | width: 85px; 164 | height: 85px; 165 | border-radius: 50%; 166 | position: relative; 167 | overflow: hidden; 168 | border: 2px solid rgb(204, 204, 204); 169 | box-shadow: 2px 3px 6px #cacaca; 170 | margin: 10px; 171 | } 172 | .circular img { 173 | height: 85px; 174 | object-fit: contain; 175 | position: absolute; 176 | left: 50%; 177 | top: 50%; 178 | -webkit-transform: translate(-50%, -50%); 179 | -moz-transform: translate(-50%, -50%); 180 | -ms-transform: translate(-50%, -50%); 181 | transform: translate(-50%, -50%); 182 | } 183 | 184 | .attendeesContainer h5 { 185 | text-align: center; 186 | } 187 | 188 | .attendees { 189 | display: flex; 190 | justify-content: center; 191 | flex-wrap: wrap; 192 | margin-bottom: 20px; 193 | } 194 | 195 | .attendeeInfo { 196 | text-align: center; 197 | font-size: 10px; 198 | } 199 | 200 | /* SearchEvent.jsx */ 201 | 202 | .searchResults { 203 | display: grid; 204 | } 205 | 206 | .searchResult { 207 | margin: 0.3em; 208 | } 209 | 210 | /* Content.jsx */ 211 | 212 | .userMessage { 213 | width: 50px; 214 | height: 50px; 215 | border-radius: 50%; 216 | position: relative; 217 | overflow: hidden; 218 | border: 1px solid rgb(204, 204, 204); 219 | box-shadow: 2px 3px 6px #cacaca; 220 | margin: 10px; 221 | } 222 | 223 | .userMessage img { 224 | height: 50px; 225 | object-fit: contain; 226 | position: absolute; 227 | left: 50%; 228 | top: 50%; 229 | -webkit-transform: translate(-50%, -50%); 230 | -moz-transform: translate(-50%, -50%); 231 | -ms-transform: translate(-50%, -50%); 232 | transform: translate(-50%, -50%); 233 | } 234 | .messageBox { 235 | display: flex; 236 | justify-content: left; 237 | 238 | } 239 | 240 | .messages { 241 | max-height: 400px; 242 | overflow: scroll; 243 | } 244 | 245 | .messages p { 246 | font-size: 12px; 247 | padding: 0; 248 | margin: 0; 249 | } 250 | .messageName { 251 | font-weight: bold; 252 | } 253 | 254 | .message { 255 | padding: 10px; 256 | } 257 | 258 | .msg-btns{ 259 | display:flex; 260 | justify-content:space-around; 261 | } 262 | 263 | 264 | /* rainbow divider */ 265 | 266 | $bg: white; 267 | $barsize: 15px; 268 | 269 | .hr { 270 | width: 80%; 271 | margin: 0 auto; 272 | height: 1px; 273 | display: block; 274 | position: relative; 275 | margin-bottom: 0em; 276 | padding: 2em 0; 277 | 278 | &:after, 279 | &:before { 280 | content: ''; 281 | position: absolute; 282 | width: 100%; 283 | height: 1px; 284 | bottom: 50%; 285 | left: 0; 286 | } 287 | 288 | &:before { 289 | background: linear-gradient( 290 | 90deg, 291 | $bg 0%, 292 | $bg 50%, 293 | transparent 50%, 294 | transparent 100% 295 | ); 296 | background-size: $barsize; 297 | background-position: center; 298 | z-index: 1; 299 | } 300 | 301 | &:after { 302 | transition: opacity 0.3s ease, animation 0.3s ease; 303 | background: linear-gradient( 304 | to right, 305 | #BD2B48 5%, 306 | #040F57 15%, 307 | #040F57 25%, 308 | #12348D 35%, 309 | #12348D 45%, 310 | #9043C3 55%, 311 | #9043C3 65%, 312 | #D75CA2 75%, 313 | #D75CA2 85%, 314 | #BD2B48 95% 315 | ); 316 | background-size: 200%; 317 | background-position: 0%; 318 | animation: bar 15s linear infinite; 319 | } 320 | 321 | 322 | @keyframes bar { 323 | 0% { 324 | background-position: 0%; 325 | } 326 | 100% { 327 | background-position: 200%; 328 | } 329 | } 330 | } 331 | 332 | .hr.anim { 333 | &:before { 334 | background: linear-gradient( 335 | 90deg, 336 | $bg 0%, 337 | $bg 5%, 338 | transparent 5%, 339 | transparent 10%, 340 | $bg 10%, 341 | $bg 15%, 342 | transparent 15%, 343 | transparent 20%, 344 | $bg 20%, 345 | $bg 25%, 346 | transparent 25%, 347 | transparent 30%, 348 | $bg 30%, 349 | $bg 35%, 350 | transparent 35%, 351 | transparent 40%, 352 | $bg 40%, 353 | $bg 45%, 354 | transparent 45%, 355 | transparent 50%, 356 | $bg 50%, 357 | $bg 55%, 358 | transparent 55%, 359 | transparent 60%, 360 | $bg 60%, 361 | $bg 65%, 362 | transparent 65%, 363 | transparent 70%, 364 | $bg 70%, 365 | $bg 75%, 366 | transparent 75%, 367 | transparent 80%, 368 | $bg 80%, 369 | $bg 85%, 370 | transparent 85%, 371 | transparent 90%, 372 | $bg 90%, 373 | $bg 95%, 374 | transparent 95%, 375 | transparent 100% 376 | ); 377 | 378 | background-size: $barsize * 10; 379 | background-position: center; 380 | z-index: 1; 381 | 382 | animation: bar 120s linear infinite; 383 | } 384 | 385 | &:hover { 386 | &:before { 387 | animation-duration: 20s; 388 | } 389 | &:after { 390 | animation-duration: 2s; 391 | } 392 | } 393 | .btn-danger { 394 | // border: 1px gainsboro solid; 395 | background-color: #9043C3; 396 | width: 150px; 397 | height: 150px; 398 | border-radius: 5px; 399 | box-shadow: 1px 1px 1px 1px gainsboro; 400 | text-align: center; 401 | padding-top: 25px; 402 | margin-top: 40px; 403 | margin-right: 25px; 404 | font-family: 'Baloo Tamma 2'; 405 | } 406 | .deleteEvent { 407 | width: 40px; 408 | height: 40px; 409 | } 410 | } 411 | --------------------------------------------------------------------------------