├── .gitignore ├── LICENSE ├── README.md ├── backend ├── __init__.py ├── database.py ├── main.py ├── models.py ├── schemas.py └── services.py └── frontend ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── index.html ├── manifest.json └── robots.txt └── src ├── App.jsx ├── components ├── ErrorMessage.jsx ├── Header.jsx ├── LeadModal.jsx ├── Login.jsx ├── Register.jsx └── Table.jsx ├── context └── UserContext.jsx └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | database.db 132 | 133 | /node_modules 134 | 135 | /build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Francis Ali 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-fastapi -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sixfwa/react-fastapi/8150226c6a91dc8df9518a5354b02af4976a4ed7/backend/__init__.py -------------------------------------------------------------------------------- /backend/database.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as _sql 2 | import sqlalchemy.ext.declarative as _declarative 3 | import sqlalchemy.orm as _orm 4 | 5 | DATABASE_URL = "sqlite:///./database.db" 6 | 7 | engine = _sql.create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) 8 | 9 | SessionLocal = _orm.sessionmaker(autocommit=False, autoflush=False, bind=engine) 10 | 11 | Base = _declarative.declarative_base() 12 | -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import fastapi as _fastapi 3 | import fastapi.security as _security 4 | 5 | import sqlalchemy.orm as _orm 6 | 7 | import services as _services, schemas as _schemas 8 | 9 | app = _fastapi.FastAPI() 10 | 11 | 12 | @app.post("/api/users") 13 | async def create_user( 14 | user: _schemas.UserCreate, db: _orm.Session = _fastapi.Depends(_services.get_db) 15 | ): 16 | db_user = await _services.get_user_by_email(user.email, db) 17 | if db_user: 18 | raise _fastapi.HTTPException(status_code=400, detail="Email already in use") 19 | 20 | user = await _services.create_user(user, db) 21 | 22 | return await _services.create_token(user) 23 | 24 | 25 | @app.post("/api/token") 26 | async def generate_token( 27 | form_data: _security.OAuth2PasswordRequestForm = _fastapi.Depends(), 28 | db: _orm.Session = _fastapi.Depends(_services.get_db), 29 | ): 30 | user = await _services.authenticate_user(form_data.username, form_data.password, db) 31 | 32 | if not user: 33 | raise _fastapi.HTTPException(status_code=401, detail="Invalid Credentials") 34 | 35 | return await _services.create_token(user) 36 | 37 | 38 | @app.get("/api/users/me", response_model=_schemas.User) 39 | async def get_user(user: _schemas.User = _fastapi.Depends(_services.get_current_user)): 40 | return user 41 | 42 | 43 | @app.post("/api/leads", response_model=_schemas.Lead) 44 | async def create_lead( 45 | lead: _schemas.LeadCreate, 46 | user: _schemas.User = _fastapi.Depends(_services.get_current_user), 47 | db: _orm.Session = _fastapi.Depends(_services.get_db), 48 | ): 49 | return await _services.create_lead(user=user, db=db, lead=lead) 50 | 51 | 52 | @app.get("/api/leads", response_model=List[_schemas.Lead]) 53 | async def get_leads( 54 | user: _schemas.User = _fastapi.Depends(_services.get_current_user), 55 | db: _orm.Session = _fastapi.Depends(_services.get_db), 56 | ): 57 | return await _services.get_leads(user=user, db=db) 58 | 59 | 60 | @app.get("/api/leads/{lead_id}", status_code=200) 61 | async def get_lead( 62 | lead_id: int, 63 | user: _schemas.User = _fastapi.Depends(_services.get_current_user), 64 | db: _orm.Session = _fastapi.Depends(_services.get_db), 65 | ): 66 | return await _services.get_lead(lead_id, user, db) 67 | 68 | 69 | @app.delete("/api/leads/{lead_id}", status_code=204) 70 | async def delete_lead( 71 | lead_id: int, 72 | user: _schemas.User = _fastapi.Depends(_services.get_current_user), 73 | db: _orm.Session = _fastapi.Depends(_services.get_db), 74 | ): 75 | await _services.delete_lead(lead_id, user, db) 76 | return {"message", "Successfully Deleted"} 77 | 78 | 79 | @app.put("/api/leads/{lead_id}", status_code=200) 80 | async def update_lead( 81 | lead_id: int, 82 | lead: _schemas.LeadCreate, 83 | user: _schemas.User = _fastapi.Depends(_services.get_current_user), 84 | db: _orm.Session = _fastapi.Depends(_services.get_db), 85 | ): 86 | await _services.update_lead(lead_id, lead, user, db) 87 | return {"message", "Successfully Updated"} 88 | 89 | 90 | @app.get("/api") 91 | async def root(): 92 | return {"message": "Awesome Leads Manager"} -------------------------------------------------------------------------------- /backend/models.py: -------------------------------------------------------------------------------- 1 | import datetime as _dt 2 | 3 | import sqlalchemy as _sql 4 | import sqlalchemy.orm as _orm 5 | import passlib.hash as _hash 6 | 7 | import database as _database 8 | 9 | 10 | class User(_database.Base): 11 | __tablename__ = "users" 12 | id = _sql.Column(_sql.Integer, primary_key=True, index=True) 13 | email = _sql.Column(_sql.String, unique=True, index=True) 14 | hashed_password = _sql.Column(_sql.String) 15 | 16 | leads = _orm.relationship("Lead", back_populates="owner") 17 | 18 | def verify_password(self, password: str): 19 | return _hash.bcrypt.verify(password, self.hashed_password) 20 | 21 | 22 | class Lead(_database.Base): 23 | __tablename__ = "leads" 24 | id = _sql.Column(_sql.Integer, primary_key=True, index=True) 25 | owner_id = _sql.Column(_sql.Integer, _sql.ForeignKey("users.id")) 26 | first_name = _sql.Column(_sql.String, index=True) 27 | last_name = _sql.Column(_sql.String, index=True) 28 | email = _sql.Column(_sql.String, index=True) 29 | company = _sql.Column(_sql.String, index=True, default="") 30 | note = _sql.Column(_sql.String, default="") 31 | date_created = _sql.Column(_sql.DateTime, default=_dt.datetime.utcnow) 32 | date_last_updated = _sql.Column(_sql.DateTime, default=_dt.datetime.utcnow) 33 | 34 | owner = _orm.relationship("User", back_populates="leads") 35 | -------------------------------------------------------------------------------- /backend/schemas.py: -------------------------------------------------------------------------------- 1 | import datetime as _dt 2 | 3 | import pydantic as _pydantic 4 | 5 | 6 | class _UserBase(_pydantic.BaseModel): 7 | email: str 8 | 9 | 10 | class UserCreate(_UserBase): 11 | hashed_password: str 12 | 13 | class Config: 14 | orm_mode = True 15 | 16 | 17 | class User(_UserBase): 18 | id: int 19 | 20 | class Config: 21 | orm_mode = True 22 | 23 | 24 | class _LeadBase(_pydantic.BaseModel): 25 | first_name: str 26 | last_name: str 27 | email: str 28 | company: str 29 | note: str 30 | 31 | 32 | class LeadCreate(_LeadBase): 33 | pass 34 | 35 | 36 | class Lead(_LeadBase): 37 | id: int 38 | owner_id: int 39 | date_created: _dt.datetime 40 | date_last_updated: _dt.datetime 41 | 42 | class Config: 43 | orm_mode = True 44 | -------------------------------------------------------------------------------- /backend/services.py: -------------------------------------------------------------------------------- 1 | import fastapi as _fastapi 2 | import fastapi.security as _security 3 | import jwt as _jwt 4 | import datetime as _dt 5 | import sqlalchemy.orm as _orm 6 | import passlib.hash as _hash 7 | 8 | import database as _database, models as _models, schemas as _schemas 9 | 10 | oauth2schema = _security.OAuth2PasswordBearer(tokenUrl="/api/token") 11 | 12 | JWT_SECRET = "myjwtsecret" 13 | 14 | 15 | def create_database(): 16 | return _database.Base.metadata.create_all(bind=_database.engine) 17 | 18 | 19 | def get_db(): 20 | db = _database.SessionLocal() 21 | try: 22 | yield db 23 | finally: 24 | db.close() 25 | 26 | 27 | async def get_user_by_email(email: str, db: _orm.Session): 28 | return db.query(_models.User).filter(_models.User.email == email).first() 29 | 30 | 31 | async def create_user(user: _schemas.UserCreate, db: _orm.Session): 32 | user_obj = _models.User( 33 | email=user.email, hashed_password=_hash.bcrypt.hash(user.hashed_password) 34 | ) 35 | db.add(user_obj) 36 | db.commit() 37 | db.refresh(user_obj) 38 | return user_obj 39 | 40 | 41 | async def authenticate_user(email: str, password: str, db: _orm.Session): 42 | user = await get_user_by_email(db=db, email=email) 43 | 44 | if not user: 45 | return False 46 | 47 | if not user.verify_password(password): 48 | return False 49 | 50 | return user 51 | 52 | 53 | async def create_token(user: _models.User): 54 | user_obj = _schemas.User.from_orm(user) 55 | 56 | token = _jwt.encode(user_obj.dict(), JWT_SECRET) 57 | 58 | return dict(access_token=token, token_type="bearer") 59 | 60 | 61 | async def get_current_user( 62 | db: _orm.Session = _fastapi.Depends(get_db), 63 | token: str = _fastapi.Depends(oauth2schema), 64 | ): 65 | try: 66 | payload = _jwt.decode(token, JWT_SECRET, algorithms=["HS256"]) 67 | user = db.query(_models.User).get(payload["id"]) 68 | except: 69 | raise _fastapi.HTTPException( 70 | status_code=401, detail="Invalid Email or Password" 71 | ) 72 | 73 | return _schemas.User.from_orm(user) 74 | 75 | 76 | async def create_lead(user: _schemas.User, db: _orm.Session, lead: _schemas.LeadCreate): 77 | lead = _models.Lead(**lead.dict(), owner_id=user.id) 78 | db.add(lead) 79 | db.commit() 80 | db.refresh(lead) 81 | return _schemas.Lead.from_orm(lead) 82 | 83 | 84 | async def get_leads(user: _schemas.User, db: _orm.Session): 85 | leads = db.query(_models.Lead).filter_by(owner_id=user.id) 86 | 87 | return list(map(_schemas.Lead.from_orm, leads)) 88 | 89 | 90 | async def _lead_selector(lead_id: int, user: _schemas.User, db: _orm.Session): 91 | lead = ( 92 | db.query(_models.Lead) 93 | .filter_by(owner_id=user.id) 94 | .filter(_models.Lead.id == lead_id) 95 | .first() 96 | ) 97 | 98 | if lead is None: 99 | raise _fastapi.HTTPException(status_code=404, detail="Lead does not exist") 100 | 101 | return lead 102 | 103 | 104 | async def get_lead(lead_id: int, user: _schemas.User, db: _orm.Session): 105 | lead = await _lead_selector(lead_id=lead_id, user=user, db=db) 106 | 107 | return _schemas.Lead.from_orm(lead) 108 | 109 | 110 | async def delete_lead(lead_id: int, user: _schemas.User, db: _orm.Session): 111 | lead = await _lead_selector(lead_id, user, db) 112 | 113 | db.delete(lead) 114 | db.commit() 115 | 116 | async def update_lead(lead_id: int, lead: _schemas.LeadCreate, user: _schemas.User, db: _orm.Session): 117 | lead_db = await _lead_selector(lead_id, user, db) 118 | 119 | lead_db.first_name = lead.first_name 120 | lead_db.last_name = lead.last_name 121 | lead_db.email = lead.email 122 | lead_db.company = lead.company 123 | lead_db.note = lead.note 124 | lead_db.date_last_updated = _dt.datetime.utcnow() 125 | 126 | db.commit() 127 | db.refresh(lead_db) 128 | 129 | return _schemas.Lead.from_orm(lead_db) 130 | 131 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea/ 3 | .vscode/ 4 | node_modules/ 5 | build 6 | .DS_Store 7 | *.tgz 8 | my-app* 9 | template/src/__tests__/__snapshots__/ 10 | lerna-debug.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | /.changelog 15 | .npm/ 16 | yarn.lock -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 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 | "bulma": "^0.9.3", 10 | "moment": "^2.29.1", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-scripts": "4.0.3", 14 | "web-vitals": "^1.1.2" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "proxy": "http://localhost:8000" 41 | } 42 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | Awesome Leads Manager 24 | 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from "react"; 2 | 3 | import Register from "./components/Register"; 4 | import Login from "./components/Login"; 5 | import Header from "./components/Header"; 6 | import Table from "./components/Table"; 7 | import { UserContext } from "./context/UserContext"; 8 | 9 | const App = () => { 10 | const [message, setMessage] = useState(""); 11 | const [token] = useContext(UserContext); 12 | 13 | const getWelcomeMessage = async () => { 14 | const requestOptions = { 15 | method: "GET", 16 | headers: { 17 | "Content-Type": "application/json", 18 | }, 19 | }; 20 | const response = await fetch("/api", requestOptions); 21 | const data = await response.json(); 22 | 23 | if (!response.ok) { 24 | console.log("something messed up"); 25 | } else { 26 | setMessage(data.message); 27 | } 28 | }; 29 | 30 | useEffect(() => { 31 | getWelcomeMessage(); 32 | }, []); 33 | 34 | return ( 35 | <> 36 |
37 |
38 |
39 |
40 | {!token ? ( 41 |
42 | 43 |
44 | ) : ( 45 | 46 | )} 47 | 48 |
49 | 50 | 51 | ); 52 | }; 53 | 54 | export default App; 55 | -------------------------------------------------------------------------------- /frontend/src/components/ErrorMessage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ErrorMessage = ({ message }) => ( 4 |

{message}

5 | ); 6 | 7 | export default ErrorMessage; 8 | -------------------------------------------------------------------------------- /frontend/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | 3 | import { UserContext } from "../context/UserContext"; 4 | 5 | const Header = ({ title }) => { 6 | const [token, setToken] = useContext(UserContext); 7 | 8 | const handleLogout = () => { 9 | setToken(null); 10 | }; 11 | 12 | return ( 13 |
14 |

{title}

15 | {token && ( 16 | 19 | )} 20 |
21 | ); 22 | }; 23 | 24 | export default Header; 25 | -------------------------------------------------------------------------------- /frontend/src/components/LeadModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | const LeadModal = ({ active, handleModal, token, id, setErrorMessage }) => { 4 | const [firstName, setFirstName] = useState(""); 5 | const [lastName, setLastName] = useState(""); 6 | const [company, setCompany] = useState(""); 7 | const [email, setEmail] = useState(""); 8 | const [note, setNote] = useState(""); 9 | 10 | useEffect(() => { 11 | const getLead = async () => { 12 | const requestOptions = { 13 | method: "GET", 14 | headers: { 15 | "Content-Type": "application/json", 16 | Authorization: "Bearer " + token, 17 | }, 18 | }; 19 | const response = await fetch(`/api/leads/${id}`, requestOptions); 20 | 21 | if (!response.ok) { 22 | setErrorMessage("Could not get the lead"); 23 | } else { 24 | const data = await response.json(); 25 | setFirstName(data.first_name); 26 | setLastName(data.last_name); 27 | setCompany(data.company); 28 | setEmail(data.email); 29 | setNote(data.note); 30 | } 31 | }; 32 | 33 | if (id) { 34 | getLead(); 35 | } 36 | }, [id, token]); 37 | 38 | const cleanFormData = () => { 39 | setFirstName(""); 40 | setLastName(""); 41 | setCompany(""); 42 | setEmail(""); 43 | setNote(""); 44 | }; 45 | 46 | const handleCreateLead = async (e) => { 47 | e.preventDefault(); 48 | const requestOptions = { 49 | method: "POST", 50 | headers: { 51 | "Content-Type": "application/json", 52 | Authorization: "Bearer " + token, 53 | }, 54 | body: JSON.stringify({ 55 | first_name: firstName, 56 | last_name: lastName, 57 | company: company, 58 | email: email, 59 | note: note, 60 | }), 61 | }; 62 | const response = await fetch("/api/leads", requestOptions); 63 | if (!response.ok) { 64 | setErrorMessage("Something went wrong when creating lead"); 65 | } else { 66 | cleanFormData(); 67 | handleModal(); 68 | } 69 | }; 70 | 71 | const handleUpdateLead = async (e) => { 72 | e.preventDefault(); 73 | const requestOptions = { 74 | method: "PUT", 75 | headers: { 76 | "Content-Type": "application/json", 77 | Authorization: "Bearer " + token, 78 | }, 79 | body: JSON.stringify({ 80 | first_name: firstName, 81 | last_name: lastName, 82 | company: company, 83 | email: email, 84 | note: note, 85 | }), 86 | }; 87 | const response = await fetch(`/api/leads/${id}`, requestOptions); 88 | if (!response.ok) { 89 | setErrorMessage("Something went wrong when updating lead"); 90 | } else { 91 | cleanFormData(); 92 | handleModal(); 93 | } 94 | }; 95 | 96 | return ( 97 |
98 |
99 |
100 |
101 |

102 | {id ? "Update Lead" : "Create Lead"} 103 |

104 |
105 |
106 |
107 |
108 | 109 |
110 | setFirstName(e.target.value)} 115 | className="input" 116 | required 117 | /> 118 |
119 |
120 |
121 | 122 |
123 | setLastName(e.target.value)} 128 | className="input" 129 | required 130 | /> 131 |
132 |
133 |
134 | 135 |
136 | setCompany(e.target.value)} 141 | className="input" 142 | /> 143 |
144 |
145 |
146 | 147 |
148 | setEmail(e.target.value)} 153 | className="input" 154 | /> 155 |
156 |
157 |
158 | 159 |
160 | setNote(e.target.value)} 165 | className="input" 166 | /> 167 |
168 |
169 | 170 |
171 |
172 | {id ? ( 173 | 176 | ) : ( 177 | 180 | )} 181 | 184 |
185 |
186 |
187 | ); 188 | }; 189 | 190 | export default LeadModal; 191 | -------------------------------------------------------------------------------- /frontend/src/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | 3 | import ErrorMessage from "./ErrorMessage"; 4 | import { UserContext } from "../context/UserContext"; 5 | 6 | const Login = () => { 7 | const [email, setEmail] = useState(""); 8 | const [password, setPassword] = useState(""); 9 | const [errorMessage, setErrorMessage] = useState(""); 10 | const [, setToken] = useContext(UserContext); 11 | 12 | const submitLogin = async () => { 13 | const requestOptions = { 14 | method: "POST", 15 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 16 | body: JSON.stringify( 17 | `grant_type=&username=${email}&password=${password}&scope=&client_id=&client_secret=` 18 | ), 19 | }; 20 | 21 | const response = await fetch("/api/token", requestOptions); 22 | const data = await response.json(); 23 | 24 | if (!response.ok) { 25 | setErrorMessage(data.detail); 26 | } else { 27 | setToken(data.access_token); 28 | } 29 | }; 30 | 31 | const handleSubmit = (e) => { 32 | e.preventDefault(); 33 | submitLogin(); 34 | }; 35 | 36 | return ( 37 |
38 |
39 |

Login

40 |
41 | 42 |
43 | setEmail(e.target.value)} 48 | className="input" 49 | required 50 | /> 51 |
52 |
53 |
54 | 55 |
56 | setPassword(e.target.value)} 61 | className="input" 62 | required 63 | /> 64 |
65 |
66 | 67 |
68 | 71 | 72 |
73 | ); 74 | }; 75 | 76 | export default Login; 77 | -------------------------------------------------------------------------------- /frontend/src/components/Register.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react"; 2 | 3 | import { UserContext } from "../context/UserContext"; 4 | import ErrorMessage from "./ErrorMessage"; 5 | 6 | const Register = () => { 7 | const [email, setEmail] = useState(""); 8 | const [password, setPassword] = useState(""); 9 | const [confirmationPassword, setConfirmationPassword] = useState(""); 10 | const [errorMessage, setErrorMessage] = useState(""); 11 | const [, setToken] = useContext(UserContext); 12 | 13 | const submitRegistration = async () => { 14 | const requestOptions = { 15 | method: "POST", 16 | headers: { "Content-Type": "application/json" }, 17 | body: JSON.stringify({ email: email, hashed_password: password }), 18 | }; 19 | 20 | const response = await fetch("/api/users", requestOptions); 21 | const data = await response.json(); 22 | 23 | if (!response.ok) { 24 | setErrorMessage(data.detail); 25 | } else { 26 | setToken(data.access_token); 27 | } 28 | }; 29 | 30 | const handleSubmit = (e) => { 31 | e.preventDefault(); 32 | if (password === confirmationPassword && password.length > 5) { 33 | submitRegistration(); 34 | } else { 35 | setErrorMessage( 36 | "Ensure that the passwords match and greater than 5 characters" 37 | ); 38 | } 39 | }; 40 | 41 | return ( 42 |
43 |
44 |

