init repo
12
README.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# How to run
|
||||||
|
|
||||||
|
## Client
|
||||||
|
```shell
|
||||||
|
npm i && npm run
|
||||||
|
```
|
||||||
|
## Server
|
||||||
|
```shell
|
||||||
|
npm i -g nodemon
|
||||||
|
npm i
|
||||||
|
nodemon index.js
|
||||||
|
```
|
3
final/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
client/node_modules
|
||||||
|
server/node_modules
|
38974
final/client/package-lock.json
generated
Normal file
50
final/client/package.json
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"name": "top5-lister-hw3",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.4.1",
|
||||||
|
"@emotion/styled": "^11.3.0",
|
||||||
|
"@fontsource/roboto": "^4.5.1",
|
||||||
|
"@mui/icons-material": "^5.0.3",
|
||||||
|
"@mui/material": "^5.0.3",
|
||||||
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
|
"@testing-library/react": "^11.2.7",
|
||||||
|
"@testing-library/user-event": "^12.8.3",
|
||||||
|
"axios": "^0.22.0",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-router-dom": "^5.3.0",
|
||||||
|
"react-scripts": "^4.0.3",
|
||||||
|
"react-window": "^1.8.6",
|
||||||
|
"web-vitals": "^1.1.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).",
|
||||||
|
"main": "index.js",
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
52
final/client/public/data/default_lists.json
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"top5Lists": [
|
||||||
|
{
|
||||||
|
"key": "0",
|
||||||
|
"name": "Games",
|
||||||
|
"likes": 2150,
|
||||||
|
"dislikes": 114,
|
||||||
|
"created": "Thu, 22 Oct 2020 01:10:53 GMT",
|
||||||
|
"modified": "Wed, 23 Jun 2021 03:21:31 GMT",
|
||||||
|
"accessed": "Thu, 26 Aug 2021 03:23:31 GMT",
|
||||||
|
"items": [
|
||||||
|
"StarCraft",
|
||||||
|
"Fallout 3",
|
||||||
|
"Katamari Damacy",
|
||||||
|
"Civilization II",
|
||||||
|
"Super Mario World"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"name": "Movies",
|
||||||
|
"likes": 50,
|
||||||
|
"dislikes": 4,
|
||||||
|
"created": "Mon, 23 Aug 2021 03:12:53 GMT",
|
||||||
|
"modified": "Mon, 23 Aug 2021 03:14:31 GMT",
|
||||||
|
"accessed": "Mon, 23 Aug 2021 03:16:31 GMT",
|
||||||
|
"items": [
|
||||||
|
"Raiders of the Lost Ark",
|
||||||
|
"Goodfellas",
|
||||||
|
"Lord of the Rings",
|
||||||
|
"Airplane!",
|
||||||
|
"Lawrence of Arabia"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"name": "Pink Floyd Songs",
|
||||||
|
"likes": 520,
|
||||||
|
"dislikes": 14,
|
||||||
|
"created": "Sun, 22 Aug 2021 03:19:53 GMT",
|
||||||
|
"modified": "Mon, 24 Aug 2021 03:21:31 GMT",
|
||||||
|
"accessed": "Wed, 26 Aug 2021 03:23:31 GMT",
|
||||||
|
"items": [
|
||||||
|
"Shine On You Crazy Diamond",
|
||||||
|
"Comfortably Numb",
|
||||||
|
"Pigs (Three Different Ones)",
|
||||||
|
"Echoes (Live at Pompeii)",
|
||||||
|
"Time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
final/client/public/favicon.ico
Normal file
After Width: | Height: | Size: 3.8 KiB |
43
final/client/public/index.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
final/client/public/logo192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
final/client/public/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
final/client/public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
3
final/client/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
453
final/client/src/App.css
Normal file
|
@ -0,0 +1,453 @@
|
||||||
|
:root {
|
||||||
|
/*
|
||||||
|
FIRST WE'LL DEFINE OUR SWATCHES, i.e. THE COMPLEMENTARY
|
||||||
|
COLORS THAT WE'LL USE TOGETHER IN MULTIPLE PLACES THAT
|
||||||
|
TOGETHER WILL MAKE UP A GIVEN THEME
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
--swatch-foundation: #eeeedd;
|
||||||
|
--swatch-primary: #e6e6e6;
|
||||||
|
--swatch-complement: #e1e4cb;
|
||||||
|
--swatch-contrast: #111111;
|
||||||
|
--swatch-accent: #669966;
|
||||||
|
--swatch-status: #123456;
|
||||||
|
--my-font-family: "Robaaaoto";
|
||||||
|
--bounceEasing: cubic-bezier(0.51, 0.92, 0.24, 1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--swatch-foundation);
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
background-color: var(--swatch-primary);
|
||||||
|
font-family: "Lexend Exa";
|
||||||
|
position: absolute;
|
||||||
|
width: 80%;
|
||||||
|
left: 10%;
|
||||||
|
height: 90%;
|
||||||
|
top: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-root {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0%;
|
||||||
|
left: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-banner {
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 0%;
|
||||||
|
width: 100%;
|
||||||
|
height: 10%;
|
||||||
|
float: left;
|
||||||
|
background-image: linear-gradient(to bottom, #b8b808, #636723);
|
||||||
|
color: white;
|
||||||
|
font-size: 48pt;
|
||||||
|
border-color: black;
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
#splash-screen {
|
||||||
|
background-image: linear-gradient(#ddeeff, #e7e7e7);
|
||||||
|
color: black;
|
||||||
|
font-size: 5vw;
|
||||||
|
padding-left: 10%;
|
||||||
|
padding-top: 5%;
|
||||||
|
padding-bottom: 5%;
|
||||||
|
height:80%;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#author {
|
||||||
|
padding-left: 30%;
|
||||||
|
font-size: 1.5vw;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-toolbar {
|
||||||
|
background-color: transparent;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-button,
|
||||||
|
.top5-button-disabled {
|
||||||
|
font-size: 36pt;
|
||||||
|
border-width: 0px;
|
||||||
|
float: left;
|
||||||
|
color: black;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-button-disabled {
|
||||||
|
opacity: 0.25;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-list-selector {
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 10%;
|
||||||
|
width: 100%;
|
||||||
|
height: 80%;
|
||||||
|
background-color: var(--swatch-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-workspace {
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 10%;
|
||||||
|
width: 100%;
|
||||||
|
height: 80%;
|
||||||
|
background-color: var(--swatch-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-statusbar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 90%;
|
||||||
|
width: 100%;
|
||||||
|
height: 10%;
|
||||||
|
background-color: lightgray;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 36pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list-selector-heading {
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 0%;
|
||||||
|
width: 100%;
|
||||||
|
height: 10%;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 24pt;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: left;
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-list-button {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list-selector-list {
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 10%;
|
||||||
|
width: 100%;
|
||||||
|
height: 90%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
#alert-box {
|
||||||
|
z-index: 99999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card,
|
||||||
|
.selected-list-card,
|
||||||
|
.unselected-list-card {
|
||||||
|
font-size: 18pt;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card:aria-disabled,
|
||||||
|
.list-card[aria-disabled] {
|
||||||
|
border: 1px solid #999999;
|
||||||
|
background-color: #cccccc;
|
||||||
|
color: #666666;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unselected-list-card:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--swatch-contrast);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-list-card {
|
||||||
|
background-color: var(--swatch-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unselected-list-card {
|
||||||
|
background-color: var(--swatch-complement);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card-button {
|
||||||
|
float: right;
|
||||||
|
transform: scale(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#workspace-home,
|
||||||
|
#workspace-edit {
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 0%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-numbering {
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 0%;
|
||||||
|
width: 20%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--swatch-status);
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-items {
|
||||||
|
position: absolute;
|
||||||
|
left: 20%;
|
||||||
|
top: 0%;
|
||||||
|
width: 80%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--swatch-complement);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-number,
|
||||||
|
.top5-item,
|
||||||
|
.top5-item-dragged-to {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 48pt;
|
||||||
|
height: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-number {
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px 0px 1px 1px;
|
||||||
|
border-color: black;
|
||||||
|
background-color: linen;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: 20pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-item,
|
||||||
|
.top5-item-dragged-to,
|
||||||
|
.top5-item-editting{
|
||||||
|
text-align: left;
|
||||||
|
width: 95%;
|
||||||
|
padding-left: 5%;
|
||||||
|
}
|
||||||
|
.top5-item,
|
||||||
|
.top5-item-editting{
|
||||||
|
background-color: var(--swatch-complement);
|
||||||
|
}
|
||||||
|
.top5-item-dragged-to {
|
||||||
|
background-color: var(--swatch-accent);
|
||||||
|
}
|
||||||
|
.top5-item-editting{
|
||||||
|
height: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled:hover {
|
||||||
|
color: var(--swatch-neutral);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* THIS STYLE SHEET MANAGES STYLE FOR OUR MODAL, i.e. DIALOG BOX */
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--black);
|
||||||
|
color: var(--swatch-text);
|
||||||
|
cursor: pointer;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.35s ease-in;
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.is-visible {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
position: relative;
|
||||||
|
max-width: 800px;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: var(--swatch-complement);
|
||||||
|
overflow: auto;
|
||||||
|
cursor: default;
|
||||||
|
border-width: 5px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-style: groove;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog > * {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
|
.modal-footer {
|
||||||
|
background: var(--lightgray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
|
.modal-close {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal p + p {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-control {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
font-size: 24pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#close-modal-button {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirm-cancel-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ANIMATIONS
|
||||||
|
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||||
|
[data-animation] .modal-dialog {
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.5s var(--bounceEasing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation].is-visible .modal-dialog {
|
||||||
|
opacity: 1;
|
||||||
|
transition-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutDown"] .modal-dialog {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutTop"] .modal-dialog {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutLeft"] .modal-dialog {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutRight"] .modal-dialog {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="zoomInOut"] .modal-dialog {
|
||||||
|
transform: scale(0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="rotateInOutDown"] .modal-dialog {
|
||||||
|
transform-origin: top left;
|
||||||
|
transform: rotate(-1turn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="mixInAnimations"].is-visible .modal-dialog {
|
||||||
|
animation: mixInAnimations 2s 0.2s linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutDown"].is-visible .modal-dialog,
|
||||||
|
[data-animation="slideInOutTop"].is-visible .modal-dialog,
|
||||||
|
[data-animation="slideInOutLeft"].is-visible .modal-dialog,
|
||||||
|
[data-animation="slideInOutRight"].is-visible .modal-dialog,
|
||||||
|
[data-animation="zoomInOut"].is-visible .modal-dialog,
|
||||||
|
[data-animation="rotateInOutDown"].is-visible .modal-dialog {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mixInAnimations {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
10% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
transform: rotate(20deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
transform: rotate(-20deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
transform: rotate(15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: rotate(-10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
transform: rotate(5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
90% {
|
||||||
|
transform: rotate(-5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
48
final/client/src/App.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import './App.css';
|
||||||
|
import { React } from 'react'
|
||||||
|
import { BrowserRouter, Route, Switch } from 'react-router-dom'
|
||||||
|
import { AuthContextProvider } from './auth';
|
||||||
|
import { GlobalStoreContextProvider } from './store'
|
||||||
|
import {
|
||||||
|
AppBanner,
|
||||||
|
HomeWrapper,
|
||||||
|
RegisterScreen,
|
||||||
|
HomeScreen,
|
||||||
|
Statusbar,
|
||||||
|
WorkspaceScreen,
|
||||||
|
Alert,
|
||||||
|
LoginScreen,
|
||||||
|
} from './components'
|
||||||
|
/*
|
||||||
|
This is our application's top-level component.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
This is the entry-point for our application. Notice that we
|
||||||
|
inject our store into all the components in our application.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<AuthContextProvider>
|
||||||
|
<GlobalStoreContextProvider>
|
||||||
|
<AppBanner />
|
||||||
|
<Alert />
|
||||||
|
<Switch>
|
||||||
|
<Route path="/" exact component={HomeWrapper} />
|
||||||
|
<Route path="/register/" exact component={RegisterScreen} />
|
||||||
|
<Route path="/top5list/:id" exact component={WorkspaceScreen} />
|
||||||
|
<Route path="/login/" exact component={LoginScreen} />
|
||||||
|
<Route path="/lists/" exact component={HomeScreen} />
|
||||||
|
</Switch>
|
||||||
|
<Statusbar />
|
||||||
|
</GlobalStoreContextProvider>
|
||||||
|
</AuthContextProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
454
final/client/src/OldApp.css
Normal file
|
@ -0,0 +1,454 @@
|
||||||
|
:root {
|
||||||
|
/*
|
||||||
|
FIRST WE'LL DEFINE OUR SWATCHES, i.e. THE COMPLEMENTARY
|
||||||
|
COLORS THAT WE'LL USE TOGETHER IN MULTIPLE PLACES THAT
|
||||||
|
TOGETHER WILL MAKE UP A GIVEN THEME
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
--swatch-foundation: #eeeedd;
|
||||||
|
--swatch-primary: #e6e6e6;
|
||||||
|
--swatch-complement: #e1e4cb;
|
||||||
|
--swatch-contrast: #111111;
|
||||||
|
--swatch-accent: #669966;
|
||||||
|
--swatch-status: #123456;
|
||||||
|
--my-font-family: "Robaaaoto";
|
||||||
|
--bounceEasing: cubic-bezier(0.51, 0.92, 0.24, 1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--swatch-foundation);
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
background-color: var(--swatch-primary);
|
||||||
|
font-family: "Lexend Exa";
|
||||||
|
position: absolute;
|
||||||
|
width: 80%;
|
||||||
|
left: 10%;
|
||||||
|
height:90%;
|
||||||
|
top: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-root {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0%;
|
||||||
|
left: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-banner {
|
||||||
|
position:absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 0%;
|
||||||
|
width: 100%;
|
||||||
|
height: 10%;
|
||||||
|
float:left;
|
||||||
|
background-image: linear-gradient(to bottom,
|
||||||
|
#b8b808, #636723);
|
||||||
|
color: white;
|
||||||
|
font-size: 48pt;
|
||||||
|
border-color: black;
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-toolbar {
|
||||||
|
|
||||||
|
background-color: transparent;
|
||||||
|
float:right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-button, .top5-button-disabled {
|
||||||
|
font-size:36pt;
|
||||||
|
border-width: 0px;
|
||||||
|
float:left;
|
||||||
|
color: black;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color:black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-button-disabled {
|
||||||
|
opacity: 0.25;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-list-selector {
|
||||||
|
position:absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 10%;
|
||||||
|
width: 100%;
|
||||||
|
height: 80%;
|
||||||
|
background-color: var(--swatch-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-workspace {
|
||||||
|
position:absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 10%;
|
||||||
|
width: 100%;
|
||||||
|
height: 80%;
|
||||||
|
background-color: var(--swatch-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-statusbar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 90%;
|
||||||
|
width: 100%;
|
||||||
|
height: 10%;
|
||||||
|
background-color: lightsalmon;
|
||||||
|
display:flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 36pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list-selector-heading {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:0%;
|
||||||
|
width:100%;
|
||||||
|
height:10%;
|
||||||
|
text-align:center;
|
||||||
|
font-size: 24pt;
|
||||||
|
font-weight: bold;
|
||||||
|
display:flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-list-button {
|
||||||
|
float:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list-selector-list {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:10%;
|
||||||
|
width:100%;
|
||||||
|
height:90%;
|
||||||
|
display:flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card, .selected-list-card, .unselected-list-card {
|
||||||
|
font-size: 18pt;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card:aria-disabled,.list-card[aria-disabled] {
|
||||||
|
border: 1px solid #999999;
|
||||||
|
background-color: #cccccc;
|
||||||
|
color: #666666;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unselected-list-card:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--swatch-contrast);
|
||||||
|
color:white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-list-card {
|
||||||
|
background-color: var(--swatch-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unselected-list-card {
|
||||||
|
background-color: var(--swatch-complement);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card-button {
|
||||||
|
float:right;
|
||||||
|
font-size:18pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#workspace-home, #workspace-edit {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:0%;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-numbering {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:0%;
|
||||||
|
width:20%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--swatch-status);
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-items {
|
||||||
|
position:absolute;
|
||||||
|
left:20%;
|
||||||
|
top:0%;
|
||||||
|
width:80%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--swatch-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-number, .top5-item, .top5-item-dragged-to {
|
||||||
|
display:flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 24pt;
|
||||||
|
height:20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-number {
|
||||||
|
justify-content: center;
|
||||||
|
width:100%;
|
||||||
|
border: 1px 0px 1px 1px;
|
||||||
|
border-color:black;
|
||||||
|
background-color: linen;
|
||||||
|
color:black;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size:20pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-item, .top5-item-dragged-to {
|
||||||
|
text-align: left;
|
||||||
|
width:95%;
|
||||||
|
padding-left:5%;
|
||||||
|
}
|
||||||
|
.top5-item {
|
||||||
|
background-color: var(--swatch-complement);
|
||||||
|
}
|
||||||
|
.top5-item-dragged-to {
|
||||||
|
background-color: var(--swatch-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled:hover {
|
||||||
|
color: var(--swatch-neutral);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* THIS STYLE SHEET MANAGES STYLE FOR OUR MODAL, i.e. DIALOG BOX */
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--black);
|
||||||
|
color: var(--swatch-text);
|
||||||
|
cursor: pointer;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.35s ease-in;
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.is-visible {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
position: relative;
|
||||||
|
max-width: 800px;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: var(--swatch-complement);
|
||||||
|
overflow: auto;
|
||||||
|
cursor: default;
|
||||||
|
border-width: 5px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-style: groove;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog > * {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
|
.modal-footer {
|
||||||
|
background: var(--lightgray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
|
.modal-close {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal p + p {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-control {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
font-size: 24pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#close-modal-button {
|
||||||
|
float:right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirm-cancel-container {
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ANIMATIONS
|
||||||
|
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||||
|
[data-animation] .modal-dialog {
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.5s var(--bounceEasing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation].is-visible .modal-dialog {
|
||||||
|
opacity: 1;
|
||||||
|
transition-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutDown"] .modal-dialog {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutTop"] .modal-dialog {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutLeft"] .modal-dialog {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutRight"] .modal-dialog {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="zoomInOut"] .modal-dialog {
|
||||||
|
transform: scale(0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="rotateInOutDown"] .modal-dialog {
|
||||||
|
transform-origin: top left;
|
||||||
|
transform: rotate(-1turn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="mixInAnimations"].is-visible .modal-dialog {
|
||||||
|
animation: mixInAnimations 2s 0.2s linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutDown"].is-visible .modal-dialog,
|
||||||
|
[data-animation="slideInOutTop"].is-visible .modal-dialog,
|
||||||
|
[data-animation="slideInOutLeft"].is-visible .modal-dialog,
|
||||||
|
[data-animation="slideInOutRight"].is-visible .modal-dialog,
|
||||||
|
[data-animation="zoomInOut"].is-visible .modal-dialog,
|
||||||
|
[data-animation="rotateInOutDown"].is-visible .modal-dialog {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mixInAnimations {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
10% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
transform: rotate(20deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
transform: rotate(-20deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
transform: rotate(15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: rotate(-10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
transform: rotate(5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
90% {
|
||||||
|
transform: rotate(-5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-button {
|
||||||
|
margin: 20px 0px 0px 0px;
|
||||||
|
border: 1px solid #2185d0;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: white;
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-content {
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: auto;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-list-item {
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-list-item:hover,
|
||||||
|
.dropdown-list-item:active {
|
||||||
|
background-color: #f3f3f3;
|
||||||
|
}
|
74
final/client/src/api/index.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
This is our http api, which we use to send requests to
|
||||||
|
our back-end API. Note we`re using the Axios library
|
||||||
|
for doing this, which is an easy to use AJAX-based
|
||||||
|
library. We could (and maybe should) use Fetch, which
|
||||||
|
is a native (to browsers) standard, but Axios is easier
|
||||||
|
to use when sending JSON back and forth and it`s a Promise-
|
||||||
|
based API which helps a lot with asynchronous communication.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
axios.defaults.withCredentials = true;
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: 'http://localhost:4000/api',
|
||||||
|
})
|
||||||
|
|
||||||
|
// THESE ARE ALL THE REQUESTS WE`LL BE MAKING, ALL REQUESTS HAVE A
|
||||||
|
// REQUEST METHOD (like get) AND PATH (like /top5list). SOME ALSO
|
||||||
|
// REQUIRE AN id SO THAT THE SERVER KNOWS ON WHICH LIST TO DO ITS
|
||||||
|
// WORK, AND SOME REQUIRE DATA, WHICH WE CALL THE payload, FOR WHEN
|
||||||
|
// WE NEED TO PUT THINGS INTO THE DATABASE OR IF WE HAVE SOME
|
||||||
|
// CUSTOM FILTERS FOR QUERIES
|
||||||
|
export const createTop5List = (payload) => api.post(`/top5list/`, payload)
|
||||||
|
//export const getAllTop5Lists = () => api.get(`/top5lists/`)
|
||||||
|
export const getTop5Lists = () => api.get(`/top5lists/`)
|
||||||
|
export const updateTop5ListById = (id, payload) => api.put(`/top5list/${id}`, payload)
|
||||||
|
export const publishTop5ListById = (id, payload) => api.put(`/publishtop5list/${id}`, payload)
|
||||||
|
export const deleteTop5ListById = (id) => api.delete(`/top5list/${id}`)
|
||||||
|
export const getTop5ListById = (id) => api.get(`/top5list/${id}`)
|
||||||
|
export const getCommunityLists = () => api.get(`/communitylists/`)
|
||||||
|
export const getCommunityListById = (id) => api.get(`/communitylist/${id}`)
|
||||||
|
export const updateTop5ListViews = (id) => api.get(`/top5listviews/${id}`)
|
||||||
|
export const updateTop5ListLikes = (id) => api.get(`/top5listlikes/${id}`)
|
||||||
|
export const updateTop5ListDislikes = (id) => api.get(`/top5listdislikes/${id}`)
|
||||||
|
export const updateCommunityListViews = (id) => api.get(`/communitylistviews/${id}`)
|
||||||
|
export const updateCommunityListLikes = (id) => api.get(`/communitylistlikes/${id}`)
|
||||||
|
export const updateCommunityListDislikes = (id) => api.get(`/communitylistdislikes/${id}`)
|
||||||
|
export const updateTop5ListComment = (id, payload) => api.put(`/commenttop5list/${id}`, payload)
|
||||||
|
export const updateCommunityListComment = (id, payload) => api.put(`/commentcommunitylist/${id}`, payload)
|
||||||
|
|
||||||
|
|
||||||
|
export const getLoggedIn = () => api.get(`/loggedIn/`);
|
||||||
|
export const registerUser = (payload) => api.post(`/register/`, payload)
|
||||||
|
export const loginUser = (payload) => api.post(`/login/`, payload)
|
||||||
|
export const logoutUser = () => api.get(`/logout/`)
|
||||||
|
|
||||||
|
const apis = {
|
||||||
|
createTop5List,
|
||||||
|
//getAllTop5Lists,
|
||||||
|
getTop5Lists,
|
||||||
|
updateTop5ListById,
|
||||||
|
deleteTop5ListById,
|
||||||
|
getTop5ListById,
|
||||||
|
getCommunityLists,
|
||||||
|
getCommunityListById,
|
||||||
|
publishTop5ListById,
|
||||||
|
updateTop5ListViews,
|
||||||
|
updateTop5ListLikes,
|
||||||
|
updateTop5ListDislikes,
|
||||||
|
updateCommunityListViews,
|
||||||
|
updateCommunityListLikes,
|
||||||
|
updateCommunityListDislikes,
|
||||||
|
updateTop5ListComment,
|
||||||
|
updateCommunityListComment,
|
||||||
|
|
||||||
|
getLoggedIn,
|
||||||
|
registerUser,
|
||||||
|
loginUser,
|
||||||
|
logoutUser
|
||||||
|
}
|
||||||
|
|
||||||
|
export default apis
|
151
final/client/src/auth/index.js
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
import React, { createContext, useEffect, useState } from "react";
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import api from '../api'
|
||||||
|
const AuthContext = createContext();
|
||||||
|
console.log("create AuthContext: " + AuthContext);
|
||||||
|
|
||||||
|
// THESE ARE ALL THE TYPES OF UPDATES TO OUR AUTH STATE THAT CAN BE PROCESSED
|
||||||
|
export const AuthActionType = {
|
||||||
|
LOGGEDIN_USER: "REGISTER_USER",
|
||||||
|
LOGOUT_USER: "LOGOUT_USER",
|
||||||
|
GUEST: "GUEST",
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthContextProvider(props) {
|
||||||
|
const [auth, setAuth] = useState({
|
||||||
|
user: null,
|
||||||
|
loggedIn: false
|
||||||
|
});
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
auth.getLoggedIn();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const authReducer = (action) => {
|
||||||
|
const { type, payload } = action;
|
||||||
|
switch (type) {
|
||||||
|
case AuthActionType.LOGIN_USER: {
|
||||||
|
return setAuth({
|
||||||
|
user: payload.user,
|
||||||
|
loggedIn: (payload.user.name!=='Guest'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case AuthActionType.GUEST: {
|
||||||
|
return setAuth({
|
||||||
|
user: {
|
||||||
|
firstName: 'Guest',
|
||||||
|
lastName: 'Guest',
|
||||||
|
name: 'Guest',
|
||||||
|
email: 'Guest@gmail.com'
|
||||||
|
},
|
||||||
|
loggedIn: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case AuthActionType.LOGOUT_USER: {
|
||||||
|
return setAuth({
|
||||||
|
user: null,
|
||||||
|
loggedIn: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.logInAsGuest = function() {
|
||||||
|
authReducer({
|
||||||
|
type: AuthActionType.GUEST,
|
||||||
|
payload: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.getLoggedIn = async function () {
|
||||||
|
try {
|
||||||
|
const response = await api.getLoggedIn();
|
||||||
|
if (response.status === 200) {
|
||||||
|
authReducer({
|
||||||
|
type: AuthActionType.LOGIN_USER,
|
||||||
|
payload: {
|
||||||
|
user: response.data.user
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.registerUser = async function(userData, store) {
|
||||||
|
try {
|
||||||
|
const response = await api.registerUser(userData);
|
||||||
|
if (response.status === 200) {
|
||||||
|
authReducer({
|
||||||
|
type: AuthActionType.LOGOUT_USER,
|
||||||
|
payload: null,
|
||||||
|
})
|
||||||
|
history.push("/");
|
||||||
|
store.loadIdNamePairs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.loginUser = async function(userData, store) {
|
||||||
|
try {
|
||||||
|
const response = await api.loginUser(userData);
|
||||||
|
if (response.status === 200) {
|
||||||
|
authReducer({
|
||||||
|
type: AuthActionType.LOGIN_USER,
|
||||||
|
payload: {
|
||||||
|
user: response.data.user
|
||||||
|
}
|
||||||
|
})
|
||||||
|
history.push("/");
|
||||||
|
store.loadIdNamePairs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.logoutUser = async function(store) {
|
||||||
|
try {
|
||||||
|
await api.logoutUser();
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
|
||||||
|
authReducer({
|
||||||
|
type: AuthActionType.LOGOUT_USER,
|
||||||
|
payload: null
|
||||||
|
});
|
||||||
|
store.logout();
|
||||||
|
document.cookie = '';
|
||||||
|
history.push("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{
|
||||||
|
auth
|
||||||
|
}}>
|
||||||
|
{props.children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthContext;
|
||||||
|
export { AuthContextProvider };
|
215
final/client/src/common/jsTPS.js
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
/**
|
||||||
|
* jsTPS_Transaction
|
||||||
|
*
|
||||||
|
* This provides the basic structure for a transaction class. Note to use
|
||||||
|
* jsTPS one should create objects that define these two methods, doTransaction
|
||||||
|
* and undoTransaction, which will update the application state accordingly.
|
||||||
|
*
|
||||||
|
* @author THE McKilla Gorilla (accept no imposters)
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
export class jsTPS_Transaction {
|
||||||
|
/**
|
||||||
|
* This method is called by jTPS when a transaction is executed.
|
||||||
|
*/
|
||||||
|
doTransaction() {
|
||||||
|
console.log("doTransaction - MISSING IMPLEMENTATION");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by jTPS when a transaction is undone.
|
||||||
|
*/
|
||||||
|
undoTransaction() {
|
||||||
|
console.log("undoTransaction - MISSING IMPLEMENTATION");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* jsTPS
|
||||||
|
*
|
||||||
|
* This class serves as the Transaction Processing System. Note that it manages
|
||||||
|
* a stack of jsTPS_Transaction objects, each of which know how to do or undo
|
||||||
|
* state changes for the given application. Note that this TPS is not platform
|
||||||
|
* specific as it is programmed in raw JavaScript.
|
||||||
|
*/
|
||||||
|
export default class jsTPS {
|
||||||
|
constructor() {
|
||||||
|
// THE TRANSACTION STACK
|
||||||
|
this.transactions = [];
|
||||||
|
|
||||||
|
// THE TOTAL NUMBER OF TRANSACTIONS ON THE STACK,
|
||||||
|
// INCLUDING THOSE THAT MAY HAVE ALREADY BEEN UNDONE
|
||||||
|
this.numTransactions = 0;
|
||||||
|
|
||||||
|
// THE INDEX OF THE MOST RECENT TRANSACTION, NOTE THAT
|
||||||
|
// THIS MAY BE IN THE MIDDLE OF THE TRANSACTION STACK
|
||||||
|
// IF SOME TRANSACTIONS ON THE STACK HAVE BEEN UNDONE
|
||||||
|
// AND STILL COULD BE REDONE.
|
||||||
|
this.mostRecentTransaction = -1;
|
||||||
|
|
||||||
|
// THESE STATE VARIABLES ARE TURNED ON AND OFF WHILE
|
||||||
|
// TRANSACTIONS ARE DOING THEIR WORK SO AS TO HELP
|
||||||
|
// MANAGE CONCURRENT UPDATES
|
||||||
|
this.performingDo = false;
|
||||||
|
this.performingUndo = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* isPerformingDo
|
||||||
|
*
|
||||||
|
* Accessor method for getting a boolean representing whether or not
|
||||||
|
* a transaction is currently in the midst of a do/redo operation.
|
||||||
|
*/
|
||||||
|
isPerformingDo() {
|
||||||
|
return this.performingDo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* isPerformingUndo
|
||||||
|
*
|
||||||
|
* Accessor method for getting a boolean representing whether or not
|
||||||
|
* a transaction is currently in the midst of an undo operation.
|
||||||
|
*/
|
||||||
|
isPerformingUndo() {
|
||||||
|
return this.performingUndo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getSize
|
||||||
|
*
|
||||||
|
* Accessor method for getting the number of transactions on the stack.
|
||||||
|
*/
|
||||||
|
getSize() {
|
||||||
|
return this.transactions.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getRedoSize
|
||||||
|
*
|
||||||
|
* Method for getting the total number of transactions on the stack
|
||||||
|
* that can possibly be redone.
|
||||||
|
*/
|
||||||
|
getRedoSize() {
|
||||||
|
return this.getSize() - this.mostRecentTransaction - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getUndoSize
|
||||||
|
*
|
||||||
|
* Method for getting the total number of transactions on the stack
|
||||||
|
* that can possible be undone.
|
||||||
|
*/
|
||||||
|
getUndoSize() {
|
||||||
|
return this.mostRecentTransaction + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hasTransactionToRedo
|
||||||
|
*
|
||||||
|
* Method for getting a boolean representing whether or not
|
||||||
|
* there are transactions on the stack that can be redone.
|
||||||
|
*/
|
||||||
|
hasTransactionToRedo() {
|
||||||
|
return (this.mostRecentTransaction+1) < this.numTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hasTransactionToUndo
|
||||||
|
*
|
||||||
|
* Method for getting a boolean representing whehter or not
|
||||||
|
* there are transactions on the stack that can be undone.
|
||||||
|
*/
|
||||||
|
hasTransactionToUndo() {
|
||||||
|
return this.mostRecentTransaction >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* addTransaction
|
||||||
|
*
|
||||||
|
* Method for adding a transaction to the TPS stack, note it
|
||||||
|
* also then does the transaction.
|
||||||
|
*
|
||||||
|
* @param {jsTPS_Transaction} transaction Transaction to add to the stack and do.
|
||||||
|
*/
|
||||||
|
addTransaction(transaction) {
|
||||||
|
// ARE WE BRANCHING?
|
||||||
|
if ((this.mostRecentTransaction < 0)
|
||||||
|
|| (this.mostRecentTransaction < (this.transactions.length - 1))) {
|
||||||
|
for (let i = this.transactions.length - 1; i > this.mostRecentTransaction; i--) {
|
||||||
|
this.transactions.splice(i, 1);
|
||||||
|
}
|
||||||
|
this.numTransactions = this.mostRecentTransaction + 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.numTransactions++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADD THE TRANSACTION
|
||||||
|
this.transactions[this.mostRecentTransaction+1] = transaction;
|
||||||
|
|
||||||
|
// AND EXECUTE IT
|
||||||
|
this.doTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* doTransaction
|
||||||
|
*
|
||||||
|
* Does the current transaction on the stack and advances the transaction
|
||||||
|
* counter. Note this function may be invoked as a result of either adding
|
||||||
|
* a transaction (which also does it), or redoing a transaction.
|
||||||
|
*/
|
||||||
|
doTransaction() {
|
||||||
|
if (this.hasTransactionToRedo()) {
|
||||||
|
this.performingDo = true;
|
||||||
|
let transaction = this.transactions[this.mostRecentTransaction+1];
|
||||||
|
transaction.doTransaction();
|
||||||
|
this.mostRecentTransaction++;
|
||||||
|
this.performingDo = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function gets the most recently executed transaction on the
|
||||||
|
* TPS stack and undoes it, moving the TPS counter accordingly.
|
||||||
|
*/
|
||||||
|
undoTransaction() {
|
||||||
|
if (this.hasTransactionToUndo()) {
|
||||||
|
this.performingUndo = true;
|
||||||
|
let transaction = this.transactions[this.mostRecentTransaction];
|
||||||
|
transaction.undoTransaction();
|
||||||
|
this.mostRecentTransaction--;
|
||||||
|
this.performingUndo = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clearAllTransactions
|
||||||
|
*
|
||||||
|
* Removes all the transactions from the TPS, leaving it with none.
|
||||||
|
*/
|
||||||
|
clearAllTransactions() {
|
||||||
|
// REMOVE ALL THE TRANSACTIONS
|
||||||
|
this.transactions = [];
|
||||||
|
|
||||||
|
// MAKE SURE TO RESET THE LOCATION OF THE
|
||||||
|
// TOP OF THE TPS STACK TOO
|
||||||
|
this.mostRecentTransaction = -1;
|
||||||
|
this.numTransactions = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* toString
|
||||||
|
*
|
||||||
|
* Builds and returns a textual represention of the full TPS and its stack.
|
||||||
|
*/
|
||||||
|
toString() {
|
||||||
|
let text = "--Number of Transactions: " + this.numTransactions + "\n";
|
||||||
|
text += "--Current Index on Stack: " + this.mostRecentTransaction + "\n";
|
||||||
|
text += "--Current Transaction Stack:\n";
|
||||||
|
for (let i = 0; i <= this.mostRecentTransaction; i++) {
|
||||||
|
let jT = this.transactions[i];
|
||||||
|
text += "----" + jT.toString() + "\n";
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
46
final/client/src/components/Alert.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { React, useContext} from "react";
|
||||||
|
import { GlobalStoreContext } from '../store';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Collapse from '@mui/material/Collapse';
|
||||||
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
|
||||||
|
export default function Alerts() {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
|
||||||
|
let msg = "";
|
||||||
|
let alertSatus = false;
|
||||||
|
if (store.alertMessage) {
|
||||||
|
msg = store.alertMessage;
|
||||||
|
}
|
||||||
|
if (store.alertSatus) {
|
||||||
|
alertSatus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box id='alert-box' sx={{ width: '100%' }}>
|
||||||
|
<Collapse in={alertSatus} id='collapse'>
|
||||||
|
<Alert id='alert'
|
||||||
|
severity="error"
|
||||||
|
action={
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
store.hideAlert();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon fontSize="inherit" />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
{msg}
|
||||||
|
</Alert>
|
||||||
|
</Collapse>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
131
final/client/src/components/AppBanner.js
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
import { useContext, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import AuthContext from '../auth';
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
import EditToolbar from './EditToolbar'
|
||||||
|
import AppBar from '@mui/material/AppBar';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import Menu from '@mui/material/Menu';
|
||||||
|
import AccountCircle from '@mui/icons-material/AccountCircle';
|
||||||
|
|
||||||
|
export default function AppBanner() {
|
||||||
|
const { auth } = useContext(AuthContext);
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const isMenuOpen = Boolean(anchorEl);
|
||||||
|
|
||||||
|
const handleProfileMenuOpen = (event) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
handleMenuClose();
|
||||||
|
auth.logoutUser(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGuest = () => {
|
||||||
|
handleMenuClose();
|
||||||
|
auth.logInAsGuest();
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuId = 'primary-search-account-menu';
|
||||||
|
const loggedOutMenu = (
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
id={menuId}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
open={isMenuOpen}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleMenuClose}><Link to='/register/'>Create New Account</Link></MenuItem>
|
||||||
|
<MenuItem onClick={handleMenuClose}><Link to='/login/'>Login</Link></MenuItem>
|
||||||
|
<MenuItem onClick={handleGuest}><Link to='/lists/'>Continue as Guest</Link></MenuItem>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
const loggedInMenu =
|
||||||
|
<Menu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
id={menuId}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
open={isMenuOpen}
|
||||||
|
onClose={handleMenuClose}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleLogout}>Logout</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
let editToolbar = "";
|
||||||
|
let menu = loggedOutMenu;
|
||||||
|
if (auth.loggedIn /*&& auth.user.name !== 'Guest'*/) {
|
||||||
|
menu = loggedInMenu;
|
||||||
|
if (store.currentList) {
|
||||||
|
editToolbar = <EditToolbar />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccountMenu(loggedIn) {
|
||||||
|
if (!loggedIn || auth.user.name === 'Guest') {
|
||||||
|
return <AccountCircle />;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return auth.user.firstName.charAt(0)+auth.user.lastName.charAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<AppBar position="static">
|
||||||
|
<Toolbar>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
noWrap
|
||||||
|
component="div"
|
||||||
|
sx={{ display: { xs: 'none', sm: 'block' } }}
|
||||||
|
>
|
||||||
|
<Link onClick={store.closeCurrentList} style={{ textDecoration: 'none', color: 'white' }} to='/'>T<sup>5</sup>L</Link>
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ flexGrow: 1 }}>{editToolbar}</Box>
|
||||||
|
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
edge="end"
|
||||||
|
aria-label="account of current user"
|
||||||
|
aria-controls={menuId}
|
||||||
|
aria-haspopup="true"
|
||||||
|
onClick={handleProfileMenuOpen}
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
{ getAccountMenu(auth.loggedIn) }
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
{
|
||||||
|
menu
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
15
final/client/src/components/Copyright.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
export default function Copyright(props) {
|
||||||
|
return (
|
||||||
|
<Typography variant="body2" color="text.secondary" align="center" {...props}>
|
||||||
|
{'Copyright © '}
|
||||||
|
<Link color="inherit" href="/">
|
||||||
|
The Top 5 Lister
|
||||||
|
</Link>{' '}
|
||||||
|
{new Date().getFullYear()}
|
||||||
|
{'.'}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
75
final/client/src/components/DeleteModal.js
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { React, useContext } from 'react';
|
||||||
|
import { GlobalStoreContext } from '../store';
|
||||||
|
import { styled, Box } from '@mui/system';
|
||||||
|
import ModalUnstyled from '@mui/core/ModalUnstyled';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
|
const StyledModal = styled(ModalUnstyled)`
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1300;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Backdrop = styled('div')`
|
||||||
|
z-index: -1;
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
width: 400,
|
||||||
|
bgcolor: '#ddd',
|
||||||
|
border: '2px solid #000',
|
||||||
|
p: 2,
|
||||||
|
px: 4,
|
||||||
|
pb: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ModalUnstyledDemo() {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
|
||||||
|
let msg = "";
|
||||||
|
let deleteStatus = false;
|
||||||
|
if (store.listMarkedForDeletion) {
|
||||||
|
msg = "Are you sure to delete list " + store.listMarkedForDeletion.name + "?";
|
||||||
|
deleteStatus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirm() {
|
||||||
|
store.deleteMarkedList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCancle() {
|
||||||
|
store.unmarkListForDeletion();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<StyledModal
|
||||||
|
aria-labelledby="unstyled-modal-title"
|
||||||
|
aria-describedby="unstyled-modal-description"
|
||||||
|
open={deleteStatus}
|
||||||
|
onClose={handleCancle}
|
||||||
|
BackdropComponent={Backdrop}
|
||||||
|
>
|
||||||
|
<Box sx={style}>
|
||||||
|
<h2 id="unstyled-modal-title">Double Check</h2>
|
||||||
|
<p id="unstyled-modal-description">{msg}</p>
|
||||||
|
<Button onClick={handleConfirm}>Confirm</Button>
|
||||||
|
<Button onClick={handleCancle}>Cancle</Button>
|
||||||
|
</Box>
|
||||||
|
</StyledModal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
67
final/client/src/components/EditToolbar.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { useContext } from 'react'
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
import { Button, TextField} from '@mui/material';
|
||||||
|
import CloseIcon from '@mui/icons-material/HighlightOff';
|
||||||
|
|
||||||
|
/*
|
||||||
|
This toolbar is a functional React component that
|
||||||
|
manages the undo/redo/close buttons.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
function EditToolbar() {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
store.updateCurrentList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePublish() {
|
||||||
|
store.publishCurrenList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
store.closeCurrentList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOnChange() {
|
||||||
|
store.checkValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
let valid = true;
|
||||||
|
if (store.valid) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="edit-toolbar">
|
||||||
|
<TextField
|
||||||
|
id='list-name'
|
||||||
|
onChange={handleOnChange}
|
||||||
|
defaultValue={store.currentList.name}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
id='save-button'
|
||||||
|
onClick={handleSave}
|
||||||
|
variant="contained">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
id='publish-button'
|
||||||
|
onClick={handlePublish}
|
||||||
|
disabled={valid}
|
||||||
|
variant="contained">
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
id='close-button'
|
||||||
|
onClick={handleClose}
|
||||||
|
variant="contained">
|
||||||
|
<CloseIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditToolbar;
|
208
final/client/src/components/HomeScreen.js
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
import React, { useContext, useEffect } from 'react'
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
import ListCard from './ListCard.js'
|
||||||
|
import { Button, Box, Typography, InputBase, Menu, MenuItem } from '@mui/material'
|
||||||
|
import HomeIcon from '@mui/icons-material/Home';
|
||||||
|
import GroupsIcon from '@mui/icons-material/Groups';
|
||||||
|
import PersonIcon from '@mui/icons-material/Person';
|
||||||
|
import FunctionsIcon from '@mui/icons-material/Functions';
|
||||||
|
import SortIcon from '@mui/icons-material/Sort';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import { DeleteModal } from '.';
|
||||||
|
import AuthContext from '../auth';
|
||||||
|
|
||||||
|
/*
|
||||||
|
This React component lists all the top5 lists in the UI.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
const HomeScreen = () => {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
const { auth } = useContext(AuthContext);
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
store.loadIdNamePairs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleLoadHome() {
|
||||||
|
if (auth.user.name !== 'Guest') {
|
||||||
|
store.loadHomeView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLoadGroup() {
|
||||||
|
store.loadGroupView();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLoadUser() {
|
||||||
|
store.loadUserView();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLoadCommunity() {
|
||||||
|
store.loadCommunityView();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFiltList(event) {
|
||||||
|
console.log('filting');
|
||||||
|
store.filter(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = (event) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleSortByNewest() {
|
||||||
|
handleClose();
|
||||||
|
store.sort(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSortByOldest() {
|
||||||
|
handleClose();
|
||||||
|
store.sort(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSortByViews() {
|
||||||
|
handleClose();
|
||||||
|
store.sort(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSortByLikes() {
|
||||||
|
handleClose();
|
||||||
|
store.sort(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSortByDislikes() {
|
||||||
|
handleClose();
|
||||||
|
store.sort(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let listCard = "";
|
||||||
|
if (store) {
|
||||||
|
listCard =
|
||||||
|
<List sx={{ width: '90%', left: '5%', bgcolor: 'var(--swatch-primary)' }}>
|
||||||
|
{
|
||||||
|
store.idNamePairs.map((pair) => (
|
||||||
|
<div><ListCard
|
||||||
|
key={pair._id}
|
||||||
|
idNamePair={pair}
|
||||||
|
selected={false}
|
||||||
|
/>
|
||||||
|
<List sx={{ width: '100%', height: '10%'}}></List></div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</List>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div id="top5-list-selector">
|
||||||
|
<DeleteModal />
|
||||||
|
<div id="list-selector-heading">
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
aria-label="home"
|
||||||
|
id="home-button"
|
||||||
|
disabled={auth.user.name === 'Guest'}
|
||||||
|
onClick={
|
||||||
|
(event) => {
|
||||||
|
handleLoadHome()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<HomeIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
aria-label="group"
|
||||||
|
id="group-button"
|
||||||
|
onClick={
|
||||||
|
(event) => {
|
||||||
|
handleLoadGroup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<GroupsIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
aria-label="user"
|
||||||
|
id="user-button"
|
||||||
|
onClick={
|
||||||
|
(event) => {
|
||||||
|
handleLoadUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PersonIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='primary'
|
||||||
|
aria-label='community'
|
||||||
|
id= 'community-button'
|
||||||
|
onClick={
|
||||||
|
(event) => {
|
||||||
|
handleLoadCommunity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FunctionsIcon />
|
||||||
|
</Button>
|
||||||
|
<InputBase sx={{width: '30%', maxWidth: '30%'}}
|
||||||
|
id='search'
|
||||||
|
placeholder=" Search…"
|
||||||
|
onChange={handleFiltList}
|
||||||
|
>
|
||||||
|
</InputBase>
|
||||||
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
|
<Box sx={{ display: { xs: 'none', md: 'flex' } }}>
|
||||||
|
<Box component="span" sx={{ p: 2}}>
|
||||||
|
<Typography variant='h6' id='sort-text'>SORT BY</Typography>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
color='primary'
|
||||||
|
aria-label='sort'
|
||||||
|
id= 'sort-button'
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<SortIcon />
|
||||||
|
</Button>
|
||||||
|
<Menu
|
||||||
|
id="sort"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'buttom',
|
||||||
|
horizontal: 'left',
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
MenuListProps={{
|
||||||
|
'aria-labelledby': 'basic-button',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleSortByNewest}>Published Date (Newest)</MenuItem>
|
||||||
|
<MenuItem onClick={handleSortByOldest}>Published Date (Oldest)</MenuItem>
|
||||||
|
<MenuItem onClick={handleSortByViews}>Views</MenuItem>
|
||||||
|
<MenuItem onClick={handleSortByLikes}>Likes</MenuItem>
|
||||||
|
<MenuItem onClick={handleSortByDislikes}>Dislikes</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
<div id="list-selector-list">
|
||||||
|
{
|
||||||
|
listCard
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomeScreen;
|
14
final/client/src/components/HomeWrapper.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { useContext } from 'react'
|
||||||
|
import HomeScreen from './HomeScreen'
|
||||||
|
import SplashScreen from './SplashScreen'
|
||||||
|
import AuthContext from '../auth'
|
||||||
|
|
||||||
|
export default function HomeWrapper() {
|
||||||
|
const { auth } = useContext(AuthContext);
|
||||||
|
console.log("HomeWrapper auth.loggedIn: " + auth.loggedIn);
|
||||||
|
|
||||||
|
if (auth.loggedIn)
|
||||||
|
return <HomeScreen />
|
||||||
|
else
|
||||||
|
return <SplashScreen />
|
||||||
|
}
|
395
final/client/src/components/ListCard.js
Normal file
|
@ -0,0 +1,395 @@
|
||||||
|
import { useContext, useState } from 'react'
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
import { Box, List, ListItem, IconButton, Grid, Typography, ListItemText, TextField, Link } from '@mui/material';
|
||||||
|
import api from '../api'
|
||||||
|
import AuthContext from '../auth'
|
||||||
|
import ThumbUpIcon from '@mui/icons-material/ThumbUp';
|
||||||
|
import ThumbDownIcon from '@mui/icons-material/ThumbDown';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is a card in our list of top 5 lists. It lets select
|
||||||
|
a list for editing and it has controls for changing its
|
||||||
|
name or deleting it.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
function ListCard(props) {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
const [ expandState, setExpandState] = useState(false);
|
||||||
|
const [ updated, setUpdated] = useState(false);
|
||||||
|
const { idNamePair } = props;
|
||||||
|
const { auth } = useContext(AuthContext);
|
||||||
|
|
||||||
|
function handleLoadList(event, id) {
|
||||||
|
if (!event.target.disabled) {
|
||||||
|
// CHANGE THE CURRENT LIST
|
||||||
|
store.setCurrentList(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteList(event, id) {
|
||||||
|
event.stopPropagation();
|
||||||
|
store.markListForDeletion(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let list = idNamePair;
|
||||||
|
async function updateList(id) {
|
||||||
|
try {
|
||||||
|
let response = '';
|
||||||
|
if (list.owner !== 'Community') {
|
||||||
|
response = await api.getTop5ListById(id);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
response = await api.getCommunityListById(id);
|
||||||
|
}
|
||||||
|
if (response.data.success) {
|
||||||
|
response = response.data.top5List;
|
||||||
|
list.likes = response.likes;
|
||||||
|
list.dislikes = response.dislikes;
|
||||||
|
list.views = response.views;
|
||||||
|
list.comments = response.comments;
|
||||||
|
list.publishedAt = response.publishedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function updateListViews(id) {
|
||||||
|
try {
|
||||||
|
if (list.owner !== 'Community') {
|
||||||
|
await api.updateTop5ListViews(id);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await api.updateCommunityListViews(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
handleExpand(id);
|
||||||
|
}
|
||||||
|
async function updateListLikes(id) {
|
||||||
|
try {
|
||||||
|
let response = '';
|
||||||
|
if (list.owner !== 'Community') {
|
||||||
|
response = await api.updateTop5ListLikes(id);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//response = await api.updateTop5ListLikes(id);
|
||||||
|
response = await api.updateCommunityListLikes(id);
|
||||||
|
}
|
||||||
|
if (response.data.success) {
|
||||||
|
response = response.data.top5List;
|
||||||
|
list.likes = response.likes;
|
||||||
|
list.dislikes = response.dislikes;
|
||||||
|
list.views = response.views;
|
||||||
|
list.comments = response.comments;
|
||||||
|
list.publishedAt = response.publishedAt;
|
||||||
|
document.getElementById('likes-'+id).innerHTML=response.likes;
|
||||||
|
document.getElementById('dislikes-'+id).innerHTML=response.dislikes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function updateListDislikes(id) {
|
||||||
|
try {
|
||||||
|
let response = '';
|
||||||
|
if (list.owner !== 'Community') {
|
||||||
|
response = await api.updateTop5ListDislikes(id);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
response = await api.updateCommunityListDislikes(id);
|
||||||
|
}
|
||||||
|
if (response.data.success) {
|
||||||
|
response = response.data.top5List;
|
||||||
|
list.likes = response.likes;
|
||||||
|
list.dislikes = response.dislikes;
|
||||||
|
list.views = response.views;
|
||||||
|
list.comments = response.comments;
|
||||||
|
list.publishedAt = response.publishedAt;
|
||||||
|
document.getElementById('likes-'+id).innerHTML=response.likes;
|
||||||
|
document.getElementById('dislikes-'+id).innerHTML=response.dislikes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function updateListComments(event, id) {
|
||||||
|
try {
|
||||||
|
if (event.code ==="Enter" && event.target.value) {
|
||||||
|
let response = '';
|
||||||
|
let payload = {
|
||||||
|
comment: event.target.value,
|
||||||
|
};
|
||||||
|
if (list.owner !== 'Community') {
|
||||||
|
response = await api.updateTop5ListComment(id, payload);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
response = await api.updateCommunityListComment(id, payload);
|
||||||
|
}
|
||||||
|
if (response.data.success) {
|
||||||
|
response = response.data.top5List;
|
||||||
|
list.likes = response.likes;
|
||||||
|
list.dislikes = response.dislikes;
|
||||||
|
list.views = response.views;
|
||||||
|
list.comments = response.comments;
|
||||||
|
list.publishedAt = response.publishedAt;
|
||||||
|
setUpdated(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExpand(id) {
|
||||||
|
await updateList(id);
|
||||||
|
setExpandState(!expandState);
|
||||||
|
}
|
||||||
|
|
||||||
|
let items = "";
|
||||||
|
if (expandState) {
|
||||||
|
items =
|
||||||
|
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
|
||||||
|
{
|
||||||
|
list.items.map((item, index) => (
|
||||||
|
<ListItem key={'item-'+(index+1)}>
|
||||||
|
<ListItemText primary={(index+1) + '. ' + item}/>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</List>;
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
items =
|
||||||
|
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
|
||||||
|
{
|
||||||
|
list.items.map((item, index) => (
|
||||||
|
<ListItem key={'item-'+(index+1)}>
|
||||||
|
<ListItemText primary={(index+1) + '. ' + item}/>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</List>;
|
||||||
|
setUpdated(false);
|
||||||
|
}
|
||||||
|
let comments = "";
|
||||||
|
if (expandState) {
|
||||||
|
comments =
|
||||||
|
//<List sx={{ width: '100%', height: '10%', bgcolor: 'background.paper' }}>
|
||||||
|
<List
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'auto',
|
||||||
|
maxHeight: 250,
|
||||||
|
height: 250,
|
||||||
|
'& ul': { padding: 0 },
|
||||||
|
}}
|
||||||
|
subheader={<li />}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
list.comments.map((comment, index) => (
|
||||||
|
<ListItem
|
||||||
|
key={'comment-'+(index+1)}>
|
||||||
|
<ListItemText secondary={'by ' + comment[0]} primary={<Typography variant='h5'>{comment[1]}</Typography>}/>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</List>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let backgroundColor = 'background.paper';
|
||||||
|
if (list.published) {
|
||||||
|
backgroundColor = '#f8f8f8';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
backgroundColor = '#e0f0ff';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let cardElement =
|
||||||
|
<Box sx={{ flexGrow: 1, borderRadius: 8, bgcolor: {backgroundColor}, borderColor: 'primary.main'}}>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ListItem
|
||||||
|
id={list._id}
|
||||||
|
key={list._id}
|
||||||
|
sx={{ marginTop: '15px', display: 'flex', p: 1 }}
|
||||||
|
style={{
|
||||||
|
fontSize: '48pt',
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ p: 1, flexGrow: 1 }}><ListItem> <ListItemText primary={<Typography variant='h3'>{list.name}</Typography>} secondary={<div><Typography>{'by ' + list.owner}</Typography><Typography>{
|
||||||
|
list.published?
|
||||||
|
<div><Typography> {'Views: ' + list.views}</Typography><Typography>{list.owner==='Community'?'Updated: ':'Published: '}{list.publishedAt[0]+'-'+list.publishedAt[1]+'-'+list.publishedAt[2]}</Typography></div>:
|
||||||
|
<Link herf='#' onClick={
|
||||||
|
(event) => {
|
||||||
|
handleLoadList(event, list._id);
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
edit</Link>}
|
||||||
|
</Typography></div>}/> </ListItem>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<IconButton aria-label='like'
|
||||||
|
onClick={
|
||||||
|
(event) => {
|
||||||
|
updateListLikes(list._id)
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
<ThumbUpIcon style={{fontSize:'24pt'}} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Typography id={'likes-'+list._id}>{list.likes}</Typography>
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<IconButton aria-label='dislike'
|
||||||
|
onClick={
|
||||||
|
(event) => {
|
||||||
|
updateListDislikes(list._id)
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
<ThumbDownIcon style={{fontSize:'24pt'}} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Typography id={'dislikes-'+list._id}>{list.dislikes}</Typography>
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<IconButton aria-label='expandMore' onClick={
|
||||||
|
(event) => {
|
||||||
|
updateListViews(list._id)
|
||||||
|
}}>
|
||||||
|
<ExpandMoreIcon style={{fontSize:'24pt'}} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
if (expandState) {
|
||||||
|
cardElement =
|
||||||
|
<Box sx={{ flexGrow: 1, borderRadius:8, bgcolor: {backgroundColor}}}>
|
||||||
|
<Grid container alignItems="flex-start" justifyContent="center" spacing={2}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<ListItem
|
||||||
|
id={list._id}
|
||||||
|
key={list._id}
|
||||||
|
sx={{ marginTop: '15px', display: 'flex', p: 1 }}
|
||||||
|
style={{
|
||||||
|
fontSize: '48pt',
|
||||||
|
width: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ p: 1, flexGrow: 1 }}><ListItem> <ListItemText primary={<Typography variant='h3'>{list.name}</Typography>} secondary={<div><Typography>{'by ' + list.owner}</Typography><Typography>{
|
||||||
|
list.published?
|
||||||
|
<div><Typography> {'Views: ' + list.views}</Typography><Typography>{list.owner==='Community'?'Updated: ':'Published: '}{list.publishedAt[0]+'-'+list.publishedAt[1]+'-'+list.publishedAt[2]}</Typography></div>:
|
||||||
|
<Link herf='#' onClick={
|
||||||
|
(event) => {
|
||||||
|
handleLoadList(event, list._id);
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
edit</Link>}
|
||||||
|
</Typography></div>}/> </ListItem>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<IconButton aria-label='like'
|
||||||
|
onClick={
|
||||||
|
(event) => {
|
||||||
|
updateListLikes(list._id)
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
<ThumbUpIcon style={{fontSize:'24pt'}} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Typography id={'likes-'+list._id}>{list.likes}</Typography>
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<IconButton aria-label='dislike'
|
||||||
|
onClick={
|
||||||
|
(event) => {
|
||||||
|
updateListDislikes(list._id)
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
<ThumbDownIcon style={{fontSize:'24pt'}} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Typography id={'dislikes-'+list._id}>{list.dislikes}</Typography>
|
||||||
|
<Box sx={{ p: 1 }}>
|
||||||
|
<IconButton aria-label='expandMore' onClick={
|
||||||
|
(event) => {
|
||||||
|
handleExpand(list._id)
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
<ExpandLessIcon style={{fontSize:'24pt'}} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={5.5} sx={{}}>
|
||||||
|
<Box sx={{bgcolor: 'black', border: '1px dashed grey'}}>{items}</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={5.5}>
|
||||||
|
<Box id={'comments-'+list._id} sx={{bgcolor: 'black', border: '1px dashed grey'}}>{comments}</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={2.5}>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={2.5}>
|
||||||
|
{
|
||||||
|
(auth.user.name===list.owner)?
|
||||||
|
<Link herf='#' onClick={
|
||||||
|
(event) => {
|
||||||
|
handleDeleteList(event, list._id);
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
delete</Link>:
|
||||||
|
<div/>
|
||||||
|
}
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={5}>
|
||||||
|
<TextField placeholder='Leave Your Comment here'
|
||||||
|
sx={{bgcolor: '#fff', width: 500, maxWidth: '100%'}}
|
||||||
|
onKeyPress={
|
||||||
|
(event) => {
|
||||||
|
updateListComments(event, list._id);
|
||||||
|
}
|
||||||
|
}/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
cardElement
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListCard;
|
123
final/client/src/components/LoginScreen.js
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import * as React from 'react';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
import AuthContext from '../auth'
|
||||||
|
import Copyright from './Copyright'
|
||||||
|
|
||||||
|
|
||||||
|
const theme = createTheme();
|
||||||
|
|
||||||
|
export default function SignInSide() {
|
||||||
|
const { auth } = useContext(AuthContext);
|
||||||
|
const { store } = useContext(GlobalStoreContext)
|
||||||
|
|
||||||
|
const handleSubmit = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const data = new FormData(event.currentTarget);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
auth.loginUser({
|
||||||
|
email: data.get('email'),
|
||||||
|
password: data.get('password')
|
||||||
|
}, store);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<Grid container component="main" sx={{ height: '100vh' }}>
|
||||||
|
<CssBaseline />
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={false}
|
||||||
|
sm={4}
|
||||||
|
md={7}
|
||||||
|
sx={{
|
||||||
|
backgroundImage: 'url(https://source.unsplash.com/random)',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundColor: (t) =>
|
||||||
|
t.palette.mode === 'light' ? t.palette.grey[50] : t.palette.grey[900],
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
my: 8,
|
||||||
|
mx: 4,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
|
||||||
|
<LockOutlinedIcon />
|
||||||
|
</Avatar>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
Sign in
|
||||||
|
</Typography>
|
||||||
|
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 1 }}>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
id="email"
|
||||||
|
label="Email Address"
|
||||||
|
name="email"
|
||||||
|
autoComplete="email"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Checkbox value="remember" color="primary" />}
|
||||||
|
label="Remember me"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
sx={{ mt: 3, mb: 2 }}
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs>
|
||||||
|
<Link href="#" variant="body2">
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Link href="#" variant="body2">
|
||||||
|
{"Don't have an account? Sign Up"}
|
||||||
|
</Link>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Copyright sx={{ mt: 5 }} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
125
final/client/src/components/RegisterScreen.js
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import AuthContext from '../auth'
|
||||||
|
import Copyright from './Copyright'
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Container from '@mui/material/Container';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
|
||||||
|
export default function RegisterScreen() {
|
||||||
|
const { auth } = useContext(AuthContext);
|
||||||
|
const { store } = useContext(GlobalStoreContext)
|
||||||
|
|
||||||
|
const handleSubmit = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const formData = new FormData(event.currentTarget);
|
||||||
|
auth.registerUser({
|
||||||
|
firstName: formData.get('firstName'),
|
||||||
|
lastName: formData.get('lastName'),
|
||||||
|
email: formData.get('email'),
|
||||||
|
password: formData.get('password'),
|
||||||
|
passwordVerify: formData.get('passwordVerify')
|
||||||
|
}, store);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container component="main" maxWidth="xs">
|
||||||
|
<CssBaseline />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginTop: 8,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
|
||||||
|
<LockOutlinedIcon />
|
||||||
|
</Avatar>
|
||||||
|
<Typography component="h1" variant="h5">
|
||||||
|
Sign up
|
||||||
|
</Typography>
|
||||||
|
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField
|
||||||
|
autoComplete="fname"
|
||||||
|
name="firstName"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
id="firstName"
|
||||||
|
label="First Name"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
id="lastName"
|
||||||
|
label="Last Name"
|
||||||
|
name="lastName"
|
||||||
|
autoComplete="lname"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
id="email"
|
||||||
|
label="Email Address"
|
||||||
|
name="email"
|
||||||
|
autoComplete="email"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
name="passwordVerify"
|
||||||
|
label="Password Verify"
|
||||||
|
type="password"
|
||||||
|
id="passwordVerify"
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
sx={{ mt: 3, mb: 2 }}
|
||||||
|
>
|
||||||
|
Sign Up
|
||||||
|
</Button>
|
||||||
|
<Grid container justifyContent="flex-end">
|
||||||
|
<Grid item>
|
||||||
|
<Link href="/login/" variant="body2">
|
||||||
|
Already have an account? Sign in
|
||||||
|
</Link>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Copyright sx={{ mt: 5 }} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
10
final/client/src/components/SplashScreen.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export default function SplashScreen() {
|
||||||
|
return (
|
||||||
|
<div id="splash-screen">
|
||||||
|
Welcome<br />
|
||||||
|
To<br/>
|
||||||
|
Top5List
|
||||||
|
<div id = "author"> by Xinyu Xu</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
68
final/client/src/components/Statusbar.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { useContext } from 'react'
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
import { Typography, Fab } from '@mui/material'
|
||||||
|
import AddIcon from '@mui/icons-material/Add'
|
||||||
|
|
||||||
|
/*
|
||||||
|
Our Status bar React component goes at the bottom of our UI.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
|
||||||
|
function Statusbar() {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
function handleCreateNewList() {
|
||||||
|
store.createNewList();
|
||||||
|
}
|
||||||
|
/*if (store.currentList){
|
||||||
|
return (
|
||||||
|
<div id="top5-statusbar">
|
||||||
|
<Typography variant="h4">{store.currentList.name}</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (<div></div>);
|
||||||
|
}*/
|
||||||
|
if (store.currentState === 1) {
|
||||||
|
return(
|
||||||
|
<div id="top5-statusbar">
|
||||||
|
<Fab
|
||||||
|
color="primary"
|
||||||
|
aria-label="add"
|
||||||
|
id="add-list-button"
|
||||||
|
onClick={handleCreateNewList}
|
||||||
|
>
|
||||||
|
<AddIcon />
|
||||||
|
</Fab>
|
||||||
|
<Typography variant="h2">Your Lists</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (store.currentState === 2){
|
||||||
|
return(
|
||||||
|
<div id='top5-statusbar'>
|
||||||
|
<Typography variant="h4">{((store.search)?store.search:'All') + ' Lists'}</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (store.currentState === 3){
|
||||||
|
return(
|
||||||
|
<div id='top5-statusbar'>
|
||||||
|
<Typography variant="h4">{((store.search)?store.search:'User') + ' Lists'}</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (store.currentState === 4){
|
||||||
|
return(
|
||||||
|
<div id='top5-statusbar'>
|
||||||
|
<Typography variant="h4">{((store.search)?store.search:'Community') + ' Lists'}</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Statusbar;
|
30
final/client/src/components/Top5Item.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { React, useContext } from "react";
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
/*
|
||||||
|
This React component represents a single item in our
|
||||||
|
Top 5 List, which can be edited or moved around.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
function Top5Item(props) {
|
||||||
|
let { index } = props;
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
|
||||||
|
async function handleOnChange() {
|
||||||
|
store.checkValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
id={'item-' + (index+1)}
|
||||||
|
key={props.key}
|
||||||
|
className='top5-item-editting'
|
||||||
|
type='text'
|
||||||
|
onChange={handleOnChange}
|
||||||
|
defaultValue={props.text}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Top5Item;
|
46
final/client/src/components/WorkspaceScreen.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { useContext } from 'react'
|
||||||
|
import Top5Item from './Top5Item.js'
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import { Typography } from '@mui/material'
|
||||||
|
import { GlobalStoreContext } from '../store/index.js'
|
||||||
|
/*
|
||||||
|
This React component lets us edit a loaded list, which only
|
||||||
|
happens when we are on the proper route.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
function WorkspaceScreen() {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
|
||||||
|
let editItems = "";
|
||||||
|
if (store.currentList) {
|
||||||
|
editItems =
|
||||||
|
<List id="edit-items" sx={{ width: '100%', bgcolor: 'background.paper' }}>
|
||||||
|
{
|
||||||
|
store.currentList.items.map((item, index) => (
|
||||||
|
<Top5Item
|
||||||
|
key={'top5-item-' + (index+1)}
|
||||||
|
text={item}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</List>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div id="top5-workspace">
|
||||||
|
<div id="workspace-edit">
|
||||||
|
<div id="edit-numbering">
|
||||||
|
<div className="item-number"><Typography variant="h3">1.</Typography></div>
|
||||||
|
<div className="item-number"><Typography variant="h3">2.</Typography></div>
|
||||||
|
<div className="item-number"><Typography variant="h3">3.</Typography></div>
|
||||||
|
<div className="item-number"><Typography variant="h3">4.</Typography></div>
|
||||||
|
<div className="item-number"><Typography variant="h3">5.</Typography></div>
|
||||||
|
</div>
|
||||||
|
{editItems}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WorkspaceScreen;
|
34
final/client/src/components/index.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import AppBanner from './AppBanner'
|
||||||
|
import EditToolbar from './EditToolbar'
|
||||||
|
import HomeScreen from './HomeScreen'
|
||||||
|
import HomeWrapper from './HomeWrapper'
|
||||||
|
import ListCard from './ListCard'
|
||||||
|
import RegisterScreen from './RegisterScreen'
|
||||||
|
import SplashScreen from './SplashScreen'
|
||||||
|
import Statusbar from './Statusbar'
|
||||||
|
import Top5Item from './Top5Item'
|
||||||
|
import WorkspaceScreen from './WorkspaceScreen'
|
||||||
|
import LoginScreen from './LoginScreen'
|
||||||
|
import Alert from './Alert'
|
||||||
|
import DeleteModal from './DeleteModal'
|
||||||
|
/*
|
||||||
|
This serves as a module so that we can import
|
||||||
|
all the other components as we wish.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
export {
|
||||||
|
AppBanner,
|
||||||
|
EditToolbar,
|
||||||
|
HomeScreen,
|
||||||
|
HomeWrapper,
|
||||||
|
ListCard,
|
||||||
|
RegisterScreen,
|
||||||
|
SplashScreen,
|
||||||
|
Statusbar,
|
||||||
|
Top5Item,
|
||||||
|
WorkspaceScreen,
|
||||||
|
Alert,
|
||||||
|
LoginScreen,
|
||||||
|
DeleteModal,
|
||||||
|
}
|
31
final/client/src/index.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import App from './App';
|
||||||
|
//import { AuthContextProvider } from './auth';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
/*
|
||||||
|
This is the entry-point for our application. Notice that we
|
||||||
|
inject our store into all the components in our application.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
/*import { GlobalStoreContext, useGlobalStore } from './store'
|
||||||
|
const AppWrapper = () => {
|
||||||
|
const store = useGlobalStore();
|
||||||
|
return (
|
||||||
|
<GlobalStoreContext.Provider value={store}>
|
||||||
|
<App />
|
||||||
|
</GlobalStoreContext.Provider>
|
||||||
|
)
|
||||||
|
}*/
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
13
final/client/src/reportWebVitals.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
const reportWebVitals = onPerfEntry => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
782
final/client/src/store/index.js
Normal file
|
@ -0,0 +1,782 @@
|
||||||
|
import { createContext, useContext, useState } from 'react'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import api from '../api'
|
||||||
|
import AuthContext from '../auth'
|
||||||
|
/*
|
||||||
|
This is our global data store. Note that it uses the Flux design pattern,
|
||||||
|
which makes use of things like actions and reducers.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
|
||||||
|
// THIS IS THE CONTEXT WE'LL USE TO SHARE OUR STORE
|
||||||
|
export const GlobalStoreContext = createContext({});
|
||||||
|
|
||||||
|
// THESE ARE ALL THE TYPES OF UPDATES TO OUR GLOBAL
|
||||||
|
// DATA STORE STATE THAT CAN BE PROCESSED
|
||||||
|
export const GlobalStoreActionType = {
|
||||||
|
CLOSE_CURRENT_LIST: "CLOSE_CURRENT_LIST",
|
||||||
|
CREATE_NEW_LIST: "CREATE_NEW_LIST",
|
||||||
|
//LOAD_ID_NAME_PAIRS: "LOAD_ID_NAME_PAIRS",
|
||||||
|
MARK_LIST_FOR_DELETION: "MARK_LIST_FOR_DELETION",
|
||||||
|
UNMARK_LIST_FOR_DELETION: "UNMARK_LIST_FOR_DELETION",
|
||||||
|
SET_CURRENT_LIST: "SET_CURRENT_LIST",
|
||||||
|
SHOW_ALERT: "SHOW_ALERT",
|
||||||
|
HIDE_ALERT: "HIDE_ALERT",
|
||||||
|
LOAD_HOME_VIEW: "LOAD_HOME_VIEW",
|
||||||
|
LOAD_GROUP_VIEW: "LOAD_GROUP_VIEW",
|
||||||
|
LOAD_USER_VIEW: "LOAD_USER_VIEW",
|
||||||
|
LOAD_COMMUNITY_VIEW: "LOAD_COMMUNITY_VIEW",
|
||||||
|
FILT_LIST: "FILT_LIST",
|
||||||
|
SORT_LIST: "SORT_LIST",
|
||||||
|
LOG_OUT: "LOG_OUT",
|
||||||
|
}
|
||||||
|
|
||||||
|
// WE'LL NEED THIS TO PROCESS TRANSACTIONS
|
||||||
|
|
||||||
|
// WITH THIS WE'RE MAKING OUR GLOBAL DATA STORE
|
||||||
|
// AVAILABLE TO THE REST OF THE APPLICATION
|
||||||
|
function GlobalStoreContextProvider(props) {
|
||||||
|
// THESE ARE ALL THE THINGS OUR DATA STORE WILL MANAGE
|
||||||
|
const [store, setStore] = useState({
|
||||||
|
totalNamePairs: [],
|
||||||
|
idNamePairs: [],
|
||||||
|
currentList: null,
|
||||||
|
currentState: 0,
|
||||||
|
newListCounter: 0,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: '',
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
// SINCE WE'VE WRAPPED THE STORE IN THE AUTH CONTEXT WE CAN ACCESS THE USER HERE
|
||||||
|
const { auth } = useContext(AuthContext);
|
||||||
|
|
||||||
|
// HERE'S THE DATA STORE'S REDUCER, IT MUST
|
||||||
|
// HANDLE EVERY TYPE OF STATE CHANGE
|
||||||
|
const storeReducer = (action) => {
|
||||||
|
const { type, payload } = action;
|
||||||
|
switch (type) {
|
||||||
|
// STOP EDITING THE CURRENT LIST
|
||||||
|
case GlobalStoreActionType.CLOSE_CURRENT_LIST: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: store.totalNamePairs,
|
||||||
|
idNamePairs: store.idNamePairs,
|
||||||
|
currentState: 1,
|
||||||
|
currentList: null,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: store.search,
|
||||||
|
valid: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// CREATE A NEW LIST
|
||||||
|
case GlobalStoreActionType.CREATE_NEW_LIST: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: store.totalNamePairs,
|
||||||
|
idNamePairs: store.idNamePairs,
|
||||||
|
currentState: 1,
|
||||||
|
currentList: payload,
|
||||||
|
newListCounter: store.newListCounter + 1,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: store.search,
|
||||||
|
valid: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// GET ALL THE LISTS SO WE CAN PRESENT THEM
|
||||||
|
/*case GlobalStoreActionType.LOAD_ID_NAME_PAIRS: {
|
||||||
|
return setStore({
|
||||||
|
idNamePairs: payload,
|
||||||
|
currentState: store.listState,
|
||||||
|
currentList: null,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
});
|
||||||
|
}*/
|
||||||
|
// PREPARE TO DELETE A LIST
|
||||||
|
case GlobalStoreActionType.MARK_LIST_FOR_DELETION: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: store.totalNamePairs,
|
||||||
|
idNamePairs: store.idNamePairs,
|
||||||
|
currentState: store.listState,
|
||||||
|
currentList: null,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: payload,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: store.search,
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// PREPARE TO DELETE A LIST
|
||||||
|
case GlobalStoreActionType.UNMARK_LIST_FOR_DELETION: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: store.totalNamePairs,
|
||||||
|
idNamePairs: store.idNamePairs,
|
||||||
|
currentState: store.listState,
|
||||||
|
currentList: null,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: store.search,
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// UPDATE A LIST
|
||||||
|
case GlobalStoreActionType.SET_CURRENT_LIST: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: store.totalNamePairs,
|
||||||
|
idNamePairs: store.idNamePairs,
|
||||||
|
currentState: store.listState,
|
||||||
|
currentList: payload,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
isItemEditActive: false,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: store.search,
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case GlobalStoreActionType.SHOW_ALERT: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: store.totalNamePairs,
|
||||||
|
idNamePairs: store.idNamePairs,
|
||||||
|
currentState: store.listState,
|
||||||
|
currentList: store.currentList,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: false,
|
||||||
|
alertMessage: payload,
|
||||||
|
alertSatus: true,
|
||||||
|
search: store.search,
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case GlobalStoreActionType.HIDE_ALERT: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: store.totalNamePairs,
|
||||||
|
idNamePairs: store.idNamePairs,
|
||||||
|
currentState: store.listState,
|
||||||
|
currentList: store.currentList,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: store.listMarkedForDeletion,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: store.search,
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case GlobalStoreActionType.LOAD_HOME_VIEW: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: payload,
|
||||||
|
idNamePairs: payload,
|
||||||
|
currentState: 1,
|
||||||
|
currentList: null,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: null,
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case GlobalStoreActionType.LOAD_GROUP_VIEW: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: payload,
|
||||||
|
idNamePairs: payload,
|
||||||
|
currentState: 2,
|
||||||
|
currentList: null,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: null,
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case GlobalStoreActionType.LOAD_USER_VIEW: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: payload,
|
||||||
|
idNamePairs: [],
|
||||||
|
currentState: 3,
|
||||||
|
currentList: null,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: null,
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case GlobalStoreActionType.LOAD_COMMUNITY_VIEW: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: payload,
|
||||||
|
idNamePairs: payload,
|
||||||
|
currentState: 4,
|
||||||
|
currentList: null,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: null,
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case GlobalStoreActionType.FILT_LIST: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: store.totalNamePairs,
|
||||||
|
idNamePairs: payload.pairs,
|
||||||
|
currentState: store.currentState,
|
||||||
|
currentList: null,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: payload.search,
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case GlobalStoreActionType.SORT_LIST: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: payload.totalLists,
|
||||||
|
idNamePairs: payload.lists,
|
||||||
|
currentState: store.currentState,
|
||||||
|
currentList: null,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: store.search,
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case GlobalStoreActionType.CHECK_VALID: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: store.totalLists,
|
||||||
|
idNamePairs: store.idNamePairs,
|
||||||
|
currentState: store.currentState,
|
||||||
|
currentList: store.currentList,
|
||||||
|
newListCounter: store.newListCounter,
|
||||||
|
listMarkedForDeletion: store.listMarkedForDeletion,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: "",
|
||||||
|
valid: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case GlobalStoreActionType.LOG_OUT: {
|
||||||
|
return setStore({
|
||||||
|
totalNamePairs: [],
|
||||||
|
idNamePairs: [],
|
||||||
|
currentState: 0,
|
||||||
|
currentList: null,
|
||||||
|
newListCounter: 0,
|
||||||
|
listMarkedForDeletion: null,
|
||||||
|
alertMessage: "",
|
||||||
|
alertSatus: false,
|
||||||
|
search: "",
|
||||||
|
valid: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// THIS FUNCTION PROCESSES CLOSING THE CURRENTLY LOADED LIST
|
||||||
|
store.closeCurrentList = function () {
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.CLOSE_CURRENT_LIST,
|
||||||
|
payload: {}
|
||||||
|
});
|
||||||
|
history.push("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FUNCTION CREATES A NEW LIST
|
||||||
|
store.createNewList = async function () {
|
||||||
|
let newListName = "Untitled" + store.newListCounter;
|
||||||
|
let payload = {
|
||||||
|
name: newListName,
|
||||||
|
items: ["?", "?", "?", "?", "?"],
|
||||||
|
published: false,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const response = await api.createTop5List(payload);
|
||||||
|
if (response.data.success) {
|
||||||
|
let newList = response.data.top5List;
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.CREATE_NEW_LIST,
|
||||||
|
payload: newList
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// IF IT'S A VALID LIST THEN LET'S START EDITING IT
|
||||||
|
history.push("/top5list/" + newList._id);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("API FAILED TO CREATE A NEW LIST");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS FUNCTION LOADS ALL THE ID, NAME PAIRS SO WE CAN LIST ALL THE LISTS
|
||||||
|
store.loadIdNamePairs = async function () {
|
||||||
|
if (auth.user && auth.user.name !== 'Guest') {
|
||||||
|
await store.loadHomeView();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await store.loadGroupView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.loadHomeView = async function() {
|
||||||
|
try {
|
||||||
|
const response = await api.getTop5Lists();
|
||||||
|
if (response.data.success) {
|
||||||
|
let pairsArray = response.data.idNamePairs.filter( list => list.owner === auth.user.name);
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.LOAD_HOME_VIEW,
|
||||||
|
payload: pairsArray,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("API FAILED TO GET THE LIST PAIRS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
document.getElementById('search').value = ''
|
||||||
|
history.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
store.loadGroupView = async function() {
|
||||||
|
try {
|
||||||
|
const response = await api.getTop5Lists();
|
||||||
|
if (response.data.success) {
|
||||||
|
let pairsArray = response.data.idNamePairs.filter( list => list.published && list.owner !== 'Community');
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.LOAD_GROUP_VIEW,
|
||||||
|
payload: pairsArray,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("API FAILED TO GET THE LIST PAIRS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
document.getElementById('search').value = ''
|
||||||
|
history.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
store.loadUserView = async function() {
|
||||||
|
try {
|
||||||
|
const response = await api.getTop5Lists();
|
||||||
|
if (response.data.success) {
|
||||||
|
let pairsArray = response.data.idNamePairs.filter( list => list.published && list.owner !== 'Community');
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.LOAD_USER_VIEW,
|
||||||
|
payload: pairsArray,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("API FAILED TO GET THE LIST PAIRS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
document.getElementById('search').value = ''
|
||||||
|
history.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
store.loadCommunityView = async function() {
|
||||||
|
try {
|
||||||
|
const response = await api.getCommunityLists();
|
||||||
|
if (response.data.success) {
|
||||||
|
let pairsArray = response.data.idNamePairs.filter( list => list.owner === 'Community');
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.LOAD_COMMUNITY_VIEW,
|
||||||
|
payload: pairsArray,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("API FAILED TO GET THE LIST PAIRS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
document.getElementById('search').value = ''
|
||||||
|
history.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
store.logout = async function() {
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.LOG_OUT,
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
store.filter = async function(str) {
|
||||||
|
let string = str;
|
||||||
|
str = str.toUpperCase();
|
||||||
|
let lists = store.totalNamePairs;
|
||||||
|
if (store.currentState !== 3) {
|
||||||
|
lists = lists.filter( list => list.name.toUpperCase().startsWith(str));
|
||||||
|
}
|
||||||
|
else if (!str) {
|
||||||
|
lists = [];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lists = lists.filter( list => list.owner.toUpperCase().startsWith(str));
|
||||||
|
}
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.FILT_LIST,
|
||||||
|
payload: {
|
||||||
|
pairs: lists,
|
||||||
|
search: string,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
store.sort = async function(num) {
|
||||||
|
let totalLists = store.totalNamePairs;
|
||||||
|
let lists = store.idNamePairs;
|
||||||
|
if (num === 0) {
|
||||||
|
totalLists.sort( function(a, b) {
|
||||||
|
if (a.publishedAt[0] > b.publishedAt[0]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (a.publishedAt[0] < b.publishedAt[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (a.publishedAt[1] > b.publishedAt[1]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (a.publishedAt[1] < b.publishedAt[1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (a.publishedAt[2] >= b.publishedAt[2]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
lists.sort( function(a, b) {
|
||||||
|
if (a.publishedAt[0] > b.publishedAt[0]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (a.publishedAt[0] < b.publishedAt[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (a.publishedAt[1] > b.publishedAt[1]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (a.publishedAt[1] < b.publishedAt[1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (a.publishedAt[2] >= b.publishedAt[2]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (num === 1) {
|
||||||
|
totalLists.sort( function(a, b) {
|
||||||
|
if (a.publishedAt[0] < b.publishedAt[0]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (a.publishedAt[0] > b.publishedAt[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (a.publishedAt[1] < b.publishedAt[1]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (a.publishedAt[1] > b.publishedAt[1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (a.publishedAt[2] <= b.publishedAt[2]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
lists.sort( function(a, b) {
|
||||||
|
if (a.publishedAt[0] < b.publishedAt[0]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (a.publishedAt[0] > b.publishedAt[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (a.publishedAt[1] < b.publishedAt[1]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (a.publishedAt[1] > b.publishedAt[1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (a.publishedAt[2] <= b.publishedAt[2]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (num === 2) {
|
||||||
|
totalLists.sort( (a,b) => b.views - a.views);
|
||||||
|
lists.sort( (a,b) => b.views - a.views);
|
||||||
|
}
|
||||||
|
if (num === 3) {
|
||||||
|
totalLists.sort( (a,b) => b.likes - a.likes);
|
||||||
|
lists.sort( (a,b) => b.likes - a.likes);
|
||||||
|
}
|
||||||
|
if (num === 4) {
|
||||||
|
totalLists.sort( (a,b) => b.dislikes - a.dislikes);
|
||||||
|
lists.sort( (a,b) => b.dislikes - a.dislikes);
|
||||||
|
}
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.SORT_LIST,
|
||||||
|
payload: {
|
||||||
|
totalLists: totalLists,
|
||||||
|
lists: lists,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// THE FOLLOWING 5 FUNCTIONS ARE FOR COORDINATING THE DELETION
|
||||||
|
// OF A LIST, WHICH INCLUDES USING A VERIFICATION MODAL. THE
|
||||||
|
// FUNCTIONS ARE markListForDeletion, deleteList, deleteMarkedList,
|
||||||
|
// showDeleteListModal, and hideDeleteListModal
|
||||||
|
store.markListForDeletion = async function (id) {
|
||||||
|
try {
|
||||||
|
// GET THE LIST
|
||||||
|
let response = await api.getTop5ListById(id);
|
||||||
|
if (response.data.success) {
|
||||||
|
let top5List = response.data.top5List;
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.MARK_LIST_FOR_DELETION,
|
||||||
|
payload: top5List
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.deleteList = async function (listToDelete) {
|
||||||
|
try {
|
||||||
|
let response = await api.deleteTop5ListById(listToDelete._id);
|
||||||
|
if (response.data.success) {
|
||||||
|
store.loadIdNamePairs();
|
||||||
|
history.push("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
store.unmarkListForDeletion();
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.deleteMarkedList = function () {
|
||||||
|
store.deleteList(store.listMarkedForDeletion);
|
||||||
|
}
|
||||||
|
|
||||||
|
store.unmarkListForDeletion = function () {
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.UNMARK_LIST_FOR_DELETION,
|
||||||
|
payload: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE FOLLOWING 8 FUNCTIONS ARE FOR COORDINATING THE UPDATING
|
||||||
|
// OF A LIST, WHICH INCLUDES DEALING WITH THE TRANSACTION STACK. THE
|
||||||
|
// FUNCTIONS ARE setCurrentList, addMoveItemTransaction, addUpdateItemTransaction,
|
||||||
|
// moveItem, updateItem, updateCurrentList, undo, and redo
|
||||||
|
store.setCurrentList = async function (id) {
|
||||||
|
try {
|
||||||
|
let response = await api.getTop5ListById(id);
|
||||||
|
if (response.data.success) {
|
||||||
|
let top5List = response.data.top5List;
|
||||||
|
|
||||||
|
//response = await api.updateTop5ListById(top5List._id, top5List);
|
||||||
|
if (response.data.success) {
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.SET_CURRENT_LIST,
|
||||||
|
payload: top5List
|
||||||
|
});
|
||||||
|
history.push("/top5list/" + top5List._id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.checkValid = async function () {
|
||||||
|
let body = {
|
||||||
|
name: document.getElementById("list-name").value,
|
||||||
|
items: [
|
||||||
|
document.getElementById("item-1").value,
|
||||||
|
document.getElementById("item-2").value,
|
||||||
|
document.getElementById("item-3").value,
|
||||||
|
document.getElementById("item-4").value,
|
||||||
|
document.getElementById("item-5").value,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let valid = true;
|
||||||
|
if (!body.name || !body.items[0] || !body.items[1] || !body.items[2] || !body.items[3] || !body.items[4]) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
if (body.items[0] === body.items[1] || body.items[0] === body.items[2] || body.items[0] === body.items[3] || body.items[0] === body.items[4] || body.items[1] === body.items[2] || body.items[1] === body.items[3] || body.items[1] === body.items[4] || body.items[2] === body.items[3] || body.items[2] === body.items[4] || body.items[3] === body.items[4]) {
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.CHECK_VALID,
|
||||||
|
payload: valid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
store.updateCurrentList = async function () {
|
||||||
|
try {
|
||||||
|
let payload = {
|
||||||
|
name: document.getElementById("list-name").value,
|
||||||
|
items: [
|
||||||
|
document.getElementById("item-1").value,
|
||||||
|
document.getElementById("item-2").value,
|
||||||
|
document.getElementById("item-3").value,
|
||||||
|
document.getElementById("item-4").value,
|
||||||
|
document.getElementById("item-5").value,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const response = await api.updateTop5ListById(store.currentList._id, payload);
|
||||||
|
if (response.data.success) {
|
||||||
|
store.loadIdNamePairs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.publishCurrenList = async function () {
|
||||||
|
try {
|
||||||
|
let payload = {
|
||||||
|
name: document.getElementById("list-name").value,
|
||||||
|
items: [
|
||||||
|
document.getElementById("item-1").value,
|
||||||
|
document.getElementById("item-2").value,
|
||||||
|
document.getElementById("item-3").value,
|
||||||
|
document.getElementById("item-4").value,
|
||||||
|
document.getElementById("item-5").value,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const response = await api.publishTop5ListById(store.currentList._id, payload);
|
||||||
|
console.log(response);
|
||||||
|
if (response.data.success) {
|
||||||
|
store.loadIdNamePairs();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("API FAILED TO PUBLISH A NEW LIST");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
try {
|
||||||
|
store.showAlert(err.response.data.errorMessage);
|
||||||
|
}
|
||||||
|
catch{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
store.showAlert = function (msg) {
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.SHOW_ALERT,
|
||||||
|
payload: msg
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
store.hideAlert = function () {
|
||||||
|
storeReducer({
|
||||||
|
type: GlobalStoreActionType.HIDE_ALERT,
|
||||||
|
payload: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GlobalStoreContext.Provider value={{
|
||||||
|
store
|
||||||
|
}}>
|
||||||
|
{props.children}
|
||||||
|
</GlobalStoreContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlobalStoreContext;
|
||||||
|
export { GlobalStoreContextProvider };
|
25
final/client/src/transactions/MoveItem_Transaction.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import jsTPS_Transaction from "../common/jsTPS.js"
|
||||||
|
/**
|
||||||
|
* MoveItem_Transaction
|
||||||
|
*
|
||||||
|
* This class represents a transaction that works with drag
|
||||||
|
* and drop. It will be managed by the transaction stack.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
export default class MoveItem_Transaction extends jsTPS_Transaction {
|
||||||
|
constructor(initStore, initOldIndex, initNewIndex) {
|
||||||
|
super();
|
||||||
|
this.store = initStore;
|
||||||
|
this.oldItemIndex = initOldIndex;
|
||||||
|
this.newItemIndex = initNewIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
doTransaction() {
|
||||||
|
this.store.moveItem(this.oldItemIndex, this.newItemIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
undoTransaction() {
|
||||||
|
this.store.moveItem(this.newItemIndex, this.oldItemIndex);
|
||||||
|
}
|
||||||
|
}
|
27
final/client/src/transactions/UpdateItem_Transaction.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import jsTPS_Transaction from "../common/jsTPS.js"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateItem_Transaction
|
||||||
|
*
|
||||||
|
* This class represents a transaction that updates the text
|
||||||
|
* for a given item. It will be managed by the transaction stack.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
export default class UpdateItem_Transaction extends jsTPS_Transaction {
|
||||||
|
constructor(initStore, initIndex, initOldText, initNewText) {
|
||||||
|
super();
|
||||||
|
this.store = initStore;
|
||||||
|
this.index = initIndex;
|
||||||
|
this.oldText = initOldText;
|
||||||
|
this.newText = initNewText;
|
||||||
|
}
|
||||||
|
|
||||||
|
doTransaction() {
|
||||||
|
this.store.updateItem(this.index, this.newText);
|
||||||
|
}
|
||||||
|
|
||||||
|
undoTransaction() {
|
||||||
|
this.store.updateItem(this.index, this.oldText);
|
||||||
|
}
|
||||||
|
}
|
3
final/server/.env
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
DB_CONNECT=mongodb://127.0.0.1:27017/top5lists
|
||||||
|
PORT=4000
|
||||||
|
JWT_SECRET=:r(4[CaQ3`N<#8EV~7<K75Rd/ZpfzBkv`m-x]+QnjQcXazr%w;
|
34
final/server/auth/index.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
const jwt = require("jsonwebtoken")
|
||||||
|
|
||||||
|
function authManager() {
|
||||||
|
verify = function (req, res, next) {
|
||||||
|
try {
|
||||||
|
const token = req.cookies.token;
|
||||||
|
if (!token) {
|
||||||
|
req.userId = 'Guest';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const verified = jwt.verify(token, process.env.JWT_SECRET)
|
||||||
|
req.userId = verified.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(401).json({
|
||||||
|
errorMessage: "Unauthorized"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signToken = function (user) {
|
||||||
|
return jwt.sign({
|
||||||
|
userId: user._id
|
||||||
|
}, process.env.JWT_SECRET);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = authManager();
|
||||||
|
module.exports = auth;
|
944
final/server/controllers/top5list-controller.js
Normal file
|
@ -0,0 +1,944 @@
|
||||||
|
const Top5List = require('../models/top5list-model');
|
||||||
|
const CommunityList = require('../models/communitylist-model');
|
||||||
|
const auth = require('../auth');
|
||||||
|
const User = require('../models/user-model')
|
||||||
|
|
||||||
|
createTop5List = async (req, res) => {
|
||||||
|
const body = req.body;
|
||||||
|
if (!body) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'You must provide a Top 5 List',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
_id = req.userId;
|
||||||
|
});
|
||||||
|
if (_id === 'Guest') {
|
||||||
|
return res.status(403).json({success: false, errorMessage: 'Please log in to creat new list'});
|
||||||
|
}
|
||||||
|
|
||||||
|
let owner = await getOwner(_id);
|
||||||
|
body.owner = owner;
|
||||||
|
body.likes = [];
|
||||||
|
body.dislikes = [];
|
||||||
|
body.views = 0;
|
||||||
|
body.comments = [];
|
||||||
|
body.published = false;
|
||||||
|
|
||||||
|
const top5List = new Top5List(body);
|
||||||
|
console.log("creating top5List: " + JSON.stringify(top5List));
|
||||||
|
if (!top5List) {
|
||||||
|
return res.status(400).json({ success: false, errorMessage: err })
|
||||||
|
}
|
||||||
|
|
||||||
|
top5List
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
return res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
top5List: top5List,
|
||||||
|
message: 'Top 5 List Created!'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
return res.status(400).json({
|
||||||
|
error,
|
||||||
|
errorMessage : 'Top 5 List Not Created!'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTop5List = async (req, res) => {
|
||||||
|
let _id = '';
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
_id = req.userId;
|
||||||
|
});
|
||||||
|
let owner = 'Guest';
|
||||||
|
if (_id !== 'Guest') {
|
||||||
|
owner = await getOwner(_id);
|
||||||
|
}
|
||||||
|
const body = req.body
|
||||||
|
console.log("updateTop5List: " + JSON.stringify(body));
|
||||||
|
if (!body) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'You must provide a body to update',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await Top5List.findOne({ _id: req.params.id }, (err, top5List) => {
|
||||||
|
console.log("top5List found: " + JSON.stringify(top5List));
|
||||||
|
if (err || top5List.owner !== owner) {
|
||||||
|
return res.status(404).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Top 5 List not found!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (top5List.published) {
|
||||||
|
return res.status(403).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Cannot modify published list!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
top5List.name = body.name;
|
||||||
|
top5List.items = body.items;
|
||||||
|
top5List
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
console.log("SUCCESS!!!");
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
id: top5List._id,
|
||||||
|
message: 'Top 5 List updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("FAILURE: " + JSON.stringify(error));
|
||||||
|
return res.status(404).json({
|
||||||
|
error,
|
||||||
|
errorMessage: 'Top 5 List not updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
publishTop5List = async (req, res) => {
|
||||||
|
let _id = '';
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
_id = req.userId;
|
||||||
|
});
|
||||||
|
let owner = 'Guest';
|
||||||
|
if (_id !== 'Guest') {
|
||||||
|
owner = await getOwner(_id);
|
||||||
|
}
|
||||||
|
const body = req.body
|
||||||
|
console.log("publishTop5List: " + JSON.stringify(body));
|
||||||
|
if (!body) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'You must provide a body to update',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.name || !body.items[0] || !body.items[1] || !body.items[2] || !body.items[3] || !body.items[4]) {
|
||||||
|
return res.status(403).json({success: false, errorMessage: 'Must fill in all items and title.'});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body.items[0] === body.items[1] || body.items[0] === body.items[2] || body.items[0] === body.items[3] || body.items[0] === body.items[4] || body.items[1] === body.items[2] || body.items[1] === body.items[3] || body.items[1] === body.items[4] || body.items[2] === body.items[3] || body.items[2] === body.items[4] || body.items[3] === body.items[4]) {
|
||||||
|
return res.status(403).json({success: false, errorMessage: 'All item should be identical'});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Top5List.findOne({ $and:[{owner: owner}, {name: body.name}, {published: true}]}, (err, top5List) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(400).json({success: false, errorMessage: err});
|
||||||
|
}
|
||||||
|
if (top5List) {
|
||||||
|
console.log('there is list with same name')
|
||||||
|
console.log(top5List);
|
||||||
|
return res.status(403).json({success: false, errorMessage: 'Cannot publish list with same name'});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
Top5List.findOne({ _id: req.params.id }, (err, top5List) => {
|
||||||
|
console.log("top5List found: " + JSON.stringify(top5List));
|
||||||
|
if (err || top5List.owner !== owner) {
|
||||||
|
return res.status(404).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Top 5 List not found!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.log(top5List);
|
||||||
|
if (top5List.published) {
|
||||||
|
return res.status(403).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Cannot modify published list!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let d = new Date;
|
||||||
|
CommunityList.findOne({name: body.name}, (err, communityList) => {
|
||||||
|
console.log("community list found: " + communityList);
|
||||||
|
if (err) {
|
||||||
|
return res.status(403).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Error with CommunityList',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!communityList) {
|
||||||
|
communityList = {
|
||||||
|
name: body.name,
|
||||||
|
items: [],
|
||||||
|
likes: [],
|
||||||
|
dislikes: [],
|
||||||
|
views: 0,
|
||||||
|
comments: [],
|
||||||
|
publishedAt: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
communityList = new CommunityList(communityList);
|
||||||
|
console.log("creating communityList: " + communityList);
|
||||||
|
}
|
||||||
|
let citem = communityList.items;
|
||||||
|
for (let i = 0; i < 5; i ++) {
|
||||||
|
let tmp = citem.find(function(a){return a[0] === body.items[i]});
|
||||||
|
|
||||||
|
if (tmp) {
|
||||||
|
tmp[1] = parseInt(tmp[1]) + (5-i);
|
||||||
|
console.log(tmp);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
citem.push([body.items[i], (5-i)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
communityList.items = citem;
|
||||||
|
communityList.publishedAt = [d.getFullYear(), (d.getMonth()+1), d.getDate()];
|
||||||
|
communityList.save().then(() => { console.log('updated communityList!')});
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
top5List.name = body.name;
|
||||||
|
top5List.items = body.items;
|
||||||
|
top5List.likes = [];
|
||||||
|
top5List.dislikes = [];
|
||||||
|
top5List.views = 0;
|
||||||
|
top5List.comments = [];
|
||||||
|
top5List.published = true;
|
||||||
|
top5List.publishedAt = [d.getFullYear(), d.getMonth()+1, d.getDate()];
|
||||||
|
top5List
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
console.log("SUCCESS!!!");
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
id: top5List._id,
|
||||||
|
message: 'Top 5 List updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("FAILURE: " + JSON.stringify(error));
|
||||||
|
return res.status(404).json({
|
||||||
|
error,
|
||||||
|
errorMessage: 'Top 5 List not updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).catch(err => console.log(err));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
deleteTop5List = async (req, res) => {
|
||||||
|
let _id = '';
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
_id = req.userId;
|
||||||
|
});
|
||||||
|
let owner = 'Guest';
|
||||||
|
if (_id !== 'Guest') {
|
||||||
|
owner = await getOwner(_id);
|
||||||
|
}
|
||||||
|
await Top5List.findById({ _id: req.params.id }, (err, top5List) => {
|
||||||
|
if (err || top5List.owner !== owner) {
|
||||||
|
return res.status(404).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Top 5 List not found!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let d = new Date;
|
||||||
|
if (top5List.published){
|
||||||
|
CommunityList.findOne({name: top5List.name}, (err, communityList) => {
|
||||||
|
console.log("community list found: " + communityList);
|
||||||
|
if (err || !communityList) {
|
||||||
|
return res.status(403).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Error with CommunityList',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let citem = communityList.items;
|
||||||
|
console.log(citem);
|
||||||
|
console.log(top5List.items);
|
||||||
|
for (let i = 0; i < 5; i ++) {
|
||||||
|
let tmp = citem.find(function(a){return a[0] === top5List.items[i]});
|
||||||
|
if (tmp) {
|
||||||
|
tmp[1] -= (5-i);
|
||||||
|
if (tmp[1] < 0) {
|
||||||
|
console.log('vote<0)');
|
||||||
|
}
|
||||||
|
else if (tmp[1] == 0) {
|
||||||
|
let index = citem.indexOf(tmp);
|
||||||
|
citem.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('Error with CommunityList(cannot find item)\ntmp:',tmp, '\ncitem:', citem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(citem);
|
||||||
|
if (citem.length < 5) {
|
||||||
|
if (citem.length > 0) {
|
||||||
|
console.log('length is less than 5');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('deleting ' + communityList);
|
||||||
|
CommunityList.findOneAndDelete({_id: communityList._id}, () => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
communityList.items = citem;
|
||||||
|
communityList.publishedAt = [d.getFullYear(), (d.getMonth()+1), d.getDate()];
|
||||||
|
communityList.save();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Top5List.findOneAndDelete({ _id: req.params.id }, () => {
|
||||||
|
return res.status(200).json({ success: true, data: top5List })
|
||||||
|
}).catch(err => console.log(err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getTop5ListById = async (req, res) => {
|
||||||
|
let _id = '';
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
_id = req.userId;
|
||||||
|
});
|
||||||
|
let owner = 'Guest';
|
||||||
|
if (_id !== 'Guest') {
|
||||||
|
owner = await getOwner(_id);
|
||||||
|
}
|
||||||
|
console.log(owner)
|
||||||
|
await Top5List.findById({ _id: req.params.id }, (err, list) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(400).json({ success: false, errorMessage: err });
|
||||||
|
}
|
||||||
|
if (!list||!list.published && list.owner !== owner) {
|
||||||
|
return res.status(400).json({success: false, errorMessage: "List does not exists"});
|
||||||
|
}
|
||||||
|
let pair = {
|
||||||
|
_id: list._id,
|
||||||
|
name: list.name,
|
||||||
|
items: list.items,
|
||||||
|
owner: list.owner,
|
||||||
|
likes: list.likes.length,
|
||||||
|
dislikes: list.dislikes.length,
|
||||||
|
views: list.views,
|
||||||
|
comments: list.comments,
|
||||||
|
published: list.published,
|
||||||
|
publishedAt: list.publishedAt,
|
||||||
|
};
|
||||||
|
return res.status(200).json({ success: true, top5List: pair })
|
||||||
|
}).catch(err => console.log(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getTop5Lists = async (req, res) => {
|
||||||
|
let _id = '';
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
_id = req.userId;
|
||||||
|
});
|
||||||
|
let owner = 'Guest';
|
||||||
|
if (_id !== 'Guest') {
|
||||||
|
owner = await getOwner(_id);
|
||||||
|
}
|
||||||
|
//let owner = 'a';
|
||||||
|
await Top5List.find({ $or:[{owner: owner}, {published: true}] }, (err, top5Lists) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(400).json({ success: false, errorMessage: err })
|
||||||
|
}
|
||||||
|
if (!top5Lists) {
|
||||||
|
console.log("!top5Lists.length");
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({ success: false, errorMessage: 'Top 5 Lists not found' })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// PUT ALL THE LISTS INTO ID, NAME PAIRS
|
||||||
|
let pairs = [];
|
||||||
|
for (let key in top5Lists) {
|
||||||
|
let list = top5Lists[key];
|
||||||
|
let pair = {
|
||||||
|
_id: list._id,
|
||||||
|
name: list.name,
|
||||||
|
items: list.items,
|
||||||
|
owner: list.owner,
|
||||||
|
likes: list.likes.length,
|
||||||
|
dislikes: list.dislikes.length,
|
||||||
|
views: list.views,
|
||||||
|
comments: list.comments,
|
||||||
|
published: list.published,
|
||||||
|
publishedAt: list.publishedAt,
|
||||||
|
};
|
||||||
|
pairs.push(pair);
|
||||||
|
}
|
||||||
|
return res.status(200).json({ success: true, idNamePairs: pairs })
|
||||||
|
}
|
||||||
|
}).catch(err => console.log(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommunityLists = async (req, res) => {
|
||||||
|
await CommunityList.find({}, (err, communityLists) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(400).json({ success: false, errorMessage: err })
|
||||||
|
}
|
||||||
|
if (!communityLists) {
|
||||||
|
console.log("!communityLists.length");
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.json({ success: false, errorMessage: 'Top 5 Lists not found' })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// PUT ALL THE LISTS INTO ID, NAME PAIRS
|
||||||
|
let pairs = [];
|
||||||
|
for (let key in communityLists) {
|
||||||
|
let list = communityLists[key];
|
||||||
|
let items = list.items.sort(function(a, b){return b[1] - a[1]});
|
||||||
|
let temp = [];
|
||||||
|
for (let i = 0; i < 5; i ++) {
|
||||||
|
temp.push(items[i][0] + ' (' + items[i][1] + ' votes)' );
|
||||||
|
}
|
||||||
|
let pair = {
|
||||||
|
_id: list._id,
|
||||||
|
name: list.name,
|
||||||
|
items: temp,
|
||||||
|
owner: 'Community',
|
||||||
|
likes: list.likes.length,
|
||||||
|
dislikes: list.dislikes.length,
|
||||||
|
views: list.views,
|
||||||
|
comments: list.comments,
|
||||||
|
published: true,
|
||||||
|
publishedAt: list.publishedAt,
|
||||||
|
};
|
||||||
|
pairs.push(pair);
|
||||||
|
}
|
||||||
|
return res.status(200).json({ success: true, idNamePairs: pairs })
|
||||||
|
}
|
||||||
|
}).catch(err => console.log(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommunityListById = async (req, res) => {
|
||||||
|
await CommunityList.findById({ _id: req.params.id }, (err, list) => {
|
||||||
|
console.log("communityList found: " + JSON.stringify(list));
|
||||||
|
if (err) {
|
||||||
|
return res.status(400).json({ success: false, errorMessage: err });
|
||||||
|
}
|
||||||
|
let items = list.items.sort(function(a, b){return b[1] - a[1]});
|
||||||
|
let temp = [];
|
||||||
|
for (let i = 0; i < 5; i ++) {
|
||||||
|
temp.push(items[i][0] + ' (' + items[i][1] + ' votes)' );
|
||||||
|
}
|
||||||
|
let pair = {
|
||||||
|
_id: list._id,
|
||||||
|
name: list.name,
|
||||||
|
items: temp,
|
||||||
|
owner: 'Community',
|
||||||
|
likes: list.likes.length,
|
||||||
|
dislikes: list.dislikes.length,
|
||||||
|
views: list.views,
|
||||||
|
comments: list.comments,
|
||||||
|
published: true,
|
||||||
|
publishedAt: list.publishedAt,
|
||||||
|
};
|
||||||
|
return res.status(200).json({ success: true, top5List: pair })
|
||||||
|
}).catch(err => console.log(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTop5ListViews = async (req, res) => {
|
||||||
|
await Top5List.findById({ _id: req.params.id }, (err, top5List) => {
|
||||||
|
console.log("top5List found: " + JSON.stringify(top5List));
|
||||||
|
if (!top5List||err) {
|
||||||
|
return res.status(404).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Top 5 List not found!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (top5List.published) {
|
||||||
|
top5List.views += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
top5List
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
console.log("SUCCESS!!!");
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
id: top5List._id,
|
||||||
|
message: 'Top 5 List views updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("FAILURE: " + JSON.stringify(error));
|
||||||
|
return res.status(404).json({
|
||||||
|
error,
|
||||||
|
errorMessage: 'Top 5 List views not updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTop5ListLikes = async (req, res) => {
|
||||||
|
let _id = '';
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
_id = req.userId;
|
||||||
|
});
|
||||||
|
let owner = 'Guest';
|
||||||
|
if (_id !== 'Guest') {
|
||||||
|
owner = await getOwner(_id);
|
||||||
|
await Top5List.findById({ _id: req.params.id }, (err, top5List) => {
|
||||||
|
console.log("top5List found: " + JSON.stringify(top5List));
|
||||||
|
if (!top5List||err) {
|
||||||
|
return res.status(404).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Top 5 List not found!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (top5List.published) {
|
||||||
|
let likes = top5List.likes;
|
||||||
|
let dislikes = top5List.dislikes;
|
||||||
|
|
||||||
|
if (likes.find((a) => a === owner)) {
|
||||||
|
let index = likes.indexOf(owner);
|
||||||
|
likes.splice(index, 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
likes.push(owner);
|
||||||
|
}
|
||||||
|
if (dislikes.find((a) => a === owner)) {
|
||||||
|
let index = likes.indexOf(owner);
|
||||||
|
dislikes.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
top5List.likes = likes;
|
||||||
|
}
|
||||||
|
|
||||||
|
top5List
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
let pair = {
|
||||||
|
_id: top5List._id,
|
||||||
|
name: top5List.name,
|
||||||
|
items: top5List.items,
|
||||||
|
owner: top5List.owner,
|
||||||
|
likes: top5List.likes.length,
|
||||||
|
dislikes: top5List.dislikes.length,
|
||||||
|
views: top5List.views,
|
||||||
|
comments: top5List.comments,
|
||||||
|
published: top5List.published,
|
||||||
|
publishedAt: top5List.publishedAt,
|
||||||
|
};
|
||||||
|
console.log("SUCCESS!!!");
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
top5List: pair,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("FAILURE: " + JSON.stringify(error));
|
||||||
|
return res.status(404).json({
|
||||||
|
error,
|
||||||
|
errorMessage: 'Top 5 List views not updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Please login to like/dislike',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTop5ListDislikes = async (req, res) => {
|
||||||
|
let _id = '';
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
_id = req.userId;
|
||||||
|
});
|
||||||
|
let owner = 'Guest';
|
||||||
|
if (_id !== 'Guest') {
|
||||||
|
owner = await getOwner(_id);
|
||||||
|
await Top5List.findById({ _id: req.params.id }, (err, top5List) => {
|
||||||
|
console.log("top5List found: " + JSON.stringify(top5List));
|
||||||
|
if (!top5List||err) {
|
||||||
|
return res.status(404).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Top 5 List not found!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (top5List.published) {
|
||||||
|
let likes = top5List.likes;
|
||||||
|
let dislikes = top5List.dislikes;
|
||||||
|
|
||||||
|
if (likes.find((a) => a === owner)) {
|
||||||
|
let index = likes.indexOf(owner);
|
||||||
|
likes.splice(index, 1);
|
||||||
|
}
|
||||||
|
if (dislikes.find((a) => a === owner)) {
|
||||||
|
let index = likes.indexOf(owner);
|
||||||
|
dislikes.splice(index, 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dislikes.push(owner);
|
||||||
|
}
|
||||||
|
top5List.dislikes = dislikes;
|
||||||
|
}
|
||||||
|
|
||||||
|
top5List
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
let pair = {
|
||||||
|
_id: top5List._id,
|
||||||
|
name: top5List.name,
|
||||||
|
items: top5List.items,
|
||||||
|
owner: top5List.owner,
|
||||||
|
likes: top5List.likes.length,
|
||||||
|
dislikes: top5List.dislikes.length,
|
||||||
|
views: top5List.views,
|
||||||
|
comments: top5List.comments,
|
||||||
|
published: top5List.published,
|
||||||
|
publishedAt: top5List.publishedAt,
|
||||||
|
};
|
||||||
|
console.log("SUCCESS!!!");
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
top5List: pair,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("FAILURE: " + JSON.stringify(error));
|
||||||
|
return res.status(404).json({
|
||||||
|
error,
|
||||||
|
errorMessage: 'Top 5 List views not updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Please login to like/dislike',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCommunityListViews = async (req, res) => {
|
||||||
|
console.log('get message');
|
||||||
|
await CommunityList.findById({ _id: req.params.id }, (err, communityList) => {
|
||||||
|
console.log("communityList found: " + JSON.stringify(communityList));
|
||||||
|
if (err) {
|
||||||
|
return res.status(404).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Top 5 List not found!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
communityList.views += 1;
|
||||||
|
|
||||||
|
communityList
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
console.log("SUCCESS!!!");
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
id: communityList._id,
|
||||||
|
message: 'Top 5 List views updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("FAILURE: " + JSON.stringify(error));
|
||||||
|
return res.status(404).json({
|
||||||
|
error,
|
||||||
|
errorMessage: 'Top 5 List views not updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCommunityListLikes = async (req, res) => {
|
||||||
|
let _id = '';
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
_id = req.userId;
|
||||||
|
});
|
||||||
|
let owner = 'Guest';
|
||||||
|
if (_id !== 'Guest') {
|
||||||
|
owner = await getOwner(_id);
|
||||||
|
await CommunityList.findOne({ _id: req.params.id }, (err, top5List) => {
|
||||||
|
console.log("top5List found: " + JSON.stringify(top5List));
|
||||||
|
if (err) {
|
||||||
|
return res.status(404).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Top 5 List not found!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let likes = top5List.likes;
|
||||||
|
let dislikes = top5List.dislikes;
|
||||||
|
|
||||||
|
if (likes.find((a) => a === owner)) {
|
||||||
|
let index = likes.indexOf(owner);
|
||||||
|
likes.splice(index, 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
likes.push(owner);
|
||||||
|
}
|
||||||
|
if (dislikes.find((a) => a === owner)) {
|
||||||
|
let index = likes.indexOf(owner);
|
||||||
|
dislikes.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
top5List.likes = likes;
|
||||||
|
|
||||||
|
top5List
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
let pair = {
|
||||||
|
_id: top5List._id,
|
||||||
|
name: top5List.name,
|
||||||
|
items: top5List.items,
|
||||||
|
owner: 'Community',
|
||||||
|
likes: top5List.likes.length,
|
||||||
|
dislikes: top5List.dislikes.length,
|
||||||
|
views: top5List.views,
|
||||||
|
comments: top5List.comments,
|
||||||
|
published: true,
|
||||||
|
publishedAt: top5List.publishedAt,
|
||||||
|
};
|
||||||
|
console.log("SUCCESS!!!");
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
top5List: pair,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("FAILURE: " + JSON.stringify(error));
|
||||||
|
return res.status(404).json({
|
||||||
|
error,
|
||||||
|
errorMessage: 'Top 5 List views not updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Please login to like/dislike',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCommunityListDislikes = async (req, res) => {
|
||||||
|
let _id = '';
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
_id = req.userId;
|
||||||
|
});
|
||||||
|
let owner = 'Guest';
|
||||||
|
if (_id !== 'Guest') {
|
||||||
|
owner = await getOwner(_id);
|
||||||
|
await CommunityList.findOne({ _id: req.params.id }, (err, top5List) => {
|
||||||
|
console.log("top5List found: " + JSON.stringify(top5List));
|
||||||
|
if (err) {
|
||||||
|
return res.status(404).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Top 5 List not found!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let likes = top5List.likes;
|
||||||
|
let dislikes = top5List.dislikes;
|
||||||
|
|
||||||
|
if (likes.find((a) => a === owner)) {
|
||||||
|
let index = likes.indexOf(owner);
|
||||||
|
likes.splice(index, 1);
|
||||||
|
}
|
||||||
|
if (dislikes.find((a) => a === owner)) {
|
||||||
|
let index = likes.indexOf(owner);
|
||||||
|
dislikes.splice(index, 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dislikes.push(owner);
|
||||||
|
}
|
||||||
|
top5List.dislikes = dislikes;
|
||||||
|
|
||||||
|
top5List
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
let pair = {
|
||||||
|
_id: top5List._id,
|
||||||
|
name: top5List.name,
|
||||||
|
items: top5List.items,
|
||||||
|
owner: 'Community',
|
||||||
|
likes: top5List.likes.length,
|
||||||
|
dislikes: top5List.dislikes.length,
|
||||||
|
views: top5List.views,
|
||||||
|
comments: top5List.comments,
|
||||||
|
published: true,
|
||||||
|
publishedAt: top5List.publishedAt,
|
||||||
|
};
|
||||||
|
console.log("SUCCESS!!!");
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
top5List: pair,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("FAILURE: " + JSON.stringify(error));
|
||||||
|
return res.status(404).json({
|
||||||
|
error,
|
||||||
|
errorMessage: 'Top 5 List views not updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Please login to like/dislike',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCommunityListComments = async (req, res) => {
|
||||||
|
const body = req.body
|
||||||
|
console.log('updating c list commens');
|
||||||
|
let _id = '';
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
_id = req.userId;
|
||||||
|
});
|
||||||
|
let owner = 'Guest';
|
||||||
|
if (_id !== 'Guest') {
|
||||||
|
owner = await getOwner(_id);
|
||||||
|
await CommunityList.findOne({ _id: req.params.id }, (err, top5List) => {
|
||||||
|
console.log("top5List found: " + JSON.stringify(top5List));
|
||||||
|
if (!top5List || err) {
|
||||||
|
return res.status(404).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Top 5 List not found!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
top5List.comments.push([owner, body.comment])
|
||||||
|
|
||||||
|
top5List
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
let pair = {
|
||||||
|
_id: top5List._id,
|
||||||
|
name: top5List.name,
|
||||||
|
items: top5List.items,
|
||||||
|
owner: 'Community',
|
||||||
|
likes: top5List.likes.length,
|
||||||
|
dislikes: top5List.dislikes.length,
|
||||||
|
views: top5List.views,
|
||||||
|
comments: top5List.comments,
|
||||||
|
published: true,
|
||||||
|
publishedAt: top5List.publishedAt,
|
||||||
|
};
|
||||||
|
console.log("SUCCESS!!!");
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
top5List: pair,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("FAILURE: " + JSON.stringify(error));
|
||||||
|
return res.status(404).json({
|
||||||
|
error,
|
||||||
|
errorMessage: 'Top 5 List views not updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Please login to leave comments',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTop5ListComments = async (req, res) => {
|
||||||
|
const body = req.body
|
||||||
|
let _id = '';
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
_id = req.userId;
|
||||||
|
});
|
||||||
|
let owner = 'Guest';
|
||||||
|
if (_id !== 'Guest') {
|
||||||
|
owner = await getOwner(_id);
|
||||||
|
await Top5List.findOne({ _id: req.params.id }, (err, top5List) => {
|
||||||
|
console.log("top5List found: " + JSON.stringify(top5List));
|
||||||
|
if (!top5List || err) {
|
||||||
|
return res.status(404).json({
|
||||||
|
err,
|
||||||
|
errorMessage: 'Top 5 List not found!',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top5List.published) {
|
||||||
|
top5List.comments.push([owner, body.comment])
|
||||||
|
|
||||||
|
top5List
|
||||||
|
.save()
|
||||||
|
.then(() => {
|
||||||
|
let pair = {
|
||||||
|
_id: top5List._id,
|
||||||
|
name: top5List.name,
|
||||||
|
items: top5List.items,
|
||||||
|
owner: top5List.owner,
|
||||||
|
likes: top5List.likes.length,
|
||||||
|
dislikes: top5List.dislikes.length,
|
||||||
|
views: top5List.views,
|
||||||
|
comments: top5List.comments,
|
||||||
|
published: top5List.published,
|
||||||
|
publishedAt: top5List.publishedAt,
|
||||||
|
};
|
||||||
|
console.log("SUCCESS!!!");
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
top5List: pair,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log("FAILURE: " + JSON.stringify(error));
|
||||||
|
return res.status(404).json({
|
||||||
|
error,
|
||||||
|
errorMessage: 'Top 5 List views not updated!',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
errorMessage: 'Please login to leave comments',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getOwner = async function (_id) {
|
||||||
|
const loggedInUser = await User.findOne({ _id: _id});
|
||||||
|
return (loggedInUser.firstName + ' ' + loggedInUser.lastName);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createTop5List,
|
||||||
|
updateTop5List,
|
||||||
|
publishTop5List,
|
||||||
|
deleteTop5List,
|
||||||
|
getTop5Lists,
|
||||||
|
getTop5ListById,
|
||||||
|
getCommunityLists,
|
||||||
|
getCommunityListById,
|
||||||
|
updateTop5ListViews,
|
||||||
|
updateTop5ListLikes,
|
||||||
|
updateTop5ListDislikes,
|
||||||
|
updateCommunityListViews,
|
||||||
|
updateCommunityListLikes,
|
||||||
|
updateCommunityListDislikes,
|
||||||
|
updateTop5ListComments,
|
||||||
|
updateCommunityListComments,
|
||||||
|
}
|
156
final/server/controllers/user-controller.js
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
const auth = require('../auth')
|
||||||
|
const User = require('../models/user-model')
|
||||||
|
const bcrypt = require('bcryptjs')
|
||||||
|
|
||||||
|
getLoggedIn = async (req, res) => {
|
||||||
|
auth.verify(req, res, async function () {
|
||||||
|
if (req.userId === 'Guest') {
|
||||||
|
return res.status(200).json({
|
||||||
|
loggedIn: false,
|
||||||
|
user: {
|
||||||
|
firstName: 'Guest',
|
||||||
|
lastName: 'Guest',
|
||||||
|
name: 'Guest',
|
||||||
|
email: 'Guest@gmail.com'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const loggedInUser = await User.findOne({ _id: req.userId });
|
||||||
|
return res.status(200).json({
|
||||||
|
loggedIn: true,
|
||||||
|
user: {
|
||||||
|
firstName: loggedInUser.firstName,
|
||||||
|
lastName: loggedInUser.lastName,
|
||||||
|
name: (loggedInUser.firstName + ' ' + loggedInUser.lastName),
|
||||||
|
email: loggedInUser.email
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
registerUser = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { firstName, lastName, email, password, passwordVerify } = req.body;
|
||||||
|
if (!firstName || !lastName || !email || !password || !passwordVerify) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ errorMessage: "Please enter all required fields." });
|
||||||
|
}
|
||||||
|
if (password.length < 8) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({
|
||||||
|
errorMessage: "Please enter a password of at least 8 characters."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (password !== passwordVerify) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({
|
||||||
|
errorMessage: "Please enter the same password twice."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const existingEmail = await User.findOne({ email: email });
|
||||||
|
const existingName = await User.findOne({ $and: [{firstName: firstName}, {lastName: lastName}]});
|
||||||
|
if (existingEmail) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({
|
||||||
|
success: false,
|
||||||
|
errorMessage: "An account with this email address already exists."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (existingName) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({
|
||||||
|
success: false,
|
||||||
|
errorMessage: "An account with the same name already exists."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saltRounds = 10;
|
||||||
|
const salt = await bcrypt.genSalt(saltRounds);
|
||||||
|
const passwordHash = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
|
const newUser = new User({
|
||||||
|
firstName, lastName, email, passwordHash
|
||||||
|
});
|
||||||
|
const savedUser = await newUser.save();
|
||||||
|
|
||||||
|
// LOGIN THE USER
|
||||||
|
const token = auth.signToken(savedUser);
|
||||||
|
|
||||||
|
await res.cookie("token", token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: "none"
|
||||||
|
}).status(200).json({
|
||||||
|
success: true,
|
||||||
|
user: {
|
||||||
|
firstName: savedUser.firstName,
|
||||||
|
lastName: savedUser.lastName,
|
||||||
|
name: (savedUser.firstName+' '+savedUser.lastName),
|
||||||
|
email: savedUser.email
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loginUser = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
if (!email) {
|
||||||
|
return res.status(400).json({ errorMessage: "Please enter email."});
|
||||||
|
}
|
||||||
|
if (!password) {
|
||||||
|
return res.status(400).json({ errorMessage: "Please enter password."});
|
||||||
|
}
|
||||||
|
const existingUser = await User.findOne({ email: email});
|
||||||
|
if (!existingUser) {
|
||||||
|
return res.status(400).json({ errorMessage: "Email address " + email + " does not exists."});
|
||||||
|
}
|
||||||
|
let hash = existingUser.passwordHash;
|
||||||
|
if (! await bcrypt.compare(password, hash)) {
|
||||||
|
return res.status(400).json({ errorMessage: "Incorrect password."})
|
||||||
|
}
|
||||||
|
const token = auth.signToken(existingUser);
|
||||||
|
|
||||||
|
await res.cookie("token", token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: "none"
|
||||||
|
}).status(200).json({
|
||||||
|
success: true,
|
||||||
|
user: {
|
||||||
|
firstName: existingUser.firstName,
|
||||||
|
lastName: existingUser.lastName,
|
||||||
|
name: (existingUser.firstName+' '+existingUser.lastName),
|
||||||
|
email: existingUser.email
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logoutUser = async(req, res) => {
|
||||||
|
return await res.cookie("token", '', {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
sameSite: "none"
|
||||||
|
}).status(200).json({
|
||||||
|
success: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getLoggedIn,
|
||||||
|
registerUser,
|
||||||
|
loginUser,
|
||||||
|
logoutUser
|
||||||
|
}
|
15
final/server/db/index.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
const mongoose = require('mongoose')
|
||||||
|
const dotenv = require('dotenv')
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
mongoose
|
||||||
|
.connect(process.env.DB_CONNECT, { useNewUrlParser: true })
|
||||||
|
.catch(e => {
|
||||||
|
console.error('Connection error', e.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
const db = mongoose.connection
|
||||||
|
|
||||||
|
module.exports = db
|
||||||
|
|
32
final/server/index.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// THESE ARE NODE APIs WE WISH TO USE
|
||||||
|
const express = require('express')
|
||||||
|
const cors = require('cors')
|
||||||
|
const dotenv = require('dotenv')
|
||||||
|
const cookieParser = require('cookie-parser')
|
||||||
|
|
||||||
|
// CREATE OUR SERVER
|
||||||
|
dotenv.config()
|
||||||
|
const PORT = process.env.PORT || 4000;
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
// SETUP THE MIDDLEWARE
|
||||||
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
app.use(cors({
|
||||||
|
origin: ["http://127.0.0.1:3000", "http://localhost:3000", "*"],
|
||||||
|
credentials: true
|
||||||
|
}))
|
||||||
|
app.use(express.json())
|
||||||
|
app.use(cookieParser())
|
||||||
|
|
||||||
|
// SETUP OUR OWN ROUTERS AS MIDDLEWARE
|
||||||
|
const top5listsRouter = require('./routes/top5lists-router')
|
||||||
|
app.use('/api', top5listsRouter)
|
||||||
|
|
||||||
|
// INITIALIZE OUR DATABASE OBJECT
|
||||||
|
const db = require('./db')
|
||||||
|
db.on('error', console.error.bind(console, 'MongoDB connection error:'))
|
||||||
|
|
||||||
|
// PUT THE SERVER IN LISTENING MODE
|
||||||
|
app.listen(PORT, () => console.log(`Server running on port ${PORT}`))
|
||||||
|
|
||||||
|
|
17
final/server/models/communitylist-model.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
const mongoose = require('mongoose')
|
||||||
|
const Schema = mongoose.Schema
|
||||||
|
|
||||||
|
const CommunityListSchema = new Schema(
|
||||||
|
{
|
||||||
|
name: { type: String, required: true },
|
||||||
|
items: { type: [[String, Number]], required: true },
|
||||||
|
likes: { type: [String], require: true},
|
||||||
|
dislikes: { type: [String], require: true},
|
||||||
|
views: { type: Number, require: true},
|
||||||
|
comments: { type: [[String, String]], required: false},
|
||||||
|
publishedAt: { type: [Number, Number, Number], require: true }
|
||||||
|
},
|
||||||
|
{ timestamps: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exports = mongoose.model('CommunityList', CommunityListSchema)
|
19
final/server/models/top5list-model.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
const mongoose = require('mongoose')
|
||||||
|
const Schema = mongoose.Schema
|
||||||
|
|
||||||
|
const Top5ListSchema = new Schema(
|
||||||
|
{
|
||||||
|
name: { type: String, required: true },
|
||||||
|
items: { type: [String], required: true },
|
||||||
|
owner: { type: String, require: true },
|
||||||
|
likes: { type: [String], require: true},
|
||||||
|
dislikes: { type: [String], require: true},
|
||||||
|
views: { type: Number, require: true},
|
||||||
|
comments: { type: [[String, String]], required: false},
|
||||||
|
published: { type: Boolean, required: true},
|
||||||
|
publishedAt: { type: [Number,Number,Number], require: false }
|
||||||
|
},
|
||||||
|
{ timestamps: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exports = mongoose.model('Top5List', Top5ListSchema)
|
15
final/server/models/user-model.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
const mongoose = require('mongoose')
|
||||||
|
const Schema = mongoose.Schema
|
||||||
|
const ObjectId = Schema.Types.ObjectId
|
||||||
|
|
||||||
|
const UserSchema = new Schema(
|
||||||
|
{
|
||||||
|
firstName: { type: String, required: true },
|
||||||
|
lastName: { type: String, required: true },
|
||||||
|
email: { type: String, required: true },
|
||||||
|
passwordHash: { type: String, required: true }
|
||||||
|
},
|
||||||
|
{ timestamps: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exports = mongoose.model('User', UserSchema)
|
1635
final/server/package-lock.json
generated
Normal file
16
final/server/package.json
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"cookie-parser": "^1.4.5",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^10.0.0",
|
||||||
|
"express": "^4.16.4",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"mongoose": "^5.7.5"
|
||||||
|
}
|
||||||
|
}
|
30
final/server/routes/top5lists-router.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
const auth = require('../auth')
|
||||||
|
const express = require('express')
|
||||||
|
const Top5ListController = require('../controllers/top5list-controller')
|
||||||
|
const UserController = require('../controllers/user-controller')
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
router.post('/top5list', auth.verify, Top5ListController.createTop5List)
|
||||||
|
router.put('/top5list/:id', auth.verify, Top5ListController.updateTop5List)
|
||||||
|
router.put('/publishtop5list/:id', auth.verify, Top5ListController.publishTop5List)
|
||||||
|
router.delete('/top5list/:id', auth.verify, Top5ListController.deleteTop5List)
|
||||||
|
router.get('/top5list/:id', Top5ListController.getTop5ListById)
|
||||||
|
//router.get('/top5lists', auth.verify, Top5ListController.getTop5Lists)
|
||||||
|
router.get('/top5lists', Top5ListController.getTop5Lists)
|
||||||
|
router.get('/communitylists', Top5ListController.getCommunityLists)
|
||||||
|
router.get('/communitylist/:id', Top5ListController.getCommunityListById)
|
||||||
|
router.get('/top5listviews/:id', Top5ListController.updateTop5ListViews)
|
||||||
|
router.get('/top5listlikes/:id', auth.verify, Top5ListController.updateTop5ListLikes)
|
||||||
|
router.get('/top5listdislikes/:id', auth.verify, Top5ListController.updateTop5ListDislikes)
|
||||||
|
router.get('/communitylistviews/:id', Top5ListController.updateCommunityListViews)
|
||||||
|
router.get('/communitylistlikes/:id', auth.verify, Top5ListController.updateCommunityListLikes)
|
||||||
|
router.get('/communitylistdislikes/:id', auth.verify, Top5ListController.updateCommunityListDislikes)
|
||||||
|
router.put('/commenttop5list/:id', auth.verify, Top5ListController.updateTop5ListComments)
|
||||||
|
router.put('/commentcommunitylist/:id', auth.verify, Top5ListController.updateCommunityListComments)
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/register', UserController.registerUser)
|
||||||
|
router.post('/login', UserController.loginUser)
|
||||||
|
router.get('/logout', UserController.logoutUser)
|
||||||
|
router.get('/loggedIn', UserController.getLoggedIn)
|
||||||
|
module.exports = router
|
34
final/server/test/postman-lists.json
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"top5Lists": [
|
||||||
|
{
|
||||||
|
"name": "Games",
|
||||||
|
"items": [
|
||||||
|
"StarCraft",
|
||||||
|
"Fallout 3",
|
||||||
|
"Katamari Damacy",
|
||||||
|
"Civilization II",
|
||||||
|
"Super Mario World"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Movies",
|
||||||
|
"items": [
|
||||||
|
"Raiders of the Lost Ark",
|
||||||
|
"Goodfellas",
|
||||||
|
"Lord of the Rings",
|
||||||
|
"Airplane!",
|
||||||
|
"Lawrence of Arabia"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pink Floyd Songs",
|
||||||
|
"items": [
|
||||||
|
"Shine On You Crazy Diamond",
|
||||||
|
"Comfortably Numb",
|
||||||
|
"Pigs (Three Different Ones)",
|
||||||
|
"Echoes (Live at Pompeii)",
|
||||||
|
"Time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
23
hw2/.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
70
hw2/README.md
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
|
The page will reload if you make edits.\
|
||||||
|
You will also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||||
|
|
||||||
|
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||||
|
|
||||||
|
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
|
|
||||||
|
### Code Splitting
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||||
|
|
||||||
|
### Analyzing the Bundle Size
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||||
|
|
||||||
|
### Making a Progressive Web App
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||||
|
|
||||||
|
### `npm run build` fails to minify
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
37832
hw2/package-lock.json
generated
Normal file
43
hw2/package.json
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"name": "top5-lister-hw2",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
|
"@testing-library/react": "^11.2.7",
|
||||||
|
"@testing-library/user-event": "^12.8.3",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-scripts": "4.0.3",
|
||||||
|
"web-vitals": "^1.1.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).",
|
||||||
|
"main": "index.js",
|
||||||
|
"devDependencies": {},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
52
hw2/public/data/default_lists.json
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"top5Lists": [
|
||||||
|
{
|
||||||
|
"key": "0",
|
||||||
|
"name": "Games",
|
||||||
|
"likes": 2150,
|
||||||
|
"dislikes": 114,
|
||||||
|
"created": "Thu, 22 Oct 2020 01:10:53 GMT",
|
||||||
|
"modified": "Wed, 23 Jun 2021 03:21:31 GMT",
|
||||||
|
"accessed": "Thu, 26 Aug 2021 03:23:31 GMT",
|
||||||
|
"items": [
|
||||||
|
"StarCraft",
|
||||||
|
"Fallout 3",
|
||||||
|
"Katamari Damacy",
|
||||||
|
"Civilization II",
|
||||||
|
"Super Mario World"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"name": "Movies",
|
||||||
|
"likes": 50,
|
||||||
|
"dislikes": 4,
|
||||||
|
"created": "Mon, 23 Aug 2021 03:12:53 GMT",
|
||||||
|
"modified": "Mon, 23 Aug 2021 03:14:31 GMT",
|
||||||
|
"accessed": "Mon, 23 Aug 2021 03:16:31 GMT",
|
||||||
|
"items": [
|
||||||
|
"Raiders of the Lost Ark",
|
||||||
|
"Goodfellas",
|
||||||
|
"Lord of the Rings",
|
||||||
|
"Airplane!",
|
||||||
|
"Lawrence of Arabia"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"name": "Pink Floyd Songs",
|
||||||
|
"likes": 520,
|
||||||
|
"dislikes": 14,
|
||||||
|
"created": "Sun, 22 Aug 2021 03:19:53 GMT",
|
||||||
|
"modified": "Mon, 24 Aug 2021 03:21:31 GMT",
|
||||||
|
"accessed": "Wed, 26 Aug 2021 03:23:31 GMT",
|
||||||
|
"items": [
|
||||||
|
"Shine On You Crazy Diamond",
|
||||||
|
"Comfortably Numb",
|
||||||
|
"Pigs (Three Different Ones)",
|
||||||
|
"Echoes (Live at Pompeii)",
|
||||||
|
"Time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
hw2/public/favicon.ico
Normal file
After Width: | Height: | Size: 470 KiB |
43
hw2/public/index.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
hw2/public/logo192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
hw2/public/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
hw2/public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
3
hw2/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
410
hw2/src/App.css
Normal file
|
@ -0,0 +1,410 @@
|
||||||
|
:root {
|
||||||
|
/*
|
||||||
|
FIRST WE'LL DEFINE OUR SWATCHES, i.e. THE COMPLEMENTARY
|
||||||
|
COLORS THAT WE'LL USE TOGETHER IN MULTIPLE PLACES THAT
|
||||||
|
TOGETHER WILL MAKE UP A GIVEN THEME
|
||||||
|
*/
|
||||||
|
--swatch-foundation: #eeeedd;
|
||||||
|
--swatch-primary: #e6e6e6;
|
||||||
|
--swatch-complement: #e1e4cb;
|
||||||
|
--swatch-contrast: #111111;
|
||||||
|
--swatch-accent: #669966;
|
||||||
|
--swatch-status: #123456;
|
||||||
|
--my-font-family: "Robaaaoto";
|
||||||
|
--bounceEasing: cubic-bezier(0.51, 0.92, 0.24, 1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--swatch-foundation);
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
background-color: var(--swatch-primary);
|
||||||
|
font-family: "Lexend Exa";
|
||||||
|
position: absolute;
|
||||||
|
width: 80%;
|
||||||
|
left: 10%;
|
||||||
|
height:90%;
|
||||||
|
top: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-root {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0%;
|
||||||
|
left: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-banner {
|
||||||
|
position:absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 0%;
|
||||||
|
width: 100%;
|
||||||
|
height: 10%;
|
||||||
|
text-align:center;
|
||||||
|
background-image: linear-gradient(to bottom,
|
||||||
|
#b8b808, #636723);
|
||||||
|
color: white;
|
||||||
|
font-size: 48pt;
|
||||||
|
border-color: black;
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-sidebar {
|
||||||
|
position:absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 10%;
|
||||||
|
width: 30%;
|
||||||
|
height: 80%;
|
||||||
|
background-color: var(--swatch-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-workspace {
|
||||||
|
position:absolute;
|
||||||
|
left: 30%;
|
||||||
|
top: 10%;
|
||||||
|
width: 70%;
|
||||||
|
height: 80%;
|
||||||
|
background-color: var(--swatch-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-statusbar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 90%;
|
||||||
|
width: 100%;
|
||||||
|
height: 10%;
|
||||||
|
background-color: lightsalmon;
|
||||||
|
display:flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 36pt;
|
||||||
|
}
|
||||||
|
#sidebar-heading {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:0%;
|
||||||
|
width:100%;
|
||||||
|
height:10%;
|
||||||
|
text-align:center;
|
||||||
|
font-size: 24pt;
|
||||||
|
font-weight: bold;
|
||||||
|
display:flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-list-button {
|
||||||
|
float:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar-list {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:10%;
|
||||||
|
width:100%;
|
||||||
|
height:90%;
|
||||||
|
display:flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card, .selected-list-card, .unselected-list-card {
|
||||||
|
font-size: 18pt;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unselected-list-card:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--swatch-contrast);
|
||||||
|
color:white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-list-card {
|
||||||
|
background-color: var(--swatch-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unselected-list-card {
|
||||||
|
background-color: var(--swatch-complement);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card-button {
|
||||||
|
float:right;
|
||||||
|
font-size:18pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#workspace-home, #workspace-edit {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:0%;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-toolbar {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:20%;
|
||||||
|
height:60%;
|
||||||
|
background-color: var(--swatch-primary);
|
||||||
|
display:flex
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-button, .top5-button-disabled {
|
||||||
|
font-size:36pt;
|
||||||
|
border-width: 0px;
|
||||||
|
float:left;
|
||||||
|
color: black;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.top5-button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color:black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-button-disabled {
|
||||||
|
opacity: 0.25;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-numbering {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:0%;
|
||||||
|
width:20%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--swatch-status);
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-items {
|
||||||
|
position:absolute;
|
||||||
|
left:20%;
|
||||||
|
top:0%;
|
||||||
|
width:80%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--swatch-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-number, .top5-item, .top5-item-dragged-to {
|
||||||
|
display:flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 24pt;
|
||||||
|
height:20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-number {
|
||||||
|
justify-content: center;
|
||||||
|
width:100%;
|
||||||
|
border: 1px 0px 1px 1px;
|
||||||
|
border-color:black;
|
||||||
|
background-color: linen;
|
||||||
|
color:black;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size:20pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-item, .top5-item-dragged-to {
|
||||||
|
text-align: left;
|
||||||
|
width:95%;
|
||||||
|
padding-left:5%;
|
||||||
|
}
|
||||||
|
.top5-item {
|
||||||
|
background-color: var(--swatch-complement);
|
||||||
|
}
|
||||||
|
.top5-item-dragged-to {
|
||||||
|
background-color: var(--swatch-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.disabled:hover {
|
||||||
|
color: var(--swatch-neutral);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* THIS STYLE SHEET MANAGES STYLE FOR OUR MODAL, i.e. DIALOG BOX */
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--black);
|
||||||
|
color: var(--swatch-text);
|
||||||
|
cursor: pointer;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.35s ease-in;
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.is-visible {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
position: relative;
|
||||||
|
max-width: 800px;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: var(--swatch-complement);
|
||||||
|
overflow: auto;
|
||||||
|
cursor: default;
|
||||||
|
border-width: 5px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-style: groove;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog > * {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
|
.modal-footer {
|
||||||
|
background: var(--lightgray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
|
.modal-close {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal p + p {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-control {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
font-size: 24pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#close-modal-button {
|
||||||
|
float:right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirm-cancel-container {
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ANIMATIONS
|
||||||
|
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||||
|
[data-animation] .modal-dialog {
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.5s var(--bounceEasing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation].is-visible .modal-dialog {
|
||||||
|
opacity: 1;
|
||||||
|
transition-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutDown"] .modal-dialog {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutTop"] .modal-dialog {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutLeft"] .modal-dialog {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutRight"] .modal-dialog {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="zoomInOut"] .modal-dialog {
|
||||||
|
transform: scale(0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="rotateInOutDown"] .modal-dialog {
|
||||||
|
transform-origin: top left;
|
||||||
|
transform: rotate(-1turn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="mixInAnimations"].is-visible .modal-dialog {
|
||||||
|
animation: mixInAnimations 2s 0.2s linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutDown"].is-visible .modal-dialog,
|
||||||
|
[data-animation="slideInOutTop"].is-visible .modal-dialog,
|
||||||
|
[data-animation="slideInOutLeft"].is-visible .modal-dialog,
|
||||||
|
[data-animation="slideInOutRight"].is-visible .modal-dialog,
|
||||||
|
[data-animation="zoomInOut"].is-visible .modal-dialog,
|
||||||
|
[data-animation="rotateInOutDown"].is-visible .modal-dialog {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mixInAnimations {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
10% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
transform: rotate(20deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
transform: rotate(-20deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
transform: rotate(15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: rotate(-10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
transform: rotate(5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
90% {
|
||||||
|
transform: rotate(-5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
315
hw2/src/App.js
Normal file
|
@ -0,0 +1,315 @@
|
||||||
|
import React from 'react';
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
// IMPORT DATA MANAGEMENT AND TRANSACTION STUFF
|
||||||
|
import DBManager from './db/DBManager';
|
||||||
|
import jsTPS from './common/jsTPS';
|
||||||
|
import RenameItem from './common/RenameItemTransaction';
|
||||||
|
import MoveItem from './common/MoveItem_Transaction';
|
||||||
|
|
||||||
|
|
||||||
|
// THESE ARE OUR REACT COMPONENTS
|
||||||
|
import DeleteModal from './components/DeleteModal';
|
||||||
|
import Banner from './components/Banner.js'
|
||||||
|
import Sidebar from './components/Sidebar.js'
|
||||||
|
import Workspace from './components/Workspace.js';
|
||||||
|
import Statusbar from './components/Statusbar.js'
|
||||||
|
|
||||||
|
class App extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// THIS WILL TALK TO LOCAL STORAGE
|
||||||
|
this.db = new DBManager();
|
||||||
|
|
||||||
|
this.tps = new jsTPS();
|
||||||
|
|
||||||
|
// GET THE SESSION DATA FROM OUR DATA MANAGER
|
||||||
|
let loadedSessionData = this.db.queryGetSessionData();
|
||||||
|
|
||||||
|
// SETUP THE INITIAL STATE
|
||||||
|
this.state = {
|
||||||
|
currentList : null,
|
||||||
|
listKeyPairMarkedForDeletion: null,
|
||||||
|
sessionData : loadedSessionData,
|
||||||
|
hasUndo: false,
|
||||||
|
hasRedo: false,
|
||||||
|
isEditing: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sortKeyNamePairsByName = (keyNamePairs) => {
|
||||||
|
keyNamePairs.sort((keyPair1, keyPair2) => {
|
||||||
|
// GET THE LISTS
|
||||||
|
return keyPair1.name.localeCompare(keyPair2.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// THIS FUNCTION BEGINS THE PROCESS OF CREATING A NEW LIST
|
||||||
|
createNewList = () => {
|
||||||
|
// FIRST FIGURE OUT WHAT THE NEW LIST'S KEY AND NAME WILL BE
|
||||||
|
let newKey = this.state.sessionData.nextKey;
|
||||||
|
|
||||||
|
let newName = "Untitled" + newKey;
|
||||||
|
|
||||||
|
// MAKE THE NEW LIST
|
||||||
|
let newList = {
|
||||||
|
key: newKey,
|
||||||
|
name: newName,
|
||||||
|
items: ["?", "?", "?", "?", "?"]
|
||||||
|
};
|
||||||
|
|
||||||
|
// MAKE THE KEY,NAME OBJECT SO WE CAN KEEP IT IN OUR
|
||||||
|
// SESSION DATA SO IT WILL BE IN OUR LIST OF LISTS
|
||||||
|
let newKeyNamePair = { "key": newKey, "name": newName };
|
||||||
|
let updatedPairs = [...this.state.sessionData.keyNamePairs, newKeyNamePair];
|
||||||
|
this.sortKeyNamePairsByName(updatedPairs);
|
||||||
|
|
||||||
|
// CHANGE THE APP STATE SO THAT IT THE CURRENT LIST IS
|
||||||
|
// THIS NEW LIST AND UPDATE THE SESSION DATA SO THAT THE
|
||||||
|
// NEXT LIST CAN BE MADE AS WELL. NOTE, THIS setState WILL
|
||||||
|
// FORCE A CALL TO render, BUT THIS UPDATE IS ASYNCHRONOUS,
|
||||||
|
// SO ANY AFTER EFFECTS THAT NEED TO USE THIS UPDATED STATE
|
||||||
|
// SHOULD BE DONE VIA ITS CALLBACK
|
||||||
|
this.setState(prevState => ({
|
||||||
|
currentList: newList,
|
||||||
|
sessionData: {
|
||||||
|
nextKey: prevState.sessionData.nextKey + 1,
|
||||||
|
counter: prevState.sessionData.counter + 1,
|
||||||
|
keyNamePairs: updatedPairs
|
||||||
|
}
|
||||||
|
}), () => {
|
||||||
|
// PUTTING THIS NEW LIST IN PERMANENT STORAGE
|
||||||
|
// IS AN AFTER EFFECT
|
||||||
|
this.db.mutationCreateList(newList);
|
||||||
|
this.db.mutationUpdateSessionData(this.state.sessionData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
renameList = (key, newName) => {
|
||||||
|
let newKeyNamePairs = [...this.state.sessionData.keyNamePairs];
|
||||||
|
// NOW GO THROUGH THE ARRAY AND FIND THE ONE TO RENAME
|
||||||
|
for (let i = 0; i < newKeyNamePairs.length; i++) {
|
||||||
|
let pair = newKeyNamePairs[i];
|
||||||
|
if (pair.key === key) {
|
||||||
|
pair.name = newName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.sortKeyNamePairsByName(newKeyNamePairs);
|
||||||
|
|
||||||
|
// WE MAY HAVE TO RENAME THE currentList
|
||||||
|
let currentList = this.state.currentList;
|
||||||
|
if (currentList && currentList.key === key) {
|
||||||
|
currentList.name = newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(prevState => ({
|
||||||
|
currentList: prevState.currentList,
|
||||||
|
sessionData: {
|
||||||
|
nextKey: prevState.sessionData.nextKey,
|
||||||
|
counter: prevState.sessionData.counter,
|
||||||
|
keyNamePairs: newKeyNamePairs
|
||||||
|
}
|
||||||
|
}), () => {
|
||||||
|
// AN AFTER EFFECT IS THAT WE NEED TO MAKE SURE
|
||||||
|
// THE TRANSACTION STACK IS CLEARED
|
||||||
|
let list = this.db.queryGetList(key);
|
||||||
|
list.name = newName;
|
||||||
|
this.db.mutationUpdateList(list);
|
||||||
|
this.db.mutationUpdateSessionData(this.state.sessionData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addRenameItemTransaction = (index, newText) => {
|
||||||
|
let oldText = this.state.currentList.items[index];
|
||||||
|
let transaction = new RenameItem(this, index, oldText, newText);
|
||||||
|
this.tps.addTransaction(transaction);
|
||||||
|
this.setState({hasUndo: this.tps.hasTransactionToUndo()});
|
||||||
|
this.setState({hasRedo: this.tps.hasTransactionToRedo()});
|
||||||
|
}
|
||||||
|
addMoveItemTransaction = (oldIndex, newIndex) => {
|
||||||
|
let transaction = new MoveItem(this, oldIndex, newIndex);
|
||||||
|
this.tps.addTransaction(transaction);
|
||||||
|
this.setState({hasUndo: this.tps.hasTransactionToUndo()});
|
||||||
|
this.setState({hasRedo: this.tps.hasTransactionToRedo()});
|
||||||
|
}
|
||||||
|
undo = () => {
|
||||||
|
if (this.state.hasUndo) {
|
||||||
|
this.tps.undoTransaction();
|
||||||
|
this.updateState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
redo = () => {
|
||||||
|
if (this.state.hasRedo) {
|
||||||
|
this.tps.doTransaction();
|
||||||
|
this.updateState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleKeydown = (event) => {
|
||||||
|
if (this.state.hasUndo && event.ctrlKey && (event.key === "z" || event.key === "Z")) {
|
||||||
|
this.undo();
|
||||||
|
}
|
||||||
|
if (this.state.hasRedo && event.ctrlKey && (event.key === "y" || event.key === "Y")) {
|
||||||
|
this.redo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renameItem = (index, newName) => {
|
||||||
|
let newKeyNamePairs = [...this.state.sessionData.keyNamePairs];
|
||||||
|
// NOW GO THROUGH THE ARRAY AND FIND THE ONE TO RENAME
|
||||||
|
|
||||||
|
let currentList = this.state.currentList;
|
||||||
|
currentList.items[index] = newName;
|
||||||
|
|
||||||
|
this.setState(prevState => ({
|
||||||
|
currentList: prevState.currentList,
|
||||||
|
sessionData: {
|
||||||
|
nextKey: prevState.sessionData.nextKey,
|
||||||
|
counter: prevState.sessionData.counter,
|
||||||
|
keyNamePairs: newKeyNamePairs
|
||||||
|
}
|
||||||
|
}), () => {
|
||||||
|
let list = this.db.queryGetList(this.state.currentList.key);
|
||||||
|
list.items[index] = newName;
|
||||||
|
this.db.mutationUpdateList(list);
|
||||||
|
this.db.mutationUpdateSessionData(this.state.sessionData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
moveItem = (oldIndex, newIndex) => {
|
||||||
|
let newKeyNamePairs = [...this.state.sessionData.keyNamePairs];
|
||||||
|
|
||||||
|
let currentList = this.state.currentList;
|
||||||
|
currentList.items.splice(newIndex, 0, currentList.items.splice(oldIndex, 1)[0]);
|
||||||
|
|
||||||
|
this.setState(prevState => ({
|
||||||
|
currentList: prevState.currentList,
|
||||||
|
sessionData: {
|
||||||
|
nextKey: prevState.sessionData.nextKey,
|
||||||
|
counter: prevState.sessionData.counter,
|
||||||
|
keyNamePairs: newKeyNamePairs
|
||||||
|
}
|
||||||
|
}), () => {
|
||||||
|
let list = this.db.queryGetList(this.state.currentList.key);
|
||||||
|
list.items.splice(newIndex, 0, list.items.splice(oldIndex, 1)[0]);
|
||||||
|
this.db.mutationUpdateList(list);
|
||||||
|
this.db.mutationUpdateSessionData(this.state.sessionData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// THIS FUNCTION BEGINS THE PROCESS OF LOADING A LIST FOR EDITING
|
||||||
|
loadList = (key) => {
|
||||||
|
if (!this.state.currentList || key !== this.state.currentList.key) {
|
||||||
|
let newCurrentList = this.db.queryGetList(key);
|
||||||
|
this.setState(prevState => ({
|
||||||
|
currentList: newCurrentList,
|
||||||
|
sessionData: prevState.sessionData
|
||||||
|
}), () => {
|
||||||
|
// ANY AFTER EFFECTS?
|
||||||
|
this.tps.clearAllTransactions();
|
||||||
|
this.updateState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// THIS FUNCTION BEGINS THE PROCESS OF CLOSING THE CURRENT LIST
|
||||||
|
closeCurrentList = () => {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
currentList: null,
|
||||||
|
listKeyPairMarkedForDeletion : prevState.listKeyPairMarkedForDeletion,
|
||||||
|
sessionData: this.state.sessionData
|
||||||
|
}), () => {
|
||||||
|
// ANY AFTER EFFECTS?
|
||||||
|
this.tps.clearAllTransactions();
|
||||||
|
this.updateState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateState = () => {
|
||||||
|
this.setState({hasUndo: this.tps.hasTransactionToUndo()});
|
||||||
|
this.setState({hasRedo: this.tps.hasTransactionToRedo()});
|
||||||
|
}
|
||||||
|
deleteList = () => {
|
||||||
|
// SOMEHOW YOU ARE GOING TO HAVE TO FIGURE OUT
|
||||||
|
// WHICH LIST IT IS THAT THE USER WANTS TO
|
||||||
|
// DELETE AND MAKE THAT CONNECTION SO THAT THE
|
||||||
|
// NAME PROPERLY DISPLAYS INSIDE THE MODAL
|
||||||
|
let newKeyNamePairs = [...this.state.sessionData.keyNamePairs];
|
||||||
|
let keyPair = this.state.listKeyPairMarkedForDeletion;
|
||||||
|
|
||||||
|
console.log("state: " + keyPair.name);
|
||||||
|
|
||||||
|
for (let i = 0; i < newKeyNamePairs.length; i ++) {
|
||||||
|
let pair = newKeyNamePairs[i];
|
||||||
|
if (keyPair.key === pair.key) {
|
||||||
|
newKeyNamePairs.splice(i, 1);
|
||||||
|
i --;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentList = this.state.currentList;
|
||||||
|
if (currentList && currentList.key === keyPair.key) {
|
||||||
|
currentList = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(prevState => ({
|
||||||
|
currentList: currentList,
|
||||||
|
sessionData: {
|
||||||
|
nextKey: prevState.sessionData.nextKey,
|
||||||
|
counter: prevState.sessionData.counter,
|
||||||
|
keyNamePairs: newKeyNamePairs}
|
||||||
|
}), () => {
|
||||||
|
let list = this.db.queryGetList(keyPair.key);
|
||||||
|
this.db.mutationDeleteList(list);
|
||||||
|
this.db.mutationUpdateSessionData(this.state.sessionData);
|
||||||
|
});
|
||||||
|
this.hideDeleteListModal();
|
||||||
|
}
|
||||||
|
// THIS FUNCTION SHOWS THE MODAL FOR PROMPTING THE USER
|
||||||
|
// TO SEE IF THEY REALLY WANT TO DELETE THE LIST
|
||||||
|
showDeleteListModal = (keyPair) => {
|
||||||
|
console.log("keyPair: " + keyPair.name);
|
||||||
|
this.setState({listKeyPairMarkedForDeletion: keyPair});
|
||||||
|
let modal = document.getElementById("delete-modal");
|
||||||
|
modal.classList.add("is-visible");
|
||||||
|
}
|
||||||
|
// THIS FUNCTION IS FOR HIDING THE MODAL
|
||||||
|
hideDeleteListModal() {
|
||||||
|
let modal = document.getElementById("delete-modal");
|
||||||
|
modal.classList.remove("is-visible");
|
||||||
|
}
|
||||||
|
setEditStatus = (bool) => {
|
||||||
|
this.setState({isEditing: bool});
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div id="app-root" onKeyDown={this.handleKeydown} tabIndex={-1}>
|
||||||
|
<Banner
|
||||||
|
title='Top 5 Lister'
|
||||||
|
closeCallback={this.closeCurrentList}
|
||||||
|
undoCallback={this.undo}
|
||||||
|
redoCallback={this.redo}
|
||||||
|
hasUndo={this.state.hasUndo}
|
||||||
|
hasRedo={this.state.hasRedo}
|
||||||
|
isEditing={this.state.isEditing}
|
||||||
|
currentList={this.state.currentList}/>
|
||||||
|
<Sidebar
|
||||||
|
heading='Your Lists'
|
||||||
|
currentList={this.state.currentList}
|
||||||
|
keyNamePairs={this.state.sessionData.keyNamePairs}
|
||||||
|
createNewListCallback={this.createNewList}
|
||||||
|
deleteListCallback={this.showDeleteListModal}
|
||||||
|
loadListCallback={this.loadList}
|
||||||
|
renameListCallback={this.renameList}
|
||||||
|
isEditing={this.state.isEditing}
|
||||||
|
setEditStatusCallback={this.setEditStatus} />
|
||||||
|
<Workspace
|
||||||
|
currentList={this.state.currentList}
|
||||||
|
renameItemCallback={this.addRenameItemTransaction}
|
||||||
|
moveItemCallback={this.addMoveItemTransaction}
|
||||||
|
isEditing={this.state.isEditing}
|
||||||
|
setEditStatusCallback={this.setEditStatus} />
|
||||||
|
<Statusbar
|
||||||
|
currentList={this.state.currentList} />
|
||||||
|
<DeleteModal
|
||||||
|
listKeyPair={this.state.listKeyPairMarkedForDeletion}
|
||||||
|
hideDeleteListModalCallback={this.hideDeleteListModal}
|
||||||
|
deleteListCallback={this.deleteList}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
26
hw2/src/common/MoveItem_Transaction.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import jsTPS_Transaction from './jsTPS';
|
||||||
|
/**
|
||||||
|
* MoveItem_Transaction
|
||||||
|
*
|
||||||
|
* This class represents a transaction that works with drag
|
||||||
|
* and drop. It will be managed by the transaction stack.
|
||||||
|
*
|
||||||
|
* @author McKilla Gorilla
|
||||||
|
* @author ?
|
||||||
|
*/
|
||||||
|
export default class MoveItem_Transaction extends jsTPS_Transaction {
|
||||||
|
constructor(initapp, initOld, initNew) {
|
||||||
|
super();
|
||||||
|
this.app = initapp;
|
||||||
|
this.oldItemIndex = initOld;
|
||||||
|
this.newItemIndex = initNew;
|
||||||
|
}
|
||||||
|
|
||||||
|
doTransaction() {
|
||||||
|
this.app.moveItem(this.oldItemIndex, this.newItemIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
undoTransaction() {
|
||||||
|
this.app.moveItem(this.newItemIndex, this.oldItemIndex);
|
||||||
|
}
|
||||||
|
}
|
28
hw2/src/common/RenameItemTransaction.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import jsTPS_Transaction from './jsTPS';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RenameItem_Transaction
|
||||||
|
*
|
||||||
|
* This class represents a transaction that updates the text
|
||||||
|
* for a given item. It will be managed by the transaction stack.
|
||||||
|
*
|
||||||
|
* @author McKilla Gorilla
|
||||||
|
* @author ?
|
||||||
|
*/
|
||||||
|
export default class RenameItem_Transaction extends jsTPS_Transaction {
|
||||||
|
constructor(initApp, initId, initOldText, initNewText) {
|
||||||
|
super();
|
||||||
|
this.app = initApp;
|
||||||
|
this.id = initId;
|
||||||
|
this.oldText = initOldText;
|
||||||
|
this.newText = initNewText;
|
||||||
|
}
|
||||||
|
|
||||||
|
doTransaction() {
|
||||||
|
this.app.renameItem(this.id, this.newText);
|
||||||
|
}
|
||||||
|
|
||||||
|
undoTransaction() {
|
||||||
|
this.app.renameItem(this.id, this.oldText);
|
||||||
|
}
|
||||||
|
}
|
217
hw2/src/common/jsTPS.js
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
//'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* jsTPS_Transaction
|
||||||
|
*
|
||||||
|
* This provides the basic structure for a transaction class. Note to use
|
||||||
|
* jsTPS one should create objects that define these two methods, doTransaction
|
||||||
|
* and undoTransaction, which will update the application state accordingly.
|
||||||
|
*
|
||||||
|
* @author THE McKilla Gorilla (accept no imposters)
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
export class jsTPS_Transaction {
|
||||||
|
/**
|
||||||
|
* This method is called by jTPS when a transaction is executed.
|
||||||
|
*/
|
||||||
|
doTransaction() {
|
||||||
|
console.log("doTransaction - MISSING IMPLEMENTATION");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by jTPS when a transaction is undone.
|
||||||
|
*/
|
||||||
|
undoTransaction() {
|
||||||
|
console.log("undoTransaction - MISSING IMPLEMENTATION");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* jsTPS
|
||||||
|
*
|
||||||
|
* This class serves as the Transaction Processing System. Note that it manages
|
||||||
|
* a stack of jsTPS_Transaction objects, each of which know how to do or undo
|
||||||
|
* state changes for the given application. Note that this TPS is not platform
|
||||||
|
* specific as it is programmed in raw JavaScript.
|
||||||
|
*/
|
||||||
|
export default class jsTPS {
|
||||||
|
constructor() {
|
||||||
|
// THE TRANSACTION STACK
|
||||||
|
this.transactions = [];
|
||||||
|
|
||||||
|
// THE TOTAL NUMBER OF TRANSACTIONS ON THE STACK,
|
||||||
|
// INCLUDING THOSE THAT MAY HAVE ALREADY BEEN UNDONE
|
||||||
|
this.numTransactions = 0;
|
||||||
|
|
||||||
|
// THE INDEX OF THE MOST RECENT TRANSACTION, NOTE THAT
|
||||||
|
// THIS MAY BE IN THE MIDDLE OF THE TRANSACTION STACK
|
||||||
|
// IF SOME TRANSACTIONS ON THE STACK HAVE BEEN UNDONE
|
||||||
|
// AND STILL COULD BE REDONE.
|
||||||
|
this.mostRecentTransaction = -1;
|
||||||
|
|
||||||
|
// THESE STATE VARIABLES ARE TURNED ON AND OFF WHILE
|
||||||
|
// TRANSACTIONS ARE DOING THEIR WORK SO AS TO HELP
|
||||||
|
// MANAGE CONCURRENT UPDATES
|
||||||
|
this.performingDo = false;
|
||||||
|
this.performingUndo = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* isPerformingDo
|
||||||
|
*
|
||||||
|
* Accessor method for getting a boolean representing whether or not
|
||||||
|
* a transaction is currently in the midst of a do/redo operation.
|
||||||
|
*/
|
||||||
|
isPerformingDo() {
|
||||||
|
return this.performingDo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* isPerformingUndo
|
||||||
|
*
|
||||||
|
* Accessor method for getting a boolean representing whether or not
|
||||||
|
* a transaction is currently in the midst of an undo operation.
|
||||||
|
*/
|
||||||
|
isPerformingUndo() {
|
||||||
|
return this.performingUndo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getSize
|
||||||
|
*
|
||||||
|
* Accessor method for getting the number of transactions on the stack.
|
||||||
|
*/
|
||||||
|
getSize() {
|
||||||
|
return this.transactions.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getRedoSize
|
||||||
|
*
|
||||||
|
* Method for getting the total number of transactions on the stack
|
||||||
|
* that can possibly be redone.
|
||||||
|
*/
|
||||||
|
getRedoSize() {
|
||||||
|
return this.getSize() - this.mostRecentTransaction - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getUndoSize
|
||||||
|
*
|
||||||
|
* Method for getting the total number of transactions on the stack
|
||||||
|
* that can possible be undone.
|
||||||
|
*/
|
||||||
|
getUndoSize() {
|
||||||
|
return this.mostRecentTransaction + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hasTransactionToRedo
|
||||||
|
*
|
||||||
|
* Method for getting a boolean representing whether or not
|
||||||
|
* there are transactions on the stack that can be redone.
|
||||||
|
*/
|
||||||
|
hasTransactionToRedo() {
|
||||||
|
return (this.mostRecentTransaction+1) < this.numTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hasTransactionToUndo
|
||||||
|
*
|
||||||
|
* Method for getting a boolean representing whehter or not
|
||||||
|
* there are transactions on the stack that can be undone.
|
||||||
|
*/
|
||||||
|
hasTransactionToUndo() {
|
||||||
|
return this.mostRecentTransaction >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* addTransaction
|
||||||
|
*
|
||||||
|
* Method for adding a transaction to the TPS stack, note it
|
||||||
|
* also then does the transaction.
|
||||||
|
*
|
||||||
|
* @param {jsTPS_Transaction} transaction Transaction to add to the stack and do.
|
||||||
|
*/
|
||||||
|
addTransaction(transaction) {
|
||||||
|
// ARE WE BRANCHING?
|
||||||
|
if ((this.mostRecentTransaction < 0)
|
||||||
|
|| (this.mostRecentTransaction < (this.transactions.length - 1))) {
|
||||||
|
for (let i = this.transactions.length - 1; i > this.mostRecentTransaction; i--) {
|
||||||
|
this.transactions.splice(i, 1);
|
||||||
|
}
|
||||||
|
this.numTransactions = this.mostRecentTransaction + 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.numTransactions++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADD THE TRANSACTION
|
||||||
|
this.transactions[this.mostRecentTransaction+1] = transaction;
|
||||||
|
|
||||||
|
// AND EXECUTE IT
|
||||||
|
this.doTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* doTransaction
|
||||||
|
*
|
||||||
|
* Does the current transaction on the stack and advances the transaction
|
||||||
|
* counter. Note this function may be invoked as a result of either adding
|
||||||
|
* a transaction (which also does it), or redoing a transaction.
|
||||||
|
*/
|
||||||
|
doTransaction() {
|
||||||
|
if (this.hasTransactionToRedo()) {
|
||||||
|
this.performingDo = true;
|
||||||
|
let transaction = this.transactions[this.mostRecentTransaction+1];
|
||||||
|
transaction.doTransaction();
|
||||||
|
this.mostRecentTransaction++;
|
||||||
|
this.performingDo = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function gets the most recently executed transaction on the
|
||||||
|
* TPS stack and undoes it, moving the TPS counter accordingly.
|
||||||
|
*/
|
||||||
|
undoTransaction() {
|
||||||
|
if (this.hasTransactionToUndo()) {
|
||||||
|
this.performingUndo = true;
|
||||||
|
let transaction = this.transactions[this.mostRecentTransaction];
|
||||||
|
transaction.undoTransaction();
|
||||||
|
this.mostRecentTransaction--;
|
||||||
|
this.performingUndo = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clearAllTransactions
|
||||||
|
*
|
||||||
|
* Removes all the transactions from the TPS, leaving it with none.
|
||||||
|
*/
|
||||||
|
clearAllTransactions() {
|
||||||
|
// REMOVE ALL THE TRANSACTIONS
|
||||||
|
this.transactions = [];
|
||||||
|
|
||||||
|
// MAKE SURE TO RESET THE LOCATION OF THE
|
||||||
|
// TOP OF THE TPS STACK TOO
|
||||||
|
this.mostRecentTransaction = -1;
|
||||||
|
this.numTransactions = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* toString
|
||||||
|
*
|
||||||
|
* Builds and returns a textual represention of the full TPS and its stack.
|
||||||
|
*/
|
||||||
|
toString() {
|
||||||
|
let text = "--Number of Transactions: " + this.numTransactions + "\n";
|
||||||
|
text += "--Current Index on Stack: " + this.mostRecentTransaction + "\n";
|
||||||
|
text += "--Current Transaction Stack:\n";
|
||||||
|
for (let i = 0; i <= this.mostRecentTransaction; i++) {
|
||||||
|
let jT = this.transactions[i];
|
||||||
|
text += "----" + jT.toString() + "\n";
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
21
hw2/src/components/Banner.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React from "react";
|
||||||
|
import EditToolbar from "./EditToolbar";
|
||||||
|
|
||||||
|
export default class Banner extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {title, closeCallback, undoCallback, redoCallback, hasUndo, hasRedo, currentList, isEditing} = this.props;
|
||||||
|
return (
|
||||||
|
<div id="top5-banner">
|
||||||
|
{title}
|
||||||
|
<EditToolbar
|
||||||
|
closeCallback={closeCallback}
|
||||||
|
undoCallback={undoCallback}
|
||||||
|
redoCallback={redoCallback}
|
||||||
|
hasUndo={hasUndo}
|
||||||
|
hasRedo={hasRedo}
|
||||||
|
isEditing={isEditing}
|
||||||
|
currentList={currentList}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
35
hw2/src/components/DeleteModal.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
export default class DeleteModal extends Component {
|
||||||
|
render() {
|
||||||
|
const { listKeyPair, hideDeleteListModalCallback, deleteListCallback } = this.props;
|
||||||
|
let name = "";
|
||||||
|
if (listKeyPair) {
|
||||||
|
name = listKeyPair.name;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="modal"
|
||||||
|
id="delete-modal"
|
||||||
|
data-animation="slideInOutLeft">
|
||||||
|
<div className="modal-dialog">
|
||||||
|
<header className="dialog-header">
|
||||||
|
Delete the {name} Top 5 List?
|
||||||
|
</header>
|
||||||
|
<div id="confirm-cancel-container">
|
||||||
|
<button
|
||||||
|
id="dialog-yes-button"
|
||||||
|
className="modal-button"
|
||||||
|
onClick={deleteListCallback}
|
||||||
|
>Confirm</button>
|
||||||
|
<button
|
||||||
|
id="dialog-no-button"
|
||||||
|
className="modal-button"
|
||||||
|
onClick={hideDeleteListModalCallback}
|
||||||
|
>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
30
hw2/src/components/EditToolbar.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default class EditToolbar extends React.Component {
|
||||||
|
doNothing() {}
|
||||||
|
render() {
|
||||||
|
const {closeCallback, undoCallback, redoCallback, hasUndo, hasRedo, currentList, isEditing} = this.props;
|
||||||
|
return (
|
||||||
|
<div id="edit-toolbar">
|
||||||
|
<div
|
||||||
|
id='undo-button'
|
||||||
|
onClick={hasUndo?undoCallback:this.doNothing}
|
||||||
|
className={(hasUndo&&!isEditing)?"top5-button":"top5-button-disabled"}>
|
||||||
|
↶
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id='redo-button'
|
||||||
|
onClick={hasRedo?redoCallback:this.doNothing}
|
||||||
|
className={(hasRedo&&!isEditing)?"top5-button":"top5-button-disabled"}>
|
||||||
|
↷
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id='close-button'
|
||||||
|
onClick={currentList?closeCallback:this.doNothing}
|
||||||
|
className={(currentList&&!isEditing)?"top5-button":"top5-button-disabled"}>
|
||||||
|
ⓧ
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
97
hw2/src/components/ListCard.js
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default class ListCard extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
text: this.props.keyNamePair.name,
|
||||||
|
editActive: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleClick = (event) => {
|
||||||
|
if (event.detail === 1) {
|
||||||
|
this.handleLoadList(event);
|
||||||
|
}
|
||||||
|
else if (!this.props.isEditing&&event.detail === 2) {
|
||||||
|
this.handleToggleEdit(event);
|
||||||
|
this.props.setEditStatusCallback(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleLoadList = (event) => {
|
||||||
|
let listKey = event.target.id;
|
||||||
|
if (listKey.startsWith("list-card-text-")) {
|
||||||
|
listKey = listKey.substring("list-card-text-".length);
|
||||||
|
}
|
||||||
|
this.props.loadListCallback(listKey);
|
||||||
|
}
|
||||||
|
handleDeleteList = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.props.deleteListCallback(this.props.keyNamePair);
|
||||||
|
}
|
||||||
|
handleToggleEdit = () => {
|
||||||
|
this.setState({
|
||||||
|
editActive: !this.state.editActive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleUpdate = (event) => {
|
||||||
|
this.setState({ text: event.target.value });
|
||||||
|
}
|
||||||
|
handleKeyPress = (event) => {
|
||||||
|
if (event.code === "Enter") {
|
||||||
|
this.handleBlur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleBlur = () => {
|
||||||
|
let key = this.props.keyNamePair.key;
|
||||||
|
let textValue = this.state.text;
|
||||||
|
console.log("ListCard handleBlur: " + textValue);
|
||||||
|
this.props.renameListCallback(key, textValue);
|
||||||
|
this.handleToggleEdit();
|
||||||
|
this.props.setEditStatusCallback(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { keyNamePair, selected } = this.props;
|
||||||
|
|
||||||
|
if (this.state.editActive) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
id={"list-" + keyNamePair.name}
|
||||||
|
className='list-card'
|
||||||
|
type='text'
|
||||||
|
onKeyPress={this.handleKeyPress}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
onChange={this.handleUpdate}
|
||||||
|
defaultValue={keyNamePair.name}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
let selectClass = "unselected-list-card";
|
||||||
|
if (selected) {
|
||||||
|
selectClass = "selected-list-card";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={keyNamePair.key}
|
||||||
|
key={keyNamePair.key}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
className={'list-card ' + selectClass}>
|
||||||
|
<span
|
||||||
|
id={"list-card-text-" + keyNamePair.key}
|
||||||
|
key={keyNamePair.key}
|
||||||
|
className="list-card-text">
|
||||||
|
{keyNamePair.name}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="button"
|
||||||
|
id={"delete-list-" + keyNamePair.key}
|
||||||
|
className="list-card-button"
|
||||||
|
onClick={this.handleDeleteList}
|
||||||
|
value={"\u2715"} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
104
hw2/src/components/ListItem.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default class ListItem extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
index: this.props.index,
|
||||||
|
editActive: false,
|
||||||
|
isOver: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleClick = (event) => {
|
||||||
|
if (!this.props.isEditing&&event.detail ===2) {
|
||||||
|
this.handleEdit();
|
||||||
|
this.props.setEditStatusCallback(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleEdit = () => {
|
||||||
|
this.setState({editActive: !this.state.editActive});
|
||||||
|
}
|
||||||
|
handleKeyPress = (event) => {
|
||||||
|
if (event.code ==="Enter") {
|
||||||
|
this.handleBlur(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handleBlur = (event) => {
|
||||||
|
let index = this.props.index;
|
||||||
|
let text = event.target.value;
|
||||||
|
console.log(text === this.props.text);
|
||||||
|
if (this.props.text !== text) {
|
||||||
|
this.props.renameItemCallback(index, text);
|
||||||
|
}
|
||||||
|
this.handleEdit();
|
||||||
|
this.props.setEditStatusCallback(false);
|
||||||
|
this.setState( {text: text} );
|
||||||
|
}
|
||||||
|
handleDragStart = (event) => {
|
||||||
|
event.dataTransfer.setData("text", this.state.index);
|
||||||
|
console.log("dragstart: " + this.state.index);
|
||||||
|
}
|
||||||
|
handleDragOver = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.setState({isOver: true});
|
||||||
|
console.log("dragover: " + this.state.index);
|
||||||
|
}
|
||||||
|
handleDragLeave = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.setState({isOver: false});
|
||||||
|
console.log("dragleave: " + this.state.index);
|
||||||
|
}
|
||||||
|
handleDrop = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
let oldIndex = event.dataTransfer.getData("text");
|
||||||
|
let newIndex = this.state.index.toString();
|
||||||
|
if (oldIndex !== newIndex) {
|
||||||
|
console.log("ondrop: old: " + oldIndex + " new: " + newIndex);
|
||||||
|
console.log(typeof newIndex + " + " + typeof oldIndex);
|
||||||
|
this.props.moveItemCallback(oldIndex, newIndex);
|
||||||
|
}
|
||||||
|
this.setState({isOver: false});
|
||||||
|
}
|
||||||
|
setClassName = () => {
|
||||||
|
if (this.state.isOver) {
|
||||||
|
return("top5-item");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return("top5-item-dragged-to");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
const {index, text, keyValue, isEditing} = this.props;
|
||||||
|
if (keyValue === -1) {
|
||||||
|
return (
|
||||||
|
<div className="top5-item" id={"item-"+index}></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (this.state.editActive) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
id={"item-"+index}
|
||||||
|
className="top5-item"
|
||||||
|
type="text"
|
||||||
|
onKeyPress={this.handleKeyPress}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
defaultValue={text}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={this.state.isOver?"top5-item-dragged-to":"top5-item"}
|
||||||
|
id={"item-"+index}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
draggable={isEditing?false:true}
|
||||||
|
onDragStart={this.handleDragStart}
|
||||||
|
onDragOver={this.handleDragOver}
|
||||||
|
onDragLeave={this.handleDragLeave}
|
||||||
|
onDrop={this.handleDrop}
|
||||||
|
>{text}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
hw2/src/components/Sidebar.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import React from "react";
|
||||||
|
import ListCard from "./ListCard";
|
||||||
|
|
||||||
|
export default class Sidebar extends React.Component {
|
||||||
|
doNothing() {}
|
||||||
|
render() {
|
||||||
|
const { heading,
|
||||||
|
currentList,
|
||||||
|
keyNamePairs,
|
||||||
|
createNewListCallback,
|
||||||
|
deleteListCallback,
|
||||||
|
loadListCallback,
|
||||||
|
setEditStatusCallback,
|
||||||
|
isEditing,
|
||||||
|
renameListCallback} = this.props;
|
||||||
|
return (
|
||||||
|
<div id="top5-sidebar">
|
||||||
|
<div id="sidebar-heading">
|
||||||
|
<input
|
||||||
|
type="button"
|
||||||
|
id="add-list-button"
|
||||||
|
onClick={!isEditing?createNewListCallback:this.doNothing}
|
||||||
|
className={isEditing?"top5-button-disabled":"top5-button"}
|
||||||
|
value="+" />
|
||||||
|
{heading}
|
||||||
|
</div>
|
||||||
|
<div id="sidebar-list">
|
||||||
|
{
|
||||||
|
keyNamePairs.map((pair) => (
|
||||||
|
<ListCard
|
||||||
|
key={pair.key}
|
||||||
|
keyNamePair={pair}
|
||||||
|
selected={(currentList !== null) && (currentList.key === pair.key)}
|
||||||
|
deleteListCallback={deleteListCallback}
|
||||||
|
loadListCallback={loadListCallback}
|
||||||
|
renameListCallback={renameListCallback}
|
||||||
|
isEditing={isEditing}
|
||||||
|
setEditStatusCallback={setEditStatusCallback}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
16
hw2/src/components/Statusbar.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default class Statusbar extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {currentList} = this.props;
|
||||||
|
let name = "";
|
||||||
|
if (currentList) {
|
||||||
|
name = currentList.name;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div id="top5-statusbar">
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
38
hw2/src/components/Workspace.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React from "react";
|
||||||
|
import ListItem from "./ListItem";
|
||||||
|
|
||||||
|
export default class Workspace extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {currentList, renameItemCallback, moveItemCallback, setEditStatusCallback, isEditing} = this.props;
|
||||||
|
let items = ["","","","",""];
|
||||||
|
let keyValue = -1;
|
||||||
|
if (currentList) {
|
||||||
|
items = currentList.items;
|
||||||
|
keyValue = currentList.key;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
items = ["","","","",""];
|
||||||
|
keyValue = -1;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div id="top5-workspace">
|
||||||
|
<div id="workspace-edit">
|
||||||
|
<div id="edit-numbering">
|
||||||
|
<div className="item-number">1.</div>
|
||||||
|
<div className="item-number">2.</div>
|
||||||
|
<div className="item-number">3.</div>
|
||||||
|
<div className="item-number">4.</div>
|
||||||
|
<div className="item-number">5.</div>
|
||||||
|
</div>
|
||||||
|
<div id="edit-items">
|
||||||
|
<ListItem index={0} text={items[0]} renameItemCallback={renameItemCallback} moveItemCallback={moveItemCallback} keyValue={keyValue} setEditStatusCallback={setEditStatusCallback} isEditing={isEditing}/>
|
||||||
|
<ListItem index={1} text={items[1]} renameItemCallback={renameItemCallback} moveItemCallback={moveItemCallback} keyValue={keyValue} setEditStatusCallback={setEditStatusCallback} isEditing={isEditing}/>
|
||||||
|
<ListItem index={2} text={items[2]} renameItemCallback={renameItemCallback} moveItemCallback={moveItemCallback} keyValue={keyValue} setEditStatusCallback={setEditStatusCallback} isEditing={isEditing}/>
|
||||||
|
<ListItem index={3} text={items[3]} renameItemCallback={renameItemCallback} moveItemCallback={moveItemCallback} keyValue={keyValue} setEditStatusCallback={setEditStatusCallback} isEditing={isEditing}/>
|
||||||
|
<ListItem index={4} text={items[4]} renameItemCallback={renameItemCallback} moveItemCallback={moveItemCallback} keyValue={keyValue} setEditStatusCallback={setEditStatusCallback} isEditing={isEditing}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
43
hw2/src/db/DBManager.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
export default class DBManager {
|
||||||
|
// QUERY AND MUTATION FUNCTIONS GET/SET DATA FROM/TO
|
||||||
|
// AN EXTERNAL SOURCE, WHICH FOR THIS APPLICATION
|
||||||
|
// MEANS THE BROWSER'S LOCAL STORAGE
|
||||||
|
queryGetSessionData = () => {
|
||||||
|
let sessionDataString = localStorage.getItem("top5-data");
|
||||||
|
return JSON.parse(sessionDataString);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryIsList = (key) => {
|
||||||
|
let list = localStorage.getItem("top5-list-" + key);
|
||||||
|
return list != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This query asks local storage for a list with a particular key,
|
||||||
|
* which is then returned by this function.
|
||||||
|
*/
|
||||||
|
queryGetList = (key) => {
|
||||||
|
let listString = localStorage.getItem("top5-list-" + key);
|
||||||
|
return JSON.parse(listString);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutationCreateList = (list) => {
|
||||||
|
this.mutationUpdateList(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutationUpdateList = (list) => {
|
||||||
|
// AND FLOW THOSE CHANGES TO LOCAL STORAGE
|
||||||
|
let listString = JSON.stringify(list);
|
||||||
|
localStorage.setItem("top5-list-" + list.key, listString);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutationDeleteList = (list) => {
|
||||||
|
let listString = JSON.stringify(list);
|
||||||
|
localStorage.removeItem("top5-list-" + list.key, listString);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutationUpdateSessionData = (sessionData) => {
|
||||||
|
let sessionDataString = JSON.stringify(sessionData);
|
||||||
|
localStorage.setItem("top5-data", sessionDataString);
|
||||||
|
}
|
||||||
|
}
|
67
hw2/src/index.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import App from './App';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
|
// THIS FUNCTION TESTS TO SEE IF THIS APP HAS
|
||||||
|
// DATA IN LOCAL STORAGE. IF IT DOES, TRUE IS
|
||||||
|
// RETURNED, ELSE FALSE
|
||||||
|
function isInLocalStorage() {
|
||||||
|
return localStorage.getItem("top5-data") != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadListsFromJSON(jsonFilePath) {
|
||||||
|
let xmlhttp = new XMLHttpRequest();
|
||||||
|
xmlhttp.onreadystatechange = function () {
|
||||||
|
if (this.readyState === 4 && this.status === 200) {
|
||||||
|
let text = this.responseText;
|
||||||
|
let lists = JSON.parse(text).top5Lists;
|
||||||
|
|
||||||
|
// GO THROUGH THE LISTS AND SAVE EACH USING THEIR KEY
|
||||||
|
for (let i = 0; i < lists.length; i++) {
|
||||||
|
let listData = lists[i];
|
||||||
|
let listString = JSON.stringify(listData);
|
||||||
|
localStorage.setItem("top5-list-" + listData.key, listString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS IS OUR SESSION DATA THAT WE'LL NEED TO
|
||||||
|
// HELP US DEAL WITH THE LISTS
|
||||||
|
localStorage.setItem("top5-data", JSON.stringify(
|
||||||
|
{
|
||||||
|
"nextKey" : 3,
|
||||||
|
"counter" : 3,
|
||||||
|
"keyNamePairs" : [
|
||||||
|
{"key": "0", "name": "Games"},
|
||||||
|
{"key": "1", "name": "Movies"},
|
||||||
|
{"key": "2", "name": "Pink Floyd Songs"}
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
launch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xmlhttp.open("GET", jsonFilePath, true);
|
||||||
|
xmlhttp.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function launch() {
|
||||||
|
// IF NO DATA IS IN LOCAL STORAGE THEN LOAD ALL THE TEST
|
||||||
|
// DATA FROM THE JSON FILE AND PUT IT THERE
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInLocalStorage()) {
|
||||||
|
loadListsFromJSON("./data/default_lists.json");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
13
hw2/src/reportWebVitals.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
const reportWebVitals = onPerfEntry => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
4
hw3/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
./client/node_modules
|
||||||
|
./server/node_modules
|
||||||
|
|
||||||
|
node_modules
|
38134
hw3/client/package-lock.json
generated
Normal file
45
hw3/client/package.json
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"name": "top5-lister-hw3",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
|
"@testing-library/react": "^11.2.7",
|
||||||
|
"@testing-library/user-event": "^12.8.3",
|
||||||
|
"axios": "^0.22.0",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-router-dom": "^5.3.0",
|
||||||
|
"react-scripts": "4.0.3",
|
||||||
|
"web-vitals": "^1.1.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).",
|
||||||
|
"main": "index.js",
|
||||||
|
"devDependencies": {},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
52
hw3/client/public/data/default_lists.json
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"top5Lists": [
|
||||||
|
{
|
||||||
|
"key": "0",
|
||||||
|
"name": "Games",
|
||||||
|
"likes": 2150,
|
||||||
|
"dislikes": 114,
|
||||||
|
"created": "Thu, 22 Oct 2020 01:10:53 GMT",
|
||||||
|
"modified": "Wed, 23 Jun 2021 03:21:31 GMT",
|
||||||
|
"accessed": "Thu, 26 Aug 2021 03:23:31 GMT",
|
||||||
|
"items": [
|
||||||
|
"StarCraft",
|
||||||
|
"Fallout 3",
|
||||||
|
"Katamari Damacy",
|
||||||
|
"Civilization II",
|
||||||
|
"Super Mario World"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "1",
|
||||||
|
"name": "Movies",
|
||||||
|
"likes": 50,
|
||||||
|
"dislikes": 4,
|
||||||
|
"created": "Mon, 23 Aug 2021 03:12:53 GMT",
|
||||||
|
"modified": "Mon, 23 Aug 2021 03:14:31 GMT",
|
||||||
|
"accessed": "Mon, 23 Aug 2021 03:16:31 GMT",
|
||||||
|
"items": [
|
||||||
|
"Raiders of the Lost Ark",
|
||||||
|
"Goodfellas",
|
||||||
|
"Lord of the Rings",
|
||||||
|
"Airplane!",
|
||||||
|
"Lawrence of Arabia"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "2",
|
||||||
|
"name": "Pink Floyd Songs",
|
||||||
|
"likes": 520,
|
||||||
|
"dislikes": 14,
|
||||||
|
"created": "Sun, 22 Aug 2021 03:19:53 GMT",
|
||||||
|
"modified": "Mon, 24 Aug 2021 03:21:31 GMT",
|
||||||
|
"accessed": "Wed, 26 Aug 2021 03:23:31 GMT",
|
||||||
|
"items": [
|
||||||
|
"Shine On You Crazy Diamond",
|
||||||
|
"Comfortably Numb",
|
||||||
|
"Pigs (Three Different Ones)",
|
||||||
|
"Echoes (Live at Pompeii)",
|
||||||
|
"Time"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
hw3/client/public/favicon.ico
Normal file
After Width: | Height: | Size: 3.8 KiB |
43
hw3/client/public/index.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
-->
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<!--
|
||||||
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
|
-->
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
hw3/client/public/logo192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
hw3/client/public/logo512.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
25
hw3/client/public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
3
hw3/client/public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
417
hw3/client/src/App.css
Normal file
|
@ -0,0 +1,417 @@
|
||||||
|
:root {
|
||||||
|
/*
|
||||||
|
FIRST WE'LL DEFINE OUR SWATCHES, i.e. THE COMPLEMENTARY
|
||||||
|
COLORS THAT WE'LL USE TOGETHER IN MULTIPLE PLACES THAT
|
||||||
|
TOGETHER WILL MAKE UP A GIVEN THEME
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
--swatch-foundation: #eeeedd;
|
||||||
|
--swatch-primary: #e6e6e6;
|
||||||
|
--swatch-complement: #e1e4cb;
|
||||||
|
--swatch-contrast: #111111;
|
||||||
|
--swatch-accent: #669966;
|
||||||
|
--swatch-status: #123456;
|
||||||
|
--my-font-family: "Robaaaoto";
|
||||||
|
--bounceEasing: cubic-bezier(0.51, 0.92, 0.24, 1.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--swatch-foundation);
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
background-color: var(--swatch-primary);
|
||||||
|
font-family: "Lexend Exa";
|
||||||
|
position: absolute;
|
||||||
|
width: 80%;
|
||||||
|
left: 10%;
|
||||||
|
height:90%;
|
||||||
|
top: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app-root {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0%;
|
||||||
|
left: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-banner {
|
||||||
|
position:absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 0%;
|
||||||
|
width: 100%;
|
||||||
|
height: 10%;
|
||||||
|
float:left;
|
||||||
|
background-image: linear-gradient(to bottom,
|
||||||
|
#b8b808, #636723);
|
||||||
|
color: white;
|
||||||
|
font-size: 48pt;
|
||||||
|
border-color: black;
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-toolbar {
|
||||||
|
|
||||||
|
background-color: transparent;
|
||||||
|
float:right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-button, .top5-button-disabled {
|
||||||
|
font-size:36pt;
|
||||||
|
border-width: 0px;
|
||||||
|
float:left;
|
||||||
|
color: black;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color:black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-button-disabled {
|
||||||
|
opacity: 0.25;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-list-selector {
|
||||||
|
position:absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 10%;
|
||||||
|
width: 100%;
|
||||||
|
height: 80%;
|
||||||
|
background-color: var(--swatch-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-workspace {
|
||||||
|
position:absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 10%;
|
||||||
|
width: 100%;
|
||||||
|
height: 80%;
|
||||||
|
background-color: var(--swatch-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#top5-statusbar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0%;
|
||||||
|
top: 90%;
|
||||||
|
width: 100%;
|
||||||
|
height: 10%;
|
||||||
|
background-color: lightsalmon;
|
||||||
|
display:flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 36pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list-selector-heading {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:0%;
|
||||||
|
width:100%;
|
||||||
|
height:10%;
|
||||||
|
text-align:center;
|
||||||
|
font-size: 24pt;
|
||||||
|
font-weight: bold;
|
||||||
|
display:flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-list-button {
|
||||||
|
float:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list-selector-list {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:10%;
|
||||||
|
width:100%;
|
||||||
|
height:90%;
|
||||||
|
display:flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card, .selected-list-card, .unselected-list-card {
|
||||||
|
font-size: 18pt;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card:aria-disabled,.list-card[aria-disabled] {
|
||||||
|
border: 1px solid #999999;
|
||||||
|
background-color: #cccccc;
|
||||||
|
color: #666666;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unselected-list-card:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--swatch-contrast);
|
||||||
|
color:white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-list-card {
|
||||||
|
background-color: var(--swatch-accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unselected-list-card {
|
||||||
|
background-color: var(--swatch-complement);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card-button {
|
||||||
|
float:right;
|
||||||
|
font-size:18pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#workspace-home, #workspace-edit {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:0%;
|
||||||
|
width:100%;
|
||||||
|
height:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-numbering {
|
||||||
|
position:absolute;
|
||||||
|
left:0%;
|
||||||
|
top:0%;
|
||||||
|
width:20%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--swatch-status);
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-items {
|
||||||
|
position:absolute;
|
||||||
|
left:20%;
|
||||||
|
top:0%;
|
||||||
|
width:80%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--swatch-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-number, .top5-item, .top5-item-dragged-to {
|
||||||
|
display:flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 24pt;
|
||||||
|
height:20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-number {
|
||||||
|
justify-content: center;
|
||||||
|
width:100%;
|
||||||
|
border: 1px 0px 1px 1px;
|
||||||
|
border-color:black;
|
||||||
|
background-color: linen;
|
||||||
|
color:black;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size:20pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top5-item, .top5-item-dragged-to {
|
||||||
|
text-align: left;
|
||||||
|
width:95%;
|
||||||
|
padding-left:5%;
|
||||||
|
}
|
||||||
|
.top5-item {
|
||||||
|
background-color: var(--swatch-complement);
|
||||||
|
}
|
||||||
|
.top5-item-dragged-to {
|
||||||
|
background-color: var(--swatch-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled:hover {
|
||||||
|
color: var(--swatch-neutral);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* THIS STYLE SHEET MANAGES STYLE FOR OUR MODAL, i.e. DIALOG BOX */
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--black);
|
||||||
|
color: var(--swatch-text);
|
||||||
|
cursor: pointer;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.35s ease-in;
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.is-visible {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
position: relative;
|
||||||
|
max-width: 800px;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: var(--swatch-complement);
|
||||||
|
overflow: auto;
|
||||||
|
cursor: default;
|
||||||
|
border-width: 5px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-style: groove;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog > * {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
|
.modal-footer {
|
||||||
|
background: var(--lightgray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
|
.modal-close {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal p + p {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-control {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
font-size: 24pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
#close-modal-button {
|
||||||
|
float:right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirm-cancel-container {
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ANIMATIONS
|
||||||
|
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||||
|
[data-animation] .modal-dialog {
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.5s var(--bounceEasing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation].is-visible .modal-dialog {
|
||||||
|
opacity: 1;
|
||||||
|
transition-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutDown"] .modal-dialog {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutTop"] .modal-dialog {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutLeft"] .modal-dialog {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutRight"] .modal-dialog {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="zoomInOut"] .modal-dialog {
|
||||||
|
transform: scale(0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="rotateInOutDown"] .modal-dialog {
|
||||||
|
transform-origin: top left;
|
||||||
|
transform: rotate(-1turn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="mixInAnimations"].is-visible .modal-dialog {
|
||||||
|
animation: mixInAnimations 2s 0.2s linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animation="slideInOutDown"].is-visible .modal-dialog,
|
||||||
|
[data-animation="slideInOutTop"].is-visible .modal-dialog,
|
||||||
|
[data-animation="slideInOutLeft"].is-visible .modal-dialog,
|
||||||
|
[data-animation="slideInOutRight"].is-visible .modal-dialog,
|
||||||
|
[data-animation="zoomInOut"].is-visible .modal-dialog,
|
||||||
|
[data-animation="rotateInOutDown"].is-visible .modal-dialog {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mixInAnimations {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
10% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
transform: rotate(20deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
transform: rotate(-20deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
transform: rotate(15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: rotate(-10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
transform: rotate(5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
90% {
|
||||||
|
transform: rotate(-5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
23
hw3/client/src/App.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import './App.css';
|
||||||
|
import { React } from 'react'
|
||||||
|
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
|
||||||
|
import { Banner, ListSelector, Statusbar, Workspace } from './components'
|
||||||
|
/*
|
||||||
|
This is our application's top-level component.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<Banner />
|
||||||
|
<Switch>
|
||||||
|
<Route path="/" exact component={ListSelector} />
|
||||||
|
<Route path="/top5list/:id" exact component={Workspace} />
|
||||||
|
</Switch>
|
||||||
|
<Statusbar />
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
40
hw3/client/src/api/index.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
This is our http api, which we use to send requests to
|
||||||
|
our back-end API. Note we're using the Axios library
|
||||||
|
for doing this, which is an easy to use AJAX-based
|
||||||
|
library. We could (and maybe should) use Fetch, which
|
||||||
|
is a native (to browsers) standard, but Axios is easier
|
||||||
|
to use when sending JSON back and forth and it's a Promise-
|
||||||
|
based API which helps a lot with asynchronous communication.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: 'http://localhost:4000/api',
|
||||||
|
})
|
||||||
|
|
||||||
|
// THESE ARE ALL THE REQUESTS WE'LL BE MAKING, ALL REQUESTS HAVE A
|
||||||
|
// REQUEST METHOD (like get) AND PATH (like /top5list). SOME ALSO
|
||||||
|
// REQUIRE AN id SO THAT THE SERVER KNOWS ON WHICH LIST TO DO ITS
|
||||||
|
// WORK, AND SOME REQUIRE DATA, WHICH WE CALL THE payload, FOR WHEN
|
||||||
|
// WE NEED TO PUT THINGS INTO THE DATABASE OR IF WE HAVE SOME
|
||||||
|
// CUSTOM FILTERS FOR QUERIES
|
||||||
|
export const createTop5List = (payload) => api.post(`/top5list`, payload)
|
||||||
|
export const getAllTop5Lists = () => api.get(`/top5lists`)
|
||||||
|
export const getTop5ListPairs = () => api.get('top5listpairs')
|
||||||
|
export const updateTop5ListById = (id, payload) => api.put(`/top5list/${id}`, payload)
|
||||||
|
export const deleteTop5ListById = (id) => api.delete(`/top5list/${id}`)
|
||||||
|
export const getTop5ListById = (id) => api.get(`/top5list/${id}`)
|
||||||
|
|
||||||
|
const apis = {
|
||||||
|
createTop5List,
|
||||||
|
getAllTop5Lists,
|
||||||
|
getTop5ListPairs,
|
||||||
|
updateTop5ListById,
|
||||||
|
deleteTop5ListById,
|
||||||
|
getTop5ListById,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default apis
|
215
hw3/client/src/common/jsTPS.js
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
/**
|
||||||
|
* jsTPS_Transaction
|
||||||
|
*
|
||||||
|
* This provides the basic structure for a transaction class. Note to use
|
||||||
|
* jsTPS one should create objects that define these two methods, doTransaction
|
||||||
|
* and undoTransaction, which will update the application state accordingly.
|
||||||
|
*
|
||||||
|
* @author THE McKilla Gorilla (accept no imposters)
|
||||||
|
* @version 1.0
|
||||||
|
*/
|
||||||
|
export class jsTPS_Transaction {
|
||||||
|
/**
|
||||||
|
* This method is called by jTPS when a transaction is executed.
|
||||||
|
*/
|
||||||
|
doTransaction() {
|
||||||
|
console.log("doTransaction - MISSING IMPLEMENTATION");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by jTPS when a transaction is undone.
|
||||||
|
*/
|
||||||
|
undoTransaction() {
|
||||||
|
console.log("undoTransaction - MISSING IMPLEMENTATION");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* jsTPS
|
||||||
|
*
|
||||||
|
* This class serves as the Transaction Processing System. Note that it manages
|
||||||
|
* a stack of jsTPS_Transaction objects, each of which know how to do or undo
|
||||||
|
* state changes for the given application. Note that this TPS is not platform
|
||||||
|
* specific as it is programmed in raw JavaScript.
|
||||||
|
*/
|
||||||
|
export default class jsTPS {
|
||||||
|
constructor() {
|
||||||
|
// THE TRANSACTION STACK
|
||||||
|
this.transactions = [];
|
||||||
|
|
||||||
|
// THE TOTAL NUMBER OF TRANSACTIONS ON THE STACK,
|
||||||
|
// INCLUDING THOSE THAT MAY HAVE ALREADY BEEN UNDONE
|
||||||
|
this.numTransactions = 0;
|
||||||
|
|
||||||
|
// THE INDEX OF THE MOST RECENT TRANSACTION, NOTE THAT
|
||||||
|
// THIS MAY BE IN THE MIDDLE OF THE TRANSACTION STACK
|
||||||
|
// IF SOME TRANSACTIONS ON THE STACK HAVE BEEN UNDONE
|
||||||
|
// AND STILL COULD BE REDONE.
|
||||||
|
this.mostRecentTransaction = -1;
|
||||||
|
|
||||||
|
// THESE STATE VARIABLES ARE TURNED ON AND OFF WHILE
|
||||||
|
// TRANSACTIONS ARE DOING THEIR WORK SO AS TO HELP
|
||||||
|
// MANAGE CONCURRENT UPDATES
|
||||||
|
this.performingDo = false;
|
||||||
|
this.performingUndo = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* isPerformingDo
|
||||||
|
*
|
||||||
|
* Accessor method for getting a boolean representing whether or not
|
||||||
|
* a transaction is currently in the midst of a do/redo operation.
|
||||||
|
*/
|
||||||
|
isPerformingDo() {
|
||||||
|
return this.performingDo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* isPerformingUndo
|
||||||
|
*
|
||||||
|
* Accessor method for getting a boolean representing whether or not
|
||||||
|
* a transaction is currently in the midst of an undo operation.
|
||||||
|
*/
|
||||||
|
isPerformingUndo() {
|
||||||
|
return this.performingUndo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getSize
|
||||||
|
*
|
||||||
|
* Accessor method for getting the number of transactions on the stack.
|
||||||
|
*/
|
||||||
|
getSize() {
|
||||||
|
return this.transactions.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getRedoSize
|
||||||
|
*
|
||||||
|
* Method for getting the total number of transactions on the stack
|
||||||
|
* that can possibly be redone.
|
||||||
|
*/
|
||||||
|
getRedoSize() {
|
||||||
|
return this.getSize() - this.mostRecentTransaction - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getUndoSize
|
||||||
|
*
|
||||||
|
* Method for getting the total number of transactions on the stack
|
||||||
|
* that can possible be undone.
|
||||||
|
*/
|
||||||
|
getUndoSize() {
|
||||||
|
return this.mostRecentTransaction + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hasTransactionToRedo
|
||||||
|
*
|
||||||
|
* Method for getting a boolean representing whether or not
|
||||||
|
* there are transactions on the stack that can be redone.
|
||||||
|
*/
|
||||||
|
hasTransactionToRedo() {
|
||||||
|
return (this.mostRecentTransaction+1) < this.numTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hasTransactionToUndo
|
||||||
|
*
|
||||||
|
* Method for getting a boolean representing whehter or not
|
||||||
|
* there are transactions on the stack that can be undone.
|
||||||
|
*/
|
||||||
|
hasTransactionToUndo() {
|
||||||
|
return this.mostRecentTransaction >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* addTransaction
|
||||||
|
*
|
||||||
|
* Method for adding a transaction to the TPS stack, note it
|
||||||
|
* also then does the transaction.
|
||||||
|
*
|
||||||
|
* @param {jsTPS_Transaction} transaction Transaction to add to the stack and do.
|
||||||
|
*/
|
||||||
|
addTransaction(transaction) {
|
||||||
|
// ARE WE BRANCHING?
|
||||||
|
if ((this.mostRecentTransaction < 0)
|
||||||
|
|| (this.mostRecentTransaction < (this.transactions.length - 1))) {
|
||||||
|
for (let i = this.transactions.length - 1; i > this.mostRecentTransaction; i--) {
|
||||||
|
this.transactions.splice(i, 1);
|
||||||
|
}
|
||||||
|
this.numTransactions = this.mostRecentTransaction + 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.numTransactions++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADD THE TRANSACTION
|
||||||
|
this.transactions[this.mostRecentTransaction+1] = transaction;
|
||||||
|
|
||||||
|
// AND EXECUTE IT
|
||||||
|
this.doTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* doTransaction
|
||||||
|
*
|
||||||
|
* Does the current transaction on the stack and advances the transaction
|
||||||
|
* counter. Note this function may be invoked as a result of either adding
|
||||||
|
* a transaction (which also does it), or redoing a transaction.
|
||||||
|
*/
|
||||||
|
doTransaction() {
|
||||||
|
if (this.hasTransactionToRedo()) {
|
||||||
|
this.performingDo = true;
|
||||||
|
let transaction = this.transactions[this.mostRecentTransaction+1];
|
||||||
|
transaction.doTransaction();
|
||||||
|
this.mostRecentTransaction++;
|
||||||
|
this.performingDo = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function gets the most recently executed transaction on the
|
||||||
|
* TPS stack and undoes it, moving the TPS counter accordingly.
|
||||||
|
*/
|
||||||
|
undoTransaction() {
|
||||||
|
if (this.hasTransactionToUndo()) {
|
||||||
|
this.performingUndo = true;
|
||||||
|
let transaction = this.transactions[this.mostRecentTransaction];
|
||||||
|
transaction.undoTransaction();
|
||||||
|
this.mostRecentTransaction--;
|
||||||
|
this.performingUndo = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clearAllTransactions
|
||||||
|
*
|
||||||
|
* Removes all the transactions from the TPS, leaving it with none.
|
||||||
|
*/
|
||||||
|
clearAllTransactions() {
|
||||||
|
// REMOVE ALL THE TRANSACTIONS
|
||||||
|
this.transactions = [];
|
||||||
|
|
||||||
|
// MAKE SURE TO RESET THE LOCATION OF THE
|
||||||
|
// TOP OF THE TPS STACK TOO
|
||||||
|
this.mostRecentTransaction = -1;
|
||||||
|
this.numTransactions = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* toString
|
||||||
|
*
|
||||||
|
* Builds and returns a textual represention of the full TPS and its stack.
|
||||||
|
*/
|
||||||
|
toString() {
|
||||||
|
let text = "--Number of Transactions: " + this.numTransactions + "\n";
|
||||||
|
text += "--Current Index on Stack: " + this.mostRecentTransaction + "\n";
|
||||||
|
text += "--Current Transaction Stack:\n";
|
||||||
|
for (let i = 0; i <= this.mostRecentTransaction; i++) {
|
||||||
|
let jT = this.transactions[i];
|
||||||
|
text += "----" + jT.toString() + "\n";
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
18
hw3/client/src/components/Banner.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import EditToolbar from './EditToolbar.js'
|
||||||
|
/*
|
||||||
|
Our Application's Banner, note we are using function-style
|
||||||
|
React. Our banner just has a left-aligned heading and a
|
||||||
|
right-aligned toolbar for undo/redo and close list buttons.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
function Banner(props) {
|
||||||
|
return (
|
||||||
|
<div id="top5-banner">
|
||||||
|
Top 5 Lister
|
||||||
|
<EditToolbar />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Banner;
|
51
hw3/client/src/components/DeleteModal.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { useContext } from 'react'
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
/*
|
||||||
|
This modal is shown when the user asks to delete a list. Note
|
||||||
|
that before this is shown a list has to be marked for deletion,
|
||||||
|
which means its id has to be known so that we can retrieve its
|
||||||
|
information and display its name in this modal. If the user presses
|
||||||
|
confirm, it will be deleted.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
function DeleteModal() {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
let name = "";
|
||||||
|
if (store.listMarkedForDeletion) {
|
||||||
|
console.log('ehei');
|
||||||
|
name = store.listMarkedForDeletion.name;
|
||||||
|
}
|
||||||
|
function handleDeleteList(event) {
|
||||||
|
store.deleteMarkedList();
|
||||||
|
}
|
||||||
|
function handleCloseModal(event) {
|
||||||
|
store.hideDeleteListModal();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="modal"
|
||||||
|
id="delete-modal"
|
||||||
|
data-animation="slideInOutLeft">
|
||||||
|
<div className="modal-dialog">
|
||||||
|
<header className="dialog-header">
|
||||||
|
Delete the {name} Top 5 List?
|
||||||
|
</header>
|
||||||
|
<div id="confirm-cancel-container">
|
||||||
|
<button
|
||||||
|
id="dialog-yes-button"
|
||||||
|
className="modal-button"
|
||||||
|
onClick={handleDeleteList}
|
||||||
|
>Confirm</button>
|
||||||
|
<button
|
||||||
|
id="dialog-no-button"
|
||||||
|
className="modal-button"
|
||||||
|
onClick={handleCloseModal}
|
||||||
|
>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeleteModal;
|
68
hw3/client/src/components/EditToolbar.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { useContext } from 'react'
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
/*
|
||||||
|
This toolbar is a functional React component that
|
||||||
|
manages the undo/redo/close buttons.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
function EditToolbar() {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
function doNothing () {
|
||||||
|
}
|
||||||
|
function handleUndo() {
|
||||||
|
store.undo();
|
||||||
|
store.updateCurrentList();
|
||||||
|
}
|
||||||
|
function handleRedo() {
|
||||||
|
store.redo();
|
||||||
|
store.updateCurrentList();
|
||||||
|
}
|
||||||
|
function handleClose() {
|
||||||
|
history.push("/");
|
||||||
|
store.closeCurrentList();
|
||||||
|
}
|
||||||
|
let editStatus = false;
|
||||||
|
if (store.isListNameEditActive) {
|
||||||
|
editStatus = true;
|
||||||
|
}
|
||||||
|
let hasUndo = false;
|
||||||
|
if (store.hasUndo) {
|
||||||
|
hasUndo = true;
|
||||||
|
}
|
||||||
|
let hasRedo = false;
|
||||||
|
if (store.hasRedo) {
|
||||||
|
hasRedo = true;
|
||||||
|
}
|
||||||
|
let isOpen = false;
|
||||||
|
if (store.isOpen) {
|
||||||
|
isOpen = true;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div id="edit-toolbar">
|
||||||
|
<div
|
||||||
|
id='undo-button'
|
||||||
|
onClick={editStatus||!hasUndo?doNothing:handleUndo}
|
||||||
|
className={(editStatus||!hasUndo)?"top5-button-disabled":"top5-button"}>
|
||||||
|
↶
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id='redo-button'
|
||||||
|
onClick={editStatus||!hasRedo?doNothing:handleRedo}
|
||||||
|
className={(editStatus||!hasRedo)?"top5-button-disabled":"top5-button"}>
|
||||||
|
↷
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id='close-button'
|
||||||
|
onClick={editStatus||!isOpen?doNothing:handleClose}
|
||||||
|
className={editStatus||!isOpen?"top5-button-disabled":"top5-button"}>
|
||||||
|
ⓧ
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditToolbar;
|
124
hw3/client/src/components/ListCard.js
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import { useContext, useState } from 'react'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
/*
|
||||||
|
This is a card in our list of top 5 lists. It lets select
|
||||||
|
a list for editing and it has controls for changing its
|
||||||
|
name or deleting it.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
function ListCard(props) {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
const [ editActive, setEditActive ] = useState(false);
|
||||||
|
//const [ text, setText ] = useState("");
|
||||||
|
store.history = useHistory();
|
||||||
|
const { idNamePair, selected } = props;
|
||||||
|
|
||||||
|
function handleLoadList(event) {
|
||||||
|
if (!event.target.disabled) {
|
||||||
|
let _id = event.target.id;
|
||||||
|
console.log(_id)
|
||||||
|
if (_id.indexOf('list-card-text-') >= 0) {
|
||||||
|
_id = ("" + _id).substring("list-card-text-".length);
|
||||||
|
}
|
||||||
|
if (_id.indexOf('delete-list-') < 0){
|
||||||
|
store.setCurrentList(_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHANGE THE CURRENT LIST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggleEdit(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
toggleEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEdit() {
|
||||||
|
let newActive = !editActive;
|
||||||
|
if (newActive) {
|
||||||
|
store.setIsListNameEditActive();
|
||||||
|
}
|
||||||
|
setEditActive(newActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyPress(event) {
|
||||||
|
if (event.code === "Enter") {
|
||||||
|
let id = event.target.id.substring("list-".length);
|
||||||
|
store.changeListName(id, event.target.value);
|
||||||
|
toggleEdit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDeleteModal(event) {
|
||||||
|
if (!event.target.disabled) {
|
||||||
|
let _id = event.target.id;
|
||||||
|
if (_id.indexOf('delete-list-') >= 0){
|
||||||
|
_id = ("" + _id).substring("delete-list-".length);
|
||||||
|
store.showDeleteModal(_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function doNothing(){}
|
||||||
|
|
||||||
|
//function handleUpdateText(event) {
|
||||||
|
// setText(event.target.value );
|
||||||
|
//}
|
||||||
|
|
||||||
|
let selectClass = "unselected-list-card";
|
||||||
|
if (selected) {
|
||||||
|
selectClass = "selected-list-card";
|
||||||
|
}
|
||||||
|
let cardStatus = false;
|
||||||
|
if (store.isListNameEditActive) {
|
||||||
|
cardStatus = true;
|
||||||
|
}
|
||||||
|
let cardElement =
|
||||||
|
<div
|
||||||
|
id={idNamePair._id}
|
||||||
|
key={idNamePair._id}
|
||||||
|
onClick={cardStatus?doNothing:handleLoadList}
|
||||||
|
className={'list-card ' + selectClass}>
|
||||||
|
<span
|
||||||
|
id={"list-card-text-" + idNamePair._id}
|
||||||
|
key={"span-" + idNamePair._id}
|
||||||
|
className="list-card-text">
|
||||||
|
{idNamePair.name}
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
disabled={cardStatus}
|
||||||
|
type="button"
|
||||||
|
id={"delete-list-" + idNamePair._id}
|
||||||
|
className="list-card-button"
|
||||||
|
onClick={showDeleteModal}
|
||||||
|
value={"\u2715"}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
disabled={cardStatus}
|
||||||
|
type="button"
|
||||||
|
id={"edit-list-" + idNamePair._id}
|
||||||
|
className="list-card-button"
|
||||||
|
onClick={handleToggleEdit}
|
||||||
|
value={"\u270E"}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
if (editActive) {
|
||||||
|
cardElement =
|
||||||
|
<input
|
||||||
|
id={"list-" + idNamePair._id}
|
||||||
|
className='list-card'
|
||||||
|
type='text'
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
//onChange={handleUpdateText}
|
||||||
|
defaultValue={idNamePair.name}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
cardElement
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListCard;
|
58
hw3/client/src/components/ListSelector.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import React, { useContext, useEffect } from 'react'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import ListCard from './ListCard.js'
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
import DeleteModal from './DeleteModal'
|
||||||
|
/*
|
||||||
|
This React component lists all the top5 lists in the UI.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
const ListSelector = () => {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
store.history = useHistory();
|
||||||
|
|
||||||
|
function handleAddList() {
|
||||||
|
store.addList();
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
store.loadIdNamePairs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
let editStatus = false;
|
||||||
|
if (store.isListNameEditActive) {
|
||||||
|
editStatus = true;
|
||||||
|
}
|
||||||
|
let listCard = "";
|
||||||
|
if (store) {
|
||||||
|
listCard = store.idNamePairs.map((pair) => (
|
||||||
|
<ListCard
|
||||||
|
key={pair._id}
|
||||||
|
idNamePair={pair}
|
||||||
|
selected={false}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div id="top5-list-selector">
|
||||||
|
<div id="list-selector-heading">
|
||||||
|
<input
|
||||||
|
type="button"
|
||||||
|
id="add-list-button"
|
||||||
|
disabled={editStatus}
|
||||||
|
onClick={handleAddList}
|
||||||
|
className={editStatus?"top5-button-disabled":"top5-button"}
|
||||||
|
value="+" />
|
||||||
|
Your Lists
|
||||||
|
</div>
|
||||||
|
<div id="list-selector-list">
|
||||||
|
{
|
||||||
|
listCard
|
||||||
|
}
|
||||||
|
<DeleteModal />
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListSelector;
|
20
hw3/client/src/components/Statusbar.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { useContext } from 'react'
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
/*
|
||||||
|
Our Status bar React component goes at the bottom of our UI.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
function Statusbar() {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
let text ="";
|
||||||
|
if (store.currentList)
|
||||||
|
text = store.currentList.name;
|
||||||
|
return (
|
||||||
|
<div id="top5-statusbar">
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Statusbar;
|
123
hw3/client/src/components/Top5Item.js
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import { React, useContext, useState } from "react";
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
/*
|
||||||
|
This React component represents a single item in our
|
||||||
|
Top 5 List, which can be edited or moved around.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
function Top5Item(props) {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
const [draggedTo, setDraggedTo] = useState(0);
|
||||||
|
const [ editActive, setEditActive ] = useState(false);
|
||||||
|
|
||||||
|
function handleToggleEdit(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
toggleEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEdit() {
|
||||||
|
let newActive = !editActive;
|
||||||
|
if (newActive) {
|
||||||
|
store.setIsListItemEditActive();
|
||||||
|
}
|
||||||
|
setEditActive(newActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyPress(event) {
|
||||||
|
if (event.code === "Enter") {
|
||||||
|
let id = event.target.id.substring(event.target.id.indexOf("-")+1);
|
||||||
|
if (props.text !== event.target.value) {
|
||||||
|
store.addChangeItemTransaction(id, props.text, event.target.value);
|
||||||
|
}
|
||||||
|
store.updateCurrentList();
|
||||||
|
toggleEdit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOnblur(event) {
|
||||||
|
let id = event.target.id.substring(event.target.id.indexOf("-")+1);
|
||||||
|
if (props.text !== event.target.value) {
|
||||||
|
store.addChangeItemTransaction(id, props.text, event.target.value);
|
||||||
|
}
|
||||||
|
store.updateCurrentList();
|
||||||
|
toggleEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragStart(event) {
|
||||||
|
event.dataTransfer.setData("item", event.target.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnter(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
setDraggedTo(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragLeave(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
setDraggedTo(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
let target = event.target;
|
||||||
|
let targetId = target.id;
|
||||||
|
targetId = targetId.substring(target.id.indexOf("-") + 1);
|
||||||
|
let sourceId = event.dataTransfer.getData("item");
|
||||||
|
sourceId = sourceId.substring(sourceId.indexOf("-") + 1);
|
||||||
|
setDraggedTo(false);
|
||||||
|
|
||||||
|
// UPDATE THE LIST
|
||||||
|
if (sourceId !== targetId) {
|
||||||
|
store.addMoveItemTransaction(sourceId, targetId);
|
||||||
|
}
|
||||||
|
store.updateCurrentList();
|
||||||
|
}
|
||||||
|
|
||||||
|
let { index } = props;
|
||||||
|
let itemClass = "top5-item";
|
||||||
|
if (draggedTo) {
|
||||||
|
itemClass = "top5-item-dragged-to";
|
||||||
|
}
|
||||||
|
let cardStatus = false;
|
||||||
|
if (store.isListNameEditActive) {
|
||||||
|
cardStatus = true;
|
||||||
|
}
|
||||||
|
if (editActive) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
id={'item-' + (index + 1)}
|
||||||
|
className='top5-item'
|
||||||
|
type='text'
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
defaultValue={props.text}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={'item-' + (index + 1)}
|
||||||
|
className={itemClass}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
draggable="true"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="button"
|
||||||
|
id={"edit-item-" + index + 1}
|
||||||
|
disabled={cardStatus}
|
||||||
|
className="list-card-button"
|
||||||
|
onClick={handleToggleEdit}
|
||||||
|
value={"\u270E"}
|
||||||
|
/>
|
||||||
|
{props.text}
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Top5Item;
|
47
hw3/client/src/components/Workspace.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { useContext } from 'react'
|
||||||
|
import { useHistory } from 'react-router-dom'
|
||||||
|
import Top5Item from './Top5Item.js'
|
||||||
|
import { GlobalStoreContext } from '../store'
|
||||||
|
/*
|
||||||
|
This React component lets us edit a loaded list, which only
|
||||||
|
happens when we are on the proper route.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
function Workspace() {
|
||||||
|
const { store } = useContext(GlobalStoreContext);
|
||||||
|
store.history = useHistory();
|
||||||
|
|
||||||
|
let editItems = "";
|
||||||
|
if (store.currentList) {
|
||||||
|
editItems =
|
||||||
|
<div id="edit-items">
|
||||||
|
{
|
||||||
|
store.currentList.items.map((item, index) => (
|
||||||
|
<Top5Item
|
||||||
|
id={'top5-item-' + (index+1)}
|
||||||
|
key={'top5-item-' + (index+1)}
|
||||||
|
text={item}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div id="top5-workspace">
|
||||||
|
<div id="workspace-edit">
|
||||||
|
<div id="edit-numbering">
|
||||||
|
<div className="item-number">1.</div>
|
||||||
|
<div className="item-number">2.</div>
|
||||||
|
<div className="item-number">3.</div>
|
||||||
|
<div className="item-number">4.</div>
|
||||||
|
<div className="item-number">5.</div>
|
||||||
|
</div>
|
||||||
|
{editItems}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Workspace;
|
15
hw3/client/src/components/index.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import Banner from './Banner'
|
||||||
|
import DeleteModal from './DeleteModal'
|
||||||
|
import EditToolbar from './EditToolbar'
|
||||||
|
import ListCard from './ListCard'
|
||||||
|
import ListSelector from './ListSelector'
|
||||||
|
import Statusbar from './Statusbar'
|
||||||
|
import Top5Item from './Top5Item'
|
||||||
|
import Workspace from './Workspace'
|
||||||
|
/*
|
||||||
|
This serves as a module so that we can import
|
||||||
|
all the other components as we wish.
|
||||||
|
|
||||||
|
@author McKilla Gorilla
|
||||||
|
*/
|
||||||
|
export { Banner, DeleteModal, EditToolbar, ListCard, ListSelector, Statusbar, Top5Item, Workspace }
|