├── .gitignore ├── Pipfile ├── Pipfile.lock ├── Procfile ├── README.md ├── auth.py ├── client ├── .gitignore ├── README.md ├── build │ ├── asset-manifest.json │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── robots.txt │ └── static │ │ ├── css │ │ ├── 2.4be38407.chunk.css │ │ ├── 2.4be38407.chunk.css.map │ │ ├── main.5bbcf992.chunk.css │ │ └── main.5bbcf992.chunk.css.map │ │ └── js │ │ ├── 2.75cc7457.chunk.js │ │ ├── 2.75cc7457.chunk.js.LICENSE.txt │ │ ├── 2.75cc7457.chunk.js.map │ │ ├── main.504c6dd8.chunk.js │ │ ├── main.504c6dd8.chunk.js.map │ │ ├── runtime-main.7545a8a1.js │ │ └── runtime-main.7545a8a1.js.map ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── auth.js │ ├── components │ ├── CreateRecipe.js │ ├── Home.js │ ├── Login.js │ ├── Navbar.js │ ├── Recipe.js │ └── SignUp.js │ ├── index.js │ └── styles │ └── main.css ├── config.py ├── dev.db ├── exts.py ├── main.py ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ └── e3226bc25f01_add_user_table.py ├── models.py ├── prod.db ├── recipes.py ├── requirements.txt ├── run.py ├── test.db └── test_api.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | flask = "*" 8 | flask-restx = "*" 9 | flask-sqlalchemy = "*" 10 | flask-jwt-extended = "*" 11 | python-decouple = "*" 12 | flask-migrate = "*" 13 | colorama = "*" 14 | flask-cors = "*" 15 | gunicorn = "*" 16 | 17 | [dev-packages] 18 | 19 | [requires] 20 | python_version = "3.9" 21 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "cf4d90f24ebddf7cd91ee984d650d3edcad7b0873cc4f9498bfbd675d2cd47dc" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "alembic": { 20 | "hashes": [ 21 | "sha256:a21fedebb3fb8f6bbbba51a11114f08c78709377051384c9c5ead5705ee93a51", 22 | "sha256:e78be5b919f5bb184e3e0e2dd1ca986f2362e29a2bc933c446fe89f39dbe4e9c" 23 | ], 24 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 25 | "version": "==1.6.5" 26 | }, 27 | "aniso8601": { 28 | "hashes": [ 29 | "sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f", 30 | "sha256:72e3117667eedf66951bb2d93f4296a56b94b078a8a95905a052611fb3f1b973" 31 | ], 32 | "markers": "python_version >= '3.5'", 33 | "version": "==9.0.1" 34 | }, 35 | "attrs": { 36 | "hashes": [ 37 | "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", 38 | "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" 39 | ], 40 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 41 | "version": "==21.2.0" 42 | }, 43 | "click": { 44 | "hashes": [ 45 | "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", 46 | "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" 47 | ], 48 | "markers": "python_version >= '3.6'", 49 | "version": "==8.0.1" 50 | }, 51 | "colorama": { 52 | "hashes": [ 53 | "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", 54 | "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" 55 | ], 56 | "index": "pypi", 57 | "version": "==0.4.4" 58 | }, 59 | "flask": { 60 | "hashes": [ 61 | "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55", 62 | "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9" 63 | ], 64 | "index": "pypi", 65 | "version": "==2.0.1" 66 | }, 67 | "flask-cors": { 68 | "hashes": [ 69 | "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438", 70 | "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de" 71 | ], 72 | "index": "pypi", 73 | "version": "==3.0.10" 74 | }, 75 | "flask-jwt-extended": { 76 | "hashes": [ 77 | "sha256:6e2b40d548b9dfc6051740c4552c097ac38e514e500c16c682d9a533d17ca418", 78 | "sha256:80d06d3893089824659c26d0cb261999a12f425a66f09c3685f993065bc47b3a" 79 | ], 80 | "index": "pypi", 81 | "version": "==4.3.0" 82 | }, 83 | "flask-migrate": { 84 | "hashes": [ 85 | "sha256:57d6060839e3a7f150eaab6fe4e726d9e3e7cffe2150fb223d73f92421c6d1d9", 86 | "sha256:a6498706241aba6be7a251078de9cf166d74307bca41a4ca3e403c9d39e2f897" 87 | ], 88 | "index": "pypi", 89 | "version": "==3.1.0" 90 | }, 91 | "flask-restx": { 92 | "hashes": [ 93 | "sha256:7e9f7cd5e843dd653a71fafb7c8ce9d7b4fef29f982a2254b1e0ebb3fac1fe12", 94 | "sha256:c3c2b724e688c0a50ee5e78f2a508b7f0c34644f00f64170fa8a3d0cdc34f67a" 95 | ], 96 | "index": "pypi", 97 | "version": "==0.5.0" 98 | }, 99 | "flask-sqlalchemy": { 100 | "hashes": [ 101 | "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912", 102 | "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390" 103 | ], 104 | "index": "pypi", 105 | "version": "==2.5.1" 106 | }, 107 | "greenlet": { 108 | "hashes": [ 109 | "sha256:04e1849c88aa56584d4a0a6e36af5ec7cc37993fdc1fda72b56aa1394a92ded3", 110 | "sha256:05e72db813c28906cdc59bd0da7c325d9b82aa0b0543014059c34c8c4ad20e16", 111 | "sha256:07e6d88242e09b399682b39f8dfa1e7e6eca66b305de1ff74ed9eb1a7d8e539c", 112 | "sha256:090126004c8ab9cd0787e2acf63d79e80ab41a18f57d6448225bbfcba475034f", 113 | "sha256:1796f2c283faab2b71c67e9b9aefb3f201fdfbee5cb55001f5ffce9125f63a45", 114 | "sha256:2f89d74b4f423e756a018832cd7a0a571e0a31b9ca59323b77ce5f15a437629b", 115 | "sha256:34e6675167a238bede724ee60fe0550709e95adaff6a36bcc97006c365290384", 116 | "sha256:3e594015a2349ec6dcceda9aca29da8dc89e85b56825b7d1f138a3f6bb79dd4c", 117 | "sha256:3f8fc59bc5d64fa41f58b0029794f474223693fd00016b29f4e176b3ee2cfd9f", 118 | "sha256:3fc6a447735749d651d8919da49aab03c434a300e9f0af1c886d560405840fd1", 119 | "sha256:40abb7fec4f6294225d2b5464bb6d9552050ded14a7516588d6f010e7e366dcc", 120 | "sha256:44556302c0ab376e37939fd0058e1f0db2e769580d340fb03b01678d1ff25f68", 121 | "sha256:476ba9435afaead4382fbab8f1882f75e3fb2285c35c9285abb3dd30237f9142", 122 | "sha256:4870b018ca685ff573edd56b93f00a122f279640732bb52ce3a62b73ee5c4a92", 123 | "sha256:4adaf53ace289ced90797d92d767d37e7cdc29f13bd3830c3f0a561277a4ae83", 124 | "sha256:4eae94de9924bbb4d24960185363e614b1b62ff797c23dc3c8a7c75bbb8d187e", 125 | "sha256:5317701c7ce167205c0569c10abc4bd01c7f4cf93f642c39f2ce975fa9b78a3c", 126 | "sha256:5c3b735ccf8fc8048664ee415f8af5a3a018cc92010a0d7195395059b4b39b7d", 127 | "sha256:5cde7ee190196cbdc078511f4df0be367af85636b84d8be32230f4871b960687", 128 | "sha256:655ab836324a473d4cd8cf231a2d6f283ed71ed77037679da554e38e606a7117", 129 | "sha256:6ce9d0784c3c79f3e5c5c9c9517bbb6c7e8aa12372a5ea95197b8a99402aa0e6", 130 | "sha256:6e0696525500bc8aa12eae654095d2260db4dc95d5c35af2b486eae1bf914ccd", 131 | "sha256:75ff270fd05125dce3303e9216ccddc541a9e072d4fc764a9276d44dee87242b", 132 | "sha256:8039f5fe8030c43cd1732d9a234fdcbf4916fcc32e21745ca62e75023e4d4649", 133 | "sha256:84488516639c3c5e5c0e52f311fff94ebc45b56788c2a3bfe9cf8e75670f4de3", 134 | "sha256:84782c80a433d87530ae3f4b9ed58d4a57317d9918dfcc6a59115fa2d8731f2c", 135 | "sha256:8ddb38fb6ad96c2ef7468ff73ba5c6876b63b664eebb2c919c224261ae5e8378", 136 | "sha256:98b491976ed656be9445b79bc57ed21decf08a01aaaf5fdabf07c98c108111f6", 137 | "sha256:990e0f5e64bcbc6bdbd03774ecb72496224d13b664aa03afd1f9b171a3269272", 138 | "sha256:9b02e6039eafd75e029d8c58b7b1f3e450ca563ef1fe21c7e3e40b9936c8d03e", 139 | "sha256:a11b6199a0b9dc868990456a2667167d0ba096c5224f6258e452bfbe5a9742c5", 140 | "sha256:a414f8e14aa7bacfe1578f17c11d977e637d25383b6210587c29210af995ef04", 141 | "sha256:a91ee268f059583176c2c8b012a9fce7e49ca6b333a12bbc2dd01fc1a9783885", 142 | "sha256:ac991947ca6533ada4ce7095f0e28fe25d5b2f3266ad5b983ed4201e61596acf", 143 | "sha256:b050dbb96216db273b56f0e5960959c2b4cb679fe1e58a0c3906fa0a60c00662", 144 | "sha256:b97a807437b81f90f85022a9dcfd527deea38368a3979ccb49d93c9198b2c722", 145 | "sha256:bad269e442f1b7ffa3fa8820b3c3aa66f02a9f9455b5ba2db5a6f9eea96f56de", 146 | "sha256:bf3725d79b1ceb19e83fb1aed44095518c0fcff88fba06a76c0891cfd1f36837", 147 | "sha256:c0f22774cd8294078bdf7392ac73cf00bfa1e5e0ed644bd064fdabc5f2a2f481", 148 | "sha256:c1862f9f1031b1dee3ff00f1027fcd098ffc82120f43041fe67804b464bbd8a7", 149 | "sha256:c8d4ed48eed7414ccb2aaaecbc733ed2a84c299714eae3f0f48db085342d5629", 150 | "sha256:cf31e894dabb077a35bbe6963285d4515a387ff657bd25b0530c7168e48f167f", 151 | "sha256:d15cb6f8706678dc47fb4e4f8b339937b04eda48a0af1cca95f180db552e7663", 152 | "sha256:dfcb5a4056e161307d103bc013478892cfd919f1262c2bb8703220adcb986362", 153 | "sha256:e02780da03f84a671bb4205c5968c120f18df081236d7b5462b380fd4f0b497b", 154 | "sha256:e2002a59453858c7f3404690ae80f10c924a39f45f6095f18a985a1234c37334", 155 | "sha256:e22a82d2b416d9227a500c6860cf13e74060cf10e7daf6695cbf4e6a94e0eee4", 156 | "sha256:e41f72f225192d5d4df81dad2974a8943b0f2d664a2a5cfccdf5a01506f5523c", 157 | "sha256:f253dad38605486a4590f9368ecbace95865fea0f2b66615d121ac91fd1a1563", 158 | "sha256:fddfb31aa2ac550b938d952bca8a87f1db0f8dc930ffa14ce05b5c08d27e7fd1" 159 | ], 160 | "markers": "python_version >= '3' and platform_machine in 'x86_64 X86_64 aarch64 AARCH64 ppc64le PPC64LE amd64 AMD64 win32 WIN32'", 161 | "version": "==1.1.1" 162 | }, 163 | "gunicorn": { 164 | "hashes": [ 165 | "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", 166 | "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" 167 | ], 168 | "index": "pypi", 169 | "version": "==20.1.0" 170 | }, 171 | "itsdangerous": { 172 | "hashes": [ 173 | "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", 174 | "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" 175 | ], 176 | "markers": "python_version >= '3.6'", 177 | "version": "==2.0.1" 178 | }, 179 | "jinja2": { 180 | "hashes": [ 181 | "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", 182 | "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" 183 | ], 184 | "markers": "python_version >= '3.6'", 185 | "version": "==3.0.1" 186 | }, 187 | "jsonschema": { 188 | "hashes": [ 189 | "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", 190 | "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" 191 | ], 192 | "version": "==3.2.0" 193 | }, 194 | "mako": { 195 | "hashes": [ 196 | "sha256:169fa52af22a91900d852e937400e79f535496191c63712e3b9fda5a9bed6fc3", 197 | "sha256:6804ee66a7f6a6416910463b00d76a7b25194cd27f1918500c5bd7be2a088a23" 198 | ], 199 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 200 | "version": "==1.1.5" 201 | }, 202 | "markupsafe": { 203 | "hashes": [ 204 | "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", 205 | "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", 206 | "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", 207 | "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", 208 | "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", 209 | "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", 210 | "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", 211 | "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", 212 | "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", 213 | "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", 214 | "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", 215 | "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", 216 | "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", 217 | "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", 218 | "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", 219 | "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", 220 | "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", 221 | "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", 222 | "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", 223 | "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", 224 | "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", 225 | "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", 226 | "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", 227 | "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", 228 | "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", 229 | "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", 230 | "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", 231 | "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", 232 | "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", 233 | "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", 234 | "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", 235 | "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", 236 | "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", 237 | "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", 238 | "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", 239 | "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", 240 | "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", 241 | "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", 242 | "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", 243 | "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", 244 | "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", 245 | "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", 246 | "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", 247 | "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", 248 | "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", 249 | "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", 250 | "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", 251 | "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", 252 | "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", 253 | "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", 254 | "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", 255 | "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", 256 | "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", 257 | "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" 258 | ], 259 | "markers": "python_version >= '3.6'", 260 | "version": "==2.0.1" 261 | }, 262 | "pyjwt": { 263 | "hashes": [ 264 | "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1", 265 | "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130" 266 | ], 267 | "markers": "python_version >= '3.6'", 268 | "version": "==2.1.0" 269 | }, 270 | "pyrsistent": { 271 | "hashes": [ 272 | "sha256:097b96f129dd36a8c9e33594e7ebb151b1515eb52cceb08474c10a5479e799f2", 273 | "sha256:2aaf19dc8ce517a8653746d98e962ef480ff34b6bc563fc067be6401ffb457c7", 274 | "sha256:404e1f1d254d314d55adb8d87f4f465c8693d6f902f67eb6ef5b4526dc58e6ea", 275 | "sha256:48578680353f41dca1ca3dc48629fb77dfc745128b56fc01096b2530c13fd426", 276 | "sha256:4916c10896721e472ee12c95cdc2891ce5890898d2f9907b1b4ae0f53588b710", 277 | "sha256:527be2bfa8dc80f6f8ddd65242ba476a6c4fb4e3aedbf281dfbac1b1ed4165b1", 278 | "sha256:58a70d93fb79dc585b21f9d72487b929a6fe58da0754fa4cb9f279bb92369396", 279 | "sha256:5e4395bbf841693eaebaa5bb5c8f5cdbb1d139e07c975c682ec4e4f8126e03d2", 280 | "sha256:6b5eed00e597b5b5773b4ca30bd48a5774ef1e96f2a45d105db5b4ebb4bca680", 281 | "sha256:73ff61b1411e3fb0ba144b8f08d6749749775fe89688093e1efef9839d2dcc35", 282 | "sha256:772e94c2c6864f2cd2ffbe58bb3bdefbe2a32afa0acb1a77e472aac831f83427", 283 | "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b", 284 | "sha256:a0c772d791c38bbc77be659af29bb14c38ced151433592e326361610250c605b", 285 | "sha256:b29b869cf58412ca5738d23691e96d8aff535e17390128a1a52717c9a109da4f", 286 | "sha256:c1a9ff320fa699337e05edcaae79ef8c2880b52720bc031b219e5b5008ebbdef", 287 | "sha256:cd3caef37a415fd0dae6148a1b6957a8c5f275a62cca02e18474608cb263640c", 288 | "sha256:d5ec194c9c573aafaceebf05fc400656722793dac57f254cd4741f3c27ae57b4", 289 | "sha256:da6e5e818d18459fa46fac0a4a4e543507fe1110e808101277c5a2b5bab0cd2d", 290 | "sha256:e79d94ca58fcafef6395f6352383fa1a76922268fa02caa2272fff501c2fdc78", 291 | "sha256:f3ef98d7b76da5eb19c37fda834d50262ff9167c65658d1d8f974d2e4d90676b", 292 | "sha256:f4c8cabb46ff8e5d61f56a037974228e978f26bfefce4f61a4b1ac0ba7a2ab72" 293 | ], 294 | "markers": "python_version >= '3.6'", 295 | "version": "==0.18.0" 296 | }, 297 | "python-dateutil": { 298 | "hashes": [ 299 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 300 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 301 | ], 302 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 303 | "version": "==2.8.2" 304 | }, 305 | "python-decouple": { 306 | "hashes": [ 307 | "sha256:2e5adb0263a4f963b58d7407c4760a2465d464ee212d733e2a2c179e54c08d8f", 308 | "sha256:a8268466e6389a639a20deab9d880faee186eb1eb6a05e54375bdf158d691981" 309 | ], 310 | "index": "pypi", 311 | "version": "==3.4" 312 | }, 313 | "python-editor": { 314 | "hashes": [ 315 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 316 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 317 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", 318 | "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", 319 | "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522" 320 | ], 321 | "version": "==1.0.4" 322 | }, 323 | "pytz": { 324 | "hashes": [ 325 | "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", 326 | "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" 327 | ], 328 | "version": "==2021.1" 329 | }, 330 | "six": { 331 | "hashes": [ 332 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 333 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 334 | ], 335 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 336 | "version": "==1.16.0" 337 | }, 338 | "sqlalchemy": { 339 | "hashes": [ 340 | "sha256:059c5f41e8630f51741a234e6ba2a034228c11b3b54a15478e61d8b55fa8bd9d", 341 | "sha256:07b9099a95dd2b2620498544300eda590741ac54915c6b20809b6de7e3c58090", 342 | "sha256:0aa312f9906ecebe133d7f44168c3cae4c76f27a25192fa7682f3fad505543c9", 343 | "sha256:0aa746d1173587743960ff17b89b540e313aacfe6c1e9c81aa48393182c36d4f", 344 | "sha256:1c15191f2430a30082f540ec6f331214746fc974cfdf136d7a1471d1c61d68ff", 345 | "sha256:25e9b2e5ca088879ce3740d9ccd4d58cb9061d49566a0b5e12166f403d6f4da0", 346 | "sha256:2bca9a6e30ee425cc321d988a152a5fe1be519648e7541ac45c36cd4f569421f", 347 | "sha256:355024cf061ed04271900414eb4a22671520241d2216ddb691bdd8a992172389", 348 | "sha256:370f4688ce47f0dc1e677a020a4d46252a31a2818fd67f5c256417faefc938af", 349 | "sha256:37f2bd1b8e32c5999280f846701712347fc0ee7370e016ede2283c71712e127a", 350 | "sha256:3a0d3b3d51c83a66f5b72c57e1aad061406e4c390bd42cf1fda94effe82fac81", 351 | "sha256:43fc207be06e50158e4dae4cc4f27ce80afbdbfa7c490b3b22feb64f6d9775a0", 352 | "sha256:448612570aa1437a5d1b94ada161805778fe80aba5b9a08a403e8ae4e071ded6", 353 | "sha256:4803a481d4c14ce6ad53dc35458c57821863e9a079695c27603d38355e61fb7f", 354 | "sha256:512f52a8872e8d63d898e4e158eda17e2ee40b8d2496b3b409422e71016db0bd", 355 | "sha256:6a8dbf3d46e889d864a57ee880c4ad3a928db5aa95e3d359cbe0da2f122e50c4", 356 | "sha256:76ff246881f528089bf19385131b966197bb494653990396d2ce138e2a447583", 357 | "sha256:82c03325111eab88d64e0ff48b6fe15c75d23787429fa1d84c0995872e702787", 358 | "sha256:967307ea52985985224a79342527c36ec2d1daa257a39748dd90e001a4be4d90", 359 | "sha256:9b128a78581faea7a5ee626ad4471353eee051e4e94616dfeff4742b6e5ba262", 360 | "sha256:a8395c4db3e1450eef2b68069abf500cc48af4b442a0d98b5d3c9535fe40cde8", 361 | "sha256:ae07895b55c7d58a7dd47438f437ac219c0f09d24c2e7d69fdebc1ea75350f00", 362 | "sha256:bd41f8063a9cd11b76d6d7d6af8139ab3c087f5dbbe5a50c02cb8ece7da34d67", 363 | "sha256:be185b3daf651c6c0639987a916bf41e97b60e68f860f27c9cb6574385f5cbb4", 364 | "sha256:cd0e85dd2067159848c7672acd517f0c38b7b98867a347411ea01b432003f8d9", 365 | "sha256:cd68c5f9d13ffc8f4d6802cceee786678c5b1c668c97bc07b9f4a60883f36cd1", 366 | "sha256:cec1a4c6ddf5f82191301a25504f0e675eccd86635f0d5e4c69e0661691931c5", 367 | "sha256:d9667260125688c71ccf9af321c37e9fb71c2693575af8210f763bfbbee847c7", 368 | "sha256:e0ce4a2e48fe0a9ea3a5160411a4c5135da5255ed9ac9c15f15f2bcf58c34194", 369 | "sha256:e9d4f4552aa5e0d1417fc64a2ce1cdf56a30bab346ba6b0dd5e838eb56db4d29" 370 | ], 371 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", 372 | "version": "==1.4.23" 373 | }, 374 | "werkzeug": { 375 | "hashes": [ 376 | "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", 377 | "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" 378 | ], 379 | "markers": "python_version >= '3.6'", 380 | "version": "==2.0.1" 381 | } 382 | }, 383 | "develop": {} 384 | } 385 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web gunicorn run:app -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask + ReactJS Recipes 2 | This is a series of videos in which I talk about full stack web application development using Python Flask and the ReactJS library. 3 | 4 | 5 | ## About the project 6 | The project is a simple recipe app that allows simple CRUD actions against a Flask REST API. 7 | 8 | 9 | 10 | ## What to cover 11 | 12 | ### Backend Development 13 | - Creating REST APIs with Flask with Flask-RestX 14 | - Using Flask-SQLAlchemy ORM 15 | - Carrying Out Database Migrations with Flask-Migrate 16 | - JWT Authentication with Flask-JWT-Extended 17 | - Testing Flask API with Unittest 18 | 19 | ### Frontend Development 20 | - Creating React Components 21 | - React Hooks 22 | - React Hooks 23 | - JWT Authentication on the frontend 24 | - Making API Calls with Fetch API 25 | 26 | 27 | ## Video Playlist 28 | [Build a Full stack web app with Flask and ReactJS](https://www.youtube.com/playlist?list=PLEt8Tae2spYkfEYQnKxQ4vrOULAnMI1iF) 29 | 30 | ## Live Demo On Render.com 31 | [View The project here](https://flask-react-recipes.onrender.com/signup) 32 | 33 | 34 | # To run this project 35 | 1. Clone the Git repository 36 | 2. In the root folder, create a virtual environment using your favorite method. 37 | 3. Install project dependencies with 38 | `` 39 | pip install -r requirements.txt 40 | `` 41 | 4. Run project with 42 | `` 43 | python run.py 44 | `` 45 | 5. Run test 46 | `` 47 | pytest 48 | `` 49 | 50 | -------------------------------------------------------------------------------- /auth.py: -------------------------------------------------------------------------------- 1 | from flask_restx import Resource, Namespace, fields 2 | from models import User 3 | from werkzeug.security import generate_password_hash, check_password_hash 4 | from flask_jwt_extended import ( 5 | JWTManager, 6 | create_access_token, 7 | create_refresh_token, 8 | get_jwt_identity, 9 | jwt_required, 10 | ) 11 | from flask import Flask, request, jsonify, make_response 12 | 13 | 14 | auth_ns = Namespace("auth", description="A namespace for our Authentication") 15 | 16 | 17 | signup_model = auth_ns.model( 18 | "SignUp", 19 | { 20 | "username": fields.String(), 21 | "email": fields.String(), 22 | "password": fields.String(), 23 | }, 24 | ) 25 | 26 | 27 | login_model = auth_ns.model( 28 | "Login", {"username": fields.String(), "password": fields.String()} 29 | ) 30 | 31 | 32 | @auth_ns.route("/signup") 33 | class SignUp(Resource): 34 | @auth_ns.expect(signup_model) 35 | def post(self): 36 | data = request.get_json() 37 | 38 | username = data.get("username") 39 | 40 | db_user = User.query.filter_by(username=username).first() 41 | 42 | if db_user is not None: 43 | return jsonify({"message": f"User with username {username} already exists"}) 44 | 45 | new_user = User( 46 | username=data.get("username"), 47 | email=data.get("email"), 48 | password=generate_password_hash(data.get("password")), 49 | ) 50 | 51 | new_user.save() 52 | 53 | return make_response(jsonify({"message": "User created successfuly"}), 201) 54 | 55 | 56 | @auth_ns.route("/login") 57 | class Login(Resource): 58 | @auth_ns.expect(login_model) 59 | def post(self): 60 | data = request.get_json() 61 | 62 | username = data.get("username") 63 | password = data.get("password") 64 | 65 | db_user = User.query.filter_by(username=username).first() 66 | 67 | if db_user and check_password_hash(db_user.password, password): 68 | 69 | access_token = create_access_token(identity=db_user.username) 70 | refresh_token = create_refresh_token(identity=db_user.username) 71 | 72 | return jsonify( 73 | {"access_token": access_token, "refresh_token": refresh_token} 74 | ) 75 | 76 | else: 77 | return jsonify({"message": "Invalid username or password"}) 78 | 79 | 80 | @auth_ns.route("/refresh") 81 | class RefreshResource(Resource): 82 | @jwt_required(refresh=True) 83 | def post(self): 84 | 85 | current_user = get_jwt_identity() 86 | 87 | new_access_token = create_access_token(identity=current_user) 88 | 89 | return make_response(jsonify({"access_token": new_access_token}), 200) 90 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /client/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.5bbcf992.chunk.css", 4 | "main.js": "/static/js/main.504c6dd8.chunk.js", 5 | "main.js.map": "/static/js/main.504c6dd8.chunk.js.map", 6 | "runtime-main.js": "/static/js/runtime-main.7545a8a1.js", 7 | "runtime-main.js.map": "/static/js/runtime-main.7545a8a1.js.map", 8 | "static/css/2.4be38407.chunk.css": "/static/css/2.4be38407.chunk.css", 9 | "static/js/2.75cc7457.chunk.js": "/static/js/2.75cc7457.chunk.js", 10 | "static/js/2.75cc7457.chunk.js.map": "/static/js/2.75cc7457.chunk.js.map", 11 | "index.html": "/index.html", 12 | "static/css/2.4be38407.chunk.css.map": "/static/css/2.4be38407.chunk.css.map", 13 | "static/css/main.5bbcf992.chunk.css.map": "/static/css/main.5bbcf992.chunk.css.map", 14 | "static/js/2.75cc7457.chunk.js.LICENSE.txt": "/static/js/2.75cc7457.chunk.js.LICENSE.txt" 15 | }, 16 | "entrypoints": [ 17 | "static/js/runtime-main.7545a8a1.js", 18 | "static/css/2.4be38407.chunk.css", 19 | "static/js/2.75cc7457.chunk.js", 20 | "static/css/main.5bbcf992.chunk.css", 21 | "static/js/main.504c6dd8.chunk.js" 22 | ] 23 | } -------------------------------------------------------------------------------- /client/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jod35/Flask-React-Recipes/574e600c507e6451e0e0147f601a89a599eab17d/client/build/favicon.ico -------------------------------------------------------------------------------- /client/build/index.html: -------------------------------------------------------------------------------- 1 | React App
-------------------------------------------------------------------------------- /client/build/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jod35/Flask-React-Recipes/574e600c507e6451e0e0147f601a89a599eab17d/client/build/logo192.png -------------------------------------------------------------------------------- /client/build/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jod35/Flask-React-Recipes/574e600c507e6451e0e0147f601a89a599eab17d/client/build/logo512.png -------------------------------------------------------------------------------- /client/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/build/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/build/static/css/main.5bbcf992.chunk.css: -------------------------------------------------------------------------------- 1 | .container{margin-top:50px}.heading{font-size:3em}.form{margin:auto;width:80%}.recipe{margin-top:20px} 2 | /*# sourceMappingURL=main.5bbcf992.chunk.css.map */ -------------------------------------------------------------------------------- /client/build/static/css/main.5bbcf992.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack://src/styles/main.css"],"names":[],"mappings":"AAAA,WACI,eACJ,CACA,SACI,aACJ,CACA,MACI,WAAW,CACX,SACJ,CACA,QACI,eACJ","file":"main.5bbcf992.chunk.css","sourcesContent":[".container{\r\n margin-top: 50px;\r\n}\r\n.heading{\r\n font-size: 3em;\r\n}\r\n.form{\r\n margin:auto;\r\n width: 80%;\r\n}\r\n.recipe{\r\n margin-top:20px;\r\n}"]} -------------------------------------------------------------------------------- /client/build/static/js/2.75cc7457.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2018 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /** @license React v0.20.2 14 | * scheduler.production.min.js 15 | * 16 | * Copyright (c) Facebook, Inc. and its affiliates. 17 | * 18 | * This source code is licensed under the MIT license found in the 19 | * LICENSE file in the root directory of this source tree. 20 | */ 21 | 22 | /** @license React v16.13.1 23 | * react-is.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** @license React v17.0.2 32 | * react-dom.production.min.js 33 | * 34 | * Copyright (c) Facebook, Inc. and its affiliates. 35 | * 36 | * This source code is licensed under the MIT license found in the 37 | * LICENSE file in the root directory of this source tree. 38 | */ 39 | 40 | /** @license React v17.0.2 41 | * react-jsx-runtime.production.min.js 42 | * 43 | * Copyright (c) Facebook, Inc. and its affiliates. 44 | * 45 | * This source code is licensed under the MIT license found in the 46 | * LICENSE file in the root directory of this source tree. 47 | */ 48 | 49 | /** @license React v17.0.2 50 | * react.production.min.js 51 | * 52 | * Copyright (c) Facebook, Inc. and its affiliates. 53 | * 54 | * This source code is licensed under the MIT license found in the 55 | * LICENSE file in the root directory of this source tree. 56 | */ 57 | -------------------------------------------------------------------------------- /client/build/static/js/main.504c6dd8.chunk.js: -------------------------------------------------------------------------------- 1 | (this.webpackJsonpclient=this.webpackJsonpclient||[]).push([[0],{50:function(e,t,c){},63:function(e,t,c){"use strict";c.r(t);c(49),c(50);var r=c(0),n=c(21),s=c.n(n),a=c(9),i=c(14),l=c(46),o=Object(l.createAuthProvider)({accessTokenKey:"access_token",onUpdateToken:function(e){return fetch("/auth/refresh",{method:"POST",body:e.refresh_token}).then((function(e){return e.json()}))}}),j=Object(a.a)(o,4),d=j[0],b=(j[1],j[2]),h=j[3],O=c(1),x=function(){return Object(O.jsxs)(O.Fragment,{children:[Object(O.jsx)("li",{className:"nav-item",children:Object(O.jsx)(i.b,{className:"nav-link active",to:"/",children:"Home"})}),Object(O.jsx)("li",{className:"nav-item",children:Object(O.jsx)(i.b,{className:"nav-link active",to:"/create_recipe",children:"Create Recipes"})}),Object(O.jsx)("li",{className:"nav-item",children:Object(O.jsx)("a",{className:"nav-link active",href:"#",onClick:function(){h()},children:"Log Out"})})]})},u=function(){return Object(O.jsxs)(O.Fragment,{children:[Object(O.jsx)("li",{className:"nav-item",children:Object(O.jsx)(i.b,{className:"nav-link active",to:"/",children:"Home"})}),Object(O.jsx)("li",{className:"nav-item",children:Object(O.jsx)(i.b,{className:"nav-link active",to:"/signup",children:"Sign Up"})}),Object(O.jsx)("li",{className:"nav-item",children:Object(O.jsx)(i.b,{className:"nav-link active",to:"/login",children:"Login"})})]})},p=function(){var e=d(),t=Object(a.a)(e,1)[0];return Object(O.jsx)("nav",{className:"navbar navbar-expand-lg navbar-dark bg-dark",children:Object(O.jsxs)("div",{className:"container-fluid",children:[Object(O.jsx)(i.b,{className:"navbar-brand",to:"/",children:"Recipes"}),Object(O.jsx)("button",{className:"navbar-toggler",type:"button","data-bs-toggle":"collapse","data-bs-target":"#navbarNav","aria-controls":"navbarNav","aria-expanded":"false","aria-label":"Toggle navigation",children:Object(O.jsx)("span",{className:"navbar-toggler-icon"})}),Object(O.jsx)("div",{className:"collapse navbar-collapse",id:"navbarNav",children:Object(O.jsx)("ul",{className:"navbar-nav",children:t?Object(O.jsx)(x,{}):Object(O.jsx)(u,{})})})]})})},m=c(10),v=c(2),g=c(69),f=c(65),y=function(e){var t=e.title,c=e.description,r=e.onClick,n=e.onDelete;return Object(O.jsx)(g.a,{className:"recipe",children:Object(O.jsxs)(g.a.Body,{children:[Object(O.jsx)(g.a.Title,{children:t}),Object(O.jsx)("p",{children:c}),Object(O.jsx)(f.a,{variant:"primary",onClick:r,children:"Update"})," ",Object(O.jsx)(f.a,{variant:"danger",onClick:n,children:"Delete"})]})})},N=c(66),L=c(67),w=c(23),S=function(){var e,t,c=Object(r.useState)([]),n=Object(a.a)(c,2),s=n[0],i=n[1],l=Object(r.useState)(!1),o=Object(a.a)(l,2),j=o[0],d=o[1],b=Object(w.a)(),h=b.register,x=(b.reset,b.handleSubmit),u=b.setValue,p=b.formState.errors,m=Object(r.useState)(0),g=Object(a.a)(m,2),S=g[0],C=g[1];Object(r.useEffect)((function(){fetch("/recipe/recipes").then((function(e){return e.json()})).then((function(e){i(e)})).catch((function(e){return console.log(e)}))}),[]);var k=localStorage.getItem("REACT_TOKEN_AUTH_KEY"),T=function(e){console.log(e);var t={method:"DELETE",headers:{"content-type":"application/json",Authorization:"Bearer ".concat(JSON.parse(k))}};fetch("/recipe/recipe/".concat(e),t).then((function(e){return e.json()})).then((function(e){console.log(e),fetch("/recipe/recipes").then((function(e){return e.json()})).then((function(e){i(e)})).catch((function(e){return console.log(e)}))})).catch((function(e){return console.log(e)}))};return Object(O.jsxs)("div",{className:"recipes container",children:[Object(O.jsxs)(N.a,{show:j,size:"lg",onHide:function(){d(!1)},children:[Object(O.jsx)(N.a.Header,{closeButton:!0,children:Object(O.jsx)(N.a.Title,{children:"Update Recipe"})}),Object(O.jsx)(N.a.Body,{children:Object(O.jsxs)("form",{children:[Object(O.jsxs)(L.a.Group,{children:[Object(O.jsx)(L.a.Label,{children:"Title"}),Object(O.jsx)(L.a.Control,Object(v.a)({type:"text"},h("title",{required:!0,maxLength:25})))]}),p.title&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Title is required"})}),"maxLength"===(null===(e=p.title)||void 0===e?void 0:e.type)&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Title should be less than 25 characters"})}),Object(O.jsxs)(L.a.Group,{children:[Object(O.jsx)(L.a.Label,{children:"Description"}),Object(O.jsx)(L.a.Control,Object(v.a)({as:"textarea",rows:5},h("description",{required:!0,maxLength:255})))]}),p.description&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Description is required"})}),"maxLength"===(null===(t=p.description)||void 0===t?void 0:t.type)&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Description should be less than 255 characters"})}),Object(O.jsx)("br",{}),Object(O.jsx)(L.a.Group,{children:Object(O.jsx)(f.a,{variant:"primary",onClick:x((function(e){console.log(e);var t={method:"PUT",headers:{"content-type":"application/json",Authorization:"Bearer ".concat(JSON.parse(k))},body:JSON.stringify(e)};fetch("/recipe/recipe/".concat(S),t).then((function(e){return e.json()})).then((function(e){console.log(e),window.location.reload()()})).catch((function(e){return console.log(e)}))})),children:"Save"})})]})})]}),Object(O.jsx)("h1",{children:"List of Recipes"}),s.map((function(e,t){return Object(O.jsx)(y,{title:e.title,description:e.description,onClick:function(){var t;t=e.id,d(!0),C(t),s.map((function(e){e.id==t&&(u("title",e.title),u("description",e.description))}))},onDelete:function(){T(e.id)}},t)}))]})},C=function(){return Object(O.jsxs)("div",{className:"home container",children:[Object(O.jsx)("h1",{className:"heading",children:"Welcome to the Recipes"}),Object(O.jsx)(i.b,{to:"/signup",className:"btn btn-primary btn-lg",children:"Get Started"})]})},k=function(){var e=d(),t=Object(a.a)(e,1)[0];return Object(O.jsx)("div",{children:t?Object(O.jsx)(S,{}):Object(O.jsx)(C,{})})},T=c(68),P=function(){var e,t,c,n,s=Object(w.a)(),l=s.register,o=s.handleSubmit,j=s.reset,d=s.formState.errors,b=Object(r.useState)(!1),h=Object(a.a)(b,2),x=h[0],u=h[1],p=Object(r.useState)(""),m=Object(a.a)(p,2),g=m[0],y=m[1];return Object(O.jsx)("div",{className:"container",children:Object(O.jsxs)("div",{className:"form",children:[x?Object(O.jsxs)(O.Fragment,{children:[Object(O.jsx)(T.a,{variant:"success",onClose:function(){u(!1)},dismissible:!0,children:Object(O.jsx)("p",{children:g})}),Object(O.jsx)("h1",{children:"Sign Up Page"})]}):Object(O.jsx)("h1",{children:"Sign Up Page"}),Object(O.jsxs)("form",{children:[Object(O.jsxs)(L.a.Group,{children:[Object(O.jsx)(L.a.Label,{children:"Username"}),Object(O.jsx)(L.a.Control,Object(v.a)({type:"text",placeholder:"Your username"},l("username",{required:!0,maxLength:25}))),d.username&&Object(O.jsx)("small",{style:{color:"red"},children:"Username is required"}),"maxLength"===(null===(e=d.username)||void 0===e?void 0:e.type)&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Max characters should be 25 "})})]}),Object(O.jsx)("br",{}),Object(O.jsxs)(L.a.Group,{children:[Object(O.jsx)(L.a.Label,{children:"Email"}),Object(O.jsx)(L.a.Control,Object(v.a)({type:"email",placeholder:"Your email"},l("email",{required:!0,maxLength:80}))),d.email&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Email is required"})}),"maxLength"===(null===(t=d.email)||void 0===t?void 0:t.type)&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Max characters should be 80"})})]}),Object(O.jsx)("br",{}),Object(O.jsxs)(L.a.Group,{children:[Object(O.jsx)(L.a.Label,{children:"Password"}),Object(O.jsx)(L.a.Control,Object(v.a)({type:"password",placeholder:"Your password"},l("password",{required:!0,minLength:8}))),d.password&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Password is required"})}),"minLength"===(null===(c=d.password)||void 0===c?void 0:c.type)&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Min characters should be 8"})})]}),Object(O.jsx)("br",{}),Object(O.jsxs)(L.a.Group,{children:[Object(O.jsx)(L.a.Label,{children:"Confirm Password"}),Object(O.jsx)(L.a.Control,Object(v.a)({type:"password",placeholder:"Your password"},l("confirmPassword",{required:!0,minLength:8}))),d.confirmPassword&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Confirm Password is required"})}),"minLength"===(null===(n=d.confirmPassword)||void 0===n?void 0:n.type)&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Min characters should be 8"})})]}),Object(O.jsx)("br",{}),Object(O.jsx)(L.a.Group,{children:Object(O.jsx)(f.a,{as:"sub",variant:"primary",onClick:o((function(e){if(e.password===e.confirmPassword){var t={username:e.username,email:e.email,password:e.password},c={method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(t)};fetch("/auth/signup",c).then((function(e){return e.json()})).then((function(e){console.log(e),y(e.message),u(!0)})).catch((function(e){return console.log(e)})),j()}else alert("Passwords do not match")})),children:"SignUp"})}),Object(O.jsx)("br",{}),Object(O.jsx)(L.a.Group,{children:Object(O.jsxs)("small",{children:["Already have an account, ",Object(O.jsx)(i.b,{to:"/login",children:"Log In"})]})}),Object(O.jsx)("br",{})]})]})})},q=function(){var e,t,c=Object(w.a)(),r=c.register,n=c.handleSubmit,s=c.reset,a=c.formState.errors,l=Object(m.f)();return Object(O.jsx)("div",{className:"container",children:Object(O.jsxs)("div",{className:"form",children:[Object(O.jsx)("h1",{children:"Login Page"}),Object(O.jsxs)("form",{children:[Object(O.jsxs)(L.a.Group,{children:[Object(O.jsx)(L.a.Label,{children:"Username"}),Object(O.jsx)(L.a.Control,Object(v.a)({type:"text",placeholder:"Your username"},r("username",{required:!0,maxLength:25})))]}),a.username&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Username is required"})}),"maxLength"===(null===(e=a.username)||void 0===e?void 0:e.type)&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Username should be 25 characters"})}),Object(O.jsx)("br",{}),Object(O.jsxs)(L.a.Group,{children:[Object(O.jsx)(L.a.Label,{children:"Password"}),Object(O.jsx)(L.a.Control,Object(v.a)({type:"password",placeholder:"Your password"},r("password",{required:!0,minLength:8})))]}),a.username&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Password is required"})}),"maxLength"===(null===(t=a.password)||void 0===t?void 0:t.type)&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Password should be more than 8 characters"})}),Object(O.jsx)("br",{}),Object(O.jsx)(L.a.Group,{children:Object(O.jsx)(f.a,{as:"sub",variant:"primary",onClick:n((function(e){console.log(e);var t={method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(e)};fetch("/auth/login",t).then((function(e){return e.json()})).then((function(e){console.log(e.access_token),e?(b(e.access_token),l.push("/")):alert("Invalid username or password")})),s()})),children:"Login"})}),Object(O.jsx)("br",{}),Object(O.jsx)(L.a.Group,{children:Object(O.jsxs)("small",{children:["Do not have an account? ",Object(O.jsx)(i.b,{to:"/signup",children:"Create One"})]})})]})]})})},G=function(){var e,t,c=Object(w.a)(),n=c.register,s=c.handleSubmit,i=c.reset,l=c.formState.errors,o=Object(r.useState)(!1),j=Object(a.a)(o,2);j[0],j[1];return Object(O.jsxs)("div",{className:"container",children:[Object(O.jsx)("h1",{children:"Create A Recipe"}),Object(O.jsxs)("form",{children:[Object(O.jsxs)(L.a.Group,{children:[Object(O.jsx)(L.a.Label,{children:"Title"}),Object(O.jsx)(L.a.Control,Object(v.a)({type:"text"},n("title",{required:!0,maxLength:25})))]}),l.title&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Title is required"})}),"maxLength"===(null===(e=l.title)||void 0===e?void 0:e.type)&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Title should be less than 25 characters"})}),Object(O.jsxs)(L.a.Group,{children:[Object(O.jsx)(L.a.Label,{children:"Description"}),Object(O.jsx)(L.a.Control,Object(v.a)({as:"textarea",rows:5},n("description",{required:!0,maxLength:255})))]}),l.description&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Description is required"})}),"maxLength"===(null===(t=l.description)||void 0===t?void 0:t.type)&&Object(O.jsx)("p",{style:{color:"red"},children:Object(O.jsx)("small",{children:"Description should be less than 255 characters"})}),Object(O.jsx)("br",{}),Object(O.jsx)(L.a.Group,{children:Object(O.jsx)(f.a,{variant:"primary",onClick:s((function(e){console.log(e);var t=localStorage.getItem("REACT_TOKEN_AUTH_KEY");console.log(t);var c={method:"POST",headers:{"content-type":"application/json",Authorization:"Bearer ".concat(JSON.parse(t))},body:JSON.stringify(e)};fetch("/recipe/recipes",c).then((function(e){return e.json()})).then((function(e){i()})).catch((function(e){return console.log(e)}))})),children:"Save"})})]})]})},U=function(){return Object(O.jsx)(i.a,{children:Object(O.jsxs)("div",{className:"",children:[Object(O.jsx)(p,{}),Object(O.jsxs)(m.c,{children:[Object(O.jsx)(m.a,{path:"/create_recipe",children:Object(O.jsx)(G,{})}),Object(O.jsx)(m.a,{path:"/login",children:Object(O.jsx)(q,{})}),Object(O.jsx)(m.a,{path:"/signup",children:Object(O.jsx)(P,{})}),Object(O.jsx)(m.a,{path:"/",children:Object(O.jsx)(k,{})})]})]})})};s.a.render(Object(O.jsx)(U,{}),document.getElementById("root"))}},[[63,1,2]]]); 2 | //# sourceMappingURL=main.504c6dd8.chunk.js.map -------------------------------------------------------------------------------- /client/build/static/js/main.504c6dd8.chunk.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["auth.js","components/Navbar.js","components/Recipe.js","components/Home.js","components/SignUp.js","components/Login.js","components/CreateRecipe.js","index.js"],"names":["createAuthProvider","accessTokenKey","onUpdateToken","token","fetch","method","body","refresh_token","then","r","json","useAuth","login","logout","LoggedInLinks","className","to","href","onClick","LoggedOutLinks","NavBar","logged","type","id","Recipe","title","description","onDelete","Card","Body","Title","Button","variant","LoggedinHome","useState","recipes","setRecipes","show","setShow","useForm","register","handleSubmit","reset","setValue","errors","formState","recipeId","setRecipeId","useEffect","res","data","catch","err","console","log","localStorage","getItem","deleteRecipe","requestOptions","headers","JSON","parse","Modal","size","onHide","Header","closeButton","Form","Group","Label","Control","required","maxLength","style","color","as","rows","stringify","window","location","reload","map","recipe","index","LoggedOutHome","HomePage","SignUpPage","serverResponse","setServerResponse","Alert","onClose","dismissible","placeholder","username","email","minLength","password","confirmPassword","message","alert","LoginPage","history","useHistory","access_token","push","CreateRecipePage","App","path","CreateRecipe","Login","SignUp","Home","ReactDOM","render","document","getElementById"],"mappings":"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","file":"static/js/main.504c6dd8.chunk.js","sourcesContent":["import {createAuthProvider} from 'react-token-auth'\r\n\r\nexport const [useAuth, authFetch, login, logout] =\r\n createAuthProvider({\r\n accessTokenKey: 'access_token',\r\n onUpdateToken: (token) => fetch('/auth/refresh', {\r\n method: 'POST',\r\n body: token.refresh_token\r\n })\r\n .then(r => r.json())\r\n })","import React from 'react'\r\nimport { Link } from 'react-router-dom'\r\nimport { useAuth ,logout} from '../auth'\r\n\r\n\r\n\r\n\r\nconst LoggedInLinks = () => {\r\n return (\r\n <>\r\n
  • \r\n Home\r\n
  • \r\n
  • \r\n Create Recipes\r\n
  • \r\n
  • \r\n {logout()}}>Log Out\r\n
  • \r\n \r\n )\r\n}\r\n\r\n\r\nconst LoggedOutLinks = () => {\r\n return (\r\n <>\r\n
  • \r\n Home\r\n
  • \r\n
  • \r\n Sign Up\r\n
  • \r\n
  • \r\n Login\r\n
  • \r\n\r\n \r\n )\r\n}\r\n\r\nconst NavBar = () => {\r\n\r\n const [logged] = useAuth();\r\n\r\n return (\r\n \r\n )\r\n}\r\n\r\nexport default NavBar","import React from 'react'\r\nimport { Button, Card ,Modal} from 'react-bootstrap';\r\n\r\n\r\nconst Recipe=({title,description,onClick,onDelete})=>{\r\n return(\r\n \r\n \r\n {title}\r\n

    {description}

    \r\n \r\n {' '}\r\n \r\n
    \r\n
    \r\n )\r\n}\r\n\r\n\r\nexport default Recipe;","import React, { useEffect, useState } from 'react'\r\nimport { Link } from 'react-router-dom'\r\nimport { useAuth } from '../auth'\r\nimport Recipe from './Recipe'\r\nimport { Modal ,Form,Button} from 'react-bootstrap'\r\nimport { useForm } from 'react-hook-form'\r\n\r\n\r\n\r\n\r\n\r\nconst LoggedinHome = () => {\r\n const [recipes, setRecipes] = useState([]);\r\n const [show, setShow] = useState(false)\r\n const {register,reset,handleSubmit,setValue,formState:{errors}}=useForm()\r\n const [recipeId,setRecipeId]=useState(0);\r\n\r\n useEffect(\r\n () => {\r\n fetch('/recipe/recipes')\r\n .then(res => res.json())\r\n .then(data => {\r\n setRecipes(data)\r\n })\r\n .catch(err => console.log(err))\r\n }, []\r\n );\r\n\r\n const getAllRecipes=()=>{\r\n fetch('/recipe/recipes')\r\n .then(res => res.json())\r\n .then(data => {\r\n setRecipes(data)\r\n })\r\n .catch(err => console.log(err))\r\n }\r\n \r\n\r\n const closeModal = () => {\r\n setShow(false)\r\n }\r\n\r\n const showModal = (id) => {\r\n setShow(true)\r\n setRecipeId(id)\r\n recipes.map(\r\n (recipe)=>{\r\n if(recipe.id==id){\r\n setValue('title',recipe.title)\r\n setValue('description',recipe.description)\r\n }\r\n }\r\n )\r\n }\r\n\r\n\r\n let token=localStorage.getItem('REACT_TOKEN_AUTH_KEY')\r\n\r\n const updateRecipe=(data)=>{\r\n console.log(data)\r\n\r\n \r\n\r\n const requestOptions={\r\n method:'PUT',\r\n headers:{\r\n 'content-type':'application/json',\r\n 'Authorization':`Bearer ${JSON.parse(token)}`\r\n },\r\n body:JSON.stringify(data)\r\n }\r\n\r\n\r\n fetch(`/recipe/recipe/${recipeId}`,requestOptions)\r\n .then(res=>res.json())\r\n .then(data=>{\r\n console.log(data)\r\n\r\n const reload =window.location.reload()\r\n reload() \r\n })\r\n .catch(err=>console.log(err))\r\n }\r\n\r\n\r\n\r\n const deleteRecipe=(id)=>{\r\n console.log(id)\r\n \r\n\r\n const requestOptions={\r\n method:'DELETE',\r\n headers:{\r\n 'content-type':'application/json',\r\n 'Authorization':`Bearer ${JSON.parse(token)}`\r\n }\r\n }\r\n\r\n\r\n fetch(`/recipe/recipe/${id}`,requestOptions)\r\n .then(res=>res.json())\r\n .then(data=>{\r\n console.log(data)\r\n getAllRecipes()\r\n \r\n })\r\n .catch(err=>console.log(err))\r\n }\r\n\r\n\r\n\r\n\r\n return (\r\n
    \r\n \r\n \r\n \r\n Update Recipe\r\n \r\n \r\n \r\n
    \r\n \r\n Title\r\n \r\n \r\n {errors.title &&

    Title is required

    }\r\n {errors.title?.type === \"maxLength\" &&

    \r\n Title should be less than 25 characters\r\n

    }\r\n \r\n Description\r\n \r\n \r\n {errors.description &&

    Description is required

    }\r\n {errors.description?.type === \"maxLength\" &&

    \r\n Description should be less than 255 characters\r\n

    }\r\n

    \r\n \r\n \r\n \r\n
    \r\n
    \r\n \r\n

    List of Recipes

    \r\n {\r\n recipes.map(\r\n (recipe,index) => (\r\n {showModal(recipe.id)}}\r\n\r\n onDelete={()=>{deleteRecipe(recipe.id)}}\r\n\r\n />\r\n )\r\n )\r\n }\r\n
    \r\n )\r\n}\r\n\r\n\r\nconst LoggedOutHome = () => {\r\n return (\r\n
    \r\n

    Welcome to the Recipes

    \r\n Get Started\r\n
    \r\n )\r\n}\r\n\r\nconst HomePage = () => {\r\n\r\n const [logged] = useAuth()\r\n\r\n return (\r\n
    \r\n {logged ? : }\r\n
    \r\n )\r\n}\r\n\r\nexport default HomePage","import React, { useState } from 'react'\r\nimport { Form, Button, Alert } from 'react-bootstrap'\r\nimport { Link } from 'react-router-dom'\r\nimport { useForm } from 'react-hook-form'\r\n\r\n\r\nconst SignUpPage = () => {\r\n\r\n\r\n const { register, handleSubmit, reset, formState: { errors } } = useForm();\r\n const [show,setShow]=useState(false)\r\n const [serverResponse,setServerResponse]=useState('')\r\n\r\n const submitForm = (data) => {\r\n\r\n\r\n if (data.password === data.confirmPassword) {\r\n\r\n\r\n const body = {\r\n username: data.username,\r\n email: data.email,\r\n password: data.password\r\n }\r\n\r\n const requestOptions = {\r\n method: \"POST\",\r\n headers: {\r\n 'content-type': 'application/json'\r\n },\r\n body: JSON.stringify(body)\r\n }\r\n\r\n\r\n fetch('/auth/signup', requestOptions)\r\n .then(res => res.json())\r\n .then(data =>{\r\n console.log(data)\r\n setServerResponse(data.message)\r\n setShow(true)\r\n })\r\n .catch(err => console.log(err))\r\n\r\n reset()\r\n }\r\n\r\n else {\r\n alert(\"Passwords do not match\")\r\n }\r\n\r\n\r\n }\r\n\r\n\r\n return (\r\n
    \r\n
    \r\n\r\n \r\n {show?\r\n <>\r\n {setShow(false)\r\n }} dismissible>\r\n

    \r\n {serverResponse}\r\n

    \r\n
    \r\n\r\n

    Sign Up Page

    \r\n \r\n \r\n :\r\n

    Sign Up Page

    \r\n \r\n }\r\n
    \r\n \r\n Username\r\n \r\n\r\n {errors.username && Username is required}\r\n {errors.username?.type === \"maxLength\" &&

    Max characters should be 25

    }\r\n
    \r\n

    \r\n \r\n Email\r\n \r\n\r\n {errors.email &&

    Email is required

    }\r\n\r\n {errors.email?.type === \"maxLength\" &&

    Max characters should be 80

    }\r\n
    \r\n

    \r\n \r\n Password\r\n \r\n\r\n {errors.password &&

    Password is required

    }\r\n {errors.password?.type === \"minLength\" &&

    Min characters should be 8

    }\r\n
    \r\n

    \r\n \r\n Confirm Password\r\n \r\n {errors.confirmPassword &&

    Confirm Password is required

    }\r\n {errors.confirmPassword?.type === \"minLength\" &&

    Min characters should be 8

    }\r\n
    \r\n

    \r\n \r\n \r\n \r\n

    \r\n \r\n Already have an account, Log In\r\n \r\n

    \r\n
    \r\n
    \r\n
    \r\n )\r\n}\r\n\r\nexport default SignUpPage","import React, { useState } from 'react'\r\nimport {Form,Button} from 'react-bootstrap'\r\nimport { Link } from 'react-router-dom'\r\nimport {useForm} from 'react-hook-form'\r\nimport { login } from '../auth'\r\nimport {useHistory} from 'react-router-dom'\r\n\r\n\r\nconst LoginPage=()=>{\r\n \r\n const {register,handleSubmit,reset,formState:{errors}}=useForm()\r\n\r\n const history=useHistory()\r\n \r\n\r\n\r\n const loginUser=(data)=>{\r\n console.log(data)\r\n\r\n const requestOptions={\r\n method:\"POST\",\r\n headers:{\r\n 'content-type':'application/json'\r\n },\r\n body:JSON.stringify(data)\r\n }\r\n \r\n fetch('/auth/login',requestOptions)\r\n .then(res=>res.json())\r\n .then(data=>{\r\n console.log(data.access_token)\r\n \r\n if (data){\r\n login(data.access_token)\r\n\r\n history.push('/')\r\n }\r\n else{\r\n alert('Invalid username or password')\r\n }\r\n\r\n\r\n })\r\n\r\n\r\n\r\n reset()\r\n }\r\n\r\n return(\r\n
    \r\n
    \r\n

    Login Page

    \r\n
    \r\n \r\n Username\r\n \r\n \r\n {errors.username &&

    Username is required

    }\r\n {errors.username?.type === \"maxLength\" &&

    Username should be 25 characters

    }\r\n

    \r\n \r\n \r\n Password\r\n \r\n \r\n {errors.username &&

    Password is required

    }\r\n {errors.password?.type === \"maxLength\" &&

    \r\n Password should be more than 8 characters\r\n

    }\r\n

    \r\n \r\n \r\n \r\n

    \r\n \r\n Do not have an account? Create One\r\n \r\n \r\n
    \r\n
    \r\n
    \r\n )\r\n}\r\n\r\nexport default LoginPage","import React, { useState } from 'react'\r\nimport { Form, Button } from 'react-bootstrap'\r\nimport { useForm } from 'react-hook-form'\r\n\r\n\r\nconst CreateRecipePage = () => {\r\n\r\n const { register, handleSubmit, reset, formState: { errors } } = useForm()\r\n const [show,setShow]=useState(false);\r\n\r\n const createRecipe = (data) => {\r\n console.log(data)\r\n\r\n const token = localStorage.getItem('REACT_TOKEN_AUTH_KEY');\r\n console.log(token)\r\n\r\n\r\n const requestOptions = {\r\n method: 'POST',\r\n headers: {\r\n 'content-type': 'application/json',\r\n 'Authorization': `Bearer ${JSON.parse(token)}`\r\n },\r\n body: JSON.stringify(data)\r\n\r\n }\r\n\r\n fetch('/recipe/recipes', requestOptions)\r\n .then(res => res.json())\r\n .then(data => {\r\n reset()\r\n })\r\n .catch(err => console.log(err))\r\n\r\n }\r\n\r\n return (\r\n
    \r\n \r\n

    Create A Recipe

    \r\n
    \r\n \r\n Title\r\n \r\n \r\n {errors.title &&

    Title is required

    }\r\n {errors.title?.type === \"maxLength\" &&

    \r\n Title should be less than 25 characters\r\n

    }\r\n \r\n Description\r\n \r\n \r\n {errors.description &&

    Description is required

    }\r\n {errors.description?.type === \"maxLength\" &&

    \r\n Description should be less than 255 characters\r\n

    }\r\n

    \r\n \r\n \r\n \r\n
    \r\n
    \r\n )\r\n}\r\n\r\nexport default CreateRecipePage","import 'bootstrap/dist/css/bootstrap.min.css';\r\nimport './styles/main.css'\r\nimport React from 'react'\r\nimport ReactDOM from 'react-dom'\r\nimport NavBar from './components/Navbar';\r\n\r\nimport {\r\n BrowserRouter as Router,\r\n Switch,\r\n Route\r\n} from 'react-router-dom'\r\nimport HomePage from './components/Home';\r\nimport SignUpPage from './components/SignUp';\r\nimport LoginPage from './components/Login';\r\nimport CreateRecipePage from './components/CreateRecipe';\r\n\r\n\r\n\r\n\r\nconst App=()=>{\r\n\r\n \r\n return (\r\n \r\n
    \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
    \r\n
    \r\n )\r\n}\r\n\r\n\r\nReactDOM.render(,document.getElementById('root'))"],"sourceRoot":""} -------------------------------------------------------------------------------- /client/build/static/js/runtime-main.7545a8a1.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(t){for(var n,l,i=t[0],f=t[1],a=t[2],p=0,s=[];p0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jod35/Flask-React-Recipes/574e600c507e6451e0e0147f601a89a599eab17d/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jod35/Flask-React-Recipes/574e600c507e6451e0e0147f601a89a599eab17d/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jod35/Flask-React-Recipes/574e600c507e6451e0e0147f601a89a599eab17d/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/src/auth.js: -------------------------------------------------------------------------------- 1 | import {createAuthProvider} from 'react-token-auth' 2 | 3 | export const [useAuth, authFetch, login, logout] = 4 | createAuthProvider({ 5 | accessTokenKey: 'access_token', 6 | onUpdateToken: (token) => fetch('/auth/refresh', { 7 | method: 'POST', 8 | body: token.refresh_token 9 | }) 10 | .then(r => r.json()) 11 | }) -------------------------------------------------------------------------------- /client/src/components/CreateRecipe.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Form, Button } from 'react-bootstrap' 3 | import { useForm } from 'react-hook-form' 4 | 5 | 6 | const CreateRecipePage = () => { 7 | 8 | const { register, handleSubmit, reset, formState: { errors } } = useForm() 9 | const [show,setShow]=useState(false); 10 | 11 | const createRecipe = (data) => { 12 | console.log(data) 13 | 14 | const token = localStorage.getItem('REACT_TOKEN_AUTH_KEY'); 15 | console.log(token) 16 | 17 | 18 | const requestOptions = { 19 | method: 'POST', 20 | headers: { 21 | 'content-type': 'application/json', 22 | 'Authorization': `Bearer ${JSON.parse(token)}` 23 | }, 24 | body: JSON.stringify(data) 25 | 26 | } 27 | 28 | fetch('/recipe/recipes', requestOptions) 29 | .then(res => res.json()) 30 | .then(data => { 31 | reset() 32 | }) 33 | .catch(err => console.log(err)) 34 | 35 | } 36 | 37 | return ( 38 |
    39 | 40 |

    Create A Recipe

    41 |
    42 | 43 | Title 44 | 47 | 48 | {errors.title &&

    Title is required

    } 49 | {errors.title?.type === "maxLength" &&

    50 | Title should be less than 25 characters 51 |

    } 52 | 53 | Description 54 | 57 | 58 | {errors.description &&

    Description is required

    } 59 | {errors.description?.type === "maxLength" &&

    60 | Description should be less than 255 characters 61 |

    } 62 |

    63 | 64 | 67 | 68 |
    69 |
    70 | ) 71 | } 72 | 73 | export default CreateRecipePage -------------------------------------------------------------------------------- /client/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { useAuth } from '../auth' 4 | import Recipe from './Recipe' 5 | import { Modal ,Form,Button} from 'react-bootstrap' 6 | import { useForm } from 'react-hook-form' 7 | 8 | 9 | 10 | 11 | 12 | const LoggedinHome = () => { 13 | const [recipes, setRecipes] = useState([]); 14 | const [show, setShow] = useState(false) 15 | const {register,reset,handleSubmit,setValue,formState:{errors}}=useForm() 16 | const [recipeId,setRecipeId]=useState(0); 17 | 18 | useEffect( 19 | () => { 20 | fetch('/recipe/recipes') 21 | .then(res => res.json()) 22 | .then(data => { 23 | setRecipes(data) 24 | }) 25 | .catch(err => console.log(err)) 26 | }, [] 27 | ); 28 | 29 | const getAllRecipes=()=>{ 30 | fetch('/recipe/recipes') 31 | .then(res => res.json()) 32 | .then(data => { 33 | setRecipes(data) 34 | }) 35 | .catch(err => console.log(err)) 36 | } 37 | 38 | 39 | const closeModal = () => { 40 | setShow(false) 41 | } 42 | 43 | const showModal = (id) => { 44 | setShow(true) 45 | setRecipeId(id) 46 | recipes.map( 47 | (recipe)=>{ 48 | if(recipe.id==id){ 49 | setValue('title',recipe.title) 50 | setValue('description',recipe.description) 51 | } 52 | } 53 | ) 54 | } 55 | 56 | 57 | let token=localStorage.getItem('REACT_TOKEN_AUTH_KEY') 58 | 59 | const updateRecipe=(data)=>{ 60 | console.log(data) 61 | 62 | 63 | 64 | const requestOptions={ 65 | method:'PUT', 66 | headers:{ 67 | 'content-type':'application/json', 68 | 'Authorization':`Bearer ${JSON.parse(token)}` 69 | }, 70 | body:JSON.stringify(data) 71 | } 72 | 73 | 74 | fetch(`/recipe/recipe/${recipeId}`,requestOptions) 75 | .then(res=>res.json()) 76 | .then(data=>{ 77 | console.log(data) 78 | 79 | const reload =window.location.reload() 80 | reload() 81 | }) 82 | .catch(err=>console.log(err)) 83 | } 84 | 85 | 86 | 87 | const deleteRecipe=(id)=>{ 88 | console.log(id) 89 | 90 | 91 | const requestOptions={ 92 | method:'DELETE', 93 | headers:{ 94 | 'content-type':'application/json', 95 | 'Authorization':`Bearer ${JSON.parse(token)}` 96 | } 97 | } 98 | 99 | 100 | fetch(`/recipe/recipe/${id}`,requestOptions) 101 | .then(res=>res.json()) 102 | .then(data=>{ 103 | console.log(data) 104 | getAllRecipes() 105 | 106 | }) 107 | .catch(err=>console.log(err)) 108 | } 109 | 110 | 111 | 112 | 113 | return ( 114 |
    115 | 120 | 121 | 122 | Update Recipe 123 | 124 | 125 | 126 |
    127 | 128 | Title 129 | 132 | 133 | {errors.title &&

    Title is required

    } 134 | {errors.title?.type === "maxLength" &&

    135 | Title should be less than 25 characters 136 |

    } 137 | 138 | Description 139 | 142 | 143 | {errors.description &&

    Description is required

    } 144 | {errors.description?.type === "maxLength" &&

    145 | Description should be less than 255 characters 146 |

    } 147 |

    148 | 149 | 152 | 153 |
    154 |
    155 |
    156 |

    List of Recipes

    157 | { 158 | recipes.map( 159 | (recipe,index) => ( 160 | {showModal(recipe.id)}} 165 | 166 | onDelete={()=>{deleteRecipe(recipe.id)}} 167 | 168 | /> 169 | ) 170 | ) 171 | } 172 |
    173 | ) 174 | } 175 | 176 | 177 | const LoggedOutHome = () => { 178 | return ( 179 |
    180 |

    Welcome to the Recipes

    181 | Get Started 182 |
    183 | ) 184 | } 185 | 186 | const HomePage = () => { 187 | 188 | const [logged] = useAuth() 189 | 190 | return ( 191 |
    192 | {logged ? : } 193 |
    194 | ) 195 | } 196 | 197 | export default HomePage -------------------------------------------------------------------------------- /client/src/components/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import {Form,Button} from 'react-bootstrap' 3 | import { Link } from 'react-router-dom' 4 | import {useForm} from 'react-hook-form' 5 | import { login } from '../auth' 6 | import {useHistory} from 'react-router-dom' 7 | 8 | 9 | const LoginPage=()=>{ 10 | 11 | const {register,handleSubmit,reset,formState:{errors}}=useForm() 12 | 13 | const history=useHistory() 14 | 15 | 16 | 17 | const loginUser=(data)=>{ 18 | console.log(data) 19 | 20 | const requestOptions={ 21 | method:"POST", 22 | headers:{ 23 | 'content-type':'application/json' 24 | }, 25 | body:JSON.stringify(data) 26 | } 27 | 28 | fetch('/auth/login',requestOptions) 29 | .then(res=>res.json()) 30 | .then(data=>{ 31 | console.log(data.access_token) 32 | 33 | if (data){ 34 | login(data.access_token) 35 | 36 | history.push('/') 37 | } 38 | else{ 39 | alert('Invalid username or password') 40 | } 41 | 42 | 43 | }) 44 | 45 | 46 | 47 | reset() 48 | } 49 | 50 | return( 51 |
    52 |
    53 |

    Login Page

    54 |
    55 | 56 | Username 57 | 61 | 62 | {errors.username &&

    Username is required

    } 63 | {errors.username?.type === "maxLength" &&

    Username should be 25 characters

    } 64 |

    65 | 66 | 67 | Password 68 | 72 | 73 | {errors.username &&

    Password is required

    } 74 | {errors.password?.type === "maxLength" &&

    75 | Password should be more than 8 characters 76 |

    } 77 |

    78 | 79 | 80 | 81 |

    82 | 83 | Do not have an account? Create One 84 | 85 | 86 |
    87 |
    88 |
    89 | ) 90 | } 91 | 92 | export default LoginPage -------------------------------------------------------------------------------- /client/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { useAuth ,logout} from '../auth' 4 | 5 | 6 | 7 | 8 | const LoggedInLinks = () => { 9 | return ( 10 | <> 11 |
  • 12 | Home 13 |
  • 14 |
  • 15 | Create Recipes 16 |
  • 17 |
  • 18 | {logout()}}>Log Out 19 |
  • 20 | 21 | ) 22 | } 23 | 24 | 25 | const LoggedOutLinks = () => { 26 | return ( 27 | <> 28 |
  • 29 | Home 30 |
  • 31 |
  • 32 | Sign Up 33 |
  • 34 |
  • 35 | Login 36 |
  • 37 | 38 | 39 | ) 40 | } 41 | 42 | const NavBar = () => { 43 | 44 | const [logged] = useAuth(); 45 | 46 | return ( 47 | 60 | ) 61 | } 62 | 63 | export default NavBar -------------------------------------------------------------------------------- /client/src/components/Recipe.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Card ,Modal} from 'react-bootstrap'; 3 | 4 | 5 | const Recipe=({title,description,onClick,onDelete})=>{ 6 | return( 7 | 8 | 9 | {title} 10 |

    {description}

    11 | 12 | {' '} 13 | 14 |
    15 |
    16 | ) 17 | } 18 | 19 | 20 | export default Recipe; -------------------------------------------------------------------------------- /client/src/components/SignUp.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Form, Button, Alert } from 'react-bootstrap' 3 | import { Link } from 'react-router-dom' 4 | import { useForm } from 'react-hook-form' 5 | 6 | 7 | const SignUpPage = () => { 8 | 9 | 10 | const { register, handleSubmit, reset, formState: { errors } } = useForm(); 11 | const [show,setShow]=useState(false) 12 | const [serverResponse,setServerResponse]=useState('') 13 | 14 | const submitForm = (data) => { 15 | 16 | 17 | if (data.password === data.confirmPassword) { 18 | 19 | 20 | const body = { 21 | username: data.username, 22 | email: data.email, 23 | password: data.password 24 | } 25 | 26 | const requestOptions = { 27 | method: "POST", 28 | headers: { 29 | 'content-type': 'application/json' 30 | }, 31 | body: JSON.stringify(body) 32 | } 33 | 34 | 35 | fetch('/auth/signup', requestOptions) 36 | .then(res => res.json()) 37 | .then(data =>{ 38 | console.log(data) 39 | setServerResponse(data.message) 40 | setShow(true) 41 | }) 42 | .catch(err => console.log(err)) 43 | 44 | reset() 45 | } 46 | 47 | else { 48 | alert("Passwords do not match") 49 | } 50 | 51 | 52 | } 53 | 54 | 55 | return ( 56 |
    57 |
    58 | 59 | 60 | {show? 61 | <> 62 | {setShow(false) 63 | }} dismissible> 64 |

    65 | {serverResponse} 66 |

    67 |
    68 | 69 |

    Sign Up Page

    70 | 71 | 72 | : 73 |

    Sign Up Page

    74 | 75 | } 76 |
    77 | 78 | Username 79 | 83 | 84 | {errors.username && Username is required} 85 | {errors.username?.type === "maxLength" &&

    Max characters should be 25

    } 86 |
    87 |

    88 | 89 | Email 90 | 94 | 95 | {errors.email &&

    Email is required

    } 96 | 97 | {errors.email?.type === "maxLength" &&

    Max characters should be 80

    } 98 |
    99 |

    100 | 101 | Password 102 | 107 | 108 | {errors.password &&

    Password is required

    } 109 | {errors.password?.type === "minLength" &&

    Min characters should be 8

    } 110 |
    111 |

    112 | 113 | Confirm Password 114 | 117 | {errors.confirmPassword &&

    Confirm Password is required

    } 118 | {errors.confirmPassword?.type === "minLength" &&

    Min characters should be 8

    } 119 |
    120 |

    121 | 122 | 123 | 124 |

    125 | 126 | Already have an account, Log In 127 | 128 |

    129 |
    130 |
    131 |
    132 | ) 133 | } 134 | 135 | export default SignUpPage -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.min.css'; 2 | import './styles/main.css' 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import NavBar from './components/Navbar'; 6 | 7 | import { 8 | BrowserRouter as Router, 9 | Switch, 10 | Route 11 | } from 'react-router-dom' 12 | import HomePage from './components/Home'; 13 | import SignUpPage from './components/SignUp'; 14 | import LoginPage from './components/Login'; 15 | import CreateRecipePage from './components/CreateRecipe'; 16 | 17 | 18 | 19 | 20 | const App=()=>{ 21 | 22 | 23 | return ( 24 | 25 |
    26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
    42 |
    43 | ) 44 | } 45 | 46 | 47 | ReactDOM.render(,document.getElementById('root')) -------------------------------------------------------------------------------- /client/src/styles/main.css: -------------------------------------------------------------------------------- 1 | .container{ 2 | margin-top: 50px; 3 | } 4 | .heading{ 5 | font-size: 3em; 6 | } 7 | .form{ 8 | margin:auto; 9 | width: 80%; 10 | } 11 | .recipe{ 12 | margin-top:20px; 13 | } -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | import os 3 | from datetime import timedelta 4 | 5 | from dotenv import load_dotenv 6 | 7 | 8 | load_dotenv() 9 | 10 | BASE_DIR = os.path.dirname(os.path.realpath(__file__)) 11 | 12 | 13 | class Config: 14 | SECRET_KEY = os.getenv("SECRET_KEY", "secret") 15 | SQLALCHEMY_TRACK_MODIFICATIONS = os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS", False) 16 | 17 | 18 | class DevConfig(Config): 19 | SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URI", "sqlite:///dev.db") 20 | DEBUG = True 21 | # SQLALCHEMY_ECHO=True 22 | 23 | 24 | class ProdConfig(Config): 25 | SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URI", "sqlite:///prod.db") 26 | DEBUG = os.getenv("DEBUG", False) 27 | SQLALCHEMY_ECHO = os.getenv("ECHO", False) 28 | SQLALCHEMY_TRACK_MODIFICATIONS = os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS", False) 29 | 30 | 31 | class TestConfig(Config): 32 | SQLALCHEMY_DATABASE_URI = "sqlite:///test.db" 33 | SQLALCHEMY_ECHO = False 34 | TESTING = True 35 | -------------------------------------------------------------------------------- /dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jod35/Flask-React-Recipes/574e600c507e6451e0e0147f601a89a599eab17d/dev.db -------------------------------------------------------------------------------- /exts.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | 3 | db = SQLAlchemy() 4 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_restx import Api 3 | from models import Recipe, User 4 | from exts import db 5 | from flask_migrate import Migrate 6 | from flask_jwt_extended import JWTManager 7 | from recipes import recipe_ns 8 | from auth import auth_ns 9 | from flask_cors import CORS 10 | 11 | 12 | def create_app(config): 13 | app = Flask(__name__, static_url_path="/", static_folder="./client/build") 14 | app.config.from_object(config) 15 | 16 | CORS(app) 17 | 18 | db.init_app(app) 19 | 20 | migrate = Migrate(app, db) 21 | JWTManager(app) 22 | 23 | api = Api(app, doc="/docs") 24 | 25 | api.add_namespace(recipe_ns) 26 | api.add_namespace(auth_ns) 27 | 28 | @app.route("/") 29 | def index(): 30 | return app.send_static_file("index.html") 31 | 32 | @app.errorhandler(404) 33 | def not_found(err): 34 | return app.send_static_file("index.html") 35 | 36 | # model (serializer) 37 | @app.shell_context_processor 38 | def make_shell_context(): 39 | return {"db": db, "Recipe": Recipe, "user": User} 40 | 41 | return app 42 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,flask_migrate 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from flask import current_app 7 | 8 | from alembic import context 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | # Interpret the config file for Python logging. 15 | # This line sets up loggers basically. 16 | fileConfig(config.config_file_name) 17 | logger = logging.getLogger("alembic.env") 18 | 19 | # add your model's MetaData object here 20 | # for 'autogenerate' support 21 | # from myapp import mymodel 22 | # target_metadata = mymodel.Base.metadata 23 | config.set_main_option( 24 | "sqlalchemy.url", 25 | str(current_app.extensions["migrate"].db.get_engine().url).replace("%", "%%"), 26 | ) 27 | target_metadata = current_app.extensions["migrate"].db.metadata 28 | 29 | # other values from the config, defined by the needs of env.py, 30 | # can be acquired: 31 | # my_important_option = config.get_main_option("my_important_option") 32 | # ... etc. 33 | 34 | 35 | def run_migrations_offline(): 36 | """Run migrations in 'offline' mode. 37 | 38 | This configures the context with just a URL 39 | and not an Engine, though an Engine is acceptable 40 | here as well. By skipping the Engine creation 41 | we don't even need a DBAPI to be available. 42 | 43 | Calls to context.execute() here emit the given string to the 44 | script output. 45 | 46 | """ 47 | url = config.get_main_option("sqlalchemy.url") 48 | context.configure(url=url, target_metadata=target_metadata, literal_binds=True) 49 | 50 | with context.begin_transaction(): 51 | context.run_migrations() 52 | 53 | 54 | def run_migrations_online(): 55 | """Run migrations in 'online' mode. 56 | 57 | In this scenario we need to create an Engine 58 | and associate a connection with the context. 59 | 60 | """ 61 | 62 | # this callback is used to prevent an auto-migration from being generated 63 | # when there are no changes to the schema 64 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 65 | def process_revision_directives(context, revision, directives): 66 | if getattr(config.cmd_opts, "autogenerate", False): 67 | script = directives[0] 68 | if script.upgrade_ops.is_empty(): 69 | directives[:] = [] 70 | logger.info("No changes in schema detected.") 71 | 72 | connectable = current_app.extensions["migrate"].db.get_engine() 73 | 74 | with connectable.connect() as connection: 75 | context.configure( 76 | connection=connection, 77 | target_metadata=target_metadata, 78 | process_revision_directives=process_revision_directives, 79 | **current_app.extensions["migrate"].configure_args 80 | ) 81 | 82 | with context.begin_transaction(): 83 | context.run_migrations() 84 | 85 | 86 | if context.is_offline_mode(): 87 | run_migrations_offline() 88 | else: 89 | run_migrations_online() 90 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/e3226bc25f01_add_user_table.py: -------------------------------------------------------------------------------- 1 | """add user table 2 | 3 | Revision ID: e3226bc25f01 4 | Revises: 5 | Create Date: 2021-07-26 23:55:19.582211 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "e3226bc25f01" 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table( 22 | "user", 23 | sa.Column("id", sa.Integer(), nullable=False), 24 | sa.Column("username", sa.String(length=25), nullable=False), 25 | sa.Column("email", sa.String(length=80), nullable=False), 26 | sa.Column("password", sa.Text(), nullable=False), 27 | sa.PrimaryKeyConstraint("id"), 28 | sa.UniqueConstraint("username"), 29 | ) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table("user") 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from exts import db 2 | 3 | 4 | """ 5 | class Recipe: 6 | id:int primary key 7 | title:str 8 | description:str (text) 9 | """ 10 | 11 | 12 | class Recipe(db.Model): 13 | id = db.Column(db.Integer(), primary_key=True) 14 | title = db.Column(db.String(), nullable=False) 15 | description = db.Column(db.Text(), nullable=False) 16 | 17 | def __repr__(self): 18 | return f"" 19 | 20 | def save(self): 21 | """ 22 | The save function is used to save the changes made to a model instance. 23 | It takes in no arguments and returns nothing. 24 | 25 | :param self: Refer to the current instance of the class 26 | :return: The object that was just saved 27 | :doc-author:jod35 28 | """ 29 | db.session.add(self) 30 | db.session.commit() 31 | 32 | def delete(self): 33 | """ 34 | The delete function is used to delete a specific row in the database. It takes no parameters and returns nothing. 35 | 36 | :param self: Refer to the current instance of the class, and is used to access variables that belongs to the class 37 | :return: Nothing 38 | :doc-author:jod35 39 | """ 40 | db.session.delete(self) 41 | db.session.commit() 42 | 43 | def update(self, title, description): 44 | """ 45 | The update function updates the title and description of a given blog post. 46 | It takes two parameters, title and description. 47 | 48 | :param self: Access variables that belongs to the class 49 | :param title: Update the title of the post 50 | :param description: Update the description of the blog post 51 | :return: A dictionary with the updated values of title and description 52 | :doc-author:jod35 53 | """ 54 | self.title = title 55 | self.description = description 56 | 57 | db.session.commit() 58 | 59 | 60 | # user model 61 | 62 | """ 63 | class User: 64 | id:integer 65 | username:string 66 | email:string 67 | password:string 68 | """ 69 | 70 | 71 | class User(db.Model): 72 | id = db.Column(db.Integer, primary_key=True) 73 | username = db.Column(db.String(25), nullable=False, unique=True) 74 | email = db.Column(db.String(80), nullable=False) 75 | password = db.Column(db.Text(), nullable=False) 76 | 77 | def __repr__(self): 78 | """ 79 | returns string rep of object 80 | 81 | """ 82 | return f"" 83 | 84 | def save(self): 85 | db.session.add(self) 86 | db.session.commit() 87 | -------------------------------------------------------------------------------- /prod.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jod35/Flask-React-Recipes/574e600c507e6451e0e0147f601a89a599eab17d/prod.db -------------------------------------------------------------------------------- /recipes.py: -------------------------------------------------------------------------------- 1 | from flask_restx import Namespace, Resource, fields 2 | from models import Recipe 3 | from flask_jwt_extended import jwt_required 4 | from flask import request 5 | 6 | 7 | recipe_ns = Namespace("recipe", description="A namespace for Recipes") 8 | 9 | 10 | recipe_model = recipe_ns.model( 11 | "Recipe", 12 | {"id": fields.Integer(), "title": fields.String(), "description": fields.String()}, 13 | ) 14 | 15 | 16 | @recipe_ns.route("/hello") 17 | class HelloResource(Resource): 18 | def get(self): 19 | return {"message": "Hello World"} 20 | 21 | 22 | @recipe_ns.route("/recipes") 23 | class RecipesResource(Resource): 24 | @recipe_ns.marshal_list_with(recipe_model) 25 | def get(self): 26 | """Get all recipes""" 27 | 28 | recipes = Recipe.query.all() 29 | 30 | return recipes 31 | 32 | @recipe_ns.marshal_with(recipe_model) 33 | @recipe_ns.expect(recipe_model) 34 | @jwt_required() 35 | def post(self): 36 | """Create a new recipe""" 37 | 38 | data = request.get_json() 39 | 40 | new_recipe = Recipe( 41 | title=data.get("title"), description=data.get("description") 42 | ) 43 | 44 | new_recipe.save() 45 | 46 | return new_recipe, 201 47 | 48 | 49 | @recipe_ns.route("/recipe/") 50 | class RecipeResource(Resource): 51 | @recipe_ns.marshal_with(recipe_model) 52 | def get(self, id): 53 | """Get a recipe by id""" 54 | recipe = Recipe.query.get_or_404(id) 55 | 56 | return recipe 57 | 58 | @recipe_ns.marshal_with(recipe_model) 59 | @jwt_required() 60 | def put(self, id): 61 | """Update a recipe by id""" 62 | 63 | recipe_to_update = Recipe.query.get_or_404(id) 64 | 65 | data = request.get_json() 66 | 67 | recipe_to_update.update(data.get("title"), data.get("description")) 68 | 69 | return recipe_to_update 70 | 71 | @recipe_ns.marshal_with(recipe_model) 72 | @jwt_required() 73 | def delete(self, id): 74 | """Delete a recipe by id""" 75 | 76 | recipe_to_delete = Recipe.query.get_or_404(id) 77 | 78 | recipe_to_delete.delete() 79 | 80 | return recipe_to_delete 81 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.6.5 2 | aniso8601==9.0.1 3 | attrs==21.2.0 4 | black==22.12.0 5 | click==8.0.1 6 | colorama==0.4.4 7 | Flask==2.0.1 8 | Flask-Cors==3.0.10 9 | Flask-JWT-Extended==4.2.3 10 | Flask-Migrate==3.1.0 11 | flask-restx==0.5.0 12 | Flask-SQLAlchemy==2.5.1 13 | greenlet==2.0.1 14 | gunicorn==20.1.0 15 | iniconfig==2.0.0 16 | itsdangerous==2.0.1 17 | Jinja2==3.0.1 18 | jsonschema==3.2.0 19 | Mako==1.1.4 20 | MarkupSafe==2.0.1 21 | mypy-extensions==0.4.3 22 | packaging==23.0 23 | pathspec==0.10.3 24 | platformdirs==2.6.2 25 | pluggy==1.0.0 26 | PyJWT==2.1.0 27 | pyrsistent==0.18.0 28 | pytest==7.2.1 29 | python-dateutil==2.8.2 30 | python-decouple==3.4 31 | python-dotenv==0.21.0 32 | python-editor==1.0.4 33 | pytz==2021.1 34 | six==1.16.0 35 | SQLAlchemy==1.4.22 36 | Werkzeug==2.0.1 37 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from main import create_app 2 | from config import DevConfig, ProdConfig 3 | 4 | app = create_app(ProdConfig) 5 | 6 | #run with 7 | if __name__ == "__main__": 8 | app.run() -------------------------------------------------------------------------------- /test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jod35/Flask-React-Recipes/574e600c507e6451e0e0147f601a89a599eab17d/test.db -------------------------------------------------------------------------------- /test_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from main import create_app 3 | from config import TestConfig 4 | from exts import db 5 | 6 | 7 | class APITestCase(unittest.TestCase): 8 | def setUp(self): 9 | self.app = create_app(TestConfig) 10 | 11 | self.client = self.app.test_client(self) 12 | 13 | with self.app.app_context(): 14 | db.init_app(self.app) 15 | 16 | db.create_all() 17 | 18 | def test_hello_world(self): 19 | hello_response = self.client.get("/recipe/hello") 20 | 21 | json = hello_response.json 22 | 23 | # print(json) 24 | self.assertEqual(json, {"message": "Hello World"}) 25 | 26 | def test_signup(self): 27 | signup_response = self.client.post( 28 | "/auth/signup", 29 | json={ 30 | "username": "testuser", 31 | "email": "testuser@test.com", 32 | "password": "password", 33 | }, 34 | ) 35 | 36 | status_code = signup_response.status_code 37 | 38 | self.assertEqual(status_code, 201) 39 | 40 | def test_login(self): 41 | signup_response = self.client.post( 42 | "/auth/signup", 43 | json={ 44 | "username": "testuser", 45 | "email": "testuser@test.com", 46 | "password": "password", 47 | }, 48 | ) 49 | 50 | login_response = self.client.post( 51 | "auth/login", json={"username": "testuser", "password": "password"} 52 | ) 53 | 54 | status_code = login_response.status_code 55 | 56 | json = login_response.json 57 | 58 | # print(json) 59 | 60 | self.assertEqual(status_code, 200) 61 | 62 | def test_get_all_recipes(self): 63 | """TEST GETTING ALL RECIPES""" 64 | response = self.client.get("/recipe/recipes") 65 | 66 | # print(response.json) 67 | 68 | status_code = response.status_code 69 | 70 | self.assertEqual(status_code, 200) 71 | 72 | def test_get_one_recipe(self): 73 | id = 1 74 | response = self.client.get(f"/recipe/recipe/{id}") 75 | 76 | status_code = response.status_code 77 | # print(status_code) 78 | 79 | self.assertEqual(status_code, 404) 80 | 81 | def test_create_recipe(self): 82 | signup_response = self.client.post( 83 | "/auth/signup", 84 | json={ 85 | "username": "testuser", 86 | "email": "testuser@test.com", 87 | "password": "password", 88 | }, 89 | ) 90 | 91 | login_response = self.client.post( 92 | "auth/login", json={"username": "testuser", "password": "password"} 93 | ) 94 | 95 | access_token = login_response.json["access_token"] 96 | 97 | create_recipe_response = self.client.post( 98 | "/recipe/recipes", 99 | json={"title": "Test Cookie", "description": "Test description"}, 100 | headers={"Authorization": f"Bearer {access_token}"}, 101 | ) 102 | 103 | status_code = create_recipe_response.status_code 104 | 105 | # print(create_recipe_response.json) 106 | 107 | self.assertEqual(status_code, 201) 108 | 109 | def test_update_recipe(self): 110 | signup_response = self.client.post( 111 | "/auth/signup", 112 | json={ 113 | "username": "testuser", 114 | "email": "testuser@test.com", 115 | "password": "password", 116 | }, 117 | ) 118 | 119 | login_response = self.client.post( 120 | "auth/login", json={"username": "testuser", "password": "password"} 121 | ) 122 | 123 | access_token = login_response.json["access_token"] 124 | 125 | create_recipe_response = self.client.post( 126 | "/recipe/recipes", 127 | json={"title": "Test Cookie", "description": "Test description"}, 128 | headers={"Authorization": f"Bearer {access_token}"}, 129 | ) 130 | 131 | status_code = create_recipe_response.status_code 132 | 133 | id = 1 134 | 135 | update_response = self.client.put( 136 | f"recipe/recipe/{id}", 137 | json={ 138 | "title": "Test Cookie Updated", 139 | "description": "Test description updated", 140 | }, 141 | headers={"Authorization": f"Bearer {access_token}"}, 142 | ) 143 | 144 | status_code = update_response.status_code 145 | self.assertEqual(status_code, 200) 146 | 147 | def test_delete_recipe(self): 148 | signup_response = self.client.post( 149 | "/auth/signup", 150 | json={ 151 | "username": "testuser", 152 | "email": "testuser@test.com", 153 | "password": "password", 154 | }, 155 | ) 156 | 157 | login_response = self.client.post( 158 | "auth/login", json={"username": "testuser", "password": "password"} 159 | ) 160 | 161 | access_token = login_response.json["access_token"] 162 | 163 | create_recipe_response = self.client.post( 164 | "/recipe/recipes", 165 | json={"title": "Test Cookie", "description": "Test description"}, 166 | headers={"Authorization": f"Bearer {access_token}"}, 167 | ) 168 | 169 | id = 1 170 | delete_response = self.client.delete( 171 | f"/recipe/recipe/{id}", headers={"Authorization": f"Bearer {access_token}"} 172 | ) 173 | 174 | status_code = delete_response.status_code 175 | 176 | print(delete_response.json) 177 | 178 | self.assertEqual(status_code, 200) 179 | 180 | def tearDown(self): 181 | with self.app.app_context(): 182 | db.session.remove() 183 | db.drop_all() 184 | 185 | 186 | if __name__ == "__main__": 187 | unittest.main() 188 | --------------------------------------------------------------------------------