Compare commits

..

No commits in common. "10cc9342b10bf0e58ff0df60cb0cb1fbf222ec80" and "7d2f47b1a2fc3146f9acda224b4fa0ba10354114" have entirely different histories.

32 changed files with 46 additions and 1191 deletions

View File

@ -27,9 +27,8 @@ jobs:
run: golangci-lint run run: golangci-lint run
- name: Validate SQL schema - name: Validate SQL schema
uses: playfulpachyderm/sqlite-lint@v0.0.3 run: |
with: sqlite3 whatever.db < pkg/db/schema.sql
schema-file: pkg/db/schema.sql
- name: Run tests - name: Run tests
run: | run: |

1
.gitignore vendored
View File

@ -1,2 +1 @@
sample_data/data sample_data/data
*_templ.go

View File

@ -7,7 +7,6 @@ import (
"path/filepath" "path/filepath"
pkg_db "recipe_book/pkg/db" pkg_db "recipe_book/pkg/db"
"recipe_book/pkg/web"
) )
const DB_FILENAME = "food.db" const DB_FILENAME = "food.db"
@ -28,15 +27,6 @@ func main() {
switch args[0] { switch args[0] {
case "init": case "init":
init_db() init_db()
case "webserver":
fs := flag.NewFlagSet("", flag.ExitOnError)
should_auto_open := fs.Bool("auto-open", false, "")
addr := fs.String("addr", "localhost:3080", "port to listen on") // Random port that's probably not in use
if err := fs.Parse(args[1:]); err != nil {
panic(err)
}
start_webserver(*addr, *should_auto_open)
default: default:
fmt.Printf(COLOR_RED+"invalid subcommand: %q\n"+COLOR_RESET, args[0]) fmt.Printf(COLOR_RED+"invalid subcommand: %q\n"+COLOR_RESET, args[0])
os.Exit(1) os.Exit(1)
@ -65,14 +55,3 @@ const (
COLOR_GRAY = "\033[37m" COLOR_GRAY = "\033[37m"
COLOR_WHITE = "\033[97m" COLOR_WHITE = "\033[97m"
) )
func start_webserver(addr string, should_auto_open bool) {
db, err := pkg_db.DBConnect(filepath.Join(db_path, DB_FILENAME))
if err != nil {
fmt.Println(COLOR_RED + "opening database: " + err.Error() + COLOR_RESET)
os.Exit(1)
}
app := web.NewApp(db)
app.Run(addr, should_auto_open)
}

View File

@ -1,29 +0,0 @@
Questions:
- should recipes have a reference to a food with its computed nutrition?
- or the other way around-- store a
- how should ingredients-that-are-recipes be linked to recipes?
Options:
1. - ingredients just store a food_id
- foods store an optional recipe_id
Problems:
- all recipes have to have an associated food entry
- foods have to have a recipe_id-- shouldn't this be the other way around?
2. - ingredients just store a food_id
- recipes store a food_id
Problems:
- all recipes have to have an associated food entry
- can't tell which ingredients are recipes without doing an indexed search
3. - ingredients store a food_id and a recipe_id, only one is valid
Implications: recipes and foods can be connected in any way
Problems:
- joining is more complicated

1
go.mod
View File

@ -10,7 +10,6 @@ require (
) )
require ( require (
github.com/a-h/templ v0.2.793 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

2
go.sum
View File

@ -1,7 +1,5 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY=
github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=

View File

@ -1,9 +0,0 @@
package db
import (
"errors"
)
var (
ErrNotInDB = errors.New("not in db")
)

View File

@ -1,38 +1,36 @@
package db package db
import ( import (
"database/sql"
"errors"
"fmt" "fmt"
) )
type FoodID uint64 type FoodID uint64
type Food struct { type Food struct {
ID FoodID `db:"rowid" json:"id"` ID FoodID `db:"rowid"`
Name string `db:"name" json:"name"` Name string `db:"name"`
Cals float32 `db:"cals" json:"cals,string"` Cals float32 `db:"cals"`
Carbs float32 `db:"carbs" json:"carbs,string"` Carbs float32 `db:"carbs"`
Protein float32 `db:"protein" json:"protein,string"` Protein float32 `db:"protein"`
Fat float32 `db:"fat" json:"fat,string"` Fat float32 `db:"fat"`
Sugar float32 `db:"sugar" json:"sugar,string"` Sugar float32 `db:"sugar"`
Alcohol float32 `db:"alcohol" json:"alcohol,string"` Alcohol float32 `db:"alcohol"`
Water float32 `db:"water" json:"water,string"` Water float32 `db:"water"`
Potassium float32 `db:"potassium" json:"potassium,string"` Potassium float32 `db:"potassium"`
Calcium float32 `db:"calcium" json:"calcium,string"` Calcium float32 `db:"calcium"`
Sodium float32 `db:"sodium" json:"sodium,string"` Sodium float32 `db:"sodium"`
Magnesium float32 `db:"magnesium" json:"magnesium,string"` Magnesium float32 `db:"magnesium"`
Phosphorus float32 `db:"phosphorus" json:"phosphorus,string"` Phosphorus float32 `db:"phosphorus"`
Iron float32 `db:"iron" json:"iron,string"` Iron float32 `db:"iron"`
Zinc float32 `db:"zinc" json:"zinc,string"` Zinc float32 `db:"zinc"`
Mass float32 `db:"mass" json:"mass,string"` // In grams Mass float32 `db:"mass"` // In grams
Price float32 `db:"price" json:"price,string"` Price float32 `db:"price"`
Density float32 `db:"density" json:"density,string"` Density float32 `db:"density"`
CookRatio float32 `db:"cook_ratio" json:"cook_ratio,string"` CookRatio float32 `db:"cook_ratio"`
} }
// Format as string // Format as string
@ -103,22 +101,5 @@ func (db *DB) GetFoodByID(id FoodID) (ret Food, err error) {
from foods from foods
where rowid = ? where rowid = ?
`, id) `, id)
if errors.Is(err, sql.ErrNoRows) {
return Food{}, ErrNotInDB
}
return return
} }
func (db *DB) GetAllBaseFoods() []Food {
var ret []Food
err := db.DB.Select(&ret, `
select rowid, name, cals, carbs, protein, fat, sugar, alcohol, water, potassium, calcium, sodium,
magnesium, phosphorus, iron, zinc, mass, price, density, cook_ratio
from foods
where rowid not in (select computed_food_id from recipes)
`)
if err != nil {
panic(err)
}
return ret
}

View File

@ -70,15 +70,3 @@ func TestFoodSaveAndLoad(t *testing.T) {
t.Error(diff) t.Error(diff)
} }
} }
// Should list all the base foods (i.e., ones that aren't recipes)
func TestListAllBaseFoods(t *testing.T) {
assert := assert.New(t)
db := get_test_db()
base_foods := db.GetAllBaseFoods()
assert.True(len(base_foods) >= 100)
for _, f := range base_foods {
assert.NotContains([]FoodID{10000, 10001}, f.ID, f) // Computed foods have ID >= 10000 in seed data
}
}

