mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-21 05:53:36 -07:00
Merge branch 'mealie-next' into move-shopping-list-add-item-button-to-top
This commit is contained in:
commit
1f59038b99
228 changed files with 15298 additions and 16114 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
||||||
name: Build Package
|
name: Build Package
|
||||||
uses: ./.github/workflows/build-package.yml
|
uses: ./.github/workflows/build-package.yml
|
||||||
with:
|
with:
|
||||||
tag: release
|
tag: ${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
permissions:
|
permissions:
|
||||||
|
|
|
@ -12,7 +12,7 @@ repos:
|
||||||
exclude: ^tests/data/
|
exclude: ^tests/data/
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.12.2
|
rev: v0.12.7
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|
|
@ -35,7 +35,7 @@ conventional_commits = true
|
||||||
filter_unconventional = true
|
filter_unconventional = true
|
||||||
# regex for preprocessing the commit messages
|
# regex for preprocessing the commit messages
|
||||||
commit_preprocessors = [
|
commit_preprocessors = [
|
||||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/hay-kot/mealie/issues/${2}))"},
|
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/mealie-recipes/mealie/issues/${2}))"},
|
||||||
]
|
]
|
||||||
# regex for parsing and grouping commits
|
# regex for parsing and grouping commits
|
||||||
commit_parsers = [
|
commit_parsers = [
|
||||||
|
|
|
@ -179,9 +179,15 @@ def inject_nuxt_values():
|
||||||
|
|
||||||
all_langs = []
|
all_langs = []
|
||||||
for match in locales_dir.glob("*.json"):
|
for match in locales_dir.glob("*.json"):
|
||||||
lang_string = f'{{ code: "{match.stem}", file: "{match.name.replace(".json", ".ts")}" }},'
|
match_data = LOCALE_DATA.get(match.stem)
|
||||||
|
match_dir = match_data.dir if match_data else "ltr"
|
||||||
|
|
||||||
|
lang_string = f'{{ code: "{match.stem}", file: "{match.name.replace(".json", ".ts")}", dir: "{match_dir}" }},'
|
||||||
all_langs.append(lang_string)
|
all_langs.append(lang_string)
|
||||||
|
|
||||||
|
all_langs.sort()
|
||||||
|
all_date_locales.sort()
|
||||||
|
|
||||||
log.debug(f"injecting locales into nuxt config -> {nuxt_config}")
|
log.debug(f"injecting locales into nuxt config -> {nuxt_config}")
|
||||||
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
|
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
|
||||||
inject_inline(i18n_config, CodeKeys.nuxt_local_dates, all_date_locales)
|
inject_inline(i18n_config, CodeKeys.nuxt_local_dates, all_date_locales)
|
||||||
|
|
|
@ -44,7 +44,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "1 cup unsalted butter, cut into cubes",
|
"note": "1 cup unsalted butter, cut into cubes",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "ea3b6702-9532-4fbc-a40b-f99917831c26",
|
"referenceId": "ea3b6702-9532-4fbc-a40b-f99917831c26",
|
||||||
|
@ -54,7 +53,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "1 cup light brown sugar",
|
"note": "1 cup light brown sugar",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "c5bbfefb-1e23-4ffd-af88-c0363a0fae82",
|
"referenceId": "c5bbfefb-1e23-4ffd-af88-c0363a0fae82",
|
||||||
|
@ -64,7 +62,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "1/2 cup granulated white sugar",
|
"note": "1/2 cup granulated white sugar",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "034f481b-c426-4a17-b983-5aea9be4974b",
|
"referenceId": "034f481b-c426-4a17-b983-5aea9be4974b",
|
||||||
|
@ -74,7 +71,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "2 large eggs",
|
"note": "2 large eggs",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "37c1f796-3bdb-4856-859f-dbec90bc27e4",
|
"referenceId": "37c1f796-3bdb-4856-859f-dbec90bc27e4",
|
||||||
|
@ -84,7 +80,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "2 tsp vanilla extract",
|
"note": "2 tsp vanilla extract",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "85561ace-f249-401d-834c-e600a2f6280e",
|
"referenceId": "85561ace-f249-401d-834c-e600a2f6280e",
|
||||||
|
@ -94,7 +89,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "1/2 cup creamy peanut butter",
|
"note": "1/2 cup creamy peanut butter",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "ac91bda0-e8a8-491a-976a-ae4e72418cfd",
|
"referenceId": "ac91bda0-e8a8-491a-976a-ae4e72418cfd",
|
||||||
|
@ -104,7 +98,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "1 tsp cornstarch",
|
"note": "1 tsp cornstarch",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "4d1256b3-115e-4475-83cd-464fbc304cb0",
|
"referenceId": "4d1256b3-115e-4475-83cd-464fbc304cb0",
|
||||||
|
@ -114,7 +107,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "1 tsp baking soda",
|
"note": "1 tsp baking soda",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "64627441-39f9-4ee3-8494-bafe36451d12",
|
"referenceId": "64627441-39f9-4ee3-8494-bafe36451d12",
|
||||||
|
@ -124,7 +116,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "1/2 tsp salt",
|
"note": "1/2 tsp salt",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "7ae212d0-3cd1-44b0-899e-ec5bd91fd384",
|
"referenceId": "7ae212d0-3cd1-44b0-899e-ec5bd91fd384",
|
||||||
|
@ -134,7 +125,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "1 cup cake flour",
|
"note": "1 cup cake flour",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "06967994-8548-4952-a8cc-16e8db228ebd",
|
"referenceId": "06967994-8548-4952-a8cc-16e8db228ebd",
|
||||||
|
@ -144,7 +134,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "2 cups all-purpose flour",
|
"note": "2 cups all-purpose flour",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "bdb33b23-c767-4465-acf8-3b8e79eb5691",
|
"referenceId": "bdb33b23-c767-4465-acf8-3b8e79eb5691",
|
||||||
|
@ -154,7 +143,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "2 cups peanut butter chips",
|
"note": "2 cups peanut butter chips",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "12ba0af8-affd-4fb2-9cca-6f1b3e8d3aef",
|
"referenceId": "12ba0af8-affd-4fb2-9cca-6f1b3e8d3aef",
|
||||||
|
@ -164,7 +152,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"note": "1½ cups Reese's Pieces candies",
|
"note": "1½ cups Reese's Pieces candies",
|
||||||
"unit": None,
|
"unit": None,
|
||||||
"food": None,
|
"food": None,
|
||||||
"disableAmount": True,
|
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"originalText": None,
|
"originalText": None,
|
||||||
"referenceId": "4bdc0598-a3eb-41ee-8af0-4da9348fbfe2",
|
"referenceId": "4bdc0598-a3eb-41ee-8af0-4da9348fbfe2",
|
||||||
|
@ -221,7 +208,6 @@ def recipe_data(name: str, slug: str, id: str, userId: str, groupId: str) -> dic
|
||||||
"showAssets": False,
|
"showAssets": False,
|
||||||
"landscapeView": False,
|
"landscapeView": False,
|
||||||
"disableComments": False,
|
"disableComments": False,
|
||||||
"disableAmount": True,
|
|
||||||
"locked": False,
|
"locked": False,
|
||||||
},
|
},
|
||||||
"assets": [],
|
"assets": [],
|
||||||
|
|
|
@ -13,14 +13,14 @@ Steps:
|
||||||
|
|
||||||
#### 1. Get your API Token
|
#### 1. Get your API Token
|
||||||
|
|
||||||
Create an API token from Mealie's User Settings page (https://hay-kot.github.io/mealie/documentation/users-groups/user-settings/#api-key-generation)
|
Create an API token from Mealie's User Settings page (https://docs.mealie.io/documentation/getting-started/api-usage/#getting-a-token)
|
||||||
|
|
||||||
#### 2. Create Home Assistant Sensors
|
#### 2. Create Home Assistant Sensors
|
||||||
|
|
||||||
Create REST sensors in home assistant to get the details of today's meal.
|
Create REST sensors in home assistant to get the details of today's meal.
|
||||||
We will create sensors to get the name and ID of the first meal in today's meal plan (note that this may not be what is wanted if there is more than one meal planned for the day). We need the ID as well as the name to be able to retrieve the image for the meal.
|
We will create sensors to get the name and ID of the first meal in today's meal plan (note that this may not be what is wanted if there is more than one meal planned for the day). We need the ID as well as the name to be able to retrieve the image for the meal.
|
||||||
|
|
||||||
Make sure the url and port (`http://mealie:9000` ) matches your installation's address and _API_ port.
|
Make sure the url and port (`http://mealie:9000`) matches your installation's address and _API_ port.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
rest:
|
rest:
|
||||||
|
|
|
@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
|
||||||
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
|
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
|
||||||
|
|
||||||
1. Take a backup just in case!
|
1. Take a backup just in case!
|
||||||
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.0.0`
|
2. Replace the image for the API container with `ghcr.io/mealie-recipes/mealie:v3.0.2`
|
||||||
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
|
3. Take the external port from the frontend container and set that as the port mapped to port `9000` on the new container. The frontend is now served on port 9000 from the new container, so it will need to be mapped for you to have access.
|
||||||
4. Restart the container
|
4. Restart the container
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
# Installing with PostgreSQL
|
# Installing with PostgreSQL
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
When upgrading postgresql major versions, manual steps are required [Postgres#37](https://github.com/docker-library/postgres/issues/37).
|
||||||
|
|
||||||
PostgreSQL might be considered if you need to support many concurrent users. In addition, some features are only enabled on PostgreSQL, such as fuzzy search.
|
PostgreSQL might be considered if you need to support many concurrent users. In addition, some features are only enabled on PostgreSQL, such as fuzzy search.
|
||||||
|
|
||||||
**For Environment Variable Configuration, see** [Backend Configuration](./backend-config.md)
|
**For Environment Variable Configuration, see** [Backend Configuration](./backend-config.md)
|
||||||
|
@ -7,7 +10,7 @@ PostgreSQL might be considered if you need to support many concurrent users. In
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mealie:
|
mealie:
|
||||||
image: ghcr.io/mealie-recipes/mealie:v3.0.0 # (3)
|
image: ghcr.io/mealie-recipes/mealie:v3.0.2 # (3)
|
||||||
container_name: mealie
|
container_name: mealie
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
@ -38,7 +41,7 @@ services:
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
container_name: postgres
|
container_name: postgres
|
||||||
image: postgres:15
|
image: postgres:17
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- mealie-pgdata:/var/lib/postgresql/data
|
- mealie-pgdata:/var/lib/postgresql/data
|
||||||
|
@ -46,6 +49,7 @@ services:
|
||||||
POSTGRES_PASSWORD: mealie
|
POSTGRES_PASSWORD: mealie
|
||||||
POSTGRES_USER: mealie
|
POSTGRES_USER: mealie
|
||||||
PGUSER: mealie
|
PGUSER: mealie
|
||||||
|
POSTGRES_DB: mealie
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "pg_isready"]
|
test: ["CMD", "pg_isready"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|
|
@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mealie:
|
mealie:
|
||||||
image: ghcr.io/mealie-recipes/mealie:v3.0.0 # (3)
|
image: ghcr.io/mealie-recipes/mealie:v3.0.2 # (3)
|
||||||
container_name: mealie
|
container_name: mealie
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|
|
@ -2,6 +2,3 @@
|
||||||
|
|
||||||
## Feature Requests
|
## Feature Requests
|
||||||
[Please request new features on Github](https://github.com/mealie-recipes/mealie/discussions/new?category=feature-request)
|
[Please request new features on Github](https://github.com/mealie-recipes/mealie/discussions/new?category=feature-request)
|
||||||
|
|
||||||
## Progress
|
|
||||||
See the [Github Projects page](https://github.com/users/hay-kot/projects/2) to see what is currently being worked on
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -351,7 +351,7 @@
|
||||||
<!-- Custom narrow footer -->
|
<!-- Custom narrow footer -->
|
||||||
<div class="md-footer-meta__inner md-grid">
|
<div class="md-footer-meta__inner md-grid">
|
||||||
<div class="md-footer-social">
|
<div class="md-footer-social">
|
||||||
<a class="md-footer-social__link" href="https://github.com/hay-kot/mealie" rel="noopener" target="_blank"
|
<a class="md-footer-social__link" href="https://github.com/mealie-recipes/mealie" rel="noopener" target="_blank"
|
||||||
title="github.com">
|
title="github.com">
|
||||||
<svg style="width: 32px; height: 32px" viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg">
|
<svg style="width: 32px; height: 32px" viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
|
|
|
@ -44,36 +44,20 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
|
||||||
import { Organizer } from "~/lib/api/types/non-generated";
|
import { Organizer } from "~/lib/api/types/non-generated";
|
||||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||||
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||||
|
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const modelValue = defineModel<ReadCookBook>({ required: true });
|
||||||
components: { QueryFilterBuilder },
|
const i18n = useI18n();
|
||||||
props: {
|
const cookbook = toRef(modelValue);
|
||||||
modelValue: {
|
function handleInput(value: string | undefined) {
|
||||||
type: Object as () => ReadCookBook,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
type: Object as () => any,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
const cookbook = toRef(() => props.modelValue);
|
|
||||||
|
|
||||||
function handleInput(value: string | undefined) {
|
|
||||||
cookbook.value.queryFilterString = value || "";
|
cookbook.value.queryFilterString = value || "";
|
||||||
emit("update:modelValue", cookbook.value);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const fieldDefs: FieldDefinition[] = [
|
const fieldDefs: FieldDefinition[] = [
|
||||||
{
|
{
|
||||||
name: "recipe_category.id",
|
name: "recipe_category.id",
|
||||||
label: i18n.t("category.categories"),
|
label: i18n.t("category.categories"),
|
||||||
|
@ -109,13 +93,5 @@ export default defineNuxtComponent({
|
||||||
label: i18n.t("general.date-updated"),
|
label: i18n.t("general.date-updated"),
|
||||||
type: "date",
|
type: "date",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
|
||||||
cookbook,
|
|
||||||
handleInput,
|
|
||||||
fieldDefs,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<CookbookEditor
|
<CookbookEditor
|
||||||
v-model="editTarget"
|
v-model="editTarget"
|
||||||
:actions="actions"
|
|
||||||
/>
|
/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
@ -65,7 +64,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useLazyRecipes } from "~/composables/recipes";
|
import { useLazyRecipes } from "~/composables/recipes";
|
||||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||||
|
@ -74,44 +73,40 @@ import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import type { RecipeCookBook } from "~/lib/api/types/cookbook";
|
import type { RecipeCookBook } from "~/lib/api/types/cookbook";
|
||||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const $auth = useMealieAuth();
|
||||||
components: { RecipeCardSection, CookbookEditor },
|
const { isOwnGroup } = useLoggedInState();
|
||||||
setup() {
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const { isOwnGroup } = useLoggedInState();
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||||
const slug = route.params.slug as string;
|
const slug = route.params.slug as string;
|
||||||
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
||||||
const { actions } = useCookbookStore();
|
const { actions } = useCookbookStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const tab = ref(null);
|
const book = getOne(slug);
|
||||||
const book = getOne(slug);
|
|
||||||
|
|
||||||
const isOwnHousehold = computed(() => {
|
const isOwnHousehold = computed(() => {
|
||||||
if (!($auth.user.value && book.value?.householdId)) {
|
if (!($auth.user.value && book.value?.householdId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $auth.user.value.householdId === book.value.householdId;
|
return $auth.user.value.householdId === book.value.householdId;
|
||||||
});
|
});
|
||||||
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||||
|
|
||||||
const dialogStates = reactive({
|
const dialogStates = reactive({
|
||||||
edit: false,
|
edit: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const editTarget = ref<RecipeCookBook | null>(null);
|
const editTarget = ref<RecipeCookBook | null>(null);
|
||||||
function handleEditCookbook() {
|
function handleEditCookbook() {
|
||||||
dialogStates.edit = true;
|
dialogStates.edit = true;
|
||||||
editTarget.value = book.value;
|
editTarget.value = book.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editCookbook() {
|
async function editCookbook() {
|
||||||
if (!editTarget.value) {
|
if (!editTarget.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -127,28 +122,9 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
dialogStates.edit = false;
|
dialogStates.edit = false;
|
||||||
editTarget.value = null;
|
editTarget.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: book?.value?.name || "Cookbook",
|
title: book?.value?.name || "Cookbook",
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
book,
|
|
||||||
slug,
|
|
||||||
tab,
|
|
||||||
appendRecipes,
|
|
||||||
assignSorted,
|
|
||||||
recipes,
|
|
||||||
removeRecipe,
|
|
||||||
replaceRecipes,
|
|
||||||
canEdit,
|
|
||||||
dialogStates,
|
|
||||||
editTarget,
|
|
||||||
handleEditCookbook,
|
|
||||||
editCookbook,
|
|
||||||
actions,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -20,45 +20,33 @@
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { parseISO, formatDistanceToNow } from "date-fns";
|
import { parseISO, formatDistanceToNow } from "date-fns";
|
||||||
import type { GroupDataExport } from "~/lib/api/types/group";
|
import type { GroupDataExport } from "~/lib/api/types/group";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
defineProps<{
|
||||||
props: {
|
exports: GroupDataExport[];
|
||||||
exports: {
|
}>();
|
||||||
type: Array as () => GroupDataExport[],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
const headers = [
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const headers = [
|
||||||
{ title: i18n.t("export.export"), value: "name" },
|
{ title: i18n.t("export.export"), value: "name" },
|
||||||
{ title: i18n.t("export.file-name"), value: "filename" },
|
{ title: i18n.t("export.file-name"), value: "filename" },
|
||||||
{ title: i18n.t("export.size"), value: "size" },
|
{ title: i18n.t("export.size"), value: "size" },
|
||||||
{ title: i18n.t("export.link-expires"), value: "expires" },
|
{ title: i18n.t("export.link-expires"), value: "expires" },
|
||||||
{ title: "", value: "actions" },
|
{ title: "", value: "actions" },
|
||||||
];
|
];
|
||||||
|
|
||||||
function getTimeToExpire(timeString: string) {
|
function getTimeToExpire(timeString: string) {
|
||||||
const expiresAt = parseISO(timeString);
|
const expiresAt = parseISO(timeString);
|
||||||
|
|
||||||
return formatDistanceToNow(expiresAt, {
|
return formatDistanceToNow(expiresAt, {
|
||||||
addSuffix: false,
|
addSuffix: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadData(_: any) {
|
function downloadData(_: any) {
|
||||||
console.log("Downloading data...");
|
console.log("Downloading data...");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
downloadData,
|
|
||||||
headers,
|
|
||||||
getTimeToExpire,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -9,30 +9,10 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
export default defineNuxtComponent({
|
import type { ReadGroupPreferences } from "~/lib/api/types/user";
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const preferences = computed({
|
|
||||||
get() {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
context.emit("update:modelValue", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
const preferences = defineModel<ReadGroupPreferences>({ required: true });
|
||||||
preferences,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
<template>
|
|
||||||
<v-select
|
|
||||||
v-model="selected"
|
|
||||||
:items="households"
|
|
||||||
:label="label"
|
|
||||||
:hint="description"
|
|
||||||
:persistent-hint="!!description"
|
|
||||||
item-title="name"
|
|
||||||
:multiple="multiselect"
|
|
||||||
:prepend-inner-icon="$globals.icons.household"
|
|
||||||
return-object
|
|
||||||
>
|
|
||||||
<template #chip="data">
|
|
||||||
<v-chip
|
|
||||||
:key="data.index"
|
|
||||||
class="ma-1"
|
|
||||||
:input-value="data.item"
|
|
||||||
size="small"
|
|
||||||
closable
|
|
||||||
label
|
|
||||||
color="accent"
|
|
||||||
dark
|
|
||||||
@click:close="removeByIndex(data.index)"
|
|
||||||
>
|
|
||||||
{{ data.item.raw.name || data.item }}
|
|
||||||
</v-chip>
|
|
||||||
</template>
|
|
||||||
</v-select>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { useHouseholdStore } from "~/composables/store/use-household-store";
|
|
||||||
|
|
||||||
interface HouseholdLike {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: Array as () => HouseholdLike[],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
multiselect: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const selected = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:modelValue", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (selected.value === undefined) {
|
|
||||||
selected.value = [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
|
||||||
const label = computed(
|
|
||||||
() => props.multiselect ? i18n.t("household.households") : i18n.t("household.household"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { store: households } = useHouseholdStore();
|
|
||||||
function removeByIndex(index: number) {
|
|
||||||
if (selected.value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
|
||||||
selected.value = [...newSelected];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
selected,
|
|
||||||
label,
|
|
||||||
households,
|
|
||||||
removeByIndex,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -18,7 +18,7 @@
|
||||||
:open-on-hover="mdAndUp"
|
:open-on-hover="mdAndUp"
|
||||||
content-class="d-print-none"
|
content-class="d-print-none"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
:class="{ 'rounded-circle': fab }"
|
:class="{ 'rounded-circle': fab }"
|
||||||
:size="fab ? 'small' : undefined"
|
:size="fab ? 'small' : undefined"
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
:icon="!fab"
|
:icon="!fab"
|
||||||
variant="text"
|
variant="text"
|
||||||
dark
|
dark
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
@click.prevent
|
@click.prevent
|
||||||
>
|
>
|
||||||
<v-icon>{{ icon }}</v-icon>
|
<v-icon>{{ icon }}</v-icon>
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Recipe } from "~/lib/api/types/recipe";
|
import type { Recipe } from "~/lib/api/types/recipe";
|
||||||
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
|
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
|
||||||
import type { ShoppingListSummary } from "~/lib/api/types/household";
|
import type { ShoppingListSummary } from "~/lib/api/types/household";
|
||||||
|
@ -64,40 +64,32 @@ export interface ContextMenuItem {
|
||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
recipes?: Recipe[];
|
||||||
RecipeDialogAddToShoppingList,
|
menuTop?: boolean;
|
||||||
},
|
fab?: boolean;
|
||||||
props: {
|
color?: string;
|
||||||
recipes: {
|
menuIcon?: string | null;
|
||||||
type: Array as () => Recipe[],
|
}
|
||||||
default: () => [],
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
},
|
recipes: () => [],
|
||||||
menuTop: {
|
menuTop: true,
|
||||||
type: Boolean,
|
fab: false,
|
||||||
default: true,
|
color: "primary",
|
||||||
},
|
menuIcon: null,
|
||||||
fab: {
|
});
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: "primary",
|
|
||||||
},
|
|
||||||
menuIcon: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props, context) {
|
|
||||||
const { mdAndUp } = useDisplay();
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
const emit = defineEmits<{
|
||||||
const { $globals } = useNuxtApp();
|
[key: string]: [];
|
||||||
const api = useUserApi();
|
}>();
|
||||||
|
|
||||||
const state = reactive({
|
const { mdAndUp } = useDisplay();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const { $globals } = useNuxtApp();
|
||||||
|
const api = useUserApi();
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
loading: false,
|
loading: false,
|
||||||
shoppingListDialog: false,
|
shoppingListDialog: false,
|
||||||
menuItems: [
|
menuItems: [
|
||||||
|
@ -109,36 +101,38 @@ export default defineNuxtComponent({
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
const { shoppingListDialog, menuItems } = toRefs(state);
|
||||||
|
|
||||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||||
const recipesWithScales = computed(() => {
|
|
||||||
|
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||||
|
const recipesWithScales = computed(() => {
|
||||||
return props.recipes.map((recipe) => {
|
return props.recipes.map((recipe) => {
|
||||||
return {
|
return {
|
||||||
scale: 1,
|
scale: 1,
|
||||||
...recipe,
|
...recipe,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getShoppingLists() {
|
async function getShoppingLists() {
|
||||||
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
||||||
if (data) {
|
if (data) {
|
||||||
shoppingLists.value = data.items as ShoppingListSummary[] ?? [];
|
shoppingLists.value = data.items as ShoppingListSummary[] ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||||
shoppingList: () => {
|
shoppingList: () => {
|
||||||
getShoppingLists();
|
getShoppingLists();
|
||||||
state.shoppingListDialog = true;
|
state.shoppingListDialog = true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function contextMenuEventHandler(eventKey: string) {
|
function contextMenuEventHandler(eventKey: string) {
|
||||||
const handler = eventHandlers[eventKey];
|
const handler = eventHandlers[eventKey];
|
||||||
|
|
||||||
if (handler && typeof handler === "function") {
|
if (handler && typeof handler === "function") {
|
||||||
|
@ -147,18 +141,7 @@ export default defineNuxtComponent({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.emit(eventKey);
|
emit(eventKey);
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
contextMenuEventHandler,
|
|
||||||
icon,
|
|
||||||
recipesWithScales,
|
|
||||||
shoppingLists,
|
|
||||||
mdAndUp,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
style="gap: 10px"
|
style="gap: 10px"
|
||||||
>
|
>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="inputDay"
|
v-model="day"
|
||||||
:items="MEAL_DAY_OPTIONS"
|
:items="MEAL_DAY_OPTIONS"
|
||||||
:label="$t('meal-plan.rule-day')"
|
:label="$t('meal-plan.rule-day')"
|
||||||
/>
|
/>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="inputEntryType"
|
v-model="entryType"
|
||||||
:items="MEAL_TYPE_OPTIONS"
|
:items="MEAL_TYPE_OPTIONS"
|
||||||
:label="$t('meal-plan.meal-type')"
|
:label="$t('meal-plan.meal-type')"
|
||||||
/>
|
/>
|
||||||
|
@ -19,64 +19,49 @@
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<QueryFilterBuilder
|
<QueryFilterBuilder
|
||||||
:field-defs="fieldDefs"
|
:field-defs="fieldDefs"
|
||||||
:initial-query-filter="queryFilter"
|
:initial-query-filter="props.queryFilter"
|
||||||
@input="handleQueryFilterInput"
|
@input="handleQueryFilterInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO: proper pluralization of inputDay -->
|
<!-- TODO: proper pluralization of inputDay -->
|
||||||
{{ $t('meal-plan.this-rule-will-apply', {
|
{{ $t('meal-plan.this-rule-will-apply', {
|
||||||
dayCriteria: inputDay === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [inputDay]),
|
dayCriteria: day === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [day]),
|
||||||
mealTypeCriteria: inputEntryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [inputEntryType]),
|
mealTypeCriteria: entryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [entryType]),
|
||||||
}) }}
|
}) }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||||
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||||
import { Organizer } from "~/lib/api/types/non-generated";
|
import { Organizer } from "~/lib/api/types/non-generated";
|
||||||
import type { QueryFilterJSON } from "~/lib/api/types/response";
|
import type { QueryFilterJSON } from "~/lib/api/types/response";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
queryFilter?: QueryFilterJSON | null;
|
||||||
QueryFilterBuilder,
|
showHelp?: boolean;
|
||||||
},
|
}
|
||||||
props: {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
day: {
|
queryFilter: null,
|
||||||
type: String,
|
showHelp: false,
|
||||||
default: "unset",
|
});
|
||||||
},
|
|
||||||
entryType: {
|
|
||||||
type: String,
|
|
||||||
default: "unset",
|
|
||||||
},
|
|
||||||
queryFilterString: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
queryFilter: {
|
|
||||||
type: Object as () => QueryFilterJSON,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
showHelp: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:day", "update:entry-type", "update:query-filter-string"],
|
|
||||||
setup(props, context) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
const MEAL_TYPE_OPTIONS = [
|
const day = defineModel<string>("day", { default: "unset" });
|
||||||
|
const entryType = defineModel<string>("entryType", { default: "unset" });
|
||||||
|
const queryFilterString = defineModel<string>("queryFilterString", { default: "" });
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const MEAL_TYPE_OPTIONS = [
|
||||||
{ title: i18n.t("meal-plan.breakfast"), value: "breakfast" },
|
{ title: i18n.t("meal-plan.breakfast"), value: "breakfast" },
|
||||||
{ title: i18n.t("meal-plan.lunch"), value: "lunch" },
|
{ title: i18n.t("meal-plan.lunch"), value: "lunch" },
|
||||||
{ title: i18n.t("meal-plan.dinner"), value: "dinner" },
|
{ title: i18n.t("meal-plan.dinner"), value: "dinner" },
|
||||||
{ title: i18n.t("meal-plan.side"), value: "side" },
|
{ title: i18n.t("meal-plan.side"), value: "side" },
|
||||||
{ title: i18n.t("meal-plan.type-any"), value: "unset" },
|
{ title: i18n.t("meal-plan.type-any"), value: "unset" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const MEAL_DAY_OPTIONS = [
|
const MEAL_DAY_OPTIONS = [
|
||||||
{ title: i18n.t("general.monday"), value: "monday" },
|
{ title: i18n.t("general.monday"), value: "monday" },
|
||||||
{ title: i18n.t("general.tuesday"), value: "tuesday" },
|
{ title: i18n.t("general.tuesday"), value: "tuesday" },
|
||||||
{ title: i18n.t("general.wednesday"), value: "wednesday" },
|
{ title: i18n.t("general.wednesday"), value: "wednesday" },
|
||||||
|
@ -85,40 +70,14 @@ export default defineNuxtComponent({
|
||||||
{ title: i18n.t("general.saturday"), value: "saturday" },
|
{ title: i18n.t("general.saturday"), value: "saturday" },
|
||||||
{ title: i18n.t("general.sunday"), value: "sunday" },
|
{ title: i18n.t("general.sunday"), value: "sunday" },
|
||||||
{ title: i18n.t("meal-plan.day-any"), value: "unset" },
|
{ title: i18n.t("meal-plan.day-any"), value: "unset" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const inputDay = computed({
|
function handleQueryFilterInput(value: string | undefined) {
|
||||||
get: () => {
|
console.warn("handleQueryFilterInput called with value:", value);
|
||||||
return props.day;
|
queryFilterString.value = value || "";
|
||||||
},
|
}
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:day", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputEntryType = computed({
|
const fieldDefs: FieldDefinition[] = [
|
||||||
get: () => {
|
|
||||||
return props.entryType;
|
|
||||||
},
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:entry-type", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputQueryFilterString = computed({
|
|
||||||
get: () => {
|
|
||||||
return props.queryFilterString;
|
|
||||||
},
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:query-filter-string", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleQueryFilterInput(value: string | undefined) {
|
|
||||||
inputQueryFilterString.value = value || "";
|
|
||||||
};
|
|
||||||
|
|
||||||
const fieldDefs: FieldDefinition[] = [
|
|
||||||
{
|
{
|
||||||
name: "recipe_category.id",
|
name: "recipe_category.id",
|
||||||
label: i18n.t("category.categories"),
|
label: i18n.t("category.categories"),
|
||||||
|
@ -159,17 +118,5 @@ export default defineNuxtComponent({
|
||||||
label: i18n.t("general.date-updated"),
|
label: i18n.t("general.date-updated"),
|
||||||
type: "date",
|
type: "date",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
|
||||||
MEAL_TYPE_OPTIONS,
|
|
||||||
MEAL_DAY_OPTIONS,
|
|
||||||
inputDay,
|
|
||||||
inputEntryType,
|
|
||||||
inputQueryFilterString,
|
|
||||||
handleQueryFilterInput,
|
|
||||||
fieldDefs,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -16,11 +16,11 @@
|
||||||
:label="$t('settings.webhooks.webhook-url')"
|
:label="$t('settings.webhooks.webhook-url')"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
/>
|
/>
|
||||||
<v-time-picker
|
<v-text-field
|
||||||
v-model="scheduledTime"
|
v-model="scheduledTime"
|
||||||
class="elevation-2"
|
type="time"
|
||||||
ampm-in-title
|
clearable
|
||||||
format="ampm"
|
variant="underlined"
|
||||||
/>
|
/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions class="py-0 justify-end">
|
<v-card-actions class="py-0 justify-end">
|
||||||
|
@ -50,24 +50,25 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ReadWebhook } from "~/lib/api/types/household";
|
import type { ReadWebhook } from "~/lib/api/types/household";
|
||||||
import { timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks";
|
import { timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const props = defineProps<{
|
||||||
props: {
|
webhook: ReadWebhook;
|
||||||
webhook: {
|
}>();
|
||||||
type: Object as () => ReadWebhook,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["delete", "save", "test"],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
const itemUTC = ref<string>(props.webhook.scheduledTime);
|
|
||||||
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
|
|
||||||
|
|
||||||
const scheduledTime = computed({
|
const emit = defineEmits<{
|
||||||
|
delete: [id: string];
|
||||||
|
save: [webhook: ReadWebhook];
|
||||||
|
test: [id: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const itemUTC = ref<string>(props.webhook.scheduledTime);
|
||||||
|
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
|
||||||
|
|
||||||
|
const scheduledTime = computed({
|
||||||
get() {
|
get() {
|
||||||
return itemLocal.value;
|
return itemLocal.value;
|
||||||
},
|
},
|
||||||
|
@ -75,27 +76,17 @@ export default defineNuxtComponent({
|
||||||
itemUTC.value = timeLocalToUTC(v);
|
itemUTC.value = timeLocalToUTC(v);
|
||||||
itemLocal.value = v;
|
itemLocal.value = v;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const webhookCopy = ref({ ...props.webhook });
|
const webhookCopy = ref({ ...props.webhook });
|
||||||
|
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
webhookCopy.value.scheduledTime = itemLocal.value;
|
webhookCopy.value.scheduledTime = itemLocal.value;
|
||||||
emit("save", webhookCopy.value);
|
emit("save", webhookCopy.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set page title using useSeoMeta
|
// Set page title using useSeoMeta
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: i18n.t("settings.webhooks.webhooks"),
|
title: i18n.t("settings.webhooks.webhooks"),
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
webhookCopy,
|
|
||||||
scheduledTime,
|
|
||||||
handleSave,
|
|
||||||
itemUTC,
|
|
||||||
itemLocal,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -41,27 +41,19 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ReadHouseholdPreferences } from "~/lib/api/types/household";
|
import type { ReadHouseholdPreferences } from "~/lib/api/types/household";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const preferences = defineModel<ReadHouseholdPreferences>({ required: true });
|
||||||
props: {
|
const i18n = useI18n();
|
||||||
modelValue: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
type Preference = {
|
type Preference = {
|
||||||
key: keyof ReadHouseholdPreferences;
|
key: keyof ReadHouseholdPreferences;
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const recipePreferences: Preference[] = [
|
const recipePreferences: Preference[] = [
|
||||||
{
|
{
|
||||||
key: "recipePublic",
|
key: "recipePublic",
|
||||||
label: i18n.t("group.allow-users-outside-of-your-group-to-see-your-recipes"),
|
label: i18n.t("group.allow-users-outside-of-your-group-to-see-your-recipes"),
|
||||||
|
@ -87,14 +79,9 @@ export default defineNuxtComponent({
|
||||||
label: i18n.t("group.disable-users-from-commenting-on-recipes"),
|
label: i18n.t("group.disable-users-from-commenting-on-recipes"),
|
||||||
description: i18n.t("group.disable-users-from-commenting-on-recipes-description"),
|
description: i18n.t("group.disable-users-from-commenting-on-recipes-description"),
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
key: "recipeDisableAmount",
|
|
||||||
label: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food"),
|
|
||||||
description: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food-description"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const allDays = [
|
const allDays = [
|
||||||
{
|
{
|
||||||
name: i18n.t("general.sunday"),
|
name: i18n.t("general.sunday"),
|
||||||
value: 0,
|
value: 0,
|
||||||
|
@ -123,24 +110,7 @@ export default defineNuxtComponent({
|
||||||
name: i18n.t("general.saturday"),
|
name: i18n.t("general.saturday"),
|
||||||
value: 6,
|
value: 6,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const preferences = computed({
|
|
||||||
get() {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set(val) {
|
|
||||||
context.emit("update:modelValue", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
allDays,
|
|
||||||
preferences,
|
|
||||||
recipePreferences,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
|
|
|
@ -147,7 +147,7 @@
|
||||||
:model-value="field.value"
|
:model-value="field.value"
|
||||||
type="number"
|
type="number"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@:model-value="setFieldValue(field, index, $event)"
|
@update:model-value="setFieldValue(field, index, $event)"
|
||||||
/>
|
/>
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-else-if="field.type === 'boolean'"
|
v-else-if="field.type === 'boolean'"
|
||||||
|
@ -163,14 +163,14 @@
|
||||||
max-width="290px"
|
max-width="290px"
|
||||||
min-width="auto"
|
min-width="auto"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="field.value"
|
v-model="field.value"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
:prepend-icon="$globals.icons.calendar"
|
:prepend-icon="$globals.icons.calendar"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -184,53 +184,53 @@
|
||||||
</v-menu>
|
</v-menu>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-else-if="field.type === Organizer.Category"
|
v-else-if="field.type === Organizer.Category"
|
||||||
:model-value="field.organizers"
|
v-model="field.organizers"
|
||||||
:selector-type="Organizer.Category"
|
:selector-type="Organizer.Category"
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-else-if="field.type === Organizer.Tag"
|
v-else-if="field.type === Organizer.Tag"
|
||||||
:model-value="field.organizers"
|
v-model="field.organizers"
|
||||||
:selector-type="Organizer.Tag"
|
:selector-type="Organizer.Tag"
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-else-if="field.type === Organizer.Tool"
|
v-else-if="field.type === Organizer.Tool"
|
||||||
:model-value="field.organizers"
|
v-model="field.organizers"
|
||||||
:selector-type="Organizer.Tool"
|
:selector-type="Organizer.Tool"
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-else-if="field.type === Organizer.Food"
|
v-else-if="field.type === Organizer.Food"
|
||||||
:model-value="field.organizers"
|
v-model="field.organizers"
|
||||||
:selector-type="Organizer.Food"
|
:selector-type="Organizer.Food"
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-else-if="field.type === Organizer.Household"
|
v-else-if="field.type === Organizer.Household"
|
||||||
:model-value="field.organizers"
|
v-model="field.organizers"
|
||||||
:selector-type="Organizer.Household"
|
:selector-type="Organizer.Household"
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<!-- right parenthesis -->
|
<!-- right parenthesis -->
|
||||||
|
@ -297,7 +297,7 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { VueDraggable } from "vue-draggable-plus";
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
import { useDebounceFn } from "@vueuse/core";
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
import { useHouseholdSelf } from "~/composables/use-households";
|
import { useHouseholdSelf } from "~/composables/use-households";
|
||||||
|
@ -307,12 +307,7 @@ import type { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalK
|
||||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||||
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const props = defineProps({
|
||||||
components: {
|
|
||||||
VueDraggable,
|
|
||||||
RecipeOrganizerSelector,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
fieldDefs: {
|
fieldDefs: {
|
||||||
type: Array as () => FieldDefinition[],
|
type: Array as () => FieldDefinition[],
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -321,57 +316,62 @@ export default defineNuxtComponent({
|
||||||
type: Object as () => QueryFilterJSON | null,
|
type: Object as () => QueryFilterJSON | null,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
emits: ["input", "inputJSON"],
|
|
||||||
setup(props, context) {
|
|
||||||
const { household } = useHouseholdSelf();
|
|
||||||
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
|
||||||
|
|
||||||
const firstDayOfWeek = computed(() => {
|
const emit = defineEmits<{
|
||||||
|
(event: "input", value: string | undefined): void;
|
||||||
|
(event: "inputJSON", value: QueryFilterJSON | undefined): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { household } = useHouseholdSelf();
|
||||||
|
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
||||||
|
|
||||||
|
const firstDayOfWeek = computed(() => {
|
||||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
showAdvanced: false,
|
showAdvanced: false,
|
||||||
qfValid: false,
|
qfValid: false,
|
||||||
datePickers: [] as boolean[],
|
datePickers: [] as boolean[],
|
||||||
drag: false,
|
drag: false,
|
||||||
});
|
});
|
||||||
|
const { showAdvanced, datePickers, drag } = toRefs(state);
|
||||||
|
|
||||||
const storeMap = {
|
const storeMap = {
|
||||||
[Organizer.Category]: useCategoryStore(),
|
[Organizer.Category]: useCategoryStore(),
|
||||||
[Organizer.Tag]: useTagStore(),
|
[Organizer.Tag]: useTagStore(),
|
||||||
[Organizer.Tool]: useToolStore(),
|
[Organizer.Tool]: useToolStore(),
|
||||||
[Organizer.Food]: useFoodStore(),
|
[Organizer.Food]: useFoodStore(),
|
||||||
[Organizer.Household]: useHouseholdStore(),
|
[Organizer.Household]: useHouseholdStore(),
|
||||||
};
|
};
|
||||||
|
|
||||||
function onDragEnd(event: any) {
|
function onDragEnd(event: any) {
|
||||||
state.drag = false;
|
state.drag = false;
|
||||||
|
|
||||||
const oldIndex: number = event.oldIndex;
|
const oldIndex: number = event.oldIndex;
|
||||||
const newIndex: number = event.newIndex;
|
const newIndex: number = event.newIndex;
|
||||||
state.datePickers[oldIndex] = false;
|
state.datePickers[oldIndex] = false;
|
||||||
state.datePickers[newIndex] = false;
|
state.datePickers[newIndex] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add id to fields to prevent reactivity issues
|
// add id to fields to prevent reactivity issues
|
||||||
type FieldWithId = Field & { id: number };
|
type FieldWithId = Field & { id: number };
|
||||||
const fields = ref<FieldWithId[]>([]);
|
const fields = ref<FieldWithId[]>([]);
|
||||||
|
|
||||||
const uid = ref(1); // init uid to pass to fields
|
const uid = ref(1); // init uid to pass to fields
|
||||||
function useUid() {
|
function useUid() {
|
||||||
return uid.value++;
|
return uid.value++;
|
||||||
}
|
}
|
||||||
function addField(field: FieldDefinition) {
|
function addField(field: FieldDefinition) {
|
||||||
fields.value.push({
|
fields.value.push({
|
||||||
...getFieldFromFieldDef(field),
|
...getFieldFromFieldDef(field),
|
||||||
id: useUid(),
|
id: useUid(),
|
||||||
});
|
});
|
||||||
state.datePickers.push(false);
|
state.datePickers.push(false);
|
||||||
};
|
}
|
||||||
|
|
||||||
function setField(index: number, fieldLabel: string) {
|
function setField(index: number, fieldLabel: string) {
|
||||||
state.datePickers[index] = false;
|
state.datePickers[index] = false;
|
||||||
const fieldDef = props.fieldDefs.find(fieldDef => fieldDef.label === fieldLabel);
|
const fieldDef = props.fieldDefs.find(fieldDef => fieldDef.label === fieldLabel);
|
||||||
if (!fieldDef) {
|
if (!fieldDef) {
|
||||||
|
@ -388,48 +388,49 @@ export default defineNuxtComponent({
|
||||||
...getFieldFromFieldDef(updatedField, resetValue),
|
...getFieldFromFieldDef(updatedField, resetValue),
|
||||||
id: fields.value[index].id, // keep the id
|
id: fields.value[index].id, // keep the id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||||
fields.value[index].leftParenthesis = value;
|
fields.value[index].leftParenthesis = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRightParenthesisValue(field: FieldWithId, index: number, value: string) {
|
function setRightParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||||
fields.value[index].rightParenthesis = value;
|
fields.value[index].rightParenthesis = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLogicalOperatorValue(field: FieldWithId, index: number, value: LogicalOperator | undefined) {
|
function setLogicalOperatorValue(field: FieldWithId, index: number, value: LogicalOperator | undefined) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
value = logOps.value.AND.value;
|
value = logOps.value.AND.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
fields.value[index].logicalOperator = value ? logOps.value[value] : undefined;
|
fields.value[index].logicalOperator = value ? logOps.value[value] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
|
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
|
||||||
fields.value[index].relationalOperatorValue = relOps.value[value];
|
fields.value[index].relationalOperatorValue = relOps.value[value];
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFieldValue(field: FieldWithId, index: number, value: FieldValue) {
|
function setFieldValue(field: FieldWithId, index: number, value: FieldValue) {
|
||||||
state.datePickers[index] = false;
|
state.datePickers[index] = false;
|
||||||
fields.value[index].value = value;
|
fields.value[index].value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
|
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
|
||||||
fields.value[index].values = values;
|
fields.value[index].values = values;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOrganizerValues(field: FieldWithId, index: number, values: OrganizerBase[]) {
|
function setFieldOrganizers(field: FieldWithId, index: number, organizers: OrganizerBase[]) {
|
||||||
setFieldValues(field, index, values.map(value => value.id.toString()));
|
fields.value[index].organizers = organizers;
|
||||||
fields.value[index].organizers = values;
|
// Sync the values array with the organizers array
|
||||||
}
|
fields.value[index].values = organizers.map(org => org.id?.toString() || "").filter(id => id);
|
||||||
|
}
|
||||||
|
|
||||||
function removeField(index: number) {
|
function removeField(index: number) {
|
||||||
fields.value.splice(index, 1);
|
fields.value.splice(index, 1);
|
||||||
state.datePickers.splice(index, 1);
|
state.datePickers.splice(index, 1);
|
||||||
};
|
}
|
||||||
|
|
||||||
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
||||||
/* newFields.forEach((field, index) => {
|
/* newFields.forEach((field, index) => {
|
||||||
const updatedField = getFieldFromFieldDef(field);
|
const updatedField = getFieldFromFieldDef(field);
|
||||||
fields.value[index] = updatedField; // recursive!!!
|
fields.value[index] = updatedField; // recursive!!!
|
||||||
|
@ -441,19 +442,17 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
state.qfValid = !!qf;
|
state.qfValid = !!qf;
|
||||||
|
|
||||||
context.emit("input", qf || undefined);
|
emit("input", qf || undefined);
|
||||||
context.emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
|
emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
watch(fields, fieldsUpdater, { deep: true });
|
watch(fields, fieldsUpdater, { deep: true });
|
||||||
|
|
||||||
async function hydrateOrganizers(field: FieldWithId, index: number) {
|
async function hydrateOrganizers(field: FieldWithId, _index: number) {
|
||||||
if (!field.values?.length || !isOrganizerType(field.type)) {
|
if (!field.values?.length || !isOrganizerType(field.type)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
field.organizers = [];
|
|
||||||
|
|
||||||
const { store, actions } = storeMap[field.type];
|
const { store, actions } = storeMap[field.type];
|
||||||
if (!store.value.length) {
|
if (!store.value.length) {
|
||||||
await actions.refresh();
|
await actions.refresh();
|
||||||
|
@ -467,11 +466,12 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
return organizer;
|
return organizer;
|
||||||
});
|
});
|
||||||
field.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[];
|
|
||||||
setOrganizerValues(field, index, field.organizers);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initFieldsError(error = "") {
|
field.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[];
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFieldsError(error = "") {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
@ -480,16 +480,17 @@ export default defineNuxtComponent({
|
||||||
if (props.fieldDefs.length) {
|
if (props.fieldDefs.length) {
|
||||||
addField(props.fieldDefs[0]);
|
addField(props.fieldDefs[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeFields() {
|
async function initializeFields() {
|
||||||
if (!props.initialQueryFilter?.parts?.length) {
|
if (!props.initialQueryFilter?.parts?.length) {
|
||||||
return initFieldsError();
|
return initFieldsError();
|
||||||
};
|
}
|
||||||
|
|
||||||
const initFields: FieldWithId[] = [];
|
const initFields: FieldWithId[] = [];
|
||||||
let error = false;
|
let error = false;
|
||||||
props.initialQueryFilter.parts.forEach((part: QueryFilterJSONPart, index: number) => {
|
|
||||||
|
for (const [index, part] of props.initialQueryFilter.parts.entries()) {
|
||||||
const fieldDef = props.fieldDefs.find(fieldDef => fieldDef.name === part.attributeName);
|
const fieldDef = props.fieldDefs.find(fieldDef => fieldDef.name === part.attributeName);
|
||||||
if (!fieldDef) {
|
if (!fieldDef) {
|
||||||
error = true;
|
error = true;
|
||||||
|
@ -522,7 +523,7 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOrganizerType(field.type)) {
|
if (isOrganizerType(field.type)) {
|
||||||
hydrateOrganizers(field, index);
|
await hydrateOrganizers(field, index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (field.type === "boolean") {
|
else if (field.type === "boolean") {
|
||||||
|
@ -553,7 +554,7 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
initFields.push(field);
|
initFields.push(field);
|
||||||
});
|
}
|
||||||
|
|
||||||
if (initFields.length && !error) {
|
if (initFields.length && !error) {
|
||||||
fields.value = initFields;
|
fields.value = initFields;
|
||||||
|
@ -561,16 +562,18 @@ export default defineNuxtComponent({
|
||||||
else {
|
else {
|
||||||
initFieldsError();
|
initFieldsError();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
initializeFields();
|
await initializeFields();
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
initFieldsError(`Error initializing fields: ${(error || "").toString()}`);
|
initFieldsError(`Error initializing fields: ${(error || "").toString()}`);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function buildQueryFilterJSON(): QueryFilterJSON {
|
function buildQueryFilterJSON(): QueryFilterJSON {
|
||||||
const parts = fields.value.map((field) => {
|
const parts = fields.value.map((field) => {
|
||||||
const part: QueryFilterJSONPart = {
|
const part: QueryFilterJSONPart = {
|
||||||
attributeName: field.name,
|
attributeName: field.name,
|
||||||
|
@ -596,9 +599,9 @@ export default defineNuxtComponent({
|
||||||
const qfJSON = { parts } as QueryFilterJSON;
|
const qfJSON = { parts } as QueryFilterJSON;
|
||||||
console.debug(`Built query filter JSON: ${JSON.stringify(qfJSON)}`);
|
console.debug(`Built query filter JSON: ${JSON.stringify(qfJSON)}`);
|
||||||
return qfJSON;
|
return qfJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = computed(() => {
|
const config = computed(() => {
|
||||||
const baseColMaxWidth = 55;
|
const baseColMaxWidth = 55;
|
||||||
return {
|
return {
|
||||||
col: {
|
col: {
|
||||||
|
@ -642,30 +645,6 @@ export default defineNuxtComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
Organizer,
|
|
||||||
...toRefs(state),
|
|
||||||
logOps,
|
|
||||||
relOps,
|
|
||||||
config,
|
|
||||||
firstDayOfWeek,
|
|
||||||
onDragEnd,
|
|
||||||
// Fields
|
|
||||||
fields,
|
|
||||||
addField,
|
|
||||||
setField,
|
|
||||||
setLeftParenthesisValue,
|
|
||||||
setRightParenthesisValue,
|
|
||||||
setLogicalOperatorValue,
|
|
||||||
setRelationalOperatorValue,
|
|
||||||
setFieldValue,
|
|
||||||
setFieldValues,
|
|
||||||
setOrganizerValues,
|
|
||||||
removeField,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
<RecipeFavoriteBadge v-if="loggedIn" color="info" button-style :recipe-id="recipe.id!" show-always />
|
<RecipeFavoriteBadge v-if="loggedIn" color="info" button-style :recipe-id="recipe.id!" show-always />
|
||||||
<RecipeTimelineBadge v-if="loggedIn" class="ml-1" color="info" button-style :slug="recipe.slug" :recipe-name="recipe.name!" />
|
<RecipeTimelineBadge v-if="loggedIn" class="ml-1" color="info" button-style :slug="recipe.slug" :recipe-name="recipe.name!" />
|
||||||
<div v-if="loggedIn">
|
<div v-if="loggedIn">
|
||||||
<v-tooltip v-if="canEdit" bottom color="info">
|
<v-tooltip v-if="canEdit" location="bottom" color="info">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: tooltipProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
icon
|
icon
|
||||||
variant="flat"
|
variant="flat"
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
color="info"
|
color="info"
|
||||||
class="ml-1"
|
class="ml-1"
|
||||||
v-bind="props"
|
v-bind="tooltipProps"
|
||||||
@click="$emit('edit', true)"
|
@click="$emit('edit', true)"
|
||||||
>
|
>
|
||||||
<v-icon size="x-large">
|
<v-icon size="x-large">
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||||
import RecipeTimelineBadge from "./RecipeTimelineBadge.vue";
|
import RecipeTimelineBadge from "./RecipeTimelineBadge.vue";
|
||||||
|
@ -97,49 +97,30 @@ const DELETE_EVENT = "delete";
|
||||||
const CLOSE_EVENT = "close";
|
const CLOSE_EVENT = "close";
|
||||||
const JSON_EVENT = "json";
|
const JSON_EVENT = "json";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: { RecipeContextMenu, RecipeFavoriteBadge, RecipeTimelineBadge },
|
recipe: Recipe;
|
||||||
props: {
|
slug: string;
|
||||||
recipe: {
|
recipeScale?: number;
|
||||||
required: true,
|
open: boolean;
|
||||||
type: Object as () => Recipe,
|
name: string;
|
||||||
},
|
loggedIn?: boolean;
|
||||||
slug: {
|
recipeId: string;
|
||||||
required: true,
|
canEdit?: boolean;
|
||||||
type: String,
|
}
|
||||||
},
|
withDefaults(defineProps<Props>(), {
|
||||||
recipeScale: {
|
recipeScale: 1,
|
||||||
type: Number,
|
loggedIn: false,
|
||||||
default: 1,
|
canEdit: false,
|
||||||
},
|
});
|
||||||
open: {
|
|
||||||
required: true,
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
required: true,
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
loggedIn: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
recipeId: {
|
|
||||||
required: true,
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
canEdit: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["print", "input", "delete", "close", "edit"],
|
|
||||||
setup(_, context) {
|
|
||||||
const deleteDialog = ref(false);
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
const emit = defineEmits(["print", "input", "delete", "close", "edit"]);
|
||||||
const { $globals } = useNuxtApp();
|
|
||||||
const editorButtons = [
|
const deleteDialog = ref(false);
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const { $globals } = useNuxtApp();
|
||||||
|
|
||||||
|
const editorButtons = [
|
||||||
{
|
{
|
||||||
text: i18n.t("general.delete"),
|
text: i18n.t("general.delete"),
|
||||||
icon: $globals.icons.delete,
|
icon: $globals.icons.delete,
|
||||||
|
@ -164,36 +145,27 @@ export default defineNuxtComponent({
|
||||||
event: SAVE_EVENT,
|
event: SAVE_EVENT,
|
||||||
color: "success",
|
color: "success",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function emitHandler(event: string) {
|
function emitHandler(event: string) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case CLOSE_EVENT:
|
case CLOSE_EVENT:
|
||||||
context.emit(CLOSE_EVENT);
|
emit("close");
|
||||||
context.emit("input", false);
|
emit("input", false);
|
||||||
break;
|
break;
|
||||||
case DELETE_EVENT:
|
case DELETE_EVENT:
|
||||||
deleteDialog.value = true;
|
deleteDialog.value = true;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
context.emit(event);
|
emit(event as any);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitDelete() {
|
function emitDelete() {
|
||||||
context.emit(DELETE_EVENT);
|
emit("delete");
|
||||||
context.emit("input", false);
|
emit("input", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
deleteDialog,
|
|
||||||
editorButtons,
|
|
||||||
emitHandler,
|
|
||||||
emitDelete,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<div class="ma-auto">
|
<div class="ma-auto">
|
||||||
<v-tooltip bottom>
|
<v-tooltip location="bottom">
|
||||||
<template #activator="{ props: tooltipProps }">
|
<template #activator="{ props: tooltipProps }">
|
||||||
<v-icon v-bind="tooltipProps">
|
<v-icon v-bind="tooltipProps">
|
||||||
{{ getIconDefinition(item.icon).icon }}
|
{{ getIconDefinition(item.icon).icon }}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- Wrap v-hover with a div to provide a proper DOM element for the transition -->
|
<!-- Wrap v-hover with a div to provide a proper DOM element for the transition -->
|
||||||
<v-lazy>
|
|
||||||
<div>
|
<div>
|
||||||
<v-hover
|
<v-hover
|
||||||
v-slot="{ isHovering, props }"
|
v-slot="{ isHovering, props: hoverProps }"
|
||||||
:open-delay="50"
|
:open-delay="50"
|
||||||
>
|
>
|
||||||
<v-card
|
<v-card
|
||||||
v-bind="props"
|
v-bind="hoverProps"
|
||||||
:class="{ 'on-hover': isHovering }"
|
:class="{ 'on-hover': isHovering }"
|
||||||
:style="{ cursor }"
|
:style="{ cursor }"
|
||||||
:elevation="isHovering ? 12 : 2"
|
:elevation="isHovering ? 12 : 2"
|
||||||
|
@ -99,10 +98,9 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-hover>
|
</v-hover>
|
||||||
</div>
|
</div>
|
||||||
</v-lazy>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||||
import RecipeChips from "./RecipeChips.vue";
|
import RecipeChips from "./RecipeChips.vue";
|
||||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||||
|
@ -110,69 +108,41 @@ import RecipeCardImage from "./RecipeCardImage.vue";
|
||||||
import RecipeRating from "./RecipeRating.vue";
|
import RecipeRating from "./RecipeRating.vue";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage },
|
name: string;
|
||||||
props: {
|
slug: string;
|
||||||
name: {
|
description?: string | null;
|
||||||
type: String,
|
rating?: number;
|
||||||
required: true,
|
ratingColor?: string;
|
||||||
},
|
image?: string;
|
||||||
slug: {
|
tags?: Array<any>;
|
||||||
type: String,
|
recipeId: string;
|
||||||
required: true,
|
imageHeight?: number;
|
||||||
},
|
}
|
||||||
description: {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
type: String,
|
description: null,
|
||||||
default: null,
|
rating: 0,
|
||||||
},
|
ratingColor: "secondary",
|
||||||
rating: {
|
image: "abc123",
|
||||||
type: Number,
|
tags: () => [],
|
||||||
required: false,
|
imageHeight: 200,
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
ratingColor: {
|
|
||||||
type: String,
|
|
||||||
default: "secondary",
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: "abc123",
|
|
||||||
},
|
|
||||||
tags: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
recipeId: {
|
|
||||||
required: true,
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
imageHeight: {
|
|
||||||
type: Number,
|
|
||||||
default: 200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["click", "delete"],
|
|
||||||
setup(props) {
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const { isOwnGroup } = useLoggedInState();
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
|
||||||
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
|
||||||
const recipeRoute = computed<string>(() => {
|
|
||||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
|
||||||
});
|
|
||||||
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
|
||||||
|
|
||||||
return {
|
|
||||||
isOwnGroup,
|
|
||||||
recipeRoute,
|
|
||||||
showRecipeContent,
|
|
||||||
cursor,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
click: [];
|
||||||
|
delete: [slug: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const $auth = useMealieAuth();
|
||||||
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||||
|
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
||||||
|
const recipeRoute = computed<string>(() => {
|
||||||
|
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||||
|
});
|
||||||
|
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -197,6 +167,7 @@ export default defineNuxtComponent({
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 8;
|
-webkit-line-clamp: 8;
|
||||||
|
line-clamp: 8;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -28,66 +28,51 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
import { useStaticRoutes } from "~/composables/api";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
tiny?: boolean | null;
|
||||||
tiny: {
|
small?: boolean | null;
|
||||||
type: Boolean,
|
large?: boolean | null;
|
||||||
default: null,
|
iconSize?: number | string;
|
||||||
},
|
slug?: string | null;
|
||||||
small: {
|
recipeId: string;
|
||||||
type: Boolean,
|
imageVersion?: string | null;
|
||||||
default: null,
|
height?: number | string;
|
||||||
},
|
}
|
||||||
large: {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
type: Boolean,
|
tiny: null,
|
||||||
default: null,
|
small: null,
|
||||||
},
|
large: null,
|
||||||
iconSize: {
|
iconSize: 100,
|
||||||
type: [Number, String],
|
slug: null,
|
||||||
default: 100,
|
imageVersion: null,
|
||||||
},
|
height: "100%",
|
||||||
slug: {
|
});
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
recipeId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
imageVersion: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: [Number, String],
|
|
||||||
default: "100%",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["click"],
|
|
||||||
setup(props) {
|
|
||||||
const api = useUserApi();
|
|
||||||
|
|
||||||
const { recipeImage, recipeSmallImage, recipeTinyImage } = useStaticRoutes();
|
defineEmits<{
|
||||||
|
click: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
const fallBackImage = ref(false);
|
const { recipeImage, recipeSmallImage, recipeTinyImage } = useStaticRoutes();
|
||||||
const imageSize = computed(() => {
|
|
||||||
|
const fallBackImage = ref(false);
|
||||||
|
const imageSize = computed(() => {
|
||||||
if (props.tiny) return "tiny";
|
if (props.tiny) return "tiny";
|
||||||
if (props.small) return "small";
|
if (props.small) return "small";
|
||||||
if (props.large) return "large";
|
if (props.large) return "large";
|
||||||
return "large";
|
return "large";
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.recipeId,
|
() => props.recipeId,
|
||||||
() => {
|
() => {
|
||||||
fallBackImage.value = false;
|
fallBackImage.value = false;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function getImage(recipeId: string) {
|
function getImage(recipeId: string) {
|
||||||
switch (imageSize.value) {
|
switch (imageSize.value) {
|
||||||
case "tiny":
|
case "tiny":
|
||||||
return recipeTinyImage(recipeId, props.imageVersion);
|
return recipeTinyImage(recipeId, props.imageVersion);
|
||||||
|
@ -96,16 +81,7 @@ export default defineNuxtComponent({
|
||||||
case "large":
|
case "large":
|
||||||
return recipeImage(recipeId, props.imageVersion);
|
return recipeImage(recipeId, props.imageVersion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
api,
|
|
||||||
fallBackImage,
|
|
||||||
imageSize,
|
|
||||||
getImage,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
<v-expand-transition>
|
<v-expand-transition>
|
||||||
<v-card
|
<v-card
|
||||||
:ripple="false"
|
:ripple="false"
|
||||||
:class="isFlat ? 'mx-auto flat' : 'mx-auto'"
|
:class="[
|
||||||
|
isFlat ? 'mx-auto flat' : 'mx-auto',
|
||||||
|
{ 'disable-highlight': disableHighlight },
|
||||||
|
]"
|
||||||
:style="{ cursor }"
|
:style="{ cursor }"
|
||||||
hover
|
hover
|
||||||
height="100%"
|
height="100%"
|
||||||
|
@ -123,7 +126,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||||
|
@ -131,78 +134,44 @@ import RecipeRating from "./RecipeRating.vue";
|
||||||
import RecipeChips from "./RecipeChips.vue";
|
import RecipeChips from "./RecipeChips.vue";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
name: string;
|
||||||
RecipeFavoriteBadge,
|
slug: string;
|
||||||
RecipeContextMenu,
|
description: string;
|
||||||
RecipeRating,
|
rating?: number;
|
||||||
RecipeCardImage,
|
image?: string;
|
||||||
RecipeChips,
|
tags?: Array<any>;
|
||||||
},
|
recipeId: string;
|
||||||
props: {
|
vertical?: boolean;
|
||||||
name: {
|
isFlat?: boolean;
|
||||||
type: String,
|
height?: number;
|
||||||
required: true,
|
disableHighlight?: boolean;
|
||||||
},
|
}
|
||||||
slug: {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
type: String,
|
rating: 0,
|
||||||
required: true,
|
image: "abc123",
|
||||||
},
|
tags: () => [],
|
||||||
description: {
|
vertical: false,
|
||||||
type: String,
|
isFlat: false,
|
||||||
required: true,
|
height: 150,
|
||||||
},
|
disableHighlight: false,
|
||||||
rating: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: "abc123",
|
|
||||||
},
|
|
||||||
tags: {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
recipeId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
vertical: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isFlat: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: [Number],
|
|
||||||
default: 150,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["selected", "delete"],
|
|
||||||
setup(props) {
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const { isOwnGroup } = useLoggedInState();
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
|
||||||
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
|
||||||
const recipeRoute = computed<string>(() => {
|
|
||||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
|
||||||
});
|
|
||||||
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
|
||||||
|
|
||||||
return {
|
|
||||||
isOwnGroup,
|
|
||||||
recipeRoute,
|
|
||||||
showRecipeContent,
|
|
||||||
cursor,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
selected: [];
|
||||||
|
delete: [slug: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const $auth = useMealieAuth();
|
||||||
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||||
|
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
||||||
|
const recipeRoute = computed<string>(() => {
|
||||||
|
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||||
|
});
|
||||||
|
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -241,4 +210,8 @@ export default defineNuxtComponent({
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disable-highlight :deep(.v-card__overlay) {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -36,11 +36,11 @@
|
||||||
offset-y
|
offset-y
|
||||||
start
|
start
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
variant="text"
|
variant="text"
|
||||||
:icon="$vuetify.display.xs"
|
:icon="$vuetify.display.xs"
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
:loading="sortLoading"
|
:loading="sortLoading"
|
||||||
>
|
>
|
||||||
<v-icon :start="!$vuetify.display.xs">
|
<v-icon :start="!$vuetify.display.xs">
|
||||||
|
@ -162,7 +162,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useThrottleFn } from "@vueuse/core";
|
import { useThrottleFn } from "@vueuse/core";
|
||||||
import RecipeCard from "./RecipeCard.vue";
|
import RecipeCard from "./RecipeCard.vue";
|
||||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||||
|
@ -175,83 +175,69 @@ import type { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
|
||||||
const REPLACE_RECIPES_EVENT = "replaceRecipes";
|
const REPLACE_RECIPES_EVENT = "replaceRecipes";
|
||||||
const APPEND_RECIPES_EVENT = "appendRecipes";
|
const APPEND_RECIPES_EVENT = "appendRecipes";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
disableToolbar?: boolean;
|
||||||
RecipeCard,
|
disableSort?: boolean;
|
||||||
RecipeCardMobile,
|
icon?: string | null;
|
||||||
},
|
title?: string | null;
|
||||||
props: {
|
singleColumn?: boolean;
|
||||||
disableToolbar: {
|
recipes?: Recipe[];
|
||||||
type: Boolean,
|
query?: RecipeSearchQuery | null;
|
||||||
default: false,
|
}
|
||||||
},
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
disableSort: {
|
disableToolbar: false,
|
||||||
type: Boolean,
|
disableSort: false,
|
||||||
default: false,
|
icon: null,
|
||||||
},
|
title: null,
|
||||||
icon: {
|
singleColumn: false,
|
||||||
type: String,
|
recipes: () => [],
|
||||||
default: null,
|
query: null,
|
||||||
},
|
});
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
singleColumn: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
recipes: {
|
|
||||||
type: Array as () => Recipe[],
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
type: Object as () => RecipeSearchQuery,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props, context) {
|
|
||||||
const { $vuetify } = useNuxtApp();
|
|
||||||
const preferences = useUserSortPreferences();
|
|
||||||
|
|
||||||
const EVENTS = {
|
const emit = defineEmits<{
|
||||||
|
replaceRecipes: [recipes: Recipe[]];
|
||||||
|
appendRecipes: [recipes: Recipe[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { $vuetify } = useNuxtApp();
|
||||||
|
const preferences = useUserSortPreferences();
|
||||||
|
|
||||||
|
const EVENTS = {
|
||||||
az: "az",
|
az: "az",
|
||||||
rating: "rating",
|
rating: "rating",
|
||||||
created: "created",
|
created: "created",
|
||||||
updated: "updated",
|
updated: "updated",
|
||||||
lastMade: "lastMade",
|
lastMade: "lastMade",
|
||||||
shuffle: "shuffle",
|
shuffle: "shuffle",
|
||||||
};
|
};
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const { $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
const useMobileCards = computed(() => {
|
const useMobileCards = computed(() => {
|
||||||
return $vuetify.display.smAndDown.value || preferences.value.useMobileCards;
|
return $vuetify.display.smAndDown.value || preferences.value.useMobileCards;
|
||||||
});
|
});
|
||||||
|
|
||||||
const displayTitleIcon = computed(() => {
|
const displayTitleIcon = computed(() => {
|
||||||
return props.icon || $globals.icons.tags;
|
return props.icon || $globals.icons.tags;
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = reactive({
|
const sortLoading = ref(false);
|
||||||
sortLoading: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const perPage = 32;
|
const perPage = 32;
|
||||||
const hasMore = ref(true);
|
const hasMore = ref(true);
|
||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const { fetchMore, getRandom } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
const { fetchMore, getRandom } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const queryFilter = computed(() => {
|
const queryFilter = computed(() => {
|
||||||
return props.query.queryFilter || null;
|
return props.query?.queryFilter || null;
|
||||||
|
|
||||||
// TODO: allow user to filter out null values when ordering by a value that may be null (such as lastMade)
|
// TODO: allow user to filter out null values when ordering by a value that may be null (such as lastMade)
|
||||||
|
|
||||||
|
@ -265,9 +251,9 @@ export default defineNuxtComponent({
|
||||||
// } else {
|
// } else {
|
||||||
// return orderByFilter;
|
// return orderByFilter;
|
||||||
// }
|
// }
|
||||||
});
|
});
|
||||||
|
|
||||||
async function fetchRecipes(pageCount = 1) {
|
async function fetchRecipes(pageCount = 1) {
|
||||||
const orderDir = props.query?.orderDirection || preferences.value.orderDirection;
|
const orderDir = props.query?.orderDirection || preferences.value.orderDirection;
|
||||||
const orderByNullPosition = props.query?.orderByNullPosition || orderDir === "asc" ? "first" : "last";
|
const orderByNullPosition = props.query?.orderByNullPosition || orderDir === "asc" ? "first" : "last";
|
||||||
return await fetchMore(
|
return await fetchMore(
|
||||||
|
@ -280,17 +266,17 @@ export default defineNuxtComponent({
|
||||||
// we use a computed queryFilter to filter out recipes that have a null value for the property we're sorting by
|
// we use a computed queryFilter to filter out recipes that have a null value for the property we're sorting by
|
||||||
queryFilter.value,
|
queryFilter.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await initRecipes();
|
await initRecipes();
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
let lastQuery: string | undefined = JSON.stringify(props.query);
|
let lastQuery: string | undefined = JSON.stringify(props.query);
|
||||||
watch(
|
watch(
|
||||||
() => props.query,
|
() => props.query,
|
||||||
async (newValue: RecipeSearchQuery | undefined) => {
|
async (newValue: RecipeSearchQuery | undefined | null) => {
|
||||||
const newValueString = JSON.stringify(newValue);
|
const newValueString = JSON.stringify(newValue);
|
||||||
if (lastQuery !== newValueString) {
|
if (lastQuery !== newValueString) {
|
||||||
lastQuery = newValueString;
|
lastQuery = newValueString;
|
||||||
|
@ -299,9 +285,9 @@ export default defineNuxtComponent({
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
async function initRecipes() {
|
async function initRecipes() {
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
hasMore.value = true;
|
hasMore.value = true;
|
||||||
|
|
||||||
|
@ -315,10 +301,10 @@ export default defineNuxtComponent({
|
||||||
// since we doubled the first call, we also need to advance the page
|
// since we doubled the first call, we also need to advance the page
|
||||||
page.value = page.value + 1;
|
page.value = page.value + 1;
|
||||||
|
|
||||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||||
}
|
}
|
||||||
|
|
||||||
const infiniteScroll = useThrottleFn(async () => {
|
const infiniteScroll = useThrottleFn(async () => {
|
||||||
if (!hasMore.value || loading.value) {
|
if (!hasMore.value || loading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -331,14 +317,14 @@ export default defineNuxtComponent({
|
||||||
hasMore.value = false;
|
hasMore.value = false;
|
||||||
}
|
}
|
||||||
if (newRecipes.length) {
|
if (newRecipes.length) {
|
||||||
context.emit(APPEND_RECIPES_EVENT, newRecipes);
|
emit(APPEND_RECIPES_EVENT, newRecipes);
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
async function sortRecipes(sortType: string) {
|
async function sortRecipes(sortType: string) {
|
||||||
if (state.sortLoading || loading.value) {
|
if (sortLoading.value || loading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,45 +389,29 @@ export default defineNuxtComponent({
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
hasMore.value = true;
|
hasMore.value = true;
|
||||||
|
|
||||||
state.sortLoading = true;
|
sortLoading.value = true;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
// fetch new recipes
|
// fetch new recipes
|
||||||
const newRecipes = await fetchRecipes();
|
const newRecipes = await fetchRecipes();
|
||||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||||
|
|
||||||
state.sortLoading = false;
|
sortLoading.value = false;
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function navigateRandom() {
|
async function navigateRandom() {
|
||||||
const recipe = await getRandom(props.query, queryFilter.value);
|
const recipe = await getRandom(props.query, queryFilter.value);
|
||||||
if (!recipe?.slug) {
|
if (!recipe?.slug) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
router.push(`/g/${groupSlug.value}/r/${recipe.slug}`);
|
router.push(`/g/${groupSlug.value}/r/${recipe.slug}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMobileCards() {
|
function toggleMobileCards() {
|
||||||
preferences.value.useMobileCards = !preferences.value.useMobileCards;
|
preferences.value.useMobileCards = !preferences.value.useMobileCards;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
displayTitleIcon,
|
|
||||||
EVENTS,
|
|
||||||
infiniteScroll,
|
|
||||||
ready,
|
|
||||||
loading,
|
|
||||||
navigateRandom,
|
|
||||||
preferences,
|
|
||||||
sortRecipes,
|
|
||||||
toggleMobileCards,
|
|
||||||
useMobileCards,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -23,66 +23,38 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import type { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
import type { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
export type UrlPrefixParam = "tags" | "categories" | "tools";
|
export type UrlPrefixParam = "tags" | "categories" | "tools";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
truncate?: boolean;
|
||||||
truncate: {
|
items?: RecipeCategory[] | RecipeTag[] | RecipeTool[];
|
||||||
type: Boolean,
|
title?: boolean;
|
||||||
default: false,
|
urlPrefix?: UrlPrefixParam;
|
||||||
},
|
limit?: number;
|
||||||
items: {
|
small?: boolean;
|
||||||
type: Array as () => RecipeCategory[] | RecipeTag[] | RecipeTool[],
|
maxWidth?: string | null;
|
||||||
default: () => [],
|
}
|
||||||
},
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
title: {
|
truncate: false,
|
||||||
type: Boolean,
|
items: () => [],
|
||||||
default: false,
|
title: false,
|
||||||
},
|
urlPrefix: "categories",
|
||||||
urlPrefix: {
|
limit: 999,
|
||||||
type: String as () => UrlPrefixParam,
|
small: false,
|
||||||
default: "categories",
|
maxWidth: null,
|
||||||
},
|
});
|
||||||
limit: {
|
|
||||||
type: Number,
|
|
||||||
default: 999,
|
|
||||||
},
|
|
||||||
small: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
maxWidth: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["item-selected"],
|
|
||||||
setup(props) {
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
|
|
||||||
const route = useRoute();
|
defineEmits(["item-selected"]);
|
||||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
function truncateText(text: string, length = 20, clamp = "...") {
|
||||||
const baseRecipeRoute = computed<string>(() => {
|
|
||||||
return `/g/${groupSlug.value}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
function truncateText(text: string, length = 20, clamp = "...") {
|
|
||||||
if (!props.truncate) return text;
|
if (!props.truncate) return text;
|
||||||
const node = document.createElement("div");
|
const node = document.createElement("div");
|
||||||
node.innerHTML = text;
|
node.innerHTML = text;
|
||||||
const content = node.textContent || "";
|
const content = node.textContent || "";
|
||||||
return content.length > length ? content.slice(0, length) + clamp : content;
|
return content.length > length ? content.slice(0, length) + clamp : content;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
baseRecipeRoute,
|
|
||||||
truncateText,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|
|
@ -12,7 +12,12 @@
|
||||||
@confirm="deleteRecipe()"
|
@confirm="deleteRecipe()"
|
||||||
>
|
>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
|
<template v-if="isAdminAndNotOwner">
|
||||||
|
{{ $t("recipe.admin-delete-confirmation") }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
{{ $t("recipe.delete-confirmation") }}
|
{{ $t("recipe.delete-confirmation") }}
|
||||||
|
</template>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
|
@ -50,12 +55,12 @@
|
||||||
max-width="290px"
|
max-width="290px"
|
||||||
min-width="auto"
|
min-width="auto"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="newMealdateString"
|
v-model="newMealdateString"
|
||||||
:label="$t('general.date')"
|
:label="$t('general.date')"
|
||||||
:prepend-icon="$globals.icons.calendar"
|
:prepend-icon="$globals.icons.calendar"
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -95,7 +100,7 @@
|
||||||
:open-on-hover="$vuetify.display.mdAndUp"
|
:open-on-hover="$vuetify.display.mdAndUp"
|
||||||
content-class="d-print-none"
|
content-class="d-print-none"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
icon
|
icon
|
||||||
:variant="fab ? 'flat' : undefined"
|
:variant="fab ? 'flat' : undefined"
|
||||||
|
@ -103,7 +108,7 @@
|
||||||
:size="fab ? 'small' : undefined"
|
:size="fab ? 'small' : undefined"
|
||||||
:color="fab ? 'info' : 'secondary'"
|
:color="fab ? 'info' : 'secondary'"
|
||||||
:fab="fab"
|
:fab="fab"
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
@click.prevent
|
@click.prevent
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-icon
|
||||||
|
@ -125,32 +130,27 @@
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<div v-if="useItems.recipeActions && recipeActions && recipeActions.length">
|
<div v-if="useItems.recipeActions && recipeActions && recipeActions.length">
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<v-list-group @click.stop>
|
|
||||||
<template #activator="{ props }">
|
|
||||||
<v-list-item-title v-bind="props">
|
|
||||||
{{ $t("recipe.recipe-actions") }}
|
|
||||||
</v-list-item-title>
|
|
||||||
</template>
|
|
||||||
<v-list density="compact" class="ma-0 pa-0">
|
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-for="(action, index) in recipeActions"
|
v-for="(action, index) in recipeActions"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="pl-6"
|
|
||||||
@click="executeRecipeAction(action)"
|
@click="executeRecipeAction(action)"
|
||||||
>
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon color="undefined">
|
||||||
|
{{ $globals.icons.linkVariantPlus }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
{{ action.title }}
|
{{ action.title }}
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
|
||||||
</v-list-group>
|
|
||||||
</div>
|
</div>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue";
|
import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue";
|
||||||
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
|
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
|
||||||
import RecipeDialogShare from "./RecipeDialogShare.vue";
|
import RecipeDialogShare from "./RecipeDialogShare.vue";
|
||||||
|
@ -186,16 +186,22 @@ export interface ContextMenuItem {
|
||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
useItems?: ContextMenuIncludes;
|
||||||
RecipeDialogAddToShoppingList,
|
appendItems?: ContextMenuItem[];
|
||||||
RecipeDialogPrintPreferences,
|
leadingItems?: ContextMenuItem[];
|
||||||
RecipeDialogShare,
|
menuTop?: boolean;
|
||||||
},
|
fab?: boolean;
|
||||||
props: {
|
color?: string;
|
||||||
useItems: {
|
slug: string;
|
||||||
type: Object as () => ContextMenuIncludes,
|
menuIcon?: string | null;
|
||||||
default: () => ({
|
name: string;
|
||||||
|
recipe?: Recipe;
|
||||||
|
recipeId: string;
|
||||||
|
recipeScale?: number;
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
useItems: () => ({
|
||||||
delete: true,
|
delete: true,
|
||||||
edit: true,
|
edit: true,
|
||||||
download: true,
|
download: true,
|
||||||
|
@ -207,94 +213,61 @@ export default defineNuxtComponent({
|
||||||
share: true,
|
share: true,
|
||||||
recipeActions: true,
|
recipeActions: true,
|
||||||
}),
|
}),
|
||||||
},
|
appendItems: () => [],
|
||||||
// Append items are added at the end of the useItems list
|
leadingItems: () => [],
|
||||||
appendItems: {
|
menuTop: true,
|
||||||
type: Array as () => ContextMenuItem[],
|
fab: false,
|
||||||
default: () => [],
|
color: "primary",
|
||||||
},
|
menuIcon: null,
|
||||||
// Append items are added at the beginning of the useItems list
|
recipe: undefined,
|
||||||
leadingItems: {
|
recipeScale: 1,
|
||||||
type: Array as () => ContextMenuItem[],
|
});
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
menuTop: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
fab: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: "primary",
|
|
||||||
},
|
|
||||||
slug: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
menuIcon: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
required: true,
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
recipe: {
|
|
||||||
type: Object as () => Recipe,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
recipeId: {
|
|
||||||
required: true,
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
recipeScale: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["delete"],
|
|
||||||
setup(props, context) {
|
|
||||||
const api = useUserApi();
|
|
||||||
|
|
||||||
const state = reactive({
|
const emit = defineEmits<{
|
||||||
printPreferencesDialog: false,
|
[key: string]: any;
|
||||||
shareDialog: false,
|
delete: [slug: string];
|
||||||
recipeDeleteDialog: false,
|
}>();
|
||||||
mealplannerDialog: false,
|
|
||||||
shoppingListDialog: false,
|
|
||||||
recipeDuplicateDialog: false,
|
|
||||||
recipeName: props.name,
|
|
||||||
loading: false,
|
|
||||||
menuItems: [] as ContextMenuItem[],
|
|
||||||
newMealdate: new Date(Date.now() - new Date().getTimezoneOffset() * 60000),
|
|
||||||
newMealType: "dinner" as PlanEntryType,
|
|
||||||
pickerMenu: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newMealdateString = computed(() => {
|
const api = useUserApi();
|
||||||
return state.newMealdate.toISOString().substring(0, 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
const printPreferencesDialog = ref(false);
|
||||||
const $auth = useMealieAuth();
|
const shareDialog = ref(false);
|
||||||
const { $globals } = useNuxtApp();
|
const recipeDeleteDialog = ref(false);
|
||||||
const { household } = useHouseholdSelf();
|
const mealplannerDialog = ref(false);
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const shoppingListDialog = ref(false);
|
||||||
|
const recipeDuplicateDialog = ref(false);
|
||||||
|
const recipeName = ref(props.name);
|
||||||
|
const loading = ref(false);
|
||||||
|
const menuItems = ref<ContextMenuItem[]>([]);
|
||||||
|
const newMealdate = ref(new Date());
|
||||||
|
const newMealType = ref<PlanEntryType>("dinner");
|
||||||
|
const pickerMenu = ref(false);
|
||||||
|
|
||||||
const route = useRoute();
|
const newMealdateString = computed(() => {
|
||||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
// Format the date to YYYY-MM-DD in the same timezone as newMealdate
|
||||||
|
const year = newMealdate.value.getFullYear();
|
||||||
|
const month = String(newMealdate.value.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(newMealdate.value.getDate()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
});
|
||||||
|
|
||||||
const firstDayOfWeek = computed(() => {
|
const i18n = useI18n();
|
||||||
|
const $auth = useMealieAuth();
|
||||||
|
const { $globals } = useNuxtApp();
|
||||||
|
const { household } = useHouseholdSelf();
|
||||||
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
|
const firstDayOfWeek = computed(() => {
|
||||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Context Menu Setup
|
// Context Menu Setup
|
||||||
|
|
||||||
const defaultItems: { [key: string]: ContextMenuItem } = {
|
const defaultItems: { [key: string]: ContextMenuItem } = {
|
||||||
edit: {
|
edit: {
|
||||||
title: i18n.t("general.edit"),
|
title: i18n.t("general.edit"),
|
||||||
icon: $globals.icons.edit,
|
icon: $globals.icons.edit,
|
||||||
|
@ -358,50 +331,64 @@ export default defineNuxtComponent({
|
||||||
event: "share",
|
event: "share",
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add leading and Appending Items
|
||||||
|
menuItems.value = [...menuItems.value, ...props.leadingItems, ...props.appendItems];
|
||||||
|
|
||||||
|
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Context Menu Event Handler
|
||||||
|
|
||||||
|
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||||
|
const recipeRef = ref<Recipe | undefined>(props.recipe);
|
||||||
|
const recipeRefWithScale = computed(() =>
|
||||||
|
recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined,
|
||||||
|
);
|
||||||
|
const isAdminAndNotOwner = computed(() => {
|
||||||
|
return (
|
||||||
|
$auth.user.value?.admin
|
||||||
|
&& $auth.user.value?.id !== recipeRef.value?.userId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const canDelete = computed(() => {
|
||||||
|
const user = $auth.user.value;
|
||||||
|
const recipe = recipeRef.value;
|
||||||
|
return user && recipe && (user.admin || user.id === recipe.userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get Default Menu Items Specified in Props
|
||||||
|
for (const [key, value] of Object.entries(props.useItems)) {
|
||||||
|
if (!value) continue;
|
||||||
|
|
||||||
|
// Skip delete if not allowed
|
||||||
|
if (key === "delete" && !canDelete.value) continue;
|
||||||
|
|
||||||
// Get Default Menu Items Specified in Props
|
|
||||||
for (const [key, value] of Object.entries(props.useItems)) {
|
|
||||||
if (value) {
|
|
||||||
const item = defaultItems[key];
|
const item = defaultItems[key];
|
||||||
if (item && (item.isPublic || isOwnGroup.value)) {
|
if (item && (item.isPublic || isOwnGroup.value)) {
|
||||||
state.menuItems.push(item);
|
menuItems.value.push(item);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add leading and Appending Items
|
async function getShoppingLists() {
|
||||||
state.menuItems = [...state.menuItems, ...props.leadingItems, ...props.appendItems];
|
|
||||||
|
|
||||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
|
||||||
|
|
||||||
// ===========================================================================
|
|
||||||
// Context Menu Event Handler
|
|
||||||
|
|
||||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
|
||||||
const recipeRef = ref<Recipe | undefined>(props.recipe);
|
|
||||||
const recipeRefWithScale = computed(() =>
|
|
||||||
recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
async function getShoppingLists() {
|
|
||||||
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
||||||
if (data) {
|
if (data) {
|
||||||
shoppingLists.value = data.items ?? [];
|
shoppingLists.value = data.items ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshRecipe() {
|
async function refreshRecipe() {
|
||||||
const { data } = await api.recipes.getOne(props.slug);
|
const { data } = await api.recipes.getOne(props.slug);
|
||||||
if (data) {
|
if (data) {
|
||||||
recipeRef.value = data;
|
recipeRef.value = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const groupRecipeActionsStore = useGroupRecipeActions();
|
const groupRecipeActionsStore = useGroupRecipeActions();
|
||||||
|
|
||||||
async function executeRecipeAction(action: GroupRecipeActionOut) {
|
async function executeRecipeAction(action: GroupRecipeActionOut) {
|
||||||
if (!props.recipe) return;
|
if (!props.recipe) return;
|
||||||
const response = await groupRecipeActionsStore.execute(action, props.recipe, props.recipeScale);
|
const response = await groupRecipeActionsStore.execute(action, props.recipe, props.recipeScale);
|
||||||
|
|
||||||
|
@ -413,30 +400,30 @@ export default defineNuxtComponent({
|
||||||
alert.error(i18n.t("events.something-went-wrong"));
|
alert.error(i18n.t("events.something-went-wrong"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteRecipe() {
|
async function deleteRecipe() {
|
||||||
const { data } = await api.recipes.deleteOne(props.slug);
|
const { data } = await api.recipes.deleteOne(props.slug);
|
||||||
if (data?.slug) {
|
if (data?.slug) {
|
||||||
router.push(`/g/${groupSlug.value}`);
|
router.push(`/g/${groupSlug.value}`);
|
||||||
}
|
}
|
||||||
context.emit("delete", props.slug);
|
emit("delete", props.slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
const download = useDownloader();
|
const download = useDownloader();
|
||||||
|
|
||||||
async function handleDownloadEvent() {
|
async function handleDownloadEvent() {
|
||||||
const { data } = await api.recipes.getZipToken(props.slug);
|
const { data } = await api.recipes.getZipToken(props.slug);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
download(api.recipes.getZipRedirectUrl(props.slug, data.token), `${props.slug}.zip`);
|
download(api.recipes.getZipRedirectUrl(props.slug, data.token), `${props.slug}.zip`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addRecipeToPlan() {
|
async function addRecipeToPlan() {
|
||||||
const { response } = await api.mealplans.createOne({
|
const { response } = await api.mealplans.createOne({
|
||||||
date: newMealdateString.value,
|
date: newMealdateString.value,
|
||||||
entryType: state.newMealType,
|
entryType: newMealType.value,
|
||||||
title: "",
|
title: "",
|
||||||
text: "",
|
text: "",
|
||||||
recipeId: props.recipeId,
|
recipeId: props.recipeId,
|
||||||
|
@ -448,34 +435,34 @@ export default defineNuxtComponent({
|
||||||
else {
|
else {
|
||||||
alert.error(i18n.t("recipe.failed-to-add-recipe-to-mealplan") as string);
|
alert.error(i18n.t("recipe.failed-to-add-recipe-to-mealplan") as string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateRecipe() {
|
async function duplicateRecipe() {
|
||||||
const { data } = await api.recipes.duplicateOne(props.slug, state.recipeName);
|
const { data } = await api.recipes.duplicateOne(props.slug, recipeName.value);
|
||||||
if (data && data.slug) {
|
if (data && data.slug) {
|
||||||
router.push(`/g/${groupSlug.value}/r/${data.slug}`);
|
router.push(`/g/${groupSlug.value}/r/${data.slug}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Print is handled as an event in the parent component
|
// Note: Print is handled as an event in the parent component
|
||||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||||
delete: () => {
|
delete: () => {
|
||||||
state.recipeDeleteDialog = true;
|
recipeDeleteDialog.value = true;
|
||||||
},
|
},
|
||||||
edit: () => router.push(`/g/${groupSlug.value}/r/${props.slug}` + "?edit=true"),
|
edit: () => router.push(`/g/${groupSlug.value}/r/${props.slug}` + "?edit=true"),
|
||||||
download: handleDownloadEvent,
|
download: handleDownloadEvent,
|
||||||
duplicate: () => {
|
duplicate: () => {
|
||||||
state.recipeDuplicateDialog = true;
|
recipeDuplicateDialog.value = true;
|
||||||
},
|
},
|
||||||
mealplanner: () => {
|
mealplanner: () => {
|
||||||
state.mealplannerDialog = true;
|
mealplannerDialog.value = true;
|
||||||
},
|
},
|
||||||
printPreferences: async () => {
|
printPreferences: async () => {
|
||||||
if (!recipeRef.value) {
|
if (!recipeRef.value) {
|
||||||
await refreshRecipe();
|
await refreshRecipe();
|
||||||
}
|
}
|
||||||
state.printPreferencesDialog = true;
|
printPreferencesDialog.value = true;
|
||||||
},
|
},
|
||||||
shoppingList: () => {
|
shoppingList: () => {
|
||||||
const promises: Promise<void>[] = [getShoppingLists()];
|
const promises: Promise<void>[] = [getShoppingLists()];
|
||||||
|
@ -484,45 +471,27 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.allSettled(promises).then(() => {
|
Promise.allSettled(promises).then(() => {
|
||||||
state.shoppingListDialog = true;
|
shoppingListDialog.value = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
share: () => {
|
share: () => {
|
||||||
state.shareDialog = true;
|
shareDialog.value = true;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function contextMenuEventHandler(eventKey: string) {
|
function contextMenuEventHandler(eventKey: string) {
|
||||||
const handler = eventHandlers[eventKey];
|
const handler = eventHandlers[eventKey];
|
||||||
|
|
||||||
if (handler && typeof handler === "function") {
|
if (handler && typeof handler === "function") {
|
||||||
handler();
|
handler();
|
||||||
state.loading = false;
|
loading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.emit(eventKey);
|
emit(eventKey);
|
||||||
state.loading = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const planTypeOptions = usePlanTypeOptions();
|
const planTypeOptions = usePlanTypeOptions();
|
||||||
|
const recipeActions = groupRecipeActionsStore.recipeActions;
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
newMealdateString,
|
|
||||||
recipeRef,
|
|
||||||
recipeRefWithScale,
|
|
||||||
executeRecipeAction,
|
|
||||||
recipeActions: groupRecipeActionsStore.recipeActions,
|
|
||||||
shoppingLists,
|
|
||||||
duplicateRecipe,
|
|
||||||
contextMenuEventHandler,
|
|
||||||
deleteRecipe,
|
|
||||||
addRecipeToPlan,
|
|
||||||
icon,
|
|
||||||
planTypeOptions,
|
|
||||||
firstDayOfWeek,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { whenever } from "@vueuse/core";
|
import { whenever } from "@vueuse/core";
|
||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||||
|
@ -42,56 +42,47 @@ export interface GenericAlias {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
data: IngredientFood | IngredientUnit;
|
||||||
modelValue: {
|
}
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: Object as () => IngredientFood | IngredientUnit,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["submit", "update:modelValue", "cancel"],
|
|
||||||
setup(props, context) {
|
|
||||||
// V-Model Support
|
|
||||||
const dialog = computed({
|
|
||||||
get: () => {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:modelValue", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function createAlias() {
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
submit: [aliases: GenericAlias[]];
|
||||||
|
cancel: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// V-Model Support
|
||||||
|
const dialog = defineModel<boolean>({ default: false });
|
||||||
|
|
||||||
|
function createAlias() {
|
||||||
aliases.value.push({
|
aliases.value.push({
|
||||||
name: "",
|
name: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteAlias(index: number) {
|
function deleteAlias(index: number) {
|
||||||
aliases.value.splice(index, 1);
|
aliases.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const aliases = ref<GenericAlias[]>(props.data.aliases || []);
|
const aliases = ref<GenericAlias[]>(props.data.aliases || []);
|
||||||
function initAliases() {
|
function initAliases() {
|
||||||
aliases.value = [...props.data.aliases || []];
|
aliases.value = [...props.data.aliases || []];
|
||||||
if (!aliases.value.length) {
|
if (!aliases.value.length) {
|
||||||
createAlias();
|
createAlias();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initAliases();
|
initAliases();
|
||||||
whenever(
|
whenever(
|
||||||
() => props.modelValue,
|
() => dialog.value,
|
||||||
() => {
|
() => {
|
||||||
initAliases();
|
initAliases();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function saveAliases() {
|
function saveAliases() {
|
||||||
const seenAliasNames: string[] = [];
|
const seenAliasNames: string[] = [];
|
||||||
const keepAliases: GenericAlias[] = [];
|
const keepAliases: GenericAlias[] = [];
|
||||||
aliases.value.forEach((alias) => {
|
aliases.value.forEach((alias) => {
|
||||||
|
@ -111,17 +102,6 @@ export default defineNuxtComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
aliases.value = keepAliases;
|
aliases.value = keepAliases;
|
||||||
context.emit("submit", keepAliases);
|
emit("submit", keepAliases);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
aliases,
|
|
||||||
createAlias,
|
|
||||||
dialog,
|
|
||||||
deleteAlias,
|
|
||||||
saveAliases,
|
|
||||||
validators,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,12 +3,13 @@
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
show-select
|
show-select
|
||||||
:sort-by="[{ key: 'dateAdded', order: 'desc' }]"
|
:sort-by="sortBy"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:items="recipes"
|
:items="recipes"
|
||||||
:items-per-page="15"
|
:items-per-page="15"
|
||||||
class="elevation-0"
|
class="elevation-0"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
return-object
|
||||||
>
|
>
|
||||||
<template #[`item.name`]="{ item }">
|
<template #[`item.name`]="{ item }">
|
||||||
<a
|
<a
|
||||||
|
@ -61,7 +62,7 @@
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import UserAvatar from "../User/UserAvatar.vue";
|
import UserAvatar from "../User/UserAvatar.vue";
|
||||||
import RecipeChip from "./RecipeChips.vue";
|
import RecipeChip from "./RecipeChips.vue";
|
||||||
import type { Recipe, RecipeCategory, RecipeTool } from "~/lib/api/types/recipe";
|
import type { Recipe, RecipeCategory, RecipeTool } from "~/lib/api/types/recipe";
|
||||||
|
@ -69,8 +70,6 @@ import { useUserApi } from "~/composables/api";
|
||||||
import type { UserSummary } from "~/lib/api/types/user";
|
import type { UserSummary } from "~/lib/api/types/user";
|
||||||
import type { RecipeTag } from "~/lib/api/types/household";
|
import type { RecipeTag } from "~/lib/api/types/household";
|
||||||
|
|
||||||
const INPUT_EVENT = "update:modelValue";
|
|
||||||
|
|
||||||
interface ShowHeaders {
|
interface ShowHeaders {
|
||||||
id: boolean;
|
id: boolean;
|
||||||
owner: boolean;
|
owner: boolean;
|
||||||
|
@ -83,53 +82,43 @@ interface ShowHeaders {
|
||||||
dateAdded: boolean;
|
dateAdded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: { RecipeChip, UserAvatar },
|
loading?: boolean;
|
||||||
props: {
|
recipes?: Recipe[];
|
||||||
modelValue: {
|
showHeaders?: ShowHeaders;
|
||||||
type: Array as PropType<Recipe[]>,
|
}
|
||||||
required: false,
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
default: () => [],
|
loading: false,
|
||||||
},
|
recipes: () => [],
|
||||||
loading: {
|
showHeaders: () => ({
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
recipes: {
|
|
||||||
type: Array as () => Recipe[],
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
showHeaders: {
|
|
||||||
type: Object as () => ShowHeaders,
|
|
||||||
required: false,
|
|
||||||
default: () => {
|
|
||||||
return {
|
|
||||||
id: true,
|
id: true,
|
||||||
owner: false,
|
owner: false,
|
||||||
tags: true,
|
tags: true,
|
||||||
categories: true,
|
categories: true,
|
||||||
|
tools: true,
|
||||||
recipeServings: true,
|
recipeServings: true,
|
||||||
recipeYieldQuantity: true,
|
recipeYieldQuantity: true,
|
||||||
recipeYield: true,
|
recipeYield: true,
|
||||||
dateAdded: true,
|
dateAdded: true,
|
||||||
};
|
}),
|
||||||
},
|
});
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["click"],
|
|
||||||
setup(props, context) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const groupSlug = $auth.user.value?.groupSlug;
|
|
||||||
const router = useRouter();
|
|
||||||
const selected = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: value => context.emit(INPUT_EVENT, value),
|
|
||||||
});
|
|
||||||
|
|
||||||
const headers = computed(() => {
|
defineEmits<{
|
||||||
const hdrs: Array<{ title: string; value: string; align?: string; sortable?: boolean }> = [];
|
click: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selected = defineModel<Recipe[]>({ default: () => [] });
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const $auth = useMealieAuth();
|
||||||
|
const groupSlug = $auth.user.value?.groupSlug;
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Initialize sort state with default sorting by dateAdded descending
|
||||||
|
const sortBy = ref([{ key: "dateAdded", order: "desc" as const }]);
|
||||||
|
|
||||||
|
const headers = computed(() => {
|
||||||
|
const hdrs: Array<{ title: string; value: string; align?: "center" | "start" | "end"; sortable?: boolean }> = [];
|
||||||
|
|
||||||
if (props.showHeaders.id) {
|
if (props.showHeaders.id) {
|
||||||
hdrs.push({ title: i18n.t("general.id"), value: "id" });
|
hdrs.push({ title: i18n.t("general.id"), value: "id" });
|
||||||
|
@ -162,57 +151,45 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
return hdrs;
|
return hdrs;
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatDate(date: string) {
|
function formatDate(date: string) {
|
||||||
try {
|
try {
|
||||||
return i18n.d(Date.parse(date), "medium");
|
return i18n.d(Date.parse(date), "medium");
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Group Members
|
// Group Members
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const members = ref<UserSummary[]>([]);
|
const members = ref<UserSummary[]>([]);
|
||||||
|
|
||||||
async function refreshMembers() {
|
async function refreshMembers() {
|
||||||
const { data } = await api.groups.fetchMembers();
|
const { data } = await api.groups.fetchMembers();
|
||||||
if (data) {
|
if (data) {
|
||||||
members.value = data.items;
|
members.value = data.items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterItems(item: RecipeTag | RecipeCategory | RecipeTool, itemType: string) {
|
function filterItems(item: RecipeTag | RecipeCategory | RecipeTool, itemType: string) {
|
||||||
if (!groupSlug || !item.id) {
|
if (!groupSlug || !item.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push(`/g/${groupSlug}?${itemType}=${item.id}`);
|
router.push(`/g/${groupSlug}?${itemType}=${item.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshMembers();
|
refreshMembers();
|
||||||
});
|
});
|
||||||
|
|
||||||
function getMember(id: string) {
|
function getMember(id: string) {
|
||||||
if (members.value[0]) {
|
if (members.value[0]) {
|
||||||
return members.value.find(m => m.id === id)?.fullName;
|
return members.value.find(m => m.id === id)?.fullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return i18n.t("general.none");
|
return i18n.t("general.none");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
selected,
|
|
||||||
groupSlug,
|
|
||||||
headers,
|
|
||||||
formatDate,
|
|
||||||
members,
|
|
||||||
getMember,
|
|
||||||
filterItems,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
v-if="shoppingListIngredientDialog"
|
v-if="shoppingListIngredientDialog"
|
||||||
v-model="dialog"
|
v-model="dialog"
|
||||||
:title="selectedShoppingList ? selectedShoppingList.name : $t('recipe.add-to-list')"
|
:title="selectedShoppingList?.name || $t('recipe.add-to-list')"
|
||||||
:icon="$globals.icons.cartCheck"
|
:icon="$globals.icons.cartCheck"
|
||||||
width="70%"
|
width="70%"
|
||||||
:submit-text="$t('recipe.add-to-list')"
|
:submit-text="$t('recipe.add-to-list')"
|
||||||
|
@ -130,20 +130,23 @@
|
||||||
.ingredients[i]
|
.ingredients[i]
|
||||||
.checked"
|
.checked"
|
||||||
>
|
>
|
||||||
|
<v-container class="pa-0 ma-0">
|
||||||
|
<v-row no-gutters>
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
hide-details
|
hide-details
|
||||||
:model-value="ingredientData.checked"
|
:model-value="ingredientData.checked"
|
||||||
class="pt-0 my-auto py-auto"
|
class="pt-0 my-auto py-auto mr-2"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
density="compact"
|
density="compact"
|
||||||
/>
|
/>
|
||||||
<div :key="ingredientData.ingredient.quantity">
|
<div :key="`${ingredientData.ingredient.quantity || 'no-qty'}-${i}`" class="pa-auto my-auto">
|
||||||
<RecipeIngredientListItem
|
<RecipeIngredientListItem
|
||||||
:ingredient="ingredientData.ingredient"
|
:ingredient="ingredientData.ingredient"
|
||||||
:disable-amount="ingredientData.disableAmount"
|
|
||||||
:scale="recipeSection.recipeScale"
|
:scale="recipeSection.recipeScale"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -172,7 +175,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { toRefs } from "@vueuse/core";
|
import { toRefs } from "@vueuse/core";
|
||||||
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
|
@ -188,7 +191,6 @@ export interface RecipeWithScale extends Recipe {
|
||||||
export interface ShoppingListIngredient {
|
export interface ShoppingListIngredient {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
ingredient: RecipeIngredient;
|
ingredient: RecipeIngredient;
|
||||||
disableAmount: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShoppingListIngredientSection {
|
export interface ShoppingListIngredientSection {
|
||||||
|
@ -203,61 +205,43 @@ export interface ShoppingListRecipeIngredientSection {
|
||||||
ingredientSections: ShoppingListIngredientSection[];
|
ingredientSections: ShoppingListIngredientSection[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
recipes?: RecipeWithScale[];
|
||||||
RecipeIngredientListItem,
|
shoppingLists?: ShoppingListSummary[];
|
||||||
},
|
}
|
||||||
props: {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
modelValue: {
|
recipes: undefined,
|
||||||
type: Boolean,
|
shoppingLists: () => [],
|
||||||
default: false,
|
});
|
||||||
},
|
|
||||||
recipes: {
|
|
||||||
type: Array as () => RecipeWithScale[],
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
shoppingLists: {
|
|
||||||
type: Array as () => ShoppingListSummary[],
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const api = useUserApi();
|
|
||||||
const preferences = useShoppingListPreferences();
|
|
||||||
const ready = ref(false);
|
|
||||||
|
|
||||||
// v-model support
|
const dialog = defineModel<boolean>({ default: false });
|
||||||
const dialog = computed({
|
|
||||||
get: () => {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:modelValue", val);
|
|
||||||
initState();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = reactive({
|
const i18n = useI18n();
|
||||||
|
const $auth = useMealieAuth();
|
||||||
|
const api = useUserApi();
|
||||||
|
const preferences = useShoppingListPreferences();
|
||||||
|
const ready = ref(false);
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
shoppingListDialog: true,
|
shoppingListDialog: true,
|
||||||
shoppingListIngredientDialog: false,
|
shoppingListIngredientDialog: false,
|
||||||
shoppingListShowAllToggled: false,
|
shoppingListShowAllToggled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const userHousehold = computed(() => {
|
const { shoppingListDialog, shoppingListIngredientDialog, shoppingListShowAllToggled: _shoppingListShowAllToggled } = toRefs(state);
|
||||||
|
|
||||||
|
const userHousehold = computed(() => {
|
||||||
return $auth.user.value?.householdSlug || "";
|
return $auth.user.value?.householdSlug || "";
|
||||||
});
|
});
|
||||||
|
|
||||||
const shoppingListChoices = computed(() => {
|
const shoppingListChoices = computed(() => {
|
||||||
return props.shoppingLists.filter(list => preferences.value.viewAllLists || list.userId === $auth.user.value?.id);
|
return props.shoppingLists.filter(list => preferences.value.viewAllLists || list.userId === $auth.user.value?.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
const recipeIngredientSections = ref<ShoppingListRecipeIngredientSection[]>([]);
|
const recipeIngredientSections = ref<ShoppingListRecipeIngredientSection[]>([]);
|
||||||
const selectedShoppingList = ref<ShoppingListSummary | null>(null);
|
const selectedShoppingList = ref<ShoppingListSummary | null>(null);
|
||||||
|
|
||||||
watchEffect(
|
watchEffect(
|
||||||
() => {
|
() => {
|
||||||
if (shoppingListChoices.value.length === 1 && !state.shoppingListShowAllToggled) {
|
if (shoppingListChoices.value.length === 1 && !state.shoppingListShowAllToggled) {
|
||||||
selectedShoppingList.value = shoppingListChoices.value[0];
|
selectedShoppingList.value = shoppingListChoices.value[0];
|
||||||
|
@ -267,9 +251,15 @@ export default defineNuxtComponent({
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
|
watch(dialog, (val) => {
|
||||||
|
if (!val) {
|
||||||
|
initState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
|
||||||
const recipeSectionMap = new Map<string, ShoppingListRecipeIngredientSection>();
|
const recipeSectionMap = new Map<string, ShoppingListRecipeIngredientSection>();
|
||||||
for (const recipe of recipes) {
|
for (const recipe of recipes) {
|
||||||
if (!recipe.slug) {
|
if (!recipe.slug) {
|
||||||
|
@ -277,7 +267,10 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recipeSectionMap.has(recipe.slug)) {
|
if (recipeSectionMap.has(recipe.slug)) {
|
||||||
recipeSectionMap.get(recipe.slug).recipeScale += recipe.scale;
|
const existingSection = recipeSectionMap.get(recipe.slug);
|
||||||
|
if (existingSection) {
|
||||||
|
existingSection.recipeScale += recipe.scale;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,7 +292,6 @@ export default defineNuxtComponent({
|
||||||
return {
|
return {
|
||||||
checked: !householdsWithFood.includes(userHousehold.value),
|
checked: !householdsWithFood.includes(userHousehold.value),
|
||||||
ingredient: ing,
|
ingredient: ing,
|
||||||
disableAmount: recipe.settings?.disableAmount || false,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -347,19 +339,19 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
recipeIngredientSections.value = Array.from(recipeSectionMap.values());
|
recipeIngredientSections.value = Array.from(recipeSectionMap.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
function initState() {
|
function initState() {
|
||||||
state.shoppingListDialog = true;
|
state.shoppingListDialog = true;
|
||||||
state.shoppingListIngredientDialog = false;
|
state.shoppingListIngredientDialog = false;
|
||||||
state.shoppingListShowAllToggled = false;
|
state.shoppingListShowAllToggled = false;
|
||||||
recipeIngredientSections.value = [];
|
recipeIngredientSections.value = [];
|
||||||
selectedShoppingList.value = null;
|
selectedShoppingList.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
initState();
|
initState();
|
||||||
|
|
||||||
async function openShoppingListIngredientDialog(list: ShoppingListSummary) {
|
async function openShoppingListIngredientDialog(list: ShoppingListSummary) {
|
||||||
if (!props.recipes?.length) {
|
if (!props.recipes?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -368,13 +360,13 @@ export default defineNuxtComponent({
|
||||||
await consolidateRecipesIntoSections(props.recipes);
|
await consolidateRecipesIntoSections(props.recipes);
|
||||||
state.shoppingListDialog = false;
|
state.shoppingListDialog = false;
|
||||||
state.shoppingListIngredientDialog = true;
|
state.shoppingListIngredientDialog = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setShowAllToggled() {
|
function setShowAllToggled() {
|
||||||
state.shoppingListShowAllToggled = true;
|
state.shoppingListShowAllToggled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bulkCheckIngredients(value = true) {
|
function bulkCheckIngredients(value = true) {
|
||||||
recipeIngredientSections.value.forEach((recipeSection) => {
|
recipeIngredientSections.value.forEach((recipeSection) => {
|
||||||
recipeSection.ingredientSections.forEach((ingSection) => {
|
recipeSection.ingredientSections.forEach((ingSection) => {
|
||||||
ingSection.ingredients.forEach((ing) => {
|
ingSection.ingredients.forEach((ing) => {
|
||||||
|
@ -382,9 +374,9 @@ export default defineNuxtComponent({
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addRecipesToList() {
|
async function addRecipesToList() {
|
||||||
if (!selectedShoppingList.value) {
|
if (!selectedShoppingList.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -420,23 +412,7 @@ export default defineNuxtComponent({
|
||||||
state.shoppingListDialog = false;
|
state.shoppingListDialog = false;
|
||||||
state.shoppingListIngredientDialog = false;
|
state.shoppingListIngredientDialog = false;
|
||||||
dialog.value = false;
|
dialog.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
dialog,
|
|
||||||
preferences,
|
|
||||||
ready,
|
|
||||||
shoppingListChoices,
|
|
||||||
...toRefs(state),
|
|
||||||
addRecipesToList,
|
|
||||||
bulkCheckIngredients,
|
|
||||||
openShoppingListIngredientDialog,
|
|
||||||
setShowAllToggled,
|
|
||||||
recipeIngredientSections,
|
|
||||||
selectedShoppingList,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="css">
|
<style scoped lang="css">
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
v-model="dialog"
|
v-model="dialog"
|
||||||
width="800"
|
width="800"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
@click="inputText = inputTextProp"
|
@click="inputText = inputTextProp"
|
||||||
>
|
>
|
||||||
{{ $t("new-recipe.bulk-add") }}
|
{{ $t("new-recipe.bulk-add") }}
|
||||||
|
@ -89,62 +89,61 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
inputTextProp?: string;
|
||||||
inputTextProp: {
|
}
|
||||||
type: String,
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
required: false,
|
inputTextProp: "",
|
||||||
default: "",
|
});
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["bulk-data"],
|
|
||||||
setup(props, context) {
|
|
||||||
const state = reactive({
|
|
||||||
dialog: false,
|
|
||||||
inputText: props.inputTextProp,
|
|
||||||
});
|
|
||||||
|
|
||||||
function splitText() {
|
const emit = defineEmits<{
|
||||||
return state.inputText.split("\n").filter(line => !(line === "\n" || !line));
|
"bulk-data": [data: string[]];
|
||||||
}
|
}>();
|
||||||
|
|
||||||
function removeFirstCharacter() {
|
const dialog = ref(false);
|
||||||
state.inputText = splitText()
|
const inputText = ref(props.inputTextProp);
|
||||||
|
|
||||||
|
function splitText() {
|
||||||
|
return inputText.value.split("\n").filter(line => !(line === "\n" || !line));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFirstCharacter() {
|
||||||
|
inputText.value = splitText()
|
||||||
.map(line => line.substring(1))
|
.map(line => line.substring(1))
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const numberedLineRegex = /\d+[.):] /gm;
|
const numberedLineRegex = /\d+[.):] /gm;
|
||||||
|
|
||||||
function splitByNumberedLine() {
|
function splitByNumberedLine() {
|
||||||
// Split inputText by numberedLineRegex
|
// Split inputText by numberedLineRegex
|
||||||
const matches = state.inputText.match(numberedLineRegex);
|
const matches = inputText.value.match(numberedLineRegex);
|
||||||
|
|
||||||
matches?.forEach((match, idx) => {
|
matches?.forEach((match, idx) => {
|
||||||
const replaceText = idx === 0 ? "" : "\n";
|
const replaceText = idx === 0 ? "" : "\n";
|
||||||
state.inputText = state.inputText.replace(match, replaceText);
|
inputText.value = inputText.value.replace(match, replaceText);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimAllLines() {
|
function trimAllLines() {
|
||||||
const splitLines = splitText();
|
const splitLines = splitText();
|
||||||
|
|
||||||
splitLines.forEach((element: string, index: number) => {
|
splitLines.forEach((element: string, index: number) => {
|
||||||
splitLines[index] = element.trim();
|
splitLines[index] = element.trim();
|
||||||
});
|
});
|
||||||
|
|
||||||
state.inputText = splitLines.join("\n");
|
inputText.value = splitLines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
context.emit("bulk-data", splitText());
|
emit("bulk-data", splitText());
|
||||||
state.dialog = false;
|
dialog.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const utilities = [
|
const utilities = [
|
||||||
{
|
{
|
||||||
id: "trim-whitespace",
|
id: "trim-whitespace",
|
||||||
description: i18n.t("new-recipe.trim-whitespace-description"),
|
description: i18n.t("new-recipe.trim-whitespace-description"),
|
||||||
|
@ -160,17 +159,5 @@ export default defineNuxtComponent({
|
||||||
description: i18n.t("new-recipe.split-by-numbered-line-description"),
|
description: i18n.t("new-recipe.split-by-numbered-line-description"),
|
||||||
action: splitByNumberedLine,
|
action: splitByNumberedLine,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
|
||||||
utilities,
|
|
||||||
splitText,
|
|
||||||
trimAllLines,
|
|
||||||
removeFirstCharacter,
|
|
||||||
splitByNumberedLine,
|
|
||||||
save,
|
|
||||||
...toRefs(state),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="preferences.showDescription"
|
v-model="preferences.showDescription"
|
||||||
hide-details
|
hide-details
|
||||||
|
color="primary"
|
||||||
:label="$t('recipe.description')"
|
:label="$t('recipe.description')"
|
||||||
/>
|
/>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
@ -51,6 +52,7 @@
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="preferences.showNotes"
|
v-model="preferences.showNotes"
|
||||||
hide-details
|
hide-details
|
||||||
|
color="primary"
|
||||||
:label="$t('recipe.notes')"
|
:label="$t('recipe.notes')"
|
||||||
/>
|
/>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
@ -63,6 +65,7 @@
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="preferences.showNutrition"
|
v-model="preferences.showNutrition"
|
||||||
hide-details
|
hide-details
|
||||||
|
color="primary"
|
||||||
:label="$t('recipe.nutrition')"
|
:label="$t('recipe.nutrition')"
|
||||||
/>
|
/>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
@ -83,45 +86,19 @@
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Recipe } from "~/lib/api/types/recipe";
|
import type { Recipe } from "~/lib/api/types/recipe";
|
||||||
import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences";
|
import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences";
|
||||||
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
|
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
|
||||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
recipe?: NoUndefinedField<Recipe>;
|
||||||
RecipePrintView,
|
}
|
||||||
},
|
withDefaults(defineProps<Props>(), {
|
||||||
props: {
|
recipe: undefined,
|
||||||
modelValue: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
recipe: {
|
|
||||||
type: Object as () => NoUndefinedField<Recipe>,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const preferences = useUserPrintPreferences();
|
|
||||||
|
|
||||||
// V-Model Support
|
|
||||||
const dialog = computed({
|
|
||||||
get: () => {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:modelValue", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
dialog,
|
|
||||||
ImagePosition,
|
|
||||||
preferences,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dialog = defineModel<boolean>({ default: false });
|
||||||
|
const preferences = useUserPrintPreferences();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -52,10 +52,6 @@
|
||||||
<div class="mr-auto">
|
<div class="mr-auto">
|
||||||
{{ $t("search.results") }}
|
{{ $t("search.results") }}
|
||||||
</div>
|
</div>
|
||||||
<!-- <router-link
|
|
||||||
:to="advancedSearchUrl"
|
|
||||||
class="text-primary"
|
|
||||||
> {{ $t("search.advanced-search") }} </router-link> -->
|
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
|
||||||
<RecipeCardMobile
|
<RecipeCardMobile
|
||||||
|
@ -76,7 +72,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import type { RecipeSummary } from "~/lib/api/types/recipe";
|
import type { RecipeSummary } from "~/lib/api/types/recipe";
|
||||||
|
@ -85,114 +81,104 @@ import { useRecipeSearch } from "~/composables/recipes/use-recipe-search";
|
||||||
import { usePublicExploreApi } from "~/composables/api/api-client";
|
import { usePublicExploreApi } from "~/composables/api/api-client";
|
||||||
|
|
||||||
const SELECTED_EVENT = "selected";
|
const SELECTED_EVENT = "selected";
|
||||||
export default defineNuxtComponent({
|
|
||||||
components: {
|
|
||||||
RecipeCardMobile,
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(_, context) {
|
// Define emits
|
||||||
const $auth = useMealieAuth();
|
const emit = defineEmits<{
|
||||||
const state = reactive({
|
selected: [recipe: RecipeSummary];
|
||||||
loading: false,
|
}>();
|
||||||
selectedIndex: -1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===========================================================================
|
const $auth = useMealieAuth();
|
||||||
// Dialog State Management
|
const loading = ref(false);
|
||||||
const dialog = ref(false);
|
const selectedIndex = ref(-1);
|
||||||
|
|
||||||
// Reset or Grab Recipes on Change
|
// ===========================================================================
|
||||||
watch(dialog, (val) => {
|
// Dialog State Management
|
||||||
|
const dialog = ref(false);
|
||||||
|
|
||||||
|
// Reset or Grab Recipes on Change
|
||||||
|
watch(dialog, (val) => {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
search.query.value = "";
|
search.query.value = "";
|
||||||
state.selectedIndex = -1;
|
selectedIndex.value = -1;
|
||||||
search.data.value = [];
|
search.data.value = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Event Handlers
|
// Event Handlers
|
||||||
|
|
||||||
function selectRecipe() {
|
function selectRecipe() {
|
||||||
const recipeCards = document.getElementsByClassName("arrow-nav");
|
const recipeCards = document.getElementsByClassName("arrow-nav");
|
||||||
if (recipeCards) {
|
if (recipeCards) {
|
||||||
if (state.selectedIndex < 0) {
|
if (selectedIndex.value < 0) {
|
||||||
state.selectedIndex = -1;
|
selectedIndex.value = -1;
|
||||||
document.getElementById("arrow-search")?.focus();
|
document.getElementById("arrow-search")?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.selectedIndex >= recipeCards.length) {
|
if (selectedIndex.value >= recipeCards.length) {
|
||||||
state.selectedIndex = recipeCards.length - 1;
|
selectedIndex.value = recipeCards.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
(recipeCards[state.selectedIndex] as HTMLElement).focus();
|
(recipeCards[selectedIndex.value] as HTMLElement).focus();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onUpDown(e: KeyboardEvent) {
|
function onUpDown(e: KeyboardEvent) {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
console.log(document.activeElement);
|
console.log(document.activeElement);
|
||||||
// (document.activeElement as HTMLElement).click();
|
// (document.activeElement as HTMLElement).click();
|
||||||
}
|
}
|
||||||
else if (e.key === "ArrowUp") {
|
else if (e.key === "ArrowUp") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
state.selectedIndex--;
|
selectedIndex.value--;
|
||||||
}
|
}
|
||||||
else if (e.key === "ArrowDown") {
|
else if (e.key === "ArrowDown") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
state.selectedIndex++;
|
selectedIndex.value++;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectRecipe();
|
selectRecipe();
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(dialog, (val) => {
|
watch(dialog, (val) => {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
document.removeEventListener("keyup", onUpDown);
|
document.removeEventListener("keyup", onUpDown);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
document.addEventListener("keyup", onUpDown);
|
document.addEventListener("keyup", onUpDown);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const route = useRoute();
|
||||||
const route = useRoute();
|
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||||
const advancedSearchUrl = computed(() => `/g/${groupSlug.value}`);
|
watch(route, close);
|
||||||
watch(route, close);
|
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
dialog.value = true;
|
dialog.value = true;
|
||||||
}
|
}
|
||||||
function close() {
|
function close() {
|
||||||
dialog.value = false;
|
dialog.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Basic Search
|
// Basic Search
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
|
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
|
||||||
const search = useRecipeSearch(api);
|
const search = useRecipeSearch(api);
|
||||||
|
|
||||||
// Select Handler
|
// Select Handler
|
||||||
|
function handleSelect(recipe: RecipeSummary) {
|
||||||
function handleSelect(recipe: RecipeSummary) {
|
|
||||||
close();
|
close();
|
||||||
context.emit(SELECTED_EVENT, recipe);
|
emit(SELECTED_EVENT, recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
// Expose functions to parent components
|
||||||
...toRefs(state),
|
defineExpose({
|
||||||
advancedSearchUrl,
|
|
||||||
dialog,
|
|
||||||
open,
|
open,
|
||||||
close,
|
close,
|
||||||
handleSelect,
|
|
||||||
search,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -14,14 +14,14 @@
|
||||||
max-width="290px"
|
max-width="290px"
|
||||||
min-width="auto"
|
min-width="auto"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="expirationDateString"
|
v-model="expirationDateString"
|
||||||
:label="$t('recipe-share.expiration-date')"
|
:label="$t('recipe-share.expiration-date')"
|
||||||
:hint="$t('recipe-share.default-30-days')"
|
:hint="$t('recipe-share.default-30-days')"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
:prepend-icon="$globals.icons.calendar"
|
:prepend-icon="$globals.icons.calendar"
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -92,113 +92,92 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useClipboard, useShare, whenever } from "@vueuse/core";
|
import { useClipboard, useShare, whenever } from "@vueuse/core";
|
||||||
import type { RecipeShareToken } from "~/lib/api/types/recipe";
|
import type { RecipeShareToken } from "~/lib/api/types/recipe";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { useHouseholdSelf } from "~/composables/use-households";
|
import { useHouseholdSelf } from "~/composables/use-households";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
recipeId: string;
|
||||||
modelValue: {
|
name: string;
|
||||||
type: Boolean,
|
}
|
||||||
default: false,
|
const props = defineProps<Props>();
|
||||||
},
|
|
||||||
recipeId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
// V-Model Support
|
|
||||||
const dialog = computed({
|
|
||||||
get: () => {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:modelValue", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = reactive({
|
const dialog = defineModel<boolean>({ default: false });
|
||||||
datePickerMenu: false,
|
|
||||||
expirationDate: new Date(Date.now() - new Date().getTimezoneOffset() * 60000),
|
|
||||||
tokens: [] as RecipeShareToken[],
|
|
||||||
});
|
|
||||||
|
|
||||||
const expirationDateString = computed(() => {
|
const datePickerMenu = ref(false);
|
||||||
return state.expirationDate.toISOString().substring(0, 10);
|
const expirationDate = ref(new Date(Date.now() - new Date().getTimezoneOffset() * 60000));
|
||||||
});
|
const tokens = ref<RecipeShareToken[]>([]);
|
||||||
|
|
||||||
whenever(
|
const expirationDateString = computed(() => {
|
||||||
() => props.modelValue,
|
return expirationDate.value.toISOString().substring(0, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
whenever(
|
||||||
|
() => dialog.value,
|
||||||
() => {
|
() => {
|
||||||
// Set expiration date to today + 30 Days
|
// Set expiration date to today + 30 Days
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
state.expirationDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
|
expirationDate.value = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||||
refreshTokens();
|
refreshTokens();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const { household } = useHouseholdSelf();
|
const { household } = useHouseholdSelf();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const firstDayOfWeek = computed(() => {
|
const firstDayOfWeek = computed(() => {
|
||||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Token Actions
|
// Token Actions
|
||||||
|
|
||||||
const userApi = useUserApi();
|
const userApi = useUserApi();
|
||||||
|
|
||||||
async function createNewToken() {
|
async function createNewToken() {
|
||||||
// Convert expiration date to timestamp
|
// Convert expiration date to timestamp
|
||||||
const { data } = await userApi.recipes.share.createOne({
|
const { data } = await userApi.recipes.share.createOne({
|
||||||
recipeId: props.recipeId,
|
recipeId: props.recipeId,
|
||||||
expiresAt: state.expirationDate.toISOString(),
|
expiresAt: expirationDate.value.toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
state.tokens.push(data);
|
tokens.value.push(data);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteToken(id: string) {
|
async function deleteToken(id: string) {
|
||||||
await userApi.recipes.share.deleteOne(id);
|
await userApi.recipes.share.deleteOne(id);
|
||||||
state.tokens = state.tokens.filter(token => token.id !== id);
|
tokens.value = tokens.value.filter(token => token.id !== id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshTokens() {
|
async function refreshTokens() {
|
||||||
const { data } = await userApi.recipes.share.getAll(1, -1, { recipe_id: props.recipeId });
|
const { data } = await userApi.recipes.share.getAll(1, -1, { recipe_id: props.recipeId });
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
// @ts-expect-error - TODO: This routes doesn't have pagination, but the type are mismatched.
|
// @ts-expect-error - TODO: This routes doesn't have pagination, but the type are mismatched.
|
||||||
state.tokens = data ?? [];
|
tokens.value = data ?? [];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { share, isSupported: shareIsSupported } = useShare();
|
const { share, isSupported: shareIsSupported } = useShare();
|
||||||
const { copy, copied, isSupported } = useClipboard();
|
const { copy, copied, isSupported } = useClipboard();
|
||||||
|
|
||||||
function getRecipeText() {
|
function getRecipeText() {
|
||||||
return i18n.t("recipe.share-recipe-message", [props.name]);
|
return i18n.t("recipe.share-recipe-message", [props.name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTokenLink(token: string) {
|
function getTokenLink(token: string) {
|
||||||
return `${window.location.origin}/g/${groupSlug.value}/shared/r/${token}`;
|
return `${window.location.origin}/g/${groupSlug.value}/shared/r/${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyTokenLink(token: string) {
|
async function copyTokenLink(token: string) {
|
||||||
if (isSupported.value) {
|
if (isSupported.value) {
|
||||||
await copy(getTokenLink(token));
|
await copy(getTokenLink(token));
|
||||||
if (copied.value) {
|
if (copied.value) {
|
||||||
|
@ -211,9 +190,9 @@ export default defineNuxtComponent({
|
||||||
else {
|
else {
|
||||||
alert.error(i18n.t("general.clipboard-not-supported") as string);
|
alert.error(i18n.t("general.clipboard-not-supported") as string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function shareRecipe(token: string) {
|
async function shareRecipe(token: string) {
|
||||||
if (shareIsSupported) {
|
if (shareIsSupported) {
|
||||||
share({
|
share({
|
||||||
title: props.name,
|
title: props.name,
|
||||||
|
@ -224,18 +203,5 @@ export default defineNuxtComponent({
|
||||||
else {
|
else {
|
||||||
await copyTokenLink(token);
|
await copyTokenLink(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
expirationDateString,
|
|
||||||
dialog,
|
|
||||||
createNewToken,
|
|
||||||
deleteToken,
|
|
||||||
firstDayOfWeek,
|
|
||||||
shareRecipe,
|
|
||||||
copyTokenLink,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
nudge-right="50"
|
nudge-right="50"
|
||||||
:color="buttonStyle ? 'info' : 'secondary'"
|
:color="buttonStyle ? 'info' : 'secondary'"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: tooltipProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="isFavorite || showAlways"
|
v-if="isFavorite || showAlways"
|
||||||
icon
|
icon
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
:color="buttonStyle ? 'info' : 'secondary'"
|
:color="buttonStyle ? 'info' : 'secondary'"
|
||||||
:fab="buttonStyle"
|
:fab="buttonStyle"
|
||||||
v-bind="{ ...props, ...$attrs }"
|
v-bind="{ ...tooltipProps, ...$attrs }"
|
||||||
@click.prevent="toggleFavorite"
|
@click.prevent="toggleFavorite"
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-icon
|
||||||
|
@ -28,36 +28,31 @@
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useUserSelfRatings } from "~/composables/use-users";
|
import { useUserSelfRatings } from "~/composables/use-users";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
recipeId?: string;
|
||||||
recipeId: {
|
showAlways?: boolean;
|
||||||
type: String,
|
buttonStyle?: boolean;
|
||||||
default: "",
|
}
|
||||||
},
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
showAlways: {
|
recipeId: "",
|
||||||
type: Boolean,
|
showAlways: false,
|
||||||
default: false,
|
buttonStyle: false,
|
||||||
},
|
});
|
||||||
buttonStyle: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const api = useUserApi();
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const { userRatings, refreshUserRatings } = useUserSelfRatings();
|
|
||||||
|
|
||||||
const isFavorite = computed(() => {
|
const api = useUserApi();
|
||||||
|
const $auth = useMealieAuth();
|
||||||
|
const { userRatings, refreshUserRatings } = useUserSelfRatings();
|
||||||
|
|
||||||
|
const isFavorite = computed(() => {
|
||||||
const rating = userRatings.value.find(r => r.recipeId === props.recipeId);
|
const rating = userRatings.value.find(r => r.recipeId === props.recipeId);
|
||||||
return rating?.isFavorite || false;
|
return rating?.isFavorite || false;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function toggleFavorite() {
|
async function toggleFavorite() {
|
||||||
if (!$auth.user.value) return;
|
if (!$auth.user.value) return;
|
||||||
if (!isFavorite.value) {
|
if (!isFavorite.value) {
|
||||||
await api.users.addFavorite($auth.user.value?.id, props.recipeId);
|
await api.users.addFavorite($auth.user.value?.id, props.recipeId);
|
||||||
|
@ -66,9 +61,5 @@ export default defineNuxtComponent({
|
||||||
await api.users.removeFavorite($auth.user.value?.id, props.recipeId);
|
await api.users.removeFavorite($auth.user.value?.id, props.recipeId);
|
||||||
}
|
}
|
||||||
await refreshUserRatings();
|
await refreshUserRatings();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isFavorite, toggleFavorite };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,11 +7,11 @@
|
||||||
nudge-top="6"
|
nudge-top="6"
|
||||||
:close-on-content-click="false"
|
:close-on-content-click="false"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
color="accent"
|
color="accent"
|
||||||
dark
|
dark
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
>
|
>
|
||||||
<v-icon start>
|
<v-icon start>
|
||||||
{{ $globals.icons.fileImage }}
|
{{ $globals.icons.fileImage }}
|
||||||
|
@ -61,52 +61,42 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
|
|
||||||
const REFRESH_EVENT = "refresh";
|
const REFRESH_EVENT = "refresh";
|
||||||
const UPLOAD_EVENT = "upload";
|
const UPLOAD_EVENT = "upload";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const props = defineProps<{ slug: string }>();
|
||||||
props: {
|
|
||||||
slug: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props, context) {
|
|
||||||
const state = reactive({
|
|
||||||
url: "",
|
|
||||||
loading: false,
|
|
||||||
menu: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
function uploadImage(fileObject: File) {
|
const emit = defineEmits<{
|
||||||
context.emit(UPLOAD_EVENT, fileObject);
|
refresh: [];
|
||||||
state.menu = false;
|
upload: [fileObject: File];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const url = ref("");
|
||||||
|
const loading = ref(false);
|
||||||
|
const menu = ref(false);
|
||||||
|
|
||||||
|
function uploadImage(fileObject: File) {
|
||||||
|
emit(UPLOAD_EVENT, fileObject);
|
||||||
|
menu.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = useUserApi();
|
||||||
|
async function getImageFromURL() {
|
||||||
|
loading.value = true;
|
||||||
|
if (await api.recipes.updateImagebyURL(props.slug, url.value)) {
|
||||||
|
emit(REFRESH_EVENT);
|
||||||
}
|
}
|
||||||
|
loading.value = false;
|
||||||
|
menu.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
const api = useUserApi();
|
const i18n = useI18n();
|
||||||
async function getImageFromURL() {
|
const messages = computed(() =>
|
||||||
state.loading = true;
|
props.slug ? [""] : [i18n.t("recipe.save-recipe-before-use")],
|
||||||
if (await api.recipes.updateImagebyURL(props.slug, state.url)) {
|
);
|
||||||
context.emit(REFRESH_EVENT);
|
|
||||||
}
|
|
||||||
state.loading = false;
|
|
||||||
state.menu = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
|
||||||
const messages = props.slug ? [""] : [i18n.t("recipe.save-recipe-before-use")];
|
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
uploadImage,
|
|
||||||
getImageFromURL,
|
|
||||||
messages,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
class="d-flex flex-wrap my-1"
|
class="d-flex flex-wrap my-1"
|
||||||
>
|
>
|
||||||
<v-col
|
<v-col
|
||||||
v-if="!disableAmount"
|
|
||||||
sm="12"
|
sm="12"
|
||||||
md="2"
|
md="2"
|
||||||
cols="12"
|
cols="12"
|
||||||
|
@ -42,7 +41,6 @@
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col
|
<v-col
|
||||||
v-if="!disableAmount"
|
|
||||||
sm="12"
|
sm="12"
|
||||||
md="3"
|
md="3"
|
||||||
cols="12"
|
cols="12"
|
||||||
|
@ -63,6 +61,22 @@
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="handleUnitEnter"
|
@keyup.enter="handleUnitEnter"
|
||||||
>
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-tooltip v-if="unitError" location="bottom">
|
||||||
|
<template #activator="{ props: unitTooltipProps }">
|
||||||
|
<v-icon
|
||||||
|
v-bind="unitTooltipProps"
|
||||||
|
class="ml-2 mr-n3 opacity-100"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{{ $globals.icons.alert }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
<span v-if="unitErrorTooltip">
|
||||||
|
{{ unitErrorTooltip }}
|
||||||
|
</span>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
<template #no-data>
|
<template #no-data>
|
||||||
<div class="caption text-center pb-2">
|
<div class="caption text-center pb-2">
|
||||||
{{ $t("recipe.press-enter-to-create") }}
|
{{ $t("recipe.press-enter-to-create") }}
|
||||||
|
@ -82,7 +96,6 @@
|
||||||
|
|
||||||
<!-- Foods Input -->
|
<!-- Foods Input -->
|
||||||
<v-col
|
<v-col
|
||||||
v-if="!disableAmount"
|
|
||||||
m="12"
|
m="12"
|
||||||
md="3"
|
md="3"
|
||||||
cols="12"
|
cols="12"
|
||||||
|
@ -104,6 +117,22 @@
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="handleFoodEnter"
|
@keyup.enter="handleFoodEnter"
|
||||||
>
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-tooltip v-if="foodError" location="bottom">
|
||||||
|
<template #activator="{ props: foodTooltipProps }">
|
||||||
|
<v-icon
|
||||||
|
v-bind="foodTooltipProps"
|
||||||
|
class="ml-2 mr-n3 opacity-100"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{{ $globals.icons.alert }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
<span v-if="foodErrorTooltip">
|
||||||
|
{{ foodErrorTooltip }}
|
||||||
|
</span>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
<template #no-data>
|
<template #no-data>
|
||||||
<div class="caption text-center pb-2">
|
<div class="caption text-center pb-2">
|
||||||
{{ $t("recipe.press-enter-to-create") }}
|
{{ $t("recipe.press-enter-to-create") }}
|
||||||
|
@ -134,16 +163,7 @@
|
||||||
:placeholder="$t('recipe.notes')"
|
:placeholder="$t('recipe.notes')"
|
||||||
class="mb-auto"
|
class="mb-auto"
|
||||||
@click="$emit('clickIngredientField', 'note')"
|
@click="$emit('clickIngredientField', 'note')"
|
||||||
>
|
/>
|
||||||
<template #prepend>
|
|
||||||
<v-icon
|
|
||||||
v-if="disableAmount && $attrs && $attrs.delete"
|
|
||||||
class="mr-n1 handle"
|
|
||||||
>
|
|
||||||
{{ $globals.icons.arrowUpDown }}
|
|
||||||
</v-icon>
|
|
||||||
</template>
|
|
||||||
</v-text-field>
|
|
||||||
<BaseButtonGroup
|
<BaseButtonGroup
|
||||||
hover
|
hover
|
||||||
:large="false"
|
:large="false"
|
||||||
|
@ -153,7 +173,6 @@
|
||||||
@toggle-original="toggleOriginalText"
|
@toggle-original="toggleOriginalText"
|
||||||
@insert-above="$emit('insert-above')"
|
@insert-above="$emit('insert-above')"
|
||||||
@insert-below="$emit('insert-below')"
|
@insert-below="$emit('insert-below')"
|
||||||
@insert-ingredient="$emit('insert-ingredient')"
|
|
||||||
@delete="$emit('delete')"
|
@delete="$emit('delete')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -184,22 +203,29 @@ import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
// defineModel replaces modelValue prop
|
// defineModel replaces modelValue prop
|
||||||
const model = defineModel<RecipeIngredient>({ required: true });
|
const model = defineModel<RecipeIngredient>({ required: true });
|
||||||
|
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
disableAmount: {
|
unitError: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
allowInsertIngredient: {
|
unitErrorTooltip: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
foodError: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
foodErrorTooltip: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits([
|
defineEmits([
|
||||||
"clickIngredientField",
|
"clickIngredientField",
|
||||||
"insert-above",
|
"insert-above",
|
||||||
"insert-below",
|
"insert-below",
|
||||||
"insert-ingredient",
|
|
||||||
"delete",
|
"delete",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -228,13 +254,6 @@ const contextMenuOptions = computed(() => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (props.allowInsertIngredient) {
|
|
||||||
options.push({
|
|
||||||
text: i18n.t("recipe.insert-ingredient"),
|
|
||||||
event: "insert-ingredient",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.value.originalText) {
|
if (model.value.originalText) {
|
||||||
options.push({
|
options.push({
|
||||||
text: i18n.t("recipe.see-original-text"),
|
text: i18n.t("recipe.see-original-text"),
|
||||||
|
|
|
@ -3,21 +3,13 @@
|
||||||
<div v-html="safeMarkup" />
|
<div v-html="safeMarkup" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { sanitizeIngredientHTML } from "~/composables/recipes/use-recipe-ingredients";
|
import { sanitizeIngredientHTML } from "~/composables/recipes/use-recipe-ingredients";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
markup: string;
|
||||||
markup: {
|
}
|
||||||
type: String,
|
const props = defineProps<Props>();
|
||||||
required: true,
|
|
||||||
},
|
const safeMarkup = computed(() => sanitizeIngredientHTML(props.markup));
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const safeMarkup = computed(() => sanitizeIngredientHTML(props.markup));
|
|
||||||
return {
|
|
||||||
safeMarkup,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -28,34 +28,20 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import type { RecipeIngredient } from "~/lib/api/types/household";
|
import type { RecipeIngredient } from "~/lib/api/types/household";
|
||||||
import { useParsedIngredientText } from "~/composables/recipes";
|
import { useParsedIngredientText } from "~/composables/recipes";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
ingredient: RecipeIngredient;
|
||||||
ingredient: {
|
scale?: number;
|
||||||
type: Object as () => RecipeIngredient,
|
}
|
||||||
required: true,
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
},
|
scale: 1,
|
||||||
disableAmount: {
|
});
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
scale: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const parsedIng = computed(() => {
|
|
||||||
return useParsedIngredientText(props.ingredient, props.disableAmount, props.scale);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
const parsedIng = computed(() => {
|
||||||
parsedIng,
|
return useParsedIngredientText(props.ingredient, props.scale);
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,6 @@
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
<RecipeIngredientListItem
|
<RecipeIngredientListItem
|
||||||
:ingredient="ingredient"
|
:ingredient="ingredient"
|
||||||
:disable-amount="disableAmount"
|
|
||||||
:scale="scale"
|
:scale="scale"
|
||||||
/>
|
/>
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
|
@ -53,42 +52,30 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||||
import { parseIngredientText } from "~/composables/recipes";
|
import { parseIngredientText } from "~/composables/recipes";
|
||||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: { RecipeIngredientListItem },
|
value?: RecipeIngredient[];
|
||||||
props: {
|
scale?: number;
|
||||||
value: {
|
isCookMode?: boolean;
|
||||||
type: Array as () => RecipeIngredient[],
|
}
|
||||||
default: () => [],
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
},
|
value: () => [],
|
||||||
disableAmount: {
|
scale: 1,
|
||||||
type: Boolean,
|
isCookMode: false,
|
||||||
default: false,
|
});
|
||||||
},
|
|
||||||
scale: {
|
function validateTitle(title?: string | null) {
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
isCookMode: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
function validateTitle(title?: string) {
|
|
||||||
return !(title === undefined || title === "" || title === null);
|
return !(title === undefined || title === "" || title === null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = reactive({
|
const checked = ref(props.value.map(() => false));
|
||||||
checked: props.value.map(() => false),
|
const showTitleEditor = computed(() => props.value.map(x => validateTitle(x.title)));
|
||||||
showTitleEditor: computed(() => props.value.map(x => validateTitle(x.title))),
|
|
||||||
});
|
|
||||||
|
|
||||||
const ingredientCopyText = computed(() => {
|
const ingredientCopyText = computed(() => {
|
||||||
const components: string[] = [];
|
const components: string[] = [];
|
||||||
props.value.forEach((ingredient) => {
|
props.value.forEach((ingredient) => {
|
||||||
if (ingredient.title) {
|
if (ingredient.title) {
|
||||||
|
@ -99,25 +86,17 @@ export default defineNuxtComponent({
|
||||||
components.push(`[${ingredient.title}]`);
|
components.push(`[${ingredient.title}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
components.push(parseIngredientText(ingredient, props.disableAmount, props.scale, false));
|
components.push(parseIngredientText(ingredient, props.scale, false));
|
||||||
});
|
});
|
||||||
|
|
||||||
return components.join("\n");
|
return components.join("\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
function toggleChecked(index: number) {
|
function toggleChecked(index: number) {
|
||||||
// TODO Find a better way to do this - $set is not available, and
|
// TODO Find a better way to do this - $set is not available, and
|
||||||
// direct array modifications are not propagated for some reason
|
// direct array modifications are not propagated for some reason
|
||||||
state.checked.splice(index, 1, !state.checked[index]);
|
checked.value.splice(index, 1, !checked.value[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
ingredientCopyText,
|
|
||||||
toggleChecked,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<div>
|
<div>
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
v-model="madeThisDialog"
|
v-model="madeThisDialog"
|
||||||
|
:loading="madeThisFormLoading"
|
||||||
:icon="$globals.icons.chefHat"
|
:icon="$globals.icons.chefHat"
|
||||||
:title="$t('recipe.made-this')"
|
:title="$t('recipe.made-this')"
|
||||||
:submit-text="$t('recipe.add-to-timeline')"
|
:submit-text="$t('recipe.add-to-timeline')"
|
||||||
|
@ -29,11 +30,11 @@
|
||||||
offset-y
|
offset-y
|
||||||
max-width="290px"
|
max-width="290px"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="newTimelineEventTimestampString"
|
v-model="newTimelineEventTimestampString"
|
||||||
:prepend-icon="$globals.icons.calendar"
|
:prepend-icon="$globals.icons.calendar"
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -85,13 +86,13 @@
|
||||||
<div>
|
<div>
|
||||||
<div v-if="lastMadeReady" class="d-flex justify-center flex-wrap">
|
<div v-if="lastMadeReady" class="d-flex justify-center flex-wrap">
|
||||||
<v-row no-gutters class="d-flex flex-wrap align-center" style="font-size: larger">
|
<v-row no-gutters class="d-flex flex-wrap align-center" style="font-size: larger">
|
||||||
<v-tooltip bottom>
|
<v-tooltip location="bottom">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: tooltipProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
rounded
|
rounded
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="x-large"
|
size="x-large"
|
||||||
v-bind="props"
|
v-bind="tooltipProps"
|
||||||
style="border-color: rgb(var(--v-theme-primary));"
|
style="border-color: rgb(var(--v-theme-primary));"
|
||||||
@click="madeThisDialog = true"
|
@click="madeThisDialog = true"
|
||||||
>
|
>
|
||||||
|
@ -116,46 +117,43 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { whenever } from "@vueuse/core";
|
import { whenever } from "@vueuse/core";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { alert } from "~/composables/use-toast";
|
||||||
import { useHouseholdSelf } from "~/composables/use-households";
|
import { useHouseholdSelf } from "~/composables/use-households";
|
||||||
import type { Recipe, RecipeTimelineEventIn } from "~/lib/api/types/recipe";
|
import type { Recipe, RecipeTimelineEventIn, RecipeTimelineEventOut } from "~/lib/api/types/recipe";
|
||||||
import type { VForm } from "~/types/auto-forms";
|
import type { VForm } from "~/types/auto-forms";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const props = defineProps<{ recipe: Recipe }>();
|
||||||
props: {
|
const emit = defineEmits<{
|
||||||
recipe: {
|
eventCreated: [event: RecipeTimelineEventOut];
|
||||||
type: Object as () => Recipe,
|
}>();
|
||||||
required: true,
|
|
||||||
},
|
const madeThisDialog = ref(false);
|
||||||
},
|
const userApi = useUserApi();
|
||||||
emits: ["eventCreated"],
|
const { household } = useHouseholdSelf();
|
||||||
setup(props, context) {
|
const i18n = useI18n();
|
||||||
const madeThisDialog = ref(false);
|
const $auth = useMealieAuth();
|
||||||
const userApi = useUserApi();
|
const domMadeThisForm = ref<VForm>();
|
||||||
const { household } = useHouseholdSelf();
|
const newTimelineEvent = ref<RecipeTimelineEventIn>({
|
||||||
const i18n = useI18n();
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const domMadeThisForm = ref<VForm>();
|
|
||||||
const newTimelineEvent = ref<RecipeTimelineEventIn>({
|
|
||||||
subject: "",
|
subject: "",
|
||||||
eventType: "comment",
|
eventType: "comment",
|
||||||
eventMessage: "",
|
eventMessage: "",
|
||||||
timestamp: undefined,
|
timestamp: undefined,
|
||||||
recipeId: props.recipe?.id || "",
|
recipeId: props.recipe?.id || "",
|
||||||
});
|
});
|
||||||
const newTimelineEventImage = ref<Blob | File>();
|
const newTimelineEventImage = ref<Blob | File>();
|
||||||
const newTimelineEventImageName = ref<string>("");
|
const newTimelineEventImageName = ref<string>("");
|
||||||
const newTimelineEventImagePreviewUrl = ref<string>();
|
const newTimelineEventImagePreviewUrl = ref<string>();
|
||||||
const newTimelineEventTimestamp = ref<Date>(new Date());
|
const newTimelineEventTimestamp = ref<Date>(new Date());
|
||||||
const newTimelineEventTimestampString = computed(() => {
|
const newTimelineEventTimestampString = computed(() => {
|
||||||
return newTimelineEventTimestamp.value.toISOString().substring(0, 10);
|
return newTimelineEventTimestamp.value.toISOString().substring(0, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
const lastMade = ref(props.recipe.lastMade);
|
const lastMade = ref(props.recipe.lastMade);
|
||||||
const lastMadeReady = ref(false);
|
const lastMadeReady = ref(false);
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!$auth.user?.value?.householdSlug) {
|
if (!$auth.user?.value?.householdSlug) {
|
||||||
lastMade.value = props.recipe.lastMade;
|
lastMade.value = props.recipe.lastMade;
|
||||||
}
|
}
|
||||||
|
@ -165,43 +163,57 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
lastMadeReady.value = true;
|
lastMadeReady.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
whenever(
|
whenever(
|
||||||
() => madeThisDialog.value,
|
() => madeThisDialog.value,
|
||||||
() => {
|
() => {
|
||||||
// Set timestamp to now
|
// Set timestamp to now
|
||||||
newTimelineEventTimestamp.value = new Date(Date.now() - new Date().getTimezoneOffset() * 60000);
|
newTimelineEventTimestamp.value = new Date(Date.now() - new Date().getTimezoneOffset() * 60000);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const firstDayOfWeek = computed(() => {
|
const firstDayOfWeek = computed(() => {
|
||||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
function clearImage() {
|
function clearImage() {
|
||||||
newTimelineEventImage.value = undefined;
|
newTimelineEventImage.value = undefined;
|
||||||
newTimelineEventImageName.value = "";
|
newTimelineEventImageName.value = "";
|
||||||
newTimelineEventImagePreviewUrl.value = undefined;
|
newTimelineEventImagePreviewUrl.value = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function uploadImage(fileObject: File) {
|
function uploadImage(fileObject: File) {
|
||||||
newTimelineEventImage.value = fileObject;
|
newTimelineEventImage.value = fileObject;
|
||||||
newTimelineEventImageName.value = fileObject.name;
|
newTimelineEventImageName.value = fileObject.name;
|
||||||
newTimelineEventImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
newTimelineEventImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUploadedImage(fileObject: Blob) {
|
function updateUploadedImage(fileObject: Blob) {
|
||||||
newTimelineEventImage.value = fileObject;
|
newTimelineEventImage.value = fileObject;
|
||||||
newTimelineEventImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
newTimelineEventImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = reactive({ datePickerMenu: false });
|
const datePickerMenu = ref(false);
|
||||||
async function createTimelineEvent() {
|
const madeThisFormLoading = ref(false);
|
||||||
|
|
||||||
|
function resetMadeThisForm() {
|
||||||
|
madeThisFormLoading.value = false;
|
||||||
|
|
||||||
|
newTimelineEvent.value.eventMessage = "";
|
||||||
|
newTimelineEvent.value.timestamp = undefined;
|
||||||
|
clearImage();
|
||||||
|
madeThisDialog.value = false;
|
||||||
|
domMadeThisForm.value?.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTimelineEvent() {
|
||||||
if (!(newTimelineEventTimestampString.value && props.recipe?.id && props.recipe?.slug)) {
|
if (!(newTimelineEventTimestampString.value && props.recipe?.id && props.recipe?.slug)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
madeThisFormLoading.value = true;
|
||||||
|
|
||||||
newTimelineEvent.value.recipeId = props.recipe.id;
|
newTimelineEvent.value.recipeId = props.recipe.id;
|
||||||
// Note: $auth.user is now a ref
|
// Note: $auth.user is now a ref
|
||||||
newTimelineEvent.value.subject = i18n.t("recipe.user-made-this", { user: $auth.user.value?.fullName });
|
newTimelineEvent.value.subject = i18n.t("recipe.user-made-this", { user: $auth.user.value?.fullName });
|
||||||
|
@ -210,17 +222,37 @@ export default defineNuxtComponent({
|
||||||
// we choose the end of day so it always comes after "new recipe" events
|
// we choose the end of day so it always comes after "new recipe" events
|
||||||
newTimelineEvent.value.timestamp = new Date(newTimelineEventTimestampString.value + "T23:59:59").toISOString();
|
newTimelineEvent.value.timestamp = new Date(newTimelineEventTimestampString.value + "T23:59:59").toISOString();
|
||||||
|
|
||||||
|
let newEvent: RecipeTimelineEventOut | null = null;
|
||||||
|
try {
|
||||||
const eventResponse = await userApi.recipes.createTimelineEvent(newTimelineEvent.value);
|
const eventResponse = await userApi.recipes.createTimelineEvent(newTimelineEvent.value);
|
||||||
const newEvent = eventResponse.data;
|
newEvent = eventResponse.data;
|
||||||
|
if (!newEvent) {
|
||||||
|
throw new Error("No event created");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Failed to create timeline event:", error);
|
||||||
|
alert.error(i18n.t("recipe.failed-to-add-to-timeline"));
|
||||||
|
resetMadeThisForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// we also update the recipe's last made value
|
// we also update the recipe's last made value
|
||||||
if (!lastMade.value || newTimelineEvent.value.timestamp > lastMade.value) {
|
if (!lastMade.value || newTimelineEvent.value.timestamp > lastMade.value) {
|
||||||
|
try {
|
||||||
lastMade.value = newTimelineEvent.value.timestamp;
|
lastMade.value = newTimelineEvent.value.timestamp;
|
||||||
await userApi.recipes.updateLastMade(props.recipe.slug, newTimelineEvent.value.timestamp);
|
await userApi.recipes.updateLastMade(props.recipe.slug, newTimelineEvent.value.timestamp);
|
||||||
}
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Failed to update last made date:", error);
|
||||||
|
alert.error(i18n.t("recipe.failed-to-update-recipe"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// update the image, if provided
|
// update the image, if provided
|
||||||
if (newTimelineEventImage.value && newEvent) {
|
let imageError = false;
|
||||||
|
if (newTimelineEventImage.value) {
|
||||||
|
try {
|
||||||
const imageResponse = await userApi.recipes.updateTimelineEventImage(
|
const imageResponse = await userApi.recipes.updateTimelineEventImage(
|
||||||
newEvent.id,
|
newEvent.id,
|
||||||
newTimelineEventImage.value,
|
newTimelineEventImage.value,
|
||||||
|
@ -230,34 +262,20 @@ export default defineNuxtComponent({
|
||||||
newEvent.image = imageResponse.data.image;
|
newEvent.image = imageResponse.data.image;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (error) {
|
||||||
// reset form
|
imageError = true;
|
||||||
newTimelineEvent.value.eventMessage = "";
|
console.error("Failed to upload image for timeline event:", error);
|
||||||
newTimelineEvent.value.timestamp = undefined;
|
}
|
||||||
clearImage();
|
|
||||||
madeThisDialog.value = false;
|
|
||||||
domMadeThisForm.value?.reset();
|
|
||||||
|
|
||||||
context.emit("eventCreated", newEvent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if (imageError) {
|
||||||
...toRefs(state),
|
alert.error(i18n.t("recipe.added-to-timeline-but-failed-to-add-image"));
|
||||||
domMadeThisForm,
|
}
|
||||||
madeThisDialog,
|
else {
|
||||||
firstDayOfWeek,
|
alert.success(i18n.t("recipe.added-to-timeline"));
|
||||||
newTimelineEvent,
|
}
|
||||||
newTimelineEventImage,
|
|
||||||
newTimelineEventImagePreviewUrl,
|
resetMadeThisForm();
|
||||||
newTimelineEventTimestamp,
|
emit("eventCreated", newEvent);
|
||||||
newTimelineEventTimestampString,
|
}
|
||||||
lastMade,
|
|
||||||
lastMadeReady,
|
|
||||||
createTimelineEvent,
|
|
||||||
clearImage,
|
|
||||||
uploadImage,
|
|
||||||
updateUploadedImage,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -51,46 +51,34 @@
|
||||||
</v-list>
|
</v-list>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { useFraction } from "~/composables/recipes/use-fraction";
|
import { useFraction } from "~/composables/recipes/use-fraction";
|
||||||
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||||
import type { RecipeSummary } from "~/lib/api/types/recipe";
|
import type { RecipeSummary } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
recipes: RecipeSummary[];
|
||||||
recipes: {
|
listItem?: ShoppingListItemOut;
|
||||||
type: Array as () => RecipeSummary[],
|
small?: boolean;
|
||||||
required: true,
|
tile?: boolean;
|
||||||
},
|
showDescription?: boolean;
|
||||||
listItem: {
|
disabled?: boolean;
|
||||||
type: Object as () => ShoppingListItemOut | undefined,
|
}
|
||||||
default: undefined,
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
},
|
listItem: undefined,
|
||||||
small: {
|
small: false,
|
||||||
type: Boolean,
|
tile: false,
|
||||||
default: false,
|
showDescription: false,
|
||||||
},
|
disabled: false,
|
||||||
tile: {
|
});
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
showDescription: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const { frac } = useFraction();
|
|
||||||
const route = useRoute();
|
|
||||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user?.value?.groupSlug || "");
|
|
||||||
|
|
||||||
const attrs = computed(() => {
|
const $auth = useMealieAuth();
|
||||||
|
const { frac } = useFraction();
|
||||||
|
const route = useRoute();
|
||||||
|
const groupSlug = computed(() => route.params.groupSlug || $auth.user?.value?.groupSlug || "");
|
||||||
|
|
||||||
|
const attrs = computed(() => {
|
||||||
return props.small
|
return props.small
|
||||||
? {
|
? {
|
||||||
class: {
|
class: {
|
||||||
|
@ -122,16 +110,16 @@ export default defineNuxtComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function sanitizeHTML(rawHtml: string) {
|
function sanitizeHTML(rawHtml: string) {
|
||||||
return DOMPurify.sanitize(rawHtml, {
|
return DOMPurify.sanitize(rawHtml, {
|
||||||
USE_PROFILES: { html: true },
|
USE_PROFILES: { html: true },
|
||||||
ALLOWED_TAGS: ["strong", "sup"],
|
ALLOWED_TAGS: ["strong", "sup"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const listItemDescriptions = computed<string[]>(() => {
|
const listItemDescriptions = computed<string[]>(() => {
|
||||||
if (
|
if (
|
||||||
props.recipes.length === 1 // we don't need to specify details if there's only one recipe ref
|
props.recipes.length === 1 // we don't need to specify details if there's only one recipe ref
|
||||||
|| !props.listItem?.recipeReferences
|
|| !props.listItem?.recipeReferences
|
||||||
|
@ -179,13 +167,5 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
return listItemDescriptions;
|
return listItemDescriptions;
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
attrs,
|
|
||||||
groupSlug,
|
|
||||||
listItemDescriptions,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -45,62 +45,48 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useNutritionLabels } from "~/composables/recipes";
|
import { useNutritionLabels } from "~/composables/recipes";
|
||||||
import type { Nutrition } from "~/lib/api/types/recipe";
|
import type { Nutrition } from "~/lib/api/types/recipe";
|
||||||
import type { NutritionLabelType } from "~/composables/recipes/use-recipe-nutrition";
|
import type { NutritionLabelType } from "~/composables/recipes/use-recipe-nutrition";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
edit?: boolean;
|
||||||
modelValue: {
|
}
|
||||||
type: Object as () => Nutrition,
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
required: true,
|
edit: true,
|
||||||
},
|
});
|
||||||
edit: {
|
|
||||||
type: Boolean,
|
const modelValue = defineModel<Nutrition>({ required: true });
|
||||||
default: true,
|
|
||||||
},
|
const { labels } = useNutritionLabels();
|
||||||
},
|
const valueNotNull = computed(() => {
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const { labels } = useNutritionLabels();
|
|
||||||
const valueNotNull = computed(() => {
|
|
||||||
let key: keyof Nutrition;
|
let key: keyof Nutrition;
|
||||||
for (key in props.modelValue) {
|
for (key in modelValue.value) {
|
||||||
if (props.modelValue[key] !== null) {
|
if (modelValue.value[key] !== null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const showViewer = computed(() => !props.edit && valueNotNull.value);
|
const showViewer = computed(() => !props.edit && valueNotNull.value);
|
||||||
|
|
||||||
function updateValue(key: number | string, event: Event) {
|
function updateValue(key: number | string, event: Event) {
|
||||||
context.emit("update:modelValue", { ...props.modelValue, [key]: event });
|
modelValue.value = { ...modelValue.value, [key]: event };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a new list that only contains nutritional information that has a value
|
// Build a new list that only contains nutritional information that has a value
|
||||||
const renderedList = computed(() => {
|
const renderedList = computed(() => {
|
||||||
return Object.entries(labels).reduce((item: NutritionLabelType, [key, label]) => {
|
return Object.entries(labels).reduce((item: NutritionLabelType, [key, label]) => {
|
||||||
if (props.modelValue[key]?.trim()) {
|
if (modelValue.value[key]?.trim()) {
|
||||||
item[key] = {
|
item[key] = {
|
||||||
...label,
|
...label,
|
||||||
value: props.modelValue[key],
|
value: modelValue.value[key],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
}, {});
|
}, {});
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels,
|
|
||||||
valueNotNull,
|
|
||||||
showViewer,
|
|
||||||
updateValue,
|
|
||||||
renderedList,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -60,60 +60,45 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { useCategoryStore, useTagStore, useToolStore } from "~/composables/store";
|
import { useCategoryStore, useTagStore, useToolStore } from "~/composables/store";
|
||||||
import { type RecipeOrganizer, Organizer } from "~/lib/api/types/non-generated";
|
import { type RecipeOrganizer, Organizer } from "~/lib/api/types/non-generated";
|
||||||
|
|
||||||
const CREATED_ITEM_EVENT = "created-item";
|
const CREATED_ITEM_EVENT = "created-item";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
color?: string | null;
|
||||||
modelValue: {
|
tagDialog?: boolean;
|
||||||
type: Boolean,
|
itemType?: RecipeOrganizer;
|
||||||
default: false,
|
}
|
||||||
},
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
color: {
|
color: null,
|
||||||
type: String,
|
tagDialog: true,
|
||||||
default: null,
|
itemType: "category" as RecipeOrganizer,
|
||||||
},
|
});
|
||||||
tagDialog: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
itemType: {
|
|
||||||
type: String as () => RecipeOrganizer,
|
|
||||||
default: "category",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
const state = reactive({
|
const emit = defineEmits<{
|
||||||
name: "",
|
"created-item": [item: any];
|
||||||
onHand: false,
|
}>();
|
||||||
});
|
|
||||||
|
|
||||||
const dialog = computed({
|
const dialog = defineModel<boolean>({ default: false });
|
||||||
get() {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
context.emit("update:modelValue", value);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
const i18n = useI18n();
|
||||||
() => props.modelValue,
|
|
||||||
|
const name = ref("");
|
||||||
|
const onHand = ref(false);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
dialog,
|
||||||
(val: boolean) => {
|
(val: boolean) => {
|
||||||
if (!val) state.name = "";
|
if (!val) name.value = "";
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const userApi = useUserApi();
|
const userApi = useUserApi();
|
||||||
|
|
||||||
const store = (() => {
|
const store = (() => {
|
||||||
switch (props.itemType) {
|
switch (props.itemType) {
|
||||||
case Organizer.Tag:
|
case Organizer.Tag:
|
||||||
return useTagStore();
|
return useTagStore();
|
||||||
|
@ -122,9 +107,9 @@ export default defineNuxtComponent({
|
||||||
default:
|
default:
|
||||||
return useCategoryStore();
|
return useCategoryStore();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const properties = computed(() => {
|
const properties = computed(() => {
|
||||||
switch (props.itemType) {
|
switch (props.itemType) {
|
||||||
case Organizer.Tag:
|
case Organizer.Tag:
|
||||||
return {
|
return {
|
||||||
|
@ -145,34 +130,23 @@ export default defineNuxtComponent({
|
||||||
api: userApi.categories,
|
api: userApi.categories,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
required: (val: string) => !!val || (i18n.t("general.a-name-is-required") as string),
|
required: (val: string) => !!val || (i18n.t("general.a-name-is-required") as string),
|
||||||
};
|
};
|
||||||
|
|
||||||
async function select() {
|
async function select() {
|
||||||
if (store) {
|
if (store) {
|
||||||
// @ts-expect-error the same state is used for different organizer types, which have different requirements
|
// @ts-expect-error the same state is used for different organizer types, which have different requirements
|
||||||
await store.actions.createOne({ ...state });
|
await store.actions.createOne({ name: name.value, onHand: onHand.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
const newItem = store.store.value.find(item => item.name === state.name);
|
const newItem = store.store.value.find(item => item.name === name.value);
|
||||||
|
|
||||||
context.emit(CREATED_ITEM_EVENT, newItem);
|
emit(CREATED_ITEM_EVENT, newItem);
|
||||||
dialog.value = false;
|
dialog.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
Organizer,
|
|
||||||
...toRefs(state),
|
|
||||||
dialog,
|
|
||||||
properties,
|
|
||||||
rules,
|
|
||||||
select,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
|
|
@ -122,9 +122,8 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
|
|
||||||
import { useContextPresets } from "~/composables/use-context-presents";
|
import { useContextPresets } from "~/composables/use-context-presents";
|
||||||
import RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue";
|
import RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue";
|
||||||
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
||||||
|
@ -138,27 +137,18 @@ interface GenericItem {
|
||||||
onHand: boolean;
|
onHand: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const props = defineProps<{
|
||||||
components: {
|
items: GenericItem[];
|
||||||
RecipeOrganizerDialog,
|
icon: string;
|
||||||
},
|
itemType: RecipeOrganizer;
|
||||||
props: {
|
}>();
|
||||||
items: {
|
|
||||||
type: Array as () => GenericItem[],
|
const emit = defineEmits<{
|
||||||
required: true,
|
update: [item: GenericItem];
|
||||||
},
|
delete: [id: string];
|
||||||
icon: {
|
}>();
|
||||||
type: String,
|
|
||||||
required: true,
|
const state = reactive({
|
||||||
},
|
|
||||||
itemType: {
|
|
||||||
type: String as () => RecipeOrganizer,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update", "delete"],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const state = reactive({
|
|
||||||
// Search Options
|
// Search Options
|
||||||
options: {
|
options: {
|
||||||
ignoreLocation: true,
|
ignoreLocation: true,
|
||||||
|
@ -171,24 +161,24 @@ export default defineNuxtComponent({
|
||||||
minMatchCharLength: 1,
|
minMatchCharLength: 1,
|
||||||
keys: ["name"],
|
keys: ["name"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user?.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user?.value?.groupSlug || "");
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// Context Menu
|
// Context Menu
|
||||||
|
|
||||||
const dialogs = ref({
|
const dialogs = ref({
|
||||||
organizer: false,
|
organizer: false,
|
||||||
update: false,
|
update: false,
|
||||||
delete: false,
|
delete: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const presets = useContextPresets();
|
const presets = useContextPresets();
|
||||||
|
|
||||||
const translationKey = computed<string>(() => {
|
const translationKey = computed<string>(() => {
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
categories: "category.category",
|
categories: "category.category",
|
||||||
tags: "tag.tag",
|
tags: "tag.tag",
|
||||||
|
@ -197,58 +187,58 @@ export default defineNuxtComponent({
|
||||||
households: "household.household",
|
households: "household.household",
|
||||||
};
|
};
|
||||||
return typeMap[props.itemType] || "";
|
return typeMap[props.itemType] || "";
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteTarget = ref<GenericItem | null>(null);
|
const deleteTarget = ref<GenericItem | null>(null);
|
||||||
const updateTarget = ref<GenericItem | null>(null);
|
const updateTarget = ref<GenericItem | null>(null);
|
||||||
|
|
||||||
function confirmDelete(item: GenericItem) {
|
function confirmDelete(item: GenericItem) {
|
||||||
deleteTarget.value = item;
|
deleteTarget.value = item;
|
||||||
dialogs.value.delete = true;
|
dialogs.value.delete = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteOne() {
|
function deleteOne() {
|
||||||
if (!deleteTarget.value) {
|
if (!deleteTarget.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("delete", deleteTarget.value.id);
|
emit("delete", deleteTarget.value.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openUpdateDialog(item: GenericItem) {
|
function openUpdateDialog(item: GenericItem) {
|
||||||
updateTarget.value = deepCopy(item);
|
updateTarget.value = deepCopy(item);
|
||||||
dialogs.value.update = true;
|
dialogs.value.update = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOne() {
|
function updateOne() {
|
||||||
if (!updateTarget.value) {
|
if (!updateTarget.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit("update", updateTarget.value);
|
emit("update", updateTarget.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Search Functions
|
// Search Functions
|
||||||
|
|
||||||
const searchString = useRouteQuery("q", "");
|
const searchString = useRouteQuery("q", "");
|
||||||
|
|
||||||
const fuse = computed(() => {
|
const fuse = computed(() => {
|
||||||
return new Fuse(props.items, state.options);
|
return new Fuse(props.items, state.options);
|
||||||
});
|
});
|
||||||
|
|
||||||
const fuzzyItems = computed<GenericItem[]>(() => {
|
const fuzzyItems = computed<GenericItem[]>(() => {
|
||||||
if (searchString.value.trim() === "") {
|
if (searchString.value.trim() === "") {
|
||||||
return props.items;
|
return props.items;
|
||||||
}
|
}
|
||||||
const result = fuse.value.search(searchString.value.trim() as string);
|
const result = fuse.value.search(searchString.value.trim() as string);
|
||||||
return result.map(x => x.item);
|
return result.map(x => x.item);
|
||||||
});
|
});
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
// Sorted Items
|
// Sorted Items
|
||||||
|
|
||||||
const itemsSorted = computed(() => {
|
const itemsSorted = computed(() => {
|
||||||
const byLetter: { [key: string]: Array<GenericItem> } = {};
|
const byLetter: { [key: string]: Array<GenericItem> } = {};
|
||||||
|
|
||||||
if (!fuzzyItems.value) {
|
if (!fuzzyItems.value) {
|
||||||
|
@ -266,28 +256,9 @@ export default defineNuxtComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
return byLetter;
|
return byLetter;
|
||||||
});
|
|
||||||
|
|
||||||
function isTitle(str: number | string) {
|
|
||||||
return typeof str === "string" && str.length === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
groupSlug,
|
|
||||||
isTitle,
|
|
||||||
dialogs,
|
|
||||||
confirmDelete,
|
|
||||||
openUpdateDialog,
|
|
||||||
updateOne,
|
|
||||||
updateTarget,
|
|
||||||
deleteOne,
|
|
||||||
deleteTarget,
|
|
||||||
Organizer,
|
|
||||||
presets,
|
|
||||||
itemsSorted,
|
|
||||||
searchString,
|
|
||||||
translationKey,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function isTitle(str: number | string) {
|
||||||
|
return typeof str === "string" && str.length === 1;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
v-bind="inputAttrs"
|
v-bind="inputAttrs"
|
||||||
v-model:search="searchInput"
|
v-model:search="searchInput"
|
||||||
:items="storeItem"
|
:items="items"
|
||||||
:label="label"
|
:label="label"
|
||||||
chips
|
chips
|
||||||
closable-chips
|
closable-chips
|
||||||
|
@ -46,78 +46,51 @@
|
||||||
</v-autocomplete>
|
</v-autocomplete>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import type { IngredientFood, RecipeCategory, RecipeTag } from "~/lib/api/types/recipe";
|
import type { IngredientFood, RecipeCategory, RecipeTag } from "~/lib/api/types/recipe";
|
||||||
import type { RecipeTool } from "~/lib/api/types/admin";
|
import type { RecipeTool } from "~/lib/api/types/admin";
|
||||||
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
||||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
selectorType: RecipeOrganizer;
|
||||||
modelValue: {
|
inputAttrs?: Record<string, any>;
|
||||||
type: Array as () => (
|
returnObject?: boolean;
|
||||||
|
showAdd?: boolean;
|
||||||
|
showLabel?: boolean;
|
||||||
|
showIcon?: boolean;
|
||||||
|
variant?: "filled" | "underlined" | "outlined" | "plain" | "solo" | "solo-inverted" | "solo-filled";
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
inputAttrs: () => ({}),
|
||||||
|
returnObject: true,
|
||||||
|
showAdd: true,
|
||||||
|
showLabel: true,
|
||||||
|
showIcon: true,
|
||||||
|
variant: "outlined",
|
||||||
|
});
|
||||||
|
|
||||||
|
const selected = defineModel<(
|
||||||
| HouseholdSummary
|
| HouseholdSummary
|
||||||
| RecipeTag
|
| RecipeTag
|
||||||
| RecipeCategory
|
| RecipeCategory
|
||||||
| RecipeTool
|
| RecipeTool
|
||||||
| IngredientFood
|
| IngredientFood
|
||||||
| string
|
| string
|
||||||
)[] | undefined,
|
)[] | undefined>({ required: true });
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* The type of organizer to use.
|
|
||||||
*/
|
|
||||||
selectorType: {
|
|
||||||
type: String as () => RecipeOrganizer,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
inputAttrs: {
|
|
||||||
type: Object as () => Record<string, any>,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
returnObject: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showAdd: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showLabel: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showIcon: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
variant: {
|
|
||||||
type: String as () => "filled" | "underlined" | "outlined" | "plain" | "solo" | "solo-inverted" | "solo-filled",
|
|
||||||
default: "outlined",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
|
|
||||||
setup(props, context) {
|
onMounted(() => {
|
||||||
const selected = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:modelValue", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (selected.value === undefined) {
|
if (selected.value === undefined) {
|
||||||
selected.value = [];
|
selected.value = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
|
|
||||||
const label = computed(() => {
|
const label = computed(() => {
|
||||||
if (!props.showLabel) {
|
if (!props.showLabel) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -136,9 +109,9 @@ export default defineNuxtComponent({
|
||||||
default:
|
default:
|
||||||
return i18n.t("general.organizer");
|
return i18n.t("general.organizer");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const icon = computed(() => {
|
const icon = computed(() => {
|
||||||
if (!props.showIcon) {
|
if (!props.showIcon) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -157,69 +130,54 @@ export default defineNuxtComponent({
|
||||||
default:
|
default:
|
||||||
return $globals.icons.tags;
|
return $globals.icons.tags;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Store & Items Setup
|
// Store & Items Setup
|
||||||
|
|
||||||
const storeMap = {
|
const storeMap = {
|
||||||
[Organizer.Category]: useCategoryStore(),
|
[Organizer.Category]: useCategoryStore(),
|
||||||
[Organizer.Tag]: useTagStore(),
|
[Organizer.Tag]: useTagStore(),
|
||||||
[Organizer.Tool]: useToolStore(),
|
[Organizer.Tool]: useToolStore(),
|
||||||
[Organizer.Food]: useFoodStore(),
|
[Organizer.Food]: useFoodStore(),
|
||||||
[Organizer.Household]: useHouseholdStore(),
|
[Organizer.Household]: useHouseholdStore(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = computed(() => {
|
const store = computed(() => {
|
||||||
const { store } = storeMap[props.selectorType];
|
const { store } = storeMap[props.selectorType];
|
||||||
return store.value;
|
return store.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = computed(() => {
|
const items = computed(() => {
|
||||||
if (!props.returnObject) {
|
if (!props.returnObject) {
|
||||||
return store.value.map(item => item.name);
|
return store.value.map(item => item.name);
|
||||||
}
|
}
|
||||||
return store.value;
|
return store.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
function removeByIndex(index: number) {
|
function removeByIndex(index: number) {
|
||||||
if (selected.value === undefined) {
|
if (selected.value === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
const newSelected = selected.value.filter((_, i) => i !== index);
|
||||||
selected.value = [...newSelected];
|
selected.value = [...newSelected];
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendCreated(item: any) {
|
function appendCreated(item: any) {
|
||||||
if (selected.value === undefined) {
|
if (selected.value === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
selected.value = [...selected.value, item];
|
selected.value = [...selected.value, item];
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialog = ref(false);
|
const dialog = ref(false);
|
||||||
|
|
||||||
const searchInput = ref("");
|
const searchInput = ref("");
|
||||||
|
|
||||||
function resetSearchInput() {
|
function resetSearchInput() {
|
||||||
searchInput.value = "";
|
searchInput.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
Organizer,
|
|
||||||
appendCreated,
|
|
||||||
dialog,
|
|
||||||
storeItem: items,
|
|
||||||
label,
|
|
||||||
icon,
|
|
||||||
selected,
|
|
||||||
removeByIndex,
|
|
||||||
searchInput,
|
|
||||||
resetSearchInput,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<RecipePageIngredientEditor v-if="isEditForm" v-model="recipe" />
|
<RecipePageIngredientEditor v-if="isEditForm" v-model="recipe" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<RecipePageScale v-model:scale="scale" :recipe="recipe" />
|
<RecipePageScale v-model="scale" :recipe="recipe" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
<WakelockSwitch />
|
<WakelockSwitch />
|
||||||
<RecipePageComments
|
<RecipePageComments
|
||||||
v-if="!recipe.settings.disableComments && !isEditForm && !isCookMode"
|
v-if="!recipe.settings?.disableComments && !isEditForm && !isCookMode"
|
||||||
v-model="recipe"
|
v-model="recipe"
|
||||||
class="px-1 my-4 d-print-none"
|
class="px-1 my-4 d-print-none"
|
||||||
/>
|
/>
|
||||||
|
@ -96,7 +96,7 @@
|
||||||
<v-row style="height: 100%" no-gutters class="overflow-hidden">
|
<v-row style="height: 100%" no-gutters class="overflow-hidden">
|
||||||
<v-col cols="12" sm="5" class="overflow-y-auto pl-4 pr-3 py-2" style="height: 100%">
|
<v-col cols="12" sm="5" class="overflow-y-auto pl-4 pr-3 py-2" style="height: 100%">
|
||||||
<div class="d-flex align-center">
|
<div class="d-flex align-center">
|
||||||
<RecipePageScale v-model:scale="scale" :recipe="recipe" />
|
<RecipePageScale v-model="scale" :recipe="recipe" />
|
||||||
</div>
|
</div>
|
||||||
<RecipePageIngredientToolsView
|
<RecipePageIngredientToolsView
|
||||||
v-if="!isEditForm"
|
v-if="!isEditForm"
|
||||||
|
@ -124,7 +124,7 @@
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
<v-sheet v-show="isCookMode && hasLinkedIngredients">
|
<v-sheet v-show="isCookMode && hasLinkedIngredients">
|
||||||
<div class="mt-2 px-2 px-md-4">
|
<div class="mt-2 px-2 px-md-4">
|
||||||
<RecipePageScale v-model:scale="scale" :recipe="recipe" />
|
<RecipePageScale v-model="scale" :recipe="recipe" />
|
||||||
</div>
|
</div>
|
||||||
<RecipePageInstructions
|
<RecipePageInstructions
|
||||||
v-model="recipe.recipeInstructions"
|
v-model="recipe.recipeInstructions"
|
||||||
|
@ -141,7 +141,6 @@
|
||||||
<RecipeIngredients
|
<RecipeIngredients
|
||||||
:value="notLinkedIngredients"
|
:value="notLinkedIngredients"
|
||||||
:scale="scale"
|
:scale="scale"
|
||||||
:disable-amount="recipe.settings.disableAmount"
|
|
||||||
:is-cook-mode="isCookMode"
|
:is-cook-mode="isCookMode"
|
||||||
/>
|
/>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
@ -278,7 +277,7 @@ async function deleteRecipe() {
|
||||||
* View Preferences
|
* View Preferences
|
||||||
*/
|
*/
|
||||||
const landscape = computed(() => {
|
const landscape = computed(() => {
|
||||||
const preferLandscape = recipe.value.settings.landscapeView;
|
const preferLandscape = recipe.value.settings?.landscapeView;
|
||||||
const smallScreen = !$vuetify.display.smAndUp.value;
|
const smallScreen = !$vuetify.display.smAndUp.value;
|
||||||
|
|
||||||
if (preferLandscape) {
|
if (preferLandscape) {
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import { useRecipePermissions } from "~/composables/recipes";
|
import { useRecipePermissions } from "~/composables/recipes";
|
||||||
import RecipePageInfoCard from "~/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInfoCard.vue";
|
import RecipePageInfoCard from "~/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInfoCard.vue";
|
||||||
|
@ -35,82 +35,48 @@ import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||||
import type { Recipe } from "~/lib/api/types/recipe";
|
import type { Recipe } from "~/lib/api/types/recipe";
|
||||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
import { usePageState, usePageUser, PageMode, EditorMode } from "~/composables/recipe-page/shared-state";
|
import { usePageState, usePageUser, PageMode } from "~/composables/recipe-page/shared-state";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
recipe: NoUndefinedField<Recipe>;
|
||||||
RecipePageInfoCard,
|
recipeScale?: number;
|
||||||
RecipeActionMenu,
|
landscape?: boolean;
|
||||||
},
|
}
|
||||||
props: {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
recipe: {
|
recipeScale: 1,
|
||||||
type: Object as () => NoUndefinedField<Recipe>,
|
landscape: false,
|
||||||
required: true,
|
});
|
||||||
},
|
|
||||||
recipeScale: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
landscape: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["save", "delete"],
|
|
||||||
setup(props) {
|
|
||||||
const { $vuetify } = useNuxtApp();
|
|
||||||
const { recipeImage } = useStaticRoutes();
|
|
||||||
const { imageKey, pageMode, editMode, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
|
|
||||||
const { user } = usePageUser();
|
|
||||||
const { isOwnGroup } = useLoggedInState();
|
|
||||||
|
|
||||||
const recipeHousehold = ref<HouseholdSummary>();
|
defineEmits(["save", "delete"]);
|
||||||
if (user) {
|
|
||||||
|
const { recipeImage } = useStaticRoutes();
|
||||||
|
const { imageKey, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
|
||||||
|
const { user } = usePageUser();
|
||||||
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
|
const recipeHousehold = ref<HouseholdSummary>();
|
||||||
|
if (user) {
|
||||||
const userApi = useUserApi();
|
const userApi = useUserApi();
|
||||||
userApi.households.getOne(props.recipe.householdId).then(({ data }) => {
|
userApi.households.getOne(props.recipe.householdId).then(({ data }) => {
|
||||||
recipeHousehold.value = data || undefined;
|
recipeHousehold.value = data || undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const { canEditRecipe } = useRecipePermissions(props.recipe, recipeHousehold, user);
|
const { canEditRecipe } = useRecipePermissions(props.recipe, recipeHousehold, user);
|
||||||
|
|
||||||
function printRecipe() {
|
function printRecipe() {
|
||||||
window.print();
|
window.print();
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideImage = ref(false);
|
const hideImage = ref(false);
|
||||||
const imageHeight = computed(() => {
|
|
||||||
return $vuetify.display.xs.value ? "200" : "400";
|
|
||||||
});
|
|
||||||
|
|
||||||
const recipeImageUrl = computed(() => {
|
const recipeImageUrl = computed(() => {
|
||||||
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
|
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => recipeImageUrl.value,
|
() => recipeImageUrl.value,
|
||||||
() => {
|
() => {
|
||||||
hideImage.value = false;
|
hideImage.value = false;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
|
||||||
isOwnGroup,
|
|
||||||
setMode,
|
|
||||||
toggleEditMode,
|
|
||||||
recipeImage,
|
|
||||||
canEditRecipe,
|
|
||||||
imageKey,
|
|
||||||
user,
|
|
||||||
PageMode,
|
|
||||||
pageMode,
|
|
||||||
EditorMode,
|
|
||||||
editMode,
|
|
||||||
printRecipe,
|
|
||||||
imageHeight,
|
|
||||||
hideImage,
|
|
||||||
isEditMode,
|
|
||||||
recipeImageUrl,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
>
|
>
|
||||||
<RecipeYield
|
<RecipeYield
|
||||||
:yield-quantity="recipe.recipeYieldQuantity"
|
:yield-quantity="recipe.recipeYieldQuantity"
|
||||||
:yield="recipe.recipeYield"
|
:yield-text="recipe.recipeYield"
|
||||||
:scale="recipeScale"
|
:scale="recipeScale"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
|
@ -76,7 +76,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
|
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
|
||||||
import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue";
|
import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue";
|
||||||
|
@ -86,34 +86,15 @@ import RecipePageInfoCardImage from "~/components/Domain/Recipe/RecipePage/Recip
|
||||||
import type { Recipe } from "~/lib/api/types/recipe";
|
import type { Recipe } from "~/lib/api/types/recipe";
|
||||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
recipe: NoUndefinedField<Recipe>;
|
||||||
RecipeRating,
|
recipeScale?: number;
|
||||||
RecipeLastMade,
|
landscape: boolean;
|
||||||
RecipeTimeCard,
|
}
|
||||||
RecipeYield,
|
|
||||||
RecipePageInfoCardImage,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
recipe: {
|
|
||||||
type: Object as () => NoUndefinedField<Recipe>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
recipeScale: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
landscape: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup() {
|
|
||||||
const { isOwnGroup } = useLoggedInState();
|
|
||||||
|
|
||||||
return {
|
withDefaults(defineProps<Props>(), {
|
||||||
isOwnGroup,
|
recipeScale: 1,
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { isOwnGroup } = useLoggedInState();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -12,60 +12,47 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||||
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
||||||
import type { Recipe } from "~/lib/api/types/recipe";
|
import type { Recipe } from "~/lib/api/types/recipe";
|
||||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
recipe: NoUndefinedField<Recipe>;
|
||||||
recipe: {
|
maxWidth?: string;
|
||||||
type: Object as () => NoUndefinedField<Recipe>,
|
}
|
||||||
required: true,
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
},
|
maxWidth: undefined,
|
||||||
maxWidth: {
|
});
|
||||||
type: String,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { $vuetify } = useNuxtApp();
|
|
||||||
const { recipeImage } = useStaticRoutes();
|
|
||||||
const { imageKey } = usePageState(props.recipe.slug);
|
|
||||||
const { user } = usePageUser();
|
|
||||||
|
|
||||||
const recipeHousehold = ref<HouseholdSummary>();
|
const { $vuetify } = useNuxtApp();
|
||||||
if (user) {
|
const { recipeImage } = useStaticRoutes();
|
||||||
|
const { imageKey } = usePageState(props.recipe.slug);
|
||||||
|
const { user } = usePageUser();
|
||||||
|
|
||||||
|
const recipeHousehold = ref<HouseholdSummary>();
|
||||||
|
if (user) {
|
||||||
const userApi = useUserApi();
|
const userApi = useUserApi();
|
||||||
userApi.households.getOne(props.recipe.householdId).then(({ data }) => {
|
userApi.households.getOne(props.recipe.householdId).then(({ data }) => {
|
||||||
recipeHousehold.value = data || undefined;
|
recipeHousehold.value = data || undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideImage = ref(false);
|
const hideImage = ref(false);
|
||||||
const imageHeight = computed(() => {
|
const imageHeight = computed(() => {
|
||||||
return $vuetify.display.xs.value ? "200" : "400";
|
return $vuetify.display.xs.value ? "200" : "400";
|
||||||
});
|
});
|
||||||
|
|
||||||
const recipeImageUrl = computed(() => {
|
const recipeImageUrl = computed(() => {
|
||||||
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
|
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => recipeImageUrl.value,
|
() => recipeImageUrl.value,
|
||||||
() => {
|
() => {
|
||||||
hideImage.value = false;
|
hideImage.value = false;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
|
||||||
recipeImageUrl,
|
|
||||||
imageKey,
|
|
||||||
hideImage,
|
|
||||||
imageHeight,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
<!-- eslint-disable vue/no-mutating-props -->
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<div class="mb-4">
|
||||||
<h2 class="mb-4 text-h5 font-weight-medium opacity-80">
|
<h2 class="mb-4 text-h5 font-weight-medium opacity-80">
|
||||||
{{ $t("recipe.ingredients") }}
|
{{ $t("recipe.ingredients") }}
|
||||||
</h2>
|
</h2>
|
||||||
|
<BannerWarning v-if="!hasFoodOrUnit">
|
||||||
|
{{ $t("recipe.ingredients-not-parsed-description", { parse: $t('recipe.parse') }) }}
|
||||||
|
</BannerWarning>
|
||||||
|
</div>
|
||||||
<VueDraggable
|
<VueDraggable
|
||||||
v-if="recipe.recipeIngredient.length > 0"
|
v-if="recipe.recipeIngredient.length > 0"
|
||||||
v-model="recipe.recipeIngredient"
|
v-model="recipe.recipeIngredient"
|
||||||
|
@ -27,7 +32,6 @@
|
||||||
:key="ingredient.referenceId"
|
:key="ingredient.referenceId"
|
||||||
v-model="recipe.recipeIngredient[index]"
|
v-model="recipe.recipeIngredient[index]"
|
||||||
class="list-group-item"
|
class="list-group-item"
|
||||||
:disable-amount="recipe.settings.disableAmount"
|
|
||||||
@delete="recipe.recipeIngredient.splice(index, 1)"
|
@delete="recipe.recipeIngredient.splice(index, 1)"
|
||||||
@insert-above="insertNewIngredient(index)"
|
@insert-above="insertNewIngredient(index)"
|
||||||
@insert-below="insertNewIngredient(index + 1)"
|
@insert-below="insertNewIngredient(index + 1)"
|
||||||
|
@ -42,14 +46,14 @@
|
||||||
/>
|
/>
|
||||||
<div class="d-flex flex-wrap justify-center justify-sm-end mt-3">
|
<div class="d-flex flex-wrap justify-center justify-sm-end mt-3">
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
top
|
location="top"
|
||||||
color="accent"
|
color="accent"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<span>
|
<span>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
class="mb-1"
|
class="mb-1"
|
||||||
:disabled="recipe.settings.disableAmount || hasFoodOrUnit"
|
:disabled="hasFoodOrUnit"
|
||||||
color="accent"
|
color="accent"
|
||||||
:to="`/g/${groupSlug}/r/${recipe.slug}/ingredient-parser`"
|
:to="`/g/${groupSlug}/r/${recipe.slug}/ingredient-parser`"
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
|
@ -109,10 +113,7 @@ const hasFoodOrUnit = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const parserToolTip = computed(() => {
|
const parserToolTip = computed(() => {
|
||||||
if (recipe.value.settings.disableAmount) {
|
if (hasFoodOrUnit.value) {
|
||||||
return i18n.t("recipe.enable-ingredient-amounts-to-use-this-feature");
|
|
||||||
}
|
|
||||||
else if (hasFoodOrUnit.value) {
|
|
||||||
return i18n.t("recipe.recipes-with-units-or-foods-defined-cannot-be-parsed");
|
return i18n.t("recipe.recipes-with-units-or-foods-defined-cannot-be-parsed");
|
||||||
}
|
}
|
||||||
return i18n.t("recipe.parse-ingredients");
|
return i18n.t("recipe.parse-ingredients");
|
||||||
|
@ -127,7 +128,6 @@ function addIngredient(ingredients: Array<string> | null = null) {
|
||||||
note: x,
|
note: x,
|
||||||
unit: undefined,
|
unit: undefined,
|
||||||
food: undefined,
|
food: undefined,
|
||||||
disableAmount: true,
|
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -146,7 +146,6 @@ function addIngredient(ingredients: Array<string> | null = null) {
|
||||||
unit: undefined,
|
unit: undefined,
|
||||||
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
|
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
|
||||||
food: undefined,
|
food: undefined,
|
||||||
disableAmount: true,
|
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -161,7 +160,6 @@ function insertNewIngredient(dest: number) {
|
||||||
unit: undefined,
|
unit: undefined,
|
||||||
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
|
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
|
||||||
food: undefined,
|
food: undefined,
|
||||||
disableAmount: true,
|
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
<RecipeIngredients
|
<RecipeIngredients
|
||||||
:value="recipe.recipeIngredient"
|
:value="recipe.recipeIngredient"
|
||||||
:scale="scale"
|
:scale="scale"
|
||||||
:disable-amount="recipe.settings.disableAmount"
|
|
||||||
:is-cook-mode="isCookMode"
|
:is-cook-mode="isCookMode"
|
||||||
/>
|
/>
|
||||||
<div v-if="!isEditMode && recipe.tools && recipe.tools.length > 0">
|
<div v-if="!isEditMode && recipe.tools && recipe.tools.length > 0">
|
||||||
|
@ -36,7 +35,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
||||||
import { useToolStore } from "~/composables/store";
|
import { useToolStore } from "~/composables/store";
|
||||||
|
@ -48,32 +47,22 @@ interface RecipeToolWithOnHand extends RecipeTool {
|
||||||
onHand: boolean;
|
onHand: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
recipe: NoUndefinedField<Recipe>;
|
||||||
RecipeIngredients,
|
scale: number;
|
||||||
},
|
isCookMode?: boolean;
|
||||||
props: {
|
}
|
||||||
recipe: {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
type: Object as () => NoUndefinedField<Recipe>,
|
isCookMode: false,
|
||||||
required: true,
|
});
|
||||||
},
|
|
||||||
scale: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
isCookMode: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { isOwnGroup } = useLoggedInState();
|
|
||||||
|
|
||||||
const toolStore = isOwnGroup.value ? useToolStore() : null;
|
const { isOwnGroup } = useLoggedInState();
|
||||||
const { user } = usePageUser();
|
|
||||||
const { isEditMode } = usePageState(props.recipe.slug);
|
|
||||||
|
|
||||||
const recipeTools = computed(() => {
|
const toolStore = isOwnGroup.value ? useToolStore() : null;
|
||||||
|
const { user } = usePageUser();
|
||||||
|
const { isEditMode } = usePageState(props.recipe.slug);
|
||||||
|
|
||||||
|
const recipeTools = computed(() => {
|
||||||
if (!(user.householdSlug && toolStore)) {
|
if (!(user.householdSlug && toolStore)) {
|
||||||
return props.recipe.tools.map(tool => ({ ...tool, onHand: false }) as RecipeToolWithOnHand);
|
return props.recipe.tools.map(tool => ({ ...tool, onHand: false }) as RecipeToolWithOnHand);
|
||||||
}
|
}
|
||||||
|
@ -83,9 +72,9 @@ export default defineNuxtComponent({
|
||||||
return { ...tool, onHand } as RecipeToolWithOnHand;
|
return { ...tool, onHand } as RecipeToolWithOnHand;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateTool(index: number) {
|
function updateTool(index: number) {
|
||||||
if (user.id && user.householdSlug && toolStore) {
|
if (user.id && user.householdSlug && toolStore) {
|
||||||
const tool = recipeTools.value[index];
|
const tool = recipeTools.value[index];
|
||||||
if (tool.onHand && !tool.householdsWithTool?.includes(user.householdSlug)) {
|
if (tool.onHand && !tool.householdsWithTool?.includes(user.householdSlug)) {
|
||||||
|
@ -105,14 +94,5 @@ export default defineNuxtComponent({
|
||||||
else {
|
else {
|
||||||
console.log("no user, skipping server update");
|
console.log("no user, skipping server update");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
toolStore,
|
|
||||||
recipeTools,
|
|
||||||
isEditMode,
|
|
||||||
updateTool,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -29,33 +29,31 @@
|
||||||
{{ activeText }}
|
{{ activeText }}
|
||||||
</p>
|
</p>
|
||||||
<v-divider class="mb-4" />
|
<v-divider class="mb-4" />
|
||||||
<v-checkbox
|
<v-checkbox-btn
|
||||||
v-for="ing in unusedIngredients"
|
v-for="ing in unusedIngredients"
|
||||||
:key="ing.referenceId"
|
:key="ing.referenceId"
|
||||||
v-model="activeRefs"
|
v-model="activeRefs"
|
||||||
:value="ing.referenceId"
|
:value="ing.referenceId"
|
||||||
class="mb-n2 mt-n2"
|
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<RecipeIngredientHtml :markup="parseIngredientText(ing, recipe.settings.disableAmount)" />
|
<RecipeIngredientHtml :markup="parseIngredientText(ing)" />
|
||||||
</template>
|
</template>
|
||||||
</v-checkbox>
|
</v-checkbox-btn>
|
||||||
|
|
||||||
<template v-if="usedIngredients.length > 0">
|
<template v-if="usedIngredients.length > 0">
|
||||||
<h4 class="py-3 ml-1">
|
<h4 class="py-3 ml-1">
|
||||||
{{ $t("recipe.linked-to-other-step") }}
|
{{ $t("recipe.linked-to-other-step") }}
|
||||||
</h4>
|
</h4>
|
||||||
<v-checkbox
|
<v-checkbox-btn
|
||||||
v-for="ing in usedIngredients"
|
v-for="ing in usedIngredients"
|
||||||
:key="ing.referenceId"
|
:key="ing.referenceId"
|
||||||
v-model="activeRefs"
|
v-model="activeRefs"
|
||||||
:value="ing.referenceId"
|
:value="ing.referenceId"
|
||||||
class="mb-n2 mt-n2"
|
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
<RecipeIngredientHtml :markup="parseIngredientText(ing, recipe.settings.disableAmount)" />
|
<RecipeIngredientHtml :markup="parseIngredientText(ing)" />
|
||||||
</template>
|
</template>
|
||||||
</v-checkbox>
|
</v-checkbox-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
|
@ -325,7 +323,6 @@
|
||||||
return step.ingredientReferences.map((ref) => ref.referenceId).includes(ing.referenceId || '')
|
return step.ingredientReferences.map((ref) => ref.referenceId).includes(ing.referenceId || '')
|
||||||
})"
|
})"
|
||||||
:scale="scale"
|
:scale="scale"
|
||||||
:disable-amount="recipe.settings.disableAmount"
|
|
||||||
:is-cook-mode="isCookMode"
|
:is-cook-mode="isCookMode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -554,7 +551,6 @@ function autoSetReferences() {
|
||||||
props.recipe.recipeIngredient,
|
props.recipe.recipeIngredient,
|
||||||
activeRefs.value,
|
activeRefs.value,
|
||||||
activeText.value,
|
activeText.value,
|
||||||
props.recipe.settings.disableAmount,
|
|
||||||
).forEach((ingredient: string) => activeRefs.value.push(ingredient));
|
).forEach((ingredient: string) => activeRefs.value.push(ingredient));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -576,7 +572,7 @@ function getIngredientByRefId(refId: string | undefined) {
|
||||||
|
|
||||||
const ing = ingredientLookup.value[refId];
|
const ing = ingredientLookup.value[refId];
|
||||||
if (!ing) return "";
|
if (!ing) return "";
|
||||||
return parseIngredientText(ing, props.recipe.settings.disableAmount, props.scale);
|
return parseIngredientText(ing, props.scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================================
|
// ===============================================================
|
||||||
|
|
|
@ -2,55 +2,37 @@
|
||||||
<div class="d-flex justify-space-between align-center pt-2 pb-3">
|
<div class="d-flex justify-space-between align-center pt-2 pb-3">
|
||||||
<RecipeScaleEditButton
|
<RecipeScaleEditButton
|
||||||
v-if="!isEditMode"
|
v-if="!isEditMode"
|
||||||
v-model.number="scaleValue"
|
v-model.number="scale"
|
||||||
:recipe-servings="recipeServings"
|
:recipe-servings="recipeServings"
|
||||||
:edit-scale="!recipe.settings.disableAmount && !isEditMode"
|
:edit-scale="hasFoodOrUnit && !isEditMode"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeScaleEditButton from "~/components/Domain/Recipe/RecipeScaleEditButton.vue";
|
import RecipeScaleEditButton from "~/components/Domain/Recipe/RecipeScaleEditButton.vue";
|
||||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
import type { Recipe } from "~/lib/api/types/recipe";
|
import type { Recipe } from "~/lib/api/types/recipe";
|
||||||
import { usePageState } from "~/composables/recipe-page/shared-state";
|
import { usePageState } from "~/composables/recipe-page/shared-state";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const props = defineProps<{ recipe: NoUndefinedField<Recipe> }>();
|
||||||
components: {
|
|
||||||
RecipeScaleEditButton,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
recipe: {
|
|
||||||
type: Object as () => NoUndefinedField<Recipe>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
scale: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:scale"],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const { isEditMode } = usePageState(props.recipe.slug);
|
|
||||||
|
|
||||||
const recipeServings = computed<number>(() => {
|
const scale = defineModel<number>({ default: 1 });
|
||||||
|
|
||||||
|
const { isEditMode } = usePageState(props.recipe.slug);
|
||||||
|
|
||||||
|
const recipeServings = computed<number>(() => {
|
||||||
return props.recipe.recipeServings || props.recipe.recipeYieldQuantity || 1;
|
return props.recipe.recipeServings || props.recipe.recipeYieldQuantity || 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
const scaleValue = computed<number>({
|
const hasFoodOrUnit = computed(() => {
|
||||||
get() {
|
if (props.recipe.recipeIngredient) {
|
||||||
return props.scale;
|
for (const ingredient of props.recipe.recipeIngredient) {
|
||||||
},
|
if (ingredient.food || ingredient.unit) {
|
||||||
set(val) {
|
return true;
|
||||||
emit("update:scale", val);
|
}
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
return false;
|
||||||
return {
|
|
||||||
recipeServings,
|
|
||||||
scaleValue,
|
|
||||||
isEditMode,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -8,24 +8,17 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
|
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
|
||||||
import type { Recipe } from "~/lib/api/types/recipe";
|
import type { Recipe } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
recipe: Recipe;
|
||||||
RecipePrintView,
|
scale?: number;
|
||||||
},
|
}
|
||||||
props: {
|
|
||||||
recipe: {
|
withDefaults(defineProps<Props>(), {
|
||||||
type: Object as () => Recipe,
|
scale: 1,
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
scale: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -166,7 +166,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
|
import RecipeTimeCard from "~/components/Domain/Recipe/RecipeTimeCard.vue";
|
||||||
import { useStaticRoutes } from "~/composables/api";
|
import { useStaticRoutes } from "~/composables/api";
|
||||||
|
@ -188,68 +188,58 @@ type InstructionSection = {
|
||||||
instructions: RecipeStep[];
|
instructions: RecipeStep[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: {
|
recipe: NoUndefinedField<Recipe>;
|
||||||
RecipeTimeCard,
|
scale?: number;
|
||||||
},
|
dense?: boolean;
|
||||||
props: {
|
}
|
||||||
recipe: {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
type: Object as () => NoUndefinedField<Recipe>,
|
scale: 1,
|
||||||
required: true,
|
dense: false,
|
||||||
},
|
});
|
||||||
scale: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
dense: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
const preferences = useUserPrintPreferences();
|
|
||||||
const { recipeImage } = useStaticRoutes();
|
|
||||||
const { imageKey } = usePageState(props.recipe.slug);
|
|
||||||
const { labels } = useNutritionLabels();
|
|
||||||
|
|
||||||
function sanitizeHTML(rawHtml: string) {
|
const i18n = useI18n();
|
||||||
|
const preferences = useUserPrintPreferences();
|
||||||
|
const { recipeImage } = useStaticRoutes();
|
||||||
|
const { imageKey } = usePageState(props.recipe.slug);
|
||||||
|
const { labels } = useNutritionLabels();
|
||||||
|
|
||||||
|
function sanitizeHTML(rawHtml: string) {
|
||||||
return DOMPurify.sanitize(rawHtml, {
|
return DOMPurify.sanitize(rawHtml, {
|
||||||
USE_PROFILES: { html: true },
|
USE_PROFILES: { html: true },
|
||||||
ALLOWED_TAGS: ["strong", "sup"],
|
ALLOWED_TAGS: ["strong", "sup"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const servingsDisplay = computed(() => {
|
||||||
const servingsDisplay = computed(() => {
|
|
||||||
const { scaledAmountDisplay } = useScaledAmount(props.recipe.recipeYieldQuantity, props.scale);
|
const { scaledAmountDisplay } = useScaledAmount(props.recipe.recipeYieldQuantity, props.scale);
|
||||||
return scaledAmountDisplay
|
return scaledAmountDisplay || props.recipe.recipeYield
|
||||||
? i18n.t("recipe.yields-amount-with-text", {
|
? i18n.t("recipe.yields-amount-with-text", {
|
||||||
amount: scaledAmountDisplay,
|
amount: scaledAmountDisplay,
|
||||||
text: props.recipe.recipeYield,
|
text: props.recipe.recipeYield,
|
||||||
}) as string
|
}) as string
|
||||||
: "";
|
: "";
|
||||||
});
|
});
|
||||||
|
|
||||||
const yieldDisplay = computed(() => {
|
const yieldDisplay = computed(() => {
|
||||||
const { scaledAmountDisplay } = useScaledAmount(props.recipe.recipeServings, props.scale);
|
const { scaledAmountDisplay } = useScaledAmount(props.recipe.recipeServings, props.scale);
|
||||||
return scaledAmountDisplay ? i18n.t("recipe.serves-amount", { amount: scaledAmountDisplay }) as string : "";
|
return scaledAmountDisplay ? i18n.t("recipe.serves-amount", { amount: scaledAmountDisplay }) as string : "";
|
||||||
});
|
});
|
||||||
|
|
||||||
const recipeYield = computed(() => {
|
const recipeYield = computed(() => {
|
||||||
if (servingsDisplay.value && yieldDisplay.value) {
|
if (servingsDisplay.value && yieldDisplay.value) {
|
||||||
return sanitizeHTML(`${yieldDisplay.value}; ${servingsDisplay.value}`);
|
return sanitizeHTML(`${yieldDisplay.value}; ${servingsDisplay.value}`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return sanitizeHTML(yieldDisplay.value || servingsDisplay.value);
|
return sanitizeHTML(yieldDisplay.value || servingsDisplay.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const recipeImageUrl = computed(() => {
|
const recipeImageUrl = computed(() => {
|
||||||
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
|
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Group ingredients by section so we can style them independently
|
// Group ingredients by section so we can style them independently
|
||||||
const ingredientSections = computed<IngredientSection[]>(() => {
|
const ingredientSections = computed<IngredientSection[]>(() => {
|
||||||
if (!props.recipe.recipeIngredient) {
|
if (!props.recipe.recipeIngredient) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -279,10 +269,10 @@ export default defineNuxtComponent({
|
||||||
sections[sections.length - 1].ingredients.push(ingredient);
|
sections[sections.length - 1].ingredients.push(ingredient);
|
||||||
return sections;
|
return sections;
|
||||||
}, [] as IngredientSection[]);
|
}, [] as IngredientSection[]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Group instructions by section so we can style them independently
|
// Group instructions by section so we can style them independently
|
||||||
const instructionSections = computed<InstructionSection[]>(() => {
|
const instructionSections = computed<InstructionSection[]>(() => {
|
||||||
if (!props.recipe.recipeInstructions) {
|
if (!props.recipe.recipeInstructions) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -324,31 +314,15 @@ export default defineNuxtComponent({
|
||||||
sections[sections.length - 1].instructions.push(step);
|
sections[sections.length - 1].instructions.push(step);
|
||||||
return sections;
|
return sections;
|
||||||
}, [] as InstructionSection[]);
|
}, [] as InstructionSection[]);
|
||||||
});
|
|
||||||
|
|
||||||
const hasNotes = computed(() => {
|
|
||||||
return props.recipe.notes && props.recipe.notes.length > 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
function parseText(ingredient: RecipeIngredient) {
|
|
||||||
return parseIngredientText(ingredient, props.recipe.settings?.disableAmount || false, props.scale);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels,
|
|
||||||
hasNotes,
|
|
||||||
imageKey,
|
|
||||||
ImagePosition,
|
|
||||||
parseText,
|
|
||||||
parseIngredientText,
|
|
||||||
preferences,
|
|
||||||
recipeImageUrl,
|
|
||||||
recipeYield,
|
|
||||||
ingredientSections,
|
|
||||||
instructionSections,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hasNotes = computed(() => {
|
||||||
|
return props.recipe.notes && props.recipe.notes.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
function parseText(ingredient: RecipeIngredient) {
|
||||||
|
return parseIngredientText(ingredient, props.scale);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -10,11 +10,11 @@
|
||||||
nudge-top="6"
|
nudge-top="6"
|
||||||
:close-on-content-click="false"
|
:close-on-content-click="false"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
v-if="canEditScale"
|
v-if="canEditScale"
|
||||||
size="small"
|
size="small"
|
||||||
top
|
location="top"
|
||||||
color="secondary-darken-1"
|
color="secondary-darken-1"
|
||||||
>
|
>
|
||||||
<template #activator="{ props: tooltipProps }">
|
<template #activator="{ props: tooltipProps }">
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
dark
|
dark
|
||||||
color="secondary-darken-1"
|
color="secondary-darken-1"
|
||||||
size="small"
|
size="small"
|
||||||
v-bind="{ ...props, ...tooltipProps }"
|
v-bind="{ ...activatorProps, ...tooltipProps }"
|
||||||
:style="{ cursor: canEditScale ? '' : 'default' }"
|
:style="{ cursor: canEditScale ? '' : 'default' }"
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-icon
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
dark
|
dark
|
||||||
color="secondary-darken-1"
|
color="secondary-darken-1"
|
||||||
size="small"
|
size="small"
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
:style="{ cursor: canEditScale ? '' : 'default' }"
|
:style="{ cursor: canEditScale ? '' : 'default' }"
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-icon
|
||||||
|
@ -66,21 +66,22 @@
|
||||||
<v-card-text class="mt-n5">
|
<v-card-text class="mt-n5">
|
||||||
<div class="mt-4 d-flex align-center">
|
<div class="mt-4 d-flex align-center">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
:model-value="yieldQuantityEditorValue"
|
:model-value="yieldQuantity"
|
||||||
type="number"
|
type="number"
|
||||||
:min="0"
|
:min="0"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
hide-spin-buttons
|
hide-spin-buttons
|
||||||
@update:model-value="recalculateScale(yieldQuantityEditorValue)"
|
@update:model-value="recalculateScale(parseFloat($event) || 0)"
|
||||||
/>
|
/>
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
end
|
location="end"
|
||||||
color="secondary-darken-1"
|
color="secondary-darken-1"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: resetTooltipProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-bind="props"
|
v-bind="resetTooltipProps"
|
||||||
icon
|
icon
|
||||||
|
flat
|
||||||
class="mx-1"
|
class="mx-1"
|
||||||
size="small"
|
size="small"
|
||||||
@click="scale = 1"
|
@click="scale = 1"
|
||||||
|
@ -121,39 +122,25 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
|
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
recipeServings?: number;
|
||||||
modelValue: {
|
editScale?: boolean;
|
||||||
type: Number,
|
}
|
||||||
required: true,
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
},
|
recipeServings: 0,
|
||||||
recipeServings: {
|
editScale: false,
|
||||||
type: Number,
|
});
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
editScale: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
const menu = ref<boolean>(false);
|
|
||||||
const canEditScale = computed(() => props.editScale && props.recipeServings > 0);
|
|
||||||
|
|
||||||
const scale = computed({
|
const scale = defineModel<number>({ required: true });
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (value) => {
|
|
||||||
const newScaleNumber = parseFloat(`${value}`);
|
|
||||||
emit("update:modelValue", isNaN(newScaleNumber) ? 0 : newScaleNumber);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function recalculateScale(newYield: number) {
|
const i18n = useI18n();
|
||||||
|
const menu = ref<boolean>(false);
|
||||||
|
const canEditScale = computed(() => props.editScale && props.recipeServings > 0);
|
||||||
|
|
||||||
|
function recalculateScale(newYield: number) {
|
||||||
if (isNaN(newYield) || newYield <= 0) {
|
if (isNaN(newYield) || newYield <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -164,47 +151,21 @@ export default defineNuxtComponent({
|
||||||
else {
|
else {
|
||||||
scale.value = newYield / props.recipeServings;
|
scale.value = newYield / props.recipeServings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipeYieldAmount = computed(() => {
|
const recipeYieldAmount = computed(() => {
|
||||||
return useScaledAmount(props.recipeServings, scale.value);
|
return useScaledAmount(props.recipeServings, scale.value);
|
||||||
});
|
});
|
||||||
const yieldQuantity = computed(() => recipeYieldAmount.value.scaledAmount);
|
const yieldQuantity = computed(() => recipeYieldAmount.value.scaledAmount);
|
||||||
const yieldDisplay = computed(() => {
|
const yieldDisplay = computed(() => {
|
||||||
return yieldQuantity.value
|
return yieldQuantity.value
|
||||||
? i18n.t(
|
? i18n.t(
|
||||||
"recipe.serves-amount", { amount: recipeYieldAmount.value.scaledAmountDisplay },
|
"recipe.serves-amount", { amount: recipeYieldAmount.value.scaledAmountDisplay },
|
||||||
) as string
|
) as string
|
||||||
: "";
|
: "";
|
||||||
});
|
});
|
||||||
|
|
||||||
// only update yield quantity when the menu opens, so we don't override the user's input
|
const disableDecrement = computed(() => {
|
||||||
const yieldQuantityEditorValue = ref(recipeYieldAmount.value.scaledAmount);
|
return yieldQuantity.value <= 1;
|
||||||
watch(
|
|
||||||
() => menu.value,
|
|
||||||
() => {
|
|
||||||
if (!menu.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
yieldQuantityEditorValue.value = recipeYieldAmount.value.scaledAmount;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const disableDecrement = computed(() => {
|
|
||||||
return recipeYieldAmount.value.scaledAmount <= 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
menu,
|
|
||||||
canEditScale,
|
|
||||||
scale,
|
|
||||||
recalculateScale,
|
|
||||||
yieldDisplay,
|
|
||||||
yieldQuantity,
|
|
||||||
yieldQuantityEditorValue,
|
|
||||||
disableDecrement,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="d-flex justify-center align-center">
|
|
||||||
<v-btn-toggle v-model="selected" tile group color="primary accent-3" mandatory="force" @change="emitMulti">
|
|
||||||
<v-btn size="small" :value="false">
|
|
||||||
{{ $t("search.include") }}
|
|
||||||
</v-btn>
|
|
||||||
<v-btn size="small" :value="true">
|
|
||||||
{{ $t("search.exclude") }}
|
|
||||||
</v-btn>
|
|
||||||
</v-btn-toggle>
|
|
||||||
<v-btn-toggle v-model="match" tile group color="primary accent-3" mandatory="force" @change="emitMulti">
|
|
||||||
<v-btn size="small" :value="false" class="text-uppercase">
|
|
||||||
{{ $t("search.and") }}
|
|
||||||
</v-btn>
|
|
||||||
<v-btn size="small" :value="true" class="text-uppercase">
|
|
||||||
{{ $t("search.or") }}
|
|
||||||
</v-btn>
|
|
||||||
</v-btn-toggle>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
type SelectionValue = "include" | "exclude" | "any";
|
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: String as () => SelectionValue,
|
|
||||||
default: "include",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue", "update"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selected: false,
|
|
||||||
match: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
emitChange() {
|
|
||||||
this.$emit("update:modelValue", this.selected);
|
|
||||||
},
|
|
||||||
emitMulti() {
|
|
||||||
const updateData = {
|
|
||||||
exclude: this.selected,
|
|
||||||
matchAny: this.match,
|
|
||||||
};
|
|
||||||
this.$emit("update", updateData);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
|
@ -31,7 +31,6 @@ const labels: Record<keyof RecipeSettings, string> = {
|
||||||
showAssets: i18n.t("asset.show-assets"),
|
showAssets: i18n.t("asset.show-assets"),
|
||||||
landscapeView: i18n.t("recipe.landscape-view-coming-soon"),
|
landscapeView: i18n.t("recipe.landscape-view-coming-soon"),
|
||||||
disableComments: i18n.t("recipe.disable-comments"),
|
disableComments: i18n.t("recipe.disable-comments"),
|
||||||
disableAmount: i18n.t("recipe.disable-amount"),
|
|
||||||
locked: i18n.t("recipe.locked"),
|
locked: i18n.t("recipe.locked"),
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -14,9 +14,7 @@
|
||||||
<div v-for="(organizer, idx) in missingOrganizers" :key="idx">
|
<div v-for="(organizer, idx) in missingOrganizers" :key="idx">
|
||||||
<v-col v-if="organizer.show" cols="12">
|
<v-col v-if="organizer.show" cols="12">
|
||||||
<div class="d-flex flex-row flex-wrap align-center pt-2">
|
<div class="d-flex flex-row flex-wrap align-center pt-2">
|
||||||
<v-icon class="ma-0 pa-0">
|
<v-icon class="ma-0 pa-0" />
|
||||||
{{ organizer.icon }}
|
|
||||||
</v-icon>
|
|
||||||
<v-card-text class="mr-0 my-0 pl-1 py-0" style="width: min-content">
|
<v-card-text class="mr-0 my-0 pl-1 py-0" style="width: min-content">
|
||||||
{{ $t("recipe-finder.missing") }}:
|
{{ $t("recipe-finder.missing") }}:
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
@ -41,7 +39,7 @@
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||||
import type { IngredientFood, RecipeSummary, RecipeTool } from "~/lib/api/types/recipe";
|
import type { IngredientFood, RecipeSummary, RecipeTool } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
|
@ -51,29 +49,27 @@ interface Organizer {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: { RecipeCardMobile },
|
recipe: RecipeSummary;
|
||||||
props: {
|
missingFoods?: IngredientFood[] | null;
|
||||||
recipe: {
|
missingTools?: RecipeTool[] | null;
|
||||||
type: Object as () => RecipeSummary,
|
disableCheckbox?: boolean;
|
||||||
required: true,
|
}
|
||||||
},
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
missingFoods: {
|
missingFoods: null,
|
||||||
type: Array as () => IngredientFood[] | null,
|
missingTools: null,
|
||||||
default: null,
|
disableCheckbox: false,
|
||||||
},
|
});
|
||||||
missingTools: {
|
|
||||||
type: Array as () => RecipeTool[] | null,
|
const emit = defineEmits<{
|
||||||
default: null,
|
"add-food": [food: IngredientFood];
|
||||||
},
|
"remove-food": [food: IngredientFood];
|
||||||
disableCheckbox: {
|
"add-tool": [tool: RecipeTool];
|
||||||
type: Boolean,
|
"remove-tool": [tool: RecipeTool];
|
||||||
default: false,
|
}>();
|
||||||
},
|
|
||||||
},
|
const { $globals } = useNuxtApp();
|
||||||
setup(props, context) {
|
const missingOrganizers = computed(() => [
|
||||||
const { $globals } = useNuxtApp();
|
|
||||||
const missingOrganizers = computed(() => [
|
|
||||||
{
|
{
|
||||||
type: "food",
|
type: "food",
|
||||||
show: props.missingFoods?.length,
|
show: props.missingFoods?.length,
|
||||||
|
@ -96,26 +92,29 @@ export default defineNuxtComponent({
|
||||||
: [],
|
: [],
|
||||||
getLabel: (item: RecipeTool) => item.name,
|
getLabel: (item: RecipeTool) => item.name,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function handleCheckbox(organizer: Organizer) {
|
function handleCheckbox(organizer: Organizer) {
|
||||||
if (props.disableCheckbox) {
|
if (props.disableCheckbox) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
organizer.selected = !organizer.selected;
|
organizer.selected = !organizer.selected;
|
||||||
if (organizer.selected) {
|
if (organizer.selected) {
|
||||||
context.emit(`add-${organizer.type}`, organizer.item);
|
if (organizer.type === "food") {
|
||||||
|
emit("add-food", organizer.item as IngredientFood);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
context.emit(`remove-${organizer.type}`, organizer.item);
|
emit("add-tool", organizer.item as RecipeTool);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
return {
|
if (organizer.type === "food") {
|
||||||
missingOrganizers,
|
emit("remove-food", organizer.item as IngredientFood);
|
||||||
handleCheckbox,
|
}
|
||||||
};
|
else {
|
||||||
},
|
emit("remove-tool", organizer.item as RecipeTool);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<template v-if="showCards">
|
<template v-if="_showCards">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<!-- Total Time -->
|
<!-- Total Time -->
|
||||||
<div
|
<div
|
||||||
|
@ -78,65 +78,46 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
prepTime?: string | null;
|
||||||
prepTime: {
|
totalTime?: string | null;
|
||||||
type: String,
|
performTime?: string | null;
|
||||||
default: null,
|
color?: string;
|
||||||
},
|
small?: boolean;
|
||||||
totalTime: {
|
}
|
||||||
type: String,
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
default: null,
|
prepTime: null,
|
||||||
},
|
totalTime: null,
|
||||||
performTime: {
|
performTime: null,
|
||||||
type: String,
|
color: "accent custom-transparent",
|
||||||
default: null,
|
small: false,
|
||||||
},
|
});
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: "accent custom-transparent",
|
|
||||||
},
|
|
||||||
small: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
function isEmpty(str: string | null) {
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
function isEmpty(str: string | null) {
|
||||||
return !str || str.length === 0;
|
return !str || str.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showCards = computed(() => {
|
const _showCards = computed(() => {
|
||||||
return [props.prepTime, props.totalTime, props.performTime].some(x => !isEmpty(x));
|
return [props.prepTime, props.totalTime, props.performTime].some(x => !isEmpty(x));
|
||||||
});
|
});
|
||||||
|
|
||||||
const validateTotalTime = computed(() => {
|
const validateTotalTime = computed(() => {
|
||||||
return !isEmpty(props.totalTime) ? { name: i18n.t("recipe.total-time"), value: props.totalTime } : null;
|
return !isEmpty(props.totalTime) ? { name: i18n.t("recipe.total-time"), value: props.totalTime } : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const validatePrepTime = computed(() => {
|
const validatePrepTime = computed(() => {
|
||||||
return !isEmpty(props.prepTime) ? { name: i18n.t("recipe.prep-time"), value: props.prepTime } : null;
|
return !isEmpty(props.prepTime) ? { name: i18n.t("recipe.prep-time"), value: props.prepTime } : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const validatePerformTime = computed(() => {
|
const validatePerformTime = computed(() => {
|
||||||
return !isEmpty(props.performTime) ? { name: i18n.t("recipe.perform-time"), value: props.performTime } : null;
|
return !isEmpty(props.performTime) ? { name: i18n.t("recipe.perform-time"), value: props.performTime } : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const fontSize = computed(() => {
|
const fontSize = computed(() => {
|
||||||
return props.small ? { fontSize: "smaller" } : { fontSize: "larger" };
|
return props.small ? { fontSize: "smaller" } : { fontSize: "larger" };
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
showCards,
|
|
||||||
validateTotalTime,
|
|
||||||
validatePrepTime,
|
|
||||||
validatePerformTime,
|
|
||||||
fontSize,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
nudge-bottom="3"
|
nudge-bottom="3"
|
||||||
:close-on-content-click="false"
|
:close-on-content-click="false"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-badge
|
<v-badge
|
||||||
:content="filterBadgeCount"
|
:content="filterBadgeCount"
|
||||||
:model-value="filterBadgeCount > 0"
|
:model-value="filterBadgeCount > 0"
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
class="rounded-circle"
|
class="rounded-circle"
|
||||||
size="small"
|
size="small"
|
||||||
color="info"
|
color="info"
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
icon
|
icon
|
||||||
>
|
>
|
||||||
<v-icon> {{ $globals.icons.filter }} </v-icon>
|
<v-icon> {{ $globals.icons.filter }} </v-icon>
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useThrottleFn, whenever } from "@vueuse/core";
|
import { useThrottleFn, whenever } from "@vueuse/core";
|
||||||
import RecipeTimelineItem from "./RecipeTimelineItem.vue";
|
import RecipeTimelineItem from "./RecipeTimelineItem.vue";
|
||||||
import { useTimelinePreferences } from "~/composables/use-users/preferences";
|
import { useTimelinePreferences } from "~/composables/use-users/preferences";
|
||||||
|
@ -115,89 +115,61 @@ import { alert } from "~/composables/use-toast";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import type { Recipe, RecipeTimelineEventOut, RecipeTimelineEventUpdate, TimelineEventType } from "~/lib/api/types/recipe";
|
import type { Recipe, RecipeTimelineEventOut, RecipeTimelineEventUpdate, TimelineEventType } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: { RecipeTimelineItem },
|
modelValue?: boolean;
|
||||||
|
queryFilter: string;
|
||||||
|
maxHeight?: number | string;
|
||||||
|
showRecipeCards?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
props: {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
modelValue: {
|
modelValue: false,
|
||||||
type: Boolean,
|
maxHeight: undefined,
|
||||||
default: false,
|
showRecipeCards: false,
|
||||||
},
|
});
|
||||||
queryFilter: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
maxHeight: {
|
|
||||||
type: [Number, String],
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
showRecipeCards: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props) {
|
const api = useUserApi();
|
||||||
const api = useUserApi();
|
const i18n = useI18n();
|
||||||
const i18n = useI18n();
|
const preferences = useTimelinePreferences();
|
||||||
const preferences = useTimelinePreferences();
|
const { eventTypeOptions } = useTimelineEventTypes();
|
||||||
const { eventTypeOptions } = useTimelineEventTypes();
|
const loading = ref(true);
|
||||||
const loading = ref(true);
|
const ready = ref(false);
|
||||||
const ready = ref(false);
|
|
||||||
|
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
const perPage = 32;
|
const perPage = 32;
|
||||||
const hasMore = ref(true);
|
const hasMore = ref(true);
|
||||||
|
|
||||||
const timelineEvents = ref([] as RecipeTimelineEventOut[]);
|
const timelineEvents = ref([] as RecipeTimelineEventOut[]);
|
||||||
const recipes = new Map<string, Recipe>();
|
const recipes = new Map<string, Recipe>();
|
||||||
const filterBadgeCount = computed(() => eventTypeOptions.value.length - preferences.value.types.length);
|
const filterBadgeCount = computed(() => eventTypeOptions.value.length - preferences.value.types.length);
|
||||||
const eventTypeFilterState = computed(() => {
|
const eventTypeFilterState = computed(() => {
|
||||||
return eventTypeOptions.value.map((option) => {
|
return eventTypeOptions.value.map((option) => {
|
||||||
return {
|
return {
|
||||||
...option,
|
...option,
|
||||||
checked: preferences.value.types.includes(option.value),
|
checked: preferences.value.types.includes(option.value),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const screenBuffer = 4;
|
||||||
|
|
||||||
interface ScrollEvent extends Event {
|
whenever(
|
||||||
target: HTMLInputElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
const screenBuffer = 4;
|
|
||||||
const onScroll = (event: ScrollEvent) => {
|
|
||||||
if (!event.target) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { scrollTop, offsetHeight, scrollHeight } = event.target;
|
|
||||||
|
|
||||||
// trigger when the user is getting close to the bottom
|
|
||||||
const bottomOfElement = scrollTop + offsetHeight >= scrollHeight - (offsetHeight * screenBuffer);
|
|
||||||
if (bottomOfElement) {
|
|
||||||
infiniteScroll();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
whenever(
|
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
() => {
|
() => {
|
||||||
initializeTimelineEvents();
|
initializeTimelineEvents();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
function reverseSort() {
|
function reverseSort() {
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences.value.orderDirection = preferences.value.orderDirection === "asc" ? "desc" : "asc";
|
preferences.value.orderDirection = preferences.value.orderDirection === "asc" ? "desc" : "asc";
|
||||||
initializeTimelineEvents();
|
initializeTimelineEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleEventTypeOption(option: TimelineEventType) {
|
function toggleEventTypeOption(option: TimelineEventType) {
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -211,10 +183,10 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeTimelineEvents();
|
initializeTimelineEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeline Actions
|
// Timeline Actions
|
||||||
async function updateTimelineEvent(index: number) {
|
async function updateTimelineEvent(index: number) {
|
||||||
const event = timelineEvents.value[index];
|
const event = timelineEvents.value[index];
|
||||||
const payload: RecipeTimelineEventUpdate = {
|
const payload: RecipeTimelineEventUpdate = {
|
||||||
subject: event.subject,
|
subject: event.subject,
|
||||||
|
@ -229,9 +201,9 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
alert.success(i18n.t("events.event-updated") as string);
|
alert.success(i18n.t("events.event-updated") as string);
|
||||||
};
|
}
|
||||||
|
|
||||||
async function deleteTimelineEvent(index: number) {
|
async function deleteTimelineEvent(index: number) {
|
||||||
const { response } = await api.recipes.deleteTimelineEvent(timelineEvents.value[index].id);
|
const { response } = await api.recipes.deleteTimelineEvent(timelineEvents.value[index].id);
|
||||||
if (response?.status !== 200) {
|
if (response?.status !== 200) {
|
||||||
alert.error(i18n.t("events.something-went-wrong") as string);
|
alert.error(i18n.t("events.something-went-wrong") as string);
|
||||||
|
@ -240,34 +212,34 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
timelineEvents.value.splice(index, 1);
|
timelineEvents.value.splice(index, 1);
|
||||||
alert.success(i18n.t("events.event-deleted") as string);
|
alert.success(i18n.t("events.event-deleted") as string);
|
||||||
};
|
}
|
||||||
|
|
||||||
async function getRecipe(recipeId: string): Promise<Recipe | null> {
|
async function getRecipes(recipeIds: string[]): Promise<Recipe[]> {
|
||||||
const { data } = await api.recipes.getOne(recipeId);
|
const qf = "id IN [" + recipeIds.map(id => `"${id}"`).join(", ") + "]";
|
||||||
return data;
|
const { data } = await api.recipes.getAll(1, -1, { queryFilter: qf });
|
||||||
};
|
return data?.items || [];
|
||||||
|
}
|
||||||
|
|
||||||
async function updateRecipes(events: RecipeTimelineEventOut[]) {
|
async function updateRecipes(events: RecipeTimelineEventOut[]) {
|
||||||
const recipePromises: Promise<Recipe | null>[] = [];
|
const recipeIds: string[] = [];
|
||||||
const seenRecipeIds: string[] = [];
|
|
||||||
events.forEach((event) => {
|
events.forEach((event) => {
|
||||||
if (seenRecipeIds.includes(event.recipeId) || recipes.has(event.recipeId)) {
|
if (recipeIds.includes(event.recipeId) || recipes.has(event.recipeId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
seenRecipeIds.push(event.recipeId);
|
recipeIds.push(event.recipeId);
|
||||||
recipePromises.push(getRecipe(event.recipeId));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const results = await Promise.all(recipePromises);
|
const results = await getRecipes(recipeIds);
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
if (result && result.id) {
|
if (!result?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
recipes.set(result.id, result);
|
recipes.set(result.id, result);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scrollTimelineEvents() {
|
async function scrollTimelineEvents() {
|
||||||
const orderBy = "timestamp";
|
const orderBy = "timestamp";
|
||||||
const orderDirection = preferences.value.orderDirection === "asc" ? "asc" : "desc";
|
const orderDirection = preferences.value.orderDirection === "asc" ? "asc" : "desc";
|
||||||
|
|
||||||
|
@ -295,9 +267,9 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
// this is set last so Vue knows to re-render
|
// this is set last so Vue knows to re-render
|
||||||
timelineEvents.value.push(...events);
|
timelineEvents.value.push(...events);
|
||||||
};
|
}
|
||||||
|
|
||||||
async function initializeTimelineEvents() {
|
async function initializeTimelineEvents() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
ready.value = false;
|
ready.value = false;
|
||||||
|
|
||||||
|
@ -308,9 +280,9 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
ready.value = true;
|
ready.value = true;
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const infiniteScroll = useThrottleFn(() => {
|
const infiniteScroll = useThrottleFn(() => {
|
||||||
useAsyncData(useAsyncKey(), async () => {
|
useAsyncData(useAsyncKey(), async () => {
|
||||||
if (!hasMore.value || loading.value) {
|
if (!hasMore.value || loading.value) {
|
||||||
return;
|
return;
|
||||||
|
@ -320,12 +292,12 @@ export default defineNuxtComponent({
|
||||||
await scrollTimelineEvents();
|
await scrollTimelineEvents();
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// preload events
|
// preload events
|
||||||
initializeTimelineEvents();
|
initializeTimelineEvents();
|
||||||
|
|
||||||
onMounted(
|
onMounted(
|
||||||
() => {
|
() => {
|
||||||
document.onscroll = () => {
|
document.onscroll = () => {
|
||||||
// if the inner element is scrollable, let its scroll event handle the infiniteScroll
|
// if the inner element is scrollable, let its scroll event handle the infiniteScroll
|
||||||
|
@ -346,21 +318,5 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
|
||||||
deleteTimelineEvent,
|
|
||||||
filterBadgeCount,
|
|
||||||
loading,
|
|
||||||
onScroll,
|
|
||||||
preferences,
|
|
||||||
eventTypeFilterState,
|
|
||||||
recipes,
|
|
||||||
reverseSort,
|
|
||||||
toggleEventTypeOption,
|
|
||||||
timelineEvents,
|
|
||||||
updateTimelineEvent,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
bottom
|
location="bottom"
|
||||||
nudge-right="50"
|
nudge-right="50"
|
||||||
:color="buttonStyle ? 'info' : 'secondary'"
|
:color="buttonStyle ? 'info' : 'secondary'"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-btn
|
<v-btn
|
||||||
icon
|
icon
|
||||||
:variant="buttonStyle ? 'flat' : undefined"
|
:variant="buttonStyle ? 'flat' : undefined"
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
:color="buttonStyle ? 'info' : 'secondary'"
|
:color="buttonStyle ? 'info' : 'secondary'"
|
||||||
:fab="buttonStyle"
|
:fab="buttonStyle"
|
||||||
v-bind="{ ...props, ...$attrs }"
|
v-bind="{ ...activatorProps, ...$attrs }"
|
||||||
@click.prevent="toggleTimeline"
|
@click.prevent="toggleTimeline"
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-icon
|
||||||
|
@ -39,36 +39,29 @@
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeTimeline from "./RecipeTimeline.vue";
|
import RecipeTimeline from "./RecipeTimeline.vue";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: { RecipeTimeline },
|
buttonStyle?: boolean;
|
||||||
|
slug?: string;
|
||||||
|
recipeName?: string;
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
buttonStyle: false,
|
||||||
|
slug: "",
|
||||||
|
recipeName: "",
|
||||||
|
});
|
||||||
|
|
||||||
props: {
|
const i18n = useI18n();
|
||||||
buttonStyle: {
|
const { smAndDown } = useDisplay();
|
||||||
type: Boolean,
|
const showTimeline = ref(false);
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
slug: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
recipeName: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props) {
|
function toggleTimeline() {
|
||||||
const i18n = useI18n();
|
|
||||||
const { smAndDown } = useDisplay();
|
|
||||||
const showTimeline = ref(false);
|
|
||||||
function toggleTimeline() {
|
|
||||||
showTimeline.value = !showTimeline.value;
|
showTimeline.value = !showTimeline.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timelineAttrs = computed(() => {
|
const timelineAttrs = computed(() => {
|
||||||
let title = i18n.t("recipe.timeline");
|
let title = i18n.t("recipe.timeline");
|
||||||
if (smAndDown.value) {
|
if (smAndDown.value) {
|
||||||
title += ` – ${props.recipeName}`;
|
title += ` – ${props.recipeName}`;
|
||||||
|
@ -78,9 +71,5 @@ export default defineNuxtComponent({
|
||||||
title,
|
title,
|
||||||
queryFilter: `recipe.slug="${props.slug}"`,
|
queryFilter: `recipe.slug="${props.slug}"`,
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
return { showTimeline, timelineAttrs, toggleTimeline };
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
<v-row :class="useMobileFormat ? 'py-3 mx-0' : 'py-3 mx-0'" style="max-width: 100%">
|
<v-row :class="useMobileFormat ? 'py-3 mx-0' : 'py-3 mx-0'" style="max-width: 100%">
|
||||||
<v-col align-self="center" class="pa-0">
|
<v-col align-self="center" class="pa-0">
|
||||||
<RecipeCardMobile
|
<RecipeCardMobile
|
||||||
|
disable-highlight
|
||||||
:vertical="useMobileFormat"
|
:vertical="useMobileFormat"
|
||||||
:name="recipe.name"
|
:name="recipe.name"
|
||||||
:slug="recipe.slug"
|
:slug="recipe.slug"
|
||||||
|
@ -90,7 +91,7 @@
|
||||||
</v-timeline-item>
|
</v-timeline-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||||
import RecipeTimelineContextMenu from "./RecipeTimelineContextMenu.vue";
|
import RecipeTimelineContextMenu from "./RecipeTimelineContextMenu.vue";
|
||||||
import { useStaticRoutes } from "~/composables/api";
|
import { useStaticRoutes } from "~/composables/api";
|
||||||
|
@ -99,41 +100,37 @@ import type { Recipe, RecipeTimelineEventOut } from "~/lib/api/types/recipe";
|
||||||
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||||
import SafeMarkdown from "~/components/global/SafeMarkdown.vue";
|
import SafeMarkdown from "~/components/global/SafeMarkdown.vue";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
components: { RecipeCardMobile, RecipeTimelineContextMenu, UserAvatar, SafeMarkdown },
|
event: RecipeTimelineEventOut;
|
||||||
|
recipe?: Recipe;
|
||||||
|
showRecipeCards?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
props: {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
event: {
|
recipe: undefined,
|
||||||
type: Object as () => RecipeTimelineEventOut,
|
showRecipeCards: false,
|
||||||
required: true,
|
});
|
||||||
},
|
|
||||||
recipe: {
|
|
||||||
type: Object as () => Recipe,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
showRecipeCards: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["selected", "update", "delete"],
|
|
||||||
|
|
||||||
setup(props) {
|
defineEmits<{
|
||||||
const { $vuetify, $globals } = useNuxtApp();
|
selected: [];
|
||||||
const { recipeTimelineEventImage } = useStaticRoutes();
|
update: [];
|
||||||
const { eventTypeOptions } = useTimelineEventTypes();
|
delete: [];
|
||||||
const timelineEvents = ref([] as RecipeTimelineEventOut[]);
|
}>();
|
||||||
|
|
||||||
const { user: currentUser } = useMealieAuth();
|
const { $vuetify, $globals } = useNuxtApp();
|
||||||
|
const { recipeTimelineEventImage } = useStaticRoutes();
|
||||||
|
const { eventTypeOptions } = useTimelineEventTypes();
|
||||||
|
|
||||||
const route = useRoute();
|
const { user: currentUser } = useMealieAuth();
|
||||||
const groupSlug = computed(() => (route.params.groupSlug as string) || currentUser?.value?.groupSlug || "");
|
|
||||||
|
|
||||||
const useMobileFormat = computed(() => {
|
const route = useRoute();
|
||||||
|
const groupSlug = computed(() => (route.params.groupSlug as string) || currentUser?.value?.groupSlug || "");
|
||||||
|
|
||||||
|
const useMobileFormat = computed(() => {
|
||||||
return $vuetify.display.smAndDown.value;
|
return $vuetify.display.smAndDown.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
const attrs = computed(() => {
|
const attrs = computed(() => {
|
||||||
if (useMobileFormat.value) {
|
if (useMobileFormat.value) {
|
||||||
return {
|
return {
|
||||||
class: "px-0",
|
class: "px-0",
|
||||||
|
@ -162,33 +159,20 @@ export default defineNuxtComponent({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const icon = computed(() => {
|
const icon = computed(() => {
|
||||||
const option = eventTypeOptions.value.find(option => option.value === props.event.eventType);
|
const option = eventTypeOptions.value.find(option => option.value === props.event.eventType);
|
||||||
return option ? option.icon : $globals.icons.informationVariant;
|
return option ? option.icon : $globals.icons.informationVariant;
|
||||||
});
|
});
|
||||||
|
|
||||||
const hideImage = ref(false);
|
const hideImage = ref(false);
|
||||||
const eventImageUrl = computed<string>(() => {
|
const eventImageUrl = computed<string>(() => {
|
||||||
if (props.event.image !== "has image") {
|
if (props.event.image !== "has image") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return recipeTimelineEventImage(props.event.recipeId, props.event.id);
|
return recipeTimelineEventImage(props.event.recipeId, props.event.id);
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
attrs,
|
|
||||||
groupSlug,
|
|
||||||
icon,
|
|
||||||
eventImageUrl,
|
|
||||||
hideImage,
|
|
||||||
timelineEvents,
|
|
||||||
useMobileFormat,
|
|
||||||
currentUser,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="scaledAmount"
|
v-if="yieldDisplay"
|
||||||
class="d-flex align-center"
|
class="d-flex align-center"
|
||||||
>
|
>
|
||||||
<v-row
|
<v-row
|
||||||
|
@ -18,53 +18,49 @@
|
||||||
<p class="my-0 opacity-80">
|
<p class="my-0 opacity-80">
|
||||||
<span class="font-weight-bold">{{ $t("recipe.yield") }}</span><br>
|
<span class="font-weight-bold">{{ $t("recipe.yield") }}</span><br>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<span v-html="scaledAmount" /> {{ text }}
|
<span v-html="yieldDisplay" />
|
||||||
</p>
|
</p>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
|
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
yieldQuantity?: number;
|
||||||
yieldQuantity: {
|
yieldText?: string;
|
||||||
type: Number,
|
scale?: number;
|
||||||
default: 0,
|
color?: string;
|
||||||
},
|
}
|
||||||
yield: {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
type: String,
|
yieldQuantity: 0,
|
||||||
default: "",
|
yieldText: "",
|
||||||
},
|
scale: 1,
|
||||||
scale: {
|
color: "accent custom-transparent",
|
||||||
type: Number,
|
});
|
||||||
default: 1,
|
|
||||||
},
|
function sanitizeHTML(rawHtml: string) {
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: "accent custom-transparent",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
function sanitizeHTML(rawHtml: string) {
|
|
||||||
return DOMPurify.sanitize(rawHtml, {
|
return DOMPurify.sanitize(rawHtml, {
|
||||||
USE_PROFILES: { html: true },
|
USE_PROFILES: { html: true },
|
||||||
ALLOWED_TAGS: ["strong", "sup"],
|
ALLOWED_TAGS: ["strong", "sup"],
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const yieldDisplay = computed<string>(() => {
|
||||||
|
const components: string[] = [];
|
||||||
|
|
||||||
|
const { scaledAmountDisplay } = useScaledAmount(props.yieldQuantity, props.scale);
|
||||||
|
if (scaledAmountDisplay) {
|
||||||
|
components.push(scaledAmountDisplay);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scaledAmount = computed(() => {
|
const text = props.yieldText;
|
||||||
const { scaledAmountDisplay } = useScaledAmount(props.yieldQuantity, props.scale);
|
if (text) {
|
||||||
return scaledAmountDisplay;
|
components.push(text);
|
||||||
});
|
}
|
||||||
const text = sanitizeHTML(props.yield);
|
|
||||||
|
|
||||||
return {
|
return sanitizeHTML(components.join(" "));
|
||||||
scaledAmount,
|
|
||||||
text,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -40,7 +40,6 @@
|
||||||
v-if="requireAll != undefined"
|
v-if="requireAll != undefined"
|
||||||
v-model="requireAllValue"
|
v-model="requireAllValue"
|
||||||
density="compact"
|
density="compact"
|
||||||
size="small"
|
|
||||||
hide-details
|
hide-details
|
||||||
class="my-auto"
|
class="my-auto"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
|
|
@ -8,24 +8,23 @@
|
||||||
class="flex-nowrap align-center"
|
class="flex-nowrap align-center"
|
||||||
>
|
>
|
||||||
<v-col :cols="itemLabelCols">
|
<v-col :cols="itemLabelCols">
|
||||||
|
<div class="d-flex align-center flex-nowrap">
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="listItem.checked"
|
v-model="listItem.checked"
|
||||||
class="mt-0"
|
|
||||||
color="null"
|
|
||||||
hide-details
|
hide-details
|
||||||
density="compact"
|
density="compact"
|
||||||
:label="listItem.note!"
|
class="mt-0"
|
||||||
|
color="null"
|
||||||
@change="$emit('checked', listItem)"
|
@change="$emit('checked', listItem)"
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<div :class="listItem.checked ? 'strike-through' : ''">
|
|
||||||
<RecipeIngredientListItem
|
|
||||||
:ingredient="listItem"
|
|
||||||
:disable-amount="!(listItem.isFood || listItem.quantity !== 1)"
|
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
class="ml-2 text-truncate"
|
||||||
|
:class="listItem.checked ? 'strike-through' : ''"
|
||||||
|
style="min-width: 0;"
|
||||||
|
>
|
||||||
|
<RecipeIngredientListItem :ingredient="listItem" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</v-checkbox>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<v-col
|
<v-col
|
||||||
|
@ -57,7 +56,7 @@
|
||||||
open-delay="200"
|
open-delay="200"
|
||||||
transition="slide-x-reverse-transition"
|
transition="slide-x-reverse-transition"
|
||||||
density="compact"
|
density="compact"
|
||||||
right
|
location="end"
|
||||||
content-class="text-caption"
|
content-class="text-caption"
|
||||||
>
|
>
|
||||||
<template #activator="{ props: tooltipProps }">
|
<template #activator="{ props: tooltipProps }">
|
||||||
|
@ -170,7 +169,6 @@
|
||||||
@save="save"
|
@save="save"
|
||||||
@cancel="toggleEdit(false)"
|
@cancel="toggleEdit(false)"
|
||||||
@delete="$emit('delete')"
|
@delete="$emit('delete')"
|
||||||
@toggle-foods="localListItem.isFood = !localListItem.isFood"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div>
|
<div>
|
||||||
<v-card variant="outlined">
|
<v-card variant="outlined">
|
||||||
<v-card-text class="pb-3 pt-1">
|
<v-card-text class="pb-3 pt-1">
|
||||||
<div v-if="listItem.isFood" class="d-md-flex align-center mb-2" style="gap: 20px">
|
<div class="d-md-flex align-center mb-2" style="gap: 20px">
|
||||||
<div>
|
<div>
|
||||||
<InputQuantity v-model="listItem.quantity" />
|
<InputQuantity v-model="listItem.quantity" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,9 +26,6 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-md-flex align-center" style="gap: 20px">
|
<div class="d-md-flex align-center" style="gap: 20px">
|
||||||
<div v-if="!listItem.isFood">
|
|
||||||
<InputQuantity v-model="listItem.quantity" />
|
|
||||||
</div>
|
|
||||||
<v-textarea
|
<v-textarea
|
||||||
v-model="listItem.note"
|
v-model="listItem.note"
|
||||||
hide-details
|
hide-details
|
||||||
|
@ -99,11 +96,6 @@
|
||||||
text: $t('general.cancel'),
|
text: $t('general.cancel'),
|
||||||
event: 'cancel',
|
event: 'cancel',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: $globals.icons.foods,
|
|
||||||
text: $t('shopping-list.toggle-food'),
|
|
||||||
event: 'toggle-foods',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: $globals.icons.save,
|
icon: $globals.icons.save,
|
||||||
text: $t('general.save'),
|
text: $t('general.save'),
|
||||||
|
@ -113,7 +105,6 @@
|
||||||
@save="$emit('save')"
|
@save="$emit('save')"
|
||||||
@cancel="$emit('cancel')"
|
@cancel="$emit('cancel')"
|
||||||
@delete="$emit('delete')"
|
@delete="$emit('delete')"
|
||||||
@toggle-foods="listItem.isFood = !listItem.isFood"
|
|
||||||
/>
|
/>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
v-if="userId"
|
v-if="userId"
|
||||||
:disabled="!user || !tooltip"
|
:disabled="!user || !tooltip"
|
||||||
right
|
location="end"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<v-avatar
|
<v-avatar
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<v-btn
|
<v-btn
|
||||||
color="white"
|
color="white"
|
||||||
icon
|
icon
|
||||||
href="https://github.com/hay-kot/mealie"
|
href="https://github.com/mealie-recipes/mealie"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<v-icon>
|
<v-icon>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
ref="copyToolTip"
|
ref="copyToolTip"
|
||||||
v-model="show"
|
v-model="show"
|
||||||
top
|
location="top"
|
||||||
:open-on-hover="false"
|
:open-on-hover="false"
|
||||||
:open-on-click="true"
|
:open-on-click="true"
|
||||||
close-delay="500"
|
close-delay="500"
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
open-delay="200"
|
open-delay="200"
|
||||||
transition="slide-y-reverse-transition"
|
transition="slide-y-reverse-transition"
|
||||||
density="compact"
|
density="compact"
|
||||||
bottom
|
location="bottom"
|
||||||
content-class="text-caption"
|
content-class="text-caption"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="canSubmit"
|
v-if="canSubmit"
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="submitDisabled"
|
:disabled="submitDisabled || loading"
|
||||||
@click="submitEvent"
|
@click="submitEvent"
|
||||||
>
|
>
|
||||||
{{ submitText }}
|
{{ submitText }}
|
||||||
|
|
|
@ -22,10 +22,9 @@
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-for="itemValue in headers"
|
v-for="itemValue in localHeaders"
|
||||||
:key="itemValue.text + itemValue.show"
|
:key="itemValue.text + itemValue.show"
|
||||||
v-model="filteredHeaders"
|
v-model="itemValue.show"
|
||||||
:value="itemValue.value"
|
|
||||||
density="compact"
|
density="compact"
|
||||||
flat
|
flat
|
||||||
inset
|
inset
|
||||||
|
@ -172,12 +171,20 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
// ===========================================================
|
// ===========================================================
|
||||||
// Reactive Headers
|
// Reactive Headers
|
||||||
|
// Create a local reactive copy of headers that we can modify
|
||||||
|
const localHeaders = ref([...props.headers]);
|
||||||
|
|
||||||
|
// Watch for changes in props.headers and update local copy
|
||||||
|
watch(() => props.headers, (newHeaders) => {
|
||||||
|
localHeaders.value = [...newHeaders];
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
const filteredHeaders = computed<string[]>(() => {
|
const filteredHeaders = computed<string[]>(() => {
|
||||||
return props.headers.filter(header => header.show).map(header => header.value);
|
return localHeaders.value.filter(header => header.show).map(header => header.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
const headersWithoutActions = computed(() =>
|
const headersWithoutActions = computed(() =>
|
||||||
props.headers
|
localHeaders.value
|
||||||
.filter(header => filteredHeaders.value.includes(header.value))
|
.filter(header => filteredHeaders.value.includes(header.value))
|
||||||
.map(header => ({
|
.map(header => ({
|
||||||
...header,
|
...header,
|
||||||
|
@ -214,6 +221,7 @@ export default defineNuxtComponent({
|
||||||
return {
|
return {
|
||||||
sortBy,
|
sortBy,
|
||||||
selected,
|
selected,
|
||||||
|
localHeaders,
|
||||||
filteredHeaders,
|
filteredHeaders,
|
||||||
headersWithoutActions,
|
headersWithoutActions,
|
||||||
activeHeaders,
|
activeHeaders,
|
||||||
|
|
|
@ -7,13 +7,14 @@
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
{{ $t("language-dialog.select-description") }}
|
{{ $t("language-dialog.select-description") }}
|
||||||
<v-autocomplete
|
<v-autocomplete
|
||||||
v-model="locale"
|
v-model="selectedLocale"
|
||||||
:items="locales"
|
:items="locales"
|
||||||
item-title="name"
|
item-title="name"
|
||||||
|
item-value="value"
|
||||||
class="my-3"
|
class="my-3"
|
||||||
hide-details
|
hide-details
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
offset
|
@update:model-value="onLocaleSelect"
|
||||||
>
|
>
|
||||||
<template #item="{ item, props }">
|
<template #item="{ item, props }">
|
||||||
<div
|
<div
|
||||||
|
@ -59,6 +60,14 @@ export default defineNuxtComponent({
|
||||||
});
|
});
|
||||||
|
|
||||||
const { locales: LOCALES, locale, i18n } = useLocales();
|
const { locales: LOCALES, locale, i18n } = useLocales();
|
||||||
|
|
||||||
|
const selectedLocale = ref(locale.value);
|
||||||
|
const onLocaleSelect = (value: string) => {
|
||||||
|
if (value && locales.some(l => l.value === value)) {
|
||||||
|
locale.value = value as any;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
watch(locale, () => {
|
watch(locale, () => {
|
||||||
dialog.value = false; // Close dialog when locale changes
|
dialog.value = false; // Close dialog when locale changes
|
||||||
});
|
});
|
||||||
|
@ -72,6 +81,8 @@ export default defineNuxtComponent({
|
||||||
i18n,
|
i18n,
|
||||||
locales,
|
locales,
|
||||||
locale,
|
locale,
|
||||||
|
selectedLocale,
|
||||||
|
onLocaleSelect,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,10 +26,10 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue"],
|
emits: ["update:modelValue"],
|
||||||
setup(_, { emit }) {
|
setup(props, { emit }) {
|
||||||
function parseEvent(event: any): object {
|
function parseEvent(event: any): object {
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return {};
|
return props.modelValue || {};
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (event.json) {
|
if (event.json) {
|
||||||
|
@ -43,11 +43,14 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
return {};
|
return props.modelValue || {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onChange(event: any) {
|
function onChange(event: any) {
|
||||||
emit("update:modelValue", parseEvent(event));
|
const parsed = parseEvent(event);
|
||||||
|
if (parsed !== props.modelValue) {
|
||||||
|
emit("update:modelValue", parsed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
onChange,
|
onChange,
|
||||||
|
|
|
@ -18,8 +18,8 @@ function removeStartingPunctuation(word: string): string {
|
||||||
return word.replace(punctuationAtBeginning, "");
|
return word.replace(punctuationAtBeginning, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function ingredientMatchesWord(ingredient: RecipeIngredient, word: string, recipeIngredientAmountsDisabled: boolean) {
|
function ingredientMatchesWord(ingredient: RecipeIngredient, word: string) {
|
||||||
const searchText = parseIngredientText(ingredient, recipeIngredientAmountsDisabled);
|
const searchText = parseIngredientText(ingredient);
|
||||||
return searchText.toLowerCase().includes(word.toLowerCase());
|
return searchText.toLowerCase().includes(word.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ function isBlackListedWord(word: string) {
|
||||||
return blackListedText.includes(word) || word.match(blackListedRegexMatch);
|
return blackListedText.includes(word) || word.match(blackListedRegexMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useExtractIngredientReferences(recipeIngredients: RecipeIngredient[], activeRefs: string[], text: string, recipeIngredientAmountsDisabled: boolean): Set<string> {
|
export function useExtractIngredientReferences(recipeIngredients: RecipeIngredient[], activeRefs: string[], text: string): Set<string> {
|
||||||
const availableIngredients = recipeIngredients
|
const availableIngredients = recipeIngredients
|
||||||
.filter(ingredient => ingredient.referenceId !== undefined)
|
.filter(ingredient => ingredient.referenceId !== undefined)
|
||||||
.filter(ingredient => !activeRefs.includes(ingredient.referenceId as string));
|
.filter(ingredient => !activeRefs.includes(ingredient.referenceId as string));
|
||||||
|
@ -50,7 +50,7 @@ export function useExtractIngredientReferences(recipeIngredients: RecipeIngredie
|
||||||
.map(normalize)
|
.map(normalize)
|
||||||
.filter(word => word.length > 2)
|
.filter(word => word.length > 2)
|
||||||
.filter(word => !isBlackListedWord(word))
|
.filter(word => !isBlackListedWord(word))
|
||||||
.flatMap(word => availableIngredients.filter(ingredient => ingredientMatchesWord(ingredient, word, recipeIngredientAmountsDisabled)))
|
.flatMap(word => availableIngredients.filter(ingredient => ingredientMatchesWord(ingredient, word)))
|
||||||
.map(ingredient => ingredient.referenceId as string);
|
.map(ingredient => ingredient.referenceId as string);
|
||||||
// deduplicate
|
// deduplicate
|
||||||
|
|
||||||
|
|
|
@ -16,33 +16,27 @@ describe(parseIngredientText.name, () => {
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
test("uses ingredient note if disableAmount: true", () => {
|
|
||||||
const ingredient = createRecipeIngredient({ note: "foo" });
|
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, true)).toEqual("foo");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("adds note section if note present", () => {
|
test("adds note section if note present", () => {
|
||||||
const ingredient = createRecipeIngredient({ note: "custom note" });
|
const ingredient = createRecipeIngredient({ note: "custom note" });
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, false)).toContain("custom note");
|
expect(parseIngredientText(ingredient)).toContain("custom note");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ingredient text with fraction", () => {
|
test("ingredient text with fraction", () => {
|
||||||
const ingredient = createRecipeIngredient({ quantity: 1.5, unit: { fraction: true, id: "1", name: "cup" } });
|
const ingredient = createRecipeIngredient({ quantity: 1.5, unit: { fraction: true, id: "1", name: "cup" } });
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, false, 1, true)).contain("1<sup>1</sup>").and.to.contain("<sub>2</sub>");
|
expect(parseIngredientText(ingredient, 1, true)).contain("1<sup>1</sup>").and.to.contain("<sub>2</sub>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ingredient text with fraction when unit is null", () => {
|
test("ingredient text with fraction when unit is null", () => {
|
||||||
const ingredient = createRecipeIngredient({ quantity: 1.5, unit: undefined });
|
const ingredient = createRecipeIngredient({ quantity: 1.5, unit: undefined });
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, false, 1, true)).contain("1<sup>1</sup>").and.to.contain("<sub>2</sub>");
|
expect(parseIngredientText(ingredient, 1, true)).contain("1<sup>1</sup>").and.to.contain("<sub>2</sub>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("ingredient text with fraction no formatting", () => {
|
test("ingredient text with fraction no formatting", () => {
|
||||||
const ingredient = createRecipeIngredient({ quantity: 1.5, unit: { fraction: true, id: "1", name: "cup" } });
|
const ingredient = createRecipeIngredient({ quantity: 1.5, unit: { fraction: true, id: "1", name: "cup" } });
|
||||||
const result = parseIngredientText(ingredient, false, 1, false);
|
const result = parseIngredientText(ingredient, 1, false);
|
||||||
|
|
||||||
expect(result).not.contain("<");
|
expect(result).not.contain("<");
|
||||||
expect(result).not.contain(">");
|
expect(result).not.contain(">");
|
||||||
|
@ -52,7 +46,7 @@ describe(parseIngredientText.name, () => {
|
||||||
test("sanitizes html", () => {
|
test("sanitizes html", () => {
|
||||||
const ingredient = createRecipeIngredient({ note: "<script>alert('foo')</script>" });
|
const ingredient = createRecipeIngredient({ note: "<script>alert('foo')</script>" });
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, false)).not.toContain("<script>");
|
expect(parseIngredientText(ingredient)).not.toContain("<script>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("plural test : plural qty : use abbreviation", () => {
|
test("plural test : plural qty : use abbreviation", () => {
|
||||||
|
@ -62,7 +56,7 @@ describe(parseIngredientText.name, () => {
|
||||||
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, false)).toEqual("2 tbsps diced onions");
|
expect(parseIngredientText(ingredient)).toEqual("2 tbsps diced onions");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("plural test : plural qty : not abbreviation", () => {
|
test("plural test : plural qty : not abbreviation", () => {
|
||||||
|
@ -72,7 +66,7 @@ describe(parseIngredientText.name, () => {
|
||||||
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, false)).toEqual("2 tablespoons diced onions");
|
expect(parseIngredientText(ingredient)).toEqual("2 tablespoons diced onions");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("plural test : single qty : use abbreviation", () => {
|
test("plural test : single qty : use abbreviation", () => {
|
||||||
|
@ -82,7 +76,7 @@ describe(parseIngredientText.name, () => {
|
||||||
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, false)).toEqual("1 tbsp diced onion");
|
expect(parseIngredientText(ingredient)).toEqual("1 tbsp diced onion");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("plural test : single qty : not abbreviation", () => {
|
test("plural test : single qty : not abbreviation", () => {
|
||||||
|
@ -92,7 +86,7 @@ describe(parseIngredientText.name, () => {
|
||||||
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, false)).toEqual("1 tablespoon diced onion");
|
expect(parseIngredientText(ingredient)).toEqual("1 tablespoon diced onion");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("plural test : small qty : use abbreviation", () => {
|
test("plural test : small qty : use abbreviation", () => {
|
||||||
|
@ -102,7 +96,7 @@ describe(parseIngredientText.name, () => {
|
||||||
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, false)).toEqual("0.5 tbsp diced onion");
|
expect(parseIngredientText(ingredient)).toEqual("0.5 tbsp diced onion");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("plural test : small qty : not abbreviation", () => {
|
test("plural test : small qty : not abbreviation", () => {
|
||||||
|
@ -112,7 +106,7 @@ describe(parseIngredientText.name, () => {
|
||||||
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, false)).toEqual("0.5 tablespoon diced onion");
|
expect(parseIngredientText(ingredient)).toEqual("0.5 tablespoon diced onion");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("plural test : zero qty", () => {
|
test("plural test : zero qty", () => {
|
||||||
|
@ -122,7 +116,7 @@ describe(parseIngredientText.name, () => {
|
||||||
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, false)).toEqual("diced onions");
|
expect(parseIngredientText(ingredient)).toEqual("diced onions");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("plural test : single qty, scaled", () => {
|
test("plural test : single qty, scaled", () => {
|
||||||
|
@ -132,6 +126,6 @@ describe(parseIngredientText.name, () => {
|
||||||
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
food: { id: "1", name: "diced onion", pluralName: "diced onions" },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(parseIngredientText(ingredient, false, 2)).toEqual("2 tablespoons diced onions");
|
expect(parseIngredientText(ingredient, 2)).toEqual("2 tablespoons diced onions");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -36,16 +36,7 @@ function useUnitName(unit: CreateIngredientUnit | IngredientUnit | undefined, us
|
||||||
return returnVal;
|
return returnVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmount: boolean, scale = 1, includeFormating = true) {
|
export function useParsedIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true) {
|
||||||
if (disableAmount) {
|
|
||||||
return {
|
|
||||||
name: ingredient.note ? sanitizeIngredientHTML(ingredient.note) : undefined,
|
|
||||||
quantity: undefined,
|
|
||||||
unit: undefined,
|
|
||||||
note: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { quantity, food, unit, note } = ingredient;
|
const { quantity, food, unit, note } = ingredient;
|
||||||
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
|
const usePluralUnit = quantity !== undefined && ((quantity || 0) * scale > 1 || (quantity || 0) * scale === 0);
|
||||||
const usePluralFood = (!quantity) || quantity * scale > 1;
|
const usePluralFood = (!quantity) || quantity * scale > 1;
|
||||||
|
@ -82,8 +73,8 @@ export function useParsedIngredientText(ingredient: RecipeIngredient, disableAmo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseIngredientText(ingredient: RecipeIngredient, disableAmount: boolean, scale = 1, includeFormating = true): string {
|
export function parseIngredientText(ingredient: RecipeIngredient, scale = 1, includeFormating = true): string {
|
||||||
const { quantity, unit, name, note } = useParsedIngredientText(ingredient, disableAmount, scale, includeFormating);
|
const { quantity, unit, name, note } = useParsedIngredientText(ingredient, scale, includeFormating);
|
||||||
|
|
||||||
const text = `${quantity || ""} ${unit || ""} ${name || ""} ${note || ""}`.replace(/ {2,}/g, " ").trim();
|
const text = `${quantity || ""} ${unit || ""} ${name || ""} ${note || ""}`.replace(/ {2,}/g, " ").trim();
|
||||||
return sanitizeIngredientHTML(text);
|
return sanitizeIngredientHTML(text);
|
||||||
|
|
|
@ -90,6 +90,8 @@ export const useLazyRecipes = function (publicGroupSlug: string | null = null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRandom(query: RecipeSearchQuery | null = null, queryFilter: string | null = null) {
|
async function getRandom(query: RecipeSearchQuery | null = null, queryFilter: string | null = null) {
|
||||||
|
query = query || {};
|
||||||
|
query._searchSeed = query._searchSeed || Date.now().toString();
|
||||||
const { data } = await api.recipes.getAll(1, 1, getParams("random", "desc", null, query, queryFilter));
|
const { data } = await api.recipes.getAll(1, 1, getParams("random", "desc", null, query, queryFilter));
|
||||||
if (data?.items.length) {
|
if (data?.items.length) {
|
||||||
return data.items[0];
|
return data.items[0];
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { useStoreActions } from "./partials/use-actions-factory";
|
import { useStoreActions } from "./partials/use-actions-factory";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { useScaledAmount } from "~/composables/recipes/use-scaled-amount";
|
|
||||||
import type { GroupRecipeActionOut, GroupRecipeActionType } from "~/lib/api/types/household";
|
import type { GroupRecipeActionOut, GroupRecipeActionType } from "~/lib/api/types/household";
|
||||||
import type { RequestResponse } from "~/lib/api/types/non-generated";
|
import type { RequestResponse } from "~/lib/api/types/non-generated";
|
||||||
import type { Recipe } from "~/lib/api/types/recipe";
|
import type { Recipe } from "~/lib/api/types/recipe";
|
||||||
|
@ -68,7 +67,7 @@ export const useGroupRecipeActions = function (
|
||||||
window.open(url, "_blank")?.focus();
|
window.open(url, "_blank")?.focus();
|
||||||
return;
|
return;
|
||||||
case "post":
|
case "post":
|
||||||
return await api.groupRecipeActions.triggerAction(action.id, recipe.slug || "", useScaledAmount(recipe.recipeServings || 1, recipeScale).scaledAmount);
|
return await api.groupRecipeActions.triggerAction(action.id, recipe.slug || "", recipeScale);
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ export const useGroupWebhooks = function () {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
enabled: false,
|
enabled: true,
|
||||||
name: "New Webhook",
|
name: "New Webhook",
|
||||||
url: "",
|
url: "",
|
||||||
scheduledTime: "00:00",
|
scheduledTime: "00:00",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import type { GroupBase, GroupSummary } from "~/lib/api/types/user";
|
import type { GroupBase, GroupInDB, GroupSummary } from "~/lib/api/types/user";
|
||||||
|
|
||||||
const groupSelfRef = ref<GroupSummary | null>(null);
|
const groupSelfRef = ref<GroupSummary | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
@ -45,28 +45,11 @@ export const useGroupSelf = function () {
|
||||||
export const useGroups = function () {
|
export const useGroups = function () {
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const groups = ref<GroupInDB[] | null>(null);
|
||||||
|
|
||||||
function getAllGroups() {
|
async function getAllGroups() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const asyncKey = String(Date.now());
|
const { data } = await api.groups.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
||||||
const { data: groups } = useAsyncData(asyncKey, async () => {
|
|
||||||
const { data } = await api.groups.getAll(1, -1, { orderBy: "name", orderDirection: "asc" }); ;
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
return data.items;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshAllGroups() {
|
|
||||||
loading.value = true;
|
|
||||||
const { data } = await api.groups.getAll(1, -1, { orderBy: "name", orderDirection: "asc" }); ;
|
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
groups.value = data.items;
|
groups.value = data.items;
|
||||||
|
@ -78,11 +61,15 @@ export const useGroups = function () {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshAllGroups() {
|
||||||
|
await getAllGroups();
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteGroup(id: string | number) {
|
async function deleteGroup(id: string | number) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const { data } = await api.groups.deleteOne(id);
|
const { data } = await api.groups.deleteOne(id);
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
refreshAllGroups();
|
await refreshAllGroups();
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,9 +80,13 @@ export const useGroups = function () {
|
||||||
if (data && groups.value) {
|
if (data && groups.value) {
|
||||||
groups.value.push(data);
|
groups.value.push(data);
|
||||||
}
|
}
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups = getAllGroups();
|
// Initialize data on first call
|
||||||
|
if (!groups.value) {
|
||||||
|
getAllGroups();
|
||||||
|
}
|
||||||
|
|
||||||
return { groups, getAllGroups, refreshAllGroups, deleteGroup, createGroup };
|
return { groups, getAllGroups, refreshAllGroups, deleteGroup, createGroup };
|
||||||
};
|
};
|
||||||
|
|
|
@ -48,29 +48,12 @@ export const useHouseholdSelf = function () {
|
||||||
export const useAdminHouseholds = function () {
|
export const useAdminHouseholds = function () {
|
||||||
const api = useAdminApi();
|
const api = useAdminApi();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const households = ref<HouseholdInDB[] | null>(null);
|
||||||
|
|
||||||
function getAllHouseholds() {
|
async function getAllHouseholds() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const asyncKey = String(Date.now());
|
|
||||||
const { data: households } = useAsyncData(asyncKey, async () => {
|
|
||||||
const { data } = await api.households.getAll(1, -1, { orderBy: "name, group.name", orderDirection: "asc" });
|
const { data } = await api.households.getAll(1, -1, { orderBy: "name, group.name", orderDirection: "asc" });
|
||||||
|
|
||||||
if (data) {
|
|
||||||
return data.items;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loading.value = false;
|
|
||||||
return households;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshAllHouseholds() {
|
|
||||||
loading.value = true;
|
|
||||||
const { data } = await api.households.getAll(1, -1, { orderBy: "name, group.name", orderDirection: "asc" }); ;
|
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
households.value = data.items;
|
households.value = data.items;
|
||||||
}
|
}
|
||||||
|
@ -81,11 +64,15 @@ export const useAdminHouseholds = function () {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshAllHouseholds() {
|
||||||
|
await getAllHouseholds();
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteHousehold(id: string | number) {
|
async function deleteHousehold(id: string | number) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const { data } = await api.households.deleteOne(id);
|
const { data } = await api.households.deleteOne(id);
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
refreshAllHouseholds();
|
await refreshAllHouseholds();
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,9 +83,9 @@ export const useAdminHouseholds = function () {
|
||||||
if (data && households.value) {
|
if (data && households.value) {
|
||||||
households.value.push(data);
|
households.value.push(data);
|
||||||
}
|
}
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const households = getAllHouseholds();
|
|
||||||
function useHouseholdsInGroup(groupIdRef: Ref<string>) {
|
function useHouseholdsInGroup(groupIdRef: Ref<string>) {
|
||||||
return computed(
|
return computed(
|
||||||
() => {
|
() => {
|
||||||
|
@ -109,6 +96,10 @@ export const useAdminHouseholds = function () {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!households.value) {
|
||||||
|
getAllHouseholds();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
households,
|
households,
|
||||||
useHouseholdsInGroup,
|
useHouseholdsInGroup,
|
||||||
|
|
|
@ -3,13 +3,13 @@ export const LOCALES = [
|
||||||
{
|
{
|
||||||
name: "繁體中文 (Chinese traditional)",
|
name: "繁體中文 (Chinese traditional)",
|
||||||
value: "zh-TW",
|
value: "zh-TW",
|
||||||
progress: 8,
|
progress: 9,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "简体中文 (Chinese simplified)",
|
name: "简体中文 (Chinese simplified)",
|
||||||
value: "zh-CN",
|
value: "zh-CN",
|
||||||
progress: 33,
|
progress: 35,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -33,7 +33,7 @@ export const LOCALES = [
|
||||||
{
|
{
|
||||||
name: "Svenska (Swedish)",
|
name: "Svenska (Swedish)",
|
||||||
value: "sv-SE",
|
value: "sv-SE",
|
||||||
progress: 39,
|
progress: 50,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -57,7 +57,7 @@ export const LOCALES = [
|
||||||
{
|
{
|
||||||
name: "Pусский (Russian)",
|
name: "Pусский (Russian)",
|
||||||
value: "ru-RU",
|
value: "ru-RU",
|
||||||
progress: 35,
|
progress: 37,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -69,31 +69,31 @@ export const LOCALES = [
|
||||||
{
|
{
|
||||||
name: "Português (Portuguese)",
|
name: "Português (Portuguese)",
|
||||||
value: "pt-PT",
|
value: "pt-PT",
|
||||||
progress: 37,
|
progress: 38,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Português do Brasil (Brazilian Portuguese)",
|
name: "Português do Brasil (Brazilian Portuguese)",
|
||||||
value: "pt-BR",
|
value: "pt-BR",
|
||||||
progress: 34,
|
progress: 36,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Polski (Polish)",
|
name: "Polski (Polish)",
|
||||||
value: "pl-PL",
|
value: "pl-PL",
|
||||||
progress: 37,
|
progress: 39,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Norsk (Norwegian)",
|
name: "Norsk (Norwegian)",
|
||||||
value: "no-NO",
|
value: "no-NO",
|
||||||
progress: 37,
|
progress: 39,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nederlands (Dutch)",
|
name: "Nederlands (Dutch)",
|
||||||
value: "nl-NL",
|
value: "nl-NL",
|
||||||
progress: 39,
|
progress: 45,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -123,7 +123,7 @@ export const LOCALES = [
|
||||||
{
|
{
|
||||||
name: "Italiano (Italian)",
|
name: "Italiano (Italian)",
|
||||||
value: "it-IT",
|
value: "it-IT",
|
||||||
progress: 37,
|
progress: 38,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -135,7 +135,7 @@ export const LOCALES = [
|
||||||
{
|
{
|
||||||
name: "Magyar (Hungarian)",
|
name: "Magyar (Hungarian)",
|
||||||
value: "hu-HU",
|
value: "hu-HU",
|
||||||
progress: 38,
|
progress: 40,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -147,19 +147,19 @@ export const LOCALES = [
|
||||||
{
|
{
|
||||||
name: "עברית (Hebrew)",
|
name: "עברית (Hebrew)",
|
||||||
value: "he-IL",
|
value: "he-IL",
|
||||||
progress: 37,
|
progress: 73,
|
||||||
dir: "rtl",
|
dir: "rtl",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Galego (Galician)",
|
name: "Galego (Galician)",
|
||||||
value: "gl-ES",
|
value: "gl-ES",
|
||||||
progress: 37,
|
progress: 38,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Français (French)",
|
name: "Français (French)",
|
||||||
value: "fr-FR",
|
value: "fr-FR",
|
||||||
progress: 38,
|
progress: 50,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -189,7 +189,7 @@ export const LOCALES = [
|
||||||
{
|
{
|
||||||
name: "Español (Spanish)",
|
name: "Español (Spanish)",
|
||||||
value: "es-ES",
|
value: "es-ES",
|
||||||
progress: 37,
|
progress: 41,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -201,31 +201,31 @@ export const LOCALES = [
|
||||||
{
|
{
|
||||||
name: "British English",
|
name: "British English",
|
||||||
value: "en-GB",
|
value: "en-GB",
|
||||||
progress: 22,
|
progress: 23,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Ελληνικά (Greek)",
|
name: "Ελληνικά (Greek)",
|
||||||
value: "el-GR",
|
value: "el-GR",
|
||||||
progress: 37,
|
progress: 39,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Deutsch (German)",
|
name: "Deutsch (German)",
|
||||||
value: "de-DE",
|
value: "de-DE",
|
||||||
progress: 46,
|
progress: 65,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Dansk (Danish)",
|
name: "Dansk (Danish)",
|
||||||
value: "da-DK",
|
value: "da-DK",
|
||||||
progress: 37,
|
progress: 39,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Čeština (Czech)",
|
name: "Čeština (Czech)",
|
||||||
value: "cs-CZ",
|
value: "cs-CZ",
|
||||||
progress: 37,
|
progress: 40,
|
||||||
dir: "ltr",
|
dir: "ltr",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,8 +3,6 @@ import { LOCALES } from "./available-locales";
|
||||||
|
|
||||||
export const useLocales = () => {
|
export const useLocales = () => {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const { isRtl } = useRtl();
|
|
||||||
const { current: vuetifyLocale } = useLocale();
|
const { current: vuetifyLocale } = useLocale();
|
||||||
|
|
||||||
const locale = computed<LocaleObject["code"]>({
|
const locale = computed<LocaleObject["code"]>({
|
||||||
|
@ -13,18 +11,21 @@ export const useLocales = () => {
|
||||||
i18n.setLocale(value);
|
i18n.setLocale(value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function updateLocale(lc: LocaleObject["code"]) {
|
||||||
|
vuetifyLocale.value = lc;
|
||||||
|
}
|
||||||
|
|
||||||
// auto update vuetify locale
|
// auto update vuetify locale
|
||||||
watch(locale, (lc) => {
|
watch(locale, (lc) => {
|
||||||
vuetifyLocale.value = lc;
|
updateLocale(lc);
|
||||||
});
|
|
||||||
// auto update rtl
|
|
||||||
watch(vuetifyLocale, (vl) => {
|
|
||||||
const currentLocale = LOCALES.find(lc => lc.value === vl);
|
|
||||||
if (currentLocale) {
|
|
||||||
isRtl.value = currentLocale.dir === "rtl";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// set initial locale
|
||||||
|
if (i18n.locale.value) {
|
||||||
|
updateLocale(i18n.locale.value);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
locale,
|
locale,
|
||||||
locales: LOCALES,
|
locales: LOCALES,
|
||||||
|
|
|
@ -1,47 +1,48 @@
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
const datetimeFormats = {
|
const datetimeFormats = {
|
||||||
// CODE_GEN_ID: DATE_LOCALES
|
// CODE_GEN_ID: DATE_LOCALES
|
||||||
"hu-HU": require("./lang/dateTimeFormats/hu-HU.json"),
|
"af-ZA": require("./lang/dateTimeFormats/af-ZA.json"),
|
||||||
"no-NO": require("./lang/dateTimeFormats/no-NO.json"),
|
"ar-SA": require("./lang/dateTimeFormats/ar-SA.json"),
|
||||||
"nl-NL": require("./lang/dateTimeFormats/nl-NL.json"),
|
"bg-BG": require("./lang/dateTimeFormats/bg-BG.json"),
|
||||||
"pl-PL": require("./lang/dateTimeFormats/pl-PL.json"),
|
"ca-ES": require("./lang/dateTimeFormats/ca-ES.json"),
|
||||||
|
"cs-CZ": require("./lang/dateTimeFormats/cs-CZ.json"),
|
||||||
"da-DK": require("./lang/dateTimeFormats/da-DK.json"),
|
"da-DK": require("./lang/dateTimeFormats/da-DK.json"),
|
||||||
"fr-CA": require("./lang/dateTimeFormats/fr-CA.json"),
|
"de-DE": require("./lang/dateTimeFormats/de-DE.json"),
|
||||||
|
"el-GR": require("./lang/dateTimeFormats/el-GR.json"),
|
||||||
|
"en-GB": require("./lang/dateTimeFormats/en-GB.json"),
|
||||||
|
"en-US": require("./lang/dateTimeFormats/en-US.json"),
|
||||||
|
"es-ES": require("./lang/dateTimeFormats/es-ES.json"),
|
||||||
|
"et-EE": require("./lang/dateTimeFormats/et-EE.json"),
|
||||||
|
"fi-FI": require("./lang/dateTimeFormats/fi-FI.json"),
|
||||||
"fr-BE": require("./lang/dateTimeFormats/fr-BE.json"),
|
"fr-BE": require("./lang/dateTimeFormats/fr-BE.json"),
|
||||||
|
"fr-CA": require("./lang/dateTimeFormats/fr-CA.json"),
|
||||||
|
"fr-FR": require("./lang/dateTimeFormats/fr-FR.json"),
|
||||||
|
"gl-ES": require("./lang/dateTimeFormats/gl-ES.json"),
|
||||||
|
"he-IL": require("./lang/dateTimeFormats/he-IL.json"),
|
||||||
|
"hr-HR": require("./lang/dateTimeFormats/hr-HR.json"),
|
||||||
|
"hu-HU": require("./lang/dateTimeFormats/hu-HU.json"),
|
||||||
|
"is-IS": require("./lang/dateTimeFormats/is-IS.json"),
|
||||||
"it-IT": require("./lang/dateTimeFormats/it-IT.json"),
|
"it-IT": require("./lang/dateTimeFormats/it-IT.json"),
|
||||||
|
"ja-JP": require("./lang/dateTimeFormats/ja-JP.json"),
|
||||||
|
"ko-KR": require("./lang/dateTimeFormats/ko-KR.json"),
|
||||||
|
"lt-LT": require("./lang/dateTimeFormats/lt-LT.json"),
|
||||||
|
"lv-LV": require("./lang/dateTimeFormats/lv-LV.json"),
|
||||||
|
"nl-NL": require("./lang/dateTimeFormats/nl-NL.json"),
|
||||||
|
"no-NO": require("./lang/dateTimeFormats/no-NO.json"),
|
||||||
|
"pl-PL": require("./lang/dateTimeFormats/pl-PL.json"),
|
||||||
|
"pt-BR": require("./lang/dateTimeFormats/pt-BR.json"),
|
||||||
|
"pt-PT": require("./lang/dateTimeFormats/pt-PT.json"),
|
||||||
|
"ro-RO": require("./lang/dateTimeFormats/ro-RO.json"),
|
||||||
|
"ru-RU": require("./lang/dateTimeFormats/ru-RU.json"),
|
||||||
|
"sk-SK": require("./lang/dateTimeFormats/sk-SK.json"),
|
||||||
"sl-SI": require("./lang/dateTimeFormats/sl-SI.json"),
|
"sl-SI": require("./lang/dateTimeFormats/sl-SI.json"),
|
||||||
"sr-SP": require("./lang/dateTimeFormats/sr-SP.json"),
|
"sr-SP": require("./lang/dateTimeFormats/sr-SP.json"),
|
||||||
"is-IS": require("./lang/dateTimeFormats/is-IS.json"),
|
|
||||||
"ja-JP": require("./lang/dateTimeFormats/ja-JP.json"),
|
|
||||||
"fr-FR": require("./lang/dateTimeFormats/fr-FR.json"),
|
|
||||||
"ca-ES": require("./lang/dateTimeFormats/ca-ES.json"),
|
|
||||||
"tr-TR": require("./lang/dateTimeFormats/tr-TR.json"),
|
|
||||||
"fi-FI": require("./lang/dateTimeFormats/fi-FI.json"),
|
|
||||||
"hr-HR": require("./lang/dateTimeFormats/hr-HR.json"),
|
|
||||||
"pt-BR": require("./lang/dateTimeFormats/pt-BR.json"),
|
|
||||||
"sk-SK": require("./lang/dateTimeFormats/sk-SK.json"),
|
|
||||||
"zh-CN": require("./lang/dateTimeFormats/zh-CN.json"),
|
|
||||||
"pt-PT": require("./lang/dateTimeFormats/pt-PT.json"),
|
|
||||||
"en-GB": require("./lang/dateTimeFormats/en-GB.json"),
|
|
||||||
"ro-RO": require("./lang/dateTimeFormats/ro-RO.json"),
|
|
||||||
"cs-CZ": require("./lang/dateTimeFormats/cs-CZ.json"),
|
|
||||||
"en-US": require("./lang/dateTimeFormats/en-US.json"),
|
|
||||||
"lv-LV": require("./lang/dateTimeFormats/lv-LV.json"),
|
|
||||||
"ko-KR": require("./lang/dateTimeFormats/ko-KR.json"),
|
|
||||||
"bg-BG": require("./lang/dateTimeFormats/bg-BG.json"),
|
|
||||||
"gl-ES": require("./lang/dateTimeFormats/gl-ES.json"),
|
|
||||||
"de-DE": require("./lang/dateTimeFormats/de-DE.json"),
|
|
||||||
"lt-LT": require("./lang/dateTimeFormats/lt-LT.json"),
|
|
||||||
"ru-RU": require("./lang/dateTimeFormats/ru-RU.json"),
|
|
||||||
"he-IL": require("./lang/dateTimeFormats/he-IL.json"),
|
|
||||||
"el-GR": require("./lang/dateTimeFormats/el-GR.json"),
|
|
||||||
"zh-TW": require("./lang/dateTimeFormats/zh-TW.json"),
|
|
||||||
"af-ZA": require("./lang/dateTimeFormats/af-ZA.json"),
|
|
||||||
"es-ES": require("./lang/dateTimeFormats/es-ES.json"),
|
|
||||||
"sv-SE": require("./lang/dateTimeFormats/sv-SE.json"),
|
"sv-SE": require("./lang/dateTimeFormats/sv-SE.json"),
|
||||||
"ar-SA": require("./lang/dateTimeFormats/ar-SA.json"),
|
"tr-TR": require("./lang/dateTimeFormats/tr-TR.json"),
|
||||||
"vi-VN": require("./lang/dateTimeFormats/vi-VN.json"),
|
|
||||||
"uk-UA": require("./lang/dateTimeFormats/uk-UA.json"),
|
"uk-UA": require("./lang/dateTimeFormats/uk-UA.json"),
|
||||||
|
"vi-VN": require("./lang/dateTimeFormats/vi-VN.json"),
|
||||||
|
"zh-CN": require("./lang/dateTimeFormats/zh-CN.json"),
|
||||||
|
"zh-TW": require("./lang/dateTimeFormats/zh-TW.json"),
|
||||||
// END: DATE_LOCALES
|
// END: DATE_LOCALES
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
21
frontend/lang/dateTimeFormats/et-EE.json
Normal file
21
frontend/lang/dateTimeFormats/et-EE.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"short": {
|
||||||
|
"month": "short",
|
||||||
|
"day": "numeric",
|
||||||
|
"weekday": "long"
|
||||||
|
},
|
||||||
|
"medium": {
|
||||||
|
"month": "long",
|
||||||
|
"day": "numeric",
|
||||||
|
"weekday": "long",
|
||||||
|
"year": "numeric"
|
||||||
|
},
|
||||||
|
"long": {
|
||||||
|
"year": "numeric",
|
||||||
|
"month": "long",
|
||||||
|
"day": "numeric",
|
||||||
|
"weekday": "long",
|
||||||
|
"hour": "numeric",
|
||||||
|
"minute": "numeric"
|
||||||
|
}
|
||||||
|
}
|
|
@ -472,6 +472,7 @@
|
||||||
"comment": "Lewer kommentaar",
|
"comment": "Lewer kommentaar",
|
||||||
"comments": "Kommentaar",
|
"comments": "Kommentaar",
|
||||||
"delete-confirmation": "Is jy seker jy wil hierdie resep uitvee?",
|
"delete-confirmation": "Is jy seker jy wil hierdie resep uitvee?",
|
||||||
|
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
||||||
"delete-recipe": "Verwyder resep",
|
"delete-recipe": "Verwyder resep",
|
||||||
"description": "Beskrywing",
|
"description": "Beskrywing",
|
||||||
"disable-amount": "Skakel bestanddeelhoeveelhede af",
|
"disable-amount": "Skakel bestanddeelhoeveelhede af",
|
||||||
|
@ -579,9 +580,14 @@
|
||||||
"made-this": "Ek het dit gemaak",
|
"made-this": "Ek het dit gemaak",
|
||||||
"how-did-it-turn-out": "Hoe het dit uitgedraai?",
|
"how-did-it-turn-out": "Hoe het dit uitgedraai?",
|
||||||
"user-made-this": "{user} het dit gemaak",
|
"user-made-this": "{user} het dit gemaak",
|
||||||
|
"added-to-timeline": "Added to timeline",
|
||||||
|
"failed-to-add-to-timeline": "Failed to add to timeline",
|
||||||
|
"failed-to-update-recipe": "Failed to update recipe",
|
||||||
|
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
|
||||||
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
|
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
|
||||||
"message-key": "Boodskap sleutel",
|
"message-key": "Boodskap sleutel",
|
||||||
"parse": "Verwerk",
|
"parse": "Verwerk",
|
||||||
|
"ingredients-not-parsed-description": "It looks like your ingredients aren't parsed yet. Click the \"{parse}\" button below to parse your ingredients into structured foods.",
|
||||||
"attach-images-hint": "Voeg prente by deur dit in die bewerker te sleep en los",
|
"attach-images-hint": "Voeg prente by deur dit in die bewerker te sleep en los",
|
||||||
"drop-image": "Drop image",
|
"drop-image": "Drop image",
|
||||||
"enable-ingredient-amounts-to-use-this-feature": "Skakel bestanddeelhoeveelhede aan om hierdie funksie te gebruik",
|
"enable-ingredient-amounts-to-use-this-feature": "Skakel bestanddeelhoeveelhede aan om hierdie funksie te gebruik",
|
||||||
|
@ -658,6 +664,8 @@
|
||||||
"no-unit": "No unit",
|
"no-unit": "No unit",
|
||||||
"missing-unit": "Create missing unit: {unit}",
|
"missing-unit": "Create missing unit: {unit}",
|
||||||
"missing-food": "Create missing food: {food}",
|
"missing-food": "Create missing food: {food}",
|
||||||
|
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
|
||||||
|
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
|
||||||
"no-food": "No Food"
|
"no-food": "No Food"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Reset Servings Count",
|
"reset-servings-count": "Reset Servings Count",
|
||||||
|
|
|
@ -472,6 +472,7 @@
|
||||||
"comment": "أضف تعليق ",
|
"comment": "أضف تعليق ",
|
||||||
"comments": "التعليقات",
|
"comments": "التعليقات",
|
||||||
"delete-confirmation": "هل انت متأكد من رغبتك بحذف هذه الوصفة؟",
|
"delete-confirmation": "هل انت متأكد من رغبتك بحذف هذه الوصفة؟",
|
||||||
|
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
||||||
"delete-recipe": "حذف الوصفة",
|
"delete-recipe": "حذف الوصفة",
|
||||||
"description": "الوصف",
|
"description": "الوصف",
|
||||||
"disable-amount": "إيقاف إظهار كميات المكونات",
|
"disable-amount": "إيقاف إظهار كميات المكونات",
|
||||||
|
@ -579,9 +580,14 @@
|
||||||
"made-this": "لقد طبخت هذا",
|
"made-this": "لقد طبخت هذا",
|
||||||
"how-did-it-turn-out": "كيف كانت النتيجة؟",
|
"how-did-it-turn-out": "كيف كانت النتيجة؟",
|
||||||
"user-made-this": "{user} طبخ هذه",
|
"user-made-this": "{user} طبخ هذه",
|
||||||
|
"added-to-timeline": "Added to timeline",
|
||||||
|
"failed-to-add-to-timeline": "Failed to add to timeline",
|
||||||
|
"failed-to-update-recipe": "Failed to update recipe",
|
||||||
|
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
|
||||||
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
|
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
|
||||||
"message-key": "مفتاح الرساله",
|
"message-key": "مفتاح الرساله",
|
||||||
"parse": "تحليل",
|
"parse": "تحليل",
|
||||||
|
"ingredients-not-parsed-description": "It looks like your ingredients aren't parsed yet. Click the \"{parse}\" button below to parse your ingredients into structured foods.",
|
||||||
"attach-images-hint": "Attach images by dragging & dropping them into the editor",
|
"attach-images-hint": "Attach images by dragging & dropping them into the editor",
|
||||||
"drop-image": "وضع الصورة",
|
"drop-image": "وضع الصورة",
|
||||||
"enable-ingredient-amounts-to-use-this-feature": "Enable ingredient amounts to use this feature",
|
"enable-ingredient-amounts-to-use-this-feature": "Enable ingredient amounts to use this feature",
|
||||||
|
@ -658,6 +664,8 @@
|
||||||
"no-unit": "لا توجد وحدة",
|
"no-unit": "لا توجد وحدة",
|
||||||
"missing-unit": "إنشاء وحدة مفقودة: {unit}",
|
"missing-unit": "إنشاء وحدة مفقودة: {unit}",
|
||||||
"missing-food": "إنشاء طعام مفقود: {food}",
|
"missing-food": "إنشاء طعام مفقود: {food}",
|
||||||
|
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
|
||||||
|
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
|
||||||
"no-food": "لا يوجد طعام"
|
"no-food": "لا يوجد طعام"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "إعادة تعيين عدد الحصص",
|
"reset-servings-count": "إعادة تعيين عدد الحصص",
|
||||||
|
|
|
@ -472,6 +472,7 @@
|
||||||
"comment": "Коментар",
|
"comment": "Коментар",
|
||||||
"comments": "Коментари",
|
"comments": "Коментари",
|
||||||
"delete-confirmation": "Сигурни ли сте, че желаете да изтриете тази рецепта?",
|
"delete-confirmation": "Сигурни ли сте, че желаете да изтриете тази рецепта?",
|
||||||
|
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
||||||
"delete-recipe": "Изтрий рецептата",
|
"delete-recipe": "Изтрий рецептата",
|
||||||
"description": "Описание",
|
"description": "Описание",
|
||||||
"disable-amount": "Изключи количествата за съставките",
|
"disable-amount": "Изключи количествата за съставките",
|
||||||
|
@ -579,9 +580,14 @@
|
||||||
"made-this": "Сготвих рецептата",
|
"made-this": "Сготвих рецептата",
|
||||||
"how-did-it-turn-out": "Как се получи?",
|
"how-did-it-turn-out": "Как се получи?",
|
||||||
"user-made-this": "{user} направи това",
|
"user-made-this": "{user} направи това",
|
||||||
|
"added-to-timeline": "Added to timeline",
|
||||||
|
"failed-to-add-to-timeline": "Failed to add to timeline",
|
||||||
|
"failed-to-update-recipe": "Failed to update recipe",
|
||||||
|
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
|
||||||
"api-extras-description": "Екстрите за рецепти са ключова характеристика на Mealie API. Те Ви позволяват да създавате персонализирани JSON двойки ключ/стойност в рамките на рецепта, за да ги препращате към други приложения. Можете да използвате тези ключове, за да предоставите информация за задействане на автоматизация или персонализирани съобщения, за препращане към желаното от Вас устройство.",
|
"api-extras-description": "Екстрите за рецепти са ключова характеристика на Mealie API. Те Ви позволяват да създавате персонализирани JSON двойки ключ/стойност в рамките на рецепта, за да ги препращате към други приложения. Можете да използвате тези ключове, за да предоставите информация за задействане на автоматизация или персонализирани съобщения, за препращане към желаното от Вас устройство.",
|
||||||
"message-key": "Ключ на съобщението",
|
"message-key": "Ключ на съобщението",
|
||||||
"parse": "Анализирай",
|
"parse": "Анализирай",
|
||||||
|
"ingredients-not-parsed-description": "It looks like your ingredients aren't parsed yet. Click the \"{parse}\" button below to parse your ingredients into structured foods.",
|
||||||
"attach-images-hint": "Прикачете снимки като ги влачете и пуснете в редактора",
|
"attach-images-hint": "Прикачете снимки като ги влачете и пуснете в редактора",
|
||||||
"drop-image": "Премахване на изображение",
|
"drop-image": "Премахване на изображение",
|
||||||
"enable-ingredient-amounts-to-use-this-feature": "Пуснете количествата на съставките за да използвате функционалността",
|
"enable-ingredient-amounts-to-use-this-feature": "Пуснете количествата на съставките за да използвате функционалността",
|
||||||
|
@ -658,6 +664,8 @@
|
||||||
"no-unit": "No unit",
|
"no-unit": "No unit",
|
||||||
"missing-unit": "Create missing unit: {unit}",
|
"missing-unit": "Create missing unit: {unit}",
|
||||||
"missing-food": "Create missing food: {food}",
|
"missing-food": "Create missing food: {food}",
|
||||||
|
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
|
||||||
|
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
|
||||||
"no-food": "No Food"
|
"no-food": "No Food"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Reset Servings Count",
|
"reset-servings-count": "Reset Servings Count",
|
||||||
|
|
|
@ -472,6 +472,7 @@
|
||||||
"comment": "Comentari",
|
"comment": "Comentari",
|
||||||
"comments": "Comentaris",
|
"comments": "Comentaris",
|
||||||
"delete-confirmation": "Estàs segur que vols suprimir-la?",
|
"delete-confirmation": "Estàs segur que vols suprimir-la?",
|
||||||
|
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
||||||
"delete-recipe": "Suprimeix la recepta",
|
"delete-recipe": "Suprimeix la recepta",
|
||||||
"description": "Descripció",
|
"description": "Descripció",
|
||||||
"disable-amount": "Oculta les quantitats",
|
"disable-amount": "Oculta les quantitats",
|
||||||
|
@ -579,9 +580,14 @@
|
||||||
"made-this": "Ho he fet",
|
"made-this": "Ho he fet",
|
||||||
"how-did-it-turn-out": "Com ha sortit?",
|
"how-did-it-turn-out": "Com ha sortit?",
|
||||||
"user-made-this": "{user} ha fet això",
|
"user-made-this": "{user} ha fet això",
|
||||||
|
"added-to-timeline": "Added to timeline",
|
||||||
|
"failed-to-add-to-timeline": "Failed to add to timeline",
|
||||||
|
"failed-to-update-recipe": "Failed to update recipe",
|
||||||
|
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
|
||||||
"api-extras-description": "Els extres de receptes són una funcionalitat clau de l'API de Mealie. Permeten crear parells clau/valor JSON personalitzats dins una recepta, per referenciar-los des d'aplicacions de tercers. Pots emprar aquestes claus per proveir informació, per exemple per a desencadenar automatitzacions o missatges personlitzats per a propagar al teu dispositiu desitjat.",
|
"api-extras-description": "Els extres de receptes són una funcionalitat clau de l'API de Mealie. Permeten crear parells clau/valor JSON personalitzats dins una recepta, per referenciar-los des d'aplicacions de tercers. Pots emprar aquestes claus per proveir informació, per exemple per a desencadenar automatitzacions o missatges personlitzats per a propagar al teu dispositiu desitjat.",
|
||||||
"message-key": "Clau del missatge",
|
"message-key": "Clau del missatge",
|
||||||
"parse": "Analitzar",
|
"parse": "Analitzar",
|
||||||
|
"ingredients-not-parsed-description": "It looks like your ingredients aren't parsed yet. Click the \"{parse}\" button below to parse your ingredients into structured foods.",
|
||||||
"attach-images-hint": "Afegeix imatges arrossegant i deixant anar la imatge a l'editor",
|
"attach-images-hint": "Afegeix imatges arrossegant i deixant anar la imatge a l'editor",
|
||||||
"drop-image": "Deixa anar la imatge",
|
"drop-image": "Deixa anar la imatge",
|
||||||
"enable-ingredient-amounts-to-use-this-feature": "Habilita les quantitats d'ingredients per a poder fer servir aquesta característica",
|
"enable-ingredient-amounts-to-use-this-feature": "Habilita les quantitats d'ingredients per a poder fer servir aquesta característica",
|
||||||
|
@ -658,6 +664,8 @@
|
||||||
"no-unit": "Sense unitat",
|
"no-unit": "Sense unitat",
|
||||||
"missing-unit": "Crear unitat que manca: {unit}",
|
"missing-unit": "Crear unitat que manca: {unit}",
|
||||||
"missing-food": "Crear menjar que manca: {food}",
|
"missing-food": "Crear menjar que manca: {food}",
|
||||||
|
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
|
||||||
|
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
|
||||||
"no-food": "Sense menjar"
|
"no-food": "Sense menjar"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Reiniciar racions servides",
|
"reset-servings-count": "Reiniciar racions servides",
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"api-docs": "Dokumentace API",
|
"api-docs": "Dokumentace API",
|
||||||
"api-port": "API port",
|
"api-port": "API port",
|
||||||
"application-mode": "Režim aplikace",
|
"application-mode": "Režim aplikace",
|
||||||
"database-type": "Typ databáze",
|
"database-type": "Database Type",
|
||||||
"database-url": "URL databáze",
|
"database-url": "URL databáze",
|
||||||
"default-group": "Výchozí skupina",
|
"default-group": "Výchozí skupina",
|
||||||
"default-household": "Výchozí domácnost",
|
"default-household": "Výchozí domácnost",
|
||||||
|
@ -472,6 +472,7 @@
|
||||||
"comment": "Komentář",
|
"comment": "Komentář",
|
||||||
"comments": "Komentáře",
|
"comments": "Komentáře",
|
||||||
"delete-confirmation": "Opravdu chcete smazat tento recept?",
|
"delete-confirmation": "Opravdu chcete smazat tento recept?",
|
||||||
|
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
||||||
"delete-recipe": "Smazat recept",
|
"delete-recipe": "Smazat recept",
|
||||||
"description": "Popis",
|
"description": "Popis",
|
||||||
"disable-amount": "Nezobrazovat množství ingrediencí",
|
"disable-amount": "Nezobrazovat množství ingrediencí",
|
||||||
|
@ -579,9 +580,14 @@
|
||||||
"made-this": "Toto jsem uvařil",
|
"made-this": "Toto jsem uvařil",
|
||||||
"how-did-it-turn-out": "Jak to dopadlo?",
|
"how-did-it-turn-out": "Jak to dopadlo?",
|
||||||
"user-made-this": "{user} udělal toto",
|
"user-made-this": "{user} udělal toto",
|
||||||
|
"added-to-timeline": "Added to timeline",
|
||||||
|
"failed-to-add-to-timeline": "Failed to add to timeline",
|
||||||
|
"failed-to-update-recipe": "Failed to update recipe",
|
||||||
|
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
|
||||||
"api-extras-description": "Recepty jsou klíčovým rysem rozhraní pro API Mealie. Umožňují vytvářet vlastní klíče/hodnoty JSON v rámci receptu pro odkazy na aplikace třetích stran. Tyto klíče můžete použít pro poskytnutí informací, například pro aktivaci automatizace nebo vlastních zpráv pro přenos do požadovaného zařízení.",
|
"api-extras-description": "Recepty jsou klíčovým rysem rozhraní pro API Mealie. Umožňují vytvářet vlastní klíče/hodnoty JSON v rámci receptu pro odkazy na aplikace třetích stran. Tyto klíče můžete použít pro poskytnutí informací, například pro aktivaci automatizace nebo vlastních zpráv pro přenos do požadovaného zařízení.",
|
||||||
"message-key": "Klíč zprávy",
|
"message-key": "Klíč zprávy",
|
||||||
"parse": "Analyzovat",
|
"parse": "Analyzovat",
|
||||||
|
"ingredients-not-parsed-description": "It looks like your ingredients aren't parsed yet. Click the \"{parse}\" button below to parse your ingredients into structured foods.",
|
||||||
"attach-images-hint": "Přiložit obrázky přetažením jich do editoru",
|
"attach-images-hint": "Přiložit obrázky přetažením jich do editoru",
|
||||||
"drop-image": "Vložit obrázek",
|
"drop-image": "Vložit obrázek",
|
||||||
"enable-ingredient-amounts-to-use-this-feature": "Chcete-li tuto funkci používat, povolte množství ingrediencí",
|
"enable-ingredient-amounts-to-use-this-feature": "Chcete-li tuto funkci používat, povolte množství ingrediencí",
|
||||||
|
@ -658,13 +664,15 @@
|
||||||
"no-unit": "Žádná jednotka",
|
"no-unit": "Žádná jednotka",
|
||||||
"missing-unit": "Vytvořit chybějící jednotku: {unit}",
|
"missing-unit": "Vytvořit chybějící jednotku: {unit}",
|
||||||
"missing-food": "Vytvořit chybějící jídlo: {food}",
|
"missing-food": "Vytvořit chybějící jídlo: {food}",
|
||||||
|
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
|
||||||
|
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
|
||||||
"no-food": "Žádné jídlo"
|
"no-food": "Žádné jídlo"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Resetovat počet porcí",
|
"reset-servings-count": "Resetovat počet porcí",
|
||||||
"not-linked-ingredients": "Další ingredience",
|
"not-linked-ingredients": "Další ingredience",
|
||||||
"upload-another-image": "Upload another image",
|
"upload-another-image": "Nahrát další obrázek",
|
||||||
"upload-images": "Upload images",
|
"upload-images": "Nahrát obrázky",
|
||||||
"upload-more-images": "Upload more images"
|
"upload-more-images": "Nahrát více obrázků"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Vyhledávač receptů",
|
"recipe-finder": "Vyhledávač receptů",
|
||||||
|
|
|
@ -472,6 +472,7 @@
|
||||||
"comment": "Kommentar",
|
"comment": "Kommentar",
|
||||||
"comments": "Kommentarer",
|
"comments": "Kommentarer",
|
||||||
"delete-confirmation": "Er du sikker på, du vil slette denne opskrift?",
|
"delete-confirmation": "Er du sikker på, du vil slette denne opskrift?",
|
||||||
|
"admin-delete-confirmation": "Du er ved at slette en opskrift, som du ikke er ejer af ved at bruge administrator rettigheder. Er du sikker?",
|
||||||
"delete-recipe": "Slet opskrift",
|
"delete-recipe": "Slet opskrift",
|
||||||
"description": "Beskrivelse",
|
"description": "Beskrivelse",
|
||||||
"disable-amount": "Slå ingrediensmængder fra",
|
"disable-amount": "Slå ingrediensmængder fra",
|
||||||
|
@ -579,9 +580,14 @@
|
||||||
"made-this": "Jeg har lavet denne",
|
"made-this": "Jeg har lavet denne",
|
||||||
"how-did-it-turn-out": "Hvordan blev det?",
|
"how-did-it-turn-out": "Hvordan blev det?",
|
||||||
"user-made-this": "{user} lavede denne",
|
"user-made-this": "{user} lavede denne",
|
||||||
|
"added-to-timeline": "Tilføjet til tidslinjen",
|
||||||
|
"failed-to-add-to-timeline": "Kunne ikke tilføje til tidslinjen",
|
||||||
|
"failed-to-update-recipe": "Kunne ikke opdatere opskrift",
|
||||||
|
"added-to-timeline-but-failed-to-add-image": "Tilføjet til tidslinjen, men kunne ikke tilføje billede",
|
||||||
"api-extras-description": "Opskrifter ekstra er en central feature i Mealie API. De giver dig mulighed for at oprette brugerdefinerede JSON nøgle / værdi par inden for en opskrift, at henvise til fra 3. parts applikationer. Du kan bruge disse nøgler til at give oplysninger, for eksempel til at udløse automatiseringer eller brugerdefinerede beskeder til at videresende til din ønskede enhed.",
|
"api-extras-description": "Opskrifter ekstra er en central feature i Mealie API. De giver dig mulighed for at oprette brugerdefinerede JSON nøgle / værdi par inden for en opskrift, at henvise til fra 3. parts applikationer. Du kan bruge disse nøgler til at give oplysninger, for eksempel til at udløse automatiseringer eller brugerdefinerede beskeder til at videresende til din ønskede enhed.",
|
||||||
"message-key": "Beskednøgle",
|
"message-key": "Beskednøgle",
|
||||||
"parse": "Behandl data",
|
"parse": "Behandl data",
|
||||||
|
"ingredients-not-parsed-description": "It looks like your ingredients aren't parsed yet. Click the \"{parse}\" button below to parse your ingredients into structured foods.",
|
||||||
"attach-images-hint": "Vedhæft billeder ved at trække dem ind i redigeringsværktøjet",
|
"attach-images-hint": "Vedhæft billeder ved at trække dem ind i redigeringsværktøjet",
|
||||||
"drop-image": "Slet billede",
|
"drop-image": "Slet billede",
|
||||||
"enable-ingredient-amounts-to-use-this-feature": "Aktiver mængde af ingredienser for at bruge denne funktion",
|
"enable-ingredient-amounts-to-use-this-feature": "Aktiver mængde af ingredienser for at bruge denne funktion",
|
||||||
|
@ -599,10 +605,10 @@
|
||||||
"create-recipe-from-an-image": "Opret opskrift fra et billede",
|
"create-recipe-from-an-image": "Opret opskrift fra et billede",
|
||||||
"create-recipe-from-an-image-description": "Opret en opskrift ved at overføre et billede af den. Mealie vil forsøge at udtrække teksten fra billedet med AI og oprette en opskrift fra det.",
|
"create-recipe-from-an-image-description": "Opret en opskrift ved at overføre et billede af den. Mealie vil forsøge at udtrække teksten fra billedet med AI og oprette en opskrift fra det.",
|
||||||
"crop-and-rotate-the-image": "Beskær og roter billedet, så kun teksten er synlig, og det vises i den rigtige retning.",
|
"crop-and-rotate-the-image": "Beskær og roter billedet, så kun teksten er synlig, og det vises i den rigtige retning.",
|
||||||
"create-from-images": "Create from Images",
|
"create-from-images": "Opret fra billede",
|
||||||
"should-translate-description": "Oversæt opskriften til mit sprog",
|
"should-translate-description": "Oversæt opskriften til mit sprog",
|
||||||
"please-wait-image-procesing": "Vent venligst, billedet behandles. Dette kan tage lidt tid.",
|
"please-wait-image-procesing": "Vent venligst, billedet behandles. Dette kan tage lidt tid.",
|
||||||
"please-wait-images-processing": "Please wait, the images are processing. This may take some time.",
|
"please-wait-images-processing": "Vent venligst, billedet behandles. Dette kan tage lidt tid.",
|
||||||
"bulk-url-import": "Import fra flere URL-adresser",
|
"bulk-url-import": "Import fra flere URL-adresser",
|
||||||
"debug-scraper": "Fejlsøg indlæser",
|
"debug-scraper": "Fejlsøg indlæser",
|
||||||
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Opret en opskrift ved at angive navnet. Alle opskrifter skal have unikke navne.",
|
"create-a-recipe-by-providing-the-name-all-recipes-must-have-unique-names": "Opret en opskrift ved at angive navnet. Alle opskrifter skal have unikke navne.",
|
||||||
|
@ -658,13 +664,15 @@
|
||||||
"no-unit": "Ingen enhed",
|
"no-unit": "Ingen enhed",
|
||||||
"missing-unit": "Opret manglende måleenhed: {unit}",
|
"missing-unit": "Opret manglende måleenhed: {unit}",
|
||||||
"missing-food": "Opret manglende fødevare: {food}",
|
"missing-food": "Opret manglende fødevare: {food}",
|
||||||
|
"this-unit-could-not-be-parsed-automatically": "Denne enhed kunne ikke fortolkes automatisk",
|
||||||
|
"this-food-could-not-be-parsed-automatically": "Denne fødevare kunne ikke fortolkes automatisk",
|
||||||
"no-food": "Ingen fødevarer"
|
"no-food": "Ingen fødevarer"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Nulstil antal serveringer",
|
"reset-servings-count": "Nulstil antal serveringer",
|
||||||
"not-linked-ingredients": "Yderligere ingredienser",
|
"not-linked-ingredients": "Yderligere ingredienser",
|
||||||
"upload-another-image": "Upload another image",
|
"upload-another-image": "Upload et andet billede",
|
||||||
"upload-images": "Upload images",
|
"upload-images": "Upload billeder",
|
||||||
"upload-more-images": "Upload more images"
|
"upload-more-images": "Upload flere billeder"
|
||||||
},
|
},
|
||||||
"recipe-finder": {
|
"recipe-finder": {
|
||||||
"recipe-finder": "Opskriftssøger",
|
"recipe-finder": "Opskriftssøger",
|
||||||
|
@ -724,7 +732,7 @@
|
||||||
"backup-restore": "Backup / gendannelse",
|
"backup-restore": "Backup / gendannelse",
|
||||||
"back-restore-description": "Gendannelse af denne sikkerhedskopi vil overskrive alle de aktuelle data i din database og i datamappen og erstatte dem med indholdet af denne sikkerhedskopi. {cannot-be-undone} Hvis gendannelsen lykkes, vil du blive logget ud.",
|
"back-restore-description": "Gendannelse af denne sikkerhedskopi vil overskrive alle de aktuelle data i din database og i datamappen og erstatte dem med indholdet af denne sikkerhedskopi. {cannot-be-undone} Hvis gendannelsen lykkes, vil du blive logget ud.",
|
||||||
"cannot-be-undone": "Denne handling kan ikke fortrydes - brug med forsigtighed.",
|
"cannot-be-undone": "Denne handling kan ikke fortrydes - brug med forsigtighed.",
|
||||||
"postgresql-note": "If you are using PostgreSQL, please review the {backup-restore-process} prior to restoring.",
|
"postgresql-note": "Hvis du bruger PostgreSQL, så gennemse venligst {backup-restore-process} før du gendanner.",
|
||||||
"backup-restore-process-in-the-documentation": "backup/restoreproces i dokumentationen",
|
"backup-restore-process-in-the-documentation": "backup/restoreproces i dokumentationen",
|
||||||
"irreversible-acknowledgment": "Jeg forstår, at denne handling er irreversibel, destruktiv og kan forårsage tab af data",
|
"irreversible-acknowledgment": "Jeg forstår, at denne handling er irreversibel, destruktiv og kan forårsage tab af data",
|
||||||
"restore-backup": "Gendan sikkerhedskopi"
|
"restore-backup": "Gendan sikkerhedskopi"
|
||||||
|
|
|
@ -472,6 +472,7 @@
|
||||||
"comment": "Kommentar",
|
"comment": "Kommentar",
|
||||||
"comments": "Kommentare",
|
"comments": "Kommentare",
|
||||||
"delete-confirmation": "Bist du dir sicher, dass du dieses Rezept löschen möchtest?",
|
"delete-confirmation": "Bist du dir sicher, dass du dieses Rezept löschen möchtest?",
|
||||||
|
"admin-delete-confirmation": "Du bist dabei ein Rezept, welches nicht deins ist, mit Admin-Rechten zu löschen. Bist du sicher?",
|
||||||
"delete-recipe": "Rezept löschen",
|
"delete-recipe": "Rezept löschen",
|
||||||
"description": "Beschreibung",
|
"description": "Beschreibung",
|
||||||
"disable-amount": "Zutatenmenge deaktivieren",
|
"disable-amount": "Zutatenmenge deaktivieren",
|
||||||
|
@ -579,9 +580,14 @@
|
||||||
"made-this": "Ich hab's gemacht",
|
"made-this": "Ich hab's gemacht",
|
||||||
"how-did-it-turn-out": "Wie ist es geworden?",
|
"how-did-it-turn-out": "Wie ist es geworden?",
|
||||||
"user-made-this": "{user} hat's gemacht",
|
"user-made-this": "{user} hat's gemacht",
|
||||||
|
"added-to-timeline": "Zur Zeitleiste hinzugefügt",
|
||||||
|
"failed-to-add-to-timeline": "Fehler beim Hinzufügen zur Zeitleiste",
|
||||||
|
"failed-to-update-recipe": "Fehler beim Aktualisieren des Rezepts",
|
||||||
|
"added-to-timeline-but-failed-to-add-image": "Zur Zeitleiste hinzugefügt, Bild hinzufügen fehlgeschlagen",
|
||||||
"api-extras-description": "Rezepte-Extras sind ein Hauptmerkmal der Mealie API. Sie ermöglichen es dir, benutzerdefinierte JSON Key-Value-Paare zu einem Rezept zu erstellen, um Drittanbieter-Anwendungen zu steuern. Du kannst diese dazu verwenden, um Automatisierungen auszulösen oder benutzerdefinierte Nachrichten an bestimmte Geräte zu senden.",
|
"api-extras-description": "Rezepte-Extras sind ein Hauptmerkmal der Mealie API. Sie ermöglichen es dir, benutzerdefinierte JSON Key-Value-Paare zu einem Rezept zu erstellen, um Drittanbieter-Anwendungen zu steuern. Du kannst diese dazu verwenden, um Automatisierungen auszulösen oder benutzerdefinierte Nachrichten an bestimmte Geräte zu senden.",
|
||||||
"message-key": "Nachrichten-Schlüssel",
|
"message-key": "Nachrichten-Schlüssel",
|
||||||
"parse": "Parsen",
|
"parse": "Parsen",
|
||||||
|
"ingredients-not-parsed-description": "Es scheint als ob deine Zutaten noch nicht eingelesen wurden. Klick unten auf \"{parse}\" um deine Zutaten einzulesen.",
|
||||||
"attach-images-hint": "Bilder durch Ziehen & Ablegen in den Editor hinzufügen",
|
"attach-images-hint": "Bilder durch Ziehen & Ablegen in den Editor hinzufügen",
|
||||||
"drop-image": "Bild hier ablegen",
|
"drop-image": "Bild hier ablegen",
|
||||||
"enable-ingredient-amounts-to-use-this-feature": "Aktiviere Zutatenmengen, um diese Funktion zu nutzen",
|
"enable-ingredient-amounts-to-use-this-feature": "Aktiviere Zutatenmengen, um diese Funktion zu nutzen",
|
||||||
|
@ -658,6 +664,8 @@
|
||||||
"no-unit": "Keine Einheit",
|
"no-unit": "Keine Einheit",
|
||||||
"missing-unit": "Fehlende Einheit erstellen: {unit}",
|
"missing-unit": "Fehlende Einheit erstellen: {unit}",
|
||||||
"missing-food": "Fehlendes Lebensmittel erstellen: {food}",
|
"missing-food": "Fehlendes Lebensmittel erstellen: {food}",
|
||||||
|
"this-unit-could-not-be-parsed-automatically": "Diese Einheit konnte nicht automatisch analysiert werden",
|
||||||
|
"this-food-could-not-be-parsed-automatically": "Dieses Lebensmittel konnte nicht automatisch analysiert werden",
|
||||||
"no-food": "Kein Lebensmittel"
|
"no-food": "Kein Lebensmittel"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Portionen zurücksetzen",
|
"reset-servings-count": "Portionen zurücksetzen",
|
||||||
|
@ -1005,7 +1013,7 @@
|
||||||
"webhooks-enabled": "Webhooks aktiviert",
|
"webhooks-enabled": "Webhooks aktiviert",
|
||||||
"you-are-not-allowed-to-create-a-user": "Du bist nicht berechtigt, einen Benutzer anzulegen",
|
"you-are-not-allowed-to-create-a-user": "Du bist nicht berechtigt, einen Benutzer anzulegen",
|
||||||
"you-are-not-allowed-to-delete-this-user": "Du bist nicht berechtigt, diesen Benutzer zu entfernen",
|
"you-are-not-allowed-to-delete-this-user": "Du bist nicht berechtigt, diesen Benutzer zu entfernen",
|
||||||
"enable-advanced-content": "Erweiterten Inhalt aktivieren",
|
"enable-advanced-content": "Erweiterte Inhalte aktivieren",
|
||||||
"enable-advanced-content-description": "Aktiviert zusätzliche Funktionen wie Rezept-Skalierung, API-Schlüssel, Webhooks und Datenverwaltung. Keine Sorge, das kann später noch geändert werden.",
|
"enable-advanced-content-description": "Aktiviert zusätzliche Funktionen wie Rezept-Skalierung, API-Schlüssel, Webhooks und Datenverwaltung. Keine Sorge, das kann später noch geändert werden.",
|
||||||
"favorite-recipes": "Favoriten",
|
"favorite-recipes": "Favoriten",
|
||||||
"email-or-username": "E-Mail oder Benutzername",
|
"email-or-username": "E-Mail oder Benutzername",
|
||||||
|
|
|
@ -472,6 +472,7 @@
|
||||||
"comment": "Σχόλιο",
|
"comment": "Σχόλιο",
|
||||||
"comments": "Σχόλια",
|
"comments": "Σχόλια",
|
||||||
"delete-confirmation": "Θέλετε σίγουρα να διαγράψετε αυτή τη συνταγή;",
|
"delete-confirmation": "Θέλετε σίγουρα να διαγράψετε αυτή τη συνταγή;",
|
||||||
|
"admin-delete-confirmation": "Πρόκειται να διαγράψετε μια συνταγή που δεν είναι δική σας χρησιμοποιώντας δικαιώματα διαχειριστή. Είστε σίγουρος/η;",
|
||||||
"delete-recipe": "Διαγραφή Συνταγής",
|
"delete-recipe": "Διαγραφή Συνταγής",
|
||||||
"description": "Περιγραφή",
|
"description": "Περιγραφή",
|
||||||
"disable-amount": "Απενεργοποίηση Ποσοτήτων Συστατικών",
|
"disable-amount": "Απενεργοποίηση Ποσοτήτων Συστατικών",
|
||||||
|
@ -538,7 +539,7 @@
|
||||||
"date-format-hint-yyyy-mm-dd": "Μορφή ΕΕΕΕ-ΜΜ-ΗΗ",
|
"date-format-hint-yyyy-mm-dd": "Μορφή ΕΕΕΕ-ΜΜ-ΗΗ",
|
||||||
"add-to-list": "Προσθήκη σε λίστα",
|
"add-to-list": "Προσθήκη σε λίστα",
|
||||||
"add-to-plan": "Προσθήκη σε πρόγραμμα γευμάτων",
|
"add-to-plan": "Προσθήκη σε πρόγραμμα γευμάτων",
|
||||||
"add-to-timeline": "Προσθήκη στο χρονοδιάγραμμα",
|
"add-to-timeline": "Προσθήκη στο χρονολόγιο",
|
||||||
"recipe-added-to-list": "Η συνταγή προστέθηκε στη λίστα",
|
"recipe-added-to-list": "Η συνταγή προστέθηκε στη λίστα",
|
||||||
"recipes-added-to-list": "Οι συνταγές προστέθηκαν στη λίστα",
|
"recipes-added-to-list": "Οι συνταγές προστέθηκαν στη λίστα",
|
||||||
"successfully-added-to-list": "Επιτυχής προσθήκη στη λίστα",
|
"successfully-added-to-list": "Επιτυχής προσθήκη στη λίστα",
|
||||||
|
@ -570,18 +571,23 @@
|
||||||
"increase-scale-label": "Αύξηση κλίμακας κατά 1",
|
"increase-scale-label": "Αύξηση κλίμακας κατά 1",
|
||||||
"locked": "Κλειδωμένο",
|
"locked": "Κλειδωμένο",
|
||||||
"public-link": "Δημόσιος σύνδεσμος",
|
"public-link": "Δημόσιος σύνδεσμος",
|
||||||
"edit-timeline-event": "Επεξεργασία συμβάντος χρονοδιαγράμματος",
|
"edit-timeline-event": "Επεξεργασία συμβάντος χρονολόγιου",
|
||||||
"timeline": "Χρονοδιάγραμμα",
|
"timeline": "Χρονολόγιο",
|
||||||
"timeline-is-empty": "Δεν υπάρχει τίποτα ακόμα στο χρονοδιάγραμμα. Δοκιμάστε να κάνετε αυτή τη συνταγή!",
|
"timeline-is-empty": "Δεν υπάρχει τίποτα ακόμα στο χρονολόγιο. Δοκιμάστε να κάνετε αυτή τη συνταγή!",
|
||||||
"timeline-no-events-found-try-adjusting-filters": "Δεν βρέθηκαν συμβαντα. Δοκιμάστε να προσαρμόσετε τα φίλτρα αναζήτησης.",
|
"timeline-no-events-found-try-adjusting-filters": "Δεν βρέθηκαν συμβαντα. Δοκιμάστε να προσαρμόσετε τα φίλτρα αναζήτησης.",
|
||||||
"group-global-timeline": "Συνολικό χρονοδιάγραμμα {groupName}",
|
"group-global-timeline": "Συνολικό χρονολόγιο {groupName}",
|
||||||
"open-timeline": "Ανοιγμα χρονοδιαγράμματος",
|
"open-timeline": "Ανοιγμα χρονολόγιου",
|
||||||
"made-this": "Το έφτιαξα",
|
"made-this": "Το έφτιαξα",
|
||||||
"how-did-it-turn-out": "Ποιό ήταν το αποτέλεσμα;",
|
"how-did-it-turn-out": "Ποιό ήταν το αποτέλεσμα;",
|
||||||
"user-made-this": "Ο/η {user} το έφτιαξε αυτό",
|
"user-made-this": "Ο/η {user} το έφτιαξε αυτό",
|
||||||
|
"added-to-timeline": "Προστέθηκε στο χρονολόγιο",
|
||||||
|
"failed-to-add-to-timeline": "Αποτυχία προσθήκης στο χρονολόγιο",
|
||||||
|
"failed-to-update-recipe": "Αποτυχία ενημέρωσης συνταγής",
|
||||||
|
"added-to-timeline-but-failed-to-add-image": "Προστέθηκε στο χρονολόγιο, αλλά απέτυχε η προσθήκη εικόνας",
|
||||||
"api-extras-description": "Τα extras συνταγών αποτελούν βασικό χαρακτηριστικό του Mealie API. Σας επιτρέπουν να δημιουργήσετε προσαρμοσμένα ζεύγη κλειδιού/τιμής JSON μέσα σε μια συνταγή, να παραπέμψετε σε εφαρμογές τρίτων. Μπορείτε να χρησιμοποιήσετε αυτά τα κλειδιά για την παροχή πληροφοριών, για παράδειγμα πυροδότηση αυτοματισμών ή μετάδοση προσαρμοσμένων μηνυμάτων στη συσκευή που επιθυμείτε.",
|
"api-extras-description": "Τα extras συνταγών αποτελούν βασικό χαρακτηριστικό του Mealie API. Σας επιτρέπουν να δημιουργήσετε προσαρμοσμένα ζεύγη κλειδιού/τιμής JSON μέσα σε μια συνταγή, να παραπέμψετε σε εφαρμογές τρίτων. Μπορείτε να χρησιμοποιήσετε αυτά τα κλειδιά για την παροχή πληροφοριών, για παράδειγμα πυροδότηση αυτοματισμών ή μετάδοση προσαρμοσμένων μηνυμάτων στη συσκευή που επιθυμείτε.",
|
||||||
"message-key": "Κλειδί Μηνύματος",
|
"message-key": "Κλειδί Μηνύματος",
|
||||||
"parse": "Ανάλυση",
|
"parse": "Ανάλυση",
|
||||||
|
"ingredients-not-parsed-description": "Φαίνεται ότι τα συστατικά σας δεν έχουν αναλυθεί ακόμα. Κάντε κλικ στο κουμπί \"{parse}\" παρακάτω για να αναλύσετε τα συστατικά σας σε δομημένα τρόφιμα.",
|
||||||
"attach-images-hint": "Επισυνάψτε εικόνες σύροντας τις & αφήνοντάς τις στον επεξεργαστή",
|
"attach-images-hint": "Επισυνάψτε εικόνες σύροντας τις & αφήνοντάς τις στον επεξεργαστή",
|
||||||
"drop-image": "Απόθεση εικόνας",
|
"drop-image": "Απόθεση εικόνας",
|
||||||
"enable-ingredient-amounts-to-use-this-feature": "Ενεργοποιήστε τις ποσότητες συστατικών για να χρησιμοποιήσετε αυτήν τη δυνατότητα",
|
"enable-ingredient-amounts-to-use-this-feature": "Ενεργοποιήστε τις ποσότητες συστατικών για να χρησιμοποιήσετε αυτήν τη δυνατότητα",
|
||||||
|
@ -658,6 +664,8 @@
|
||||||
"no-unit": "Καμία μονάδα",
|
"no-unit": "Καμία μονάδα",
|
||||||
"missing-unit": "Δημιουργία μονάδας που λείπει: {unit}",
|
"missing-unit": "Δημιουργία μονάδας που λείπει: {unit}",
|
||||||
"missing-food": "Δημιουργία τροφίμου που λείπει: {food}",
|
"missing-food": "Δημιουργία τροφίμου που λείπει: {food}",
|
||||||
|
"this-unit-could-not-be-parsed-automatically": "Δεν ήταν δυνατή η αυτόματη ανάλυση αυτής της μονάδας",
|
||||||
|
"this-food-could-not-be-parsed-automatically": "Δεν ήταν δυνατή η αυτόματη ανάλυση αυτού του φαγητού",
|
||||||
"no-food": "Χωρίς Τρόφιμο"
|
"no-food": "Χωρίς Τρόφιμο"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Επαναφορά μέτρησης μερίδων",
|
"reset-servings-count": "Επαναφορά μέτρησης μερίδων",
|
||||||
|
|
|
@ -472,6 +472,7 @@
|
||||||
"comment": "Comment",
|
"comment": "Comment",
|
||||||
"comments": "Comments",
|
"comments": "Comments",
|
||||||
"delete-confirmation": "Are you sure you want to delete this recipe?",
|
"delete-confirmation": "Are you sure you want to delete this recipe?",
|
||||||
|
"admin-delete-confirmation": "You're about to delete a recipe that isn't yours using admin permissions. Are you sure?",
|
||||||
"delete-recipe": "Delete Recipe",
|
"delete-recipe": "Delete Recipe",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"disable-amount": "Disable Ingredient Amounts",
|
"disable-amount": "Disable Ingredient Amounts",
|
||||||
|
@ -579,9 +580,14 @@
|
||||||
"made-this": "I Made This",
|
"made-this": "I Made This",
|
||||||
"how-did-it-turn-out": "How did it turn out?",
|
"how-did-it-turn-out": "How did it turn out?",
|
||||||
"user-made-this": "{user} made this",
|
"user-made-this": "{user} made this",
|
||||||
|
"added-to-timeline": "Added to timeline",
|
||||||
|
"failed-to-add-to-timeline": "Failed to add to timeline",
|
||||||
|
"failed-to-update-recipe": "Failed to update recipe",
|
||||||
|
"added-to-timeline-but-failed-to-add-image": "Added to timeline, but failed to add image",
|
||||||
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
|
"api-extras-description": "Recipes extras are a key feature of the Mealie API. They allow you to create custom JSON key/value pairs within a recipe, to reference from 3rd party applications. You can use these keys to provide information, for example to trigger automations or custom messages to relay to your desired device.",
|
||||||
"message-key": "Message Key",
|
"message-key": "Message Key",
|
||||||
"parse": "Parse",
|
"parse": "Parse",
|
||||||
|
"ingredients-not-parsed-description": "It looks like your ingredients aren't parsed yet. Click the \"{parse}\" button below to parse your ingredients into structured foods.",
|
||||||
"attach-images-hint": "Attach images by dragging & dropping them into the editor",
|
"attach-images-hint": "Attach images by dragging & dropping them into the editor",
|
||||||
"drop-image": "Drop image",
|
"drop-image": "Drop image",
|
||||||
"enable-ingredient-amounts-to-use-this-feature": "Enable ingredient amounts to use this feature",
|
"enable-ingredient-amounts-to-use-this-feature": "Enable ingredient amounts to use this feature",
|
||||||
|
@ -658,6 +664,8 @@
|
||||||
"no-unit": "No unit",
|
"no-unit": "No unit",
|
||||||
"missing-unit": "Create missing unit: {unit}",
|
"missing-unit": "Create missing unit: {unit}",
|
||||||
"missing-food": "Create missing food: {food}",
|
"missing-food": "Create missing food: {food}",
|
||||||
|
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
|
||||||
|
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
|
||||||
"no-food": "No Food"
|
"no-food": "No Food"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Reset Servings Count",
|
"reset-servings-count": "Reset Servings Count",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue