fuzzy search

This commit is contained in:
Hayden 2021-01-12 20:47:38 -09:00
commit 0e59b13394
21 changed files with 350 additions and 221 deletions

View file

@ -1,6 +1,6 @@
{
"python.formatting.provider": "black",
"python.pythonPath": "/home/hayden/projects/mealie/.venv/bin/python3",
"python.pythonPath": "/home/hayden/Projects/mealie/.venv/bin/python3",
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.autoComplete.extraPaths": ["mealie", "mealie/mealie"],

View file

@ -5663,6 +5663,11 @@
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
"dev": true
},
"fuse.js": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.4.6.tgz",
"integrity": "sha512-/gYxR/0VpXmWSfZOIPS3rWwU8SHgsRTwWuXhyb2O6s7aRuVtHtxCkR33bNYu3wyLyNx/Wpv0vU7FZy8Vj53VNw=="
},
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npm.taobao.org/gensync/download/gensync-1.0.0-beta.2.tgz?cache=0&sync_timestamp=1603829621482&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fgensync%2Fdownload%2Fgensync-1.0.0-beta.2.tgz",

View file

@ -10,6 +10,7 @@
"dependencies": {
"axios": "^0.21.1",
"core-js": "^3.8.2",
"fuse.js": "^6.4.6",
"qs": "^6.9.4",
"v-jsoneditor": "^1.4.2",
"vue": "^2.6.11",

View file

@ -2,9 +2,7 @@
<v-app>
<v-app-bar dense app color="primary" dark class="d-print-none">
<v-btn @click="$router.push('/')" icon class="d-flex align-center">
<v-icon size="40">
mdi-silverware-variant
</v-icon>
<v-icon size="40"> mdi-silverware-variant </v-icon>
</v-btn>
<div btn class="pl-2">
<v-toolbar-title @click="$router.push('/')">Mealie</v-toolbar-title>
@ -12,7 +10,7 @@
<v-spacer></v-spacer>
<v-btn icon @click="toggleSearch">
<v-btn icon @click="$router.push('/search')">
<v-icon>mdi-magnify</v-icon>
</v-btn>
@ -22,10 +20,6 @@
<v-container>
<AddRecipeFab />
<SnackBar />
<v-expand-transition>
<SearchHeader v-show="search" />
</v-expand-transition>
<router-view></router-view>
</v-container>
</v-main>
@ -34,7 +28,6 @@
<script>
import Menu from "./components/UI/Menu";
import SearchHeader from "./components/UI/SearchHeader";
import AddRecipeFab from "./components/UI/AddRecipeFab";
import SnackBar from "./components/UI/SnackBar";
import Vuetify from "./plugins/vuetify";
@ -44,14 +37,13 @@ export default {
components: {
Menu,
AddRecipeFab,
SearchHeader,
SnackBar
SnackBar,
},
watch: {
$route() {
this.search = false;
}
},
},
mounted() {
@ -62,7 +54,7 @@ export default {
},
data: () => ({
search: false
search: false,
}),
methods: {
/**
@ -90,8 +82,8 @@ export default {
} else {
this.search = true;
}
}
}
},
},
};
</script>

View file

@ -0,0 +1,100 @@
<template>
<div>
<v-text-field
label="Search"
v-model="search"
solo
></v-text-field>
<v-card v-if="search && showResults">
<v-hover
square
v-for="(item, index) in result.slice(0, 5)"
:key="index"
v-slot="{ hover }"
>
<v-card
class="color-transition"
@click="$router.push(`/recipe/${item.item.slug}`)"
:color="hover ? highlightColor : null"
>
<v-row dense no-gutters>
<v-col cols="12" md="2" sm="6">
<v-img
:src="getImage(item.item.image)"
width="100%"
height="100%"
rounded
>
</v-img>
</v-col>
<v-col cols="12" md="10" sm="6">
<v-card-title> {{ item.item.name }}</v-card-title>
<v-card-text> {{ item.item.description }}</v-card-text></v-col
>
</v-row>
</v-card>
</v-hover>
</v-card>
</div>
</template>
<script>
import Fuse from "fuse.js";
import utils from "../../utils";
export default {
props: {
showResults: {
default: false,
},
},
data() {
return {
search: "",
result: [],
isDark: false,
options: {
shouldSort: true,
threshold: 0.6,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ["name", "slug"],
},
};
},
mounted() {
this.isDark = this.$store.getters.getIsDark;
},
computed: {
data() {
return this.$store.getters.getRecentRecipes;
},
fuse() {
return new Fuse(this.data, this.options);
},
highlightColor() {
return this.isDark ? "primary lighten-5" : "primary lighten-5";
},
},
watch: {
search() {
if (this.search.trim() === "") this.result = this.list;
else this.result = this.fuse.search(this.search.trim());
this.$emit("results", this.result);
},
},
methods: {
getImage(image) {
return utils.getImageURL(image);
},
},
};
</script>
<style>
.color-transition {
transition: background-color 0.3s ease;
}
</style>

View file

@ -0,0 +1,57 @@
<template>
<div>
<v-row justify="center">
<v-col cols="1"> </v-col>
<v-col>
<SearchBar @results="updateResults" />
</v-col>
<v-col cols="2">
<v-btn icon>
<v-icon large> mdi-filter </v-icon>
</v-btn>
</v-col>
</v-row>
<v-row v-if="searchResults">
<v-col
:sm="6"
:md="6"
:lg="4"
:xl="3"
v-for="item in searchResults.slice(0, 10)"
:key="item.item.name"
>
<RecipeCard
:name="item.item.name"
:description="item.item.description"
:slug="item.item.slug"
:rating="item.item.rating"
:image="item.item.image"
/>
</v-col>
</v-row>
</div>
</template>
<script>
import SearchBar from "../components/UI/SearchBar";
import RecipeCard from "../components/UI/RecipeCard";
export default {
components: {
SearchBar,
RecipeCard,
},
data() {
return {
searchResults: null,
};
},
methods: {
updateResults(results) {
this.searchResults = results;
},
},
};
</script>
<style>
</style>

View file

@ -1,5 +1,6 @@
import HomePage from "./pages/HomePage";
import Page404 from "./pages/404Page";
import SearchPage from "./pages/SearchPage";
import RecipePage from "./pages/RecipePage";
import RecipeNewPage from "./pages/RecipeNewPage";
import SettingsPage from "./pages/SettingsPage";
@ -10,6 +11,7 @@ import api from "./api";
export const routes = [
{ path: "/", component: HomePage },
{ path: "/mealie", component: HomePage },
{ path: "/search", component: SearchPage },
{ path: "/recipe/:recipe", component: RecipePage },
{ path: "/new/", component: RecipeNewPage },
{ path: "/settings/site", component: SettingsPage },

View file

@ -17,6 +17,7 @@ function inDarkMode(payload) {
const state = {
activeTheme: {},
darkMode: "system",
isDark: false,
};
const mutations = {
@ -30,6 +31,7 @@ const mutations = {
if (isDark !== null) {
Vuetify.framework.theme.dark = isDark;
state.isDark = isDark;
state.darkMode = payload;
}
},
@ -60,6 +62,7 @@ const actions = {
const getters = {
getActiveTheme: (state) => state.activeTheme,
getDarkMode: (state) => state.darkMode,
getIsDark: (state) => state.isDark,
};
export default {

File diff suppressed because one or more lines are too long

View file

@ -94,11 +94,11 @@ class BaseDocument:
else:
return db_entry
def save_new(self, document: dict) -> str:
def save_new(self, document: dict) -> dict:
if USE_MONGO:
new_document = self.document(**document)
new_document.save()
return new_document
return BaseDocument._unpack_mongo(new_document)
elif USE_TINYDB:
return self.store.save(document)

View file

@ -13,7 +13,7 @@ class StoreBase:
if data != []:
raise Exception(
f"Cannot Save, Primary Key: {self.primary_key} already exists"
f"Cannot Save, Primary Key: {document[self.primary_key]} already exists"
)
else:
self.store.insert(document)

View file

@ -2,7 +2,6 @@ import shutil
from pathlib import Path
import requests
from unsync import unsync
CWD = Path(__file__).parent
IMG_DIR = CWD.parent.joinpath("data", "img")

View file

@ -47,7 +47,7 @@ else:
# DATABASE ENV
DATABASE_TYPE = os.getenv("db_type", "tinydb") # mongo, tinydb
DATABASE_TYPE = os.getenv("db_type", "mongo") # mongo, tinydb
if DATABASE_TYPE == "tinydb":
USE_TINYDB = True
USE_MONGO = False
@ -58,7 +58,7 @@ elif DATABASE_TYPE == "mongo":
else:
raise Exception(
"Unable to determine database type. Acceptible options are 'mongo' or 'sqlite' "
"Unable to determine database type. Acceptible options are 'mongo' or 'tinydb' "
)
# Mongo Database

80
poetry.lock generated
View file

@ -1,80 +0,0 @@
[[package]]
name = "fastapi"
version = "0.63.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pydantic = ">=1.0.0,<2.0.0"
starlette = "0.13.6"
[package.extras]
all = ["requests (>=2.24.0,<3.0.0)", "aiofiles (>=0.5.0,<0.6.0)", "jinja2 (>=2.11.2,<3.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<2.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "graphene (>=2.1.8,<3.0.0)", "ujson (>=3.0.0,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.14.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)"]
dev = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.14.0)", "graphene (>=2.1.8,<3.0.0)"]
doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=6.1.4,<7.0.0)", "markdown-include (>=0.5.1,<0.6.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.2.0)", "typer-cli (>=0.0.9,<0.0.10)", "pyyaml (>=5.3.1,<6.0.0)"]
test = ["pytest (==5.4.3)", "pytest-cov (==2.10.0)", "pytest-asyncio (>=0.14.0,<0.15.0)", "mypy (==0.790)", "flake8 (>=3.8.3,<4.0.0)", "black (==20.8b1)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.15.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.4.0)", "orjson (>=3.2.1,<4.0.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "aiofiles (>=0.5.0,<0.6.0)", "flask (>=1.1.2,<2.0.0)"]
[[package]]
name = "pydantic"
version = "1.7.3"
description = "Data validation and settings management using python 3.6 type hinting"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
typing_extensions = ["typing-extensions (>=3.7.2)"]
[[package]]
name = "starlette"
version = "0.13.6"
description = "The little ASGI library that shines."
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"]
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "fd85f1208985afed8ec0a1322873546cc1c1eb44f0f443225647806e9f6eead0"
[metadata.files]
fastapi = [
{file = "fastapi-0.63.0-py3-none-any.whl", hash = "sha256:98d8ea9591d8512fdadf255d2a8fa56515cdd8624dca4af369da73727409508e"},
{file = "fastapi-0.63.0.tar.gz", hash = "sha256:63c4592f5ef3edf30afa9a44fa7c6b7ccb20e0d3f68cd9eba07b44d552058dcb"},
]
pydantic = [
{file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127"},
{file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e"},
{file = "pydantic-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23"},
{file = "pydantic-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f"},
{file = "pydantic-1.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1"},
{file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c"},
{file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f"},
{file = "pydantic-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2"},
{file = "pydantic-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef"},
{file = "pydantic-1.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef"},
{file = "pydantic-1.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608"},
{file = "pydantic-1.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec"},
{file = "pydantic-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e"},
{file = "pydantic-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e"},
{file = "pydantic-1.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730"},
{file = "pydantic-1.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95"},
{file = "pydantic-1.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b"},
{file = "pydantic-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af"},
{file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"},
{file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"},
]
starlette = [
{file = "starlette-0.13.6-py3-none-any.whl", hash = "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9"},
{file = "starlette-0.13.6.tar.gz", hash = "sha256:ebe8ee08d9be96a3c9f31b2cb2a24dbdf845247b745664bd8a3f9bd0c977fdbc"},
]

View file

@ -1,2 +0,0 @@
[virtualenvs]
in-project = true

View file

@ -1,116 +0,0 @@
[tool.poetry]
name = "mealie"
version = "0.1.0"
description = "Recipe Manager"
authors = ["Hayden <hay-kot@pm.me>"]
license = "MIT"
[tool.poetry.dependencies]
python = "^3.8"
fastapi = "0.61.1"
aiofiles = "0.5.0"
aniso8601 = "7.0.0"
appdirs = "1.4.4"
APScheduler = "3.6.3"
astroid = "2.4.2"
async-exit-stack = "1.0.1"
async_generator = "1.10"
attrs = "20.3.0"
beautifulsoup4 = "4.9.1"
black = "20.8b1"
certifi = "2020.6.20"
chardet = "3.0.4"
click = "7.1.2"
colorama = "0.4.3"
decorator = "4.4.2"
dnspython = "2.0.0"
email-validator = "1.1.1"
extruct = "0.10.0"
fastapi-login = "1.5.1"
future = "0.18.2"
gitdb = "4.0.5"
GitPython = "3.1.11"
graphene = "2.1.8"
graphql-core = "2.3.2"
graphql-relay = "2.0.1"
h11 = "0.9.0"
html-text = "0.5.2"
html5lib = "1.1"
httptools = "0.1.1"
idna = "2.10"
iniconfig = "1.1.1"
isodate = "0.6.0"
isort = "5.4.2"
itsdangerous = "1.1.0"
Jinja2 = "2.11.2"
joblib = "1.0.0"
jstyleson = "0.0.2"
lazy-object-proxy = "1.4.3"
livereload = "2.6.3"
lunr = "0.5.8"
lxml = "4.6.2"
Markdown = "3.3.3"
MarkupSafe = "1.1.1"
mccabe = "0.6.1"
mf2py = "1.1.2"
mkdocs = "1.1.2"
mkdocs-material = "6.1.7"
mkdocs-material-extensions = "1.0.1"
mongoengine = "0.21.0"
mypy-extensions = "0.4.3"
nltk = "3.5"
packaging = "20.8"
passlib = "1.7.4"
pathspec = "0.8.0"
pdfkit = "0.6.1"
pip-chill = "1.0.0"
pluggy = "0.13.1"
promise = "2.3"
py = "1.10.0"
pydantic = "1.6.1"
Pygments = "2.7.3"
PyJWT = "1.7.1"
pylint = "2.6.0"
pymdown-extensions = "8.0.1"
pymongo = "3.11.1"
pyparsing = "2.4.7"
pytest = "6.2.1"
python-dateutil = "2.8.1"
python-dotenv = "0.15.0"
python-multipart = "0.0.5"
python-slugify = "4.0.1"
pytz = "2020.4"
PyYAML = "5.3.1"
rdflib = "4.2.2"
rdflib-jsonld = "0.5.0"
regex = "2020.7.14"
requests = "2.24.0"
Rx = "1.6.1"
scrape-schema-recipe = "0.1.1"
six = "1.15.0"
smmap = "3.0.4"
soupsieve = "2.0.1"
SQLAlchemy = "1.3.22"
starlette = "0.13.6"
text-unidecode = "1.3"
toml = "0.10.1"
tornado = "6.1"
tqdm = "4.54.1"
typed-ast = "1.4.1"
typing-extensions = "3.7.4.3"
tzlocal = "2.1"
ujson = "3.1.0"
urllib3 = "1.25.10"
uvicorn = "0.11.8"
uvloop = "0.14.0"
validators = "0.18.0"
w3lib = "1.22.0"
webencodings = "0.5.1"
websockets = "8.1"
wrapt = "1.12.1"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

168
scratch.json Normal file
View file

@ -0,0 +1,168 @@
[
{
"item": {
"name": "Pizzettes",
"slug": "pizzettes",
"image": "pizzettes.jpg",
"description": "Not to be confused with wafer-thin waffled pizzelles, this rich brownie-like cookie packed with warm spices is exactly the opposite.",
"dateAdded": 1610409600000,
"rating": 0
},
"refIndex": 25
},
{
"item": {
"name": "Detroit-Style Pepperoni Pizza",
"slug": "detroit-style-pepperoni-pizza",
"image": "detroit-style-pepperoni-pizza.jpg",
"description": "So you want to make pizza, but its your first time. This Detroit-style pizza from Bryan Ford, which uses a half-batch of his Master Dough, is perfect for beginners because you dont have to shape the dough or wrangle a hot pizza stone. If you dont have a Detroit pizza pan (its deep sides and black anodized aluminum help achieve a crust thats fluffy on the inside but crisp all around), thats okay: Use a 13x9\" baking pan or two 8x8\" baking pans.",
"dateAdded": 1610409600000,
"rating": 3
},
"refIndex": 13
},
{
"item": {
"name": "Marranitos Enfiestados",
"slug": "marranitos-enfiestados",
"image": "marranitos-enfiestados.jpg",
"description": "Rick Martinezs take on the popular Mexican pig-shaped cookie goes all out with a ginger-spiced dough and tons of sprinkles.",
"dateAdded": 1610409600000,
"rating": 4
},
"refIndex": 23
},
{
"item": {
"name": "Roasted Okra",
"slug": "roasted-okra",
"image": "roasted-okra.jpg",
"description": "I love me some okra, but prep is usually enough to avoid making it. This simple dish requires only a single cut per pod and a dash of seasoning- that's it.",
"dateAdded": 1610409600000,
"rating": 0
},
"refIndex": 20
},
{
"item": {
"name": "Pork Steaks",
"slug": "pork-steaks",
"image": "pork-steaks.jpg",
"description": "Pork is called \"the other white meat\", but this sous vide technique is so dang good I'm gonna graduate it up to \"THE white meat\".",
"dateAdded": 1610409600000,
"rating": 0
},
"refIndex": 29
},
{
"item": {
"name": "Roasted Brussels Sprouts",
"slug": "roasted-brussels-sprouts",
"image": "roasted-brussels-sprouts.jpg",
"description": "A lot of places are doing dang good brussels lately, but none can touch [Salty Sow](http://saltysow.com/). They serve them fried, topped with pecorino and golden raisins. Yes, please.\nWe started trying to emulate it, but it's such a versatile dish, you can make it however you want. Consider this a loose guide.",
"dateAdded": 1610409600000,
"rating": 0
},
"refIndex": 34
},
{
"item": {
"name": "Buttery Kimchi Chicken",
"slug": "buttery-kimchi-chicken",
"image": "buttery-kimchi-chicken.jpg",
"description": "This spicy and sweet, slightly nutty, and creamy stew from developer Sunny Lee is worth the tending (and time) it requires over the stove.",
"dateAdded": 1610323200000,
"rating": 0
},
"refIndex": 1
},
{
"item": {
"name": "Tequila, Beer, and Citrus Cocktail",
"slug": "tequila-beer-and-citrus-cocktail",
"image": "tequila-beer-and-citrus-cocktail.jpg",
"description": "The holiday cocktail that takes you straight to the beach (and isnt that where you want to be anyway?). ",
"dateAdded": 1610409600000,
"rating": 0
},
"refIndex": 35
},
{
"item": {
"name": "Pasta With Mushrooms and Cashew Cream",
"slug": "pasta-with-mushrooms-and-cashew-cream",
"image": "pasta-with-mushrooms-and-cashew-cream.jpg",
"description": "Might you mistake this vegan cream for actual alfredo sauce? Maybe. Maybe not. Its delicious either way.",
"dateAdded": 1610323200000,
"rating": 0
},
"refIndex": 0
},
{
"item": {
"name": "New York Strip",
"slug": "new-york-strip",
"image": "new-york-strip.jpg",
"description": "Cooking a steak in a water bath is the ultimate, both in terms of flavor and laziness. Dial in a precise temperature, then go take a nap.",
"dateAdded": 1610409600000,
"rating": 0
},
"refIndex": 12
},
{
"item": {
"name": "Green Spaghetti",
"slug": "green-spaghetti",
"image": "green-spaghetti.jpg",
"description": "We saw this on Jamie Oliver's TV show and made it that night. He calls for **cavolo nero** as the greens, but dinosaur kale or spinach or any dark leafy green will do. The bigger the green, the easier it is to fish out of the boiling water, so keep that in mind.\nStart to finish this whole recipe takes about 10 minutes, but it's pretty rapid fire once you get stated. Have plates and an appetite standing by.",
"dateAdded": 1610409600000,
"rating": 0
},
"refIndex": 37
},
{
"item": {
"name": "One Minute Muffin",
"slug": "one-minute-muffin",
"image": "one-minute-muffin.jpg",
"description": "This flax muffin is quick, vertsatile (just mix in other goodies), and only makes one at a time, so you've got no huge tray to tempt you. Custom muffins every morning!",
"dateAdded": 1610409600000,
"rating": 0
},
"refIndex": 10
},
{
"item": {
"name": "Bon Appétit's Perfect Pizza",
"slug": "bon-appetit-s-perfect-pizza",
"image": "bon-appetit-s-perfect-pizza.jpg",
"description": "With the amount of love, time, and research that went into making what constitutes the “perfect” pizza, youll definitely want to share. Welcome to the best-ever excuse to invite people over for a pizza party.",
"dateAdded": 1610409600000,
"rating": 3
},
"refIndex": 39
},
{
"item": {
"name": "Green Chile Stew",
"slug": "green-chile-stew",
"image": "green-chile-stew.jpg",
"description": "This is a simple one, consisting of a pre-made soup mix and whatever you've got around the kitchen (or campsite). Feel free to sub/swap/switch anything in the ingredient list— anything's fair game!",
"dateAdded": 1610409600000,
"rating": 0
},
"refIndex": 27
},
{
"item": {
"name": "Marinated Tofu With Brussels Sprouts and Farro",
"slug": "marinated-tofu-with-brussels-sprouts-and-farro",
"image": "marinated-tofu-with-brussels-sprouts-and-farro.jpg",
"description": "The soul of this recipe comes from the ginger and tamari marinade that gives a sweet and savory winter coat to the crispy tofu. ",
"dateAdded": 1610323200000,
"rating": 0
},
"refIndex": 6
}
]
Pasta With Mushrooms and Cashe...