├── .dockerignore ├── .env.example ├── .flaskenv ├── .gitignore ├── .vim └── coc-settings.json ├── .vscode └── settings.json ├── Dockerfile ├── Pipfile ├── Pipfile.lock ├── README.md ├── app ├── __init__.py ├── api │ ├── auth_routes.py │ └── user_routes.py ├── config.py ├── forms │ ├── __init__.py │ ├── login_form.py │ └── signup_form.py ├── models │ ├── __init__.py │ ├── db.py │ └── user.py └── seeds │ ├── __init__.py │ └── users.py ├── dev-requirements.txt ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ └── 20201120_150602_create_users_table.py ├── react-app ├── .gitignore ├── README.md ├── craco.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.js │ ├── components │ │ ├── LoginForm │ │ │ ├── LoginForm.js │ │ │ └── index.js │ │ ├── LogoutButton │ │ │ ├── LogoutButton.js │ │ │ └── index.js │ │ ├── NavBar │ │ │ ├── NavBar.js │ │ │ └── index.js │ │ ├── ProtectedRoute │ │ │ ├── ProtectedRoute.js │ │ │ └── index.js │ │ └── SignUpForm │ │ │ ├── SignUpForm.js │ │ │ └── index.js │ ├── context │ │ ├── Modal.css │ │ └── Modal.js │ ├── index.css │ ├── index.js │ └── store │ │ ├── index.js │ │ └── session.js └── tailwind.config.js └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | react-app/node_modules 2 | .venv 3 | Pipfile 4 | Pipfile.lock 5 | .env 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | FLASK_APP=app 2 | FLASK_ENV=development 3 | SECRET_KEY=lkasjdf09ajsdkfljalsiorj12n3490re9485309irefvn,u90818734902139489230 4 | DATABASE_URL=postgresql://starter_app_dev:password@localhost/starter_app 5 | -------------------------------------------------------------------------------- /.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_APP=app -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | __pycache__/ 3 | *.py[cod] 4 | .venv 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.vim/coc-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/Users/petermai/Documents/FlaskReact/.venv/bin/python" 3 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/home/echo/dev/appacademy/starters/python-project-starter/.venv/bin/python", 3 | "python.linting.pylintEnabled": false, 4 | "python.linting.enabled": true, 5 | "python.linting.pycodestyleEnabled": true 6 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 AS build-stage 2 | 3 | WORKDIR /react-app 4 | COPY react-app/. . 5 | 6 | # You have to set this because it should be set during build time. 7 | ENV REACT_APP_BASE_URL= 8 | 9 | # Build our React App 10 | RUN npm install 11 | RUN npm run build 12 | 13 | FROM python:3.8 14 | 15 | # Setup Flask environment 16 | ENV FLASK_APP=app 17 | ENV FLASK_ENV=production 18 | ENV SQLALCHEMY_ECHO=True 19 | 20 | EXPOSE 8000 21 | 22 | WORKDIR /var/www 23 | COPY . . 24 | COPY --from=build-stage /react-app/build/* app/static/ 25 | 26 | # Install Python Dependencies 27 | RUN pip install -r requirements.txt 28 | RUN pip install psycopg2 29 | 30 | # Run flask environment 31 | CMD gunicorn app:app 32 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | click = "==7.1.2" 8 | gunicorn = "==20.0.4" 9 | itsdangerous = "==1.1.0" 10 | python-dotenv = "==0.14.0" 11 | six = "==1.15.0" 12 | Flask = "==1.1.2" 13 | Flask-Cors = "==3.0.8" 14 | Flask-SQLAlchemy = "==2.4.4" 15 | Flask-WTF = "==0.14.3" 16 | Jinja2 = "==2.11.2" 17 | MarkupSafe = "==1.1.1" 18 | SQLAlchemy = "==1.3.19" 19 | Werkzeug = "==1.0.1" 20 | WTForms = "==2.3.3" 21 | Flask-JWT-Extended = "==3.24.1" 22 | email-validator = "*" 23 | Flask-Migrate = "==2.5.3" 24 | Flask-Login = "==0.5.0" 25 | alembic = "==1.4.3" 26 | python-dateutil = "==2.8.1" 27 | python-editor = "==1.0.4" 28 | Mako = "==1.1.3" 29 | PyJWT = "==1.7.1" 30 | 31 | [dev-packages] 32 | psycopg2-binary = "==2.8.6" 33 | autopep8 = "*" 34 | pylint = "*" 35 | 36 | [requires] 37 | python_version = "3.8" 38 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "5c8e25a8a80e32d2d92baa84dd45978ac903380fdb4af3e0c53573583bbbc52d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 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:4e02ed2aa796bd179965041afa092c55b51fb077de19d61835673cc80672c01c", 22 | "sha256:5334f32314fb2a56d86b4c4dd1ae34b08c03cae4cb888bc699942104d66bc245" 23 | ], 24 | "index": "pypi", 25 | "version": "==1.4.3" 26 | }, 27 | "click": { 28 | "hashes": [ 29 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 30 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 31 | ], 32 | "index": "pypi", 33 | "version": "==7.1.2" 34 | }, 35 | "dnspython": { 36 | "hashes": [ 37 | "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216", 38 | "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4" 39 | ], 40 | "markers": "python_version >= '3.6'", 41 | "version": "==2.1.0" 42 | }, 43 | "email-validator": { 44 | "hashes": [ 45 | "sha256:094b1d1c60d790649989d38d34f69e1ef07792366277a2cf88684d03495d018f", 46 | "sha256:1a13bd6050d1db4475f13e444e169b6fe872434922d38968c67cea9568cce2f0" 47 | ], 48 | "index": "pypi", 49 | "version": "==1.1.2" 50 | }, 51 | "flask": { 52 | "hashes": [ 53 | "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", 54 | "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" 55 | ], 56 | "index": "pypi", 57 | "version": "==1.1.2" 58 | }, 59 | "flask-cors": { 60 | "hashes": [ 61 | "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16", 62 | "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a" 63 | ], 64 | "index": "pypi", 65 | "version": "==3.0.8" 66 | }, 67 | "flask-jwt-extended": { 68 | "hashes": [ 69 | "sha256:0aa8ee6fa7eb3be9314e39dd199ac8e19389a95371f9d54e155c7aa635e319dd" 70 | ], 71 | "index": "pypi", 72 | "version": "==3.24.1" 73 | }, 74 | "flask-login": { 75 | "hashes": [ 76 | "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b", 77 | "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0" 78 | ], 79 | "index": "pypi", 80 | "version": "==0.5.0" 81 | }, 82 | "flask-migrate": { 83 | "hashes": [ 84 | "sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732", 85 | "sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee" 86 | ], 87 | "index": "pypi", 88 | "version": "==2.5.3" 89 | }, 90 | "flask-sqlalchemy": { 91 | "hashes": [ 92 | "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e", 93 | "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5" 94 | ], 95 | "index": "pypi", 96 | "version": "==2.4.4" 97 | }, 98 | "flask-wtf": { 99 | "hashes": [ 100 | "sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2", 101 | "sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720" 102 | ], 103 | "index": "pypi", 104 | "version": "==0.14.3" 105 | }, 106 | "gunicorn": { 107 | "hashes": [ 108 | "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", 109 | "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" 110 | ], 111 | "index": "pypi", 112 | "version": "==20.0.4" 113 | }, 114 | "idna": { 115 | "hashes": [ 116 | "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", 117 | "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" 118 | ], 119 | "markers": "python_version >= '3.4'", 120 | "version": "==3.1" 121 | }, 122 | "itsdangerous": { 123 | "hashes": [ 124 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 125 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 126 | ], 127 | "index": "pypi", 128 | "version": "==1.1.0" 129 | }, 130 | "jinja2": { 131 | "hashes": [ 132 | "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", 133 | "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" 134 | ], 135 | "index": "pypi", 136 | "version": "==2.11.2" 137 | }, 138 | "mako": { 139 | "hashes": [ 140 | "sha256:8195c8c1400ceb53496064314c6736719c6f25e7479cd24c77be3d9361cddc27", 141 | "sha256:93729a258e4ff0747c876bd9e20df1b9758028946e976324ccd2d68245c7b6a9" 142 | ], 143 | "index": "pypi", 144 | "version": "==1.1.3" 145 | }, 146 | "markupsafe": { 147 | "hashes": [ 148 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 149 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 150 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 151 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 152 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 153 | "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", 154 | "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", 155 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 156 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 157 | "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", 158 | "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", 159 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 160 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 161 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 162 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 163 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 164 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 165 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 166 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 167 | "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", 168 | "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", 169 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 170 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 171 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 172 | "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", 173 | "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", 174 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 175 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 176 | "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", 177 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 178 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 179 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 180 | "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", 181 | "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", 182 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 183 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 184 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 185 | "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", 186 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 187 | "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", 188 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 189 | "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", 190 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 191 | "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", 192 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 193 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 194 | "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", 195 | "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", 196 | "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", 197 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 198 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", 199 | "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" 200 | ], 201 | "index": "pypi", 202 | "version": "==1.1.1" 203 | }, 204 | "pyjwt": { 205 | "hashes": [ 206 | "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", 207 | "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" 208 | ], 209 | "index": "pypi", 210 | "version": "==1.7.1" 211 | }, 212 | "python-dateutil": { 213 | "hashes": [ 214 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", 215 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" 216 | ], 217 | "index": "pypi", 218 | "version": "==2.8.1" 219 | }, 220 | "python-dotenv": { 221 | "hashes": [ 222 | "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d", 223 | "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423" 224 | ], 225 | "index": "pypi", 226 | "version": "==0.14.0" 227 | }, 228 | "python-editor": { 229 | "hashes": [ 230 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", 231 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", 232 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", 233 | "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", 234 | "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522" 235 | ], 236 | "index": "pypi", 237 | "version": "==1.0.4" 238 | }, 239 | "six": { 240 | "hashes": [ 241 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 242 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 243 | ], 244 | "index": "pypi", 245 | "version": "==1.15.0" 246 | }, 247 | "sqlalchemy": { 248 | "hashes": [ 249 | "sha256:072766c3bd09294d716b2d114d46ffc5ccf8ea0b714a4e1c48253014b771c6bb", 250 | "sha256:107d4af989831d7b091e382d192955679ec07a9209996bf8090f1f539ffc5804", 251 | "sha256:15c0bcd3c14f4086701c33a9e87e2c7ceb3bcb4a246cd88ec54a49cf2a5bd1a6", 252 | "sha256:26c5ca9d09f0e21b8671a32f7d83caad5be1f6ff45eef5ec2f6fd0db85fc5dc0", 253 | "sha256:276936d41111a501cf4a1a0543e25449108d87e9f8c94714f7660eaea89ae5fe", 254 | "sha256:3292a28344922415f939ee7f4fc0c186f3d5a0bf02192ceabd4f1129d71b08de", 255 | "sha256:33d29ae8f1dc7c75b191bb6833f55a19c932514b9b5ce8c3ab9bc3047da5db36", 256 | "sha256:3bba2e9fbedb0511769780fe1d63007081008c5c2d7d715e91858c94dbaa260e", 257 | "sha256:465c999ef30b1c7525f81330184121521418a67189053bcf585824d833c05b66", 258 | "sha256:51064ee7938526bab92acd049d41a1dc797422256086b39c08bafeffb9d304c6", 259 | "sha256:5a49e8473b1ab1228302ed27365ea0fadd4bf44bc0f9e73fe38e10fdd3d6b4fc", 260 | "sha256:618db68745682f64cedc96ca93707805d1f3a031747b5a0d8e150cfd5055ae4d", 261 | "sha256:6547b27698b5b3bbfc5210233bd9523de849b2bb8a0329cd754c9308fc8a05ce", 262 | "sha256:6557af9e0d23f46b8cd56f8af08eaac72d2e3c632ac8d5cf4e20215a8dca7cea", 263 | "sha256:73a40d4fcd35fdedce07b5885905753d5d4edf413fbe53544dd871f27d48bd4f", 264 | "sha256:8280f9dae4adb5889ce0bb3ec6a541bf05434db5f9ab7673078c00713d148365", 265 | "sha256:83469ad15262402b0e0974e612546bc0b05f379b5aa9072ebf66d0f8fef16bea", 266 | "sha256:860d0fe234922fd5552b7f807fbb039e3e7ca58c18c8d38aa0d0a95ddf4f6c23", 267 | "sha256:883c9fb62cebd1e7126dd683222b3b919657590c3e2db33bdc50ebbad53e0338", 268 | "sha256:8afcb6f4064d234a43fea108859942d9795c4060ed0fbd9082b0f280181a15c1", 269 | "sha256:96f51489ac187f4bab588cf51f9ff2d40b6d170ac9a4270ffaed535c8404256b", 270 | "sha256:9e865835e36dfbb1873b65e722ea627c096c11b05f796831e3a9b542926e979e", 271 | "sha256:aa0554495fe06172b550098909be8db79b5accdf6ffb59611900bea345df5eba", 272 | "sha256:b595e71c51657f9ee3235db8b53d0b57c09eee74dfb5b77edff0e46d2218dc02", 273 | "sha256:b6ff91356354b7ff3bd208adcf875056d3d886ed7cef90c571aef2ab8a554b12", 274 | "sha256:b70bad2f1a5bd3460746c3fb3ab69e4e0eb5f59d977a23f9b66e5bdc74d97b86", 275 | "sha256:c7adb1f69a80573698c2def5ead584138ca00fff4ad9785a4b0b2bf927ba308d", 276 | "sha256:c898b3ebcc9eae7b36bd0b4bbbafce2d8076680f6868bcbacee2d39a7a9726a7", 277 | "sha256:e49947d583fe4d29af528677e4f0aa21f5e535ca2ae69c48270ebebd0d8843c0", 278 | "sha256:eb1d71643e4154398b02e88a42fc8b29db8c44ce4134cf0f4474bfc5cb5d4dac", 279 | "sha256:f2e8a9c0c8813a468aa659a01af6592f71cd30237ec27c4cc0683f089f90dcfc", 280 | "sha256:fe7fe11019fc3e6600819775a7d55abc5446dda07e9795f5954fdbf8a49e1c37" 281 | ], 282 | "index": "pypi", 283 | "version": "==1.3.19" 284 | }, 285 | "werkzeug": { 286 | "hashes": [ 287 | "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", 288 | "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" 289 | ], 290 | "index": "pypi", 291 | "version": "==1.0.1" 292 | }, 293 | "wtforms": { 294 | "hashes": [ 295 | "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c", 296 | "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c" 297 | ], 298 | "index": "pypi", 299 | "version": "==2.3.3" 300 | } 301 | }, 302 | "develop": { 303 | "astroid": { 304 | "hashes": [ 305 | "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", 306 | "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" 307 | ], 308 | "markers": "python_version >= '3.5'", 309 | "version": "==2.4.2" 310 | }, 311 | "autopep8": { 312 | "hashes": [ 313 | "sha256:9e136c472c475f4ee4978b51a88a494bfcd4e3ed17950a44a988d9e434837bea", 314 | "sha256:cae4bc0fb616408191af41d062d7ec7ef8679c7f27b068875ca3a9e2878d5443" 315 | ], 316 | "index": "pypi", 317 | "version": "==1.5.5" 318 | }, 319 | "isort": { 320 | "hashes": [ 321 | "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e", 322 | "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc" 323 | ], 324 | "markers": "python_version >= '3.6' and python_version < '4.0'", 325 | "version": "==5.7.0" 326 | }, 327 | "lazy-object-proxy": { 328 | "hashes": [ 329 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", 330 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", 331 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", 332 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", 333 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", 334 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", 335 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", 336 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", 337 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", 338 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", 339 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", 340 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", 341 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", 342 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", 343 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", 344 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", 345 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", 346 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", 347 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", 348 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", 349 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" 350 | ], 351 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 352 | "version": "==1.4.3" 353 | }, 354 | "mccabe": { 355 | "hashes": [ 356 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 357 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 358 | ], 359 | "version": "==0.6.1" 360 | }, 361 | "psycopg2-binary": { 362 | "hashes": [ 363 | "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", 364 | "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", 365 | "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", 366 | "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", 367 | "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", 368 | "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", 369 | "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", 370 | "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", 371 | "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", 372 | "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", 373 | "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", 374 | "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", 375 | "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", 376 | "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", 377 | "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", 378 | "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", 379 | "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", 380 | "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", 381 | "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", 382 | "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", 383 | "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", 384 | "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", 385 | "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", 386 | "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", 387 | "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", 388 | "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", 389 | "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", 390 | "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", 391 | "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", 392 | "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", 393 | "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", 394 | "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", 395 | "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", 396 | "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", 397 | "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" 398 | ], 399 | "index": "pypi", 400 | "version": "==2.8.6" 401 | }, 402 | "pycodestyle": { 403 | "hashes": [ 404 | "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", 405 | "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" 406 | ], 407 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 408 | "version": "==2.6.0" 409 | }, 410 | "pylint": { 411 | "hashes": [ 412 | "sha256:718b74786ea7ed07aa0c58bf572154d4679f960d26e9641cc1de204a30b87fc9", 413 | "sha256:e71c2e9614a4f06e36498f310027942b0f4f2fde20aebb01655b31edc63b9eaf" 414 | ], 415 | "index": "pypi", 416 | "version": "==2.6.2" 417 | }, 418 | "six": { 419 | "hashes": [ 420 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 421 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 422 | ], 423 | "index": "pypi", 424 | "version": "==1.15.0" 425 | }, 426 | "toml": { 427 | "hashes": [ 428 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 429 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 430 | ], 431 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 432 | "version": "==0.10.2" 433 | }, 434 | "wrapt": { 435 | "hashes": [ 436 | "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" 437 | ], 438 | "version": "==1.12.1" 439 | } 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask React Project 2 | 3 | This is the starter for a Flask React project. 4 | 5 | ## Getting started 6 | 7 | 1. Clone this repository (only this branch) 8 | 9 | ```bash 10 | git clone https://github.com/Lazytangent/flask-react-template.git 11 | ``` 12 | 13 | 2. Install dependencies 14 | 15 | ```bash 16 | pipenv install --dev -r dev-requirements.txt && pipenv install -r requirements.txt 17 | ``` 18 | 19 | 3. Create a **.env** file based on the example with proper settings for your 20 | development environment 21 | 22 | ```bash 23 | cp .env.example .env 24 | ``` 25 | 4. Setup your PostgreSQL user, password and database and make sure it matches your **.env** file 26 | 27 | ```bash 28 | psql 29 | ``` 30 | 31 | ```sql 32 | CREATE USER starter_app_dev WITH PASSWORD 'password'; 33 | CREATE DATABASE starter_app WITH OWNER starter_app_dev; 34 | ``` 35 | 36 | 5. Get into your pipenv, migrate your database, seed your database, and run your flask app 37 | 38 | ```bash 39 | pipenv shell 40 | ``` 41 | 42 | ```bash 43 | flask db upgrade 44 | ``` 45 | 46 | ```bash 47 | flask seed all 48 | ``` 49 | 50 | ```bash 51 | flask run 52 | ``` 53 | 54 | 6. To run the React App in development, checkout the [README](./react-app/README.md) inside the `react-app` directory. 55 | 56 | *** 57 | *IMPORTANT!* 58 | If you add any python dependencies to your pipfiles, you'll need to regenerate your requirements.txt before deployment. 59 | You can do this by running: 60 | 61 | ```bash 62 | pipenv lock -r > requirements.txt 63 | ``` 64 | 65 | *ALSO IMPORTANT!* 66 | psycopg2-binary MUST remain a dev dependency because you can't install it on apline-linux. 67 | There is a layer in the Dockerfile that will install psycopg2 (not binary) for us. 68 | *** 69 | 70 | ## Deploy to Heroku 71 | 72 | 1. Create a new project on Heroku 73 | 2. Under Resources click "Find more add-ons" and add the add on called "Heroku Postgres" 74 | 3. Install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-command-line) 75 | 4. Run 76 | 77 | ```bash 78 | heroku login 79 | ``` 80 | 81 | 5. Login to the heroku container registry 82 | 83 | ```bash 84 | heroku container:login 85 | ``` 86 | 87 | 6. Connect your local app to the Heroku app (this will make the commands a little shorter later) 88 | 89 | ```bash 90 | heroku config:set -a {NAME_OF_HEROKU_APP} 91 | ``` 92 | 93 | 7. Update the `REACT_APP_BASE_URL` variable in the Dockerfile. 94 | This should be the full URL of your Heroku app: i.e. "https://flask-react-aa.herokuapp.com" 95 | 8. Push your docker container to heroku from the root directory of your project. 96 | This will build the dockerfile and push the image to your heroku container registry 97 | 98 | ```bash 99 | heroku container:push web 100 | ``` 101 | 102 | 9. Release your docker container to heroku 103 | 104 | ```bash 105 | heroku container:release web 106 | ``` 107 | 108 | 10. set up your database: 109 | 110 | ```bash 111 | heroku run flask db upgrade 112 | heroku run flask seed all 113 | ``` 114 | 115 | 11. Under Settings find "Config Vars" and add any additional/secret .env variables. 116 | 117 | 12. profit 118 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, render_template, request, session, redirect 3 | from flask_cors import CORS 4 | from flask_migrate import Migrate 5 | from flask_wtf.csrf import CSRFProtect, generate_csrf 6 | from flask_login import LoginManager 7 | 8 | from .models import db, User 9 | from .api.user_routes import user_routes 10 | from .api.auth_routes import auth_routes 11 | 12 | from .seeds import seed_commands 13 | 14 | from .config import Config 15 | 16 | app = Flask(__name__) 17 | 18 | # Setup login manager 19 | login = LoginManager(app) 20 | login.login_view = 'auth.unauthorized' 21 | 22 | 23 | @login.user_loader 24 | def load_user(id): 25 | return User.query.get(int(id)) 26 | 27 | 28 | # Tell flask about our seed commands 29 | app.cli.add_command(seed_commands) 30 | 31 | app.config.from_object(Config) 32 | app.register_blueprint(user_routes, url_prefix='/api/users') 33 | app.register_blueprint(auth_routes, url_prefix='/api/auth') 34 | db.init_app(app) 35 | Migrate(app, db) 36 | 37 | # Application Security 38 | CORS(app) 39 | 40 | # Since we are deploying with Docker and Flask, 41 | # we won't be using a buildpack when we deploy to Heroku. 42 | # Therefore, we need to make sure that in production any 43 | # request made over http is redirected to https. 44 | # Well......... 45 | 46 | 47 | @app.before_request 48 | def https_redirect(): 49 | if os.environ.get('FLASK_ENV') == 'production': 50 | if request.headers.get('X-Forwarded-Proto') == 'http': 51 | url = request.url.replace('http://', 'https://', 1) 52 | code = 301 53 | return redirect(url, code=code) 54 | 55 | 56 | @app.after_request 57 | def inject_csrf_token(response): 58 | response.set_cookie( 59 | 'csrf_token', 60 | generate_csrf(), 61 | secure=True if os.environ.get('FLASK_ENV') == 'production' else False, 62 | samesite='Strict' 63 | if os.environ.get('FLASK_ENV') == 'production' else None, 64 | httponly=True) 65 | return response 66 | 67 | 68 | @app.route('/', defaults={'path': ''}) 69 | @app.route('/') 70 | def react_root(path): 71 | print("path", path) 72 | if path == 'favicon.ico': 73 | return app.send_static_file('favicon.ico') 74 | return app.send_static_file('index.html') 75 | -------------------------------------------------------------------------------- /app/api/auth_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify, session, request 2 | from app.models import User, db 3 | from app.forms import LoginForm 4 | from app.forms import SignUpForm 5 | from flask_login import current_user, login_user, logout_user, login_required 6 | 7 | auth_routes = Blueprint('auth', __name__) 8 | 9 | 10 | def validation_errors_to_error_messages(validation_errors): 11 | """ 12 | Simple function that turns the WTForms validation errors into a simple list 13 | """ 14 | errorMessages = [] 15 | for field in validation_errors: 16 | for error in validation_errors[field]: 17 | errorMessages.append(f"{field} : {error}") 18 | return errorMessages 19 | 20 | 21 | @auth_routes.route('/') 22 | def authenticate(): 23 | """ 24 | Authenticates a user. 25 | """ 26 | if current_user.is_authenticated: 27 | return current_user.to_dict() 28 | return {'errors': ['Unauthorized']}, 401 29 | 30 | 31 | @auth_routes.route('/login', methods=['POST']) 32 | def login(): 33 | """ 34 | Logs a user in 35 | """ 36 | form = LoginForm() 37 | print(request.get_json()) 38 | # Get the csrf_token from the request cookie and put it into the 39 | # form manually to validate_on_submit can be used 40 | form['csrf_token'].data = request.cookies['csrf_token'] 41 | if form.validate_on_submit(): 42 | # Add the user to the session, we are logged in! 43 | user = User.query.filter(User.email == form.data['email']).first() 44 | login_user(user) 45 | return user.to_dict() 46 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401 47 | 48 | 49 | @auth_routes.route('/logout') 50 | def logout(): 51 | """ 52 | Logs a user out 53 | """ 54 | logout_user() 55 | return {'message': 'User logged out'} 56 | 57 | 58 | @auth_routes.route('/signup', methods=['POST']) 59 | def sign_up(): 60 | """ 61 | Creates a new user and logs them in 62 | """ 63 | form = SignUpForm() 64 | form['csrf_token'].data = request.cookies['csrf_token'] 65 | if form.validate_on_submit(): 66 | user = User( 67 | username=form.data['username'], 68 | email=form.data['email'], 69 | password=form.data['password'] 70 | ) 71 | db.session.add(user) 72 | db.session.commit() 73 | login_user(user) 74 | return user.to_dict() 75 | return {'errors': validation_errors_to_error_messages(form.errors)} 76 | 77 | 78 | @auth_routes.route('/unauthorized') 79 | def unauthorized(): 80 | """ 81 | Returns unauthorized JSON when flask-login authentication fails 82 | """ 83 | return {'errors': ['Unauthorized']}, 401 84 | -------------------------------------------------------------------------------- /app/api/user_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, jsonify 2 | from flask_login import login_required 3 | from app.models import User 4 | 5 | user_routes = Blueprint('users', __name__) 6 | 7 | 8 | @user_routes.route('/') 9 | @login_required 10 | def users(): 11 | users = User.query.all() 12 | return {"users": [user.to_dict() for user in users]} 13 | 14 | 15 | @user_routes.route('/') 16 | @login_required 17 | def user(id): 18 | user = User.query.get(id) 19 | return user.to_dict() 20 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Config: 5 | SECRET_KEY = os.environ.get('SECRET_KEY') 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') 8 | SQLALCHEMY_ECHO = True 9 | -------------------------------------------------------------------------------- /app/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from .login_form import LoginForm 2 | from .signup_form import SignUpForm -------------------------------------------------------------------------------- /app/forms/login_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField 3 | from wtforms.validators import DataRequired, Email, ValidationError 4 | from app.models import User 5 | 6 | 7 | def user_exists(form, field): 8 | print("Checking if user exists", field.data) 9 | email = field.data 10 | user = User.query.filter(User.email == email).first() 11 | if not user: 12 | raise ValidationError("Email provided not found.") 13 | 14 | 15 | def password_matches(form, field): 16 | print("Checking if password matches") 17 | password = field.data 18 | email = form.data['email'] 19 | user = User.query.filter(User.email == email).first() 20 | if not user: 21 | raise ValidationError("No such user exists.") 22 | if not user.check_password(password): 23 | raise ValidationError("Password was incorrect.") 24 | 25 | 26 | class LoginForm(FlaskForm): 27 | email = StringField('email', validators=[DataRequired(), user_exists]) 28 | password = StringField('password', validators=[ 29 | DataRequired(), password_matches]) 30 | -------------------------------------------------------------------------------- /app/forms/signup_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField 3 | from wtforms.validators import DataRequired, Email, ValidationError 4 | from app.models import User 5 | 6 | 7 | def user_exists(form, field): 8 | print("Checking if user exits", field.data) 9 | email = field.data 10 | user = User.query.filter(User.email == email).first() 11 | if user: 12 | raise ValidationError("User is already registered.") 13 | 14 | 15 | class SignUpForm(FlaskForm): 16 | username = StringField('username', validators=[DataRequired()]) 17 | email = StringField('email', validators=[DataRequired(), user_exists]) 18 | password = StringField('password', validators=[DataRequired()]) 19 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from .user import User 3 | -------------------------------------------------------------------------------- /app/models/db.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | db = SQLAlchemy() 3 | -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | from .db import db 2 | from werkzeug.security import generate_password_hash, check_password_hash 3 | from flask_login import UserMixin 4 | 5 | class User(db.Model, UserMixin): 6 | __tablename__ = 'users' 7 | 8 | id = db.Column(db.Integer, primary_key = True) 9 | username = db.Column(db.String(40), nullable = False, unique = True) 10 | email = db.Column(db.String(255), nullable = False, unique = True) 11 | hashed_password = db.Column(db.String(255), nullable = False) 12 | 13 | 14 | @property 15 | def password(self): 16 | return self.hashed_password 17 | 18 | 19 | @password.setter 20 | def password(self, password): 21 | self.hashed_password = generate_password_hash(password) 22 | 23 | 24 | def check_password(self, password): 25 | return check_password_hash(self.password, password) 26 | 27 | 28 | def to_dict(self): 29 | return { 30 | "id": self.id, 31 | "username": self.username, 32 | "email": self.email 33 | } 34 | -------------------------------------------------------------------------------- /app/seeds/__init__.py: -------------------------------------------------------------------------------- 1 | from flask.cli import AppGroup 2 | from .users import seed_users, undo_users 3 | 4 | # Creates a seed group to hold our commands 5 | # So we can type `flask seed --help` 6 | seed_commands = AppGroup('seed') 7 | 8 | # Creates the `flask seed all` command 9 | @seed_commands.command('all') 10 | def seed(): 11 | seed_users() 12 | # Add other seed functions here 13 | 14 | # Creates the `flask seed undo` command 15 | @seed_commands.command('undo') 16 | def undo(): 17 | undo_users() 18 | # Add other undo functions here 19 | -------------------------------------------------------------------------------- /app/seeds/users.py: -------------------------------------------------------------------------------- 1 | from werkzeug.security import generate_password_hash 2 | from app.models import db, User 3 | 4 | # Adds a demo user, you can add other users here if you want 5 | def seed_users(): 6 | 7 | demo = User(username='Demo', email='demo@aa.io', 8 | password='password') 9 | 10 | db.session.add(demo) 11 | 12 | db.session.commit() 13 | 14 | # Uses a raw SQL query to TRUNCATE the users table. 15 | # SQLAlchemy doesn't have a built in function to do this 16 | # TRUNCATE Removes all the data from the table, and resets 17 | # the auto incrementing primary key 18 | def undo_users(): 19 | db.session.execute('TRUNCATE users;') 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2-binary==2.8.6 2 | -------------------------------------------------------------------------------- /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 = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(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 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 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic.env') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option( 26 | 'sqlalchemy.url', 27 | str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%')) 28 | target_metadata = current_app.extensions['migrate'].db.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, target_metadata=target_metadata, literal_binds=True 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | 65 | # this callback is used to prevent an auto-migration from being generated 66 | # when there are no changes to the schema 67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 68 | def process_revision_directives(context, revision, directives): 69 | if getattr(config.cmd_opts, 'autogenerate', False): 70 | script = directives[0] 71 | if script.upgrade_ops.is_empty(): 72 | directives[:] = [] 73 | logger.info('No changes in schema detected.') 74 | 75 | connectable = engine_from_config( 76 | config.get_section(config.config_ini_section), 77 | prefix='sqlalchemy.', 78 | poolclass=pool.NullPool, 79 | ) 80 | 81 | with connectable.connect() as connection: 82 | context.configure( 83 | connection=connection, 84 | target_metadata=target_metadata, 85 | process_revision_directives=process_revision_directives, 86 | **current_app.extensions['migrate'].configure_args 87 | ) 88 | 89 | with context.begin_transaction(): 90 | context.run_migrations() 91 | 92 | 93 | if context.is_offline_mode(): 94 | run_migrations_offline() 95 | else: 96 | run_migrations_online() 97 | -------------------------------------------------------------------------------- /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/20201120_150602_create_users_table.py: -------------------------------------------------------------------------------- 1 | """create_users_table 2 | 3 | Revision ID: ffdc0a98111c 4 | Revises: 5 | Create Date: 2020-11-20 15:06:02.230689 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'ffdc0a98111c' 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('users', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('username', sa.String(length=40), nullable=False), 24 | sa.Column('email', sa.String(length=255), nullable=False), 25 | sa.Column('hashed_password', sa.String(length=255), nullable=False), 26 | sa.PrimaryKeyConstraint('id'), 27 | sa.UniqueConstraint('email'), 28 | sa.UniqueConstraint('username') 29 | ) 30 | # ### end Alembic commands ###qqqqqqqqq 31 | 32 | 33 | def downgrade(): 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('users') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /react-app/.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /react-app/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | Make sure to install dependencies in here with `npm install`. 4 | 5 | For local development, you'll need to also make a copy of the `.env.example` as `.env` to set your environment. 6 | 7 | ```bash 8 | cp .env.example .env 9 | ``` 10 | 11 | Your React App will live here. While is development, run this application from this location using `npm start`. 12 | 13 | No environment variables are needed to run this application in development, but be sure to set the `REACT_APP_BASE_URL` environment variable in the Dockerfile in the root of this repository! It needs to match the url at which Heroku will host your site. 14 | 15 | This app will be automatically built when you deploy to heroku. 16 | 17 | 18 | --- 19 | 20 | # Getting Started with Create React App 21 | 22 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 23 | 24 | ## Available Scripts 25 | 26 | In the project directory, you can run: 27 | 28 | ### `npm start` 29 | 30 | Runs the app in the development mode.\ 31 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 32 | 33 | The page will reload if you make edits.\ 34 | You will also see any lint errors in the console. 35 | 36 | ### `npm test` 37 | 38 | Launches the test runner in the interactive watch mode.\ 39 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 40 | 41 | ### `npm run build` 42 | 43 | Builds the app for production to the `build` folder.\ 44 | It correctly bundles React in production mode and optimizes the build for the best performance. 45 | 46 | The build is minified and the filenames include the hashes.\ 47 | Your app is ready to be deployed! 48 | 49 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 50 | 51 | ### `npm run eject` 52 | 53 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 54 | 55 | 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. 56 | 57 | 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. 58 | 59 | 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. 60 | 61 | ## Learn More 62 | 63 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 64 | 65 | To learn React, check out the [React documentation](https://reactjs.org/). 66 | 67 | ### Code Splitting 68 | 69 | 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) 70 | 71 | ### Analyzing the Bundle Size 72 | 73 | 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) 74 | 75 | ### Making a Progressive Web App 76 | 77 | 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) 78 | 79 | ### Advanced Configuration 80 | 81 | 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) 82 | 83 | ### Deployment 84 | 85 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 86 | 87 | ### `npm run build` fails to minify 88 | 89 | 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) 90 | -------------------------------------------------------------------------------- /react-app/craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | style: { 3 | postcss: { 4 | plugins: [ 5 | require('tailwindcss'), 6 | require('autoprefixer'), 7 | ], 8 | }, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^6.1.1", 7 | "@testing-library/jest-dom": "^5.11.9", 8 | "@testing-library/react": "^11.2.5", 9 | "@testing-library/user-event": "^12.7.3", 10 | "react": "^17.0.1", 11 | "react-dom": "^17.0.1", 12 | "react-redux": "^7.2.2", 13 | "react-router-dom": "^5.2.0", 14 | "react-scripts": "4.0.3", 15 | "redux": "^4.0.5", 16 | "redux-thunk": "^2.3.0", 17 | "web-vitals": "^1.1.0" 18 | }, 19 | "scripts": { 20 | "start": "craco start", 21 | "build": "craco build", 22 | "test": "craco test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "devDependencies": { 44 | "@tailwindcss/postcss7-compat": "^2.0.3", 45 | "autoprefixer": "^9.8.6", 46 | "postcss": "^7.0.35", 47 | "redux-logger": "^3.0.6", 48 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.3" 49 | }, 50 | "proxy": "http://localhost:5000" 51 | } 52 | -------------------------------------------------------------------------------- /react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lazytangent/flask-react-template/0ee5dc47908826cd08f6cca35ffbe67331ac7985/react-app/public/favicon.ico -------------------------------------------------------------------------------- /react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Flask React Template App", 3 | "name": "Flask React Template App", 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 | -------------------------------------------------------------------------------- /react-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /react-app/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | 4 | import { authenticate } from './store/session'; 5 | import NavBar from './components/NavBar'; 6 | import ProtectedRoute from './components/ProtectedRoute'; 7 | import LoginForm from './components/LoginForm'; 8 | import SignUpForm from './components/SignUpForm'; 9 | 10 | const App = () => { 11 | const [authenticated, setAuthenticated] = useState(false); 12 | const [loaded, setLoaded] = useState(false); 13 | 14 | useEffect(() => { 15 | (async () => { 16 | const user = await authenticate(); 17 | if (!user.errors) { 18 | setAuthenticated(true); 19 | } 20 | setLoaded(true); 21 | })(); 22 | }, []); 23 | 24 | if (!loaded) { 25 | return null; 26 | } 27 | 28 | return ( 29 | <> 30 | 31 | 32 | 33 | 37 | 38 | 39 | 43 | 44 | 45 |