View File

@ -2,8 +2,6 @@ package db
import ( import (
"fmt" "fmt"
"math"
"strings"
) )
type IngredientID uint64 type IngredientID uint64
@ -86,29 +84,6 @@ func (db *DB) DeleteIngredient(i Ingredient) {
} }
} }
func (i Ingredient) DisplayAmount() string { // func (i Ingredient) AddTo(r *Recipe) {
var f float32
switch i.Units { // }
case COUNT:
f = i.Quantity
case GRAMS:
f = i.Quantity * i.Food.Mass
case LBS:
f = i.Quantity * i.Food.Mass / 454
case OZ:
f = i.Quantity * i.Food.Mass / 28
case ML:
f = i.Quantity * i.Food.Mass / i.Food.Density
case CUPS:
f = i.Quantity * i.Food.Mass / i.Food.Density / 250
case TSP:
f = i.Quantity * i.Food.Mass / i.Food.Density / 5
case TBSP:
f = i.Quantity * i.Food.Mass / i.Food.Density / 15
case FLOZ:
f = i.Quantity * i.Food.Mass / i.Food.Density / 30
default:
panic(i)
}
return strings.TrimSpace(fmt.Sprintf("%d %s", int(math.Round(float64(f))), i.Units.Abbreviation()))
}

View File

@ -68,24 +68,3 @@ func TestSaveAndLoadIngredient(t *testing.T) {
assert.NoError(err) assert.NoError(err)
require.Len(new_recipe.Ingredients, 0) require.Len(new_recipe.Ingredients, 0)
} }
func TestDisplayAmount(t *testing.T) {
assert := assert.New(t)
db := get_test_db()
onion := get_food(db, 28)
test_cases := []struct {
Ingredient
Expected string
}{
{Ingredient{Quantity: 1, Units: COUNT, Food: onion}, "1"},
{Ingredient{Quantity: 2, Units: COUNT, Food: onion}, "2"},
{Ingredient{Quantity: 1.818, Units: GRAMS, Food: onion}, "400 g"},
{Ingredient{Quantity: 9, Units: LBS, Food: onion}, "4 lbs"},
{Ingredient{Quantity: 2, Units: ML, Food: onion}, "440 mL"},
}
for _, tc := range test_cases {
assert.Equal(tc.Ingredient.DisplayAmount(), tc.Expected)
}
}

View File

