mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-21 14:03:32 -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
|
||||
uses: ./.github/workflows/build-package.yml
|
||||
with:
|
||||
tag: release
|
||||
tag: ${{ github.event.release.tag_name }}
|
||||
|
||||
publish:
|
||||
permissions:
|
||||
|
|
|
@ -12,7 +12,7 @@ repos:
|
|||
exclude: ^tests/data/
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.2
|
||||
rev: v0.12.7
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
|
|
|
@ -35,7 +35,7 @@ conventional_commits = true
|
|||
filter_unconventional = true
|
||||
# regex for preprocessing the commit messages
|
||||
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
|
||||
commit_parsers = [
|
||||
|
|
|
@ -179,9 +179,15 @@ def inject_nuxt_values():
|
|||
|
||||
all_langs = []
|
||||
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.sort()
|
||||
all_date_locales.sort()
|
||||
|
||||
log.debug(f"injecting locales into nuxt config -> {nuxt_config}")
|
||||
inject_inline(nuxt_config, CodeKeys.nuxt_local_messages, all_langs)
|
||||
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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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",
|
||||
"unit": None,
|
||||
"food": None,
|
||||
"disableAmount": True,
|
||||
"quantity": 1,
|
||||
"originalText": None,
|
||||
"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,
|
||||
"landscapeView": False,
|
||||
"disableComments": False,
|
||||
"disableAmount": True,
|
||||
"locked": False,
|
||||
},
|
||||
"assets": [],
|
||||
|
|
|
@ -20,7 +20,7 @@ RUN yarn generate
|
|||
###############################################
|
||||
# Base Image - Python
|
||||
###############################################
|
||||
FROM python:3.12-slim as python-base
|
||||
FROM python:3.12-slim AS python-base
|
||||
|
||||
ENV MEALIE_HOME="/app"
|
||||
|
||||
|
@ -119,7 +119,7 @@ RUN . $VENV_PATH/bin/activate \
|
|||
###############################################
|
||||
# Production Image
|
||||
###############################################
|
||||
FROM python-base as production
|
||||
FROM python-base AS production
|
||||
LABEL org.opencontainers.image.source="https://github.com/mealie-recipes/mealie"
|
||||
ENV PRODUCTION=true
|
||||
ENV TESTING=false
|
||||
|
|
|
@ -13,14 +13,14 @@ Steps:
|
|||
|
||||
#### 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
|
||||
|
||||
Create REST sensors in home assistant to get the details of today's meal.
|
||||
We will create sensors to get the name and ID of the first meal in today's meal plan (note that this may not be what is wanted if there is more than one meal planned for the day). We need the ID as well as the name to be able to retrieve the image for the meal.
|
||||
|
||||
Make sure the url and port (`http://mealie:9000` ) matches your installation's address and _API_ port.
|
||||
Make sure the url and port (`http://mealie:9000`) matches your installation's address and _API_ port.
|
||||
|
||||
```yaml
|
||||
rest:
|
||||
|
|
|
@ -31,7 +31,7 @@ To deploy mealie on your local network, it is highly recommended to use Docker t
|
|||
We've gone through a few versions of Mealie v1 deployment targets. We have settled on a single container deployment, and we've begun publishing the nightly container on github containers. If you're looking to move from the old nightly (split containers _or_ the omni image) to the new nightly, there are a few things you need to do:
|
||||
|
||||
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.
|
||||
4. Restart the container
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
# 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.
|
||||
|
||||
**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
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v2.8.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.0.2 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
@ -38,7 +41,7 @@ services:
|
|||
|
||||
postgres:
|
||||
container_name: postgres
|
||||
image: postgres:15
|
||||
image: postgres:17
|
||||
restart: always
|
||||
volumes:
|
||||
- mealie-pgdata:/var/lib/postgresql/data
|
||||
|
@ -46,6 +49,7 @@ services:
|
|||
POSTGRES_PASSWORD: mealie
|
||||
POSTGRES_USER: mealie
|
||||
PGUSER: mealie
|
||||
POSTGRES_DB: mealie
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready"]
|
||||
interval: 30s
|
||||
|
|
|
@ -11,7 +11,7 @@ SQLite is a popular, open source, self-contained, zero-configuration database th
|
|||
```yaml
|
||||
services:
|
||||
mealie:
|
||||
image: ghcr.io/mealie-recipes/mealie:v2.8.0 # (3)
|
||||
image: ghcr.io/mealie-recipes/mealie:v3.0.2 # (3)
|
||||
container_name: mealie
|
||||
restart: always
|
||||
ports:
|
||||
|
|
|
@ -2,6 +2,3 @@
|
|||
|
||||
## Feature Requests
|
||||
[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 -->
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
<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">
|
||||
<svg style="width: 32px; height: 32px" viewBox="0 0 480 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<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,36 +44,20 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
<script setup lang="ts">
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
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 cookbook = toRef(() => props.modelValue);
|
||||
|
||||
function handleInput(value: string | undefined) {
|
||||
const modelValue = defineModel<ReadCookBook>({ required: true });
|
||||
const i18n = useI18n();
|
||||
const cookbook = toRef(modelValue);
|
||||
function handleInput(value: string | undefined) {
|
||||
cookbook.value.queryFilterString = value || "";
|
||||
emit("update:modelValue", cookbook.value);
|
||||
}
|
||||
}
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "recipe_category.id",
|
||||
label: i18n.t("category.categories"),
|
||||
|
@ -109,13 +93,5 @@ export default defineNuxtComponent({
|
|||
label: i18n.t("general.date-updated"),
|
||||
type: "date",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
cookbook,
|
||||
handleInput,
|
||||
fieldDefs,
|
||||
};
|
||||
},
|
||||
});
|
||||
];
|
||||
</script>
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
<v-card-text>
|
||||
<CookbookEditor
|
||||
v-model="editTarget"
|
||||
:actions="actions"
|
||||
/>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
|
@ -65,7 +64,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||
|
@ -74,44 +73,40 @@ import { useLoggedInState } from "~/composables/use-logged-in-state";
|
|||
import type { RecipeCookBook } from "~/lib/api/types/cookbook";
|
||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeCardSection, CookbookEditor },
|
||||
setup() {
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const slug = route.params.slug as string;
|
||||
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
||||
const { actions } = useCookbookStore();
|
||||
const router = useRouter();
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const slug = route.params.slug as string;
|
||||
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
||||
const { actions } = useCookbookStore();
|
||||
const router = useRouter();
|
||||
|
||||
const tab = ref(null);
|
||||
const book = getOne(slug);
|
||||
const book = getOne(slug);
|
||||
|
||||
const isOwnHousehold = computed(() => {
|
||||
const isOwnHousehold = computed(() => {
|
||||
if (!($auth.user.value && book.value?.householdId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $auth.user.value.householdId === book.value.householdId;
|
||||
});
|
||||
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||
});
|
||||
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||
|
||||
const dialogStates = reactive({
|
||||
const dialogStates = reactive({
|
||||
edit: false,
|
||||
});
|
||||
});
|
||||
|
||||
const editTarget = ref<RecipeCookBook | null>(null);
|
||||
function handleEditCookbook() {
|
||||
const editTarget = ref<RecipeCookBook | null>(null);
|
||||
function handleEditCookbook() {
|
||||
dialogStates.edit = true;
|
||||
editTarget.value = book.value;
|
||||
}
|
||||
}
|
||||
|
||||
async function editCookbook() {
|
||||
async function editCookbook() {
|
||||
if (!editTarget.value) {
|
||||
return;
|
||||
}
|
||||
|
@ -127,28 +122,9 @@ export default defineNuxtComponent({
|
|||
}
|
||||
dialogStates.edit = false;
|
||||
editTarget.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
useSeoMeta({
|
||||
useSeoMeta({
|
||||
title: book?.value?.name || "Cookbook",
|
||||
});
|
||||
|
||||
return {
|
||||
book,
|
||||
slug,
|
||||
tab,
|
||||
appendRecipes,
|
||||
assignSorted,
|
||||
recipes,
|
||||
removeRecipe,
|
||||
replaceRecipes,
|
||||
canEdit,
|
||||
dialogStates,
|
||||
editTarget,
|
||||
handleEditCookbook,
|
||||
editCookbook,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -20,45 +20,33 @@
|
|||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { parseISO, formatDistanceToNow } from "date-fns";
|
||||
import type { GroupDataExport } from "~/lib/api/types/group";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
exports: {
|
||||
type: Array as () => GroupDataExport[],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const i18n = useI18n();
|
||||
defineProps<{
|
||||
exports: GroupDataExport[];
|
||||
}>();
|
||||
|
||||
const headers = [
|
||||
const i18n = useI18n();
|
||||
|
||||
const headers = [
|
||||
{ title: i18n.t("export.export"), value: "name" },
|
||||
{ title: i18n.t("export.file-name"), value: "filename" },
|
||||
{ title: i18n.t("export.size"), value: "size" },
|
||||
{ title: i18n.t("export.link-expires"), value: "expires" },
|
||||
{ title: "", value: "actions" },
|
||||
];
|
||||
];
|
||||
|
||||
function getTimeToExpire(timeString: string) {
|
||||
function getTimeToExpire(timeString: string) {
|
||||
const expiresAt = parseISO(timeString);
|
||||
|
||||
return formatDistanceToNow(expiresAt, {
|
||||
addSuffix: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function downloadData(_: any) {
|
||||
function downloadData(_: any) {
|
||||
console.log("Downloading data...");
|
||||
}
|
||||
|
||||
return {
|
||||
downloadData,
|
||||
headers,
|
||||
getTimeToExpire,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -9,30 +9,10 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
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);
|
||||
},
|
||||
});
|
||||
<script setup lang="ts">
|
||||
import type { ReadGroupPreferences } from "~/lib/api/types/user";
|
||||
|
||||
return {
|
||||
preferences,
|
||||
};
|
||||
},
|
||||
});
|
||||
const preferences = defineModel<ReadGroupPreferences>({ required: true });
|
||||
</script>
|
||||
|
||||
<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"
|
||||
content-class="d-print-none"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
:class="{ 'rounded-circle': fab }"
|
||||
:size="fab ? 'small' : undefined"
|
||||
|
@ -26,7 +26,7 @@
|
|||
:icon="!fab"
|
||||
variant="text"
|
||||
dark
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
@click.prevent
|
||||
>
|
||||
<v-icon>{{ icon }}</v-icon>
|
||||
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
import RecipeDialogAddToShoppingList from "~/components/Domain/Recipe/RecipeDialogAddToShoppingList.vue";
|
||||
import type { ShoppingListSummary } from "~/lib/api/types/household";
|
||||
|
@ -64,40 +64,32 @@ export interface ContextMenuItem {
|
|||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeDialogAddToShoppingList,
|
||||
},
|
||||
props: {
|
||||
recipes: {
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
},
|
||||
menuTop: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
fab: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
},
|
||||
menuIcon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { mdAndUp } = useDisplay();
|
||||
interface Props {
|
||||
recipes?: Recipe[];
|
||||
menuTop?: boolean;
|
||||
fab?: boolean;
|
||||
color?: string;
|
||||
menuIcon?: string | null;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
recipes: () => [],
|
||||
menuTop: true,
|
||||
fab: false,
|
||||
color: "primary",
|
||||
menuIcon: null,
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const api = useUserApi();
|
||||
const emit = defineEmits<{
|
||||
[key: string]: [];
|
||||
}>();
|
||||
|
||||
const state = reactive({
|
||||
const { mdAndUp } = useDisplay();
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
shoppingListDialog: false,
|
||||
menuItems: [
|
||||
|
@ -109,36 +101,38 @@ export default defineNuxtComponent({
|
|||
isPublic: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||
const { shoppingListDialog, menuItems } = toRefs(state);
|
||||
|
||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||
const recipesWithScales = computed(() => {
|
||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||
|
||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||
const recipesWithScales = computed(() => {
|
||||
return props.recipes.map((recipe) => {
|
||||
return {
|
||||
scale: 1,
|
||||
...recipe,
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function getShoppingLists() {
|
||||
async function getShoppingLists() {
|
||||
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
||||
if (data) {
|
||||
shoppingLists.value = data.items as ShoppingListSummary[] ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||
shoppingList: () => {
|
||||
getShoppingLists();
|
||||
state.shoppingListDialog = true;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function contextMenuEventHandler(eventKey: string) {
|
||||
function contextMenuEventHandler(eventKey: string) {
|
||||
const handler = eventHandlers[eventKey];
|
||||
|
||||
if (handler && typeof handler === "function") {
|
||||
|
@ -147,18 +141,7 @@ export default defineNuxtComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
context.emit(eventKey);
|
||||
emit(eventKey);
|
||||
state.loading = false;
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
contextMenuEventHandler,
|
||||
icon,
|
||||
recipesWithScales,
|
||||
shoppingLists,
|
||||
mdAndUp,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
style="gap: 10px"
|
||||
>
|
||||
<v-select
|
||||
v-model="inputDay"
|
||||
v-model="day"
|
||||
:items="MEAL_DAY_OPTIONS"
|
||||
:label="$t('meal-plan.rule-day')"
|
||||
/>
|
||||
<v-select
|
||||
v-model="inputEntryType"
|
||||
v-model="entryType"
|
||||
:items="MEAL_TYPE_OPTIONS"
|
||||
:label="$t('meal-plan.meal-type')"
|
||||
/>
|
||||
|
@ -19,64 +19,49 @@
|
|||
<div class="mb-5">
|
||||
<QueryFilterBuilder
|
||||
:field-defs="fieldDefs"
|
||||
:initial-query-filter="queryFilter"
|
||||
:initial-query-filter="props.queryFilter"
|
||||
@input="handleQueryFilterInput"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- TODO: proper pluralization of inputDay -->
|
||||
{{ $t('meal-plan.this-rule-will-apply', {
|
||||
dayCriteria: inputDay === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [inputDay]),
|
||||
mealTypeCriteria: inputEntryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [inputEntryType]),
|
||||
dayCriteria: day === "unset" ? $t('meal-plan.to-all-days') : $t('meal-plan.on-days', [day]),
|
||||
mealTypeCriteria: entryType === "unset" ? $t('meal-plan.for-all-meal-types') : $t('meal-plan.for-type-meal-types', [entryType]),
|
||||
}) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||
import { Organizer } from "~/lib/api/types/non-generated";
|
||||
import type { QueryFilterJSON } from "~/lib/api/types/response";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
QueryFilterBuilder,
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: String,
|
||||
default: "unset",
|
||||
},
|
||||
entryType: {
|
||||
type: String,
|
||||
default: "unset",
|
||||
},
|
||||
queryFilterString: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
queryFilter: {
|
||||
type: Object as () => QueryFilterJSON,
|
||||
default: null,
|
||||
},
|
||||
showHelp: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:day", "update:entry-type", "update:query-filter-string"],
|
||||
setup(props, context) {
|
||||
const i18n = useI18n();
|
||||
interface Props {
|
||||
queryFilter?: QueryFilterJSON | null;
|
||||
showHelp?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
queryFilter: null,
|
||||
showHelp: false,
|
||||
});
|
||||
|
||||
const MEAL_TYPE_OPTIONS = [
|
||||
const day = defineModel<string>("day", { default: "unset" });
|
||||
const entryType = defineModel<string>("entryType", { default: "unset" });
|
||||
const queryFilterString = defineModel<string>("queryFilterString", { default: "" });
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const MEAL_TYPE_OPTIONS = [
|
||||
{ title: i18n.t("meal-plan.breakfast"), value: "breakfast" },
|
||||
{ title: i18n.t("meal-plan.lunch"), value: "lunch" },
|
||||
{ title: i18n.t("meal-plan.dinner"), value: "dinner" },
|
||||
{ title: i18n.t("meal-plan.side"), value: "side" },
|
||||
{ title: i18n.t("meal-plan.type-any"), value: "unset" },
|
||||
];
|
||||
];
|
||||
|
||||
const MEAL_DAY_OPTIONS = [
|
||||
const MEAL_DAY_OPTIONS = [
|
||||
{ title: i18n.t("general.monday"), value: "monday" },
|
||||
{ title: i18n.t("general.tuesday"), value: "tuesday" },
|
||||
{ title: i18n.t("general.wednesday"), value: "wednesday" },
|
||||
|
@ -85,40 +70,14 @@ export default defineNuxtComponent({
|
|||
{ title: i18n.t("general.saturday"), value: "saturday" },
|
||||
{ title: i18n.t("general.sunday"), value: "sunday" },
|
||||
{ title: i18n.t("meal-plan.day-any"), value: "unset" },
|
||||
];
|
||||
];
|
||||
|
||||
const inputDay = computed({
|
||||
get: () => {
|
||||
return props.day;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:day", val);
|
||||
},
|
||||
});
|
||||
function handleQueryFilterInput(value: string | undefined) {
|
||||
console.warn("handleQueryFilterInput called with value:", value);
|
||||
queryFilterString.value = value || "";
|
||||
}
|
||||
|
||||
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) {
|
||||
inputQueryFilterString.value = value || "";
|
||||
};
|
||||
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
const fieldDefs: FieldDefinition[] = [
|
||||
{
|
||||
name: "recipe_category.id",
|
||||
label: i18n.t("category.categories"),
|
||||
|
@ -159,17 +118,5 @@ export default defineNuxtComponent({
|
|||
label: i18n.t("general.date-updated"),
|
||||
type: "date",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
MEAL_TYPE_OPTIONS,
|
||||
MEAL_DAY_OPTIONS,
|
||||
inputDay,
|
||||
inputEntryType,
|
||||
inputQueryFilterString,
|
||||
handleQueryFilterInput,
|
||||
fieldDefs,
|
||||
};
|
||||
},
|
||||
});
|
||||
];
|
||||
</script>
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
:label="$t('settings.webhooks.webhook-url')"
|
||||
variant="underlined"
|
||||
/>
|
||||
<v-time-picker
|
||||
<v-text-field
|
||||
v-model="scheduledTime"
|
||||
class="elevation-2"
|
||||
ampm-in-title
|
||||
format="ampm"
|
||||
type="time"
|
||||
clearable
|
||||
variant="underlined"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-actions class="py-0 justify-end">
|
||||
|
@ -50,24 +50,25 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { ReadWebhook } from "~/lib/api/types/household";
|
||||
import { timeLocalToUTC, timeUTCToLocal } from "~/composables/use-group-webhooks";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
webhook: {
|
||||
type: Object as () => ReadWebhook,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["delete", "save", "test"],
|
||||
setup(props, { emit }) {
|
||||
const i18n = useI18n();
|
||||
const itemUTC = ref<string>(props.webhook.scheduledTime);
|
||||
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
|
||||
const props = defineProps<{
|
||||
webhook: ReadWebhook;
|
||||
}>();
|
||||
|
||||
const scheduledTime = computed({
|
||||
const emit = defineEmits<{
|
||||
delete: [id: string];
|
||||
save: [webhook: ReadWebhook];
|
||||
test: [id: string];
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const itemUTC = ref<string>(props.webhook.scheduledTime);
|
||||
const itemLocal = ref<string>(timeUTCToLocal(props.webhook.scheduledTime));
|
||||
|
||||
const scheduledTime = computed({
|
||||
get() {
|
||||
return itemLocal.value;
|
||||
},
|
||||
|
@ -75,27 +76,17 @@ export default defineNuxtComponent({
|
|||
itemUTC.value = timeLocalToUTC(v);
|
||||
itemLocal.value = v;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const webhookCopy = ref({ ...props.webhook });
|
||||
const webhookCopy = ref({ ...props.webhook });
|
||||
|
||||
function handleSave() {
|
||||
function handleSave() {
|
||||
webhookCopy.value.scheduledTime = itemLocal.value;
|
||||
emit("save", webhookCopy.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Set page title using useSeoMeta
|
||||
useSeoMeta({
|
||||
// Set page title using useSeoMeta
|
||||
useSeoMeta({
|
||||
title: i18n.t("settings.webhooks.webhooks"),
|
||||
});
|
||||
|
||||
return {
|
||||
webhookCopy,
|
||||
scheduledTime,
|
||||
handleSave,
|
||||
itemUTC,
|
||||
itemLocal,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -41,27 +41,19 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { ReadHouseholdPreferences } from "~/lib/api/types/household";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const i18n = useI18n();
|
||||
const preferences = defineModel<ReadHouseholdPreferences>({ required: true });
|
||||
const i18n = useI18n();
|
||||
|
||||
type Preference = {
|
||||
type Preference = {
|
||||
key: keyof ReadHouseholdPreferences;
|
||||
label: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
|
||||
const recipePreferences: Preference[] = [
|
||||
const recipePreferences: Preference[] = [
|
||||
{
|
||||
key: "recipePublic",
|
||||
label: i18n.t("group.allow-users-outside-of-your-group-to-see-your-recipes"),
|
||||
|
@ -87,14 +79,9 @@ export default defineNuxtComponent({
|
|||
label: i18n.t("group.disable-users-from-commenting-on-recipes"),
|
||||
description: i18n.t("group.disable-users-from-commenting-on-recipes-description"),
|
||||
},
|
||||
{
|
||||
key: "recipeDisableAmount",
|
||||
label: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food"),
|
||||
description: i18n.t("group.disable-organizing-recipe-ingredients-by-units-and-food-description"),
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
const allDays = [
|
||||
const allDays = [
|
||||
{
|
||||
name: i18n.t("general.sunday"),
|
||||
value: 0,
|
||||
|
@ -123,24 +110,7 @@ export default defineNuxtComponent({
|
|||
name: i18n.t("general.saturday"),
|
||||
value: 6,
|
||||
},
|
||||
];
|
||||
|
||||
const preferences = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(val) {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
allDays,
|
||||
preferences,
|
||||
recipePreferences,
|
||||
};
|
||||
},
|
||||
});
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
|
|
|
@ -147,7 +147,7 @@
|
|||
:model-value="field.value"
|
||||
type="number"
|
||||
variant="underlined"
|
||||
@:model-value="setFieldValue(field, index, $event)"
|
||||
@update:model-value="setFieldValue(field, index, $event)"
|
||||
/>
|
||||
<v-checkbox
|
||||
v-else-if="field.type === 'boolean'"
|
||||
|
@ -163,14 +163,14 @@
|
|||
max-width="290px"
|
||||
min-width="auto"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-text-field
|
||||
v-model="field.value"
|
||||
persistent-hint
|
||||
:prepend-icon="$globals.icons.calendar"
|
||||
variant="underlined"
|
||||
color="primary"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
|
@ -184,53 +184,53 @@
|
|||
</v-menu>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Category"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Category"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Tag"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Tag"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Tool"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Tool"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Food"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Food"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Household"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Household"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
@update:model-value="setFieldOrganizers(field, index, $event)"
|
||||
/>
|
||||
</v-col>
|
||||
<!-- right parenthesis -->
|
||||
|
@ -297,7 +297,7 @@
|
|||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
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 { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
VueDraggable,
|
||||
RecipeOrganizerSelector,
|
||||
},
|
||||
props: {
|
||||
const props = defineProps({
|
||||
fieldDefs: {
|
||||
type: Array as () => FieldDefinition[],
|
||||
required: true,
|
||||
|
@ -321,57 +316,62 @@ export default defineNuxtComponent({
|
|||
type: Object as () => QueryFilterJSON | null,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ["input", "inputJSON"],
|
||||
setup(props, context) {
|
||||
const { household } = useHouseholdSelf();
|
||||
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
||||
});
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
const emit = defineEmits<{
|
||||
(event: "input", value: string | undefined): void;
|
||||
(event: "inputJSON", value: QueryFilterJSON | undefined): void;
|
||||
}>();
|
||||
|
||||
const { household } = useHouseholdSelf();
|
||||
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||
});
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
const state = reactive({
|
||||
showAdvanced: false,
|
||||
qfValid: false,
|
||||
datePickers: [] as boolean[],
|
||||
drag: false,
|
||||
});
|
||||
});
|
||||
const { showAdvanced, datePickers, drag } = toRefs(state);
|
||||
|
||||
const storeMap = {
|
||||
const storeMap = {
|
||||
[Organizer.Category]: useCategoryStore(),
|
||||
[Organizer.Tag]: useTagStore(),
|
||||
[Organizer.Tool]: useToolStore(),
|
||||
[Organizer.Food]: useFoodStore(),
|
||||
[Organizer.Household]: useHouseholdStore(),
|
||||
};
|
||||
};
|
||||
|
||||
function onDragEnd(event: any) {
|
||||
function onDragEnd(event: any) {
|
||||
state.drag = false;
|
||||
|
||||
const oldIndex: number = event.oldIndex;
|
||||
const newIndex: number = event.newIndex;
|
||||
state.datePickers[oldIndex] = false;
|
||||
state.datePickers[newIndex] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// add id to fields to prevent reactivity issues
|
||||
type FieldWithId = Field & { id: number };
|
||||
const fields = ref<FieldWithId[]>([]);
|
||||
// add id to fields to prevent reactivity issues
|
||||
type FieldWithId = Field & { id: number };
|
||||
const fields = ref<FieldWithId[]>([]);
|
||||
|
||||
const uid = ref(1); // init uid to pass to fields
|
||||
function useUid() {
|
||||
const uid = ref(1); // init uid to pass to fields
|
||||
function useUid() {
|
||||
return uid.value++;
|
||||
}
|
||||
function addField(field: FieldDefinition) {
|
||||
}
|
||||
function addField(field: FieldDefinition) {
|
||||
fields.value.push({
|
||||
...getFieldFromFieldDef(field),
|
||||
id: useUid(),
|
||||
});
|
||||
state.datePickers.push(false);
|
||||
};
|
||||
}
|
||||
|
||||
function setField(index: number, fieldLabel: string) {
|
||||
function setField(index: number, fieldLabel: string) {
|
||||
state.datePickers[index] = false;
|
||||
const fieldDef = props.fieldDefs.find(fieldDef => fieldDef.label === fieldLabel);
|
||||
if (!fieldDef) {
|
||||
|
@ -388,48 +388,49 @@ export default defineNuxtComponent({
|
|||
...getFieldFromFieldDef(updatedField, resetValue),
|
||||
id: fields.value[index].id, // keep the id
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
fields.value[index].leftParenthesis = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setRightParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
function setRightParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
fields.value[index].rightParenthesis = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setLogicalOperatorValue(field: FieldWithId, index: number, value: LogicalOperator | undefined) {
|
||||
function setLogicalOperatorValue(field: FieldWithId, index: number, value: LogicalOperator | undefined) {
|
||||
if (!value) {
|
||||
value = logOps.value.AND.value;
|
||||
}
|
||||
|
||||
fields.value[index].logicalOperator = value ? logOps.value[value] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
|
||||
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
|
||||
fields.value[index].relationalOperatorValue = relOps.value[value];
|
||||
}
|
||||
}
|
||||
|
||||
function setFieldValue(field: FieldWithId, index: number, value: FieldValue) {
|
||||
function setFieldValue(field: FieldWithId, index: number, value: FieldValue) {
|
||||
state.datePickers[index] = false;
|
||||
fields.value[index].value = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
|
||||
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
|
||||
fields.value[index].values = values;
|
||||
}
|
||||
}
|
||||
|
||||
function setOrganizerValues(field: FieldWithId, index: number, values: OrganizerBase[]) {
|
||||
setFieldValues(field, index, values.map(value => value.id.toString()));
|
||||
fields.value[index].organizers = values;
|
||||
}
|
||||
function setFieldOrganizers(field: FieldWithId, index: number, organizers: OrganizerBase[]) {
|
||||
fields.value[index].organizers = organizers;
|
||||
// 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);
|
||||
state.datePickers.splice(index, 1);
|
||||
};
|
||||
}
|
||||
|
||||
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
||||
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
||||
/* newFields.forEach((field, index) => {
|
||||
const updatedField = getFieldFromFieldDef(field);
|
||||
fields.value[index] = updatedField; // recursive!!!
|
||||
|
@ -441,19 +442,17 @@ export default defineNuxtComponent({
|
|||
}
|
||||
state.qfValid = !!qf;
|
||||
|
||||
context.emit("input", qf || undefined);
|
||||
context.emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
|
||||
}, 500);
|
||||
emit("input", qf || undefined);
|
||||
emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
|
||||
}, 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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
field.organizers = [];
|
||||
|
||||
const { store, actions } = storeMap[field.type];
|
||||
if (!store.value.length) {
|
||||
await actions.refresh();
|
||||
|
@ -467,11 +466,12 @@ export default defineNuxtComponent({
|
|||
}
|
||||
return organizer;
|
||||
});
|
||||
field.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[];
|
||||
setOrganizerValues(field, index, field.organizers);
|
||||
}
|
||||
|
||||
function initFieldsError(error = "") {
|
||||
field.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[];
|
||||
return field;
|
||||
}
|
||||
|
||||
function initFieldsError(error = "") {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
@ -480,16 +480,17 @@ export default defineNuxtComponent({
|
|||
if (props.fieldDefs.length) {
|
||||
addField(props.fieldDefs[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFields() {
|
||||
async function initializeFields() {
|
||||
if (!props.initialQueryFilter?.parts?.length) {
|
||||
return initFieldsError();
|
||||
};
|
||||
}
|
||||
|
||||
const initFields: FieldWithId[] = [];
|
||||
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);
|
||||
if (!fieldDef) {
|
||||
error = true;
|
||||
|
@ -522,7 +523,7 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
if (isOrganizerType(field.type)) {
|
||||
hydrateOrganizers(field, index);
|
||||
await hydrateOrganizers(field, index);
|
||||
}
|
||||
}
|
||||
else if (field.type === "boolean") {
|
||||
|
@ -553,7 +554,7 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
initFields.push(field);
|
||||
});
|
||||
}
|
||||
|
||||
if (initFields.length && !error) {
|
||||
fields.value = initFields;
|
||||
|
@ -561,16 +562,18 @@ export default defineNuxtComponent({
|
|||
else {
|
||||
initFieldsError();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
initializeFields();
|
||||
await initializeFields();
|
||||
}
|
||||
catch (error) {
|
||||
initFieldsError(`Error initializing fields: ${(error || "").toString()}`);
|
||||
}
|
||||
});
|
||||
|
||||
function buildQueryFilterJSON(): QueryFilterJSON {
|
||||
function buildQueryFilterJSON(): QueryFilterJSON {
|
||||
const parts = fields.value.map((field) => {
|
||||
const part: QueryFilterJSONPart = {
|
||||
attributeName: field.name,
|
||||
|
@ -596,9 +599,9 @@ export default defineNuxtComponent({
|
|||
const qfJSON = { parts } as QueryFilterJSON;
|
||||
console.debug(`Built query filter JSON: ${JSON.stringify(qfJSON)}`);
|
||||
return qfJSON;
|
||||
}
|
||||
}
|
||||
|
||||
const config = computed(() => {
|
||||
const config = computed(() => {
|
||||
const baseColMaxWidth = 55;
|
||||
return {
|
||||
col: {
|
||||
|
@ -642,30 +645,6 @@ export default defineNuxtComponent({
|
|||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
Organizer,
|
||||
...toRefs(state),
|
||||
logOps,
|
||||
relOps,
|
||||
config,
|
||||
firstDayOfWeek,
|
||||
onDragEnd,
|
||||
// Fields
|
||||
fields,
|
||||
addField,
|
||||
setField,
|
||||
setLeftParenthesisValue,
|
||||
setRightParenthesisValue,
|
||||
setLogicalOperatorValue,
|
||||
setRelationalOperatorValue,
|
||||
setFieldValue,
|
||||
setFieldValues,
|
||||
setOrganizerValues,
|
||||
removeField,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
<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!" />
|
||||
<div v-if="loggedIn">
|
||||
<v-tooltip v-if="canEdit" bottom color="info">
|
||||
<template #activator="{ props }">
|
||||
<v-tooltip v-if="canEdit" location="bottom" color="info">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
icon
|
||||
variant="flat"
|
||||
|
@ -26,7 +26,7 @@
|
|||
size="small"
|
||||
color="info"
|
||||
class="ml-1"
|
||||
v-bind="props"
|
||||
v-bind="tooltipProps"
|
||||
@click="$emit('edit', true)"
|
||||
>
|
||||
<v-icon size="x-large">
|
||||
|
@ -86,7 +86,7 @@
|
|||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeTimelineBadge from "./RecipeTimelineBadge.vue";
|
||||
|
@ -97,49 +97,30 @@ const DELETE_EVENT = "delete";
|
|||
const CLOSE_EVENT = "close";
|
||||
const JSON_EVENT = "json";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeContextMenu, RecipeFavoriteBadge, RecipeTimelineBadge },
|
||||
props: {
|
||||
recipe: {
|
||||
required: true,
|
||||
type: Object as () => Recipe,
|
||||
},
|
||||
slug: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
recipeScale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
open: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
loggedIn: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipeId: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["print", "input", "delete", "close", "edit"],
|
||||
setup(_, context) {
|
||||
const deleteDialog = ref(false);
|
||||
interface Props {
|
||||
recipe: Recipe;
|
||||
slug: string;
|
||||
recipeScale?: number;
|
||||
open: boolean;
|
||||
name: string;
|
||||
loggedIn?: boolean;
|
||||
recipeId: string;
|
||||
canEdit?: boolean;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), {
|
||||
recipeScale: 1,
|
||||
loggedIn: false,
|
||||
canEdit: false,
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const editorButtons = [
|
||||
const emit = defineEmits(["print", "input", "delete", "close", "edit"]);
|
||||
|
||||
const deleteDialog = ref(false);
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const editorButtons = [
|
||||
{
|
||||
text: i18n.t("general.delete"),
|
||||
icon: $globals.icons.delete,
|
||||
|
@ -164,36 +145,27 @@ export default defineNuxtComponent({
|
|||
event: SAVE_EVENT,
|
||||
color: "success",
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
function emitHandler(event: string) {
|
||||
function emitHandler(event: string) {
|
||||
switch (event) {
|
||||
case CLOSE_EVENT:
|
||||
context.emit(CLOSE_EVENT);
|
||||
context.emit("input", false);
|
||||
emit("close");
|
||||
emit("input", false);
|
||||
break;
|
||||
case DELETE_EVENT:
|
||||
deleteDialog.value = true;
|
||||
break;
|
||||
default:
|
||||
context.emit(event);
|
||||
emit(event as any);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emitDelete() {
|
||||
context.emit(DELETE_EVENT);
|
||||
context.emit("input", false);
|
||||
}
|
||||
|
||||
return {
|
||||
deleteDialog,
|
||||
editorButtons,
|
||||
emitHandler,
|
||||
emitDelete,
|
||||
};
|
||||
},
|
||||
});
|
||||
function emitDelete() {
|
||||
emit("delete");
|
||||
emit("input", false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
>
|
||||
<template #prepend>
|
||||
<div class="ma-auto">
|
||||
<v-tooltip bottom>
|
||||
<v-tooltip location="bottom">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-icon v-bind="tooltipProps">
|
||||
{{ getIconDefinition(item.icon).icon }}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<template>
|
||||
<!-- Wrap v-hover with a div to provide a proper DOM element for the transition -->
|
||||
<v-lazy>
|
||||
<div>
|
||||
<v-hover
|
||||
v-slot="{ isHovering, props }"
|
||||
v-slot="{ isHovering, props: hoverProps }"
|
||||
:open-delay="50"
|
||||
>
|
||||
<v-card
|
||||
v-bind="props"
|
||||
v-bind="hoverProps"
|
||||
:class="{ 'on-hover': isHovering }"
|
||||
:style="{ cursor }"
|
||||
:elevation="isHovering ? 12 : 2"
|
||||
|
@ -99,10 +98,9 @@
|
|||
</v-card>
|
||||
</v-hover>
|
||||
</div>
|
||||
</v-lazy>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeChips from "./RecipeChips.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
|
@ -110,69 +108,41 @@ import RecipeCardImage from "./RecipeCardImage.vue";
|
|||
import RecipeRating from "./RecipeRating.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage },
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
ratingColor: {
|
||||
type: String,
|
||||
default: "secondary",
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "abc123",
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
recipeId: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
imageHeight: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
},
|
||||
emits: ["click", "delete"],
|
||||
setup(props) {
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||
});
|
||||
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
||||
|
||||
return {
|
||||
isOwnGroup,
|
||||
recipeRoute,
|
||||
showRecipeContent,
|
||||
cursor,
|
||||
};
|
||||
},
|
||||
interface Props {
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string | null;
|
||||
rating?: number;
|
||||
ratingColor?: string;
|
||||
image?: string;
|
||||
tags?: Array<any>;
|
||||
recipeId: string;
|
||||
imageHeight?: number;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
description: null,
|
||||
rating: 0,
|
||||
ratingColor: "secondary",
|
||||
image: "abc123",
|
||||
tags: () => [],
|
||||
imageHeight: 200,
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
click: [];
|
||||
delete: [slug: string];
|
||||
}>();
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||
});
|
||||
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -197,6 +167,7 @@ export default defineNuxtComponent({
|
|||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 8;
|
||||
line-clamp: 8;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -28,66 +28,51 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||
<script setup lang="ts">
|
||||
import { useStaticRoutes } from "~/composables/api";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
tiny: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
large: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
iconSize: {
|
||||
type: [Number, String],
|
||||
default: 100,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
recipeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
imageVersion: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: "100%",
|
||||
},
|
||||
},
|
||||
emits: ["click"],
|
||||
setup(props) {
|
||||
const api = useUserApi();
|
||||
interface Props {
|
||||
tiny?: boolean | null;
|
||||
small?: boolean | null;
|
||||
large?: boolean | null;
|
||||
iconSize?: number | string;
|
||||
slug?: string | null;
|
||||
recipeId: string;
|
||||
imageVersion?: string | null;
|
||||
height?: number | string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tiny: null,
|
||||
small: null,
|
||||
large: null,
|
||||
iconSize: 100,
|
||||
slug: null,
|
||||
imageVersion: null,
|
||||
height: "100%",
|
||||
});
|
||||
|
||||
const { recipeImage, recipeSmallImage, recipeTinyImage } = useStaticRoutes();
|
||||
defineEmits<{
|
||||
click: [];
|
||||
}>();
|
||||
|
||||
const fallBackImage = ref(false);
|
||||
const imageSize = computed(() => {
|
||||
const { recipeImage, recipeSmallImage, recipeTinyImage } = useStaticRoutes();
|
||||
|
||||
const fallBackImage = ref(false);
|
||||
const imageSize = computed(() => {
|
||||
if (props.tiny) return "tiny";
|
||||
if (props.small) return "small";
|
||||
if (props.large) return "large";
|
||||
return "large";
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
watch(
|
||||
() => props.recipeId,
|
||||
() => {
|
||||
fallBackImage.value = false;
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
function getImage(recipeId: string) {
|
||||
function getImage(recipeId: string) {
|
||||
switch (imageSize.value) {
|
||||
case "tiny":
|
||||
return recipeTinyImage(recipeId, props.imageVersion);
|
||||
|
@ -96,16 +81,7 @@ export default defineNuxtComponent({
|
|||
case "large":
|
||||
return recipeImage(recipeId, props.imageVersion);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
api,
|
||||
fallBackImage,
|
||||
imageSize,
|
||||
getImage,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
<v-expand-transition>
|
||||
<v-card
|
||||
:ripple="false"
|
||||
:class="isFlat ? 'mx-auto flat' : 'mx-auto'"
|
||||
:class="[
|
||||
isFlat ? 'mx-auto flat' : 'mx-auto',
|
||||
{ 'disable-highlight': disableHighlight },
|
||||
]"
|
||||
:style="{ cursor }"
|
||||
hover
|
||||
height="100%"
|
||||
|
@ -123,7 +126,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||
|
@ -131,78 +134,44 @@ import RecipeRating from "./RecipeRating.vue";
|
|||
import RecipeChips from "./RecipeChips.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeFavoriteBadge,
|
||||
RecipeContextMenu,
|
||||
RecipeRating,
|
||||
RecipeCardImage,
|
||||
RecipeChips,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "abc123",
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
recipeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isFlat: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
height: {
|
||||
type: [Number],
|
||||
default: 150,
|
||||
},
|
||||
},
|
||||
emits: ["selected", "delete"],
|
||||
setup(props) {
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||
});
|
||||
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
||||
|
||||
return {
|
||||
isOwnGroup,
|
||||
recipeRoute,
|
||||
showRecipeContent,
|
||||
cursor,
|
||||
};
|
||||
},
|
||||
interface Props {
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
rating?: number;
|
||||
image?: string;
|
||||
tags?: Array<any>;
|
||||
recipeId: string;
|
||||
vertical?: boolean;
|
||||
isFlat?: boolean;
|
||||
height?: number;
|
||||
disableHighlight?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
rating: 0,
|
||||
image: "abc123",
|
||||
tags: () => [],
|
||||
vertical: false,
|
||||
isFlat: false,
|
||||
height: 150,
|
||||
disableHighlight: false,
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
selected: [];
|
||||
delete: [slug: string];
|
||||
}>();
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
const showRecipeContent = computed(() => props.recipeId && props.slug);
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return showRecipeContent.value ? `/g/${groupSlug.value}/r/${props.slug}` : "";
|
||||
});
|
||||
const cursor = computed(() => showRecipeContent.value ? "pointer" : "auto");
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -241,4 +210,8 @@ export default defineNuxtComponent({
|
|||
box-shadow: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.disable-highlight :deep(.v-card__overlay) {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -36,11 +36,11 @@
|
|||
offset-y
|
||||
start
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
variant="text"
|
||||
:icon="$vuetify.display.xs"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
:loading="sortLoading"
|
||||
>
|
||||
<v-icon :start="!$vuetify.display.xs">
|
||||
|
@ -162,7 +162,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useThrottleFn } from "@vueuse/core";
|
||||
import RecipeCard from "./RecipeCard.vue";
|
||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||
|
@ -175,83 +175,69 @@ import type { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
|
|||
const REPLACE_RECIPES_EVENT = "replaceRecipes";
|
||||
const APPEND_RECIPES_EVENT = "appendRecipes";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeCard,
|
||||
RecipeCardMobile,
|
||||
},
|
||||
props: {
|
||||
disableToolbar: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disableSort: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
singleColumn: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipes: {
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
},
|
||||
query: {
|
||||
type: Object as () => RecipeSearchQuery,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { $vuetify } = useNuxtApp();
|
||||
const preferences = useUserSortPreferences();
|
||||
interface Props {
|
||||
disableToolbar?: boolean;
|
||||
disableSort?: boolean;
|
||||
icon?: string | null;
|
||||
title?: string | null;
|
||||
singleColumn?: boolean;
|
||||
recipes?: Recipe[];
|
||||
query?: RecipeSearchQuery | null;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disableToolbar: false,
|
||||
disableSort: false,
|
||||
icon: null,
|
||||
title: null,
|
||||
singleColumn: false,
|
||||
recipes: () => [],
|
||||
query: null,
|
||||
});
|
||||
|
||||
const EVENTS = {
|
||||
const emit = defineEmits<{
|
||||
replaceRecipes: [recipes: Recipe[]];
|
||||
appendRecipes: [recipes: Recipe[]];
|
||||
}>();
|
||||
|
||||
const { $vuetify } = useNuxtApp();
|
||||
const preferences = useUserSortPreferences();
|
||||
|
||||
const EVENTS = {
|
||||
az: "az",
|
||||
rating: "rating",
|
||||
created: "created",
|
||||
updated: "updated",
|
||||
lastMade: "lastMade",
|
||||
shuffle: "shuffle",
|
||||
};
|
||||
};
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const { $globals } = useNuxtApp();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const useMobileCards = computed(() => {
|
||||
const $auth = useMealieAuth();
|
||||
const { $globals } = useNuxtApp();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const useMobileCards = computed(() => {
|
||||
return $vuetify.display.smAndDown.value || preferences.value.useMobileCards;
|
||||
});
|
||||
});
|
||||
|
||||
const displayTitleIcon = computed(() => {
|
||||
const displayTitleIcon = computed(() => {
|
||||
return props.icon || $globals.icons.tags;
|
||||
});
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
sortLoading: false,
|
||||
});
|
||||
const sortLoading = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
|
||||
const page = ref(1);
|
||||
const perPage = 32;
|
||||
const hasMore = ref(true);
|
||||
const ready = ref(false);
|
||||
const loading = ref(false);
|
||||
const page = ref(1);
|
||||
const perPage = 32;
|
||||
const hasMore = ref(true);
|
||||
const ready = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const { fetchMore, getRandom } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const router = useRouter();
|
||||
const { fetchMore, getRandom } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
const router = useRouter();
|
||||
|
||||
const queryFilter = computed(() => {
|
||||
return props.query.queryFilter || null;
|
||||
const queryFilter = computed(() => {
|
||||
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)
|
||||
|
||||
|
@ -265,9 +251,9 @@ export default defineNuxtComponent({
|
|||
// } else {
|
||||
// return orderByFilter;
|
||||
// }
|
||||
});
|
||||
});
|
||||
|
||||
async function fetchRecipes(pageCount = 1) {
|
||||
async function fetchRecipes(pageCount = 1) {
|
||||
const orderDir = props.query?.orderDirection || preferences.value.orderDirection;
|
||||
const orderByNullPosition = props.query?.orderByNullPosition || orderDir === "asc" ? "first" : "last";
|
||||
return await fetchMore(
|
||||
|
@ -280,17 +266,17 @@ export default defineNuxtComponent({
|
|||
// we use a computed queryFilter to filter out recipes that have a null value for the property we're sorting by
|
||||
queryFilter.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
onMounted(async () => {
|
||||
await initRecipes();
|
||||
ready.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
let lastQuery: string | undefined = JSON.stringify(props.query);
|
||||
watch(
|
||||
let lastQuery: string | undefined = JSON.stringify(props.query);
|
||||
watch(
|
||||
() => props.query,
|
||||
async (newValue: RecipeSearchQuery | undefined) => {
|
||||
async (newValue: RecipeSearchQuery | undefined | null) => {
|
||||
const newValueString = JSON.stringify(newValue);
|
||||
if (lastQuery !== newValueString) {
|
||||
lastQuery = newValueString;
|
||||
|
@ -299,9 +285,9 @@ export default defineNuxtComponent({
|
|||
ready.value = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
async function initRecipes() {
|
||||
async function initRecipes() {
|
||||
page.value = 1;
|
||||
hasMore.value = true;
|
||||
|
||||
|
@ -315,10 +301,10 @@ export default defineNuxtComponent({
|
|||
// since we doubled the first call, we also need to advance the page
|
||||
page.value = page.value + 1;
|
||||
|
||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
}
|
||||
emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
}
|
||||
|
||||
const infiniteScroll = useThrottleFn(async () => {
|
||||
const infiniteScroll = useThrottleFn(async () => {
|
||||
if (!hasMore.value || loading.value) {
|
||||
return;
|
||||
}
|
||||
|
@ -331,14 +317,14 @@ export default defineNuxtComponent({
|
|||
hasMore.value = false;
|
||||
}
|
||||
if (newRecipes.length) {
|
||||
context.emit(APPEND_RECIPES_EVENT, newRecipes);
|
||||
emit(APPEND_RECIPES_EVENT, newRecipes);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}, 500);
|
||||
}, 500);
|
||||
|
||||
async function sortRecipes(sortType: string) {
|
||||
if (state.sortLoading || loading.value) {
|
||||
async function sortRecipes(sortType: string) {
|
||||
if (sortLoading.value || loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -403,45 +389,29 @@ export default defineNuxtComponent({
|
|||
page.value = 1;
|
||||
hasMore.value = true;
|
||||
|
||||
state.sortLoading = true;
|
||||
sortLoading.value = true;
|
||||
loading.value = true;
|
||||
|
||||
// fetch new recipes
|
||||
const newRecipes = await fetchRecipes();
|
||||
context.emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
emit(REPLACE_RECIPES_EVENT, newRecipes);
|
||||
|
||||
state.sortLoading = false;
|
||||
sortLoading.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function navigateRandom() {
|
||||
async function navigateRandom() {
|
||||
const recipe = await getRandom(props.query, queryFilter.value);
|
||||
if (!recipe?.slug) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.push(`/g/${groupSlug.value}/r/${recipe.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMobileCards() {
|
||||
function toggleMobileCards() {
|
||||
preferences.value.useMobileCards = !preferences.value.useMobileCards;
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
displayTitleIcon,
|
||||
EVENTS,
|
||||
infiniteScroll,
|
||||
ready,
|
||||
loading,
|
||||
navigateRandom,
|
||||
preferences,
|
||||
sortRecipes,
|
||||
toggleMobileCards,
|
||||
useMobileCards,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -23,66 +23,38 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
|
||||
|
||||
export type UrlPrefixParam = "tags" | "categories" | "tools";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
truncate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
items: {
|
||||
type: Array as () => RecipeCategory[] | RecipeTag[] | RecipeTool[],
|
||||
default: () => [],
|
||||
},
|
||||
title: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
urlPrefix: {
|
||||
type: String as () => UrlPrefixParam,
|
||||
default: "categories",
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 999,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: ["item-selected"],
|
||||
setup(props) {
|
||||
const $auth = useMealieAuth();
|
||||
interface Props {
|
||||
truncate?: boolean;
|
||||
items?: RecipeCategory[] | RecipeTag[] | RecipeTool[];
|
||||
title?: boolean;
|
||||
urlPrefix?: UrlPrefixParam;
|
||||
limit?: number;
|
||||
small?: boolean;
|
||||
maxWidth?: string | null;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
truncate: false,
|
||||
items: () => [],
|
||||
title: false,
|
||||
urlPrefix: "categories",
|
||||
limit: 999,
|
||||
small: false,
|
||||
maxWidth: null,
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
const baseRecipeRoute = computed<string>(() => {
|
||||
return `/g/${groupSlug.value}`;
|
||||
});
|
||||
|
||||
function truncateText(text: string, length = 20, clamp = "...") {
|
||||
defineEmits(["item-selected"]);
|
||||
function truncateText(text: string, length = 20, clamp = "...") {
|
||||
if (!props.truncate) return text;
|
||||
const node = document.createElement("div");
|
||||
node.innerHTML = text;
|
||||
const content = node.textContent || "";
|
||||
return content.length > length ? content.slice(0, length) + clamp : content;
|
||||
}
|
||||
|
||||
return {
|
||||
baseRecipeRoute,
|
||||
truncateText,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
|
|
@ -12,7 +12,12 @@
|
|||
@confirm="deleteRecipe()"
|
||||
>
|
||||
<v-card-text>
|
||||
<template v-if="isAdminAndNotOwner">
|
||||
{{ $t("recipe.admin-delete-confirmation") }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t("recipe.delete-confirmation") }}
|
||||
</template>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<BaseDialog
|
||||
|
@ -50,12 +55,12 @@
|
|||
max-width="290px"
|
||||
min-width="auto"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-text-field
|
||||
v-model="newMealdateString"
|
||||
:label="$t('general.date')"
|
||||
:prepend-icon="$globals.icons.calendar"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
|
@ -95,7 +100,7 @@
|
|||
:open-on-hover="$vuetify.display.mdAndUp"
|
||||
content-class="d-print-none"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
icon
|
||||
:variant="fab ? 'flat' : undefined"
|
||||
|
@ -103,7 +108,7 @@
|
|||
:size="fab ? 'small' : undefined"
|
||||
:color="fab ? 'info' : 'secondary'"
|
||||
:fab="fab"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
@click.prevent
|
||||
>
|
||||
<v-icon
|
||||
|
@ -125,32 +130,27 @@
|
|||
</v-list-item>
|
||||
<div v-if="useItems.recipeActions && recipeActions && recipeActions.length">
|
||||
<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-for="(action, index) in recipeActions"
|
||||
:key="index"
|
||||
class="pl-6"
|
||||
@click="executeRecipeAction(action)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon color="undefined">
|
||||
{{ $globals.icons.linkVariantPlus }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ action.title }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-list-group>
|
||||
</div>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeDialogAddToShoppingList from "./RecipeDialogAddToShoppingList.vue";
|
||||
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
|
||||
import RecipeDialogShare from "./RecipeDialogShare.vue";
|
||||
|
@ -186,16 +186,22 @@ export interface ContextMenuItem {
|
|||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeDialogAddToShoppingList,
|
||||
RecipeDialogPrintPreferences,
|
||||
RecipeDialogShare,
|
||||
},
|
||||
props: {
|
||||
useItems: {
|
||||
type: Object as () => ContextMenuIncludes,
|
||||
default: () => ({
|
||||
interface Props {
|
||||
useItems?: ContextMenuIncludes;
|
||||
appendItems?: ContextMenuItem[];
|
||||
leadingItems?: ContextMenuItem[];
|
||||
menuTop?: boolean;
|
||||
fab?: boolean;
|
||||
color?: string;
|
||||
slug: string;
|
||||
menuIcon?: string | null;
|
||||
name: string;
|
||||
recipe?: Recipe;
|
||||
recipeId: string;
|
||||
recipeScale?: number;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
useItems: () => ({
|
||||
delete: true,
|
||||
edit: true,
|
||||
download: true,
|
||||
|
@ -207,94 +213,61 @@ export default defineNuxtComponent({
|
|||
share: true,
|
||||
recipeActions: true,
|
||||
}),
|
||||
},
|
||||
// Append items are added at the end of the useItems list
|
||||
appendItems: {
|
||||
type: Array as () => ContextMenuItem[],
|
||||
default: () => [],
|
||||
},
|
||||
// Append items are added at the beginning of the useItems list
|
||||
leadingItems: {
|
||||
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();
|
||||
appendItems: () => [],
|
||||
leadingItems: () => [],
|
||||
menuTop: true,
|
||||
fab: false,
|
||||
color: "primary",
|
||||
menuIcon: null,
|
||||
recipe: undefined,
|
||||
recipeScale: 1,
|
||||
});
|
||||
|
||||
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 newMealdateString = computed(() => {
|
||||
return state.newMealdate.toISOString().substring(0, 10);
|
||||
});
|
||||
const api = useUserApi();
|
||||
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const { $globals } = useNuxtApp();
|
||||
const { household } = useHouseholdSelf();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
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 route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
const newMealdateString = computed(() => {
|
||||
// Format the date to YYYY-MM-DD in the same timezone as newMealdate
|
||||
const year = newMealdate.value.getFullYear();
|
||||
const month = String(newMealdate.value.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(newMealdate.value.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
});
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const { $globals } = useNuxtApp();
|
||||
const { household } = useHouseholdSelf();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user.value?.groupSlug || "");
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||
});
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// Context Menu Setup
|
||||
// ===========================================================================
|
||||
// Context Menu Setup
|
||||
|
||||
const defaultItems: { [key: string]: ContextMenuItem } = {
|
||||
const defaultItems: { [key: string]: ContextMenuItem } = {
|
||||
edit: {
|
||||
title: i18n.t("general.edit"),
|
||||
icon: $globals.icons.edit,
|
||||
|
@ -358,50 +331,64 @@ export default defineNuxtComponent({
|
|||
event: "share",
|
||||
isPublic: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Add leading and Appending Items
|
||||
menuItems.value = [...menuItems.value, ...props.leadingItems, ...props.appendItems];
|
||||
|
||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||
|
||||
// ===========================================================================
|
||||
// Context Menu Event Handler
|
||||
|
||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||
const recipeRef = ref<Recipe | undefined>(props.recipe);
|
||||
const recipeRefWithScale = computed(() =>
|
||||
recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined,
|
||||
);
|
||||
const isAdminAndNotOwner = computed(() => {
|
||||
return (
|
||||
$auth.user.value?.admin
|
||||
&& $auth.user.value?.id !== recipeRef.value?.userId
|
||||
);
|
||||
});
|
||||
const canDelete = computed(() => {
|
||||
const user = $auth.user.value;
|
||||
const recipe = recipeRef.value;
|
||||
return user && recipe && (user.admin || user.id === recipe.userId);
|
||||
});
|
||||
|
||||
// Get Default Menu Items Specified in Props
|
||||
for (const [key, value] of Object.entries(props.useItems)) {
|
||||
if (!value) continue;
|
||||
|
||||
// Skip delete if not allowed
|
||||
if (key === "delete" && !canDelete.value) continue;
|
||||
|
||||
// Get Default Menu Items Specified in Props
|
||||
for (const [key, value] of Object.entries(props.useItems)) {
|
||||
if (value) {
|
||||
const item = defaultItems[key];
|
||||
if (item && (item.isPublic || isOwnGroup.value)) {
|
||||
state.menuItems.push(item);
|
||||
}
|
||||
}
|
||||
menuItems.value.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Add leading and Appending Items
|
||||
state.menuItems = [...state.menuItems, ...props.leadingItems, ...props.appendItems];
|
||||
|
||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
||||
|
||||
// ===========================================================================
|
||||
// Context Menu Event Handler
|
||||
|
||||
const shoppingLists = ref<ShoppingListSummary[]>();
|
||||
const recipeRef = ref<Recipe | undefined>(props.recipe);
|
||||
const recipeRefWithScale = computed(() =>
|
||||
recipeRef.value ? { scale: props.recipeScale, ...recipeRef.value } : undefined,
|
||||
);
|
||||
|
||||
async function getShoppingLists() {
|
||||
async function getShoppingLists() {
|
||||
const { data } = await api.shopping.lists.getAll(1, -1, { orderBy: "name", orderDirection: "asc" });
|
||||
if (data) {
|
||||
shoppingLists.value = data.items ?? [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRecipe() {
|
||||
async function refreshRecipe() {
|
||||
const { data } = await api.recipes.getOne(props.slug);
|
||||
if (data) {
|
||||
recipeRef.value = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
const groupRecipeActionsStore = useGroupRecipeActions();
|
||||
const router = useRouter();
|
||||
const groupRecipeActionsStore = useGroupRecipeActions();
|
||||
|
||||
async function executeRecipeAction(action: GroupRecipeActionOut) {
|
||||
async function executeRecipeAction(action: GroupRecipeActionOut) {
|
||||
if (!props.recipe) return;
|
||||
const response = await groupRecipeActionsStore.execute(action, props.recipe, props.recipeScale);
|
||||
|
||||
|
@ -413,30 +400,30 @@ export default defineNuxtComponent({
|
|||
alert.error(i18n.t("events.something-went-wrong"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRecipe() {
|
||||
async function deleteRecipe() {
|
||||
const { data } = await api.recipes.deleteOne(props.slug);
|
||||
if (data?.slug) {
|
||||
router.push(`/g/${groupSlug.value}`);
|
||||
}
|
||||
context.emit("delete", props.slug);
|
||||
}
|
||||
emit("delete", props.slug);
|
||||
}
|
||||
|
||||
const download = useDownloader();
|
||||
const download = useDownloader();
|
||||
|
||||
async function handleDownloadEvent() {
|
||||
async function handleDownloadEvent() {
|
||||
const { data } = await api.recipes.getZipToken(props.slug);
|
||||
|
||||
if (data) {
|
||||
download(api.recipes.getZipRedirectUrl(props.slug, data.token), `${props.slug}.zip`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function addRecipeToPlan() {
|
||||
async function addRecipeToPlan() {
|
||||
const { response } = await api.mealplans.createOne({
|
||||
date: newMealdateString.value,
|
||||
entryType: state.newMealType,
|
||||
entryType: newMealType.value,
|
||||
title: "",
|
||||
text: "",
|
||||
recipeId: props.recipeId,
|
||||
|
@ -448,34 +435,34 @@ export default defineNuxtComponent({
|
|||
else {
|
||||
alert.error(i18n.t("recipe.failed-to-add-recipe-to-mealplan") as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function duplicateRecipe() {
|
||||
const { data } = await api.recipes.duplicateOne(props.slug, state.recipeName);
|
||||
async function duplicateRecipe() {
|
||||
const { data } = await api.recipes.duplicateOne(props.slug, recipeName.value);
|
||||
if (data && data.slug) {
|
||||
router.push(`/g/${groupSlug.value}/r/${data.slug}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Print is handled as an event in the parent component
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||
// Note: Print is handled as an event in the parent component
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
const eventHandlers: { [key: string]: () => void | Promise<any> } = {
|
||||
delete: () => {
|
||||
state.recipeDeleteDialog = true;
|
||||
recipeDeleteDialog.value = true;
|
||||
},
|
||||
edit: () => router.push(`/g/${groupSlug.value}/r/${props.slug}` + "?edit=true"),
|
||||
download: handleDownloadEvent,
|
||||
duplicate: () => {
|
||||
state.recipeDuplicateDialog = true;
|
||||
recipeDuplicateDialog.value = true;
|
||||
},
|
||||
mealplanner: () => {
|
||||
state.mealplannerDialog = true;
|
||||
mealplannerDialog.value = true;
|
||||
},
|
||||
printPreferences: async () => {
|
||||
if (!recipeRef.value) {
|
||||
await refreshRecipe();
|
||||
}
|
||||
state.printPreferencesDialog = true;
|
||||
printPreferencesDialog.value = true;
|
||||
},
|
||||
shoppingList: () => {
|
||||
const promises: Promise<void>[] = [getShoppingLists()];
|
||||
|
@ -484,45 +471,27 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
Promise.allSettled(promises).then(() => {
|
||||
state.shoppingListDialog = true;
|
||||
shoppingListDialog.value = true;
|
||||
});
|
||||
},
|
||||
share: () => {
|
||||
state.shareDialog = true;
|
||||
shareDialog.value = true;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function contextMenuEventHandler(eventKey: string) {
|
||||
function contextMenuEventHandler(eventKey: string) {
|
||||
const handler = eventHandlers[eventKey];
|
||||
|
||||
if (handler && typeof handler === "function") {
|
||||
handler();
|
||||
state.loading = false;
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
context.emit(eventKey);
|
||||
state.loading = false;
|
||||
}
|
||||
emit(eventKey);
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
const planTypeOptions = usePlanTypeOptions();
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
newMealdateString,
|
||||
recipeRef,
|
||||
recipeRefWithScale,
|
||||
executeRecipeAction,
|
||||
recipeActions: groupRecipeActionsStore.recipeActions,
|
||||
shoppingLists,
|
||||
duplicateRecipe,
|
||||
contextMenuEventHandler,
|
||||
deleteRecipe,
|
||||
addRecipeToPlan,
|
||||
icon,
|
||||
planTypeOptions,
|
||||
firstDayOfWeek,
|
||||
};
|
||||
},
|
||||
});
|
||||
const planTypeOptions = usePlanTypeOptions();
|
||||
const recipeActions = groupRecipeActionsStore.recipeActions;
|
||||
</script>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { whenever } from "@vueuse/core";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import type { IngredientFood, IngredientUnit } from "~/lib/api/types/recipe";
|
||||
|
@ -42,56 +42,47 @@ export interface GenericAlias {
|
|||
name: string;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
data: {
|
||||
type: Object as () => IngredientFood | IngredientUnit,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["submit", "update:modelValue", "cancel"],
|
||||
setup(props, context) {
|
||||
// V-Model Support
|
||||
const dialog = computed({
|
||||
get: () => {
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
interface Props {
|
||||
data: IngredientFood | IngredientUnit;
|
||||
}
|
||||
|
||||
function createAlias() {
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
submit: [aliases: GenericAlias[]];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
// V-Model Support
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
|
||||
function createAlias() {
|
||||
aliases.value.push({
|
||||
name: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function deleteAlias(index: number) {
|
||||
function deleteAlias(index: number) {
|
||||
aliases.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const aliases = ref<GenericAlias[]>(props.data.aliases || []);
|
||||
function initAliases() {
|
||||
const aliases = ref<GenericAlias[]>(props.data.aliases || []);
|
||||
function initAliases() {
|
||||
aliases.value = [...props.data.aliases || []];
|
||||
if (!aliases.value.length) {
|
||||
createAlias();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initAliases();
|
||||
whenever(
|
||||
() => props.modelValue,
|
||||
initAliases();
|
||||
whenever(
|
||||
() => dialog.value,
|
||||
() => {
|
||||
initAliases();
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
function saveAliases() {
|
||||
function saveAliases() {
|
||||
const seenAliasNames: string[] = [];
|
||||
const keepAliases: GenericAlias[] = [];
|
||||
aliases.value.forEach((alias) => {
|
||||
|
@ -111,17 +102,6 @@ export default defineNuxtComponent({
|
|||
});
|
||||
|
||||
aliases.value = keepAliases;
|
||||
context.emit("submit", keepAliases);
|
||||
}
|
||||
|
||||
return {
|
||||
aliases,
|
||||
createAlias,
|
||||
dialog,
|
||||
deleteAlias,
|
||||
saveAliases,
|
||||
validators,
|
||||
};
|
||||
},
|
||||
});
|
||||
emit("submit", keepAliases);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -3,12 +3,13 @@
|
|||
v-model="selected"
|
||||
item-key="id"
|
||||
show-select
|
||||
:sort-by="[{ key: 'dateAdded', order: 'desc' }]"
|
||||
:sort-by="sortBy"
|
||||
:headers="headers"
|
||||
:items="recipes"
|
||||
:items-per-page="15"
|
||||
class="elevation-0"
|
||||
:loading="loading"
|
||||
return-object
|
||||
>
|
||||
<template #[`item.name`]="{ item }">
|
||||
<a
|
||||
|
@ -61,7 +62,7 @@
|
|||
</v-data-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import UserAvatar from "../User/UserAvatar.vue";
|
||||
import RecipeChip from "./RecipeChips.vue";
|
||||
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 { RecipeTag } from "~/lib/api/types/household";
|
||||
|
||||
const INPUT_EVENT = "update:modelValue";
|
||||
|
||||
interface ShowHeaders {
|
||||
id: boolean;
|
||||
owner: boolean;
|
||||
|
@ -83,53 +82,43 @@ interface ShowHeaders {
|
|||
dateAdded: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeChip, UserAvatar },
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as PropType<Recipe[]>,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
recipes: {
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
},
|
||||
showHeaders: {
|
||||
type: Object as () => ShowHeaders,
|
||||
required: false,
|
||||
default: () => {
|
||||
return {
|
||||
interface Props {
|
||||
loading?: boolean;
|
||||
recipes?: Recipe[];
|
||||
showHeaders?: ShowHeaders;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
recipes: () => [],
|
||||
showHeaders: () => ({
|
||||
id: true,
|
||||
owner: false,
|
||||
tags: true,
|
||||
categories: true,
|
||||
tools: true,
|
||||
recipeServings: true,
|
||||
recipeYieldQuantity: true,
|
||||
recipeYield: true,
|
||||
dateAdded: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ["click"],
|
||||
setup(props, context) {
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const groupSlug = $auth.user.value?.groupSlug;
|
||||
const router = useRouter();
|
||||
const selected = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => context.emit(INPUT_EVENT, value),
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
const headers = computed(() => {
|
||||
const hdrs: Array<{ title: string; value: string; align?: string; sortable?: boolean }> = [];
|
||||
defineEmits<{
|
||||
click: [];
|
||||
}>();
|
||||
|
||||
const selected = defineModel<Recipe[]>({ default: () => [] });
|
||||
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const groupSlug = $auth.user.value?.groupSlug;
|
||||
const router = useRouter();
|
||||
|
||||
// Initialize sort state with default sorting by dateAdded descending
|
||||
const sortBy = ref([{ key: "dateAdded", order: "desc" as const }]);
|
||||
|
||||
const headers = computed(() => {
|
||||
const hdrs: Array<{ title: string; value: string; align?: "center" | "start" | "end"; sortable?: boolean }> = [];
|
||||
|
||||
if (props.showHeaders.id) {
|
||||
hdrs.push({ title: i18n.t("general.id"), value: "id" });
|
||||
|
@ -162,57 +151,45 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
return hdrs;
|
||||
});
|
||||
});
|
||||
|
||||
function formatDate(date: string) {
|
||||
function formatDate(date: string) {
|
||||
try {
|
||||
return i18n.d(Date.parse(date), "medium");
|
||||
}
|
||||
catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============
|
||||
// Group Members
|
||||
const api = useUserApi();
|
||||
const members = ref<UserSummary[]>([]);
|
||||
// ============
|
||||
// Group Members
|
||||
const api = useUserApi();
|
||||
const members = ref<UserSummary[]>([]);
|
||||
|
||||
async function refreshMembers() {
|
||||
async function refreshMembers() {
|
||||
const { data } = await api.groups.fetchMembers();
|
||||
if (data) {
|
||||
members.value = data.items;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterItems(item: RecipeTag | RecipeCategory | RecipeTool, itemType: string) {
|
||||
function filterItems(item: RecipeTag | RecipeCategory | RecipeTool, itemType: string) {
|
||||
if (!groupSlug || !item.id) {
|
||||
return;
|
||||
}
|
||||
router.push(`/g/${groupSlug}?${itemType}=${item.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(() => {
|
||||
refreshMembers();
|
||||
});
|
||||
});
|
||||
|
||||
function getMember(id: string) {
|
||||
function getMember(id: string) {
|
||||
if (members.value[0]) {
|
||||
return members.value.find(m => m.id === id)?.fullName;
|
||||
}
|
||||
|
||||
return i18n.t("general.none");
|
||||
}
|
||||
|
||||
return {
|
||||
selected,
|
||||
groupSlug,
|
||||
headers,
|
||||
formatDate,
|
||||
members,
|
||||
getMember,
|
||||
filterItems,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<BaseDialog
|
||||
v-if="shoppingListIngredientDialog"
|
||||
v-model="dialog"
|
||||
:title="selectedShoppingList ? selectedShoppingList.name : $t('recipe.add-to-list')"
|
||||
:title="selectedShoppingList?.name || $t('recipe.add-to-list')"
|
||||
:icon="$globals.icons.cartCheck"
|
||||
width="70%"
|
||||
:submit-text="$t('recipe.add-to-list')"
|
||||
|
@ -130,20 +130,23 @@
|
|||
.ingredients[i]
|
||||
.checked"
|
||||
>
|
||||
<v-container class="pa-0 ma-0">
|
||||
<v-row no-gutters>
|
||||
<v-checkbox
|
||||
hide-details
|
||||
:model-value="ingredientData.checked"
|
||||
class="pt-0 my-auto py-auto"
|
||||
class="pt-0 my-auto py-auto mr-2"
|
||||
color="secondary"
|
||||
density="compact"
|
||||
/>
|
||||
<div :key="ingredientData.ingredient.quantity">
|
||||
<div :key="`${ingredientData.ingredient.quantity || 'no-qty'}-${i}`" class="pa-auto my-auto">
|
||||
<RecipeIngredientListItem
|
||||
:ingredient="ingredientData.ingredient"
|
||||
:disable-amount="ingredientData.disableAmount"
|
||||
:scale="recipeSection.recipeScale"
|
||||
/>
|
||||
</div>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -172,7 +175,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { toRefs } from "@vueuse/core";
|
||||
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
@ -188,7 +191,6 @@ export interface RecipeWithScale extends Recipe {
|
|||
export interface ShoppingListIngredient {
|
||||
checked: boolean;
|
||||
ingredient: RecipeIngredient;
|
||||
disableAmount: boolean;
|
||||
}
|
||||
|
||||
export interface ShoppingListIngredientSection {
|
||||
|
@ -203,61 +205,43 @@ export interface ShoppingListRecipeIngredientSection {
|
|||
ingredientSections: ShoppingListIngredientSection[];
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeIngredientListItem,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipes: {
|
||||
type: Array as () => RecipeWithScale[],
|
||||
default: undefined,
|
||||
},
|
||||
shoppingLists: {
|
||||
type: Array as () => ShoppingListSummary[],
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const api = useUserApi();
|
||||
const preferences = useShoppingListPreferences();
|
||||
const ready = ref(false);
|
||||
interface Props {
|
||||
recipes?: RecipeWithScale[];
|
||||
shoppingLists?: ShoppingListSummary[];
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
recipes: undefined,
|
||||
shoppingLists: () => [],
|
||||
});
|
||||
|
||||
// v-model support
|
||||
const dialog = computed({
|
||||
get: () => {
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:modelValue", val);
|
||||
initState();
|
||||
},
|
||||
});
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
|
||||
const state = reactive({
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const api = useUserApi();
|
||||
const preferences = useShoppingListPreferences();
|
||||
const ready = ref(false);
|
||||
|
||||
const state = reactive({
|
||||
shoppingListDialog: true,
|
||||
shoppingListIngredientDialog: false,
|
||||
shoppingListShowAllToggled: false,
|
||||
});
|
||||
});
|
||||
|
||||
const userHousehold = computed(() => {
|
||||
const { shoppingListDialog, shoppingListIngredientDialog, shoppingListShowAllToggled: _shoppingListShowAllToggled } = toRefs(state);
|
||||
|
||||
const userHousehold = computed(() => {
|
||||
return $auth.user.value?.householdSlug || "";
|
||||
});
|
||||
});
|
||||
|
||||
const shoppingListChoices = computed(() => {
|
||||
const shoppingListChoices = computed(() => {
|
||||
return props.shoppingLists.filter(list => preferences.value.viewAllLists || list.userId === $auth.user.value?.id);
|
||||
});
|
||||
});
|
||||
|
||||
const recipeIngredientSections = ref<ShoppingListRecipeIngredientSection[]>([]);
|
||||
const selectedShoppingList = ref<ShoppingListSummary | null>(null);
|
||||
const recipeIngredientSections = ref<ShoppingListRecipeIngredientSection[]>([]);
|
||||
const selectedShoppingList = ref<ShoppingListSummary | null>(null);
|
||||
|
||||
watchEffect(
|
||||
watchEffect(
|
||||
() => {
|
||||
if (shoppingListChoices.value.length === 1 && !state.shoppingListShowAllToggled) {
|
||||
selectedShoppingList.value = shoppingListChoices.value[0];
|
||||
|
@ -267,9 +251,15 @@ export default defineNuxtComponent({
|
|||
ready.value = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
|
||||
watch(dialog, (val) => {
|
||||
if (!val) {
|
||||
initState();
|
||||
}
|
||||
});
|
||||
|
||||
async function consolidateRecipesIntoSections(recipes: RecipeWithScale[]) {
|
||||
const recipeSectionMap = new Map<string, ShoppingListRecipeIngredientSection>();
|
||||
for (const recipe of recipes) {
|
||||
if (!recipe.slug) {
|
||||
|
@ -277,7 +267,10 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -299,7 +292,6 @@ export default defineNuxtComponent({
|
|||
return {
|
||||
checked: !householdsWithFood.includes(userHousehold.value),
|
||||
ingredient: ing,
|
||||
disableAmount: recipe.settings?.disableAmount || false,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -347,19 +339,19 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
recipeIngredientSections.value = Array.from(recipeSectionMap.values());
|
||||
}
|
||||
}
|
||||
|
||||
function initState() {
|
||||
function initState() {
|
||||
state.shoppingListDialog = true;
|
||||
state.shoppingListIngredientDialog = false;
|
||||
state.shoppingListShowAllToggled = false;
|
||||
recipeIngredientSections.value = [];
|
||||
selectedShoppingList.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
initState();
|
||||
initState();
|
||||
|
||||
async function openShoppingListIngredientDialog(list: ShoppingListSummary) {
|
||||
async function openShoppingListIngredientDialog(list: ShoppingListSummary) {
|
||||
if (!props.recipes?.length) {
|
||||
return;
|
||||
}
|
||||
|
@ -368,13 +360,13 @@ export default defineNuxtComponent({
|
|||
await consolidateRecipesIntoSections(props.recipes);
|
||||
state.shoppingListDialog = false;
|
||||
state.shoppingListIngredientDialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
function setShowAllToggled() {
|
||||
function setShowAllToggled() {
|
||||
state.shoppingListShowAllToggled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function bulkCheckIngredients(value = true) {
|
||||
function bulkCheckIngredients(value = true) {
|
||||
recipeIngredientSections.value.forEach((recipeSection) => {
|
||||
recipeSection.ingredientSections.forEach((ingSection) => {
|
||||
ingSection.ingredients.forEach((ing) => {
|
||||
|
@ -382,9 +374,9 @@ export default defineNuxtComponent({
|
|||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function addRecipesToList() {
|
||||
async function addRecipesToList() {
|
||||
if (!selectedShoppingList.value) {
|
||||
return;
|
||||
}
|
||||
|
@ -420,23 +412,7 @@ export default defineNuxtComponent({
|
|||
state.shoppingListDialog = false;
|
||||
state.shoppingListIngredientDialog = false;
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
dialog,
|
||||
preferences,
|
||||
ready,
|
||||
shoppingListChoices,
|
||||
...toRefs(state),
|
||||
addRecipesToList,
|
||||
bulkCheckIngredients,
|
||||
openShoppingListIngredientDialog,
|
||||
setShowAllToggled,
|
||||
recipeIngredientSections,
|
||||
selectedShoppingList,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
v-model="dialog"
|
||||
width="800"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<BaseButton
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
@click="inputText = inputTextProp"
|
||||
>
|
||||
{{ $t("new-recipe.bulk-add") }}
|
||||
|
@ -89,62 +89,61 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
inputTextProp: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
emits: ["bulk-data"],
|
||||
setup(props, context) {
|
||||
const state = reactive({
|
||||
dialog: false,
|
||||
inputText: props.inputTextProp,
|
||||
});
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
inputTextProp?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
inputTextProp: "",
|
||||
});
|
||||
|
||||
function splitText() {
|
||||
return state.inputText.split("\n").filter(line => !(line === "\n" || !line));
|
||||
}
|
||||
const emit = defineEmits<{
|
||||
"bulk-data": [data: string[]];
|
||||
}>();
|
||||
|
||||
function removeFirstCharacter() {
|
||||
state.inputText = splitText()
|
||||
const dialog = ref(false);
|
||||
const inputText = ref(props.inputTextProp);
|
||||
|
||||
function splitText() {
|
||||
return inputText.value.split("\n").filter(line => !(line === "\n" || !line));
|
||||
}
|
||||
|
||||
function removeFirstCharacter() {
|
||||
inputText.value = splitText()
|
||||
.map(line => line.substring(1))
|
||||
.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
const numberedLineRegex = /\d+[.):] /gm;
|
||||
const numberedLineRegex = /\d+[.):] /gm;
|
||||
|
||||
function splitByNumberedLine() {
|
||||
function splitByNumberedLine() {
|
||||
// Split inputText by numberedLineRegex
|
||||
const matches = state.inputText.match(numberedLineRegex);
|
||||
const matches = inputText.value.match(numberedLineRegex);
|
||||
|
||||
matches?.forEach((match, idx) => {
|
||||
const replaceText = idx === 0 ? "" : "\n";
|
||||
state.inputText = state.inputText.replace(match, replaceText);
|
||||
inputText.value = inputText.value.replace(match, replaceText);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function trimAllLines() {
|
||||
function trimAllLines() {
|
||||
const splitLines = splitText();
|
||||
|
||||
splitLines.forEach((element: string, index: number) => {
|
||||
splitLines[index] = element.trim();
|
||||
});
|
||||
|
||||
state.inputText = splitLines.join("\n");
|
||||
}
|
||||
inputText.value = splitLines.join("\n");
|
||||
}
|
||||
|
||||
function save() {
|
||||
context.emit("bulk-data", splitText());
|
||||
state.dialog = false;
|
||||
}
|
||||
function save() {
|
||||
emit("bulk-data", splitText());
|
||||
dialog.value = false;
|
||||
}
|
||||
|
||||
const i18n = useI18n();
|
||||
const i18n = useI18n();
|
||||
|
||||
const utilities = [
|
||||
const utilities = [
|
||||
{
|
||||
id: "trim-whitespace",
|
||||
description: i18n.t("new-recipe.trim-whitespace-description"),
|
||||
|
@ -160,17 +159,5 @@ export default defineNuxtComponent({
|
|||
description: i18n.t("new-recipe.split-by-numbered-line-description"),
|
||||
action: splitByNumberedLine,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
utilities,
|
||||
splitText,
|
||||
trimAllLines,
|
||||
removeFirstCharacter,
|
||||
splitByNumberedLine,
|
||||
save,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
];
|
||||
</script>
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
<v-switch
|
||||
v-model="preferences.showDescription"
|
||||
hide-details
|
||||
color="primary"
|
||||
:label="$t('recipe.description')"
|
||||
/>
|
||||
</v-row>
|
||||
|
@ -51,6 +52,7 @@
|
|||
<v-switch
|
||||
v-model="preferences.showNotes"
|
||||
hide-details
|
||||
color="primary"
|
||||
:label="$t('recipe.notes')"
|
||||
/>
|
||||
</v-row>
|
||||
|
@ -63,6 +65,7 @@
|
|||
<v-switch
|
||||
v-model="preferences.showNutrition"
|
||||
hide-details
|
||||
color="primary"
|
||||
:label="$t('recipe.nutrition')"
|
||||
/>
|
||||
</v-row>
|
||||
|
@ -83,45 +86,19 @@
|
|||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
import { ImagePosition, useUserPrintPreferences } from "~/composables/use-users/preferences";
|
||||
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
|
||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipePrintView,
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recipe: {
|
||||
type: Object as () => NoUndefinedField<Recipe>,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const preferences = useUserPrintPreferences();
|
||||
|
||||
// V-Model Support
|
||||
const dialog = computed({
|
||||
get: () => {
|
||||
return props.modelValue;
|
||||
},
|
||||
set: (val) => {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
dialog,
|
||||
ImagePosition,
|
||||
preferences,
|
||||
};
|
||||
},
|
||||
interface Props {
|
||||
recipe?: NoUndefinedField<Recipe>;
|
||||
}
|
||||
withDefaults(defineProps<Props>(), {
|
||||
recipe: undefined,
|
||||
});
|
||||
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
const preferences = useUserPrintPreferences();
|
||||
</script>
|
||||
|
|
|
@ -52,10 +52,6 @@
|
|||
<div class="mr-auto">
|
||||
{{ $t("search.results") }}
|
||||
</div>
|
||||
<!-- <router-link
|
||||
:to="advancedSearchUrl"
|
||||
class="text-primary"
|
||||
> {{ $t("search.advanced-search") }} </router-link> -->
|
||||
</v-card-actions>
|
||||
|
||||
<RecipeCardMobile
|
||||
|
@ -76,7 +72,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import type { RecipeSummary } from "~/lib/api/types/recipe";
|
||||
|
@ -85,114 +81,104 @@ import { useRecipeSearch } from "~/composables/recipes/use-recipe-search";
|
|||
import { usePublicExploreApi } from "~/composables/api/api-client";
|
||||
|
||||
const SELECTED_EVENT = "selected";
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeCardMobile,
|
||||
},
|
||||
|
||||
setup(_, context) {
|
||||
const $auth = useMealieAuth();
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
selectedIndex: -1,
|
||||
});
|
||||
// Define emits
|
||||
const emit = defineEmits<{
|
||||
selected: [recipe: RecipeSummary];
|
||||
}>();
|
||||
|
||||
// ===========================================================================
|
||||
// Dialog State Management
|
||||
const dialog = ref(false);
|
||||
const $auth = useMealieAuth();
|
||||
const loading = ref(false);
|
||||
const selectedIndex = ref(-1);
|
||||
|
||||
// Reset or Grab Recipes on Change
|
||||
watch(dialog, (val) => {
|
||||
// ===========================================================================
|
||||
// Dialog State Management
|
||||
const dialog = ref(false);
|
||||
|
||||
// Reset or Grab Recipes on Change
|
||||
watch(dialog, (val) => {
|
||||
if (!val) {
|
||||
search.query.value = "";
|
||||
state.selectedIndex = -1;
|
||||
selectedIndex.value = -1;
|
||||
search.data.value = [];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// Event Handlers
|
||||
// ===========================================================================
|
||||
// Event Handlers
|
||||
|
||||
function selectRecipe() {
|
||||
function selectRecipe() {
|
||||
const recipeCards = document.getElementsByClassName("arrow-nav");
|
||||
if (recipeCards) {
|
||||
if (state.selectedIndex < 0) {
|
||||
state.selectedIndex = -1;
|
||||
if (selectedIndex.value < 0) {
|
||||
selectedIndex.value = -1;
|
||||
document.getElementById("arrow-search")?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.selectedIndex >= recipeCards.length) {
|
||||
state.selectedIndex = recipeCards.length - 1;
|
||||
if (selectedIndex.value >= recipeCards.length) {
|
||||
selectedIndex.value = recipeCards.length - 1;
|
||||
}
|
||||
|
||||
(recipeCards[state.selectedIndex] as HTMLElement).focus();
|
||||
}
|
||||
(recipeCards[selectedIndex.value] as HTMLElement).focus();
|
||||
}
|
||||
}
|
||||
|
||||
function onUpDown(e: KeyboardEvent) {
|
||||
function onUpDown(e: KeyboardEvent) {
|
||||
if (e.key === "Enter") {
|
||||
console.log(document.activeElement);
|
||||
// (document.activeElement as HTMLElement).click();
|
||||
}
|
||||
else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
state.selectedIndex--;
|
||||
selectedIndex.value--;
|
||||
}
|
||||
else if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
state.selectedIndex++;
|
||||
selectedIndex.value++;
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
selectRecipe();
|
||||
}
|
||||
}
|
||||
|
||||
watch(dialog, (val) => {
|
||||
watch(dialog, (val) => {
|
||||
if (!val) {
|
||||
document.removeEventListener("keyup", onUpDown);
|
||||
}
|
||||
else {
|
||||
document.addEventListener("keyup", onUpDown);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const route = useRoute();
|
||||
const advancedSearchUrl = computed(() => `/g/${groupSlug.value}`);
|
||||
watch(route, close);
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
watch(route, close);
|
||||
|
||||
function open() {
|
||||
function open() {
|
||||
dialog.value = true;
|
||||
}
|
||||
function close() {
|
||||
}
|
||||
function close() {
|
||||
dialog.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Basic Search
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
|
||||
const search = useRecipeSearch(api);
|
||||
// ===========================================================================
|
||||
// Basic Search
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
|
||||
const search = useRecipeSearch(api);
|
||||
|
||||
// Select Handler
|
||||
|
||||
function handleSelect(recipe: RecipeSummary) {
|
||||
// Select Handler
|
||||
function handleSelect(recipe: RecipeSummary) {
|
||||
close();
|
||||
context.emit(SELECTED_EVENT, recipe);
|
||||
}
|
||||
emit(SELECTED_EVENT, recipe);
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
advancedSearchUrl,
|
||||
dialog,
|
||||
// Expose functions to parent components
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
handleSelect,
|
||||
search,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -14,14 +14,14 @@
|
|||
max-width="290px"
|
||||
min-width="auto"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-text-field
|
||||
v-model="expirationDateString"
|
||||
:label="$t('recipe-share.expiration-date')"
|
||||
:hint="$t('recipe-share.default-30-days')"
|
||||
persistent-hint
|
||||
:prepend-icon="$globals.icons.calendar"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
|
@ -92,113 +92,92 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useClipboard, useShare, whenever } from "@vueuse/core";
|
||||
import type { RecipeShareToken } from "~/lib/api/types/recipe";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useHouseholdSelf } from "~/composables/use-households";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
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);
|
||||
},
|
||||
});
|
||||
interface Props {
|
||||
recipeId: string;
|
||||
name: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const state = reactive({
|
||||
datePickerMenu: false,
|
||||
expirationDate: new Date(Date.now() - new Date().getTimezoneOffset() * 60000),
|
||||
tokens: [] as RecipeShareToken[],
|
||||
});
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
|
||||
const expirationDateString = computed(() => {
|
||||
return state.expirationDate.toISOString().substring(0, 10);
|
||||
});
|
||||
const datePickerMenu = ref(false);
|
||||
const expirationDate = ref(new Date(Date.now() - new Date().getTimezoneOffset() * 60000));
|
||||
const tokens = ref<RecipeShareToken[]>([]);
|
||||
|
||||
whenever(
|
||||
() => props.modelValue,
|
||||
const expirationDateString = computed(() => {
|
||||
return expirationDate.value.toISOString().substring(0, 10);
|
||||
});
|
||||
|
||||
whenever(
|
||||
() => dialog.value,
|
||||
() => {
|
||||
// Set expiration date to today + 30 Days
|
||||
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();
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const { household } = useHouseholdSelf();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const { household } = useHouseholdSelf();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
const firstDayOfWeek = computed(() => {
|
||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Token Actions
|
||||
// ============================================================
|
||||
// Token Actions
|
||||
|
||||
const userApi = useUserApi();
|
||||
const userApi = useUserApi();
|
||||
|
||||
async function createNewToken() {
|
||||
async function createNewToken() {
|
||||
// Convert expiration date to timestamp
|
||||
const { data } = await userApi.recipes.share.createOne({
|
||||
recipeId: props.recipeId,
|
||||
expiresAt: state.expirationDate.toISOString(),
|
||||
expiresAt: expirationDate.value.toISOString(),
|
||||
});
|
||||
|
||||
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);
|
||||
state.tokens = state.tokens.filter(token => token.id !== id);
|
||||
}
|
||||
tokens.value = tokens.value.filter(token => token.id !== id);
|
||||
}
|
||||
|
||||
async function refreshTokens() {
|
||||
async function refreshTokens() {
|
||||
const { data } = await userApi.recipes.share.getAll(1, -1, { recipe_id: props.recipeId });
|
||||
|
||||
if (data) {
|
||||
// @ts-expect-error - TODO: This routes doesn't have pagination, but the type are mismatched.
|
||||
state.tokens = data ?? [];
|
||||
}
|
||||
tokens.value = data ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
const { share, isSupported: shareIsSupported } = useShare();
|
||||
const { copy, copied, isSupported } = useClipboard();
|
||||
const { share, isSupported: shareIsSupported } = useShare();
|
||||
const { copy, copied, isSupported } = useClipboard();
|
||||
|
||||
function getRecipeText() {
|
||||
function getRecipeText() {
|
||||
return i18n.t("recipe.share-recipe-message", [props.name]);
|
||||
}
|
||||
}
|
||||
|
||||
function getTokenLink(token: string) {
|
||||
function getTokenLink(token: string) {
|
||||
return `${window.location.origin}/g/${groupSlug.value}/shared/r/${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyTokenLink(token: string) {
|
||||
async function copyTokenLink(token: string) {
|
||||
if (isSupported.value) {
|
||||
await copy(getTokenLink(token));
|
||||
if (copied.value) {
|
||||
|
@ -211,9 +190,9 @@ export default defineNuxtComponent({
|
|||
else {
|
||||
alert.error(i18n.t("general.clipboard-not-supported") as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function shareRecipe(token: string) {
|
||||
async function shareRecipe(token: string) {
|
||||
if (shareIsSupported) {
|
||||
share({
|
||||
title: props.name,
|
||||
|
@ -224,18 +203,5 @@ export default defineNuxtComponent({
|
|||
else {
|
||||
await copyTokenLink(token);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
expirationDateString,
|
||||
dialog,
|
||||
createNewToken,
|
||||
deleteToken,
|
||||
firstDayOfWeek,
|
||||
shareRecipe,
|
||||
copyTokenLink,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
nudge-right="50"
|
||||
:color="buttonStyle ? 'info' : 'secondary'"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
v-if="isFavorite || showAlways"
|
||||
icon
|
||||
|
@ -13,7 +13,7 @@
|
|||
size="small"
|
||||
:color="buttonStyle ? 'info' : 'secondary'"
|
||||
:fab="buttonStyle"
|
||||
v-bind="{ ...props, ...$attrs }"
|
||||
v-bind="{ ...tooltipProps, ...$attrs }"
|
||||
@click.prevent="toggleFavorite"
|
||||
>
|
||||
<v-icon
|
||||
|
@ -28,36 +28,31 @@
|
|||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useUserSelfRatings } from "~/composables/use-users";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
recipeId: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
showAlways: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
buttonStyle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const api = useUserApi();
|
||||
const $auth = useMealieAuth();
|
||||
const { userRatings, refreshUserRatings } = useUserSelfRatings();
|
||||
interface Props {
|
||||
recipeId?: string;
|
||||
showAlways?: boolean;
|
||||
buttonStyle?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
recipeId: "",
|
||||
showAlways: false,
|
||||
buttonStyle: false,
|
||||
});
|
||||
|
||||
const isFavorite = computed(() => {
|
||||
const api = useUserApi();
|
||||
const $auth = useMealieAuth();
|
||||
const { userRatings, refreshUserRatings } = useUserSelfRatings();
|
||||
|
||||
const isFavorite = computed(() => {
|
||||
const rating = userRatings.value.find(r => r.recipeId === props.recipeId);
|
||||
return rating?.isFavorite || false;
|
||||
});
|
||||
});
|
||||
|
||||
async function toggleFavorite() {
|
||||
async function toggleFavorite() {
|
||||
if (!$auth.user.value) return;
|
||||
if (!isFavorite.value) {
|
||||
await api.users.addFavorite($auth.user.value?.id, props.recipeId);
|
||||
|
@ -66,9 +61,5 @@ export default defineNuxtComponent({
|
|||
await api.users.removeFavorite($auth.user.value?.id, props.recipeId);
|
||||
}
|
||||
await refreshUserRatings();
|
||||
}
|
||||
|
||||
return { isFavorite, toggleFavorite };
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
nudge-top="6"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
color="accent"
|
||||
dark
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
>
|
||||
<v-icon start>
|
||||
{{ $globals.icons.fileImage }}
|
||||
|
@ -61,52 +61,42 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
const REFRESH_EVENT = "refresh";
|
||||
const UPLOAD_EVENT = "upload";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const state = reactive({
|
||||
url: "",
|
||||
loading: false,
|
||||
menu: false,
|
||||
});
|
||||
const props = defineProps<{ slug: string }>();
|
||||
|
||||
function uploadImage(fileObject: File) {
|
||||
context.emit(UPLOAD_EVENT, fileObject);
|
||||
state.menu = false;
|
||||
const emit = defineEmits<{
|
||||
refresh: [];
|
||||
upload: [fileObject: File];
|
||||
}>();
|
||||
|
||||
const url = ref("");
|
||||
const loading = ref(false);
|
||||
const menu = ref(false);
|
||||
|
||||
function uploadImage(fileObject: File) {
|
||||
emit(UPLOAD_EVENT, fileObject);
|
||||
menu.value = false;
|
||||
}
|
||||
|
||||
const api = useUserApi();
|
||||
async function getImageFromURL() {
|
||||
loading.value = true;
|
||||
if (await api.recipes.updateImagebyURL(props.slug, url.value)) {
|
||||
emit(REFRESH_EVENT);
|
||||
}
|
||||
loading.value = false;
|
||||
menu.value = false;
|
||||
}
|
||||
|
||||
const api = useUserApi();
|
||||
async function getImageFromURL() {
|
||||
state.loading = true;
|
||||
if (await api.recipes.updateImagebyURL(props.slug, state.url)) {
|
||||
context.emit(REFRESH_EVENT);
|
||||
}
|
||||
state.loading = false;
|
||||
state.menu = false;
|
||||
}
|
||||
|
||||
const i18n = useI18n();
|
||||
const messages = props.slug ? [""] : [i18n.t("recipe.save-recipe-before-use")];
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
uploadImage,
|
||||
getImageFromURL,
|
||||
messages,
|
||||
};
|
||||
},
|
||||
});
|
||||
const i18n = useI18n();
|
||||
const messages = computed(() =>
|
||||
props.slug ? [""] : [i18n.t("recipe.save-recipe-before-use")],
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
class="d-flex flex-wrap my-1"
|
||||
>
|
||||
<v-col
|
||||
v-if="!disableAmount"
|
||||
sm="12"
|
||||
md="2"
|
||||
cols="12"
|
||||
|
@ -42,7 +41,6 @@
|
|||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="!disableAmount"
|
||||
sm="12"
|
||||
md="3"
|
||||
cols="12"
|
||||
|
@ -63,6 +61,22 @@
|
|||
clearable
|
||||
@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>
|
||||
<div class="caption text-center pb-2">
|
||||
{{ $t("recipe.press-enter-to-create") }}
|
||||
|
@ -82,7 +96,6 @@
|
|||
|
||||
<!-- Foods Input -->
|
||||
<v-col
|
||||
v-if="!disableAmount"
|
||||
m="12"
|
||||
md="3"
|
||||
cols="12"
|
||||
|
@ -104,6 +117,22 @@
|
|||
clearable
|
||||
@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>
|
||||
<div class="caption text-center pb-2">
|
||||
{{ $t("recipe.press-enter-to-create") }}
|
||||
|
@ -134,16 +163,7 @@
|
|||
:placeholder="$t('recipe.notes')"
|
||||
class="mb-auto"
|
||||
@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
|
||||
hover
|
||||
:large="false"
|
||||
|
@ -153,7 +173,6 @@
|
|||
@toggle-original="toggleOriginalText"
|
||||
@insert-above="$emit('insert-above')"
|
||||
@insert-below="$emit('insert-below')"
|
||||
@insert-ingredient="$emit('insert-ingredient')"
|
||||
@delete="$emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
|
@ -184,22 +203,29 @@ import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
|||
// defineModel replaces modelValue prop
|
||||
const model = defineModel<RecipeIngredient>({ required: true });
|
||||
|
||||
const props = defineProps({
|
||||
disableAmount: {
|
||||
defineProps({
|
||||
unitError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allowInsertIngredient: {
|
||||
unitErrorTooltip: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
foodError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
foodErrorTooltip: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits([
|
||||
"clickIngredientField",
|
||||
"insert-above",
|
||||
"insert-below",
|
||||
"insert-ingredient",
|
||||
"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) {
|
||||
options.push({
|
||||
text: i18n.t("recipe.see-original-text"),
|
||||
|
|
|
@ -3,21 +3,13 @@
|
|||
<div v-html="safeMarkup" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { sanitizeIngredientHTML } from "~/composables/recipes/use-recipe-ingredients";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
markup: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const safeMarkup = computed(() => sanitizeIngredientHTML(props.markup));
|
||||
return {
|
||||
safeMarkup,
|
||||
};
|
||||
},
|
||||
});
|
||||
interface Props {
|
||||
markup: string;
|
||||
}
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const safeMarkup = computed(() => sanitizeIngredientHTML(props.markup));
|
||||
</script>
|
||||
|
|
|
@ -28,34 +28,20 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { RecipeIngredient } from "~/lib/api/types/household";
|
||||
import { useParsedIngredientText } from "~/composables/recipes";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
ingredient: {
|
||||
type: Object as () => RecipeIngredient,
|
||||
required: true,
|
||||
},
|
||||
disableAmount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const parsedIng = computed(() => {
|
||||
return useParsedIngredientText(props.ingredient, props.disableAmount, props.scale);
|
||||
});
|
||||
interface Props {
|
||||
ingredient: RecipeIngredient;
|
||||
scale?: number;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
scale: 1,
|
||||
});
|
||||
|
||||
return {
|
||||
parsedIng,
|
||||
};
|
||||
},
|
||||
const parsedIng = computed(() => {
|
||||
return useParsedIngredientText(props.ingredient, props.scale);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -43,7 +43,6 @@
|
|||
<v-list-item-title>
|
||||
<RecipeIngredientListItem
|
||||
:ingredient="ingredient"
|
||||
:disable-amount="disableAmount"
|
||||
:scale="scale"
|
||||
/>
|
||||
</v-list-item-title>
|
||||
|
@ -53,42 +52,30 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
|
||||
import { parseIngredientText } from "~/composables/recipes";
|
||||
import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { RecipeIngredientListItem },
|
||||
props: {
|
||||
value: {
|
||||
type: Array as () => RecipeIngredient[],
|
||||
default: () => [],
|
||||
},
|
||||
disableAmount: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
isCookMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
function validateTitle(title?: string) {
|
||||
interface Props {
|
||||
value?: RecipeIngredient[];
|
||||
scale?: number;
|
||||
isCookMode?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
value: () => [],
|
||||
scale: 1,
|
||||
isCookMode: false,
|
||||
});
|
||||
|
||||
function validateTitle(title?: string | null) {
|
||||
return !(title === undefined || title === "" || title === null);
|
||||
}
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
checked: props.value.map(() => false),
|
||||
showTitleEditor: computed(() => props.value.map(x => validateTitle(x.title))),
|
||||
});
|
||||
const checked = ref(props.value.map(() => false));
|
||||
const showTitleEditor = computed(() => props.value.map(x => validateTitle(x.title)));
|
||||
|
||||
const ingredientCopyText = computed(() => {
|
||||
const ingredientCopyText = computed(() => {
|
||||
const components: string[] = [];
|
||||
props.value.forEach((ingredient) => {
|
||||
if (ingredient.title) {
|
||||
|
@ -99,25 +86,17 @@ export default defineNuxtComponent({
|
|||
components.push(`[${ingredient.title}]`);
|
||||
}
|
||||
|
||||
components.push(parseIngredientText(ingredient, props.disableAmount, props.scale, false));
|
||||
components.push(parseIngredientText(ingredient, props.scale, false));
|
||||
});
|
||||
|
||||
return components.join("\n");
|
||||
});
|
||||
});
|
||||
|
||||
function toggleChecked(index: number) {
|
||||
function toggleChecked(index: number) {
|
||||
// TODO Find a better way to do this - $set is not available, and
|
||||
// direct array modifications are not propagated for some reason
|
||||
state.checked.splice(index, 1, !state.checked[index]);
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
ingredientCopyText,
|
||||
toggleChecked,
|
||||
};
|
||||
},
|
||||
});
|
||||
checked.value.splice(index, 1, !checked.value[index]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<div>
|
||||
<BaseDialog
|
||||
v-model="madeThisDialog"
|
||||
:loading="madeThisFormLoading"
|
||||
:icon="$globals.icons.chefHat"
|
||||
:title="$t('recipe.made-this')"
|
||||
:submit-text="$t('recipe.add-to-timeline')"
|
||||
|
@ -29,11 +30,11 @@
|
|||
offset-y
|
||||
max-width="290px"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-text-field
|
||||
v-model="newTimelineEventTimestampString"
|
||||
:prepend-icon="$globals.icons.calendar"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
|
@ -85,13 +86,13 @@
|
|||
<div>
|
||||
<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-tooltip bottom>
|
||||
<template #activator="{ props }">
|
||||
<v-tooltip location="bottom">
|
||||
<template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
rounded
|
||||
variant="outlined"
|
||||
size="x-large"
|
||||
v-bind="props"
|
||||
v-bind="tooltipProps"
|
||||
style="border-color: rgb(var(--v-theme-primary));"
|
||||
@click="madeThisDialog = true"
|
||||
>
|
||||
|
@ -116,46 +117,43 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { whenever } from "@vueuse/core";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
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";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object as () => Recipe,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["eventCreated"],
|
||||
setup(props, context) {
|
||||
const madeThisDialog = ref(false);
|
||||
const userApi = useUserApi();
|
||||
const { household } = useHouseholdSelf();
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const domMadeThisForm = ref<VForm>();
|
||||
const newTimelineEvent = ref<RecipeTimelineEventIn>({
|
||||
const props = defineProps<{ recipe: Recipe }>();
|
||||
const emit = defineEmits<{
|
||||
eventCreated: [event: RecipeTimelineEventOut];
|
||||
}>();
|
||||
|
||||
const madeThisDialog = ref(false);
|
||||
const userApi = useUserApi();
|
||||
const { household } = useHouseholdSelf();
|
||||
const i18n = useI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const domMadeThisForm = ref<VForm>();
|
||||
const newTimelineEvent = ref<RecipeTimelineEventIn>({
|
||||
subject: "",
|
||||
eventType: "comment",
|
||||
eventMessage: "",
|
||||
timestamp: undefined,
|
||||
recipeId: props.recipe?.id || "",
|
||||
});
|
||||
const newTimelineEventImage = ref<Blob | File>();
|
||||
const newTimelineEventImageName = ref<string>("");
|
||||
const newTimelineEventImagePreviewUrl = ref<string>();
|
||||
const newTimelineEventTimestamp = ref<Date>(new Date());
|
||||
const newTimelineEventTimestampString = computed(() => {
|
||||
});
|
||||
const newTimelineEventImage = ref<Blob | File>();
|
||||
const newTimelineEventImageName = ref<string>("");
|
||||
const newTimelineEventImagePreviewUrl = ref<string>();
|
||||
const newTimelineEventTimestamp = ref<Date>(new Date());
|
||||
const newTimelineEventTimestampString = computed(() => {
|
||||
return newTimelineEventTimestamp.value.toISOString().substring(0, 10);
|
||||
});
|
||||
});
|
||||
|
||||
const lastMade = ref(props.recipe.lastMade);
|
||||
const lastMadeReady = ref(false);
|
||||
onMounted(async () => {
|
||||
const lastMade = ref(props.recipe.lastMade);
|
||||
const lastMadeReady = ref(false);
|
||||
onMounted(async () => {
|
||||
if (!$auth.user?.value?.householdSlug) {
|
||||
lastMade.value = props.recipe.lastMade;
|
||||
}
|
||||
|
@ -165,43 +163,57 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
lastMadeReady.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
whenever(
|
||||
whenever(
|
||||
() => madeThisDialog.value,
|
||||
() => {
|
||||
// Set timestamp to now
|
||||
newTimelineEventTimestamp.value = new Date(Date.now() - new Date().getTimezoneOffset() * 60000);
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
const firstDayOfWeek = computed(() => {
|
||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||
});
|
||||
});
|
||||
|
||||
function clearImage() {
|
||||
function clearImage() {
|
||||
newTimelineEventImage.value = undefined;
|
||||
newTimelineEventImageName.value = "";
|
||||
newTimelineEventImagePreviewUrl.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function uploadImage(fileObject: File) {
|
||||
function uploadImage(fileObject: File) {
|
||||
newTimelineEventImage.value = fileObject;
|
||||
newTimelineEventImageName.value = fileObject.name;
|
||||
newTimelineEventImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUploadedImage(fileObject: Blob) {
|
||||
function updateUploadedImage(fileObject: Blob) {
|
||||
newTimelineEventImage.value = fileObject;
|
||||
newTimelineEventImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
||||
}
|
||||
}
|
||||
|
||||
const state = reactive({ datePickerMenu: false });
|
||||
async function createTimelineEvent() {
|
||||
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() {
|
||||
if (!(newTimelineEventTimestampString.value && props.recipe?.id && props.recipe?.slug)) {
|
||||
return;
|
||||
}
|
||||
|
||||
madeThisFormLoading.value = true;
|
||||
|
||||
newTimelineEvent.value.recipeId = props.recipe.id;
|
||||
// Note: $auth.user is now a ref
|
||||
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
|
||||
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 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
|
||||
if (!lastMade.value || newTimelineEvent.value.timestamp > lastMade.value) {
|
||||
try {
|
||||
lastMade.value = 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
|
||||
if (newTimelineEventImage.value && newEvent) {
|
||||
let imageError = false;
|
||||
if (newTimelineEventImage.value) {
|
||||
try {
|
||||
const imageResponse = await userApi.recipes.updateTimelineEventImage(
|
||||
newEvent.id,
|
||||
newTimelineEventImage.value,
|
||||
|
@ -230,34 +262,20 @@ export default defineNuxtComponent({
|
|||
newEvent.image = imageResponse.data.image;
|
||||
}
|
||||
}
|
||||
|
||||
// reset form
|
||||
newTimelineEvent.value.eventMessage = "";
|
||||
newTimelineEvent.value.timestamp = undefined;
|
||||
clearImage();
|
||||
madeThisDialog.value = false;
|
||||
domMadeThisForm.value?.reset();
|
||||
|
||||
context.emit("eventCreated", newEvent);
|
||||
catch (error) {
|
||||
imageError = true;
|
||||
console.error("Failed to upload image for timeline event:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
domMadeThisForm,
|
||||
madeThisDialog,
|
||||
firstDayOfWeek,
|
||||
newTimelineEvent,
|
||||
newTimelineEventImage,
|
||||
newTimelineEventImagePreviewUrl,
|
||||
newTimelineEventTimestamp,
|
||||
newTimelineEventTimestampString,
|
||||
lastMade,
|
||||
lastMadeReady,
|
||||
createTimelineEvent,
|
||||
clearImage,
|
||||
uploadImage,
|
||||
updateUploadedImage,
|
||||
};
|
||||
},
|
||||
});
|
||||
if (imageError) {
|
||||
alert.error(i18n.t("recipe.added-to-timeline-but-failed-to-add-image"));
|
||||
}
|
||||
else {
|
||||
alert.success(i18n.t("recipe.added-to-timeline"));
|
||||
}
|
||||
|
||||
resetMadeThisForm();
|
||||
emit("eventCreated", newEvent);
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -51,46 +51,34 @@
|
|||
</v-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from "dompurify";
|
||||
import { useFraction } from "~/composables/recipes/use-fraction";
|
||||
import type { ShoppingListItemOut } from "~/lib/api/types/household";
|
||||
import type { RecipeSummary } from "~/lib/api/types/recipe";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
recipes: {
|
||||
type: Array as () => RecipeSummary[],
|
||||
required: true,
|
||||
},
|
||||
listItem: {
|
||||
type: Object as () => ShoppingListItemOut | undefined,
|
||||
default: undefined,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tile: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showDescription: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const $auth = useMealieAuth();
|
||||
const { frac } = useFraction();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user?.value?.groupSlug || "");
|
||||
interface Props {
|
||||
recipes: RecipeSummary[];
|
||||
listItem?: ShoppingListItemOut;
|
||||
small?: boolean;
|
||||
tile?: boolean;
|
||||
showDescription?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
listItem: undefined,
|
||||
small: false,
|
||||
tile: false,
|
||||
showDescription: false,
|
||||
disabled: false,
|
||||
});
|
||||
|
||||
const attrs = computed(() => {
|
||||
const $auth = useMealieAuth();
|
||||
const { frac } = useFraction();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug || $auth.user?.value?.groupSlug || "");
|
||||
|
||||
const attrs = computed(() => {
|
||||
return props.small
|
||||
? {
|
||||
class: {
|
||||
|
@ -122,16 +110,16 @@ export default defineNuxtComponent({
|
|||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
function sanitizeHTML(rawHtml: string) {
|
||||
function sanitizeHTML(rawHtml: string) {
|
||||
return DOMPurify.sanitize(rawHtml, {
|
||||
USE_PROFILES: { html: true },
|
||||
ALLOWED_TAGS: ["strong", "sup"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const listItemDescriptions = computed<string[]>(() => {
|
||||
const listItemDescriptions = computed<string[]>(() => {
|
||||
if (
|
||||
props.recipes.length === 1 // we don't need to specify details if there's only one recipe ref
|
||||
|| !props.listItem?.recipeReferences
|
||||
|
@ -179,13 +167,5 @@ export default defineNuxtComponent({
|
|||
}
|
||||
|
||||
return listItemDescriptions;
|
||||
});
|
||||
|
||||
return {
|
||||
attrs,
|
||||
groupSlug,
|
||||
listItemDescriptions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
:suffix="labels[key].suffix"
|
||||
type="number"
|
||||
autocomplete="off"
|
||||
variant="underlined"
|
||||
@update:model-value="updateValue(key, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -31,77 +32,61 @@
|
|||
:key="index"
|
||||
style="min-height: 25px"
|
||||
>
|
||||
<div>
|
||||
<v-list-item-title class="pl-4 caption flex row">
|
||||
<v-list-item-title class="pl-2 d-flex">
|
||||
<div>{{ item.label }}</div>
|
||||
<div class="ml-auto mr-1">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
<div>{{ item.suffix }}</div>
|
||||
</v-list-item-title>
|
||||
</div>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useNutritionLabels } from "~/composables/recipes";
|
||||
import type { Nutrition } from "~/lib/api/types/recipe";
|
||||
import type { NutritionLabelType } from "~/composables/recipes/use-recipe-nutrition";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as () => Nutrition,
|
||||
required: true,
|
||||
},
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const { labels } = useNutritionLabels();
|
||||
const valueNotNull = computed(() => {
|
||||
interface Props {
|
||||
edit?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
edit: true,
|
||||
});
|
||||
|
||||
const modelValue = defineModel<Nutrition>({ required: true });
|
||||
|
||||
const { labels } = useNutritionLabels();
|
||||
const valueNotNull = computed(() => {
|
||||
let key: keyof Nutrition;
|
||||
for (key in props.modelValue) {
|
||||
if (props.modelValue[key] !== null) {
|
||||
for (key in modelValue.value) {
|
||||
if (modelValue.value[key] !== null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
const showViewer = computed(() => !props.edit && valueNotNull.value);
|
||||
const showViewer = computed(() => !props.edit && valueNotNull.value);
|
||||
|
||||
function updateValue(key: number | string, event: Event) {
|
||||
context.emit("update:modelValue", { ...props.modelValue, [key]: event });
|
||||
}
|
||||
function updateValue(key: number | string, event: Event) {
|
||||
modelValue.value = { ...modelValue.value, [key]: event };
|
||||
}
|
||||
|
||||
// Build a new list that only contains nutritional information that has a value
|
||||
const renderedList = computed(() => {
|
||||
// Build a new list that only contains nutritional information that has a value
|
||||
const renderedList = computed(() => {
|
||||
return Object.entries(labels).reduce((item: NutritionLabelType, [key, label]) => {
|
||||
if (props.modelValue[key]?.trim()) {
|
||||
if (modelValue.value[key]?.trim()) {
|
||||
item[key] = {
|
||||
...label,
|
||||
value: props.modelValue[key],
|
||||
value: modelValue.value[key],
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}, {});
|
||||
});
|
||||
|
||||
return {
|
||||
labels,
|
||||
valueNotNull,
|
||||
showViewer,
|
||||
updateValue,
|
||||
renderedList,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -60,60 +60,45 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useCategoryStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
import { type RecipeOrganizer, Organizer } from "~/lib/api/types/non-generated";
|
||||
|
||||
const CREATED_ITEM_EVENT = "created-item";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
tagDialog: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
itemType: {
|
||||
type: String as () => RecipeOrganizer,
|
||||
default: "category",
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
setup(props, context) {
|
||||
const i18n = useI18n();
|
||||
interface Props {
|
||||
color?: string | null;
|
||||
tagDialog?: boolean;
|
||||
itemType?: RecipeOrganizer;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
color: null,
|
||||
tagDialog: true,
|
||||
itemType: "category" as RecipeOrganizer,
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
name: "",
|
||||
onHand: false,
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
"created-item": [item: any];
|
||||
}>();
|
||||
|
||||
const dialog = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(value) {
|
||||
context.emit("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
const dialog = defineModel<boolean>({ default: false });
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
const i18n = useI18n();
|
||||
|
||||
const name = ref("");
|
||||
const onHand = ref(false);
|
||||
|
||||
watch(
|
||||
dialog,
|
||||
(val: boolean) => {
|
||||
if (!val) state.name = "";
|
||||
if (!val) name.value = "";
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
const userApi = useUserApi();
|
||||
const userApi = useUserApi();
|
||||
|
||||
const store = (() => {
|
||||
const store = (() => {
|
||||
switch (props.itemType) {
|
||||
case Organizer.Tag:
|
||||
return useTagStore();
|
||||
|
@ -122,9 +107,9 @@ export default defineNuxtComponent({
|
|||
default:
|
||||
return useCategoryStore();
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
||||
const properties = computed(() => {
|
||||
const properties = computed(() => {
|
||||
switch (props.itemType) {
|
||||
case Organizer.Tag:
|
||||
return {
|
||||
|
@ -145,34 +130,23 @@ export default defineNuxtComponent({
|
|||
api: userApi.categories,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const rules = {
|
||||
const rules = {
|
||||
required: (val: string) => !!val || (i18n.t("general.a-name-is-required") as string),
|
||||
};
|
||||
};
|
||||
|
||||
async function select() {
|
||||
async function select() {
|
||||
if (store) {
|
||||
// @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;
|
||||
}
|
||||
|
||||
return {
|
||||
Organizer,
|
||||
...toRefs(state),
|
||||
dialog,
|
||||
properties,
|
||||
rules,
|
||||
select,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
|
|
@ -122,9 +122,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
import { useContextPresets } from "~/composables/use-context-presents";
|
||||
import RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue";
|
||||
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
||||
|
@ -138,27 +137,18 @@ interface GenericItem {
|
|||
onHand: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeOrganizerDialog,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array as () => GenericItem[],
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
itemType: {
|
||||
type: String as () => RecipeOrganizer,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["update", "delete"],
|
||||
setup(props, { emit }) {
|
||||
const state = reactive({
|
||||
const props = defineProps<{
|
||||
items: GenericItem[];
|
||||
icon: string;
|
||||
itemType: RecipeOrganizer;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: [item: GenericItem];
|
||||
delete: [id: string];
|
||||
}>();
|
||||
|
||||
const state = reactive({
|
||||
// Search Options
|
||||
options: {
|
||||
ignoreLocation: true,
|
||||
|
@ -171,24 +161,24 @@ export default defineNuxtComponent({
|
|||
minMatchCharLength: 1,
|
||||
keys: ["name"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const $auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user?.value?.groupSlug || "");
|
||||
const $auth = useMealieAuth();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user?.value?.groupSlug || "");
|
||||
|
||||
// =================================================================
|
||||
// Context Menu
|
||||
// =================================================================
|
||||
// Context Menu
|
||||
|
||||
const dialogs = ref({
|
||||
const dialogs = ref({
|
||||
organizer: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
});
|
||||
});
|
||||
|
||||
const presets = useContextPresets();
|
||||
const presets = useContextPresets();
|
||||
|
||||
const translationKey = computed<string>(() => {
|
||||
const translationKey = computed<string>(() => {
|
||||
const typeMap = {
|
||||
categories: "category.category",
|
||||
tags: "tag.tag",
|
||||
|
@ -197,58 +187,58 @@ export default defineNuxtComponent({
|
|||
households: "household.household",
|
||||
};
|
||||
return typeMap[props.itemType] || "";
|
||||
});
|
||||
});
|
||||
|
||||
const deleteTarget = ref<GenericItem | null>(null);
|
||||
const updateTarget = ref<GenericItem | null>(null);
|
||||
const deleteTarget = ref<GenericItem | null>(null);
|
||||
const updateTarget = ref<GenericItem | null>(null);
|
||||
|
||||
function confirmDelete(item: GenericItem) {
|
||||
function confirmDelete(item: GenericItem) {
|
||||
deleteTarget.value = item;
|
||||
dialogs.value.delete = true;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteOne() {
|
||||
function deleteOne() {
|
||||
if (!deleteTarget.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit("delete", deleteTarget.value.id);
|
||||
}
|
||||
}
|
||||
|
||||
function openUpdateDialog(item: GenericItem) {
|
||||
function openUpdateDialog(item: GenericItem) {
|
||||
updateTarget.value = deepCopy(item);
|
||||
dialogs.value.update = true;
|
||||
}
|
||||
}
|
||||
|
||||
function updateOne() {
|
||||
function updateOne() {
|
||||
if (!updateTarget.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit("update", updateTarget.value);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Search Functions
|
||||
// ================================================================
|
||||
// Search Functions
|
||||
|
||||
const searchString = useRouteQuery("q", "");
|
||||
const searchString = useRouteQuery("q", "");
|
||||
|
||||
const fuse = computed(() => {
|
||||
const fuse = computed(() => {
|
||||
return new Fuse(props.items, state.options);
|
||||
});
|
||||
});
|
||||
|
||||
const fuzzyItems = computed<GenericItem[]>(() => {
|
||||
const fuzzyItems = computed<GenericItem[]>(() => {
|
||||
if (searchString.value.trim() === "") {
|
||||
return props.items;
|
||||
}
|
||||
const result = fuse.value.search(searchString.value.trim() as string);
|
||||
return result.map(x => x.item);
|
||||
});
|
||||
});
|
||||
|
||||
// =================================================================
|
||||
// Sorted Items
|
||||
// =================================================================
|
||||
// Sorted Items
|
||||
|
||||
const itemsSorted = computed(() => {
|
||||
const itemsSorted = computed(() => {
|
||||
const byLetter: { [key: string]: Array<GenericItem> } = {};
|
||||
|
||||
if (!fuzzyItems.value) {
|
||||
|
@ -266,28 +256,9 @@ export default defineNuxtComponent({
|
|||
});
|
||||
|
||||
return byLetter;
|
||||
});
|
||||
|
||||
function isTitle(str: number | string) {
|
||||
return typeof str === "string" && str.length === 1;
|
||||
}
|
||||
|
||||
return {
|
||||
groupSlug,
|
||||
isTitle,
|
||||
dialogs,
|
||||
confirmDelete,
|
||||
openUpdateDialog,
|
||||
updateOne,
|
||||
updateTarget,
|
||||
deleteOne,
|
||||
deleteTarget,
|
||||
Organizer,
|
||||
presets,
|
||||
itemsSorted,
|
||||
searchString,
|
||||
translationKey,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function isTitle(str: number | string) {
|
||||
return typeof str === "string" && str.length === 1;
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
v-model="selected"
|
||||
v-bind="inputAttrs"
|
||||
v-model:search="searchInput"
|
||||
:items="storeItem"
|
||||
:items="items"
|
||||
:label="label"
|
||||
chips
|
||||
closable-chips
|
||||
|
@ -46,78 +46,51 @@
|
|||
</v-autocomplete>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { IngredientFood, RecipeCategory, RecipeTag } from "~/lib/api/types/recipe";
|
||||
import type { RecipeTool } from "~/lib/api/types/admin";
|
||||
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as () => (
|
||||
interface Props {
|
||||
selectorType: RecipeOrganizer;
|
||||
inputAttrs?: Record<string, any>;
|
||||
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
|
||||
| RecipeTag
|
||||
| RecipeCategory
|
||||
| RecipeTool
|
||||
| IngredientFood
|
||||
| string
|
||||
)[] | undefined,
|
||||
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"],
|
||||
)[] | undefined>({ required: true });
|
||||
|
||||
setup(props, context) {
|
||||
const selected = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(() => {
|
||||
if (selected.value === undefined) {
|
||||
selected.value = [];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const label = computed(() => {
|
||||
const label = computed(() => {
|
||||
if (!props.showLabel) {
|
||||
return "";
|
||||
}
|
||||
|
@ -136,9 +109,9 @@ export default defineNuxtComponent({
|
|||
default:
|
||||
return i18n.t("general.organizer");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const icon = computed(() => {
|
||||
const icon = computed(() => {
|
||||
if (!props.showIcon) {
|
||||
return "";
|
||||
}
|
||||
|
@ -157,69 +130,54 @@ export default defineNuxtComponent({
|
|||
default:
|
||||
return $globals.icons.tags;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// Store & Items Setup
|
||||
// ===========================================================================
|
||||
// Store & Items Setup
|
||||
|
||||
const storeMap = {
|
||||
const storeMap = {
|
||||
[Organizer.Category]: useCategoryStore(),
|
||||
[Organizer.Tag]: useTagStore(),
|
||||
[Organizer.Tool]: useToolStore(),
|
||||
[Organizer.Food]: useFoodStore(),
|
||||
[Organizer.Household]: useHouseholdStore(),
|
||||
};
|
||||
};
|
||||
|
||||
const store = computed(() => {
|
||||
const store = computed(() => {
|
||||
const { store } = storeMap[props.selectorType];
|
||||
return store.value;
|
||||
});
|
||||
});
|
||||
|
||||
const items = computed(() => {
|
||||
const items = computed(() => {
|
||||
if (!props.returnObject) {
|
||||
return store.value.map(item => item.name);
|
||||
}
|
||||
return store.value;
|
||||
});
|
||||
});
|
||||
|
||||
function removeByIndex(index: number) {
|
||||
function removeByIndex(index: number) {
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
||||
selected.value = [...newSelected];
|
||||
}
|
||||
}
|
||||
|
||||
function appendCreated(item: any) {
|
||||
function appendCreated(item: any) {
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
selected.value = [...selected.value, item];
|
||||
}
|
||||
}
|
||||
|
||||
const dialog = ref(false);
|
||||
const dialog = ref(false);
|
||||
|
||||
const searchInput = ref("");
|
||||
const searchInput = ref("");
|
||||
|
||||
function resetSearchInput() {
|
||||
function resetSearchInput() {
|
||||
searchInput.value = "";
|
||||
}
|
||||
|
||||
return {
|
||||
Organizer,
|
||||
appendCreated,
|
||||
dialog,
|
||||
storeItem: items,
|
||||
label,
|
||||
icon,
|
||||
selected,
|
||||
removeByIndex,
|
||||
searchInput,
|
||||
resetSearchInput,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<RecipePageIngredientEditor v-if="isEditForm" v-model="recipe" />
|
||||
</div>
|
||||
<div>
|
||||
<RecipePageScale v-model:scale="scale" :recipe="recipe" />
|
||||
<RecipePageScale v-model="scale" :recipe="recipe" />
|
||||
</div>
|
||||
|
||||
<!--
|
||||
|
@ -81,7 +81,7 @@
|
|||
</v-card>
|
||||
<WakelockSwitch />
|
||||
<RecipePageComments
|
||||
v-if="!recipe.settings.disableComments && !isEditForm && !isCookMode"
|
||||
v-if="!recipe.settings?.disableComments && !isEditForm && !isCookMode"
|
||||
v-model="recipe"
|
||||
class="px-1 my-4 d-print-none"
|
||||
/>
|
||||
|
@ -96,7 +96,7 @@
|
|||
<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%">
|
||||
<div class="d-flex align-center">
|
||||
<RecipePageScale v-model:scale="scale" :recipe="recipe" />
|
||||
<RecipePageScale v-model="scale" :recipe="recipe" />
|
||||
</div>
|
||||
<RecipePageIngredientToolsView
|
||||
v-if="!isEditForm"
|
||||
|
@ -124,7 +124,7 @@
|
|||
</v-sheet>
|
||||
<v-sheet v-show="isCookMode && hasLinkedIngredients">
|
||||
<div class="mt-2 px-2 px-md-4">
|
||||
<RecipePageScale v-model:scale="scale" :recipe="recipe" />
|
||||
<RecipePageScale v-model="scale" :recipe="recipe" />
|
||||
</div>
|
||||
<RecipePageInstructions
|
||||
v-model="recipe.recipeInstructions"
|
||||
|
@ -141,7 +141,6 @@
|
|||
<RecipeIngredients
|
||||
:value="notLinkedIngredients"
|
||||
:scale="scale"
|
||||
:disable-amount="recipe.settings.disableAmount"
|
||||
:is-cook-mode="isCookMode"
|
||||
/>
|
||||
</v-card>
|
||||
|
@ -278,7 +277,7 @@ async function deleteRecipe() {
|
|||
* View Preferences
|
||||
*/
|
||||
const landscape = computed(() => {
|
||||
const preferLandscape = recipe.value.settings.landscapeView;
|
||||
const preferLandscape = recipe.value.settings?.landscapeView;
|
||||
const smallScreen = !$vuetify.display.smAndUp.value;
|
||||
|
||||
if (preferLandscape) {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useRecipePermissions } from "~/composables/recipes";
|
||||
import RecipePageInfoCard from "~/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageInfoCard.vue";
|
||||
|
@ -35,82 +35,48 @@ import { useStaticRoutes, useUserApi } from "~/composables/api";
|
|||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
import { usePageState, usePageUser, PageMode, EditorMode } from "~/composables/recipe-page/shared-state";
|
||||
import { usePageState, usePageUser, PageMode } from "~/composables/recipe-page/shared-state";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
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 { imageKey, pageMode, editMode, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
|
||||
const { user } = usePageUser();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
interface Props {
|
||||
recipe: NoUndefinedField<Recipe>;
|
||||
recipeScale?: number;
|
||||
landscape?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
recipeScale: 1,
|
||||
landscape: false,
|
||||
});
|
||||
|
||||
const recipeHousehold = ref<HouseholdSummary>();
|
||||
if (user) {
|
||||
defineEmits(["save", "delete"]);
|
||||
|
||||
const { recipeImage } = useStaticRoutes();
|
||||
const { imageKey, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
|
||||
const { user } = usePageUser();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const recipeHousehold = ref<HouseholdSummary>();
|
||||
if (user) {
|
||||
const userApi = useUserApi();
|
||||
userApi.households.getOne(props.recipe.householdId).then(({ data }) => {
|
||||
recipeHousehold.value = data || undefined;
|
||||
});
|
||||
}
|
||||
const { canEditRecipe } = useRecipePermissions(props.recipe, recipeHousehold, user);
|
||||
}
|
||||
const { canEditRecipe } = useRecipePermissions(props.recipe, recipeHousehold, user);
|
||||
|
||||
function printRecipe() {
|
||||
function printRecipe() {
|
||||
window.print();
|
||||
}
|
||||
}
|
||||
|
||||
const hideImage = ref(false);
|
||||
const imageHeight = computed(() => {
|
||||
return $vuetify.display.xs.value ? "200" : "400";
|
||||
});
|
||||
const hideImage = ref(false);
|
||||
|
||||
const recipeImageUrl = computed(() => {
|
||||
const recipeImageUrl = computed(() => {
|
||||
return recipeImage(props.recipe.id, props.recipe.image, imageKey.value);
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
watch(
|
||||
() => recipeImageUrl.value,
|
||||
() => {
|
||||
hideImage.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
isOwnGroup,
|
||||
setMode,
|
||||
toggleEditMode,
|
||||
recipeImage,
|
||||
canEditRecipe,
|
||||
imageKey,
|
||||
user,
|
||||
PageMode,
|
||||
pageMode,
|
||||
EditorMode,
|
||||
editMode,
|
||||
printRecipe,
|
||||
imageHeight,
|
||||
hideImage,
|
||||
isEditMode,
|
||||
recipeImageUrl,
|
||||
};
|
||||
},
|
||||
});
|
||||
);
|
||||
</script>
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
>
|
||||
<RecipeYield
|
||||
:yield-quantity="recipe.recipeYieldQuantity"
|
||||
:yield="recipe.recipeYield"
|
||||
:yield-text="recipe.recipeYield"
|
||||
:scale="recipeScale"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
@ -76,7 +76,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.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 { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeRating,
|
||||
RecipeLastMade,
|
||||
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();
|
||||
interface Props {
|
||||
recipe: NoUndefinedField<Recipe>;
|
||||
recipeScale?: number;
|
||||
landscape: boolean;
|
||||
}
|
||||
|
||||
return {
|
||||
isOwnGroup,
|
||||
};
|
||||
},
|
||||
withDefaults(defineProps<Props>(), {
|
||||
recipeScale: 1,
|
||||
});
|
||||
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
</script>
|
||||
|
|
|
@ -12,60 +12,47 @@
|
|||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useStaticRoutes, useUserApi } from "~/composables/api";
|
||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object as () => NoUndefinedField<Recipe>,
|
||||
required: true,
|
||||
},
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { $vuetify } = useNuxtApp();
|
||||
const { recipeImage } = useStaticRoutes();
|
||||
const { imageKey } = usePageState(props.recipe.slug);
|
||||
const { user } = usePageUser();
|
||||
interface Props {
|
||||
recipe: NoUndefinedField<Recipe>;
|
||||
maxWidth?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
maxWidth: undefined,
|
||||
});
|
||||
|
||||
const recipeHousehold = ref<HouseholdSummary>();
|
||||
if (user) {
|
||||
const { $vuetify } = useNuxtApp();
|
||||
const { recipeImage } = useStaticRoutes();
|
||||
const { imageKey } = usePageState(props.recipe.slug);
|
||||
const { user } = usePageUser();
|
||||
|
||||
const recipeHousehold = ref<HouseholdSummary>();
|
||||
if (user) {
|
||||
const userApi = useUserApi();
|
||||
userApi.households.getOne(props.recipe.householdId).then(({ data }) => {
|
||||
recipeHousehold.value = data || undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const hideImage = ref(false);
|
||||
const imageHeight = computed(() => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
watch(
|
||||
() => recipeImageUrl.value,
|
||||
() => {
|
||||
hideImage.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
recipeImageUrl,
|
||||
imageKey,
|
||||
hideImage,
|
||||
imageHeight,
|
||||
};
|
||||
},
|
||||
});
|
||||
);
|
||||
</script>
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<h2 class="mb-4 text-h5 font-weight-medium opacity-80">
|
||||
{{ $t("recipe.ingredients") }}
|
||||
</h2>
|
||||
<BannerWarning v-if="!hasFoodOrUnit">
|
||||
{{ $t("recipe.ingredients-not-parsed-description", { parse: $t('recipe.parse') }) }}
|
||||
</BannerWarning>
|
||||
</div>
|
||||
<VueDraggable
|
||||
v-if="recipe.recipeIngredient.length > 0"
|
||||
v-model="recipe.recipeIngredient"
|
||||
|
@ -27,7 +32,6 @@
|
|||
:key="ingredient.referenceId"
|
||||
v-model="recipe.recipeIngredient[index]"
|
||||
class="list-group-item"
|
||||
:disable-amount="recipe.settings.disableAmount"
|
||||
@delete="recipe.recipeIngredient.splice(index, 1)"
|
||||
@insert-above="insertNewIngredient(index)"
|
||||
@insert-below="insertNewIngredient(index + 1)"
|
||||
|
@ -42,14 +46,14 @@
|
|||
/>
|
||||
<div class="d-flex flex-wrap justify-center justify-sm-end mt-3">
|
||||
<v-tooltip
|
||||
top
|
||||
location="top"
|
||||
color="accent"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<span>
|
||||
<BaseButton
|
||||
class="mb-1"
|
||||
:disabled="recipe.settings.disableAmount || hasFoodOrUnit"
|
||||
:disabled="hasFoodOrUnit"
|
||||
color="accent"
|
||||
:to="`/g/${groupSlug}/r/${recipe.slug}/ingredient-parser`"
|
||||
v-bind="props"
|
||||
|
@ -109,10 +113,7 @@ const hasFoodOrUnit = computed(() => {
|
|||
});
|
||||
|
||||
const parserToolTip = computed(() => {
|
||||
if (recipe.value.settings.disableAmount) {
|
||||
return i18n.t("recipe.enable-ingredient-amounts-to-use-this-feature");
|
||||
}
|
||||
else if (hasFoodOrUnit.value) {
|
||||
if (hasFoodOrUnit.value) {
|
||||
return i18n.t("recipe.recipes-with-units-or-foods-defined-cannot-be-parsed");
|
||||
}
|
||||
return i18n.t("recipe.parse-ingredients");
|
||||
|
@ -127,7 +128,6 @@ function addIngredient(ingredients: Array<string> | null = null) {
|
|||
note: x,
|
||||
unit: undefined,
|
||||
food: undefined,
|
||||
disableAmount: true,
|
||||
quantity: 1,
|
||||
};
|
||||
});
|
||||
|
@ -146,7 +146,6 @@ function addIngredient(ingredients: Array<string> | null = null) {
|
|||
unit: undefined,
|
||||
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
|
||||
food: undefined,
|
||||
disableAmount: true,
|
||||
quantity: 1,
|
||||
});
|
||||
}
|
||||
|
@ -161,7 +160,6 @@ function insertNewIngredient(dest: number) {
|
|||
unit: undefined,
|
||||
// @ts-expect-error - prop can be null-type by NoUndefinedField type forces it to be set
|
||||
food: undefined,
|
||||
disableAmount: true,
|
||||
quantity: 1,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
<RecipeIngredients
|
||||
:value="recipe.recipeIngredient"
|
||||
:scale="scale"
|
||||
:disable-amount="recipe.settings.disableAmount"
|
||||
:is-cook-mode="isCookMode"
|
||||
/>
|
||||
<div v-if="!isEditMode && recipe.tools && recipe.tools.length > 0">
|
||||
|
@ -36,7 +35,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
||||
import { useToolStore } from "~/composables/store";
|
||||
|
@ -48,32 +47,22 @@ interface RecipeToolWithOnHand extends RecipeTool {
|
|||
onHand: boolean;
|
||||
}
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
RecipeIngredients,
|
||||
},
|
||||
props: {
|
||||
recipe: {
|
||||
type: Object as () => NoUndefinedField<Recipe>,
|
||||
required: true,
|
||||
},
|
||||
scale: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isCookMode: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
interface Props {
|
||||
recipe: NoUndefinedField<Recipe>;
|
||||
scale: number;
|
||||
isCookMode?: boolean;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isCookMode: false,
|
||||
});
|
||||
|
||||
const toolStore = isOwnGroup.value ? useToolStore() : null;
|
||||
const { user } = usePageUser();
|
||||
const { isEditMode } = usePageState(props.recipe.slug);
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const recipeTools = computed(() => {
|
||||
const toolStore = isOwnGroup.value ? useToolStore() : null;
|
||||
const { user } = usePageUser();
|
||||
const { isEditMode } = usePageState(props.recipe.slug);
|
||||
|
||||
const recipeTools = computed(() => {
|
||||
if (!(user.householdSlug && toolStore)) {
|
||||
return props.recipe.tools.map(tool => ({ ...tool, onHand: false }) as RecipeToolWithOnHand);
|
||||
}
|
||||
|
@ -83,9 +72,9 @@ export default defineNuxtComponent({
|
|||
return { ...tool, onHand } as RecipeToolWithOnHand;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function updateTool(index: number) {
|
||||
function updateTool(index: number) {
|
||||
if (user.id && user.householdSlug && toolStore) {
|
||||
const tool = recipeTools.value[index];
|
||||
if (tool.onHand && !tool.householdsWithTool?.includes(user.householdSlug)) {
|
||||
|
@ -105,14 +94,5 @@ export default defineNuxtComponent({
|
|||
else {
|
||||
console.log("no user, skipping server update");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
toolStore,
|
||||
recipeTools,
|
||||
isEditMode,
|
||||
updateTool,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
</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