deprecated old meal plan
This commit is contained in:
parent
b6845e06a5
commit
31720927b1
70
README.md
70
README.md
@ -1,6 +1,6 @@
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<a href="https://app.tandoor.dev"><img src="https://github.com/vabene1111/recipes/raw/develop/docs/logo_color.svg" height="256px" width="256px"></a>
|
||||
<a href="https://tandoor.dev"><img src="https://github.com/vabene1111/recipes/raw/develop/docs/logo_color.svg" height="256px" width="256px"></a>
|
||||
<br>
|
||||
Tandoor Recipes
|
||||
<br>
|
||||
@ -15,46 +15,74 @@
|
||||
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
|
||||
<a href="https://hub.docker.com/r/vabene1111/recipes" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/docker/pulls/vabene1111/recipes" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/releases/latest" rel="noopener noreferrer"><img src="https://img.shields.io/github/v/release/vabene1111/recipes" ></a>
|
||||
<a href="https://app.tandoor.dev/accounts/login/?demo" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://tandoor.dev" target="_blank" rel="noopener noreferrer">Website</a> •
|
||||
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a> •
|
||||
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Documentation</a> •
|
||||
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Docs</a> •
|
||||
<a href="https://app.tandoor.dev/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</a> •
|
||||
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer">Discord server</a>
|
||||
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer">Discord</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
# Your Feedback
|
||||
## Core Features
|
||||
- 🥗 **Manage your recipes** - Manage your ever growing recipe collection
|
||||
- 📆 **Plan** - multiple meals for each day
|
||||
- 🛒 **Shopping lists** - via the meal plan or straight from recipes
|
||||
- 📚 **Cookbooks** - collect recipes into books
|
||||
- 👪 **Share and collaborate** on recipes with friends and family
|
||||
|
||||
Share some information on how you use Tandoor to help me improve the application [Google Survey](https://forms.gle/qNfLK2tWTeWHe9Qd7)
|
||||
## Made by and for power users
|
||||
|
||||
## Features
|
||||
|
||||
- 📦 **Sync** files with Dropbox and Nextcloud (more can easily be added)
|
||||
- 🔍 Powerful **search** with Djangos [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
|
||||
- 🔍 Powerful & customizable **search** with fulltext support and [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
|
||||
- 🏷️ Create and search for **tags**, assign them in batch to all files matching certain filters
|
||||
- 📄 **Create recipes** locally within a nice, standardized web interface
|
||||
- ⬇️ **Import recipes** from thousands of websites supporting [ld+json or microdata](https://schema.org/Recipe)
|
||||
- 📱 Optimized for use on **mobile** devices like phones and tablets
|
||||
- 🛒 Generate **shopping** lists from recipes
|
||||
- 📆 Create a **Plan** on what to eat when
|
||||
- 👪 **Share** recipes with friends and comment on them to suggest or remember changes you made
|
||||
- ➗ automatically convert decimal units to **fractions** for those who like this
|
||||
- 🐳 Easy setup with **Docker** and included examples for Kubernetes, Unraid and Synology
|
||||
- ↔️ Quickly merge and rename ingredients, tags and units
|
||||
- 📥️ **Import recipes** from thousands of websites supporting [ld+json or microdata](https://schema.org/Recipe)
|
||||
- ➗ Support for **fractions** or decimals
|
||||
- 🐳 Easy setup with **Docker** and included examples for **Kubernetes**, **Unraid** and **Synology**
|
||||
- 🎨 Customize your interface with **themes**
|
||||
- ✉️ Export and import recipes from other users
|
||||
- 📦 **Sync** files with Dropbox and Nextcloud
|
||||
|
||||
## All the must haves
|
||||
|
||||
- 📱Optimized for use on **mobile** devices
|
||||
- 🌍 localized in many languages thanks to the awesome community
|
||||
- ➕ Many more like recipe scaling, image compression, cookbooks, printing views, ...
|
||||
- 📥️ **Import your collection** from many other [recipe managers](https://docs.tandoor.dev/features/import_export/)
|
||||
- ➕ Many more like recipe scaling, image compression, printing views and supermarkets
|
||||
|
||||
This application is meant for people with a collection of recipes they want to share with family and friends or simply
|
||||
store them in a nicely organized way. A basic permission system exists but this application is not meant to be run as
|
||||
a public page.
|
||||
|
||||
## Docs
|
||||
|
||||
Documentation can be found [here](https://docs.tandoor.dev/).
|
||||
|
||||
While this application has been around for a while and is actively used by many (including myself), it is still considered
|
||||
**beta** software that has a lot of rough edges and unpolished parts.
|
||||
## Contributing
|
||||
|
||||
You can help out with the ongoing development by looking for potential bugs in our code base, or by contributing new features. We are always welcoming new pull requests containing bug fixes, refactors and new features. We have a list of tasks and bugs on our issue tracker on Github. Please comment on issues if you want to contribute with, to avoid duplicating effort.
|
||||
|
||||
## Your Feedback
|
||||
|
||||
Share some information on how you use Tandoor to help me improve the application [Google Survey](https://forms.gle/qNfLK2tWTeWHe9Qd7)
|
||||
|
||||
## Get in touch
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="https://discord.gg/RhzBrfWgtp">Discord</a></td>
|
||||
<td>We have a public Discord server that anyone can join. This is where all our developers and contributors hang out and where we make announcements</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><a href="https://twitter.com/TandoorRecipes">Twitter</a></td>
|
||||
<td>You can follow our Twitter account to get updates on new features or releases</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## License
|
||||
|
||||
Beginning with version 0.10.0 the code in this repository is licensed under the [GNU AGPL v3](https://www.gnu.org/licenses/agpl-3.0.de.html) license with a
|
||||
|
@ -1,742 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
|
||||
{% block title %}{% trans 'Meal-Plan' %}{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
{% block content_fluid %}
|
||||
|
||||
{% include 'include/vue_base.html' %}
|
||||
<div id="app">
|
||||
<meal-plan-view></meal-plan-view>
|
||||
</div>
|
||||
|
||||
<script src="{% static 'js/moment-with-locales.min.js' %}"></script>
|
||||
|
||||
<script src="{% static 'js/Sortable.min.js' %}"></script>
|
||||
<script src="{% static 'js/vuedraggable.umd.min.js' %}"></script>
|
||||
<script src="{% static 'js/vue-cookies.js' %}"></script>
|
||||
|
||||
<script src="{% static 'js/js.cookie.min.js' %}"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="app">
|
||||
<div class="row mt-2 mb-1">
|
||||
<div class="col-md-4 offset-md-4">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<button class="btn btn-outline-secondary shadow-none"
|
||||
@click="changeStartDate(number_of_days * -1)">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</button>
|
||||
</div>
|
||||
<input name="date" id="id_date" class="form-control" type="date" v-model="start_date"
|
||||
@change="updatePlan()">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary shadow-none" @click="changeStartDate(number_of_days)">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<a href="{% url 'view_plan_new' %}" class="float-right">
|
||||
<button class="btn btn-outline-secondary shadow-none">
|
||||
<i class="fas fa-star"></i> {% trans 'Try the new meal planner' %}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-sm table-striped table-responsive-sm" style=" table-layout:fixed;">
|
||||
<thead class="thead-dark" style="background-image: url({% static 'assets/header.svg' %});">
|
||||
<tr>
|
||||
<th class="thead-blank" v-for="d in dates" style="width: 14.2%; text-align: center;">
|
||||
[[formatDateDayname(d)]]<br/>[[formatDateDay(d)]].
|
||||
<button class="btn btn-sm btn-outline-secondary shadow-none" @click="addDayToShopping(d)"><i
|
||||
class="fas fa-cart-plus fa-sm"></i></button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-for="t in meal_types">
|
||||
<tr v-if="meal_plan[t.name] !== undefined">
|
||||
<td :colspan="number_of_days" style="text-align: center">
|
||||
[[ meal_plan[t.name].name]]
|
||||
<template
|
||||
v-if="t.created_by !== {{ request.user.pk }} && user_names[t.created_by] !== undefined">
|
||||
([[ user_names[t.created_by] ]])
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="meal_plan[t.name] !== undefined">
|
||||
<td v-for="d in meal_plan[t.name].days">
|
||||
<draggable class="list-group" :list="d.items" group="plan" style="min-height: 40px"
|
||||
@change="dragChanged(d.date, t, $event)"
|
||||
:empty-insert-threshold="10" handle=".handle">
|
||||
<div class="" v-for="(element, index) in d.items" :key="element.id">
|
||||
<!-- small layout with handle -->
|
||||
<div class="d-block d-md-none">
|
||||
<div class="col-">
|
||||
<i class="fas fa-arrows-alt handle input-group-text"
|
||||
style="width: 100%"></i>
|
||||
</div>
|
||||
<div class="list-group-item" style="word-wrap: break-word;">
|
||||
<a href="#" @click="plan_detail = element" data-toggle="modal"
|
||||
data-target="#id_plan_detail_modal">[[ planElementName(element)]]</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- big layout -->
|
||||
<div class="list-group-item handle d-md-block d-none"
|
||||
style="word-wrap: break-word; padding: 2;margin-bottom: 4">
|
||||
<div class="col-md-12" style="padding: 0">
|
||||
<a href="#" @click="plan_detail = element" data-toggle="modal"
|
||||
data-target="#id_plan_detail_modal">[[ planElementName(element)]]</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</draggable>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-calendar-plus"></i> {% trans 'New Entry' %} <a href="#" data-toggle="modal"
|
||||
data-target="#id_plan_help_modal"><i
|
||||
class="far fa-question-circle"></i></a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" v-model="recipe_query" @keyup="getRecipes"
|
||||
placeholder="{% trans 'Search Recipe' %}">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button"
|
||||
@click="getRandomRecipes">
|
||||
<i class="fas fa-dice"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<draggable class="list-group" :list="recipes"
|
||||
:group="{ name: 'plan', pull: 'clone', put: false }" :clone="cloneRecipe">
|
||||
<div class="list-group-item d-flex align-items-center justify-content-between"
|
||||
v-for="(element, index) in recipes" :key="element.id">
|
||||
<span>
|
||||
<i class="fas fa-arrows-alt"></i> [[element.name]]
|
||||
</span>
|
||||
<span class="badge badge-light badge-pill">[[element.servings]]</span>
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<div class="card-body">
|
||||
<input type="text" class="form-control" v-model="new_note_title"
|
||||
placeholder="{% trans 'Title' %}" style="margin-bottom: 8px">
|
||||
<textarea class="form-control" v-model="new_note_text"
|
||||
placeholder="{% trans 'Note (optional)' %}"></textarea>
|
||||
<small><span
|
||||
class="text-muted">{% trans 'You can use markdown to format this field. See the <a href="/docs/markdown/" target="_blank" rel="noopener noreferrer">docs here</a>' %}</span></small>
|
||||
<br/>
|
||||
<br/>
|
||||
<input type="number" class="form-control" v-model="new_note_servings"
|
||||
placeholder="{% trans 'Serving Count' %}" style="margin-bottom: 8px">
|
||||
<br/>
|
||||
<draggable :list="pseudo_note_list"
|
||||
:group="{ name: 'plan', pull: 'clone', put: false }" :clone="cloneNote">
|
||||
<div class="list-group-item" v-for="(element, index) in pseudo_note_list"
|
||||
:key="element.id">
|
||||
<i class="fas fa-arrows-alt"></i> {% trans 'Create only note' %}
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-shopping-cart"></i> {% trans 'Shopping List' %}
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<template v-if="shopping_list.length < 1">{% trans 'Shopping list currently empty' %}</template>
|
||||
<template v-else>
|
||||
<a v-bind:href="getShoppingUrl()" class="btn btn-success"
|
||||
target="_blank">{% trans 'Open Shopping List' %}</a>
|
||||
<br/>
|
||||
<br/>
|
||||
{% trans 'Recipes' %}
|
||||
<ul class="list-group" style="margin-top: 8px">
|
||||
<li class="list-group-item" v-for="item in shopping_list"> [[ item.recipe_name ]]</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6" style="margin-top: 8px">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="fas fa-shopping-cart"></i> {% trans 'Plan' %}
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label>
|
||||
{% trans 'Number of Days' %}
|
||||
<input class="form-control" type="number" v-model="number_of_days"
|
||||
@change="updatePlan(); $cookies.set('number_of_days',number_of_days, -1)">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label>
|
||||
{% trans 'Weekday offset' %}
|
||||
<input class="form-control" type="number" v-model="start_offset"
|
||||
@change="updatePlan(); $cookies.set('start_offset',start_offset, -1)">
|
||||
<small class="text-muted">{% trans 'Number of days starting from the first day of the week to offset the default view.' %}</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<a href="#" data-toggle="modal"
|
||||
data-target="#id_plan_types_modal">{% trans 'Edit plan types' %}</a> <br/>
|
||||
<a href="#" data-toggle="modal"
|
||||
data-target="#id_plan_help_modal">{% trans 'Show help' %}</a><br/>
|
||||
<a v-bind:href="getIcalUrl()">{% trans 'Week iCal export' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="modal fade" id="id_plan_detail_modal" tabindex="-1" role="dialog"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<template v-if="plan_detail.title !==''">[[ plan_detail.title ]]</template>
|
||||
<template v-else>[[ plan_detail.recipe_name ]]</template>
|
||||
<small
|
||||
class="text-muted"><br/>[[ plan_detail.meal_type_name ]] [[
|
||||
formatLocalDate(plan_detail.date) ]]</small>
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<template v-if="plan_detail.recipe_name !== undefined ">
|
||||
<small class="text-muted">{% trans 'Recipe' %}</small><br/>
|
||||
<a v-bind:href="planDetailRecipeUrl()" target="_blank">[[ plan_detail.recipe_name ]]</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<small class="text-muted">{% trans 'Serving Count' %}</small><br/>
|
||||
<span>[[ plan_detail.servings ]]</span>
|
||||
</template>
|
||||
|
||||
<template v-if="plan_detail.note !== ''">
|
||||
<small class="text-muted">{% trans 'Note' %}</small><br/>
|
||||
<span v-html="plan_detail.note_markdown"></span>
|
||||
<br/>
|
||||
</template>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<template v-if="plan_detail.created_by !== undefined ">
|
||||
<small class="text-muted">{% trans 'Created by' %}</small><br/>
|
||||
[[ user_names[plan_detail.created_by] ]]
|
||||
<br/>
|
||||
</template>
|
||||
|
||||
<template v-if="plan_detail.shared.length > 0">
|
||||
<small class="text-muted">{% trans 'Shared with' %}</small><br/>
|
||||
<span>[[ planDetailUserList() ]]</span>
|
||||
<br/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger"
|
||||
@click="deleteEntry(plan_detail)">{% trans 'Delete' %}</button>
|
||||
<button type="button" class="btn btn-success"
|
||||
v-if="!shopping_list.includes(plan_detail) && plan_detail.recipe_name !== undefined"
|
||||
@click="shopping_list.push(plan_detail)">{% trans 'Add to Shopping' %}</button>
|
||||
<a class="btn btn-primary" v-bind:href="planDetailEditUrl()">{% trans 'Edit' %}</a>
|
||||
<button type="button" class="btn btn-secondary"
|
||||
data-dismiss="modal">{% trans 'Close' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="id_plan_types_modal" tabindex="-1" role="dialog"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans 'Edit plan types' %}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<draggable :list="meal_types_edit" handle=".handle"
|
||||
:group="{ name: 'types'}">
|
||||
<div v-for="(element, index) in meal_types_edit"
|
||||
:key="element.id">
|
||||
<template v-if="!element.delete">
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-prepend handle">
|
||||
<button tabindex="-1" class="btn btn-outline-secondary"><i
|
||||
class="fas fa-arrows-alt-v"></i></button>
|
||||
</div>
|
||||
<input class="form-control" v-model="element.name">
|
||||
<div class="input-group-append">
|
||||
<button tabindex="-1" class="btn btn-outline-danger" type="button"
|
||||
@click="markTypeDelete(element)"><i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary"
|
||||
@click="meal_types_edit.push({name:'{% trans 'New meal type' %}', delete:false})">{% trans 'New' %}</button>
|
||||
<button type="button" class="btn btn-success"
|
||||
@click="updatePlanTypes()">{% trans 'Save' %}</button>
|
||||
<button type="button" class="btn btn-secondary"
|
||||
data-dismiss="modal">{% trans 'Close' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="id_plan_help_modal" tabindex="-1" role="dialog"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans 'Meal Plan Help' %}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% blocktrans %}
|
||||
<p>The meal plan module allows planning of meals both with recipes and notes.</p>
|
||||
<p>Simply select a recipe from the list of recently viewed recipes or search the one you
|
||||
want and drag it to the desired plan position. You can also add a note and a title and
|
||||
then drag the recipe to create a plan entry with a custom title and note. Creating only
|
||||
Notes is possible by dragging the create note box into the plan.</p>
|
||||
<p>Click on a recipe in order to open the detailed view. There you can also add it to the
|
||||
shopping list. You can also add all recipes of a day to the shopping list by
|
||||
clicking the shopping cart at the top of the table.</p>
|
||||
<p>Since a common use case is to plan meals together you can define
|
||||
users you want to share your plan with in the settings.
|
||||
</p>
|
||||
<p>You can also edit the types of meals you want to plan. If you share your plan with
|
||||
someone with
|
||||
different meals, their meal types will appear in your list as well. To prevent
|
||||
duplicates (e.g. Other and Misc.)
|
||||
name your meal types the same as the users you share your meals with and they will be
|
||||
merged.</p>
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary"
|
||||
data-dismiss="modal">{% trans 'Close' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="{% url 'javascript-catalog' %}"></script>
|
||||
{% block script %}
|
||||
{% if debug %}
|
||||
<script src="{% url 'js_reverse' %}"></script>
|
||||
{% else %}
|
||||
<script src="{% static 'django_js_reverse/reverse.js' %}"></script>
|
||||
{% endif %}
|
||||
|
||||
<script type="application/javascript">
|
||||
moment.locale('{{request.LANGUAGE_CODE}}');
|
||||
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
|
||||
|
||||
let csrftoken = Cookies.get('csrftoken');
|
||||
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
|
||||
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
|
||||
|
||||
let app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
start_date: undefined,
|
||||
start_offset: 0,
|
||||
dates: [],
|
||||
number_of_days: $cookies.isKey('number_of_days') ? $cookies.get('number_of_days') : 7,
|
||||
plan_entries: [],
|
||||
meal_types: [],
|
||||
meal_types_edit: [],
|
||||
meal_plan: {},
|
||||
plan_detail: {shared: []},
|
||||
recipes: [],
|
||||
recipe_query: '',
|
||||
pseudo_note_list: [
|
||||
{id: 0, title: '', text: ''}
|
||||
],
|
||||
new_note_title: '',
|
||||
new_note_text: '',
|
||||
new_note_servings: '',
|
||||
default_shared_users: [],
|
||||
user_id_update: [],
|
||||
user_names: {},
|
||||
shopping: false,
|
||||
shopping_list: [],
|
||||
},
|
||||
mounted: function () {
|
||||
this.default_shared_users = [{% for u in request.user.userpreference.plan_share.all %}
|
||||
{{ u.pk }},
|
||||
{% endfor %}]
|
||||
|
||||
this.$set(this.user_names, {{ request.user.pk }}, '{{ request.user.get_user_name }}')
|
||||
this.user_id_update = Array.from(this.default_shared_users)
|
||||
|
||||
this.start_offset = $cookies.isKey('start_offset') ? $cookies.get('start_offset') : 0;
|
||||
this.start_date = moment().weekday(0).add(this.start_offset, 'days').format('YYYY-MM-DD')
|
||||
|
||||
this.updatePlan();
|
||||
this.getRecipes();
|
||||
|
||||
//this.makeToast('success', 'this actually works', 'success')
|
||||
},
|
||||
methods: {
|
||||
makeToast: function (title, message, variant = null) {
|
||||
this.$bvToast.toast(message, {
|
||||
title: title,
|
||||
variant: variant,
|
||||
toaster: 'b-toaster-top-center',
|
||||
solid: true
|
||||
})
|
||||
},
|
||||
updatePlan: function () {
|
||||
this.dates = [];
|
||||
for (var i = 0; i <= (this.number_of_days - 1); i++) {
|
||||
this.dates.push(moment(this.start_date).add(i, 'days'));
|
||||
}
|
||||
|
||||
let planEntryPromise = this.getPlanEntries();
|
||||
let planTypePromise = this.getPlanTypes();
|
||||
|
||||
Promise.allSettled([planEntryPromise, planTypePromise]).then(() => {
|
||||
this.buildGrid()
|
||||
})
|
||||
},
|
||||
getPlanEntries: function () {
|
||||
return this.$http.get("{% url 'api:mealplan-list' %}?from_date=" + this.dates[0].format('YYYY-MM-DD') + "&to_date=" + this.dates[this.dates.length - 1].format('YYYY-MM-DD')).then((response) => {
|
||||
this.plan_entries = response.data;
|
||||
}).catch((err) => {
|
||||
console.log("getPlanEntries error: ", err);
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
getPlanTypes: function () {
|
||||
return this.$http.get("{% url 'api:mealtype-list' %}").then((response) => {
|
||||
this.meal_types = response.data;
|
||||
this.meal_types_edit = jQuery.extend(true, [], response.data);
|
||||
for (let mte of this.meal_types_edit) {
|
||||
this.$set(mte, 'delete', false)
|
||||
}
|
||||
|
||||
if (this.meal_types.length === 0) {
|
||||
this.makeToast(gettext('Information'), gettext('To use the meal plan please first create at least one meal plan type.'), 'warning')
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log("getPlanTypes error: ", err);
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
buildGrid: function () {
|
||||
this.meal_plan = {}
|
||||
|
||||
for (let e of this.plan_entries) {
|
||||
let new_type = {id: e.meal_type, name: e.meal_type_name, created_by: e.created_by}
|
||||
if (this.meal_types.filter(el => el.name === new_type.name).length === 0) {
|
||||
this.meal_types.push(new_type)
|
||||
}
|
||||
}
|
||||
|
||||
for (let t of this.meal_types) {
|
||||
this.$set(this.meal_plan, t.name, {
|
||||
name: t.name,
|
||||
meal_type: t.id,
|
||||
days: {}
|
||||
})
|
||||
for (let d of this.dates) {
|
||||
this.$set(this.meal_plan[t.name].days, d.format('YYYY-MM-DD'), {
|
||||
name: this.formatDateDayname(d),
|
||||
date: d.format('YYYY-MM-DD'),
|
||||
items: []
|
||||
})
|
||||
}
|
||||
}
|
||||
for (let e of this.plan_entries) {
|
||||
this.meal_plan[e.meal_type_name].days[e.date].items.push(e)
|
||||
|
||||
for (let u of e.shared) {
|
||||
if (!this.user_id_update.includes(parseInt(u))) {
|
||||
this.user_id_update.push(parseInt(u))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateUserNames()
|
||||
},
|
||||
getRandomRecipes: function () {
|
||||
this.$set(this, 'recipe_query', '');
|
||||
this.getRecipes();
|
||||
},
|
||||
getRecipes: function () {
|
||||
let url = "{% url 'api:recipe-list' %}?page_size=5"
|
||||
if (this.recipe_query !== '') {
|
||||
url += '&query=' + this.recipe_query;
|
||||
} else {
|
||||
url += '&random=true'
|
||||
}
|
||||
|
||||
this.$http.get(url).then((response) => {
|
||||
this.recipes = this.removeDuplicates(response.data.results, recipe => recipe.id);
|
||||
}).catch((err) => {
|
||||
console.log("getRecipes error: ", err);
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
getMdNote: function () {
|
||||
let url = "{% url 'api:recipe-list' %}?page_size=5"
|
||||
if (this.recipe_query !== '') {
|
||||
url += '&query=' + this.recipe_query;
|
||||
}
|
||||
|
||||
this.$http.get(url).then((response) => {
|
||||
this.recipes = response.data.results;
|
||||
}).catch((err) => {
|
||||
console.log("getRecipes error: ", err);
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
updateUserNames: function () {
|
||||
return this.$http.get("{% url 'api:username-list' %}?filter_list=[" + this.user_id_update + ']').then((response) => {
|
||||
for (let u of response.data) {
|
||||
this.$set(this.user_names, u.id, u.username);
|
||||
}
|
||||
|
||||
}).catch((err) => {
|
||||
console.log("updateUserNames error: ", err);
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
dragChanged: function (date, meal_type, evt) {
|
||||
if (evt.added !== undefined) {
|
||||
let plan_entry = evt.added.element
|
||||
|
||||
plan_entry.date = date
|
||||
plan_entry.meal_type = meal_type
|
||||
plan_entry.meal_type_name = meal_type.name
|
||||
|
||||
if (plan_entry.is_new) { // its not a meal plan object
|
||||
plan_entry.created_by = {{ request.user.id }};
|
||||
plan_entry.shared = this.default_shared_users
|
||||
|
||||
this.$http.post(`{% url 'api:mealplan-list' %}`, plan_entry).then((response) => {
|
||||
let entry = response.data
|
||||
this.meal_plan[entry.meal_type_name].days[entry.date].items = this.meal_plan[entry.meal_type_name].days[entry.date].items.filter(item => !item.is_new)
|
||||
this.meal_plan[entry.meal_type_name].days[entry.date].items.push(entry)
|
||||
}).catch((err) => {
|
||||
console.log("dragChanged create error", err);
|
||||
})
|
||||
} else {
|
||||
this.$http.put(`{% url 'api:mealplan-list' %}${plan_entry.id}/`, plan_entry).then((response) => {
|
||||
}).catch((err) => {
|
||||
console.log("dragChanged update error", err);
|
||||
this.makeToast(gettext('Error'), gettext('There was an error updating a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
deleteEntry: function (entry) {
|
||||
$('#id_plan_detail_modal').modal('hide')
|
||||
this.$http.delete(`{% url 'api:mealplan-list' %}${entry.id}/`, entry).then((response) => {
|
||||
this.meal_plan[entry.meal_type_name].days[entry.date].items = this.meal_plan[entry.meal_type_name].days[entry.date].items.filter(item => item !== entry)
|
||||
}).catch((err) => {
|
||||
console.log("deleteEntry error: ", err);
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
removeDuplicates: function (data, key) {
|
||||
return [
|
||||
...new Map(data.map(item => [key(item), item])).values()
|
||||
]
|
||||
},
|
||||
updatePlanTypes: function () {
|
||||
let promise_list = []
|
||||
let i = 0
|
||||
for (let x of this.meal_types_edit) {
|
||||
x.order = i
|
||||
i++
|
||||
if (x.id === undefined && !x.delete) {
|
||||
x.created_by = {{ request.user.id }}
|
||||
promise_list.push(this.$http.post("{% url 'api:mealtype-list' %}", x).then((response) => {
|
||||
}).catch((err) => {
|
||||
console.log("updatePlanTypes create error: ", err);
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
}))
|
||||
} else if (x.delete) {
|
||||
if (x.id !== undefined) {
|
||||
promise_list.push(this.$http.delete(`{% url 'api:mealtype-list' %}${x.id}/`, x).then((response) => {
|
||||
}).catch((err) => {
|
||||
console.log("updatePlanTypes delete error: ", err);
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
promise_list.push(this.$http.put(`{% url 'api:mealtype-list' %}${x.id}/`, x).then((response) => {
|
||||
|
||||
}).catch((err) => {
|
||||
console.log("updatePlanTypes update error: ", err);
|
||||
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
|
||||
}))
|
||||
}
|
||||
}
|
||||
Promise.allSettled(promise_list).then(() => {
|
||||
this.updatePlan()
|
||||
$('#id_plan_types_modal').modal('hide')
|
||||
})
|
||||
},
|
||||
markTypeDelete: function (element) {
|
||||
if (confirm(gettext('When deleting a meal type all entries using that type will be deleted as well. Deletion will apply when configuration is saved. Do you want to proceed?'))) {
|
||||
element.delete = true
|
||||
}
|
||||
},
|
||||
cloneRecipe: function (recipe) {
|
||||
let r = {
|
||||
id: Math.round(Math.random() * 1000) + 10000,
|
||||
recipe: recipe,
|
||||
recipe_name: recipe.name,
|
||||
servings: (this.new_note_servings > 1) ? this.new_note_servings : recipe.servings,
|
||||
title: this.new_note_title,
|
||||
note: this.new_note_text,
|
||||
is_new: true
|
||||
}
|
||||
console.log(recipe)
|
||||
|
||||
this.new_note_title = ''
|
||||
this.new_note_text = ''
|
||||
this.new_note_servings = ''
|
||||
|
||||
return r
|
||||
},
|
||||
cloneNote: function () {
|
||||
let new_entry = {
|
||||
id: Math.round(Math.random() * 1000) + 10000,
|
||||
title: this.new_note_title,
|
||||
note: this.new_note_text,
|
||||
servings: 1,
|
||||
is_new: true,
|
||||
}
|
||||
|
||||
if (new_entry.title === '') {
|
||||
new_entry.title = gettext('Title')
|
||||
}
|
||||
|
||||
this.new_note_title = ''
|
||||
this.new_note_text = ''
|
||||
this.new_note_servings = ''
|
||||
return new_entry
|
||||
},
|
||||
planElementName: function (element) {
|
||||
if (element.title) {
|
||||
return element.title
|
||||
} else if (element.recipe_name) {
|
||||
return element.recipe_name
|
||||
} else {
|
||||
return element.name
|
||||
}
|
||||
},
|
||||
planDetailRecipeUrl: function () {
|
||||
return "{% url 'view_recipe' 12345 %}".replace(/12345/, this.plan_detail.recipe.id);
|
||||
},
|
||||
planDetailEditUrl: function () {
|
||||
return "{% url 'edit_meal_plan' 12345 %}".replace(/12345/, this.plan_detail.id);
|
||||
},
|
||||
planDetailUserList: function () {
|
||||
let users = []
|
||||
for (let u of this.plan_detail.shared) {
|
||||
users.push(this.user_names[u])
|
||||
}
|
||||
return users.join(', ')
|
||||
},
|
||||
formatLocalDate: function (date) {
|
||||
return moment(date).format('LL')
|
||||
},
|
||||
formatDateDay: function (date) {
|
||||
return moment(date).format('D')
|
||||
},
|
||||
formatDateDayname: function (date) {
|
||||
return moment(date).format('dddd')
|
||||
},
|
||||
changeStartDate: function (change) {
|
||||
this.start_date = moment(this.start_date).add(change, 'days').format('YYYY-MM-DD')
|
||||
this.updatePlan();
|
||||
},
|
||||
getShoppingUrl: function () {
|
||||
let url = "{% url 'view_shopping' %}"
|
||||
let first = true
|
||||
for (let se of this.shopping_list) {
|
||||
if (first) {
|
||||
url += `?r=[${se.recipe.id},${se.servings}]`
|
||||
first = false
|
||||
} else {
|
||||
url += `&r=[${se.recipe.id},${se.servings}]`
|
||||
}
|
||||
}
|
||||
return url
|
||||
},
|
||||
getIcalUrl: function () {
|
||||
if (this.dates.length === 0) {
|
||||
return ""
|
||||
}
|
||||
return "{% url 'api_get_plan_ical' 12345 6789 %}".replace(/12345/, this.dates[0].format('YYYY-MM-DD')).replace(/6789/, this.dates[this.dates.length - 1].format('YYYY-MM-DD'));
|
||||
},
|
||||
addDayToShopping: function (date) {
|
||||
for (let t of this.meal_types) {
|
||||
for (let i of this.meal_plan[t.name].days[date.format('YYYY-MM-DD')].items) {
|
||||
if (!this.shopping_list.includes(i)) {
|
||||
this.shopping_list.push(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
window.ICAL_URL = '{% url 'api_get_plan_ical' 12345 6789 %}'
|
||||
window.SHOPPING_URL = '{% url 'view_shopping' %}'
|
||||
</script>
|
||||
|
||||
{% render_bundle 'meal_plan_view' %}
|
||||
{% endblock %}
|
@ -58,7 +58,6 @@ urlpatterns = [
|
||||
path('search/v2/', views.search_v2, name='view_search_v2'),
|
||||
path('books/', views.books, name='view_books'),
|
||||
path('plan/', views.meal_plan, name='view_plan'),
|
||||
path('plan_new/', views.meal_plan_new, name='view_plan_new'),
|
||||
path('plan/entry/<int:pk>', views.meal_plan_entry, name='view_plan_entry'),
|
||||
path('shopping/', views.shopping_list, name='view_shopping'),
|
||||
path('shopping/<int:pk>', views.shopping_list, name='view_shopping'),
|
||||
|
@ -220,11 +220,6 @@ def meal_plan(request):
|
||||
return render(request, 'meal_plan.html', {})
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def meal_plan_new(request):
|
||||
return render(request, 'meal_plan_new.html', {})
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def supermarket(request):
|
||||
return render(request, 'supermarket.html', {})
|
||||
@ -343,10 +338,13 @@ def user_settings(request):
|
||||
if fields_searched == 0:
|
||||
search_form.add_error(None, _('You must select at least one field to search!'))
|
||||
search_error = True
|
||||
elif search_form.cleaned_data['search'] in ['websearch', 'raw'] and len(search_form.cleaned_data['fulltext']) == 0:
|
||||
search_form.add_error('search', _('To use this search method you must select at least one full text search field!'))
|
||||
elif search_form.cleaned_data['search'] in ['websearch', 'raw'] and len(
|
||||
search_form.cleaned_data['fulltext']) == 0:
|
||||
search_form.add_error('search',
|
||||
_('To use this search method you must select at least one full text search field!'))
|
||||
search_error = True
|
||||
elif search_form.cleaned_data['search'] in ['websearch', 'raw'] and len(search_form.cleaned_data['trigram']) > 0:
|
||||
elif search_form.cleaned_data['search'] in ['websearch', 'raw'] and len(
|
||||
search_form.cleaned_data['trigram']) > 0:
|
||||
search_form.add_error(None, _('Fuzzy search is not compatible with this search method!'))
|
||||
search_error = True
|
||||
else:
|
||||
@ -385,7 +383,8 @@ def user_settings(request):
|
||||
else:
|
||||
preference_form = UserPreferenceForm()
|
||||
|
||||
fields_searched = len(sp.icontains.all()) + len(sp.istartswith.all()) + len(sp.trigram.all()) + len(sp.fulltext.all())
|
||||
fields_searched = len(sp.icontains.all()) + len(sp.istartswith.all()) + len(sp.trigram.all()) + len(
|
||||
sp.fulltext.all())
|
||||
if sp and not search_error and fields_searched > 0:
|
||||
search_form = SearchPreferenceForm(instance=sp)
|
||||
elif not search_error:
|
||||
@ -395,7 +394,8 @@ def user_settings(request):
|
||||
api_token = Token.objects.create(user=request.user)
|
||||
|
||||
# these fields require postgress - just disable them if postgress isn't available
|
||||
if not settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
|
||||
if not settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
|
||||
'django.db.backends.postgresql']:
|
||||
search_form.fields['search'].disabled = True
|
||||
search_form.fields['lookup'].disabled = True
|
||||
search_form.fields['trigram'].disabled = True
|
||||
|
@ -1,72 +1,79 @@
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<a href="https://app.tandoor.dev"><img src="https://github.com/vabene1111/recipes/raw/develop/docs/logo_color.svg" height="256px" width="256px"></a>
|
||||
<a href="https://tandoor.dev"><img src="https://github.com/vabene1111/recipes/raw/develop/docs/logo_color.svg" height="256px" width="256px"></a>
|
||||
<br>
|
||||
Tandoor Recipes
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<h4 align="center">This is my personal beta of vabene's excellent recipe app. It includes many of the new features I've developed and should be considered experimental.</h4>
|
||||
## Experimental Features
|
||||
- Manual import recipes from URL & Source (HTML/JSON)
|
||||
- Bookmarklet to import recipes from any website
|
||||
- Full Text Search
|
||||
- Hierarchical Keywords
|
||||
|
||||
## Coming Next
|
||||
- Heirarchical Ingredients
|
||||
- Faceted Search
|
||||
- Search filter by rating
|
||||
- What Can I Make Now?
|
||||
- Better ingredient/unit matching on import
|
||||
- Custom word replacement on import (e.g. 'grams' automatically imported as 'g')
|
||||
- improved ingredient parser (items in parens moved to notes)
|
||||
- quick view ingredients
|
||||
- quick view associated recipe
|
||||
- favorite recipes
|
||||
|
||||
<h4 align="center">The recipe manager that allows you to manage your ever growing collection of digital recipes.</h4>
|
||||
|
||||
<p align="center">
|
||||
|
||||
<img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=develop" >
|
||||
<img src="https://img.shields.io/github/stars/vabene1111/recipes" >
|
||||
<img src="https://img.shields.io/github/forks/vabene1111/recipes" >
|
||||
<img src="https://img.shields.io/docker/pulls/vabene1111/recipes" >
|
||||
|
||||
<a href="https://github.com/vabene1111/recipes/actions" target="_blank" rel="noopener noreferrer"><img src="https://github.com/vabene1111/recipes/workflows/Continous%20Integration/badge.svg?branch=master" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/stargazers" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/stars/vabene1111/recipes" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/network/members" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/github/forks/vabene1111/recipes" ></a>
|
||||
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer"><img src="https://badgen.net/badge/icon/discord?icon=discord&label" ></a>
|
||||
<a href="https://hub.docker.com/r/vabene1111/recipes" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/docker/pulls/vabene1111/recipes" ></a>
|
||||
<a href="https://github.com/vabene1111/recipes/releases/latest" rel="noopener noreferrer"><img src="https://img.shields.io/github/v/release/vabene1111/recipes" ></a>
|
||||
<a href="https://app.tandoor.dev/accounts/login/?demo" rel="noopener noreferrer"><img src="https://img.shields.io/badge/demo-available-success" ></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.tandoor.dev/install/docker.html" rel="noopener noreferrer">Installation</a> •
|
||||
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Documentation</a> •
|
||||
<a href="https://app.tandoor.dev/" target="_blank" rel="noopener noreferrer">Demo</a>
|
||||
<a href="https://tandoor.dev" target="_blank" rel="noopener noreferrer">Website</a> •
|
||||
<a href="https://docs.tandoor.dev/install/docker/" target="_blank" rel="noopener noreferrer">Installation</a> •
|
||||
<a href="https://docs.tandoor.dev/" target="_blank" rel="noopener noreferrer">Docs</a> •
|
||||
<a href="https://app.tandoor.dev/accounts/login/?demo" target="_blank" rel="noopener noreferrer">Demo</a> •
|
||||
<a href="https://discord.gg/RhzBrfWgtp" target="_blank" rel="noopener noreferrer">Discord</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
!!! info "WIP"
|
||||
The documentation is work in progress. New information will be added over time.
|
||||
Feel free to open pull requests to enhance the documentation.
|
||||
## Core Features
|
||||
- 🥗 **Manage your recipes** - Manage your ever growing recipe collection
|
||||
- 📆 **Plan** - multiple meals for each day
|
||||
- 🛒 **Shopping lists** - via the meal plan or straight from recipes
|
||||
- 📚 **Cookbooks** - collect recipes into books
|
||||
- 👪 **Share and collaborate** on recipes with friends and family
|
||||
|
||||
## Features
|
||||
## Made by and for power users
|
||||
|
||||
- 📦 **Sync** files with Dropbox and Nextcloud (more can easily be added)
|
||||
- 🔍 Powerful **search** with Djangos [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
|
||||
- 🔍 Powerful & customizable **search** with fulltext support and [TrigramSimilarity](https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/search/#trigram-similarity)
|
||||
- 🏷️ Create and search for **tags**, assign them in batch to all files matching certain filters
|
||||
- 📄 **Create recipes** locally within a nice, standardized web interface
|
||||
- ⬇️ **Import recipes** from thousands of websites supporting [ld+json or microdata](https://schema.org/Recipe)
|
||||
- 📱 Optimized for use on **mobile** devices like phones and tablets
|
||||
- 🛒 Generate **shopping** lists from recipes
|
||||
- 📆 Create a **Plan** on what to eat when
|
||||
- 👪 **Share** recipes with friends and comment on them to suggest or remember changes you made
|
||||
- ➗ automatically convert decimal units to **fractions** for those who like this
|
||||
- 🐳 Easy setup with **Docker** and included examples for Kubernetes, Unraid and Synology
|
||||
- ↔️ Quickly merge and rename ingredients, tags and units
|
||||
- 📥️ **Import recipes** from thousands of websites supporting [ld+json or microdata](https://schema.org/Recipe)
|
||||
- ➗ Support for **fractions** or decimals
|
||||
- 🐳 Easy setup with **Docker** and included examples for **Kubernetes**, **Unraid** and **Synology**
|
||||
- 🎨 Customize your interface with **themes**
|
||||
- ✉️ Export and import recipes from other users
|
||||
- 📦 **Sync** files with Dropbox and Nextcloud
|
||||
|
||||
## All the must haves
|
||||
|
||||
- 📱Optimized for use on **mobile** devices
|
||||
- 🌍 localized in many languages thanks to the awesome community
|
||||
- ➕ Many more like recipe scaling, image compression, cookbooks, printing views, ...
|
||||
- 📥️ **Import your collection** from many other [recipe managers](https://docs.tandoor.dev/features/import_export/)
|
||||
- ➕ Many more like recipe scaling, image compression, printing views and supermarkets
|
||||
|
||||
This application is meant for people with a collection of recipes they want to share with family and friends or simply
|
||||
store them in a nicely organized way. A basic permission system exists but this application is not meant to be run as
|
||||
a public page.
|
||||
|
||||
## Your Feedback
|
||||
|
||||
Share some information on how you use Tandoor to help me improve the application [Google Survey](https://forms.gle/qNfLK2tWTeWHe9Qd7)
|
||||
|
||||
## Get in touch
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><a href="https://discord.gg/RhzBrfWgtp">Discord</a></td>
|
||||
<td>We have a public Discord server that anyone can join. This is where all our developers and contributors hang out and where we make announcements</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><a href="https://twitter.com/TandoorRecipes">Twitter</a></td>
|
||||
<td>You can follow our Twitter account to get updates on new features or releases</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Roadmap
|
||||
This application has been under rapid development over the last year.
|
||||
|
@ -61,10 +61,10 @@ import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
import {ApiApiFactory} from "../../utils/openapi/api";
|
||||
import CookbookSlider from "../../components/CookbookSlider";
|
||||
import LoadingSpinner from "../../components/LoadingSpinner";
|
||||
import {StandardToasts} from "../../utils/utils";
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import CookbookSlider from "@/components/CookbookSlider";
|
||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||
import {StandardToasts} from "@/utils/utils";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
|
@ -231,22 +231,24 @@
|
||||
|
||||
<script>
|
||||
|
||||
import Vue from "vue";
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
|
||||
import ContextMenu from "@/components/ContextMenu/ContextMenu";
|
||||
import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem";
|
||||
import {CalendarView, CalendarMathMixin} from "vue-simple-calendar/src/components/bundle";
|
||||
import Vue from "vue";
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import MealPlanCard from "../../components/MealPlanCard";
|
||||
import moment from 'moment'
|
||||
import {ApiMixin, StandardToasts} from "@/utils/utils";
|
||||
import MealPlanEditModal from "../../components/MealPlanEditModal";
|
||||
import VueCookies from "vue-cookies";
|
||||
import MealPlanCard from "@/components/MealPlanCard";
|
||||
import MealPlanEditModal from "@/components/MealPlanEditModal";
|
||||
import MealPlanCalenderHeader from "@/components/MealPlanCalenderHeader";
|
||||
import EmojiInput from "../../components/Modals/EmojiInput";
|
||||
import draggable from 'vuedraggable'
|
||||
import EmojiInput from "@/components/Modals/EmojiInput";
|
||||
|
||||
import moment from "moment"
|
||||
import draggable from "vuedraggable"
|
||||
import VueCookies from "vue-cookies";
|
||||
|
||||
import {ApiMixin, StandardToasts} from "@/utils/utils";
|
||||
import {CalendarView, CalendarMathMixin} from "vue-simple-calendar/src/components/bundle";
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
|
||||
const {makeToast} = require("@/utils/utils");
|
||||
|
||||
|
@ -108,7 +108,7 @@ export default {
|
||||
mixins: [ApiMixin],
|
||||
components: {
|
||||
GenericMultiselect,
|
||||
RecipeCard: () => import('./RecipeCard.vue')
|
||||
RecipeCard: () => import('@/components/RecipeCard.vue')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
Loading…
Reference in New Issue
Block a user