From 314f130e1efb77f8fd818e65bc50b42aad9cc99d Mon Sep 17 00:00:00 2001 From: Ndibe Raymond Olisaemeka Date: Thu, 28 Jan 2021 21:35:16 +0100 Subject: [PATCH 1/2] Phase2 -------- partial 5 (#98) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD (#75) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * phase 2 partial ------ 3 (#73) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch (#77) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch * made deploy_frontend.sh more explanatory * Code Refactor (#67) * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * Issue #54: switched from class based views to function based views, moved styles to seprate files and changed the general structure of the project to be more intuitive * more refactoring * more refactor -- added new prettier rules and prettified more files not being covered by prettier initially * Customized form submission error (#80) * fixed issue #25 --- initial * prettified * fixed issue #34: Increased upload image size, added image compression and functionality to remove image metadata (#79) * Removed line behind dob field label on the signup page (#81) * fixed issue #26: removed line behind DOB input label * fixed issue #26: removed line behind DOB field text in signup --- patch * fixed issue #52: Added help text to project creation desc field (#82) * fixed issue #59: Added projects, followers and following links to profile dropdown menu (#86) * added functionality to show creators we are following (#83) * added functionality to show creators we are following * fixed issue #58: added functionality to show creators we are following --- patch * First Iteration of CI/CD workflow (#89) * Added more actions to profile dropdown (#84) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD (#75) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * phase 2 partial ------ 3 (#73) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch (#77) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch * made deploy_frontend.sh more explanatory * Code Refactor (#67) * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * Issue #54: switched from class based views to function based views, moved styles to seprate files and changed the general structure of the project to be more intuitive * more refactoring * more refactor -- added new prettier rules and prettified more files not being covered by prettier initially * Customized form submission error (#80) * fixed issue #25 --- initial * prettified * fixed issue #34: Increased upload image size, added image compression and functionality to remove image metadata (#79) * Removed line behind dob field label on the signup page (#81) * fixed issue #26: removed line behind DOB input label * fixed issue #26: removed line behind DOB field text in signup --- patch * fixed issue #52: Added help text to project creation desc field (#82) * fixed issue #59: Added projects, followers and following links to profile dropdown menu * Revert "Added more actions to profile dropdown (#84)" (#85) This reverts commit 271408a0cf132863759bfd8a2d510b07c0106832. * ci/cd ---- test 1 * Second Iteration of CI/CD (#90) * Added more actions to profile dropdown (#84) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD (#75) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * phase 2 partial ------ 3 (#73) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch (#77) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch * made deploy_frontend.sh more explanatory * Code Refactor (#67) * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * Issue #54: switched from class based views to function based views, moved styles to seprate files and changed the general structure of the project to be more intuitive * more refactoring * more refactor -- added new prettier rules and prettified more files not being covered by prettier initially * Customized form submission error (#80) * fixed issue #25 --- initial * prettified * fixed issue #34: Increased upload image size, added image compression and functionality to remove image metadata (#79) * Removed line behind dob field label on the signup page (#81) * fixed issue #26: removed line behind DOB input label * fixed issue #26: removed line behind DOB field text in signup --- patch * fixed issue #52: Added help text to project creation desc field (#82) * fixed issue #59: Added projects, followers and following links to profile dropdown menu * Revert "Added more actions to profile dropdown (#84)" (#85) This reverts commit 271408a0cf132863759bfd8a2d510b07c0106832. * ci/cd ---- test 1 * ci/cd ---- test 2 * third iteration of CI/CD (#91) * Added more actions to profile dropdown (#84) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD (#75) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * phase 2 partial ------ 3 (#73) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch (#77) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch * made deploy_frontend.sh more explanatory * Code Refactor (#67) * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * Issue #54: switched from class based views to function based views, moved styles to seprate files and changed the general structure of the project to be more intuitive * more refactoring * more refactor -- added new prettier rules and prettified more files not being covered by prettier initially * Customized form submission error (#80) * fixed issue #25 --- initial * prettified * fixed issue #34: Increased upload image size, added image compression and functionality to remove image metadata (#79) * Removed line behind dob field label on the signup page (#81) * fixed issue #26: removed line behind DOB input label * fixed issue #26: removed line behind DOB field text in signup --- patch * fixed issue #52: Added help text to project creation desc field (#82) * fixed issue #59: Added projects, followers and following links to profile dropdown menu * Revert "Added more actions to profile dropdown (#84)" (#85) This reverts commit 271408a0cf132863759bfd8a2d510b07c0106832. * ci/cd ---- test 1 * ci/cd ---- test 2 * ci/cd ---- test 3 * Update build_deploy_backend.yml manually added manual workflow dispatch to github actions file * Fourth Iteration of CI/CD (#92) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch * made deploy_frontend.sh more explanatory * Added more actions to profile dropdown (#84) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD (#75) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * phase 2 partial ------ 3 (#73) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch (#77) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch * made deploy_frontend.sh more explanatory * Code Refactor (#67) * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * Issue #54: switched from class based views to function based views, moved styles to seprate files and changed the general structure of the project to be more intuitive * more refactoring * more refactor -- added new prettier rules and prettified more files not being covered by prettier initially * Customized form submission error (#80) * fixed issue #25 --- initial * prettified * fixed issue #34: Increased upload image size, added image compression and functionality to remove image metadata (#79) * Removed line behind dob field label on the signup page (#81) * fixed issue #26: removed line behind DOB input label * fixed issue #26: removed line behind DOB field text in signup --- patch * fixed issue #52: Added help text to project creation desc field (#82) * fixed issue #59: Added projects, followers and following links to profile dropdown menu * Revert "Added more actions to profile dropdown (#84)" (#85) This reverts commit 271408a0cf132863759bfd8a2d510b07c0106832. * ci/cd ---- test 1 * ci/cd ---- test 2 * ci/cd ---- test 3 * Added functionalities to update and delete projects (#88) * fixed issue #87: Added project edit functionality * fixed issue #87: Added project edit and delete functionality --- patch * Internationalization (#74) * issue #55: internationalization * issue #55: internationalization --- patch * issue #55: internationalization --- changed snake-case to camel-case * Added more actions to profile dropdown (#84) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD (#75) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * phase 2 partial ------ 3 (#73) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch (#77) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch * made deploy_frontend.sh more explanatory * Code Refactor (#67) * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * Issue #54: switched from class based views to function based views, moved styles to seprate files and changed the general structure of the project to be more intuitive * more refactoring * more refactor -- added new prettier rules and prettified more files not being covered by prettier initially * Customized form submission error (#80) * fixed issue #25 --- initial * prettified * fixed issue #34: Increased upload image size, added image compression and functionality to remove image metadata (#79) * Removed line behind dob field label on the signup page (#81) * fixed issue #26: removed line behind DOB input label * fixed issue #26: removed line behind DOB field text in signup --- patch * fixed issue #52: Added help text to project creation desc field (#82) * fixed issue #59: Added projects, followers and following links to profile dropdown menu * Revert "Added more actions to profile dropdown (#84)" (#85) This reverts commit 271408a0cf132863759bfd8a2d510b07c0106832. * fixed merge conflicts while merging i18n to phase2 and made other improvements * Added feature for editing and deleting of user profiles (#97) * Added more actions to profile dropdown (#84) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD (#75) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * phase 2 partial ------ 3 (#73) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed bug that occurs when user submit google drive video link (#72) * added functionality to format youtube video url to embedable format * made video url optional * switched image upload location from cloudinary to digital ocean spaces * added functionality to automatically delete image from digitalocean space once image is deleted from db * added image count indicator and made video optional. also added project create button to navbar * removed .ssl from git * untracked .ssl-data * added support for various forms of youtube video url, vimeo and google drive * fixed issues #35, #33, #32, #30, #29 * fixed issue #46 * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * fixed issue #68 * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch (#77) * domain setup step 1 test 5 * domain setup step 2(backend) test 1 * domain setup step2(backend) test2 * removed .ssl-data from .dockerignore * added custom nginx container to handle reverse proxying and https requests * made important changes to deploy_frontend.sh, added google tracking code to index.html, enabled crawling * switched handling of ssl back to valian/docker-nginx-auto-ssl * separated docker-compose files into dev and prod in preparation for CI/CD * separated docker-compose files into dev and prod in preparation for CI/CD -- backend * separated docker-compose files into dev and prod in preparation for CI/CD --- patch * made deploy_frontend.sh more explanatory * Code Refactor (#67) * phase2 patial ----- 2 (#66) * added description to the video url field in project creation form issue #50 (#61) * new deployment changes (#62) * increased pagination limit from 6 to 20 (#63) * phase2 patial ----- 2 (#65) switched handling of ssl back to valian/docker-nginx-auto-ssl * Issue #54: switched from class based views to function based views, moved styles to seprate files and changed the general structure of the project to be more intuitive * more refactoring * more refactor -- added new prettier rules and prettified more files not being covered by prettier initially * Customized form submission error (#80) * fixed issue #25 --- initial * prettified * fixed issue #34: Increased upload image size, added image compression and functionality to remove image metadata (#79) * Removed line behind dob field label on the signup page (#81) * fixed issue #26: removed line behind DOB input label * fixed issue #26: removed line behind DOB field text in signup --- patch * fixed issue #52: Added help text to project creation desc field (#82) * fixed issue #59: Added projects, followers and following links to profile dropdown menu * Revert "Added more actions to profile dropdown (#84)" (#85) This reverts commit 271408a0cf132863759bfd8a2d510b07c0106832. * fixed issue #57: Made user profile details fully editable, and allowed creators to delete their accounts * merged phase2 into make_profile_fully_editable * prettified * phase2 ----- partial 5 ---- patch --- zubhub_backend/zubhub/creators/serializers.py | 4 +- zubhub_backend/zubhub/creators/urls.py | 1 + zubhub_backend/zubhub/creators/views.py | 14 +- .../zubhub/locale/hi/LC_MESSAGES/django.mo | Bin 0 -> 2692 bytes .../zubhub/locale/hi/LC_MESSAGES/django.po | 86 ++++ zubhub_backend/zubhub/projects/admin.py | 29 +- zubhub_backend/zubhub/projects/models.py | 3 + zubhub_backend/zubhub/projects/permissions.py | 8 + zubhub_backend/zubhub/projects/serializers.py | 30 ++ zubhub_backend/zubhub/projects/signals.py | 12 +- zubhub_backend/zubhub/projects/tasks.py | 21 + zubhub_backend/zubhub/projects/urls.py | 2 + zubhub_backend/zubhub/projects/views.py | 28 +- zubhub_backend/zubhub/zubhub/settings.py | 3 + zubhub_frontend/zubhub/package-lock.json | 51 ++ zubhub_frontend/zubhub/package.json | 4 + .../zubhub/public/locales/en/translation.json | 448 +++++++++++++++++ .../zubhub/public/locales/hi/translation.json | 450 +++++++++++++++++ zubhub_frontend/zubhub/src/App.js | 51 +- zubhub_frontend/zubhub/src/api/api.js | 78 ++- .../zubhub/src/assets/js/dFormatter.js | 24 +- .../zubhub/src/assets/js/languageMap.json | 4 + .../styles/components/button/buttonStyles.js | 9 + .../zubhub/src/assets/js/styles/index.js | 3 + .../create_project/createProjectStyles.js | 10 + .../views/edit_profile/editProfileStyles.js | 97 ++++ .../views/page_wrapper/pageWrapperStyles.js | 4 + .../zubhub/src/components/button/Button.js | 3 + .../zubhub/src/components/project/Project.jsx | 7 +- zubhub_frontend/zubhub/src/i18n.js | 28 ++ zubhub_frontend/zubhub/src/index.js | 10 +- .../zubhub/src/store/actions/authActions.js | 89 ++-- .../src/store/actions/projectActions.js | 92 ++-- .../zubhub/src/store/actions/userActions.js | 72 +-- .../zubhub/src/views/PageWrapper.jsx | 59 ++- .../views/create_project/CreateProject.jsx | 351 ++++++++----- .../src/views/edit_profile/EditProfile.jsx | 464 ++++++++++++++++++ .../src/views/email_confirm/EmailConfirm.jsx | 51 +- .../zubhub/src/views/login/Login.jsx | 105 ++-- .../views/password_reset/PasswordReset.jsx | 97 ++-- .../PasswordResetConfirm.jsx | 93 ++-- .../zubhub/src/views/profile/Profile.jsx | 235 +++++---- .../views/project_details/ProjectDetails.jsx | 197 ++++++-- .../zubhub/src/views/projects/Projects.jsx | 31 +- .../views/saved_projects/SavedProjects.jsx | 27 +- .../zubhub/src/views/signup/Signup.jsx | 203 +++++--- .../views/user_followers/UserFollowers.jsx | 31 +- .../views/user_following/UserFollowing.jsx | 35 +- .../src/views/user_projects/UserProjects.jsx | 35 +- 49 files changed, 3063 insertions(+), 726 deletions(-) create mode 100644 zubhub_backend/zubhub/locale/hi/LC_MESSAGES/django.mo create mode 100644 zubhub_backend/zubhub/locale/hi/LC_MESSAGES/django.po create mode 100644 zubhub_backend/zubhub/projects/permissions.py create mode 100644 zubhub_backend/zubhub/projects/tasks.py create mode 100644 zubhub_frontend/zubhub/public/locales/en/translation.json create mode 100644 zubhub_frontend/zubhub/public/locales/hi/translation.json create mode 100644 zubhub_frontend/zubhub/src/assets/js/languageMap.json create mode 100644 zubhub_frontend/zubhub/src/assets/js/styles/views/edit_profile/editProfileStyles.js create mode 100644 zubhub_frontend/zubhub/src/i18n.js create mode 100644 zubhub_frontend/zubhub/src/views/edit_profile/EditProfile.jsx diff --git a/zubhub_backend/zubhub/creators/serializers.py b/zubhub_backend/zubhub/creators/serializers.py index 316d54749..9a47d4c22 100644 --- a/zubhub_backend/zubhub/creators/serializers.py +++ b/zubhub_backend/zubhub/creators/serializers.py @@ -13,10 +13,12 @@ class CreatorSerializer(serializers.ModelSerializer): id = serializers.UUIDField(read_only=True) followers = serializers.SlugRelatedField( slug_field="id", read_only=True, many=True) + location = serializers.SlugRelatedField( + slug_field='name', queryset=Location.objects.all()) class Meta: model = Creator - fields = ('id', 'username', 'email', 'avatar', + fields = ('id', 'username', 'email', 'avatar', 'location', 'dateOfBirth', 'bio', 'followers', 'following_count', 'projects_count') diff --git a/zubhub_backend/zubhub/creators/urls.py b/zubhub_backend/zubhub/creators/urls.py index 51b3181e0..7f28060bc 100644 --- a/zubhub_backend/zubhub/creators/urls.py +++ b/zubhub_backend/zubhub/creators/urls.py @@ -6,6 +6,7 @@ urlpatterns = [ path('authUser/', auth_user_api_view, name='auth_user_detail'), path('edit_creator/', EditCreatorAPIView.as_view(), name='edit_creator'), + path('delete/', DeleteCreatorAPIView.as_view(), name='delete_creator'), path('locations/', LocationListAPIView.as_view(), name='location_list'), path('/projects/', UserProjectsAPIView.as_view(), name="user_projects"), diff --git a/zubhub_backend/zubhub/creators/views.py b/zubhub_backend/zubhub/creators/views.py index 7696d1fe6..20039579a 100644 --- a/zubhub_backend/zubhub/creators/views.py +++ b/zubhub_backend/zubhub/creators/views.py @@ -1,5 +1,5 @@ from rest_framework.decorators import api_view, permission_classes -from rest_framework.generics import UpdateAPIView, RetrieveAPIView, ListAPIView +from rest_framework.generics import UpdateAPIView, RetrieveAPIView, ListAPIView, DestroyAPIView from rest_framework.permissions import IsAuthenticated, AllowAny, IsAuthenticatedOrReadOnly from rest_framework.response import Response from projects.serializers import ProjectListSerializer @@ -44,6 +44,18 @@ def get_object(self): return obj +class DeleteCreatorAPIView(DestroyAPIView): + queryset = Creator.objects.all() + serializer_class = CreatorSerializer + permission_classes = [IsAuthenticated, IsOwner] + lookup_field = "pk" + + def get_object(self): + obj = self.queryset.get(pk=self.request.user.pk) + self.check_object_permissions(self.request, obj) + return obj + + class UserProjectsAPIView(ListAPIView): serializer_class = ProjectListSerializer permission_classes = [AllowAny] diff --git a/zubhub_backend/zubhub/locale/hi/LC_MESSAGES/django.mo b/zubhub_backend/zubhub/locale/hi/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..f19dd49729b09c667f5573def9b706ceb84fcdc6 GIT binary patch literal 2692 zcmbuA-)|IE6vwZMzgE=57&XSk3(-(Qr-}~-7fL9wg~t9$Suh|88Fp@WM|bDenb~6F z3u06ZF}(02K@5`27D7THBp{6jq7VKHzL|OP!I=2Yo1b%M+fpe{PP+TubMLw5e9!ls zJN@H@Ri_ot^L$?8bBfQ)d{#fg7f%EH3=F}?!9T%X@Ne)L@RdiEdJ%jBTnoMjmcfJI zbKqI=JBH~d7`!)X)g8QYE%jp;`VNBtm(O_J!;>Y9xrQ%i|#!dUyY znr%mEUrFhz@mp%ApBil^_4Y7M8@km_Qqt$Y6E{}U7`d0)Na#kT0I%tmR7^I z=BJ^JG<;+8Njr>9pvIfVhwu&?O@?txQ(depMKJ>fqXn0`6>f0Sla`=Ir1mMlZm1#9 zwT88X@pWX5u$m-CY#fwT6+$0NwT?nzCY+B^NX|>PBuvd3wv1L)SGyzPRE22G#z>v^ z>LR^*i2TdcmwZy7Nm3KQX@L)miAi00T!aH%>dLGAR7>z;HS$}gSBH_Vi8wf~o0hWU zx=m9Pm6c?0%1_01It+|9A;QEuY#~TbvR+f6tXtiVn`%6^A5nuh6nOhgoX~=@9v|58 z{=lw^-dCB}Gc-0@nlMu~PQ8(&9tPg_c0KW`wyaCzW8Q?B4&{s|jW6r{m4OLwWX#(+ zpm&dzM}{^W7#ppWhW#YO+U49Sq~0P?5XO}AY?Ws{F#=eNn0W@YhwkSq~44?%FHtwhRw%YleLkzT{KZjgUa4k)e^wQau~{-Yzjjr~{9g zDTt=#)ATJSKa4gr9)67U{d=o}-lio#;X7$!uM*X4z&w=oro&-c8gI5^zv&IyxRsQ3 zH08{s|DDac_}SkR={5a&C>^Xz(_ z-C6jZv3i;%faQ5S`2`%$vtw>c4qVGSC$OF8T~@!5)7{mx#d#MvBk+UwJkKr{VTGCV zzC(=JJiF^yicusa>}A<^9L_kBSz)kt0=M&y$HhGRc`;_!`-Y2l5}%Vq0RCHdwiHP! zyqv0gvX{8SL8& zl-)rpLplTy*K@9>`GTujbfxGyXON6uNVH-To0Qm>IowT0@|$^f%jr#z@pq~h8gdM` zFS_la8F}`lGrAq1tM?DXeWD0GrsMNG`_YBIP?Rd8K|M5oh0yRu6avCi7;>J}#rYqc z?dA$0DS^JoK%F8W(Crcts6l8l7&zT_#gUPS7g;^$7zue!m(`+T_~CukeT!B|ih?Xj z8#q_&bLawwND3?)+G3fB?+Ww47+rE((10I$6MA-ya@M?FK$nLGOGcUCUXYw5@Am3p zg07sEuyR8PLQZ#gmv>7<`QKMpGAvfTY&Mpz(8|3iCS0a_nXJS>L*|BkCjAoh5qyQK zP1NE&^1vH|d*c+ALGFsMa@X8yF&*8ZXMkwUoG6cIv&GPMRlMr%xV-krD6fX%E#u~> ukbySIYvnuFV!7GfVZ7-sGw&H!20r}TVt4WFNglEmmr`0HEZARuz5fM}&ikMM literal 0 HcmV?d00001 diff --git a/zubhub_backend/zubhub/locale/hi/LC_MESSAGES/django.po b/zubhub_backend/zubhub/locale/hi/LC_MESSAGES/django.po new file mode 100644 index 000000000..741b76328 --- /dev/null +++ b/zubhub_backend/zubhub/locale/hi/LC_MESSAGES/django.po @@ -0,0 +1,86 @@ +# Translation of custom error messages to hindi. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST Ndibe Raymond , 2021. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-01-10 06:00+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FIRST Ndibe Raymond \n" +"Language-Team: LANGUAGE \n" +"Language: hindi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: zubhub/creators/serializers.py:46 +msgid "Date of Birth must be less than today's date" +msgstr "जन्म तिथि आज की तारीख से कम होनी चाहिए" + +#: zubhub/creators/serializers.py:51 +msgid "Location is required" +msgstr "स्थान की आवश्यकता है" + +#: zubhub/projects/serializers.py:68 zubhub/projects/serializers.py:74 +msgid "you must provide either image(s) or video url" +msgstr "आपको छवि या वीडियो url प्रदान करना चाहिए" + +#: zubhub/templates/account/email/email_confirmation_message.txt:6 +msgid "Hello from " +msgstr "से नमस्कार " + +#: zubhub/templates/account/email/email_confirmation_message.txt:7 +#, python-format +msgid "" +"\n" +"You're receiving this e-mail because user %(user_display)s has given yours " +"as an e-mail address to connect their account.\n" +msgstr "" +"\n" +"आप यह ई-मेल प्राप्त कर रहे हैं क्योंकि उपयोगकर्ता %(user_display)s ने आपका दिया है" +"उनके खाते को जोड़ने के लिए एक ई-मेल पते के रूप में। \n" + +#: zubhub/templates/account/email/email_confirmation_message.txt:10 +msgid "To confirm this is correct, go to " +msgstr "यह सही है, इसकी पुष्टि करने के लिए " + +#: zubhub/templates/account/email/email_confirmation_message.txt:12 +msgid "Thank you from" +msgstr "से साभार" + +#: zubhub/templates/account/email/email_confirmation_subject.txt:3 +msgid "Please Confirm Your E-mail Address" +msgstr "कृपया अपने ईमेल पते की पुष्टि करें" + +#: zubhub/templates/registration/password_reset_email.html:4 +msgid "" +"You're receiving this email because you requested a password reset for your " +"user account at " +msgstr "" +"आप यह ईमेल प्राप्त कर रहे हैं क्योंकि आपने अपने लिए पासवर्ड रीसेट का अनुरोध किया है " +"उपयोगकर्ता खाता " + +#: zubhub/templates/registration/password_reset_email.html:5 +msgid "Please go to the following page and choose a new password:" +msgstr "कृपया निम्न पृष्ठ पर जाएं और एक नया पासवर्ड चुनें:" + +#: zubhub/templates/registration/password_reset_email.html:10 +msgid "Your username, in case you've forgotten:" +msgstr "आपका उपयोगकर्ता नाम, यदि आप भूल गए हैं:" + +#: zubhub/templates/registration/password_reset_email.html:11 +msgid "Thanks for using our site!" +msgstr "हमारी साइट का उपयोग करने के लिए धन्यवाद!" + +#: zubhub/templates/registration/password_reset_email.html:12 +msgid "The " +msgstr "यह " + +#: zubhub/templates/registration/password_reset_email.html:12 +msgid " Team" +msgstr "टीम" diff --git a/zubhub_backend/zubhub/projects/admin.py b/zubhub_backend/zubhub/projects/admin.py index 078f12939..a13bf20a8 100644 --- a/zubhub_backend/zubhub/projects/admin.py +++ b/zubhub_backend/zubhub/projects/admin.py @@ -8,6 +8,21 @@ admin.site.index_title = "ZubHub Administration" +class ImageAdmin(admin.ModelAdmin): + # model = Image + search_fields = ["project__title", "image_url"] + list_display = ["image_url"] + + +class CommentAdmin(admin.ModelAdmin): + # model = Comment + list_display = [ + "text", "created_on"] + search_fields = ["project__tite", "creator__username", + "text", "created_on"] + list_filter = ["created_on"] + + class ProjectImages(admin.StackedInline): model = Image @@ -17,11 +32,11 @@ class ProjectComments(admin.StackedInline): class ProjectAdmin(admin.ModelAdmin): - list_display = ("title", "creator", "views_count", "likes_count", - "comments_count", "created_on", "published") - search_fields = ("title", 'creator__username', 'creator__email', - "created_on") - list_filter = ('created_on', "published") + list_display = ["title", "creator", "views_count", "likes_count", + "comments_count", "created_on", "published"] + search_fields = ["title", 'creator__username', 'creator__email', + "created_on"] + list_filter = ['created_on', "published"] inlines = [ProjectImages, ProjectComments] def get_readonly_fields(self, request, obj=None): @@ -29,5 +44,5 @@ def get_readonly_fields(self, request, obj=None): admin.site.register(Project, ProjectAdmin) -admin.site.register(Image) -admin.site.register(Comment) +admin.site.register(Image, ImageAdmin) +admin.site.register(Comment, CommentAdmin) diff --git a/zubhub_backend/zubhub/projects/models.py b/zubhub_backend/zubhub/projects/models.py index 0c65fb0f4..75944bc65 100644 --- a/zubhub_backend/zubhub/projects/models.py +++ b/zubhub_backend/zubhub/projects/models.py @@ -96,6 +96,9 @@ class Comment(models.Model): text = models.CharField(max_length=10000) created_on = models.DateTimeField(default=timezone.now) + def __str__(self): + return self.text + def save(self, *args, **kwargs): self.project.save() super().save(*args, **kwargs) diff --git a/zubhub_backend/zubhub/projects/permissions.py b/zubhub_backend/zubhub/projects/permissions.py new file mode 100644 index 000000000..02c58ab22 --- /dev/null +++ b/zubhub_backend/zubhub/projects/permissions.py @@ -0,0 +1,8 @@ +from rest_framework.permissions import BasePermission + + +class IsOwner(BasePermission): + message = "You must be the owner of this object to perform this function" + + def has_object_permission(self, request, view, object): + return object.creator.pk == request.user.pk diff --git a/zubhub_backend/zubhub/projects/serializers.py b/zubhub_backend/zubhub/projects/serializers.py index b60192385..c3590c084 100644 --- a/zubhub_backend/zubhub/projects/serializers.py +++ b/zubhub_backend/zubhub/projects/serializers.py @@ -3,6 +3,7 @@ from django.contrib.auth import get_user_model from creators.serializers import CreatorSerializer from .models import Project, Comment, Image +import time Creator = get_user_model() @@ -81,6 +82,35 @@ def create(self, validated_data): Image.objects.create(project=project, **image) return project + def update(self, project, validated_data): + images_data = validated_data.pop('images') + + project.title = validated_data.pop("title") + project.description = validated_data.pop("description") + project.video = validated_data.pop("video") + project.materials_used = validated_data.pop("materials_used") + + project.save() + + images = project.images.all() + images_to_save = [] + if len(images) != len(images_data): + for image_dict in images_data: + exist = False + for image in images: + if image_dict["image_url"] == image.image_url: + exist = True + if not exist: + images_to_save.append(image_dict) + + for image in images: + image.delete() + + for image in images_to_save: + Image.objects.create(project=project, **image) + + return project + class ProjectListSerializer(serializers.ModelSerializer): creator = CreatorSerializer(read_only=True) diff --git a/zubhub_backend/zubhub/projects/signals.py b/zubhub_backend/zubhub/projects/signals.py index 9336a1161..29d029d9c 100644 --- a/zubhub_backend/zubhub/projects/signals.py +++ b/zubhub_backend/zubhub/projects/signals.py @@ -1,7 +1,6 @@ from django.db.models.signals import pre_delete, post_save from django.dispatch import receiver -import boto3 -from django.conf import settings +from .tasks import delete_image_from_DO_space from .models import Project, Image @@ -13,11 +12,4 @@ def project_saved(sender, instance, **kwargs): @receiver(pre_delete, sender=Image) def image_to_be_deleted(sender, instance, **kwargs): - session = boto3.session.Session() - client = session.client('s3', - region_name=settings.DOSPACE_REGION, - endpoint_url=settings.DOSPACE_ENDPOINT_URL, - aws_access_key_id=settings.DOSPACE_ACCESS_KEY_ID, - aws_secret_access_key=settings.DOSPACE_ACCESS_SECRET_KEY) - - client.delete_object(Bucket="zubhub", Key=instance.public_id) + delete_image_from_DO_space.delay("zubhub", instance.public_id) diff --git a/zubhub_backend/zubhub/projects/tasks.py b/zubhub_backend/zubhub/projects/tasks.py new file mode 100644 index 000000000..1c7e06a61 --- /dev/null +++ b/zubhub_backend/zubhub/projects/tasks.py @@ -0,0 +1,21 @@ +import boto3 +from django.conf import settings +from celery import shared_task + +from random import uniform + + +@shared_task(bind=True, acks_late=True, max_retries=10) +def delete_image_from_DO_space(self, bucket, key): + session = boto3.session.Session() + client = session.client('s3', + region_name=settings.DOSPACE_REGION, + endpoint_url=settings.DOSPACE_ENDPOINT_URL, + aws_access_key_id=settings.DOSPACE_ACCESS_KEY_ID, + aws_secret_access_key=settings.DOSPACE_ACCESS_SECRET_KEY) + + try: + client.delete_object(Bucket=bucket, Key=key) + except Exception as e: + raise self.retry(exc=e, countdown=int( + uniform(2, 4) ** self.request.retries)) diff --git a/zubhub_backend/zubhub/projects/urls.py b/zubhub_backend/zubhub/projects/urls.py index 12d3ce73c..39900b4cc 100644 --- a/zubhub_backend/zubhub/projects/urls.py +++ b/zubhub_backend/zubhub/projects/urls.py @@ -6,6 +6,8 @@ urlpatterns = [ path('', ProjectListAPIView.as_view(), name='list_projects'), path('create/', ProjectCreateAPIView.as_view(), name='create_project'), + path('/update/', ProjectUpdateAPIView.as_view(), name='update_project'), + path('/delete/', ProjectDeleteAPIView.as_view(), name='delete_project'), path('saved/', SavedProjectsAPIView.as_view(), name="saved_projects"), path('/toggle_like/', ToggleLikeAPIView.as_view(), name="toggle_like"), diff --git a/zubhub_backend/zubhub/projects/views.py b/zubhub_backend/zubhub/projects/views.py index c187e110e..72ce4f044 100644 --- a/zubhub_backend/zubhub/projects/views.py +++ b/zubhub_backend/zubhub/projects/views.py @@ -2,9 +2,9 @@ from rest_framework.response import Response from django.contrib.auth.models import AnonymousUser from rest_framework import status -from rest_framework.generics import CreateAPIView, ListAPIView, RetrieveAPIView +from rest_framework.generics import UpdateAPIView, CreateAPIView, ListAPIView, RetrieveAPIView, DestroyAPIView from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly, AllowAny -from creators.permissions import IsOwner +from projects.permissions import IsOwner from .models import Project from .serializers import ProjectSerializer, ProjectListSerializer, CommentSerializer from .pagination import ProjectNumberPagination @@ -19,6 +19,26 @@ def perform_create(self, serializer): serializer.save(creator=self.request.user) +class ProjectUpdateAPIView(UpdateAPIView): + queryset = Project.objects.all() + serializer_class = ProjectSerializer + permission_classes = [IsAuthenticated, IsOwner] + + def perform_update(self, serializer): + serializer.save(creator=self.request.user) + + +class ProjectDeleteAPIView(DestroyAPIView): + queryset = Project.objects.all() + serializer_class = ProjectSerializer + permission_classes = [IsAuthenticated, IsOwner] + + def delete(self, request, *args, **kwargs): + result = self.destroy(request, *args, **kwargs) + request.user.save() + return result + + class ProjectListAPIView(ListAPIView): queryset = Project.objects.filter(published=True).order_by("-created_on") serializer_class = ProjectListSerializer @@ -62,7 +82,7 @@ def get_queryset(self): class ToggleLikeAPIView(RetrieveAPIView): serializer_class = ProjectSerializer - permission_classes = [IsAuthenticatedOrReadOnly] + permission_classes = [IsAuthenticated] def get_queryset(self): return Project.objects.filter(published=True) @@ -83,7 +103,7 @@ def get_object(self): class ToggleSaveAPIView(RetrieveAPIView): serializer_class = ProjectSerializer - permission_classes = [IsAuthenticatedOrReadOnly] + permission_classes = [IsAuthenticated] def get_queryset(self): return Project.objects.filter(published=True) diff --git a/zubhub_backend/zubhub/zubhub/settings.py b/zubhub_backend/zubhub/zubhub/settings.py index 7c90ca13e..59777cd46 100644 --- a/zubhub_backend/zubhub/zubhub/settings.py +++ b/zubhub_backend/zubhub/zubhub/settings.py @@ -153,6 +153,7 @@ 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -229,6 +230,8 @@ USE_TZ = True +LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')] + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.2/howto/static-files/ diff --git a/zubhub_frontend/zubhub/package-lock.json b/zubhub_frontend/zubhub/package-lock.json index 20d60cf5d..0b9c61675 100644 --- a/zubhub_frontend/zubhub/package-lock.json +++ b/zubhub_frontend/zubhub/package-lock.json @@ -7222,6 +7222,14 @@ "terser": "^4.6.3" } }, + "html-parse-stringify2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz", + "integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=", + "requires": { + "void-elements": "^2.0.1" + } + }, "html-webpack-plugin": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz", @@ -7454,6 +7462,30 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, + "i18next": { + "version": "19.8.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.8.4.tgz", + "integrity": "sha512-FfVPNWv+felJObeZ6DSXZkj9QM1Ivvh7NcFCgA8XPtJWHz0iXVa9BUy+QY8EPrCLE+vWgDfV/sc96BgXVo6HAA==", + "requires": { + "@babel/runtime": "^7.12.0" + } + }, + "i18next-browser-languagedetector": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.0.1.tgz", + "integrity": "sha512-3H+OsNQn3FciomUU0d4zPFHsvJv4X66lBelXk9hnIDYDsveIgT7dWZ3/VvcSlpKk9lvCK770blRZ/CwHMXZqWw==", + "requires": { + "@babel/runtime": "^7.5.5" + } + }, + "i18next-http-backend": { + "version": "1.0.23", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.0.23.tgz", + "integrity": "sha512-2iXwUmawM4kozvGN+k7G9u/bYQdgqtTXVK0cWvLSOpUCTaW30ZzRhgu0FBfinb71XjwUEvdqb95jNrEFjrwGKw==", + "requires": { + "node-fetch": "2.6.1" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -9847,6 +9879,11 @@ "tslib": "^1.10.0" } }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", @@ -12104,6 +12141,15 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" }, + "react-i18next": { + "version": "11.8.5", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.8.5.tgz", + "integrity": "sha512-2jY/8NkhNv2KWBnZuhHxTn13aMxAbvhiDUNskm+1xVVnrPId78l8fA7fCyVeO3XU1kptM0t4MtvxV1Nu08cjLw==", + "requires": { + "@babel/runtime": "^7.3.1", + "html-parse-stringify2": "2.0.1" + } + }, "react-is": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", @@ -15095,6 +15141,11 @@ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/zubhub_frontend/zubhub/package.json b/zubhub_frontend/zubhub/package.json index 472e0d408..5aa6fad11 100644 --- a/zubhub_frontend/zubhub/package.json +++ b/zubhub_frontend/zubhub/package.json @@ -15,9 +15,13 @@ "compressorjs": "^1.0.7", "date-fns": "^2.16.1", "formik": "^2.2.5", + "i18next": "^19.8.4", + "i18next-browser-languagedetector": "^6.0.1", + "i18next-http-backend": "^1.0.23", "nanoid": "^3.1.20", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-i18next": "^11.8.5", "react-redux": "^7.2.2", "react-router-dom": "^5.2.0", "react-scripts": "4.0.0", diff --git a/zubhub_frontend/zubhub/public/locales/en/translation.json b/zubhub_frontend/zubhub/public/locales/en/translation.json new file mode 100644 index 000000000..80dea64f3 --- /dev/null +++ b/zubhub_frontend/zubhub/public/locales/en/translation.json @@ -0,0 +1,448 @@ +{ + "date": { + "years": "years", + "year": "year", + "months": "months", + "month": "month", + "days": "days", + "day": "day", + "hours": "hours", + "hour": "hour", + "minutes": "minutes", + "minute": "minute", + "seconds": "seconds", + "second": "second", + "ago": "ago" + }, + + "pageWrapper": { + "navbar": { + "login": "Login", + "signup": "Sign Up", + "createProject": "Create Project", + "projects": "Projects", + "followers": "Followers", + "following": "Following", + "savedProjects": "Saved Projects", + "logout": "Logout" + }, + "errors": { + "unexpected": "an error occured while getting user profile, please try again later", + "logoutFailed": "An error occured while signing you out. please try again" + } + }, + + "signup": { + "welcomeMsg": { + "primary": "Welcome to Zubhub", + "secondary": "Create an account to submit a project" + }, + "inputs": { + "username": { + "label": "Username", + "errors": { + "required": "this field is required" + } + }, + "email": { + "label": "Email", + "errors": { + "invalid": "invalid email", + "required": "this field is required" + } + }, + "dateOfBirth": { + "label": "Date Of Birth", + "errors": { + "max": "your date of birth can't be greater than today", + "required": "this field is required" + } + }, + "location": { + "label": "Location", + "errors": { + "min": "your location is too short", + "required": "this field is required" + } + }, + "password1": { + "label": "Password", + "errors": { + "min": "your password is too short", + "required": "this field is required" + } + }, + "password2": { + "label": "Confirm Password", + "errors": { + "noMatch": "Passwords must match", + "required": "this field is required" + } + }, + "submit": "Signup" + }, + "alreadyAMember": "Already a Member ?", + "login": "Login", + "errors": { + "unexpected": "An error occured while performing this action. Please try again later" + }, + "ariaLabels": { + "togglePasswordVisibility": "toggle password visibility" + }, + "tooltips": { + "noRealName": "Do not use your real name here!" + } + }, + + "login": { + "welcomeMsg": { + "primary": "Welcome to Zubhub", + "secondary": "Login to get started!" + }, + "inputs": { + "username": { + "label": "Username Or Email", + "errors": { + "required": "this field is required" + } + }, + "password": { + "label": "Password", + "errors": { + "min": "your password is too short", + "required": "this field is required" + } + }, + "submit": "Login" + }, + "notAMember": "Not a Member ?", + "signup": "Sign Up", + "forgotPassword": "Forgot Password?", + "errors": { + "unexpected": "An error occured while performing this action. Please try again later" + } + }, + + "passwordReset": { + "welcomeMsg": { + "primary": "Password Reset", + "secondary": "Input your email so we can send you a password reset link" + }, + "inputs": { + "email": { + "label": "Email", + "errors": { + "invalid": "invalid email", + "required": "this field is required" + } + }, + "submit": "Send Reset Link" + }, + "toastSuccess": "We just sent a password reset link to your email!", + "errors": { + "unexpected": "An error occured while performing this action. Please try again later" + } + }, + + "passwordResetConfirm": { + "welcomeMsg": { + "primary": "Password Reset Confirmation" + }, + "inputs": { + "newPassword1": { + "label": "New Password", + "errors": { + "min": "your password is too short", + "required": "this field is required" + } + }, + "newPassword2": { + "label": "Confirm Password", + "errors": { + "noMatch": "Passwords must match", + "required": "this field is required" + } + }, + "submit": "Reset Password" + }, + "toastSuccess": "Congratulations! your password reset was successful! you will now be redirected to login", + "errors": { + "unexpected": "An error occured while performing this action. Please try again later" + } + }, + + "emailConfirm": { + "welcomeMsg": { + "primary": "Email Confirmation", + "secondary": " Please Confirm that you are <> and that the email belongs to you:" + }, + "inputs": { + "submit": "Confirm" + }, + "toastSuccess": "Congratulations!, your email has been confirmed!", + "errors": { + "unexpected": "An error occured while performing this action. Please try again later" + } + }, + + "projects": { + "prev": "Prev", + "next": "Next", + "ariaLabels": { + "prevNxtButtons": "previous and next page buttons" + }, + "errors": { + "unexpected": "An error occured while performing this action, please try again later" + } + }, + + "createProject": { + "welcomeMsg": { + "primary": "Create Project", + "secondary": "Tell us about your project!" + }, + "inputs": { + "title": { + "label": "Title", + "errors": { + "max": "your project title shouldn't be more than 100 characters", + "required": "this field is required" + } + }, + "description": { + "label": "Description", + "helperText": "Tell us something interesting about the project! You can share what it is about, what inspired you to make it, your making process, fun and challenging moments you experienced, etc.", + "errors": { + "max": "your description shouldn't be more than 10,000 characters", + "required": "this field is required" + } + }, + "projectImages": { + "label": "Images", + "errors": { + "onlyImages": "only images are allowed in this field", + "imageOrVideo": "you must provide either image(s) or video url", + "imageSizeTooLarge": "one or more of your image is greater than 10mb", + "tooManyImages": "too many images uploaded" + } + }, + "video": { + "label": "Video URL", + "helperText": "YouTube, Vimeo, Google Drive links are supported", + "errors": { + "shouldBeVideoUrl": "you are required to submit a video url here", + "max": "you are required to submit a video url here", + "imageOrVideo": "you must provide either image(s) or video url" + } + }, + "materialsUsed": { + "label": "Materials Used", + "placeholder": "Add a material and click the + button", + "errors": { + "max": "you are required to submit a video url here", + "required": "this field is required" + } + }, + "submit": "Create Project", + "edit": "Edit Project" + }, + "createToastSuccess": "Your project was created successfully!!", + "updateToastSuccess": "Your project was updated successfully!!", + "errors": { + "notLoggedIn": "You are not logged in. Click on the signin button to get started", + "unexpected": "An error occured while performing this action. Please try again later" + } + }, + + "savedProjects": { + "title": "Your saved projects", + "prev": "Prev", + "next": "Next", + "ariaLabels": { + "prevNxtButtons": "previous and next page buttons" + }, + "errors": { + "noSavedProjects": "user have no saved projects", + "unexpected": "An error occured while performing this action. Please try again later" + } + }, + + "projectDetails": { + "project": { + "creator": { + "follow": "Follow", + "unfollow": "Unfollow" + }, + "description": "Description", + "materials": "Materials used", + "comments": { + "label": "Comments", + "write": "write a comment", + "action": "Comment" + }, + "edit": "Edit", + "delete": { + "label": "Delete", + "dialog": { + "primary": "Delete Project", + "secondary": "Are you sure you want to delete this project? you can't undo this action!!!", + "cancel": "Cancel", + "proceed": "Proceed" + } + } + }, + "ariaLabels": { + "likeButton": { + "label": "like button", + "like": "like", + "unlike": "unlike", + "deleteProject": "delete project dialog" + }, + "saveButton": { + "label": "save button", + "save": "save", + "unsave": "unsave" + }, + "imageDialog": "enlarged image dialog" + }, + "deleteToastSuccess": "Your project was deleted successfully!!", + "errors": { + "unexpected": "An error occured while performing this action. Please try again later" + } + }, + + "profile": { + "edit": "Edit", + "delete": { + "label": "Delete Account", + "dialog": { + "primary": "Delete Account", + "secondary": "Are you sure you want to delete this account? you can't undo this action!!! if you still want procceed, type your username into the field below and click procceed", + "inputs": { + "username": "Your Username" + }, + "cancel": "Cancel", + "procceed": "Procceed" + }, + "ariaLabels": { + "deleteAccount": "delete account" + }, + "toastSuccess": "account have been deleted successfully!!!", + "errors": { + "incorrectUsername": "The username you submitted is incorrect", + "unexpected": "An error occured while performing this action. Please try again later" + } + }, + "follow": "Follow", + "unfollow": "Unfollow", + "projectsCount": "Projects", + "followersCount": "Followers", + "followingCount": "Following", + "about": { + "label": "About Me", + "placeholder": "You will be able to change this next month 😀!" + }, + "projects": { + "label": "Latest projects of", + "viewAll": "View all >>" + }, + "tooltips": { + "shareProfile": "Share your profile with friends!" + }, + "ariaLabels": { + "shareProfile": "share profile url", + "editProfile": "edit user profile" + }, + "toastSuccess": "your profile url has been successfully copied to your clipboard!", + "errors": { + "profileFetchError": "An error occured while fetching profile, please try again later", + "profileUpdateError": "An error occured while updating your profile, please try again later", + "unexpected": "an error occured while fetching user profile, please try again later" + } + }, + + "editProfile": { + "welcomeMsg": { + "primary": "Edit Profile", + "secondary": "Have changes to make to your Profile? go ahead!" + }, + "inputs": { + "username": { + "label": "Username", + "errors": { + "required": "this field is required" + } + }, + "location": { + "label": "Location", + "errors": { + "min": "your location is too short", + "required": "this field is required" + } + }, + "bio": { + "label": "Bio", + "helpText": "Tell us something interesting about you! You can share what care about, your hobbies, things you have been working on recently, etc.", + "errors": { + "tooLong": "your bio shouldn't be more than 255 characters" + } + }, + "submit": "Edit Profile" + }, + "or": "OR", + "backToProfile": "Back To Profile", + "toastSuccess": "Your profile was updated successfully!!", + "errors": { + "unexpected": "an error occured while performing this action, please try again later" + }, + "tooltips": { + "noRealName": "Do not use your real name here!" + } + }, + + "userProjects": { + "title": "projects", + "prev": "Prev", + "next": "Next", + "ariaLabels": { + "prevNxtButtons": "previous and next page buttons" + }, + "errors": { + "noUserProjects": "user have not created any projects yet" + } + }, + + "userFollowers": { + "title": "followers", + "follower": { + "follow": "Follow", + "unfollow": "Unfollow" + }, + "prev": "Prev", + "next": "Next", + "ariaLabels": { + "prevNxtButtons": "previous and next page buttons" + }, + "errors": { + "noFollowers": "user have no followers yet", + "unexpected": "An error occured while performing this action. Please try again later" + } + }, + + "userFollowing": { + "title": "Creators <> is following", + "following": { + "follow": "Follow", + "unfollow": "Unfollow" + }, + "prev": "Prev", + "next": "Next", + "ariaLabels": { + "prevNxtButtons": "previous and next page buttons" + }, + "errors": { + "noFollowing": "Creator is not following anyone yet", + "unexpected": "An error occured while performing this action. Please try again later" + } + } +} diff --git a/zubhub_frontend/zubhub/public/locales/hi/translation.json b/zubhub_frontend/zubhub/public/locales/hi/translation.json new file mode 100644 index 000000000..d046ef792 --- /dev/null +++ b/zubhub_frontend/zubhub/public/locales/hi/translation.json @@ -0,0 +1,450 @@ +{ + "date": { + "years": "वर्षों", + "year": "साल", + "months": "महीने", + "month": "महीना", + "days": "दिन", + "day": "दिन", + "hours": "घंटे", + "hour": "इस घंटे", + "minutes": "मिनट", + "minute": "मिनट", + "seconds": "सेकंड", + "second": "सेकंड", + "ago": "पूर्व" + }, + + "pageWrapper": { + "navbar": { + "login": "लॉग इन करें", + "signup": "साइन अप करें", + "createProject": "प्रोजेक्ट बनाएं", + "savedProjects": "बचाए गए प्रोजेक्ट", + "projects": "परियोजनाओं", + "followers": "अनुयायियों", + "following": "निम्नलिखित", + "logout": "लॉग आउट" + }, + "errors": { + "unexpected": "उपयोगकर्ता प्रोफ़ाइल प्राप्त करते समय एक त्रुटि हुई, कृपया बाद में पुनः प्रयास करें", + "logoutFailed": "आपको साइन आउट करते समय एक त्रुटि हुई। कृपया पुन: प्रयास करें" + } + }, + + "signup": { + "welcomeMsg": { + "primary": "ZubHub में आपका स्वागत है", + "secondary": "प्रोजेक्ट सबमिट करने के लिए एक खाता बनाएँ" + }, + "inputs": { + "username": { + "label": "उपयोगकर्ता नाम", + "errors": { + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "email": { + "label": "ईमेल", + "errors": { + "invalid": "अवैध ईमेल", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "dateOfBirth": { + "label": "जन्म की तारीख", + "errors": { + "max": "आपकी जन्म तिथि आज से अधिक नहीं हो सकती", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "location": { + "label": "स्थान", + "errors": { + "min": "आपका स्थान बहुत छोटा है", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "password1": { + "label": "कुंजिका", + "errors": { + "min": "आपका पासवर्ड बहुत छोटा है", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "password2": { + "label": "पासवर्ड की पुष्टि कीजिये", + "errors": { + "noMatch": "पासवर्डों को मेल खाना चाहिए", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "submit": "साइन अप करें" + }, + + "alreadyAMember": "पहले से सदस्य हैं ?", + "login": "लॉग इन करें", + "errors": { + "unexpected": "यह क्रिया करते समय एक त्रुटि हुई। बाद में पुन: प्रयास करें" + }, + "ariaLabels": { + "togglePasswordVisibility": "पासवर्ड दृश्यता टॉगल करें" + }, + "tooltips": { + "noRealName": "यहां अपने असली नाम का उपयोग न करें!" + } + }, + + "login": { + "welcomeMsg": { + "primary": "ZubHub में आपका स्वागत है", + "secondary": "आरंभ करने के लिए लॉगिन करें!" + }, + "inputs": { + "username": { + "label": "उपयोगकर्ता का नाम या ईमेल", + "errors": { + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "password": { + "label": "कुंजिका", + "errors": { + "min": "आपका पासवर्ड बहुत छोटा है", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "submit": "लॉग इन करें" + }, + + "notAMember": "सदस्य नहीं है ?", + "signup": "साइन अप करें", + "forgotPassword": "पासवर्ड भूल गए?", + "errors": { + "unexpected": "यह क्रिया करते समय एक त्रुटि हुई। बाद में पुन: प्रयास करें" + } + }, + + "passwordReset": { + "welcomeMsg": { + "primary": "पासवर्ड रीसेट", + "secondary": "अपना ईमेल इनपुट करें ताकि हम आपको पासवर्ड रीसेट लिंक भेज सकें" + }, + "inputs": { + "email": { + "label": "ईमेल", + "errors": { + "invalid": "अवैध ईमेल", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "submit": "रीसेट लिंक भेजें" + }, + "toastSuccess": "हमने आपके ईमेल पर एक पासवर्ड रीसेट लिंक भेजा है!", + "errors": { + "unexpected": "यह क्रिया करते समय एक त्रुटि हुई। बाद में पुन: प्रयास करें" + } + }, + + "passwordResetConfirm": { + "welcomeMsg": { + "primary": "पासवर्ड रीसेट पुष्टि" + }, + "inputs": { + "newPassword1": { + "label": "नया पासवर्ड", + "errors": { + "min": "आपका पासवर्ड बहुत छोटा है", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "newPassword2": { + "label": "पासवर्ड की पुष्टि कीजिये", + "errors": { + "noMatch": "पासवर्डों को मेल खाना चाहिए", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "submit": "पासवर्ड रीसेट" + }, + "toastSuccess": "बधाई हो! आपका पासवर्ड रीसेट सफल रहा! अब आप लॉगिन करने के लिए पुनः निर्देशित किए जाएंगे", + "errors": { + "unexpected": "यह क्रिया करते समय एक त्रुटि हुई। बाद में पुन: प्रयास करें" + } + }, + + "emailConfirm": { + "welcomeMsg": { + "primary": "ईमेल की पुष्टि", + "secondary": "कृपया पुष्टि करें कि आप <> हैं और ईमेल आपसे संबंधित है:" + }, + "inputs": { + "submit": "पुष्टि करें" + }, + "toastSuccess": "बधाई !, आपके ईमेल की पुष्टि हो गई है!", + "errors": { + "unexpected": "यह क्रिया करते समय एक त्रुटि हुई। बाद में पुन: प्रयास करें" + } + }, + + "projects": { + "prev": "पिछला", + "next": "आगे", + "ariaLabels": { + "prevNxtButtons": "पिछले और अगले पृष्ठ बटन" + }, + "errors": { + "unexpected": "इस क्रिया को करते समय एक त्रुटि हुई, कृपया बाद में पुनः प्रयास करें" + } + }, + + "createProject": { + "welcomeMsg": { + "primary": "प्रोजेक्ट बनाएं", + "secondary": "हमें अपनी परियोजना के बारे में बताएं!" + }, + "inputs": { + "title": { + "label": "शीर्षक", + "errors": { + "max": "आपकी परियोजना का शीर्षक 100 से अधिक वर्ण नहीं होना चाहिए", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "description": { + "label": "विवरण", + "helperText": "प्रोजेक्ट के बारे में कुछ दिलचस्प बताएं! आप इसे साझा कर सकते हैं कि यह किस बारे में है, आपको इसे बनाने के लिए क्या प्रेरणा मिली है, आपकी बनाने की प्रक्रिया, आपके द्वारा अनुभव किए गए मजेदार और चुनौतीपूर्ण क्षण आदि।", + "errors": { + "max": "आपका वर्णन 10,000 से अधिक वर्णों का नहीं होना चाहिए", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "projectImages": { + "label": "इमेजिस", + "errors": { + "onlyImages": "इस क्षेत्र में केवल छवियों की अनुमति है", + "imageOrVideo": "आपको छवि या वीडियो url प्रदान करना चाहिए", + "imageSizeTooLarge": "आपकी एक या अधिक छवि 10mb से अधिक है", + "tooManyImages": "बहुत से चित्र अपलोड किए गए" + } + }, + "video": { + "label": "वीडियो यूआरएल", + "helperText": "YouTube, Vimeo, Google Drive लिंक समर्थित हैं", + "errors": { + "shouldBeVideoUrl": "आपको यहां एक वीडियो url प्रस्तुत करना आवश्यक है", + "max": "आपको यहां एक वीडियो url प्रस्तुत करना आवश्यक है", + "imageOrVideo": "आपको छवि या वीडियो url प्रदान करना चाहिए" + } + }, + "materialsUsed": { + "label": "उपयोग किया गया सामन", + "placeholder": "एक सामग्री जोड़ें और + बटन पर क्लिक करें", + "errors": { + "max": "आपके द्वारा उपयोग की जाने वाली सामग्री 10,000 से अधिक वर्णों की नहीं होनी चाहिए", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "submit": "प्रोजेक्ट बनाएं", + "edit": "प्रोजेक्ट संपादित करें" + }, + "createToastSuccess": "आपका प्रोजेक्ट सफलतापूर्वक बनाया गया था !!", + "updateToastSuccess": "आपका प्रोजेक्ट सफलतापूर्वक अपडेट किया गया था !!", + "errors": { + "notLoggedIn": "आप लॉग इन नहीं हैं। आरंभ करने के लिए साइन इन बटन पर क्लिक करें", + "unexpected": "यह क्रिया करते समय एक त्रुटि हुई। बाद में पुन: प्रयास करें" + } + }, + + "savedProjects": { + "title": "आपके सहेजे गए प्रोजेक्ट", + "prev": "पिछला", + "next": "आगे", + "ariaLabels": { + "prevNxtButtons": "पिछले और अगले पृष्ठ बटन" + }, + "errors": { + "noSavedProjects": "उपयोगकर्ता के पास कोई भी सहेजा हुआ प्रोजेक्ट नहीं है", + "unexpected": "इस क्रिया को करते समय एक त्रुटि हुई। कृपया बाद में पुनः प्रयास करें" + } + }, + + "projectDetails": { + "project": { + "creator": { + "follow": "का पालन करें", + "unfollow": "करें" + }, + "description": "विवरण", + "materials": "उपयोग किया गया सामन", + "comments": { + "label": "टिप्पणियाँ", + "write": "टिप्पणी लिखें", + "action": "टिप्पणी" + }, + "edit": "संपादित करें", + "delete": { + "label": "हटाएं", + "dialog": { + "primary": "प्रोजेक्ट हटाएं", + "secondary": "क्या आप वाकई इस प्रोजेक्ट को हटाना चाहते हैं? आप इस कार्रवाई को पूर्ववत नहीं कर सकते हैं !!!", + "cancel": "रद्द करना", + "proceed": "बढ़ना" + } + } + }, + "ariaLabels": { + "likeButton": { + "label": "बटन की तरह", + "like": "पसंद", + "unlike": "भिन्न", + "deleteProject": "परियोजना संवाद हटाएं" + }, + "saveButton": { + "label": "बटन सहेजें", + "save": "सहेजें", + "unsave": "बिना सोचे समझे" + }, + "imageDialog": "बढ़े हुए छवि संवाद" + }, + "deleteToastSuccess": "आपका प्रोजेक्ट सफलतापूर्वक हटा दिया गया था !!", + "errors": { + "unexpected": "यह क्रिया करते समय एक त्रुटि हुई। बाद में पुन: प्रयास करें" + } + }, + + "profile": { + "edit": "संपादित करें", + "delete": { + "label": "खाता हटा दो", + "dialog": { + "primary": "खाता हटा दो", + "secondary": "क्या आप वाकई इस खाते को हटाना चाहते हैं? आप इस कार्रवाई को पूर्ववत नहीं कर सकते हैं !!! यदि आप अभी भी खरीद चाहते हैं, तो नीचे दिए गए फ़ील्ड में अपना उपयोगकर्ता नाम लिखें और अधिप्राप्ति पर क्लिक करें", + "inputs": { + "username": "तुम्हारा प्रयोगकर्ती नाम" + }, + "cancel": "रद्द करना", + "procceed": "बढ़ना" + }, + "ariaLabels": { + "deleteAccount": "खाता हटा दो" + }, + "toastSuccess": "खाता सफलतापूर्वक हटा दिया गया है !!!", + "errors": { + "incorrectUsername": "आपके द्वारा सबमिट किया गया उपयोगकर्ता नाम गलत है", + "unexpected": "यह क्रिया करते समय एक त्रुटि हुई। बाद में पुन: प्रयास करें" + } + }, + "follow": "का पालन करें", + "unfollow": "करें", + "projectsCount": "परियोजनाओं", + "followersCount": "समर्थक", + "followingCount": "निम्नलिखित", + "about": { + "label": "मेरे बारे में", + "placeholder": "आप इसे अगले महीने बदल पाएंगे 😀!" + }, + "projects": { + "label": "की नवीनतम परियोजनाएँ", + "viewAll": "सभी देखें >>" + }, + "tooltips": { + "shareProfile": "अपने प्रोफ़ाइल को दोस्तों के साथ साझा करें!" + }, + "ariaLabels": { + "shareProfile": "शेयर प्रोफ़ाइल यूआरएल", + "editProfile": "उपयोगकर्ता प्रोफ़ाइल संपादित करें" + }, + "toastSuccess": "आपकी प्रोफ़ाइल url को सफलतापूर्वक आपके क्लिपबोर्ड पर कॉपी कर लिया गया है!", + "errors": { + "profileFetchError": "प्रोफ़ाइल प्राप्त करते समय एक त्रुटि हुई, कृपया बाद में पुनः प्रयास करें", + "profileUpdateError": "आपकी प्रोफ़ाइल अपडेट करते समय एक त्रुटि हुई, कृपया बाद में पुनः प्रयास करें", + "unexpected": "उपयोगकर्ता प्रोफ़ाइल प्राप्त करते समय एक त्रुटि हुई, कृपया बाद में पुनः प्रयास करें" + } + }, + + "editProfile": { + "welcomeMsg": { + "primary": "प्रोफ़ाइल संपादित करें", + "secondary": "आपकी प्रोफ़ाइल में परिवर्तन करने के लिए है? आगे बढ़ें!" + }, + "inputs": { + "username": { + "label": "उपयोगकर्ता नाम", + "errors": { + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "location": { + "label": "स्थान", + "errors": { + "min": "आपका स्थान बहुत छोटा है", + "required": "यह फ़ील्ड आवश्यक है" + } + }, + "bio": { + "label": "जैव", + "helpText": "हमें आपके बारे में कुछ रोचक बताएं! आप इस बात की परवाह कर सकते हैं कि आपके शौक, हाल ही में आपके द्वारा किए गए काम, आदि", + "errors": { + "tooLong": "आपका जैव 255 से अधिक वर्ण नहीं होना चाहिए" + } + }, + "submit": "प्रोफ़ाइल संपादित करें" + }, + "or": "या", + "backToProfile": "बैक टू प्रोफाइल", + "toastSuccess": "आपकी प्रोफ़ाइल सफलतापूर्वक अपडेट की गई थी !!", + "errors": { + "unexpected": "इस क्रिया को करते समय एक त्रुटि हुई, कृपया बाद में पुनः प्रयास करें" + }, + "tooltips": { + "noRealName": "यहां अपने असली नाम का उपयोग न करें!" + } + }, + + "userProjects": { + "title": "परियोजनाओं", + "prev": "पिछला", + "next": "आगे", + "ariaLabels": { + "prevNxtButtons": "पिछले और अगले पृष्ठ बटन" + }, + "errors": { + "noUserProjects": "उपयोगकर्ता ने अभी तक कोई प्रोजेक्ट नहीं बनाया है" + } + }, + + "userFollowers": { + "title": "अनुयायियों", + "follower": { + "follow": "का पालन करें", + "unfollow": "करें" + }, + "prev": "पिछला", + "next": "आगे", + "ariaLabels": { + "prevNxtButtons": "पिछले और अगले पृष्ठ बटन" + }, + "errors": { + "noFollowers": "उपयोगकर्ता का अभी तक कोई अनुयायी नहीं है", + "unexpected": "यह क्रिया करते समय एक त्रुटि हुई। बाद में पुन: प्रयास करें" + } + }, + + "userFollowing": { + "title": "निर्माता <> अनुसरण कर रहा है", + "following": { + "follow": "का पालन करें", + "unfollow": "करें" + }, + "prev": "पिछला", + "next": "आगे", + "ariaLabels": { + "prevNxtButtons": "पिछले और अगले पृष्ठ बटन" + }, + "errors": { + "noFollowing": "निर्माता अभी तक किसी का अनुसरण नहीं कर रहा है", + "unexpected": "यह क्रिया करते समय एक त्रुटि हुई। बाद में पुन: प्रयास करें" + } + } +} diff --git a/zubhub_frontend/zubhub/src/App.js b/zubhub_frontend/zubhub/src/App.js index 2a75da42b..57065f1e9 100644 --- a/zubhub_frontend/zubhub/src/App.js +++ b/zubhub_frontend/zubhub/src/App.js @@ -2,6 +2,8 @@ import React from 'react'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; +import { withTranslation } from 'react-i18next'; + import PageWrapper from './views/PageWrapper'; import Signup from './views/signup/Signup'; import Login from './views/login/Login'; @@ -9,6 +11,7 @@ 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 EditProfile from './views/edit_profile/EditProfile'; import UserProjects from './views/user_projects/UserProjects'; import UserFollowers from './views/user_followers/UserFollowers'; import UserFollowing from './views/user_following/UserFollowing'; @@ -25,7 +28,7 @@ function App(props) { exact={true} path="/" render={routeProps => ( - + )} @@ -34,7 +37,7 @@ function App(props) { ( - + )} @@ -43,7 +46,7 @@ function App(props) { ( - + )} @@ -52,7 +55,7 @@ function App(props) { ( - + )} @@ -61,7 +64,7 @@ function App(props) { ( - + )} @@ -70,7 +73,7 @@ function App(props) { ( - + )} @@ -79,7 +82,7 @@ function App(props) { ( - + )} @@ -88,7 +91,7 @@ function App(props) { ( - + )} @@ -97,7 +100,7 @@ function App(props) { ( - + )} @@ -106,7 +109,7 @@ function App(props) { ( - + )} @@ -115,16 +118,25 @@ function App(props) { ( - + )} /> + ( + + + + )} + /> + ( - + )} @@ -133,15 +145,24 @@ function App(props) { ( - + )} /> + ( + + + + )} + /> + ( - + )} @@ -151,4 +172,4 @@ function App(props) { ); } -export default App; +export default withTranslation()(App); diff --git a/zubhub_frontend/zubhub/src/api/api.js b/zubhub_frontend/zubhub/src/api/api.js index 267a0b4bc..5af60d08c 100644 --- a/zubhub_frontend/zubhub/src/api/api.js +++ b/zubhub_frontend/zubhub/src/api/api.js @@ -1,3 +1,5 @@ +import i18next from 'i18next'; + class API { constructor() { this.domain = @@ -15,6 +17,7 @@ class API { withCredentials: 'true', headers: new Headers({ 'Content-Type': 'application/json', + 'Accept-Language': `${i18next.language},en;q=0.5`, }), }); } else if (token && body) { @@ -26,6 +29,7 @@ class API { headers: new Headers({ Authorization: `Token ${token}`, 'Content-Type': 'application/json', + 'Accept-Language': `${i18next.language},en;q=0.5`, }), body, }); @@ -38,6 +42,7 @@ class API { headers: new Headers({ Authorization: `Token ${token}`, 'Content-Type': 'application/json', + 'Accept-Language': `${i18next.language},en;q=0.5`, }), }); } else if (body) { @@ -48,6 +53,7 @@ class API { withCredentials: 'true', headers: new Headers({ 'Content-Type': 'application/json', + 'Accept-Language': `${i18next.language},en;q=0.5`, }), body, }); @@ -59,6 +65,7 @@ class API { const url = 'rest-auth/login/'; const method = 'POST'; const body = JSON.stringify({ username, password }); + console.log('stringified json', body); return this.request({ url, method, body }).then(res => res.json()); }; @@ -102,7 +109,9 @@ class API { const method = 'POST'; const body = JSON.stringify({ key }); - return this.request({ url, method, body }).then(res => res.json()); + return this.request({ url, method, body }).then(res => + Promise.resolve(res.status === 200 ? { detail: 'ok' } : res.json()), + ); }; /*******************************************************************/ @@ -112,7 +121,9 @@ class API { const method = 'POST'; const body = JSON.stringify({ email }); - return this.request({ url, method, body }).then(res => res.json()); + return this.request({ url, method, body }).then(res => + Promise.resolve(res.status === 200 ? { detail: 'ok' } : res.json()), + ); }; /********************************************************************/ @@ -122,7 +133,9 @@ class API { const method = 'POST'; const body = JSON.stringify({ new_password1, new_password2, uid, token }); - return this.request({ url, method, body }).then(res => res.json()); + return this.request({ url, method, body }).then(res => + Promise.resolve(res.status === 200 ? { detail: 'ok' } : res.json()), + ); }; /********************************************************************/ @@ -190,17 +203,31 @@ class API { /*****************************************************************/ /************************** edit user profile **************************/ - edit_user_profile = profile => { - const { username } = profile; + edit_user_profile = props => { + const { token, username, dateOfBirth, bio, user_location } = props; const url = 'creators/edit_creator/'; const method = 'PUT'; - const token = profile.token; - const body = JSON.stringify({ username }); + const body = JSON.stringify({ + username, + dateOfBirth, + bio, + location: user_location, + }); return this.request({ url, method, token, body }).then(res => res.json()); }; /********************************************************************/ + /************************** delete account **************************/ + delete_account = ({ token }) => { + const url = 'creators/delete/'; + const method = 'DELETE'; + return this.request({ url, method, token }).then(res => + Promise.resolve(res.status === 204 ? { detail: 'ok' } : res.json()), + ); + }; + /********************************************************************/ + /************************** follow creator **************************/ toggle_follow = ({ id, token }) => { const url = `creators/${id}/toggle_follow/`; @@ -236,9 +263,44 @@ class API { }); return this.request({ url, method, token, body }).then(res => res.json()); }; + /************************************************************************/ + + /************************** update project **************************/ + update_project = ({ + token, + id, + title, + description, + video, + images, + materials_used, + }) => { + const url = `projects/${id}/update/`; + const method = 'PATCH'; + const body = JSON.stringify({ + id, + title, + description, + images, + video, + materials_used, + }); + return this.request({ url, method, token, body }).then(res => res.json()); + }; + /************************************************************************/ + + /************************** delete project **************************/ + delete_project = ({ token, id }) => { + const url = `projects/${id}/delete/`; + const method = 'DELETE'; + return this.request({ url, method, token }).then(res => + Promise.resolve(res.status === 204 ? { detail: 'ok' } : res.json()), + ); + }; + /************************************************************************/ /************************** get projects **************************/ - get_projects = page => { + get_projects = ({ page }) => { const url = page ? `projects/?${page}` : `projects/`; return this.request({ url }).then(res => res.json()); }; diff --git a/zubhub_frontend/zubhub/src/assets/js/dFormatter.js b/zubhub_frontend/zubhub/src/assets/js/dFormatter.js index 92ad38fec..ef583f710 100644 --- a/zubhub_frontend/zubhub/src/assets/js/dFormatter.js +++ b/zubhub_frontend/zubhub/src/assets/js/dFormatter.js @@ -6,31 +6,31 @@ const dFormatter = str => { let interval = seconds / 31536000; if (interval > 1) { - const result = Math.round(interval); - return result + (result > 1 ? ' years' : ' year') + ' ago'; + let result = Math.round(interval); + return { value: result, key: result > 1 ? 'years' : 'year' }; } interval = seconds / 2592000; if (interval > 1) { - const result = Math.round(interval); - return result + (result > 1 ? ' months' : ' month') + ' ago'; + let result = Math.round(interval); + return { value: result, key: result > 1 ? 'months' : 'month' }; } interval = seconds / 86400; if (interval > 1) { - const result = Math.round(interval); - return result + (result > 1 ? ' days' : ' day') + ' ago'; + let result = Math.round(interval); + return { value: result, key: result > 1 ? 'days' : 'day' }; } interval = seconds / 3600; if (interval > 1) { - const result = Math.round(interval); - return result + (result > 1 ? ' hours' : ' hour') + ' ago'; + let result = Math.round(interval); + return { value: result, key: result > 1 ? 'hours' : 'hour' }; } interval = seconds / 60; if (interval > 1) { - const result = Math.round(interval); - return result + (result > 1 ? ' minutes' : ' minute') + ' ago'; + let result = Math.round(interval); + return { value: result, key: result > 1 ? 'minutes' : 'minute' }; } - const result = Math.round(interval); - return result + (result > 1 ? ' seconds' : ' second') + ' ago'; + let result = Math.round(interval); + return { value: result, key: result > 1 ? 'seconds' : 'second' }; }; export default dFormatter; diff --git a/zubhub_frontend/zubhub/src/assets/js/languageMap.json b/zubhub_frontend/zubhub/src/assets/js/languageMap.json new file mode 100644 index 000000000..9379956d8 --- /dev/null +++ b/zubhub_frontend/zubhub/src/assets/js/languageMap.json @@ -0,0 +1,4 @@ +{ + "en":"English", + "hi":"हिंदी" +} \ No newline at end of file diff --git a/zubhub_frontend/zubhub/src/assets/js/styles/components/button/buttonStyles.js b/zubhub_frontend/zubhub/src/assets/js/styles/components/button/buttonStyles.js index 81147ba89..02c8753b3 100644 --- a/zubhub_frontend/zubhub/src/assets/js/styles/components/button/buttonStyles.js +++ b/zubhub_frontend/zubhub/src/assets/js/styles/components/button/buttonStyles.js @@ -16,6 +16,15 @@ const styles = theme => ({ backgroundColor: 'rgba(255,255,255,0.8)', }, }, + dangerButtonStyle: { + borderRadius: 15, + backgroundColor: 'rgb(220, 0, 78)', + color: 'white', + '&:hover': { + color: 'white', + backgroundColor: '#9A0036', + }, + }, imageUploadButtonStyle: { '& MuiButton-label': { width: '100%', diff --git a/zubhub_frontend/zubhub/src/assets/js/styles/index.js b/zubhub_frontend/zubhub/src/assets/js/styles/index.js index a859072d8..1d983a2d6 100644 --- a/zubhub_frontend/zubhub/src/assets/js/styles/index.js +++ b/zubhub_frontend/zubhub/src/assets/js/styles/index.js @@ -2,6 +2,9 @@ const styles = theme => ({ marginLeft1em: { marginLeft: '1em', }, + colorRed: { + color: 'red', + }, }); export default styles; diff --git a/zubhub_frontend/zubhub/src/assets/js/styles/views/create_project/createProjectStyles.js b/zubhub_frontend/zubhub/src/assets/js/styles/views/create_project/createProjectStyles.js index d35a16018..4fca2a7b2 100644 --- a/zubhub_frontend/zubhub/src/assets/js/styles/views/create_project/createProjectStyles.js +++ b/zubhub_frontend/zubhub/src/assets/js/styles/views/create_project/createProjectStyles.js @@ -43,6 +43,16 @@ const styles = theme => ({ }, }, }, + staticLabelInputStyle: { + '&.MuiOutlinedInput-root fieldset legend': { + width: '75.5px !important', + }, + }, + staticLabelInputSmallStyle: { + '&.MuiOutlinedInput-root fieldset legend': { + width: '40px !important', + }, + }, uploadProgressLabelStyle: { color: 'white', }, diff --git a/zubhub_frontend/zubhub/src/assets/js/styles/views/edit_profile/editProfileStyles.js b/zubhub_frontend/zubhub/src/assets/js/styles/views/edit_profile/editProfileStyles.js new file mode 100644 index 000000000..85a3a709d --- /dev/null +++ b/zubhub_frontend/zubhub/src/assets/js/styles/views/edit_profile/editProfileStyles.js @@ -0,0 +1,97 @@ +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`, + }, + }, + }, + staticLabelInputStyle: { + '&.MuiOutlinedInput-root fieldset legend': { + width: '75.5px !important', + }, + }, + staticLabelInputSmallStyle: { + '&.MuiOutlinedInput-root fieldset legend': { + width: '40px !important', + }, + }, + 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/page_wrapper/pageWrapperStyles.js b/zubhub_frontend/zubhub/src/assets/js/styles/views/page_wrapper/pageWrapperStyles.js index 3293e01b4..050ad91bc 100644 --- a/zubhub_frontend/zubhub/src/assets/js/styles/views/page_wrapper/pageWrapperStyles.js +++ b/zubhub_frontend/zubhub/src/assets/js/styles/views/page_wrapper/pageWrapperStyles.js @@ -48,6 +48,10 @@ const styles = theme => ({ bottom: theme.spacing(2), right: theme.spacing(2), }, + languageSelectStyle: { + display: 'block', + maxWidth: '7em', + }, center: { display: 'flex', justifyContent: 'center', diff --git a/zubhub_frontend/zubhub/src/components/button/Button.js b/zubhub_frontend/zubhub/src/components/button/Button.js index bc9aa05b9..ae2acf71f 100644 --- a/zubhub_frontend/zubhub/src/components/button/Button.js +++ b/zubhub_frontend/zubhub/src/components/button/Button.js @@ -17,6 +17,7 @@ const CustomButton = React.forwardRef((props, ref) => { children, primaryButtonStyle, secondaryButtonStyle, + dangerButtonStyle, imageUploadButtonStyle, fullWidth, className, @@ -26,6 +27,7 @@ const CustomButton = React.forwardRef((props, ref) => { const btnClasses = classNames({ [classes.primaryButtonStyle]: primaryButtonStyle, [classes.secondaryButtonStyle]: secondaryButtonStyle, + [classes.dangerButtonStyle]: dangerButtonStyle, [classes.imageUploadButtonStyle]: imageUploadButtonStyle, [classes.fullWidth]: fullWidth, [className]: className, @@ -42,6 +44,7 @@ Button.propTypes = { size: PropTypes.oneOf(['small', 'large']), primaryButtonStyle: PropTypes.bool, secondaryButtonStyle: PropTypes.bool, + dangerButtonStyle: PropTypes.bool, imageUploadButtonStyle: PropTypes.bool, className: PropTypes.string, variant: PropTypes.string, diff --git a/zubhub_frontend/zubhub/src/components/project/Project.jsx b/zubhub_frontend/zubhub/src/components/project/Project.jsx index edb70853f..d4c82cd4d 100644 --- a/zubhub_frontend/zubhub/src/components/project/Project.jsx +++ b/zubhub_frontend/zubhub/src/components/project/Project.jsx @@ -50,12 +50,13 @@ function Project(props) { const toggle_save_promise = props.toggle_save({ id, token: props.auth.token, + t: props.t, }); props.updateProjects(toggle_save_promise); } }; - const { project } = props; + const { project, t } = props; return ( @@ -165,7 +166,9 @@ function Project(props) { variant="caption" component="span" > - {dFormatter(project.created_on)} + {`${dFormatter(project.created_on).value} ${t( + `date.${dFormatter(project.created_on).key}`, + )} ${t('date.ago')}`} diff --git a/zubhub_frontend/zubhub/src/i18n.js b/zubhub_frontend/zubhub/src/i18n.js new file mode 100644 index 000000000..bfacedc37 --- /dev/null +++ b/zubhub_frontend/zubhub/src/i18n.js @@ -0,0 +1,28 @@ +import i18n from 'i18next'; +import Backend from 'i18next-http-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; + +i18n + // load translation using http -> see /public/locales + // learn more: https://github.com/i18next/i18next-http-backend + .use(Backend) + // detect user language + // learn more: https://github.com/i18next/i18next-browser-languageDetector + .use(LanguageDetector) + // pass the i18n instance to the react-i18next components. + // Alternative use the I18nextProvider: https://react.i18next.com/components/i18nextprovider + .use(initReactI18next) + // init i18next + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + fallbackLng: 'en', + whitelist: ['en', 'hi'], + debug: false, + load: 'all', + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + }); + +export default i18n; diff --git a/zubhub_frontend/zubhub/src/index.js b/zubhub_frontend/zubhub/src/index.js index 9189c1a4c..1a33063a9 100644 --- a/zubhub_frontend/zubhub/src/index.js +++ b/zubhub_frontend/zubhub/src/index.js @@ -1,6 +1,8 @@ -import React from 'react'; +import React, { Suspense } from 'react'; import ReactDOM from 'react-dom'; +import reportWebVitals from './reportWebVitals'; + import { PersistGate } from 'redux-persist/integration/react'; import { Provider } from 'react-redux'; @@ -10,7 +12,7 @@ import { theme } from './assets/js/muiTheme'; import App from './App'; import './assets/css/index.css'; import configureStore from './store/configureStore'; -import reportWebVitals from './reportWebVitals'; +import './i18n'; let { store, persistor } = configureStore(); @@ -19,7 +21,9 @@ ReactDOM.render( - + + + diff --git a/zubhub_frontend/zubhub/src/store/actions/authActions.js b/zubhub_frontend/zubhub/src/store/actions/authActions.js index e391e3c76..3158dd21e 100644 --- a/zubhub_frontend/zubhub/src/store/actions/authActions.js +++ b/zubhub_frontend/zubhub/src/store/actions/authActions.js @@ -12,9 +12,10 @@ export const setAuthUser = auth_user => { }; }; -export const login = props => { +export const login = args => { return dispatch => { - return API.login(props.values) + console.log(args); + return API.login(args.values) .then(res => { if (!res.key) { throw new Error(JSON.stringify(res)); @@ -24,13 +25,13 @@ export const login = props => { payload: { token: res.key }, }); }) - .then(() => props.history.push('/profile')); + .then(() => args.history.push('/profile')); }; }; -export const logout = props => { +export const logout = args => { return dispatch => { - API.logout(props.auth.token) + API.logout(args.token) .then(res => { dispatch({ type: 'SET_AUTH_USER', @@ -38,12 +39,10 @@ export const logout = props => { }); }) .then(res => { - props.history.push('/'); + args.history.push('/'); }) .catch(error => { - toast.warning( - 'An error occured while signing you out. please try again', - ); + toast.warning(args.t('pageWrapper.errors.logoutFailed')); }); }; }; @@ -52,24 +51,24 @@ 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', - ); + if (!res.id) { + throw new Error(props.t('pageWrapper.errors.unexpected')); } dispatch({ type: 'SET_AUTH_USER', payload: { ...props.auth, username: res.username, id: res.id }, }); + + return res; }) .catch(error => toast.warning(error.message)); }; }; -export const signup = props => { +export const signup = args => { return dispatch => { - return API.signup(props.values) + return API.signup(args.values) .then(res => { if (!res.key) { throw new Error(JSON.stringify(res)); @@ -79,58 +78,56 @@ export const signup = props => { payload: { token: res.key }, }); }) - .then(() => props.history.push('/profile')); + .then(() => args.history.push('/profile')); }; }; -export const send_email_confirmation = (props, key) => { +export const send_email_confirmation = args => { return () => { - return API.send_email_confirmation(key).then(res => { + return API.send_email_confirmation(args.key).then(res => { if (res.detail !== 'ok') { throw new Error(res.detail); } else { - toast.success('Congratulations!, your email has been confirmed!'); + toast.success(args.t('emailConfirm.toastSuccess')); setTimeout(() => { - props.history.push('/'); + args.history.push('/'); }, 4000); } }); }; }; -export const send_password_reset_link = props => { +export const send_password_reset_link = args => { return () => { - return API.send_password_reset_link(props.values.email).then(res => { - if (res.detail !== 'Password reset e-mail has been sent.') { + return API.send_password_reset_link(args.email).then(res => { + if (res.detail !== 'ok') { throw new Error(JSON.stringify(res)); } else { - toast.success('We just sent a password reset link to your email!'); + toast.success(args.t('passwordReset.toastSuccess')); setTimeout(() => { - props.history.push('/'); + args.history.push('/'); }, 4000); } }); }; }; -export const password_reset_confirm = props => { +export const password_reset_confirm = args => { return () => { - return API.password_reset_confirm(props).then(res => { - if (res.detail !== 'Password has been reset with the new password.') { + return API.password_reset_confirm(args).then(res => { + if (res.detail !== 'ok') { throw new Error(JSON.stringify(res)); } else { - toast.success( - 'Congratulations! your password reset was successful! you will now be redirected to login', - ); + toast.success(args.t('passwordResetConfirm.toastSuccess')); setTimeout(() => { - props.history.push('/login'); + args.history.push('/login'); }, 4000); } }); }; }; -export const get_locations = () => { +export const get_locations = args => { return () => { return API.get_locations() .then(res => { @@ -146,8 +143,7 @@ export const get_locations = () => { .catch(error => { if (error.message.startsWith('Unexpected')) { return { - error: - 'An error occured while performing this action. Please try again later', + error: args.t('signup.errors.unexpected'), }; } else { return { error: error.message }; @@ -155,3 +151,26 @@ export const get_locations = () => { }); }; }; + +export const delete_account = args => { + return () => { + return API.delete_account(args) + .then(res => { + if (res.detail !== 'ok') { + throw new Error(res.detail); + } else { + toast.success(args.t('profile.delete.toastSuccess')); + args.logout(args); + } + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + return { + dialogError: args.t('profile.delete.errors.unexpected'), + }; + } else { + return { dialogError: error.message }; + } + }); + }; +}; diff --git a/zubhub_frontend/zubhub/src/store/actions/projectActions.js b/zubhub_frontend/zubhub/src/store/actions/projectActions.js index e7d3176f4..43fb65a0a 100644 --- a/zubhub_frontend/zubhub/src/store/actions/projectActions.js +++ b/zubhub_frontend/zubhub/src/store/actions/projectActions.js @@ -12,21 +12,47 @@ export const set_projects = projects => { }; export const create_project = props => { - return dispatch => { + return () => { return API.create_project(props).then(res => { if (!res.id) { throw new Error(JSON.stringify(res)); } else { - toast.success('Your project was created successfully!!'); + toast.success(props.t('createProject.createToastSuccess')); + return props.history.push('/profile'); + } + }); + }; +}; + +export const update_project = props => { + return () => { + return API.update_project(props).then(res => { + if (!res.id) { + throw new Error(JSON.stringify(res)); + } else { + toast.success(props.t('createProject.updateToastSuccess')); return props.history.push('/profile'); } }); }; }; -export const get_project = value => { +export const delete_project = args => { return () => { - return API.get_project(value) + return API.delete_project({ token: args.token, id: args.id }).then(res => { + if (res.detail !== 'ok') { + throw new Error(res.detail); + } else { + toast.success(args.t('projectDetails.deleteToastSuccess')); + return args.history.push('/profile'); + } + }); + }; +}; + +export const get_project = args => { + return () => { + return API.get_project(args) .then(res => { if (res.title) { return { project: res, loading: false }; @@ -38,15 +64,19 @@ export const get_project = value => { } }) .catch(error => { - toast.warning(error.message); + if (error.message.startsWith('Unexpected')) { + toast.warning(args.t('projectDetails.errors.unexpected')); + } else { + toast.warning(error.message); + } return { loading: false }; }); }; }; -export const get_projects = page => { +export const get_projects = args => { return dispatch => { - return API.get_projects(page) + return API.get_projects(args) .then(res => { if (Array.isArray(res.results)) { dispatch({ @@ -62,15 +92,19 @@ export const get_projects = page => { } }) .catch(error => { - toast.warning(error.message); + if (error.message.startsWith('Unexpected')) { + toast.warning(args.t('projects.errors.unexpected')); + } else { + toast.warning(error.message); + } return { loading: false }; }); }; }; -export const get_user_projects = props => { +export const get_user_projects = args => { return () => { - return API.get_user_projects(props) + return API.get_user_projects(args) .then(res => { if (Array.isArray(res.results)) { return { ...res, loading: false }; @@ -82,15 +116,19 @@ export const get_user_projects = props => { } }) .catch(error => { - toast.warning(error.message); + if (error.message.startsWith('Unexpected')) { + toast.warning(args.t('projects.errors.unexpected')); + } else { + toast.warning(error.message); + } return { loading: false }; }); }; }; -export const get_saved = value => { +export const get_saved = args => { return () => { - return API.get_saved(value) + return API.get_saved(args) .then(res => { if (Array.isArray(res.results)) { return { @@ -106,9 +144,7 @@ export const get_saved = value => { }) .catch(error => { if (error.message.startsWith('Unexpected')) { - toast.warning( - 'An error occured while performing this action. Please try again later', - ); + toast.warning(args.t('savedProjects.errors.unexpected')); } else { toast.warning(error.message); } @@ -117,9 +153,9 @@ export const get_saved = value => { }; }; -export const toggle_like = props => { +export const toggle_like = args => { return () => { - return API.toggle_like(props) + return API.toggle_like(args) .then(res => { if (res.title) { return { project: res }; @@ -132,9 +168,7 @@ export const toggle_like = props => { }) .catch(error => { if (error.message.startsWith('Unexpected')) { - toast.warning( - 'An error occured while performing this action. Please try again later', - ); + toast.warning(args.t('projectDetails.errors.unexpected')); } else { toast.warning(error.message); } @@ -144,9 +178,9 @@ export const toggle_like = props => { }; }; -export const toggle_save = props => { +export const toggle_save = args => { return () => { - return API.toggle_save(props) + return API.toggle_save(args) .then(res => { if (res.title) { return { project: res }; @@ -159,9 +193,7 @@ export const toggle_save = props => { }) .catch(error => { if (error.message.startsWith('Unexpected')) { - toast.warning( - 'An error occured while performing this action. Please try again later', - ); + toast.warning(args.t('projects.errors.unexpected')); } else { toast.warning(error.message); } @@ -170,9 +202,9 @@ export const toggle_save = props => { }; }; -export const add_comment = props => { +export const add_comment = args => { return () => { - return API.add_comment(props) + return API.add_comment(args) .then(res => { if (res.title) { return { project: res }; @@ -185,9 +217,7 @@ export const add_comment = props => { }) .catch(error => { if (error.message.startsWith('Unexpected')) { - toast.warning( - 'An error occured while performing this action. Please try again later', - ); + toast.warning(args.t('projectDetails.errors.unexpected')); } else { toast.warning(error.message); } diff --git a/zubhub_frontend/zubhub/src/store/actions/userActions.js b/zubhub_frontend/zubhub/src/store/actions/userActions.js index ffa7212c7..f3d391113 100644 --- a/zubhub_frontend/zubhub/src/store/actions/userActions.js +++ b/zubhub_frontend/zubhub/src/store/actions/userActions.js @@ -5,15 +5,13 @@ import { toast } from 'react-toastify'; const API = new ZubhubAPI(); -export const get_user_profile = props => { +export const get_user_profile = args => { return dispatch => { let profile; - return API.get_user_profile(props) + return API.get_user_profile(args) .then(res => { if (!res.username) { - throw new Error( - 'an error occured while fetching user profile, please try again later', - ); + throw new Error(args.t('profile.errors.profileFetchError')); } else { profile = res; return dispatch( @@ -28,37 +26,37 @@ export const get_user_profile = props => { return { ...res, profile, loading: false }; }) .catch(error => { - toast.warning(error.message); + if (error.message.startsWith('Unexpected')) { + toast.warning(args.t('profile.errors.unexpected')); + } else { + toast.warning(error.message); + } return { loading: false }; }); }; }; -export const edit_user_profile = props => { +export const edit_user_profile = args => { return dispatch => { - return API.edit_user_profile(props) - .then(res => { - if (res.username) { - dispatch( - AuthActions.setAuthUser({ - username: res.username, - }), - ); + return API.edit_user_profile(args).then(res => { + if (res.id) { + 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)); + return { profile: res }; + } else { + throw new Error(JSON.stringify(res)); + } + }); }; }; -export const toggle_follow = props => { +export const toggle_follow = args => { return () => { - return API.toggle_follow(props) + return API.toggle_follow(args) .then(res => { if (res.username) { return { profile: res }; @@ -71,9 +69,7 @@ export const toggle_follow = props => { }) .catch(error => { if (error.message.startsWith('Unexpected')) { - toast.warning( - 'An error occured while performing this action. Please try again later', - ); + toast.warning(args.t('profile.errors.unexpected')); } else { toast.warning(error.message); } @@ -82,9 +78,9 @@ export const toggle_follow = props => { }; }; -export const get_followers = value => { +export const get_followers = args => { return () => { - return API.get_followers(value) + return API.get_followers(args) .then(res => { if (Array.isArray(res.results)) { return { @@ -101,15 +97,19 @@ export const get_followers = value => { } }) .catch(error => { - toast.warning(error.message); + if (error.message.startsWith('Unexpected')) { + toast.warning(args.t('profile.errors.unexpected')); + } else { + toast.warning(error.message); + } return { loading: false }; }); }; }; -export const get_following = value => { +export const get_following = args => { return () => { - return API.get_following(value) + return API.get_following(args) .then(res => { if (Array.isArray(res.results)) { return { @@ -126,7 +126,11 @@ export const get_following = value => { } }) .catch(error => { - toast.warning(error.message); + if (error.message.startsWith('Unexpected')) { + toast.warning(args.t('profile.errors.unexpected')); + } else { + toast.warning(error.message); + } return { loading: false }; }); }; diff --git a/zubhub_frontend/zubhub/src/views/PageWrapper.jsx b/zubhub_frontend/zubhub/src/views/PageWrapper.jsx index 75cbe3c5e..f486372c9 100644 --- a/zubhub_frontend/zubhub/src/views/PageWrapper.jsx +++ b/zubhub_frontend/zubhub/src/views/PageWrapper.jsx @@ -22,6 +22,7 @@ import { Menu, MenuItem, Avatar, + Select, } from '@material-ui/core'; import CustomButton from '../components/button/Button.js'; @@ -32,11 +33,18 @@ import logo from '../assets/images/logos/logo.png'; import styles from '../assets/js/styles/views/page_wrapper/pageWrapperStyles'; import commonStyles from '../assets/js/styles'; +import languageMap from '../assets/js/languageMap.json'; + const useStyles = makeStyles(styles); +const useCommonStyles = makeStyles(commonStyles); const logout = (e, props) => { e.preventDefault(); - return props.logout(props); + return props.logout({ + token: props.auth.token, + history: props.history, + t: props.t, + }); }; const handleScrollTopClick = (e, ref) => { @@ -54,10 +62,14 @@ const handleProfileMenuClose = () => { return { anchorEl: null }; }; +const handleChangeLanguage = ({ e, props }) => { + props.i18n.changeLanguage(e.target.value); +}; + function PageWrapper(props) { const backToTopEl = React.useRef(null); const classes = useStyles(); - const commonClasses = makeStyles(commonStyles)(); + const commonClasses = useCommonStyles(); const [state, setState] = React.useState({ username: null, @@ -83,6 +95,7 @@ function PageWrapper(props) { }; const { anchorEl, loading } = state; + const { t } = props; const profileMenuOpen = Boolean(anchorEl); return ( <> @@ -105,7 +118,7 @@ function PageWrapper(props) { size="large" secondaryButtonStyle > - Login + {t('pageWrapper.navbar.login')} @@ -115,7 +128,7 @@ function PageWrapper(props) { primaryButtonStyle className={commonClasses.marginLeft1em} > - Sign Up + {t('pageWrapper.navbar.signup')} @@ -131,7 +144,7 @@ function PageWrapper(props) { primaryButtonStyle size="small" > - Create Project + {t('pageWrapper.navbar.createProject')} - Projects + {t('pageWrapper.navbar.projects')} @@ -194,7 +207,7 @@ function PageWrapper(props) { color="textPrimary" component="span" > - Followers + {t('pageWrapper.navbar.followers')} @@ -208,7 +221,7 @@ function PageWrapper(props) { color="textPrimary" component="span" > - Following + {t('pageWrapper.navbar.following')} @@ -222,22 +235,18 @@ function PageWrapper(props) { color="textPrimary" component="span" > - Saved Projects + {t('pageWrapper.navbar.savedProjects')} logout(e, props)} > - - Logout - + {t('pageWrapper.navbar.logout')} @@ -262,6 +271,18 @@ function PageWrapper(props) { alt="unstructured-studio-logo" /> +
handleScrollTopClick(e, backToTopEl)} @@ -296,8 +317,8 @@ const mapDispatchToProps = dispatch => { set_auth_user: auth_user => { dispatch(AuthActions.setAuthUser(auth_user)); }, - logout: props => { - return dispatch(AuthActions.logout(props)); + logout: args => { + return dispatch(AuthActions.logout(args)); }, get_auth_user: props => { return dispatch(AuthActions.get_auth_user(props)); diff --git a/zubhub_frontend/zubhub/src/views/create_project/CreateProject.jsx b/zubhub_frontend/zubhub/src/views/create_project/CreateProject.jsx index 8bc043e32..069fad071 100644 --- a/zubhub_frontend/zubhub/src/views/create_project/CreateProject.jsx +++ b/zubhub_frontend/zubhub/src/views/create_project/CreateProject.jsx @@ -44,6 +44,53 @@ const useStyles = makeStyles(styles); let image_field_touched = false; let video_field_touched = false; +const getProject = (refs, props, state) => { + return props + .get_project({ + id: props.match.params.id, + token: props.auth.token, + }) + .then(obj => { + if (!obj.project) { + return obj; + } else { + const { image_upload } = state; + + props.setFieldValue('title', obj.project.title); + if (refs.titleEl.current) + refs.titleEl.current.firstChild.value = obj.project.title; + + props.setFieldValue('description', obj.project.description); + if (refs.descEl.current) + refs.descEl.current.firstChild.value = obj.project.description; + + if (obj.project.video) { + props.setFieldValue('video', obj.project.video); + if (refs.videoEl.current) + refs.videoEl.current.firstChild.value = obj.project.video; + } + + if (refs.imageCountEl.current) { + refs.imageCountEl.current.innerText = obj.project.images.length; + refs.imageCountEl.current.style.fontSize = '0.8rem'; + } + + if (refs.materialsUsedEl.current) { + props.setFieldValue('materials_used', obj.project.materials_used); + refs.materialsUsedEl.current.value = obj.project.materials_used; + } + + image_upload.uploaded_images_url = obj.project.images; + + return { + loading: false, + materials_used: obj.project.materials_used.split(','), + image_upload, + }; + } + }); +}; + const handleImageFieldChange = (refs, props, state, handleSetState) => { refs.imageCountEl.current.innerText = refs.imageEl.current.files.length; refs.imageCountEl.current.style.fontSize = '0.8rem'; @@ -107,6 +154,8 @@ const addMaterialUsed = (e, props, refs) => { function CreateProject(props) { const refs = { + titleEl: React.useRef(null), + descEl: React.useRef(null), imageEl: React.useRef(null), imageUploadButtonEl: React.useRef(null), imageCountEl: React.useRef(null), @@ -117,6 +166,7 @@ function CreateProject(props) { const classes = useStyles(); const [state, setState] = React.useState({ + loading: true, error: null, materialsUsedModalOpen: false, materials_used: [], @@ -130,12 +180,20 @@ function CreateProject(props) { }, }); + React.useEffect(() => { + if (props.match.params.id) { + handleSetState(getProject(refs, props, state)); + } else { + handleSetState({ loading: false }); + } + }, []); + useStateUpdateCallback(() => { if ( state.image_upload.images_to_upload.length === state.image_upload.successful_uploads ) { - handleSetState(upload_project()); + upload_project(); } }, [state.image_upload.successful_uploads]); @@ -201,8 +259,7 @@ function CreateProject(props) { if (err.message.startsWith('Unexpected')) { handleSetState({ - error: - 'An error occured while performing this action. Please try again later', + error: props.t('createProject.errors.unexpected'), image_upload, }); } else { @@ -228,37 +285,42 @@ function CreateProject(props) { 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 : '', - }) - .catch(error => { - const messages = JSON.parse(error.message); - if (typeof messages === 'object') { - let non_field_errors; - Object.keys(messages).forEach(key => { - if (key === 'non_field_errors') { - non_field_errors = { error: messages[key][0] }; - } else { - props.setFieldTouched(key, true, false); - props.setFieldError(key, messages[key][0]); - } - }); - if (non_field_errors) return non_field_errors; - } else { - return { - error: - 'An error occured while performing this action. Please try again later', - }; - } - }); + + const create_or_update = props.match.params.id + ? props.update_project + : props.create_project; + + return create_or_update({ + ...props.values, + id: props.match.params.id, + token: props.auth.token, + images: state.image_upload.uploaded_images_url, + video: props.values.video ? props.values.video : '', + t: props.t, + }).catch(error => { + const messages = JSON.parse(error.message); + if (typeof messages === 'object') { + const server_errors = {}; + Object.keys(messages).forEach(key => { + if (key === 'non_field_errors') { + server_errors['non_field_errors'] = messages[key][0]; + } else { + server_errors[key] = messages[key][0]; + } + }); + props.setStatus({ ...props.status, ...server_errors }); + } else { + props.setStatus({ + ...props.status, + non_field_errors: props.t('createProject.errors.unexpected'), + }); + } + }); }; const init_project = e => { e.preventDefault(); + if (!props.auth.token) { props.history.push('/login'); } else { @@ -272,10 +334,15 @@ function CreateProject(props) { video_field_touched = true; props.validateForm().then(errors => { - if (Object.keys(errors).length > 0) { + if ( + Object.keys(errors).length > 0 && + !(Object.keys(errors).length === 2) && + !(errors['project_images'] === 'imageOrVideo') && + state.image_upload.uploaded_images_url.length === 0 + ) { return; } else if (refs.imageEl.current.files.length === 0) { - handleSetState(upload_project()); + upload_project(); } else { const { image_upload } = state; image_upload.upload_dialog = true; @@ -302,11 +369,11 @@ function CreateProject(props) { } }; - const { error, image_upload, materials_used } = state; + const { image_upload, materials_used } = state; + const { t } = props; + const id = props.match.params.id; if (!props.auth.token) { - return ( - - ); + return ; } else { return ( @@ -327,22 +394,31 @@ function CreateProject(props) { component="h2" color="textPrimary" > - Create Project + {!id + ? t('createProject.welcomeMsg.primary') + : t('createProject.inputs.edit')} - Tell us about your project! + {t('createProject.welcomeMsg.secondary')} - - {error && ( + + {props.status && props.status['non_field_errors'] && ( - {error} + {props.status['non_field_errors']} )} @@ -355,16 +431,24 @@ function CreateProject(props) { size="small" fullWidth margin="small" - error={props.touched['title'] && props.errors['title']} + error={ + (props.status && props.status['title']) || + (props.touched['title'] && props.errors['title']) + } > - Title + {t('createProject.inputs.title.label')} - {props.touched['title'] && props.errors['title']} + {(props.status && props.status['title']) || + (props.touched['title'] && + props.errors['title'] && + t( + `createProject.inputs.title.errors.${props.errors['title']}`, + ))} @@ -387,22 +476,29 @@ function CreateProject(props) { fullWidth margin="small" error={ - props.touched['description'] && - props.errors['description'] + (props.status && props.status['description']) || + (props.touched['description'] && + props.errors['description']) } > - Description + {t('createProject.inputs.description.label')} - Tell us something interesting about the project! You - can share what it is about, what inspired you to - make it, your making process, fun and challenging - moments you experienced, etc. + {t('createProject.inputs.description.helperText')}
- {props.touched['description'] && - props.errors['description']} + {(props.status && props.status['description']) || + (props.touched['description'] && + props.errors['description'] && + t( + `createProject.inputs.description.errors.${props.errors['description']}`, + ))}
@@ -430,8 +527,9 @@ function CreateProject(props) { - {props.errors['project_images']} + {(props.status && props.status['project_images']) || + (props.errors['project_images'] && + t( + `createProject.inputs.projectImages.errors.${props.errors['project_images']}`, + ))} @@ -490,17 +592,24 @@ function CreateProject(props) { size="small" fullWidth margin="small" - error={props.touched['video'] && props.errors['video']} + error={ + (props.status && props.status['video']) || + (props.touched['video'] && props.errors['video']) + } > - Video URL + {t('createProject.inputs.video.label')} - YouTube, Vimeo, Google Drive links are supported + {t('createProject.inputs.video.helperText')}
- {props.touched['video'] && props.errors['video']} + {(props.status && props.status['video']) || + (props.touched['video'] && + props.errors['video'] && + t( + `createProject.inputs.video.errors.${props.errors['video']}`, + ))} @@ -530,8 +644,9 @@ function CreateProject(props) { fullWidth margin="small" error={ - props.touched['materials_used'] && - props.errors['materials_used'] + (props.status && props.status['materials_used']) || + (props.touched['materials_used'] && + props.errors['materials_used']) } > - Materials Used + {t('createProject.inputs.materialsUsed.label')} {materials_used.map((material, num) => @@ -575,7 +690,9 @@ function CreateProject(props) { id="add_materials_used" name="add_materials_used" type="text" - placeholder="Add a material and click the + button" + placeholder={t( + 'createProject.inputs.materialsUsed.placeholder', + )} onClick={() => props.setFieldTouched('materials_used') } @@ -589,8 +706,13 @@ function CreateProject(props) { } /> - {props.touched['materials_used'] && - props.errors['materials_used']} + {(props.status && + props.status['materials_used']) || + (props.touched['materials_used'] && + props.errors['materials_used'] && + t( + `createProject.inputs.materialsUsed.errors.${props.errors['materials_used']}`, + ))} @@ -625,7 +747,9 @@ function CreateProject(props) { primaryButtonStyle fullWidth > - Create Project + {!id + ? t('createProject.inputs.submit') + : t('createProject.inputs.edit')} @@ -680,7 +804,9 @@ function CreateProject(props) { CreateProject.propTypes = { auth: PropTypes.object.isRequired, + get_project: PropTypes.func.isRequired, create_project: PropTypes.func.isRequired, + update_project: PropTypes.func.isRequired, }; const mapStateToProps = state => { @@ -691,9 +817,15 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { + get_project: values => { + return dispatch(ProjectActions.get_project(values)); + }, create_project: props => { return dispatch(ProjectActions.create_project(props)); }, + update_project: props => { + return dispatch(ProjectActions.update_project(props)); + }, }; }; @@ -709,23 +841,15 @@ export default connect( 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'), + title: Yup.string().max(100, 'max').required('required'), + description: Yup.string().max(10000, 'max').required('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('not_an_image', 'only images are allowed', value => { + .test('image_is_empty', 'imageOrVideo', function (value) { + return image_field_touched && !value && !this.parent.video + ? false + : true; + }) + .test('not_an_image', 'onlyImages', value => { if (value) { let not_an_image = false; for (let index = 0; index < value.files.length; index++) { @@ -738,48 +862,35 @@ export default connect( return true; } }) - .test('too_many_images', 'too many images uploaded', value => { + .test('too_many_images', 'tooManyImages', 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 10mb', - value => { - if (value) { - let image_size_too_large = false; - for (let index = 0; index < value.files.length; index++) { - if (value.files[index].size / 1000 > 10240) { - image_size_too_large = true; - } + .test('image_size_too_large', 'imageSizeTooLarge', value => { + if (value) { + let image_size_too_large = false; + for (let index = 0; index < value.files.length; index++) { + if (value.files[index].size / 1000 > 10240) { + image_size_too_large = true; } - return image_size_too_large ? false : true; - } else { - return 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'), + .url('shouldBeVideoUrl') + .max(1000, 'max') + .test('video_is_empty', 'imageOrVideo', function (value) { + return video_field_touched && !value && !this.parent.project_images + ? false + : true; + }), + materials_used: Yup.string().max(10000, 'max').required('required'), }), })(CreateProject), ); diff --git a/zubhub_frontend/zubhub/src/views/edit_profile/EditProfile.jsx b/zubhub_frontend/zubhub/src/views/edit_profile/EditProfile.jsx new file mode 100644 index 000000000..fa4160c30 --- /dev/null +++ b/zubhub_frontend/zubhub/src/views/edit_profile/EditProfile.jsx @@ -0,0 +1,464 @@ +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 { toast } from 'react-toastify'; + +import { makeStyles } from '@material-ui/core/styles'; +import { + Grid, + Box, + Divider, + Container, + Card, + CardActionArea, + CardContent, + Select, + MenuItem, + Typography, + OutlinedInput, + Tooltip, + ClickAwayListener, + InputLabel, + FormHelperText, + FormControl, +} 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 styles from '../../assets/js/styles/views/edit_profile/editProfileStyles'; + +const useStyles = makeStyles(styles); + +const get_locations = props => { + return props.get_locations({ t: props.t }); +}; + +const getProfile = (refs, props) => { + return props.get_auth_user(props).then(obj => { + if (!obj.id) { + return obj; + } else { + props.setFieldValue('username', obj.username); + if (refs.usernameEl.current) + refs.usernameEl.current.firstChild.value = obj.username; + + props.setFieldValue('bio', obj.bio); + if (refs.bioEl.current) refs.bioEl.current.firstChild.value = obj.bio; + + if (obj.dateOfBirth) { + props.setFieldValue('dateOfBirth', obj.dateOfBirth); + } + + if (obj.location) { + props.setFieldValue('user_location', obj.location); + } + } + }); +}; + +const editProfile = (e, props) => { + e.preventDefault(); + if (props.values.user_location.length < 1) { + props.validateField('user_location'); + } else { + return props + .edit_user_profile({ ...props.values, token: props.auth.token }) + .then(res => { + toast.success(props.t('editProfile.toastSuccess')); + props.history.push('/profile'); + }) + .catch(error => { + const messages = JSON.parse(error.message); + if (typeof messages === 'object') { + const server_errors = {}; + Object.keys(messages).forEach(key => { + if (key === 'non_field_errors') { + server_errors['non_field_errors'] = messages[key][0]; + } else if (key === 'location') { + server_errors['user_location'] = messages[key][0]; + } else { + server_errors[key] = messages[key][0]; + } + }); + props.setStatus({ ...props.status, ...server_errors }); + } else { + props.setStatus({ + ...props.status, + non_field_errors: props.t('editProfile.errors.unexpected'), + }); + } + }); + } +}; + +const handleTooltipToggle = ({ toolTipOpen }) => { + return { toolTipOpen: !toolTipOpen }; +}; + +function EditProfile(props) { + const refs = { + usernameEl: React.useRef(null), + bioEl: React.useRef(null), + dobEl: React.useRef(null), + locationEl: React.useRef(null), + }; + + const [state, setState] = React.useState({ + locations: [], + current_location: '', + toolTipOpen: false, + }); + + React.useEffect(() => { + getProfile(refs, props); + handleSetState(get_locations(props)); + }, []); + + const classes = useStyles(); + + const handleSetState = obj => { + if (obj) { + Promise.resolve(obj).then(obj => { + setState({ ...state, ...obj }); + }); + } + }; + + const { locations, toolTipOpen } = state; + const { t } = props; + + return ( + + + + + +
editProfile(e, props)} + > + + {t('editProfile.welcomeMsg.primary')} + + + {t('editProfile.welcomeMsg.secondary')} + + + + + {props.status && props.status['non_field_errors'] && ( + + {props.status['non_field_errors']} + + )} + + + + + + {t('editProfile.inputs.username.label')} + + + 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.status && props.status['username']) || + (props.touched['username'] && + props.errors['username'] && + t( + `editProfile.inputs.username.errors.${this.props.errors['username']}`, + ))} + + + + + + + + {t('editProfile.inputs.location.label')} + + + + {(props.status && props.status['user_location']) || + (props.touched['user_location'] && + props.errors['user_location'] && + t( + `editProfile.inputs.location.errors.${props.errors['user_location']}`, + ))} + + + + + + + + {t('editProfile.inputs.bio.label')} + + + + + {t('editProfile.inputs.bio.helpText')} + +
+ {(props.status && props.status['bio']) || + (props.touched['bio'] && + props.errors['bio'] && + t( + `editProfile.inputs.bio.errors.${props.errors['bio']}`, + ))} +
+
+
+ + + + {t('editProfile.inputs.submit')} + + +
+
+ + + + + + {t('editProfile.or')} + + + + + + + + {t('editProfile.backToProfile')} + + + + +
+
+
+
+
+ ); +} + +EditProfile.propTypes = { + auth: PropTypes.object.isRequired, + get_auth_user: PropTypes.func.isRequired, + set_auth_user: PropTypes.func.isRequired, + edit_user_profile: PropTypes.func.isRequired, + get_locations: PropTypes.func.isRequired, +}; + +const mapStateToProps = state => { + return { + auth: state.auth, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + get_auth_user: props => { + return dispatch(AuthActions.get_auth_user(props)); + }, + edit_user_profile: props => { + return dispatch(UserActions.edit_user_profile(props)); + }, + get_locations: props => { + return dispatch(AuthActions.get_locations()); + }, + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)( + withFormik({ + mapPropsToValue: () => ({ + username: '', + user_location: '', + bio: '', + }), + validationSchema: Yup.object().shape({ + username: Yup.string().required('required'), + user_location: Yup.string().min(1, 'min').required('required'), + bio: Yup.string().max(255, 'tooLong'), + }), + })(EditProfile), +); diff --git a/zubhub_frontend/zubhub/src/views/email_confirm/EmailConfirm.jsx b/zubhub_frontend/zubhub/src/views/email_confirm/EmailConfirm.jsx index c67fd2e06..0051c7787 100644 --- a/zubhub_frontend/zubhub/src/views/email_confirm/EmailConfirm.jsx +++ b/zubhub_frontend/zubhub/src/views/email_confirm/EmailConfirm.jsx @@ -31,16 +31,21 @@ const getUsernameAndKey = queryString => { const confirmEmail = (e, props, state) => { e.preventDefault(); - return props.send_email_confirmation(props, state.key).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 }; - } - }); + return props + .send_email_confirmation({ + key: state.key, + t: props.t, + history: props.history, + }) + .catch(error => { + if (error.message.startsWith('Unexpected')) { + props.setStatus({ + non_field_errors: props.t('emailConfirm.errors.unexpected'), + }); + } else { + props.setStatus({ non_field_errors: error.message }); + } + }); }; function EmailConfirm(props) { @@ -49,7 +54,6 @@ function EmailConfirm(props) { let { username, key } = getUsernameAndKey(props.location.search); const [state, setState] = React.useState({ - error: null, username: username ?? null, key: key ?? null, }); @@ -64,6 +68,7 @@ function EmailConfirm(props) { const { error } = state; username = state.username; + const { t } = props; return ( @@ -84,22 +89,28 @@ function EmailConfirm(props) { color="textPrimary" className={classes.titleStyle} > - Email Confirmation + {t('emailConfirm.welcomeMsg.primary')} - Please Confirm that you are {username} and that the email - belongs to you: + {t('emailConfirm.welcomeMsg.secondary').replace( + '<>', + username, + )} - {error !== null && ( + {props.status && props.status['non_field_errors'] && ( - {error} + {props.status['non_field_errors']} )} @@ -112,7 +123,7 @@ function EmailConfirm(props) { fullWidth primaryButtonStyle > - Confirm + {t('emailConfirm.inputs.submit')} @@ -138,8 +149,8 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - send_email_confirmation: (props, key) => { - return dispatch(AuthActions.send_email_confirmation(props, key)); + send_email_confirmation: args => { + return dispatch(AuthActions.send_email_confirmation(args)); }, }; }; diff --git a/zubhub_frontend/zubhub/src/views/login/Login.jsx b/zubhub_frontend/zubhub/src/views/login/Login.jsx index 5bb75a3ca..5c697d96c 100644 --- a/zubhub_frontend/zubhub/src/views/login/Login.jsx +++ b/zubhub_frontend/zubhub/src/views/login/Login.jsx @@ -45,33 +45,33 @@ const handleMouseDownPassword = e => { const login = (e, props) => { e.preventDefault(); - return props.login(props).catch(error => { - const messages = JSON.parse(error.message); - if (typeof messages === 'object') { - let non_field_errors; - Object.keys(messages).forEach(key => { - if (key === 'non_field_errors') { - non_field_errors = { error: messages[key][0] }; - } else { - props.setFieldTouched(key, true, false); - props.setFieldError(key, messages[key][0]); - } - }); - return non_field_errors; - } else { - return { - error: - 'An error occured while performing this action. Please try again later', - }; - } - }); + return props + .login({ values: props.values, history: props.history }) + .catch(error => { + const messages = JSON.parse(error.message); + if (typeof messages === 'object') { + const server_errors = {}; + Object.keys(messages).forEach(key => { + if (key === 'non_field_errors') { + server_errors['non_field_errors'] = messages[key][0]; + } else { + server_errors[key] = messages[key][0]; + } + }); + props.setStatus({ ...props.status, ...server_errors }); + } else { + props.setStatus({ + ...props.status, + non_field_errors: props.t('login.errors.unexpected'), + }); + } + }); }; function Login(props) { const classes = useStyles(); const [state, setState] = React.useState({ - error: null, showPassword: false, }); @@ -83,7 +83,8 @@ function Login(props) { } }; - const { error, showPassword } = state; + const { showPassword } = state; + const { t } = props; return ( @@ -104,17 +105,24 @@ function Login(props) { color="textPrimary" className={classes.titleStyle} > - Welcome to Zubhub + {t('login.welcomeMsg.primary')} - Login to get started! + {t('login.welcomeMsg.secondary')} - - {error && ( + + {props.status && props.status['non_field_errors'] && ( - {error} + {props.status['non_field_errors']} )} @@ -127,14 +135,15 @@ function Login(props) { fullWidth margin="normal" error={ - props.touched['username'] && props.errors['username'] + (props.status && props.status['username']) || + (props.touched['username'] && props.errors['username']) } > - Username Or Email + {t('login.inputs.username.label')} - {props.touched['username'] && props.errors['username']} + {(props.status && props.status['username']) || + (props.touched['username'] && + props.errors['username'] && + t( + `login.inputs.username.errors.${props.errors['username']}`, + ))} @@ -159,10 +173,13 @@ function Login(props) { fullWidth margin="normal" error={ - props.touched['password'] && props.errors['password'] + (props.status && props.status['password']) || + (props.touched['password'] && props.errors['password']) } > - Password + + {t('login.inputs.password.label')} + - {props.touched['password'] && props.errors['password']} + {(props.status && props.status['password']) || + (props.touched['password'] && + props.errors['password'] && + t( + `login.inputs.password.errors.${props.errors['password']}`, + ))} @@ -203,7 +225,7 @@ function Login(props) { primaryButtonStyle fullWidth > - Login + {t('login.inputs.submit')} @@ -217,7 +239,7 @@ function Login(props) { color="textSecondary" component="p" > - Not a Member ? + {t('login.notAMember')} @@ -230,7 +252,7 @@ function Login(props) { secondaryButtonStyle fullWidth > - Signup + {t('login.signup')} @@ -240,7 +262,7 @@ function Login(props) { to="/password-reset" className={classes.secondaryLink} > - Forgot Password? + {t('login.forgotPassword')} @@ -270,8 +292,8 @@ const mapDispatchToProps = dispatch => { set_auth_user: auth_user => { dispatch(AuthActions.setAuthUser(auth_user)); }, - login: props => { - return dispatch(AuthActions.login(props)); + login: args => { + return dispatch(AuthActions.login(args)); }, }; }; @@ -285,9 +307,8 @@ export default connect( password: '', }), validationSchema: Yup.object().shape({ - password: Yup.string() - .min(8, 'your password is too short') - .required('input your password'), + username: Yup.string().required('required'), + password: Yup.string().min(8, 'min').required('required'), }), })(Login), ); diff --git a/zubhub_frontend/zubhub/src/views/password_reset/PasswordReset.jsx b/zubhub_frontend/zubhub/src/views/password_reset/PasswordReset.jsx index cf1ff7ba3..96b0621bc 100644 --- a/zubhub_frontend/zubhub/src/views/password_reset/PasswordReset.jsx +++ b/zubhub_frontend/zubhub/src/views/password_reset/PasswordReset.jsx @@ -30,44 +30,36 @@ const useStyles = makeStyles(styles); const sendPasswordResetLink = (e, props) => { e.preventDefault(); - return props.send_password_reset_link(props).catch(error => { - const messages = JSON.parse(error.message); - let non_field_errors; - if (typeof messages === 'object') { - Object.keys(messages).forEach(key => { - if (key !== 'email') { - non_field_errors = { error: messages[key][0] }; - } else { - props.setFieldTouched(key, true, false); - props.setFieldError(key, messages[key][0]); - } - }); - return non_field_errors; - } else { - return { - error: - 'An error occured while performing this action. Please try again later', - }; - } - }); + return props + .send_password_reset_link({ + email: props.values.email, + t: props.t, + history: props.history, + }) + .catch(error => { + const messages = JSON.parse(error.message); + if (typeof messages === 'object') { + const server_errors = {}; + Object.keys(messages).forEach(key => { + if (key !== 'email') { + server_errors['non_field_errors'] = messages[key][0]; + } else { + server_errors[key] = messages[key][0]; + } + }); + props.setStatus({ ...props.status, ...server_errors }); + } else { + props.setStatus({ + ...props.status, + non_field_errors: props.t('passwordResetConfirm.errors.unexpected'), + }); + } + }); }; 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; + const { t } = props; return ( @@ -79,7 +71,7 @@ function PasswordReset(props) { className="auth-form" name="password_reset" noValidate="noValidate" - onSubmit={e => handleSetState(sendPasswordResetLink(e, props))} + onSubmit={e => sendPasswordResetLink(e, props)} > - Password Reset + {t('passwordReset.welcomeMsg.primary')} - Input your email so we can send you a password reset link + {t('passwordReset.welcomeMsg.secondary')} - - {error && ( + + {props.status && props.status['non_field_errors'] && ( - {error} + {props.status['non_field_errors']} )} @@ -113,13 +112,16 @@ function PasswordReset(props) { shrink: true, }} margin="normal" - error={props.touched['email'] && props.errors['email']} + error={ + (props.status && props.status['email']) || + (props.touched['email'] && props.errors['email']) + } > - Email + {t('passwordReset.inputs.email.label')} - {props.touched['email'] && props.errors['email']} + {(props.status && props.status['email']) || + (props.touched['email'] && + props.errors['email'] && + t( + `passwordReset.inputs.email.errors.${props.errors['email']}`, + ))} @@ -143,7 +150,7 @@ function PasswordReset(props) { type="submit" fullWidth > - Send Reset Link + {t('passwordReset.inputs.submit')} @@ -184,7 +191,7 @@ export default connect( email: '', }), validationSchema: Yup.object().shape({ - email: Yup.string().email('invalid email').required('email required'), + email: Yup.string().email('invalid').required('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 index b87775979..e1b08aec2 100644 --- a/zubhub_frontend/zubhub/src/views/password_reset_confirm/PasswordResetConfirm.jsx +++ b/zubhub_frontend/zubhub/src/views/password_reset_confirm/PasswordResetConfirm.jsx @@ -4,8 +4,6 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { toast } from 'react-toastify'; - import { withFormik } from 'formik'; import * as Yup from 'yup'; @@ -45,25 +43,31 @@ const resetPassword = (e, props) => { e.preventDefault(); const { uid, token } = getUidAndToken(props.location.search); return props - .password_reset_confirm({ ...props.values, uid, token }) + .password_reset_confirm({ + ...props.values, + uid, + token, + t: props.t, + history: props.history, + }) .catch(error => { const messages = JSON.parse(error.message); if (typeof messages === 'object') { - let non_field_errors; + const server_errors = {}; Object.keys(messages).forEach(key => { if (key !== 'new_password1' && key !== 'new_password2') { - non_field_errors = { error: messages[key][0] }; + server_errors['non_field_errors'] = messages[key][0]; } else { - props.setFieldTouched(key, true, false); - props.setFieldError(key, messages[key][0]); + server_errors[key] = messages[key][0]; } }); - return non_field_errors; + + props.setStatus({ ...props.status, ...server_errors }); } else { - return { - error: - 'An error occured while performing this action. Please try again later', - }; + props.setStatus({ + ...props.status, + non_field_errors: props.t('passwordResetConfirm.errors.unexpected'), + }); } }); }; @@ -86,7 +90,6 @@ function PasswordResetConfirm(props) { const classes = useStyles(); const [state, setState] = React.useState({ - error: null, showPassword1: false, showPassword2: false, }); @@ -99,7 +102,8 @@ function PasswordResetConfirm(props) { } }; - const { error, showPassword1, showPassword2 } = state; + const { showPassword1, showPassword2 } = state; + const { t } = props; return ( @@ -111,7 +115,7 @@ function PasswordResetConfirm(props) { className="auth-form" name="password_reset_confirm" noValidate="noValidate" - onSubmit={e => handleSetState(resetPassword(e, props))} + onSubmit={e => resetPassword(e, props)} > - Password Reset Confirmation + {t('passwordResetConfirm.welcomeMsg.primary')} - - {error && ( + + {props.status && props.status['non_field_errors'] && ( - {error} + {props.status['non_field_errors']} )} @@ -141,15 +152,16 @@ function PasswordResetConfirm(props) { fullWidth margin="normal" error={ - props.touched['new_password1'] && - props.errors['new_password1'] + (props.status && props.status['new_password1']) || + (props.touched['new_password1'] && + props.errors['new_password1']) } > - New Password + {t('passwordResetConfirm.inputs.newPassword1.label')} - {props.touched['new_password'] && - props.errors['new_password']} + {(props.status && props.status['new_password1']) || + (props.touched['new_password1'] && + props.errors['new_password1'] && + t( + `passwordResetConfirm.inputs.newPassword1.errors.${props.errors['new_password1']}`, + ))} @@ -195,15 +211,16 @@ function PasswordResetConfirm(props) { fullWidth margin="normal" error={ - props.touched['new_password2'] && - props.errors['new_password2'] + (props.status && props.status['new_password2']) || + (props.touched['new_password2'] && + props.errors['new_password2']) } > - Confirm Password + {t('passwordResetConfirm.inputs.newPassword2.label')} - {props.touched['new_password2'] && - props.errors['new_password2']} + {(props.status && props.status['new_password2']) || + (props.touched['new_password2'] && + props.errors['new_password2'] && + t( + `passwordResetConfirm.inputs.newPassword2.errors.${props.errors['new_password2']}`, + ))} @@ -248,7 +269,7 @@ function PasswordResetConfirm(props) { primaryButtonStyle fullWidth > - Reset Password + {t('passwordResetConfirm.inputs.submit')} @@ -274,8 +295,8 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - password_reset_confirm: props => { - return dispatch(AuthActions.password_reset_confirm(props)); + password_reset_confirm: args => { + return dispatch(AuthActions.password_reset_confirm(args)); }, }; }; @@ -290,12 +311,10 @@ export default connect( new_password2: '', }), validationSchema: Yup.object().shape({ - new_password1: Yup.string() - .min(8, 'your password is too short') - .required('input your password'), + new_password1: Yup.string().min(8, 'min').required('required'), new_password2: Yup.string() - .oneOf([Yup.ref('new_password1'), null], 'Passwords must match') - .required('input a confirmation password'), + .oneOf([Yup.ref('new_password1'), null], 'noMatch') + .required('required'), }), })(PasswordResetConfirm), ); diff --git a/zubhub_frontend/zubhub/src/views/profile/Profile.jsx b/zubhub_frontend/zubhub/src/views/profile/Profile.jsx index e74b91762..5b664553f 100644 --- a/zubhub_frontend/zubhub/src/views/profile/Profile.jsx +++ b/zubhub_frontend/zubhub/src/views/profile/Profile.jsx @@ -9,6 +9,7 @@ import { toast } from 'react-toastify'; import { makeStyles } from '@material-ui/core/styles'; import ShareIcon from '@material-ui/icons/Share'; +import MoreVertIcon from '@material-ui/icons/MoreVert'; import { Tooltip, Badge, @@ -17,6 +18,8 @@ import { Box, Container, Paper, + Menu, + MenuItem, Dialog, DialogActions, DialogContent, @@ -37,13 +40,10 @@ 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'; +import commonStyles from '../../assets/js/styles'; const useStyles = makeStyles(styles); - -const handleToggleEditProfileModal = ({ openEditProfileModal }) => { - openEditProfileModal = !openEditProfileModal; - return { openEditProfileModal }; -}; +const useCommonStyles = makeStyles(commonStyles); const getUserPofile = props => { let username = props.match.params.username; @@ -51,35 +51,14 @@ const getUserPofile = props => { 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 => { - if (!res.id) { - res = Object.keys(res) - .map(key => res[key]) - .join('\n'); - throw new Error(res); - } - username.value = ''; - return { ...res, ...handleToggleEditProfileModal(state) }; - }) - .catch(error => ({ dialogError: error.message })); - } else { - return handleToggleEditProfileModal(state); - } + return props.get_user_profile({ + username, + token: props.auth.token, + t: props.t, + }); }; -const copyProfileUrl = profile => { +const copyProfileUrl = ({ profile, props }) => { const tempInput = document.createElement('textarea'); tempInput.value = `${document.location.origin}/creators/${profile.username}`; tempInput.style.top = '0'; @@ -90,14 +69,12 @@ const copyProfileUrl = profile => { tempInput.focus(); tempInput.select(); if (document.execCommand('copy')) { - toast.success( - 'your profile url has been successfully copied to your clipboard!', - ); + toast.success(props.t('profile.toastSuccess')); rootElem.removeChild(tempInput); } }; -const updateProjects = (res, { results: projects }) => { +const updateProjects = (res, { results: projects }, props) => { return res .then(res => { if (res.project && res.project.title) { @@ -113,7 +90,11 @@ const updateProjects = (res, { results: projects }) => { } }) .catch(error => { - toast.warning(error.message); + if (error.message.startsWith('Unexpected')) { + toast.warning(props.t('profile.errors.unexpected')); + } else { + toast.warning(error.message); + } return { loading: false }; }); }; @@ -122,20 +103,48 @@ const toggle_follow = (id, props) => { if (!props.auth.token) { props.history.push('/login'); } else { - return props.toggle_follow({ id, token: props.auth.token }); + return props.toggle_follow({ id, token: props.auth.token, t: props.t }); + } +}; + +const handleMoreMenuOpen = e => { + return { moreAnchorEl: e.currentTarget }; +}; + +const handleMoreMenuClose = () => { + return { moreAnchorEl: null }; +}; + +const handleToggleDeleteAccountModal = state => { + const openDeleteAccountModal = !state.openDeleteAccountModal; + return { openDeleteAccountModal, moreAnchorEl: null }; +}; + +const deleteAccount = (usernameEl, props) => { + if (usernameEl.current.firstChild.value !== props.auth.username) { + return { dialogError: props.t('profile.delete.errors.incorrectUsernme') }; + } else { + return props.delete_account({ + token: props.auth.token, + history: props.history, + logout: props.logout, + t: props.t, + }); } }; function Profile(props) { - const newUserNameEL = React.useRef(null); + const usernameEl = React.useRef(null); const classes = useStyles(); + const commonClasses = useCommonStyles(); const [state, setState] = React.useState({ results: [], - openEditProfileModal: false, loading: true, profile: {}, + openDeleteAccountModal: false, dialogError: null, + moreAnchorEl: null, }); React.useEffect(() => { @@ -154,10 +163,14 @@ function Profile(props) { results: projects, profile, loading, - openEditProfileModal, + openDeleteAccountModal, dialogError, + moreAnchorEl, } = state; + const moreMenuOpen = Boolean(moreAnchorEl); + const { t } = props; + if (loading) { return ; } else if (profile && Object.keys(profile).length > 0) { @@ -167,17 +180,52 @@ function Profile(props) { {props.auth.username === profile.username ? ( - - handleSetState(handleToggleEditProfileModal(state)) - } - > - Edit - + <> + handleSetState(handleMoreMenuOpen(e))} + > + + + props.history.push('/edit-profile')} + > + {t('profile.edit')} + + handleSetState(handleMoreMenuClose(e))} + > + + + handleSetState(handleToggleDeleteAccountModal(state)) + } + > + {t('profile.delete.label')} + + + + ) : ( {profile.followers.includes(props.auth.id) - ? 'Unfollow' - : 'Follow'} + ? t('profile.unfollow') + : t('profile.follow')} )} @@ -203,7 +251,7 @@ function Profile(props) { badgeContent={ props.auth.id === profile.id ? ( @@ -212,8 +260,8 @@ function Profile(props) { classes.secondaryButton, classes.profileShareButtonStyle, )} - aria-label="share profile url" - onClick={() => copyProfileUrl(profile)} + aria-label={t('profile.ariaLabels.shareProfile')} + onClick={() => copyProfileUrl({ profile, props })} > @@ -251,7 +299,7 @@ function Profile(props) { className={classes.moreInfoStyle} component="h5" > - {profile.projects_count} Projects + {profile.projects_count} {t('profile.projectsCount')} - {profile.followers.length} Followers + {profile.followers.length} {t('profile.followersCount')} - {profile.following_count} Following + {profile.following_count} {t('profile.followingCount')} @@ -290,12 +338,10 @@ function Profile(props) { color="textPrimary" className={classes.titleStyle} > - About Me + {t('profile.about.label')} - {profile.bio - ? profile.bio - : 'You will be able to change this next month 😀!'} + {profile.bio ? profile.bio : t('profile.about.placeholder')} @@ -308,7 +354,7 @@ function Profile(props) { color="textPrimary" className={classes.titleStyle} > - Latest projects of {profile.username} + {t('profile.projects.label')} {profile.username} - View all >> + {t('profile.projects.viewAll')} @@ -335,7 +381,7 @@ function Profile(props) { project={project} key={project.id} updateProjects={res => - handleSetState(updateProjects(res, state)) + handleSetState(updateProjects(res, state, props)) } {...props} /> @@ -347,11 +393,13 @@ function Profile(props) { handleSetState(handleToggleEditProfileModal(state))} - aria-labelledby="edit user profile" + open={openDeleteAccountModal} + onClose={() => handleSetState(handleToggleDeleteAccountModal(state))} + aria-labelledby={t('profile.delete.ariaLabels.deleteAccount')} > - Edit User Profile + + {t('profile.delete.dialog.primary')} + {' '} + {t('profile.delete.dialog.secondary')} - New Username + {t('profile.delete.dialog.inputs.username')} - handleSetState(handleToggleEditProfileModal(state)) + handleSetState(handleToggleDeleteAccountModal(state)) } color="primary" secondaryButtonStyle > - Cancel + {t('profile.delete.dialog.cancel')} - handleSetState(updateProfile(e, props, state, newUserNameEL)) + handleSetState(deleteAccount(usernameEl, props, state)) } - primaryButtonStyle + dangerButtonStyle > - Save + {t('profile.delete.dialog.procceed')} ); } else { - return ( - - ); + return ; } } @@ -420,7 +467,8 @@ Profile.propTypes = { auth: PropTypes.object.isRequired, set_auth_user: PropTypes.func.isRequired, get_user_profile: PropTypes.func.isRequired, - edit_user_profile: PropTypes.func.isRequired, + delete_account: PropTypes.func.isRequired, + logout: PropTypes.func.isRequired, toggle_follow: PropTypes.func.isRequired, toggle_like: PropTypes.func.isRequired, toggle_save: PropTypes.func.isRequired, @@ -437,20 +485,23 @@ const mapDispatchToProps = dispatch => { set_auth_user: auth_user => { dispatch(AuthActions.setAuthUser(auth_user)); }, - get_user_profile: props => { - return dispatch(UserActions.get_user_profile(props)); + get_user_profile: args => { + return dispatch(UserActions.get_user_profile(args)); + }, + delete_account: args => { + return dispatch(AuthActions.delete_account(args)); }, - edit_user_profile: props => { - return dispatch(UserActions.edit_user_profile(props)); + logout: args => { + return dispatch(AuthActions.logout(args)); }, - toggle_follow: props => { - return dispatch(UserActions.toggle_follow(props)); + toggle_follow: args => { + return dispatch(UserActions.toggle_follow(args)); }, - toggle_like: props => { - return dispatch(ProjectActions.toggle_like(props)); + toggle_like: args => { + return dispatch(ProjectActions.toggle_like(args)); }, - toggle_save: props => { - return dispatch(ProjectActions.toggle_save(props)); + toggle_save: args => { + return dispatch(ProjectActions.toggle_save(args)); }, }; }; diff --git a/zubhub_frontend/zubhub/src/views/project_details/ProjectDetails.jsx b/zubhub_frontend/zubhub/src/views/project_details/ProjectDetails.jsx index 5aad0fe58..94377edf7 100644 --- a/zubhub_frontend/zubhub/src/views/project_details/ProjectDetails.jsx +++ b/zubhub_frontend/zubhub/src/views/project_details/ProjectDetails.jsx @@ -21,6 +21,9 @@ import { Container, Paper, Dialog, + DialogTitle, + DialogContent, + DialogActions, } from '@material-ui/core'; import * as UserActions from '../../store/actions/userActions'; @@ -76,6 +79,26 @@ const handleOpenEnlargedImageDialog = (e, state) => { return { enlargedImageUrl: image_url, openEnlargedImageDialog }; }; +const handleToggleDeleteProjectModal = state => { + const openDeleteProjectModal = !state.openDeleteProjectModal; + return { openDeleteProjectModal }; +}; + +const deleteProject = (props, state) => { + if (props.auth.token && props.auth.id === state.project.creator.id) { + return props + .delete_project({ + token: props.auth.token, + id: state.project.id, + t: props.t, + history: props.history, + }) + .catch(error => ({ dialogError: error.message })); + } else { + return handleToggleDeleteProjectModal(state); + } +}; + const add_comment = (e, props, refs, state) => { e.preventDefault(); if (!props.auth.token) { @@ -88,6 +111,7 @@ const add_comment = (e, props, refs, state) => { id: state.project.id, token: props.auth.token, text: comment_text, + t: props.t, }); } }; @@ -100,6 +124,7 @@ const toggle_save = (e, props, id) => { return props.toggle_save({ id, token: props.auth.token, + t: props.t, }); } }; @@ -109,7 +134,7 @@ const toggle_like = (e, props, id) => { if (!props.auth.token) { return props.history.push('/login'); } else { - return props.toggle_like({ id, token: props.auth.token }); + return props.toggle_like({ id, token: props.auth.token, t: props.t }); } }; @@ -119,7 +144,7 @@ const toggle_follow = (e, props, id, state) => { props.history.push('/login'); } else { return props - .toggle_follow({ id, token: props.auth.token }) + .toggle_follow({ id, token: props.auth.token, t: props.t }) .then(({ profile }) => { const { project } = state; if (project.creator.id === profile.id) { @@ -160,6 +185,8 @@ function ProjectDetails(props) { loading: true, enlargedImageUrl: '', openEnlargedImageDialog: false, + openDeleteProjectModal: false, + dialogError: null, }); React.useEffect(() => { @@ -168,6 +195,7 @@ function ProjectDetails(props) { props.get_project({ id: props.match.params.id, token: props.auth.token, + t: props.t, }), ); @@ -200,7 +228,15 @@ function ProjectDetails(props) { } }; - const { project, loading, enlargedImageUrl, openEnlargedImageDialog } = state; + const { + project, + loading, + enlargedImageUrl, + openEnlargedImageDialog, + openDeleteProjectModal, + dialogError, + } = state; + const { t } = props; if (loading) { return ; } else if (Object.keys(project).length > 0) { @@ -231,7 +267,32 @@ function ProjectDetails(props) { {project.creator.username} - {project.creator.id !== props.auth.id ? ( + {project.creator.id === props.auth.id ? ( + <> + + + {t('projectDetails.project.edit')} + + + + handleSetState(handleToggleDeleteProjectModal(state)) + } + > + {t('projectDetails.project.delete.label')} + + + ) : ( {project.creator.followers.includes(props.auth.id) - ? 'Unfollow' - : 'Follow'} + ? t('projectDetails.project.creator.unfollow') + : t('projectDetails.project.creator.follow')} - ) : null} + )} @@ -290,16 +351,26 @@ function ProjectDetails(props) { handleSetState(toggle_like(e, props, project.id)) } > {project.likes.includes(props.auth.id) ? ( - + ) : ( - + )} {nFormatter(project.likes.length)} @@ -308,15 +379,25 @@ function ProjectDetails(props) { handleSetState(toggle_save(e, props, project.id)) } > {project.saved_by.includes(props.auth.id) ? ( - + ) : ( - + )} - Description + {t('projectDetails.project.description')} - Materials used + {t('projectDetails.project.materials')} - {nFormatter(project.comments.length)} Comments + {nFormatter(project.comments.length)}{' '} + {t('projectDetails.project.comments.label')} - Comment + {t('projectDetails.project.comments.action')} @@ -460,7 +544,9 @@ function ProjectDetails(props) { {comment.creator} - {dFormatter(comment.created_on)} + {`${dFormatter(comment.created_on).value} ${t( + `date.${dFormatter(comment.created_on).key}`, + )} ${t('date.ago')}`}{' '} @@ -486,7 +572,7 @@ function ProjectDetails(props) { openEnlargedImageDialog: !openEnlargedImageDialog, }) } - aria-labelledby="enlarged image dialog" + aria-labelledby={t('projectDetails.ariaLabels.imageDialog')} > {`${project.title}`} + + handleSetState(handleToggleDeleteProjectModal(state))} + aria-labelledby={t('projectDetails.ariaLabels.deleteProject')} + > + + {t('projectDetails.project.delete.dialog.primary')} + + + {dialogError !== null && ( + + {dialogError} + + )} + {' '} + + + {t('projectDetails.project.delete.dialog.secondary')} + + + + + handleSetState(handleToggleDeleteProjectModal(state)) + } + color="primary" + secondaryButtonStyle + > + {t('projectDetails.project.delete.dialog.cancel')} + + handleSetState(deleteProject(props, state))} + dangerButtonStyle + > + {t('projectDetails.project.delete.dialog.proceed')} + + + ); } else { - return ( - - ); + return ; } } @@ -520,20 +648,23 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - get_project: values => { - return dispatch(ProjectActions.get_project(values)); + get_project: args => { + return dispatch(ProjectActions.get_project(args)); + }, + delete_project: args => { + return dispatch(ProjectActions.delete_project(args)); }, - toggle_follow: values => { - return dispatch(UserActions.toggle_follow(values)); + toggle_follow: args => { + return dispatch(UserActions.toggle_follow(args)); }, - toggle_like: values => { - return dispatch(ProjectActions.toggle_like(values)); + toggle_like: args => { + return dispatch(ProjectActions.toggle_like(args)); }, - toggle_save: values => { - return dispatch(ProjectActions.toggle_save(values)); + toggle_save: args => { + return dispatch(ProjectActions.toggle_save(args)); }, - add_comment: values => { - return dispatch(ProjectActions.add_comment(values)); + add_comment: args => { + return dispatch(ProjectActions.add_comment(args)); }, }; }; diff --git a/zubhub_frontend/zubhub/src/views/projects/Projects.jsx b/zubhub_frontend/zubhub/src/views/projects/Projects.jsx index cede343bb..0fb655280 100644 --- a/zubhub_frontend/zubhub/src/views/projects/Projects.jsx +++ b/zubhub_frontend/zubhub/src/views/projects/Projects.jsx @@ -20,7 +20,7 @@ import styles from '../../assets/js/styles/views/projects/projectsStyles'; const useStyles = makeStyles(styles); const fetchPage = (page, props) => { - return props.get_projects(page); + return props.get_projects({ page, t: props.t }); }; const updateProjects = (res, props) => { @@ -40,7 +40,11 @@ const updateProjects = (res, props) => { } }) .catch(error => { - toast.warning(error.message); + if (error.message.startsWith('Unexpected')) { + toast.warning(props.t('projects.errors.unexpected')); + } else { + toast.warning(error.message); + } return { loading: false }; }); }; @@ -70,6 +74,7 @@ function Projects(props) { previous: prevPage, next: nextPage, } = props.projects; + const { t } = props; if (loading) { return ; @@ -100,7 +105,7 @@ function Projects(props) { ))} {prevPage ? ( @@ -114,7 +119,7 @@ function Projects(props) { }} primaryButtonStyle > - Prev + {t('projects.prev')} ) : null} {nextPage ? ( @@ -128,7 +133,7 @@ function Projects(props) { }} primaryButtonStyle > - Next + {t('projects.next')} ) : null} @@ -136,9 +141,7 @@ function Projects(props) {
); } else { - return ( - - ); + return ; } } @@ -162,14 +165,14 @@ const mapDispatchToProps = dispatch => { get_projects: page => { return dispatch(ProjectActions.get_projects(page)); }, - set_projects: projects => { - return dispatch(ProjectActions.set_projects(projects)); + set_projects: args => { + return dispatch(ProjectActions.set_projects(args)); }, - toggle_like: props => { - return dispatch(ProjectActions.toggle_like(props)); + toggle_like: args => { + return dispatch(ProjectActions.toggle_like(args)); }, - toggle_save: props => { - return dispatch(ProjectActions.toggle_save(props)); + toggle_save: args => { + return dispatch(ProjectActions.toggle_save(args)); }, }; }; diff --git a/zubhub_frontend/zubhub/src/views/saved_projects/SavedProjects.jsx b/zubhub_frontend/zubhub/src/views/saved_projects/SavedProjects.jsx index 746a92c55..b549f6993 100644 --- a/zubhub_frontend/zubhub/src/views/saved_projects/SavedProjects.jsx +++ b/zubhub_frontend/zubhub/src/views/saved_projects/SavedProjects.jsx @@ -29,11 +29,11 @@ const fetchPage = (page, props) => { if (!props.auth.token) { props.history.push('/login'); } else { - return props.get_saved({ page, token: props.auth.token }); + return props.get_saved({ page, token: props.auth.token, t: props.t }); } }; -const updateProjects = (res, { results: projects }) => { +const updateProjects = (res, { results: projects }, props) => { return res .then(res => { if (res.project && res.project.title) { @@ -49,7 +49,11 @@ const updateProjects = (res, { results: projects }) => { } }) .catch(error => { - toast.warning(error.message); + if (error.message.startsWith('Unexpected')) { + toast.warning(props.t('savedProjects.errors.unexpected')); + } else { + toast.warning(error.message); + } return { loading: false }; }); }; @@ -77,6 +81,7 @@ function SavedProjects(props) { }; const { results: projects, prevPage, nextPage, loading } = state; + const { t } = props; if (loading) { return ; } else if (projects && projects.length > 0) { @@ -90,7 +95,7 @@ function SavedProjects(props) { variant="h3" gutterBottom > - Your saved projects + {t('savedProjects.title')} {projects.map(project => ( @@ -107,7 +112,7 @@ function SavedProjects(props) { project={project} key={project.id} updateProjects={res => - handleSetState(updateProjects(res, state)) + handleSetState(updateProjects(res, state, props)) } {...props} /> @@ -115,7 +120,7 @@ function SavedProjects(props) { ))} {prevPage ? ( @@ -129,7 +134,7 @@ function SavedProjects(props) { }} primaryButtonStyle > - Prev + {t('savedProjects.prev')} ) : null} {nextPage ? ( @@ -143,7 +148,7 @@ function SavedProjects(props) { }} primaryButtonStyle > - Next + {t('savedProjects.next')} ) : null} @@ -151,7 +156,7 @@ function SavedProjects(props) {
); } else { - return ; + return ; } } @@ -170,8 +175,8 @@ const mapStateToProps = state => { const mapDispatchToProps = dispatch => { return { - get_saved: value => { - return dispatch(ProjectActions.get_saved(value)); + get_saved: props => { + return dispatch(ProjectActions.get_saved(props)); }, toggle_like: props => { return dispatch(ProjectActions.toggle_like(props)); diff --git a/zubhub_frontend/zubhub/src/views/signup/Signup.jsx b/zubhub_frontend/zubhub/src/views/signup/Signup.jsx index 1547ed244..191bd81cb 100644 --- a/zubhub_frontend/zubhub/src/views/signup/Signup.jsx +++ b/zubhub_frontend/zubhub/src/views/signup/Signup.jsx @@ -43,37 +43,37 @@ const handleMouseDownPassword = e => { }; const get_locations = props => { - return props.get_locations(); + return props.get_locations({ t: props.t }); }; const signup = (e, props) => { e.preventDefault(); - if (props.values.location.length < 1) { - props.validateField('location'); + if (props.values.user_location.length < 1) { + props.validateField('user_location'); } else { - return props.signup(props).catch(error => { - const messages = JSON.parse(error.message); - if (typeof messages === 'object') { - let non_field_errors; - Object.keys(messages).forEach(key => { - if (key === 'non_field_errors') { - non_field_errors = { error: messages[key][0] }; - } else if (key === 'location') { - props.setFieldTouched('user_location', true, false); - props.setFieldError('user_location', messages[key][0]); - } else { - props.setFieldTouched(key, true, false); - props.setFieldError(key, messages[key][0]); - } - }); - return non_field_errors; - } else { - return { - error: - 'An error occured while performing this action. Please try again later', - }; - } - }); + return props + .signup({ values: props.values, history: props.history }) + .catch(error => { + const messages = JSON.parse(error.message); + if (typeof messages === 'object') { + const server_errors = {}; + Object.keys(messages).forEach(key => { + if (key === 'non_field_errors') { + server_errors['non_field_errors'] = messages[key][0]; + } else if (key === 'location') { + server_errors['user_location'] = messages[key][0]; + } else { + server_errors[key] = messages[key][0]; + } + }); + props.setStatus({ ...props.status, ...server_errors }); + } else { + props.setStatus({ + ...props.status, + non_field_errors: props.t('signup.errors.unexpected'), + }); + } + }); } }; @@ -83,7 +83,6 @@ const handleTooltipToggle = ({ toolTipOpen }) => { function Signup(props) { const [state, setState] = React.useState({ - error: null, locations: [], showPassword1: false, showPassword2: false, @@ -104,7 +103,8 @@ function Signup(props) { } }; - const { error, locations, toolTipOpen, showPassword1, showPassword2 } = state; + const { locations, toolTipOpen, showPassword1, showPassword2 } = state; + const { t } = props; return ( @@ -116,7 +116,7 @@ function Signup(props) { className="auth-form" name="signup" noValidate="noValidate" - onSubmit={e => handleSetState(signup(e, props))} + onSubmit={e => signup(e, props)} > - Welcome to Zubhub + {t('signup.welcomeMsg.primary')} - Create an account to submit a project + {t('signup.welcomeMsg.secondary')} - - {error && ( + + {props.status && props.status['non_field_errors'] && ( - {error} + {props.status['non_field_errors']} )} @@ -148,14 +155,15 @@ function Signup(props) { fullWidth margin="normal" error={ - props.touched['username'] && props.errors['username'] + (props.status && props.status['username']) || + (props.touched['username'] && props.errors['username']) } > - Username + {t('signup.inputs.username.label')} @@ -163,7 +171,7 @@ function Signup(props) { } > @@ -192,7 +200,12 @@ function Signup(props) { - {props.touched['username'] && props.errors['username']} + {(props.status && props.status['username']) || + (props.touched['username'] && + props.errors['username'] && + t( + `signup.inputs.username.errors.${this.props.errors['username']}`, + ))} @@ -207,13 +220,16 @@ function Signup(props) { shrink: true, }} margin="normal" - error={props.touched['email'] && props.errors['email']} + error={ + (props.status && props.status['email']) || + (props.touched['email'] && props.errors['email']) + } > - Email + {t('signup.inputs.email.label')} - {props.touched['email'] && props.errors['email']} + {(props.status && props.status['email']) || + (props.touched['email'] && + props.errors['email'] && + t( + `signup.inputs.email.errors.${props.errors['email']}`, + ))} @@ -238,8 +259,9 @@ function Signup(props) { fullWidth margin="normal" error={ - props.touched['dateOfBirth'] && - props.errors['dateOfBirth'] + (props.status && props.status['dateOfBirth']) || + (props.touched['dateOfBirth'] && + props.errors['dateOfBirth']) } > - Date Of Birth + {t('signup.inputs.dateOfBirth.label')} - {props.touched['dateOfBirth'] && - props.errors['dateOfBirth']} + {(props.status && props.status['dateOfBirth']) || + (props.touched['dateOfBirth'] && + props.errors['dateOfBirth'] && + t( + `signup.inputs.dateOfBirth.errors.${props.errors['dateOfBirth']}`, + ))} @@ -277,15 +303,16 @@ function Signup(props) { fullWidth margin="normal" error={ - props.touched['user_location'] && - props.errors['user_location'] + (props.status && props.status['user_location']) || + (props.touched['user_location'] && + props.errors['user_location']) } > - Location + {t('signup.inputs.location.label')} - + - + + 4 + {t('createProject.inputs.video.label')} + + + - {t('createProject.inputs.video.label')} - + {t('createProject.inputs.video.topHelperText')} + - - {t('createProject.inputs.video.helperText')} - -
{(props.status && props.status['video']) || (props.touched['video'] && props.errors['video'] && @@ -633,10 +741,17 @@ function CreateProject(props) { `createProject.inputs.video.errors.${props.errors['video']}`, ))}
+ + {t('createProject.inputs.video.bottomHelperText')} +
- + - - {t('createProject.inputs.materialsUsed.label')} - - - {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.status && - props.status['materials_used']) || - (props.touched['materials_used'] && - props.errors['materials_used'] && - t( - `createProject.inputs.materialsUsed.errors.${props.errors['materials_used']}`, - ))} - + + + + + + {BuildMaterialUsedNodes(props, refs)} + - + - handleSetState(addMaterialUsed(e, props, refs)) - } + onClick={e => addMaterialsUsedNode(e, props)} secondaryButtonStyle fullWidth > - + {' '} + {t('createProject.inputs.materialsUsed.addMore')} + + {(props.status && props.status['materials_used']) || + (props.touched['materials_used'] && + props.errors['materials_used'] && + t( + `createProject.inputs.materialsUsed.errors.${props.errors['materials_used']}`, + ))} + - @@ -890,7 +957,20 @@ export default connect( ? false : true; }), - materials_used: Yup.string().max(10000, 'max').required('required'), + materials_used: Yup.string() + .max(10000, 'max') + .test('empty', 'required', value => { + let is_empty = true; + + value && + value.split(',').forEach(material => { + if (material) { + is_empty = false; + } + }); + + return !is_empty; + }), }), })(CreateProject), );