├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------