feat: Remove Explore URLs and make the normal URLs public (#2632)

* add groupSlug to most routes

* fixed more routing issues

* fixed jank and incorrect routes

* remove public explore links

* remove unused groupSlug and explore routes

* nuked explore pages

* fixed public toolstore bug

* fixed various routes missing group slug

* restored public app header menu

* fix janky login redirect

* 404 recipe API call returns to login

* removed unused explore layout

* force redirect when using the wrong group slug

* fixed dead admin links

* removed unused middleware from earlier attempt

* 🧹

* improve cookbooks sidebar
fixed sidebar link not working
fixed sidebar link target
hide cookbooks header when there are none

* added group slug to user

* fix $auth typehints

* vastly simplified groupSlug logic

* allow logged-in users to view other groups

* fixed some edgecases that bypassed isOwnGroup

* fixed static home ref

* 🧹

* fixed redirect logic

* lint warning

* removed group slug from group and user pages
refactored all components to use route groupSlug or user group slug
moved some group pages to recipe pages

* fixed some bad types

* 🧹

* moved groupSlug routes under /g/groupSlug

* move /recipe/ to /r/

* fix backend url generation and metadata injection

* moved shopping lists to root/other route fixes

* changed shared from /recipes/ to /r/

* fixed 404 redirect not awaiting

* removed unused import

* fix doc links

* fix public recipe setting not affecting public API

* fixed backend tests

* fix nuxt-generate command

---------

Co-authored-by: Hayden <64056131+hay-kot@users.noreply.github.com>
This commit is contained in:
Michael Genson 2023-11-05 19:07:02 -06:00 committed by GitHub
parent 94cf690e8f
commit 80968b02bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 555 additions and 501 deletions

View File

@ -17,7 +17,7 @@ body:
- label: | - label: |
I have verified that this issue _is not_ related to the underlying library I have verified that this issue _is not_ related to the underlying library
[hhyrsev/recipe-scrapers](https://github.com/hhursev/recipe-scrapers) by **1)** checking [hhyrsev/recipe-scrapers](https://github.com/hhursev/recipe-scrapers) by **1)** checking
the [debugger](https://demo.mealie.io/recipe/create/debug) and data is returned, **2)** the [debugger](https://demo.mealie.io/g/home/r/create/debug) and data is returned, **2)**
verifying that there _are_ errors in the log related to application level code, or verifying that there _are_ errors in the log related to application level code, or
**3)** verified that the site provides recipe data, or is otherwise supported by **3)** verified that the site provides recipe data, or is otherwise supported by
[hhyrsev/recipe-scrapers](https://github.com/hhursev/recipe-scrapers) [hhyrsev/recipe-scrapers](https://github.com/hhursev/recipe-scrapers)

View File

@ -15,6 +15,6 @@ var url = document.URL;
var mealie = "http://localhost:8080"; var mealie = "http://localhost:8080";
var use_keywords= "&use_keywords=1" // Optional - use keywords from recipe - update to "" if you don't want that var use_keywords= "&use_keywords=1" // Optional - use keywords from recipe - update to "" if you don't want that
var edity = "&edit=1" // Optional - keep in edit mode - update to "" if you don't want that var edity = "&edit=1" // Optional - keep in edit mode - update to "" if you don't want that
var dest = mealie + "/recipe/create/url?recipe_import_url=" + url + use_keywords + edity; var dest = mealie + "/r/create/url?recipe_import_url=" + url + use_keywords + edity;
window.open(dest, "_blank"); window.open(dest, "_blank");
``` ```

View File

@ -8,7 +8,7 @@
Mealie offers two main ways to create recipes. You can use the integrated recipe-scraper to create recipes from hundreds of websites, or you can create recipes manually using the recipe editor. Mealie offers two main ways to create recipes. You can use the integrated recipe-scraper to create recipes from hundreds of websites, or you can create recipes manually using the recipe editor.
[Creation Demo](https://demo.mealie.io/recipe/create/url){ .md-button .md-button--primary .align-right } [Creation Demo](https://demo.mealie.io/g/home/r/create/url){ .md-button .md-button--primary .align-right }
### Importing Recipes ### Importing Recipes
@ -34,13 +34,13 @@ Mealie has a robust and flexible recipe organization system with a few different
Categories are the overarching organizer for recipes. You can assign as many categories as you'd like to a recipe, but we recommend that you try to limit the categories you assign to a recipe to one or two. This helps keep categories as focused as possible while still allowing you to find recipes that are related to each other. For example, you might assign a recipe to the category **Breakfast**, **Lunch**, **Dinner**, or **Side**. Categories are the overarching organizer for recipes. You can assign as many categories as you'd like to a recipe, but we recommend that you try to limit the categories you assign to a recipe to one or two. This helps keep categories as focused as possible while still allowing you to find recipes that are related to each other. For example, you might assign a recipe to the category **Breakfast**, **Lunch**, **Dinner**, or **Side**.
[Categories Demo](https://demo.mealie.io/recipes/categories){ .md-button .md-button--primary } [Categories Demo](https://demo.mealie.io/g/home/recipes/categories){ .md-button .md-button--primary }
#### Tags #### Tags
Tags, are nearly identical to categories in function but play a secondary role in some cases. As such, we recommend that you use tags freely to help you organize your recipes by more specific topics. For example, if a recipe can be frozen or is a great left-over meal, you could assign the tags **frozen** and **left-over** and easily filter for those at a later time. Tags, are nearly identical to categories in function but play a secondary role in some cases. As such, we recommend that you use tags freely to help you organize your recipes by more specific topics. For example, if a recipe can be frozen or is a great left-over meal, you could assign the tags **frozen** and **left-over** and easily filter for those at a later time.
[Tags Demo](https://demo.mealie.io/recipes/tags){ .md-button .md-button--primary } [Tags Demo](https://demo.mealie.io/g/home/recipes/tags){ .md-button .md-button--primary }
#### Tools #### Tools
@ -48,7 +48,7 @@ Tools, are another way that some users like to organize their recipes. If a reci
Each of the above organizers can be filtered in searches, and have their own pages where you can view all the recipes that are associated with those organizers. Each of the above organizers can be filtered in searches, and have their own pages where you can view all the recipes that are associated with those organizers.
[Tools Demo](https://demo.mealie.io/recipes/tools){ .md-button .md-button--primary } [Tools Demo](https://demo.mealie.io/g/home/recipes/tools){ .md-button .md-button--primary }
#### Cookbooks #### Cookbooks
@ -60,7 +60,7 @@ Mealie also has the concept of cookbooks. These can be created inside of a group
- Pasta Sides: Recipes that have both the **Side** category and the **Pasta** tag - Pasta Sides: Recipes that have both the **Side** category and the **Pasta** tag
- Dessert Breads: Recipes that have both the **Bread** category and the **Dessert** tag - Dessert Breads: Recipes that have both the **Bread** category and the **Dessert** tag
[Cookbooks Demo](https://demo.mealie.io/group/cookbooks){ .md-button .md-button--primary } [Cookbooks Demo](https://demo.mealie.io/g/home/cookbooks){ .md-button .md-button--primary }
## Meal Planning ## Meal Planning

View File

@ -15,7 +15,6 @@
class="mb-5 mx-1" class="mb-5 mx-1"
:recipes="recipes" :recipes="recipes"
:query="{ cookbook: slug }" :query="{ cookbook: slug }"
:group-slug="groupSlug"
@sortRecipes="assignSorted" @sortRecipes="assignSorted"
@replaceRecipes="replaceRecipes" @replaceRecipes="replaceRecipes"
@appendRecipes="appendRecipes" @appendRecipes="appendRecipes"
@ -30,24 +29,20 @@
import { useLazyRecipes } from "~/composables/recipes"; import { useLazyRecipes } from "~/composables/recipes";
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue"; import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
import { useCookbook } from "~/composables/use-group-cookbooks"; import { useCookbook } from "~/composables/use-group-cookbooks";
import { useLoggedInState } from "~/composables/use-logged-in-state";
export default defineComponent({ export default defineComponent({
components: { RecipeCardSection }, components: { RecipeCardSection },
props: { setup() {
groupSlug: {
type: String,
default: undefined,
}
},
setup(props) {
const { $auth } = useContext(); const { $auth } = useContext();
const loggedIn = computed(() => $auth.loggedIn); const { isOwnGroup } = useLoggedInState();
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(loggedIn.value ? null : props.groupSlug);
const route = useRoute(); const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
const slug = route.value.params.slug; const slug = route.value.params.slug;
const { getOne } = useCookbook(loggedIn.value ? null : props.groupSlug); const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
const tab = ref(null); const tab = ref(null);
const book = getOne(slug); const book = getOne(slug);

View File

@ -70,7 +70,6 @@
print: true, print: true,
printPreferences: true, printPreferences: true,
share: loggedIn, share: loggedIn,
publicUrl: recipe.settings && loggedIn ? recipe.settings.public : false,
}" }"
@print="$emit('print')" @print="$emit('print')"
/> />

View File

@ -34,7 +34,7 @@
<slot name="actions"> <slot name="actions">
<v-card-actions class="px-1"> <v-card-actions class="px-1">
<RecipeFavoriteBadge v-if="loggedIn" class="absolute" :slug="slug" show-always /> <RecipeFavoriteBadge v-if="isOwnGroup" class="absolute" :slug="slug" show-always />
<RecipeRating class="pb-1" :value="rating" :name="name" :slug="slug" :small="true" /> <RecipeRating class="pb-1" :value="rating" :name="name" :slug="slug" :small="true" />
<v-spacer></v-spacer> <v-spacer></v-spacer>
@ -42,7 +42,7 @@
<!-- If we're not logged-in, no items display, so we hide this menu --> <!-- If we're not logged-in, no items display, so we hide this menu -->
<RecipeContextMenu <RecipeContextMenu
v-if="loggedIn" v-if="isOwnGroup"
color="grey darken-2" color="grey darken-2"
:slug="slug" :slug="slug"
:name="name" :name="name"
@ -56,7 +56,6 @@
print: false, print: false,
printPreferences: false, printPreferences: false,
share: true, share: true,
publicUrl: false,
}" }"
@delete="$emit('delete', slug)" @delete="$emit('delete', slug)"
/> />
@ -69,12 +68,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api"; import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue"; import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
import RecipeChips from "./RecipeChips.vue"; import RecipeChips from "./RecipeChips.vue";
import RecipeContextMenu from "./RecipeContextMenu.vue"; import RecipeContextMenu from "./RecipeContextMenu.vue";
import RecipeCardImage from "./RecipeCardImage.vue"; import RecipeCardImage from "./RecipeCardImage.vue";
import RecipeRating from "./RecipeRating.vue"; import RecipeRating from "./RecipeRating.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
export default defineComponent({ export default defineComponent({
components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage }, components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage },
@ -83,10 +83,6 @@ export default defineComponent({
type: String, type: String,
required: true, required: true,
}, },
groupSlug: {
type: String,
default: null,
},
slug: { slug: {
type: String, type: String,
required: true, required: true,
@ -124,16 +120,16 @@ export default defineComponent({
}, },
setup(props) { setup(props) {
const { $auth } = useContext(); const { $auth } = useContext();
const loggedIn = computed(() => { const { isOwnGroup } = useLoggedInState();
return $auth.loggedIn;
});
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "")
const recipeRoute = computed<string>(() => { const recipeRoute = computed<string>(() => {
return loggedIn.value ? `/recipe/${props.slug}` : `/explore/recipes/${props.groupSlug}/${props.slug}`; return `/g/${groupSlug.value}/r/${props.slug}`;
}); });
return { return {
loggedIn, isOwnGroup,
recipeRoute, recipeRoute,
}; };
}, },

View File

@ -37,10 +37,10 @@
</v-list-item-subtitle> </v-list-item-subtitle>
<div class="d-flex flex-wrap justify-end align-center"> <div class="d-flex flex-wrap justify-end align-center">
<slot name="actions"> <slot name="actions">
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always /> <RecipeFavoriteBadge v-if="isOwnGroup" :slug="slug" show-always />
<v-rating <v-rating
color="secondary" color="secondary"
:class="loggedIn ? 'ml-auto' : 'ml-auto pb-2'" :class="isOwnGroup ? 'ml-auto' : 'ml-auto pb-2'"
background-color="secondary lighten-3" background-color="secondary lighten-3"
dense dense
length="5" length="5"
@ -52,7 +52,7 @@
<!-- If we're not logged-in, no items display, so we hide this menu --> <!-- If we're not logged-in, no items display, so we hide this menu -->
<!-- We also add padding to the v-rating above to compensate --> <!-- We also add padding to the v-rating above to compensate -->
<RecipeContextMenu <RecipeContextMenu
v-if="loggedIn" v-if="isOwnGroup"
:slug="slug" :slug="slug"
:menu-icon="$globals.icons.dotsHorizontal" :menu-icon="$globals.icons.dotsHorizontal"
:name="name" :name="name"
@ -66,7 +66,6 @@
print: false, print: false,
printPreferences: false, printPreferences: false,
share: true, share: true,
publicUrl: false,
}" }"
@deleted="$emit('delete', slug)" @deleted="$emit('delete', slug)"
/> />
@ -80,10 +79,11 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api"; import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue"; import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
import RecipeContextMenu from "./RecipeContextMenu.vue"; import RecipeContextMenu from "./RecipeContextMenu.vue";
import RecipeCardImage from "./RecipeCardImage.vue"; import RecipeCardImage from "./RecipeCardImage.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
export default defineComponent({ export default defineComponent({
components: { components: {
@ -96,10 +96,6 @@ export default defineComponent({
type: String, type: String,
required: true, required: true,
}, },
groupSlug: {
type: String,
default: null,
},
slug: { slug: {
type: String, type: String,
required: true, required: true,
@ -136,16 +132,16 @@ export default defineComponent({
}, },
setup(props) { setup(props) {
const { $auth } = useContext(); const { $auth } = useContext();
const loggedIn = computed(() => { const { isOwnGroup } = useLoggedInState();
return $auth.loggedIn;
});
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "")
const recipeRoute = computed<string>(() => { const recipeRoute = computed<string>(() => {
return loggedIn.value ? `/recipe/${props.slug}` : `/explore/recipes/${props.groupSlug}/${props.slug}`; return `/g/${groupSlug.value}/r/${props.slug}`;
}); });
return { return {
loggedIn, isOwnGroup,
recipeRoute, recipeRoute,
}; };
}, },

View File

@ -76,7 +76,6 @@
<RecipeCard <RecipeCard
:name="recipe.name" :name="recipe.name"
:description="recipe.description" :description="recipe.description"
:group-slug="groupSlug"
:slug="recipe.slug" :slug="recipe.slug"
:rating="recipe.rating" :rating="recipe.rating"
:image="recipe.image" :image="recipe.image"
@ -100,7 +99,6 @@
<RecipeCardMobile <RecipeCardMobile
:name="recipe.name" :name="recipe.name"
:description="recipe.description" :description="recipe.description"
:group-slug="groupSlug"
:slug="recipe.slug" :slug="recipe.slug"
:rating="recipe.rating" :rating="recipe.rating"
:image="recipe.image" :image="recipe.image"
@ -128,12 +126,14 @@ import {
toRefs, toRefs,
useAsync, useAsync,
useContext, useContext,
useRoute,
useRouter, useRouter,
watch, watch,
} from "@nuxtjs/composition-api"; } from "@nuxtjs/composition-api";
import { useThrottleFn } from "@vueuse/core"; import { useThrottleFn } from "@vueuse/core";
import RecipeCard from "./RecipeCard.vue"; import RecipeCard from "./RecipeCard.vue";
import RecipeCardMobile from "./RecipeCardMobile.vue"; import RecipeCardMobile from "./RecipeCardMobile.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { useAsyncKey } from "~/composables/use-utils"; import { useAsyncKey } from "~/composables/use-utils";
import { useLazyRecipes } from "~/composables/recipes"; import { useLazyRecipes } from "~/composables/recipes";
import { Recipe } from "~/lib/api/types/recipe"; import { Recipe } from "~/lib/api/types/recipe";
@ -165,10 +165,6 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
groupSlug: {
type: String,
default: null,
},
recipes: { recipes: {
type: Array as () => Recipe[], type: Array as () => Recipe[],
default: () => [], default: () => [],
@ -191,9 +187,7 @@ export default defineComponent({
}; };
const { $auth, $globals, $vuetify } = useContext(); const { $auth, $globals, $vuetify } = useContext();
const loggedIn = computed(() => { const { isOwnGroup } = useLoggedInState();
return $auth.loggedIn;
});
const useMobileCards = computed(() => { const useMobileCards = computed(() => {
return $vuetify.breakpoint.smAndDown || preferences.value.useMobileCards; return $vuetify.breakpoint.smAndDown || preferences.value.useMobileCards;
}); });
@ -206,12 +200,15 @@ export default defineComponent({
sortLoading: false, sortLoading: false,
}); });
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const router = useRouter(); const router = useRouter();
function navigateRandom() { function navigateRandom() {
if (props.recipes.length > 0) { if (props.recipes.length > 0) {
const recipe = props.recipes[Math.floor(Math.random() * props.recipes.length)]; const recipe = props.recipes[Math.floor(Math.random() * props.recipes.length)];
if (recipe.slug !== undefined) { if (recipe.slug !== undefined) {
router.push(loggedIn.value ? `/recipe/${recipe.slug}` : `/explore/recipes/${props.groupSlug}/${recipe.slug}`); router.push(`/g/${groupSlug.value}/r/${recipe.slug}`);
} }
} }
} }
@ -222,7 +219,7 @@ export default defineComponent({
const ready = ref(false); const ready = ref(false);
const loading = ref(false); const loading = ref(false);
const { fetchMore } = useLazyRecipes(loggedIn.value ? null : props.groupSlug); const { fetchMore } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
const queryFilter = computed(() => { const queryFilter = computed(() => {
const orderBy = props.query?.orderBy || preferences.value.orderBy; const orderBy = props.query?.orderBy || preferences.value.orderBy;

View File

@ -9,7 +9,7 @@
color="accent" color="accent"
:small="small" :small="small"
dark dark
:to=" loggedIn ? `/?${urlPrefix}=${category.id}` : undefined" :to="isOwnGroup ? `${baseRecipeRoute}?${urlPrefix}=${category.id}` : undefined"
> >
{{ truncateText(category.name) }} {{ truncateText(category.name) }}
</v-chip> </v-chip>
@ -17,7 +17,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, useContext } from "@nuxtjs/composition-api"; import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/user"; import { RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/user";
export type UrlPrefixParam = "tags" | "categories" | "tools"; export type UrlPrefixParam = "tags" | "categories" | "tools";
@ -55,9 +56,13 @@ export default defineComponent({
}, },
setup(props) { setup(props) {
const { $auth } = useContext(); const { $auth } = useContext();
const loggedIn = computed(() => { const { isOwnGroup } = useLoggedInState();
return $auth.loggedIn
}) const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "")
const baseRecipeRoute = computed<string>(() => {
return `/g/${groupSlug.value}`
});
function truncateText(text: string, length = 20, clamp = "...") { function truncateText(text: string, length = 20, clamp = "...") {
if (!props.truncate) return text; if (!props.truncate) return text;
@ -68,7 +73,8 @@ export default defineComponent({
} }
return { return {
loggedIn, baseRecipeRoute,
isOwnGroup,
truncateText, truncateText,
}; };
}, },

View File

@ -170,10 +170,11 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, reactive, toRefs, useContext, useRouter, ref } from "@nuxtjs/composition-api"; import { computed, defineComponent, reactive, toRefs, useContext, useRoute, useRouter, ref } from "@nuxtjs/composition-api";
import RecipeIngredientListItem from "./RecipeIngredientListItem.vue"; import RecipeIngredientListItem from "./RecipeIngredientListItem.vue";
import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue"; import RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
import RecipeDialogShare from "./RecipeDialogShare.vue"; import RecipeDialogShare from "./RecipeDialogShare.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
import { usePlanTypeOptions } from "~/composables/use-group-mealplan"; import { usePlanTypeOptions } from "~/composables/use-group-mealplan";
@ -181,7 +182,6 @@ import { Recipe, RecipeIngredient } from "~/lib/api/types/recipe";
import { ShoppingListSummary } from "~/lib/api/types/group"; import { ShoppingListSummary } from "~/lib/api/types/group";
import { PlanEntryType } from "~/lib/api/types/meal-plan"; import { PlanEntryType } from "~/lib/api/types/meal-plan";
import { useAxiosDownloader } from "~/composables/api/use-axios-download"; import { useAxiosDownloader } from "~/composables/api/use-axios-download";
import { useCopy } from "~/composables/use-copy";
export interface ContextMenuIncludes { export interface ContextMenuIncludes {
delete: boolean; delete: boolean;
@ -192,7 +192,6 @@ export interface ContextMenuIncludes {
print: boolean; print: boolean;
printPreferences: boolean; printPreferences: boolean;
share: boolean; share: boolean;
publicUrl: boolean;
} }
export interface ContextMenuItem { export interface ContextMenuItem {
@ -222,7 +221,6 @@ export default defineComponent({
print: true, print: true,
printPreferences: true, printPreferences: true,
share: true, share: true,
publicUrl: false,
}), }),
}, },
// Append items are added at the end of the useItems list // Append items are added at the end of the useItems list
@ -291,10 +289,11 @@ export default defineComponent({
pickerMenu: false, pickerMenu: false,
}); });
const { $auth, i18n, $globals } = useContext(); const { i18n, $auth, $globals } = useContext();
const loggedIn = computed(() => { const { isOwnGroup } = useLoggedInState();
return $auth.loggedIn;
}); const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
// =========================================================================== // ===========================================================================
// Context Menu Setup // Context Menu Setup
@ -363,20 +362,13 @@ export default defineComponent({
event: "share", event: "share",
isPublic: false, isPublic: false,
}, },
publicUrl: {
title: i18n.tc("recipe.public-link"),
icon: $globals.icons.contentCopy,
color: undefined,
event: "publicUrl",
isPublic: true,
},
}; };
// Get Default Menu Items Specified in Props // Get Default Menu Items Specified in Props
for (const [key, value] of Object.entries(props.useItems)) { for (const [key, value] of Object.entries(props.useItems)) {
if (value) { if (value) {
const item = defaultItems[key]; const item = defaultItems[key];
if (item && (item.isPublic || loggedIn.value)) { if (item && (item.isPublic || isOwnGroup.value)) {
state.menuItems.push(item); state.menuItems.push(item);
} }
} }
@ -500,24 +492,7 @@ export default defineComponent({
async function duplicateRecipe() { async function duplicateRecipe() {
const { data } = await api.recipes.duplicateOne(props.slug, state.recipeName); const { data } = await api.recipes.duplicateOne(props.slug, state.recipeName);
if (data && data.slug) { if (data && data.slug) {
router.push(`/recipe/${data.slug}`); router.push(`/g/${groupSlug.value}/r/${data.slug}`);
}
}
const { copyText } = useCopy();
const groupSlug = ref<string>("");
async function setGroupSlug() {
if (groupSlug.value) {
return;
}
const { data } = await api.users.getSelfGroup();
if (data) {
groupSlug.value = data.slug;
} else {
// @ts-ignore this will either be a string or undefined
groupSlug.value = $auth.user?.groupId
} }
} }
@ -526,7 +501,7 @@ export default defineComponent({
delete: () => { delete: () => {
state.recipeDeleteDialog = true; state.recipeDeleteDialog = true;
}, },
edit: () => router.push(`/recipe/${props.slug}` + "?edit=true"), edit: () => router.push(`/g/${groupSlug.value}/r/${props.slug}` + "?edit=true"),
download: handleDownloadEvent, download: handleDownloadEvent,
duplicate: () => { duplicate: () => {
state.recipeDuplicateDialog = true; state.recipeDuplicateDialog = true;
@ -549,14 +524,6 @@ export default defineComponent({
share: () => { share: () => {
state.shareDialog = true; state.shareDialog = true;
}, },
publicUrl: async () => {
await setGroupSlug();
if (!groupSlug.value) {
return;
}
copyText(`${window.location.origin}/explore/recipes/${groupSlug.value}/${props.slug}`);
},
}; };
function contextMenuEventHandler(eventKey: string) { function contextMenuEventHandler(eventKey: string) {

View File

@ -18,7 +18,7 @@
</tr> </tr>
</template> </template>
<template #item.name="{ item }"> <template #item.name="{ item }">
<a :href="`/recipe/${item.slug}`" style="color: inherit; text-decoration: inherit; " @click="$emit('click')">{{ item.name }}</a> <a :href="`/r/${item.slug}`" style="color: inherit; text-decoration: inherit; " @click="$emit('click')">{{ item.name }}</a>
</template> </template>
<template #item.tags="{ item }"> <template #item.tags="{ item }">
<RecipeChip small :items="item.tags" :is-category="false" url-prefix="tags" /> <RecipeChip small :items="item.tags" :is-category="false" url-prefix="tags" />

View File

@ -31,7 +31,7 @@
<div class="mr-auto"> <div class="mr-auto">
{{ $t("search.results") }} {{ $t("search.results") }}
</div> </div>
<router-link to="/"> {{ $t("search.advanced-search") }} </router-link> <router-link :to="advancedSearchUrl"> {{ $t("search.advanced-search") }} </router-link>
</v-card-actions> </v-card-actions>
<RecipeCardMobile <RecipeCardMobile
@ -54,11 +54,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, toRefs, reactive, ref, watch, useRoute } from "@nuxtjs/composition-api"; import { computed, defineComponent, toRefs, reactive, ref, watch, useContext, useRoute } from "@nuxtjs/composition-api";
import RecipeCardMobile from "./RecipeCardMobile.vue"; import RecipeCardMobile from "./RecipeCardMobile.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { RecipeSummary } from "~/lib/api/types/recipe"; import { RecipeSummary } from "~/lib/api/types/recipe";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { useRecipeSearch } from "~/composables/recipes/use-recipe-search"; import { useRecipeSearch } from "~/composables/recipes/use-recipe-search";
import { usePublicExploreApi } from "~/composables/api/api-client";
const SELECTED_EVENT = "selected"; const SELECTED_EVENT = "selected";
export default defineComponent({ export default defineComponent({
components: { components: {
@ -66,6 +68,7 @@ export default defineComponent({
}, },
setup(_, context) { setup(_, context) {
const { $auth } = useContext();
const state = reactive({ const state = reactive({
loading: false, loading: false,
selectedIndex: -1, selectedIndex: -1,
@ -128,7 +131,9 @@ export default defineComponent({
} }
}); });
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const route = useRoute(); const route = useRoute();
const advancedSearchUrl = computed(() => `/g/${groupSlug.value}`)
watch(route, close); watch(route, close);
function open() { function open() {
@ -140,7 +145,8 @@ export default defineComponent({
// =========================================================================== // ===========================================================================
// Basic Search // Basic Search
const api = useUserApi(); const { isOwnGroup } = useLoggedInState();
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
const search = useRecipeSearch(api); const search = useRecipeSearch(api);
// Select Handler // Select Handler
@ -152,6 +158,7 @@ export default defineComponent({
return { return {
...toRefs(state), ...toRefs(state),
advancedSearchUrl,
dialog, dialog,
open, open,
close, close,

View File

@ -56,7 +56,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, computed, toRefs, reactive, useContext } from "@nuxtjs/composition-api"; import { defineComponent, computed, toRefs, reactive, useContext, useRoute } from "@nuxtjs/composition-api";
import { useClipboard, useShare, whenever } from "@vueuse/core"; import { useClipboard, useShare, whenever } from "@vueuse/core";
import { RecipeShareToken } from "~/lib/api/types/recipe"; import { RecipeShareToken } from "~/lib/api/types/recipe";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
@ -105,6 +105,10 @@ export default defineComponent({
} }
); );
const { $auth, i18n } = useContext();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
// ============================================================ // ============================================================
// Token Actions // Token Actions
@ -138,7 +142,6 @@ export default defineComponent({
} }
} }
const { i18n } = useContext();
const { share, isSupported: shareIsSupported } = useShare(); const { share, isSupported: shareIsSupported } = useShare();
const { copy } = useClipboard(); const { copy } = useClipboard();
@ -147,7 +150,7 @@ export default defineComponent({
} }
function getTokenLink(token: string) { function getTokenLink(token: string) {
return `${window.location.origin}/shared/recipes/${token}`; return `${window.location.origin}/g/${groupSlug.value}/shared/r/${token}`;
} }
async function copyTokenLink(token: string) { async function copyTokenLink(token: string) {

View File

@ -123,7 +123,6 @@
class="mt-n5" class="mt-n5"
:icon="$globals.icons.search" :icon="$globals.icons.search"
:title="$tc('search.results')" :title="$tc('search.results')"
:group-slug="groupSlug"
:recipes="recipes" :recipes="recipes"
:query="passedQuery" :query="passedQuery"
@replaceRecipes="replaceRecipes" @replaceRecipes="replaceRecipes"
@ -134,9 +133,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, defineComponent, useRouter, onMounted, useContext, computed, Ref } from "@nuxtjs/composition-api"; import { ref, defineComponent, useRouter, onMounted, useContext, computed, Ref, useRoute } from "@nuxtjs/composition-api";
import { watchDebounced } from "@vueuse/shared"; import { watchDebounced } from "@vueuse/shared";
import SearchFilter from "~/components/Domain/SearchFilter.vue"; import SearchFilter from "~/components/Domain/SearchFilter.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store"; import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe"; import { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
@ -150,19 +150,11 @@ import { usePublicToolStore } from "~/composables/store/use-tool-store";
export default defineComponent({ export default defineComponent({
components: { SearchFilter, RecipeCardSection }, components: { SearchFilter, RecipeCardSection },
props: { setup() {
groupSlug: {
type: String,
required: true,
},
},
setup(props) {
const router = useRouter(); const router = useRouter();
const { $auth, $globals, i18n } = useContext(); const { $auth, $globals, i18n } = useContext();
const loggedIn = computed(() => { const { isOwnGroup } = useLoggedInState();
return $auth.loggedIn;
});
const state = ref({ const state = ref({
auto: true, auto: true,
search: "", search: "",
@ -176,17 +168,20 @@ export default defineComponent({
requireAllFoods: false, requireAllFoods: false,
}); });
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(loggedIn.value ? null : props.groupSlug); const route = useRoute();
const categories = loggedIn.value ? useCategoryStore() : usePublicCategoryStore(props.groupSlug); const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
const categories = isOwnGroup.value ? useCategoryStore() : usePublicCategoryStore(groupSlug.value);
const selectedCategories = ref<NoUndefinedField<RecipeCategory>[]>([]); const selectedCategories = ref<NoUndefinedField<RecipeCategory>[]>([]);
const foods = loggedIn.value ? useFoodStore() : usePublicFoodStore(props.groupSlug); const foods = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
const selectedFoods = ref<IngredientFood[]>([]); const selectedFoods = ref<IngredientFood[]>([]);
const tags = loggedIn.value ? useTagStore() : usePublicTagStore(props.groupSlug); const tags = isOwnGroup.value ? useTagStore() : usePublicTagStore(groupSlug.value);
const selectedTags = ref<NoUndefinedField<RecipeTag>[]>([]); const selectedTags = ref<NoUndefinedField<RecipeTag>[]>([]);
const tools = loggedIn.value ? useToolStore() : usePublicToolStore(props.groupSlug); const tools = isOwnGroup.value ? useToolStore() : usePublicToolStore(groupSlug.value);
const selectedTools = ref<NoUndefinedField<RecipeTool>[]>([]); const selectedTools = ref<NoUndefinedField<RecipeTool>[]>([]);
const passedQuery = ref<RecipeSearchQuery | null>(null); const passedQuery = ref<RecipeSearchQuery | null>(null);

View File

@ -7,7 +7,7 @@
:class="attrs.class.sheet" :class="attrs.class.sheet"
:style="tile ? 'max-width: 100%; width: fit-content;' : 'width: 100%;'" :style="tile ? 'max-width: 100%; width: fit-content;' : 'width: 100%;'"
> >
<v-list-item :to="'/recipe/' + recipe.slug" :class="attrs.class.listItem"> <v-list-item :to="'/' + groupSlug + '/r/' + recipe.slug" :class="attrs.class.listItem">
<v-list-item-avatar :class="attrs.class.avatar"> <v-list-item-avatar :class="attrs.class.avatar">
<v-icon :class="attrs.class.icon" dark :small="small"> {{ $globals.icons.primary }} </v-icon> <v-icon :class="attrs.class.icon" dark :small="small"> {{ $globals.icons.primary }} </v-icon>
</v-list-item-avatar> </v-list-item-avatar>
@ -28,7 +28,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent } from "@nuxtjs/composition-api"; import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { useFraction } from "~/composables/recipes/use-fraction"; import { useFraction } from "~/composables/recipes/use-fraction";
import { ShoppingListItemOut } from "~/lib/api/types/group"; import { ShoppingListItemOut } from "~/lib/api/types/group";
@ -58,7 +58,10 @@ export default defineComponent({
}, },
}, },
setup(props) { setup(props) {
const { $auth } = useContext();
const { frac } = useFraction(); const { frac } = useFraction();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const attrs = computed(() => { const attrs = computed(() => {
return props.small ? { return props.small ? {
@ -150,6 +153,7 @@ export default defineComponent({
return { return {
attrs, attrs,
groupSlug,
listItemDescriptions, listItemDescriptions,
}; };
}, },

View File

@ -140,7 +140,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, onMounted, reactive, toRefs, useRouter } from "@nuxtjs/composition-api"; import { defineComponent, ref, onMounted, reactive, toRefs, useContext, useRouter, computed, useRoute } from "@nuxtjs/composition-api";
import { until } from "@vueuse/core"; import { until } from "@vueuse/core";
import { invoke } from "@vueuse/shared"; import { invoke } from "@vueuse/shared";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
@ -179,6 +179,10 @@ export default defineComponent({
}, },
}, },
setup(props) { setup(props) {
const { $auth } = useContext();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const router = useRouter(); const router = useRouter();
const api = useUserApi(); const api = useUserApi();
@ -328,12 +332,12 @@ export default defineComponent({
async function updateRecipe() { async function updateRecipe() {
const { data } = await api.recipes.updateOne(props.recipe.slug, props.recipe); const { data } = await api.recipes.updateOne(props.recipe.slug, props.recipe);
if (data?.slug) { if (data?.slug) {
router.push("/recipe/" + data.slug); router.push(`/g/${groupSlug.value}/r/${data.slug}`);
} }
} }
function closeEditor() { function closeEditor() {
router.push("/recipe/" + props.recipe.slug); router.push(`/g/${groupSlug.value}/r/${props.recipe.slug}`);
} }
const canvasSetText = function () { const canvasSetText = function () {

View File

@ -48,7 +48,7 @@
<BaseCardSectionTitle v-if="isTitle(key)" :title="key" /> <BaseCardSectionTitle v-if="isTitle(key)" :title="key" />
<v-row> <v-row>
<v-col v-for="(item, index) in itms" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3"> <v-col v-for="(item, index) in itms" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
<v-card v-if="item" class="left-border" hover :to="`/?${itemType}=${item.id}`"> <v-card v-if="item" class="left-border" hover :to="`/g/${groupSlug}?${itemType}=${item.id}`">
<v-card-actions> <v-card-actions>
<v-icon> <v-icon>
{{ icon }} {{ icon }}
@ -72,7 +72,7 @@
<script lang="ts"> <script lang="ts">
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { defineComponent, computed, ref, reactive } from "@nuxtjs/composition-api"; import { defineComponent, computed, ref, reactive, useContext, useRoute } from "@nuxtjs/composition-api";
import { useContextPresets } from "~/composables/use-context-presents"; import { useContextPresets } from "~/composables/use-context-presents";
import RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue"; import RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue";
import { RecipeOrganizer } from "~/lib/api/types/non-generated"; import { RecipeOrganizer } from "~/lib/api/types/non-generated";
@ -119,6 +119,10 @@ export default defineComponent({
}, },
}); });
const { $auth } = useContext();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
// ================================================================= // =================================================================
// Context Menu // Context Menu
@ -204,6 +208,7 @@ export default defineComponent({
} }
return { return {
groupSlug,
isTitle, isTitle,
dialogs, dialogs,
confirmDelete, confirmDelete,

View File

@ -72,7 +72,7 @@
</div> </div>
<RecipePageComments <RecipePageComments
v-if="user.id && !recipe.settings.disableComments && !isEditForm && !isCookMode" v-if="isOwnGroup && !recipe.settings.disableComments && !isEditForm && !isCookMode"
:recipe="recipe" :recipe="recipe"
class="px-1 my-4 d-print-none" class="px-1 my-4 d-print-none"
/> />
@ -89,6 +89,7 @@ import {
ref, ref,
onMounted, onMounted,
onUnmounted, onUnmounted,
useRoute,
} from "@nuxtjs/composition-api"; } from "@nuxtjs/composition-api";
import { invoke, until, useWakeLock } from "@vueuse/core"; import { invoke, until, useWakeLock } from "@vueuse/core";
import RecipePageEditorToolbar from "./RecipePageParts/RecipePageEditorToolbar.vue"; import RecipePageEditorToolbar from "./RecipePageParts/RecipePageEditorToolbar.vue";
@ -101,6 +102,7 @@ import RecipePageOrganizers from "./RecipePageParts/RecipePageOrganizers.vue";
import RecipePageScale from "./RecipePageParts/RecipePageScale.vue"; import RecipePageScale from "./RecipePageParts/RecipePageScale.vue";
import RecipePageTitleContent from "./RecipePageParts/RecipePageTitleContent.vue"; import RecipePageTitleContent from "./RecipePageParts/RecipePageTitleContent.vue";
import RecipePageComments from "./RecipePageParts/RecipePageComments.vue"; import RecipePageComments from "./RecipePageParts/RecipePageComments.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import RecipePrintContainer from "~/components/Domain/Recipe/RecipePrintContainer.vue"; import RecipePrintContainer from "~/components/Domain/Recipe/RecipePrintContainer.vue";
import { EditorMode, PageMode, usePageState, usePageUser } from "~/composables/recipe-page/shared-state"; import { EditorMode, PageMode, usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
import { NoUndefinedField } from "~/lib/api/types/non-generated"; import { NoUndefinedField } from "~/lib/api/types/non-generated";
@ -140,6 +142,11 @@ export default defineComponent({
}, },
}, },
setup(props) { setup(props) {
const { $auth, $vuetify } = useContext();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const { isOwnGroup } = useLoggedInState();
const router = useRouter(); const router = useRouter();
const api = useUserApi(); const api = useUserApi();
const { pageMode, editMode, setMode, isEditForm, isEditJSON, isCookMode, isEditMode, toggleCookMode } = const { pageMode, editMode, setMode, isEditForm, isEditJSON, isCookMode, isEditMode, toggleCookMode } =
@ -226,21 +233,20 @@ export default defineComponent({
const { data } = await api.recipes.updateOne(props.recipe.slug, props.recipe); const { data } = await api.recipes.updateOne(props.recipe.slug, props.recipe);
setMode(PageMode.VIEW); setMode(PageMode.VIEW);
if (data?.slug) { if (data?.slug) {
router.push("/recipe/" + data.slug); router.push(`/g/${groupSlug.value}/r/` + data.slug);
} }
} }
async function deleteRecipe() { async function deleteRecipe() {
const { data } = await api.recipes.deleteOne(props.recipe.slug); const { data } = await api.recipes.deleteOne(props.recipe.slug);
if (data?.slug) { if (data?.slug) {
router.push("/"); router.push(`/g/${groupSlug.value}`);
} }
} }
/** ============================================================= /** =============================================================
* View Preferences * View Preferences
*/ */
const { $vuetify } = useContext();
const landscape = computed(() => { const landscape = computed(() => {
const preferLandscape = props.recipe.settings.landscapeView; const preferLandscape = props.recipe.settings.landscapeView;
@ -283,6 +289,7 @@ export default defineComponent({
return { return {
user, user,
isOwnGroup,
api, api,
scale: ref(1), scale: ref(1),
EDITOR_OPTIONS, EDITOR_OPTIONS,

View File

@ -10,7 +10,7 @@
<v-divider class="my-2"></v-divider> <v-divider class="my-2"></v-divider>
<SafeMarkdown :source="recipe.description" /> <SafeMarkdown :source="recipe.description" />
<v-divider></v-divider> <v-divider></v-divider>
<div v-if="user.id" class="d-flex justify-center mt-5"> <div v-if="isOwnGroup" class="d-flex justify-center mt-5">
<RecipeLastMade <RecipeLastMade
v-model="recipe.lastMade" v-model="recipe.lastMade"
:recipe="recipe" :recipe="recipe"
@ -45,9 +45,9 @@
:recipe="recipe" :recipe="recipe"
:slug="recipe.slug" :slug="recipe.slug"
:recipe-scale="recipeScale" :recipe-scale="recipeScale"
:locked="user.id !== recipe.userId && recipe.settings.locked" :locked="isOwnGroup && user.id !== recipe.userId && recipe.settings.locked"
:name="recipe.name" :name="recipe.name"
:logged-in="$auth.loggedIn" :logged-in="isOwnGroup"
:open="isEditMode" :open="isEditMode"
:recipe-id="recipe.id" :recipe-id="recipe.id"
:show-ocr-button="recipe.isOcrRecipe" :show-ocr-button="recipe.isOcrRecipe"
@ -64,7 +64,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, useContext, computed, ref, watch, useRouter } from "@nuxtjs/composition-api"; import { defineComponent, useContext, computed, ref, watch, useRouter, useRoute } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue"; import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue"; import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue";
import RecipeActionMenu from "~/components/Domain/Recipe/RecipeActionMenu.vue"; import RecipeActionMenu from "~/components/Domain/Recipe/RecipeActionMenu.vue";
@ -95,17 +96,20 @@ export default defineComponent({
}, },
}, },
setup(props) { setup(props) {
const { $auth, $vuetify } = useContext();
const { recipeImage } = useStaticRoutes(); const { recipeImage } = useStaticRoutes();
const { imageKey, pageMode, editMode, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug); const { imageKey, pageMode, editMode, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
const { user } = usePageUser(); const { user } = usePageUser();
const { isOwnGroup } = useLoggedInState();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const router = useRouter(); const router = useRouter();
function printRecipe() { function printRecipe() {
window.print(); window.print();
} }
const { $vuetify } = useContext();
const hideImage = ref(false); const hideImage = ref(false);
const imageHeight = computed(() => { const imageHeight = computed(() => {
return $vuetify.breakpoint.xs ? "200" : "400"; return $vuetify.breakpoint.xs ? "200" : "400";
@ -116,7 +120,7 @@ export default defineComponent({
}); });
function goToOcrEditor() { function goToOcrEditor() {
router.push("/recipe/" + props.recipe.slug + "/ocr-editor"); router.push(`/g/${groupSlug.value}/r/${props.recipe.slug}/ocr-editor`);
} }
watch( watch(
@ -127,6 +131,7 @@ export default defineComponent({
); );
return { return {
isOwnGroup,
setMode, setMode,
toggleEditMode, toggleEditMode,
recipeImage, recipeImage,

View File

@ -34,7 +34,7 @@
class="mb-1" class="mb-1"
:disabled="recipe.settings.disableAmount || hasFoodOrUnit" :disabled="recipe.settings.disableAmount || hasFoodOrUnit"
color="accent" color="accent"
:to="`${recipe.slug}/ingredient-parser`" :to="`/g/${groupSlug}/${recipe.slug}/ingredient-parser`"
v-bind="attrs" v-bind="attrs"
> >
<template #icon> <template #icon>
@ -54,7 +54,7 @@
<script lang="ts"> <script lang="ts">
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import { computed, defineComponent, ref, useContext } from "@nuxtjs/composition-api"; import { computed, defineComponent, ref, useContext, useRoute } from "@nuxtjs/composition-api";
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state"; import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
import { NoUndefinedField } from "~/lib/api/types/non-generated"; import { NoUndefinedField } from "~/lib/api/types/non-generated";
import { Recipe } from "~/lib/api/types/recipe"; import { Recipe } from "~/lib/api/types/recipe";
@ -76,10 +76,13 @@ export default defineComponent({
setup(props) { setup(props) {
const { user } = usePageUser(); const { user } = usePageUser();
const { imageKey } = usePageState(props.recipe.slug); const { imageKey } = usePageState(props.recipe.slug);
const { i18n } = useContext(); const { $auth, i18n } = useContext();
const drag = ref(false); const drag = ref(false);
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const hasFoodOrUnit = computed(() => { const hasFoodOrUnit = computed(() => {
if (!props.recipe) { if (!props.recipe) {
return false; return false;
@ -139,6 +142,7 @@ export default defineComponent({
return { return {
user, user,
groupSlug,
addIngredient, addIngredient,
parserToolTip, parserToolTip,
hasFoodOrUnit, hasFoodOrUnit,

View File

@ -26,6 +26,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"; import { defineComponent } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state"; import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
import { useToolStore } from "~/composables/store"; import { useToolStore } from "~/composables/store";
import { NoUndefinedField } from "~/lib/api/types/non-generated"; import { NoUndefinedField } from "~/lib/api/types/non-generated";
@ -47,12 +48,14 @@ export default defineComponent({
}, },
}, },
setup(props) { setup(props) {
const toolStore = useToolStore(); const { isOwnGroup } = useLoggedInState();
const toolStore = isOwnGroup.value ? useToolStore() : null;
const { user } = usePageUser(); const { user } = usePageUser();
const { isEditMode } = usePageState(props.recipe.slug); const { isEditMode } = usePageState(props.recipe.slug);
function updateTool(index: number) { function updateTool(index: number) {
if (user.id) { if (user.id && toolStore) {
toolStore.actions.updateOne(props.recipe.tools[index]); toolStore.actions.updateOne(props.recipe.tools[index]);
} else { } else {
console.log("no user, skipping server update"); console.log("no user, skipping server update");

View File

@ -5,7 +5,7 @@
{{ recipe.name }} {{ recipe.name }}
</v-card-title> </v-card-title>
<SafeMarkdown :source="recipe.description" /> <SafeMarkdown :source="recipe.description" />
<div v-if="user.id" class="pb-2 d-flex justify-center flex-wrap"> <div v-if="isOwnGroup" class="pb-2 d-flex justify-center flex-wrap">
<RecipeLastMade <RecipeLastMade
v-model="recipe.lastMade" v-model="recipe.lastMade"
:recipe="recipe" :recipe="recipe"
@ -50,6 +50,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"; import { defineComponent } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state"; import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
import { validators } from "~/composables/use-validators"; import { validators } from "~/composables/use-validators";
import { NoUndefinedField } from "~/lib/api/types/non-generated"; import { NoUndefinedField } from "~/lib/api/types/non-generated";
@ -77,12 +78,14 @@ export default defineComponent({
setup(props) { setup(props) {
const { user } = usePageUser(); const { user } = usePageUser();
const { imageKey, isEditMode } = usePageState(props.recipe.slug); const { imageKey, isEditMode } = usePageState(props.recipe.slug);
const { isOwnGroup } = useLoggedInState();
return { return {
user, user,
imageKey, imageKey,
validators, validators,
isEditMode, isEditMode,
isOwnGroup,
}; };
}, },
}); });

View File

@ -2,7 +2,7 @@
<div @click.prevent> <div @click.prevent>
<v-rating <v-rating
v-model="rating" v-model="rating"
:readonly="!loggedIn" :readonly="!isOwnGroup"
color="secondary" color="secondary"
background-color="secondary lighten-3" background-color="secondary lighten-3"
length="5" length="5"
@ -18,7 +18,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, ref, useContext } from "@nuxtjs/composition-api"; import { defineComponent, ref } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
export default defineComponent({ export default defineComponent({
props: { props: {
@ -45,10 +46,7 @@ export default defineComponent({
}, },
}, },
setup(props, context) { setup(props, context) {
const { $auth } = useContext(); const { isOwnGroup } = useLoggedInState();
const loggedIn = computed(() => {
return $auth.loggedIn;
});
const rating = ref(props.value); const rating = ref(props.value);
@ -65,7 +63,7 @@ export default defineComponent({
context.emit("input", val); context.emit("input", val);
} }
return { loggedIn, rating, updateRating }; return { isOwnGroup, rating, updateRating };
}, },
}); });
</script> </script>

View File

@ -13,7 +13,7 @@
</template> </template>
<v-card <v-card
hover hover
:to="$listeners.selected || !recipe ? undefined : `/recipe/${recipe.slug}`" :to="$listeners.selected || !recipe ? undefined : `/g/${groupSlug}/r/${recipe.slug}`"
class="elevation-12" class="elevation-12"
@click="$emit('selected')" @click="$emit('selected')"
> >
@ -95,7 +95,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, ref, useContext } from "@nuxtjs/composition-api"; import { computed, defineComponent, ref, useContext, useRoute } from "@nuxtjs/composition-api";
import RecipeCardMobile from "./RecipeCardMobile.vue"; import RecipeCardMobile from "./RecipeCardMobile.vue";
import RecipeTimelineContextMenu from "./RecipeTimelineContextMenu.vue"; import RecipeTimelineContextMenu from "./RecipeTimelineContextMenu.vue";
import { useStaticRoutes } from "~/composables/api"; import { useStaticRoutes } from "~/composables/api";
@ -121,10 +121,13 @@ export default defineComponent({
}, },
setup(props) { setup(props) {
const { $globals, $vuetify } = useContext(); const { $auth, $globals, $vuetify } = useContext();
const { recipeTimelineEventImage } = useStaticRoutes(); const { recipeTimelineEventImage } = useStaticRoutes();
const timelineEvents = ref([] as RecipeTimelineEventOut[]); const timelineEvents = ref([] as RecipeTimelineEventOut[]);
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const useMobileFormat = computed(() => { const useMobileFormat = computed(() => {
return $vuetify.breakpoint.smAndDown; return $vuetify.breakpoint.smAndDown;
}); });
@ -187,6 +190,7 @@ export default defineComponent({
return { return {
attrs, attrs,
groupSlug,
icon, icon,
eventImageUrl, eventImageUrl,
hideImage, hideImage,

View File

@ -6,14 +6,14 @@
v-model="sidebar" v-model="sidebar"
absolute absolute
:top-link="topLinks" :top-link="topLinks"
:secondary-header="$t('sidebar.cookbooks')" :secondary-header="cookbookLinks.length ? $tc('sidebar.cookbooks') : undefined"
:secondary-header-link="loggedIn ? '/group/cookbooks' : undefined" :secondary-header-link="isOwnGroup && cookbookLinks.length ? `/g/${groupSlug}/cookbooks` : undefined"
:secondary-links="cookbookLinks || []" :secondary-links="cookbookLinks || []"
:bottom-links="isAdmin ? bottomLink : []" :bottom-links="isAdmin ? bottomLinks : []"
> >
<v-menu offset-y nudge-bottom="5" close-delay="50" nudge-right="15"> <v-menu offset-y nudge-bottom="5" close-delay="50" nudge-right="15">
<template #activator="{ on, attrs }"> <template #activator="{ on, attrs }">
<v-btn v-if="loggedIn" rounded large class="ml-2 mt-3" v-bind="attrs" v-on="on"> <v-btn v-if="isOwnGroup" rounded large class="ml-2 mt-3" v-bind="attrs" v-on="on">
<v-icon left large color="primary"> <v-icon left large color="primary">
{{ $globals.icons.createAlt }} {{ $globals.icons.createAlt }}
</v-icon> </v-icon>
@ -23,7 +23,7 @@
<v-list dense class="my-0 py-0"> <v-list dense class="my-0 py-0">
<template v-for="(item, index) in createLinks"> <template v-for="(item, index) in createLinks">
<v-divider v-if="item.insertDivider" :key="index" class="mx-2"></v-divider> <v-divider v-if="item.insertDivider" :key="index" class="mx-2"></v-divider>
<v-list-item v-if="!item.restricted || loggedIn" :key="item.title" :to="item.to" exact> <v-list-item v-if="!item.restricted || isOwnGroup" :key="item.title" :to="item.to" exact>
<v-list-item-avatar> <v-list-item-avatar>
<v-icon> <v-icon>
{{ item.icon }} {{ item.icon }}
@ -64,7 +64,7 @@
</template> </template>
</AppSidebar> </AppSidebar>
<AppHeader :menu="loggedIn"> <AppHeader>
<v-btn icon @click.stop="sidebar = !sidebar"> <v-btn icon @click.stop="sidebar = !sidebar">
<v-icon> {{ $globals.icons.menu }}</v-icon> <v-icon> {{ $globals.icons.menu }}</v-icon>
</v-btn> </v-btn>
@ -79,6 +79,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, onMounted, ref, useContext, useRoute } from "@nuxtjs/composition-api"; import { computed, defineComponent, onMounted, ref, useContext, useRoute } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import AppHeader from "@/components/Layout/LayoutParts/AppHeader.vue"; import AppHeader from "@/components/Layout/LayoutParts/AppHeader.vue";
import AppSidebar from "@/components/Layout/LayoutParts/AppSidebar.vue"; import AppSidebar from "@/components/Layout/LayoutParts/AppSidebar.vue";
import { SidebarLinks } from "~/types/application-types"; import { SidebarLinks } from "~/types/application-types";
@ -91,13 +92,12 @@
components: { AppHeader, AppSidebar, LanguageDialog, TheSnackbar }, components: { AppHeader, AppSidebar, LanguageDialog, TheSnackbar },
setup() { setup() {
const { $globals, $auth, $vuetify, i18n } = useContext(); const { $globals, $auth, $vuetify, i18n } = useContext();
const { isOwnGroup } = useLoggedInState();
const isAdmin = computed(() => $auth.user?.admin); const isAdmin = computed(() => $auth.user?.admin);
const loggedIn = computed(() => $auth.loggedIn);
const route = useRoute(); const route = useRoute();
const groupSlug = route.value.params.groupSlug; const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const { cookbooks } = loggedIn.value ? useCookbooks() : usePublicCookbooks(groupSlug); const { cookbooks } = isOwnGroup.value ? useCookbooks() : usePublicCookbooks(groupSlug.value || "");
const toggleDark = useToggleDarkMode(); const toggleDark = useToggleDarkMode();
@ -115,7 +115,7 @@
return { return {
icon: $globals.icons.pages, icon: $globals.icons.pages,
title: cookbook.name, title: cookbook.name,
to: loggedIn.value ? `/cookbooks/${cookbook.slug as string}` : `/explore/cookbooks/${groupSlug}/${cookbook.slug as string}`, to: `/g/${groupSlug.value}/cookbooks/${cookbook.slug as string}`,
}; };
}); });
}); });
@ -129,13 +129,13 @@
restricted: boolean; restricted: boolean;
} }
const createLinks: Link[] = [ const createLinks = computed<Link[]>(() => [
{ {
insertDivider: false, insertDivider: false,
icon: $globals.icons.link, icon: $globals.icons.link,
title: i18n.tc("general.import"), title: i18n.tc("general.import"),
subtitle: i18n.tc("new-recipe.import-by-url"), subtitle: i18n.tc("new-recipe.import-by-url"),
to: "/recipe/create/url", to: `/g/${groupSlug.value}/r/create/url`,
restricted: true, restricted: true,
}, },
{ {
@ -143,7 +143,7 @@
icon: $globals.icons.edit, icon: $globals.icons.edit,
title: i18n.tc("general.create"), title: i18n.tc("general.create"),
subtitle: i18n.tc("new-recipe.create-manually"), subtitle: i18n.tc("new-recipe.create-manually"),
to: "/recipe/create/new", to: `/g/${groupSlug.value}/r/create/new`,
restricted: true, restricted: true,
}, },
{ {
@ -151,24 +151,24 @@
icon: $globals.icons.pages, icon: $globals.icons.pages,
title: i18n.tc("sidebar.cookbook"), title: i18n.tc("sidebar.cookbook"),
subtitle: i18n.tc("sidebar.create-cookbook"), subtitle: i18n.tc("sidebar.create-cookbook"),
to: "/group/cookbooks", to: `/g/${groupSlug.value}/cookbooks`,
restricted: true, restricted: true,
}, },
]; ]);
const bottomLinks: SidebarLinks = [ const bottomLinks = computed<SidebarLinks>(() => [
{ {
icon: $globals.icons.cog, icon: $globals.icons.cog,
title: i18n.tc("general.settings"), title: i18n.tc("general.settings"),
to: "/admin/site-settings", to: "/admin/site-settings",
restricted: true, restricted: true,
}, },
]; ]);
const topLinks: SidebarLinks = [ const topLinks = computed<SidebarLinks>(() => [
{ {
icon: $globals.icons.search, icon: $globals.icons.search,
to: "/", to: `/g/${groupSlug.value}`,
title: i18n.tc("sidebar.search"), title: i18n.tc("sidebar.search"),
restricted: true, restricted: true,
}, },
@ -187,30 +187,41 @@
{ {
icon: $globals.icons.timelineText, icon: $globals.icons.timelineText,
title: i18n.tc("recipe.timeline"), title: i18n.tc("recipe.timeline"),
to: "/group/timeline", to: `/g/${groupSlug.value}/recipes/timeline`,
restricted: true, restricted: true,
}, },
{ {
icon: $globals.icons.categories, icon: $globals.icons.categories,
to: "/recipes/categories", to: `/g/${groupSlug.value}/recipes/categories`,
title: i18n.tc("sidebar.categories"), title: i18n.tc("sidebar.categories"),
restricted: true, restricted: true,
}, },
{ {
icon: $globals.icons.tags, icon: $globals.icons.tags,
to: "/recipes/tags", to: `/g/${groupSlug.value}/recipes/tags`,
title: i18n.tc("sidebar.tags"), title: i18n.tc("sidebar.tags"),
restricted: true, restricted: true,
}, },
{ {
icon: $globals.icons.potSteam, icon: $globals.icons.potSteam,
to: "/recipes/tools", to: `/g/${groupSlug.value}/recipes/tools`,
title: i18n.tc("tool.tools"), title: i18n.tc("tool.tools"),
restricted: true, restricted: true,
}, },
]; ]);
return { cookbookLinks, createLinks, bottomLink: bottomLinks, topLinks, isAdmin, loggedIn, languageDialog, toggleDark, sidebar }; return {
groupSlug,
cookbookLinks,
createLinks,
bottomLinks,
topLinks,
isAdmin,
isOwnGroup,
languageDialog,
toggleDark,
sidebar,
};
}, },
}); });
</script> </script>

View File

@ -35,7 +35,7 @@
<v-btn v-else icon @click="activateSearch"> <v-btn v-else icon @click="activateSearch">
<v-icon> {{ $globals.icons.search }}</v-icon> <v-icon> {{ $globals.icons.search }}</v-icon>
</v-btn> </v-btn>
<v-btn v-if="$auth.loggedIn" :text="$vuetify.breakpoint.smAndUp" :icon="$vuetify.breakpoint.xs" @click="$auth.logout()"> <v-btn v-if="loggedIn" :text="$vuetify.breakpoint.smAndUp" :icon="$vuetify.breakpoint.xs" @click="$auth.logout()">
<v-icon :left="$vuetify.breakpoint.smAndUp">{{ $globals.icons.logout }}</v-icon> <v-icon :left="$vuetify.breakpoint.smAndUp">{{ $globals.icons.logout }}</v-icon>
{{ $vuetify.breakpoint.smAndUp ? $t("user.logout") : "" }} {{ $vuetify.breakpoint.smAndUp ? $t("user.logout") : "" }}
</v-btn> </v-btn>
@ -49,6 +49,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, onBeforeUnmount, onMounted, ref, useContext, useRoute } from "@nuxtjs/composition-api"; import { computed, defineComponent, onBeforeUnmount, onMounted, ref, useContext, useRoute } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import RecipeDialogSearch from "~/components/Domain/Recipe/RecipeDialogSearch.vue"; import RecipeDialogSearch from "~/components/Domain/Recipe/RecipeDialogSearch.vue";
export default defineComponent({ export default defineComponent({
@ -61,14 +62,11 @@ export default defineComponent({
}, },
setup() { setup() {
const { $auth } = useContext(); const { $auth } = useContext();
const { loggedIn } = useLoggedInState();
const route = useRoute(); const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const loggedIn = computed(() => { const routerLink = computed(() => groupSlug.value ? `/g/${groupSlug.value}` : "/");
return $auth.loggedIn;
});
const groupSlug = route.value.params.groupSlug;
const routerLink = !loggedIn.value && groupSlug ? `/explore/recipes/${groupSlug}` : "/"
const domSearchDialog = ref<InstanceType<typeof RecipeDialogSearch> | null>(null); const domSearchDialog = ref<InstanceType<typeof RecipeDialogSearch> | null>(null);
function activateSearch() { function activateSearch() {
@ -95,6 +93,7 @@ export default defineComponent({
activateSearch, activateSearch,
domSearchDialog, domSearchDialog,
routerLink, routerLink,
loggedIn,
}; };
}, },
}); });

View File

@ -1,14 +1,14 @@
<template> <template>
<v-navigation-drawer v-model="drawer" class="d-flex flex-column d-print-none" clipped app width="240px"> <v-navigation-drawer v-model="drawer" class="d-flex flex-column d-print-none" clipped app width="240px">
<!-- User Profile --> <!-- User Profile -->
<template v-if="$auth.user"> <template v-if="loggedIn">
<v-list-item two-line to="/user/profile" exact> <v-list-item two-line :to="userProfileLink" exact>
<UserAvatar list :user-id="$auth.user.id" /> <UserAvatar list :user-id="$auth.user.id" />
<v-list-item-content> <v-list-item-content>
<v-list-item-title class="pr-2"> {{ $auth.user.fullName }}</v-list-item-title> <v-list-item-title class="pr-2"> {{ $auth.user.fullName }}</v-list-item-title>
<v-list-item-subtitle> <v-list-item-subtitle>
<v-btn class="px-2 pa-0" text :to="`/user/${$auth.user.id}/favorites`" small> <v-btn v-if="isOwnGroup" class="px-2 pa-0" text :to="userFavoritesLink" small>
<v-icon left small> <v-icon left small>
{{ $globals.icons.heart }} {{ $globals.icons.heart }}
</v-icon> </v-icon>
@ -26,7 +26,7 @@
<template v-if="topLink"> <template v-if="topLink">
<v-list nav dense> <v-list nav dense>
<template v-for="nav in topLink"> <template v-for="nav in topLink">
<div v-if="!nav.restricted || loggedIn" :key="nav.title"> <div v-if="!nav.restricted || isOwnGroup" :key="nav.title">
<!-- Multi Items --> <!-- Multi Items -->
<v-list-group <v-list-group
v-if="nav.children" v-if="nav.children"
@ -69,13 +69,20 @@
<!-- Secondary Links --> <!-- Secondary Links -->
<template v-if="secondaryLinks"> <template v-if="secondaryLinks">
<v-subheader v-if="secondaryHeader" :to="secondaryHeaderLink" class="pb-0"> <router-link v-if="secondaryHeader && secondaryHeaderLink" :to="secondaryHeaderLink" style="text-decoration: none;">
{{ secondaryHeader }} <v-subheader :to="secondaryHeaderLink" class="pb-0">
</v-subheader> {{ secondaryHeader }}
<v-divider></v-divider> </v-subheader>
</router-link>
<div v-else-if="secondaryHeader">
<v-subheader :to="secondaryHeaderLink" class="pb-0">
{{ secondaryHeader }}
</v-subheader>
</div>
<v-divider v-if="secondaryHeader"></v-divider>
<v-list nav dense exact> <v-list nav dense exact>
<template v-for="nav in secondaryLinks"> <template v-for="nav in secondaryLinks">
<div v-if="!nav.restricted || loggedIn" :key="nav.title"> <div v-if="!nav.restricted || isOwnGroup" :key="nav.title">
<!-- Multi Items --> <!-- Multi Items -->
<v-list-group <v-list-group
v-if="nav.children" v-if="nav.children"
@ -116,7 +123,7 @@
<v-list nav dense> <v-list nav dense>
<v-list-item-group v-model="bottomSelected" color="primary"> <v-list-item-group v-model="bottomSelected" color="primary">
<template v-for="nav in bottomLinks"> <template v-for="nav in bottomLinks">
<div v-if="!nav.restricted || loggedIn" :key="nav.title"> <div v-if="!nav.restricted || isOwnGroup" :key="nav.title">
<v-list-item <v-list-item
:key="nav.title" :key="nav.title"
exact exact
@ -141,6 +148,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api"; import { computed, defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { SidebarLinks } from "~/types/application-types"; import { SidebarLinks } from "~/types/application-types";
import UserAvatar from "~/components/Domain/User/UserAvatar.vue"; import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
@ -198,7 +206,10 @@ export default defineComponent({
}); });
const { $auth } = useContext(); const { $auth } = useContext();
const loggedIn = computed(() => $auth.loggedIn); const { loggedIn, isOwnGroup } = useLoggedInState();
const userFavoritesLink = computed(() => $auth.user ? `/user/${$auth.user.id}/favorites` : undefined);
const userProfileLink = computed(() => $auth.user ? "/user/profile" : undefined);
const state = reactive({ const state = reactive({
dropDowns: {}, dropDowns: {},
@ -210,8 +221,11 @@ export default defineComponent({
return { return {
...toRefs(state), ...toRefs(state),
userFavoritesLink,
userProfileLink,
drawer, drawer,
loggedIn, loggedIn,
isOwnGroup,
}; };
}, },
}); });

View File

@ -37,8 +37,8 @@ export default defineComponent({
}, },
setup(_, context) { setup(_, context) {
const router = useRouter();
const { i18n } = useContext(); const { i18n } = useContext();
const router = useRouter();
const headers = [ const headers = [
{ text: i18n.t("category.category"), value: "category" }, { text: i18n.t("category.category"), value: "category" },
@ -49,7 +49,7 @@ export default defineComponent({
]; ];
function handleRowClick(item: ReportSummary) { function handleRowClick(item: ReportSummary) {
router.push("/group/reports/" + item.id); router.push(`/group/reports/${item.id}`);
} }
function capitalize(str: string) { function capitalize(str: string) {

View File

@ -151,12 +151,12 @@ export function usePageUser(): { user: UserOut } {
id: "", id: "",
group: "", group: "",
groupId: "", groupId: "",
groupSlug: "",
cacheKey: "", cacheKey: "",
email: "", email: "",
}, },
}; };
} }
// @ts-expect-error - We know that the API always returns a UserOut, but I'm unsure how to type the $auth to know what type user is return { user: $auth.user };
return { user: $auth.user as UserOut };
} }

View File

@ -1,6 +1,7 @@
import { Ref, ref } from "@nuxtjs/composition-api"; import { Ref, ref } from "@nuxtjs/composition-api";
import { watchDebounced } from "@vueuse/core"; import { watchDebounced } from "@vueuse/core";
import { UserApi } from "~/lib/api"; import { UserApi } from "~/lib/api";
import { ExploreApi } from "~/lib/api/public/explore";
import { Recipe } from "~/lib/api/types/recipe"; import { Recipe } from "~/lib/api/types/recipe";
export interface UseRecipeSearchReturn { export interface UseRecipeSearchReturn {
@ -17,7 +18,7 @@ export interface UseRecipeSearchReturn {
* on the query. Useful for searchable list views. For advanced * on the query. Useful for searchable list views. For advanced
* search, use the `useRecipeQuery` composable. * search, use the `useRecipeQuery` composable.
*/ */
export function useRecipeSearch(api: UserApi): UseRecipeSearchReturn { export function useRecipeSearch(api: UserApi | ExploreApi): UseRecipeSearchReturn {
const query = ref(""); const query = ref("");
const error = ref(""); const error = ref("");
const loading = ref(false); const loading = ref(false);

View File

@ -1,4 +1,4 @@
import { useAsync, ref } from "@nuxtjs/composition-api"; import { useAsync, useRouter, ref } from "@nuxtjs/composition-api";
import { useAsyncKey } from "../use-utils"; import { useAsyncKey } from "../use-utils";
import { usePublicExploreApi } from "~/composables/api/api-client"; import { usePublicExploreApi } from "~/composables/api/api-client";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
@ -9,6 +9,8 @@ export const allRecipes = ref<Recipe[]>([]);
export const recentRecipes = ref<Recipe[]>([]); export const recentRecipes = ref<Recipe[]>([]);
export const useLazyRecipes = function (publicGroupSlug: string | null = null) { export const useLazyRecipes = function (publicGroupSlug: string | null = null) {
const router = useRouter();
// passing the group slug switches to using the public API // passing the group slug switches to using the public API
const api = publicGroupSlug ? usePublicExploreApi(publicGroupSlug).explore : useUserApi(); const api = publicGroupSlug ? usePublicExploreApi(publicGroupSlug).explore : useUserApi();
@ -23,7 +25,7 @@ export const useLazyRecipes = function (publicGroupSlug: string | null = null) {
queryFilter: string | null = null, queryFilter: string | null = null,
) { ) {
const { data } = await api.recipes.getAll(page, perPage, { const { data, error } = await api.recipes.getAll(page, perPage, {
orderBy, orderBy,
orderDirection, orderDirection,
paginationSeed: query?._searchSeed, // propagate searchSeed to stabilize random order pagination paginationSeed: query?._searchSeed, // propagate searchSeed to stabilize random order pagination
@ -40,6 +42,11 @@ export const useLazyRecipes = function (publicGroupSlug: string | null = null) {
requireAllFoods: query?.requireAllFoods, requireAllFoods: query?.requireAllFoods,
queryFilter, queryFilter,
}); });
if (error?.response?.status === 404) {
router.push("/login");
}
return data ? data.items : []; return data ? data.items : [];
} }

View File

@ -0,0 +1,17 @@
import { computed, useContext, useRoute } from "@nuxtjs/composition-api";
export const useLoggedInState = function () {
const { $auth } = useContext();
const route = useRoute();
const loggedIn = computed(() => $auth.loggedIn);
const isOwnGroup = computed(() => {
if (!route.value.params.groupSlug) {
return loggedIn.value;
} else {
return loggedIn.value && $auth.user?.groupSlug === route.value.params.groupSlug;
}
});
return { loggedIn, isOwnGroup };
}

View File

@ -9,6 +9,5 @@ import DefaultLayout from "@/components/Layout/DefaultLayout.vue";
export default defineComponent({ export default defineComponent({
components: { DefaultLayout }, components: { DefaultLayout },
middleware: "auth",
}); });
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div v-if="ready">
<v-card-title> <v-card-title>
<slot> <slot>
<h1 class="mx-auto">{{ $t("page.404-page-not-found") }}</h1> <h1 class="mx-auto">{{ $t("page.404-page-not-found") }}</h1>
@ -28,7 +28,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, useContext, useMeta } from "@nuxtjs/composition-api"; import { defineComponent, ref, useContext, useMeta, useRoute, useRouter } from "@nuxtjs/composition-api";
export default defineComponent({ export default defineComponent({
layout: "basic", layout: "basic",
@ -39,7 +39,48 @@ export default defineComponent({
}, },
}, },
setup(props) { setup(props) {
const { $globals, i18n } = useContext(); const { $auth, $globals, i18n } = useContext();
const ready = ref(false);
const route = useRoute();
const router = useRouter();
async function insertGroupSlugIntoRoute() {
const groupSlug = ref($auth.user?.groupSlug);
if (!groupSlug.value) {
return;
}
let replaceRoute = false;
let routeVal = route.value.fullPath || "/";
if (routeVal[0] !== "/") {
routeVal = `/${routeVal}`;
}
// replace "recipe" in URL with "r"
if (routeVal.includes("/recipe/")) {
replaceRoute = true;
routeVal = routeVal.replace("/recipe/", "/r/");
}
// insert groupSlug into URL
const routeComponents = routeVal.split("/");
if (routeComponents.length < 2 || routeComponents[1].toLowerCase() !== "g") {
replaceRoute = true;
routeVal = `/g/${groupSlug.value}${routeVal}`;
}
if (replaceRoute) {
await router.replace(routeVal);
}
}
if (props.error.statusCode === 404) {
// see if adding the groupSlug fixes the error
insertGroupSlugIntoRoute().then(() => { ready.value = true });
} else {
ready.value = true;
}
useMeta({ useMeta({
title: title:
@ -54,6 +95,7 @@ export default defineComponent({
return { return {
buttons, buttons,
ready,
}; };
}, },
// Needed for useMeta // Needed for useMeta

View File

@ -1,13 +0,0 @@
<template>
<DefaultLayout />
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
import DefaultLayout from "@/components/Layout/DefaultLayout.vue";
export default defineComponent({
components: { DefaultLayout },
});
</script>

View File

@ -1,6 +1,8 @@
import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients"; import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
import { route } from "../../base";
import { Recipe } from "~/lib/api/types/recipe"; import { Recipe } from "~/lib/api/types/recipe";
import { ApiRequestInstance } from "~/lib/api/types/non-generated"; import { ApiRequestInstance, PaginationData } from "~/lib/api/types/non-generated";
import { RecipeSearchQuery } from "../../user/recipes/recipe";
const prefix = "/api"; const prefix = "/api";
@ -16,4 +18,8 @@ export class PublicRecipeApi extends BaseCRUDAPIReadOnly<Recipe> {
constructor(requests: ApiRequestInstance, private readonly groupSlug: string) { constructor(requests: ApiRequestInstance, private readonly groupSlug: string) {
super(requests); super(requests);
} }
async search(rsq: RecipeSearchQuery) {
return await this.requests.get<PaginationData<Recipe>>(route(routes.recipesGroupSlug(this.groupSlug), rsq));
}
} }

View File

@ -66,6 +66,7 @@ export interface UserOut {
canOrganize?: boolean; canOrganize?: boolean;
id: string; id: string;
groupId: string; groupId: string;
groupSlug: string;
tokens?: LongLiveTokenOut[]; tokens?: LongLiveTokenOut[];
cacheKey: string; cacheKey: string;
} }
@ -113,6 +114,7 @@ export interface PrivateUser {
canOrganize?: boolean; canOrganize?: boolean;
id: string; id: string;
groupId: string; groupId: string;
groupSlug: string;
tokens?: LongLiveTokenOut[]; tokens?: LongLiveTokenOut[];
cacheKey: string; cacheKey: string;
password: string; password: string;

View File

@ -342,7 +342,7 @@ export default {
background_color: "#FFFFFF", background_color: "#FFFFFF",
display: "standalone", display: "standalone",
share_target: { share_target: {
action: "/recipe/create/url", action: "/r/create/url",
method: "GET", method: "GET",
params: { params: {
/* title and url are not currently used in Mealie. If there are issues /* title and url are not currently used in Mealie. If there are issues

View File

@ -6,7 +6,7 @@
"dev": "nuxt", "dev": "nuxt",
"build": "nuxt build", "build": "nuxt build",
"start": "nuxt start", "start": "nuxt start",
"generate": "nuxt generate", "generate": "nuxt generate --spa",
"lint:js": "eslint --ext \".ts,.js,.vue\" --ignore-path .gitignore .", "lint:js": "eslint --ext \".ts,.js,.vue\" --ignore-path .gitignore .",
"lint": "yarn lint:js", "lint": "yarn lint:js",
"test": "vitest", "test": "vitest",

View File

@ -105,13 +105,13 @@
</section> </section>
</section> </section>
<v-container class="mt-4 d-flex justify-center text-center"> <v-container class="mt-4 d-flex justify-center text-center">
<nuxt-link to="/group/migrations"> {{ $t('recipe.looking-for-migrations') }} </nuxt-link> <nuxt-link :to="`/group/migrations`"> {{ $t('recipe.looking-for-migrations') }} </nuxt-link>
</v-container> </v-container>
</v-container> </v-container>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, ref, toRefs, useContext, onMounted } from "@nuxtjs/composition-api"; import { computed, defineComponent, reactive, ref, toRefs, useContext, onMounted, useRoute } from "@nuxtjs/composition-api";
import { useAdminApi } from "~/composables/api"; import { useAdminApi } from "~/composables/api";
import { AllBackups } from "~/lib/api/types/admin"; import { AllBackups } from "~/lib/api/types/admin";
@ -119,6 +119,8 @@ export default defineComponent({
layout: "admin", layout: "admin",
setup() { setup() {
const { i18n, $auth } = useContext(); const { i18n, $auth } = useContext();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const adminApi = useAdminApi(); const adminApi = useAdminApi();
const selected = ref(""); const selected = ref("");
@ -192,6 +194,7 @@ export default defineComponent({
onMounted(refreshBackups); onMounted(refreshBackups);
return { return {
groupSlug,
restoreBackup, restoreBackup,
selected, selected,
...toRefs(state), ...toRefs(state),

View File

@ -77,7 +77,7 @@ export default defineComponent({
const refUserDialog = ref(); const refUserDialog = ref();
const { $auth } = useContext(); const { $auth } = useContext();
const user = computed(() => $auth.user as UserOut | null); const user = computed(() => $auth.user);
const { $globals, i18n } = useContext(); const { $globals, i18n } = useContext();

View File

@ -1,23 +0,0 @@
<template>
<client-only>
<CookbookPage :group-slug="groupSlug" />
</client-only>
</template>
<script lang="ts">
import { defineComponent, useRoute } from "@nuxtjs/composition-api";
import CookbookPage from "@/components/Domain/Cookbook/CookbookPage.vue";
export default defineComponent({
components: { CookbookPage },
layout: "explore",
setup() {
const route = useRoute();
const groupSlug = route.value.params.groupSlug;
return {
groupSlug,
}
},
})
</script>

View File

@ -1,47 +0,0 @@
<template>
<div v-if="recipe">
<client-only>
<RecipePage :recipe="recipe" />
</client-only>
</div>
</template>
<script lang="ts">
import { defineComponent, useAsync, useMeta, useRoute, useRouter } from "@nuxtjs/composition-api";
import RecipePage from "~/components/Domain/Recipe/RecipePage/RecipePage.vue";
import { usePublicExploreApi } from "~/composables/api/api-client";
export default defineComponent({
components: { RecipePage },
layout: "explore",
setup() {
const route = useRoute();
const router = useRouter();
const groupSlug = route.value.params.groupSlug;
const recipeSlug = route.value.params.recipeSlug;
const api = usePublicExploreApi(groupSlug);
const { title } = useMeta();
const recipe = useAsync(async () => {
const { data, error } = await api.explore.recipes.getOne(recipeSlug);
if (error) {
console.error("error loading recipe -> ", error);
router.push("/");
}
if (data) {
title.value = data?.name || "";
}
return data;
});
return {
recipe,
};
},
head: {},
});
</script>

View File

@ -1,42 +0,0 @@
<template>
<div v-if="groupSlug">
<client-only>
<RecipeExplorerPage :group-slug="groupSlug" />
</client-only>
</div>
</template>
<script lang="ts">
import { defineComponent, useRoute, useRouter } from "@nuxtjs/composition-api";
import { invoke } from "@vueuse/core";
import RecipeExplorerPage from "~/components/Domain/Recipe/RecipeExplorerPage.vue";
import { usePublicExploreApi } from "~/composables/api/api-client";
export default defineComponent({
components: { RecipeExplorerPage },
layout: "explore",
setup() {
const route = useRoute();
const router = useRouter();
const groupSlug = route.value.params.groupSlug;
const api = usePublicExploreApi(groupSlug);
invoke(async () => {
if (!groupSlug) {
return;
}
// try to fetch one tag to make sure the group slug is valid
const { data } = await api.explore.tags.getAll(1, 1);
if (!data) {
// the group slug is invalid, so leave the page (this results in a 404)
router.push("/explore/recipes");
}
});
return {
groupSlug,
};
},
});
</script>

View File

@ -90,14 +90,22 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"; import { defineComponent, useRouter } from "@nuxtjs/composition-api";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import { useCookbooks } from "@/composables/use-group-cookbooks"; import { useCookbooks } from "@/composables/use-group-cookbooks";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue"; import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
export default defineComponent({ export default defineComponent({
components: { draggable, RecipeOrganizerSelector }, components: { draggable, RecipeOrganizerSelector },
setup() { setup() {
const { isOwnGroup, loggedIn } = useLoggedInState();
const router = useRouter();
if (!(loggedIn.value && isOwnGroup.value)) {
router.back();
}
const { cookbooks, actions } = useCookbooks(); const { cookbooks, actions } = useCookbooks();
return { return {

View File

@ -0,0 +1,14 @@
<template>
<div>
<RecipeExplorerPage />
</div>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
import RecipeExplorerPage from "~/components/Domain/Recipe/RecipeExplorerPage.vue";
export default defineComponent({
components: { RecipeExplorerPage },
});
</script>

View File

@ -0,0 +1,59 @@
<template>
<div>
<RecipePage v-if="recipe" :recipe="recipe" />
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, useAsync, useContext, useMeta, useRoute, useRouter } from "@nuxtjs/composition-api";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import RecipePage from "~/components/Domain/Recipe/RecipePage/RecipePage.vue";
import { usePublicExploreApi } from "~/composables/api/api-client";
import { useRecipe } from "~/composables/recipes";
import { Recipe } from "~/lib/api/types/recipe";
export default defineComponent({
components: { RecipePage },
setup() {
const { $auth } = useContext();
const { isOwnGroup } = useLoggedInState();
const route = useRoute();
const router = useRouter();
const slug = route.value.params.slug;
const { title } = useMeta();
let recipe = ref<Recipe | null>(null);
if (isOwnGroup.value) {
const { recipe: data } = useRecipe(slug);
recipe = data;
} else {
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "")
const api = usePublicExploreApi(groupSlug.value);
recipe = useAsync(async () => {
const { data, error } = await api.explore.recipes.getOne(slug);
if (error) {
console.error("error loading recipe -> ", error);
router.push(`/g/${groupSlug.value}`);
}
return data;
})
}
title.value = recipe.value?.name || "";
return {
recipe,
};
},
head() {
if (this.recipe) {
return {
title: this.recipe.name
}
}
}
});
</script>

View File

@ -94,7 +94,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, useRoute, useRouter } from "@nuxtjs/composition-api"; import { computed, defineComponent, ref, useContext, useRoute, useRouter } from "@nuxtjs/composition-api";
import { invoke, until } from "@vueuse/core"; import { invoke, until } from "@vueuse/core";
import { import {
CreateIngredientFood, CreateIngredientFood,
@ -124,9 +124,12 @@ export default defineComponent({
RecipeIngredientEditor, RecipeIngredientEditor,
}, },
setup() { setup() {
const { $auth } = useContext();
const panels = ref<number[]>([]); const panels = ref<number[]>([]);
const route = useRoute(); const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const router = useRouter(); const router = useRouter();
const slug = route.value.params.slug; const slug = route.value.params.slug;
const api = useUserApi(); const api = useUserApi();
@ -324,7 +327,7 @@ export default defineComponent({
const { response } = await api.recipes.updateOne(recipe.value.slug, recipe.value); const { response } = await api.recipes.updateOne(recipe.value.slug, recipe.value);
if (response?.status === 200) { if (response?.status === 200) {
router.push("/recipe/" + recipe.value.slug); router.push(`/g/${groupSlug.value}/r/${recipe.value.slug}`);
} }
} }

View File

@ -20,7 +20,7 @@
<AdvancedOnly> <AdvancedOnly>
<v-container class="d-flex justify-center align-center my-4"> <v-container class="d-flex justify-center align-center my-4">
<a to="/group/migrations"> {{ $t('recipe.looking-for-migrations') }}</a> <a :to="`/group/migrations`"> {{ $t('recipe.looking-for-migrations') }}</a>
</v-container> </v-container>
</AdvancedOnly> </AdvancedOnly>
</div> </div>
@ -34,7 +34,7 @@ import AdvancedOnly from "~/components/global/AdvancedOnly.vue";
export default defineComponent({ export default defineComponent({
components: { AdvancedOnly }, components: { AdvancedOnly },
setup() { setup() {
const { $globals, i18n } = useContext(); const { $auth, $globals, i18n } = useContext();
const subpages: MenuItem[] = [ const subpages: MenuItem[] = [
{ {
@ -71,10 +71,11 @@ export default defineComponent({
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const subpage = computed({ const subpage = computed({
set(subpage: string) { set(subpage: string) {
router.push({ path: `/recipe/create/${subpage}`, query: route.value.query }); router.push({ path: `/g/${groupSlug.value}/r/create/${subpage}`, query: route.value.query });
}, },
get() { get() {
return route.value.path.split("/").pop() ?? "url"; return route.value.path.split("/").pop() ?? "url";
@ -82,6 +83,7 @@ export default defineComponent({
}); });
return { return {
groupSlug,
subpages, subpages,
subpage, subpage,
}; };

View File

@ -10,7 +10,7 @@ export default defineComponent({
const router = useRouter(); const router = useRouter();
onMounted(() => { onMounted(() => {
// Force redirect to first valid page // Force redirect to first valid page
router.replace("/recipe/create/url"); router.replace("/r/create/url");
}); });
return {}; return {};
}, },

View File

@ -35,7 +35,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, toRefs, ref, useRouter } from "@nuxtjs/composition-api"; import { defineComponent, reactive, toRefs, ref, useContext, useRouter, computed, useRoute } from "@nuxtjs/composition-api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { validators } from "~/composables/use-validators"; import { validators } from "~/composables/use-validators";
@ -47,6 +47,10 @@ export default defineComponent({
error: false, error: false,
loading: false, loading: false,
}); });
const { $auth } = useContext();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const api = useUserApi(); const api = useUserApi();
const router = useRouter(); const router = useRouter();
@ -56,7 +60,7 @@ export default defineComponent({
state.loading = false; state.loading = false;
return; return;
} }
router.push(`/recipe/${response.data}?edit=${edit.toString()}`); router.push(`/g/${groupSlug.value}/r/${response.data}?edit=${edit.toString()}`);
} }
const newRecipeName = ref(""); const newRecipeName = ref("");

View File

@ -32,7 +32,7 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, toRefs, ref, useRouter } from "@nuxtjs/composition-api"; import { defineComponent, reactive, toRefs, ref, useRouter, computed, useContext, useRoute } from "@nuxtjs/composition-api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { validators } from "~/composables/use-validators"; import { validators } from "~/composables/use-validators";
@ -45,6 +45,10 @@ export default defineComponent({
loading: false, loading: false,
makeFileRecipeImage: false, makeFileRecipeImage: false,
}); });
const { $auth } = useContext();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const api = useUserApi(); const api = useUserApi();
const router = useRouter(); const router = useRouter();
@ -56,7 +60,7 @@ export default defineComponent({
state.loading = false; state.loading = false;
return; return;
} }
router.push(`/recipe/${response.data}/ocr-editor`); router.push(`/g/${groupSlug.value}/r/${response.data}/ocr-editor`);
} }
const domCreateByOcr = ref<VForm | null>(null); const domCreateByOcr = ref<VForm | null>(null);

View File

@ -69,6 +69,7 @@ import {
ref, ref,
useRouter, useRouter,
computed, computed,
useContext,
useRoute, useRoute,
onMounted, onMounted,
} from "@nuxtjs/composition-api"; } from "@nuxtjs/composition-api";
@ -85,8 +86,11 @@ export default defineComponent({
loading: false, loading: false,
}); });
const { $auth } = useContext();
const api = useUserApi(); const api = useUserApi();
const route = useRoute(); const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const router = useRouter(); const router = useRouter();
const tags = useTagStore(); const tags = useTagStore();
@ -99,7 +103,7 @@ export default defineComponent({
if (refreshTags) { if (refreshTags) {
tags.actions.refresh(); tags.actions.refresh();
} }
router.push(`/recipe/${response.data}?edit=${edit.toString()}`); router.push(`/g/${groupSlug.value}/r/${response.data}?edit=${edit.toString()}`);
} }
const recipeUrl = computed({ const recipeUrl = computed({

View File

@ -30,7 +30,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, toRefs, ref, useRouter } from "@nuxtjs/composition-api"; import { computed, defineComponent, reactive, toRefs, ref, useContext, useRoute, useRouter } from "@nuxtjs/composition-api";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { validators } from "~/composables/use-validators"; import { validators } from "~/composables/use-validators";
@ -41,6 +41,10 @@ export default defineComponent({
error: false, error: false,
loading: false, loading: false,
}); });
const { $auth } = useContext();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const api = useUserApi(); const api = useUserApi();
const router = useRouter(); const router = useRouter();
@ -50,7 +54,7 @@ export default defineComponent({
state.loading = false; state.loading = false;
return; return;
} }
router.push(`/recipe/${response.data}?edit=${edit.toString()}`); router.push(`/g/${groupSlug.value}/r/${response.data}?edit=${edit.toString()}`);
} }
const newRecipeZip = ref<File | null>(null); const newRecipeZip = ref<File | null>(null);

View File

@ -7,7 +7,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, useAsync, useMeta, useRoute, useRouter } from "@nuxtjs/composition-api"; import { computed, defineComponent, useAsync, useContext, useMeta, useRoute, useRouter } from "@nuxtjs/composition-api";
import RecipePage from "~/components/Domain/Recipe/RecipePage/RecipePage.vue"; import RecipePage from "~/components/Domain/Recipe/RecipePage/RecipePage.vue";
import { usePublicApi } from "~/composables/api/api-client"; import { usePublicApi } from "~/composables/api/api-client";
@ -15,7 +15,10 @@ export default defineComponent({
components: { RecipePage }, components: { RecipePage },
layout: "basic", layout: "basic",
setup() { setup() {
const { $auth } = useContext();
const route = useRoute(); const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const router = useRouter(); const router = useRouter();
const recipeId = route.value.params.id; const recipeId = route.value.params.id;
const api = usePublicApi(); const api = usePublicApi();
@ -27,7 +30,7 @@ export default defineComponent({
if (error) { if (error) {
console.error("error loading recipe -> ", error); console.error("error loading recipe -> ", error);
router.push("/"); router.push(`/g/${groupSlug.value}`);
} }
if (data) { if (data) {

View File

@ -46,7 +46,9 @@ export default defineComponent({
labels: i18n.tc("data-pages.labels.labels"), labels: i18n.tc("data-pages.labels.labels"),
}; };
const DATA_TYPE_OPTIONS = [ const route = useRoute();
const DATA_TYPE_OPTIONS = computed(() => [
{ {
text: i18n.t("general.recipes"), text: i18n.t("general.recipes"),
value: "new", value: "new",
@ -67,9 +69,7 @@ export default defineComponent({
value: "new", value: "new",
to: "/group/data/labels", to: "/group/data/labels",
}, },
]; ]);
const route = useRoute();
const buttonText = computed(() => { const buttonText = computed(() => {
const last = route.value.path.split("/").pop(); const last = route.value.path.split("/").pop();

View File

@ -26,10 +26,10 @@
<div class="d-flex align-center justify-space-between mb-2"> <div class="d-flex align-center justify-space-between mb-2">
<v-tabs> <v-tabs>
<v-tab to="/group/mealplan/planner/view">{{ $t('meal-plan.meal-planner') }}</v-tab> <v-tab :to="`/group/mealplan/planner/view`">{{ $t('meal-plan.meal-planner') }}</v-tab>
<v-tab to="/group/mealplan/planner/edit">{{ $t('general.edit') }}</v-tab> <v-tab :to="`/group/mealplan/planner/edit`">{{ $t('general.edit') }}</v-tab>
</v-tabs> </v-tabs>
<ButtonLink :icon="$globals.icons.calendar" to="/group/mealplan/settings" :text="$tc('general.settings')" /> <ButtonLink :icon="$globals.icons.calendar" :to="`/group/mealplan/settings`" :text="$tc('general.settings')" />
</div> </div>
<div> <div>

View File

@ -14,7 +14,7 @@
</template> </template>
</i18n> </i18n>
<v-container class="mt-1 px-0"> <v-container class="mt-1 px-0">
<nuxt-link class="text-center" to="/user/profile/edit"> {{ $t('group.looking-to-update-your-profile') }} </nuxt-link> <nuxt-link class="text-center" :to="`/user/profile/edit`"> {{ $t('group.looking-to-update-your-profile') }} </nuxt-link>
</v-container> </v-container>
</BasePageTitle> </BasePageTitle>
<v-data-table <v-data-table

View File

@ -1,29 +1,21 @@
<template> <template>
<div v-if="groupSlug"> <div></div>
<RecipeExplorerPage :group-slug="groupSlug" />
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api"; import { computed, defineComponent, useContext, useRouter } from "@nuxtjs/composition-api";
import { invoke } from "@vueuse/core";
import { useUserApi } from "~/composables/api/api-client";
import RecipeExplorerPage from "~/components/Domain/Recipe/RecipeExplorerPage.vue";
export default defineComponent({ export default defineComponent({
components: { RecipeExplorerPage }, layout: "blank",
setup() { setup() {
const api = useUserApi(); const { $auth } = useContext();
const groupSlug = ref<string>(); const router = useRouter();
const groupSlug = computed(() => $auth.user?.groupSlug);
invoke(async () => { if (groupSlug.value) {
const { data } = await api.users.getSelfGroup(); router.push(`/g/${groupSlug.value}`);
groupSlug.value = data?.slug; } else {
}); router.push("/login");
}
return { }
groupSlug,
};
},
}); });
</script> </script>

View File

@ -103,6 +103,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, useContext, computed, reactive, useRouter } from "@nuxtjs/composition-api"; import { defineComponent, ref, useContext, computed, reactive, useRouter } from "@nuxtjs/composition-api";
import { useDark, whenever } from "@vueuse/core"; import { useDark, whenever } from "@vueuse/core";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { useAppInfo } from "~/composables/api"; import { useAppInfo } from "~/composables/api";
import { usePasswordField } from "~/composables/use-passwords"; import { usePasswordField } from "~/composables/use-passwords";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
@ -115,11 +116,13 @@ export default defineComponent({
const router = useRouter(); const router = useRouter();
const { $auth, i18n } = useContext(); const { $auth, i18n } = useContext();
const { loggedIn } = useLoggedInState();
const groupSlug = computed(() => $auth.user?.groupSlug);
whenever( whenever(
() => $auth.loggedIn, () => loggedIn.value && groupSlug.value,
() => { () => {
router.push("/"); router.push(`/g/${groupSlug.value || ""}`);
}, },
{ immediate: true }, { immediate: true },
); );

View File

@ -1,33 +0,0 @@
<template>
<div>
<RecipePage v-if="recipe" :recipe="recipe" />
</div>
</template>
<script lang="ts">
import { defineComponent, useRoute } from "@nuxtjs/composition-api";
import RecipePage from "~/components/Domain/Recipe/RecipePage/RecipePage.vue";
import { useRecipe } from "~/composables/recipes";
export default defineComponent({
components: { RecipePage },
setup() {
const route = useRoute();
const slug = route.value.params.slug;
const { recipe, loading } = useRecipe(slug);
return {
recipe,
loading,
};
},
head() {
if (this.recipe) {
return {
title: this.recipe.name
}
}
}
});
</script>

View File

@ -199,7 +199,7 @@
<v-lazy> <v-lazy>
<div class="d-flex justify-end mt-10"> <div class="d-flex justify-end mt-10">
<ButtonLink to="/group/data/labels" :text="$tc('shopping-list.manage-labels')" :icon="$globals.icons.tags" /> <ButtonLink :to="`/group/data/labels`" :text="$tc('shopping-list.manage-labels')" :icon="$globals.icons.tags" />
</div> </div>
</v-lazy> </v-lazy>
</v-container> </v-container>
@ -236,6 +236,7 @@ export default defineComponent({
ShoppingListItemEditor, ShoppingListItemEditor,
}, },
setup() { setup() {
const { $auth, i18n } = useContext();
const preferences = useShoppingListPreferences(); const preferences = useShoppingListPreferences();
const { idle } = useIdle(5 * 60 * 1000) // 5 minutes const { idle } = useIdle(5 * 60 * 1000) // 5 minutes
@ -247,10 +248,9 @@ export default defineComponent({
const reorderLabelsDialog = ref(false); const reorderLabelsDialog = ref(false);
const route = useRoute(); const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const id = route.value.params.id; const id = route.value.params.id;
const { i18n } = useContext();
// =============================================================== // ===============================================================
// Shopping List Actions // Shopping List Actions
@ -755,6 +755,7 @@ export default defineComponent({
deleteListItem, deleteListItem,
edit, edit,
getLabelColor, getLabelColor,
groupSlug,
itemsByLabel, itemsByLabel,
listItems, listItems,
loadingCounter, loadingCounter,

View File

@ -33,19 +33,22 @@
</v-card> </v-card>
</section> </section>
<div class="d-flex justify-end mt-10"> <div class="d-flex justify-end mt-10">
<ButtonLink to="/group/data/labels" :text="$tc('shopping-list.manage-labels')" :icon="$globals.icons.tags" /> <ButtonLink :to="`/group/data/labels`" :text="$tc('shopping-list.manage-labels')" :icon="$globals.icons.tags" />
</div> </div>
</v-container> </v-container>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, useAsync, reactive, toRefs } from "@nuxtjs/composition-api"; import { computed, defineComponent, useAsync, useContext, reactive, toRefs, useRoute } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils"; import { useAsyncKey } from "~/composables/use-utils";
export default defineComponent({ export default defineComponent({
setup() { setup() {
const { $auth } = useContext();
const userApi = useUserApi(); const userApi = useUserApi();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
const state = reactive({ const state = reactive({
createName: "", createName: "",
@ -95,6 +98,7 @@ export default defineComponent({
return { return {
...toRefs(state), ...toRefs(state),
groupSlug,
shoppingLists, shoppingLists,
createOne, createOne,
deleteOne, deleteOne,

View File

@ -1,6 +1,6 @@
<template> <template>
<v-container> <v-container>
<RecipeCardSection v-if="user" :icon="$globals.icons.heart" :title="$tc('user.user-favorites')" :recipes="user.favoriteRecipes"> <RecipeCardSection v-if="user && isOwnGroup" :icon="$globals.icons.heart" :title="$tc('user.user-favorites')" :recipes="user.favoriteRecipes">
</RecipeCardSection> </RecipeCardSection>
</v-container> </v-container>
</template> </template>
@ -8,6 +8,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api"; import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useLoggedInState } from "~/composables/use-logged-in-state";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils"; import { useAsyncKey } from "~/composables/use-utils";
@ -16,6 +17,7 @@ export default defineComponent({
setup() { setup() {
const api = useUserApi(); const api = useUserApi();
const route = useRoute(); const route = useRoute();
const { isOwnGroup } = useLoggedInState();
const userId = route.value.params.id; const userId = route.value.params.id;
@ -26,6 +28,7 @@ export default defineComponent({
return { return {
user, user,
isOwnGroup,
}; };
}, },
head() { head() {

View File

@ -1,7 +1,7 @@
<template> <template>
<div></div> <div></div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"; import { defineComponent } from "@nuxtjs/composition-api";
@ -16,6 +16,6 @@ export default defineComponent({
}, },
}); });
</script> </script>
<style scoped> <style scoped>
</style> </style>

View File

@ -108,9 +108,9 @@
:label="$t('profile.show-advanced-description')" :label="$t('profile.show-advanced-description')"
@change="updateUser" @change="updateUser"
></v-checkbox> ></v-checkbox>
<nuxt-link class="mt-5 d-flex flex-column justify-center text-center" to="/group"> {{ $t('profile.looking-for-privacy-settings') }} </nuxt-link> <nuxt-link class="mt-5 d-flex flex-column justify-center text-center" :to="`/group`"> {{ $t('profile.looking-for-privacy-settings') }} </nuxt-link>
<div class="d-flex flex-wrap justify-center mt-5"> <div class="d-flex flex-wrap justify-center mt-5">
<v-btn outlined class="rounded-xl my-1 mx-1" to="/user/profile" nuxt exact> <v-btn outlined class="rounded-xl my-1 mx-1" :to="`/user/profile`" nuxt exact>
<v-icon left> <v-icon left>
{{ $globals.icons.backArrow }} {{ $globals.icons.backArrow }}
</v-icon> </v-icon>

View File

@ -16,17 +16,6 @@
</v-icon> </v-icon>
{{ $t('profile.get-invite-link') }} {{ $t('profile.get-invite-link') }}
</v-btn> </v-btn>
<v-btn
v-if="group && group.preferences && !group.preferences.privateGroup"
outlined
rounded
@click="getPublicLink()"
>
<v-icon left>
{{ $globals.icons.shareVariant }}
</v-icon>
{{ $t('profile.get-public-link') }}
</v-btn>
</v-card-actions> </v-card-actions>
<div v-show="generatedSignupLink !== ''"> <div v-show="generatedSignupLink !== ''">
<v-card-text> <v-card-text>
@ -114,7 +103,7 @@
<v-row tag="section"> <v-row tag="section">
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
<UserProfileLinkCard <UserProfileLinkCard
:link="{ text: $tc('profile.manage-user-profile'), to: '/user/profile/edit' }" :link="{ text: $tc('profile.manage-user-profile'), to: `/user/profile/edit` }"
:image="require('~/static/svgs/manage-profile.svg')" :image="require('~/static/svgs/manage-profile.svg')"
> >
<template #title> {{ $t('profile.user-settings') }} </template> <template #title> {{ $t('profile.user-settings') }} </template>
@ -124,7 +113,7 @@
<AdvancedOnly> <AdvancedOnly>
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
<UserProfileLinkCard <UserProfileLinkCard
:link="{ text: $tc('profile.manage-your-api-tokens'), to: '/user/profile/api-tokens' }" :link="{ text: $tc('profile.manage-your-api-tokens'), to: `/user/profile/api-tokens` }"
:image="require('~/static/svgs/manage-api-tokens.svg')" :image="require('~/static/svgs/manage-api-tokens.svg')"
> >
<template #title> {{ $t('settings.token.api-tokens') }} </template> <template #title> {{ $t('settings.token.api-tokens') }} </template>
@ -143,7 +132,7 @@
<v-row tag="section"> <v-row tag="section">
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
<UserProfileLinkCard <UserProfileLinkCard
:link="{ text: $tc('profile.group-settings'), to: '/group' }" :link="{ text: $tc('profile.group-settings'), to: `/group` }"
:image="require('~/static/svgs/manage-group-settings.svg')" :image="require('~/static/svgs/manage-group-settings.svg')"
> >
<template #title> {{ $t('profile.group-settings') }} </template> <template #title> {{ $t('profile.group-settings') }} </template>
@ -152,7 +141,7 @@
</v-col> </v-col>
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
<UserProfileLinkCard <UserProfileLinkCard
:link="{ text: $tc('profile.manage-cookbooks'), to: '/group/cookbooks' }" :link="{ text: $tc('profile.manage-cookbooks'), to: `/g/${groupSlug}/cookbooks` }"
:image="require('~/static/svgs/manage-cookbooks.svg')" :image="require('~/static/svgs/manage-cookbooks.svg')"
> >
<template #title> {{ $t('sidebar.cookbooks') }} </template> <template #title> {{ $t('sidebar.cookbooks') }} </template>
@ -161,7 +150,7 @@
</v-col> </v-col>
<v-col v-if="user.canManage" cols="12" sm="12" md="6"> <v-col v-if="user.canManage" cols="12" sm="12" md="6">
<UserProfileLinkCard <UserProfileLinkCard
:link="{ text: $tc('profile.manage-members'), to: '/group/members' }" :link="{ text: $tc('profile.manage-members'), to: `/group/members` }"
:image="require('~/static/svgs/manage-members.svg')" :image="require('~/static/svgs/manage-members.svg')"
> >
<template #title> {{ $t('profile.members') }} </template> <template #title> {{ $t('profile.members') }} </template>
@ -171,7 +160,7 @@
<AdvancedOnly> <AdvancedOnly>
<v-col v-if="user.advanced" cols="12" sm="12" md="6"> <v-col v-if="user.advanced" cols="12" sm="12" md="6">
<UserProfileLinkCard <UserProfileLinkCard
:link="{ text: $tc('profile.manage-webhooks'), to: '/group/webhooks' }" :link="{ text: $tc('profile.manage-webhooks'), to: `/group/webhooks` }"
:image="require('~/static/svgs/manage-webhooks.svg')" :image="require('~/static/svgs/manage-webhooks.svg')"
> >
<template #title> {{ $t('settings.webhooks.webhooks') }} </template> <template #title> {{ $t('settings.webhooks.webhooks') }} </template>
@ -182,7 +171,7 @@
<AdvancedOnly> <AdvancedOnly>
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
<UserProfileLinkCard <UserProfileLinkCard
:link="{ text: $tc('profile.manage-notifiers'), to: '/group/notifiers' }" :link="{ text: $tc('profile.manage-notifiers'), to: `/group/notifiers` }"
:image="require('~/static/svgs/manage-notifiers.svg')" :image="require('~/static/svgs/manage-notifiers.svg')"
> >
<template #title> {{ $t('profile.notifiers') }} </template> <template #title> {{ $t('profile.notifiers') }} </template>
@ -193,7 +182,7 @@
<AdvancedOnly> <AdvancedOnly>
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
<UserProfileLinkCard <UserProfileLinkCard
:link="{ text: $tc('profile.manage-data'), to: '/group/data/foods' }" :link="{ text: $tc('profile.manage-data'), to: `/group/data/foods` }"
:image="require('~/static/svgs/manage-recipes.svg')" :image="require('~/static/svgs/manage-recipes.svg')"
> >
<template #title> {{ $t('profile.manage-data') }} </template> <template #title> {{ $t('profile.manage-data') }} </template>
@ -204,7 +193,7 @@
<AdvancedOnly> <AdvancedOnly>
<v-col cols="12" sm="12" md="6"> <v-col cols="12" sm="12" md="6">
<UserProfileLinkCard <UserProfileLinkCard
:link="{ text: $tc('profile.manage-data-migrations'), to: '/group/migrations' }" :link="{ text: $tc('profile.manage-data-migrations'), to: `/group/migrations` }"
:image="require('~/static/svgs/manage-data-migrations.svg')" :image="require('~/static/svgs/manage-data-migrations.svg')"
> >
<template #title>{{ $t('profile.data-migrations') }} </template> <template #title>{{ $t('profile.data-migrations') }} </template>
@ -218,7 +207,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, useContext, ref, toRefs, reactive, useAsync } from "@nuxtjs/composition-api"; import { computed, defineComponent, useContext, ref, toRefs, reactive, useAsync, useRoute } from "@nuxtjs/composition-api";
import { invoke, until } from "@vueuse/core"; import { invoke, until } from "@vueuse/core";
import UserProfileLinkCard from "@/components/Domain/User/UserProfileLinkCard.vue"; import UserProfileLinkCard from "@/components/Domain/User/UserProfileLinkCard.vue";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
@ -239,6 +228,8 @@ export default defineComponent({
scrollToTop: true, scrollToTop: true,
setup() { setup() {
const { $auth, i18n } = useContext(); const { $auth, i18n } = useContext();
const route = useRoute();
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
// @ts-ignore $auth.user is typed as unknown, but it's a user // @ts-ignore $auth.user is typed as unknown, but it's a user
const user = computed<UserOut | null>(() => $auth.user); const user = computed<UserOut | null>(() => $auth.user);
@ -261,14 +252,6 @@ export default defineComponent({
group.value = data; group.value = data;
}); });
function getPublicLink() {
if (group.value) {
publicLink.value = `${window.location.origin}/explore/recipes/${group.value.slug}`
showPublicLink.value = true;
generatedSignupLink.value = "";
}
}
async function getSignupLink() { async function getSignupLink() {
const { data } = await api.groups.createInvitation({ uses: 1 }); const { data } = await api.groups.createInvitation({ uses: 1 });
if (data) { if (data) {
@ -350,16 +333,16 @@ export default defineComponent({
return iconText[key] ?? $globals.icons.primary; return iconText[key] ?? $globals.icons.primary;
} }
const statsTo: { [key: string]: string } = { const statsTo = computed<{ [key: string]: string }>(() => { return {
totalRecipes: "/", totalRecipes: `/g/${groupSlug.value}/`,
totalUsers: "/group/members", totalUsers: "/group/members",
totalCategories: "/recipes/categories", totalCategories: `/g/${groupSlug.value}/recipes/categories`,
totalTags: "/recipes/tags", totalTags: `/g/${groupSlug.value}/recipes/tags`,
totalTools: "/recipes/tools", totalTools: `/g/${groupSlug.value}/recipes/tools`,
}; }});
function getStatsTo(key: string) { function getStatsTo(key: string) {
return statsTo[key] ?? "unknown"; return statsTo.value[key] ?? "unknown";
} }
const storage = useAsync(async () => { const storage = useAsync(async () => {
@ -386,6 +369,7 @@ export default defineComponent({
}); });
return { return {
groupSlug,
storageText, storageText,
storageUsedPercentage, storageUsedPercentage,
getStatsTitle, getStatsTitle,
@ -399,7 +383,6 @@ export default defineComponent({
showPublicLink, showPublicLink,
publicLink, publicLink,
getSignupLink, getSignupLink,
getPublicLink,
sendInvite, sendInvite,
validators, validators,
validEmail, validEmail,

View File

@ -1,6 +1,7 @@
import { Plugin } from "@nuxt/types"; import { Plugin } from "@nuxt/types";
import { Auth } from "@nuxtjs/auth-next"; import { Auth as NuxtAuth } from "@nuxtjs/auth-next";
import { Framework } from "vuetify"; import { Framework } from "vuetify";
import { UserOut } from "~/lib/api/types/user";
import { icons } from "~/lib/icons"; import { icons } from "~/lib/icons";
import { Icon } from "~/lib/icons/icon-type"; import { Icon } from "~/lib/icons/icon-type";
@ -15,6 +16,11 @@ declare module "vue/types/vue" {
} }
declare module "@nuxt/types" { declare module "@nuxt/types" {
// @ts-ignore https://github.com/nuxt-community/auth-module/issues/1097#issuecomment-840249428
interface Auth extends NuxtAuth {
user: UserOut | null;
}
interface Context { interface Context {
$globals: Globals; $globals: Globals;
$vuetify: Framework; $vuetify: Framework;

View File

@ -58,7 +58,7 @@ async def get_public_group(group_slug: str = fastapi.Path(...), session=Depends(
repos = get_repositories(session) repos = get_repositories(session)
group = repos.groups.get_by_slug_or_id(group_slug) group = repos.groups.get_by_slug_or_id(group_slug)
if not group or group.preferences.private_group: if not group or group.preferences.private_group or not group.preferences.recipe_public:
raise HTTPException(404, "group not found") raise HTTPException(404, "group not found")
else: else:
return group return group

View File

@ -3,6 +3,7 @@ from datetime import datetime
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, orm from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, orm
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from mealie.core.config import get_app_settings from mealie.core.config import get_app_settings
@ -94,6 +95,10 @@ class User(SqlAlchemyBase, BaseMixins):
"group", "group",
} }
@hybrid_property
def group_slug(self) -> str:
return self.group.slug
@auto_init() @auto_init()
def __init__(self, session, full_name, password, group: str | None = None, **kwargs) -> None: def __init__(self, session, full_name, password, group: str | None = None, **kwargs) -> None:
if group is None: if group is None:

View File

@ -187,7 +187,7 @@ class RecipeController(BaseRecipeController):
message=self.t( message=self.t(
"notifications.generic-created-with-url", "notifications.generic-created-with-url",
name=new_recipe.name, name=new_recipe.name,
url=urls.recipe_url(new_recipe.slug, self.settings.BASE_URL), url=urls.recipe_url(self.group.slug, new_recipe.slug, self.settings.BASE_URL),
), ),
) )
@ -306,7 +306,7 @@ class RecipeController(BaseRecipeController):
message=self.t( message=self.t(
"notifications.generic-created-with-url", "notifications.generic-created-with-url",
name=new_recipe.name, name=new_recipe.name,
url=urls.recipe_url(new_recipe.slug, self.settings.BASE_URL), url=urls.recipe_url(self.group.slug, new_recipe.slug, self.settings.BASE_URL),
), ),
) )
@ -347,7 +347,7 @@ class RecipeController(BaseRecipeController):
message=self.t( message=self.t(
"notifications.generic-updated-with-url", "notifications.generic-updated-with-url",
name=recipe.name, name=recipe.name,
url=urls.recipe_url(recipe.slug, self.settings.BASE_URL), url=urls.recipe_url(self.group.slug, recipe.slug, self.settings.BASE_URL),
), ),
) )
@ -368,7 +368,7 @@ class RecipeController(BaseRecipeController):
message=self.t( message=self.t(
"notifications.generic-updated-with-url", "notifications.generic-updated-with-url",
name=recipe.name, name=recipe.name,
url=urls.recipe_url(recipe.slug, self.settings.BASE_URL), url=urls.recipe_url(self.group.slug, recipe.slug, self.settings.BASE_URL),
), ),
) )
@ -390,7 +390,7 @@ class RecipeController(BaseRecipeController):
message=self.t( message=self.t(
"notifications.generic-updated-with-url", "notifications.generic-updated-with-url",
name=recipe.name, name=recipe.name,
url=urls.recipe_url(recipe.slug, self.settings.BASE_URL), url=urls.recipe_url(self.group.slug, recipe.slug, self.settings.BASE_URL),
), ),
) )

View File

@ -72,7 +72,7 @@ class RecipeTimelineEventsController(BaseCrudController):
message=self.t( message=self.t(
"notifications.generic-updated-with-url", "notifications.generic-updated-with-url",
name=recipe.name, name=recipe.name,
url=urls.recipe_url(recipe.slug, self.settings.BASE_URL), url=urls.recipe_url(self.group.slug, recipe.slug, self.settings.BASE_URL),
), ),
) )
@ -95,7 +95,7 @@ class RecipeTimelineEventsController(BaseCrudController):
message=self.t( message=self.t(
"notifications.generic-updated-with-url", "notifications.generic-updated-with-url",
name=recipe.name, name=recipe.name,
url=urls.recipe_url(recipe.slug, self.settings.BASE_URL), url=urls.recipe_url(self.group.slug, recipe.slug, self.settings.BASE_URL),
), ),
) )
@ -120,7 +120,7 @@ class RecipeTimelineEventsController(BaseCrudController):
message=self.t( message=self.t(
"notifications.generic-updated-with-url", "notifications.generic-updated-with-url",
name=recipe.name, name=recipe.name,
url=urls.recipe_url(recipe.slug, self.settings.BASE_URL), url=urls.recipe_url(self.group.slug, recipe.slug, self.settings.BASE_URL),
), ),
) )
@ -148,7 +148,7 @@ class RecipeTimelineEventsController(BaseCrudController):
message=self.t( message=self.t(
"notifications.generic-updated-with-url", "notifications.generic-updated-with-url",
name=recipe.name, name=recipe.name,
url=urls.recipe_url(recipe.slug, self.settings.BASE_URL), url=urls.recipe_url(self.group.slug, recipe.slug, self.settings.BASE_URL),
), ),
) )

View File

@ -33,9 +33,9 @@ __app_settings = get_app_settings()
__contents = "" __contents = ""
def content_with_meta(recipe: Recipe) -> str: def content_with_meta(group_slug: str, recipe: Recipe) -> str:
# Inject meta tags # Inject meta tags
recipe_url = f"{__app_settings.BASE_URL}/recipe/{recipe.slug}" recipe_url = f"{__app_settings.BASE_URL}/g/{group_slug}/r/{recipe.slug}"
image_url = f"{__app_settings.BASE_URL}/api/media/recipes/{recipe.id}/images/original.webp?version={recipe.image}" image_url = f"{__app_settings.BASE_URL}/api/media/recipes/{recipe.id}/images/original.webp?version={recipe.image}"
ingredients: list[str] = [] ingredients: list[str] = []
@ -122,28 +122,29 @@ def serve_recipe_with_meta_public(
return response_404() return response_404()
# Inject meta tags # Inject meta tags
return Response(content_with_meta(recipe), media_type="text/html") return Response(content_with_meta(group_slug, recipe), media_type="text/html")
except Exception: except Exception:
return response_404() return response_404()
async def serve_recipe_with_meta( async def serve_recipe_with_meta(
slug: str, group_slug: str,
user: PrivateUser = Depends(try_get_current_user), recipe_slug: str,
user: PrivateUser | None = Depends(try_get_current_user),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
if not user: if not user:
return Response(__contents, media_type="text/html", status_code=401) return serve_recipe_with_meta_public(group_slug, recipe_slug, session)
try: try:
repos = AllRepositories(session) repos = AllRepositories(session)
recipe = repos.recipes.by_group(user.group_id).get_one(slug, "slug") recipe = repos.recipes.by_group(user.group_id).get_one(recipe_slug, "slug")
if recipe is None: if recipe is None:
return response_404() return response_404()
# Serve contents as HTML # Serve contents as HTML
return Response(content_with_meta(recipe), media_type="text/html") return Response(content_with_meta(group_slug, recipe), media_type="text/html")
except Exception: except Exception:
return response_404() return response_404()
@ -155,6 +156,5 @@ def mount_spa(app: FastAPI):
global __contents global __contents
__contents = pathlib.Path(__app_settings.STATIC_FILES).joinpath("index.html").read_text() __contents = pathlib.Path(__app_settings.STATIC_FILES).joinpath("index.html").read_text()
app.get("/recipe/{slug}")(serve_recipe_with_meta) app.get("/g/{group_slug}/r/{recipe_slug}")(serve_recipe_with_meta)
app.get("/explore/recipes/{group_slug}/{recipe_slug}")(serve_recipe_with_meta_public)
app.mount("/", SPAStaticFiles(directory=__app_settings.STATIC_FILES, html=True), name="spa") app.mount("/", SPAStaticFiles(directory=__app_settings.STATIC_FILES, html=True), name="spa")

View File

@ -104,6 +104,7 @@ class UserOut(UserBase):
id: UUID4 id: UUID4
group: str group: str
group_id: UUID4 group_id: UUID4
group_slug: str
tokens: list[LongLiveTokenOut] | None tokens: list[LongLiveTokenOut] | None
cache_key: str cache_key: str
favorite_recipes: list[str] | None = [] favorite_recipes: list[str] | None = []

View File

@ -11,9 +11,9 @@ def _base_or(base_url: str | None) -> str:
return base_url return base_url
def recipe_url(recipe_slug: str, base_url: str | None) -> str: def recipe_url(group_slug: str, recipe_slug: str, base_url: str | None) -> str:
base = _base_or(base_url) base = _base_or(base_url)
return f"{base}/recipe/{recipe_slug}" return f"{base}/g/{group_slug}/r/{recipe_slug}"
def shopping_list_url(shopping_list_id: UUID4 | str, base_url: str | None) -> str: def shopping_list_url(shopping_list_id: UUID4 | str, base_url: str | None) -> str:

View File

@ -24,6 +24,7 @@ def test_get_all_cookbooks(
assert group and group.preferences assert group and group.preferences
group.preferences.private_group = is_private_group group.preferences.private_group = is_private_group
group.preferences.recipe_public = not is_private_group
database.group_preferences.update(group.id, group.preferences) database.group_preferences.update(group.id, group.preferences)
## Set Up Cookbooks ## Set Up Cookbooks
@ -88,6 +89,7 @@ def test_get_one_cookbook(
assert group and group.preferences assert group and group.preferences
group.preferences.private_group = is_private_group group.preferences.private_group = is_private_group
group.preferences.recipe_public = not is_private_group
database.group_preferences.update(group.id, group.preferences) database.group_preferences.update(group.id, group.preferences)
## Set Up Cookbook ## Set Up Cookbook
@ -116,6 +118,7 @@ def test_get_cookbooks_with_recipes(api_client: TestClient, unique_user: TestUse
assert group and group.preferences assert group and group.preferences
group.preferences.private_group = False group.preferences.private_group = False
group.preferences.recipe_public = True
database.group_preferences.update(group.id, group.preferences) database.group_preferences.update(group.id, group.preferences)
tag = database.tags.create(TagSave(name=random_string(), group_id=unique_user.group_id)) tag = database.tags.create(TagSave(name=random_string(), group_id=unique_user.group_id))

View File

@ -20,6 +20,7 @@ def test_get_all_foods(
assert group and group.preferences assert group and group.preferences
group.preferences.private_group = is_private_group group.preferences.private_group = is_private_group
group.preferences.recipe_public = not is_private_group
database.group_preferences.update(group.id, group.preferences) database.group_preferences.update(group.id, group.preferences)
## Set Up Foods ## Set Up Foods
@ -53,6 +54,7 @@ def test_get_one_food(
assert group and group.preferences assert group and group.preferences
group.preferences.private_group = is_private_group group.preferences.private_group = is_private_group
group.preferences.recipe_public = not is_private_group
database.group_preferences.update(group.id, group.preferences) database.group_preferences.update(group.id, group.preferences)
## Set Up Food ## Set Up Food

View File

@ -48,6 +48,7 @@ def test_get_all_organizers(
assert group and group.preferences assert group and group.preferences
group.preferences.private_group = is_private_group group.preferences.private_group = is_private_group
group.preferences.recipe_public = not is_private_group
database.group_preferences.update(group.id, group.preferences) database.group_preferences.update(group.id, group.preferences)
## Set Up Organizers ## Set Up Organizers
@ -113,6 +114,7 @@ def test_get_one_organizer(
assert group and group.preferences assert group and group.preferences
group.preferences.private_group = is_private_group group.preferences.private_group = is_private_group
group.preferences.recipe_public = not is_private_group
database.group_preferences.update(group.id, group.preferences) database.group_preferences.update(group.id, group.preferences)
## Set Up Organizer ## Set Up Organizer

View File

@ -32,6 +32,7 @@ def test_get_all_public_recipes(
assert group and group.preferences assert group and group.preferences
group.preferences.private_group = is_private_group group.preferences.private_group = is_private_group
group.preferences.recipe_public = not is_private_group
database.group_preferences.update(group.id, group.preferences) database.group_preferences.update(group.id, group.preferences)
default_recipes = database.recipes.create_many( default_recipes = database.recipes.create_many(
@ -106,6 +107,7 @@ def test_get_all_public_recipes_filtered(
assert group and group.preferences assert group and group.preferences
group.preferences.private_group = False group.preferences.private_group = False
group.preferences.recipe_public = True
database.group_preferences.update(group.id, group.preferences) database.group_preferences.update(group.id, group.preferences)
assert random_recipe.settings assert random_recipe.settings
@ -140,6 +142,7 @@ def test_public_recipe_success(
assert group and group.preferences assert group and group.preferences
group.preferences.private_group = test_case.private_group group.preferences.private_group = test_case.private_group
group.preferences.recipe_public = not test_case.private_group
database.group_preferences.update(group.id, group.preferences) database.group_preferences.update(group.id, group.preferences)
# Set Recipe `settings.public` attribute # Set Recipe `settings.public` attribute