@ -43,6 +43,7 @@ type Recipe struct {
func (db *DB) SaveRecipe(r *Recipe) { func (db *DB) SaveRecipe(r *Recipe) {
if r.ID == RecipeID(0) { if r.ID == RecipeID(0) {
// Do create // Do create
// Create the computed food // Create the computed food
computed_food := Food{Name: r.Name} computed_food := Food{Name: r.Name}
db.SaveFood(&computed_food) db.SaveFood(&computed_food)
@ -81,9 +82,6 @@ func (db *DB) SaveRecipe(r *Recipe) {
} }
for i := range r.Ingredients { for i := range r.Ingredients {
r.Ingredients[i].InRecipeID = r.ID r.Ingredients[i].InRecipeID = r.ID
if r.Ingredients[i].ListOrder == 0 {
r.Ingredients[i].ListOrder = int64(i)
}
db.SaveIngredient(&r.Ingredients[i]) db.SaveIngredient(&r.Ingredients[i])
} }
// Update the computed food // Update the computed food
@ -132,15 +130,6 @@ func (db *DB) GetRecipeByID(id RecipeID) (ret Recipe, err error) {
return return
} }
func (db *DB) GetAllRecipes() []Recipe {
var ret []Recipe
err := db.DB.Select(&ret, `select rowid, name, blurb, instructions, computed_food_id from recipes`)
if err != nil {
panic(err)
}
return ret
}
func (r Recipe) ComputeFood() Food { func (r Recipe) ComputeFood() Food {
// If r.ComputedFoodID is 0, so should be the ID of returned Food // If r.ComputedFoodID is 0, so should be the ID of returned Food
ret := Food{ID: r.ComputedFoodID, Name: r.Name} ret := Food{ID: r.ComputedFoodID, Name: r.Name}

View File

@ -52,8 +52,8 @@ func TestRecipeComputeFood(t *testing.T) {
f2 := Food{0, "", 16.5, 15.5, 14.5, 13.5, 12.5, 11.5, 10.5, 9.5, 8.5, 7.5, 6.5, 5.5, 4.5, 3.5, 2.5, 1.5, 0, 0} f2 := Food{0, "", 16.5, 15.5, 14.5, 13.5, 12.5, 11.5, 10.5, 9.5, 8.5, 7.5, 6.5, 5.5, 4.5, 3.5, 2.5, 1.5, 0, 0}
recipe := Recipe{Ingredients: []Ingredient{ recipe := Recipe{Ingredients: []Ingredient{
COUNT.Of(f1, 1), {Quantity: 1, Food: f1},
COUNT.Of(f2, 1), {Quantity: 1, Food: f2},
}} }}
computed_food := recipe.ComputeFood() computed_food := recipe.ComputeFood()
assert.Equal(computed_food.Cals, float32(17.5)) assert.Equal(computed_food.Cals, float32(17.5))
@ -74,8 +74,8 @@ func TestRecipeComputeFood(t *testing.T) {
assert.Equal(computed_food.Price, float32(17.5)) assert.Equal(computed_food.Price, float32(17.5))
recipe2 := Recipe{Ingredients: []Ingredient{ recipe2 := Recipe{Ingredients: []Ingredient{
COUNT.Of(f1, 1.5), {Quantity: 1.5, Food: f1},
COUNT.Of(f2, 0.5), {Quantity: 0.5, Food: f2},
}} }}
computed_food2 := recipe2.ComputeFood() computed_food2 := recipe2.ComputeFood()
assert.Equal(computed_food2.Cals, float32(9.75)) assert.Equal(computed_food2.Cals, float32(9.75))
@ -94,14 +94,6 @@ func TestRecipeComputeFood(t *testing.T) {
assert.Equal(computed_food2.Zinc, float32(22.75)) assert.Equal(computed_food2.Zinc, float32(22.75))
assert.Equal(computed_food2.Mass, float32(23.75)) assert.Equal(computed_food2.Mass, float32(23.75))
assert.Equal(computed_food2.Price, float32(24.75)) assert.Equal(computed_food2.Price, float32(24.75))
// Combine recipes in a recipe
recipe3 := Recipe{Ingredients: []Ingredient{
COUNT.Portion(recipe, 2),
COUNT.Portion(recipe2, 1),
}}
computed_food3 := recipe3.ComputeFood()
assert.Equal(computed_food3.Cals, float32(44.75))
} }
func TestRecipeSaveComputedFood(t *testing.T) { func TestRecipeSaveComputedFood(t *testing.T) {
@ -117,35 +109,13 @@ func TestRecipeSaveComputedFood(t *testing.T) {
require.Equal(parm.Name, "parmigiano") require.Equal(parm.Name, "parmigiano")
recipe := Recipe{Name: "pasta w/ sauce", Ingredients: []Ingredient{ recipe := Recipe{Name: "pasta w/ sauce", Ingredients: []Ingredient{
COUNT.Of(pasta, 2), {Quantity: 2, FoodID: pasta.ID, Food: pasta, ListOrder: 0},
COUNT.Of(tomatoes, 2.5), {Quantity: 2.5, FoodID: tomatoes.ID, Food: tomatoes, ListOrder: 1},
COUNT.Of(tomatoes, 0.5), {Quantity: 0.5, FoodID: parm.ID, Food: parm, ListOrder: 2},
}} }}
db.SaveRecipe(&recipe) db.SaveRecipe(&recipe)
computed_food := get_food(db, recipe.ComputedFoodID) computed_food := get_food(db, recipe.ComputedFoodID)
if diff := deep.Equal(recipe.ComputeFood(), computed_food); diff != nil { if diff := deep.Equal(recipe.ComputeFood(), computed_food); diff != nil {
t.Error(diff) t.Error(diff)
} }
// Test putting a recipe in a recipe
mozza := get_food(db, 83)
require.Equal(mozza.Name, "mozzarella")
recipe2 := Recipe{Name: "baked pasta w/ mozza", Ingredients: []Ingredient{
COUNT.Portion(recipe, 0.5),
GRAMS.Of(mozza, 300),
}}
db.SaveRecipe(&recipe2)
computed_food2 := get_food(db, recipe2.ComputedFoodID)
if diff := deep.Equal(recipe2.ComputeFood(), computed_food2); diff != nil {
t.Error(diff)
}
}
// Should list all the recipes
func TestListAllRecipes(t *testing.T) {
assert := assert.New(t)
db := get_test_db()
recipes := db.GetAllRecipes()
assert.True(len(recipes) >= 2)
} }

View File

@ -5,7 +5,7 @@ PRAGMA foreign_keys = on;
-- ======= -- =======
create table db_version ( create table db_version (
version integer primary key version integer not null
) strict; ) strict;
insert into db_version values(0); insert into db_version values(0);
@ -93,7 +93,8 @@ create table iterations (rowid integer primary key,
unique(derived_recipe_id) unique(derived_recipe_id)
) strict; ) strict;
-- create table daily_logs ( create table daily_logs (rowid integer primary key,
-- date integer not null unique, date integer not null unique,
-- computed_food_id integer references foods(rowid) not null
-- ); computed_food_id integer references foods(rowid) not null
);

View File

@ -3,7 +3,7 @@ package db
type Units uint64 type Units uint64
const ( const (
COUNT = Units(iota + 1) // Start at 1 to match SQLite ID column COUNT = Units(iota + 1)
GRAMS GRAMS
LBS LBS
OZ OZ
@ -15,7 +15,7 @@ const (
) )
var names = []string{"", "count", "grams", "pounds", "ounces", "milliliters", "cups", "teaspoons", "tablespoons", "fluid ounces"} var names = []string{"", "count", "grams", "pounds", "ounces", "milliliters", "cups", "teaspoons", "tablespoons", "fluid ounces"}
var abbreviations = []string{"", "", "g", "lbs", "oz", "mL", "cups", "tsp", "tbsp", "fl-oz"} var abbreviations = []string{"", "ct", "g", "lbs", "oz", "mL", "cups", "tsp", "tbsp", "fl-oz"}
func (u Units) Name() string { func (u Units) Name() string {
return names[u] return names[u]
@ -48,29 +48,3 @@ func (u Units) Of(f Food, n float32) Ingredient {
panic(u) panic(u)
} }
} }
func (u Units) Portion(r Recipe, n float32) Ingredient {
f := r.ComputeFood()
switch u {
case COUNT:
return Ingredient{RecipeID: r.ID, Quantity: n, Units: u, Food: f}
case GRAMS:
return Ingredient{RecipeID: r.ID, Quantity: n / f.Mass, Units: u, Food: f}
case LBS:
return Ingredient{RecipeID: r.ID, Quantity: n * 454 / f.Mass, Units: u, Food: f}
case OZ:
return Ingredient{RecipeID: r.ID, Quantity: n * 28 / f.Mass, Units: u, Food: f}
case ML:
return Ingredient{RecipeID: r.ID, Quantity: n * f.Density / f.Mass, Units: u, Food: f}
case CUPS:
return Ingredient{RecipeID: r.ID, Quantity: n * f.Density * 250 / f.Mass, Units: u, Food: f}
case TSP:
return Ingredient{RecipeID: r.ID, Quantity: n * f.Density * 5 / f.Mass, Units: u, Food: f}
case TBSP:
return Ingredient{RecipeID: r.ID, Quantity: n * f.Density * 15 / f.Mass, Units: u, Food: f}
case FLOZ:
return Ingredient{RecipeID: r.ID, Quantity: n * f.Density * 30 / f.Mass, Units: u, Food: f}
default:
panic(u)
}
}

View File

@ -1,105 +0,0 @@
package web
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/a-h/templ"
"io"
"net/http"
"strconv"
"strings"
. "recipe_book/pkg/db"
"recipe_book/pkg/web/tpl/pages"
)
// Router: `/ingredients`
func (app *Application) Ingredients(w http.ResponseWriter, r *http.Request) {
app.traceLog.Printf("Ingredient: %s", r.URL.Path)
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if parts[0] == "" {
// Index page
switch r.Method {
case "GET":
app.IngredientsIndex(w, r)
case "POST":
app.IngredientCreate(w, r)
}
return
}
// Details page
food_id, err := strconv.Atoi(parts[0])
if err != nil {
app.error_400(w, r, fmt.Sprintf("invalid ID: %s", parts[0]))
return
}
food, err := app.DB.GetFoodByID(FoodID(food_id))
if errors.Is(err, ErrNotInDB) {
app.error_404(w, r)
return
} else if err != nil {
panic(err)
}
app.IngredientDetail(food, w, r)
}
// Handler: `GET /ingredients`
func (app *Application) IngredientsIndex(w http.ResponseWriter, r *http.Request) {
foods := app.DB.GetAllBaseFoods()
err := pages.Base("Ingredients").Render(
templ.WithChildren(
context.Background(),
pages.IngredientsIndex(foods),
),
w)
panic_if(err)
}
// Handler: `POST /ingredients`
func (app *Application) IngredientCreate(w http.ResponseWriter, r *http.Request) {
var food Food
data, err := io.ReadAll(r.Body)
if err != nil {
panic(err)
}
err = json.Unmarshal(data, &food)
if err != nil {
app.ErrorLog.Print(err)
panic(err)
}
app.DB.SaveFood(&food)
http.Redirect(w, r, fmt.Sprintf("/ingredients/%d", food.ID), 303)
}
// Handler: `GET /ingredients/:id`
func (app *Application) IngredientDetail(food Food, w http.ResponseWriter, r *http.Request) {
// If it's a POST request, update the food
if r.Method == "POST" {
data, err := io.ReadAll(r.Body)
if err != nil {
panic(err)
}
err = json.Unmarshal(data, &food)
if err != nil {
app.ErrorLog.Print(err)
panic(err)
}
// Save the updated food
app.DB.SaveFood(&food)
app.traceLog.Printf("POST Ingredient Detail: %#v", food)
}
err := pages.Base(fmt.Sprintf("Ingredient: %s", food.Name)).Render(
templ.WithChildren(
context.Background(),
pages.IngredientDetail(food),
),
w)
panic_if(err)
}

View File

@ -1,85 +0,0 @@
package web
import (
"context"
"errors"
"fmt"
"github.com/a-h/templ"
"net/http"
"strconv"
"strings"
. "recipe_book/pkg/db"
"recipe_book/pkg/web/tpl/pages"
)
// Router: `/ingredients`
func (app *Application) Recipes(w http.ResponseWriter, r *http.Request) {
app.traceLog.Printf("Recipe: %s", r.URL.Path)
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if parts[0] == "" {
// Index page
switch r.Method {
case "GET":
app.RecipesIndex(w, r)
}
return
}
// Details page
recipe_id, err := strconv.Atoi(parts[0])
if err != nil {
app.error_400(w, r, fmt.Sprintf("invalid ID: %s", parts[0]))
return
}
recipe, err := app.DB.GetRecipeByID(RecipeID(recipe_id))
if errors.Is(err, ErrNotInDB) {
app.error_404(w, r)
return
} else if err != nil {
panic(err)
}
app.RecipeDetail(recipe, w, r)
}
// Handler: `GET /recipes`
func (app *Application) RecipesIndex(w http.ResponseWriter, r *http.Request) {
foods := app.DB.GetAllRecipes()
err := pages.Base("Ingredients").Render(
templ.WithChildren(
context.Background(),
pages.RecipesIndex(foods),
),
w)
panic_if(err)
}
// // Handler: `POST /ingredients`
// func (app *Application) IngredientCreate(w http.ResponseWriter, r *http.Request) {
// var food Food
// data, err := io.ReadAll(r.Body)
// if err != nil {
// panic(err)
// }
// err = json.Unmarshal(data, &food)
// if err != nil {
// app.ErrorLog.Print(err)
// panic(err)
// }
// app.DB.SaveFood(&food)
// http.Redirect(w, r, fmt.Sprintf("/ingredients/%d", food.ID), 303)
// }
// Handler: `GET /ingredients/:id`
func (app *Application) RecipeDetail(recipe Recipe, w http.ResponseWriter, r *http.Request) {
err := pages.Base(recipe.Name).Render(
templ.WithChildren(
context.Background(),
templ.Join(pages.RecipeDetail(recipe)),
),
w)
panic_if(err)
}

View File

@ -1,25 +0,0 @@
package web
import (
"fmt"
"net/http"
)
func panic_if(err error) {
if err != nil {
panic(err)
}
}
func (app *Application) error_400(w http.ResponseWriter, r *http.Request, msg string) {
http.Error(w, fmt.Sprintf("Bad Request\n\n%s", msg), 400)
}
func (app *Application) error_404(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not Found", 404)
}
func (app *Application) error_500(w http.ResponseWriter, r *http.Request, err error) {
panic("TODO")
}

View File

@ -1,50 +0,0 @@
package web
import (
"fmt"
"net/http"
"time"
)
// Middlewares are wrappers around `http.Handler`s
type Middleware func(http.Handler) http.Handler
func (app *Application) WithMiddlewares() http.Handler {
var ret http.Handler = app
for i := range app.Middlewares {
ret = app.Middlewares[i](ret)
}
return ret
}
func secureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Referrer-Policy", "same-origin")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "deny")
next.ServeHTTP(w, r)
})
}
func (app *Application) logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t := time.Now()
next.ServeHTTP(w, r)
duration := time.Since(t)
app.accessLog.Printf("%s - %s %s %s\t%s", r.RemoteAddr, r.Proto, r.Method, r.URL.RequestURI(), duration)
})
}
func (app *Application) recoverPanic(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
w.Header().Set("Connection", "close")
app.error_500(w, r, fmt.Errorf("%s", err))
}
}()
next.ServeHTTP(w, r)
})
}

View File

@ -1,95 +0,0 @@
package web
import (
"crypto/tls"
"log"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"time"
pkg_db "recipe_book/pkg/db"
)
type Application struct {
accessLog *log.Logger
traceLog *log.Logger
InfoLog *log.Logger
ErrorLog *log.Logger
Middlewares []Middleware
DB pkg_db.DB
}
func NewApp(db pkg_db.DB) Application {
ret := Application{
accessLog: log.New(os.Stdout, "ACCESS\t", log.Ldate|log.Ltime),
traceLog: log.New(os.Stdout, "TRACE\t", log.Ldate|log.Ltime),
InfoLog: log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime),
ErrorLog: log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile),
DB: db,
}
ret.Middlewares = []Middleware{
secureHeaders,
ret.logRequest,
ret.recoverPanic,
}
return ret
}
// Manual router implementation.
// I don't like the weird matching behavior of http.ServeMux, and it's not hard to write by hand.
func (app *Application) ServeHTTP(w http.ResponseWriter, r *http.Request) {
app.traceLog.Printf("base handler: %s", r.URL.Path)
parts := strings.Split(r.URL.Path, "/")[1:]
switch parts[0] {
case "static":
http.StripPrefix("/static", http.HandlerFunc(app.ServeStatic)).ServeHTTP(w, r)
case "ingredients":
http.StripPrefix("/ingredients", http.HandlerFunc(app.Ingredients)).ServeHTTP(w, r)
case "recipes":
http.StripPrefix("/recipes", http.HandlerFunc(app.Recipes)).ServeHTTP(w, r)
default:
app.error_404(w, r)
return
}
}
func (app *Application) Run(address string, should_auto_open bool) {
srv := &http.Server{
Addr: address,
ErrorLog: app.ErrorLog,
Handler: app.WithMiddlewares(),
TLSConfig: &tls.Config{
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
},
IdleTimeout: time.Minute,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
app.InfoLog.Printf("Starting server on %s", address)
if should_auto_open {
go func(url string) {
var cmd *exec.Cmd
switch runtime.GOOS {
case "darwin": // macOS
cmd = exec.Command("open", url)
case "windows":
cmd = exec.Command("cmd", "/c", "start", url)
default: // Linux and others
cmd = exec.Command("xdg-open", url)
}
if err := cmd.Run(); err != nil {
log.Printf("Failed to open homepage: %s", err.Error())
}
}("http://" + address)
}
err := srv.ListenAndServe()
app.ErrorLog.Fatal(err)
}

View File

@ -1,33 +0,0 @@
package web
import (
"embed"
"net/http"
"path"
"runtime"
)
//go:embed "static"
var embedded_files embed.FS
var use_embedded = ""
var this_dir string
func init() {
_, this_file, _, _ := runtime.Caller(0) // `this_file` is absolute path to this source file
this_dir = path.Dir(this_file)
}
// Serve static assets, either from the disk (if running in development mode), or from go:embedded files
func (app *Application) ServeStatic(w http.ResponseWriter, r *http.Request) {
// Static files can be stored in browser cache
w.Header().Set("Cache-Control", "public, max-age=3600")
if use_embedded == "true" {
// Serve directly from the embedded files
http.FileServer(http.FS(embedded_files)).ServeHTTP(w, r)
} else {
// Serve from disk
http.FileServer(http.Dir(path.Join(this_dir, "static"))).ServeHTTP(w, r)
}
}

View File

@ -1,157 +0,0 @@
body {
display: flex;
flex-direction: row;
margin: 0;
}
nav {
width: 20em;
max-width: 20%;
border-right: 1px solid #ddd;
min-height: 100vh;
}
input#search-bar {
max-width: 96%;
width: 20em;
}
main {
flex-grow: 1;
margin-right: 10%;
padding-left: 2em;
height: fit-content;
}
input[type='text'], select, textarea, span.select2-container {
margin: 0 0.5em 0 0.2em;
}
div.nothing {
/* Used because the Hoon type checker requires a `manx` sometimes (can't use ~) */
display: none;
}
.buttons-container {
display: flex;
margin: 0em 0 1em 0.2em;
gap: 2em;
}
.share-container {
display: flex;
}
button#share-this-recipe {
}
#shareLink {
width: 30em;
}
#shareLink.hidden {
display: none;
}
div.labelled-input {
display: flex;
}
div.labelled-input label {
flex-basis: 12em;
text-align: right;
margin-right: 0.3em;
}
div.labelled-input input {
flex-basis: 30em;
}
div.labelled-input .title-text-parent {
color: #888;
position: relative;
}
.ingredient-editor input[type="submit"] {
margin: 2em 0 0 41em;
}
.new-item-button {
margin-bottom: 1em;
}
.title-text {
display: none;
position: absolute;
width: max-content;
border: 1px solid black;
padding: 0.2em;
background-color: white;
margin-top: 0.3em;
text-align: left;
}
.title-text-parent:focus .title-text, .title-text-parent:hover .title-text {
display: revert;
}
table {
border-collapse: collapse;
}
th {
padding: 0 0.3em;
}
td {
text-align: right;
padding: 0.3em 0.6em;
}
td.name {
text-align: left;
}
td.total {
border-top: 1px solid darkgray;
}
form.x-button {
display: inline;
margin: 0;
}
form.x-button input {
padding: 0.1em 0.3em;
}
form.add-instr {
display: flex;
align-items: flex-end;
}
ol#instructions {
padding: 0 1em;
}
ol#instructions li {
list-style: none;
display: flex;
align-items: flex-start;
margin: 0.3em 0;
}
ol#instructions li span {
margin-top: 0.2em;
}
ol#instructions li span p {
margin: 0 0 0.5em 0;
}
ol#instructions li .instr-number{
margin-left: 1em;
margin-right: 0.5em;
cursor: move;
cursor: -webkit-grab;
}
img#logo {
width: 60%;
display: block;
margin: 1em auto 3em auto;
background-color: #cef;
border-radius: 50%;
border: 5px solid #8bd;
box-sizing: border-box;
}
div.original-author-container {
display: flex;
align-items: center;
gap: 2em;
}
div.original-author-container form {
margin: 0;
}

