├── LICENSE ├── README.md ├── chapter2 ├── cars_data.csv ├── commands_used_in_the_book.txt └── download_links.txt ├── chapter3 ├── chapter3_body2.py ├── chapter3_custom_status_code.py ├── chapter3_first_endpoint.py ├── chapter3_form_data.py ├── chapter3_header.py ├── chapter3_path.py ├── chapter3_pydantic.py ├── chapter3_pydantic_corrected.py ├── chapter3_query_string.py ├── chapter3_raw_request.py ├── chapter3_request_body.py ├── chapter3_restrict_path.py ├── chapter3_types.py ├── chapter3_upload.py ├── chapter3_wrong_path_order.py └── setup_python.txt ├── chapter4 ├── cars │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── components │ │ │ ├── Card.js │ │ │ └── Header.js │ │ ├── data.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ ├── reportWebVitals.js │ │ └── setupTests.js │ └── tailwind.config.js ├── instructions.txt └── users │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.js │ ├── components │ │ └── Header.js │ ├── index.css │ └── index.js │ └── tailwind.config.js ├── chapter5 ├── backend │ ├── .gitignore │ ├── Procfile │ ├── importScript.py │ ├── main.py │ ├── models.py │ ├── requirements.txt │ ├── routers │ │ ├── __init__.py │ │ └── cars.py │ └── sample_data.csv └── instructions.txt ├── chapter6 ├── Instructions.txt └── frontend │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── favicon.ico │ ├── images │ │ ├── back.jpg │ │ └── pexels-alexgtacar-1592384.jpg │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.js │ ├── App.test.js │ ├── components │ │ ├── Card.jsx │ │ ├── Footer.jsx │ │ ├── FormInput.jsx │ │ ├── Header.jsx │ │ ├── Layout.jsx │ │ ├── Loading.jsx │ │ └── Lorem.jsx │ ├── index.css │ ├── index.js │ ├── pages │ │ ├── Car.jsx │ │ ├── Cars.jsx │ │ └── NewCar.jsx │ ├── reportWebVitals.js │ └── setupTests.js │ └── tailwind.config.js ├── chapter7 ├── backend │ ├── .gitignore │ ├── Instructions.txt │ ├── authentication.py │ ├── importScript.py │ ├── main.py │ ├── models.py │ ├── reqs.txt │ └── routers │ │ ├── __init__.py │ │ ├── cars.py │ │ └── users.py └── frontend │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── pexels-johnmark-smith-280783.jpg │ └── robots.txt │ ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── components │ │ ├── Admin.jsx │ │ ├── Card.jsx │ │ ├── Footer.jsx │ │ ├── Header.jsx │ │ ├── HomePage.jsx │ │ ├── Layout.jsx │ │ ├── Login.jsx │ │ ├── NewCar.jsx │ │ ├── NotFound.jsx │ │ ├── Protected.jsx │ │ ├── Register.jsx │ │ └── RequireAuthentication.jsx │ ├── context │ │ └── AuthProvider.js │ ├── hooks │ │ └── useAuth.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js │ └── tailwind.config.js ├── chapter8 ├── backend │ ├── .gitignore │ ├── Procfile │ ├── authentication.py │ ├── main.py │ ├── models.py │ ├── requirements.txt │ └── routers │ │ ├── __init__.py │ │ ├── cars.py │ │ └── users.py └── frontend │ └── next-cars │ ├── .gitignore │ ├── README.md │ ├── components │ ├── Card.js │ ├── Footer.jsx │ └── Header.jsx │ ├── context │ └── AuthContext.js │ ├── hooks │ └── useAuth.js │ ├── middleware.js │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.js │ ├── account │ │ ├── login.jsx │ │ ├── logout.jsx │ │ └── register.jsx │ ├── api │ │ ├── login.js │ │ ├── logout.js │ │ └── user.js │ ├── cars │ │ ├── [id].jsx │ │ ├── add.jsx │ │ └── index.js │ └── index.js │ ├── postcss.config.js │ ├── public │ ├── favicon.ico │ └── vercel.svg │ ├── styles │ └── globals.css │ └── tailwind.config.js └── chapter9 ├── backend ├── .gitignore ├── cars.json ├── filteredCars.csv ├── importScript.py ├── main.py ├── models.py ├── random_forest_pipe.joblib ├── requirements.txt ├── routers │ ├── __init__.py │ └── cars.py └── utils │ ├── __init__.py │ ├── report.py │ ├── report_query.py │ └── send_report.py └── frontend ├── .gitignore ├── README.md ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.js ├── components │ ├── BrandCount.jsx │ ├── BrandValue.jsx │ ├── Card.jsx │ ├── CarsDropdown.jsx │ ├── Dashboard.jsx │ ├── Footer.jsx │ ├── Header.jsx │ ├── Home.jsx │ ├── Layout.jsx │ ├── ModelCount.jsx │ └── Report.jsx ├── index.css └── index.js └── tailwind.config.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Packt 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 | 2 | 3 | 4 | # Full Stack FastAPI, React, and MongoDB 5 | Full Stack FastAPI, React, and MongoDB, published by Packt 6 | 7 | Full Stack FastAPI, React, and MongoDB 8 | 9 | This is the code repository for [Full Stack FastAPI, React, and MongoDB,](https://www.packtpub.com/product/full-stack-fastAPI-react-and-mongoDB/9781803231822), published by Packt. 10 | 11 | **Build Python web applications with the FARM stack** 12 | 13 | ## What is this book about? 14 | 15 | This book is for web developers and analysts who want to include the power of a modern asynchronous Python framework, a flexible data store and a powerful UI library with the combination of two of the most important programming languages today in their web development toolkit. Beginners in the field of information presentation will also find this book helpful. You must have a beginner-level understanding of Python, JavaScript, and HTML and CSS to get the most out of this book. 16 | 17 | This book covers the following exciting features: 18 | 19 | * Discover the flexibility of the FARM stack 20 | * Implement complete JWT authentication with FastAPI 21 | * Explore the various Python drivers for MongoDB 22 | * Discover the problems that React libraries solve 23 | * Build simple and medium web applications with the FARM stack 24 | 25 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1803231823) today! 26 | 27 | https://www.packtpub.com/ 29 | 30 | 31 | ## Instructions and Navigations 32 | All of the project files are organized into folders. For example, Chapter2. 33 | 34 | The code will look like the following: 35 | 36 | ``` 37 | class APIError(Exception): 38 | def __init__(self, status_code: int, code: str) -> None: 39 | self.status_code = status_code 40 | self.code = code 41 | ``` 42 | 43 | **Following is what you need for this book:** 44 | 45 | If you need to develop web applications quickly, where do you turn? Enter the FARM stack. The FARM stack combines the power of the Python ecosystem with REST and MongoDB and makes building web applications easy and fast.This book is a fast-paced, concise, and hands-on beginner’s guide that will equip you with the skills you need to quickly build web applications by diving just deep enough into the intricacies of the stack's components. 46 | 47 | With the following software and hardware list you can run all code files present in the book (Chapter 2-9). 48 | 49 | We also provide a PDF file that has color images of the screenshots/diagrams used in this book. [Click here to download it](https://packt.link/18OWu). 50 | 51 | 52 | ### Related products 53 | * Building Python Web APIs with FastAPI [[Packt]](https://www.packtpub.com/product/building-web-apis-with-fastapi-and-python/9781801076630) [[Amazon]](https://www.amazon.com/Building-Python-APIs-FastAPI-high-performance/dp/1801076634) 54 | 55 | * React Projects - Second Edition [[Packt]](https://www.packtpub.com/product/react-projects-second-edition/9781801070638) [[Amazon]](https://www.amazon.com/React-Projects-cross-platform-professional-developer/dp/1801070636) 56 | 57 | ## Errata 58 | * Page 68 (Subtitle : How does FastAPI speak REST?) :-The output should have been "Hello FastAPI" as per the code of file chapter3_first_endpoint.py. But it is mentioned as "Success" in the output image i.e Figure 3.1. 59 | 60 | ## Get to Know the Author 61 | **Marko Aleksendrić** is a graduate of the University of Belgrade, Serbia, with a Ph.D. and also a Master’s degree in control engineering. He is a self-taught full-stack developer and former scientist and works as an analyst in a trade promotion agency. He started his programming journey with Visual Basic and Fortran 77 for numeric simulations. Upon discovering Python (version 2.3), he started using it for all kinds of automation tasks: building reporting pipelines and exporting measurement data from instruments into Excel or similar user-friendly reporting tools. 62 | ### Download a free PDF 63 | 64 | If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost.
Simply click on the link to claim your free PDF.
65 |

https://packt.link/free-ebook/9781803231822

-------------------------------------------------------------------------------- /chapter2/commands_used_in_the_book.txt: -------------------------------------------------------------------------------- 1 | mongo - start the shell 2 | show dbs - show the databases 3 | use carsDB - select the database 4 | show collections - show the collections in the selected database 5 | 6 | db.cars.find() - select * documents 7 | db.cars.find({year:2019}) - select documents where year=2019 8 | db.cars.find({year:{'$gt':2015},price:{$lt:7000},brand:'Ford'}).pretty() - se;ect cars where the year is greater thatn 2015 price is less than 7000 and the brand is Ford 9 | db.cars.find({brand:'Ford',make:'Fiesta'},{year:1,km:1,_id:0}).sort({'year':1}).limit(5) - select cars sort by year and limit to 5 results 10 | db.insertOne({'brand':'Magic Car','make':'Green Dragon', 'year':1200}) - insert one car into database 11 | .cars.insertMany([{brand:'Magic Car',make:'Yellow Dragon',year:1200},{brand:'Magic Car',make:'Red Dragon',legs:4}]) - insert many 12 | db.cars.updateOne({make:'Fiesta'},{$set:{'Salesman':'Marko'}}) - update one document 13 | db.cars.updateMany({make:'Fiesta'},{$inc:{price:-200}}) - update many documents 14 | db.cars.deleteMany({brand:'Magic Car'}) - delete many documents 15 | db.cars.drop() - drop collection 16 | 17 | let fiesta_cars = db.cars.find({'make':'Fiesta'}) - cursor creation 18 | db.cars.find({year:2015},{brand:1,make:1,year:1,_id:0,price:1}).sort({price:1}).limit(5) - projection sorting and limiting 19 | 20 | db.cars.aggregate([{$match: {brand:"Fiat"}}]) - aggregation - match 21 | db.cars.aggregate([ 22 | {$match: {brand:"Fiat"}}, 23 | {$group:{_id:{model:"$make"},avgPrice: { $avg: "$price"} }} 24 | ]) - aggregation: match, group, aggregation function (average) 25 | 26 | db.cars.aggregate([ 27 | {$match:{brand:"Opel"}}, 28 | {$project:{_id:0,price:1,year:1,fullName: 29 | {$concat:["$make"," ","$brand"]}}}, 30 | {$group:{_id:{make:"$fullName"},avgPrice:{$avg:"$price"} }}, 31 | {$sort: {avgPrice: -1}}, 32 | {$limit: 10} 33 | ]).pretty() - aggregation pipeline 34 | 35 | {$project:{_id:0,price:1,year:1,fullName:{$concat:["$make"," ","$brand"]}}} - projection and concatenation 36 | -------------------------------------------------------------------------------- /chapter2/download_links.txt: -------------------------------------------------------------------------------- 1 | https://www.mongodb.com/docs/manual/installation/ MongoDB installation download and guides 2 | https://www.mongodb.com/try/download/community MongoDB Community Edition 3 | https://www.mongodb.com/try/download/database-tools MongoDB database tools 4 | https://www.mongodb.com/docs/compass/current/install/ - MongoDB Compass download and guide 5 | https://www.mongodb.com/atlas/database - MongoDB Atlas, cloud database 6 | 7 | -------------------------------------------------------------------------------- /chapter3/chapter3_body2.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Body 2 | from pydantic import BaseModel 3 | 4 | 5 | class InsertUser(BaseModel): 6 | username: str 7 | name: str 8 | 9 | 10 | class InsertCar(BaseModel): 11 | brand: str 12 | model: str 13 | year: int 14 | 15 | 16 | app = FastAPI() 17 | 18 | 19 | @app.post("/car/user") 20 | async def new_car_model(car: InsertCar, user: InsertUser, code: int = Body(None)): 21 | return {"car": car, "user": user, "code": code} 22 | 23 | 24 | """ 25 | Example of input for REST client - Insomnia, HTTPie 26 | 27 | { 28 | "car":{ 29 | "brand":"some brand", 30 | "model":"some model", 31 | "year":2002 32 | }, 33 | "user":{ 34 | "username":"Mike", 35 | "name":"Michael Jones" 36 | } 37 | } 38 | """ 39 | -------------------------------------------------------------------------------- /chapter3/chapter3_custom_status_code.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, status, HTTPException 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class InsertCar(BaseModel): 7 | brand: str 8 | model: str 9 | year: int 10 | 11 | 12 | app = FastAPI() 13 | 14 | @app.get("/", status_code=status.HTTP_208_ALREADY_REPORTED) 15 | async def raw_fa_response(): 16 | return { 17 | "message":"fastapi response" 18 | } 19 | 20 | 21 | @app.post("/carsmodel") 22 | async def new_car_model(car:InsertCar): 23 | if car.year>2022: 24 | raise HTTPException( 25 | status.HTTP_406_NOT_ACCEPTABLE, 26 | detail="The car doesn’t exist yet!" 27 | ) 28 | return { 29 | "message":car 30 | } 31 | -------------------------------------------------------------------------------- /chapter3/chapter3_first_endpoint.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/") 6 | async def root(): 7 | return {"message": "Success!"} 8 | 9 | @app.post("/") 10 | async def post_root(): 11 | return {"message": "Post request success"} 12 | 13 | 14 | 15 | # uvicorn chapter_3_first_endpoint:app --reload -------------------------------------------------------------------------------- /chapter3/chapter3_form_data.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Form, File, UploadFile 2 | app = FastAPI() 3 | 4 | @app.post("/upload") 5 | async def upload(file:UploadFile=File(...), brand:str=Form(...), model:str=Form(...)): 6 | 7 | return { 8 | "brand": brand, 9 | "model": model, 10 | "file_name":file.filename} 11 | -------------------------------------------------------------------------------- /chapter3/chapter3_header.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Header 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/headers") 6 | async def read_headers(user_agent: str | None = Header(None)): 7 | 8 | return {"User-Agent": user_agent} 9 | -------------------------------------------------------------------------------- /chapter3/chapter3_path.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/car/{id}") 6 | async def root(id): 7 | 8 | return {"car_id":id} 9 | 10 | 11 | @app.get("/carh/{id}") 12 | async def hinted_car_id(id:int): 13 | 14 | return {"car_id":id} 15 | -------------------------------------------------------------------------------- /chapter3/chapter3_pydantic.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List 3 | from pydantic import BaseModel, ValidationError 4 | 5 | 6 | class Fuel(str, Enum): 7 | PETROL = 'PETROL' 8 | DIESEL = 'DIESEL' 9 | LPG = 'LPG' 10 | 11 | class Car(BaseModel): 12 | brand: str 13 | model: str 14 | year: int 15 | fuel: Fuel 16 | countries: List[str] 17 | 18 | car = Car( 19 | brand="Lancia", 20 | model="Musa", 21 | fuel="PETROL", 22 | year="2006", 23 | countries=["Italy","France"] 24 | ) 25 | 26 | print(car.json()) 27 | 28 | try: 29 | invalid_car = Car( 30 | brand="Lancia", 31 | fuel="PETROL", 32 | year="something", 33 | countries=["Italy","France"] 34 | ) 35 | except ValidationError as e: 36 | print(e) 37 | -------------------------------------------------------------------------------- /chapter3/chapter3_pydantic_corrected.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import List 3 | from pydantic import BaseModel, ValidationError 4 | 5 | 6 | class Fuel(str, Enum): 7 | PETROL = 'PETROL' 8 | DIESEL = 'DIESEL' 9 | LPG = 'LPG' 10 | 11 | class Car(BaseModel): 12 | brand: str 13 | model: str 14 | year: int 15 | fuel: Fuel 16 | countries: List[str] 17 | 18 | car = Car( 19 | brand="Lancia", 20 | model="Musa", 21 | fuel="PETROL", 22 | year="2006", 23 | countries=["Italy","France"] 24 | ) 25 | 26 | print(car.json()) 27 | 28 | try: 29 | invalid_car = Car( 30 | brand="Lancia", 31 | fuel="PETROL", 32 | year="something", 33 | countries=["Italy","France"] 34 | ) 35 | except ValidationError as e: 36 | print(e) 37 | -------------------------------------------------------------------------------- /chapter3/chapter3_query_string.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/cars/price") 6 | async def cars_by_price(min_price:int=0, max_price:int=100000): 7 | return{"Message":f"Listing cars with prices between {min_price} and {max_price}"} 8 | -------------------------------------------------------------------------------- /chapter3/chapter3_raw_request.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/cars") 6 | async def raw_request(request:Request): 7 | return { 8 | "message":request.base_url, 9 | "all":dir(request) 10 | } -------------------------------------------------------------------------------- /chapter3/chapter3_request_body.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Body 2 | from pydantic import BaseModel 3 | 4 | # the following is necessary for handling the Body, which comes in a dictionary 5 | from typing import Dict 6 | 7 | 8 | class InsertCar(BaseModel): 9 | brand: str 10 | model: str 11 | year: int 12 | 13 | 14 | # the InsertCar model is NOT used here, so everything just flows through the endpoint 15 | 16 | app = FastAPI() 17 | 18 | 19 | @app.post("/cars") 20 | async def new_car(data: Dict = Body(...)): 21 | print(data) 22 | return {"message": data} 23 | -------------------------------------------------------------------------------- /chapter3/chapter3_restrict_path.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from fastapi import FastAPI, Path 3 | 4 | app = FastAPI() 5 | class AccountType(str, Enum): 6 | FREE = "free" 7 | PRO = "pro" 8 | 9 | @app.get("/account/{acc_type}/{months}") 10 | async def account( acc_type:AccountType, months:int = Path(..., ge=3,le=12)): 11 | 12 | return { 13 | "message":"Account created", 14 | "account_type":acc_type, 15 | "months":months 16 | } 17 | -------------------------------------------------------------------------------- /chapter3/chapter3_types.py: -------------------------------------------------------------------------------- 1 | def annotated_function(name: str, age: int)->str: 2 | return f"Your name is {name.upper()} and you are {age} years old!" 3 | 4 | print(annotated_function(name="marko", age=99)) -------------------------------------------------------------------------------- /chapter3/chapter3_upload.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from fastapi import FastAPI, Form, File, UploadFile 3 | 4 | app = FastAPI() 5 | 6 | @app.post("/upload") 7 | async def upload(picture:UploadFile=File(...), brand:str=Form(...), model:str=Form(...)): 8 | 9 | with open("saved_file.png", "wb") as buffer: 10 | shutil.copyfileobj(picture.file, buffer) 11 | return { 12 | "brand": brand, 13 | "model": model, 14 | "file_name":picture.filename 15 | } 16 | -------------------------------------------------------------------------------- /chapter3/chapter3_wrong_path_order.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | app = FastAPI() 4 | 5 | @app.get("/user/{id}") 6 | async def user(id:int): 7 | return {"User_id":id} 8 | 9 | @app.get("/user/me") 10 | async def user(): 11 | return {"User_id":"This is me!"} 12 | -------------------------------------------------------------------------------- /chapter3/setup_python.txt: -------------------------------------------------------------------------------- 1 | Python download page: https://www.python.org/downloads/ 2 | Git download page: https://git-scm.com/downloads 3 | Visual Studio Code download: https://code.visualstudio.com/download 4 | Cmder (console emulator for Windows) download: https://github.com/cmderdev/cmder 5 | Postman REST Client download: https://www.postman.com/ 6 | Insomnia REST Client download: https://insomnia.rest/ 7 | HTTPie REST Client: https://httpie.io/ 8 | 9 | 10 | $ python -m venv venv - create a virtual environment called "venv" 11 | - navigate to /venv/Scripts 12 | $ activate - to activate the virtual environment 13 | 14 | $ (venv) pip install httpie - install httpie 15 | $ (venv) pip install fastapi uvicorn - install FastAPI and Uvicorn packages 16 | $ (venv) pip install pydantic - install Pydantic (although it should already be installed with FastAPI!) 17 | 18 | $ (venv) python chapter3_pydantic.py - running the examples in the Python interpreter 19 | 20 | $ (venv) uvicorn chapter_3_first_endpoint:app --reload - starting the server - filename:instance_name 21 | 22 | $ (venv) (venv) λ http "http://localhost:8000/" - testing with httpie 23 | 24 | $ (venv) pip install python-multipart - install Python Multipart for handling form data -------------------------------------------------------------------------------- /chapter4/cars/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /chapter4/cars/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 your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may 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 | -------------------------------------------------------------------------------- /chapter4/cars/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cars", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^12.1.5", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^18.0.0", 10 | "react-dom": "^18.0.0", 11 | "react-scripts": "5.0.0", 12 | "web-vitals": "^2.1.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "autoprefixer": "^10.4.4", 40 | "postcss": "^8.4.12", 41 | "tailwindcss": "^3.0.23" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /chapter4/cars/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /chapter4/cars/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter4/cars/public/favicon.ico -------------------------------------------------------------------------------- /chapter4/cars/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /chapter4/cars/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter4/cars/public/logo192.png -------------------------------------------------------------------------------- /chapter4/cars/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter4/cars/public/logo512.png -------------------------------------------------------------------------------- /chapter4/cars/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 | -------------------------------------------------------------------------------- /chapter4/cars/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /chapter4/cars/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /chapter4/cars/src/App.js: -------------------------------------------------------------------------------- 1 | import Header from "./components/Header"; 2 | import Card from "./components/Card"; 3 | import {useState} from 'react' 4 | 5 | function App() { 6 | 7 | const data = [ 8 | {brand:"Fiat", color:"green", model:"500L", price:7000, "year":2020,"id":1}, 9 | {brand:"Peugeot", color:"red", model:"5008", price:8000, "year":2018,"id":2}, 10 | {brand:"Volkswagen", color:"white", model:"Golf 7", price:8500, "year":2019,"id":3}, 11 | {brand:"Fiat", color:"green", model:"Tipo", price:10000, "year":2019,"id":4}, 12 | {brand:"Kia", color:"black", model:"Ceed", price:6000, "year":2010,"id":5}, 13 | {brand:"Volkswagen", color:"white", model:"Golf 7", price:8500, "year":2019,"id":15}, 14 | {brand:"Fiat", color:"gray", model:"Ritmo", price:300, "year":1990,"id":21} 15 | ] 16 | 17 | const [budget, setBudget] = useState(4000) 18 | const onChangeHandler = event=>setBudget(event.target.value) 19 | 20 | return ( 21 |
22 |
23 |
Your current budget is: {budget}
24 |
25 | {data.map( 26 | (el)=>{ 27 | 28 | return ( 29 | (el.price 30 | ) 31 | } 32 | )} 33 |
34 |
35 | 36 | 37 |
38 |
39 | ); 40 | } 41 | export default App; 42 | -------------------------------------------------------------------------------- /chapter4/cars/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /chapter4/cars/src/components/Card.js: -------------------------------------------------------------------------------- 1 | const Card = ({car}) => { 2 | let {brand, price, model, year, color} = car 3 | return ( 4 |
5 |
{brand} {model}
6 |
Year: {year}
7 |
Price: {price}
8 |
Color: {color}
9 |
10 | ) 11 | } 12 | export default Card -------------------------------------------------------------------------------- /chapter4/cars/src/components/Header.js: -------------------------------------------------------------------------------- 1 | const Header = () => { 2 | return ( 3 |
4 |

Cars Sales App

5 |
6 | ) 7 | } 8 | export default Header -------------------------------------------------------------------------------- /chapter4/cars/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /chapter4/cars/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /chapter4/cars/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter4/cars/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /chapter4/cars/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /chapter4/cars/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./src/**/*.{js,jsx,ts,tsx}", 4 | ], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | } -------------------------------------------------------------------------------- /chapter4/instructions.txt: -------------------------------------------------------------------------------- 1 | install Node.js and npm: https://nodejs.org/en/download/ 2 | 3 | 4 | npx create-react-app cars - creates a React app called cars in the current directory 5 | npx create-react-app users - creates a React app called cars in the current directory 6 | npm start - starts the development server 7 | 8 | npm install -D tailwindcss postcss@latest autoprefixer - install packages necessary for Tailwind integration 9 | 10 | npm tailwind init -p - initialize the Tailwind configuration file 11 | 12 | edit the file index.css: 13 | @tailwind base; 14 | @tailwind components; 15 | @tailwind utilities; 16 | 17 | 18 | -------------------------------------------------------------------------------- /chapter4/users/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /chapter4/users/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 your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may 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 | -------------------------------------------------------------------------------- /chapter4/users/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "users", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.3.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "react-scripts": "5.0.1", 12 | "web-vitals": "^2.1.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "autoprefixer": "^10.4.7", 40 | "postcss": "^8.4.14", 41 | "tailwindcss": "^3.1.6" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /chapter4/users/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /chapter4/users/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter4/users/public/favicon.ico -------------------------------------------------------------------------------- /chapter4/users/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /chapter4/users/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter4/users/public/logo192.png -------------------------------------------------------------------------------- /chapter4/users/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter4/users/public/logo512.png -------------------------------------------------------------------------------- /chapter4/users/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 | -------------------------------------------------------------------------------- /chapter4/users/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /chapter4/users/src/App.js: -------------------------------------------------------------------------------- 1 | import Header from "./components/Header"; 2 | import {useState, useEffect} from 'react' 3 | 4 | function App() { 5 | 6 | let [users, setUsers] = useState([]) 7 | let [page, setPage] = useState(1) 8 | 9 | useEffect(()=>{ 10 | fetch(`https://reqres.in/api/users?page=${page}`) 11 | .then(response=>response.json()) 12 | .then(json=>setUsers(json['data'])) 13 | },[page]) 14 | 15 | 16 | return ( 17 |
18 |
19 | 20 |
    21 | {users&&users.map(el=>{ 22 | return ( 23 |
  • {el.email}
  • 24 | ) 25 | })} 26 |
27 |
28 | ); 29 | } 30 | export default App; -------------------------------------------------------------------------------- /chapter4/users/src/components/Header.js: -------------------------------------------------------------------------------- 1 | const Header = () => { 2 | return ( 3 |
4 |

Cars Sales App

5 |
6 | ) 7 | } 8 | export default Header -------------------------------------------------------------------------------- /chapter4/users/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /chapter4/users/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | 15 | -------------------------------------------------------------------------------- /chapter4/users/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./src/**/*.{js,jsx,ts,tsx}", 4 | ], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | } -------------------------------------------------------------------------------- /chapter5/backend/.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | __pycache__/ 3 | .env 4 | .vercel 5 | .vscode/settings.json 6 | -------------------------------------------------------------------------------- /chapter5/backend/Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn main:app --host 0.0.0.0 --port=$PORT -------------------------------------------------------------------------------- /chapter5/backend/importScript.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from fastapi.encoders import jsonable_encoder 3 | 4 | 5 | 6 | # dotenv environment variables 7 | from dotenv import dotenv_values 8 | 9 | from models import CarBase 10 | 11 | config = dotenv_values(".env") 12 | 13 | # 14 | 15 | # read csv 16 | with open("sample_data.csv",encoding='utf-8') as f: 17 | csv_reader = csv.DictReader(f) 18 | name_records = list(csv_reader) 19 | 20 | 21 | 22 | # Mongo db - we do not need Motor here 23 | from pymongo import MongoClient 24 | client = MongoClient() 25 | 26 | 27 | 28 | client = MongoClient(config['DB_URL']) 29 | db = client[config['DB_NAME']] 30 | cars = db[config['COLLECTION_NAME']] 31 | 32 | 33 | for rec in name_records[51:250]: 34 | 35 | try: 36 | rec['cm3'] = int(rec['cm3']) 37 | rec['price'] = int(rec['price']) 38 | rec['year'] = int(rec['year']) 39 | 40 | if rec['price']>1000: 41 | car = jsonable_encoder(CarBase(**rec)) 42 | print("Inserting:",car) 43 | cars.insert_one(car) 44 | 45 | except ValueError: 46 | pass 47 | -------------------------------------------------------------------------------- /chapter5/backend/main.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | 3 | import uvicorn 4 | 5 | from fastapi import FastAPI 6 | from fastapi.middleware.cors import CORSMiddleware 7 | 8 | from motor.motor_asyncio import AsyncIOMotorClient 9 | 10 | from routers.cars import router as cars_router 11 | 12 | 13 | DB_URL = config("DB_URL", cast=str) 14 | DB_NAME = config("DB_NAME", cast=str) 15 | 16 | 17 | # define origins 18 | origins = ["*"] 19 | 20 | # instantiate the app 21 | app = FastAPI() 22 | 23 | # add CORS middleware 24 | app.add_middleware( 25 | CORSMiddleware, 26 | allow_origins=origins, 27 | allow_credentials=True, 28 | allow_methods=["*"], 29 | allow_headers=["*"], 30 | ) 31 | 32 | 33 | @app.on_event("startup") 34 | async def startup_db_client(): 35 | app.mongodb_client = AsyncIOMotorClient(DB_URL) 36 | app.mongodb = app.mongodb_client[DB_NAME] 37 | 38 | 39 | @app.on_event("shutdown") 40 | async def shutdown_db_client(): 41 | app.mongodb_client.close() 42 | 43 | 44 | app.include_router(cars_router, prefix="/cars", tags=["cars"]) 45 | 46 | if __name__ == "__main__": 47 | uvicorn.run("main:app", reload=True) 48 | -------------------------------------------------------------------------------- /chapter5/backend/models.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | from pydantic import Field, BaseModel 3 | from typing import Optional 4 | 5 | 6 | class PyObjectId(ObjectId): 7 | @classmethod 8 | def __get_validators__(cls): 9 | yield cls.validate 10 | 11 | @classmethod 12 | def validate(cls, v): 13 | if not ObjectId.is_valid(v): 14 | raise ValueError("Invalid objectid") 15 | return ObjectId(v) 16 | 17 | @classmethod 18 | def __modify_schema__(cls, field_schema): 19 | field_schema.update(type="string") 20 | 21 | class MongoBaseModel(BaseModel): 22 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 23 | 24 | class Config: 25 | json_encoders = {ObjectId: str} 26 | 27 | class CarBase(MongoBaseModel): 28 | 29 | brand: str = Field(..., min_length=3) 30 | make: str = Field(..., min_length=3) 31 | year: int = Field(..., gt=1975, lt=2023) 32 | price: int = Field(...) 33 | km: int = Field(...) 34 | cm3: int = Field(...) 35 | 36 | class CarUpdate(MongoBaseModel): 37 | price: Optional[int] = None 38 | 39 | class CarDB(CarBase): 40 | pass -------------------------------------------------------------------------------- /chapter5/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | dnspython 2 | fastapi 3 | motor 4 | PythonDNS 5 | uvicorn 6 | python-decouple 7 | -------------------------------------------------------------------------------- /chapter5/backend/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter5/backend/routers/__init__.py -------------------------------------------------------------------------------- /chapter5/backend/routers/cars.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, List, Optional 2 | 3 | from fastapi import APIRouter, Request, Body, status, HTTPException 4 | from fastapi.encoders import jsonable_encoder 5 | from fastapi.responses import JSONResponse 6 | 7 | 8 | from models import CarBase, CarDB, CarUpdate 9 | 10 | 11 | router = APIRouter() 12 | 13 | @router.get("/", response_description="List all cars") 14 | async def list_all_cars( 15 | request: Request, 16 | min_price: int=0, 17 | max_price:int=100000, 18 | brand: Optional[str] = None, 19 | page:int=1, 20 | ) -> List[CarDB]: 21 | 22 | RESULTS_PER_PAGE = 25 23 | skip = (page-1)*RESULTS_PER_PAGE 24 | 25 | 26 | query = {"price":{"$lt":max_price, "$gt":min_price}} 27 | if brand: 28 | query["brand"] = brand 29 | 30 | full_query = request.app.mongodb['cars1'].find(query).sort("_id",-1).skip(skip).limit(RESULTS_PER_PAGE) 31 | 32 | results = [CarDB(**raw_car) async for raw_car in full_query] 33 | 34 | # this is also possible 35 | # results = await full_query.to_list(1000) 36 | 37 | return results 38 | 39 | # create new car 40 | @router.post("/", response_description="Add new car") 41 | async def create_car(request: Request, car: CarBase = Body(...)): 42 | car = jsonable_encoder(car) 43 | 44 | new_car = await request.app.mongodb["cars1"].insert_one(car) 45 | created_car = await request.app.mongodb["cars1"].find_one( 46 | {"_id": new_car.inserted_id} 47 | ) 48 | 49 | return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_car) 50 | 51 | # get car by ID 52 | @router.get("/{id}", response_description="Get a single car") 53 | async def show_car(id: str, request: Request): 54 | if (car := await request.app.mongodb["cars1"].find_one({"_id": id})) is not None: 55 | return CarDB(**car) 56 | raise HTTPException(status_code=404, detail=f"Car with {id} not found") 57 | 58 | @router.patch("/{id}", response_description="Update car") 59 | async def update_task(id: str, request: Request, car: CarUpdate = Body(...)): 60 | 61 | await request.app.mongodb['cars1'].update_one( 62 | {"_id": id}, {"$set": car.dict(exclude_unset=True)} 63 | ) 64 | 65 | if (car := await request.app.mongodb['cars1'].find_one({"_id": id})) is not None: 66 | return CarDB(**car) 67 | 68 | raise HTTPException(status_code=404, detail=f"Car with {id} not found") 69 | 70 | 71 | @router.delete("/{id}", response_description="Delete car") 72 | async def delete_task(id: str, request: Request): 73 | delete_result = await request.app.mongodb['cars1'].delete_one({"_id": id}) 74 | 75 | if delete_result.deleted_count == 1: 76 | return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None) 77 | 78 | raise HTTPException(status_code=404, detail=f"Car with {id} not found") 79 | 80 | 81 | 82 | 83 | # optional 84 | @router.get("/brand/{brand}", response_description="Get brand overview") 85 | async def brand_price(brand: str,request: Request): 86 | 87 | query = [ 88 | { 89 | '$match': { 90 | 'brand': brand 91 | } 92 | }, { 93 | '$project': { 94 | '_id': 0, 95 | 'price': 1, 96 | 'year': 1, 97 | 'make': 1 98 | } 99 | }, { 100 | '$group': { 101 | '_id': { 102 | 'model': '$make' 103 | }, 104 | 'avgPrice': { 105 | '$avg': '$price' 106 | } 107 | }, 108 | }, { 109 | '$sort': { 110 | 'avgPrice':1 111 | } 112 | } 113 | ] 114 | 115 | full_query = request.app.mongodb['cars1'].aggregate(query) 116 | 117 | results = [el async for el in full_query] 118 | 119 | return results -------------------------------------------------------------------------------- /chapter5/instructions.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter5/instructions.txt -------------------------------------------------------------------------------- /chapter6/Instructions.txt: -------------------------------------------------------------------------------- 1 | Once into the folder, run npm install to install all the dependencies 2 | npm start to start the app 3 | set your own Heroku URL if you are deploying to Heroku -------------------------------------------------------------------------------- /chapter6/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /chapter6/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 your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may 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 | -------------------------------------------------------------------------------- /chapter6/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.1.1", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^18.1.0", 10 | "react-dom": "^18.1.0", 11 | "react-router-dom": "^6.3.0", 12 | "react-scripts": "5.0.1", 13 | "web-vitals": "^2.1.4" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "autoprefixer": "^10.4.7", 41 | "postcss": "^8.4.13", 42 | "tailwindcss": "^3.0.24" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /chapter6/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /chapter6/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter6/frontend/public/favicon.ico -------------------------------------------------------------------------------- /chapter6/frontend/public/images/back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter6/frontend/public/images/back.jpg -------------------------------------------------------------------------------- /chapter6/frontend/public/images/pexels-alexgtacar-1592384.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter6/frontend/public/images/pexels-alexgtacar-1592384.jpg -------------------------------------------------------------------------------- /chapter6/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /chapter6/frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter6/frontend/public/logo192.png -------------------------------------------------------------------------------- /chapter6/frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter6/frontend/public/logo512.png -------------------------------------------------------------------------------- /chapter6/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 | -------------------------------------------------------------------------------- /chapter6/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /chapter6/frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import Layout from "./components/Layout"; 2 | import Lorem from "./components/Lorem"; 3 | 4 | function App() { 5 | return ( 6 | 7 |
8 | 9 |
10 |
11 | ); 12 | } 13 | 14 | export default App; -------------------------------------------------------------------------------- /chapter6/frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /chapter6/frontend/src/components/Card.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom" 2 | const Card = ({car}) => { 3 | let {brand, price, make, year, km, cm3,_id} = car 4 | 5 | 6 | return ( 7 | 8 |
9 |
{brand} {make}
10 | 11 |
Year: {year}
12 |
Price: {price}
13 |
Km: {km}
14 |
Engine: {cm3}cm3
15 |
16 | 17 | ) 18 | } 19 | export default Card -------------------------------------------------------------------------------- /chapter6/frontend/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return ( 3 |
4 |
A FARM stack based application for cars
5 |
6 | ) 7 | } 8 | export default Footer -------------------------------------------------------------------------------- /chapter6/frontend/src/components/FormInput.jsx: -------------------------------------------------------------------------------- 1 | const FormInput = (props) => { 2 | const { label, placeholder, type, onChange, name } = props 3 | return ( 4 |
5 | 6 | 14 |
15 | ) 16 | } 17 | export default FormInput -------------------------------------------------------------------------------- /chapter6/frontend/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import { Link, NavLink } from "react-router-dom"; 2 | 3 | 4 | 5 | const Header = () => { 6 | return ( 7 | 21 | ) 22 | } 23 | 24 | export default Header -------------------------------------------------------------------------------- /chapter6/frontend/src/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import Header from "./Header" 2 | import Footer from "./Footer" 3 | 4 | 5 | const Layout = ({children}) => { 6 | return ( 7 |
8 |
9 |
{children}
10 |
11 |
12 | ) 13 | } 14 | 15 | export default Layout -------------------------------------------------------------------------------- /chapter6/frontend/src/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Loading = ({brand}) => { 4 | return ( 5 |
6 |

Loading cars, brand:{brand?brand:'any'}...

7 |
8 | ) 9 | } 10 | 11 | export default Loading -------------------------------------------------------------------------------- /chapter6/frontend/src/components/Lorem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Lorem = ({title}) => { 4 | return ( 5 |
6 |

{title}

7 |
Reprehenderit laborum sit amet aute occaecat enim nostrud cillum cupidatat commodo ad culpa nisi. Mollit excepteur amet anim adipisicing et. Dolore proident est proident anim exercitation. Cillum irure est velit consequat anim incididunt. Reprehenderit ipsum anim labore enim. In esse commodo commodo minim culpa pariatur deserunt tempor. Officia nisi labore sint aliqua duis.
8 |
9 | ) 10 | } 11 | 12 | export default Lorem -------------------------------------------------------------------------------- /chapter6/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities{ 6 | .active-link{ 7 | @apply bg-yellow-500 p-4 shadow-md text-white 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /chapter6/frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | 4 | import { 5 | BrowserRouter, 6 | Routes, 7 | Route, 8 | } from "react-router-dom"; 9 | 10 | 11 | 12 | import Car from './pages/Car' 13 | import Cars from './pages/Cars' 14 | import NewCar from './pages/NewCar' 15 | 16 | 17 | 18 | import './index.css'; 19 | import App from './App'; 20 | import reportWebVitals from './reportWebVitals'; 21 | 22 | 23 | const root = ReactDOM.createRoot(document.getElementById('root')); 24 | root.render( 25 | 26 | 27 | 28 | 29 | } /> 30 | } /> 31 | } /> 32 | } /> 33 | 35 |