My Home Page

46 |
47 |
48 | 49 | ); 50 | }; 51 | 52 | export default App; 53 | -------------------------------------------------------------------------------- /react-app/src/components/LoginForm/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Redirect } from "react-router-dom"; 3 | import { useDispatch } from 'react-redux'; 4 | 5 | import { login } from '../../store/session'; 6 | 7 | const LoginForm = ({ authenticated, setAuthenticated }) => { 8 | const dispatch = useDispatch(); 9 | 10 | const [errors, setErrors] = useState([]); 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | 14 | const onLogin = async (e) => { 15 | e.preventDefault(); 16 | const user = await dispatch(login(email, password)); 17 | if (!user.errors) { 18 | setAuthenticated(true); 19 | } else { 20 | setErrors(user.errors); 21 | } 22 | }; 23 | 24 | const updateEmail = (e) => { 25 | setEmail(e.target.value); 26 | }; 27 | 28 | const updatePassword = (e) => { 29 | setPassword(e.target.value); 30 | }; 31 | 32 | if (authenticated) { 33 | return ; 34 | } 35 | 36 | return ( 37 |
38 |
39 | {errors.map((error) => ( 40 |
{error}
41 | ))} 42 |
43 |
44 | 45 | 52 |
53 |
54 | 55 | 62 | 63 |
64 |
65 | ); 66 | }; 67 | 68 | export default LoginForm; 69 | -------------------------------------------------------------------------------- /react-app/src/components/LoginForm/index.js: -------------------------------------------------------------------------------- 1 | import LoginForm from './LoginForm'; 2 | 3 | export default LoginForm; 4 | -------------------------------------------------------------------------------- /react-app/src/components/LogoutButton/LogoutButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { logout } from '../../store/session'; 5 | 6 | const LogoutButton = ({setAuthenticated}) => { 7 | const dispatch = useDispatch(); 8 | 9 | const onLogout = async (e) => { 10 | await dispatch(logout()); 11 | setAuthenticated(false); 12 | }; 13 | 14 | return ; 15 | }; 16 | 17 | export default LogoutButton; 18 | -------------------------------------------------------------------------------- /react-app/src/components/LogoutButton/index.js: -------------------------------------------------------------------------------- 1 | import LogoutButton from './LogoutButton'; 2 | 3 | export default LogoutButton; 4 | -------------------------------------------------------------------------------- /react-app/src/components/NavBar/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import LogoutButton from '../LogoutButton'; 4 | 5 | const NavBar = ({ setAuthenticated }) => { 6 | return ( 7 | 34 | ); 35 | }; 36 | 37 | export default NavBar; 38 | -------------------------------------------------------------------------------- /react-app/src/components/NavBar/index.js: -------------------------------------------------------------------------------- 1 | import NavBar from './NavBar'; 2 | 3 | export default NavBar; 4 | -------------------------------------------------------------------------------- /react-app/src/components/ProtectedRoute/ProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | 4 | const ProtectedRoute = props => { 5 | return ( 6 | 7 | {(props.authenticated)? props.children : } 8 | 9 | ); 10 | }; 11 | 12 | 13 | export default ProtectedRoute; 14 | -------------------------------------------------------------------------------- /react-app/src/components/ProtectedRoute/index.js: -------------------------------------------------------------------------------- 1 | import ProtectedRoute from './ProtectedRoute'; 2 | 3 | export default ProtectedRoute; 4 | -------------------------------------------------------------------------------- /react-app/src/components/SignUpForm/SignUpForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useDispatch } from 'react-redux'; 3 | import { Redirect } from 'react-router-dom'; 4 | 5 | import { signUp } from '../../store/session'; 6 | 7 | const SignUpForm = ({authenticated, setAuthenticated}) => { 8 | const dispatch = useDispatch(); 9 | 10 | const [username, setUsername] = useState(""); 11 | const [email, setEmail] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [repeatPassword, setRepeatPassword] = useState(""); 14 | 15 | const onSignUp = async (e) => { 16 | e.preventDefault(); 17 | if (password === repeatPassword) { 18 | const user = await dispatch(signUp(username, email, password)); 19 | if (!user.errors) { 20 | setAuthenticated(true); 21 | } 22 | } 23 | }; 24 | 25 | const updateUsername = (e) => { 26 | setUsername(e.target.value); 27 | }; 28 | 29 | const updateEmail = (e) => { 30 | setEmail(e.target.value); 31 | }; 32 | 33 | const updatePassword = (e) => { 34 | setPassword(e.target.value); 35 | }; 36 | 37 | const updateRepeatPassword = (e) => { 38 | setRepeatPassword(e.target.value); 39 | }; 40 | 41 | if (authenticated) { 42 | return ; 43 | } 44 | 45 | return ( 46 |
47 |
48 | 49 | 55 |
56 |
57 | 58 | 64 |
65 |
66 | 67 | 73 |
74 |
75 | 76 | 83 |
84 | 85 |
86 | ); 87 | }; 88 | 89 | export default SignUpForm; 90 | -------------------------------------------------------------------------------- /react-app/src/components/SignUpForm/index.js: -------------------------------------------------------------------------------- 1 | import SignUpForm from './SignUpForm'; 2 | 3 | export default SignUpForm; 4 | -------------------------------------------------------------------------------- /react-app/src/context/Modal.css: -------------------------------------------------------------------------------- 1 | #modal { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | left: 0; 6 | bottom: 0; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | 12 | #modal-background { 13 | position: fixed; 14 | top: 0; 15 | right: 0; 16 | left: 0; 17 | bottom: 0; 18 | background-color: rgba(0, 0, 0, 0.7); 19 | } 20 | 21 | #modal-content { 22 | position: absolute; 23 | background-color:white; 24 | border-radius: 0.5rem; 25 | } 26 | -------------------------------------------------------------------------------- /react-app/src/context/Modal.js: -------------------------------------------------------------------------------- 1 | import { createContext, useRef, useState, useEffect, useContext } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './Modal.css'; 4 | 5 | const ModalContext = createContext(); 6 | 7 | export const ModalProvider = ({ children }) => { 8 | const modalRef = useRef(); 9 | const [value, setValue] = useState(); 10 | 11 | useEffect(() => { 12 | setValue(modalRef.current); 13 | }, []); 14 | 15 | return ( 16 | <> 17 | 18 | {children} 19 | 20 |
21 | 22 | ); 23 | }; 24 | 25 | export const Modal = ({ onClose, children }) => { 26 | const modalNode = useContext(ModalContext); 27 | if (!modalNode) return null; 28 | 29 | return ReactDOM.createPortal( 30 |