├── .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 You need to enable JavaScript to run this 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
Recipes\r\n
\r\n \r\n \r\n
\r\n
\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 Update \r\n {' '}\r\n Delete \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 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 Save\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 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 SignUp \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 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 Login \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 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 Save\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 | You need to enable JavaScript to run this app.
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 |
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 |
65 | Save
66 |
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 |
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 |
150 | Save
151 |
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 |
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 | Login
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 |
48 |
49 |
Recipes
50 |
51 |
52 |
53 |
58 |
59 |
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 | Update
12 | {' '}
13 | Delete
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 |
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 | SignUp
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 |
--------------------------------------------------------------------------------