There's nothing here!

36 | 37 | } 38 | /> 39 |
40 |
41 |
42 | 43 | ); 44 | 45 | // If you want to start measuring performance in your app, pass a function 46 | // to log results (for example: reportWebVitals(console.log)) 47 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 48 | reportWebVitals(); 49 | -------------------------------------------------------------------------------- /chapter6/frontend/src/pages/Car.jsx: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react' 2 | import {useParams, useNavigate} from "react-router-dom" 3 | import Layout from '../components/Layout'; 4 | import FormInput from '../components/FormInput'; 5 | 6 | 7 | let BASE_URL = "https://agile-crag-18296.herokuapp.com/cars/" 8 | 9 | const Car = () => { 10 | 11 | let {id} = useParams() 12 | const navigate = useNavigate() 13 | 14 | const [car, setCar] = useState(null) 15 | const [price, setPrice] = useState(null) 16 | const [error, setError] = useState([]) 17 | const [isPending, setIsPending] = useState(true) 18 | 19 | const onChange = (event)=>{ 20 | setPrice(event.target.value) 21 | } 22 | 23 | const getCar = async() => { 24 | const res = await fetch(`${BASE_URL}${id}`) 25 | if (!res.ok){ 26 | setError("Error fetching car") 27 | 28 | } else { 29 | const data = await res.json() 30 | setCar(data) 31 | setPrice(data.price) 32 | } 33 | setIsPending(false) 34 | } 35 | 36 | const handleDelete = async () => { 37 | const response = await fetch(`${BASE_URL}${id}`,{ 38 | method:"DELETE", 39 | headers:{ 40 | 'Content-Type':'application/json' 41 | } 42 | }) 43 | 44 | if(!response.ok) { 45 | const data = await response.json() 46 | let errArray = data.detail.map(el=>{ 47 | return `${el.loc[1]} -${el.msg}` 48 | }) 49 | setError(errArray) 50 | } else { 51 | setError([]) 52 | navigate("/cars") 53 | } 54 | } 55 | 56 | const updatePrice = async () => { 57 | console.log("What",`${BASE_URL}${id}`) 58 | const response = await fetch(`${BASE_URL}${id}`,{ 59 | method:"PATCH", 60 | headers:{ 61 | 'Content-Type':'application/json' 62 | }, 63 | body: JSON.stringify({price}) 64 | }) 65 | 66 | const data = await response.json() 67 | if(!response.ok) { 68 | let errArray = data.detail.map(el=>{ 69 | return `${el.loc[1]} -${el.msg}` 70 | }) 71 | setError(errArray) 72 | } else { 73 | setError([]) 74 | getCar() 75 | } 76 | 77 | 78 | } 79 | 80 | 81 | 82 | useEffect(()=>{ 83 | getCar(id) 84 | },[id]) 85 | 86 | return ( 87 | 88 | {isPending &&
89 |

Loading car...

