init repo

This commit is contained in:
Renge 2022-05-23 06:22:34 -04:00
commit 6ee80f6baa
162 changed files with 171289 additions and 0 deletions

12
README.md Normal file
View 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
View File

@ -0,0 +1,3 @@
node_modules
client/node_modules
server/node_modules

38974
final/client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
final/client/package.json Normal file
View 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"
}

View 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"
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View 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"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

453
final/client/src/App.css Normal file
View 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
View 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
View 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;
}

View 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

View 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 };

View 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;
}
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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;

View 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;

View 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 />
}

View 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;

View 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>
);
}

View 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>
);
}

View 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>
)
}

View 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;

View 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;

View 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;

View 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
View 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();

View 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;

View 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 };

View 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);
}
}

View 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
View 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;

View 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;

View 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,
}

View 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
View 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
View 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}`))

View 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)

View 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)

View 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

File diff suppressed because it is too large Load Diff

16
final/server/package.json Normal file
View 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"
}
}

View 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

View 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
View 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
View 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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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

File diff suppressed because it is too large Load Diff

43
hw2/package.json Normal file
View 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"
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

43
hw2/public/index.html Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
hw2/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
hw2/public/manifest.json Normal file
View 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
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

410
hw2/src/App.css Normal file
View 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
View 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;

View 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);
}
}

View 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
View 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;
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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"}>
&#x21B6;
</div>
<div
id='redo-button'
onClick={hasRedo?redoCallback:this.doNothing}
className={(hasRedo&&!isEditing)?"top5-button":"top5-button-disabled"}>
&#x21B7;
</div>
<div
id='close-button'
onClick={currentList?closeCallback:this.doNothing}
className={(currentList&&!isEditing)?"top5-button":"top5-button-disabled"}>
&#x24E7;
</div>
</div>
)
}
}

View 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>
);
}
}
}

View 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>
)
}
}
}

View 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>
);
}
}

View 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>
)
}
}

View 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
View 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
View 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();

View 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
View File

@ -0,0 +1,4 @@
./client/node_modules
./server/node_modules
node_modules

38134
hw3/client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
hw3/client/package.json Normal file
View 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"
}

View 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"
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View 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"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

417
hw3/client/src/App.css Normal file
View 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
View 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

View 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

View 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;
}
}

View 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;

View 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;

View 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"}>
&#x21B6;
</div>
<div
id='redo-button'
onClick={editStatus||!hasRedo?doNothing:handleRedo}
className={(editStatus||!hasRedo)?"top5-button-disabled":"top5-button"}>
&#x21B7;
</div>
<div
id='close-button'
onClick={editStatus||!isOpen?doNothing:handleClose}
className={editStatus||!isOpen?"top5-button-disabled":"top5-button"}>
&#x24E7;
</div>
</div>
)
}
export default EditToolbar;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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 }

Some files were not shown because too many files have changed in this diff Show More