├── .flaskenv
├── dev-requirements.txt
├── migrations
├── README
├── script.py.mako
├── alembic.ini
├── versions
│ ├── 0dc4227077a4_.py
│ └── 9fcfa773ef84_.py
└── env.py
├── react-app
├── .env.example
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── assets
│ │ ├── redketchup.zip
│ │ ├── git_frames_bmp.zip
│ │ ├── ezgif.com-gif-maker.gif
│ │ ├── git_frames_bmp (2).zip
│ │ ├── dancing_stickman_test.gif
│ │ ├── gif_frames_png
│ │ │ ├── IMG00000.png
│ │ │ ├── IMG00001.png
│ │ │ ├── IMG00002.png
│ │ │ ├── IMG00003.png
│ │ │ ├── IMG00004.png
│ │ │ ├── IMG00005.png
│ │ │ ├── IMG00006.png
│ │ │ ├── IMG00007.png
│ │ │ ├── IMG00008.png
│ │ │ ├── IMG00009.png
│ │ │ ├── IMG00010.png
│ │ │ ├── IMG00011.png
│ │ │ ├── IMG00012.png
│ │ │ ├── IMG00013.png
│ │ │ ├── IMG00014.png
│ │ │ ├── IMG00015.png
│ │ │ ├── IMG00016.png
│ │ │ ├── IMG00017.png
│ │ │ ├── IMG00018.png
│ │ │ ├── IMG00019.png
│ │ │ ├── IMG00020.png
│ │ │ ├── IMG00021.png
│ │ │ ├── IMG00022.png
│ │ │ ├── IMG00023.png
│ │ │ ├── IMG00024.png
│ │ │ ├── IMG00025.png
│ │ │ ├── IMG00026.png
│ │ │ ├── IMG00027.png
│ │ │ ├── IMG00028.png
│ │ │ ├── IMG00029.png
│ │ │ ├── IMG00030.png
│ │ │ ├── IMG00031.png
│ │ │ ├── IMG00032.png
│ │ │ ├── IMG00033.png
│ │ │ ├── IMG00034.png
│ │ │ ├── IMG00035.png
│ │ │ ├── IMG00036.png
│ │ │ ├── IMG00037.png
│ │ │ ├── IMG00038.png
│ │ │ ├── IMG00039.png
│ │ │ ├── IMG00040.png
│ │ │ ├── IMG00041.png
│ │ │ ├── IMG00042.png
│ │ │ ├── IMG00043.png
│ │ │ ├── IMG00044.png
│ │ │ ├── IMG00045.png
│ │ │ ├── IMG00046.png
│ │ │ ├── IMG00047.png
│ │ │ ├── IMG00048.png
│ │ │ ├── IMG00049.png
│ │ │ ├── IMG00050.png
│ │ │ ├── IMG00051.png
│ │ │ ├── IMG00052.png
│ │ │ ├── IMG00053.png
│ │ │ ├── IMG00054.png
│ │ │ ├── IMG00055.png
│ │ │ ├── IMG00056.png
│ │ │ ├── IMG00057.png
│ │ │ ├── IMG00058.png
│ │ │ ├── IMG00059.png
│ │ │ ├── IMG00060.png
│ │ │ └── IMG00061.png
│ │ ├── git_frames_bmp
│ │ │ ├── IMG00000.bmp
│ │ │ ├── IMG00001.bmp
│ │ │ ├── IMG00002.bmp
│ │ │ ├── IMG00003.bmp
│ │ │ ├── IMG00004.bmp
│ │ │ ├── IMG00005.bmp
│ │ │ ├── IMG00006.bmp
│ │ │ ├── IMG00007.bmp
│ │ │ ├── IMG00008.bmp
│ │ │ ├── IMG00009.bmp
│ │ │ ├── IMG00010.bmp
│ │ │ ├── IMG00011.bmp
│ │ │ ├── IMG00012.bmp
│ │ │ ├── IMG00013.bmp
│ │ │ ├── IMG00014.bmp
│ │ │ ├── IMG00015.bmp
│ │ │ ├── IMG00016.bmp
│ │ │ ├── IMG00017.bmp
│ │ │ ├── IMG00018.bmp
│ │ │ ├── IMG00019.bmp
│ │ │ ├── IMG00020.bmp
│ │ │ ├── IMG00021.bmp
│ │ │ ├── IMG00022.bmp
│ │ │ ├── IMG00023.bmp
│ │ │ ├── IMG00024.bmp
│ │ │ ├── IMG00025.bmp
│ │ │ ├── IMG00026.bmp
│ │ │ ├── IMG00027.bmp
│ │ │ ├── IMG00028.bmp
│ │ │ ├── IMG00029.bmp
│ │ │ ├── IMG00030.bmp
│ │ │ ├── IMG00031.bmp
│ │ │ ├── IMG00032.bmp
│ │ │ ├── IMG00033.bmp
│ │ │ ├── IMG00034.bmp
│ │ │ ├── IMG00035.bmp
│ │ │ ├── IMG00036.bmp
│ │ │ ├── IMG00037.bmp
│ │ │ ├── IMG00038.bmp
│ │ │ ├── IMG00039.bmp
│ │ │ ├── IMG00040.bmp
│ │ │ ├── IMG00041.bmp
│ │ │ ├── IMG00042.bmp
│ │ │ ├── IMG00043.bmp
│ │ │ ├── IMG00044.bmp
│ │ │ ├── IMG00045.bmp
│ │ │ ├── IMG00046.bmp
│ │ │ ├── IMG00047.bmp
│ │ │ ├── IMG00048.bmp
│ │ │ ├── IMG00049.bmp
│ │ │ ├── IMG00050.bmp
│ │ │ ├── IMG00051.bmp
│ │ │ ├── IMG00052.bmp
│ │ │ ├── IMG00053.bmp
│ │ │ ├── IMG00054.bmp
│ │ │ ├── IMG00055.bmp
│ │ │ ├── IMG00056.bmp
│ │ │ ├── IMG00057.bmp
│ │ │ ├── IMG00058.bmp
│ │ │ ├── IMG00059.bmp
│ │ │ ├── IMG00060.bmp
│ │ │ ├── IMG00061.bmp
│ │ │ └── frameslist.gsf
│ │ └── frames.js
│ ├── components
│ │ ├── Footer
│ │ │ └── index.js
│ │ ├── Header
│ │ │ └── index.js
│ │ ├── auth
│ │ │ ├── LogoutButton.js
│ │ │ ├── ProtectedRoute.js
│ │ │ ├── LoginForm.js
│ │ │ └── SignUpForm.js
│ │ ├── NavBanner
│ │ │ └── index.js
│ │ ├── SubComments
│ │ │ └── index.js
│ │ ├── UsersList.js
│ │ ├── SplashPage
│ │ │ └── index.js
│ │ ├── User.js
│ │ ├── NavBar.js
│ │ ├── Comments
│ │ │ └── index.js
│ │ ├── Posts
│ │ │ └── index.js
│ │ └── Gif
│ │ │ └── index.js
│ ├── seed
│ │ ├── seed.py
│ │ └── gif_1.json
│ ├── index.js
│ ├── store
│ │ ├── index.js
│ │ ├── state_shape.txt
│ │ ├── gif.js
│ │ ├── session.js
│ │ ├── comments.js
│ │ └── posts.js
│ ├── App.js
│ ├── logo.svg
│ └── index.css
├── .gitignore
├── README.md
└── package.json
├── .gitignore
├── flask_backend
├── models
│ ├── db.py
│ ├── __init__.py
│ ├── posts.py
│ ├── subcomments.py
│ ├── comments.py
│ └── user.py
├── forms
│ ├── __init__.py
│ ├── signup_form.py
│ └── login_form.py
├── api
│ ├── __init__.py
│ ├── gif.py
│ ├── user_routes.py
│ ├── auth_routes.py
│ └── posts.py
├── seeds
│ ├── __init__.py
│ └── users.py
├── config.py
└── __init__.py
├── .dockerignore
├── .env.example
├── Dockerfile
├── requirements.txt
├── Pipfile
├── .devcontainer
├── Dockerfile
└── devcontainer.json
├── .github
└── workflows
│ └── main.yml
├── README.md
└── Pipfile.lock
/.flaskenv:
--------------------------------------------------------------------------------
1 | FLASK_APP=flask_backend
2 |
--------------------------------------------------------------------------------
/dev-requirements.txt:
--------------------------------------------------------------------------------
1 | psycopg2-binary==2.8.6
2 |
--------------------------------------------------------------------------------
/migrations/README:
--------------------------------------------------------------------------------
1 | Generic single-database configuration.
--------------------------------------------------------------------------------
/react-app/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_BASE_URL=http://localhost:5000
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | __pycache__/
3 | *.py[cod]
4 | .venv
5 | .DS_Store
6 | .vscode/
7 |
--------------------------------------------------------------------------------
/flask_backend/models/db.py:
--------------------------------------------------------------------------------
1 | from flask_sqlalchemy import SQLAlchemy
2 |
3 | db = SQLAlchemy()
4 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | react-app/node_modules
2 | .venv
3 | Pipfile
4 | Pipfile.lock
5 | .env
6 | .DS_Store
7 |
--------------------------------------------------------------------------------
/flask_backend/forms/__init__.py:
--------------------------------------------------------------------------------
1 | from .login_form import LoginForm
2 | from .signup_form import SignUpForm
3 |
--------------------------------------------------------------------------------
/react-app/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/public/favicon.ico
--------------------------------------------------------------------------------
/react-app/src/assets/redketchup.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/redketchup.zip
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp.zip
--------------------------------------------------------------------------------
/react-app/src/assets/ezgif.com-gif-maker.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/ezgif.com-gif-maker.gif
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp (2).zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp (2).zip
--------------------------------------------------------------------------------
/react-app/src/assets/dancing_stickman_test.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/dancing_stickman_test.gif
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00000.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00000.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00001.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00002.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00003.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00004.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00004.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00005.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00005.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00006.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00006.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00007.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00007.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00008.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00008.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00009.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00009.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00010.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00010.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00011.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00011.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00012.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00012.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00013.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00013.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00014.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00014.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00015.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00015.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00016.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00016.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00017.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00017.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00018.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00018.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00019.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00019.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00020.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00020.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00021.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00021.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00022.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00022.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00023.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00023.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00024.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00025.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00025.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00026.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00026.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00027.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00027.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00028.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00028.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00029.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00029.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00030.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00030.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00031.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00031.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00032.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00032.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00033.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00033.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00034.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00034.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00035.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00035.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00036.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00036.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00037.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00037.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00038.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00038.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00039.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00039.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00040.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00040.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00041.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00041.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00042.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00042.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00043.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00043.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00044.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00044.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00045.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00045.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00046.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00046.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00047.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00047.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00048.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00048.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00049.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00049.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00050.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00050.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00051.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00051.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00052.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00052.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00053.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00053.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00054.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00054.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00055.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00055.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00056.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00056.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00057.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00057.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00058.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00058.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00059.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00059.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00060.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00060.png
--------------------------------------------------------------------------------
/react-app/src/assets/gif_frames_png/IMG00061.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/gif_frames_png/IMG00061.png
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00000.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00000.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00001.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00001.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00002.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00002.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00003.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00003.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00004.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00004.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00005.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00005.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00006.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00006.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00007.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00007.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00008.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00008.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00009.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00009.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00010.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00010.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00011.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00011.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00012.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00012.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00013.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00013.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00014.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00014.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00015.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00015.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00016.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00016.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00017.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00017.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00018.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00018.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00019.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00019.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00020.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00020.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00021.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00021.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00022.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00022.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00023.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00023.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00024.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00024.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00025.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00025.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00026.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00026.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00027.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00027.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00028.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00028.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00029.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00029.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00030.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00030.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00031.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00031.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00032.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00032.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00033.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00033.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00034.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00034.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00035.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00035.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00036.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00036.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00037.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00037.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00038.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00038.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00039.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00039.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00040.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00040.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00041.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00041.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00042.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00042.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00043.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00043.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00044.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00044.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00045.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00045.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00046.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00046.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00047.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00047.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00048.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00048.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00049.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00049.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00050.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00050.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00051.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00051.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00052.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00052.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00053.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00053.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00054.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00054.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00055.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00055.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00056.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00056.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00057.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00057.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00058.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00058.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00059.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00059.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00060.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00060.bmp
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/IMG00061.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/flow-state-15/react-redux-python-demo/HEAD/react-app/src/assets/git_frames_bmp/IMG00061.bmp
--------------------------------------------------------------------------------
/flask_backend/api/__init__.py:
--------------------------------------------------------------------------------
1 | from .gif import gif_routes
2 | from .posts import posts_routes
3 | from .auth_routes import auth_routes
4 | from .user_routes import user_routes
5 |
--------------------------------------------------------------------------------
/flask_backend/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 | from .user import User
3 | from .posts import Post
4 | from .comments import Comment
5 | from .subcomments import SubComment
6 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | FLASK_APP=app
2 | FLASK_ENV=development
3 | SECRET_KEY=lkasjdf09ajsdkfljalsiorj12n3490re9485309irefvn,u90818734902139489230
4 | DATABASE_URL=postgresql://starter_app_dev@localhost/starter_app
5 |
--------------------------------------------------------------------------------
/react-app/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Simple React App
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/react-app/src/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | export default function Footer() {
2 | return (
3 |
4 |
React/Redux EOD series
5 |
created by Dan Purcell 2022
6 |
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/react-app/src/seed/seed.py:
--------------------------------------------------------------------------------
1 |
2 | import os
3 | import json
4 |
5 |
6 | cwd = os.getcwd()
7 | file_list = os.listdir(cwd + "/assets/gif_frames_png")
8 | file_list.sort()
9 |
10 | jsonified = json.dumps(file_list)
11 |
12 | new_file = open(cwd + "/seed/gif_1.json", "w")
13 | new_file.write(jsonified)
14 | new_file.close()
15 |
--------------------------------------------------------------------------------
/react-app/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import logo from "../../logo.svg";
2 | import NavBar from "../NavBar"
3 |
4 | function Header() {
5 | return (
6 |
11 | );
12 | }
13 |
14 | export default Header;
15 |
--------------------------------------------------------------------------------
/react-app/src/components/auth/LogoutButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { logout } from '../../store/session';
4 |
5 | const LogoutButton = () => {
6 | const dispatch = useDispatch()
7 | const onLogout = async (e) => {
8 | await dispatch(logout());
9 | };
10 |
11 | return Logout ;
12 | };
13 |
14 | export default LogoutButton;
15 |
--------------------------------------------------------------------------------
/flask_backend/api/gif.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 |
3 | gif_routes = Blueprint('gif', __name__)
4 |
5 |
6 | index = -1
7 |
8 |
9 | @gif_routes.route('/get-index', methods=['GET'])
10 | def get_index():
11 |
12 | global index
13 |
14 | if index > 60:
15 | index = -1
16 |
17 | index += 1
18 |
19 | # convert = str(index)
20 | # padded = convert.zfill(5) # --> '00001'
21 |
22 | return {"frame_index": index}
23 |
--------------------------------------------------------------------------------
/react-app/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 | /node_modules
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/react-app/src/components/auth/ProtectedRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { Route, Redirect } from 'react-router-dom';
4 |
5 | const ProtectedRoute = props => {
6 | const user = useSelector(state => state.session.user)
7 | return (
8 |
9 | {(user)? props.children : }
10 |
11 | )
12 | };
13 |
14 |
15 | export default ProtectedRoute;
16 |
--------------------------------------------------------------------------------
/react-app/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 | import './index.css';
5 | import App from './App';
6 | import configureStore from './store';
7 |
8 | const store = configureStore();
9 |
10 | ReactDOM.render(
11 |
12 |
13 |
14 |
15 | ,
16 | document.getElementById('root')
17 | );
18 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.9
2 |
3 | # Setup Flask environment
4 | ENV REACT_APP_BASE_URL=https://aa-react-eod-series.herokuapp.com
5 | ENV FLASK_APP=app
6 | ENV FLASK_ENV=production
7 | ENV SQLALCHEMY_ECHO=True
8 |
9 | WORKDIR /var/www
10 | COPY . .
11 | COPY /react-app/build/* flask_backend/static/
12 |
13 | # Install Python Dependencies
14 | RUN pip install -r requirements.txt
15 | RUN pip install psycopg2
16 |
17 | # Run flask environment
18 | CMD gunicorn flask_backend:app
19 |
--------------------------------------------------------------------------------
/flask_backend/api/user_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify
2 | from flask_login import login_required
3 | from flask_backend.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 |
--------------------------------------------------------------------------------
/react-app/src/components/NavBanner/index.js:
--------------------------------------------------------------------------------
1 |
2 | import { NavLink } from 'react-router-dom';
3 |
4 |
5 | export default function NavBanner (){
6 | return(
7 |
8 |
9 |
10 | Gif Demo
11 |
12 |
13 |
14 |
15 | Posts Demo
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/flask_backend/models/posts.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class Post(db.Model):
5 | __tablename__ = 'posts'
6 |
7 | id = db.Column(db.Integer, primary_key=True)
8 | content = db.Column(db.Text, nullable=False)
9 |
10 | comments = db.relationship("Comment", back_populates="post", cascade="all, delete")
11 |
12 |
13 | def to_dict(self):
14 | return {
15 | 'id': self.id,
16 | 'content': self.content,
17 | 'comments': [c.to_dict() for c in self.comments]
18 | }
19 |
--------------------------------------------------------------------------------
/flask_backend/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 |
9 | # Creates the `flask seed all` command
10 | @seed_commands.command('all')
11 | def seed():
12 | seed_users()
13 | # Add other seed functions here
14 |
15 |
16 | # Creates the `flask seed undo` command
17 | @seed_commands.command('undo')
18 | def undo():
19 | undo_users()
20 | # Add other undo functions here
21 |
--------------------------------------------------------------------------------
/react-app/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | Your React App will live here. While is development, run this application from this location using `npm start`.
4 |
5 |
6 | No environment variables are needed to run this application in development, but be sure to set the REACT_APP_BASE_URL environment variable in heroku!
7 |
8 | This app will be automatically built when you deploy to heroku, please see the `heroku-postbuild` script in your `express.js` applications `package.json` to see how this works.
9 |
--------------------------------------------------------------------------------
/flask_backend/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 1.4 no longer supports url strings that start with 'postgres'
8 | # (only 'postgresql') but heroku's postgres add-on automatically sets the
9 | # url in the hidden config vars to start with postgres.
10 | # so the connection uri must be updated here
11 | SQLALCHEMY_DATABASE_URI = os.environ.get(
12 | 'DATABASE_URL').replace('postgres://', 'postgresql://')
13 | SQLALCHEMY_ECHO = True
14 |
--------------------------------------------------------------------------------
/flask_backend/models/subcomments.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class SubComment(db.Model):
5 | __tablename__ = 'subcomments'
6 |
7 | id = db.Column(db.Integer, primary_key=True)
8 | content = db.Column(db.Text, nullable=False)
9 |
10 | comment_id = db.Column(db.Integer, db.ForeignKey('comments.id'), nullable=False)
11 | comment = db.relationship("Comment", back_populates="subcomments")
12 |
13 |
14 | def to_dict(self):
15 | return {
16 | 'id': self.id,
17 | 'comment_id': self.comment_id,
18 | 'content': self.content
19 | }
20 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | #
2 | # These requirements were autogenerated by pipenv
3 | # To regenerate from the project's Pipfile, run:
4 | #
5 | # pipenv lock --requirements
6 | #
7 |
8 | -i https://pypi.org/simple
9 | alembic==1.6.5
10 | click==7.1.2
11 | flask-cors==3.0.8
12 | flask-login==0.5.0
13 | flask-migrate==3.0.1
14 | flask-sqlalchemy==2.5.1
15 | flask-wtf==0.15.1
16 | flask==2.0.1
17 | greenlet==1.1.0
18 | gunicorn==20.1.0
19 | itsdangerous==2.0.1
20 | jinja2==3.0.1
21 | mako==1.1.4
22 | markupsafe==2.0.1
23 | python-dateutil==2.8.1
24 | python-dotenv==0.14.0
25 | python-editor==1.0.4
26 | six==1.15.0
27 | sqlalchemy==1.4.19
28 | werkzeug==2.0.1
29 | wtforms==2.3.3
30 |
--------------------------------------------------------------------------------
/flask_backend/models/comments.py:
--------------------------------------------------------------------------------
1 | from .db import db
2 |
3 |
4 | class Comment(db.Model):
5 | __tablename__ = 'comments'
6 |
7 | id = db.Column(db.Integer, primary_key=True)
8 | content = db.Column(db.Text, nullable=False)
9 |
10 | post_id = db.Column(db.Integer, db.ForeignKey("posts.id"), nullable=False)
11 | post = db.relationship("Post", back_populates="comments")
12 |
13 |
14 | subcomments = db.relationship("SubComment", back_populates="comment", cascade="all, delete")
15 |
16 |
17 | def to_dict(self):
18 | return {
19 | 'id': self.id,
20 | 'post_id': self.post_id,
21 | 'content': self.content,
22 | 'subcomments': [s.to_dict() for s in self.subcomments]
23 | }
24 |
--------------------------------------------------------------------------------
/react-app/src/components/SubComments/index.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 |
3 | export default function SubComments({ props }) {
4 | const all_sub_comments = useSelector(
5 | (state) => state.posts[props.post_id].comments[props.comment_id].all
6 | );
7 |
8 | const sub_comments = all_sub_comments
9 | ? all_sub_comments.map((sc) => {
10 | const ids = { subcomment_id: sc.id, post_id: props.post_id };
11 | return (
12 |
13 | {sc.content}
14 | props.handle_delete(ids)}>
15 | delete
16 |
17 |
18 | );
19 | })
20 | : null;
21 |
22 | return {sub_comments}
;
23 | }
24 |
--------------------------------------------------------------------------------
/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.1.0"
9 | itsdangerous = "==2.0.1"
10 | python-dotenv = "==0.14.0"
11 | six = "==1.15.0"
12 | Flask = "==2.0.1"
13 | Flask-Cors = "==3.0.8"
14 | Flask-SQLAlchemy = "==2.5.1"
15 | Flask-WTF = "==0.15.1"
16 | Jinja2 = "==3.0.1"
17 | MarkupSafe = "==2.0.1"
18 | SQLAlchemy = "==1.4.19"
19 | Werkzeug = "==2.0.1"
20 | WTForms = "==2.3.3"
21 | Flask-Migrate = "==3.0.1"
22 | Flask-Login = "==0.5.0"
23 | alembic = "==1.6.5"
24 | python-dateutil = "==2.8.1"
25 | python-editor = "==1.0.4"
26 | greenlet = "==1.1.0"
27 | Mako = "==1.1.4"
28 |
29 | [dev-packages]
30 | psycopg2-binary = "==2.8.6"
31 |
32 | [requires]
33 | python_version = "3.9"
34 |
--------------------------------------------------------------------------------
/react-app/src/components/UsersList.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | function UsersList() {
5 | const [users, setUsers] = useState([]);
6 |
7 | useEffect(() => {
8 | async function fetchData() {
9 | const response = await fetch('/api/users/');
10 | const responseData = await response.json();
11 | setUsers(responseData.users);
12 | }
13 | fetchData();
14 | }, []);
15 |
16 | const userComponents = users.map((user) => {
17 | return (
18 |
19 | {user.username}
20 |
21 | );
22 | });
23 |
24 | return (
25 | <>
26 | User List:
27 |
28 | >
29 | );
30 | }
31 |
32 | export default UsersList;
33 |
--------------------------------------------------------------------------------
/flask_backend/seeds/users.py:
--------------------------------------------------------------------------------
1 | from flask_backend.models import db, User
2 |
3 |
4 | # Adds a demo user, you can add other users here if you want
5 | def seed_users():
6 | demo = User(
7 | username='Demo', email='demo@aa.io', password='password')
8 | danp = User(
9 | username='danp', email='danp@aa.io', password='password')
10 |
11 | db.session.add(demo)
12 | db.session.add(danp)
13 |
14 | db.session.commit()
15 |
16 |
17 | # Uses a raw SQL query to TRUNCATE the users table.
18 | # SQLAlchemy doesn't have a built in function to do this
19 | # TRUNCATE Removes all the data from the table, and RESET IDENTITY
20 | # resets the auto incrementing primary key, CASCADE deletes any
21 | # dependent entities
22 | def undo_users():
23 | db.session.execute('TRUNCATE users RESTART IDENTITY CASCADE;')
24 | db.session.commit()
25 |
--------------------------------------------------------------------------------
/react-app/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import session from './session';
4 | import gif from './gif';
5 | import posts from './posts';
6 |
7 | const rootReducer = combineReducers({
8 | session,
9 | gif,
10 | posts,
11 | });
12 |
13 |
14 | let enhancer;
15 |
16 | if (process.env.NODE_ENV === 'production') {
17 | enhancer = applyMiddleware(thunk);
18 | } else {
19 | const logger = require('redux-logger').default;
20 | const composeEnhancers =
21 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
22 | enhancer = composeEnhancers(applyMiddleware(thunk, logger));
23 | }
24 |
25 | const configureStore = (preloadedState) => {
26 | return createStore(rootReducer, preloadedState, enhancer);
27 | };
28 |
29 | export default configureStore;
30 |
--------------------------------------------------------------------------------
/react-app/src/components/SplashPage/index.js:
--------------------------------------------------------------------------------
1 |
2 | import { Link } from 'react-router-dom';
3 |
4 |
5 | export default function SplashPage(){
6 | return (
7 |
8 |
9 |
10 | The gif animation demonstration displays the rendering power of the useSelector and useDispatch hooks.
11 |
12 |
13 |
14 |
15 | The posts demonstration samples a much more complex scenario that involves nested objects in state. Dynamic rerendering is still achieved through only the useSelector and useDispatch hooks.
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/react-app/src/components/User.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useParams } from 'react-router-dom';
3 |
4 | function User() {
5 | const [user, setUser] = useState({});
6 | const { userId } = useParams();
7 |
8 | useEffect(() => {
9 | if (!userId) {
10 | return;
11 | }
12 | (async () => {
13 | const response = await fetch(`/api/users/${userId}`);
14 | const user = await response.json();
15 | setUser(user);
16 | })();
17 | }, [userId]);
18 |
19 | if (!user) {
20 | return null;
21 | }
22 |
23 | return (
24 |
25 |
26 | User Id {userId}
27 |
28 |
29 | Username {user.username}
30 |
31 |
32 | Email {user.email}
33 |
34 |
35 | );
36 | }
37 | export default User;
38 |
--------------------------------------------------------------------------------
/react-app/src/store/state_shape.txt:
--------------------------------------------------------------------------------
1 | This file maps our state shape and paths we'll need in reducer
2 | Start at top level of state:
3 |
4 |
5 | state: {
6 | sessions: {},
7 | gif: {},
8 | posts: {}
9 | }
10 |
11 | --> state.posts
12 | posts: {
13 | id: {},
14 | all_posts: []
15 | }
16 |
17 | --> state.posts[id]
18 | posts[id] = {
19 | id: Number,
20 | content: String,
21 | comments: {}
22 | }
23 |
24 | --> state.posts[id].comments
25 | comments = {
26 | id: {},
27 | all: [comment, comment]
28 | }
29 |
30 | --> state.posts[id].comments[id]
31 | comments[id] = {
32 | id: Number,
33 | post_id: Number,
34 | content: String,
35 | subcomments: {}
36 | }
37 |
38 | --> state.posts[id].comments[id].subcomments
39 | subcomments = {
40 | id: {},
41 | all: []
42 | }
43 |
44 | --> state.posts[id].comments[id].subcomments[id]
45 | subcomments[id] = {
46 | id: Number,
47 | comment_id: Number,
48 | content: String,
49 | }
50 |
--------------------------------------------------------------------------------
/react-app/src/seed/gif_1.json:
--------------------------------------------------------------------------------
1 | ["IMG00000.png", "IMG00001.png", "IMG00002.png", "IMG00003.png", "IMG00004.png", "IMG00005.png", "IMG00006.png", "IMG00007.png", "IMG00008.png", "IMG00009.png", "IMG00010.png", "IMG00011.png", "IMG00012.png", "IMG00013.png", "IMG00014.png", "IMG00015.png", "IMG00016.png", "IMG00017.png", "IMG00018.png", "IMG00019.png", "IMG00020.png", "IMG00021.png", "IMG00022.png", "IMG00023.png", "IMG00024.png", "IMG00025.png", "IMG00026.png", "IMG00027.png", "IMG00028.png", "IMG00029.png", "IMG00030.png", "IMG00031.png", "IMG00032.png", "IMG00033.png", "IMG00034.png", "IMG00035.png", "IMG00036.png", "IMG00037.png", "IMG00038.png", "IMG00039.png", "IMG00040.png", "IMG00041.png", "IMG00042.png", "IMG00043.png", "IMG00044.png", "IMG00045.png", "IMG00046.png", "IMG00047.png", "IMG00048.png", "IMG00049.png", "IMG00050.png", "IMG00051.png", "IMG00052.png", "IMG00053.png", "IMG00054.png", "IMG00055.png", "IMG00056.png", "IMG00057.png", "IMG00058.png", "IMG00059.png", "IMG00060.png", "IMG00061.png"]
--------------------------------------------------------------------------------
/flask_backend/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 |
6 | class User(db.Model, UserMixin):
7 | __tablename__ = 'users'
8 |
9 | id = db.Column(db.Integer, primary_key=True)
10 | username = db.Column(db.String(40), nullable=False, unique=True)
11 | email = db.Column(db.String(255), nullable=False, unique=True)
12 | hashed_password = db.Column(db.String(255), nullable=False)
13 |
14 | @property
15 | def password(self):
16 | return self.hashed_password
17 |
18 | @password.setter
19 | def password(self, password):
20 | self.hashed_password = generate_password_hash(password)
21 |
22 | def check_password(self, password):
23 | return check_password_hash(self.password, password)
24 |
25 | def to_dict(self):
26 | return {
27 | 'id': self.id,
28 | 'username': self.username,
29 | 'email': self.email
30 | }
31 |
--------------------------------------------------------------------------------
/flask_backend/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 flask_backend.models import User
5 |
6 |
7 | def user_exists(form, field):
8 | # Checking if user exists
9 | email = field.data
10 | user = User.query.filter(User.email == email).first()
11 | if user:
12 | raise ValidationError('Email address is already in use.')
13 |
14 |
15 | def username_exists(form, field):
16 | # Checking if username is already in use
17 | username = field.data
18 | user = User.query.filter(User.username == username).first()
19 | if user:
20 | raise ValidationError('Username is already in use.')
21 |
22 |
23 | class SignUpForm(FlaskForm):
24 | username = StringField(
25 | 'username', validators=[DataRequired(), username_exists])
26 | email = StringField('email', validators=[DataRequired(), user_exists])
27 | password = StringField('password', validators=[DataRequired()])
28 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/react-app/src/components/NavBar.js:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { NavLink } from 'react-router-dom';
4 | import LogoutButton from './auth/LogoutButton';
5 |
6 | const NavBar = () => {
7 | return (
8 |
9 |
10 |
11 |
12 | Home
13 |
14 |
15 |
16 |
17 | Login
18 |
19 |
20 |
21 |
22 | Sign Up
23 |
24 |
25 |
26 |
27 | Users
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | export default NavBar;
39 |
--------------------------------------------------------------------------------
/flask_backend/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 flask_backend.models import User
5 |
6 |
7 | def user_exists(form, field):
8 | # Checking if user exists
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 | # 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 |
--------------------------------------------------------------------------------
/react-app/src/components/Comments/index.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 |
3 |
4 | export default function Comments({ props }) {
5 |
6 | //Knowing your state is shaped the way you want it, key into the specific object you need to render your component and pass into props, if needed:
7 |
8 | //NOTE: if your conditional is working properly in your parent, you'll know this path is valid and won't return the wrong type.
9 | const all_comments = useSelector(
10 | (state) => state.posts[props.post_id].comments.all
11 | );
12 |
13 | const element = all_comments ?
14 | all_comments.map((comment) => {
15 |
16 | const ids = { post_id: props.post_id, comment_id: comment.id };
17 |
18 | return (
19 |
20 | {comment.content}
21 | props.handle_delete_c(ids)}>
22 | delete
23 |
24 |
25 | );
26 | })
27 | : null;
28 |
29 | return !(all_comments == null) > 0 && {element}
;
30 | }
31 |
--------------------------------------------------------------------------------
/react-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-app",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.14.1",
7 | "@testing-library/react": "^11.2.7",
8 | "@testing-library/user-event": "^12.8.3",
9 | "http-proxy-middleware": "^1.0.5",
10 | "react": "^17.0.2",
11 | "react-dom": "^17.0.2",
12 | "react-redux": "^7.2.4",
13 | "react-router-dom": "^5.2.0",
14 | "react-scripts": "^4.0.3",
15 | "redux": "^4.1.0",
16 | "redux-logger": "^3.0.6",
17 | "redux-thunk": "^2.3.0"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build",
22 | "test": "react-scripts test",
23 | "eject": "react-scripts eject"
24 | },
25 | "eslintConfig": {
26 | "extends": "react-app"
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.3%",
31 | "not ie 11",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | },
41 | "proxy": "http://localhost:5000"
42 | }
43 |
--------------------------------------------------------------------------------
/migrations/versions/0dc4227077a4_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 0dc4227077a4
4 | Revises: 9fcfa773ef84
5 | Create Date: 2022-02-27 09:14:39.162011
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '0dc4227077a4'
14 | down_revision = '9fcfa773ef84'
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.add_column('comments', sa.Column('post_id', sa.Integer(), nullable=False))
22 | op.create_foreign_key(None, 'comments', 'posts', ['post_id'], ['id'])
23 | op.add_column('subcomments', sa.Column('comment_id', sa.Integer(), nullable=False))
24 | op.create_foreign_key(None, 'subcomments', 'comments', ['comment_id'], ['id'])
25 | # ### end Alembic commands ###
26 |
27 |
28 | def downgrade():
29 | # ### commands auto generated by Alembic - please adjust! ###
30 | op.drop_constraint(None, 'subcomments', type_='foreignkey')
31 | op.drop_column('subcomments', 'comment_id')
32 | op.drop_constraint(None, 'comments', type_='foreignkey')
33 | op.drop_column('comments', 'post_id')
34 | # ### end Alembic commands ###
35 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.224.3/containers/python-3/.devcontainer/base.Dockerfile
2 |
3 | # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
4 | ARG VARIANT="3.10-bullseye"
5 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
6 |
7 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
8 | ARG NODE_VERSION="none"
9 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
10 |
11 | # Install Postgres
12 | RUN sudo apt update
13 | RUN sudo apt install postgresql -y && \
14 | sudo service postgresql start && \
15 | sudo -u postgres psql -c "CREATE USER vscode WITH PASSWORD 'password';" && \
16 | sudo -u postgres psql -c "ALTER USER vscode WITH SUPERUSER;" && \
17 | sudo -u postgres psql -c "CREATE DATABASE vscode WITH OWNER vscode" && \
18 | sudo -u postgres psql -c "CREATE DATABASE python_project WITH OWNER vscode"
19 |
20 | # auto configure flask app to connect to internal postgres
21 | ENV DATABASE_URL=postgresql://vscode:password@localhost/python_project
22 |
--------------------------------------------------------------------------------
/react-app/src/store/gif.js:
--------------------------------------------------------------------------------
1 |
2 | //switch cases
3 | const switch_frame = "frame/INCREMENT";
4 | const set_error = "error/NEW_ERROR";
5 |
6 | //actions
7 | const increment = (frame_index) => ({
8 | type: switch_frame,
9 | frame_index,
10 | });
11 |
12 | const new_error = (error) => ({
13 | type: set_error,
14 | error,
15 | });
16 |
17 | //thunks
18 | export const increment_frame = () => async (dispatch) => {
19 | const response = await fetch(`/api/gif/get-index`);
20 |
21 | if (response.ok) {
22 | const frame_index = await response.json();
23 | dispatch(increment(frame_index));
24 | } else {
25 | const error = {
26 | status_code: response.status,
27 | error_desc: "increment_frame failed",
28 | };
29 | dispatch(new_error(error));
30 | }
31 | };
32 |
33 | //reducer
34 | export default function reducer(state = {}, action){
35 | switch (action.type) {
36 | case switch_frame:
37 | return {
38 | ...state,
39 | frame: action.frame_index
40 | };
41 | case set_error:
42 | return { ...state, error: action.error };
43 | default:
44 | return state;
45 | }
46 | };
47 |
48 | // //apply redux middlewares to make reducer work
49 | // const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware));
50 |
51 | // const store = createStore(rootReducer, composedEnhancer);
52 |
53 | // //sends store to react entrypoint
54 | // export default store;
55 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | BuildAndDeploy:
9 | runs-on: ubuntu-20.04
10 | steps:
11 | - uses: actions/checkout@v3
12 | # https://github.com/actions/setup-node
13 | - uses: actions/setup-node@v3
14 | with:
15 | node-version: '16'
16 | cache: 'npm'
17 | cache-dependency-path: react-app/package-lock.json
18 | - name: Build React App
19 | working-directory: react-app
20 | run: npm install && npm run build
21 |
22 | # https://github.com/actions/setup-python
23 | # If a student sees this, they could speed up their builds
24 | # by moving install from docker to cached GH action runner
25 | # Have fun (after you have a passing project!) :)
26 | - uses: actions/setup-python@v3
27 | with:
28 | python-version: '3.9'
29 | cache: 'pip'
30 | - run: pip install pipenv
31 |
32 | - name: Update requirments.txt for container install
33 | run: pipenv lock -r > requirements.txt
34 |
35 | - name: Login to Heroku Container registry
36 | env:
37 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
38 | run: heroku container:login
39 |
40 | - name: Build Container and push
41 | env:
42 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
43 | run: heroku container:push -a ${{ secrets.HEROKU_APP_NAME }} web
44 |
45 | - name: Release
46 | env:
47 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
48 | run: heroku container:release -a ${{ secrets.HEROKU_APP_NAME }} web
49 |
50 | - name: Run DB Migration
51 | env:
52 | HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
53 | run: heroku run -a ${{ secrets.HEROKU_APP_NAME }} flask run db upgrade
54 |
--------------------------------------------------------------------------------
/migrations/versions/9fcfa773ef84_.py:
--------------------------------------------------------------------------------
1 | """empty message
2 |
3 | Revision ID: 9fcfa773ef84
4 | Revises:
5 | Create Date: 2022-02-26 16:15:47.236435
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '9fcfa773ef84'
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('comments',
22 | sa.Column('id', sa.Integer(), nullable=False),
23 | sa.Column('content', sa.Text(), nullable=False),
24 | sa.PrimaryKeyConstraint('id')
25 | )
26 | op.create_table('posts',
27 | sa.Column('id', sa.Integer(), nullable=False),
28 | sa.Column('content', sa.Text(), nullable=False),
29 | sa.PrimaryKeyConstraint('id')
30 | )
31 | op.create_table('subcomments',
32 | sa.Column('id', sa.Integer(), nullable=False),
33 | sa.Column('content', sa.Text(), nullable=False),
34 | sa.PrimaryKeyConstraint('id')
35 | )
36 | op.create_table('users',
37 | sa.Column('id', sa.Integer(), nullable=False),
38 | sa.Column('username', sa.String(length=40), nullable=False),
39 | sa.Column('email', sa.String(length=255), nullable=False),
40 | sa.Column('hashed_password', sa.String(length=255), nullable=False),
41 | sa.PrimaryKeyConstraint('id'),
42 | sa.UniqueConstraint('email'),
43 | sa.UniqueConstraint('username')
44 | )
45 | # ### end Alembic commands ###
46 |
47 |
48 | def downgrade():
49 | # ### commands auto generated by Alembic - please adjust! ###
50 | op.drop_table('users')
51 | op.drop_table('subcomments')
52 | op.drop_table('posts')
53 | op.drop_table('comments')
54 | # ### end Alembic commands ###
55 |
--------------------------------------------------------------------------------
/react-app/src/components/auth/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { Redirect } from 'react-router-dom';
4 | import { login } from '../../store/session';
5 |
6 | const LoginForm = () => {
7 | const [errors, setErrors] = useState([]);
8 | const [email, setEmail] = useState('');
9 | const [password, setPassword] = useState('');
10 | const user = useSelector(state => state.session.user);
11 | const dispatch = useDispatch();
12 |
13 | const onLogin = async (e) => {
14 | e.preventDefault();
15 | const data = await dispatch(login(email, password));
16 | if (data) {
17 | setErrors(data);
18 | }
19 | };
20 |
21 | const updateEmail = (e) => {
22 | setEmail(e.target.value);
23 | };
24 |
25 | const updatePassword = (e) => {
26 | setPassword(e.target.value);
27 | };
28 |
29 | if (user) {
30 | return ;
31 | }
32 |
33 | return (
34 |
62 | );
63 | };
64 |
65 | export default LoginForm;
66 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.224.3/containers/python-3
3 | {
4 | "name": "Python 3",
5 | "build": {
6 | "dockerfile": "Dockerfile",
7 | "context": "..",
8 | "args": {
9 | "VARIANT": "3.9",
10 | "NODE_VERSION": "16"
11 | }
12 | },
13 |
14 | // Set *default* container specific settings.json values on container create.
15 | "settings": {
16 | // "python.defaultInterpreterPath": "/usr/local/bin/python",
17 | // "python.linting.enabled": true,
18 | // "python.linting.pylintEnabled": true,
19 | // "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
20 | // "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
21 | // "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
22 | // "python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
23 | // "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
24 | // "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
25 | // "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
26 | // "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
27 | // "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
28 | },
29 |
30 | // Add the IDs of extensions you want installed when the container is created.
31 | "extensions": [
32 | // "ms-python.python",
33 | // "ms-python.vscode-pylance"
34 | ],
35 |
36 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
37 | "forwardPorts": [5000, 5432, 3000],
38 |
39 | // Use 'postCreateCommand' to run commands after the container is created.
40 | "postCreateCommand": "pipenv install --dev && cd react-app && npm install",
41 | // Flask python-dotenv SQLAlchemy Flask-SQLAlchemy psycopg2-binary WTForms Flask-WTF Flask-Login
42 |
43 | "postStartCommand": "sudo service postgresql start",
44 |
45 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
46 | "remoteUser": "vscode"
47 | }
48 |
--------------------------------------------------------------------------------
/react-app/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { BrowserRouter, Route, Switch } from "react-router-dom";
3 | import { useDispatch } from "react-redux";
4 |
5 | import LoginForm from "./components/auth/LoginForm";
6 | import SignUpForm from "./components/auth/SignUpForm";
7 | import ProtectedRoute from "./components/auth/ProtectedRoute";
8 | import NavBanner from "./components/NavBanner";
9 | import SplashPage from "./components/SplashPage";
10 | import UsersList from "./components/UsersList";
11 | import User from "./components/User";
12 | import Header from "./components/Header";
13 | import Posts from "./components/Posts";
14 | import Footer from "./components/Footer";
15 | import Gif from "./components/Gif";
16 |
17 | import { authenticate } from "./store/session";
18 | import { get_all_posts } from "./store/posts";
19 |
20 | function App() {
21 | const [loaded, setLoaded] = useState(false);
22 | const dispatch = useDispatch();
23 |
24 | useEffect(() => {
25 | (async () => {
26 | await dispatch(authenticate());
27 | dispatch(get_all_posts());
28 | setLoaded(true);
29 | })();
30 | }, [dispatch]);
31 |
32 | if (!loaded) {
33 | return null;
34 | }
35 |
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | );
70 | }
71 |
72 | export default App;
73 |
--------------------------------------------------------------------------------
/react-app/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/flask_backend/__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 import auth_routes, gif_routes, posts_routes, user_routes
10 |
11 | from .seeds import seed_commands
12 |
13 | from .config import Config
14 |
15 | app = Flask(__name__)
16 |
17 | # Setup login manager
18 | login = LoginManager(app)
19 | login.login_view = 'auth.unauthorized'
20 |
21 |
22 | @login.user_loader
23 | def load_user(id):
24 | return User.query.get(int(id))
25 |
26 |
27 | # Tell flask about our seed commands
28 | app.cli.add_command(seed_commands)
29 |
30 | app.config.from_object(Config)
31 | app.register_blueprint(user_routes, url_prefix='/api/users')
32 | app.register_blueprint(auth_routes, url_prefix='/api/auth')
33 | app.register_blueprint(gif_routes, url_prefix='/api/gif')
34 | app.register_blueprint(posts_routes, url_prefix='/api/posts')
35 | db.init_app(app)
36 | Migrate(app, db)
37 |
38 | # Application Security
39 | CORS(app)
40 |
41 |
42 | # Since we are deploying with Docker and Flask,
43 | # we won't be using a buildpack when we deploy to Heroku.
44 | # Therefore, we need to make sure that in production any
45 | # request made over http is redirected to https.
46 | # Well.........
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 | print("<<< in before_request, url before: >>>", request.url)
52 | url = request.url.replace('http://', 'https://', 1)
53 | print("<<< in before_request, url after: >>>", request.url)
54 | code = 301
55 | return redirect(url, code=code)
56 |
57 |
58 | @app.after_request
59 | def inject_csrf_token(response):
60 | response.set_cookie(
61 | 'csrf_token',
62 | generate_csrf(),
63 | secure=True if os.environ.get('FLASK_ENV') == 'production' else False,
64 | samesite='Strict' if os.environ.get(
65 | 'FLASK_ENV') == 'production' else None,
66 | httponly=True)
67 | return response
68 |
69 |
70 | @app.route('/', defaults={'path': ''})
71 | @app.route('/')
72 | def react_root(path):
73 | print("<<< in app.route('/'), before favicon:")
74 | if path == 'favicon.ico':
75 | print("<<< in app.route('/'), after favicon:")
76 | return app.send_static_file('favicon.ico')
77 | return app.send_static_file('index.html')
78 |
--------------------------------------------------------------------------------
/react-app/src/components/auth/SignUpForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux'
3 | import { Redirect } from 'react-router-dom';
4 | import { signUp } from '../../store/session';
5 |
6 | const SignUpForm = () => {
7 | const [errors, setErrors] = useState([]);
8 | const [username, setUsername] = useState('');
9 | const [email, setEmail] = useState('');
10 | const [password, setPassword] = useState('');
11 | const [repeatPassword, setRepeatPassword] = useState('');
12 | const user = useSelector(state => state.session.user);
13 | const dispatch = useDispatch();
14 |
15 | const onSignUp = async (e) => {
16 | e.preventDefault();
17 | if (password === repeatPassword) {
18 | const data = await dispatch(signUp(username, email, password));
19 | if (data) {
20 | setErrors(data)
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 (user) {
42 | return ;
43 | }
44 |
45 | return (
46 |
91 | );
92 | };
93 |
94 | export default SignUpForm;
95 |
--------------------------------------------------------------------------------
/flask_backend/api/auth_routes.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, session, request
2 | from flask_backend.models import User, db
3 | from flask_backend.forms import LoginForm
4 | from flask_backend.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']}
29 |
30 |
31 | @auth_routes.route('/login', methods=['POST'])
32 | def login():
33 | """
34 | Logs a user in
35 | """
36 | form = LoginForm()
37 | # Get the csrf_token from the request cookie and put it into the
38 | # form manually to validate_on_submit can be used
39 | form['csrf_token'].data = request.cookies['csrf_token']
40 | if form.validate_on_submit():
41 | # Add the user to the session, we are logged in!
42 | user = User.query.filter(User.email == form.data['email']).first()
43 | login_user(user)
44 | return user.to_dict()
45 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401
46 |
47 |
48 | @auth_routes.route('/logout')
49 | def logout():
50 | """
51 | Logs a user out
52 | """
53 | logout_user()
54 | return {'message': 'User logged out'}
55 |
56 |
57 | @auth_routes.route('/signup', methods=['POST'])
58 | def sign_up():
59 | """
60 | Creates a new user and logs them in
61 | """
62 | form = SignUpForm()
63 | form['csrf_token'].data = request.cookies['csrf_token']
64 | if form.validate_on_submit():
65 | user = User(
66 | username=form.data['username'],
67 | email=form.data['email'],
68 | password=form.data['password']
69 | )
70 | db.session.add(user)
71 | db.session.commit()
72 | login_user(user)
73 | return user.to_dict()
74 | return {'errors': validation_errors_to_error_messages(form.errors)}, 401
75 |
76 |
77 | @auth_routes.route('/unauthorized')
78 | def unauthorized():
79 | """
80 | Returns unauthorized JSON when flask-login authentication fails
81 | """
82 | return {'errors': ['Unauthorized']}, 401
83 |
--------------------------------------------------------------------------------
/react-app/src/store/session.js:
--------------------------------------------------------------------------------
1 | // constants
2 | const SET_USER = 'session/SET_USER';
3 | const REMOVE_USER = 'session/REMOVE_USER';
4 |
5 | const setUser = (user) => ({
6 | type: SET_USER,
7 | payload: user
8 | });
9 |
10 | const removeUser = () => ({
11 | type: REMOVE_USER,
12 | })
13 |
14 | const initialState = { user: null };
15 |
16 | export const authenticate = () => async (dispatch) => {
17 | const response = await fetch('/api/auth/', {
18 | headers: {
19 | 'Content-Type': 'application/json'
20 | }
21 | });
22 | if (response.ok) {
23 | const data = await response.json();
24 | if (data.errors) {
25 | return;
26 | }
27 |
28 | dispatch(setUser(data));
29 | }
30 | }
31 |
32 | export const login = (email, password) => async (dispatch) => {
33 | const response = await fetch('/api/auth/login', {
34 | method: 'POST',
35 | headers: {
36 | 'Content-Type': 'application/json'
37 | },
38 | body: JSON.stringify({
39 | email,
40 | password
41 | })
42 | });
43 |
44 |
45 | if (response.ok) {
46 | const data = await response.json();
47 | dispatch(setUser(data))
48 | return null;
49 | } else if (response.status < 500) {
50 | const data = await response.json();
51 | if (data.errors) {
52 | return data.errors;
53 | }
54 | } else {
55 | return ['An error occurred. Please try again.']
56 | }
57 |
58 | }
59 |
60 | export const logout = () => async (dispatch) => {
61 | const response = await fetch('/api/auth/logout', {
62 | headers: {
63 | 'Content-Type': 'application/json',
64 | }
65 | });
66 |
67 | if (response.ok) {
68 | dispatch(removeUser());
69 | }
70 | };
71 |
72 |
73 | export const signUp = (username, email, password) => async (dispatch) => {
74 | const response = await fetch('/api/auth/signup', {
75 | method: 'POST',
76 | headers: {
77 | 'Content-Type': 'application/json',
78 | },
79 | body: JSON.stringify({
80 | username,
81 | email,
82 | password,
83 | }),
84 | });
85 |
86 | if (response.ok) {
87 | const data = await response.json();
88 | dispatch(setUser(data))
89 | return null;
90 | } else if (response.status < 500) {
91 | const data = await response.json();
92 | if (data.errors) {
93 | return data.errors;
94 | }
95 | } else {
96 | return ['An error occurred. Please try again.']
97 | }
98 | }
99 |
100 | export default function reducer(state = initialState, action) {
101 | switch (action.type) {
102 | case SET_USER:
103 | return { user: action.payload }
104 | case REMOVE_USER:
105 | return { user: null }
106 | default:
107 | return state;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/flask_backend/api/posts.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, request
2 | from ..models import db, Post, Comment, SubComment
3 |
4 | posts_routes = Blueprint('posts', __name__)
5 |
6 |
7 |
8 | @posts_routes.route('/', methods=['GET','POST'])
9 | def create_post():
10 |
11 | if request.method == 'POST':
12 | data = request.get_json(force=True)
13 |
14 | post = Post(content=data["content"])
15 | db.session.add(post)
16 | db.session.flush()
17 | post.content = f"{post.content}: {post.id}"
18 | db.session.commit()
19 |
20 | return post.to_dict()
21 |
22 | posts = Post.query.all()
23 |
24 | return { "posts": sorted([p.to_dict() for p in posts], key=lambda p: p["id"], reverse=True) }
25 |
26 |
27 | @posts_routes.route('/delete/post/', methods=['DELETE'])
28 | def delete_post(id):
29 | post = Post.query.filter(Post.id == id).first()
30 | db.session.delete(post)
31 | db.session.commit()
32 | return f"Deleted post id {id}"
33 |
34 |
35 | #comments route handlers
36 | @posts_routes.route('/comments', methods=['POST'])
37 | def create_comment():
38 |
39 | if request.method == 'POST':
40 | data = request.get_json(force=True)
41 |
42 | comment = Comment(content=data["content"], post_id=data["post_id"])
43 | db.session.add(comment)
44 | db.session.flush()
45 | comment.content = f"{comment.content}: {comment.id}"
46 | db.session.commit()
47 |
48 | return comment.to_dict()
49 |
50 | comments = Comment.query.all()
51 |
52 | return { "comments": sorted([c.to_dict() for c in comments], key=lambda c: c["id"], reverse=True) }
53 |
54 |
55 | @posts_routes.route('/delete/comment/', methods=['DELETE'])
56 | def delete_comment(id):
57 | comment = Comment.query.filter(Comment.id == id).first()
58 | db.session.delete(comment)
59 | db.session.commit()
60 | return f"Deleted comment by id {id}"
61 |
62 |
63 | #subcomments route handlers
64 | @posts_routes.route('/subcomments', methods=['GET','POST'])
65 | def create_subcomment():
66 |
67 | if request.method == 'subcomment':
68 | data = request.get_json(force=True)
69 |
70 | subcomment = SubComment(content=data["content"], comment_id=data["comment_id"])
71 | db.session.add(subcomment)
72 | db.session.flush()
73 | subcomment.content = f"{subcomment.content}: {subcomment.id}"
74 | db.session.commit()
75 |
76 | return subcomment.to_dict()
77 |
78 | subcomments = SubComment.query.all()
79 |
80 | return { "subcomments": sorted([s.to_dict() for s in subcomments], key=lambda s: s["id"], reverse=True) }
81 |
82 |
83 | @posts_routes.route('/delete/subcomment/', methods=['DELETE'])
84 | def delete_subcomment(id):
85 | SubComment.query.filter(SubComment.id == id).delete()
86 | db.session.commit()
87 | return f"Deleted subcomment id {id}"
88 |
--------------------------------------------------------------------------------
/react-app/src/components/Posts/index.js:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 |
3 | import {
4 | create_post,
5 | get_all_posts,
6 | delete_post,
7 | create_comment,
8 | delete_comment,
9 | } from "../../store/posts";
10 | import Comments from "../Comments";
11 |
12 |
13 | export default function Posts() {
14 | const dispatch = useDispatch();
15 | const p_from_reducer = useSelector((state) => state.posts.all_posts);
16 | // const p_from_reducer = useSelector((state) => state.posts.values().sort())
17 |
18 | const handle_create_post = () => {
19 | dispatch(create_post({ content: "post id" }));
20 | };
21 |
22 | const handle_get_posts = () => {
23 | dispatch(get_all_posts());
24 | };
25 |
26 | const handle_delete_posts = (post_id) => {
27 | dispatch(delete_post(post_id));
28 | };
29 |
30 | const handle_create_comment = (post_id) => {
31 | dispatch(create_comment({ content: "comment id", post_id }));
32 | };
33 |
34 | const handle_delete_comment = (ids) => {
35 | dispatch(delete_comment(ids));
36 | };
37 |
38 | const posts = p_from_reducer
39 | ? p_from_reducer.map((post) => {
40 | const child_props = {
41 | post_id: post.id,
42 | comments: post.comments.all,
43 | handle_create_c: handle_create_comment,
44 | handle_delete_c: handle_delete_comment,
45 | };
46 |
47 | return (
48 |
49 |
50 |
{post.content}
51 | handle_delete_posts(post.id)}
55 | >
56 | delete post
57 |
58 | handle_create_comment(post.id)}
62 | >
63 | new comment
64 |
65 |
66 | {/* ------>> test these two conditional render methods <<------ */}
67 | {/* {post.comments.all.length > 0 &&
} */}
68 | {post.comments.all ?
: null}
69 |
70 | );
71 | })
72 | : null;
73 |
74 | return (
75 |
76 |
Nested Objects: CRUD for posts, comments and subcomments.
77 |
82 | get posts from backend
83 |
84 |
89 | new post
90 |
91 |
{posts}
92 |
93 | );
94 | }
95 |
--------------------------------------------------------------------------------
/react-app/src/index.css:
--------------------------------------------------------------------------------
1 | /* * {
2 | box-sizing: border-box;
3 | } */
4 |
5 | html,
6 | body,
7 | #root {
8 | height: 100%;
9 | margin: 0;
10 | font-family: "Comic Sans MS";
11 | font-family: "Lucida Sans";
12 | }
13 |
14 | #root {
15 | display: flex;
16 | flex-direction: column;
17 | }
18 |
19 | a {
20 | text-decoration: none;
21 | }
22 |
23 | a:hover {
24 | /* color: lightblue; */
25 | text-decoration: underline;
26 | }
27 |
28 | .App {
29 | text-align: center;
30 | }
31 |
32 | .App-logo {
33 | height: 40vmin;
34 | pointer-events: none;
35 | }
36 |
37 | @media (prefers-reduced-motion: no-preference) {
38 | .App-logo {
39 | animation: App-logo-spin infinite 20s linear;
40 | }
41 | }
42 |
43 | .App-link {
44 | color: #61dafb;
45 | }
46 |
47 | @keyframes App-logo-spin {
48 | from {
49 | transform: rotate(0deg);
50 | }
51 | to {
52 | transform: rotate(360deg);
53 | }
54 | }
55 |
56 | .App-header {
57 | height: 150px;
58 | display: flex;
59 | flex-direction: row;
60 | align-items: center;
61 | justify-content: space-around;
62 | background-color: #282c34;
63 | font-size: calc(10px + 2vmin);
64 | color: white;
65 | padding: 30px;
66 | }
67 |
68 | .App-header img {
69 | width: 300px;
70 | height: 200px;
71 | }
72 |
73 | .App-main {
74 | display: flex;
75 | width: 100%;
76 | flex-direction: column;
77 | align-items: center;
78 | }
79 |
80 | .gif-wrapper {
81 | display: flex;
82 | flex-direction: column;
83 | width: 400px;
84 | }
85 |
86 |
87 | .App-footer {
88 | width: 100%;
89 | background-color: #282c34;
90 | color: #fff;
91 | text-align: center;
92 | margin-top: auto;
93 | }
94 |
95 | .btn {
96 | height: 40px;
97 | border-radius: 7px;
98 | margin: 10px;
99 | }
100 |
101 | .nav-link {
102 | color: white;
103 | }
104 |
105 | .nav-banner-wrapper {
106 | display: flex;
107 | justify-content: space-around;
108 | padding: 10px;
109 | background-color: lightblue;
110 | margin-bottom: 10px;
111 | }
112 |
113 | .gif-blurb-wrap, .nesting-blurb-wrap {
114 | width: 600px;
115 | padding: 20px;
116 | }
117 |
118 | .posts-wrapper {
119 | display: flex;
120 | flex-direction: column;
121 | width: 400px;
122 | }
123 |
124 | .single-post-wrap {
125 | box-sizing: border-box;
126 | display: flex;
127 | flex-direction: column;
128 | width: 100%;
129 | margin-top: 10px;
130 | margin-bottom: 10px;
131 | }
132 |
133 | .single-post-header {
134 | box-sizing:border-box;
135 | display: flex;
136 | flex-direction: row;
137 | justify-content: space-between;
138 | align-items: center;
139 | width: 100%;
140 | height: 30px;
141 | }
142 |
143 | .btn-small {
144 | height: fit-content;
145 | border-radius: 3px;
146 | }
147 |
148 | .single-comment {
149 | width: 100%;
150 | display: flex;
151 | justify-content: space-around;
152 | margin: 5px;
153 | }
154 |
155 | .posts-list {
156 | padding: 10px;
157 | }
158 |
--------------------------------------------------------------------------------
/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(
49 | url=url, target_metadata=target_metadata, literal_binds=True
50 | )
51 |
52 | with context.begin_transaction():
53 | context.run_migrations()
54 |
55 |
56 | def run_migrations_online():
57 | """Run migrations in 'online' mode.
58 |
59 | In this scenario we need to create an Engine
60 | and associate a connection with the context.
61 |
62 | """
63 |
64 | # this callback is used to prevent an auto-migration from being generated
65 | # when there are no changes to the schema
66 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
67 | def process_revision_directives(context, revision, directives):
68 | if getattr(config.cmd_opts, 'autogenerate', False):
69 | script = directives[0]
70 | if script.upgrade_ops.is_empty():
71 | directives[:] = []
72 | logger.info('No changes in schema detected.')
73 |
74 | connectable = current_app.extensions['migrate'].db.get_engine()
75 |
76 | with connectable.connect() as connection:
77 | context.configure(
78 | connection=connection,
79 | target_metadata=target_metadata,
80 | process_revision_directives=process_revision_directives,
81 | **current_app.extensions['migrate'].configure_args
82 | )
83 |
84 | with context.begin_transaction():
85 | context.run_migrations()
86 |
87 |
88 | if context.is_offline_mode():
89 | run_migrations_offline()
90 | else:
91 | run_migrations_online()
92 |
--------------------------------------------------------------------------------
/react-app/src/store/comments.js:
--------------------------------------------------------------------------------
1 |
2 | // //switch cases
3 | // const create = "comments/CREATE";
4 | // const get = "comments/GET";
5 | // const remove = "comments/DELETE";
6 | // const set_error = "error/NEW_ERROR";
7 |
8 | // //actions
9 | // const add_comment = (comment) => ({
10 | // type: create,
11 | // comment,
12 | // });
13 |
14 | // const all_comments = (comments) => ({
15 | // type: get,
16 | // comments
17 | // })
18 |
19 | // const del_comment_action = (comment_id) => ({
20 | // type: remove,
21 | // comment_id: comment_id
22 | // })
23 |
24 | // const new_error = (error) => ({
25 | // type: set_error,
26 | // error,
27 | // });
28 |
29 | // //thunks
30 | // export const create_comment = (comment) => async (dispatch) => {
31 | // const response = await fetch(`/api/comments/`, {
32 | // method: "comment",
33 | // headers: {
34 | // 'Accept': 'application/json',
35 | // "Content-Type": "application/json",
36 | // },
37 | // body: JSON.stringify(comment)
38 | // });
39 |
40 | // if (response.ok) {
41 | // const comment = await response.json();
42 | // dispatch(add_comment(comment));
43 | // } else {
44 | // const error = {
45 | // status_code: response.status,
46 | // error_desc: "increment_frame failed",
47 | // };
48 | // dispatch(new_error(error));
49 | // }
50 | // };
51 |
52 | // export const get_all_comments = () => async (dispatch) => {
53 | // console.log("<<<< DISPATCHING GET ALL commentS >>>>")
54 |
55 | // const response = await fetch("/api/comments/")
56 | // if(response.ok){
57 | // const { comments } = await response.json();
58 | // dispatch(all_comments(comments))
59 | // } else {
60 | // const error = {
61 | // status_code: response.status,
62 | // error_desc: "increment_frame failed",
63 | // };
64 | // dispatch(new_error(error));
65 | // }
66 | // }
67 |
68 | // export const delete_comment = (comment_id) => async (dispatch) => {
69 | // console.log("delete_comment", comment_id);
70 | // const response = await fetch(`/api/comments/delete/${comment_id}`, {
71 | // method: "DELETE",
72 | // });
73 |
74 | // if (response.ok) {
75 | // dispatch(del_comment_action(comment_id));
76 | // }
77 | // }
78 |
79 |
80 | // //reducer
81 | // export default function reducer(state = {all_comments: []}, action){
82 | // switch (action.type) {
83 | // case create:
84 | // return {...state, [action.comment.id]: action.comment, all_comments: [action.comment, ...state.all_comments] }
85 |
86 | // case get:
87 | // const update_keys = (array) => {
88 | // const obj = {}
89 | // array.forEach(p => obj[p.id] = p)
90 | // return obj
91 | // }
92 | // return { ...state, ...update_keys(action.comments), all_comments: [...action.comments] }
93 |
94 | // case remove:
95 | // const newState = { ...state };
96 | // delete newState[action.comment_id];
97 | // newState.all_comments.splice(newState.all_comments.findIndex(p => p.id === action.comment_id), 1);
98 | // newState.all_comments = [...newState.all_comments]
99 | // return newState
100 |
101 | // case set_error:
102 | // return { ...state, error: action.error };
103 | // default:
104 | // return state;
105 | // }
106 | // };
107 |
--------------------------------------------------------------------------------
/react-app/src/assets/frames.js:
--------------------------------------------------------------------------------
1 | import f0 from './gif_frames_png/IMG00000.png'
2 | import f1 from './gif_frames_png/IMG00001.png'
3 | import f2 from './gif_frames_png/IMG00002.png'
4 | import f3 from './gif_frames_png/IMG00003.png'
5 | import f4 from './gif_frames_png/IMG00004.png'
6 | import f5 from './gif_frames_png/IMG00005.png'
7 | import f6 from './gif_frames_png/IMG00006.png'
8 | import f7 from './gif_frames_png/IMG00007.png'
9 | import f8 from './gif_frames_png/IMG00008.png'
10 | import f9 from './gif_frames_png/IMG00009.png'
11 | import f10 from './gif_frames_png/IMG00010.png'
12 | import f11 from './gif_frames_png/IMG00011.png'
13 | import f12 from './gif_frames_png/IMG00012.png'
14 | import f13 from './gif_frames_png/IMG00013.png'
15 | import f14 from './gif_frames_png/IMG00014.png'
16 | import f15 from './gif_frames_png/IMG00015.png'
17 | import f16 from './gif_frames_png/IMG00016.png'
18 | import f17 from './gif_frames_png/IMG00017.png'
19 | import f18 from './gif_frames_png/IMG00018.png'
20 | import f19 from './gif_frames_png/IMG00019.png'
21 | import f20 from './gif_frames_png/IMG00020.png'
22 | import f21 from './gif_frames_png/IMG00021.png'
23 | import f22 from './gif_frames_png/IMG00022.png'
24 | import f23 from './gif_frames_png/IMG00023.png'
25 | import f24 from './gif_frames_png/IMG00024.png'
26 | import f25 from './gif_frames_png/IMG00025.png'
27 | import f26 from './gif_frames_png/IMG00026.png'
28 | import f27 from './gif_frames_png/IMG00027.png'
29 | import f28 from './gif_frames_png/IMG00028.png'
30 | import f29 from './gif_frames_png/IMG00029.png'
31 | import f30 from './gif_frames_png/IMG00030.png'
32 | import f31 from './gif_frames_png/IMG00031.png'
33 | import f32 from './gif_frames_png/IMG00032.png'
34 | import f33 from './gif_frames_png/IMG00033.png'
35 | import f34 from './gif_frames_png/IMG00034.png'
36 | import f35 from './gif_frames_png/IMG00035.png'
37 | import f36 from './gif_frames_png/IMG00036.png'
38 | import f37 from './gif_frames_png/IMG00037.png'
39 | import f38 from './gif_frames_png/IMG00038.png'
40 | import f39 from './gif_frames_png/IMG00039.png'
41 | import f40 from './gif_frames_png/IMG00040.png'
42 | import f41 from './gif_frames_png/IMG00041.png'
43 | import f42 from './gif_frames_png/IMG00042.png'
44 | import f43 from './gif_frames_png/IMG00043.png'
45 | import f44 from './gif_frames_png/IMG00044.png'
46 | import f45 from './gif_frames_png/IMG00045.png'
47 | import f46 from './gif_frames_png/IMG00046.png'
48 | import f47 from './gif_frames_png/IMG00047.png'
49 | import f48 from './gif_frames_png/IMG00048.png'
50 | import f49 from './gif_frames_png/IMG00049.png'
51 | import f50 from './gif_frames_png/IMG00050.png'
52 | import f51 from './gif_frames_png/IMG00051.png'
53 | import f52 from './gif_frames_png/IMG00052.png'
54 | import f53 from './gif_frames_png/IMG00053.png'
55 | import f54 from './gif_frames_png/IMG00054.png'
56 | import f55 from './gif_frames_png/IMG00055.png'
57 | import f56 from './gif_frames_png/IMG00056.png'
58 | import f57 from './gif_frames_png/IMG00057.png'
59 | import f58 from './gif_frames_png/IMG00058.png'
60 | import f59 from './gif_frames_png/IMG00059.png'
61 | import f60 from './gif_frames_png/IMG00060.png'
62 | import f61 from './gif_frames_png/IMG00061.png'
63 |
64 | const frame_array = [
65 | f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20, f21, f22, f23, f24, f25, f26, f27, f28, f29, f30, f31, f32, f33, f34, f35, f36, f37, f38, f39, f40, f41, f42, f43, f44, f45, f46, f47, f48, f49, f50, f51, f52, f53, f54, f55, f56, f57, f58, f59, f60, f61
66 | ]
67 |
68 |
69 | export default frame_array
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Flask React Project
2 |
3 | This is the starter for the 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/appacademy-starters/python-project-starter.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 | 4. Setup your PostgreSQL user, password and database and make sure it matches your **.env** file
22 |
23 | 5. Get into your pipenv, migrate your database, seed your database, and run your flask app
24 |
25 | ```bash
26 | pipenv shell
27 | ```
28 |
29 | ```bash
30 | flask db upgrade
31 | ```
32 |
33 | ```bash
34 | flask seed all
35 | ```
36 |
37 | ```bash
38 | flask run
39 | ```
40 |
41 | 6. To run the React App in development, checkout the [README](./react-app/README.md) inside the `react-app` directory.
42 |
43 | ***
44 | *IMPORTANT!*
45 | If you add any python dependencies to your pipfiles, you'll need to regenerate your requirements.txt before deployment.
46 | You can do this by running:
47 |
48 | ```bash
49 | pipenv lock -r > requirements.txt
50 | ```
51 |
52 | *ALSO IMPORTANT!*
53 | psycopg2-binary MUST remain a dev dependency because you can't install it on apline-linux.
54 | There is a layer in the Dockerfile that will install psycopg2 (not binary) for us.
55 | ***
56 |
57 | ## Deploy to Heroku
58 |
59 | 1. Before you deploy, don't forget to run the following command in order to
60 | ensure that your production environment has all of your up-to-date
61 | dependencies. You only have to run this command when you have installed new
62 | Python packages since your last deployment, but if you aren't sure, it won't
63 | hurt to run it again.
64 |
65 | ```bash
66 | pipenv lock -r > requirements.txt
67 | ```
68 |
69 | 2. Create a new project on Heroku
70 | 3. Under Resources click "Find more add-ons" and add the add on called "Heroku Postgres"
71 | 4. Install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-command-line)
72 | 5. Run
73 |
74 | ```bash
75 | heroku login
76 | ```
77 |
78 | 6. Login to the heroku container registry
79 |
80 | ```bash
81 | heroku container:login
82 | ```
83 |
84 | 7. Update the `REACT_APP_BASE_URL` variable in the Dockerfile.
85 | This should be the full URL of your Heroku app: i.e. "https://flask-react-aa.herokuapp.com"
86 | 8. Push your docker container to heroku from the root directory of your project.
87 | (If you are using an M1 mac, follow [these steps below](#for-m1-mac-users) instead, then continue on to step 9.)
88 | This will build the Dockerfile and push the image to your heroku container registry.
89 |
90 | ```bash
91 | heroku container:push web -a {NAME_OF_HEROKU_APP}
92 | ```
93 |
94 | 9. Release your docker container to heroku
95 |
96 | ```bash
97 | heroku container:release web -a {NAME_OF_HEROKU_APP}
98 | ```
99 |
100 | 10. set up your database
101 |
102 | ```bash
103 | heroku run -a {NAME_OF_HEROKU_APP} flask db upgrade
104 | heroku run -a {NAME_OF_HEROKU_APP} flask seed all
105 | ```
106 |
107 | 11. Under Settings find "Config Vars" and add any additional/secret .env
108 | variables.
109 |
110 | 12. profit
111 |
112 | ### For M1 Mac users
113 |
114 | (Replaces **Step 8**)
115 |
116 | 1. Build image with linux platform for heroku servers. Replace
117 | {NAME_OF_HEROKU_APP} with your own tag:
118 |
119 | ```bash=
120 | docker buildx build --platform linux/amd64 -t {NAME_OF_HEROKU_APP} .
121 | ```
122 |
123 | 2. Tag your app with the url for your apps registry. Make sure to use the name
124 | of your Heroku app in the url and tag name:
125 |
126 | ```bash=2
127 | docker tag {NAME_OF_HEROKU_APP} registry.heroku.com/{NAME_OF_HEROKU_APP}/web
128 | ```
129 |
130 | 3. Use docker to push the image to the Heroku container registry:
131 |
132 | ```bash=3
133 | docker push registry.heroku.com/{NAME_OF_HEROKU_APP}/web
134 | ```
135 |
--------------------------------------------------------------------------------
/react-app/src/assets/git_frames_bmp/frameslist.gsf:
--------------------------------------------------------------------------------
1 | [Header]
2 | Type=GifSplitter Frames
3 | Version=2.0
4 | Count=62
5 | [Frames]
6 | IMG00000=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00000.bmp
7 | IMG00001=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00001.bmp
8 | IMG00002=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00002.bmp
9 | IMG00003=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00003.bmp
10 | IMG00004=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00004.bmp
11 | IMG00005=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00005.bmp
12 | IMG00006=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00006.bmp
13 | IMG00007=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00007.bmp
14 | IMG00008=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00008.bmp
15 | IMG00009=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00009.bmp
16 | IMG00010=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00010.bmp
17 | IMG00011=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00011.bmp
18 | IMG00012=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00012.bmp
19 | IMG00013=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00013.bmp
20 | IMG00014=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00014.bmp
21 | IMG00015=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00015.bmp
22 | IMG00016=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00016.bmp
23 | IMG00017=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00017.bmp
24 | IMG00018=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00018.bmp
25 | IMG00019=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00019.bmp
26 | IMG00020=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00020.bmp
27 | IMG00021=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00021.bmp
28 | IMG00022=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00022.bmp
29 | IMG00023=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00023.bmp
30 | IMG00024=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00024.bmp
31 | IMG00025=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00025.bmp
32 | IMG00026=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00026.bmp
33 | IMG00027=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00027.bmp
34 | IMG00028=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00028.bmp
35 | IMG00029=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00029.bmp
36 | IMG00030=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00030.bmp
37 | IMG00031=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00031.bmp
38 | IMG00032=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00032.bmp
39 | IMG00033=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00033.bmp
40 | IMG00034=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00034.bmp
41 | IMG00035=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00035.bmp
42 | IMG00036=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00036.bmp
43 | IMG00037=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00037.bmp
44 | IMG00038=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00038.bmp
45 | IMG00039=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00039.bmp
46 | IMG00040=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00040.bmp
47 | IMG00041=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00041.bmp
48 | IMG00042=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00042.bmp
49 | IMG00043=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00043.bmp
50 | IMG00044=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00044.bmp
51 | IMG00045=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00045.bmp
52 | IMG00046=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00046.bmp
53 | IMG00047=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00047.bmp
54 | IMG00048=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00048.bmp
55 | IMG00049=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00049.bmp
56 | IMG00050=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00050.bmp
57 | IMG00051=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00051.bmp
58 | IMG00052=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00052.bmp
59 | IMG00053=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00053.bmp
60 | IMG00054=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00054.bmp
61 | IMG00055=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00055.bmp
62 | IMG00056=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00056.bmp
63 | IMG00057=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00057.bmp
64 | IMG00058=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00058.bmp
65 | IMG00059=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00059.bmp
66 | IMG00060=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00060.bmp
67 | IMG00061=C:\Users\purce\OneDrive\Desktop\gif_image_tools_output\IMG00061.bmp
68 |
--------------------------------------------------------------------------------
/react-app/src/components/Gif/index.js:
--------------------------------------------------------------------------------
1 |
2 | import React from "react"
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { increment_frame } from '../../store/gif'
5 |
6 | import frame_array from '../../assets/frames'
7 |
8 | //lets pretend this is an object you imported or something
9 | // const bad_ref = { index: 0 }
10 |
11 | export default function Gif() {
12 |
13 | //TODO: step 1. start here using Redux. useSelector alone will cause rerenders
14 | const dispatch = useDispatch();
15 | //note: state var is a primitive; makes it guaranteed to rerender
16 | const gif_frame = useSelector((state) => state.gif?.frame?.frame_index);
17 |
18 | //edge case
19 | const image = frame_array[0]
20 |
21 | const manual_click = () => {
22 | dispatch(increment_frame());
23 | }
24 |
25 |
26 | // //TODO: step 2. the same react principles apply using local component state
27 | // //same effect but with local component state instead
28 | // // todo: note the use of a callback in useState. it fixes race conditions
29 |
30 | // const [compState, setCompState] = React.useState(0)
31 |
32 | // const manual_click = () => {
33 |
34 | // setCompState(prev => {
35 | // if(prev < frame_array.length-1){
36 | // return prev + 1
37 | // } else return 0
38 | // })
39 | // };
40 |
41 |
42 |
43 | //todo: HERE BE DRAGONS! -> using state improperly
44 | // //react component lifecycle is based on object equality. react will do a === on state to decide when to rerender
45 | // //recall the gotchas you learned when doing a === on objects
46 |
47 | // const [compState, setCompState] = React.useState(bad_ref)
48 |
49 | // const manual_click = () => {
50 | // //scenario 1:
51 | // //dont modify objects outside of react state!!
52 | // bad_ref.index = bad_ref.index + 1
53 | // setCompState(bad_ref)
54 |
55 | // //scenario 2:
56 | // // setCompState(state => {
57 | // // console.log("running useState callback!", state)
58 | // // state.index += 1
59 | // // const modified_state = state
60 | // // console.log("same object in memory? ", state === modified_state)
61 | // // return modified_state
62 | // // })
63 |
64 | // //bad reference: we are mutating an existing object
65 | // //reference in memory is the same
66 | // //react runs oldState === newState and it is true, so nothing happens
67 |
68 |
69 | // //todo: how to fix this
70 | // //copy old state and return a new reference in memory
71 | // //the logs will show the asynchronous nature of useState hook
72 | // // setCompState(state => {
73 |
74 | // // console.log("logging previous state:: ", state)
75 | // // const modified_state = {...state, index: state.index + 1}
76 | // // console.log("same object in memory? ", state === modified_state)
77 |
78 | // // return modified_state
79 | // // })
80 | // console.log("I run before the useState callback!", compState)
81 | // }
82 |
83 | // React.useEffect(() => {
84 | // console.log("useEffect runs before render so you can see the change!", compState)
85 | // }, [compState]);
86 |
87 |
88 | //TODO for good times
89 | //make it a video lol
90 | // const [vidStart, setVidStart] = React.useState(false)
91 | // const [compStateAuto, setCompStateAuto] = React.useState(0)
92 | // const [intID, setIntID] = React.useState(0)
93 | // React.useEffect(() => {
94 | // if((vidStart === true) && (intID === 0)){
95 | // const newIntID = setInterval(() => {
96 | // setCompStateAuto(prev => {
97 | // if(prev < frame_array.length-1){
98 | // return prev + 1
99 | // } else return 0
100 | // })
101 | // }, 150);
102 | // setIntID(newIntID)
103 | // } else if ((vidStart === false) && intID){
104 | // clearInterval(intID);
105 | // setIntID(0);
106 | // }
107 | // }, [vidStart, intID]);
108 |
109 |
110 |
111 | //pro tip: to correctly log async code, you need a useEffect
112 | // React.useEffect(() => {
113 | // console.log("intervalID, vidStart:: ", intID, vidStart)
114 | // }, [vidStart, intID]);
115 |
116 | // const vid_click = () => {
117 | // setVidStart(prev => !prev)
118 | // }
119 |
120 |
121 | return (
122 |
123 |
Write code to advance the frames!
124 |
145 |
manual frames:
146 |
147 | Next Frame
148 |
149 | {/*
auto frames:
150 |
151 | {vidStart ? "Stop gif" : "Start gif"}
152 | */}
153 |
154 |
155 | {/* //todo: switch up the var depending on your source */}
156 |
Using a redux useSelector! Frame: {gif_frame}
157 | {/*
Using local useState! Frame: {compState} */}
158 | {/*
Using a bad object reference! Frame: {compState.index} */}
159 |
160 | );
161 | }
162 |
--------------------------------------------------------------------------------
/react-app/src/store/posts.js:
--------------------------------------------------------------------------------
1 |
2 | //post switch cases
3 | const create = "posts/CREATE";
4 | const get = "posts/GET";
5 | const remove = "posts/DELETE";
6 | const set_error = "error/NEW_ERROR";
7 |
8 | //comment switch cases
9 | const create_c = "comment/CREATE";
10 | const get_c = "comment/GET";
11 | const remove_c = "comment/DELETE";
12 |
13 | //subcomment switch cases
14 | const create_s = "subcomment/CREATE";
15 | const get_s = "subcomment/GET";
16 | const remove_s = "subcomment/DELETE";
17 |
18 |
19 | //actions
20 | //post actions --------------------------------------------------------
21 | const add_post = (post) => ({
22 | type: create,
23 | post,
24 | });
25 |
26 | const all_posts = (posts) => ({
27 | type: get,
28 | posts
29 | })
30 |
31 | const del_post_action = (post_id) => ({
32 | type: remove,
33 | post_id: post_id
34 | })
35 |
36 | const new_error = (error) => ({
37 | type: set_error,
38 | error,
39 | });
40 |
41 | //comment actions --------------------------------------------------------
42 | const add_comment = (comment) => ({
43 | type: create_c,
44 | comment,
45 | });
46 |
47 | const all_comments = (comments) => ({
48 | type: get_c,
49 | comments
50 | })
51 |
52 | const del_comment_action = (ids) => ({
53 | type: remove_c,
54 | ...ids
55 | })
56 |
57 | //subcomment actions --------------------------------------------------------
58 | const add_subcomment = (subcomment) => ({
59 | type: create_s,
60 | subcomment,
61 | });
62 |
63 | const all_subcomments = (subcomments) => ({
64 | type: get_s,
65 | subcomments
66 | })
67 |
68 | const del_subcomment_action = (ids) => ({
69 | type: remove_s,
70 | ...ids
71 | })
72 |
73 |
74 | //thunks
75 | //posts thunks --------------------------------------------------------
76 | export const create_post = (post) => async (dispatch) => {
77 | const response = await fetch(`/api/posts/`, {
78 | method: "POST",
79 | headers: {
80 | 'Accept': 'application/json',
81 | "Content-Type": "application/json",
82 | },
83 | body: JSON.stringify(post)
84 | });
85 |
86 | if (response.ok) {
87 | const post = await response.json();
88 | dispatch(add_post(post));
89 | } else {
90 | const error = {
91 | status_code: response.status,
92 | error_desc: "increment_frame failed",
93 | };
94 | dispatch(new_error(error));
95 | }
96 | };
97 |
98 | export const get_all_posts = () => async (dispatch) => {
99 |
100 | const response = await fetch("/api/posts/")
101 | if(response.ok){
102 | const { posts } = await response.json();
103 | dispatch(all_posts(posts))
104 | } else {
105 | const error = {
106 | status_code: response.status,
107 | error_desc: "increment_frame failed",
108 | };
109 | dispatch(new_error(error));
110 | }
111 | }
112 |
113 | export const delete_post = (post_id) => async (dispatch) => {
114 | const response = await fetch(`/api/posts/delete/post/${post_id}`, {
115 | method: "DELETE",
116 | });
117 |
118 | if (response.ok) {
119 | dispatch(del_post_action(post_id));
120 | }
121 | }
122 |
123 |
124 | //comments thunks --------------------------------------------------------
125 | export const create_comment = (comment) => async (dispatch) => {
126 | const response = await fetch('/api/posts/comments', {
127 | method: "POST",
128 | headers: {
129 | 'Accept': 'application/json',
130 | "Content-Type": "application/json",
131 | },
132 | body: JSON.stringify(comment)
133 | });
134 |
135 | if (response.ok) {
136 | const comment = await response.json();
137 | dispatch(add_comment(comment));
138 | } else {
139 | const error = {
140 | status_code: response.status,
141 | error_desc: "increment_frame failed",
142 | };
143 | dispatch(new_error(error));
144 | }
145 | };
146 |
147 | export const get_all_comments = () => async (dispatch) => {
148 |
149 | const response = await fetch("/api/posts/comments")
150 | if(response.ok){
151 | const { comments } = await response.json();
152 | dispatch(all_comments(comments))
153 | } else {
154 | const error = {
155 | status_code: response.status,
156 | error_desc: "increment_frame failed",
157 | };
158 | dispatch(new_error(error));
159 | }
160 | }
161 |
162 | export const delete_comment = (ids) => async (dispatch) => {
163 | const response = await fetch(`/api/posts/delete/comment/${ids.comment_id}`, {
164 | method: "DELETE",
165 | });
166 |
167 | if (response.ok) {
168 | dispatch(del_comment_action(ids));
169 | }
170 | }
171 |
172 |
173 | //subcomments thunks --------------------------------------------------------
174 | export const create_subcomment = (subcomment) => async (dispatch) => {
175 | const response = await fetch('/api/posts/subcomments', {
176 | method: "POST",
177 | headers: {
178 | 'Accept': 'application/json',
179 | "Content-Type": "application/json",
180 | },
181 | body: JSON.stringify(subcomment)
182 | });
183 |
184 | if (response.ok) {
185 | const subcomment = await response.json();
186 | dispatch(add_subcomment(subcomment));
187 | } else {
188 | const error = {
189 | status_code: response.status,
190 | error_desc: "increment_frame failed",
191 | };
192 | dispatch(new_error(error));
193 | }
194 | };
195 |
196 | export const get_all_subcomments = () => async (dispatch) => {
197 | const response = await fetch("/api/posts/subcomments")
198 | if(response.ok){
199 | const { subcomments } = await response.json();
200 | dispatch(all_subcomments(subcomments))
201 | } else {
202 | const error = {
203 | status_code: response.status,
204 | error_desc: "increment_frame failed",
205 | };
206 | dispatch(new_error(error));
207 | }
208 | }
209 |
210 | export const delete_subcomment = (ids) => async (dispatch) => {
211 |
212 | const response = await fetch(`/api/posts/delete/subcomment/${ids.subcomment_id}`, {
213 | method: "DELETE",
214 | });
215 |
216 | if (response.ok) {
217 | dispatch(del_subcomment_action(ids));
218 | }
219 | }
220 |
221 |
222 |
223 | //reducer --------------------------------------------------------
224 | export default function reducer(state = {all_posts: []}, action){
225 | //helpers
226 | //normalize an array into object kvp's
227 | const update_keys = (array) => {
228 | const obj = {}
229 | array.forEach(i => {
230 | obj[i.id] = i
231 | if (i.hasOwnProperty("comments")){
232 | obj[i.id].comments = { ...update_keys(i.comments), all: i.comments}
233 | } else if (i.hasOwnProperty("subcomments")){
234 | obj[i.id].subcomments = { ...update_keys(i.subcomments), all: i.subcomments}
235 | }
236 | });
237 | console.log("\narray param in update_keys(), array:: ", array)
238 | console.log("\nfilled object in update_keys(), obj:: ", obj)
239 | return obj
240 | }
241 |
242 | //NOTE: whenever accessing ${state}, (previous state variable), copy it first and mutate the copy. This is critical for redux to work properly. Additionally, spread syntax is a highly performant way to make a copy of an object. The problem with spread is that the copy is shallow, so you must manually spread every level of a nested object.
243 |
244 | //prep newState for multiple cases
245 | const newState = { ...state };
246 |
247 | switch (action.type) {
248 |
249 | //NOTE: you can organize this code by create a helper reducer for comments
250 | //posts cases ================================================
251 | case create:
252 | //create structure of new object in state, not using the object returned from flask:
253 | const new_post = {
254 | id: action.post.id,
255 | content: action.post.content,
256 | comments: { all: action.post.comments}
257 | }
258 |
259 | //Normalize new object into slice of state, and insert new object into appropriate place in new array literal:
260 | return {
261 | ...state,
262 | [action.post.id]: new_post,
263 | all_posts: [new_post, ...state.all_posts]
264 | }
265 |
266 | case get:
267 |
268 | //You may need to normalize objects from database if your store is not in sync with backend. Normalize redux using a helper function:
269 | return {
270 | ...state,
271 | ...update_keys(action.posts),
272 | all_posts: [...action.posts]
273 | }
274 |
275 | case remove:
276 | //Delete existing kvp for this id. Remember to mutate the NEW state object:
277 | delete newState[action.post_id];
278 |
279 | //Mutate a COPY of the old array, find object by id and splice it out:
280 | newState.all_posts.splice(newState.all_posts.findIndex(p => p.id === action.post_id), 1);
281 |
282 | //Overwrite array key with new array value:
283 | newState.all_posts = [...newState.all_posts]
284 |
285 | return newState
286 |
287 |
288 | //comments cases ================================================
289 | case create_c: {
290 | //saving this path to a var saves space
291 | const post_id = action.comment.post_id
292 |
293 | //insert new comment into NEW array, and spread old values behind it(keep newest comments on top when component maps this):
294 | const new_array = [action.comment, ...state[post_id].comments.all]
295 |
296 | //Shape your new comment object:
297 | const new_comment = {
298 | ...action.comment,
299 | subcomments: {
300 | //NOTE: you don't need a new array literal here since array came from action and not old state.
301 | all: action.comment.subcomments
302 | }
303 | }
304 |
305 | //Return the new shape of this slice of state. Spread all old state objects into new objects. Making a deep copy of nested state can get confusing; try to organize your syntax to make it easier to keep track of where you are in the object.
306 | return {
307 | ...state,
308 | [post_id]: {
309 | ...state[post_id],
310 | comments: {
311 | ...state[post_id].comments,
312 | [action.comment.id]: new_comment,
313 | all: new_array
314 | }
315 | }
316 | }
317 | }
318 |
319 | //NOTE: we aren't using this case in our components.
320 | case get_c: {
321 | //Save yourself some space:
322 | const post_id = action.comment.post_id
323 |
324 | //Return a normalized deep copy of state:
325 | return { ...state,
326 | [post_id]: {
327 | ...state[post_id],
328 | comments: {
329 | ...update_keys(action.comments),
330 | all: action.comments
331 | }
332 | }
333 | }
334 | }
335 |
336 | case remove_c: {
337 | //saving path to object saves space. Note it is referencing the array in new state object.
338 | const c_array = newState[action.post_id].comments.all;
339 |
340 | //Delete comment from new copy of state:
341 | delete newState[action.post_id].comments[action.comment_id];
342 |
343 | //Mutate the array copy only. Remove object from array copy:
344 | c_array.splice(c_array.findIndex(c => c.id === action.comment_id), 1);
345 |
346 | //Construct the proper state shape:
347 | newState[action.post_id].comments = {
348 | ...newState[action.post_id].comments,
349 | all: [...c_array]
350 | }
351 |
352 | return newState
353 | }
354 |
355 | //subcomments cases ================================================
356 |
357 | //NOTE, YOU'LL NEED ID'S OF ALL OBJECTS TO PATH THIS FAR. WE ARE MISSING POST_ID IN SUBCOMMENT OBJECT RETURNED FROM DATABASE
358 |
359 | // case create_s: {
360 | // const post_id = action.comment.post_id
361 | // const new_array = [action.subcomment, ...state[post_id].comments.subcomments.all]
362 | // // console.log("<<<< comment structure in reducer, comment:: ", action.comment)
363 | // // console.log("<<<< key path, comments array:: ", action.comment.post_id)
364 | // // console.log("<<<< check new_array === state[post_id].comments:: ", new_array === state[post_id].comments)
365 |
366 | // return {...state, [post_id]:
367 | // {...state[post_id], comments:
368 | // {...state[post_id].comments, state[post_id].comments[action.subcomment.id]: } } }
369 | // }
370 |
371 | // case get_s: {
372 | // const post_id = action.comment.post_id
373 | // return { ...state, [state[post_id]]: {...state[post_id], ...update_keys(action.comments)}, all_comments: [...action.comments] }
374 | // }
375 |
376 | // case remove_s: {
377 | // const c_array = newState[action.post_id].comments
378 | // delete newState[action.post_id][action.comment_id];
379 | // c_array.splice(c_array.findIndex(c => c.id === action.comment_id), 1);
380 | // newState[action.post_id].comments = [...c_array]
381 | // return newState
382 | // }
383 |
384 |
385 |
386 | //error cases ================================================
387 | case set_error:
388 | return { ...state, error: action.error };
389 |
390 | default:
391 | return state;
392 | }
393 | };
394 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "9fc860d3e10029bfe21591a9a6990c841c6c5740e0e610681712a2a84f57a0c9"
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 | "index": "pypi",
25 | "version": "==1.6.5"
26 | },
27 | "click": {
28 | "hashes": [
29 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
30 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
31 | ],
32 | "index": "pypi",
33 | "version": "==7.1.2"
34 | },
35 | "flask": {
36 | "hashes": [
37 | "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55",
38 | "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9"
39 | ],
40 | "index": "pypi",
41 | "version": "==2.0.1"
42 | },
43 | "flask-cors": {
44 | "hashes": [
45 | "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16",
46 | "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"
47 | ],
48 | "index": "pypi",
49 | "version": "==3.0.8"
50 | },
51 | "flask-login": {
52 | "hashes": [
53 | "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b",
54 | "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0"
55 | ],
56 | "index": "pypi",
57 | "version": "==0.5.0"
58 | },
59 | "flask-migrate": {
60 | "hashes": [
61 | "sha256:4d42e8f861d78cb6e9319afcba5bf76062e5efd7784184dd2a1cccd9de34a702",
62 | "sha256:df9043d2050df3c0e0f6313f6b529b62c837b6033c20335e9d0b4acdf2c40e23"
63 | ],
64 | "index": "pypi",
65 | "version": "==3.0.1"
66 | },
67 | "flask-sqlalchemy": {
68 | "hashes": [
69 | "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912",
70 | "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390"
71 | ],
72 | "index": "pypi",
73 | "version": "==2.5.1"
74 | },
75 | "flask-wtf": {
76 | "hashes": [
77 | "sha256:6ff7af73458f182180906a37a783e290bdc8a3817fe4ad17227563137ca285bf",
78 | "sha256:ff177185f891302dc253437fe63081e7a46a4e99aca61dfe086fb23e54fff2dc"
79 | ],
80 | "index": "pypi",
81 | "version": "==0.15.1"
82 | },
83 | "greenlet": {
84 | "hashes": [
85 | "sha256:03f28a5ea20201e70ab70518d151116ce939b412961c33827519ce620957d44c",
86 | "sha256:06d7ac89e6094a0a8f8dc46aa61898e9e1aec79b0f8b47b2400dd51a44dbc832",
87 | "sha256:06ecb43b04480e6bafc45cb1b4b67c785e183ce12c079473359e04a709333b08",
88 | "sha256:096cb0217d1505826ba3d723e8981096f2622cde1eb91af9ed89a17c10aa1f3e",
89 | "sha256:0c557c809eeee215b87e8a7cbfb2d783fb5598a78342c29ade561440abae7d22",
90 | "sha256:0de64d419b1cb1bfd4ea544bedea4b535ef3ae1e150b0f2609da14bbf48a4a5f",
91 | "sha256:14927b15c953f8f2d2a8dffa224aa78d7759ef95284d4c39e1745cf36e8cdd2c",
92 | "sha256:16183fa53bc1a037c38d75fdc59d6208181fa28024a12a7f64bb0884434c91ea",
93 | "sha256:206295d270f702bc27dbdbd7651e8ebe42d319139e0d90217b2074309a200da8",
94 | "sha256:22002259e5b7828b05600a762579fa2f8b33373ad95a0ee57b4d6109d0e589ad",
95 | "sha256:2325123ff3a8ecc10ca76f062445efef13b6cf5a23389e2df3c02a4a527b89bc",
96 | "sha256:258f9612aba0d06785143ee1cbf2d7361801c95489c0bd10c69d163ec5254a16",
97 | "sha256:3096286a6072553b5dbd5efbefc22297e9d06a05ac14ba017233fedaed7584a8",
98 | "sha256:3d13da093d44dee7535b91049e44dd2b5540c2a0e15df168404d3dd2626e0ec5",
99 | "sha256:408071b64e52192869129a205e5b463abda36eff0cebb19d6e63369440e4dc99",
100 | "sha256:598bcfd841e0b1d88e32e6a5ea48348a2c726461b05ff057c1b8692be9443c6e",
101 | "sha256:5d928e2e3c3906e0a29b43dc26d9b3d6e36921eee276786c4e7ad9ff5665c78a",
102 | "sha256:5f75e7f237428755d00e7460239a2482fa7e3970db56c8935bd60da3f0733e56",
103 | "sha256:60848099b76467ef09b62b0f4512e7e6f0a2c977357a036de602b653667f5f4c",
104 | "sha256:6b1d08f2e7f2048d77343279c4d4faa7aef168b3e36039cba1917fffb781a8ed",
105 | "sha256:70bd1bb271e9429e2793902dfd194b653221904a07cbf207c3139e2672d17959",
106 | "sha256:76ed710b4e953fc31c663b079d317c18f40235ba2e3d55f70ff80794f7b57922",
107 | "sha256:7920e3eccd26b7f4c661b746002f5ec5f0928076bd738d38d894bb359ce51927",
108 | "sha256:7db68f15486d412b8e2cfcd584bf3b3a000911d25779d081cbbae76d71bd1a7e",
109 | "sha256:8833e27949ea32d27f7e96930fa29404dd4f2feb13cce483daf52e8842ec246a",
110 | "sha256:944fbdd540712d5377a8795c840a97ff71e7f3221d3fddc98769a15a87b36131",
111 | "sha256:9a6b035aa2c5fcf3dbbf0e3a8a5bc75286fc2d4e6f9cfa738788b433ec894919",
112 | "sha256:9bdcff4b9051fb1aa4bba4fceff6a5f770c6be436408efd99b76fc827f2a9319",
113 | "sha256:a9017ff5fc2522e45562882ff481128631bf35da444775bc2776ac5c61d8bcae",
114 | "sha256:aa4230234d02e6f32f189fd40b59d5a968fe77e80f59c9c933384fe8ba535535",
115 | "sha256:ad80bb338cf9f8129c049837a42a43451fc7c8b57ad56f8e6d32e7697b115505",
116 | "sha256:adb94a28225005890d4cf73648b5131e885c7b4b17bc762779f061844aabcc11",
117 | "sha256:b3090631fecdf7e983d183d0fad7ea72cfb12fa9212461a9b708ff7907ffff47",
118 | "sha256:b33b51ab057f8a20b497ffafdb1e79256db0c03ef4f5e3d52e7497200e11f821",
119 | "sha256:b97c9a144bbeec7039cca44df117efcbeed7209543f5695201cacf05ba3b5857",
120 | "sha256:be13a18cec649ebaab835dff269e914679ef329204704869f2f167b2c163a9da",
121 | "sha256:be9768e56f92d1d7cd94185bab5856f3c5589a50d221c166cc2ad5eb134bd1dc",
122 | "sha256:c1580087ab493c6b43e66f2bdd165d9e3c1e86ef83f6c2c44a29f2869d2c5bd5",
123 | "sha256:c35872b2916ab5a240d52a94314c963476c989814ba9b519bc842e5b61b464bb",
124 | "sha256:c70c7dd733a4c56838d1f1781e769081a25fade879510c5b5f0df76956abfa05",
125 | "sha256:c767458511a59f6f597bfb0032a1c82a52c29ae228c2c0a6865cfeaeaac4c5f5",
126 | "sha256:c87df8ae3f01ffb4483c796fe1b15232ce2b219f0b18126948616224d3f658ee",
127 | "sha256:ca1c4a569232c063615f9e70ff9a1e2fee8c66a6fb5caf0f5e8b21a396deec3e",
128 | "sha256:cc407b68e0a874e7ece60f6639df46309376882152345508be94da608cc0b831",
129 | "sha256:da862b8f7de577bc421323714f63276acb2f759ab8c5e33335509f0b89e06b8f",
130 | "sha256:dfe7eac0d253915116ed0cd160a15a88981a1d194c1ef151e862a5c7d2f853d3",
131 | "sha256:ed1377feed808c9c1139bdb6a61bcbf030c236dd288d6fca71ac26906ab03ba6",
132 | "sha256:f42ad188466d946f1b3afc0a9e1a266ac8926461ee0786c06baac6bd71f8a6f3",
133 | "sha256:f92731609d6625e1cc26ff5757db4d32b6b810d2a3363b0ff94ff573e5901f6f"
134 | ],
135 | "index": "pypi",
136 | "version": "==1.1.0"
137 | },
138 | "gunicorn": {
139 | "hashes": [
140 | "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e",
141 | "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"
142 | ],
143 | "index": "pypi",
144 | "version": "==20.1.0"
145 | },
146 | "itsdangerous": {
147 | "hashes": [
148 | "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c",
149 | "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"
150 | ],
151 | "index": "pypi",
152 | "version": "==2.0.1"
153 | },
154 | "jinja2": {
155 | "hashes": [
156 | "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4",
157 | "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"
158 | ],
159 | "index": "pypi",
160 | "version": "==3.0.1"
161 | },
162 | "mako": {
163 | "hashes": [
164 | "sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab",
165 | "sha256:aea166356da44b9b830c8023cd9b557fa856bd8b4035d6de771ca027dfc5cc6e"
166 | ],
167 | "index": "pypi",
168 | "version": "==1.1.4"
169 | },
170 | "markupsafe": {
171 | "hashes": [
172 | "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
173 | "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
174 | "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
175 | "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
176 | "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
177 | "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
178 | "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
179 | "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
180 | "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
181 | "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
182 | "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
183 | "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
184 | "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
185 | "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
186 | "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
187 | "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
188 | "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
189 | "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
190 | "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
191 | "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
192 | "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
193 | "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
194 | "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
195 | "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
196 | "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
197 | "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
198 | "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
199 | "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
200 | "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
201 | "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
202 | "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
203 | "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
204 | "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
205 | "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
206 | ],
207 | "index": "pypi",
208 | "version": "==2.0.1"
209 | },
210 | "python-dateutil": {
211 | "hashes": [
212 | "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
213 | "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
214 | ],
215 | "index": "pypi",
216 | "version": "==2.8.1"
217 | },
218 | "python-dotenv": {
219 | "hashes": [
220 | "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d",
221 | "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"
222 | ],
223 | "index": "pypi",
224 | "version": "==0.14.0"
225 | },
226 | "python-editor": {
227 | "hashes": [
228 | "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d",
229 | "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b",
230 | "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8",
231 | "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77",
232 | "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522"
233 | ],
234 | "index": "pypi",
235 | "version": "==1.0.4"
236 | },
237 | "six": {
238 | "hashes": [
239 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
240 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
241 | ],
242 | "index": "pypi",
243 | "version": "==1.15.0"
244 | },
245 | "sqlalchemy": {
246 | "hashes": [
247 | "sha256:0fb3f73e5009f5a4c9b24469939d3d57cc3ad8099a09c0cfefc47fe45ab7ffbe",
248 | "sha256:20f4bf1459548a74aade997cb045015e4d72f0fde1789b09b3bb380be28f6511",
249 | "sha256:2ace9ab2af9d7d7b0e2ff2178809941c56ab8921e38128278192a73a8a1c08a2",
250 | "sha256:311051c06f905774427b4a92dcb3924d6ee563dea3a88176da02fdfc572d0d1d",
251 | "sha256:45b0f773e195d8d51e2fd67cb5b5fb32f5a1f5e7f0752016207091bed108909a",
252 | "sha256:57ba8a96b6d058c7dcf44de8ac0955b7a787f7177a0221dd4b8016e0191268f5",
253 | "sha256:58d4f79d119010fdced6e7fd7e4b9f2230dbf55a8235d7c58b1c8207ef74791b",
254 | "sha256:5c92d9ebf4b38c22c0c9e4f203a80e101910a50dc555b4578816932015b97d7f",
255 | "sha256:6317701c06a829b066c794545512bb70b1a10a74574cfa5658a0aaf49f31aa93",
256 | "sha256:64eab458619ef759f16f0f82242813d3289e829f8557fbc7c212ca4eadf96472",
257 | "sha256:6fd1b745ade2020a1a7bf1e22536d8afe86287882c81ca5d860bdf231d5854e9",
258 | "sha256:89a5a13dcf33b7e47c7a9404a297c836965a247c7f076a0fe0910cae2bee5ce2",
259 | "sha256:8cba69545246d16c6d2a12ce45865947cbdd814bacddf2e532fdd4512e70728c",
260 | "sha256:8f1e7f4de05c15d6b46af12f3cf0c2552f2940d201a49926703249a62402d851",
261 | "sha256:9014fd1d8aebcb4eb6bc69a382dd149200e1d5924412b1d08b4443f6c1ce526f",
262 | "sha256:9133635edcec1e7fbfc16eba5dc2b5b3b11818d25b7a57cfcbfa8d3b3e9594fd",
263 | "sha256:93ba458b3c279581288a10a55df2aa6ac3509882228fcbad9d9d88069f899337",
264 | "sha256:942ca49b7ec7449d2473a6587825c55ad99534ddfc4eee249dd42be3cc1aa8c9",
265 | "sha256:95a9fd0a11f89a80d8815418eccba034f3fec8ea1f04c41b6b8decc5c95852e9",
266 | "sha256:96d3d4a7ead376d738775a1fa9786dc17a31975ec664cea284e53735c79a5686",
267 | "sha256:9c0945c79cbe507b49524e31a4bb8700060bbccb60bb553df6432e176baff3d5",
268 | "sha256:a34a7fd3353ee61a1dca72fc0c3e38d4e56bdc2c343e712f60a8c70acd4ef5bf",
269 | "sha256:c6efc7477551ba9ce632d5c3b448b7de0277c86005eec190a1068fcc7115fd0e",
270 | "sha256:cefd44faca7c57534503261f6fab49bd47eb9c2945ee0bab09faaa8cb047c24f",
271 | "sha256:d04160462f874eaa4d88721a0d5ecca8ebf433616801efe779f252ef87b0e216",
272 | "sha256:d3cf5f543d048a7c8da500133068c5c90c97a2c4bf0c027928a85028a519f33d",
273 | "sha256:d7b21a4b62921cf6dca97e8f9dea1fbe2432aebbb09895a2bd4f527105af41a4",
274 | "sha256:ddbce8fe4d0190db21db602e38aaf4c158c540b49f1ef7475323ec682a9fbf2d",
275 | "sha256:e2761b925fda550debfd5a8bc3cef9debc9a23c6a280429c4ec3a07c35c6b4b3",
276 | "sha256:fa05a77662c23226c9ec031638fd90ae767009e05cd092b948740f09d10645f0"
277 | ],
278 | "index": "pypi",
279 | "version": "==1.4.19"
280 | },
281 | "werkzeug": {
282 | "hashes": [
283 | "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42",
284 | "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8"
285 | ],
286 | "index": "pypi",
287 | "version": "==2.0.1"
288 | },
289 | "wtforms": {
290 | "hashes": [
291 | "sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c",
292 | "sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"
293 | ],
294 | "index": "pypi",
295 | "version": "==2.3.3"
296 | }
297 | },
298 | "develop": {
299 | "psycopg2-binary": {
300 | "hashes": [
301 | "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
302 | "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
303 | "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
304 | "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6",
305 | "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
306 | "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
307 | "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
308 | "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056",
309 | "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
310 | "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
311 | "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
312 | "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679",
313 | "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83",
314 | "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
315 | "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
316 | "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
317 | "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2",
318 | "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd",
319 | "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859",
320 | "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1",
321 | "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25",
322 | "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152",
323 | "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf",
324 | "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f",
325 | "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729",
326 | "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71",
327 | "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66",
328 | "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4",
329 | "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449",
330 | "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
331 | "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
332 | "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c",
333 | "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb",
334 | "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4",
335 | "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"
336 | ],
337 | "index": "pypi",
338 | "version": "==2.8.6"
339 | }
340 | }
341 | }
342 |
--------------------------------------------------------------------------------