90 |
} 91 | 92 | {error &&
    93 | { error && error.map( 94 | (el, index)=>( 95 |
  • {el}
  • 96 | ) 97 | ) 98 | } 99 |
} 100 | 101 | {car&&
102 |
103 |
104 | {car.brand} {car.make} 105 |
106 |
107 | A car! 110 |
111 |
112 |
Price: {car.price}
113 |
Year: {car.year}
114 |
Km: {car.km}
115 |
116 | 117 |
118 | 119 | 126 | 127 | 132 | 137 |
138 |

Warning: deleting is permanent!

139 | 140 |
141 | 142 | 143 |
} 144 | 145 | 146 |
147 | 148 | ) 149 | } 150 | 151 | export default Car -------------------------------------------------------------------------------- /chapter6/frontend/src/pages/Cars.jsx: -------------------------------------------------------------------------------- 1 | import Layout from "../components/Layout" 2 | import Card from "../components/Card" 3 | import Loading from "../components/Loading" 4 | import {useState, useEffect} from 'react' 5 | 6 | let BASE_URL = "https://agile-crag-18296.herokuapp.com/cars" 7 | 8 | 9 | const Cars = () => { 10 | 11 | const [cars, setCars] = useState([]) 12 | const [brand, setBrand] = useState('') 13 | const [isPending, setIsPending] = useState(true) 14 | const [page, setPage] = useState(1) 15 | 16 | const handleChangeBrand = (ev) => { 17 | setCars([]) 18 | setBrand(ev.target.value) 19 | setIsPending(true) 20 | } 21 | 22 | const handleChangePage = (ev) => { 23 | setCars([]) 24 | setPage(ev.target.value) 25 | setIsPending(true) 26 | } 27 | 28 | 29 | 30 | useEffect(()=>{ 31 | fetch(`${BASE_URL}?brand=${brand}&page=${page}`) 32 | 33 | .then(response=>response.json()) 34 | .then(json=>{ 35 | setCars(json) 36 | setIsPending(false) 37 | }) 38 | 39 | },[brand, page]) 40 | 41 | return ( 42 | 43 |

Cars - {brand?brand:"all brands"}

44 |
45 | 46 | 53 | 54 | 61 |
62 |
63 | {isPending && } 64 |
65 | {cars && cars.map( 66 | (el)=>{ 67 | return ( 68 | 69 | ) 70 | } 71 | )} 72 |
73 | 74 |
75 |
76 | ) 77 | } 78 | 79 | export default Cars -------------------------------------------------------------------------------- /chapter6/frontend/src/pages/NewCar.jsx: -------------------------------------------------------------------------------- 1 | 2 | import {useState} from 'react' 3 | import {useNavigate} from "react-router-dom"; 4 | 5 | import Layout from "../components/Layout" 6 | import FormInput from '../components/FormInput'; 7 | 8 | let BASE_URL = "https://agile-crag-18296.herokuapp.com/cars/" 9 | 10 | const NewCar = () => { 11 | 12 | const emptyCar = { 13 | "brand":"", 14 | "make":"", 15 | "year":null, 16 | "cm3":null, 17 | "price":null 18 | } 19 | 20 | const inputs = [ 21 | { 22 | id:"brand", 23 | name:"brand", 24 | type:"text", 25 | placeholder:"Brand", 26 | label:"Brand" 27 | }, 28 | { 29 | id:"make", 30 | name:"make", 31 | type:"text", 32 | placeholder:"Make", 33 | label:"Make" 34 | }, 35 | { 36 | id:"year", 37 | name:"year", 38 | type:"number", 39 | placeholder:"Year", 40 | label:"Year" 41 | }, 42 | { 43 | id:"price", 44 | name:"price", 45 | type:"number", 46 | placeholder:"Price", 47 | label:"Price" 48 | }, 49 | { 50 | id:"cm3", 51 | name:"cm3", 52 | type:"number", 53 | placeholder:"Cm3", 54 | label:"Cm3" 55 | }, 56 | { 57 | id:"km", 58 | name:"km", 59 | type:"number", 60 | placeholder:"km", 61 | label:"km" 62 | }, 63 | 64 | ] 65 | 66 | const [newCar, setNewCar] = useState(emptyCar) 67 | const [error, setError] = useState([]) 68 | 69 | const navigate = useNavigate(); 70 | 71 | const handleSubmit = (e)=>{ 72 | e.preventDefault() 73 | addCar(newCar) 74 | } 75 | 76 | const onChange = (e) => { 77 | setNewCar({...newCar, [e.target.name]: e.target.value}) 78 | } 79 | 80 | const handleReset = (e) => { 81 | setNewCar(emptyCar) 82 | } 83 | 84 | const addCar = async (newCar)=>{ 85 | 86 | const response = await fetch(BASE_URL,{ 87 | method:"POST", 88 | headers:{ 89 | 'Content-Type':'application/json' 90 | }, 91 | body:JSON.stringify(newCar) 92 | }) 93 | 94 | const data = await response.json() 95 | 96 | 97 | if(!response.ok) { 98 | let errArray = data.detail.map(el=>{ 99 | return `${el.loc[1]} -${el.msg}` 100 | }) 101 | setError(errArray) 102 | } else { 103 | 104 | setError([]) 105 | navigate('/cars') 106 | } 107 | } 108 | 109 | 110 | return ( 111 | 112 |
113 |

Insert a New Car

114 |
115 |
New car status: {JSON.stringify(newCar)}
116 | {error &&
    117 | { error && error.map( 118 | (el, index)=>( 119 |
  • {el}
  • 120 | ) 121 | ) 122 | } 123 |
} 124 |
125 |
126 | {inputs.map((input) => ( 127 | 134 | ))} 135 | 136 | 137 | 138 | 139 |
140 |
141 | ) 142 | } 143 | 144 | export default NewCar -------------------------------------------------------------------------------- /chapter6/frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /chapter6/frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /chapter6/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./src/**/*.{js,jsx,ts,tsx}", 4 | ], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | } -------------------------------------------------------------------------------- /chapter7/backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ -------------------------------------------------------------------------------- /chapter7/backend/Instructions.txt: -------------------------------------------------------------------------------- 1 | Create a .env file with the following content: 2 | 3 | DB_URL=mongodb+srv:// 4 | DB_NAME=carsApp -------------------------------------------------------------------------------- /chapter7/backend/authentication.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | from fastapi import HTTPException, Security 3 | from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer 4 | from passlib.context import CryptContext 5 | from datetime import datetime, timedelta 6 | 7 | 8 | class AuthHandler: 9 | 10 | security = HTTPBearer() 11 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 12 | secret = "FARMSTACKsecretString" 13 | 14 | def get_password_hash(self, password): 15 | return self.pwd_context.hash(password) 16 | 17 | def verify_password(self, plain_password, hashed_password): 18 | return self.pwd_context.verify(plain_password, hashed_password) 19 | 20 | def encode_token(self, user_id): 21 | payload = { 22 | "exp": datetime.utcnow() + timedelta(days=0, minutes=35), 23 | "iat": datetime.utcnow(), 24 | "sub": user_id, 25 | } 26 | return jwt.encode(payload, self.secret, algorithm="HS256") 27 | 28 | def decode_token(self, token): 29 | try: 30 | payload = jwt.decode(token, self.secret, algorithms=["HS256"]) 31 | return payload["sub"] 32 | except jwt.ExpiredSignatureError: 33 | raise HTTPException(status_code=401, detail="Signature has expired") 34 | except jwt.InvalidTokenError as e: 35 | raise HTTPException(status_code=401, detail="Invalid token") 36 | 37 | def auth_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)): 38 | return self.decode_token(auth.credentials) 39 | -------------------------------------------------------------------------------- /chapter7/backend/importScript.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from fastapi.encoders import jsonable_encoder 3 | 4 | import random 5 | 6 | 7 | 8 | from decouple import config 9 | 10 | from models import CarBase2 11 | 12 | 13 | 14 | # 15 | 16 | # read csv 17 | with open("sample_data.csv",encoding='utf-8') as f: 18 | csv_reader = csv.DictReader(f) 19 | name_records = list(csv_reader) 20 | 21 | 22 | 23 | # Mongo db - we do not need Motor here 24 | from pymongo import MongoClient 25 | client = MongoClient() 26 | 27 | DB_URL = config('DB_URL', cast=str) 28 | DB_NAME = config('DB_NAME', cast=str) 29 | 30 | 31 | client = MongoClient(DB_URL) 32 | db = client[DB_NAME] 33 | cars = db['cars2'] 34 | 35 | users = db['users'] 36 | 37 | 38 | ids_list = [] 39 | all_users=users.find({}) 40 | 41 | for x in all_users: 42 | ids_list.append(x['_id']) 43 | 44 | print(ids_list) 45 | for rec in name_records[1:250]: 46 | 47 | try: 48 | rec['cm3'] = int(rec['cm3']) 49 | rec['price'] = int(rec['price']) 50 | rec['year'] = int(rec['year']) 51 | rec['owner'] = random.choice(ids_list) 52 | 53 | if rec['price']>1000: 54 | car = jsonable_encoder(CarBase2(**rec)) 55 | print("Inserting:",car) 56 | cars.insert_one(car) 57 | 58 | except ValueError: 59 | pass 60 | -------------------------------------------------------------------------------- /chapter7/backend/main.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | 3 | from fastapi import FastAPI 4 | from fastapi.middleware.cors import CORSMiddleware 5 | 6 | from motor.motor_asyncio import AsyncIOMotorClient 7 | 8 | from routers.cars import router as cars_router 9 | from routers.users import router as users_router 10 | 11 | 12 | DB_URL = config('DB_URL', cast=str) 13 | DB_NAME = config('DB_NAME', cast=str) 14 | 15 | 16 | # define origins 17 | origins = [ 18 | "*" 19 | ] 20 | 21 | # instantiate the app 22 | app = FastAPI() 23 | 24 | # add CORS middleware 25 | app.add_middleware( 26 | CORSMiddleware, 27 | allow_origins=origins, 28 | allow_credentials=True, 29 | allow_methods=["*"], 30 | allow_headers=["*"] 31 | ) 32 | 33 | @app.on_event("startup") 34 | async def startup_db_client(): 35 | app.mongodb_client = AsyncIOMotorClient(DB_URL) 36 | app.mongodb = app.mongodb_client[DB_NAME] 37 | 38 | @app.on_event("shutdown") 39 | async def shutdown_db_client(): 40 | app.mongodb_client.close() 41 | 42 | app.include_router(cars_router, prefix="/cars", tags=["cars"]) 43 | app.include_router(users_router, prefix="/users", tags=["users"]) -------------------------------------------------------------------------------- /chapter7/backend/models.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from bson import ObjectId 3 | from typing import Optional 4 | 5 | from pydantic import EmailStr, Field, BaseModel, validator 6 | 7 | from email_validator import validate_email, EmailNotValidError 8 | 9 | 10 | class PyObjectId(ObjectId): 11 | @classmethod 12 | def __get_validators__(cls): 13 | yield cls.validate 14 | 15 | @classmethod 16 | def validate(cls, v): 17 | if not ObjectId.is_valid(v): 18 | raise ValueError("Invalid objectid") 19 | return ObjectId(v) 20 | 21 | @classmethod 22 | def __modify_schema__(cls, field_schema): 23 | field_schema.update(type="string") 24 | 25 | 26 | class MongoBaseModel(BaseModel): 27 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 28 | 29 | class Config: 30 | json_encoders = {ObjectId: str} 31 | 32 | 33 | class Role(str, Enum): 34 | 35 | SALESMAN = "SALESMAN" 36 | ADMIN = "ADMIN" 37 | 38 | 39 | class UserBase(MongoBaseModel): 40 | 41 | username: str = Field(..., min_length=3, max_length=15) 42 | email: str = Field(...) 43 | password: str = Field(...) 44 | role: Role 45 | 46 | @validator("email") 47 | def valid_email(cls, v): 48 | 49 | try: 50 | email = validate_email(v).email 51 | return email 52 | except EmailNotValidError as e: 53 | 54 | raise EmailNotValidError 55 | 56 | 57 | class LoginBase(BaseModel): 58 | email: str = EmailStr(...) 59 | password: str = Field(...) 60 | 61 | 62 | class CurrentUser(BaseModel): 63 | email: str = EmailStr(...) 64 | username: str = Field(...) 65 | role: str = Field(...) 66 | 67 | 68 | class CarBase(MongoBaseModel): 69 | 70 | brand: str = Field(..., min_length=3) 71 | make: str = Field(..., min_length=1) 72 | year: int = Field(..., gt=1975, lt=2023) 73 | price: int = Field(...) 74 | km: int = Field(...) 75 | cm3: int = Field(..., gt=600, lt=8000) 76 | 77 | 78 | class CarDB(CarBase): 79 | owner: str = Field(...) 80 | 81 | 82 | class CarUpdate(MongoBaseModel): 83 | price: Optional[int] = None 84 | -------------------------------------------------------------------------------- /chapter7/backend/reqs.txt: -------------------------------------------------------------------------------- 1 | anyio==3.5.0 2 | asgiref==3.5.0 3 | backports.entry-points-selectable==1.1.1 4 | bcrypt==3.2.2 5 | bson==0.5.10 6 | certifi==2021.10.8 7 | cffi==1.15.0 8 | charset-normalizer==2.0.12 9 | click==8.1.2 10 | colorama==0.4.4 11 | cryptography==37.0.2 12 | defusedxml==0.7.1 13 | Deprecated==1.2.13 14 | distlib==0.3.4 15 | dnslib==0.9.19 16 | dnspython==2.2.1 17 | email-validator==1.2.1 18 | fastapi==0.75.2 19 | filelock==3.4.0 20 | h11==0.13.0 21 | httpie==3.1.0 22 | idna==3.3 23 | jwcrypto==1.3.1 24 | motor==2.5.1 25 | multidict==6.0.2 26 | passlib==1.7.4 27 | pipenv==2021.11.23 28 | platformdirs==2.4.0 29 | pycparser==2.21 30 | pydantic==1.9.0 31 | Pygments==2.12.0 32 | PyJWT==2.4.0 33 | pymongo==3.12.3 34 | PySocks==1.7.1 35 | python-dateutil==2.8.2 36 | python-decouple==3.6 37 | python-dotenv==0.20.0 38 | python-jwt==3.3.2 39 | PythonDNS==0.1 40 | requests==2.27.1 41 | requests-toolbelt==0.9.1 42 | six==1.16.0 43 | sniffio==1.2.0 44 | starlette==0.17.1 45 | typing_extensions==4.2.0 46 | urllib3==1.26.9 47 | uvicorn==0.17.6 48 | virtualenv==20.10.0 49 | virtualenv-clone==0.5.7 50 | wrapt==1.14.1 51 | -------------------------------------------------------------------------------- /chapter7/backend/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter7/backend/routers/__init__.py -------------------------------------------------------------------------------- /chapter7/backend/routers/cars.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from fastapi import APIRouter, Request, Body, status, HTTPException, Depends 4 | from fastapi.encoders import jsonable_encoder 5 | from fastapi.responses import JSONResponse 6 | 7 | from models import CarBase, CarDB, CarUpdate 8 | 9 | from authentication import AuthHandler 10 | 11 | router = APIRouter() 12 | 13 | # instantiate the Auth Handler 14 | auth_handler = AuthHandler() 15 | 16 | 17 | @router.get("/", response_description="List all cars") 18 | async def list_all_cars( 19 | request: Request, 20 | min_price: int = 0, 21 | max_price: int = 100000, 22 | brand: Optional[str] = None, 23 | page: int = 1, 24 | userId=Depends(auth_handler.auth_wrapper), 25 | ) -> List[CarDB]: 26 | 27 | RESULTS_PER_PAGE = 25 28 | skip = (page - 1) * RESULTS_PER_PAGE 29 | 30 | query = {"price": {"$lt": max_price, "$gt": min_price}} 31 | if brand: 32 | query["brand"] = brand 33 | 34 | full_query = ( 35 | request.app.mongodb["cars2"] 36 | .find(query) 37 | .sort("_id", -1) 38 | .skip(skip) 39 | .limit(RESULTS_PER_PAGE) 40 | ) 41 | 42 | results = [CarDB(**raw_car) async for raw_car in full_query] 43 | 44 | return results 45 | 46 | 47 | # create new car 48 | @router.post("/", response_description="Add new car") 49 | async def create_car( 50 | request: Request, 51 | car: CarBase = Body(...), 52 | userId=Depends(auth_handler.auth_wrapper), 53 | ): 54 | 55 | car = jsonable_encoder(car) 56 | car["owner"] = userId 57 | 58 | new_car = await request.app.mongodb["cars2"].insert_one(car) 59 | created_car = await request.app.mongodb["cars2"].find_one( 60 | {"_id": new_car.inserted_id} 61 | ) 62 | 63 | return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_car) 64 | 65 | 66 | # get car by ID 67 | @router.get("/{id}", response_description="Get a single car") 68 | async def show_car(id: str, request: Request): 69 | if (car := await request.app.mongodb["cars2"].find_one({"_id": id})) is not None: 70 | return CarDB(**car) 71 | raise HTTPException(status_code=404, detail=f"Car with {id} not found") 72 | 73 | 74 | @router.patch("/{id}", response_description="Update car") 75 | async def update_task( 76 | id: str, 77 | request: Request, 78 | car: CarUpdate = Body(...), 79 | userId=Depends(auth_handler.auth_wrapper), 80 | ): 81 | 82 | # check if the user trying to modify is an admin: 83 | user = await request.app.mongodb["users"].find_one({"_id": userId}) 84 | 85 | # check if the car is owned by the user trying to modify it 86 | findCar = await request.app.mongodb["cars2"].find_one({"_id": id}) 87 | 88 | if (findCar["owner"] != userId) and user["role"] != "ADMIN": 89 | raise HTTPException( 90 | status_code=401, detail="Only the owner or an admin can update the car" 91 | ) 92 | 93 | await request.app.mongodb["cars2"].update_one( 94 | {"_id": id}, {"$set": car.dict(exclude_unset=True)} 95 | ) 96 | 97 | if (car := await request.app.mongodb["cars2"].find_one({"_id": id})) is not None: 98 | return CarDB(**car) 99 | 100 | raise HTTPException(status_code=404, detail=f"Car with {id} not found") 101 | 102 | 103 | @router.delete("/{id}", response_description="Delete car") 104 | async def delete_task( 105 | id: str, request: Request, userId=Depends(auth_handler.auth_wrapper) 106 | ): 107 | 108 | # check if the car is owned by the user trying to delete it 109 | findCar = await request.app.mongodb["cars2"].find_one({"_id": id}) 110 | 111 | if findCar["owner"] != userId: 112 | raise HTTPException(status_code=401, detail="Only the owner can delete the car") 113 | 114 | delete_result = await request.app.mongodb["cars2"].delete_one({"_id": id}) 115 | 116 | if delete_result.deleted_count == 1: 117 | return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None) 118 | 119 | raise HTTPException(status_code=404, detail=f"Car with {id} not found") 120 | 121 | 122 | # optional 123 | @router.get("/brand/{brand}", response_description="Get brand overview") 124 | async def brand_price(brand: str, request: Request): 125 | 126 | query = [ 127 | {"$match": {"brand": brand}}, 128 | {"$project": {"_id": 0, "price": 1, "year": 1, "make": 1}}, 129 | { 130 | "$group": {"_id": {"model": "$make"}, "avgPrice": {"$avg": "$price"}}, 131 | }, 132 | {"$sort": {"avgPrice": 1}}, 133 | ] 134 | 135 | full_query = request.app.mongodb["cars2"].aggregate(query) 136 | 137 | results = [el async for el in full_query] 138 | 139 | return results 140 | -------------------------------------------------------------------------------- /chapter7/backend/routers/users.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, Body, status, HTTPException, Depends 2 | from fastapi.encoders import jsonable_encoder 3 | from fastapi.responses import JSONResponse 4 | 5 | from models import UserBase, LoginBase, CurrentUser 6 | 7 | from authentication import AuthHandler 8 | 9 | router = APIRouter() 10 | 11 | # instantiate the Auth Handler 12 | auth_handler = AuthHandler() 13 | 14 | # register user 15 | # validate the data and create a user if the username and the email are valid and available 16 | 17 | 18 | @router.post("/register", response_description="Register user") 19 | async def register(request: Request, newUser: UserBase = Body(...)) -> UserBase: 20 | 21 | # hash the password before inserting it into MongoDB 22 | newUser.password = auth_handler.get_password_hash(newUser.password) 23 | 24 | newUser = jsonable_encoder(newUser) 25 | 26 | # check existing user or email 409 Conflict: 27 | if ( 28 | existing_email := await request.app.mongodb["users"].find_one( 29 | {"email": newUser["email"]} 30 | ) 31 | is not None 32 | ): 33 | raise HTTPException( 34 | status_code=409, detail=f"User with email {newUser['email']} already exists" 35 | ) 36 | 37 | # check existing user or email 409 Conflict: 38 | if ( 39 | existing_username := await request.app.mongodb["users"].find_one( 40 | {"username": newUser["username"]} 41 | ) 42 | is not None 43 | ): 44 | raise HTTPException( 45 | status_code=409, 46 | detail=f"User with username {newUser['username']} already exists", 47 | ) 48 | 49 | user = await request.app.mongodb["users"].insert_one(newUser) 50 | created_user = await request.app.mongodb["users"].find_one( 51 | {"_id": user.inserted_id} 52 | ) 53 | 54 | return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_user) 55 | 56 | 57 | # post user 58 | @router.post("/login", response_description="Login user") 59 | async def login(request: Request, loginUser: LoginBase = Body(...)) -> str: 60 | 61 | # find the user by email 62 | user = await request.app.mongodb["users"].find_one({"email": loginUser.email}) 63 | 64 | # check password 65 | if (user is None) or ( 66 | not auth_handler.verify_password(loginUser.password, user["password"]) 67 | ): 68 | raise HTTPException(status_code=401, detail="Invalid email and/or password") 69 | token = auth_handler.encode_token(user["_id"]) 70 | response = JSONResponse(content={"token": token}) 71 | 72 | return response 73 | 74 | 75 | # me route 76 | @router.get("/me", response_description="Logged in user data") 77 | async def me(request: Request, userId=Depends(auth_handler.auth_wrapper)): 78 | 79 | currentUser = await request.app.mongodb["users"].find_one({"_id": userId}) 80 | result = CurrentUser(**currentUser).dict() 81 | result["id"] = userId 82 | 83 | return JSONResponse(status_code=status.HTTP_200_OK, content=result) 84 | -------------------------------------------------------------------------------- /chapter7/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /chapter7/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 your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may 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 | -------------------------------------------------------------------------------- /chapter7/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.2.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "cookie-parser": "^1.4.6", 10 | "daisyui": "^2.15.0", 11 | "react": "^18.1.0", 12 | "react-dom": "^18.1.0", 13 | "react-hook-form": "^7.31.2", 14 | "react-router-dom": "^6.3.0", 15 | "react-scripts": "5.0.1", 16 | "web-vitals": "^2.1.4" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "autoprefixer": "^10.4.7", 44 | "postcss": "^8.4.14", 45 | "tailwindcss": "^3.0.24" 46 | }, 47 | "proxy": "http://127.0.0.1:8000" 48 | } 49 | -------------------------------------------------------------------------------- /chapter7/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /chapter7/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter7/frontend/public/favicon.ico -------------------------------------------------------------------------------- /chapter7/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /chapter7/frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter7/frontend/public/logo192.png -------------------------------------------------------------------------------- /chapter7/frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter7/frontend/public/logo512.png -------------------------------------------------------------------------------- /chapter7/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 | -------------------------------------------------------------------------------- /chapter7/frontend/public/pexels-johnmark-smith-280783.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter7/frontend/public/pexels-johnmark-smith-280783.jpg -------------------------------------------------------------------------------- /chapter7/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /chapter7/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /chapter7/frontend/src/App.js: -------------------------------------------------------------------------------- 1 | 2 | import {Route, Routes} from "react-router-dom" 3 | 4 | 5 | import Layout from "./components/Layout"; 6 | import HomePage from "./components/HomePage"; 7 | import Login from "./components/Login"; 8 | import Register from "./components/Register"; 9 | import Protected from "./components/Protected"; 10 | import Admin from "./components/Admin"; 11 | import NotFound from "./components/NotFound"; 12 | import NewCar from "./components/NewCar"; 13 | import RequireAuthentication from "./components/RequireAuthentication"; 14 | 15 | function App() { 16 | 17 | return ( 18 | 19 | }> 20 | } /> 21 | } /> 22 | } /> 23 | 24 | }> 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /chapter7/frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /chapter7/frontend/src/components/Admin.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useAuth from '../hooks/useAuth' 3 | 4 | const Admin = () => { 5 | const {auth} = useAuth() 6 | 7 | 8 | 9 | return ( 10 |
11 | {auth?.role==="ADMIN"? 12 | Ok Admin! You are {auth.username} and you seem to be an Admin. 13 | :Only admins, sorry} 14 |
15 | 16 | ) 17 | } 18 | 19 | export default Admin -------------------------------------------------------------------------------- /chapter7/frontend/src/components/Card.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Card = ({ car }) => { 4 | let { brand, price, make, year, km, cm3 } = car; 5 | return ( 6 |
7 |
8 | Shoes 9 |
10 |
11 |

12 | {brand} {make} 13 |

14 |
15 |
16 | Year: {year} / Cm3: {cm3} / Km: {km} 17 |
18 |
19 | Price: {price}{" "} 20 | EURO 21 |
22 |
23 |
24 |
25 | ); 26 | }; 27 | 28 | export default Card; 29 | -------------------------------------------------------------------------------- /chapter7/frontend/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Footer = () => { 4 | return ( 5 |
6 |
7 |

Copyright © 2022 - All right reserved by FARM Cars fullstack car sales

8 |
9 |
10 | ) 11 | } 12 | 13 | export default Footer -------------------------------------------------------------------------------- /chapter7/frontend/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, useNavigate } from 'react-router-dom' 3 | import useAuth from '../hooks/useAuth' 4 | 5 | 6 | const Header = () => { 7 | 8 | const {auth,setAuth} = useAuth() 9 | let navigate = useNavigate(); 10 | 11 | const logout = () =>{ 12 | setAuth({}) 13 | navigate("/login", {replace:true}) 14 | 15 | 16 | } 17 | 18 | return ( 19 |
20 |
21 | FARM Cars 22 | 23 | {auth?.username?`Logged in as ${auth?.username} - ${auth.role}`:"Not logged in"} 24 | 25 |
26 |
27 |
    28 | {!auth?.username && 29 |
  • Login
  • } 30 | {!auth?.username && 31 |
  • Register
  • } 32 |
  • Admin
  • 33 |
  • Protected
  • 34 |
  • New Car
  • 35 | {auth?.username && 36 |
  • 38 | } 39 |
40 |
41 |
42 | ) 43 | } 44 | 45 | export default Header -------------------------------------------------------------------------------- /chapter7/frontend/src/components/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const HomePage = () => { 4 | return ( 5 |
9 | 10 |
11 |
12 |
13 |

FARM Stack Cars App!

14 |

FastAPI + MongoDB + React and some really affordable cars.

15 | 16 |
17 |
18 |
19 | ) 20 | } 21 | 22 | export default HomePage -------------------------------------------------------------------------------- /chapter7/frontend/src/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Outlet } from 'react-router-dom' 4 | 5 | import Header from './Header' 6 | import Footer from './Footer' 7 | 8 | 9 | const Layout = () => { 10 | 11 | 12 | return ( 13 |
14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 |
22 | ) 23 | } 24 | 25 | export default Layout -------------------------------------------------------------------------------- /chapter7/frontend/src/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import { useForm } from "react-hook-form"; 2 | import { useState } from "react"; 3 | import { useNavigate } from "react-router-dom"; 4 | 5 | import useAuth from "../hooks/useAuth"; 6 | 7 | const Login = () => { 8 | const [apiError, setApiError] = useState(); 9 | 10 | const { setAuth } = useAuth(); 11 | 12 | let navigate = useNavigate(); 13 | 14 | const { 15 | register, 16 | handleSubmit, 17 | formState: { errors }, 18 | } = useForm(); 19 | 20 | const getUserData = async (token) => { 21 | const response = await fetch("http://127.0.0.1:8000/users/me", { 22 | method: "GET", 23 | headers: { 24 | "Content-Type": "application/json", 25 | Authorization: `Bearer ${token}`, 26 | }, 27 | }); 28 | if (response.ok) { 29 | let userData = await response.json(); 30 | console.log(userData); 31 | userData["token"] = token; 32 | setAuth(userData); 33 | setApiError(null); 34 | navigate("/", { replace: true }); 35 | } 36 | }; 37 | 38 | const onFormSubmit = async (data) => { 39 | const response = await fetch("/users/login", { 40 | method: "POST", 41 | headers: { 42 | "Content-Type": "application/json", 43 | }, 44 | body: JSON.stringify(data), 45 | }); 46 | 47 | // if the login is successful - get the token and then get the remaining data from the /me route 48 | if (response.ok) { 49 | const token = await response.json(); 50 | await getUserData(token["token"]); 51 | } else { 52 | let errorResponse = await response.json(); 53 | setApiError(errorResponse["detail"]); 54 | setAuth(null); 55 | } 56 | }; 57 | 58 | const onErrors = (errors) => console.error(errors); 59 | 60 | return ( 61 |
62 |

63 | Login page 64 |

65 | 66 |
67 |
68 | 76 | {errors?.email && errors.email.message} 77 | 78 | 85 | {errors?.password && errors.password.message} 86 | 87 | 90 |
91 |
92 | 93 | {apiError && ( 94 |
95 |
96 | 102 | 108 | 109 | {apiError} 110 |
111 |
112 | )} 113 |
114 | ); 115 | }; 116 | 117 | export default Login; 118 | -------------------------------------------------------------------------------- /chapter7/frontend/src/components/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const NotFound = () => { 4 | return ( 5 |
NotFound
6 | ) 7 | } 8 | 9 | export default NotFound -------------------------------------------------------------------------------- /chapter7/frontend/src/components/Protected.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useEffect, useState } from "react"; 3 | 4 | import useAuth from "../hooks/useAuth"; 5 | 6 | import Card from "./Card"; 7 | 8 | const Protected = () => { 9 | const { auth } = useAuth(); 10 | 11 | const [cars, setCars] = useState([]); 12 | 13 | useEffect(() => { 14 | fetch("http://127.0.0.1:8000/cars/", { 15 | method: "GET", 16 | headers: { 17 | "Content-Type": "application/json", 18 | Authorization: `Bearer ${auth.token}`, 19 | }, 20 | }) 21 | .then((response) => response.json()) 22 | .then((json) => { 23 | setCars(json); 24 | }); 25 | }, [auth.token]); 26 | 27 | return ( 28 |
29 |

30 | Cars Page 31 |

32 | 33 |
34 | {cars && 35 | cars.map((el) => { 36 | return ; 37 | })} 38 |
39 |
40 | ); 41 | }; 42 | 43 | export default Protected; 44 | -------------------------------------------------------------------------------- /chapter7/frontend/src/components/Register.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Register = () => { 4 | return ( 5 |
6 |

Register

7 |

Registration is not implemented on the client side.

8 |
9 | ) 10 | } 11 | 12 | export default Register -------------------------------------------------------------------------------- /chapter7/frontend/src/components/RequireAuthentication.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from "react-router-dom"; 2 | import useAuth from "../hooks/useAuth"; 3 | 4 | const RequireAuthentication = () => { 5 | const { auth } = useAuth(); 6 | 7 | return auth?.username ? : ; 8 | }; 9 | 10 | export default RequireAuthentication; 11 | -------------------------------------------------------------------------------- /chapter7/frontend/src/context/AuthProvider.js: -------------------------------------------------------------------------------- 1 | import { createContext, useState } from "react"; 2 | 3 | const AuthContext = createContext({ 4 | 5 | }) 6 | 7 | export const AuthProvider = ({children}) => { 8 | const [auth, setAuth] = useState({ 9 | 10 | }) 11 | return 12 | {children} 13 | 14 | } 15 | 16 | 17 | export default AuthContext -------------------------------------------------------------------------------- /chapter7/frontend/src/hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import AuthContext from "../context/AuthProvider"; 3 | 4 | const useAuth = () => { 5 | return useContext(AuthContext) 6 | } 7 | 8 | export default useAuth; -------------------------------------------------------------------------------- /chapter7/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /chapter7/frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import {BrowserRouter, Routes, Route} from "react-router-dom" 5 | import App from './App'; 6 | import { AuthProvider } from './context/AuthProvider'; 7 | 8 | 9 | const root = ReactDOM.createRoot(document.getElementById('root')); 10 | root.render( 11 | 12 | 13 | 14 | 15 | } /> 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /chapter7/frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chapter7/frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /chapter7/frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /chapter7/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./src/**/*.{js,jsx,ts,tsx}", 4 | ], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [require("daisyui")], 9 | } -------------------------------------------------------------------------------- /chapter8/backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ -------------------------------------------------------------------------------- /chapter8/backend/Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn main:app --host=0.0.0.0 --port=${PORT:-5000} -------------------------------------------------------------------------------- /chapter8/backend/authentication.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | from fastapi import HTTPException, Security 3 | from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer 4 | from passlib.context import CryptContext 5 | from datetime import datetime, timedelta 6 | 7 | 8 | class AuthHandler: 9 | 10 | security = HTTPBearer() 11 | pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") 12 | secret = "FARMSTACKsecretString" 13 | 14 | def get_password_hash(self, password): 15 | return self.pwd_context.hash(password) 16 | 17 | def verify_password(self, plain_password, hashed_password): 18 | return self.pwd_context.verify(plain_password, hashed_password) 19 | 20 | def encode_token(self, user_id): 21 | payload = { 22 | "exp": datetime.utcnow() + timedelta(days=0, minutes=30), 23 | "iat": datetime.utcnow(), 24 | "sub": user_id, 25 | } 26 | return jwt.encode(payload, self.secret, algorithm="HS256") 27 | 28 | def decode_token(self, token): 29 | try: 30 | payload = jwt.decode(token, self.secret, algorithms=["HS256"]) 31 | return payload["sub"] 32 | except jwt.ExpiredSignatureError: 33 | raise HTTPException(status_code=401, detail="Signature has expired") 34 | except jwt.InvalidTokenError as e: 35 | raise HTTPException(status_code=401, detail="Invalid token") 36 | 37 | def auth_wrapper(self, auth: HTTPAuthorizationCredentials = Security(security)): 38 | return self.decode_token(auth.credentials) 39 | -------------------------------------------------------------------------------- /chapter8/backend/main.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | 3 | from fastapi import FastAPI 4 | 5 | # there seems to be a bug with FastAPI's middleware 6 | # https://stackoverflow.com/questions/65191061/fastapi-cors-middleware-not-working-with-get-method/65994876#65994876 7 | 8 | from starlette.middleware import Middleware 9 | from starlette.middleware.cors import CORSMiddleware 10 | 11 | 12 | middleware = [ 13 | Middleware( 14 | CORSMiddleware, 15 | allow_origins=["*"], 16 | allow_credentials=True, 17 | allow_methods=["*"], 18 | allow_headers=["*"], 19 | ) 20 | ] 21 | 22 | from motor.motor_asyncio import AsyncIOMotorClient 23 | 24 | from routers.cars import router as cars_router 25 | from routers.users import router as users_router 26 | 27 | 28 | DB_URL = config("DB_URL", cast=str) 29 | DB_NAME = config("DB_NAME", cast=str) 30 | 31 | 32 | # define origins 33 | origins = [ 34 | "*", 35 | ] 36 | 37 | # instantiate the app 38 | app = FastAPI(middleware=middleware) 39 | 40 | app.include_router(cars_router, prefix="/cars", tags=["cars"]) 41 | app.include_router(users_router, prefix="/users", tags=["users"]) 42 | 43 | 44 | @app.on_event("startup") 45 | async def startup_db_client(): 46 | app.mongodb_client = AsyncIOMotorClient(DB_URL) 47 | app.mongodb = app.mongodb_client[DB_NAME] 48 | 49 | 50 | @app.on_event("shutdown") 51 | async def shutdown_db_client(): 52 | app.mongodb_client.close() 53 | -------------------------------------------------------------------------------- /chapter8/backend/models.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from bson import ObjectId 3 | from typing import Optional 4 | 5 | from pydantic import EmailStr, Field, BaseModel, validator 6 | 7 | from email_validator import validate_email, EmailNotValidError 8 | 9 | 10 | class PyObjectId(ObjectId): 11 | @classmethod 12 | def __get_validators__(cls): 13 | yield cls.validate 14 | 15 | @classmethod 16 | def validate(cls, v): 17 | if not ObjectId.is_valid(v): 18 | raise ValueError("Invalid objectid") 19 | return ObjectId(v) 20 | 21 | @classmethod 22 | def __modify_schema__(cls, field_schema): 23 | field_schema.update(type="string") 24 | 25 | 26 | class MongoBaseModel(BaseModel): 27 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 28 | 29 | class Config: 30 | json_encoders = {ObjectId: str} 31 | 32 | 33 | class Role(str, Enum): 34 | 35 | SALESMAN = "SALESMAN" 36 | ADMIN = "ADMIN" 37 | 38 | 39 | class UserBase(MongoBaseModel): 40 | 41 | username: str = Field(..., min_length=3, max_length=15) 42 | email: str = Field(...) 43 | password: str = Field(...) 44 | role: Role 45 | 46 | @validator("email") 47 | def valid_email(cls, v): 48 | 49 | try: 50 | email = validate_email(v).email 51 | return email 52 | except EmailNotValidError as e: 53 | 54 | raise EmailNotValidError 55 | 56 | 57 | class LoginBase(BaseModel): 58 | email: str = EmailStr(...) 59 | password: str = Field(...) 60 | 61 | 62 | class CurrentUser(BaseModel): 63 | email: str = EmailStr(...) 64 | username: str = Field(...) 65 | role: str = Field(...) 66 | 67 | 68 | class CarBase(MongoBaseModel): 69 | 70 | brand: str = Field(..., min_length=3) 71 | make: str = Field(..., min_length=1) 72 | year: int = Field(..., gt=1975, lt=2023) 73 | price: int = Field(...) 74 | km: int = Field(...) 75 | cm3: int = Field(..., gt=600, lt=8000) 76 | picture: Optional[str] = None 77 | 78 | 79 | class CarDB(CarBase): 80 | owner: str = Field(...) 81 | 82 | 83 | class CarUpdate(MongoBaseModel): 84 | price: Optional[int] = None 85 | -------------------------------------------------------------------------------- /chapter8/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | bcrypt 2 | cloudinary 3 | colorama 4 | cryptography 5 | defusedxml 6 | Deprecated 7 | distlib 8 | dnslib 9 | dnspython 10 | email-validator 11 | fastapi 12 | idna 13 | jwcrypto 14 | motor 15 | multidict 16 | passlib 17 | Pillow 18 | pipenv 19 | pycparser 20 | pydantic 21 | PyJWT 22 | pymongo 23 | PySocks 24 | python-dateutil 25 | python-decouple 26 | python-dotenv 27 | python-jwt 28 | python-multipart 29 | PythonDNS 30 | requests 31 | uvicorn 32 | virtualenv 33 | virtualenv-clone -------------------------------------------------------------------------------- /chapter8/backend/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter8/backend/routers/__init__.py -------------------------------------------------------------------------------- /chapter8/backend/routers/users.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, Body, status, HTTPException, Depends 2 | from fastapi.encoders import jsonable_encoder 3 | from fastapi.responses import JSONResponse 4 | 5 | from models import UserBase, LoginBase, CurrentUser 6 | 7 | from authentication import AuthHandler 8 | 9 | router = APIRouter() 10 | 11 | # instantiate the Auth Handler 12 | auth_handler = AuthHandler() 13 | 14 | # register user 15 | # validate the data and create a user if the username and the email are valid and available 16 | 17 | 18 | @router.post("/register", response_description="Register user") 19 | async def register(request: Request, newUser: UserBase = Body(...)) -> UserBase: 20 | 21 | # hash the password before inserting it into MongoDB 22 | newUser.password = auth_handler.get_password_hash(newUser.password) 23 | 24 | newUser = jsonable_encoder(newUser) 25 | 26 | # check existing user or email 409 Conflict: 27 | if ( 28 | existing_email := await request.app.mongodb["users"].find_one( 29 | {"email": newUser["email"]} 30 | ) 31 | is not None 32 | ): 33 | raise HTTPException( 34 | status_code=409, detail=f"User with email {newUser['email']} already exists" 35 | ) 36 | 37 | # check existing user or email 409 Conflict: 38 | if ( 39 | existing_username := await request.app.mongodb["users"].find_one( 40 | {"username": newUser["username"]} 41 | ) 42 | is not None 43 | ): 44 | raise HTTPException( 45 | status_code=409, 46 | detail=f"User with username {newUser['username']} already exists", 47 | ) 48 | 49 | user = await request.app.mongodb["users"].insert_one(newUser) 50 | created_user = await request.app.mongodb["users"].find_one( 51 | {"_id": user.inserted_id} 52 | ) 53 | 54 | return JSONResponse(status_code=status.HTTP_201_CREATED, content=created_user) 55 | 56 | 57 | # post user 58 | @router.post("/login", response_description="Login user") 59 | async def login(request: Request, loginUser: LoginBase = Body(...)) -> str: 60 | 61 | # find the user by email 62 | user = await request.app.mongodb["users"].find_one({"email": loginUser.email}) 63 | 64 | # check password 65 | if (user is None) or ( 66 | not auth_handler.verify_password(loginUser.password, user["password"]) 67 | ): 68 | raise HTTPException(status_code=401, detail="Invalid email and/or password") 69 | token = auth_handler.encode_token(user["_id"]) 70 | response = JSONResponse( 71 | content={"token": token, "user": CurrentUser(**user).dict()} 72 | ) 73 | 74 | return response 75 | 76 | 77 | # me route 78 | @router.get("/me", response_description="Logged in user data") 79 | async def me(request: Request, userId=Depends(auth_handler.auth_wrapper)): 80 | 81 | currentUser = await request.app.mongodb["users"].find_one({"_id": userId}) 82 | result = CurrentUser(**currentUser).dict() 83 | result["id"] = userId 84 | 85 | return JSONResponse(status_code=status.HTTP_200_OK, content=result) 86 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/components/Card.js: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import Link from 'next/link' 3 | import { buildUrl } from 'cloudinary-build-url' 4 | 5 | 6 | const transformedUrl = (id)=> buildUrl(id, { 7 | cloud: { 8 | cloudName: 'ddyjlwyjv', 9 | }, 10 | 11 | transformations: { 12 | effect: { 13 | name: 'grayscale' 14 | }, 15 | effect: { 16 | name: 'tint', 17 | value: '60:blue:white', 18 | 19 | } 20 | } 21 | }); 22 | 23 | const Card = ({brand, make, year, url, km, price, cm3, id}) => { 24 | return ( 25 | 26 |
27 |
{brand}
28 |
29 |
{brand} {make}
30 |

