diff --git a/zubhub_backend/deploy_backend.sh b/zubhub_backend/deploy_backend.sh index 28a15696e..9fa91648a 100644 --- a/zubhub_backend/deploy_backend.sh +++ b/zubhub_backend/deploy_backend.sh @@ -6,6 +6,6 @@ cp -r zubhub/zubhub_backend/ zubhub_backend/ rm -rf zubhub/ zubhub_backend/.env.example cd zubhub_backend docker-compose down -docker-compose up -d --build +docker-compose -f ./docker-compose.prod.yml up -d --build echo "Updated backend" # EOT \ No newline at end of file diff --git a/zubhub_backend/docker-compose.prod.yml b/zubhub_backend/docker-compose.prod.yml new file mode 100644 index 000000000..c3ffc502f --- /dev/null +++ b/zubhub_backend/docker-compose.prod.yml @@ -0,0 +1,124 @@ +version: "3.3" + +services: + web: + env_file: .env + build: + context: . + dockerfile: ./Dockerfile + command: /start + restart: on-failure + environment: + - ENVIRONMENT=${ENVIRONMENT} + - SECRET_KEY:${SECRET_KEY} + - DEBUG:${DEBUG} + - POSTGRES_NAME=${POSTGRES_NAME} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_HOST=${POSTGRES_HOST} + - SENDGRID_API_KEY=${SENDGRID_API_KEY} + volumes: + - .:/zubhub_backend + ports: + - 8000 + depends_on: + - db + - rabbitmq + + db: + env_file: .env + image: postgres:11 + restart: on-failure + environment: + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data/ + ports: + - 5432 + + reverse-proxy: + image: valian/docker-nginx-auto-ssl:1.0.0 + container_name: reverse-proxy + restart: on-failure + ports: + - 80:80 + - 443:443 + volumes: + - ./.ssl-data:/etc/resty-auto-ssl + environment: + ALLOWED_DOMAINS: "(api|www.api|rabbitmq|flower|prometheus).zubhub.unstructured.studio" + SITES: + "api.zubhub.unstructured.studio=web:8000;www.api.zubhub.unstructured.studio=web:8000;\ + rabbitmq.zubhub.unstructured.studio=rabbitmq:15672;flower.zubhub.unstructured.studio=flower:5555;\ + prometheus.zubhub.unstructured.studio=prometheus:9090" + FORCE_HTTPS: "true" + depends_on: + - web + - rabbitmq + - flower + - prometheus + + rabbitmq: + env_file: .env + image: rabbitmq:3-management + restart: on-failure + ports: + - 5672 + - 15672 + + celery_worker: + env_file: .env + build: + context: . + dockerfile: ./Dockerfile + image: zubhub_celery_worker + command: /start-celeryworker + restart: on-failure + volumes: + - .:/zubhub_backend + depends_on: + - rabbitmq + - db + + flower: + env_file: .env + build: + context: . + dockerfile: ./Dockerfile + image: zubhub_celery_flower + ports: + - 5555 + command: /start-flower + restart: on-failure + volumes: + - .:/zubhub_backend + depends_on: + - celery_worker + - rabbitmq + - db + + prometheus: + image: prom/prometheus + ports: + - 9090 + command: + - --config.file=/etc/prometheus/prometheus.yml + restart: on-failure + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + depends_on: + - cadvisor + + cadvisor: + image: google/cadvisor + container_name: cadvisor + restart: on-failure + volumes: + - /:/rootfs:ro + - /var/run:/var/run:rw + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + +volumes: + postgres_data: + .ssl-data: diff --git a/zubhub_backend/docker-compose.yml b/zubhub_backend/docker-compose.yml index b2b9e5daf..1927c7fcc 100644 --- a/zubhub_backend/docker-compose.yml +++ b/zubhub_backend/docker-compose.yml @@ -36,28 +36,6 @@ services: ports: - 5432:5432 - reverse-proxy: - image: valian/docker-nginx-auto-ssl:1.0.0 - container_name: reverse-proxy - restart: on-failure - ports: - - 80:80 - - 443:443 - volumes: - - ./.ssl-data:/etc/resty-auto-ssl - environment: - ALLOWED_DOMAINS: "(api|www.api|rabbitmq|flower|prometheus).zubhub.unstructured.studio" - SITES: - "api.zubhub.unstructured.studio=web:8000;www.api.zubhub.unstructured.studio=web:8000;\ - rabbitmq.zubhub.unstructured.studio=rabbitmq:15672;flower.zubhub.unstructured.studio=flower:5555;\ - prometheus.zubhub.unstructured.studio=prometheus:9090" - FORCE_HTTPS: "true" - depends_on: - - web - - rabbitmq - - flower - - prometheus - rabbitmq: env_file: .env image: rabbitmq:3-management @@ -121,4 +99,3 @@ services: volumes: postgres_data: - .ssl-data: diff --git a/zubhub_frontend/zubhub/.prettierrc.yaml b/zubhub_frontend/zubhub/.prettierrc.yaml new file mode 100644 index 000000000..50da50785 --- /dev/null +++ b/zubhub_frontend/zubhub/.prettierrc.yaml @@ -0,0 +1,10 @@ +printWidth: 80 +tabWidth: 2 +useTabs: false +semi: true +singleQuote: true +trailingComma: all +bracketSpacing: true +jsxBracketSameLine: false +arrowParens: avoid +requirePragma: false diff --git a/zubhub_frontend/zubhub/Dockerfile b/zubhub_frontend/zubhub/Dockerfile index 9cd823857..51aab0df5 100644 --- a/zubhub_frontend/zubhub/Dockerfile +++ b/zubhub_frontend/zubhub/Dockerfile @@ -13,6 +13,6 @@ RUN npm run build FROM nginx:stable-alpine COPY --from=build /zubhub_frontend/build /usr/share/nginx/html # new -COPY nginx/default.conf /etc/nginx/conf.d/default.conf -EXPOSE 80 +COPY nginx/dev/default.conf /etc/nginx/conf.d/default.conf +EXPOSE 3000 CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/zubhub_frontend/zubhub/Dockerfile.prod b/zubhub_frontend/zubhub/Dockerfile.prod new file mode 100644 index 000000000..b68286ead --- /dev/null +++ b/zubhub_frontend/zubhub/Dockerfile.prod @@ -0,0 +1,18 @@ +# build environment +FROM node:13.12.0-alpine as build +WORKDIR /zubhub_frontend +ENV PATH /zubhub_frontend/node_modules/.bin:$PATH +COPY package.json ./ +COPY package-lock.json ./ +RUN npm ci --silent +RUN npm install react-scripts@3.4.1 -g --silent +COPY . ./ +RUN npm run build + +# production environment +FROM nginx:stable-alpine +COPY --from=build /zubhub_frontend/build /usr/share/nginx/html +# new +COPY nginx/prod/default.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/zubhub_frontend/zubhub/deploy_frontend.sh b/zubhub_frontend/zubhub/deploy_frontend.sh index 27f1c686d..89dc1e353 100644 --- a/zubhub_frontend/zubhub/deploy_frontend.sh +++ b/zubhub_frontend/zubhub/deploy_frontend.sh @@ -1,12 +1,23 @@ #! /bin/bash +echo "copying .env file and ssl folder" mv zubhub_frontend/zubhub/.env zubhub/zubhub_frontend/zubhub/.env mv zubhub_frontend/zubhub/.ssl-data zubhub/zubhub_frontend/zubhub/ +echo "done copying .env file and ssl folder" + +echo "removing unneccessary files and folders" rm -rf zubhub_frontend cp -r zubhub/zubhub_frontend/ zubhub_frontend/ rm -rf zubhub/ zubhub_frontend/zubhub/.env.example +rm -rf zubhub/ zubhub_frontend/zubhub/Dockerfile +rm -rf zubhub/ zubhub_frontend/zubhub/docker-compose.yml +rm -rf zubhub/ zubhub_frontend/zubhub/nginx/dev +echo "done removing unneccessary files and folders" + cd zubhub_frontend/zubhub/ + +echo "stopping and rebuilding the containers" docker-compose down -docker-compose up -d --build +docker-compose -f ./docker-compose.prod.yml up -d --build echo "Updated frontend" # EOT \ No newline at end of file diff --git a/zubhub_frontend/zubhub/docker-compose.prod.yml b/zubhub_frontend/zubhub/docker-compose.prod.yml new file mode 100644 index 000000000..e0c527af6 --- /dev/null +++ b/zubhub_frontend/zubhub/docker-compose.prod.yml @@ -0,0 +1,32 @@ +version: "3.3" + +services: + zubhub_frontend: + container_name: zubhub_frontend + build: + context: . + dockerfile: Dockerfile.prod + volumes: + - ./nginx/prod/default.conf:/etc/nginx/conf.d/default.conf + restart: on-failure + ports: + - "80" + + reverse-proxy: + image: valian/docker-nginx-auto-ssl:1.0.0 + container_name: reverse-proxy + restart: on-failure + ports: + - 80:80 + - 443:443 + volumes: + - ./.ssl-data:/etc/resty-auto-ssl + environment: + ALLOWED_DOMAINS: "(zubhub|www.zubhub).unstructured.studio" + SITES: "zubhub.unstructured.studio=zubhub_frontend;www.zubhub.unstructured.studio=zubhub_frontend" + FORCE_HTTPS: "true" + depends_on: + - zubhub_frontend + +volumes: + .ssl-data: diff --git a/zubhub_frontend/zubhub/docker-compose.yml b/zubhub_frontend/zubhub/docker-compose.yml index 5ece3d7fc..7db4ee546 100644 --- a/zubhub_frontend/zubhub/docker-compose.yml +++ b/zubhub_frontend/zubhub/docker-compose.yml @@ -7,26 +7,7 @@ services: context: . dockerfile: Dockerfile volumes: - - ./nginx/default.conf:/etc/nginx/conf.d/default.conf + - ./nginx/dev/default.conf:/etc/nginx/conf.d/default.conf restart: on-failure ports: - - "8080:80" - - reverse-proxy: - image: valian/docker-nginx-auto-ssl:1.0.0 - container_name: reverse-proxy - restart: on-failure - ports: - - 80:80 - - 443:443 - volumes: - - ./.ssl-data:/etc/resty-auto-ssl - environment: - ALLOWED_DOMAINS: "(zubhub|www.zubhub).unstructured.studio" - SITES: "zubhub.unstructured.studio=zubhub_frontend;www.zubhub.unstructured.studio=zubhub_frontend" - FORCE_HTTPS: "true" - depends_on: - - zubhub_frontend - -volumes: - .ssl-data: + - "3000:3000" diff --git a/zubhub_frontend/zubhub/nginx/dev/default.conf b/zubhub_frontend/zubhub/nginx/dev/default.conf new file mode 100644 index 000000000..79faab382 --- /dev/null +++ b/zubhub_frontend/zubhub/nginx/dev/default.conf @@ -0,0 +1,33 @@ +map $sent_http_content_type $expires { + default off; + text/html epoch; + text/css max; + application/javascript max; + ~image/ max; +} +server { + listen 3000; + server_name localhost; + + gzip on; + gzip_min_length 1000; + gzip_buffers 4 32k; + gzip_proxied any; + gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css; + gzip_vary on; + gzip_disable “MSIE [1–6]\.(?!.*SV1)”; + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + } +} \ No newline at end of file diff --git a/zubhub_frontend/zubhub/nginx/default.conf b/zubhub_frontend/zubhub/nginx/prod/default.conf similarity index 100% rename from zubhub_frontend/zubhub/nginx/default.conf rename to zubhub_frontend/zubhub/nginx/prod/default.conf diff --git a/zubhub_frontend/zubhub/package-lock.json b/zubhub_frontend/zubhub/package-lock.json index e45e64352..b1c50af10 100644 --- a/zubhub_frontend/zubhub/package-lock.json +++ b/zubhub_frontend/zubhub/package-lock.json @@ -11661,6 +11661,12 @@ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" }, + "prettier": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "dev": true + }, "pretty-bytes": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.4.1.tgz", diff --git a/zubhub_frontend/zubhub/package.json b/zubhub_frontend/zubhub/package.json index ff3bb616c..c24a548ff 100644 --- a/zubhub_frontend/zubhub/package.json +++ b/zubhub_frontend/zubhub/package.json @@ -11,6 +11,7 @@ "@testing-library/react": "^11.1.2", "@testing-library/user-event": "^12.2.2", "aws-sdk": "^2.813.0", + "classnames": "^2.2.6", "date-fns": "^2.16.1", "formik": "^2.2.5", "nanoid": "^3.1.20", @@ -32,7 +33,8 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "pretty": "prettier --config ./.prettierrc.yaml --write \"src/**/*.{js,jsx}\"" }, "eslintConfig": { "extends": [ @@ -51,5 +53,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "prettier": "2.2.1" } } diff --git a/zubhub_frontend/zubhub/src/App.css b/zubhub_frontend/zubhub/src/App.css deleted file mode 100644 index 74b5e0534..000000000 --- a/zubhub_frontend/zubhub/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/zubhub_frontend/zubhub/src/App.js b/zubhub_frontend/zubhub/src/App.js index de4eedef5..e443f415b 100644 --- a/zubhub_frontend/zubhub/src/App.js +++ b/zubhub_frontend/zubhub/src/App.js @@ -1,126 +1,146 @@ -import React,{Component} from 'react'; - -import {BrowserRouter as Router,Route,Switch} from 'react-router-dom'; -import PageWrapper from './components/PageWrapper'; - -import {withAPI} from './components/api'; - -import Signup from './components/pages/user_auth/Signup'; -import Login from './components/pages/user_auth/Login'; -import PasswordReset from './components/pages/user_auth/PasswordReset'; -import PasswordResetConfirm from './components/pages/user_auth/PasswordResetConfirm'; -import EmailConfirm from './components/pages/user_auth/EmailConfirm'; -import Profile from './components/pages/profile/Profile'; -import UserProjects from './components/pages/profile/profile_components/UserProjects'; -import UserFollowers from './components/pages/profile/profile_components/UserFollowers'; -import Projects from './components/pages/projects/Projects'; -import SavedProjects from './components/pages/projects/projects_components/SavedProjects'; -import CreateProject from './components/pages/projects/projects_components/CreateProject'; -import ProjectDetails from './components/pages/projects/projects_components/ProjectDetails'; - -class App extends Component { - -render(){ -return( +import React from 'react'; + +import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; + +import { withAPI } from './api'; + +import PageWrapper from './views/PageWrapper'; +import Signup from './views/signup/Signup'; +import Login from './views/login/Login'; +import PasswordReset from './views/password_reset/PasswordReset'; +import PasswordResetConfirm from './views/password_reset_confirm/PasswordResetConfirm'; +import EmailConfirm from './views/email_confirm/EmailConfirm'; +import Profile from './views/profile/Profile'; +import UserProjects from './views/user_projects/UserProjects'; +import UserFollowers from './views/user_followers/UserFollowers'; +import Projects from './views/projects/Projects'; +import SavedProjects from './views/saved_projects/SavedProjects'; +import CreateProject from './views/create_project/CreateProject'; +import ProjectDetails from './views/project_details/ProjectDetails'; + +function App(props) { + return ( - - ( - - - - )}/> - - ( - - - - )}/> - - ( - - - - )}/> - - ( - - - - )}/> - - ( - - - - )}/> - - ( - - - - )}/> - - ( - - - - )}/> - - ( - - - - )}/> - - ( - - - - )}/> - - ( - - - - )}/> - - ( - - - - )}/> - - ( - - - - )}/> - ( - - - - )}/> + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + ( + + + + )} + /> - ); } -} -export default withAPI(App) +export default App; diff --git a/zubhub_frontend/zubhub/src/api/api.js b/zubhub_frontend/zubhub/src/api/api.js new file mode 100644 index 000000000..2e46bd47b --- /dev/null +++ b/zubhub_frontend/zubhub/src/api/api.js @@ -0,0 +1,272 @@ +class API { + constructor() { + this.domain = + process.env.REACT_APP_NODE_ENV === 'production' + ? process.env.REACT_APP_BACKEND_PRODUCTION_URL + '/api/' + : process.env.REACT_APP_BACKEND_DEVELOPMENT_URL + '/api/'; + } + + request = ({ url = '/', method = 'GET', token, body }) => { + if (method === 'GET' && !token) { + return fetch(this.domain + url, { + method, + xsrfCookieName: 'csrftoken', + xsrfHeaderName: 'X-CSRFToken', + withCredentials: 'true', + headers: new Headers({ + 'Content-Type': 'application/json', + }), + }); + } else if (token && body) { + return fetch(this.domain + url, { + method, + xsrfCookieName: 'csrftoken', + xsrfHeaderName: 'X-CSRFToken', + withCredentials: 'true', + headers: new Headers({ + Authorization: `Token ${token}`, + 'Content-Type': 'application/json', + }), + body, + }); + } else if (token) { + return fetch(this.domain + url, { + method, + xsrfCookieName: 'csrftoken', + xsrfHeaderName: 'X-CSRFToken', + withCredentials: 'true', + headers: new Headers({ + Authorization: `Token ${token}`, + 'Content-Type': 'application/json', + }), + }); + } else if (body) { + return fetch(this.domain + url, { + method, + xsrfCookieName: 'csrftoken', + xsrfHeaderName: 'X-CSRFToken', + withCredentials: 'true', + headers: new Headers({ + 'Content-Type': 'application/json', + }), + body, + }); + } + }; + + /**********************login with email and password******************************/ + login = ({ username, password }) => { + const url = 'rest-auth/login/'; + const method = 'POST'; + const body = JSON.stringify({ username, password }); + + return this.request({ url, method, body }).then(res => res.json()); + }; + /****************************************************/ + + /**********************logout******************************/ + logout = token => { + const url = 'rest-auth/logout/'; + const method = 'POST'; + return this.request({ url, method, token }).then(res => res.json()); + }; + /****************************************************/ + + /**********************signup******************************/ + signup = ({ + username, + email, + dateOfBirth, + user_location, + password1, + password2, + }) => { + const url = 'rest-auth/registration/'; + const method = 'POST'; + const body = JSON.stringify({ + username, + email, + dateOfBirth, + location: user_location, + password1, + password2, + }); + + return this.request({ url, method, body }).then(res => res.json()); + }; + /****************************************************/ + + /*****************send email confirmation ******************/ + send_email_confirmation = key => { + const url = 'rest-auth/registration/verify-email/'; + const method = 'POST'; + const body = JSON.stringify({ key }); + + return this.request({ url, method, body }).then(res => res.json()); + }; + /*******************************************************************/ + + /********************send password reset link********************************/ + send_password_reset_link = email => { + const url = 'rest-auth/password/reset/'; + const method = 'POST'; + const body = JSON.stringify({ email }); + + return this.request({ url, method, body }).then(res => res.json()); + }; + /********************************************************************/ + + /********************password reset confirmation********************************/ + password_reset_confirm = ({ new_password1, new_password2, uid, token }) => { + const url = 'rest-auth/password/reset/confirm/'; + const method = 'POST'; + const body = JSON.stringify({ new_password1, new_password2, uid, token }); + + return this.request({ url, method, body }).then(res => res.json()); + }; + /********************************************************************/ + + /************************** get authenticated user's details **************************/ + get_auth_user = token => { + const url = 'creators/authUser/'; + return this.request({ url, token }).then(res => res.json()); + }; + + /********************************************************************/ + + /************************** get user profile **************************/ + get_user_profile = ({ username, token }) => { + const url = `creators/${username}/`; + if (token) { + return this.request({ url, token }).then(res => res.json()); + } else { + return this.request({ url }).then(res => res.json()); + } + }; + + /*************************** get user projects *********************************/ + get_user_projects = ({ username, page, limit }) => { + let url; + if (limit && page) { + url = `creators/${username}/projects/?limit=${limit}&&${page}`; + } else if (limit) { + url = `creators/${username}/projects/?limit=${limit}`; + } else if (page) { + url = `creators/${username}/projects/?${page}`; + } else { + url = `creators/${username}/projects/`; + } + + return this.request({ url }).then(res => res.json()); + }; + /*********************************************************************/ + + /********************** get followers *******************************/ + get_followers = ({ page, username }) => { + const url = page + ? `creators/${username}/followers/?${page}` + : `creators/${username}/followers/`; + + return this.request({ url }).then(res => res.json()); + }; + /*****************************************************************/ + + /********************** get saved *******************************/ + get_saved = ({ page, token }) => { + const url = page ? `projects/saved/?${page}` : `projects/saved/`; + + return this.request({ url, token }).then(res => res.json()); + }; + /*****************************************************************/ + + /************************** edit user profile **************************/ + edit_user_profile = profile => { + const { username } = profile; + + const url = 'creators/edit_creator/'; + const method = 'PUT'; + const token = profile.token; + const body = JSON.stringify({ username }); + return this.request({ url, method, token, body }).then(res => res.json()); + }; + /********************************************************************/ + + /************************** follow creator **************************/ + toggle_follow = ({ id, token }) => { + const url = `creators/${id}/toggle_follow/`; + + return this.request({ url, token }).then(res => res.json()); + }; + /******************************************************************/ + + /************************** get all locations **************************/ + get_locations = () => { + const url = 'creators/locations/'; + return this.request({ url }).then(res => res.json()); + }; + /********************************************************************/ + + /************************** create project **************************/ + create_project = ({ + token, + title, + description, + video, + images, + materials_used, + }) => { + const url = 'projects/create/'; + const method = 'POST'; + const body = JSON.stringify({ + title, + description, + images, + video, + materials_used, + }); + return this.request({ url, method, token, body }).then(res => res.json()); + }; + + /************************** get projects **************************/ + get_projects = page => { + const url = page ? `projects/?${page}` : `projects/`; + return this.request({ url }).then(res => res.json()); + }; + + /************************** get project **************************/ + get_project = ({ id, token }) => { + const url = `projects/${id}`; + if (token) { + return this.request({ url, token }).then(res => res.json()); + } else { + return this.request({ url }).then(res => res.json()); + } + }; + + /************************** like project **************************/ + toggle_like = ({ id, token }) => { + const url = `projects/${id}/toggle_like/`; + + return this.request({ url, token }).then(res => res.json()); + }; + /******************************************************************/ + + /************************** save project for future viewing **************************/ + toggle_save = ({ id, token }) => { + const url = `projects/${id}/toggle_save/`; + + return this.request({ url, token }).then(res => res.json()); + }; + /******************************************************************/ + + /************************** add comment **************************/ + add_comment = ({ id, text, token }) => { + const url = `projects/${id}/add_comment/`; + const method = 'POST'; + const body = JSON.stringify({ text }); + + return this.request({ url, method, body, token }).then(res => res.json()); + }; +} + +export default API; diff --git a/zubhub_frontend/zubhub/src/components/api/context.js b/zubhub_frontend/zubhub/src/api/context.js similarity index 100% rename from zubhub_frontend/zubhub/src/components/api/context.js rename to zubhub_frontend/zubhub/src/api/context.js diff --git a/zubhub_frontend/zubhub/src/components/api/index.js b/zubhub_frontend/zubhub/src/api/index.js similarity index 61% rename from zubhub_frontend/zubhub/src/components/api/index.js rename to zubhub_frontend/zubhub/src/api/index.js index 528093218..afdc0fb51 100644 --- a/zubhub_frontend/zubhub/src/components/api/index.js +++ b/zubhub_frontend/zubhub/src/api/index.js @@ -1,4 +1,4 @@ -import APIContext, {withAPI} from './context'; +import APIContext, { withAPI } from './context'; import API from './api'; export default API; export { APIContext, withAPI }; diff --git a/zubhub_frontend/zubhub/src/index.css b/zubhub_frontend/zubhub/src/assets/css/index.css similarity index 100% rename from zubhub_frontend/zubhub/src/index.css rename to zubhub_frontend/zubhub/src/assets/css/index.css diff --git a/zubhub_frontend/zubhub/src/assets/js/DO.js b/zubhub_frontend/zubhub/src/assets/js/DO.js index ba1d1cbbe..8544e9295 100644 --- a/zubhub_frontend/zubhub/src/assets/js/DO.js +++ b/zubhub_frontend/zubhub/src/assets/js/DO.js @@ -1,16 +1,15 @@ import AWS from 'aws-sdk'; export const doConfig = { - digitalOceanSpaces: 'https://zubhub.sfo2.digitaloceanspaces.com/', - bucketName: 'zubhub', - project_images: 'project_images', - }; - + digitalOceanSpaces: 'https://zubhub.sfo2.digitaloceanspaces.com/', + bucketName: 'zubhub', + project_images: 'project_images', +}; const spacesEndpoint = new AWS.Endpoint('sfo2.digitaloceanspaces.com'); const s3 = new AWS.S3({ - endpoint: spacesEndpoint, - accessKeyId: process.env.REACT_APP_DOSPACE_ACCESS_KEY_ID, - secretAccessKey: process.env.REACT_APP_DOSPACE_ACCESS_SECRET_KEY - }); -export default s3; \ No newline at end of file + endpoint: spacesEndpoint, + accessKeyId: process.env.REACT_APP_DOSPACE_ACCESS_KEY_ID, + secretAccessKey: process.env.REACT_APP_DOSPACE_ACCESS_SECRET_KEY, +}); +export default s3; diff --git a/zubhub_frontend/zubhub/src/assets/js/customHooks.js b/zubhub_frontend/zubhub/src/assets/js/customHooks.js new file mode 100644 index 000000000..4389ea622 --- /dev/null +++ b/zubhub_frontend/zubhub/src/assets/js/customHooks.js @@ -0,0 +1,15 @@ +import { useRef, useEffect } from 'react'; + +export function useStateUpdateCallback(effect, deps) { + const isFirstRender = useRef(true); + + useEffect(() => { + if (!isFirstRender.current) { + effect(); + } + }, deps); + + useEffect(() => { + isFirstRender.current = false; + }, []); +} diff --git a/zubhub_frontend/zubhub/src/assets/js/dFormatter.js b/zubhub_frontend/zubhub/src/assets/js/dFormatter.js index 01dba1a3a..92ad38fec 100644 --- a/zubhub_frontend/zubhub/src/assets/js/dFormatter.js +++ b/zubhub_frontend/zubhub/src/assets/js/dFormatter.js @@ -1,34 +1,36 @@ -export default function dFormatter(str) { - let date = new Date(str); +const dFormatter = str => { + const date = new Date(str); - var seconds = Math.floor((new Date() - date) / 1000); - - var interval = seconds / 31536000; - - if (interval > 1) { - let result = Math.round(interval); - return result + (result > 1 ? " years" : " year") + " ago"; - } - interval = seconds / 2592000; - if (interval > 1) { - let result = Math.round(interval); - return result + (result > 1 ? " months" : " month") + " ago"; - } - interval = seconds / 86400; - if (interval > 1) { - let result = Math.round(interval); - return result + (result > 1 ? " days" : " day") + " ago"; - } - interval = seconds / 3600; - if (interval > 1) { - let result = Math.round(interval); - return result + (result > 1 ? " hours" : " hour") + " ago"; - } - interval = seconds / 60; - if (interval > 1) { - let result = Math.round(interval); - return result + (result > 1 ? " minutes" : " minute") + " ago"; - } - let result = Math.round(interval); - return result + (result > 1 ? " seconds" : " second") + " ago"; - } \ No newline at end of file + const seconds = Math.floor((new Date() - date) / 1000); + + let interval = seconds / 31536000; + + if (interval > 1) { + const result = Math.round(interval); + return result + (result > 1 ? ' years' : ' year') + ' ago'; + } + interval = seconds / 2592000; + if (interval > 1) { + const result = Math.round(interval); + return result + (result > 1 ? ' months' : ' month') + ' ago'; + } + interval = seconds / 86400; + if (interval > 1) { + const result = Math.round(interval); + return result + (result > 1 ? ' days' : ' day') + ' ago'; + } + interval = seconds / 3600; + if (interval > 1) { + const result = Math.round(interval); + return result + (result > 1 ? ' hours' : ' hour') + ' ago'; + } + interval = seconds / 60; + if (interval > 1) { + const result = Math.round(interval); + return result + (result > 1 ? ' minutes' : ' minute') + ' ago'; + } + const result = Math.round(interval); + return result + (result > 1 ? ' seconds' : ' second') + ' ago'; +}; + +export default dFormatter; diff --git a/zubhub_frontend/zubhub/src/assets/js/icons/ClapIcon.js b/zubhub_frontend/zubhub/src/assets/js/icons/ClapIcon.js index 10722b929..7602b970b 100644 --- a/zubhub_frontend/zubhub/src/assets/js/icons/ClapIcon.js +++ b/zubhub_frontend/zubhub/src/assets/js/icons/ClapIcon.js @@ -1,24 +1,22 @@ import React from 'react'; +export default function ClapIcon() { + return ( + + + + + + + ); +} -export default function ClapIcon(){ - return ( - - - - - - - ) - } - - export function ClapBorderIcon(){ - return ( - - - - - - - ) - } \ No newline at end of file +export function ClapBorderIcon() { + return ( + + + + + + ); +} diff --git a/zubhub_frontend/zubhub/src/assets/js/icons/CommentIcon.js b/zubhub_frontend/zubhub/src/assets/js/icons/CommentIcon.js index 16324cd1d..800121c68 100644 --- a/zubhub_frontend/zubhub/src/assets/js/icons/CommentIcon.js +++ b/zubhub_frontend/zubhub/src/assets/js/icons/CommentIcon.js @@ -1,10 +1,16 @@ import React from 'react'; -export default function CommentIcon(props){ - return ( -
+ ); +} + +function PrevArrow(props) { + const { className, style, onClick } = props; + return ( +
+ ); +} + +export default styles; diff --git a/zubhub_frontend/zubhub/src/assets/js/styles/views/projects/projectsStyles.js b/zubhub_frontend/zubhub/src/assets/js/styles/views/projects/projectsStyles.js new file mode 100644 index 000000000..19b85f078 --- /dev/null +++ b/zubhub_frontend/zubhub/src/assets/js/styles/views/projects/projectsStyles.js @@ -0,0 +1,51 @@ +const styles = theme => ({ + root: { + paddingBottom: '2em', + flex: '1 0 auto', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + background: 'rgba(255,204,0,1)', + background: + '-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + background: + '-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))', + background: + '-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + background: + '-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + background: + '-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + background: + 'linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + filter: + "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", + '& .MuiGrid-root.MuiGrid-container': { + width: '100%', + }, + }, + mainContainerStyle: { + maxWidth: '2000px', + width: '100%', + }, + projectGridStyle: { + marginBottom: '2em', + }, + buttonGroupStyle: { + paddingLeft: '2em', + paddingRight: '2em', + display: 'block', + marginTop: '2em', + maxWidth: '2000px', + width: '100%', + }, + floatRight: { + float: 'right', + }, + floatLeft: { + float: 'left', + }, +}); + +export default styles; diff --git a/zubhub_frontend/zubhub/src/assets/js/styles/views/saved_projects/savedProjectsStyles.js b/zubhub_frontend/zubhub/src/assets/js/styles/views/saved_projects/savedProjectsStyles.js new file mode 100644 index 000000000..17fbd9715 --- /dev/null +++ b/zubhub_frontend/zubhub/src/assets/js/styles/views/saved_projects/savedProjectsStyles.js @@ -0,0 +1,44 @@ +const styles = theme => ({ + root: { + paddingBottom: '2em', + flex: '1 0 auto', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + background: 'rgba(255,204,0,1)', + background: + 'linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + '& .MuiGrid-root.MuiGrid-container': { + width: '100%', + }, + }, + mainContainerStyle: { + maxWidth: '2000px', + width: '100%', + }, + pageHeaderStyle: { + marginTop: '1em', + fontWeight: 900, + textAlign: 'center', + }, + projectGridStyle: { + marginBottom: '2em', + }, + buttonGroupStyle: { + paddingLeft: '2em', + paddingRight: '2em', + display: 'block', + marginTop: '2em', + maxWidth: '2000px', + width: '100%', + }, + floatRight: { + float: 'right', + }, + floatLeft: { + float: 'left', + }, +}); + +export default styles; diff --git a/zubhub_frontend/zubhub/src/assets/js/styles/views/signup/signupStyles.js b/zubhub_frontend/zubhub/src/assets/js/styles/views/signup/signupStyles.js new file mode 100644 index 000000000..751895f43 --- /dev/null +++ b/zubhub_frontend/zubhub/src/assets/js/styles/views/signup/signupStyles.js @@ -0,0 +1,87 @@ +import { fade } from '@material-ui/core/styles'; + +const styles = theme => ({ + root: { + paddingTop: '2em', + paddingBottom: '2em', + flex: '1 0 auto', + background: 'rgba(255,204,0,1)', + background: + 'linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + }, + cardStyle: { + border: 0, + borderRadius: 15, + boxShadow: '0 3px 5px 2px rgba(0, 0, 0, .12)', + color: 'white', + padding: '0 30px', + }, + titleStyle: { + fontWeight: 900, + }, + customLabelStyle: { + '&.MuiFormLabel-root.Mui-focused': { + color: '#00B8C4', + }, + }, + + customInputStyle: { + borderRadius: 15, + '&.MuiOutlinedInput-notchedOutline': { + border: '1px solid #00B8C4', + boxShadow: `${fade('#00B8C4', 0.25)} 0 0 0 0.2rem`, + }, + '&.MuiOutlinedInput-root': { + '&:hover fieldset': { + border: '1px solid #00B8C4', + boxShadow: `${fade('#00B8C4', 0.25)} 0 0 0 0.2rem`, + }, + '&.Mui-focused fieldset': { + border: '1px solid #00B8C4', + boxShadow: `${fade('#00B8C4', 0.25)} 0 0 0 0.2rem`, + }, + }, + }, + secondaryLink: { + color: '#00B8C4', + '&:hover': { + color: '#03848C', + }, + }, + center: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + divider: { + width: '30%', + marginRight: '1em', + marginLeft: '1em', + [theme.breakpoints.down('573')]: { + width: '20%', + }, + [theme.breakpoints.down('423')]: { + marginLeft: '0.5em', + marginRight: '0.5em', + }, + [theme.breakpoints.down('378')]: { + width: '10%', + }, + }, + textDecorationNone: { + textDecoration: 'none', + }, + errorBox: { + width: '100%', + padding: '1em', + borderRadius: 6, + borderWidth: '1px', + borderColor: '#a94442', + backgroundColor: '#ffcdd2', + }, + error: { + color: '#a94442', + }, +}); + +export default styles; diff --git a/zubhub_frontend/zubhub/src/assets/js/styles/views/user_followers/userFollowersStyles.js b/zubhub_frontend/zubhub/src/assets/js/styles/views/user_followers/userFollowersStyles.js new file mode 100644 index 000000000..be83931f9 --- /dev/null +++ b/zubhub_frontend/zubhub/src/assets/js/styles/views/user_followers/userFollowersStyles.js @@ -0,0 +1,126 @@ +const styles = theme => ({ + root: { + paddingBottom: '2em', + flex: '1 0 auto', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + background: 'rgba(255,204,0,1)', + background: + '-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + background: + '-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))', + background: + '-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + background: + '-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + background: + '-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + background: + 'linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + filter: + "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", + '& .MuiGrid-root.MuiGrid-container': { + width: '100%', + }, + }, + mainContainerStyle: { + maxWidth: '2000px', + width: '100%', + }, + pageHeaderStyle: { + marginTop: '1em', + fontWeight: 900, + textAlign: 'center', + }, + buttonGroupStyle: { + paddingLeft: '2em', + paddingRight: '2em', + display: 'block', + marginTop: '2em', + maxWidth: '2000px', + width: '100%', + }, + followersGridStyle: { + marginBottom: '2em', + }, + cardStyle: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + maxWidth: 345, + paddingTop: 0, + paddingBottom: '0 !important', + marginTop: '1em', + marginBottom: '0', + borderRadius: '15px', + textAlign: 'left', + backgroundColor: '#ffffff', + }, + avatarStyle: { + width: '100%', + height: '100%', + paddingTop: '1.5em', + paddingBottom: '1.5em', + '& img': { + width: '6em', + backgroundColor: 'white', + height: '6em', + borderRadius: '50%', + boxShadow: `0 3px 5px 2px rgba(0, 0, 0, .12)`, + }, + }, + userNameStyle: { + margin: '0.5em', + fontWeight: 'bold', + fontSize: '1.5rem', + }, + primaryButton: { + backgroundColor: '#00B8C4', + borderRadius: 15, + color: 'white', + marginLeft: '1em', + '&:hover': { + backgroundColor: '#03848C', + }, + }, + secondaryButton: { + backgroundColor: 'white', + borderRadius: 15, + borderColor: '#00B8C4', + color: '#00B8C4', + marginLeft: '1em', + '&:hover': { + color: '#03848C', + borderColor: '#03848C', + backgroundColor: '#F2F2F2', + }, + }, + secondaryLink: { + color: '#00B8C4', + '&:hover': { + color: '#03848C', + }, + }, + center: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + textDecorationNone: { + textDecoration: 'none', + }, + floatRight: { + float: 'right', + }, + floatLeft: { + float: 'left', + }, + displayNone: { display: 'none' }, + largeLabel: { + fontSize: '1.3rem', + }, +}); + +export default styles; diff --git a/zubhub_frontend/zubhub/src/assets/js/styles/views/user_projects/userProjectsStyles.js b/zubhub_frontend/zubhub/src/assets/js/styles/views/user_projects/userProjectsStyles.js new file mode 100644 index 000000000..7957887a9 --- /dev/null +++ b/zubhub_frontend/zubhub/src/assets/js/styles/views/user_projects/userProjectsStyles.js @@ -0,0 +1,45 @@ +const styles = theme => ({ + root: { + paddingBottom: '2em', + flex: '1 0 auto', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + background: 'rgba(255,204,0,1)', + background: + 'linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)', + '& .MuiGrid-root.MuiGrid-container': { + width: '100%', + }, + }, + mainContainerStyle: { + maxWidth: '2000px', + width: '100%', + }, + + pageHeaderStyle: { + marginTop: '1em', + fontWeight: 900, + textAlign: 'center', + }, + projectGridStyle: { + marginBottom: '2em', + }, + buttonGroupStyle: { + paddingLeft: '2em', + paddingRight: '2em', + display: 'block', + marginTop: '2em', + maxWidth: '2000px', + width: '100%', + }, + floatRight: { + float: 'right', + }, + floatLeft: { + float: 'left', + }, +}); + +export default styles; diff --git a/zubhub_frontend/zubhub/src/components/PageWrapper.jsx b/zubhub_frontend/zubhub/src/components/PageWrapper.jsx deleted file mode 100644 index 08ccc6262..000000000 --- a/zubhub_frontend/zubhub/src/components/PageWrapper.jsx +++ /dev/null @@ -1,364 +0,0 @@ -import React, { Component } from "react"; -import { Link } from "react-router-dom"; -import { connect } from "react-redux"; - -import { withAPI } from "./api"; - -import { ToastContainer, toast } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; -import * as AuthActions from "../store/actions/authActions"; -import unstructuredLogo from "../assets/images/logos/unstructured-logo.png"; -import logo from "../assets/images/logos/logo.png"; - -import PropTypes from "prop-types"; -import { withStyles } from "@material-ui/core/styles"; -import CssBaseline from "@material-ui/core/CssBaseline"; -import Container from "@material-ui/core/Container"; -import AppBar from "@material-ui/core/AppBar"; -import Toolbar from "@material-ui/core/Toolbar"; -import Typography from "@material-ui/core/Typography"; -import useScrollTrigger from "@material-ui/core/useScrollTrigger"; -import Box from "@material-ui/core/Box"; -import Fab from "@material-ui/core/Fab"; -import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp"; -import Zoom from "@material-ui/core/Zoom"; -import Menu from "@material-ui/core/Menu"; -import MenuItem from "@material-ui/core/MenuItem"; -import Avatar from "@material-ui/core/Avatar"; -import Button from "@material-ui/core/Button"; - -const styles = (theme) => ({ - navBarStyle: { - backgroundColor: "#DC3545", - }, - mainContainerStyle: { - maxWidth: "2000px", - }, - logoStyle: { - flexGrow: 1, - "& img": { - height: "2em", - }, - [theme.breakpoints.down("402")]: { - "& img": { - height: "1em", - }, - }, - }, - footerLogoStyle: { - height: "5em", - [theme.breakpoints.down("376")]: { - height: "3em", - }, - [theme.breakpoints.down("230")]: { - height: "2em", - }, - }, - navActionStyle: { - display: "flex", - alignItems: "center", - }, - avatarStyle: { - cursor: "pointer", - backgroundColor: "white", - marginLeft: "1em", - }, - profileMenuStyle: { - paddingTop: 0, - paddingBottom: 0, - }, - profileStyle: { backgroundColor: "#F5F5F5" }, - logOutStyle: { - borderTop: "1px solid #C4C4C4", - }, - scrollTopButtonStyle: { - zIndex: 100, - position: "fixed", - bottom: theme.spacing(2), - right: theme.spacing(2), - }, - primaryButtonStyle: { - marginLeft: "1em", - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - secondaryButtonStyle: { - borderRadius: 15, - backgroundColor: "white", - color: "#00B8C4", - "&:hover": { - color: "#03848C", - backgroundColor: "rgba(255,255,255,0.8)", - }, - }, - center: { - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - textDecorationNone: { - textDecoration: "none", - }, - displayNone: { display: "none" }, - largeLabel: { - fontSize: "1.3rem", - }, -}); - -class PageWrapper extends Component { - constructor(props) { - super(props); - - this.state = { - username: null, - anchorEl: null, - }; - } - - componentDidMount() { - if (this.props.auth.token) { - this.props.api - .get_auth_user(this.props.auth.token) - .then((res) => { - if (!res.username) { - throw new Error( - "an error occured while getting user profile, please try again later" - ); - } - this.props.set_auth_user({ - ...this.props.auth, - username: res.username, - id: res.id, - }); - }) - .catch((error) => toast.warning(error.message)); - } - } - - logout = (e) => { - e.preventDefault(); - this.props.api - .logout(this.props.auth.token) - .then((res) => { - this.props.set_auth_user({ token: null, username: null, id: null }); - }) - .then((res) => { - this.props.history.push("/"); - }) - .catch((error) => { - toast.warning( - "An error occured while signing you out. please try again" - ); - }); - }; - - handleScrollTopClick = (e) => { - const anchor = (e.target.ownerDocument || document).querySelector( - "#back-to-top-anchor" - ); - - if (anchor) { - anchor.scrollIntoView({ behavior: "smooth", block: "center" }); - } - }; - - scrollTop = (props) => { - const { classes } = props; - - return ( - -
- - - -
-
- ); - }; - - handleProfileMenuOpen = (e) => { - this.setState({ anchorEl: e.currentTarget }); - }; - - handleProfileMenuClose = () => { - this.setState({ anchorEl: null }); - }; - - render() { - let { anchorEl } = this.state; - let { classes } = this.props; - let profileMenuOpen = Boolean(anchorEl); - return ( - <> - - - - - - - - logo - - -
- {!this.props.auth.token ? ( - <> - - - - - - - - ) : ( - <> - - - - - - - - - {this.props.auth.username} - - - - - - - Saved Projects - - - - - - - Logout - - - - - - )} -
-
-
-
- - - {this.props.children} - -
-
- -
- - unstructured-studio-logo - - {this.scrollTop(this.props)} -
- - ); - } -} - -PageWrapper.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const mapStateToProps = (state) => { - return { - auth: state.auth, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - set_auth_user: (auth_user) => { - dispatch(AuthActions.setAuthUser(auth_user)); - }, - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(withAPI(withStyles(styles)(PageWrapper))); diff --git a/zubhub_frontend/zubhub/src/components/api/api.js b/zubhub_frontend/zubhub/src/components/api/api.js deleted file mode 100644 index 9619015bd..000000000 --- a/zubhub_frontend/zubhub/src/components/api/api.js +++ /dev/null @@ -1,288 +0,0 @@ -class API { - constructor(){ - this.domain = process.env.REACT_APP_NODE_ENV === 'production' ? - (process.env.REACT_APP_BACKEND_PRODUCTION_URL ) + "/api/" - : (process.env.REACT_APP_BACKEND_DEVELOPMENT_URL) + "/api/"; - } - - request=({url = '/', method = 'GET', token, body})=>{ - - if(method === 'GET' && !token){ - return fetch(this.domain + url, { - method, - xsrfCookieName: 'csrftoken', - xsrfHeaderName: 'X-CSRFToken', - withCredentials:'true', - headers: new Headers({ - 'Content-Type': 'application/json' - }) - }) - } - else if(token && body){ - - return fetch(this.domain + url, { - method, - xsrfCookieName: 'csrftoken', - xsrfHeaderName: 'X-CSRFToken', - withCredentials:'true', - headers: new Headers({ - 'Authorization':`Token ${token}`, - 'Content-Type': 'application/json' - }), - body - }) - } - else if(token){ - return fetch(this.domain + url, { - method, - xsrfCookieName: 'csrftoken', - xsrfHeaderName: 'X-CSRFToken', - withCredentials:'true', - headers: new Headers({ - 'Authorization':`Token ${token}`, - 'Content-Type': 'application/json' - }) - }) - } - else if(body){ - return fetch(this.domain + url, { - method, - xsrfCookieName: 'csrftoken', - xsrfHeaderName: 'X-CSRFToken', - withCredentials:'true', - headers: new Headers({ - 'Content-Type': 'application/json' - }), - body, - }) - } - } - - /**********************login with email and password******************************/ - login=({username, password})=>{ - let url = 'rest-auth/login/'; - let method = 'POST'; - let body = JSON.stringify({username, password}); - - return this.request({url, method, body }) - .then(res=>res.json()) - } - /****************************************************/ - - /**********************logout******************************/ - logout=(token)=>{ - let url = "rest-auth/logout/"; - let method = "POST"; - return this.request({url, method, token}) - .then(res=>res.json()) - } - /****************************************************/ - -/**********************signup******************************/ - signup=({username, email, dateOfBirth, user_location, password1, password2})=>{ - let url = 'rest-auth/registration/'; - let method = 'POST'; - let body = JSON.stringify({username, email, dateOfBirth, location: user_location, password1, password2}); - - return this.request({url, method, body }) - .then(res=>res.json()) - } - /****************************************************/ - - -/*****************send email confirmation ******************/ -send_email_confirmation=(key)=>{ - let url = 'rest-auth/registration/verify-email/'; - let method = 'POST'; - let body = JSON.stringify({key}) - - return this.request({url, method, body}) - .then(res=>res.json()) -} -/*******************************************************************/ - - -/********************send password reset link********************************/ -send_password_reset_link=(email)=>{ - let url = 'rest-auth/password/reset/'; - let method = 'POST'; - let body = JSON.stringify({email}); - - return this.request({url, method, body}) - .then(res=>res.json()) -} -/********************************************************************/ - -/********************password reset confirmation********************************/ -password_reset_confirm=({new_password1, new_password2, uid, token})=>{ - let url = 'rest-auth/password/reset/confirm/'; - let method = 'POST'; - let body = JSON.stringify({new_password1, new_password2, uid, token}); - - return this.request({url, method, body}) - .then(res=>res.json()) -} -/********************************************************************/ - -/************************** get authenticated user's details **************************/ -get_auth_user=(token)=>{ - let url = "creators/authUser/"; - return this.request({url, token}) - .then(res=>res.json()) -} - -/********************************************************************/ - - -/************************** get user profile **************************/ -get_user_profile=({username, token})=>{ - - let url = `creators/${username}/`; - if(token){ - return this.request({url, token}) - .then(res=>res.json()) - } else { - return this.request({url}) - .then(res=>res.json()) - } -} - -/*************************** get user projects *********************************/ -get_user_projects=({username, page, limit})=>{ - let url; - if(limit && page){ - url = `creators/${username}/projects/?limit=${limit}&&${page}`; - } - else if(limit){ - url = `creators/${username}/projects/?limit=${limit}`; - } - else if(page){ - url = `creators/${username}/projects/?${page}`; - } - else { - url = `creators/${username}/projects/`; - } - - return this.request({url}) - .then(res=>res.json()) -} -/*********************************************************************/ - -/********************** get followers *******************************/ -get_followers=({ page, username })=>{ - let url = page ? `creators/${username}/followers/?${page}`: `creators/${username}/followers/`; - - return this.request({url}) - .then(res=>res.json()) - -} -/*****************************************************************/ - -/********************** get saved *******************************/ -get_saved=({ page, token })=>{ - let url = page ? `projects/saved/?${page}` : `projects/saved/`; - - return this.request({url, token}) - .then(res=>res.json()) - -} -/*****************************************************************/ - - -/************************** edit user profile **************************/ -edit_user_profile=(profile)=>{ - let {username} = profile; - - let url = "creators/edit_creator/"; - let method = "PUT"; - let token = profile.token; - let body = JSON.stringify({ username }) - return this.request({url, method, token, body}) - .then(res=>res.json()) -} -/********************************************************************/ - -/************************** follow creator **************************/ -toggle_follow=({id, token})=>{ - let url = `creators/${id}/toggle_follow/`; - - return this.request({url, token}) - .then(res=>res.json()) - - -} -/******************************************************************/ - -/************************** get all locations **************************/ -get_locations=()=>{ - let url = "creators/locations/"; - return this.request({url}) - .then(res=> res.json()) -} -/********************************************************************/ - -/************************** create project **************************/ -create_project=({token, title, description, video, images, materials_used})=>{ - let url = "projects/create/"; - let method = "POST"; - let body = JSON.stringify({ title, description, images, video, materials_used }) - return this.request({url, method, token, body}) - .then(res=>res.json()) -} - -/************************** get projects **************************/ -get_projects=(page)=>{ - let url = page ? `projects/?${page}` : `projects/`; - return this.request({url}) - .then(res=>res.json()) -} - -/************************** get project **************************/ -get_project=({id, token})=>{ - let url = `projects/${id}`; - if(token){ - return this.request({url, token}) - .then(res=>res.json()) - } - else { - return this.request({url}) - .then(res=>res.json()) - } -} - -/************************** like project **************************/ -toggle_like=({id, token})=>{ - let url = `projects/${id}/toggle_like/`; - - return this.request({url, token}) - .then(res=>res.json()) - - -} -/******************************************************************/ - -/************************** save project for future viewing **************************/ -toggle_save=({id, token})=>{ - let url = `projects/${id}/toggle_save/`; - - return this.request({url, token}) - .then(res=>res.json()) - - -} -/******************************************************************/ - -/************************** add comment **************************/ -add_comment=({id, text, token})=>{ - let url = `projects/${id}/add_comment/`; - let method = "POST"; - let body = JSON.stringify({text}) - - return this.request({url, method, body, token}) - .then(res=>res.json()) - -} -} - - -export default API; diff --git a/zubhub_frontend/zubhub/src/components/button/Button.js b/zubhub_frontend/zubhub/src/components/button/Button.js new file mode 100644 index 000000000..bc9aa05b9 --- /dev/null +++ b/zubhub_frontend/zubhub/src/components/button/Button.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import classNames from 'classnames'; + +import { makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; + +import styles from '../../assets/js/styles/components/button/buttonStyles'; + +const useStyles = makeStyles(styles); + +const CustomButton = React.forwardRef((props, ref) => { + const classes = useStyles(); + + const { + children, + primaryButtonStyle, + secondaryButtonStyle, + imageUploadButtonStyle, + fullWidth, + className, + muiClasses, + ...rest + } = props; + const btnClasses = classNames({ + [classes.primaryButtonStyle]: primaryButtonStyle, + [classes.secondaryButtonStyle]: secondaryButtonStyle, + [classes.imageUploadButtonStyle]: imageUploadButtonStyle, + [classes.fullWidth]: fullWidth, + [className]: className, + }); + + return ( + + ); +}); + +Button.propTypes = { + size: PropTypes.oneOf(['small', 'large']), + primaryButtonStyle: PropTypes.bool, + secondaryButtonStyle: PropTypes.bool, + imageUploadButtonStyle: PropTypes.bool, + className: PropTypes.string, + variant: PropTypes.string, + fullWidth: PropTypes.bool, + type: PropTypes.string, + muiClasses: PropTypes.object, + children: PropTypes.node, +}; + +export default CustomButton; diff --git a/zubhub_frontend/zubhub/src/components/common/common.js b/zubhub_frontend/zubhub/src/components/common/common.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/zubhub_frontend/zubhub/src/components/pages/infos/ErrorPage.jsx b/zubhub_frontend/zubhub/src/components/pages/infos/ErrorPage.jsx deleted file mode 100644 index 0fbdd9d09..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/infos/ErrorPage.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { Component } from "react"; -import Box from "@material-ui/core/Box"; -import PropTypes from "prop-types"; -import { withStyles } from "@material-ui/core/styles"; -import { Typography, Container } from "@material-ui/core"; -import disconnected from "../../../assets/images/disconnected-chains.svg"; - -const styles = (theme) => ({ - root: { - paddingBottom: "2em", - display: "flex", - flex: "1 0 auto", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - "& .MuiGrid-root.MuiGrid-container": { - width: "100%", - }, - }, - mainContainerStyle: { - display: "flex", - alignItems: "center", - justifyContent: "center", - [theme.breakpoints.down("500")]: { - flexDirection: "column", - justifyContent: "center", - alignItems: "center", - }, - }, - disconnectedStyle: { - [theme.breakpoints.down("500")]: { - height: "10em", - }, - }, - errorBoxStyle: { - display: "flex", - flexDirection: "column", - justifyContent: "center", - alignItems: "center", - "& h1": { - fontWeight: "bold", - }, - }, -}); - -class ErrorPage extends Component { - render() { - let { classes } = this.props; - return ( - - - {this.props.error} - - Oops!! - {this.props.error} - - - - ); - } -} - -ErrorPage.propTypes = { - classes: PropTypes.object.isRequired, -}; - -export default withStyles(styles)(ErrorPage); diff --git a/zubhub_frontend/zubhub/src/components/pages/infos/LoadingPage.jsx b/zubhub_frontend/zubhub/src/components/pages/infos/LoadingPage.jsx deleted file mode 100644 index 83cdf0cea..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/infos/LoadingPage.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { Component } from "react"; -import Container from "@material-ui/core/Container"; -import Box from "@material-ui/core/Box"; -import PropTypes from "prop-types"; -import { withStyles } from "@material-ui/core/styles"; -import { CircularProgress } from "@material-ui/core"; - -const styles = (theme) => ({ - root: { - paddingBottom: "2em", - display: "flex", - flex: "1 0 auto", - alignItems: "center", - justifyContent: "center", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - "& .MuiGrid-root.MuiGrid-container": { - width: "100%", - }, - }, - mainContainerStyle: { - display: "flex", - justifyContent: "center", - alignItems: "center", - maxWidth: "2000px", - width: "100%", - }, - circularProgressStyle: { - color: "#00B8C4", - }, -}); - -class LoadingPage extends Component { - render() { - let { classes } = this.props; - return ( - - - - - - ); - } -} - -LoadingPage.propTypes = { - classes: PropTypes.object.isRequired, -}; - -export default withStyles(styles)(LoadingPage); diff --git a/zubhub_frontend/zubhub/src/components/pages/profile/Profile.jsx b/zubhub_frontend/zubhub/src/components/pages/profile/Profile.jsx deleted file mode 100644 index ed76af345..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/profile/Profile.jsx +++ /dev/null @@ -1,619 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import { toast } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; -import ErrorPage from "../infos/ErrorPage"; -import LoadingPage from "../infos/LoadingPage"; -import { Link } from "react-router-dom"; -import Project from "../projects/projects_components/Project"; -import * as AuthActions from "../../../store/actions/authActions"; -import "react-toastify/dist/ReactToastify.css"; -import "react-toastify/dist/ReactToastify.css"; -import clsx from "clsx"; -import PropTypes from "prop-types"; -import { withStyles, fade } from "@material-ui/core/styles"; -import ShareIcon from "@material-ui/icons/Share"; -import Tooltip from "@material-ui/core/Tooltip"; -import Badge from "@material-ui/core/Badge"; -import Avatar from "@material-ui/core/Avatar"; -import Grid from "@material-ui/core/Grid"; -import Box from "@material-ui/core/Box"; -import Container from "@material-ui/core/Container"; -import Paper from "@material-ui/core/Paper"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import Button from "@material-ui/core/Button"; -import Fab from "@material-ui/core/Fab"; -import Typography from "@material-ui/core/Typography"; -import OutlinedInput from "@material-ui/core/OutlinedInput"; -import InputLabel from "@material-ui/core/InputLabel"; -import FormControl from "@material-ui/core/FormControl"; -import { Divider } from "@material-ui/core"; - -const styles = (theme) => ({ - root: { - flex: "1 0 auto", - }, - profileHeaderStyle: { - paddingTop: "1.5em", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - }, - avatarBoxStyle: { - width: "100%", - display: "flex", - justifyContent: "center", - }, - profileShareButtonStyle: { - borderRadius: "50% !important", - }, - avatarStyle: { - width: "100%", - height: "100%", - paddingTop: "1.5em", - paddingBottom: "1.5em", - "& img": { - width: "10em", - backgroundColor: "white", - height: "10em", - borderRadius: "50%", - boxShadow: `0 3px 5px 2px rgba(0, 0, 0, .12)`, - }, - }, - ProfileDetailStyle: { - width: "100%", - display: "flex", - flexDirection: "column", - alignItems: "center", - }, - userNameStyle: { - fontWeight: 900, - fontSize: "2rem", - }, - emailStyle: { marginBottom: "0.5em" }, - dividerStyle: { - width: "100vw", - }, - moreInfoBoxStyle: { - height: "3em", - display: "flex", - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - }, - moreInfoStyle: { - marginLeft: "0.5em", - marginRight: "0.5em", - fontWeight: "bold", - fontSize: "0.9rem", - color: "#00B8C4", - }, - profileLowerStyle: { - margin: "1em", - padding: "1em", - paddingBottom: "4em", - }, - titleStyle: { - fontWeight: 900, - fontSize: "1.5rem", - }, - aboutMeBoxStyle: { - display: "flex", - justifyContent: "center", - alignItems: "center", - minHeight: "7em", - borderRadius: "15px", - backgroundColor: "#E4E4E4", - }, - cardStyle: { - border: 0, - borderRadius: 15, - boxShadow: "0 3px 5px 2px rgba(0, 0, 0, .12)", - color: "white", - padding: "0 30px", - }, - customLabelStyle: { - "&.MuiFormLabel-root.Mui-focused": { - color: "#00B8C4", - }, - }, - - projectGridStyle: { - marginBottom: "2em", - }, - customInputStyle: { - borderRadius: 15, - "&.MuiOutlinedInput-notchedOutline": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.MuiOutlinedInput-root": { - "&:hover fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.Mui-focused fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - }, - }, - primaryButton: { - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - marginLeft: "1em", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - secondaryButton: { - backgroundColor: "white", - borderRadius: 15, - borderColor: "#00B8C4", - color: "#00B8C4", - marginLeft: "1em", - "&:hover": { - color: "#03848C", - borderColor: "#03848C", - backgroundColor: "#F2F2F2", - }, - }, - secondaryLink: { - color: "#00B8C4", - "&:hover": { - color: "#03848C", - }, - }, - center: { - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - textDecorationNone: { - textDecoration: "none", - }, - floatRight: { float: "right" }, - displayNone: { display: "none" }, - largeLabel: { - fontSize: "1.3rem", - }, - errorBox: { - width: "100%", - padding: "1em", - borderRadius: 6, - borderWidth: "1px", - borderColor: "#a94442", - backgroundColor: "#ffcdd2", - }, - error: { - color: "#a94442", - }, -}); - -class Profile extends Component { - constructor(props) { - super(props); - this.state = { - profile: {}, - projects: [], - openEditProfileModal: false, - loading: true, - }; - } - - componentDidMount() { - this.get_user_profile(); - } - - get_user_profile = () => { - let username = this.props.match.params.username; - - if (!username) { - username = this.props.auth.username; - } else if (this.props.auth.username === username) - this.props.history.push("/profile"); - this.props.api - .get_user_profile({ username, token: this.props.auth.token }) - .then((res) => { - if (!res.username) { - throw new Error( - "an error occured while fetching user profile, please try again later" - ); - } - this.setState({ profile: res }); - return this.props.api.get_user_projects({ - username: res.username, - limit: 4, - }); - }) - .then((res) => { - if (Array.isArray(res.results)) { - return this.setState({ projects: res.results, loading: false }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - toast.warning(error.message); - this.setState({ loading: false }); - }); - }; - - toggle_follow = (id) => { - if (!this.props.auth.token) { - this.props.history.push("/login"); - } else { - this.props.api - .toggle_follow({ id, token: this.props.auth.token }) - .then((res) => { - if (res.id) { - return this.setState({ profile: res }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - if (error.message.startsWith("Unexpected")) { - toast.warning( - "An error occured while performing this action. Please try again later" - ); - } else { - toast.warning(error.message); - } - }); - } - }; - - handleToggleEditProfileModal = () => { - let { openEditProfileModal } = this.state; - openEditProfileModal = !openEditProfileModal; - this.setState({ openEditProfileModal }); - }; - - copyProfileUrl = (e) => { - let tempInput = document.createElement("textarea"); - tempInput.value = `${document.location.origin}/creators/${this.state.profile.username}`; - tempInput.style.top = "0"; - tempInput.style.top = "0"; - tempInput.style.position = "fixed"; - let rootElem = document.querySelector("#root"); - rootElem.appendChild(tempInput); - tempInput.focus(); - tempInput.select(); - if (document.execCommand("copy")) { - toast.success( - "your profile url has been successfully copied to your clipboard!" - ); - rootElem.removeChild(tempInput); - } - }; - - updateProfile = (e) => { - e.preventDefault(); - let username = document.querySelector("#new_username"); - this.props.api - .edit_user_profile({ - token: this.props.auth.token, - username: username.value, - }) - .then((res) => { - if (res.username) { - this.setState({ profile: res }); - this.props.set_auth_user({ - ...this.props.auth, - username: res.username, - }); - this.handleToggleEditProfileModal(); - username.value = ""; - } else { - throw new Error( - "An error occured while updating your profile, please try again later" - ); - } - }) - .catch((error) => toast.warning(error.message)); - }; - - setProfile = (value) => {}; - - updateProjects = (res) => { - res - .then((res) => { - if (res.id) { - let { projects } = this.state; - projects = projects.map((project) => - project.id === res.id ? res : project - ); - return this.setState({ projects }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - toast.warning(error.message); - }); - }; - - render() { - let { profile, projects, loading, openEditProfileModal } = this.state; - let { classes } = this.props; - - if (loading) { - return ; - } else if (Object.keys(profile).length > 0) { - return ( - <> - - - - {this.props.auth.username === profile.username ? ( - - ) : ( - - )} - - - - - - - ) : null - } - > - - - - - - {profile.username} - - {this.props.auth.username === profile.username ? ( - - {profile.email} - - ) : null} - - - - - {profile.projects_count} Projects - - - - - {profile.followers.length} Followers - - - - - - - - - - - About Me - - - {profile.bio - ? profile.bio - : "You will be able to change this next month 😀!"} - - - - {profile.projects_count > 0 ? ( - - - Latest projects of {profile.username} - - View all >> - - - - {projects.map((project) => ( - - - - ))} - - - ) : null} - - - - Edit User Profile - - - - New Username - - - - - - - - - - - ); - } else { - return ( - - ); - } - } -} - -Profile.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const mapStateToProps = (state) => { - return { - auth: state.auth, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - set_auth_user: (auth_user) => { - dispatch(AuthActions.setAuthUser(auth_user)); - }, - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(Profile)); diff --git a/zubhub_frontend/zubhub/src/components/pages/profile/profile_components/UserFollowers.jsx b/zubhub_frontend/zubhub/src/components/pages/profile/profile_components/UserFollowers.jsx deleted file mode 100644 index 120d950b7..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/profile/profile_components/UserFollowers.jsx +++ /dev/null @@ -1,340 +0,0 @@ -import React, { Component } from "react"; -import { Link } from "react-router-dom"; -import { connect } from "react-redux"; -import { toast } from "react-toastify"; -import ErrorPage from "../../infos/ErrorPage"; -import LoadingPage from "../../infos/LoadingPage"; -import clsx from "clsx"; -import Grid from "@material-ui/core/Grid"; -import Container from "@material-ui/core/Container"; -import Box from "@material-ui/core/Box"; -import Card from "@material-ui/core/Card"; -import Button from "@material-ui/core/Button"; -import ButtonGroup from "@material-ui/core/ButtonGroup"; -import Typography from "@material-ui/core/Typography"; -import PropTypes from "prop-types"; -import { withStyles } from "@material-ui/core/styles"; -import Avatar from "@material-ui/core/Avatar"; -import NavigateBeforeIcon from "@material-ui/icons/NavigateBefore"; -import NavigateNextIcon from "@material-ui/icons/NavigateNext"; - -const styles = (theme) => ({ - root: { - paddingBottom: "2em", - flex: "1 0 auto", - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - "& .MuiGrid-root.MuiGrid-container": { - width: "100%", - }, - }, - mainContainerStyle: { - maxWidth: "2000px", - width: "100%", - }, - pageHeaderStyle: { - marginTop: "1em", - fontWeight: 900, - textAlign: "center", - }, - buttonGroupStyle: { - paddingLeft: "2em", - paddingRight: "2em", - display: "block", - marginTop: "2em", - maxWidth: "2000px", - width: "100%", - }, - followersGridStyle: { - marginBottom: "2em", - }, - cardStyle: { - display: "flex", - flexDirection: "column", - alignItems: "center", - maxWidth: 345, - paddingTop: 0, - paddingBottom: "0 !important", - marginTop: "1em", - marginBottom: "0", - borderRadius: "15px", - textAlign: "left", - backgroundColor: "#ffffff", - }, - avatarStyle: { - width: "100%", - height: "100%", - paddingTop: "1.5em", - paddingBottom: "1.5em", - "& img": { - width: "6em", - backgroundColor: "white", - height: "6em", - borderRadius: "50%", - boxShadow: `0 3px 5px 2px rgba(0, 0, 0, .12)`, - }, - }, - userNameStyle: { - margin: "0.5em", - fontWeight: "bold", - fontSize: "1.5rem", - }, - primaryButton: { - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - marginLeft: "1em", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - secondaryButton: { - backgroundColor: "white", - borderRadius: 15, - borderColor: "#00B8C4", - color: "#00B8C4", - marginLeft: "1em", - "&:hover": { - color: "#03848C", - borderColor: "#03848C", - backgroundColor: "#F2F2F2", - }, - }, - secondaryLink: { - color: "#00B8C4", - "&:hover": { - color: "#03848C", - }, - }, - center: { - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - textDecorationNone: { - textDecoration: "none", - }, - floatRight: { - float: "right", - }, - floatLeft: { - float: "left", - }, - displayNone: { display: "none" }, - largeLabel: { - fontSize: "1.3rem", - }, -}); - -class UserFollowers extends Component { - constructor(props) { - super(props); - this.state = { - followers: [], - prevPage: null, - nextPage: null, - loading: true, - }; - } - - componentDidMount() { - this.fetchPage(); - } - - fetchPage = (page) => { - let username = this.props.match.params.username; - this.props.api - .get_followers({ page, username }) - .then((res) => { - if (Array.isArray(res.results)) { - return this.setState({ - followers: res.results, - prevPage: res.previous, - nextPage: res.next, - loading: false, - }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - toast.warning(error.message); - }); - }; - - toggle_follow = (e, id) => { - e.preventDefault(); - if (!this.props.auth.token) { - this.props.history.push("/login"); - } else { - this.props.api - .toggle_follow({ id, token: this.props.auth.token }) - .then((res) => { - if (res.id) { - let followers = this.state.followers.map((follower) => - follower.id !== res.id ? follower : res - ); - return this.setState({ followers }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - if (error.message.startsWith("Unexpected")) { - toast.warning( - "An error occured while performing this action. Please try again later" - ); - } else { - toast.warning(error.message); - } - }); - } - }; - - followers = (followers) => - followers.map((follower) => ( - - - - - {follower.id !== this.props.auth.id ? ( - - ) : null} - - {follower.username} - - - - - )); - - render() { - let { followers, prevPage, nextPage, loading } = this.state; - let { classes } = this.props; - let username = this.props.match.params.username; - if (loading) { - return ; - } else if (followers.length > 0) { - return ( - - - - - - {username}'s followers - - - {this.followers(followers)} - - - {prevPage ? ( - - ) : null} - {nextPage ? ( - - ) : null} - - - - ); - } else { - return ; - } - } -} - -UserFollowers.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const mapStateToProps = (state) => { - return { - auth: state.auth, - }; -}; - -export default connect(mapStateToProps)(withStyles(styles)(UserFollowers)); diff --git a/zubhub_frontend/zubhub/src/components/pages/profile/profile_components/UserProjects.jsx b/zubhub_frontend/zubhub/src/components/pages/profile/profile_components/UserProjects.jsx deleted file mode 100644 index 55ffe5741..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/profile/profile_components/UserProjects.jsx +++ /dev/null @@ -1,239 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import { toast } from "react-toastify"; -import ErrorPage from "../../infos/ErrorPage"; -import LoadingPage from "../../infos/LoadingPage"; -import clsx from "clsx"; -import Grid from "@material-ui/core/Grid"; -import Box from "@material-ui/core/Box"; -import Button from "@material-ui/core/Button"; -import ButtonGroup from "@material-ui/core/ButtonGroup"; -import NavigateBeforeIcon from "@material-ui/icons/NavigateBefore"; -import NavigateNextIcon from "@material-ui/icons/NavigateNext"; -import PropTypes from "prop-types"; -import { withStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; - -import Project from "../../projects/projects_components/Project"; -import { Container } from "@material-ui/core"; - -const styles = (theme) => ({ - root: { - paddingBottom: "2em", - flex: "1 0 auto", - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - "& .MuiGrid-root.MuiGrid-container": { - width: "100%", - }, - }, - mainContainerStyle: { - maxWidth: "2000px", - width: "100%", - }, - - pageHeaderStyle: { - marginTop: "1em", - fontWeight: 900, - textAlign: "center", - }, - projectGridStyle: { - marginBottom: "2em", - }, - buttonGroupStyle: { - paddingLeft: "2em", - paddingRight: "2em", - display: "block", - marginTop: "2em", - maxWidth: "2000px", - width: "100%", - }, - primaryButtonStyle: { - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - floatRight: { - float: "right", - }, - floatLeft: { - float: "left", - }, -}); - -class UserProjects extends Component { - constructor(props) { - super(props); - this.state = { - projects: [], - prevPage: null, - nextPage: null, - loading: true, - }; - } - - componentDidMount() { - this.fetchPage(); - } - - fetchPage = (page) => { - let username = this.props.match.params.username; - this.props.api - .get_user_projects({ page, username }) - .then((res) => { - if (Array.isArray(res.results)) { - return this.setState({ - projects: res.results, - prevPage: res.previous, - nextPage: res.next, - loading: false, - }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - toast.warning(error.message); - }); - }; - - updateProjects = (res) => { - res - .then((res) => { - if (res.id) { - let { projects } = this.state; - projects = projects.map((project) => - project.id === res.id ? res : project - ); - return this.setState({ projects }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - toast.warning(error.message); - }); - }; - - render() { - let { projects, prevPage, nextPage, loading } = this.state; - let { classes } = this.props; - let username = this.props.match.params.username; - if (loading) { - return ; - } else if (projects.length > 0) { - return ( - - - - - - {username}'s projects - - - {projects.map((project) => ( - - - - ))} - - - {prevPage ? ( - - ) : null} - {nextPage ? ( - - ) : null} - - - - ); - } else { - return ; - } - } -} - -UserProjects.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const mapStateToProps = (state) => { - return { - auth: state.auth, - }; -}; - -export default connect(mapStateToProps)(withStyles(styles)(UserProjects)); diff --git a/zubhub_frontend/zubhub/src/components/pages/projects/Projects.jsx b/zubhub_frontend/zubhub/src/components/pages/projects/Projects.jsx deleted file mode 100644 index e451aeca9..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/projects/Projects.jsx +++ /dev/null @@ -1,222 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import { toast } from "react-toastify"; -import ErrorPage from "../infos/ErrorPage"; -import LoadingPage from "../infos/LoadingPage"; -import Grid from "@material-ui/core/Grid"; -import Box from "@material-ui/core/Box"; -import Button from "@material-ui/core/Button"; -import ButtonGroup from "@material-ui/core/ButtonGroup"; -import NavigateBeforeIcon from "@material-ui/icons/NavigateBefore"; -import NavigateNextIcon from "@material-ui/icons/NavigateNext"; -import clsx from "clsx"; -import PropTypes from "prop-types"; -import { withStyles } from "@material-ui/core/styles"; -import Project from "./projects_components/Project"; -import { Container } from "@material-ui/core"; - -const styles = (theme) => ({ - root: { - paddingBottom: "2em", - flex: "1 0 auto", - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - "& .MuiGrid-root.MuiGrid-container": { - width: "100%", - }, - }, - mainContainerStyle: { - maxWidth: "2000px", - width: "100%", - }, - projectGridStyle: { - marginBottom: "2em", - }, - buttonGroupStyle: { - paddingLeft: "2em", - paddingRight: "2em", - display: "block", - marginTop: "2em", - maxWidth: "2000px", - width: "100%", - }, - primaryButtonStyle: { - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - floatRight: { - float: "right", - }, - floatLeft: { - float: "left", - }, -}); - -class Projects extends Component { - constructor(props) { - super(props); - this.state = { - projects: [], - prevPage: null, - nextPage: null, - loading: true, - }; - } - - componentDidMount() { - this.fetchPage(); - } - - fetchPage = (page) => { - this.props.api - .get_projects(page) - .then((res) => { - if (Array.isArray(res.results)) { - return this.setState({ - projects: res.results, - prevPage: res.previous, - nextPage: res.next, - loading: false, - }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - toast.warning(error.message); - }); - }; - - updateProjects = (res) => { - res - .then((res) => { - if (res.id) { - let { projects } = this.state; - projects = projects.map((project) => - project.id === res.id ? res : project - ); - return this.setState({ projects }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - toast.warning(error.message); - }); - }; - - render() { - let { projects, prevPage, nextPage, loading } = this.state; - let { classes } = this.props; - if (loading) { - return ; - } else if (projects.length > 0) { - return ( - - - - {projects.map((project) => ( - - - - ))} - - - {prevPage ? ( - - ) : null} - {nextPage ? ( - - ) : null} - - - - ); - } else { - return ( - - ); - } - } -} - -Projects.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const mapStateToProps = (state) => { - return { - auth: state.auth, - }; -}; - -export default connect(mapStateToProps)(withStyles(styles)(Projects)); diff --git a/zubhub_frontend/zubhub/src/components/pages/projects/projects_components/CreateProject.jsx b/zubhub_frontend/zubhub/src/components/pages/projects/projects_components/CreateProject.jsx deleted file mode 100644 index 6ae455f18..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/projects/projects_components/CreateProject.jsx +++ /dev/null @@ -1,805 +0,0 @@ -import React, { Component } from "react"; -import { withFormik } from "formik"; -import { toast } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; -import * as Yup from "yup"; -import { connect } from "react-redux"; -import "react-toastify/dist/ReactToastify.css"; -import ErrorPage from "../../infos/ErrorPage"; -import clsx from "clsx"; -import PropTypes from "prop-types"; -import DO, { doConfig } from "../../../../assets/js/DO"; -import { nanoid } from "nanoid"; -import { withStyles, fade } from "@material-ui/core/styles"; -import Grid from "@material-ui/core/Grid"; -import Box from "@material-ui/core/Box"; -import Container from "@material-ui/core/Container"; -import Card from "@material-ui/core/Card"; -import CardActionArea from "@material-ui/core/CardActionArea"; -import CardContent from "@material-ui/core/CardContent"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import Chip from "@material-ui/core/Chip"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import Button from "@material-ui/core/Button"; -import Typography from "@material-ui/core/Typography"; -import CircularProgress from "@material-ui/core/CircularProgress"; -import OutlinedInput from "@material-ui/core/OutlinedInput"; -import InputLabel from "@material-ui/core/InputLabel"; -import ImageIcon from "@material-ui/icons/Image"; -import AddIcon from "@material-ui/icons/Add"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import FormControl from "@material-ui/core/FormControl"; - -const styles = (theme) => ({ - root: { - paddingTop: "2em", - paddingBottom: "2em", - flex: "1 0 auto", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - }, - cardStyle: { - border: 0, - borderRadius: 15, - boxShadow: "0 3px 5px 2px rgba(0, 0, 0, .12)", - color: "white", - padding: "0 30px", - }, - titleStyle: { - fontWeight: 900, - }, - customLabelStyle: { - "&.MuiFormLabel-root.Mui-focused": { - color: "#00B8C4", - }, - }, - - customInputStyle: { - width: "100%", - borderRadius: 15, - "&.MuiOutlinedInput-notchedOutline": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.MuiOutlinedInput-root": { - "&:hover fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.Mui-focused fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - }, - }, - primaryButton: { - width: "100%", - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - secondaryButton: { - width: "100%", - borderRadius: 15, - borderColor: "#00B8C4", - color: "#00B8C4", - "&:hover": { - color: "#03848C", - borderColor: "#03848C", - }, - }, - imageUploadButton: { - "& MuiButton-label": { - width: "100%", - display: "flex", - justifyContent: "flex-end", - "& imageCountStyle": { - flexGrow: 1, - }, - }, - }, - uploadProgressLabelStyle: { - color: "white", - }, - uploadProgressStyle: { - color: "#00B8C4", - }, - customChipStyle: { - border: "1px solid #00B8C4", - color: "#00B8C4", - margin: "0.5em", - }, - materialsUsedViewStyle: { - padding: "0.5em", - }, - center: { - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - textDecorationNone: { - textDecoration: "none", - }, - displayNone: { display: "none" }, - largeLabel: { - fontSize: "1.3rem", - }, - errorBox: { - width: "100%", - padding: "1em", - borderRadius: 6, - borderWidth: "1px", - borderColor: "#a94442", - backgroundColor: "#ffcdd2", - }, - error: { - color: "#a94442", - }, -}); - -class CreateProject extends Component { - constructor(props) { - super(props); - this.state = { - error: null, - materialsUsedModalOpen: false, - materials_used: [], - image_upload: { - upload_dialog: false, - images_to_upload: 0, - successful_uploads: 0, - upload_info: {}, - upload_percent: 0, - uploaded_images_url: [], - }, - }; - } - - upload = (image) => { - const params = { - Bucket: `${doConfig.bucketName}`, - Key: `${doConfig.project_images}/${nanoid()}`, - Body: image, - ContentType: image.type, - ACL: "public-read", - }; - - DO.upload(params) - .on("httpUploadProgress", (e) => { - let progress = Math.round((e.loaded * 100.0) / e.total); - let { image_upload } = this.state; - image_upload.upload_info[image.name] = progress; - - let total = 0; - Object.keys(image_upload.upload_info).forEach((each) => { - total = total + image_upload.upload_info[each]; - }); - - total = total / Object.keys(image_upload.upload_info).length; - image_upload.upload_percent = total; - - this.setState({ image_upload }); - }) - .send((err, data) => { - if (err) { - let { image_upload } = this.state; - image_upload.upload_dialog = false; - - if (err.message.startsWith("Unexpected")) { - this.setState({ - error: - "An error occured while performing this action. Please try again later", - image_upload, - }); - } else { - this.setState({ error: err.message, image_upload }); - } - } else { - let secure_url = data.Location; - let public_id = data.Key; - let { image_upload } = this.state; - - image_upload.uploaded_images_url.push({ - image_url: secure_url, - public_id, - }); - image_upload.successful_uploads = image_upload.successful_uploads + 1; - - this.setState({ image_upload }, () => { - if ( - this.state.image_upload.images_to_upload === - this.state.image_upload.successful_uploads - ) { - this.upload_project(); - } - }); - } - }); - }; - - upload_project = () => { - let { image_upload } = this.state; - image_upload.upload_dialog = false; - this.setState({ image_upload }); - this.props.api - .create_project({ - ...this.props.values, - token: this.props.auth.token, - images: this.state.image_upload.uploaded_images_url, - video: this.props.values.video ? this.props.values.video : "", - }) - .then((res) => { - if (!res.title) { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - toast.success("Your project was created successfully!!"); - return this.props.history.push("/profile"); - }) - .catch((error) => { - if (error.message.startsWith("Unexpected")) { - this.setState({ - error: - "An error occured while performing this action. Please try again later", - }); - } else { - this.setState({ error: error.message }); - } - }); - }; - - init_project = (e) => { - if (!this.props.auth.token) { - this.props.history.push("/login"); - } else { - let media_fields = this.mediaFieldsValidation(); - - if (media_fields.image_is_empty && media_fields.video_is_empty) { - this.props.setErrors({ - project_images: "you must provide either image(s) or video url", - }); - this.props.setErrors({ - video: "you must provide either image(s) or video url", - }); - } else if (media_fields.too_many_images === true) { - this.props.setErrors({ project_images: "too many images uploaded" }); - } else if (media_fields.image_size_too_large === true) { - this.props.setErrors({ - project_images: "one or more of your image is greater than 3mb", - }); - } else if (media_fields.image_is_empty) { - this.upload_project(); - } else { - let project_images = document.querySelector("#project_images").files; - - let { image_upload } = this.state; - image_upload.images_to_upload = project_images.length; - image_upload.upload_dialog = true; - image_upload.upload_percent = 0; - this.setState({ image_upload }); - - for (let index = 0; index < project_images.length; index++) { - this.upload(project_images[index]); - } - } - } - }; - - mediaFieldsValidation = () => { - let image_upload_button = document.querySelector("#image_upload_button"); - let media_fields = document.querySelector("#project_images"); - let video = document.querySelector("#video"); - let imageCount = document.querySelector(".imageCountStyle"); - imageCount.innerText = media_fields.files.length; - imageCount.style.fontSize = "0.8rem"; - - let result = {}; - - if (media_fields.files.length < 1) { - result["image_is_empty"] = true; - if (video.value === "") { - image_upload_button.setAttribute( - "style", - "border-color:#F54336; color:#F54336" - ); - this.props.setErrors({ - project_images: "you must provide either image(s) or video url", - }); - this.props.setErrors({ - video: "you must provide either image(s) or video url", - }); - result["video_is_empty"] = true; - } - } else if (media_fields.files.length > 10) { - image_upload_button.setAttribute( - "style", - "border-color:#F54336; color:#F54336" - ); - this.props.setErrors({ project_images: "too many images uploaded" }); - result["too_many_images"] = true; - } else { - let image_size_too_large = false; - - for (let index = 0; index < media_fields.files.length; index++) { - if (media_fields.files[index].size / 1000 > 3072) { - image_size_too_large = true; - } - } - if (image_size_too_large) { - image_upload_button.setAttribute( - "style", - "border-color:#F54336; color:#F54336" - ); - this.props.setErrors({ - project_images: "one or more of your image is greater than 3mb", - }); - result["image_size_too_large"] = image_size_too_large; - } - - return result; - } - - image_upload_button.setAttribute( - "style", - "border-color: #00B8C4; color:#00B8C4" - ); - return result; - }; - - handleImageButtonClick = () => { - document.querySelector("#project_images").click(); - this.props.setFieldTouched("project_images"); - }; - - handleAddMaterialFieldChange = (e) => { - this.props.validateField("materials_used"); - let value = e.currentTarget.value; - if (value.includes(",")) { - e.currentTarget.value = value.split(",")[0]; - this.addMaterialUsed(e); - } - }; - - addMaterialUsed = (e) => { - e.preventDefault(); - let new_material = document.querySelector("#add_materials_used"); - if (new_material.value !== "") { - let hidden_materials_field = document.querySelector("#materials_used"); - hidden_materials_field.value = hidden_materials_field.value - ? `${hidden_materials_field.value},${new_material.value}` - : new_material.value; - new_material.value = ""; - this.props.setFieldValue( - "materials_used", - hidden_materials_field.value, - true - ); - this.setState({ - materials_used: hidden_materials_field.value.split(","), - }); - } - new_material.focus(); - }; - - removeMaterialsUsed = (e, value) => { - e.preventDefault(); - let hidden_materials_field = document.querySelector("#materials_used"); - hidden_materials_field.value = hidden_materials_field.value - .split(",") - .filter((material) => material !== value) - .join(","); - - this.props.setFieldValue( - "materials_used", - hidden_materials_field.value, - true - ); - this.setState({ materials_used: hidden_materials_field.value.split(",") }); - }; - - render() { - let { error, image_upload, materials_used } = this.state; - let { classes } = this.props; - if (!this.props.auth.token) { - return ( - - ); - } else { - return ( - - - - - -
- - Create Project - - - Tell us about your project! - - - - - - {error !== null && ( - - {error} - - )} - - - - - - - Title - - - - {this.props.touched["title"] && - this.props.errors["title"]} - - - - - - - - Description - - - - {this.props.touched["description"] && - this.props.errors["description"]} - - - - - - - - - - {this.props.errors["project_images"]} - - - - - - - - Video URL - - - - - YouTube, Vimeo, Google Drive links are supported - -
- {this.props.touched["video"] && - this.props.errors["video"]} -
-
-
- - - - - Materials Used - - - {materials_used.map((material, num) => - material !== "" ? ( - - this.removeMaterialsUsed(e, value) - } - color="secondary" - variant="outlined" - /> - ) : null - )} - - - - - this.props.setFieldTouched("materials_used") - } - onChange={this.handleAddMaterialFieldChange} - onBlur={() => - this.props.validateField("materials_used") - } - /> - - {this.props.touched["materials_used"] && - this.props.errors["materials_used"]} - - - - - - - - - - - - -
-
- - - - - {`${Math.round( - image_upload.upload_percent - )}%`} - - - -
-
-
-
-
- ); - } - } -} - -CreateProject.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const mapStateToProps = (state) => { - return { - auth: state.auth, - }; -}; - -export default connect(mapStateToProps)( - withFormik({ - mapPropsToValue: () => ({ - title: "", - description: "", - video: "", - materials_used: "", - }), - validationSchema: Yup.object().shape({ - title: Yup.string() - .max(100, "your project title shouldn't be more than 100 characters") - .required("title is required"), - description: Yup.string() - .max(10000, "your description shouldn't be more than 10,000 characters") - .required("description is required"), - video: Yup.string() - .url("you are required to submit a video url here") - .max(1000, "your video url shouldn't be more than 1000 characters"), - materials_used: Yup.string() - .max( - 10000, - "your materials used shouldn't be more than 10,000 characters" - ) - .required("materials used is required"), - }), - handleSubmit: (values, { setSubmitting }) => {}, - })(withStyles(styles)(CreateProject)) -); diff --git a/zubhub_frontend/zubhub/src/components/pages/projects/projects_components/Project.jsx b/zubhub_frontend/zubhub/src/components/pages/projects/projects_components/Project.jsx deleted file mode 100644 index 0fe672b57..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/projects/projects_components/Project.jsx +++ /dev/null @@ -1,359 +0,0 @@ -import React, { Component } from "react"; -import { Link } from "react-router-dom"; -import "react-toastify/dist/ReactToastify.css"; -import clsx from "clsx"; -import PropTypes from "prop-types"; -import { withStyles } from "@material-ui/core/styles"; -import Avatar from "@material-ui/core/Avatar"; -import Box from "@material-ui/core/Box"; -import Card from "@material-ui/core/Card"; -import CardActionArea from "@material-ui/core/CardActionArea"; -import CardMedia from "@material-ui/core/CardMedia"; -import CardContent from "@material-ui/core/CardContent"; -import Fab from "@material-ui/core/Fab"; -import BookmarkIcon from "@material-ui/icons/Bookmark"; -import BookmarkBorderIcon from "@material-ui/icons/BookmarkBorder"; -import ClapIcon, { ClapBorderIcon } from "../../../../assets/js/icons/ClapIcon"; -import CommentIcon from "../../../../assets/js/icons/CommentIcon"; -import VisibilityIcon from "@material-ui/icons/Visibility"; -import nFormatter from "../../../../assets/js/nFormatter"; -import dFormatter from "../../../../assets/js/dFormatter"; -import Typography from "@material-ui/core/Typography"; - -const styles = (theme) => ({ - root: { - maxWidth: 345, - height: "100%", - paddingTop: 0, - paddingBottom: "0!important", - marginTop: "1em", - marginBottom: "1em", - marginLeft: "1em", - marginRight: "1em", - borderRadius: "15px", - textAlign: "left", - backgroundColor: "#ffffff", - flex: "1 0 auto", - display: "flex", - flexDirection: "column", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - alignItems: "center", - flexDirection: "column", - justifyContent: "center", - }, - mediaBoxStyle: { - width: "100%", - height: "13em", - position: "relative", - }, - mediaStyle: { - width: "100%", - height: "100%", - borderStyle: "none", - }, - mediaImageStyle: { - objectFit: "cover", - width: "100%", - height: "13em", - position: "absolute", - }, - actionAreaStyle: { - flexGrow: 100, - }, - contentStyle: { - height: "100%", - display: "flex", - flexDirection: "column", - }, - fabButtonStyle: { - color: "#ffcc00", - backgroundColor: "#dc3545", - position: "absolute", - marginLeft: "1em", - right: "1em", - top: "-1.8em", - "&:hover": { - backgroundColor: "#b52836", - backgroundSize: "100%", - }, - "& svg": { - fill: "#ffcc00", - }, - "& svg:hover": { - fill: "#ffcc00", - }, - }, - likeButtonStyle: { - right: "4.5em", - top: "-1.6em", - }, - titleStyle: { - fontWeight: 900, - fontSize: "1.3rem", - }, - descriptionStyle: { - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - flexGrow: 1, - }, - creatorBoxStyle: { - marginTop: "0.5em", - marginBottom: "0.5em", - display: "flex", - justifyContent: "flex-start", - alignItems: "center", - }, - creatorAvatarStyle: { - marginRight: "0.5em", - boxShadow: `0 3px 5px 2px rgba(0, 0, 0, .12)`, - }, - captionStyle: { - display: "flex", - justifyContent: "space-between", - }, - captionIconStyle: { - display: "flex", - alignItems: "center", - marginRight: "1em", - "& svg": { - fill: "rgba(0,0,0,0.54)", - marginRight: "0.5em", - }, - }, - VisibilityIconStyle: { - "& svg": { - fontSize: "1.1rem", - }, - }, - moreInfoBoxStyle: { - height: "3em", - display: "flex", - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - }, - moreInfoStyle: { - marginLeft: "0.5em", - marginRight: "0.5em", - fontWeight: "bold", - fontSize: "0.9rem", - color: "#00B8C4", - }, - primaryButton: { - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - marginLeft: "1em", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - secondaryButton: { - backgroundColor: "white", - borderRadius: 15, - borderColor: "#00B8C4", - color: "#00B8C4", - marginLeft: "1em", - "&:hover": { - color: "#03848C", - borderColor: "#03848C", - backgroundColor: "#F2F2F2", - }, - }, - secondaryLink: { - color: "#00B8C4", - "&:hover": { - color: "#03848C", - }, - }, - center: { - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - textDecorationNone: { - textDecoration: "none", - }, - floatRight: { float: "right" }, - displayNone: { display: "none" }, - positionRelative: { position: "relative" }, - largeLabel: { - fontSize: "1.3rem", - }, -}); - -class Project extends Component { - toggle_like = (e, id) => { - e.preventDefault(); - if (!this.props.auth.token) { - this.props.history.push("/login"); - } else { - let toggle_like_promise = this.props.api.toggle_like({ - id, - token: this.props.auth.token, - }); - this.props.updateProjects(toggle_like_promise); - } - }; - - toggle_save = (e, id) => { - e.preventDefault(); - if (!this.props.auth.token) { - this.props.history.push("/login"); - } else { - let toggle_save_promise = this.props.api.toggle_save({ - id, - token: this.props.auth.token, - }); - this.props.updateProjects(toggle_save_promise); - } - }; - - render() { - let { project, classes } = this.props; - return ( - - - - {project.video ? ( - - ) : project.images.length > 0 ? ( - {project.title} - ) : null} - - - - this.toggle_save(e, id)} - > - {project.saved_by.includes(this.props.auth.id) ? ( - - ) : ( - - )} - - this.toggle_like(e, id)} - > - {project.likes.includes(this.props.auth.id) ? ( - - ) : ( - - )} - {nFormatter(project.likes.length)} - - - {project.title} - - - {project.description} - - - - - - {project.creator.username} - - - - - - - {project.views_count} - - - {project.comments_count} - - - - {dFormatter(project.created_on)} - - - - - - - ); - } -} - -Project.propTypes = { - classes: PropTypes.object.isRequired, -}; - -export default withStyles(styles)(Project); diff --git a/zubhub_frontend/zubhub/src/components/pages/projects/projects_components/ProjectDetails.jsx b/zubhub_frontend/zubhub/src/components/pages/projects/projects_components/ProjectDetails.jsx deleted file mode 100644 index 2e1858823..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/projects/projects_components/ProjectDetails.jsx +++ /dev/null @@ -1,942 +0,0 @@ -import React, { Component } from "react"; -import { Link } from "react-router-dom"; -import { toast } from "react-toastify"; -import { connect } from "react-redux"; -import "react-toastify/dist/ReactToastify.css"; -import ErrorPage from "../../infos/ErrorPage"; -import LoadingPage from "../../infos/LoadingPage"; -import Slider from "react-slick"; -import "slick-carousel/slick/slick.css"; -import "slick-carousel/slick/slick-theme.css"; -import clsx from "clsx"; -import PropTypes from "prop-types"; -import { withStyles } from "@material-ui/core/styles"; -import Avatar from "@material-ui/core/Avatar"; -import Box from "@material-ui/core/Box"; -import BookmarkIcon from "@material-ui/icons/Bookmark"; -import BookmarkBorderIcon from "@material-ui/icons/BookmarkBorder"; -import ClapIcon, { ClapBorderIcon } from "../../../../assets/js/icons/ClapIcon"; -import CommentIcon from "../../../../assets/js/icons/CommentIcon"; -import VisibilityIcon from "@material-ui/icons/Visibility"; -import nFormatter from "../../../../assets/js/nFormatter"; -import dFormatter from "../../../../assets/js/dFormatter"; -import Typography from "@material-ui/core/Typography"; -import "react-toastify/dist/ReactToastify.css"; -import "react-toastify/dist/ReactToastify.css"; -import "react-toastify/dist/ReactToastify.css"; -import Grid from "@material-ui/core/Grid"; -import Container from "@material-ui/core/Container"; -import Paper from "@material-ui/core/Paper"; -import Dialog from "@material-ui/core/Dialog"; -import Button from "@material-ui/core/Button"; - -const styles = (theme) => ({ - root: { - flex: "1 0 auto", - }, - projectDetailHeaderStyle: { - paddingTop: "1.5em", - boxShadow: - "0px 2px 1px -1px rgba(0,0,0,0), 0px 1px 1px 0px rgba(0,0,0,0), 0px 1px 3px 0px rgba(0,0,0,0)", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - }, - titleStyle: { - fontWeight: 900, - textAlign: "center", - }, - metaInfoStyle: { - display: "flex", - justifyContent: "space-between", - alignItems: "center", - }, - creatorProfileStyle: { - display: "flex", - alignItems: "center", - marginBottom: "1em", - "& a": { - display: "flex", - alignItems: "center", - }, - [theme.breakpoints.down("500")]: { - width: "100%", - justifyContent: "space-between", - }, - }, - creatorAvatarStyle: { - display: "inline-block", - boxShadow: `0 3px 5px 2px rgba(0, 0, 0, .12)`, - backgroundColor: "#ffffff", - marginRight: "0.5em", - }, - headerStyle: { - maxWidth: "1000px", - }, - detailStyle: { - maxWidth: "1000px", - }, - videoWrapperStyle: { - backgroundColor: "black", - marginBottom: "1em", - height: "100%", - paddingBottom: "56.25%", - [theme.breakpoints.down("1080")]: { - height: 0, - }, - [theme.breakpoints.down("959")]: { - paddingBottom: "56.25%", - }, - }, - iframeStyle: { - position: "absolute", - borderStyle: "none", - top: 0, - left: 0, - width: "100%", - height: "100%", - [theme.breakpoints.down("959")]: { - width: "100%", - height: "100%", - }, - zIndex: 1, - }, - actionBoxStyle: { - backgroundColor: "#00B8C4", - "&:hover": { - backgroundColor: "#03848C", - }, - borderRadius: "15px", - position: "absolute", - top: "0", - right: "-4.5em", - width: "3.5em", - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - boxShadow: - "0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)", - [theme.breakpoints.down("1080")]: { - position: "static", - height: "3.5em", - width: "100%", - flexDirection: "row", - justifyContent: "flex-start", - }, - }, - actionBoxButtonStyle: { - margin: "0.5em", - display: "flex", - maxWidth: "100%", - minWidth: 0, - flexDirection: "column", - "& span": { display: "flex", flexDirection: "column" }, - [theme.breakpoints.down("1080")]: { - flexDirection: "row", - marginBottom: "1em", - "& span": { - flexDirection: "row", - }, - }, - textAlign: "center", - color: "white", - "& MuiFab-root:hover": { - color: "#F2F2F2", - }, - "& svg": { - fill: "white", - }, - "& svg:hover": { - fill: "#F2F2F2", - }, - }, - sliderBoxStyle: { - [theme.breakpoints.down("1080")]: { - width: "90%", - }, - }, - carouselImageStyle: { - borderRadius: "15px", - objectFit: "cover", - width: "180px", - height: "200px", - }, - enlargedImageDialogStyle: { - display: "flex", - justifyContent: "center", - }, - enlargedImageStyle: { - alignSelf: "center", - width: "80%", - maxWidth: "600px", - height: "auto", - }, - descriptionHeadingStyle: { - marginTop: "1em", - fontWeight: 900, - fontSize: "2.2rem", - }, - descriptionBodyStyle: { - fontSize: "1.5rem", - marginBottom: "0.7em", - }, - materialsUsedStyle: { - display: "inline-block", - fontSize: "1.5rem", - padding: "0.2em 0.5em", - color: "#00B8C4", - borderRadius: "15px", - border: "1px solid #00B8C4", - marginRight: "0.5em", - marginBottom: "0.5em", - }, - commentSectionStyle: { - maxWidth: "1000px", - display: "flex", - flexDirection: "column", - alignItems: "center", - marginTop: "2.5em", - marginBottom: "2.5em", - borderRadius: "15px", - backgroundColor: "#E4E4E4", - }, - commentAvatarStyle: { - backgroundColor: "#c4c4c4", - marginRight: "1em", - }, - commentsStyle: { - display: "flex", - flexDirection: "column", - padding: "1em", - backgroundColor: "white", - maxWidth: "1000px", - width: "100%", - fontSize: "1.5rem", - borderRadius: "15px", - boxShadow: "0 1px 4px rgba(0,0,0,.06)", - border: "1px solid rgba(0,0,0,.1)", - margin: "0.8em", - }, - commentMetaStyle: { - display: "flex", - marginBottom: "1em", - }, - customLabelStyle: { - "&.MuiFormLabel-root.Mui-focused": { - color: "#00B8C4", - }, - }, - - primaryButton: { - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - marginLeft: "1em", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - secondaryButton: { - backgroundColor: "white", - borderRadius: 15, - borderColor: "#00B8C4", - color: "#00B8C4", - marginLeft: "1em", - "&:hover": { - color: "#03848C", - borderColor: "#03848C", - backgroundColor: "#F2F2F2", - }, - }, - secondaryLink: { - color: "#00B8C4", - "&:hover": { - color: "#03848C", - }, - }, - - materialsUsedViewStyle: { - padding: "0.5em", - }, - center: { - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - textDecorationNone: { - textDecoration: "none", - }, - floatRight: { float: "right" }, - displayNone: { display: "none" }, - largeLabel: { - fontSize: "1.3rem", - }, - positionRelative: { position: "relative" }, - positionAbsolute: { position: "absolute" }, - marginBottom1em: { marginBottom: "1em" }, - errorBox: { - width: "100%", - padding: "1em", - borderRadius: 6, - borderWidth: "1px", - borderColor: "#a94442", - backgroundColor: "#ffcdd2", - }, - error: { - color: "#a94442", - }, -}); - -const sliderSettings = (images_num) => ({ - className: "center slider", - centerMode: true, - infinite: true, - centerPadding: "60px", - dots: false, - autoplay: true, - speed: 500, - slidesToShow: 4 > images_num ? images_num : 4, - slidesToScroll: 4 > images_num ? images_num : 4, - focusOnSelect: true, - swipeToSlide: true, - nextArrow: , - prevArrow: , - responsive: [ - { - breakpoint: 980, - settings: { - slidesToShow: 3 > images_num ? images_num : 3, - slidesToScroll: 3 > images_num ? images_num : 3, - infinite: true, - }, - }, - { - breakpoint: 770, - settings: { - slidesToShow: 2 > images_num ? images_num : 2, - slidesToScroll: 2 > images_num ? images_num : 2, - infinite: true, - }, - }, - { - breakpoint: 550, - settings: { - slidesToShow: 1 > images_num ? images_num : 1, - slidesToScroll: 1 > images_num ? images_num : 1, - infinite: true, - }, - }, - ], -}); - -function NextArrow(props) { - const { className, style, onClick } = props; - return ( -
- ); -} - -function PrevArrow(props) { - const { className, style, onClick } = props; - return ( -
- ); -} - -class ProjectDetails extends Component { - constructor(props) { - super(props); - this.state = { - project: {}, - loading: true, - }; - } - - componentDidMount() { - this.props.api - .get_project({ - id: this.props.match.params.id, - token: this.props.auth.token, - }) - .then((res) => { - if (res.title) { - return this.setState({ project: res, loading: false }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - toast.warning(error.message); - }); - } - - componentDidUpdate(prevProps, prevState) { - if (this.state.project !== prevState.project) { - this.constructCommentBox(); - } - } - - componentWillUnmount() { - try { - this.commentText.removeEventListener( - "focus", - this.handleCommentTextFocus - ); - } catch {} - - try { - document.removeEventListener("click", this.handleDocumentClick); - } catch {} - } - - toggle_like = (e, id) => { - e.preventDefault(); - if (!this.props.auth.token) { - this.props.history.push("/login"); - } else { - this.props.api - .toggle_like({ id, token: this.props.auth.token }) - .then((res) => { - if (res.id) { - return this.setState({ project: res }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - if (error.message.startsWith("Unexpected")) { - toast.warning( - "An error occured while performing this action. Please try again later" - ); - } else { - toast.warning(error.message); - } - }); - } - }; - - toggle_follow = (id) => { - if (!this.props.auth.token) { - this.props.history.push("/login"); - } else { - this.props.api - .toggle_follow({ id, token: this.props.auth.token }) - .then((res) => { - if (res.id) { - let { project } = this.state; - if (project.creator.id === res.id) { - project.creator = res; - } - - return this.setState({ project }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - if (error.message.startsWith("Unexpected")) { - toast.warning( - "An error occured while performing this action. Please try again later" - ); - } else { - toast.warning(error.message); - } - }); - } - }; - - toggle_save = (e, id) => { - e.preventDefault(); - if (!this.props.auth.token) { - this.props.history.push("/login"); - } else { - this.props.api - .toggle_save({ - id, - token: this.props.auth.token, - }) - .then((res) => { - if (res.id) { - return this.setState({ project: res }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - if (error.message.startsWith("Unexpected")) { - toast.warning( - "An error occured while performing this action. Please try again later" - ); - } else { - toast.warning(error.message); - } - }); - } - }; - - handleOpenEnlargedImageDialog = (e) => { - let image_url = e.currentTarget.getAttribute("src"); - let openEnlargedImageDialog = !this.state.openEnlargedImageDialog; - this.setState({ enlargedImageUrl: image_url, openEnlargedImageDialog }); - }; - - buildMaterialsUsedComponent = () => { - let arr = this.state.project.materials_used.split(","); - return arr.map((material, index) => ( - - {material} - - )); - }; - - add_comment = (e) => { - e.preventDefault(); - if (!this.props.auth.token) { - this.props.history.push("/login"); - } else { - let textarea = document.querySelector("#comment"); - let comment_text = textarea.value; - this.props.api - .add_comment({ - id: this.state.project.id, - token: this.props.auth.token, - text: comment_text, - }) - .then((res) => { - if (res.id) { - textarea.value = ""; - return this.setState({ project: res }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - if (error.message.startsWith("Unexpected")) { - toast.warning( - "An error occured while performing this action. Please try again later" - ); - } else { - toast.warning(error.message); - } - }); - } - }; - - constructCommentBox = () => { - this.commentBox = document.querySelector(".comment-box"); - this.commentText = document.querySelector(".comment-text"); - this.commentAuthorName = document.querySelector( - ".comment-box .comment-meta__a" - ); - this.commentPublishButton = document.querySelector( - ".comment-publish-button" - ); - this.commentText.addEventListener("focus", this.handleCommentTextFocus); - - document.addEventListener("click", this.handleDocumentClick); - }; - - handleCommentTextFocus = () => { - this.commentBox.classList.remove("comment-collapsed"); - this.commentBox.classList.add("comment"); - this.commentAuthorName.classList.remove("display-none"); - this.commentPublishButton.classList.remove("display-none"); - }; - - handleDocumentClick = (e) => { - if ( - ![this.commentBox, this.commentPublishButton, this.commentText].includes( - e.target - ) - ) { - this.commentBox.classList.remove("comment"); - this.commentBox.classList.add("comment-collapsed"); - this.commentAuthorName.classList.add("display-none"); - this.commentPublishButton.classList.add("display-none"); - } - }; - - render() { - let { - project, - loading, - enlargedImageUrl, - openEnlargedImageDialog, - } = this.state; - let { classes } = this.props; - if (loading) { - return ; - } else if (Object.keys(project).length > 0) { - return ( - <> - - - - - {project.title} - - - - - - - {project.creator.username} - - - {project.creator.id !== this.props.auth.id ? ( - - ) : null} - - - - - - - - {project.video ? ( - - ) : project.images.length > 0 ? ( - {project.title} - ) : null} - - - - - - - {project.views_count} - - - - {project.images.length > 0 ? ( - - - - {project.images.map((image) => ( -
- {image.public_id} -
- ))} -
-
-
- ) : null} - - - Description - - - {project.description} - - - - - Materials used - - - {this.buildMaterialsUsedComponent()} - - -
-
- - - {nFormatter(project.comments.length)} Comments - - - - - {this.props.auth.token ? ( - - ) : null} - - - {this.props.auth.username} - - -
- - -
-
- {project.comments.map((comment) => ( - - - - - - {comment.creator} - - - {dFormatter(comment.created_on)} - - - - - {comment.text} - - ))} -
-
-
- - this.setState({ - openEnlargedImageDialog: !openEnlargedImageDialog, - }) - } - aria-labelledby="enlarged image dialog" - > - {`${project.title}`} - - - ); - } else { - return ( - - ); - } - } -} - -ProjectDetails.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const mapStateToProps = (state) => { - return { - auth: state.auth, - }; -}; - -export default connect(mapStateToProps)(withStyles(styles)(ProjectDetails)); diff --git a/zubhub_frontend/zubhub/src/components/pages/projects/projects_components/SavedProjects.jsx b/zubhub_frontend/zubhub/src/components/pages/projects/projects_components/SavedProjects.jsx deleted file mode 100644 index 9d3c6313c..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/projects/projects_components/SavedProjects.jsx +++ /dev/null @@ -1,246 +0,0 @@ -import React, { Component } from "react"; -import { connect } from "react-redux"; -import { toast } from "react-toastify"; -import ErrorPage from "../../infos/ErrorPage"; -import LoadingPage from "../../infos/LoadingPage"; -import clsx from "clsx"; -import Grid from "@material-ui/core/Grid"; -import Box from "@material-ui/core/Box"; -import Button from "@material-ui/core/Button"; -import ButtonGroup from "@material-ui/core/ButtonGroup"; -import NavigateBeforeIcon from "@material-ui/icons/NavigateBefore"; -import NavigateNextIcon from "@material-ui/icons/NavigateNext"; -import PropTypes from "prop-types"; -import { withStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; - -import Project from "./Project"; -import { Container } from "@material-ui/core"; - -const styles = (theme) => ({ - root: { - paddingBottom: "2em", - flex: "1 0 auto", - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - "& .MuiGrid-root.MuiGrid-container": { - width: "100%", - }, - }, - mainContainerStyle: { - maxWidth: "2000px", - width: "100%", - }, - pageHeaderStyle: { - marginTop: "1em", - fontWeight: 900, - textAlign: "center", - }, - projectGridStyle: { - marginBottom: "2em", - }, - buttonGroupStyle: { - paddingLeft: "2em", - paddingRight: "2em", - display: "block", - marginTop: "2em", - maxWidth: "2000px", - width: "100%", - }, - primaryButtonStyle: { - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - floatRight: { - float: "right", - }, - floatLeft: { - float: "left", - }, -}); - -class SavedProjects extends Component { - constructor(props) { - super(props); - this.state = { - projects: [], - prevPage: null, - nextPage: null, - loading: true, - }; - } - - componentDidMount() { - this.fetchPage(); - } - - fetchPage = (page) => { - if (!this.props.auth.token) { - this.props.history.push("/login"); - } else { - this.props.api - .get_saved({ page, token: this.props.auth.token }) - .then((res) => { - if (Array.isArray(res.results)) { - return this.setState({ - projects: res.results, - prevPage: res.previous, - nextPage: res.next, - loading: false, - }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - if (error.message.startsWith("Unexpected")) { - toast.warning( - "An error occured while performing this action. Please try again later" - ); - } else { - toast.warning(error.message); - } - }); - } - }; - - updateProjects = (res) => { - res - .then((res) => { - if (res.id) { - let { projects } = this.state; - projects = projects.map((project) => - project.id === res.id ? res : project - ); - return this.setState({ projects }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - this.setState({ loading: false }); - toast.warning(error.message); - }); - }; - - render() { - let { projects, prevPage, nextPage, loading } = this.state; - let { classes } = this.props; - if (loading) { - return ; - } else if (projects.length > 0) { - return ( - - - - - - Your saved projects - - - {projects.map((project) => ( - - - - ))} - - - {prevPage ? ( - - ) : null} - {nextPage ? ( - - ) : null} - - - - ); - } else { - return ; - } - } -} - -SavedProjects.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const mapStateToProps = (state) => { - return { - auth: state.auth, - }; -}; - -export default connect(mapStateToProps)(withStyles(styles)(SavedProjects)); diff --git a/zubhub_frontend/zubhub/src/components/pages/user_auth/EmailConfirm.jsx b/zubhub_frontend/zubhub/src/components/pages/user_auth/EmailConfirm.jsx deleted file mode 100644 index 8d2a48ecb..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/user_auth/EmailConfirm.jsx +++ /dev/null @@ -1,249 +0,0 @@ -import React, { Component } from "react"; -import { toast } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; -import PropTypes from "prop-types"; -import { withStyles, fade } from "@material-ui/core/styles"; -import Grid from "@material-ui/core/Grid"; -import Box from "@material-ui/core/Box"; -import Container from "@material-ui/core/Container"; -import Card from "@material-ui/core/Card"; -import CardActionArea from "@material-ui/core/CardActionArea"; -import CardContent from "@material-ui/core/CardContent"; -import Button from "@material-ui/core/Button"; -import Typography from "@material-ui/core/Typography"; -import robots from "../../../assets/images/robots.png"; - -const styles = { - root: { - paddingTop: "2em", - paddingBottom: "2em", - flex: "1 0 auto", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - }, - background: { - position: "absolute", - backgroundImage: `url(${robots})`, - filter: "blur(5px)", - webkitFilter: "blur(8px)", - top: -100, - height: "100%", - width: "100%", - backgroundPosition: "center", - backgroundRepeat: "no-repeat", - backgroundSize: "cover", - zIndex: -1, - }, - cardStyle: { - border: 0, - borderRadius: 15, - boxShadow: "0 3px 5px 2px rgba(0, 0, 0, .12)", - color: "white", - padding: "0 30px", - }, - titleStyle: { - fontWeight: 900, - }, - customLabelStyle: { - "&.MuiFormLabel-root.Mui-focused": { - color: "#00B8C4", - }, - }, - customInputStyle: { - borderRadius: 15, - "&.MuiOutlinedInput-notchedOutline": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.MuiOutlinedInput-root": { - "&:hover fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.Mui-focused fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - }, - }, - primaryButton: { - width: "100%", - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - secondaryButton: { - width: "100%", - borderRadius: 15, - borderColor: "#00B8C4", - color: "#00B8C4", - "&:hover": { - color: "#03848C", - borderColor: "#03848C", - }, - }, - secondaryLink: { - color: "#00B8C4", - "&:hover": { - color: "#03848C", - }, - }, - center: { - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - divider: { - width: "30%", - marginRight: "1em", - marginLeft: "1em", - }, - textDecorationNone: { - textDecoration: "none", - }, - errorBox: { - width: "100%", - padding: "1em", - borderRadius: 6, - borderWidth: "1px", - borderColor: "#a94442", - backgroundColor: "#ffcdd2", - }, - error: { - color: "#a94442", - }, -}; - -class EmailConfirm extends Component { - constructor(props) { - super(props); - this.state = { - error: null, - username: null, - key: null, - }; - } - - componentDidMount() { - let { username, key } = this.getUsernameAndKey(this.props.location.search); - this.setState({ username, key }); - } - - getUsernameAndKey = (queryString) => { - let username = queryString.split("&&"); - let key = username[1].split("=")[1]; - username = username[0].split("=")[1]; - return { username, key }; - }; - - confirmEmail = (e) => { - e.preventDefault(); - this.props.api - .send_email_confirmation(this.state.key) - .then((res) => { - toast.success("Congratulations!, your email has been confirmed!"); - setTimeout(() => { - this.props.history.push("/"); - }, 4000); - }) - .catch((error) => { - if (error.message.startsWith("Unexpected")) { - this.setState({ - error: - "An error occured while performing this action. Please try again later", - }); - } else { - this.setState({ error: error.message }); - } - }); - }; - - render() { - let { error, username } = this.state; - let { classes } = this.props; - - return ( - - - - - -
- - Email Confirmation - - - Please Confirm that you are {username} and that the email - belongs to you: - - - - - - {error !== null && ( - - {error} - - )} - - - - - - -
-
-
-
-
-
- ); - } -} - -EmailConfirm.propTypes = { - classes: PropTypes.object.isRequired, -}; - -export default withStyles(styles)(EmailConfirm); diff --git a/zubhub_frontend/zubhub/src/components/pages/user_auth/Login.jsx b/zubhub_frontend/zubhub/src/components/pages/user_auth/Login.jsx deleted file mode 100644 index 7f4230812..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/user_auth/Login.jsx +++ /dev/null @@ -1,421 +0,0 @@ -import React, { Component } from "react"; -import clsx from "clsx"; -import { Link } from "react-router-dom"; -import PropTypes from "prop-types"; -import "react-toastify/dist/ReactToastify.css"; -import { withStyles, fade } from "@material-ui/core/styles"; -import Grid from "@material-ui/core/Grid"; -import Box from "@material-ui/core/Box"; -import Divider from "@material-ui/core/Divider"; -import Container from "@material-ui/core/Container"; -import Card from "@material-ui/core/Card"; -import CardActionArea from "@material-ui/core/CardActionArea"; -import CardContent from "@material-ui/core/CardContent"; -import Button from "@material-ui/core/Button"; -import Typography from "@material-ui/core/Typography"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import IconButton from "@material-ui/core/IconButton"; -import OutlinedInput from "@material-ui/core/OutlinedInput"; -import InputLabel from "@material-ui/core/InputLabel"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import FormControl from "@material-ui/core/FormControl"; -import Visibility from "@material-ui/icons/Visibility"; -import VisibilityOff from "@material-ui/icons/VisibilityOff"; - -import { withFormik } from "formik"; -import * as Yup from "yup"; -import { connect } from "react-redux"; -import * as AuthActions from "../../../store/actions/authActions"; -import robots from "../../../assets/images/robots.png"; - -const styles = (theme) => ({ - root: { - paddingTop: "2em", - paddingBottom: "2em", - flex: "1 0 auto", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - }, - background: { - position: "absolute", - backgroundImage: `url(${robots})`, - filter: "blur(5px)", - webkitFilter: "blur(8px)", - top: -100, - height: "100%", - width: "100%", - backgroundPosition: "center", - backgroundRepeat: "no-repeat", - backgroundSize: "cover", - zIndex: -1, - }, - cardStyle: { - border: 0, - borderRadius: 15, - boxShadow: "0 3px 5px 2px rgba(0, 0, 0, .12)", - color: "white", - padding: "0 30px", - }, - titleStyle: { - fontWeight: 900, - }, - customLabelStyle: { - "&.MuiFormLabel-root.Mui-focused": { - color: "#00B8C4", - }, - }, - customInputStyle: { - borderRadius: 15, - "&.MuiOutlinedInput-notchedOutline": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.MuiOutlinedInput-root": { - "&:hover fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.Mui-focused fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - }, - }, - primaryButton: { - width: "100%", - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - secondaryButton: { - width: "100%", - borderRadius: 15, - borderColor: "#00B8C4", - color: "#00B8C4", - "&:hover": { - color: "#03848C", - borderColor: "#03848C", - }, - }, - secondaryLink: { - color: "#00B8C4", - "&:hover": { - color: "#03848C", - }, - }, - center: { - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - divider: { - width: "30%", - marginRight: "1em", - marginLeft: "1em", - [theme.breakpoints.down("510")]: { - width: "20%", - }, - [theme.breakpoints.down("381")]: { - marginLeft: "0.5em", - marginRight: "0.5em", - }, - }, - textDecorationNone: { - textDecoration: "none", - }, - errorBox: { - width: "100%", - padding: "1em", - borderRadius: 6, - borderWidth: "1px", - borderColor: "#a94442", - backgroundColor: "#ffcdd2", - }, - error: { - color: "#a94442", - }, -}); - -class Login extends Component { - constructor(props) { - super(props); - this.state = { - error: null, - showPassword: false, - }; - } - - login = (e) => { - this.props.api - .login(this.props.values) - .then((res) => { - if (!res.key) { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - return this.props.set_auth_user({ token: res.key }); - }) - .then((val) => this.props.api.get_auth_user(this.props.auth.token)) - .then((res) => { - this.props.set_auth_user({ - ...this.props.auth, - username: res.username, - id: res.id, - }); - }) - .then((val) => this.props.history.push("/profile")) - .catch((error) => { - if (error.message.startsWith("Unexpected")) { - this.setState({ - error: - "An error occured while performing this action. Please try again later", - }); - } else { - this.setState({ error: error.message }); - } - }); - }; - - handleClickShowPassword = () => { - let { showPassword } = this.state; - this.setState({ showPassword: !showPassword }); - }; - - handleMouseDownPassword = (e) => { - e.preventDefault(); - }; - - render() { - let { error, showPassword } = this.state; - let { classes } = this.props; - - return ( - - - - - -
- - Welcome to Zubhub - - - Login to get started! - - - - - {error !== null && ( - - {error} - - )} - - - - - - Username Or Email - - - - {this.props.touched["username"] && - this.props.errors["username"]} - - - - - - - Password - - - {showPassword ? ( - - ) : ( - - )} - - - } - labelWidth={70} - /> - - {this.props.touched["password"] && - this.props.errors["password"]} - - - - - - - -
- - - - - - Not a Member ? - - - - - - - - - - - - - Forgot Password? - - - - -
-
-
-
-
- ); - } -} - -Login.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const mapStateToProps = (state) => { - return { - auth: state.auth, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - set_auth_user: (auth_user) => { - dispatch(AuthActions.setAuthUser(auth_user)); - }, - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)( - withFormik({ - mapPropsToValue: () => ({ - email: "", - password: "", - }), - validationSchema: Yup.object().shape({ - email: Yup.string().email("invalid email").required("invalid email"), - password: Yup.string() - .min(8, "your password is too short") - .required("input your password"), - }), - handleSubmit: (values, { setSubmitting }) => {}, - })(withStyles(styles)(Login)) -); diff --git a/zubhub_frontend/zubhub/src/components/pages/user_auth/PasswordReset.jsx b/zubhub_frontend/zubhub/src/components/pages/user_auth/PasswordReset.jsx deleted file mode 100644 index 3d929b6aa..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/user_auth/PasswordReset.jsx +++ /dev/null @@ -1,257 +0,0 @@ -import React, { Component } from "react"; -import { toast } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; -import { withFormik } from "formik"; -import * as Yup from "yup"; -import clsx from "clsx"; -import PropTypes from "prop-types"; -import { withStyles, fade } from "@material-ui/core/styles"; -import Grid from "@material-ui/core/Grid"; -import Box from "@material-ui/core/Box"; -import Container from "@material-ui/core/Container"; -import Card from "@material-ui/core/Card"; -import CardActionArea from "@material-ui/core/CardActionArea"; -import CardContent from "@material-ui/core/CardContent"; -import Button from "@material-ui/core/Button"; -import Typography from "@material-ui/core/Typography"; -import OutlinedInput from "@material-ui/core/OutlinedInput"; -import InputLabel from "@material-ui/core/InputLabel"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import FormControl from "@material-ui/core/FormControl"; - -import robots from "../../../assets/images/robots.png"; - -const styles = { - root: { - paddingTop: "2em", - paddingBottom: "2em", - flex: "1 0 auto", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - }, - background: { - position: "absolute", - backgroundImage: `url(${robots})`, - filter: "blur(5px)", - webkitFilter: "blur(8px)", - top: -20, - height: "100%", - width: "100%", - backgroundPosition: "center", - backgroundRepeat: "no-repeat", - backgroundSize: "cover", - zIndex: -1, - }, - cardStyle: { - border: 0, - borderRadius: 15, - boxShadow: "0 3px 5px 2px rgba(0, 0, 0, .12)", - color: "white", - padding: "0 30px", - }, - titleStyle: { - fontWeight: 900, - }, - customLabelStyle: { - "&.MuiFormLabel-root.Mui-focused": { - color: "#00B8C4", - }, - }, - - customInputStyle: { - borderRadius: 15, - "&.MuiOutlinedInput-notchedOutline": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.MuiOutlinedInput-root": { - "&:hover fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.Mui-focused fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - }, - }, - primaryButton: { - width: "100%", - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - errorBox: { - width: "100%", - padding: "1em", - borderRadius: 6, - borderWidth: "1px", - borderColor: "#a94442", - backgroundColor: "#ffcdd2", - }, - error: { - color: "#a94442", - }, -}; - -class PasswordReset extends Component { - constructor(props) { - super(props); - this.state = { - error: null, - }; - } - - sendPasswordResetLink = (e) => { - e.preventDefault(); - this.props.api - .send_password_reset_link(this.props.values.email) - .then((res) => { - toast.success("We just sent a password reset link to your email!"); - setTimeout(() => { - this.props.history.push("/"); - }, 4000); - }) - .catch((error) => { - if (error.message.startsWith("Unexpected")) { - this.setState({ - error: - "An error occured while performing this action. Please try again later", - }); - } else { - this.setState({ error: error.message }); - } - }); - }; - - render() { - let { error } = this.state; - let { classes } = this.props; - - return ( - - - - - -
- - Password Reset - - - Input your email so we can send you a password reset link - - - - - {error !== null && ( - - {error} - - )} - - - - - - Email - - - - {this.props.touched["email"] && - this.props.errors["email"]} - - - - - - - -
-
-
-
-
-
- ); - } -} - -PasswordReset.propTypes = { - classes: PropTypes.object.isRequired, -}; - -export default withFormik({ - mapPropsToValue: () => ({ - email: "", - }), - validationSchema: Yup.object().shape({ - email: Yup.string().email("invalid email").required("email required"), - }), - handleSubmit: (values, { setSubmitting }) => {}, -})(withStyles(styles)(PasswordReset)); diff --git a/zubhub_frontend/zubhub/src/components/pages/user_auth/PasswordResetConfirm.jsx b/zubhub_frontend/zubhub/src/components/pages/user_auth/PasswordResetConfirm.jsx deleted file mode 100644 index 907f53904..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/user_auth/PasswordResetConfirm.jsx +++ /dev/null @@ -1,383 +0,0 @@ -import React, { Component } from "react"; -import { toast } from "react-toastify"; -import "react-toastify/dist/ReactToastify.css"; -import { withFormik } from "formik"; -import * as Yup from "yup"; -import clsx from "clsx"; -import PropTypes from "prop-types"; -import { withStyles, fade } from "@material-ui/core/styles"; -import Grid from "@material-ui/core/Grid"; -import Box from "@material-ui/core/Box"; -import Container from "@material-ui/core/Container"; -import Card from "@material-ui/core/Card"; -import CardActionArea from "@material-ui/core/CardActionArea"; -import CardContent from "@material-ui/core/CardContent"; -import Button from "@material-ui/core/Button"; -import Typography from "@material-ui/core/Typography"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import IconButton from "@material-ui/core/IconButton"; -import OutlinedInput from "@material-ui/core/OutlinedInput"; -import InputLabel from "@material-ui/core/InputLabel"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import FormControl from "@material-ui/core/FormControl"; -import Visibility from "@material-ui/icons/Visibility"; -import VisibilityOff from "@material-ui/icons/VisibilityOff"; - -import robots from "../../../assets/images/robots.png"; - -const styles = { - root: { - paddingTop: "2em", - paddingBottom: "2em", - flex: "1 0 auto", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - }, - background: { - position: "absolute", - backgroundImage: `url(${robots})`, - filter: "blur(5px)", - webkitFilter: "blur(8px)", - top: -20, - height: "100%", - width: "100%", - backgroundPosition: "center", - backgroundRepeat: "no-repeat", - backgroundSize: "cover", - zIndex: -1, - }, - cardStyle: { - border: 0, - borderRadius: 15, - boxShadow: "0 3px 5px 2px rgba(0, 0, 0, .12)", - color: "white", - padding: "0 30px", - }, - titleStyle: { - fontWeight: 900, - }, - customLabelStyle: { - "&.MuiFormLabel-root.Mui-focused": { - color: "#00B8C4", - }, - }, - - customInputStyle: { - borderRadius: 15, - "&.MuiOutlinedInput-notchedOutline": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.MuiOutlinedInput-root": { - "&:hover fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.Mui-focused fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - }, - }, - primaryButton: { - width: "100%", - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - secondaryButton: { - width: "100%", - borderRadius: 15, - borderColor: "#00B8C4", - color: "#00B8C4", - "&:hover": { - color: "#03848C", - borderColor: "#03848C", - }, - }, - secondaryLink: { - color: "#00B8C4", - "&:hover": { - color: "#03848C", - }, - }, - center: { - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - divider: { - width: "30%", - marginRight: "1em", - marginLeft: "1em", - }, - textDecorationNone: { - textDecoration: "none", - }, - errorBox: { - width: "100%", - padding: "1em", - borderRadius: 6, - borderWidth: "1px", - borderColor: "#a94442", - backgroundColor: "#ffcdd2", - }, - error: { - color: "#a94442", - }, -}; - -class PasswordResetConfirm extends Component { - constructor(props) { - super(props); - this.state = { - error: null, - showPassword1: false, - showPassword2: false, - }; - } - - getUidAndToken = (queryString) => { - let uid = queryString.split("&&"); - let token = uid[1].split("=")[1]; - uid = uid[0].split("=")[1]; - return { uid, token }; - }; - - resetPassword = (e) => { - e.preventDefault(); - let { uid, token } = this.getUidAndToken(this.props.location.search); - this.props.api - .password_reset_confirm({ ...this.props.values, uid, token }) - .then((res) => { - toast.success( - "Congratulations! your password reset was successful! you will now be redirected to login" - ); - setTimeout(() => { - this.props.history.push("/login"); - }, 4000); - }) - .catch((error) => { - if (error.message.startsWith("Unexpected")) { - this.setState({ - error: - "An error occured while performing this action. Please try again later", - }); - } else { - this.setState({ error: error.message }); - } - }); - }; - - handleClickShowPassword = (field) => { - if (field === 1) { - let { showPassword1 } = this.state; - this.setState({ showPassword1: !showPassword1 }); - } else if (field === 2) { - let { showPassword2 } = this.state; - this.setState({ showPassword2: !showPassword2 }); - } - }; - - handleMouseDownPassword = (e) => { - e.preventDefault(); - }; - - render() { - let { error, showPassword1, showPassword2 } = this.state; - let { classes } = this.props; - - return ( - - - - - -
- - Password Reset Confirmation - - - - - {error !== null && ( - - {error} - - )} - - - - - - - New Password - - - - this.handleClickShowPassword(field) - } - onMouseDown={this.handleMouseDownPassword} - edge="end" - > - {showPassword1 ? ( - - ) : ( - - )} - - - } - labelWidth={70} - /> - - {this.props.touched["new_password"] && - this.props.errors["new_password"]} - - - - - - - - Confirm Password - - - - this.handleClickShowPassword(field) - } - onMouseDown={this.handleMouseDownPassword} - edge="end" - > - {showPassword2 ? ( - - ) : ( - - )} - - - } - labelWidth={150} - /> - - {this.props.touched["new_password2"] && - this.props.errors["new_password2"]} - - - - - - - -
-
-
-
-
-
- ); - } -} - -PasswordResetConfirm.propTypes = { - classes: PropTypes.object.isRequired, -}; - -export default withFormik({ - mapPropsToValue: () => ({ - new_password1: "", - new_password2: "", - }), - validationSchema: Yup.object().shape({ - new_password1: Yup.string() - .min(8, "your password is too short") - .required("input your password"), - new_password2: Yup.string() - .oneOf([Yup.ref("new_password1"), null], "Passwords must match") - .required("input a confirmation password"), - }), - handleSubmit: (values, { setSubmitting }) => {}, -})(withStyles(styles)(PasswordResetConfirm)); diff --git a/zubhub_frontend/zubhub/src/components/pages/user_auth/Signup.jsx b/zubhub_frontend/zubhub/src/components/pages/user_auth/Signup.jsx deleted file mode 100644 index 62f029f8e..000000000 --- a/zubhub_frontend/zubhub/src/components/pages/user_auth/Signup.jsx +++ /dev/null @@ -1,674 +0,0 @@ -import React, { Component } from "react"; -import { withFormik } from "formik"; -import { Link } from "react-router-dom"; -import "react-toastify/dist/ReactToastify.css"; -import clsx from "clsx"; -import PropTypes from "prop-types"; -import { withStyles, fade } from "@material-ui/core/styles"; -import Grid from "@material-ui/core/Grid"; -import Box from "@material-ui/core/Box"; -import Divider from "@material-ui/core/Divider"; -import Container from "@material-ui/core/Container"; -import Card from "@material-ui/core/Card"; -import CardActionArea from "@material-ui/core/CardActionArea"; -import CardContent from "@material-ui/core/CardContent"; -import Select from "@material-ui/core/Select"; -import MenuItem from "@material-ui/core/MenuItem"; -import Button from "@material-ui/core/Button"; -import Typography from "@material-ui/core/Typography"; -import InputAdornment from "@material-ui/core/InputAdornment"; -import IconButton from "@material-ui/core/IconButton"; -import OutlinedInput from "@material-ui/core/OutlinedInput"; -import Tooltip from "@material-ui/core/Tooltip"; -import ClickAwayListener from "@material-ui/core/ClickAwayListener"; -import InputLabel from "@material-ui/core/InputLabel"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import FormControl from "@material-ui/core/FormControl"; -import Visibility from "@material-ui/icons/Visibility"; -import VisibilityOff from "@material-ui/icons/VisibilityOff"; -import * as Yup from "yup"; -import { connect } from "react-redux"; -import * as AuthActions from "../../../store/actions/authActions"; -import robots from "../../../assets/images/robots.png"; - -const styles = (theme) => ({ - root: { - paddingTop: "2em", - paddingBottom: "2em", - flex: "1 0 auto", - background: "rgba(255,204,0,1)", - background: - "-moz-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,204,0,1)), color-stop(25%, rgba(255,229,133,1)), color-stop(61%, rgba(255,255,255,1)), color-stop(100%, rgba(255,255,255,1)))", - background: - "-webkit-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-o-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "-ms-linear-gradient(top, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - background: - "linear-gradient(to bottom, rgba(255,204,0,1) 0%, rgba(255,229,133,1) 25%, rgba(255,255,255,1) 61%, rgba(255,255,255,1) 100%)", - filter: - "progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffcc00', endColorstr='#ffffff', GradientType=0 )", - }, - background: { - position: "absolute", - backgroundImage: `url(${robots})`, - filter: "blur(5px)", - webkitFilter: "blur(8px)", - top: -20, - height: "100%", - width: "100%", - backgroundPosition: "center", - backgroundRepeat: "no-repeat", - backgroundSize: "cover", - zIndex: -1, - }, - cardStyle: { - border: 0, - borderRadius: 15, - boxShadow: "0 3px 5px 2px rgba(0, 0, 0, .12)", - color: "white", - padding: "0 30px", - }, - titleStyle: { - fontWeight: 900, - }, - customLabelStyle: { - "&.MuiFormLabel-root.Mui-focused": { - color: "#00B8C4", - }, - }, - - customInputStyle: { - borderRadius: 15, - "&.MuiOutlinedInput-notchedOutline": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.MuiOutlinedInput-root": { - "&:hover fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - "&.Mui-focused fieldset": { - border: "1px solid #00B8C4", - boxShadow: `${fade("#00B8C4", 0.25)} 0 0 0 0.2rem`, - }, - }, - }, - primaryButton: { - width: "100%", - backgroundColor: "#00B8C4", - borderRadius: 15, - color: "white", - "&:hover": { - backgroundColor: "#03848C", - }, - }, - secondaryButton: { - width: "100%", - borderRadius: 15, - borderColor: "#00B8C4", - color: "#00B8C4", - "&:hover": { - color: "#03848C", - borderColor: "#03848C", - }, - }, - secondaryLink: { - color: "#00B8C4", - "&:hover": { - color: "#03848C", - }, - }, - center: { - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - divider: { - width: "30%", - marginRight: "1em", - marginLeft: "1em", - [theme.breakpoints.down("573")]: { - width: "20%", - }, - [theme.breakpoints.down("423")]: { - marginLeft: "0.5em", - marginRight: "0.5em", - }, - [theme.breakpoints.down("378")]: { - width: "10%", - }, - }, - textDecorationNone: { - textDecoration: "none", - }, - errorBox: { - width: "100%", - padding: "1em", - borderRadius: 6, - borderWidth: "1px", - borderColor: "#a94442", - backgroundColor: "#ffcdd2", - }, - error: { - color: "#a94442", - }, -}); - -class Signup extends Component { - constructor(props) { - super(props); - this.state = { - locations: [], - error: null, - showPassword1: false, - showPassword2: false, - toolTipOpen: false, - }; - } - - componentDidMount() { - this.props.api - .get_locations() - .then((res) => { - if (Array.isArray(res) && res.length > 0 && res[0].name) { - return this.setState({ locations: res }); - } else { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - }) - .catch((error) => { - if (error.message.startsWith("Unexpected")) { - this.setState({ - error: - "An error occured while performing this action. Please try again later", - }); - } else { - this.setState({ error: error.message }); - } - }); - } - - signup = (e) => { - if (this.props.values.location.length < 1) { - this.props.validateField("location"); - } else { - this.props.api - .signup(this.props.values) - .then((res) => { - if (!res.key) { - res = Object.keys(res) - .map((key) => res[key]) - .join("\n"); - throw new Error(res); - } - return this.props.set_auth_user({ token: res.key }); - }) - .then((val) => this.props.api.get_auth_user(this.props.auth.token)) - .then((res) => - this.props.set_auth_user({ - ...this.props.auth, - username: res.username, - id: res.id, - }) - ) - .then((val) => this.props.history.push("/profile")) - .catch((error) => { - if (error.message.startsWith("Unexpected")) { - this.setState({ - error: - "An error occured while performing this action. Please try again later", - }); - } else { - this.setState({ error: error.message }); - } - }); - } - }; - - handleClickShowPassword = (field) => { - if (field === 1) { - let { showPassword1 } = this.state; - this.setState({ showPassword1: !showPassword1 }); - } else if (field === 2) { - let { showPassword2 } = this.state; - this.setState({ showPassword2: !showPassword2 }); - } - }; - - handleMouseDownPassword = (e) => { - e.preventDefault(); - }; - - handleTooltipClose = () => { - this.setState({ toolTipOpen: false }); - }; - - handleTooltipOpen = () => { - this.setState({ toolTipOpen: true }); - }; - - render() { - let { - error, - locations, - showPassword1, - showPassword2, - toolTipOpen, - } = this.state; - let { classes } = this.props; - return ( - - - - - -
- - Welcome to Zubhub - - - Create an account to submit a project - - - - - {error !== null && ( - - {error} - - )} - - - - - - Username - - - - - - - - {this.props.touched["username"] && - this.props.errors["username"]} - - - - - - - - Email - - - - {this.props.touched["email"] && - this.props.errors["email"]} - - - - - - - - Date Of Birth - - - - {this.props.touched["dateOfBirth"] && - this.props.errors["dateOfBirth"]} - - - - - - - - Location - - - - {this.props.touched["user_location"] && - this.props.errors["user_location"]} - - - - - - - - Password - - - - this.handleClickShowPassword(field) - } - onMouseDown={this.handleMouseDownPassword} - edge="end" - > - {showPassword1 ? ( - - ) : ( - - )} - - - } - labelWidth={70} - /> - - {this.props.touched["password1"] && - this.props.errors["password1"]} - - - - - - - - Confirm Password - - - - this.handleClickShowPassword(field) - } - onMouseDown={this.handleMouseDownPassword} - edge="end" - > - {showPassword2 ? ( - - ) : ( - - )} - - - } - labelWidth={70} - /> - - {this.props.touched["password2"] && - this.props.errors["password2"]} - - - - - - - -
- - - - - - Already a Member ? - - - - - - - - - - -
-
-
-
-
- ); - } -} - -Signup.propTypes = { - classes: PropTypes.object.isRequired, -}; - -const mapStateToProps = (state) => { - return { - auth: state.auth, - }; -}; - -const mapDispatchToProps = (dispatch) => { - return { - set_auth_user: (auth_user) => { - dispatch(AuthActions.setAuthUser(auth_user)); - }, - }; -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)( - withFormik({ - mapPropsToValue: () => ({ - email: "", - user_location: "", - password1: "", - password2: "", - }), - validationSchema: Yup.object().shape({ - email: Yup.string().email("invalid email").required("invalid email"), - dateOfBirth: Yup.date() - .max(new Date(), "your date of birth can't be greater than today") - .required("please input your date of birth"), - user_location: Yup.string() - .min(1, "your location is too short") - .required("please input your location"), - password1: Yup.string() - .min(8, "your password is too short") - .required("input your password"), - password2: Yup.string() - .oneOf([Yup.ref("password1"), null], "Passwords must match") - .required("input a confirmation password"), - }), - handleSubmit: (values, { setSubmitting }) => {}, - })(withStyles(styles)(Signup)) -); diff --git a/zubhub_frontend/zubhub/src/components/project/Project.jsx b/zubhub_frontend/zubhub/src/components/project/Project.jsx new file mode 100644 index 000000000..edb70853f --- /dev/null +++ b/zubhub_frontend/zubhub/src/components/project/Project.jsx @@ -0,0 +1,186 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import 'react-toastify/dist/ReactToastify.css'; +import clsx from 'clsx'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import BookmarkIcon from '@material-ui/icons/Bookmark'; +import BookmarkBorderIcon from '@material-ui/icons/BookmarkBorder'; +import VisibilityIcon from '@material-ui/icons/Visibility'; +import { + Avatar, + Box, + Card, + CardActionArea, + CardMedia, + CardContent, + Fab, + Typography, +} from '@material-ui/core'; + +import ClapIcon, { ClapBorderIcon } from '../../assets/js/icons/ClapIcon'; +import CommentIcon from '../../assets/js/icons/CommentIcon'; +import nFormatter from '../../assets/js/nFormatter'; +import dFormatter from '../../assets/js/dFormatter'; +import styles from '../../assets/js/styles/components/project/projectStyles'; + +const useStyles = makeStyles(styles); + +function Project(props) { + const classes = useStyles(); + + const toggle_like = (e, id) => { + e.preventDefault(); + if (!props.auth.token) { + props.history.push('/login'); + } else { + const toggle_like_promise = props.toggle_like({ + id, + token: props.auth.token, + }); + props.updateProjects(toggle_like_promise); + } + }; + + const toggle_save = (e, id) => { + e.preventDefault(); + if (!props.auth.token) { + props.history.push('/login'); + } else { + const toggle_save_promise = props.toggle_save({ + id, + token: props.auth.token, + }); + props.updateProjects(toggle_save_promise); + } + }; + + const { project } = props; + return ( + + + + {project.video ? ( + + ) : project.images.length > 0 ? ( + {project.title} + ) : null} + + + + toggle_save(e, id)} + > + {project.saved_by.includes(props.auth.id) ? ( + + ) : ( + + )} + + toggle_like(e, id)} + > + {project.likes.includes(props.auth.id) ? ( + + ) : ( + + )} + {nFormatter(project.likes.length)} + + + {project.title} + + + {project.description} + + + + + + {project.creator.username} + + + + + + + {project.views_count} + + + {project.comments_count} + + + + {dFormatter(project.created_on)} + + + + + + + ); +} + +Project.propTypes = { + auth: PropTypes.object.isRequired, + updateProjects: PropTypes.func.isRequired, + toggle_like: PropTypes.func.isRequired, + toggle_save: PropTypes.func.isRequired, + project: PropTypes.object.isRequired, +}; + +export default Project; diff --git a/zubhub_frontend/zubhub/src/index.js b/zubhub_frontend/zubhub/src/index.js index ba3982296..9189c1a4c 100644 --- a/zubhub_frontend/zubhub/src/index.js +++ b/zubhub_frontend/zubhub/src/index.js @@ -1,30 +1,30 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import './index.css'; + +import { PersistGate } from 'redux-persist/integration/react'; +import { Provider } from 'react-redux'; + import { ThemeProvider } from '@material-ui/styles'; -import {theme} from './assets/js/muiTheme'; +import { theme } from './assets/js/muiTheme'; + import App from './App'; -import reportWebVitals from './reportWebVitals'; -import {Provider} from 'react-redux'; +import './assets/css/index.css'; import configureStore from './store/configureStore'; -import {PersistGate} from 'redux-persist/integration/react'; -import API, {APIContext} from './components/api'; +import reportWebVitals from './reportWebVitals'; -let {store, persistor} = configureStore(); +let { store, persistor } = configureStore(); ReactDOM.render( - - - - - - - - - - , - document.getElementById('root') + + + + + + + + , + document.getElementById('root'), ); // If you want to start measuring performance in your app, pass a function diff --git a/zubhub_frontend/zubhub/src/logo.svg b/zubhub_frontend/zubhub/src/logo.svg deleted file mode 100644 index 6b60c1042..000000000 --- a/zubhub_frontend/zubhub/src/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/zubhub_frontend/zubhub/src/store/actions/authActions.js b/zubhub_frontend/zubhub/src/store/actions/authActions.js index de008025e..877810508 100644 --- a/zubhub_frontend/zubhub/src/store/actions/authActions.js +++ b/zubhub_frontend/zubhub/src/store/actions/authActions.js @@ -1,9 +1,204 @@ +import ZubhubAPI from '../../api'; +import { toast } from 'react-toastify'; -export const setAuthUser =(auth_user)=>{ +const API = new ZubhubAPI(); + +export const setAuthUser = auth_user => { return dispatch => { dispatch({ - type:'SET_AUTH_USER', - payload:auth_user - }) - } -} \ No newline at end of file + type: 'SET_AUTH_USER', + payload: auth_user, + }); + }; +}; + +export const login = props => { + return dispatch => { + return API.login(props.values) + .then(res => { + if (!res.key) { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + dispatch({ + type: 'SET_AUTH_USER', + payload: { token: res.key }, + }); + }) + .then(() => props.history.push('/profile')) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + return { + error: + 'An error occured while performing this action. Please try again later', + }; + } else { + return { error: error.message }; + } + }); + }; +}; + +export const logout = props => { + return dispatch => { + API.logout(props.auth.token) + .then(res => { + dispatch({ + type: 'SET_AUTH_USER', + payload: { token: null, username: null, id: null }, + }); + }) + .then(res => { + props.history.push('/'); + }) + .catch(error => { + toast.warning( + 'An error occured while signing you out. please try again', + ); + }); + }; +}; + +export const get_auth_user = props => { + return dispatch => { + return API.get_auth_user(props.auth.token) + .then(res => { + if (!res.username) { + throw new Error( + 'an error occured while getting user profile, please try again later', + ); + } + + dispatch({ + type: 'SET_AUTH_USER', + payload: { ...props.auth, username: res.username, id: res.id }, + }); + }) + .catch(error => toast.warning(error.message)); + }; +}; + +export const signup = props => { + return dispatch => { + return API.signup(props.values) + .then(res => { + if (!res.key) { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + dispatch({ + type: 'SET_AUTH_USER', + payload: { token: res.key }, + }); + }) + .then(() => props.history.push('/profile')) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + return { + error: + 'An error occured while performing this action. Please try again later', + }; + } else { + return { error: error.message }; + } + }); + }; +}; + +export const send_email_confirmation = (props, key) => { + return () => { + return API.send_email_confirmation(key) + .then(res => { + toast.success('Congratulations!, your email has been confirmed!'); + setTimeout(() => { + props.history.push('/'); + }, 4000); + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + return { + error: + 'An error occured while performing this action. Please try again later', + }; + } else { + return { error: error.message }; + } + }); + }; +}; + +export const send_password_reset_link = props => { + return () => { + return API.send_password_reset_link(props.values.email) + .then(res => { + toast.success('We just sent a password reset link to your email!'); + setTimeout(() => { + props.history.push('/'); + }, 4000); + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + return { + error: + 'An error occured while performing this action. Please try again later', + }; + } else { + return { error: error.message }; + } + }); + }; +}; + +export const password_reset_confirm = props => { + return () => { + return API.password_reset_confirm(props) + .then(res => { + toast.success( + 'Congratulations! your password reset was successful! you will now be redirected to login', + ); + setTimeout(() => { + props.history.push('/login'); + }, 4000); + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + return { + error: + 'An error occured while performing this action. Please try again later', + }; + } else { + return { error: error.message }; + } + }); + }; +}; + +export const get_locations = () => { + return () => { + return API.get_locations() + .then(res => { + if (Array.isArray(res) && res.length > 0 && res[0].name) { + return { locations: res }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + return { + error: + 'An error occured while performing this action. Please try again later', + }; + } else { + return { error: error.message }; + } + }); + }; +}; diff --git a/zubhub_frontend/zubhub/src/store/actions/projectActions.js b/zubhub_frontend/zubhub/src/store/actions/projectActions.js new file mode 100644 index 000000000..2254c7a7b --- /dev/null +++ b/zubhub_frontend/zubhub/src/store/actions/projectActions.js @@ -0,0 +1,211 @@ +import ZubhubAPI from '../../api'; +import { toast } from 'react-toastify'; +const API = new ZubhubAPI(); + +export const set_projects = projects => { + return dispatch => { + dispatch({ + type: 'SET_PROJECTS', + payload: { all_projects: projects }, + }); + }; +}; + +export const create_project = props => { + return dispatch => { + return API.create_project(props) + .then(res => { + if (!res.comments) { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } else { + toast.success('Your project was created successfully!!'); + return props.history.push('/profile'); + } + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + return { + error: + 'An error occured while performing this action. Please try again later', + }; + } else { + return { error: error.message }; + } + }); + }; +}; + +export const get_project = value => { + return () => { + return API.get_project(value) + .then(res => { + if (res.title) { + return { project: res, loading: false }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + toast.warning(error.message); + return { loading: false }; + }); + }; +}; + +export const get_projects = page => { + return dispatch => { + return API.get_projects(page) + .then(res => { + if (Array.isArray(res.results)) { + dispatch({ + type: 'SET_PROJECTS', + payload: { all_projects: res }, + }); + return { loading: false }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + toast.warning(error.message); + return { loading: false }; + }); + }; +}; + +export const get_user_projects = props => { + return () => { + return API.get_user_projects(props) + .then(res => { + if (Array.isArray(res.results)) { + return { ...res, loading: false }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + toast.warning(error.message); + return { loading: false }; + }); + }; +}; + +export const get_saved = value => { + return () => { + return API.get_saved(value) + .then(res => { + if (Array.isArray(res.results)) { + return { + ...res, + loading: false, + }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + toast.warning( + 'An error occured while performing this action. Please try again later', + ); + } else { + toast.warning(error.message); + } + return { loading: false }; + }); + }; +}; + +export const toggle_like = props => { + return () => { + return API.toggle_like(props) + .then(res => { + if (res.title) { + return { project: res }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + toast.warning( + 'An error occured while performing this action. Please try again later', + ); + } else { + toast.warning(error.message); + } + + return { loading: false }; + }); + }; +}; + +export const toggle_save = props => { + return () => { + return API.toggle_save(props) + .then(res => { + if (res.title) { + return { project: res }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + toast.warning( + 'An error occured while performing this action. Please try again later', + ); + } else { + toast.warning(error.message); + } + return { loading: false }; + }); + }; +}; + +export const add_comment = props => { + return () => { + return API.add_comment(props) + .then(res => { + if (res.title) { + return { project: res }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + toast.warning( + 'An error occured while performing this action. Please try again later', + ); + } else { + toast.warning(error.message); + } + return { loading: false }; + }); + }; +}; diff --git a/zubhub_frontend/zubhub/src/store/actions/userActions.js b/zubhub_frontend/zubhub/src/store/actions/userActions.js new file mode 100644 index 000000000..8e5b6dafc --- /dev/null +++ b/zubhub_frontend/zubhub/src/store/actions/userActions.js @@ -0,0 +1,108 @@ +import ZubhubAPI from '../../api'; +import * as ProjectActions from './projectActions'; +import * as AuthActions from './authActions'; +import { toast } from 'react-toastify'; + +const API = new ZubhubAPI(); + +export const get_user_profile = props => { + return dispatch => { + let profile; + return API.get_user_profile(props) + .then(res => { + if (!res.username) { + throw new Error( + 'an error occured while fetching user profile, please try again later', + ); + } else { + profile = res; + return dispatch( + ProjectActions.get_user_projects({ + username: res.username, + limit: 4, + }), + ); + } + }) + .then(res => { + return { ...res, profile, loading: false }; + }) + .catch(error => { + toast.warning(error.message); + return { loading: false }; + }); + }; +}; + +export const edit_user_profile = props => { + return dispatch => { + return API.edit_user_profile(props) + .then(res => { + if (res.username) { + dispatch( + AuthActions.setAuthUser({ + username: res.username, + }), + ); + + return { profile: res }; + } else { + throw new Error( + 'An error occured while updating your profile, please try again later', + ); + } + }) + .catch(error => toast.warning(error.message)); + }; +}; + +export const toggle_follow = props => { + return () => { + return API.toggle_follow(props) + .then(res => { + if (res.username) { + return { profile: res }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + toast.warning( + 'An error occured while performing this action. Please try again later', + ); + } else { + toast.warning(error.message); + } + return { loading: false }; + }); + }; +}; + +export const get_followers = value => { + return () => { + return API.get_followers(value) + .then(res => { + if (Array.isArray(res.results)) { + return { + followers: res.results, + prevPage: res.previous, + nextPage: res.next, + loading: false, + }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + toast.warning(error.message); + return { loading: false }; + }); + }; +}; diff --git a/zubhub_frontend/zubhub/src/store/configureStore.js b/zubhub_frontend/zubhub/src/store/configureStore.js index 867d09be6..ced49a2b9 100644 --- a/zubhub_frontend/zubhub/src/store/configureStore.js +++ b/zubhub_frontend/zubhub/src/store/configureStore.js @@ -1,23 +1,22 @@ -import {createStore,applyMiddleware} from 'redux'; -import {persistStore,persistReducer} from 'redux-persist'; +import { createStore, applyMiddleware } from 'redux'; +import { persistStore, persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; import rootReducer from './reducers'; import thunk from 'redux-thunk'; - const persistConfig = { - key:'root', + key: 'root', storage, - whitelist:['auth'] -} + whitelist: ['auth', 'projects'], +}; -const persistedReducer = persistReducer(persistConfig,rootReducer); +const persistedReducer = persistReducer(persistConfig, rootReducer); -export default ()=>{ - let store = createStore(persistedReducer,applyMiddleware(thunk)); - let persistor = persistStore(store); - return{ +export default () => { + const store = createStore(persistedReducer, applyMiddleware(thunk)); + const persistor = persistStore(store); + return { store, - persistor - } -} \ No newline at end of file + persistor, + }; +}; diff --git a/zubhub_frontend/zubhub/src/store/reducers/authReducer.js b/zubhub_frontend/zubhub/src/store/reducers/authReducer.js index 45e37d069..3b4e648b9 100644 --- a/zubhub_frontend/zubhub/src/store/reducers/authReducer.js +++ b/zubhub_frontend/zubhub/src/store/reducers/authReducer.js @@ -1,18 +1,15 @@ -const defaultState = {token:null, username:null, id:null} - - - const auth = (state=defaultState, action)=>{ - switch(action.type){ - case 'SET_AUTH_USER': - return{ - ...state, - ...action.payload - } - default: - return state - } +const defaultState = { token: null, username: null, id: null }; + +const auth = (state = defaultState, action) => { + switch (action.type) { + case 'SET_AUTH_USER': + return { + ...state, + ...action.payload, + }; + default: + return state; } - - - export default auth; - \ No newline at end of file +}; + +export default auth; diff --git a/zubhub_frontend/zubhub/src/store/reducers/index.js b/zubhub_frontend/zubhub/src/store/reducers/index.js index 01794855d..2b383e875 100644 --- a/zubhub_frontend/zubhub/src/store/reducers/index.js +++ b/zubhub_frontend/zubhub/src/store/reducers/index.js @@ -1,7 +1,9 @@ -import {combineReducers} from 'redux'; +import { combineReducers } from 'redux'; import auth from './authReducer'; +import projects from './projectReducer'; export default combineReducers({ - auth -}) \ No newline at end of file + auth, + projects, +}); diff --git a/zubhub_frontend/zubhub/src/store/reducers/projectReducer.js b/zubhub_frontend/zubhub/src/store/reducers/projectReducer.js new file mode 100644 index 000000000..59b0734a1 --- /dev/null +++ b/zubhub_frontend/zubhub/src/store/reducers/projectReducer.js @@ -0,0 +1,15 @@ +const defaultState = { all_projects: {} }; + +const projects = (state = defaultState, action) => { + switch (action.type) { + case 'SET_PROJECTS': + return { + ...state, + ...action.payload, + }; + default: + return state; + } +}; + +export default projects; diff --git a/zubhub_frontend/zubhub/src/views/PageWrapper.jsx b/zubhub_frontend/zubhub/src/views/PageWrapper.jsx new file mode 100644 index 000000000..d090c93c9 --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/PageWrapper.jsx @@ -0,0 +1,266 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; + +import { connect } from 'react-redux'; + +import { ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +import { makeStyles } from '@material-ui/core/styles'; +import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; +import { + CssBaseline, + Container, + AppBar, + Toolbar, + Typography, + useScrollTrigger, + Box, + Fab, + Zoom, + Menu, + MenuItem, + Avatar, +} from '@material-ui/core'; + +import CustomButton from '../components/button/Button.js'; +import LoadingPage from './loading/LoadingPage'; +import * as AuthActions from '../store/actions/authActions'; +import unstructuredLogo from '../assets/images/logos/unstructured-logo.png'; +import logo from '../assets/images/logos/logo.png'; +import styles from '../assets/js/styles/views/page_wrapper/pageWrapperStyles'; +import commonStyles from '../assets/js/styles'; + +const useStyles = makeStyles(styles); + +const logout = (e, props) => { + e.preventDefault(); + return props.logout(props); +}; + +const handleScrollTopClick = (e, ref) => { + e.preventDefault(); + if (ref.current) { + ref.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } +}; + +const handleProfileMenuOpen = e => { + return { anchorEl: e.currentTarget }; +}; + +const handleProfileMenuClose = () => { + return { anchorEl: null }; +}; + +function PageWrapper(props) { + const backToTopEl = React.useRef(null); + const classes = useStyles(); + const commonClasses = makeStyles(commonStyles)(); + + const [state, setState] = React.useState({ + username: null, + anchorEl: null, + loading: false, + }); + + React.useEffect(() => { + if (props.auth.token) { + handleSetState({ loading: true }); + props.get_auth_user(props).finally(() => { + handleSetState({ loading: false }); + }); + } + }, [props.auth.token]); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { anchorEl, loading } = state; + const profileMenuOpen = Boolean(anchorEl); + return ( + <> + + + + + + + + logo + + +
+ {!props.auth.token ? ( + <> + + + Login + + + + + Sign Up + + + + ) : ( + <> + + + Create Project + + + handleSetState(handleProfileMenuOpen(e))} + src={`https://robohash.org/${props.auth.username}`} + alt={props.auth.username} + /> + handleSetState(handleProfileMenuClose(e))} + > + + + + {props.auth.username} + + + + + + + Saved Projects + + + + + logout(e, props)} + > + + Logout + + + + + + )} +
+
+
+
+ + + {loading ? : props.children} + +
+
+ +
+ + unstructured-studio-logo + + +
handleScrollTopClick(e, backToTopEl)} + role="presentation" + className={classes.scrollTopButtonStyle} + > + + + +
+
+
+ + ); +} + +PageWrapper.propTypes = { + auth: PropTypes.object.isRequired, + set_auth_user: PropTypes.func.isRequired, + logout: PropTypes.func.isRequired, + get_auth_user: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + set_auth_user: auth_user => { + dispatch(AuthActions.setAuthUser(auth_user)); + }, + logout: props => { + return dispatch(AuthActions.logout(props)); + }, + get_auth_user: props => { + return dispatch(AuthActions.get_auth_user(props)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(PageWrapper); diff --git a/zubhub_frontend/zubhub/src/views/create_project/CreateProject.jsx b/zubhub_frontend/zubhub/src/views/create_project/CreateProject.jsx new file mode 100644 index 000000000..88e249767 --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/create_project/CreateProject.jsx @@ -0,0 +1,715 @@ +import React from 'react'; +import clsx from 'clsx'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { withFormik } from 'formik'; +import * as Yup from 'yup'; + +import 'react-toastify/dist/ReactToastify.css'; + +import { nanoid } from 'nanoid'; + +import { makeStyles } from '@material-ui/core/styles'; +import AddIcon from '@material-ui/icons/Add'; +import ImageIcon from '@material-ui/icons/Image'; +import { + Grid, + Box, + Container, + Card, + CardActionArea, + CardContent, + Dialog, + Chip, + Typography, + CircularProgress, + OutlinedInput, + InputLabel, + FormHelperText, + FormControl, +} from '@material-ui/core'; + +import * as ProjectActions from '../../store/actions/projectActions'; +import ErrorPage from '../error/ErrorPage'; +import DO, { doConfig } from '../../assets/js/DO'; +import { useStateUpdateCallback } from '../../assets/js/customHooks'; +import CustomButton from '../../components/button/Button'; +import styles from '../../assets/js/styles/views/create_project/createProjectStyles'; + +const useStyles = makeStyles(styles); + +let image_field_touched = false; +let video_field_touched = false; + +const handleImageFieldChange = (e, refs, props) => { + props.setFieldValue(e.currentTarget.name, refs.imageEl.current); + refs.imageCountEl.current.innerText = refs.imageEl.current.files.length; + refs.imageCountEl.current.style.fontSize = '0.8rem'; +}; + +const handleImageButtonClick = (e, props, refs) => { + e.preventDefault(); + refs.imageEl.current.click(); + props.setFieldTouched('project_images'); +}; + +const removeMaterialsUsed = (e, props, refs, value) => { + e.preventDefault(); + const hidden_materials_field = refs.materialsUsedEl.current; + hidden_materials_field.value = hidden_materials_field.value + .split(',') + .filter(material => material !== value) + .join(','); + + props.setFieldValue('materials_used', hidden_materials_field.value, true); + return { materials_used: hidden_materials_field.value.split(',') }; +}; + +const handleAddMaterialFieldChange = (e, props, refs) => { + props.validateField('materials_used'); + const value = refs.addMaterialsUsedEl.current.firstChild.value; + if (value.includes(',')) { + refs.addMaterialsUsedEl.current.firstChild.value = value.split(',')[0]; + return addMaterialUsed(e, props, refs); + } +}; + +const addMaterialUsed = (e, props, refs) => { + e.preventDefault(); + const new_material = refs.addMaterialsUsedEl.current.firstChild; + if (new_material.value !== '') { + const hidden_materials_field = refs.materialsUsedEl.current; + hidden_materials_field.value = hidden_materials_field.value + ? `${hidden_materials_field.value},${new_material.value}` + : new_material.value; + new_material.value = ''; + props.setFieldValue('materials_used', hidden_materials_field.value, true); + new_material.focus(); + return { materials_used: hidden_materials_field.value.split(',') }; + } +}; + +function CreateProject(props) { + const refs = { + imageEl: React.useRef(null), + imageUploadButtonEl: React.useRef(null), + imageCountEl: React.useRef(null), + videoEl: React.useRef(null), + addMaterialsUsedEl: React.useRef(null), + materialsUsedEl: React.useRef(null), + }; + const classes = useStyles(); + + const [state, setState] = React.useState({ + error: null, + materialsUsedModalOpen: false, + materials_used: [], + image_upload: { + upload_dialog: false, + images_to_upload: 0, + successful_uploads: 0, + upload_info: {}, + upload_percent: 0, + uploaded_images_url: [], + }, + }); + + useStateUpdateCallback(() => { + if ( + state.image_upload.images_to_upload === + state.image_upload.successful_uploads + ) { + handleSetState(upload_project()); + } + }, [state.image_upload.successful_uploads]); + + React.useEffect(() => { + if (props.touched['project_images'] && props.errors['project_images']) { + refs.imageUploadButtonEl.current.setAttribute( + 'style', + 'border-color:#F54336; color:#F54336', + ); + } else { + refs.imageUploadButtonEl.current.setAttribute( + 'style', + 'border-color: #00B8C4; color:#00B8C4', + ); + } + + if (props.touched['project_images']) { + image_field_touched = true; + } else { + image_field_touched = false; + } + + if (props.touched['video']) { + video_field_touched = true; + } else { + video_field_touched = false; + } + }, [ + props.errors['project_images'], + props.touched['project_images'], + props.touched['video'], + ]); + + const upload = image => { + const params = { + Bucket: `${doConfig.bucketName}`, + Key: `${doConfig.project_images}/${nanoid()}`, + Body: image, + ContentType: image.type, + ACL: 'public-read', + }; + + DO.upload(params) + .on('httpUploadProgress', e => { + const progress = Math.round((e.loaded * 100.0) / e.total); + const { image_upload } = state; + image_upload.upload_info[image.name] = progress; + + let total = 0; + Object.keys(image_upload.upload_info).forEach(each => { + total = total + image_upload.upload_info[each]; + }); + + total = total / Object.keys(image_upload.upload_info).length; + image_upload.upload_percent = total; + + handleSetState({ image_upload }); + }) + .send((err, data) => { + if (err) { + const { image_upload } = state; + image_upload.upload_dialog = false; + + if (err.message.startsWith('Unexpected')) { + handleSetState({ + error: + 'An error occured while performing this action. Please try again later', + image_upload, + }); + } else { + handleSetState({ error: err.message, image_upload }); + } + } else { + const secure_url = data.Location; + const public_id = data.Key; + const { image_upload } = state; + + image_upload.uploaded_images_url.push({ + image_url: secure_url, + public_id, + }); + image_upload.successful_uploads = image_upload.successful_uploads + 1; + + handleSetState({ image_upload }); + } + }); + }; + + const upload_project = () => { + const { image_upload } = state; + image_upload.upload_dialog = false; + handleSetState({ image_upload }); + return props.create_project({ + ...props.values, + token: props.auth.token, + images: state.image_upload.uploaded_images_url, + video: props.values.video ? props.values.video : '', + }); + }; + + const init_project = e => { + e.preventDefault(); + if (!props.auth.token) { + props.history.push('/login'); + } else { + props.setFieldTouched('title'); + props.setFieldTouched('description'); + props.setFieldTouched('project_images'); + props.setFieldTouched('video'); + props.setFieldTouched('materials_used'); + props.validateField('title'); + props.validateField('description'); + props.validateField('project_images'); + props.validateField('video'); + props.validateField('materials_used'); + + if ( + props.errors['title'] || + props.errors['description'] || + props.errors['project_images'] || + props.errors['video'] || + props.errors['materials_used'] + ) { + return; + } else if (refs.imageEl.current.files.length === 0) { + handleSetState(upload_project()); + } else { + const project_images = refs.imageEl.current.files; + + const { image_upload } = state; + image_upload.images_to_upload = project_images.length; + image_upload.upload_dialog = true; + image_upload.upload_percent = 0; + handleSetState({ image_upload }); + + for (let index = 0; index < project_images.length; index++) { + upload(project_images[index]); + } + } + } + }; + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { error, image_upload, materials_used } = state; + if (!props.auth.token) { + return ( + + ); + } else { + return ( + + + + + +
+ + Create Project + + + Tell us about your project! + + + + + + {error && ( + + {error} + + )} + + + + + + + Title + + + + {props.touched['title'] && props.errors['title']} + + + + + + + + Description + + + + {props.touched['description'] && + props.errors['description']} + + + + + + + + handleImageFieldChange(e, refs, props)} + onBlur={props.handleBlur} + /> + + {props.errors['project_images']} + + + + + + + + Video URL + + + + {props.touched['video'] && props.errors['video']} + + + + + + + + Materials Used + + + {materials_used.map((material, num) => + material !== '' ? ( + + handleSetState( + removeMaterialsUsed( + e, + props, + refs, + material, + ), + ) + } + color="secondary" + variant="outlined" + /> + ) : null, + )} + + + + + props.setFieldTouched('materials_used') + } + onChange={e => + handleSetState( + handleAddMaterialFieldChange(e, props, refs), + ) + } + onBlur={() => + props.validateField('materials_used') + } + /> + + {props.touched['materials_used'] && + props.errors['materials_used']} + + + + + handleSetState(addMaterialUsed(e, props, refs)) + } + secondaryButtonStyle + fullWidth + > + + + + + + + + + + Create Project + + + +
+ + + + + {`${Math.round( + image_upload.upload_percent, + )}%`} + + + +
+
+
+
+
+ ); + } +} + +CreateProject.propTypes = { + auth: PropTypes.object.isRequired, + create_project: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + create_project: props => { + return dispatch(ProjectActions.create_project(props)); + }, + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)( + withFormik({ + mapPropsToValue: () => ({ + title: '', + description: '', + video: '', + materials_used: '', + }), + validationSchema: Yup.object().shape({ + title: Yup.string() + .max(100, "your project title shouldn't be more than 100 characters") + .required('title is required'), + description: Yup.string() + .max(10000, "your description shouldn't be more than 10,000 characters") + .required('description is required'), + project_images: Yup.mixed() + .test( + 'image_is_empty', + 'you must provide either image(s) or video url', + function (value) { + return image_field_touched && !value && !this.parent.video + ? false + : true; + }, + ) + .test('too_many_images', 'too many images uploaded', value => { + if (value) { + return value.files.length > 10 ? false : true; + } else { + return true; + } + }) + .test( + 'image_size_too_large', + 'one or more of your image is greater than 3mb', + value => { + if (value) { + let image_size_too_large = false; + for (let index = 0; index < value.files.length; index++) { + if (value.files[index].size / 1000 > 3072) { + image_size_too_large = true; + } + } + return image_size_too_large ? false : true; + } else { + return true; + } + }, + ), + video: Yup.string() + .url('you are required to submit a video url here') + .max(1000, "your video url shouldn't be more than 1000 characters") + .test( + 'video_is_empty', + 'you must provide either image(s) or video url', + function (value) { + return video_field_touched && !value && !this.parent.project_images + ? false + : true; + }, + ), + materials_used: Yup.string() + .max( + 10000, + "your materials used shouldn't be more than 10,000 characters", + ) + .required('materials used is required'), + }), + })(CreateProject), +); diff --git a/zubhub_frontend/zubhub/src/views/email_confirm/EmailConfirm.jsx b/zubhub_frontend/zubhub/src/views/email_confirm/EmailConfirm.jsx new file mode 100644 index 000000000..f5d22e46b --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/email_confirm/EmailConfirm.jsx @@ -0,0 +1,138 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { connect } from 'react-redux'; + +import 'react-toastify/dist/ReactToastify.css'; + +import { makeStyles } from '@material-ui/core/styles'; +import { + Grid, + Box, + Container, + Card, + CardActionArea, + CardContent, + Typography, +} from '@material-ui/core'; + +import * as AuthActions from '../../store/actions/authActions'; +import CustomButton from '../../components/button/Button'; +import styles from '../../assets/js/styles/views/email_confirm/emailConfirmStyles'; + +const useStyles = makeStyles(styles); + +const getUsernameAndKey = queryString => { + let username = queryString.split('&&'); + const key = username[1].split('=')[1]; + username = username[0].split('=')[1]; + return { username, key }; +}; + +const confirmEmail = (e, props, state) => { + e.preventDefault(); + return props.send_email_confirmation(props, state.key); +}; + +function EmailConfirm(props) { + const classes = useStyles(); + + let { username, key } = getUsernameAndKey(props.location.search); + + const [state, setState] = React.useState({ + error: null, + username: username ?? null, + key: key ?? null, + }); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { error } = state; + username = state.username; + + return ( + + + + + +
handleSetState(confirmEmail(e, props, state))} + > + + Email Confirmation + + + Please Confirm that you are {username} and that the email + belongs to you: + + + + + + {error !== null && ( + + {error} + + )} + + + + + Confirm + + + +
+
+
+
+
+
+ ); +} + +EmailConfirm.propTypes = { + auth: PropTypes.object.isRequired, + send_email_confirmation: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + send_email_confirmation: (props, key) => { + return dispatch(AuthActions.send_email_confirmation(props, key)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(EmailConfirm); diff --git a/zubhub_frontend/zubhub/src/views/error/ErrorPage.jsx b/zubhub_frontend/zubhub/src/views/error/ErrorPage.jsx new file mode 100644 index 000000000..ce8e0f670 --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/error/ErrorPage.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { makeStyles } from '@material-ui/core/styles'; +import { Box, Typography, Container } from '@material-ui/core'; + +import disconnected from '../../assets/images/disconnected-chains.svg'; +import styles from '../../assets/js/styles/views/error/errorPageStyles'; + +const useStyles = makeStyles(styles); + +function ErrorPage(props) { + const classes = useStyles(); + return ( + + + {props.error} + + Oops!! + {props.error} + + + + ); +} + +ErrorPage.propTypes = { + error: PropTypes.string.isRequired, +}; + +export default ErrorPage; diff --git a/zubhub_frontend/zubhub/src/views/loading/LoadingPage.jsx b/zubhub_frontend/zubhub/src/views/loading/LoadingPage.jsx new file mode 100644 index 000000000..dc6f8afb7 --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/loading/LoadingPage.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { makeStyles } from '@material-ui/core/styles'; +import { Container, Box, CircularProgress } from '@material-ui/core'; +import styles from '../../assets/js/styles/views/loading/loadingPageStyles'; + +const useStyles = makeStyles(styles); + +function LoadingPage() { + const classes = useStyles(); + return ( + + + + + + ); +} + +export default LoadingPage; diff --git a/zubhub_frontend/zubhub/src/views/login/Login.jsx b/zubhub_frontend/zubhub/src/views/login/Login.jsx new file mode 100644 index 000000000..9cc98a2de --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/login/Login.jsx @@ -0,0 +1,274 @@ +import React from 'react'; +import clsx from 'clsx'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; + +import { connect } from 'react-redux'; + +import { makeStyles } from '@material-ui/core/styles'; +import Visibility from '@material-ui/icons/Visibility'; +import VisibilityOff from '@material-ui/icons/VisibilityOff'; +import { + Grid, + Box, + Divider, + Container, + Card, + CardActionArea, + CardContent, + Typography, + InputAdornment, + IconButton, + OutlinedInput, + InputLabel, + FormHelperText, + FormControl, +} from '@material-ui/core'; + +import { withFormik } from 'formik'; +import * as Yup from 'yup'; + +import CustomButton from '../../components/button/Button'; +import * as AuthActions from '../../store/actions/authActions'; +import styles from '../../assets/js/styles/views/login/loginStyles'; + +const useStyles = makeStyles(styles); + +const handleClickShowPassword = state => { + const { showPassword } = state; + return { showPassword: !showPassword }; +}; + +const handleMouseDownPassword = e => { + e.preventDefault(); +}; + +const login = (e, props) => { + e.preventDefault(); + return props.login(props); +}; + +function Login(props) { + const classes = useStyles(); + + const [state, setState] = React.useState({ + error: null, + showPassword: false, + }); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { error, showPassword } = state; + + return ( + + + + + +
handleSetState(login(e, props))} + > + + Welcome to Zubhub + + + Login to get started! + + + + + {error && ( + + {error} + + )} + + + + + + Username Or Email + + + + {props.touched['username'] && props.errors['username']} + + + + + + + Password + + + handleSetState(handleClickShowPassword(state)) + } + onMouseDown={handleMouseDownPassword} + edge="end" + > + {showPassword ? ( + + ) : ( + + )} + + + } + labelWidth={70} + /> + + {props.touched['password'] && props.errors['password']} + + + + + + Login + + + +
+ + + + + + Not a Member ? + + + + + + + + Signup + + + + + + + Forgot Password? + + + + +
+
+
+
+
+ ); +} + +Login.propTypes = { + auth: PropTypes.object.isRequired, + set_auth_user: PropTypes.object.isRequired, + login: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + set_auth_user: auth_user => { + dispatch(AuthActions.setAuthUser(auth_user)); + }, + login: props => { + return dispatch(AuthActions.login(props)); + }, + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)( + withFormik({ + mapPropsToValue: () => ({ + password: '', + }), + validationSchema: Yup.object().shape({ + password: Yup.string() + .min(8, 'your password is too short') + .required('input your password'), + }), + })(Login), +); diff --git a/zubhub_frontend/zubhub/src/views/password_reset/PasswordReset.jsx b/zubhub_frontend/zubhub/src/views/password_reset/PasswordReset.jsx new file mode 100644 index 000000000..5156b237f --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/password_reset/PasswordReset.jsx @@ -0,0 +1,171 @@ +import React from 'react'; +import clsx from 'clsx'; +import PropTypes from 'prop-types'; + +import { connect } from 'react-redux'; + +import { withFormik } from 'formik'; +import * as Yup from 'yup'; + +import { makeStyles } from '@material-ui/core/styles'; +import { + Grid, + Box, + Container, + Card, + CardActionArea, + CardContent, + Typography, + OutlinedInput, + InputLabel, + FormHelperText, + FormControl, +} from '@material-ui/core'; + +import * as AuthActions from '../../store/actions/authActions'; +import CustomButton from '../../components/button/Button'; +import styles from '../../assets/js/styles/views/password_reset/passwordResetStyles'; + +const useStyles = makeStyles(styles); + +const sendPasswordResetLink = (e, props) => { + e.preventDefault(); + return props.send_password_reset_link(props); +}; + +function PasswordReset(props) { + const classes = useStyles(); + + const [state, setState] = React.useState({ + error: null, + }); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { error } = state; + + return ( + + + + + +
handleSetState(sendPasswordResetLink(e, props))} + > + + Password Reset + + + Input your email so we can send you a password reset link + + + + + {error && ( + + {error} + + )} + + + + + + Email + + + + {props.touched['email'] && props.errors['email']} + + + + + + Send Reset Link + + + +
+
+
+
+
+
+ ); +} + +PasswordReset.propTypes = { + auth: PropTypes.object.isRequired, + send_password_reset_link: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + send_password_reset_link: props => { + return dispatch(AuthActions.send_password_reset_link(props)); + }, + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)( + withFormik({ + mapPropsToValue: () => ({ + email: '', + }), + validationSchema: Yup.object().shape({ + email: Yup.string().email('invalid email').required('email required'), + }), + })(PasswordReset), +); diff --git a/zubhub_frontend/zubhub/src/views/password_reset_confirm/PasswordResetConfirm.jsx b/zubhub_frontend/zubhub/src/views/password_reset_confirm/PasswordResetConfirm.jsx new file mode 100644 index 000000000..1f510d032 --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/password_reset_confirm/PasswordResetConfirm.jsx @@ -0,0 +1,280 @@ +import React from 'react'; +import clsx from 'clsx'; +import PropTypes from 'prop-types'; + +import { connect } from 'react-redux'; + +import { toast } from 'react-toastify'; + +import { withFormik } from 'formik'; +import * as Yup from 'yup'; + +import { makeStyles } from '@material-ui/core/styles'; +import Visibility from '@material-ui/icons/Visibility'; +import VisibilityOff from '@material-ui/icons/VisibilityOff'; +import { + Grid, + Box, + Container, + Card, + CardActionArea, + CardContent, + Typography, + InputAdornment, + IconButton, + OutlinedInput, + InputLabel, + FormHelperText, + FormControl, +} from '@material-ui/core'; + +import * as AuthActions from '../../store/actions/authActions'; +import CustomButton from '../../components/button/Button'; +import styles from '../../assets/js/styles/views/password_reset_confirm/passwordResetConfirmStyles'; + +const useStyles = makeStyles(styles); + +const getUidAndToken = queryString => { + let uid = queryString.split('&&'); + const token = uid[1].split('=')[1]; + uid = uid[0].split('=')[1]; + return { uid, token }; +}; + +const resetPassword = (e, props) => { + e.preventDefault(); + const { uid, token } = getUidAndToken(props.location.search); + return props.password_reset_confirm({ ...props.values, uid, token }); +}; + +const handleClickShowPassword = (field, state) => { + if (field === 1) { + const { showPassword1 } = state; + return { showPassword1: !showPassword1 }; + } else if (field === 2) { + const { showPassword2 } = state; + return { showPassword2: !showPassword2 }; + } +}; + +const handleMouseDownPassword = e => { + e.preventDefault(); +}; + +function PasswordResetConfirm(props) { + const classes = useStyles(); + + const [state, setState] = React.useState({ + error: null, + showPassword1: false, + showPassword2: false, + }); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { error, showPassword1, showPassword2 } = state; + + return ( + + + + + +
handleSetState(resetPassword(e, props))} + > + + Password Reset Confirmation + + + + + {error && ( + + {error} + + )} + + + + + + + New Password + + + + handleSetState( + handleClickShowPassword(field, state), + ) + } + onMouseDown={handleMouseDownPassword} + edge="end" + > + {showPassword1 ? ( + + ) : ( + + )} + + + } + labelWidth={70} + /> + + {props.touched['new_password'] && + props.errors['new_password']} + + + + + + + + Confirm Password + + + + handleSetState( + handleClickShowPassword(field, state), + ) + } + onMouseDown={handleMouseDownPassword} + edge="end" + > + {showPassword2 ? ( + + ) : ( + + )} + + + } + labelWidth={150} + /> + + {props.touched['new_password2'] && + props.errors['new_password2']} + + + + + + Reset Password + + + +
+
+
+
+
+
+ ); +} + +PasswordResetConfirm.propTypes = { + auth: PropTypes.object.isRequired, + password_reset_confirm: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + password_reset_confirm: props => { + return dispatch(AuthActions.password_reset_confirm(props)); + }, + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)( + withFormik({ + mapPropsToValue: () => ({ + new_password1: '', + new_password2: '', + }), + validationSchema: Yup.object().shape({ + new_password1: Yup.string() + .min(8, 'your password is too short') + .required('input your password'), + new_password2: Yup.string() + .oneOf([Yup.ref('new_password1'), null], 'Passwords must match') + .required('input a confirmation password'), + }), + })(PasswordResetConfirm), +); diff --git a/zubhub_frontend/zubhub/src/views/profile/Profile.jsx b/zubhub_frontend/zubhub/src/views/profile/Profile.jsx new file mode 100644 index 000000000..bbfa64ad3 --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/profile/Profile.jsx @@ -0,0 +1,423 @@ +import React from 'react'; +import clsx from 'clsx'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; + +import { connect } from 'react-redux'; + +import { toast } from 'react-toastify'; + +import { makeStyles } from '@material-ui/core/styles'; +import ShareIcon from '@material-ui/icons/Share'; +import { + Tooltip, + Badge, + Avatar, + Grid, + Box, + Container, + Paper, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Fab, + Typography, + OutlinedInput, + InputLabel, + FormControl, + Divider, +} from '@material-ui/core'; + +import CustomButton from '../../components/button/Button'; +import * as AuthActions from '../../store/actions/authActions'; +import * as UserActions from '../../store/actions/userActions'; +import * as ProjectActions from '../../store/actions/projectActions'; +import ErrorPage from '../error/ErrorPage'; +import LoadingPage from '../loading/LoadingPage'; +import Project from '../../components/project/Project'; +import styles from '../../assets/js/styles/views/profile/profileStyles'; + +const useStyles = makeStyles(styles); + +const handleToggleEditProfileModal = ({ openEditProfileModal }) => { + openEditProfileModal = !openEditProfileModal; + return { openEditProfileModal }; +}; + +const getUserPofile = props => { + let username = props.match.params.username; + + if (!username) { + username = props.auth.username; + } else if (props.auth.username === username) props.history.push('/profile'); + return props.get_user_profile({ username, token: props.auth.token }); +}; + +const updateProfile = (e, props, state, newUserNameEL) => { + e.preventDefault(); + const username = newUserNameEL.current.firstChild; + if (username.value) { + return props + .edit_user_profile({ + token: props.auth.token, + username: username.value, + }) + .then(res => { + username.value = ''; + return { ...res, ...handleToggleEditProfileModal(state) }; + }); + } else { + return handleToggleEditProfileModal(state); + } +}; + +const copyProfileUrl = profile => { + const tempInput = document.createElement('textarea'); + tempInput.value = `${document.location.origin}/creators/${profile.username}`; + tempInput.style.top = '0'; + tempInput.style.top = '0'; + tempInput.style.position = 'fixed'; + const rootElem = document.querySelector('#root'); + rootElem.appendChild(tempInput); + tempInput.focus(); + tempInput.select(); + if (document.execCommand('copy')) { + toast.success( + 'your profile url has been successfully copied to your clipboard!', + ); + rootElem.removeChild(tempInput); + } +}; + +const updateProjects = (res, { results: projects }) => { + return res + .then(res => { + if (res.project && res.project.title) { + projects = projects.map(project => + project.id === res.project.id ? res.project : project, + ); + return { results: projects }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + toast.warning(error.message); + return { loading: false }; + }); +}; + +const toggle_follow = (id, props) => { + if (!props.auth.token) { + props.history.push('/login'); + } else { + return props.toggle_follow({ id, token: props.auth.token }); + } +}; + +function Profile(props) { + const newUserNameEL = React.useRef(null); + const classes = useStyles(); + + const [state, setState] = React.useState({ + results: [], + openEditProfileModal: false, + loading: true, + profile: {}, + }); + + React.useEffect(() => { + handleSetState(getUserPofile(props)); + }, []); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { results: projects, profile, loading, openEditProfileModal } = state; + + if (loading) { + return ; + } else if (profile && Object.keys(profile).length > 0) { + return ( + <> + + + + {props.auth.username === profile.username ? ( + + handleSetState(handleToggleEditProfileModal(state)) + } + > + Edit + + ) : ( + + handleSetState(toggle_follow(profile.id, props)) + } + > + {profile.followers.includes(props.auth.id) + ? 'Unfollow' + : 'Follow'} + + )} + + + copyProfileUrl(profile)} + > + + + + ) : null + } + > + + + + + + {profile.username} + + {props.auth.username === profile.username ? ( + + {profile.email} + + ) : null} + + + + + {profile.projects_count} Projects + + + + + {profile.followers.length} Followers + + + + + + + + + + + About Me + + + {profile.bio + ? profile.bio + : 'You will be able to change this next month 😀!'} + + + + {profile.projects_count > 0 ? ( + + + Latest projects of {profile.username} + + View all >> + + + + {Array.isArray(projects) && + projects.map(project => ( + + + handleSetState(updateProjects(res, state)) + } + {...props} + /> + + ))} + + + ) : null} + + + handleSetState(handleToggleEditProfileModal(state))} + aria-labelledby="edit user profile" + > + Edit User Profile + + + + New Username + + + + + + + handleSetState(handleToggleEditProfileModal(state)) + } + color="primary" + secondaryButtonStyle + > + Cancel + + + handleSetState(updateProfile(e, props, state, newUserNameEL)) + } + primaryButtonStyle + > + Save + + + + + ); + } else { + return ( + + ); + } +} + +Profile.propTypes = { + auth: PropTypes.object.isRequired, + set_auth_user: PropTypes.func.isRequired, + get_user_profile: PropTypes.func.isRequired, + edit_user_profile: PropTypes.func.isRequired, + toggle_follow: PropTypes.func.isRequired, + toggle_like: PropTypes.func.isRequired, + toggle_save: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + set_auth_user: auth_user => { + dispatch(AuthActions.setAuthUser(auth_user)); + }, + get_user_profile: props => { + return dispatch(UserActions.get_user_profile(props)); + }, + edit_user_profile: props => { + return dispatch(UserActions.edit_user_profile(props)); + }, + toggle_follow: props => { + return dispatch(UserActions.toggle_follow(props)); + }, + toggle_like: props => { + return dispatch(ProjectActions.toggle_like(props)); + }, + toggle_save: props => { + return dispatch(ProjectActions.toggle_save(props)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Profile); diff --git a/zubhub_frontend/zubhub/src/views/project_details/ProjectDetails.jsx b/zubhub_frontend/zubhub/src/views/project_details/ProjectDetails.jsx new file mode 100644 index 000000000..fc7308cf8 --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/project_details/ProjectDetails.jsx @@ -0,0 +1,542 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { connect } from 'react-redux'; +import clsx from 'clsx'; +import PropTypes from 'prop-types'; + +import Slider from 'react-slick'; +import 'slick-carousel/slick/slick.css'; +import 'slick-carousel/slick/slick-theme.css'; + +import { makeStyles } from '@material-ui/core/styles'; +import BookmarkIcon from '@material-ui/icons/Bookmark'; +import BookmarkBorderIcon from '@material-ui/icons/BookmarkBorder'; +import VisibilityIcon from '@material-ui/icons/Visibility'; +import CommentIcon from '@material-ui/icons/Comment'; +import { + Avatar, + Box, + Typography, + Grid, + Container, + Paper, + Dialog, +} from '@material-ui/core'; + +import * as UserActions from '../../store/actions/userActions'; +import * as ProjectActions from '../../store/actions/projectActions'; +import CustomButton from '../../components/button/Button'; +import ErrorPage from '../error/ErrorPage'; +import LoadingPage from '../loading/LoadingPage'; +import ClapIcon, { ClapBorderIcon } from '../../assets/js/icons/ClapIcon'; +import nFormatter from '../../assets/js/nFormatter'; +import dFormatter from '../../assets/js/dFormatter'; +import styles, { + sliderSettings, +} from '../../assets/js/styles/views/project_details/projectDetailsStyles'; +import commonStyles from '../../assets/js/styles'; +import Project from '../../components/project/Project'; + +const useStyles = makeStyles(styles); + +const constructCommentBox = refs => { + refs.commentText.current.addEventListener('focus', e => + handleCommentTextFocus(refs), + ); + + document.addEventListener('click', e => handleDocumentClick(e, refs)); +}; + +const handleCommentTextFocus = refs => { + refs.commentBox.current.classList.remove('comment-collapsed'); + refs.commentBox.current.classList.add('comment'); + refs.commentAuthorName.current.classList.remove('display-none'); + refs.commentPublishButton.current.classList.remove('display-none'); +}; + +const handleDocumentClick = (e, refs) => { + try { + if ( + ![ + refs.commentBox.current, + refs.commentPublishButton.current, + refs.commentText.current, + ].includes(e.target) + ) { + refs.commentBox.current.classList.remove('comment'); + refs.commentBox.current.classList.add('comment-collapsed'); + refs.commentAuthorName.current.classList.add('display-none'); + refs.commentPublishButton.current.classList.add('display-none'); + } + } catch {} +}; + +const handleOpenEnlargedImageDialog = (e, state) => { + const image_url = e.currentTarget.getAttribute('src'); + const openEnlargedImageDialog = !state.openEnlargedImageDialog; + return { enlargedImageUrl: image_url, openEnlargedImageDialog }; +}; + +const add_comment = (e, props, refs, state) => { + e.preventDefault(); + if (!props.auth.token) { + props.history.push('/login'); + } else { + const textarea = refs.commentText.current; + const comment_text = textarea.value; + textarea.value = ''; + return props.add_comment({ + id: state.project.id, + token: props.auth.token, + text: comment_text, + }); + } +}; + +const toggle_save = (e, props, id) => { + e.preventDefault(); + if (!props.auth.token) { + props.history.push('/login'); + } else { + return props.toggle_save({ + id, + token: props.auth.token, + }); + } +}; + +const toggle_like = (e, props, id) => { + e.preventDefault(); + if (!props.auth.token) { + return props.history.push('/login'); + } else { + return props.toggle_like({ id, token: props.auth.token }); + } +}; + +const toggle_follow = (e, props, id, state) => { + e.preventDefault(); + if (!props.auth.token) { + props.history.push('/login'); + } else { + return props + .toggle_follow({ id, token: props.auth.token }) + .then(({ profile }) => { + const { project } = state; + if (project.creator.id === profile.id) { + project.creator = profile; + } + + return { project }; + }); + } +}; + +const buildMaterialsUsedComponent = (classes, state) => { + const arr = + state.project.materials_used && state.project.materials_used.split(','); + return arr.map((material, index) => ( + + {material} + + )); +}; + +function ProjectDetails(props) { + const refs = { + commentText: React.useRef(null), + commentBox: React.useRef(null), + commentAuthorName: React.useRef(null), + commentPublishButton: React.useRef(null), + }; + const classes = useStyles(); + const commonClasses = makeStyles(commonStyles)(); + + const [state, setState] = React.useState({ + project: {}, + loading: true, + enlargedImageUrl: '', + openEnlargedImageDialog: false, + }); + + React.useEffect(() => { + const commentTextEl = refs.commentText.current; + handleSetState( + props.get_project({ + id: props.match.params.id, + token: props.auth.token, + }), + ); + + return () => { + try { + commentTextEl.removeEventListener('focus', () => + handleCommentTextFocus(refs), + ); + } catch {} + + try { + document.removeEventListener('click', e => + handleDocumentClick(e, refs), + ); + } catch {} + }; + }, []); + + React.useEffect(() => { + try { + constructCommentBox(refs); + } catch {} + }, [state.project]); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { project, loading, enlargedImageUrl, openEnlargedImageDialog } = state; + if (loading) { + return ; + } else if (Object.keys(project).length > 0) { + return ( + <> + + + + + {project.title} + + + + + + + {project.creator.username} + + + {project.creator.id !== props.auth.id ? ( + + handleSetState( + toggle_follow(e, props, project.creator.id, state), + ) + } + primaryButtonStyle + > + {project.creator.followers.includes(props.auth.id) + ? 'Unfollow' + : 'Follow'} + + ) : null} + + + + + + + + {project.video ? ( + + ) : project.images.length > 0 ? ( + {project.title} + ) : null} + + + + handleSetState(toggle_like(e, props, project.id)) + } + > + {project.likes.includes(props.auth.id) ? ( + + ) : ( + + )} + + {nFormatter(project.likes.length)} + + + + handleSetState(toggle_save(e, props, project.id)) + } + > + {project.saved_by.includes(props.auth.id) ? ( + + ) : ( + + )} + + + + {project.views_count} + + + + {project.images && project.images.length > 0 ? ( + + + + {project.images.map(image => ( +
+ {image.public_id} + handleSetState( + handleOpenEnlargedImageDialog(e, state), + ) + } + /> +
+ ))} +
+
+
+ ) : null} + + + Description + + + {project.description} + + + + + Materials used + + + {buildMaterialsUsedComponent(classes, state)} + + +
+
+ + + {nFormatter(project.comments.length)} Comments + + + + + {props.auth.token ? ( + + ) : null} + + + {props.auth.username} + + +
+ + + handleSetState(add_comment(e, props, refs, state)) + } + className={clsx('comment-publish-button', 'display-none')} + variant="contained" + primaryButtonStyle + > + Comment + +
+
+ {project.comments && + project.comments.map(comment => ( + + + + + + {comment.creator} + + + {dFormatter(comment.created_on)} + + + + + {comment.text} + + ))} +
+
+
+ + setState({ + ...state, + openEnlargedImageDialog: !openEnlargedImageDialog, + }) + } + aria-labelledby="enlarged image dialog" + > + {`${project.title}`} + + + ); + } else { + return ( + + ); + } +} + +ProjectDetails.propTypes = { + auth: PropTypes.object.isRequired, + get_project: PropTypes.func.isRequired, + toggle_follow: PropTypes.func.isRequired, + toggle_like: PropTypes.func.isRequired, + toggle_save: PropTypes.func.isRequired, + add_comment: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + get_project: values => { + return dispatch(ProjectActions.get_project(values)); + }, + toggle_follow: values => { + return dispatch(UserActions.toggle_follow(values)); + }, + toggle_like: values => { + return dispatch(ProjectActions.toggle_like(values)); + }, + toggle_save: values => { + return dispatch(ProjectActions.toggle_save(values)); + }, + add_comment: values => { + return dispatch(ProjectActions.add_comment(values)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ProjectDetails); diff --git a/zubhub_frontend/zubhub/src/views/projects/Projects.jsx b/zubhub_frontend/zubhub/src/views/projects/Projects.jsx new file mode 100644 index 000000000..cede343bb --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/projects/Projects.jsx @@ -0,0 +1,177 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { connect } from 'react-redux'; + +import { toast } from 'react-toastify'; + +import { makeStyles } from '@material-ui/core/styles'; +import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; +import NavigateNextIcon from '@material-ui/icons/NavigateNext'; +import { Grid, Box, ButtonGroup, Container } from '@material-ui/core'; + +import * as ProjectActions from '../../store/actions/projectActions'; +import CustomButton from '../../components/button/Button'; +import ErrorPage from '../error/ErrorPage'; +import LoadingPage from '../loading/LoadingPage'; +import Project from '../../components/project/Project'; +import styles from '../../assets/js/styles/views/projects/projectsStyles'; + +const useStyles = makeStyles(styles); + +const fetchPage = (page, props) => { + return props.get_projects(page); +}; + +const updateProjects = (res, props) => { + return res + .then(res => { + if (res.project && res.project.title) { + const results = props.projects.results.map(project => + project.id === res.project.id ? res.project : project, + ); + props.set_projects({ ...props.projects, results }); + return { loading: false }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + toast.warning(error.message); + return { loading: false }; + }); +}; + +function Projects(props) { + const classes = useStyles(); + + const [state, setState] = React.useState({ + loading: true, + }); + + React.useEffect(() => { + handleSetState(fetchPage(null, props)); + }, []); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { loading } = state; + const { + results: projects, + previous: prevPage, + next: nextPage, + } = props.projects; + + if (loading) { + return ; + } else if (projects && projects.length > 0) { + return ( + + + + {projects.map(project => ( + + + handleSetState(updateProjects(res, props)) + } + {...props} + /> + + ))} + + + {prevPage ? ( + } + onClick={(e, page = prevPage.split('?')[1]) => { + handleSetState({ loading: true }); + handleSetState(fetchPage(page, props)); + }} + primaryButtonStyle + > + Prev + + ) : null} + {nextPage ? ( + } + onClick={(e, page = nextPage.split('?')[1]) => { + handleSetState({ loading: true }); + handleSetState(fetchPage(page, props)); + }} + primaryButtonStyle + > + Next + + ) : null} + + + + ); + } else { + return ( + + ); + } +} + +Projects.propTypes = { + auth: PropTypes.object.isRequired, + get_projects: PropTypes.func.isRequired, + set_projects: PropTypes.func.isRequired, + toggle_like: PropTypes.func.isRequired, + toggle_save: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + projects: state.projects.all_projects, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + get_projects: page => { + return dispatch(ProjectActions.get_projects(page)); + }, + set_projects: projects => { + return dispatch(ProjectActions.set_projects(projects)); + }, + toggle_like: props => { + return dispatch(ProjectActions.toggle_like(props)); + }, + toggle_save: props => { + return dispatch(ProjectActions.toggle_save(props)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Projects); diff --git a/zubhub_frontend/zubhub/src/views/saved_projects/SavedProjects.jsx b/zubhub_frontend/zubhub/src/views/saved_projects/SavedProjects.jsx new file mode 100644 index 000000000..746a92c55 --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/saved_projects/SavedProjects.jsx @@ -0,0 +1,185 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { connect } from 'react-redux'; + +import { toast } from 'react-toastify'; + +import { makeStyles } from '@material-ui/core/styles'; +import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; +import NavigateNextIcon from '@material-ui/icons/NavigateNext'; +import { + Grid, + Box, + ButtonGroup, + Typography, + Container, +} from '@material-ui/core'; + +import * as ProjectActions from '../../store/actions/projectActions'; +import CustomButton from '../../components/button/Button'; +import ErrorPage from '../error/ErrorPage'; +import LoadingPage from '../loading/LoadingPage'; +import Project from '../../components/project/Project'; +import styles from '../../assets/js/styles/views/saved_projects/savedProjectsStyles'; + +const useStyles = makeStyles(styles); + +const fetchPage = (page, props) => { + if (!props.auth.token) { + props.history.push('/login'); + } else { + return props.get_saved({ page, token: props.auth.token }); + } +}; + +const updateProjects = (res, { results: projects }) => { + return res + .then(res => { + if (res.project && res.project.title) { + projects = projects.map(project => + project.id === res.project.id ? res.project : project, + ); + return { results: projects }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + toast.warning(error.message); + return { loading: false }; + }); +}; + +function SavedProjects(props) { + const classes = useStyles(); + + const [state, setState] = React.useState({ + results: [], + prevPage: null, + nextPage: null, + loading: true, + }); + + React.useEffect(() => { + handleSetState(fetchPage(null, props)); + }, []); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { results: projects, prevPage, nextPage, loading } = state; + if (loading) { + return ; + } else if (projects && projects.length > 0) { + return ( + + + + + + Your saved projects + + + {projects.map(project => ( + + + handleSetState(updateProjects(res, state)) + } + {...props} + /> + + ))} + + + {prevPage ? ( + } + onClick={(e, page = prevPage.split('?')[1]) => { + handleSetState({ loading: true }); + handleSetState(fetchPage(page, props)); + }} + primaryButtonStyle + > + Prev + + ) : null} + {nextPage ? ( + } + onClick={(e, page = nextPage.split('?')[1]) => { + handleSetState({ loading: true }); + handleSetState(fetchPage(page, props)); + }} + primaryButtonStyle + > + Next + + ) : null} + + + + ); + } else { + return ; + } +} + +SavedProjects.propTypes = { + auth: PropTypes.object.isRequired, + get_saved: PropTypes.func.isRequired, + toggle_like: PropTypes.func.isRequired, + toggle_save: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + get_saved: value => { + return dispatch(ProjectActions.get_saved(value)); + }, + toggle_like: props => { + return dispatch(ProjectActions.toggle_like(props)); + }, + toggle_save: props => { + return dispatch(ProjectActions.toggle_save(props)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(SavedProjects); diff --git a/zubhub_frontend/zubhub/src/views/signup/Signup.jsx b/zubhub_frontend/zubhub/src/views/signup/Signup.jsx new file mode 100644 index 000000000..7c7d1998f --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/signup/Signup.jsx @@ -0,0 +1,501 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import clsx from 'clsx'; +import PropTypes from 'prop-types'; + +import { connect } from 'react-redux'; + +import { withFormik } from 'formik'; +import * as Yup from 'yup'; + +import { makeStyles } from '@material-ui/core/styles'; +import Visibility from '@material-ui/icons/Visibility'; +import VisibilityOff from '@material-ui/icons/VisibilityOff'; +import { + Grid, + Box, + Divider, + Container, + Card, + CardActionArea, + CardContent, + Select, + MenuItem, + Typography, + InputAdornment, + IconButton, + OutlinedInput, + Tooltip, + ClickAwayListener, + InputLabel, + FormHelperText, + FormControl, +} from '@material-ui/core'; + +import CustomButton from '../../components/button/Button'; +import * as AuthActions from '../../store/actions/authActions'; +import styles from '../../assets/js/styles/views/signup/signupStyles'; + +const useStyles = makeStyles(styles); + +const handleMouseDownPassword = e => { + e.preventDefault(); +}; + +const get_locations = props => { + return props.get_locations(); +}; + +const signup = (e, props) => { + e.preventDefault(); + if (props.values.location.length < 1) { + props.validateField('location'); + } else { + return props.signup(props); + } +}; + +const handleTooltipToggle = ({ toolTipOpen }) => { + return { toolTipOpen: !toolTipOpen }; +}; + +function Signup(props) { + const [state, setState] = React.useState({ + error: null, + locations: [], + showPassword1: false, + showPassword2: false, + toolTipOpen: false, + }); + + React.useEffect(() => { + handleSetState(get_locations(props)); + }, []); + + const classes = useStyles(); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { error, locations, toolTipOpen, showPassword1, showPassword2 } = state; + + return ( + + + + + +
handleSetState(signup(e, props))} + > + + Welcome to Zubhub + + + Create an account to submit a project + + + + + {error && ( + + {error} + + )} + + + + + + Username + + + handleSetState(handleTooltipToggle(state)) + } + > + + handleSetState(handleTooltipToggle(state)) + } + PopperProps={{ + disablePortal: true, + }} + open={toolTipOpen} + disableFocusListener + disableHoverListener + disableTouchListener + > + + handleSetState(handleTooltipToggle(state)) + } + onChange={props.handleChange} + onBlur={props.handleBlur} + labelWidth={90} + /> + + + + {props.touched['username'] && props.errors['username']} + + + + + + + + Email + + + + {props.touched['email'] && props.errors['email']} + + + + + + + + Date Of Birth + + + + {props.touched['dateOfBirth'] && + props.errors['dateOfBirth']} + + + + + + + + Location + + + + {props.touched['user_location'] && + props.errors['user_location']} + + + + + + + + Password + + + + setState({ + ...state, + showPassword1: !showPassword1, + }) + } + onMouseDown={handleMouseDownPassword} + edge="end" + > + {showPassword1 ? ( + + ) : ( + + )} + + + } + labelWidth={70} + /> + + {props.touched['password1'] && + props.errors['password1']} + + + + + + + + Confirm Password + + + + setState({ + ...state, + showPassword2: !showPassword2, + }) + } + onMouseDown={handleMouseDownPassword} + edge="end" + > + {showPassword2 ? ( + + ) : ( + + )} + + + } + labelWidth={70} + /> + + {props.touched['password2'] && + props.errors['password2']} + + + + + + Signup + + + +
+ + + + + + Already a Member ? + + + + + + + + Login + + + + +
+
+
+
+
+ ); +} + +Signup.propTypes = { + auth: PropTypes.object.isRequired, + set_auth_user: PropTypes.func.isRequired, + signup: PropTypes.func.isRequired, + get_locations: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + set_auth_user: auth_user => { + dispatch(AuthActions.setAuthUser(auth_user)); + }, + signup: props => { + return dispatch(AuthActions.signup(props)); + }, + get_locations: props => { + return dispatch(AuthActions.get_locations()); + }, + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)( + withFormik({ + mapPropsToValue: () => ({ + email: '', + user_location: '', + password1: '', + password2: '', + }), + validationSchema: Yup.object().shape({ + email: Yup.string().email('invalid email').required('invalid email'), + dateOfBirth: Yup.date() + .max(new Date(), "your date of birth can't be greater than today") + .required('please input your date of birth'), + user_location: Yup.string() + .min(1, 'your location is too short') + .required('please input your location'), + password1: Yup.string() + .min(8, 'your password is too short') + .required('input your password'), + password2: Yup.string() + .oneOf([Yup.ref('password1'), null], 'Passwords must match') + .required('input a confirmation password'), + }), + })(Signup), +); diff --git a/zubhub_frontend/zubhub/src/views/user_followers/UserFollowers.jsx b/zubhub_frontend/zubhub/src/views/user_followers/UserFollowers.jsx new file mode 100644 index 000000000..bcaff1dc9 --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/user_followers/UserFollowers.jsx @@ -0,0 +1,220 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import PropTypes from 'prop-types'; + +import { connect } from 'react-redux'; +import { toast } from 'react-toastify'; + +import { makeStyles } from '@material-ui/core/styles'; +import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; +import NavigateNextIcon from '@material-ui/icons/NavigateNext'; +import { + Grid, + Container, + Box, + Card, + ButtonGroup, + Typography, + Avatar, +} from '@material-ui/core'; + +import * as UserActions from '../../store/actions/userActions'; +import CustomButton from '../../components/button/Button'; +import ErrorPage from '../error/ErrorPage'; +import LoadingPage from '../loading/LoadingPage'; +import styles from '../../assets/js/styles/views/user_followers/userFollowersStyles'; + +const useStyles = makeStyles(styles); + +const fetchPage = (page, props) => { + const username = props.match.params.username; + return props.get_followers({ page, username }); +}; + +const toggle_follow = (e, props, state, id) => { + e.preventDefault(); + if (!props.auth.token) { + props.history.push('/login'); + } else { + return props + .toggle_follow({ id, token: props.auth.token }) + .then(res => { + if (res.profile && res.profile.username) { + const followers = state.followers.map(follower => + follower.id !== res.profile.id ? follower : res.profile, + ); + return { followers }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + toast.warning( + 'An error occured while performing this action. Please try again later', + ); + } else { + toast.warning(error.message); + } + return { loading: false }; + }); + } +}; + +const buildFollowers = (followers, classes, props, state, handleSetState) => + followers.map(follower => ( + + + + + {follower.id !== props.auth.id ? ( + + handleSetState(toggle_follow(e, props, state, id)) + } + primaryButtonStyle + > + {follower.followers.includes(props.auth.id) + ? 'Unfollow' + : 'Follow'} + + ) : null} + + {follower.username} + + + + + )); + +function UserFollowers(props) { + const classes = useStyles(); + + const [state, setState] = React.useState({ + followers: [], + prevPage: null, + nextPage: null, + loading: true, + }); + + React.useEffect(() => { + handleSetState(fetchPage(null, props)); + }, []); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { followers, prevPage, nextPage, loading } = state; + const username = props.match.params.username; + if (loading) { + return ; + } else if (followers && followers.length > 0) { + return ( + + + + + + {username}'s followers + + + {buildFollowers(followers, classes, props, state, handleSetState)} + + + {prevPage ? ( + } + onClick={(e, page = prevPage.split('?')[1]) => { + handleSetState({ loading: true }); + handleSetState(fetchPage(page, props)); + }} + primaryButtonStyle + > + Prev + + ) : null} + {nextPage ? ( + } + onClick={(e, page = nextPage.split('?')[1]) => { + handleSetState({ loading: true }); + handleSetState(fetchPage(page, props)); + }} + primaryButtonStyle + > + Next + + ) : null} + + + + ); + } else { + return ; + } +} + +UserFollowers.propTypes = { + auth: PropTypes.object.isRequired, + toggle_follow: PropTypes.func.isRequired, + get_followers: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + toggle_follow: value => { + return dispatch(UserActions.toggle_follow(value)); + }, + get_followers: value => { + return dispatch(UserActions.get_followers(value)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(UserFollowers); diff --git a/zubhub_frontend/zubhub/src/views/user_projects/UserProjects.jsx b/zubhub_frontend/zubhub/src/views/user_projects/UserProjects.jsx new file mode 100644 index 000000000..1e70c4ccb --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/user_projects/UserProjects.jsx @@ -0,0 +1,181 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { connect } from 'react-redux'; + +import { toast } from 'react-toastify'; + +import { makeStyles } from '@material-ui/core/styles'; +import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; +import NavigateNextIcon from '@material-ui/icons/NavigateNext'; +import { + Grid, + Box, + ButtonGroup, + Typography, + Container, +} from '@material-ui/core'; + +import * as ProjectActions from '../../store/actions/projectActions'; +import CustomButton from '../../components/button/Button'; +import ErrorPage from '../error/ErrorPage'; +import LoadingPage from '../loading/LoadingPage'; +import Project from '../../components/project/Project'; +import styles from '../../assets/js/styles/views/user_projects/userProjectsStyles'; + +const useStyles = makeStyles(styles); + +const fetchPage = (page, props) => { + const username = props.match.params.username; + return props.get_user_projects({ page, username }); +}; + +const updateProjects = (res, { results: projects }) => { + return res + .then(res => { + if (res.project && res.project.title) { + projects = projects.map(project => + project.id === res.project.id ? res.project : project, + ); + return { results: projects }; + } else { + res = Object.keys(res) + .map(key => res[key]) + .join('\n'); + throw new Error(res); + } + }) + .catch(error => { + toast.warning(error.message); + return { loading: false }; + }); +}; + +function UserProjects(props) { + const classes = useStyles(); + + const [state, setState] = React.useState({ + results: [], + prevPage: null, + nextPage: null, + loading: true, + }); + + React.useEffect(() => { + handleSetState(fetchPage(null, props)); + }, []); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { results: projects, prevPage, nextPage, loading } = state; + const username = props.match.params.username; + if (loading) { + return ; + } else if (projects && projects.length > 0) { + return ( + + + + + + {username}'s projects + + + {projects.map(project => ( + + + handleSetState(updateProjects(res, state)) + } + {...props} + /> + + ))} + + + {prevPage ? ( + } + onClick={(e, page = prevPage.split('?')[1]) => + handleSetState(fetchPage(page, props)) + } + primaryButtonStyle + > + Prev + + ) : null} + {nextPage ? ( + } + onClick={(e, page = nextPage.split('?')[1]) => + handleSetState(fetchPage(page, props)) + } + primaryButtonStyle + > + Next + + ) : null} + + + + ); + } else { + return ; + } +} + +UserProjects.propTypes = { + auth: PropTypes.object.isRequired, + get_user_projects: PropTypes.func.isRequired, + toggle_like: PropTypes.func.isRequired, + toggle_save: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + get_user_projects: values => { + return dispatch(ProjectActions.get_user_projects(values)); + }, + toggle_like: props => { + return dispatch(ProjectActions.toggle_like(props)); + }, + toggle_save: props => { + return dispatch(ProjectActions.toggle_save(props)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(UserProjects);