View File

@ -1,35 +0,0 @@
(function() {
let api
htmx.defineExtension('json-enc', {
init: function(apiRef) {
api = apiRef
},
onEvent: function(name, evt) {
if (name === 'htmx:configRequest') {
evt.detail.headers['Content-Type'] = 'application/json'
}
},
encodeParameters: function(xhr, parameters, elt) {
xhr.overrideMimeType('text/json')
const vals = api.getExpressionVars(elt)
const object = {}
parameters.forEach(function(value, key) {
// FormData encodes values as strings, restore hx-vals/hx-vars with their initial types
const typedValue = Object.hasOwn(vals, key) ? vals[key] : value
if (Object.hasOwn(object, key)) {
if (!Array.isArray(object[key])) {
object[key] = [object[key]]
}
object[key].push(typedValue)
} else {
object[key] = typedValue
}
})
return (JSON.stringify(object))
}
})
})()

File diff suppressed because one or more lines are too long

View File

@ -1,120 +0,0 @@
package pages
import (
"fmt"
. "recipe_book/pkg/db"
)
templ IngredientDetail(f Food) {
<h1>{ f.Name }</h1>
<form hx-post={ fmt.Sprintf("/ingredients/%d", f.ID) } hx-ext="json-enc" hx-target="body">
<div class="labelled-input">
<label>name</label>
<input type="name" name="name" value={ f.Name }>
</div>
<br>
<div class="labelled-input">
<label>calories</label>
<input type="number" name="cals" value={ fmt.Sprint(f.Cals) }>
</div>
<div class="labelled-input">
<label>carbs</label>
<input type="number" name="carbs" value={ fmt.Sprint(f.Carbs) }>
</div>
<div class="labelled-input">
<label>protein</label>
<input type="number" name="protein" value={ fmt.Sprint(f.Protein) }>
</div>
<div class="labelled-input">
<label>fat</label>
<input type="number" name="fat" value={ fmt.Sprint(f.Fat) }>
</div>
<br>
<div class="labelled-input">
<label>sugar</label>
<input type="number" name="sugar" value={ fmt.Sprint(f.Sugar) }>
</div>
<div class="labelled-input">
<label>alcohol</label>
<input type="number" name="alcohol" value={ fmt.Sprint(f.Alcohol) }>
</div>
<div class="labelled-input">
<label>water</label>
<input type="number" name="water" value={ fmt.Sprint(f.Water) }>
</div>
<br>
<div class="labelled-input">
<label>potassium</label>
<input type="number" name="potassium" value={ fmt.Sprint(f.Potassium) }>
</div>
<div class="labelled-input">
<label>calcium</label>
<input type="number" name="calcium" value={ fmt.Sprint(f.Calcium) }>
</div>
<div class="labelled-input">
<label>sodium</label>
<input type="number" name="sodium" value={ fmt.Sprint(f.Sodium) }>
</div>
<div class="labelled-input">
<label>magnesium</label>
<input type="number" name="magnesium" value={ fmt.Sprint(f.Magnesium) }>
</div>
<div class="labelled-input">
<label>phosphorus</label>
<input type="number" name="phosphorus" value={ fmt.Sprint(f.Phosphorus) }>
</div>
<div class="labelled-input">
<label>iron</label>
<input type="number" name="iron" value={ fmt.Sprint(f.Iron) }>
</div>
<div class="labelled-input">
<label>zinc</label>
<input type="number" name="zinc" value={ fmt.Sprint(f.Zinc) }>
</div>
<br>
<div class="labelled-input">
<label>
<span class="title-text-parent">(?)<div class="title-text">Mass of one serving or 'unit' of this food. in grams</div></span>
serving size (g)
</label>
<input type="number" name="mass" value={ fmt.Sprint(f.Mass) }>
</div>
<div class="labelled-input">
<label>price</label>
<input type="number" name="price" value={ fmt.Sprint(f.Price) }>
</div>
<div class="labelled-input">
<label>density</label>
<input type="number" name="density" value={ fmt.Sprint(f.Density) }>
</div>
<div class="labelled-input">
<label>
<span class="title-text-parent">(?)
<div class="title-text">
How much will it weigh after cooking, as a multiple of its original weight?
<br><br>
1 = no change
<br>
&lt;1 = weighs less than before (e.g., it released some water)
<br>
&gt;1 = weighs more than before (e.g., it soaked up water)
</div>
</span>
cook ratio
</label>
<input type="number" name="cook_ratio" value={ fmt.Sprint(f.CookRatio) }>
</div>
<input type="submit" value="Save">
</form>
}