Price: {price} EUR

31 |

32 | A detailed car description from the Cars FARM crew. 33 |

34 |
35 |
36 | made in {year} 37 | Cm3:{cm3} 38 | Km:{km} 39 |
40 |
41 | 42 | ) 43 | } 44 | 45 | export default Card 46 | 47 | 48 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return
Footer
; 3 | }; 4 | 5 | export default Footer; 6 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import useAuth from "../hooks/useAuth"; 3 | import { useEffect } from "react"; 4 | 5 | const Header = () => { 6 | const { user, setUser, loading, setLoading } = useAuth(); 7 | useEffect(() => { 8 | (async () => { 9 | const userData = await fetch("/api/user"); 10 | try { 11 | const user = await userData.json(); 12 | 13 | setUser(user); 14 | } catch (error) { 15 | // if error: set user to null, destroy the cookie 16 | setUser(null); 17 | } 18 | })(); 19 | }, []); 20 | return ( 21 |
22 | 37 |
    38 |
  • 39 | 40 | Cars 41 | 42 |
  • 43 | {user && user.role === "ADMIN" ? ( 44 |
  • 45 | 46 | Add Car 47 | 48 |
  • 49 | ) : ( 50 | "" 51 | )} 52 | 53 | {!user ? ( 54 | <> 55 |
  • 56 | 57 | Register 58 | 59 |
  • 60 |
  • 61 | 62 | Login 63 | 64 |
  • 65 | 66 | ) : ( 67 | <> 68 |
  • 69 | 70 | Log out {user.username} 71 | 72 |
  • 73 | 74 | )} 75 |
76 |
77 | ); 78 | }; 79 | export default Header; 80 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/context/AuthContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useState } from "react"; 2 | 3 | const AuthContext = createContext({}) 4 | 5 | export const AuthProvider = ({children}) => { 6 | const [user, setUser] = useState(null) 7 | const [authError, setAuthError] = useState(null) 8 | const [loading, setLoading] = useState(false) 9 | 10 | return 11 | {children} 12 | 13 | } 14 | 15 | export default AuthContext -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import AuthContext from "../context/AuthContext"; 3 | 4 | const useAuth = () => { 5 | return useContext(AuthContext) 6 | } 7 | 8 | export default useAuth; -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/middleware.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | 4 | export function middleware(req){ 5 | 6 | const url = req.url 7 | const cookie = req.cookies.get('jwt') 8 | 9 | if(url.includes('/cars/add') && (cookie===undefined)){ 10 | return NextResponse.redirect('http://localhost:3000/account/login') 11 | } 12 | return NextResponse.next() 13 | } 14 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | images: { 3 | domains: ['res.cloudinary.com'], 4 | }, 5 | } -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-cars", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.27.2", 13 | "cloudinary-build-url": "^0.2.4", 14 | "cookie": "^0.5.0", 15 | "cookies-next": "^2.1.1", 16 | "jsonwebtoken": "^8.5.1", 17 | "jwt-decode": "^3.1.2", 18 | "next": "12.2.0", 19 | "react": "18.2.0", 20 | "react-dom": "18.2.0", 21 | "swr": "^1.3.0" 22 | }, 23 | "devDependencies": { 24 | "@tailwindcss/forms": "^0.5.2", 25 | "autoprefixer": "^10.4.7", 26 | "eslint": "8.19.0", 27 | "eslint-config-next": "12.2.0", 28 | "postcss": "^8.4.14", 29 | "tailwindcss": "^3.1.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | import Header from '../components/Header' 3 | import Footer from '../components/Footer' 4 | 5 | import {AuthProvider} from '../context/AuthContext' 6 | 7 | 8 | function MyApp({ Component, pageProps }) { 9 | 10 | return ( 11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 | ) 19 | } 20 | export default MyApp 21 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/pages/account/login.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useRouter } from "next/router"; 3 | import useAuth from "../../hooks/useAuth"; 4 | 5 | const Login = () => { 6 | const [email, setEmail] = useState(""); 7 | const [password, setPassword] = useState(""); 8 | const [error, setError] = useState(null); 9 | 10 | const { setUser } = useAuth(); 11 | 12 | const router = useRouter(); 13 | const handleSubmit = async (e) => { 14 | e.preventDefault(); 15 | // call the API route 16 | 17 | const res = await fetch("/api/login", { 18 | method: "POST", 19 | headers: { "Content-Type": "application/json" }, 20 | body: JSON.stringify({ email, password }), 21 | }); 22 | if (res.ok) { 23 | const user = await res.json(); 24 | setUser(user); 25 | router.push("/"); 26 | } else { 27 | const errData = await res.json(); 28 | setError(errData); 29 | } 30 | }; 31 | 32 | return ( 33 |
34 |

Login

35 | {error && ( 36 |
37 | {error.detail} 38 |
39 | )} 40 |
41 |
45 | 56 | 67 | 70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | export default Login; 77 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/pages/account/logout.jsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect } from "react"; 3 | import useAuth from "../../hooks/useAuth"; 4 | 5 | const Logout = () => { 6 | const { user, setUser } = useAuth(); 7 | const removeCookie = async () => { 8 | const res = await fetch("/api/logout", { 9 | method: "POST", 10 | headers: { "Content-Type": "application/json" }, 11 | }); 12 | }; 13 | const router = useRouter(); 14 | useEffect(() => { 15 | removeCookie(); 16 | setUser(null); 17 | 18 | router.push("/"); 19 | }, []); 20 | 21 | return <>; 22 | }; 23 | 24 | export default Logout; 25 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/pages/account/register.jsx: -------------------------------------------------------------------------------- 1 | const Register = () => { 2 | return
register
; 3 | }; 4 | 5 | export default Register; 6 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/pages/api/login.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie' 2 | 3 | export default async (req, res)=>{ 4 | if (req.method==='POST'){ 5 | 6 | const {email, password} = req.body 7 | 8 | const result = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users/login`, { 9 | method:'POST', 10 | headers:{'Content-Type':'application/json'}, 11 | body:JSON.stringify({email, password}) 12 | }) 13 | 14 | const data = await result.json() 15 | if (result.ok){ 16 | const jwt = data.token 17 | res.status(200).setHeader('Set-Cookie', cookie.serialize( 18 | 'jwt',jwt, 19 | { 20 | path:'/', 21 | httpOnly: true, 22 | sameSite:'strict', 23 | maxAge:1800 24 | 25 | } 26 | )).json({ 27 | 'username':data['user']['username'], 28 | 'email':data['user']['email'], 29 | 'role':data['user']['role'], 30 | 'jwt':jwt 31 | }) 32 | } else { 33 | 34 | data['error'] = data['detail'] 35 | res.status(401) 36 | res.json(data) 37 | return 38 | } 39 | 40 | 41 | } else { 42 | res.setHeader('Allow',['POST']) 43 | res.status(405).json({message:`Method ${req.method} not allowed`}) 44 | return 45 | } 46 | } -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/pages/api/logout.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie' 2 | 3 | 4 | export default async (req, res)=>{ 5 | 6 | res.status(200).setHeader('Set-Cookie', cookie.serialize( 7 | 'jwt','', 8 | { 9 | path:'/', 10 | httpOnly: true, 11 | sameSite:'strict', 12 | maxAge:-1 13 | } 14 | ) 15 | 16 | ) 17 | res.json({"message":"success"}) 18 | res.end() 19 | } -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/pages/api/user.js: -------------------------------------------------------------------------------- 1 | 2 | export default async function(req, res){ 3 | if (req.method==='GET'){ 4 | const {jwt} = req.cookies; 5 | 6 | if(!jwt){ 7 | res.status(401).end() 8 | return; 9 | } 10 | 11 | try { 12 | const result = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/users/me`, { 13 | method:'GET', 14 | headers:{ 15 | 'Content-Type':'application/json', 16 | 'Authorization':`Bearer ${jwt}` 17 | 18 | } 19 | }) 20 | const userData = await result.json() 21 | userData['jwt'] = jwt 22 | res.status(200).json(userData).end() 23 | } catch (error) { 24 | res.status(401).end() 25 | return; 26 | } 27 | 28 | 29 | } else { 30 | res.setHeader('Allow',['GET']) 31 | res.status(405).json({message:`Method ${req.method} not allowed`}).end() 32 | } 33 | } -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/pages/cars/[id].jsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | export const getStaticPaths = async () => { 3 | const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/cars`); 4 | const cars = await res.json(); 5 | 6 | const paths = cars.map((car) => ({ 7 | params: { id: car._id }, 8 | })); 9 | 10 | return { paths, fallback: "blocking" }; 11 | }; 12 | 13 | export const getStaticProps = async ({ params: { id } }) => { 14 | const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/cars/${id}`); 15 | const car = await res.json(); 16 | 17 | return { 18 | props: { car }, 19 | revalidate: 10, 20 | }; 21 | }; 22 | 23 | const CarById = ({ car }) => { 24 | return ( 25 |
26 |

27 | {car.brand} - {car.make} 28 |

29 |
30 | 31 |
32 |
{`This fine car was manufactured in ${car.year}, it made just ${car.km} km and it sports a ${car.cm3} cm3 engine.`}
33 | 34 |
Price: {car.price} eur
35 |
36 | ); 37 | }; 38 | 39 | export default CarById; 40 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/pages/cars/index.js: -------------------------------------------------------------------------------- 1 | import Card from "../../components/Card" 2 | 3 | export const getServerSideProps = async () => { 4 | 5 | 6 | 7 | const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/cars/`); 8 | const cars = await res.json(); 9 | 10 | return { 11 | props: { 12 | cars, 13 | revalidate: 10, 14 | }, 15 | }; 16 | }; 17 | 18 | const Cars = ({ cars }) => { 19 | 20 | return ( 21 |
22 |

Available Cars

23 | 24 |
25 | {cars.map((car) => { 26 | const {_id, brand, make, picture, year, km, cm3, price} = car 27 | return ( 28 | 29 | 40 | 41 | ); 42 | })} 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default Cars; 49 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/pages/index.js: -------------------------------------------------------------------------------- 1 | const HomePage = () => { 2 | 3 | 4 | return ( 5 |
6 | 7 | FARM Cars 8 | 9 |
10 | ) 11 | } 12 | export default HomePage 13 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter8/frontend/next-cars/public/favicon.ico -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /chapter8/frontend/next-cars/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./pages/**/*.{js,ts,jsx,tsx}", 4 | "./components/**/*.{js,ts,jsx,tsx}", 5 | ], 6 | theme: { 7 | extend: { 8 | 9 | }, 10 | }, 11 | plugins: [ 12 | require('@tailwindcss/forms') 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /chapter9/backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | testvenv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | 142 | # pytype static type analyzer 143 | .pytype/ 144 | 145 | # Cython debug symbols 146 | cython_debug/ 147 | 148 | # PyCharm 149 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 150 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 151 | # and can be added to the global gitignore or merged into this file. For a more nuclear 152 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 153 | #.idea/ 154 | .vercel 155 | -------------------------------------------------------------------------------- /chapter9/backend/cars.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "_id": { 3 | "model": "Marea" 4 | }, 5 | "avgKm": 226773.5 6 | },{ 7 | "_id": { 8 | "model": "Ulysse" 9 | }, 10 | "avgKm": 226565 11 | },{ 12 | "_id": { 13 | "model": "Stilo" 14 | }, 15 | "avgKm": 207230 16 | },{ 17 | "_id": { 18 | "model": "Tempra" 19 | }, 20 | "avgKm": 203500 21 | },{ 22 | "_id": { 23 | "model": "Multipla" 24 | }, 25 | "avgKm": 194302.14285714287 26 | },{ 27 | "_id": { 28 | "model": "Croma" 29 | }, 30 | "avgKm": 192818.45833333334 31 | },{ 32 | "_id": { 33 | "model": "Palio" 34 | }, 35 | "avgKm": 188650 36 | },{ 37 | "_id": { 38 | "model": "Punto" 39 | }, 40 | "avgKm": 179875.5 41 | },{ 42 | "_id": { 43 | "model": "Idea" 44 | }, 45 | "avgKm": 174078.58333333334 46 | },{ 47 | "_id": { 48 | "model": "Sedici" 49 | }, 50 | "avgKm": 171750.2 51 | },{ 52 | "_id": { 53 | "model": "Grande Punto" 54 | }, 55 | "avgKm": 167311.23376623375 56 | },{ 57 | "_id": { 58 | "model": "Seicento" 59 | }, 60 | "avgKm": 165000 61 | },{ 62 | "_id": { 63 | "model": "Bravo" 64 | }, 65 | "avgKm": 161005.92 66 | },{ 67 | "_id": { 68 | "model": "Doblo" 69 | }, 70 | "avgKm": 158813.56603773584 71 | },{ 72 | "_id": { 73 | "model": "500L" 74 | }, 75 | "avgKm": 154006 76 | },{ 77 | "_id": { 78 | "model": "Ducato" 79 | }, 80 | "avgKm": 152316.75 81 | },{ 82 | "_id": { 83 | "model": "Panda" 84 | }, 85 | "avgKm": 147225.375 86 | },{ 87 | "_id": { 88 | "model": "Freemonte" 89 | }, 90 | "avgKm": 145444.14285714287 91 | },{ 92 | "_id": { 93 | "model": "Fiorino" 94 | }, 95 | "avgKm": 139750 96 | },{ 97 | "_id": { 98 | "model": "Qubo" 99 | }, 100 | "avgKm": 134404 101 | },{ 102 | "_id": { 103 | "model": "Scudo" 104 | }, 105 | "avgKm": 129957.66666666667 106 | },{ 107 | "_id": { 108 | "model": "500" 109 | }, 110 | "avgKm": 125122.86363636363 111 | },{ 112 | "_id": { 113 | "model": "Tipo" 114 | }, 115 | "avgKm": 91632.66666666667 116 | },{ 117 | "_id": { 118 | "model": "500X" 119 | }, 120 | "avgKm": 83000 121 | },{ 122 | "_id": { 123 | "model": "500C" 124 | }, 125 | "avgKm": 55000 126 | }] -------------------------------------------------------------------------------- /chapter9/backend/importScript.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from fastapi.encoders import jsonable_encoder 3 | from pymongo import MongoClient 4 | from decouple import config 5 | from models import CarBase 6 | 7 | # connect to mongodb 8 | DB_URL = config("DB_URL", cast=str) 9 | DB_NAME = config("DB_NAME", cast=str) 10 | 11 | client = MongoClient(DB_URL) 12 | db = client[DB_NAME] 13 | cars = db["cars"] 14 | 15 | 16 | # 17 | 18 | # read csv 19 | with open("filteredCars.csv", encoding="utf-8") as f: 20 | csv_reader = csv.DictReader(f) 21 | name_records = list(csv_reader) 22 | 23 | 24 | for rec in name_records: 25 | print(rec) 26 | 27 | try: 28 | rec["cm3"] = int(rec["cm3"]) 29 | rec["price"] = int(rec["price"]) 30 | rec["year"] = int(rec["year"]) 31 | rec["km"] = int(rec["km"]) 32 | rec["brand"] = str(rec["brand"]) 33 | rec["make"] = str(rec["make"]) 34 | 35 | car = jsonable_encoder(CarBase(**rec)) 36 | print("Inserting:", car) 37 | cars.insert_one(car) 38 | 39 | except ValueError: 40 | pass 41 | -------------------------------------------------------------------------------- /chapter9/backend/main.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | from fastapi import FastAPI 3 | 4 | import aioredis 5 | from fastapi_cache import FastAPICache 6 | from fastapi_cache.backends.redis import RedisBackend 7 | 8 | # there seems to be a bug with FastAPI's middleware 9 | # https://stackoverflow.com/questions/65191061/fastapi-cors-middleware-not-working-with-get-method/65994876#65994876 10 | 11 | from starlette.middleware import Middleware 12 | from starlette.middleware.cors import CORSMiddleware 13 | 14 | 15 | middleware = [ 16 | Middleware( 17 | CORSMiddleware, 18 | allow_origins=["*"], 19 | allow_credentials=True, 20 | allow_methods=["*"], 21 | allow_headers=["*"], 22 | ) 23 | ] 24 | 25 | from motor.motor_asyncio import AsyncIOMotorClient 26 | 27 | from routers.cars import router as cars_router 28 | 29 | 30 | DB_URL = config("DB_URL", cast=str) 31 | DB_NAME = config("DB_NAME", cast=str) 32 | 33 | 34 | # define origins 35 | origins = [ 36 | "*", 37 | ] 38 | 39 | # instantiate the app 40 | app = FastAPI(middleware=middleware) 41 | 42 | app.include_router(cars_router, prefix="/cars", tags=["cars"]) 43 | 44 | 45 | @app.on_event("startup") 46 | async def startup_db_client(): 47 | app.mongodb_client = AsyncIOMotorClient(DB_URL) 48 | app.mongodb = app.mongodb_client[DB_NAME] 49 | redis = aioredis.from_url( 50 | "redis://localhost:6379", encoding="utf8", decode_responses=True 51 | ) 52 | FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache") 53 | 54 | 55 | @app.on_event("shutdown") 56 | async def shutdown_db_client(): 57 | app.mongodb_client.close() 58 | -------------------------------------------------------------------------------- /chapter9/backend/models.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | 3 | from pydantic import Field, BaseModel, validator 4 | 5 | 6 | class PyObjectId(ObjectId): 7 | @classmethod 8 | def __get_validators__(cls): 9 | yield cls.validate 10 | 11 | @classmethod 12 | def validate(cls, v): 13 | if not ObjectId.is_valid(v): 14 | raise ValueError("Invalid objectid") 15 | return ObjectId(v) 16 | 17 | @classmethod 18 | def __modify_schema__(cls, field_schema): 19 | field_schema.update(type="string") 20 | 21 | 22 | class MongoBaseModel(BaseModel): 23 | id: PyObjectId = Field(default_factory=PyObjectId, alias="_id") 24 | 25 | class Config: 26 | json_encoders = {ObjectId: str} 27 | 28 | 29 | class CarBase(MongoBaseModel): 30 | 31 | brand: str = Field(..., min_length=2) 32 | make: str = Field(..., min_length=1) 33 | year: int = Field(..., gt=1975, lt=2023) 34 | price: int = Field(...) 35 | km: int = Field(...) 36 | cm3: int = Field(..., gt=400, lt=8000) 37 | -------------------------------------------------------------------------------- /chapter9/backend/random_forest_pipe.joblib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter9/backend/random_forest_pipe.joblib -------------------------------------------------------------------------------- /chapter9/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.6.1 2 | click==8.1.3 3 | colorama==0.4.5 4 | dnslib==0.9.20 5 | dnspython==2.2.1 6 | fastapi==0.79.0 7 | h11==0.13.0 8 | idna==3.3 9 | motor==3.0.0 10 | numpy==1.23.1 11 | pandas==1.4.3 12 | pydantic==1.9.1 13 | pymongo==4.2.0 14 | python-dateutil==2.8.2 15 | python-decouple==3.6 16 | python-http-client==3.3.7 17 | PythonDNS==0.1 18 | pytz==2022.1 19 | sendgrid==6.9.7 20 | six==1.16.0 21 | sniffio==1.2.0 22 | starkbank-ecdsa==2.0.3 23 | starlette==0.19.1 24 | typing_extensions==4.3.0 25 | uvicorn==0.18.2 26 | joblib 27 | -------------------------------------------------------------------------------- /chapter9/backend/routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter9/backend/routers/__init__.py -------------------------------------------------------------------------------- /chapter9/backend/routers/cars.py: -------------------------------------------------------------------------------- 1 | from json import load 2 | from math import ceil 3 | 4 | from typing import List, Optional 5 | 6 | from fastapi import APIRouter, Request, Body, HTTPException, BackgroundTasks 7 | 8 | 9 | from fastapi_cache.decorator import cache 10 | 11 | from models import CarBase 12 | 13 | from utils.report import report_pipeline 14 | 15 | import joblib 16 | import pandas as pd 17 | 18 | 19 | router = APIRouter() 20 | 21 | 22 | @router.get("/all", response_description="List all cars") 23 | async def list_all_cars( 24 | request: Request, 25 | min_price: int = 0, 26 | max_price: int = 100000, 27 | brand: Optional[str] = None, 28 | page: int = 1, 29 | ) -> List[CarBase]: 30 | 31 | RESULTS_PER_PAGE = 25 32 | skip = (page - 1) * RESULTS_PER_PAGE 33 | 34 | query = {"price": {"$lt": max_price, "$gt": min_price}} 35 | if brand: 36 | query["brand"] = brand 37 | 38 | # count total docs 39 | pages = ceil( 40 | await request.app.mongodb["cars"].count_documents(query) / RESULTS_PER_PAGE 41 | ) 42 | 43 | full_query = ( 44 | request.app.mongodb["cars"] 45 | .find(query) 46 | .sort("km", -1) 47 | .skip(skip) 48 | .limit(RESULTS_PER_PAGE) 49 | ) 50 | 51 | results = [CarBase(**raw_car) async for raw_car in full_query] 52 | 53 | return {"results": results, "pages": pages} 54 | 55 | 56 | # sample of N cars 57 | @router.get("/sample/{n}", response_description="Sample of N cars") 58 | @cache(expire=60) 59 | async def get_sample(n: int, request: Request): 60 | 61 | query = [ 62 | {"$match": {"year": {"$gt": 2010}}}, 63 | { 64 | "$project": { 65 | "_id": 0, 66 | } 67 | }, 68 | {"$sample": {"size": n}}, 69 | {"$sort": {"brand": 1, "make": 1, "year": 1}}, 70 | ] 71 | 72 | full_query = request.app.mongodb["cars"].aggregate(query) 73 | results = [el async for el in full_query] 74 | return results 75 | 76 | 77 | # aggregation by model / avg price 78 | @router.get("/brand/{val}/{brand}", response_description="Get brand models by val") 79 | async def brand_price(brand: str, val: str, request: Request): 80 | 81 | query = [ 82 | {"$match": {"brand": brand}}, 83 | {"$project": {"_id": 0}}, 84 | { 85 | "$group": {"_id": {"model": "$make"}, f"avg_{val}": {"$avg": f"${val}"}}, 86 | }, 87 | {"$sort": {f"avg_{val}": 1}}, 88 | ] 89 | 90 | full_query = request.app.mongodb["cars"].aggregate(query) 91 | return [el async for el in full_query] 92 | 93 | 94 | # count cars by brand 95 | @router.get("/brand/count", response_description="Count by brand") 96 | async def brand_count(request: Request): 97 | 98 | query = [{"$group": {"_id": "$brand", "count": {"$sum": 1}}}] 99 | 100 | full_query = request.app.mongodb["cars"].aggregate(query) 101 | return [el async for el in full_query] 102 | 103 | 104 | # count cars by make 105 | @router.get("/make/count/{brand}", response_description="Count by brand") 106 | async def brand_count(brand: str, request: Request): 107 | 108 | query = [ 109 | {"$match": {"brand": brand}}, 110 | {"$group": {"_id": "$make", "count": {"$sum": 1}}}, 111 | {"$sort": {"count": -1}}, 112 | ] 113 | 114 | full_query = request.app.mongodb["cars"].aggregate(query) 115 | results = [el async for el in full_query] 116 | return results 117 | 118 | 119 | @router.post("/email", response_description="Send email") 120 | async def send_mail( 121 | background_tasks: BackgroundTasks, 122 | cars_num: int = Body(...), 123 | email: str = Body(...), 124 | ): 125 | 126 | background_tasks.add_task(report_pipeline, email, cars_num) 127 | 128 | return {"Received": {"email": email, "cars_num": cars_num}} 129 | 130 | 131 | @router.post("/predict", response_description="Predict price") 132 | async def predict( 133 | brand: str = Body(...), 134 | make: str = Body(...), 135 | year: int = Body(...), 136 | cm3: int = Body(...), 137 | km: int = Body(...), 138 | ): 139 | 140 | print(brand, make, year, cm3, km) 141 | loaded_model = joblib.load("./random_forest_pipe.joblib") 142 | 143 | # = Body? 144 | input_data = { 145 | "brand": brand, 146 | "make": make, 147 | "year": year, 148 | "cm3": cm3, 149 | "km": km, 150 | } 151 | 152 | from_db_df = pd.DataFrame(input_data, index=[0]) 153 | 154 | prediction = float(loaded_model.predict(from_db_df)[0]) 155 | return {"prediction": prediction} 156 | -------------------------------------------------------------------------------- /chapter9/backend/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter9/backend/utils/__init__.py -------------------------------------------------------------------------------- /chapter9/backend/utils/report.py: -------------------------------------------------------------------------------- 1 | from .report_query import make_query 2 | from .send_report import send_report 3 | 4 | 5 | def report_pipeline(email, cars_number): 6 | 7 | # make the query - get the data and some HTML 8 | try: 9 | query_data = make_query(cars_number) 10 | except Exception as e: 11 | print(e) 12 | print("Couldn't make the query") 13 | 14 | try: 15 | send_report(email=email, subject="FARM Cars Report", HTMLcontent=query_data) 16 | 17 | except Exception as e: 18 | print(e) 19 | -------------------------------------------------------------------------------- /chapter9/backend/utils/report_query.py: -------------------------------------------------------------------------------- 1 | # a function that returns a sample of N cars 2 | from pymongo import MongoClient 3 | from decouple import config 4 | 5 | import pandas as pd 6 | 7 | # connect to mongodb 8 | DB_URL = config("DB_URL", cast=str) 9 | DB_NAME = config("DB_NAME", cast=str) 10 | 11 | client = MongoClient(DB_URL) 12 | db = client[DB_NAME] 13 | cars = db["cars"] 14 | 15 | 16 | def make_query(cars_number: int): 17 | 18 | query = [ 19 | {"$match": {"year": {"$gt": 2010}}}, 20 | { 21 | "$project": { 22 | "_id": 0, 23 | } 24 | }, 25 | {"$sample": {"size": cars_number}}, 26 | {"$sort": {"brand": 1, "make": 1, "year": 1}}, 27 | ] 28 | 29 | full_query = cars.aggregate(query) 30 | results = [el for el in full_query] 31 | 32 | HTML = pd.DataFrame(results).to_html(index=False) 33 | 34 | return HTML 35 | -------------------------------------------------------------------------------- /chapter9/backend/utils/send_report.py: -------------------------------------------------------------------------------- 1 | # send report by email 2 | from decouple import config 3 | 4 | import sendgrid 5 | from sendgrid.helpers.mail import * 6 | 7 | SENDGRID_ID = config("SENDGRID_ID", cast=str) 8 | 9 | 10 | def send_report(email, subject, HTMLcontent): 11 | 12 | sg = sendgrid.SendGridAPIClient(api_key=SENDGRID_ID) 13 | from_email = Email("FARM@freethrow.rs") 14 | to_email = To(email) 15 | subject = "FARM Cars daily report" 16 | content = Content( 17 | "text/plain", "this is dynamic text, potentially coming from our database" 18 | ) 19 | 20 | mail = Mail(from_email, to_email, subject, content, html_content=HTMLcontent) 21 | 22 | try: 23 | response = sg.client.mail.send.post(request_body=mail.get()) 24 | print(response) 25 | print("Sending email") 26 | print(response.status_code) 27 | print(response.body) 28 | print(response.headers) 29 | 30 | except Exception as e: 31 | print(e) 32 | print("Could not send email") 33 | -------------------------------------------------------------------------------- /chapter9/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /chapter9/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 your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may 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 | -------------------------------------------------------------------------------- /chapter9/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.4", 7 | "@testing-library/react": "^13.3.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "chart.js": "^3.8.0", 10 | "d3-scale-chromatic": "^3.0.0", 11 | "daisyui": "^2.20.0", 12 | "react": "^18.2.0", 13 | "react-chartjs-2": "^4.3.1", 14 | "react-dom": "^18.2.0", 15 | "react-pdf-tailwind": "^1.0.1", 16 | "react-router-dom": "^6.3.0", 17 | "react-scripts": "5.0.1", 18 | "react-select": "^5.4.0", 19 | "swr": "^1.3.0", 20 | "web-vitals": "^2.1.4" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@tailwindcss/forms": "^0.5.2", 48 | "autoprefixer": "^10.4.7", 49 | "postcss": "^8.4.14", 50 | "tailwindcss": "^3.1.6" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /chapter9/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /chapter9/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter9/frontend/public/favicon.ico -------------------------------------------------------------------------------- /chapter9/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | FARM Car Analytics 28 | 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /chapter9/frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter9/frontend/public/logo192.png -------------------------------------------------------------------------------- /chapter9/frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Full-Stack-FastAPI-React-and-MongoDB/ff5c29643f25ac5aa05095787f80a766da1ce127/chapter9/frontend/public/logo512.png -------------------------------------------------------------------------------- /chapter9/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 | -------------------------------------------------------------------------------- /chapter9/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /chapter9/frontend/src/App.js: -------------------------------------------------------------------------------- 1 | 2 | function App() { 3 | return ( 4 |
5 |
Some test text
6 |
7 | ); 8 | } 9 | 10 | export default App; 11 | -------------------------------------------------------------------------------- /chapter9/frontend/src/components/BrandCount.jsx: -------------------------------------------------------------------------------- 1 | import useSWR from "swr"; 2 | 3 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js"; 4 | import { Pie } from "react-chartjs-2"; 5 | 6 | import { schemePastel1 } from "d3-scale-chromatic"; 7 | 8 | const colors = schemePastel1; 9 | 10 | ChartJS.register(ArcElement, Tooltip, Legend); 11 | 12 | const fetcher = (...args) => fetch(...args).then((res) => res.json()); 13 | 14 | const BrandCount = () => { 15 | const { data, error } = useSWR( 16 | `${process.env.REACT_APP_API_URL}/cars/brand/count`, 17 | fetcher 18 | ); 19 | if (error) return
failed to load
; 20 | if (!data) return
loading...
; 21 | 22 | const chartData = { 23 | labels: data.map((item) => { 24 | return item["_id"]; 25 | }), 26 | datasets: [ 27 | { 28 | data: data.map((item) => item.count), 29 | backgroundColor: data.map((item, index) => colors[index]), 30 | borderWidth: 3, 31 | }, 32 | ], 33 | }; 34 | 35 | return ( 36 |
37 |

38 | Vehicle Count by Brand 39 |

40 | 41 |
42 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default BrandCount; 49 | -------------------------------------------------------------------------------- /chapter9/frontend/src/components/BrandValue.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import useSWR from "swr"; 3 | 4 | import { 5 | Chart as ChartJS, 6 | CategoryScale, 7 | LinearScale, 8 | BarElement, 9 | Title, 10 | Tooltip, 11 | Legend, 12 | } from "chart.js"; 13 | import { Bar } from "react-chartjs-2"; 14 | import CarsDropdown from "./CarsDropdown"; 15 | 16 | ChartJS.register( 17 | CategoryScale, 18 | LinearScale, 19 | BarElement, 20 | Title, 21 | Tooltip, 22 | Legend 23 | ); 24 | 25 | export const options = (val) => { 26 | let optObj = { 27 | responsive: true, 28 | plugins: { 29 | legend: { 30 | position: "top", 31 | }, 32 | title: { 33 | display: true, 34 | text: `Average ${val} of car models by brand`, 35 | }, 36 | }, 37 | }; 38 | if (val === "year") { 39 | optObj["scales"] = { 40 | y: { 41 | min: 1980, 42 | }, 43 | }; 44 | } 45 | return optObj; 46 | }; 47 | 48 | const fetcher = (...args) => fetch(...args).then((res) => res.json()); 49 | 50 | const BrandValue = ({ val }) => { 51 | const queryStr = `avg_${val}`; 52 | 53 | const [brand, setBrand] = useState("Fiat"); 54 | 55 | const { data, error } = useSWR( 56 | `${process.env.REACT_APP_API_URL}/cars/brand/${val}/${brand}`, 57 | fetcher 58 | ); 59 | 60 | if (error) return
failed to load
; 61 | if (!data) return
loading...
; 62 | 63 | const chartData = { 64 | labels: data.map((item) => { 65 | return item["_id"]["model"]; 66 | }), 67 | datasets: [ 68 | { 69 | label: brand, 70 | data: data.map((item) => Math.round(item[queryStr])), 71 | hoverBackgroundColor: ["#B91C1C"], 72 | }, 73 | ], 74 | }; 75 | 76 | return ( 77 |
78 |

79 | {val.toUpperCase()} by model for a given brand - {brand} 80 |

81 | 82 |
83 | setBrand(event.target.value)} 85 | elValue={brand} 86 | /> 87 |
88 |
89 | 90 |
91 |
92 | ); 93 | }; 94 | 95 | export default BrandValue; 96 | -------------------------------------------------------------------------------- /chapter9/frontend/src/components/Card.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Card = ({ car }) => { 4 | const { make, brand, km, cm3, price, year } = car; 5 | 6 | return ( 7 |
8 |
9 | {brand} - {make} ({cm3}cm3) 10 |
11 |
12 | {km} Km / Year: {year} 13 |
14 |
Price: {price} eur
15 |
16 | ); 17 | }; 18 | 19 | export default Card; 20 | -------------------------------------------------------------------------------- /chapter9/frontend/src/components/CarsDropdown.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CarsDropdown = ({ selectHandler, allCars, elValue }) => { 4 | const carBrands = [ 5 | "Fiat", 6 | "Opel", 7 | "Renault", 8 | "Peugeot", 9 | "VW", 10 | "Ford", 11 | "Honda", 12 | "Toyota", 13 | ]; 14 | return ( 15 | 29 | ); 30 | }; 31 | 32 | CarsDropdown.defaultProps = { 33 | allCars: false, 34 | elValue: "", 35 | }; 36 | export default CarsDropdown; 37 | -------------------------------------------------------------------------------- /chapter9/frontend/src/components/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import BrandCount from "./BrandCount"; 2 | import ModelCount from "./ModelCount"; 3 | import BrandValue from "./BrandValue"; 4 | 5 | const Dashboard = () => { 6 | return ( 7 |
8 |

9 | DashBoard 10 |

11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default Dashboard; 24 | -------------------------------------------------------------------------------- /chapter9/frontend/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Footer = () => { 4 | return ( 5 |
6 | Footer 7 |
8 | ); 9 | }; 10 | 11 | export default Footer; 12 | -------------------------------------------------------------------------------- /chapter9/frontend/src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | const Header = () => { 3 | return ( 4 |
5 |
6 | (isActive ? "active menu" : "menu")} 8 | to="/" 9 | > 10 | Cars 11 | 12 | (isActive ? "active menu" : "menu")} 14 | to="/dashboard" 15 | > 16 | Dashboard 17 | 18 | (isActive ? "active menu" : "menu")} 20 | to="/report" 21 | > 22 | Report 23 | 24 |
25 |
26 | ); 27 | }; 28 | export default Header; 29 | -------------------------------------------------------------------------------- /chapter9/frontend/src/components/Home.jsx: -------------------------------------------------------------------------------- 1 | // fetcher for SWR 2 | import { useState } from "react"; 3 | import useSWR from "swr"; 4 | import Card from "./Card"; 5 | import CarsDropdown from "./CarsDropdown"; 6 | 7 | const fetcher = (...args) => fetch(...args).then((res) => res.json()); 8 | 9 | const Home = () => { 10 | const [pageIndex, setPageIndex] = useState(1); 11 | const [brand, setBrand] = useState(""); 12 | 13 | const { nextData, nextError } = useSWR( 14 | `${process.env.REACT_APP_API_URL}/cars/all?page=${ 15 | pageIndex + 1 16 | }&brand=${brand}`, 17 | fetcher 18 | ); 19 | 20 | const { data, error } = useSWR( 21 | `${process.env.REACT_APP_API_URL}/cars/all?page=${pageIndex}&brand=${brand}`, 22 | fetcher 23 | ); 24 | 25 | if (error) return
`failed to load {process.env.REACT_APP_API_URL}`
; 26 | if (!data) return
loading...
; 27 | 28 | return ( 29 |
30 |

31 | Explore Cars 32 |

33 |
34 | {JSON.stringify(nextData)} {JSON.stringify(nextError)} 35 |
36 | 37 |
38 | { 40 | setBrand(event.target.value); 41 | setPageIndex(1); 42 | }} 43 | allCars={true} 44 | elValue={brand} 45 | /> 46 |
47 | {pageIndex > 1 ? ( 48 | 54 | ) : ( 55 | <> 56 | )} 57 | {pageIndex < data.pages ? ( 58 | 64 | ) : ( 65 | <> 66 | )} 67 |
68 |
69 | Brand: 70 | 71 | {brand ? brand : "All brands"} 72 | 73 | Page: 74 | 75 | {pageIndex} of {data.pages} 76 | 77 |
78 |
79 | 80 |
81 | {data.results.map((car) => ( 82 | 83 | ))} 84 |
85 |
86 | ); 87 | }; 88 | 89 | export default Home; 90 | -------------------------------------------------------------------------------- /chapter9/frontend/src/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import Header from "./Header"; 2 | import Footer from "./Footer"; 3 | 4 | const Layout = ({ children }) => { 5 | return ( 6 |
7 |
8 |
9 | {children} 10 |
11 |
12 |
13 | ); 14 | }; 15 | export default Layout; 16 | -------------------------------------------------------------------------------- /chapter9/frontend/src/components/ModelCount.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import useSWR from "swr"; 3 | 4 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js"; 5 | import { Pie } from "react-chartjs-2"; 6 | 7 | import { interpolateGnBu } from "d3-scale-chromatic"; 8 | import CarsDropdown from "./CarsDropdown"; 9 | 10 | ChartJS.register(ArcElement, Tooltip, Legend); 11 | 12 | const fetcher = (...args) => fetch(...args).then((res) => res.json()); 13 | 14 | const ModelCount = () => { 15 | const [brand, setBrand] = useState("Fiat"); 16 | const { data, error } = useSWR( 17 | `${process.env.REACT_APP_API_URL}/cars/make/count/${brand}`, 18 | fetcher 19 | ); 20 | if (error) return
failed to load
; 21 | if (!data) return
loading...
; 22 | 23 | const chartData = { 24 | labels: data.map((item) => { 25 | return item["_id"]; 26 | }), 27 | datasets: [ 28 | { 29 | data: data.map((item) => item.count), 30 | backgroundColor: data 31 | .map((item, index) => interpolateGnBu(index / data.length)) 32 | .sort(() => 0.5 - Math.random()), 33 | borderWidth: 3, 34 | }, 35 | ], 36 | }; 37 | 38 | console.log(chartData); 39 | 40 | return ( 41 |
42 |

43 | Number of vehicles by model for a given brand 44 |

45 |
46 | setBrand(event.target.value)} 48 | elValue={brand} 49 | /> 50 |
51 | 52 |
53 | 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default ModelCount; 60 | -------------------------------------------------------------------------------- /chapter9/frontend/src/components/Report.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | const Report = () => { 4 | const [email, setEmail] = useState(""); 5 | const [carsNum, setCarsNum] = useState(10); 6 | const [message, setMessage] = useState(""); 7 | const [loading, setLoading] = useState(false); 8 | 9 | const handleForm = async (e) => { 10 | e.preventDefault(); 11 | setLoading(true); 12 | const res = await fetch(`${process.env.REACT_APP_API_URL}/cars/email`, { 13 | method: "POST", 14 | headers: { "Content-Type": "application/json" }, 15 | body: JSON.stringify({ email, cars_num: carsNum }), 16 | }); 17 | 18 | if (res.ok) { 19 | setLoading(false); 20 | setMessage(`Report with ${carsNum} cars sent to ${email}!`); 21 | } 22 | }; 23 | 24 | return ( 25 |
26 |

27 | Generate Report 28 |

29 |
30 | {loading && ( 31 |
32 | Generating and sending report in the background... 33 |
34 | )} 35 | 36 | {message && ( 37 |
38 | {message} 39 |
40 | )} 41 | 42 | {!loading && !message && ( 43 |
44 | 45 | setEmail(e.target.value)} 51 | className="px-2 py-1 my-2 rounded-full" 52 | /> 53 | 54 | setCarsNum(e.target.value)} 62 | className="p-2 py-1 my-2 rounded-full" 63 | /> 64 | 70 |
71 | )} 72 |
73 |
74 | ); 75 | }; 76 | 77 | export default Report; 78 | -------------------------------------------------------------------------------- /chapter9/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | @layer components { 7 | .menu { 8 | @apply p-2 my-2 rounded-lg 9 | } 10 | .active { 11 | @apply text-red-800 border border-red-800 12 | } 13 | } -------------------------------------------------------------------------------- /chapter9/frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import {BrowserRouter,Routes,Route, 4 | } from "react-router-dom"; 5 | 6 | import './index.css'; 7 | import Layout from './components/Layout'; 8 | import Home from './components/Home'; 9 | import Dashboard from './components/Dashboard'; 10 | import Report from './components/Report'; 11 | 12 | 13 | const root = ReactDOM.createRoot(document.getElementById('root')); 14 | root.render( 15 | 16 | 17 | 18 | 19 | } /> 20 | } /> 21 | } /> 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | 30 | -------------------------------------------------------------------------------- /chapter9/frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./src/**/*.{js,jsx,ts,tsx}", 4 | ], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [ 9 | require('@tailwindcss/forms'), 10 | require("daisyui") 11 | ], 12 | } 13 | --------------------------------------------------------------------------------