27
loading...
This website collects cookies to deliver better user experience
$ mkdir farm-stack-tut
$ cd farm-stack-tut
$ mkdir backend
$ code .
$ git init
$ yarn create react-app frontend --template typescript
$ cd backend
$ git init
$ touch requirements.txt main.py model.py database.py
fastapi == 0.65.1
uvicorn == 0.14.0
motor == 2.4.0
gunicorn == 20.1.0
pymongo[srv] == 3.12.0
$ pipenv install -r requirements.txt
$ yarn create farm-app --name=farm-stack-tut
$ git remote add origin <url>
$ git add *
$ git commit -m "first commit"
$ git branch -M main
$ git push -u origin main
$ git submodule add <frontend-url> frontend
$ git submodule add <backend-url> backend
main.py
, where we need this code to get started:from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
origins = ["*"] # This will eventually be changed to only the origins you will use once it's deployed, to secure the app a bit more.
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)
@app.get('/')
def get_root():
return {"Ping": "Pong"}
$ uvicorn main:app --reload
http://localhost:8000
, you should get the message { "Ping": "Pong" } that we set to return. If you did, we can get started building the rest of the backend.model.py
. We're going to include 3 strings, a nanoid, a title, and a description, plus a boolean value to check if it is finished or not. The model looks like this:from pydantic import BaseModel
class Todo(BaseModel):
nanoid: str
title: str
desc: str
checked: bool
$ pipenv install python-dotenv
DATABASE_URI = "<URI>"
.gitignore
file, and put .env
inside.from model import *
import motor.motor_asyncio
from dotenv import dotenv_values
import os
config = dotenv_values(".env")
DATABASE_URI = config.get("DATABASE_URI")
if os.getenv("DATABASE_URI"): DATABASE_URI = os.getenv("DATABASE_URI") #ensures that if we have a system environment variable, it uses that instead
client = motor.motor_asyncio.AsyncIOMotorClient(DATABASE_URI)
database = client.TodoDatabase
collection = database.todos
async def fetch_all_todos():
todos = []
cursor = collection.find()
async for doc in cursor:
todos.append(Todo(**doc))
return todos
async def fetch_one_todo(nanoid):
doc = await collection.find_one({"nanoid": nanoid}, {"_id": 0})
return doc
async def create_todo(todo):
doc = todo.dict()
await collection.insert_one(doc)
result = await fetch_one_todo(todo.nanoid)
return result
async def change_todo(nanoid, title, desc, checked):
await collection.update_one({"nanoid": nanoid}, {"$set": {"title": title, "desc": desc, "checked": checked}})
result = await fetch_one_todo(nanoid)
return result
async def remove_todo(nanoid):
await collection.delete_one({"nanoid": nanoid})
return True
main.py
:@app.get("/api/get-todo/{nanoid}", response_model=Todo)
async def get_one_todo(nanoid):
todo = await fetch_one_todo(nanoid)
if not todo: raise HTTPException(404)
return todo
@app.get("/api/get-todo")
async def get_todos():
todos = await fetch_all_todos()
if not todos: raise HTTPException(404)
return todos
@app.post("/api/add-todo", response_model=Todo)
async def add_todo(todo: Todo):
result = await create_todo(todo)
if not result: raise HTTPException(400)
return result
@app.put("/api/update-todo/{nanoid}", response_model=Todo)
async def update_todo(todo: Todo):
result = await change_todo(nanoid, title, desc, checked)
if not result: raise HTTPException(400)
return result
@app.delete("/api/delete-todo/{nanoid}")
async def delete_todo(nanoid):
result = await remove_todo(nanoid)
if not result: raise HTTPException(400)
return result
import React from "react";
import TodoList from "./components/TodoList";
function App() {
return (
<div className="app-container">
<header className="app-header">
<h1>To-Do List</h1>
</header>
<div className="content">
<TodoList />
</div>
</div>
);
}
export default App;
TodoList.tsx
, Todo.tsx
, and AddTodo.tsx
. They should all look basically the same for now, just a div with a className depending on what they are, like this for the todo:import React from "react";
function Todo() {
return(
<div className="todo-container">
</div>
);
}
export default Todo;
index.scss
:$primary: #146286;
$secondary: #641486;
$accent: #3066b8;
.app-header {
background-color: $primary;
color: white;
padding: 5px;
border-radius: 10px;
margin-bottom: 5px;
}
.content {
.todo-list-container {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(5, 1fr);
grid-gap: 10px;
.todo-container {
display: flex;
flex-direction: column;
justify-content: space-evenly;
border-radius: 6px;
padding: 10px 6px;
background-color: $secondary;
color: white;
h1 {
font-size: 20px;
}
span {
font-size: 14px;
}
footer {
display: flex;
flex-direction: row-reverse;
}
}
}
}
import { nanoid } from "nanoid";
import React, { useState } from "react";
import { TodoType } from "./components/Todo";
import TodoList from "./components/TodoList";
function App() {
const [todoList, setTodoList] = useState<TodoType[]>([]);
const [title, setTitle] = useState<string>("");
const [desc, setDesc] = useState<string>("");
const changeTitle = (event: React.ChangeEvent<HTMLInputElement>) => {
setTitle(event.currentTarget.value);
};
const changeDesc = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setDesc(event.currentTarget.value);
}
const changeChecked = (event: React.MouseEvent<HTMLInputElement>, id: string) => {
let temp = [...todoList];
temp.forEach((item) => {
if (item.nanoid === id) {
item.checked = !item.checked;
}
});
setTodoList(temp);
};
const addTodo = (event: React.MouseEvent<HTMLButtonElement>) => {
let newTodo: TodoType = {
nanoid: nanoid(),
title: title,
desc: desc,
checked: false
};
setTodoList([...todoList, newTodo]);
}
return (
<div className="app-container">
<header className="app-header">
<h1>To-Do List</h1>
</header>
<div className="content">
<TodoList submit={addTodo} changeDesc={changeDesc} changeTitle={changeTitle} list={todoList} changeChecked={changeChecked} />
</div>
</div>
);
}
export default App;
import React from "react";
import AddTodo from "./AddTodo";
import Todo, { TodoType } from "./Todo";
interface TodoListProps {
list: TodoType[]
changeChecked: (event: React.MouseEvent<HTMLInputElement>, nanoid: string) => void;
changeTitle: (event: React.ChangeEvent<HTMLInputElement>) => void;
changeDesc: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
submit: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
function TodoList(props: TodoListProps) {
return(
<div className="todo-list-container">
{props.list.map((item) => {
return(
<Todo nanoid={item.nanoid} title={item.title} desc={item.desc} checked={item.checked} changeChecked={props.changeChecked} />
);
})}
<AddTodo changeTitle={props.changeTitle} changeDesc={props.changeDesc} submit={props.submit} />
</div>
);
}
export default TodoList;
import React from "react";
export type TodoType = {
nanoid: string;
title: string;
desc: string;
checked: boolean;
}
interface TodoProps extends TodoType {
changeChecked: (event: React.MouseEvent<HTMLInputElement>, nanoid: string) => void;
}
function Todo(props: TodoProps) {
return(
<div className="todo-container">
<h1>{props.title}</h1>
<span>{props.desc}</span>
<footer>
<input type="checkbox" checked={props.checked} onClick={(e) => props.changeChecked(e, props.nanoid)} />
</footer>
</div>
);
}
export default Todo;
import React from "react";
interface AddTodoProps {
submit: (event: React.MouseEvent<HTMLButtonElement>) => void;
changeTitle: (event: React.ChangeEvent<HTMLInputElement>) => void;
changeDesc: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
}
function AddTodo(props: AddTodoProps) {
return(
<div className="todo-container add-todo-container">
<input type="text" className="title" placeholder="Title..." onChange={props.changeTitle} />
<textarea className="desc" placeholder="Description..." onChange={props.changeDesc}>
</textarea>
<button className="submit" onClick={props.submit}>Add Todo</button>
</div>
);
}
export default AddTodo;
useEffect()
and axios to store all this data in the database.App.tsx
:import axios from "axios";
import { nanoid } from "nanoid";
import React, { useEffect, useState } from "react";
import { TodoType } from "./components/Todo";
import TodoList from "./components/TodoList";
function App() {
const [todoList, setTodoList] = useState<TodoType[]>([]);
const [title, setTitle] = useState<string>("");
const [desc, setDesc] = useState<string>("");
useEffect(() => {
axios
.get(process.env.REACT_APP_BACKEND_URL + "/api/get-todo")
.then((res) => {
setTodoList(res.data);
});
}, []);
const changeTitle = (event: React.ChangeEvent<HTMLInputElement>) => {
setTitle(event.currentTarget.value);
};
const changeDesc = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setDesc(event.currentTarget.value);
};
const changeChecked = (
event: React.MouseEvent<HTMLInputElement>,
id: string
) => {
let temp = [...todoList];
let tempIndex = 0;
temp.forEach((item, i) => {
if (item.nanoid === id) {
item.checked = !item.checked;
tempIndex = i;
}
});
setTodoList(temp);
let item = todoList[tempIndex];
axios.put(
process.env.REACT_APP_BACKEND_URL +
`/api/update-todo/${item.nanoid}`,
{ nanoid: item.nanoid, title: item.title, desc: item.desc, checked: item.checked}
);
};
const addTodo = (event: React.MouseEvent<HTMLButtonElement>) => {
let newTodo: TodoType = {
nanoid: nanoid(),
title: title,
desc: desc,
checked: false,
};
setTodoList([...todoList, newTodo]);
axios.post(
process.env.REACT_APP_BACKEND_URL + "/api/add-todo",
JSON.stringify(newTodo)
);
};
return (
<div className="app-container">
<header className="app-header">
<h1>To-Do List</h1>
</header>
<div className="content">
<TodoList
submit={addTodo}
changeDesc={changeDesc}
changeTitle={changeTitle}
list={todoList}
changeChecked={changeChecked}
/>
</div>
</div>
);
}
export default App;
Procfile
at the root of the backend, and put this in it:web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app
python-dotenv == 0.19.0
to your requirements.txt
file and reinstall dependencies to ensure everything boots properly.main.py
, and replace the "*"
in the origins array with "https://<username>.github.io"
.DATABASE_URI
in as a config var.package.json
, but it's pretty straight forward still. .env
's backend url to be the heroku app url, commit and push, then do:$ yarn add --dev gh-pages
package.json
, and add these lines to "scripts"
:"predeploy": "yarn build",
"deploy": "REACT_APP_BACKEND_URL=<backend-url> gh-pages -d build"
"homepage": "https://<username>.github.io/<project-name>-frontend/"
$ yarn start
^C
$ yarn deploy