View File

@ -1,43 +0,0 @@
package pages
import (
"fmt"
. "recipe_book/pkg/db"
)
templ IngredientsIndex(foods []Food) {
<h1>Ingredients</h1>
<dialog id="newIngredientDialog">
<h3>Create new ingredient</h3>
<form hx-post="/ingredients" hx-ext="json-enc" hx-target="body" hx-push-url="true">
<label for="name">Name</label>
<input name="name" />
<input type="submit" value="Create" />
</form>
<button onclick="newIngredientDialog.close()">Cancel</button>
</dialog>
<button class=".new-item-button" onclick="newIngredientDialog.showModal()">New Ingredient</button>
<table>
<thead>
<th>Name</th>
<th>Cals</th>
<th>Carbs</th>
<th>Protein</th>
<th>Fat</th>
<th>Sugar</th>
</thead>
<tbody>
for _, f := range foods {
<tr>
<td><a href={ templ.URL(fmt.Sprintf("/ingredients/%d", f.ID)) } hx-boost="true">{ f.Name }</a></td>
<td>{ fmt.Sprint(f.Cals) }</td>
<td>{ fmt.Sprint(f.Carbs) }</td>
<td>{ fmt.Sprint(f.Protein) }</td>
<td>{ fmt.Sprint(f.Fat) }</td>
<td>{ fmt.Sprint(f.Sugar) }</td>
</tr>
}
</tbody>
</table>
}

View File

@ -1,86 +0,0 @@
package pages
import (
"fmt"
"strings"
. "recipe_book/pkg/db"
)
func to_food_list(r Recipe) []Food {
ret := make([]Food, len(r.Ingredients))
for i := range r.Ingredients {
ret[i] = r.Ingredients[i].Food
}
return ret
}
templ RecipeDetail(recipe Recipe) {
<h1>{ recipe.Name }</h1>
<dialog id="renameRecipeDialog">
<h3>Rename recipe</h3>
<form hx-post={ fmt.Sprintf("/recipes/%d", recipe.ID) } hx-ext="json-enc" hx-target="body" hx-push-url="true">
<label for="name">New name:</label>
<input name="name" />
<input type="submit" value="Update" />
</form>
<button onclick="renameRecipeDialog.close()">Cancel</button>
</dialog>
<button class="new-item-button" onclick="renameRecipeDialog.showModal()">Rename</button>
<table class="recipe-table">
<thead>
<th></th>
<th>Amount</th>
<th>Ingredient</th>
<th>Calories</th>
<th>Carbs</th>
<th>Protein</th>
<th>Fat</th>
<th>Sugar</th>
</thead>
<tbody>
for _, i := range recipe.Ingredients {
<tr>
<td class="delete-button"></td>
<td class="amount">{ i.DisplayAmount() }</td>
<td class="name">{ i.Food.Name }</td>
<td class="cals">{ fmt.Sprint(int(i.Food.Cals * i.Quantity)) }</td>
<td class="carbs">{ fmt.Sprint(int(i.Food.Carbs * i.Quantity)) }</td>
<td class="protein">{ fmt.Sprint(int(i.Food.Protein * i.Quantity)) }</td>
<td class="fat">{ fmt.Sprint(int(i.Food.Fat * i.Quantity)) }</td>
<td class="sugar">{ fmt.Sprint(int(i.Food.Sugar * i.Quantity)) }</td>
</tr>
}
{{ computed_food := recipe.ComputeFood() }}
<tr class="recipe-table__total-row">
<td class="delete-button"></td>
<td class="amount"></td>
<td class="name">Total</td>
<td class="cals">{ fmt.Sprint(int(computed_food.Cals)) }</td>
<td class="carbs">{ fmt.Sprint(int(computed_food.Carbs)) }</td>
<td class="protein">{ fmt.Sprint(int(computed_food.Protein)) }</td>
<td class="fat">{ fmt.Sprint(int(computed_food.Fat)) }</td>
<td class="sugar">{ fmt.Sprint(int(computed_food.Sugar)) }</td>
</tr>
</tbody>
</table>
if recipe.Blurb != "" {
<h2>Blurb</h2>
for _, line := range strings.Split(recipe.Blurb, "\n") {
<p><i>{ line }</i></p>
}
}
<h2>Instructions</h2>
<ol class="instructions-list">
for _, instr := range recipe.Instructions {
<li>
for _, line := range strings.Split(instr, "\n") {
<p>{ line }</p>
}
</li>
}
</ol>
}

View File

@ -1,26 +0,0 @@
package pages
import (
"fmt"
. "recipe_book/pkg/db"
)
templ RecipesIndex(recipes []Recipe) {
<h1>Recipes</h1>
<dialog id="newRecipeDialog">
<h3>Create new recipe</h3>
<form hx-post="/recipes" hx-ext="json-enc" hx-target="body" hx-push-url="true">
<label for="name">Name</label>
<input name="name" />
<input type="submit" value="Create" />
</form>
<button onclick="newRecipeDialog.close()">Cancel</button>
</dialog>
<button class="new-item-button" onclick="newRecipeDialog.showModal()">New Recipe</button>
<ul>
for _, r := range recipes {
<li><a hx-post={ fmt.Sprintf("/recipes/%d", r.ID) } hx-target="body" hx-push-url="true">{ r.Name }</a></li>
}
</ul>
}