Register

45 |
46 | 47 |
48 | setEmail(e.target.value)} 53 | className="input" 54 | required 55 | /> 56 |
57 |
58 |
59 | 60 |
61 | setPassword(e.target.value)} 66 | className="input" 67 | required 68 | /> 69 |
70 |
71 |
72 | 73 |
74 | setConfirmationPassword(e.target.value)} 79 | className="input" 80 | required 81 | /> 82 |
83 |
84 | 85 |
86 | 89 | 90 |
91 | ); 92 | }; 93 | 94 | export default Register; 95 | -------------------------------------------------------------------------------- /frontend/src/components/Table.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from "react"; 2 | import moment from "moment"; 3 | 4 | import ErrorMessage from "./ErrorMessage"; 5 | import LeadModal from "./LeadModal"; 6 | import { UserContext } from "../context/UserContext"; 7 | 8 | const Table = () => { 9 | const [token] = useContext(UserContext); 10 | const [leads, setLeads] = useState(null); 11 | const [errorMessage, setErrorMessage] = useState(""); 12 | const [loaded, setLoaded] = useState(false); 13 | const [activeModal, setActiveModal] = useState(false); 14 | const [id, setId] = useState(null); 15 | 16 | const handleUpdate = async (id) => { 17 | setId(id); 18 | setActiveModal(true); 19 | }; 20 | 21 | const handleDelete = async (id) => { 22 | const requestOptions = { 23 | method: "DELETE", 24 | headers: { 25 | "Content-Type": "application/json", 26 | Authorization: "Bearer " + token, 27 | }, 28 | }; 29 | const response = await fetch(`/api/leads/${id}`, requestOptions); 30 | if (!response.ok) { 31 | setErrorMessage("Failed to delete lead"); 32 | } 33 | 34 | getLeads(); 35 | }; 36 | 37 | const getLeads = async () => { 38 | const requestOptions = { 39 | method: "GET", 40 | headers: { 41 | "Content-Type": "application/json", 42 | Authorization: "Bearer " + token, 43 | }, 44 | }; 45 | const response = await fetch("/api/leads", requestOptions); 46 | if (!response.ok) { 47 | setErrorMessage("Something went wrong. Couldn't load the leads"); 48 | } else { 49 | const data = await response.json(); 50 | setLeads(data); 51 | setLoaded(true); 52 | } 53 | }; 54 | 55 | useEffect(() => { 56 | getLeads(); 57 | }, []); 58 | 59 | const handleModal = () => { 60 | setActiveModal(!activeModal); 61 | getLeads(); 62 | setId(null); 63 | }; 64 | 65 | return ( 66 | <> 67 | 74 | 80 | 81 | {loaded && leads ? ( 82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | {leads.map((lead) => ( 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 117 | 118 | ))} 119 | 120 |
First NameLast NameCompanyEmailNoteLast UpdatedActions
{lead.first_name}{lead.last_name}{lead.company}{lead.email}{lead.note}{moment(lead.date_last_updated).format("MMM Do YY")} 104 | 110 | 116 |
121 | ) : ( 122 |

Loading

123 | )} 124 | 125 | ); 126 | }; 127 | 128 | export default Table; 129 | -------------------------------------------------------------------------------- /frontend/src/context/UserContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useEffect, useState } from "react"; 2 | 3 | export const UserContext = createContext(); 4 | 5 | export const UserProvider = (props) => { 6 | const [token, setToken] = useState(localStorage.getItem("awesomeLeadsToken")); 7 | 8 | useEffect(() => { 9 | const fetchUser = async () => { 10 | const requestOptions = { 11 | method: "GET", 12 | headers: { 13 | "Content-Type": "application/json", 14 | Authorization: "Bearer " + token, 15 | }, 16 | }; 17 | 18 | const response = await fetch("/api/users/me", requestOptions); 19 | 20 | if (!response.ok) { 21 | setToken(null); 22 | } 23 | localStorage.setItem("awesomeLeadsToken", token); 24 | }; 25 | fetchUser(); 26 | }, [token]); 27 | 28 | return ( 29 | 30 | {props.children} 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "bulma/css/bulma.min.css"; 4 | import App from "./App"; 5 | 6 | import { UserProvider } from "./context/UserContext"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | --------------------------------------------------------------------------------