mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-21 22:13:31 -07:00
Merge branch 'mealie-next' into mealie-next
This commit is contained in:
commit
76ff81f508
287 changed files with 16340 additions and 17006 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": [],
|
||||||
|
|
|
@ -20,7 +20,7 @@ RUN yarn generate
|
||||||
###############################################
|
###############################################
|
||||||
# Base Image - Python
|
# Base Image - Python
|
||||||
###############################################
|
###############################################
|
||||||
FROM python:3.12-slim as python-base
|
FROM python:3.12-slim AS python-base
|
||||||
|
|
||||||
ENV MEALIE_HOME="/app"
|
ENV MEALIE_HOME="/app"
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ RUN . $VENV_PATH/bin/activate \
|
||||||
###############################################
|
###############################################
|
||||||
# Production Image
|
# Production Image
|
||||||
###############################################
|
###############################################
|
||||||
FROM python-base as production
|
FROM python-base AS production
|
||||||
LABEL org.opencontainers.image.source="https://github.com/mealie-recipes/mealie"
|
LABEL org.opencontainers.image.source="https://github.com/mealie-recipes/mealie"
|
||||||
ENV PRODUCTION=true
|
ENV PRODUCTION=true
|
||||||
ENV TESTING=false
|
ENV TESTING=false
|
||||||
|
|
|
@ -13,7 +13,7 @@ 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
|
||||||
|
|
||||||
|
|
|
@ -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:v2.8.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:v2.8.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:v2.8.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
|
||||||
|
|
|
@ -1,390 +0,0 @@
|
||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-100-cyrillic-ext1.woff2") format("woff2");
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-100-cyrillic2.woff2") format("woff2");
|
|
||||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* greek-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-100-greek-ext3.woff2") format("woff2");
|
|
||||||
unicode-range: U+1F00-1FFF;
|
|
||||||
}
|
|
||||||
/* greek */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-100-greek4.woff2") format("woff2");
|
|
||||||
unicode-range: U+0370-03FF;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-100-vietnamese5.woff2") format("woff2");
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-100-latin-ext6.woff2") format("woff2");
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-100-latin7.woff2") format("woff2");
|
|
||||||
unicode-range:
|
|
||||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
|
||||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-300-cyrillic-ext8.woff2") format("woff2");
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-300-cyrillic9.woff2") format("woff2");
|
|
||||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* greek-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-300-greek-ext10.woff2") format("woff2");
|
|
||||||
unicode-range: U+1F00-1FFF;
|
|
||||||
}
|
|
||||||
/* greek */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-300-greek11.woff2") format("woff2");
|
|
||||||
unicode-range: U+0370-03FF;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-300-vietnamese12.woff2") format("woff2");
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-300-latin-ext13.woff2") format("woff2");
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-300-latin14.woff2") format("woff2");
|
|
||||||
unicode-range:
|
|
||||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
|
||||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-400-cyrillic-ext15.woff2") format("woff2");
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-400-cyrillic16.woff2") format("woff2");
|
|
||||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* greek-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-400-greek-ext17.woff2") format("woff2");
|
|
||||||
unicode-range: U+1F00-1FFF;
|
|
||||||
}
|
|
||||||
/* greek */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-400-greek18.woff2") format("woff2");
|
|
||||||
unicode-range: U+0370-03FF;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-400-vietnamese19.woff2") format("woff2");
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-400-latin-ext20.woff2") format("woff2");
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-400-latin21.woff2") format("woff2");
|
|
||||||
unicode-range:
|
|
||||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
|
||||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-500-cyrillic-ext22.woff2") format("woff2");
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-500-cyrillic23.woff2") format("woff2");
|
|
||||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* greek-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-500-greek-ext24.woff2") format("woff2");
|
|
||||||
unicode-range: U+1F00-1FFF;
|
|
||||||
}
|
|
||||||
/* greek */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-500-greek25.woff2") format("woff2");
|
|
||||||
unicode-range: U+0370-03FF;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-500-vietnamese26.woff2") format("woff2");
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-500-latin-ext27.woff2") format("woff2");
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-500-latin28.woff2") format("woff2");
|
|
||||||
unicode-range:
|
|
||||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
|
||||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-700-cyrillic-ext29.woff2") format("woff2");
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-700-cyrillic30.woff2") format("woff2");
|
|
||||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* greek-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-700-greek-ext31.woff2") format("woff2");
|
|
||||||
unicode-range: U+1F00-1FFF;
|
|
||||||
}
|
|
||||||
/* greek */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-700-greek32.woff2") format("woff2");
|
|
||||||
unicode-range: U+0370-03FF;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-700-vietnamese33.woff2") format("woff2");
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-700-latin-ext34.woff2") format("woff2");
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-700-latin35.woff2") format("woff2");
|
|
||||||
unicode-range:
|
|
||||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
|
||||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
/* cyrillic-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-900-cyrillic-ext36.woff2") format("woff2");
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
/* cyrillic */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-900-cyrillic37.woff2") format("woff2");
|
|
||||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
/* greek-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-900-greek-ext38.woff2") format("woff2");
|
|
||||||
unicode-range: U+1F00-1FFF;
|
|
||||||
}
|
|
||||||
/* greek */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-900-greek39.woff2") format("woff2");
|
|
||||||
unicode-range: U+0370-03FF;
|
|
||||||
}
|
|
||||||
/* vietnamese */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-900-vietnamese40.woff2") format("woff2");
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
/* latin-ext */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-900-latin-ext41.woff2") format("woff2");
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: "Roboto";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 900;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("~assets/fonts/Roboto-900-latin42.woff2") format("woff2");
|
|
||||||
unicode-range:
|
|
||||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
|
|
||||||
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -44,33 +44,17 @@
|
||||||
</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 },
|
|
||||||
props: {
|
|
||||||
modelValue: {
|
|
||||||
type: Object as () => ReadCookBook,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
type: Object as () => any,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const cookbook = toRef(modelValue);
|
||||||
const cookbook = toRef(() => props.modelValue);
|
|
||||||
|
|
||||||
function handleInput(value: string | undefined) {
|
function handleInput(value: string | undefined) {
|
||||||
cookbook.value.queryFilterString = value || "";
|
cookbook.value.queryFilterString = value || "";
|
||||||
emit("update:modelValue", cookbook.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldDefs: FieldDefinition[] = [
|
const fieldDefs: FieldDefinition[] = [
|
||||||
|
@ -110,12 +94,4 @@ export default defineNuxtComponent({
|
||||||
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,9 +73,6 @@ 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({
|
|
||||||
components: { RecipeCardSection, CookbookEditor },
|
|
||||||
setup() {
|
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
|
@ -89,7 +85,6 @@ export default defineNuxtComponent({
|
||||||
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(() => {
|
||||||
|
@ -132,23 +127,4 @@ export default defineNuxtComponent({
|
||||||
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,18 +20,14 @@
|
||||||
</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 i18n = useI18n();
|
||||||
|
|
||||||
const headers = [
|
const headers = [
|
||||||
|
@ -53,12 +49,4 @@ export default defineNuxtComponent({
|
||||||
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,33 +64,25 @@ 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,
|
const emit = defineEmits<{
|
||||||
},
|
[key: string]: [];
|
||||||
color: {
|
}>();
|
||||||
type: String,
|
|
||||||
default: "primary",
|
|
||||||
},
|
|
||||||
menuIcon: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props, context) {
|
|
||||||
const { mdAndUp } = useDisplay();
|
const { mdAndUp } = useDisplay();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
@ -111,6 +103,8 @@ export default defineNuxtComponent({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { shoppingListDialog, menuItems } = toRefs(state);
|
||||||
|
|
||||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||||
|
|
||||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||||
|
@ -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,53 +19,38 @@
|
||||||
<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: {
|
const day = defineModel<string>("day", { default: "unset" });
|
||||||
type: String,
|
const entryType = defineModel<string>("entryType", { default: "unset" });
|
||||||
default: "unset",
|
const queryFilterString = defineModel<string>("queryFilterString", { default: "" });
|
||||||
},
|
|
||||||
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 i18n = useI18n();
|
||||||
|
|
||||||
const MEAL_TYPE_OPTIONS = [
|
const MEAL_TYPE_OPTIONS = [
|
||||||
|
@ -87,36 +72,10 @@ export default defineNuxtComponent({
|
||||||
{ title: i18n.t("meal-plan.day-any"), value: "unset" },
|
{ title: i18n.t("meal-plan.day-any"), value: "unset" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const inputDay = computed({
|
|
||||||
get: () => {
|
|
||||||
return props.day;
|
|
||||||
},
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:day", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputEntryType = computed({
|
|
||||||
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) {
|
function handleQueryFilterInput(value: string | undefined) {
|
||||||
inputQueryFilterString.value = value || "";
|
console.warn("handleQueryFilterInput called with value:", value);
|
||||||
};
|
queryFilterString.value = value || "";
|
||||||
|
}
|
||||||
|
|
||||||
const fieldDefs: FieldDefinition[] = [
|
const fieldDefs: FieldDefinition[] = [
|
||||||
{
|
{
|
||||||
|
@ -160,16 +119,4 @@ export default defineNuxtComponent({
|
||||||
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,19 +50,20 @@
|
||||||
</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,
|
const emit = defineEmits<{
|
||||||
},
|
delete: [id: string];
|
||||||
},
|
save: [webhook: ReadWebhook];
|
||||||
emits: ["delete", "save", "test"],
|
test: [id: string];
|
||||||
setup(props, { emit }) {
|
}>();
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const itemUTC = ref<string>(props.webhook.scheduledTime);
|
const itemUTC = ref<string>(props.webhook.scheduledTime);
|
||||||
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
|
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
|
||||||
|
@ -88,14 +89,4 @@ export default defineNuxtComponent({
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: i18n.t("settings.webhooks.webhooks"),
|
title: i18n.t("settings.webhooks.webhooks"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
|
||||||
webhookCopy,
|
|
||||||
scheduledTime,
|
|
||||||
handleSave,
|
|
||||||
itemUTC,
|
|
||||||
itemLocal,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -41,18 +41,10 @@
|
||||||
</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: {
|
|
||||||
modelValue: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
type Preference = {
|
type Preference = {
|
||||||
|
@ -87,11 +79,6 @@ 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 = [
|
||||||
|
@ -124,23 +111,6 @@ export default defineNuxtComponent({
|
||||||
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,9 +316,13 @@ export default defineNuxtComponent({
|
||||||
type: Object as () => QueryFilterJSON | null,
|
type: Object as () => QueryFilterJSON | null,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
emits: ["input", "inputJSON"],
|
|
||||||
setup(props, context) {
|
const emit = defineEmits<{
|
||||||
|
(event: "input", value: string | undefined): void;
|
||||||
|
(event: "inputJSON", value: QueryFilterJSON | undefined): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
const { household } = useHouseholdSelf();
|
const { household } = useHouseholdSelf();
|
||||||
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
||||||
|
|
||||||
|
@ -337,6 +336,7 @@ export default defineNuxtComponent({
|
||||||
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(),
|
||||||
|
@ -369,7 +369,7 @@ export default defineNuxtComponent({
|
||||||
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;
|
||||||
|
@ -419,15 +419,16 @@ export default defineNuxtComponent({
|
||||||
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) => {
|
||||||
|
@ -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,8 +466,9 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
return organizer;
|
return organizer;
|
||||||
});
|
});
|
||||||
|
|
||||||
field.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[];
|
field.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[];
|
||||||
setOrganizerValues(field, index, field.organizers);
|
return field;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initFieldsError(error = "") {
|
function initFieldsError(error = "") {
|
||||||
|
@ -482,14 +482,15 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,14 +562,16 @@ 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) => {
|
||||||
|
@ -643,30 +646,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>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -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,48 +97,29 @@ 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,
|
const emit = defineEmits(["print", "input", "delete", "close", "edit"]);
|
||||||
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 deleteDialog = ref(false);
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { $globals } = useNuxtApp();
|
const { $globals } = useNuxtApp();
|
||||||
|
|
||||||
const editorButtons = [
|
const editorButtons = [
|
||||||
{
|
{
|
||||||
text: i18n.t("general.delete"),
|
text: i18n.t("general.delete"),
|
||||||
|
@ -169,31 +150,22 @@ export default defineNuxtComponent({
|
||||||
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,50 +108,31 @@ 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: {
|
defineEmits<{
|
||||||
type: String,
|
click: [];
|
||||||
default: "secondary",
|
delete: [slug: string];
|
||||||
},
|
}>();
|
||||||
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 $auth = useMealieAuth();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
|
@ -164,15 +143,6 @@ export default defineNuxtComponent({
|
||||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||||
});
|
});
|
||||||
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
||||||
|
|
||||||
return {
|
|
||||||
isOwnGroup,
|
|
||||||
recipeRoute,
|
|
||||||
showRecipeContent,
|
|
||||||
cursor,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</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,47 +28,32 @@
|
||||||
</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,
|
defineEmits<{
|
||||||
},
|
click: [];
|
||||||
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();
|
const { recipeImage, recipeSmallImage, recipeTinyImage } = useStaticRoutes();
|
||||||
|
|
||||||
|
@ -97,15 +82,6 @@ export default defineNuxtComponent({
|
||||||
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,59 +134,34 @@ 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,
|
defineEmits<{
|
||||||
},
|
selected: [];
|
||||||
image: {
|
delete: [slug: string];
|
||||||
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 $auth = useMealieAuth();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
|
@ -194,15 +172,6 @@ export default defineNuxtComponent({
|
||||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||||
});
|
});
|
||||||
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
||||||
|
|
||||||
return {
|
|
||||||
isOwnGroup,
|
|
||||||
recipeRoute,
|
|
||||||
showRecipeContent,
|
|
||||||
cursor,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</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,42 +175,30 @@ 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,
|
const emit = defineEmits<{
|
||||||
default: null,
|
replaceRecipes: [recipes: Recipe[]];
|
||||||
},
|
appendRecipes: [recipes: Recipe[]];
|
||||||
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 { $vuetify } = useNuxtApp();
|
||||||
const preferences = useUserSortPreferences();
|
const preferences = useUserSortPreferences();
|
||||||
|
|
||||||
|
@ -234,9 +222,7 @@ export default defineNuxtComponent({
|
||||||
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 || "");
|
||||||
|
@ -251,7 +237,7 @@ export default defineNuxtComponent({
|
||||||
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)
|
||||||
|
|
||||||
|
@ -290,7 +276,7 @@ export default defineNuxtComponent({
|
||||||
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;
|
||||||
|
@ -315,7 +301,7 @@ 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 () => {
|
||||||
|
@ -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,14 +389,14 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,22 +412,6 @@ export default defineNuxtComponent({
|
||||||
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,52 +23,31 @@
|
||||||
</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();
|
|
||||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
|
||||||
const baseRecipeRoute = computed<string>(() => {
|
|
||||||
return `/g/${groupSlug.value}`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineEmits(["item-selected"]);
|
||||||
function truncateText(text: string, length = 20, clamp = "...") {
|
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");
|
||||||
|
@ -76,13 +55,6 @@ export default defineNuxtComponent({
|
||||||
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,75 +213,42 @@ 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({
|
|
||||||
printPreferencesDialog: false,
|
|
||||||
shareDialog: false,
|
|
||||||
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 emit = defineEmits<{
|
||||||
|
[key: string]: any;
|
||||||
|
delete: [slug: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const api = useUserApi();
|
||||||
|
|
||||||
|
const printPreferencesDialog = ref(false);
|
||||||
|
const shareDialog = ref(false);
|
||||||
|
const recipeDeleteDialog = ref(false);
|
||||||
|
const mealplannerDialog = ref(false);
|
||||||
|
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 newMealdateString = computed(() => {
|
const newMealdateString = computed(() => {
|
||||||
return state.newMealdate.toISOString().substring(0, 10);
|
// 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 i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
@ -360,18 +333,8 @@ export default defineNuxtComponent({
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get Default Menu Items Specified in Props
|
|
||||||
for (const [key, value] of Object.entries(props.useItems)) {
|
|
||||||
if (value) {
|
|
||||||
const item = defaultItems[key];
|
|
||||||
if (item && (item.isPublic || isOwnGroup.value)) {
|
|
||||||
state.menuItems.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add leading and Appending Items
|
// Add leading and Appending Items
|
||||||
state.menuItems = [...state.menuItems, ...props.leadingItems, ...props.appendItems];
|
menuItems.value = [...menuItems.value, ...props.leadingItems, ...props.appendItems];
|
||||||
|
|
||||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||||
|
|
||||||
|
@ -383,6 +346,30 @@ export default defineNuxtComponent({
|
||||||
const recipeRefWithScale = computed(() =>
|
const recipeRefWithScale = computed(() =>
|
||||||
recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined,
|
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;
|
||||||
|
|
||||||
|
const item = defaultItems[key];
|
||||||
|
if (item && (item.isPublic || isOwnGroup.value)) {
|
||||||
|
menuItems.value.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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" });
|
||||||
|
@ -420,7 +407,7 @@ export default defineNuxtComponent({
|
||||||
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();
|
||||||
|
@ -436,7 +423,7 @@ export default defineNuxtComponent({
|
||||||
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,
|
||||||
|
@ -451,7 +438,7 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
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}`);
|
||||||
}
|
}
|
||||||
|
@ -461,21 +448,21 @@ export default defineNuxtComponent({
|
||||||
// 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,11 +471,11 @@ 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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -497,32 +484,14 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
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,28 +42,19 @@ export interface GenericAlias {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
data: IngredientFood | IngredientUnit;
|
||||||
modelValue: {
|
}
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
const props = defineProps<Props>();
|
||||||
},
|
|
||||||
data: {
|
const emit = defineEmits<{
|
||||||
type: Object as () => IngredientFood | IngredientUnit,
|
submit: [aliases: GenericAlias[]];
|
||||||
required: true,
|
cancel: [];
|
||||||
},
|
}>();
|
||||||
},
|
|
||||||
emits: ["submit", "update:modelValue", "cancel"],
|
|
||||||
setup(props, context) {
|
|
||||||
// V-Model Support
|
// V-Model Support
|
||||||
const dialog = computed({
|
const dialog = defineModel<boolean>({ default: false });
|
||||||
get: () => {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:modelValue", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function createAlias() {
|
function createAlias() {
|
||||||
aliases.value.push({
|
aliases.value.push({
|
||||||
|
@ -85,7 +76,7 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
initAliases();
|
initAliases();
|
||||||
whenever(
|
whenever(
|
||||||
() => props.modelValue,
|
() => dialog.value,
|
||||||
() => {
|
() => {
|
||||||
initAliases();
|
initAliases();
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
};
|
}),
|
||||||
},
|
});
|
||||||
},
|
|
||||||
},
|
defineEmits<{
|
||||||
emits: ["click"],
|
click: [];
|
||||||
setup(props, context) {
|
}>();
|
||||||
|
|
||||||
|
const selected = defineModel<Recipe[]>({ default: () => [] });
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const groupSlug = $auth.user.value?.groupSlug;
|
const groupSlug = $auth.user.value?.groupSlug;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const selected = computed({
|
|
||||||
get: () => props.modelValue,
|
// Initialize sort state with default sorting by dateAdded descending
|
||||||
set: value => context.emit(INPUT_EVENT, value),
|
const sortBy = ref([{ key: "dateAdded", order: "desc" as const }]);
|
||||||
});
|
|
||||||
|
|
||||||
const headers = computed(() => {
|
const headers = computed(() => {
|
||||||
const hdrs: Array<{ title: string; value: string; align?: string; sortable?: boolean }> = [];
|
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" });
|
||||||
|
@ -203,16 +192,4 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
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,49 +205,31 @@ 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: {
|
const dialog = defineModel<boolean>({ default: false });
|
||||||
type: Array as () => RecipeWithScale[],
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
shoppingLists: {
|
|
||||||
type: Array as () => ShoppingListSummary[],
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const preferences = useShoppingListPreferences();
|
const preferences = useShoppingListPreferences();
|
||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
|
|
||||||
// v-model support
|
|
||||||
const dialog = computed({
|
|
||||||
get: () => {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:modelValue", val);
|
|
||||||
initState();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
shoppingListDialog: true,
|
shoppingListDialog: true,
|
||||||
shoppingListIngredientDialog: false,
|
shoppingListIngredientDialog: false,
|
||||||
shoppingListShowAllToggled: false,
|
shoppingListShowAllToggled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { shoppingListDialog, shoppingListIngredientDialog, shoppingListShowAllToggled: _shoppingListShowAllToggled } = toRefs(state);
|
||||||
|
|
||||||
const userHousehold = computed(() => {
|
const userHousehold = computed(() => {
|
||||||
return $auth.user.value?.householdSlug || "";
|
return $auth.user.value?.householdSlug || "";
|
||||||
});
|
});
|
||||||
|
@ -269,6 +253,12 @@ export default defineNuxtComponent({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(dialog, (val) => {
|
||||||
|
if (!val) {
|
||||||
|
initState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
|
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) {
|
||||||
|
@ -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,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -421,22 +413,6 @@ export default defineNuxtComponent({
|
||||||
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,28 +89,27 @@
|
||||||
</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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"bulk-data": [data: string[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dialog = ref(false);
|
||||||
|
const inputText = ref(props.inputTextProp);
|
||||||
|
|
||||||
function splitText() {
|
function splitText() {
|
||||||
return state.inputText.split("\n").filter(line => !(line === "\n" || !line));
|
return inputText.value.split("\n").filter(line => !(line === "\n" || !line));
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFirstCharacter() {
|
function removeFirstCharacter() {
|
||||||
state.inputText = splitText()
|
inputText.value = splitText()
|
||||||
.map(line => line.substring(1))
|
.map(line => line.substring(1))
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
@ -119,11 +118,11 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,12 +133,12 @@ export default defineNuxtComponent({
|
||||||
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();
|
||||||
|
@ -161,16 +160,4 @@ export default defineNuxtComponent({
|
||||||
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,
|
const dialog = defineModel<boolean>({ default: false });
|
||||||
},
|
|
||||||
recipe: {
|
|
||||||
type: Object as () => NoUndefinedField<Recipe>,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const preferences = useUserPrintPreferences();
|
const preferences = useUserPrintPreferences();
|
||||||
|
|
||||||
// V-Model Support
|
|
||||||
const dialog = computed({
|
|
||||||
get: () => {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:modelValue", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
dialog,
|
|
||||||
ImagePosition,
|
|
||||||
preferences,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</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,17 +81,15 @@ 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 emit = defineEmits<{
|
||||||
|
selected: [recipe: RecipeSummary];
|
||||||
|
}>();
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const state = reactive({
|
const loading = ref(false);
|
||||||
loading: false,
|
const selectedIndex = ref(-1);
|
||||||
selectedIndex: -1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Dialog State Management
|
// Dialog State Management
|
||||||
|
@ -105,7 +99,7 @@ export default defineNuxtComponent({
|
||||||
watch(dialog, (val) => {
|
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 = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -116,17 +110,17 @@ export default defineNuxtComponent({
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,11 +131,11 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
@ -158,9 +152,8 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const advancedSearchUrl = computed(() => `/g/${groupSlug.value}`);
|
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||||
watch(route, close);
|
watch(route, close);
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
|
@ -177,22 +170,15 @@ export default defineNuxtComponent({
|
||||||
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,56 +92,35 @@
|
||||||
</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),
|
const datePickerMenu = ref(false);
|
||||||
tokens: [] as RecipeShareToken[],
|
const expirationDate = ref(new Date(Date.now() - new Date().getTimezoneOffset() * 60000));
|
||||||
});
|
const tokens = ref<RecipeShareToken[]>([]);
|
||||||
|
|
||||||
const expirationDateString = computed(() => {
|
const expirationDateString = computed(() => {
|
||||||
return state.expirationDate.toISOString().substring(0, 10);
|
return expirationDate.value.toISOString().substring(0, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
whenever(
|
whenever(
|
||||||
() => props.modelValue,
|
() => 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();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -165,17 +144,17 @@ export default defineNuxtComponent({
|
||||||
// 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() {
|
||||||
|
@ -183,7 +162,7 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
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 ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,17 +204,4 @@ export default defineNuxtComponent({
|
||||||
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,26 +28,21 @@
|
||||||
</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 api = useUserApi();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const { userRatings, refreshUserRatings } = useUserSelfRatings();
|
const { userRatings, refreshUserRatings } = useUserSelfRatings();
|
||||||
|
@ -67,8 +62,4 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
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: {
|
const emit = defineEmits<{
|
||||||
type: String,
|
refresh: [];
|
||||||
required: true,
|
upload: [fileObject: File];
|
||||||
},
|
}>();
|
||||||
},
|
|
||||||
setup(props, context) {
|
const url = ref("");
|
||||||
const state = reactive({
|
const loading = ref(false);
|
||||||
url: "",
|
const menu = ref(false);
|
||||||
loading: false,
|
|
||||||
menu: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
function uploadImage(fileObject: File) {
|
function uploadImage(fileObject: File) {
|
||||||
context.emit(UPLOAD_EVENT, fileObject);
|
emit(UPLOAD_EVENT, fileObject);
|
||||||
state.menu = false;
|
menu.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
async function getImageFromURL() {
|
async function getImageFromURL() {
|
||||||
state.loading = true;
|
loading.value = true;
|
||||||
if (await api.recipes.updateImagebyURL(props.slug, state.url)) {
|
if (await api.recipes.updateImagebyURL(props.slug, url.value)) {
|
||||||
context.emit(REFRESH_EVENT);
|
emit(REFRESH_EVENT);
|
||||||
}
|
}
|
||||||
state.loading = false;
|
loading.value = false;
|
||||||
state.menu = false;
|
menu.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const messages = props.slug ? [""] : [i18n.t("recipe.save-recipe-before-use")];
|
const messages = computed(() =>
|
||||||
|
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const safeMarkup = computed(() => sanitizeIngredientHTML(props.markup));
|
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,40 +52,28 @@
|
||||||
</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[] = [];
|
||||||
|
@ -99,7 +86,7 @@ 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");
|
||||||
|
@ -108,16 +95,8 @@ export default defineNuxtComponent({
|
||||||
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,22 +117,19 @@
|
||||||
</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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["eventCreated"],
|
|
||||||
setup(props, context) {
|
|
||||||
const madeThisDialog = ref(false);
|
const madeThisDialog = ref(false);
|
||||||
const userApi = useUserApi();
|
const userApi = useUserApi();
|
||||||
const { household } = useHouseholdSelf();
|
const { household } = useHouseholdSelf();
|
||||||
|
@ -196,12 +194,26 @@ export default defineNuxtComponent({
|
||||||
newTimelineEventImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
newTimelineEventImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = reactive({ datePickerMenu: false });
|
const datePickerMenu = ref(false);
|
||||||
|
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() {
|
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,40 +51,28 @@
|
||||||
</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 $auth = useMealieAuth();
|
||||||
const { frac } = useFraction();
|
const { frac } = useFraction();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
@ -180,12 +168,4 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
return listItemDescriptions;
|
return listItemDescriptions;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
|
||||||
attrs,
|
|
||||||
groupSlug,
|
|
||||||
listItemDescriptions,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
:suffix="labels[key].suffix"
|
:suffix="labels[key].suffix"
|
||||||
type="number"
|
type="number"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
variant="underlined"
|
||||||
@update:model-value="updateValue(key, $event)"
|
@update:model-value="updateValue(key, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,44 +32,38 @@
|
||||||
:key="index"
|
:key="index"
|
||||||
style="min-height: 25px"
|
style="min-height: 25px"
|
||||||
>
|
>
|
||||||
<div>
|
<v-list-item-title class="pl-2 d-flex">
|
||||||
<v-list-item-title class="pl-4 caption flex row">
|
|
||||||
<div>{{ item.label }}</div>
|
<div>{{ item.label }}</div>
|
||||||
<div class="ml-auto mr-1">
|
<div class="ml-auto mr-1">
|
||||||
{{ item.value }}
|
{{ item.value }}
|
||||||
</div>
|
</div>
|
||||||
<div>{{ item.suffix }}</div>
|
<div>{{ item.suffix }}</div>
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</div>
|
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-card>
|
</v-card>
|
||||||
</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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const { labels } = useNutritionLabels();
|
const { labels } = useNutritionLabels();
|
||||||
const valueNotNull = computed(() => {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,31 +73,21 @@ export default defineNuxtComponent({
|
||||||
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>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|
|
@ -60,54 +60,39 @@
|
||||||
</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,
|
const emit = defineEmits<{
|
||||||
default: true,
|
"created-item": [item: any];
|
||||||
},
|
}>();
|
||||||
itemType: {
|
|
||||||
type: String as () => RecipeOrganizer,
|
const dialog = defineModel<boolean>({ default: false });
|
||||||
default: "category",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
setup(props, context) {
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const state = reactive({
|
const name = ref("");
|
||||||
name: "",
|
const onHand = ref(false);
|
||||||
onHand: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const dialog = computed({
|
|
||||||
get() {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
context.emit("update:modelValue", value);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
dialog,
|
||||||
(val: boolean) => {
|
(val: boolean) => {
|
||||||
if (!val) state.name = "";
|
if (!val) name.value = "";
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -154,25 +139,14 @@ export default defineNuxtComponent({
|
||||||
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,26 +137,17 @@ 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,
|
|
||||||
},
|
|
||||||
itemType: {
|
|
||||||
type: String as () => RecipeOrganizer,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update", "delete"],
|
|
||||||
setup(props, { emit }) {
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
// Search Options
|
// Search Options
|
||||||
options: {
|
options: {
|
||||||
|
@ -271,23 +261,4 @@ export default defineNuxtComponent({
|
||||||
function isTitle(str: number | string) {
|
function isTitle(str: number | string) {
|
||||||
return typeof str === "string" && str.length === 1;
|
return typeof str === "string" && str.length === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
groupSlug,
|
|
||||||
isTitle,
|
|
||||||
dialogs,
|
|
||||||
confirmDelete,
|
|
||||||
openUpdateDialog,
|
|
||||||
updateOne,
|
|
||||||
updateTarget,
|
|
||||||
deleteOne,
|
|
||||||
deleteTarget,
|
|
||||||
Organizer,
|
|
||||||
presets,
|
|
||||||
itemsSorted,
|
|
||||||
searchString,
|
|
||||||
translationKey,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</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,67 +46,40 @@
|
||||||
</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) {
|
|
||||||
const selected = computed({
|
|
||||||
get: () => props.modelValue,
|
|
||||||
set: (val) => {
|
|
||||||
context.emit("update:modelValue", val);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (selected.value === undefined) {
|
if (selected.value === undefined) {
|
||||||
|
@ -205,21 +178,6 @@ export default defineNuxtComponent({
|
||||||
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,32 +35,22 @@ 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";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
recipe: NoUndefinedField<Recipe>;
|
||||||
|
recipeScale?: number;
|
||||||
|
landscape?: boolean;
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
recipeScale: 1,
|
||||||
|
landscape: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["save", "delete"]);
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
|
||||||
components: {
|
|
||||||
RecipePageInfoCard,
|
|
||||||
RecipeActionMenu,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
recipe: {
|
|
||||||
type: Object as () => NoUndefinedField<Recipe>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
recipeScale: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
landscape: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["save", "delete"],
|
|
||||||
setup(props) {
|
|
||||||
const { $vuetify } = useNuxtApp();
|
|
||||||
const { recipeImage } = useStaticRoutes();
|
const { recipeImage } = useStaticRoutes();
|
||||||
const { imageKey, pageMode, editMode, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
|
const { imageKey, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
|
||||||
const { user } = usePageUser();
|
const { user } = usePageUser();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
|
@ -78,9 +68,6 @@ export default defineNuxtComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
@ -92,25 +79,4 @@ export default defineNuxtComponent({
|
||||||
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,25 +12,21 @@
|
||||||
/>
|
/>
|
||||||
</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 { $vuetify } = useNuxtApp();
|
||||||
const { recipeImage } = useStaticRoutes();
|
const { recipeImage } = useStaticRoutes();
|
||||||
const { imageKey } = usePageState(props.recipe.slug);
|
const { imageKey } = usePageState(props.recipe.slug);
|
||||||
|
@ -59,13 +55,4 @@ export default defineNuxtComponent({
|
||||||
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,25 +47,15 @@ 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 { isOwnGroup } = useLoggedInState();
|
||||||
|
|
||||||
const toolStore = isOwnGroup.value ? useToolStore() : null;
|
const toolStore = isOwnGroup.value ? useToolStore() : null;
|
||||||
|
@ -106,13 +95,4 @@ export default defineNuxtComponent({
|
||||||
console.log("no user, skipping server update");
|
console.log("no user, skipping server update");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
toolStore,
|
|
||||||
recipeTools,
|
|
||||||
isEditMode,
|
|
||||||
updateTool,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
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