View File

@ -1,36 +0,0 @@
package pages
templ Base(title string) {
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>{ title } | Recipe Book</title>
// Page content
<link rel='shortcut icon' href='/static/chili-garlic.png' type='image/x-icon'>
<link rel='stylesheet' href='/static/styles.css'>
// HTMX
<script src="/static/vendor/htmx.min.js" integrity="sha384-oecSB0HeI5gdFcssloeKf3nByrZ7XjyAKxoykSkH8A4WPwT6suR+Ie4wGSLaQJBu" crossorigin="anonymous"></script>
<script src="/static/vendor/htmx-extension-json-enc.js"></script>
</head>
<body>
<nav>
<img id="logo" src="/static/chili-garlic.png">
<ul>
<li><a hx-get="/ingredients" hx-target="body" hx-push-url="true">Ingredients</a></li>
<li><a hx-get="/recipes" hx-target="body" hx-push-url="true">Recipes</a></li>
</ul>
<ul>
<li><a hx-get="/about" hx-target="body" hx-push-url="true">About this app</a></li>
<li><a hx-get="/help" hx-target="body" hx-push-url="true">Help</a></li>
<li><a hx-get="/changelog" hx-target="body" hx-push-url="true">Changelog</a></li>
</ul>
</nav>
<main>
{ children... }
</main>
</body>
</html>
}

View File

@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
sudo mount -t tmpfs -o size=100M tmpfs sample_data/data mount -t tmpfs -o size=100M tmpfs sample_data/data

View File

@ -1,6 +0,0 @@
#!/usr/bin/env bash
test -e sample_data/data/food.db && rm sample_data/data/food.db
go run ./cmd init
sqlite3 sample_data/data/food.db < sample_data/seed.sql

View File

@ -1,7 +1,3 @@
-- ==========
-- Base foods
-- ==========
INSERT INTO foods (name,cals,carbs,protein,fat,sugar,alcohol,water,potassium,sodium,calcium,magnesium,phosphorus,iron,zinc,mass,price,density,cook_ratio) VALUES INSERT INTO foods (name,cals,carbs,protein,fat,sugar,alcohol,water,potassium,sodium,calcium,magnesium,phosphorus,iron,zinc,mass,price,density,cook_ratio) VALUES
('bread',289.0,56.0,12.0,2.0,3.0,0.0,0.0,0.1,0.381,0.151,0.023,0.099,0.0037,0.0007,100.0,44.0,1.0,1.0), ('bread',289.0,56.0,12.0,2.0,3.0,0.0,0.0,0.1,0.381,0.151,0.023,0.099,0.0037,0.0007,100.0,44.0,1.0,1.0),
('oats',400.0,67.0,17.0,7.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,33.0,0.44,1.0), ('oats',400.0,67.0,17.0,7.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,33.0,0.44,1.0),
@ -111,10 +107,12 @@ INSERT INTO foods (name,cals,carbs,protein,fat,sugar,alcohol,water,potassium,sod
('frozen mixed veggies',71.0,13.0,3.5,0.0,3.5,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,27.0,1.0,1.0), ('frozen mixed veggies',71.0,13.0,3.5,0.0,3.5,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,27.0,1.0,1.0),
('canned olives',167.0,7.0,0.0,13.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,150.0,1.0,1.0), ('canned olives',167.0,7.0,0.0,13.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,150.0,1.0,1.0),
('frozen peas',59.0,14.0,4.7,0.0,4.7,0.0,0.0,0.11,0.072,0.024,0.022,0.077,0.0035,0.0007,100.0,27.5,1.0,1.0), ('frozen peas',59.0,14.0,4.7,0.0,4.7,0.0,0.0,0.11,0.072,0.024,0.022,0.077,0.0035,0.0007,100.0,27.5,1.0,1.0),
('pecan butter tarts',360.0,49.0,3.0,17.0,24.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,0.0,1.0,1.0),
('ravioli',220.0,23.0,14.0,8.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,119.0,1.0,1.0), ('ravioli',220.0,23.0,14.0,8.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,119.0,1.0,1.0),
('panko',366.0,77.0,10.0,2.0,7.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,0.0,1.0,1.0), ('panko',366.0,77.0,10.0,2.0,7.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,0.0,1.0,1.0),
('doritos',500.0,57.0,7.0,28.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,0.0,1.0,1.0), ('doritos',500.0,57.0,7.0,28.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,0.0,1.0,1.0),
('pepperoni',90.0,2.0,7.0,7.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,40.0,44.0,1.0,1.0), ('pepperoni',90.0,2.0,7.0,7.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,40.0,44.0,1.0,1.0),
('kirkland croissants',300.0,30.0,6.0,17.0,4.0,0.0,0.0,0.05,0.36,0.04,0.01,0.051,0.0018,0.00032,69.0,50.0,1.0,1.0),
('bacon',190.0,1.0,5.0,18.0,0.0,0.0,0.0,0.08,0.24,0.0012,0.005,0.078,0.0002,0.0005,44.0,45.0,1.0,1.0), ('bacon',190.0,1.0,5.0,18.0,0.0,0.0,0.0,0.08,0.24,0.0012,0.005,0.078,0.0002,0.0005,44.0,45.0,1.0,1.0),
('ketchup',133.0,33.0,2.0,0.0,27.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,0.0,1.0,1.0), ('ketchup',133.0,33.0,2.0,0.0,27.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,0.0,1.0,1.0),
('ranch',467.0,7.0,3.0,53.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,0.0,1.0,1.0), ('ranch',467.0,7.0,3.0,53.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,0.0,1.0,1.0),
@ -124,7 +122,7 @@ INSERT INTO foods (name,cals,carbs,protein,fat,sugar,alcohol,water,potassium,sod
('fries',320.0,41.0,3.4,15.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,0.0,1.0,1.0), ('fries',320.0,41.0,3.4,15.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,0.0,1.0,1.0),
('salt',0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,40.0,0.0,0.0,0.0,0.0,0.0,100.0,17.0,2.2,1.0), ('salt',0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,40.0,0.0,0.0,0.0,0.0,0.0,100.0,17.0,2.2,1.0),
('KCL',0.0,0.0,0.0,0.0,0.0,0.0,0.0,52.4,0.0,0.0,0.0,0.0,0.0,0.0,100.0,176.0,1.0,1.0), ('KCL',0.0,0.0,0.0,0.0,0.0,0.0,0.0,52.4,0.0,0.0,0.0,0.0,0.0,0.0,100.0,176.0,1.0,1.0),
('black pepper',255.0,65.0,11.0,3.0,1.0,0.0,0.0,1.26,0.044,0.437,0.194,0.173,0.0289,0.0014,100.0,0.0,1.0,1.0), ('pepper',255.0,65.0,11.0,3.0,1.0,0.0,0.0,1.26,0.044,0.437,0.194,0.173,0.0289,0.0014,100.0,0.0,1.0,1.0),
('oregano',265.0,48.0,9.0,4.3,4.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,162.0,1.0,1.0), ('oregano',265.0,48.0,9.0,4.3,4.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,100.0,162.0,1.0,1.0),
('garlic powder',331.0,68.0,13.0,1.0,2.0,0.0,0.0,1.101,0.026,0.08,0.058,0.417,0.0027,0.0026,100.0,107.0,1.0,1.0), ('garlic powder',331.0,68.0,13.0,1.0,2.0,0.0,0.0,1.101,0.026,0.08,0.058,0.417,0.0027,0.0026,100.0,107.0,1.0,1.0),
('onion powder',347.0,75.0,10.0,1.0,35.0,0.0,0.0,0.943,0.053,0.363,0.122,0.34,0.0026,0.0023,100.0,152.0,1.0,1.0), ('onion powder',347.0,75.0,10.0,1.0,35.0,0.0,0.0,0.943,0.053,0.363,0.122,0.34,0.0026,0.0023,100.0,152.0,1.0,1.0),
@ -149,37 +147,4 @@ INSERT INTO foods (name,cals,carbs,protein,fat,sugar,alcohol,water,potassium,sod
('sage',315.0,40.0,11.0,13.0,2.0,0.0,0.0,1.07,0.011,1.652,0.428,0.0,0.028,0.0047,100.0,210.0,1.0,1.0), ('sage',315.0,40.0,11.0,13.0,2.0,0.0,0.0,1.07,0.011,1.652,0.428,0.0,0.028,0.0047,100.0,210.0,1.0,1.0),
('nutmeg',525.0,44.0,6.0,36.0,28.0,0.0,0.0,0.35,0.016,0.184,0.183,0.0,0.003,0.0021,100.0,246.0,1.0,1.0), ('nutmeg',525.0,44.0,6.0,36.0,28.0,0.0,0.0,0.35,0.016,0.184,0.183,0.0,0.003,0.0021,100.0,246.0,1.0,1.0),
('turmeric',354.0,60.0,8.0,10.0,3.0,0.0,0.0,2.525,0.038,0.183,0.193,0.268,0.041,0.0043,100.0,0.0,1.0,1.0), ('turmeric',354.0,60.0,8.0,10.0,3.0,0.0,0.0,2.525,0.038,0.183,0.193,0.268,0.041,0.0043,100.0,0.0,1.0,1.0),
('cloves',323.0,55.0,6.0,20.0,2.0,0.0,0.0,1.102,0.243,0.646,0.264,0.105,0.0087,0.0011,100.0,0.0,1.0,1.0), ('cloves',323.0,55.0,6.0,20.0,2.0,0.0,0.0,1.102,0.243,0.646,0.264,0.105,0.0087,0.0011,100.0,0.0,1.0,1.0);
('coffee', 0,0,0,0,0,0,100,0.047,0,0,0.003,0,0,0,100,0,1,1),
('chicken broth', 0,0,0,0,0,0,100,0,0,0,0,0,0,0,100,0,1,1);
-- =======
-- Recipes
-- =======
insert into foods (rowid, name, cals,carbs,protein,fat,sugar,alcohol,water,potassium,sodium,calcium,magnesium,phosphorus,iron,zinc,mass,price,density,cook_ratio) values (10000, 'Chicken parmigiana soup', 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
insert into recipes (rowid, name, blurb, instructions, computed_food_id) values (1, 'Chicken parmigiana soup', 'This is a nice recipe which is basically what it sounds like: chicken parmigiana, but in soup form. It''s easy to make, hard to mess up, and simply delicious.', replace('In large pot, heat butter and oil. Cook onions to 1/2 cooked (translucent and soft, starting to turn golden-brown)\aAdd garlic, cook 1-2 minutes\aadd tomatoes and chicken broth, bring to a simmer\asalt and season the soup, tasting until seasonings are right\acut chicken into bite-sized pieces, add to soup and boil until fully cooked\aadd pasta and boil until it is cooked "al dente"; add more stock as needed\aadd half the cheese and stir it into the soup\asprinkle remaining cheese on top and serve', '\a', char(31)), 10000);
insert into ingredients(food_id, quantity, units, in_recipe_id, list_order) values
(28, 2, 1, 1, 0), -- 2 yellow onions
(78, 0.3, 2, 1, 1), -- 30g butter
(12, 0.3, 2, 1, 2), -- 30g olive oil
(32, 0.4, 2, 1, 3), -- 40g garlic
(105, 8, 5, 1, 4), -- 800mL canned tomatoes,
(62, 4.5, 2, 1, 5), -- 450g chicken breast
(3, 3, 2, 1, 6), -- 300g pasta
(83, 1.5, 2, 1, 7), -- 150g mozzarella
(86, 1.5, 2, 1, 8), -- 150g parmigiano
(120, 0.2, 2, 1, 9), -- 20g salt
(122, 0.02, 2, 1, 10), -- 2g pepper
(126, 0.05, 2, 1, 11), --5g chilis
(149, 10, 5, 1, 12); -- 1000mL chicken stock
insert into foods (rowid, name, cals,carbs,protein,fat,sugar,alcohol,water,potassium,sodium,calcium,magnesium,phosphorus,iron,zinc,mass,price,density,cook_ratio) values (10001, 'Breakfast shake', 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
insert into recipes (rowid, name, blurb, instructions, computed_food_id) values (2, 'Breakfast shake', 'A quick breakfast-in-a-bottle you can have on the go', replace(replace('In shaker bottle, add milk, peanut butter, yogurt, *hot* coffee (in that order). Mix until the peanut butter is melted/dissolved\aKey point of previous step is to get the temperatures right:\n- Hot coffee will melt the shaker bottle or dissolve microplastics, so don''t add it directly\n- without hot coffee, peanut butter won''t dissolve\n- hot coffee will make whey protein curdle, so shake coffee with cold ingredients until it ends up warm-ish, then it''s safe to add whey\aOnce temperature has equilibrated between hot coffee and cold stuff, add whey protein, shake again, and drink', '\a', char(31)), '\n', char(10)), 10001);
insert into ingredients(food_id, quantity, units, in_recipe_id, list_order) values
(10, 0.3, 2, 2, 0), -- 30g peanut butter
(69, 3.5, 5, 2, 1), -- 350mL skim milk
(81, 2.5, 6, 2, 2), -- 1 cup yogurt
(7, 0.4, 2, 2, 3), -- 1 cup yogurt
(148, 2, 5, 2, 4); -- 200 mL coffee