mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
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:
parent
94cf690e8f
commit
80968b02bb
@ -17,7 +17,7 @@ body:
|
||||
- label: |
|
||||
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
|
||||
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
|
||||
**3)** verified that the site provides recipe data, or is otherwise supported by
|
||||
[hhyrsev/recipe-scrapers](https://github.com/hhursev/recipe-scrapers)
|
||||
|
@ -15,6 +15,6 @@ var url = document.URL;
|
||||
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 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");
|
||||
```
|
||||
|
@ -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.
|
||||
|
||||
[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
|
||||
|
||||
@ -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 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, 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
|
||||
|
||||
@ -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.
|
||||
|
||||
[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
|
||||
|
||||
@ -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
|
||||
- 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
|
||||
|
||||
|
@ -15,7 +15,6 @@
|
||||
class="mb-5 mx-1"
|
||||
:recipes="recipes"
|
||||
:query="{ cookbook: slug }"
|
||||
:group-slug="groupSlug"
|
||||
@sortRecipes="assignSorted"
|
||||
@replaceRecipes="replaceRecipes"
|
||||
@appendRecipes="appendRecipes"
|
||||
@ -30,24 +29,20 @@
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { useCookbook } from "~/composables/use-group-cookbooks";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeCardSection },
|
||||
props: {
|
||||
groupSlug: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
setup() {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => $auth.loggedIn);
|
||||
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(loggedIn.value ? null : props.groupSlug);
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
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 { getOne } = useCookbook(loggedIn.value ? null : props.groupSlug);
|
||||
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
||||
|
||||
const tab = ref(null);
|
||||
const book = getOne(slug);
|
||||
|
@ -70,7 +70,6 @@
|
||||
print: true,
|
||||
printPreferences: true,
|
||||
share: loggedIn,
|
||||
publicUrl: recipe.settings && loggedIn ? recipe.settings.public : false,
|
||||
}"
|
||||
@print="$emit('print')"
|
||||
/>
|
||||
|
@ -34,7 +34,7 @@
|
||||
|
||||
<slot name="actions">
|
||||
<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" />
|
||||
<v-spacer></v-spacer>
|
||||
@ -42,7 +42,7 @@
|
||||
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
<RecipeContextMenu
|
||||
v-if="loggedIn"
|
||||
v-if="isOwnGroup"
|
||||
color="grey darken-2"
|
||||
:slug="slug"
|
||||
:name="name"
|
||||
@ -56,7 +56,6 @@
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
publicUrl: false,
|
||||
}"
|
||||
@delete="$emit('delete', slug)"
|
||||
/>
|
||||
@ -69,12 +68,13 @@
|
||||
</template>
|
||||
|
||||
<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 RecipeChips from "./RecipeChips.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||
import RecipeRating from "./RecipeRating.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeFavoriteBadge, RecipeChips, RecipeContextMenu, RecipeRating, RecipeCardImage },
|
||||
@ -83,10 +83,6 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
groupSlug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
@ -124,16 +120,16 @@ export default defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "")
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return loggedIn.value ? `/recipe/${props.slug}` : `/explore/recipes/${props.groupSlug}/${props.slug}`;
|
||||
return `/g/${groupSlug.value}/r/${props.slug}`;
|
||||
});
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
isOwnGroup,
|
||||
recipeRoute,
|
||||
};
|
||||
},
|
||||
|
@ -37,10 +37,10 @@
|
||||
</v-list-item-subtitle>
|
||||
<div class="d-flex flex-wrap justify-end align-center">
|
||||
<slot name="actions">
|
||||
<RecipeFavoriteBadge v-if="loggedIn" :slug="slug" show-always />
|
||||
<RecipeFavoriteBadge v-if="isOwnGroup" :slug="slug" show-always />
|
||||
<v-rating
|
||||
color="secondary"
|
||||
:class="loggedIn ? 'ml-auto' : 'ml-auto pb-2'"
|
||||
:class="isOwnGroup ? 'ml-auto' : 'ml-auto pb-2'"
|
||||
background-color="secondary lighten-3"
|
||||
dense
|
||||
length="5"
|
||||
@ -52,7 +52,7 @@
|
||||
<!-- 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 -->
|
||||
<RecipeContextMenu
|
||||
v-if="loggedIn"
|
||||
v-if="isOwnGroup"
|
||||
:slug="slug"
|
||||
:menu-icon="$globals.icons.dotsHorizontal"
|
||||
:name="name"
|
||||
@ -66,7 +66,6 @@
|
||||
print: false,
|
||||
printPreferences: false,
|
||||
share: true,
|
||||
publicUrl: false,
|
||||
}"
|
||||
@deleted="$emit('delete', slug)"
|
||||
/>
|
||||
@ -80,10 +79,11 @@
|
||||
</template>
|
||||
|
||||
<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 RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -96,10 +96,6 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
groupSlug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
slug: {
|
||||
type: String,
|
||||
required: true,
|
||||
@ -136,16 +132,16 @@ export default defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "")
|
||||
const recipeRoute = computed<string>(() => {
|
||||
return loggedIn.value ? `/recipe/${props.slug}` : `/explore/recipes/${props.groupSlug}/${props.slug}`;
|
||||
return `/g/${groupSlug.value}/r/${props.slug}`;
|
||||
});
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
isOwnGroup,
|
||||
recipeRoute,
|
||||
};
|
||||
},
|
||||
|
@ -76,7 +76,6 @@
|
||||
<RecipeCard
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:group-slug="groupSlug"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
@ -100,7 +99,6 @@
|
||||
<RecipeCardMobile
|
||||
:name="recipe.name"
|
||||
:description="recipe.description"
|
||||
:group-slug="groupSlug"
|
||||
:slug="recipe.slug"
|
||||
:rating="recipe.rating"
|
||||
:image="recipe.image"
|
||||
@ -128,12 +126,14 @@ import {
|
||||
toRefs,
|
||||
useAsync,
|
||||
useContext,
|
||||
useRoute,
|
||||
useRouter,
|
||||
watch,
|
||||
} from "@nuxtjs/composition-api";
|
||||
import { useThrottleFn } from "@vueuse/core";
|
||||
import RecipeCard from "./RecipeCard.vue";
|
||||
import RecipeCardMobile from "./RecipeCardMobile.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { useLazyRecipes } from "~/composables/recipes";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
@ -165,10 +165,6 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
groupSlug: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
recipes: {
|
||||
type: Array as () => Recipe[],
|
||||
default: () => [],
|
||||
@ -191,9 +187,7 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const { $auth, $globals, $vuetify } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const useMobileCards = computed(() => {
|
||||
return $vuetify.breakpoint.smAndDown || preferences.value.useMobileCards;
|
||||
});
|
||||
@ -206,12 +200,15 @@ export default defineComponent({
|
||||
sortLoading: false,
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const router = useRouter();
|
||||
function navigateRandom() {
|
||||
if (props.recipes.length > 0) {
|
||||
const recipe = props.recipes[Math.floor(Math.random() * props.recipes.length)];
|
||||
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 loading = ref(false);
|
||||
|
||||
const { fetchMore } = useLazyRecipes(loggedIn.value ? null : props.groupSlug);
|
||||
const { fetchMore } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||
|
||||
const queryFilter = computed(() => {
|
||||
const orderBy = props.query?.orderBy || preferences.value.orderBy;
|
||||
|
@ -9,7 +9,7 @@
|
||||
color="accent"
|
||||
:small="small"
|
||||
dark
|
||||
:to=" loggedIn ? `/?${urlPrefix}=${category.id}` : undefined"
|
||||
:to="isOwnGroup ? `${baseRecipeRoute}?${urlPrefix}=${category.id}` : undefined"
|
||||
>
|
||||
{{ truncateText(category.name) }}
|
||||
</v-chip>
|
||||
@ -17,7 +17,8 @@
|
||||
</template>
|
||||
|
||||
<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";
|
||||
|
||||
export type UrlPrefixParam = "tags" | "categories" | "tools";
|
||||
@ -55,9 +56,13 @@ export default defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn
|
||||
})
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
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 = "...") {
|
||||
if (!props.truncate) return text;
|
||||
@ -68,7 +73,8 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return {
|
||||
loggedIn,
|
||||
baseRecipeRoute,
|
||||
isOwnGroup,
|
||||
truncateText,
|
||||
};
|
||||
},
|
||||
|
@ -170,10 +170,11 @@
|
||||
</template>
|
||||
|
||||
<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 RecipeDialogPrintPreferences from "./RecipeDialogPrintPreferences.vue";
|
||||
import RecipeDialogShare from "./RecipeDialogShare.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
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 { PlanEntryType } from "~/lib/api/types/meal-plan";
|
||||
import { useAxiosDownloader } from "~/composables/api/use-axios-download";
|
||||
import { useCopy } from "~/composables/use-copy";
|
||||
|
||||
export interface ContextMenuIncludes {
|
||||
delete: boolean;
|
||||
@ -192,7 +192,6 @@ export interface ContextMenuIncludes {
|
||||
print: boolean;
|
||||
printPreferences: boolean;
|
||||
share: boolean;
|
||||
publicUrl: boolean;
|
||||
}
|
||||
|
||||
export interface ContextMenuItem {
|
||||
@ -222,7 +221,6 @@ export default defineComponent({
|
||||
print: true,
|
||||
printPreferences: true,
|
||||
share: true,
|
||||
publicUrl: false,
|
||||
}),
|
||||
},
|
||||
// Append items are added at the end of the useItems list
|
||||
@ -291,10 +289,11 @@ export default defineComponent({
|
||||
pickerMenu: false,
|
||||
});
|
||||
|
||||
const { $auth, i18n, $globals } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const { i18n, $auth, $globals } = useContext();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
// ===========================================================================
|
||||
// Context Menu Setup
|
||||
@ -363,20 +362,13 @@ export default defineComponent({
|
||||
event: "share",
|
||||
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
|
||||
for (const [key, value] of Object.entries(props.useItems)) {
|
||||
if (value) {
|
||||
const item = defaultItems[key];
|
||||
if (item && (item.isPublic || loggedIn.value)) {
|
||||
if (item && (item.isPublic || isOwnGroup.value)) {
|
||||
state.menuItems.push(item);
|
||||
}
|
||||
}
|
||||
@ -500,24 +492,7 @@ export default defineComponent({
|
||||
async function duplicateRecipe() {
|
||||
const { data } = await api.recipes.duplicateOne(props.slug, state.recipeName);
|
||||
if (data && data.slug) {
|
||||
router.push(`/recipe/${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
|
||||
router.push(`/g/${groupSlug.value}/r/${data.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -526,7 +501,7 @@ export default defineComponent({
|
||||
delete: () => {
|
||||
state.recipeDeleteDialog = true;
|
||||
},
|
||||
edit: () => router.push(`/recipe/${props.slug}` + "?edit=true"),
|
||||
edit: () => router.push(`/g/${groupSlug.value}/r/${props.slug}` + "?edit=true"),
|
||||
download: handleDownloadEvent,
|
||||
duplicate: () => {
|
||||
state.recipeDuplicateDialog = true;
|
||||
@ -549,14 +524,6 @@ export default defineComponent({
|
||||
share: () => {
|
||||
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) {
|
||||
|
@ -18,7 +18,7 @@
|
||||
</tr>
|
||||
</template>
|
||||
<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 #item.tags="{ item }">
|
||||
<RecipeChip small :items="item.tags" :is-category="false" url-prefix="tags" />
|
||||
|
@ -31,7 +31,7 @@
|
||||
<div class="mr-auto">
|
||||
{{ $t("search.results") }}
|
||||
</div>
|
||||
<router-link to="/"> {{ $t("search.advanced-search") }} </router-link>
|
||||
<router-link :to="advancedSearchUrl"> {{ $t("search.advanced-search") }} </router-link>
|
||||
</v-card-actions>
|
||||
|
||||
<RecipeCardMobile
|
||||
@ -54,11 +54,13 @@
|
||||
</template>
|
||||
|
||||
<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 { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { RecipeSummary } from "~/lib/api/types/recipe";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useRecipeSearch } from "~/composables/recipes/use-recipe-search";
|
||||
import { usePublicExploreApi } from "~/composables/api/api-client";
|
||||
const SELECTED_EVENT = "selected";
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -66,6 +68,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
setup(_, context) {
|
||||
const { $auth } = useContext();
|
||||
const state = reactive({
|
||||
loading: false,
|
||||
selectedIndex: -1,
|
||||
@ -128,7 +131,9 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
const route = useRoute();
|
||||
const advancedSearchUrl = computed(() => `/g/${groupSlug.value}`)
|
||||
watch(route, close);
|
||||
|
||||
function open() {
|
||||
@ -140,7 +145,8 @@ export default defineComponent({
|
||||
|
||||
// ===========================================================================
|
||||
// Basic Search
|
||||
const api = useUserApi();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const api = isOwnGroup.value ? useUserApi() : usePublicExploreApi(groupSlug.value).explore;
|
||||
const search = useRecipeSearch(api);
|
||||
|
||||
// Select Handler
|
||||
@ -152,6 +158,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
advancedSearchUrl,
|
||||
dialog,
|
||||
open,
|
||||
close,
|
||||
|
@ -56,7 +56,7 @@
|
||||
</template>
|
||||
|
||||
<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 { RecipeShareToken } from "~/lib/api/types/recipe";
|
||||
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
|
||||
|
||||
@ -138,7 +142,6 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
const { i18n } = useContext();
|
||||
const { share, isSupported: shareIsSupported } = useShare();
|
||||
const { copy } = useClipboard();
|
||||
|
||||
@ -147,7 +150,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -123,7 +123,6 @@
|
||||
class="mt-n5"
|
||||
:icon="$globals.icons.search"
|
||||
:title="$tc('search.results')"
|
||||
:group-slug="groupSlug"
|
||||
:recipes="recipes"
|
||||
:query="passedQuery"
|
||||
@replaceRecipes="replaceRecipes"
|
||||
@ -134,9 +133,10 @@
|
||||
</template>
|
||||
|
||||
<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 SearchFilter from "~/components/Domain/SearchFilter.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
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({
|
||||
components: { SearchFilter, RecipeCardSection },
|
||||
props: {
|
||||
groupSlug: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
const { $auth, $globals, i18n } = useContext();
|
||||
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const state = ref({
|
||||
auto: true,
|
||||
search: "",
|
||||
@ -176,17 +168,20 @@ export default defineComponent({
|
||||
requireAllFoods: false,
|
||||
});
|
||||
|
||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(loggedIn.value ? null : props.groupSlug);
|
||||
const categories = loggedIn.value ? useCategoryStore() : usePublicCategoryStore(props.groupSlug);
|
||||
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 categories = isOwnGroup.value ? useCategoryStore() : usePublicCategoryStore(groupSlug.value);
|
||||
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 tags = loggedIn.value ? useTagStore() : usePublicTagStore(props.groupSlug);
|
||||
const tags = isOwnGroup.value ? useTagStore() : usePublicTagStore(groupSlug.value);
|
||||
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 passedQuery = ref<RecipeSearchQuery | null>(null);
|
||||
|
@ -7,7 +7,7 @@
|
||||
:class="attrs.class.sheet"
|
||||
: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-icon :class="attrs.class.icon" dark :small="small"> {{ $globals.icons.primary }} </v-icon>
|
||||
</v-list-item-avatar>
|
||||
@ -28,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from "@nuxtjs/composition-api";
|
||||
import { computed, defineComponent, useContext, useRoute } from "@nuxtjs/composition-api";
|
||||
import DOMPurify from "dompurify";
|
||||
import { useFraction } from "~/composables/recipes/use-fraction";
|
||||
import { ShoppingListItemOut } from "~/lib/api/types/group";
|
||||
@ -58,7 +58,10 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const { frac } = useFraction();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const attrs = computed(() => {
|
||||
return props.small ? {
|
||||
@ -150,6 +153,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
attrs,
|
||||
groupSlug,
|
||||
listItemDescriptions,
|
||||
};
|
||||
},
|
||||
|
@ -140,7 +140,7 @@
|
||||
</template>
|
||||
|
||||
<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 { invoke } from "@vueuse/shared";
|
||||
import draggable from "vuedraggable";
|
||||
@ -179,6 +179,10 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth } = useContext();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const router = useRouter();
|
||||
const api = useUserApi();
|
||||
|
||||
@ -328,12 +332,12 @@ export default defineComponent({
|
||||
async function updateRecipe() {
|
||||
const { data } = await api.recipes.updateOne(props.recipe.slug, props.recipe);
|
||||
if (data?.slug) {
|
||||
router.push("/recipe/" + data.slug);
|
||||
router.push(`/g/${groupSlug.value}/r/${data.slug}`);
|
||||
}
|
||||
}
|
||||
|
||||
function closeEditor() {
|
||||
router.push("/recipe/" + props.recipe.slug);
|
||||
router.push(`/g/${groupSlug.value}/r/${props.recipe.slug}`);
|
||||
}
|
||||
|
||||
const canvasSetText = function () {
|
||||
|
@ -48,7 +48,7 @@
|
||||
<BaseCardSectionTitle v-if="isTitle(key)" :title="key" />
|
||||
<v-row>
|
||||
<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-icon>
|
||||
{{ icon }}
|
||||
@ -72,7 +72,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
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 RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue";
|
||||
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
|
||||
|
||||
@ -204,6 +208,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return {
|
||||
groupSlug,
|
||||
isTitle,
|
||||
dialogs,
|
||||
confirmDelete,
|
||||
|
@ -72,7 +72,7 @@
|
||||
</div>
|
||||
|
||||
<RecipePageComments
|
||||
v-if="user.id && !recipe.settings.disableComments && !isEditForm && !isCookMode"
|
||||
v-if="isOwnGroup && !recipe.settings.disableComments && !isEditForm && !isCookMode"
|
||||
:recipe="recipe"
|
||||
class="px-1 my-4 d-print-none"
|
||||
/>
|
||||
@ -89,6 +89,7 @@ import {
|
||||
ref,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
useRoute,
|
||||
} from "@nuxtjs/composition-api";
|
||||
import { invoke, until, useWakeLock } from "@vueuse/core";
|
||||
import RecipePageEditorToolbar from "./RecipePageParts/RecipePageEditorToolbar.vue";
|
||||
@ -101,6 +102,7 @@ import RecipePageOrganizers from "./RecipePageParts/RecipePageOrganizers.vue";
|
||||
import RecipePageScale from "./RecipePageParts/RecipePageScale.vue";
|
||||
import RecipePageTitleContent from "./RecipePageParts/RecipePageTitleContent.vue";
|
||||
import RecipePageComments from "./RecipePageParts/RecipePageComments.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import RecipePrintContainer from "~/components/Domain/Recipe/RecipePrintContainer.vue";
|
||||
import { EditorMode, PageMode, usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
@ -140,6 +142,11 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
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 api = useUserApi();
|
||||
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);
|
||||
setMode(PageMode.VIEW);
|
||||
if (data?.slug) {
|
||||
router.push("/recipe/" + data.slug);
|
||||
router.push(`/g/${groupSlug.value}/r/` + data.slug);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRecipe() {
|
||||
const { data } = await api.recipes.deleteOne(props.recipe.slug);
|
||||
if (data?.slug) {
|
||||
router.push("/");
|
||||
router.push(`/g/${groupSlug.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** =============================================================
|
||||
* View Preferences
|
||||
*/
|
||||
const { $vuetify } = useContext();
|
||||
|
||||
const landscape = computed(() => {
|
||||
const preferLandscape = props.recipe.settings.landscapeView;
|
||||
@ -283,6 +289,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
user,
|
||||
isOwnGroup,
|
||||
api,
|
||||
scale: ref(1),
|
||||
EDITOR_OPTIONS,
|
||||
|
@ -10,7 +10,7 @@
|
||||
<v-divider class="my-2"></v-divider>
|
||||
<SafeMarkdown :source="recipe.description" />
|
||||
<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
|
||||
v-model="recipe.lastMade"
|
||||
:recipe="recipe"
|
||||
@ -45,9 +45,9 @@
|
||||
:recipe="recipe"
|
||||
:slug="recipe.slug"
|
||||
:recipe-scale="recipeScale"
|
||||
:locked="user.id !== recipe.userId && recipe.settings.locked"
|
||||
:locked="isOwnGroup && user.id !== recipe.userId && recipe.settings.locked"
|
||||
:name="recipe.name"
|
||||
:logged-in="$auth.loggedIn"
|
||||
:logged-in="isOwnGroup"
|
||||
:open="isEditMode"
|
||||
:recipe-id="recipe.id"
|
||||
:show-ocr-button="recipe.isOcrRecipe"
|
||||
@ -64,7 +64,8 @@
|
||||
</template>
|
||||
|
||||
<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 RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue";
|
||||
import RecipeActionMenu from "~/components/Domain/Recipe/RecipeActionMenu.vue";
|
||||
@ -95,17 +96,20 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { $auth, $vuetify } = useContext();
|
||||
const { recipeImage } = useStaticRoutes();
|
||||
const { imageKey, pageMode, editMode, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
|
||||
const { user } = usePageUser();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
const router = useRouter();
|
||||
|
||||
function printRecipe() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
const { $vuetify } = useContext();
|
||||
|
||||
const hideImage = ref(false);
|
||||
const imageHeight = computed(() => {
|
||||
return $vuetify.breakpoint.xs ? "200" : "400";
|
||||
@ -116,7 +120,7 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
function goToOcrEditor() {
|
||||
router.push("/recipe/" + props.recipe.slug + "/ocr-editor");
|
||||
router.push(`/g/${groupSlug.value}/r/${props.recipe.slug}/ocr-editor`);
|
||||
}
|
||||
|
||||
watch(
|
||||
@ -127,6 +131,7 @@ export default defineComponent({
|
||||
);
|
||||
|
||||
return {
|
||||
isOwnGroup,
|
||||
setMode,
|
||||
toggleEditMode,
|
||||
recipeImage,
|
||||
|
@ -34,7 +34,7 @@
|
||||
class="mb-1"
|
||||
:disabled="recipe.settings.disableAmount || hasFoodOrUnit"
|
||||
color="accent"
|
||||
:to="`${recipe.slug}/ingredient-parser`"
|
||||
:to="`/g/${groupSlug}/${recipe.slug}/ingredient-parser`"
|
||||
v-bind="attrs"
|
||||
>
|
||||
<template #icon>
|
||||
@ -54,7 +54,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
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 { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
@ -76,10 +76,13 @@ export default defineComponent({
|
||||
setup(props) {
|
||||
const { user } = usePageUser();
|
||||
const { imageKey } = usePageState(props.recipe.slug);
|
||||
const { i18n } = useContext();
|
||||
const { $auth, i18n } = useContext();
|
||||
|
||||
const drag = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const hasFoodOrUnit = computed(() => {
|
||||
if (!props.recipe) {
|
||||
return false;
|
||||
@ -139,6 +142,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
user,
|
||||
groupSlug,
|
||||
addIngredient,
|
||||
parserToolTip,
|
||||
hasFoodOrUnit,
|
||||
|
@ -26,6 +26,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
||||
import { useToolStore } from "~/composables/store";
|
||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
@ -47,12 +48,14 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const toolStore = useToolStore();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const toolStore = isOwnGroup.value ? useToolStore() : null;
|
||||
const { user } = usePageUser();
|
||||
const { isEditMode } = usePageState(props.recipe.slug);
|
||||
|
||||
function updateTool(index: number) {
|
||||
if (user.id) {
|
||||
if (user.id && toolStore) {
|
||||
toolStore.actions.updateOne(props.recipe.tools[index]);
|
||||
} else {
|
||||
console.log("no user, skipping server update");
|
||||
|
@ -5,7 +5,7 @@
|
||||
{{ recipe.name }}
|
||||
</v-card-title>
|
||||
<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
|
||||
v-model="recipe.lastMade"
|
||||
:recipe="recipe"
|
||||
@ -50,6 +50,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { usePageState, usePageUser } from "~/composables/recipe-page/shared-state";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||
@ -77,12 +78,14 @@ export default defineComponent({
|
||||
setup(props) {
|
||||
const { user } = usePageUser();
|
||||
const { imageKey, isEditMode } = usePageState(props.recipe.slug);
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
return {
|
||||
user,
|
||||
imageKey,
|
||||
validators,
|
||||
isEditMode,
|
||||
isOwnGroup,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div @click.prevent>
|
||||
<v-rating
|
||||
v-model="rating"
|
||||
:readonly="!loggedIn"
|
||||
:readonly="!isOwnGroup"
|
||||
color="secondary"
|
||||
background-color="secondary lighten-3"
|
||||
length="5"
|
||||
@ -18,7 +18,8 @@
|
||||
</template>
|
||||
|
||||
<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";
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@ -45,10 +46,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const { $auth } = useContext();
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const rating = ref(props.value);
|
||||
|
||||
@ -65,7 +63,7 @@ export default defineComponent({
|
||||
context.emit("input", val);
|
||||
}
|
||||
|
||||
return { loggedIn, rating, updateRating };
|
||||
return { isOwnGroup, rating, updateRating };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -13,7 +13,7 @@
|
||||
</template>
|
||||
<v-card
|
||||
hover
|
||||
:to="$listeners.selected || !recipe ? undefined : `/recipe/${recipe.slug}`"
|
||||
:to="$listeners.selected || !recipe ? undefined : `/g/${groupSlug}/r/${recipe.slug}`"
|
||||
class="elevation-12"
|
||||
@click="$emit('selected')"
|
||||
>
|
||||
@ -95,7 +95,7 @@
|
||||
</template>
|
||||
|
||||
<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 RecipeTimelineContextMenu from "./RecipeTimelineContextMenu.vue";
|
||||
import { useStaticRoutes } from "~/composables/api";
|
||||
@ -121,10 +121,13 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const { $globals, $vuetify } = useContext();
|
||||
const { $auth, $globals, $vuetify } = useContext();
|
||||
const { recipeTimelineEventImage } = useStaticRoutes();
|
||||
const timelineEvents = ref([] as RecipeTimelineEventOut[]);
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const useMobileFormat = computed(() => {
|
||||
return $vuetify.breakpoint.smAndDown;
|
||||
});
|
||||
@ -187,6 +190,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
attrs,
|
||||
groupSlug,
|
||||
icon,
|
||||
eventImageUrl,
|
||||
hideImage,
|
||||
|
@ -6,14 +6,14 @@
|
||||
v-model="sidebar"
|
||||
absolute
|
||||
:top-link="topLinks"
|
||||
:secondary-header="$t('sidebar.cookbooks')"
|
||||
:secondary-header-link="loggedIn ? '/group/cookbooks' : undefined"
|
||||
:secondary-header="cookbookLinks.length ? $tc('sidebar.cookbooks') : undefined"
|
||||
:secondary-header-link="isOwnGroup && cookbookLinks.length ? `/g/${groupSlug}/cookbooks` : undefined"
|
||||
:secondary-links="cookbookLinks || []"
|
||||
:bottom-links="isAdmin ? bottomLink : []"
|
||||
:bottom-links="isAdmin ? bottomLinks : []"
|
||||
>
|
||||
<v-menu offset-y nudge-bottom="5" close-delay="50" nudge-right="15">
|
||||
<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">
|
||||
{{ $globals.icons.createAlt }}
|
||||
</v-icon>
|
||||
@ -23,7 +23,7 @@
|
||||
<v-list dense class="my-0 py-0">
|
||||
<template v-for="(item, index) in createLinks">
|
||||
<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-icon>
|
||||
{{ item.icon }}
|
||||
@ -64,7 +64,7 @@
|
||||
</template>
|
||||
</AppSidebar>
|
||||
|
||||
<AppHeader :menu="loggedIn">
|
||||
<AppHeader>
|
||||
<v-btn icon @click.stop="sidebar = !sidebar">
|
||||
<v-icon> {{ $globals.icons.menu }}</v-icon>
|
||||
</v-btn>
|
||||
@ -79,6 +79,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
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 AppSidebar from "@/components/Layout/LayoutParts/AppSidebar.vue";
|
||||
import { SidebarLinks } from "~/types/application-types";
|
||||
@ -91,13 +92,12 @@
|
||||
components: { AppHeader, AppSidebar, LanguageDialog, TheSnackbar },
|
||||
setup() {
|
||||
const { $globals, $auth, $vuetify, i18n } = useContext();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const isAdmin = computed(() => $auth.user?.admin);
|
||||
const loggedIn = computed(() => $auth.loggedIn);
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = route.value.params.groupSlug;
|
||||
const { cookbooks } = loggedIn.value ? useCookbooks() : usePublicCookbooks(groupSlug);
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
const { cookbooks } = isOwnGroup.value ? useCookbooks() : usePublicCookbooks(groupSlug.value || "");
|
||||
|
||||
const toggleDark = useToggleDarkMode();
|
||||
|
||||
@ -115,7 +115,7 @@
|
||||
return {
|
||||
icon: $globals.icons.pages,
|
||||
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;
|
||||
}
|
||||
|
||||
const createLinks: Link[] = [
|
||||
const createLinks = computed<Link[]>(() => [
|
||||
{
|
||||
insertDivider: false,
|
||||
icon: $globals.icons.link,
|
||||
title: i18n.tc("general.import"),
|
||||
subtitle: i18n.tc("new-recipe.import-by-url"),
|
||||
to: "/recipe/create/url",
|
||||
to: `/g/${groupSlug.value}/r/create/url`,
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
@ -143,7 +143,7 @@
|
||||
icon: $globals.icons.edit,
|
||||
title: i18n.tc("general.create"),
|
||||
subtitle: i18n.tc("new-recipe.create-manually"),
|
||||
to: "/recipe/create/new",
|
||||
to: `/g/${groupSlug.value}/r/create/new`,
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
@ -151,24 +151,24 @@
|
||||
icon: $globals.icons.pages,
|
||||
title: i18n.tc("sidebar.cookbook"),
|
||||
subtitle: i18n.tc("sidebar.create-cookbook"),
|
||||
to: "/group/cookbooks",
|
||||
to: `/g/${groupSlug.value}/cookbooks`,
|
||||
restricted: true,
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
const bottomLinks: SidebarLinks = [
|
||||
const bottomLinks = computed<SidebarLinks>(() => [
|
||||
{
|
||||
icon: $globals.icons.cog,
|
||||
title: i18n.tc("general.settings"),
|
||||
to: "/admin/site-settings",
|
||||
restricted: true,
|
||||
},
|
||||
];
|
||||
]);
|
||||
|
||||
const topLinks: SidebarLinks = [
|
||||
const topLinks = computed<SidebarLinks>(() => [
|
||||
{
|
||||
icon: $globals.icons.search,
|
||||
to: "/",
|
||||
to: `/g/${groupSlug.value}`,
|
||||
title: i18n.tc("sidebar.search"),
|
||||
restricted: true,
|
||||
},
|
||||
@ -187,30 +187,41 @@
|
||||
{
|
||||
icon: $globals.icons.timelineText,
|
||||
title: i18n.tc("recipe.timeline"),
|
||||
to: "/group/timeline",
|
||||
to: `/g/${groupSlug.value}/recipes/timeline`,
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.categories,
|
||||
to: "/recipes/categories",
|
||||
to: `/g/${groupSlug.value}/recipes/categories`,
|
||||
title: i18n.tc("sidebar.categories"),
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.tags,
|
||||
to: "/recipes/tags",
|
||||
to: `/g/${groupSlug.value}/recipes/tags`,
|
||||
title: i18n.tc("sidebar.tags"),
|
||||
restricted: true,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.potSteam,
|
||||
to: "/recipes/tools",
|
||||
to: `/g/${groupSlug.value}/recipes/tools`,
|
||||
title: i18n.tc("tool.tools"),
|
||||
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>
|
||||
|
@ -35,7 +35,7 @@
|
||||
<v-btn v-else icon @click="activateSearch">
|
||||
<v-icon> {{ $globals.icons.search }}</v-icon>
|
||||
</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>
|
||||
{{ $vuetify.breakpoint.smAndUp ? $t("user.logout") : "" }}
|
||||
</v-btn>
|
||||
@ -49,6 +49,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
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";
|
||||
|
||||
export default defineComponent({
|
||||
@ -61,14 +62,11 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const { $auth } = useContext();
|
||||
const { loggedIn } = useLoggedInState();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const loggedIn = computed(() => {
|
||||
return $auth.loggedIn;
|
||||
});
|
||||
|
||||
const groupSlug = route.value.params.groupSlug;
|
||||
const routerLink = !loggedIn.value && groupSlug ? `/explore/recipes/${groupSlug}` : "/"
|
||||
const routerLink = computed(() => groupSlug.value ? `/g/${groupSlug.value}` : "/");
|
||||
const domSearchDialog = ref<InstanceType<typeof RecipeDialogSearch> | null>(null);
|
||||
|
||||
function activateSearch() {
|
||||
@ -95,6 +93,7 @@ export default defineComponent({
|
||||
activateSearch,
|
||||
domSearchDialog,
|
||||
routerLink,
|
||||
loggedIn,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<v-navigation-drawer v-model="drawer" class="d-flex flex-column d-print-none" clipped app width="240px">
|
||||
<!-- User Profile -->
|
||||
<template v-if="$auth.user">
|
||||
<v-list-item two-line to="/user/profile" exact>
|
||||
<template v-if="loggedIn">
|
||||
<v-list-item two-line :to="userProfileLink" exact>
|
||||
<UserAvatar list :user-id="$auth.user.id" />
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="pr-2"> {{ $auth.user.fullName }}</v-list-item-title>
|
||||
<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>
|
||||
{{ $globals.icons.heart }}
|
||||
</v-icon>
|
||||
@ -26,7 +26,7 @@
|
||||
<template v-if="topLink">
|
||||
<v-list nav dense>
|
||||
<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 -->
|
||||
<v-list-group
|
||||
v-if="nav.children"
|
||||
@ -69,13 +69,20 @@
|
||||
|
||||
<!-- Secondary Links -->
|
||||
<template v-if="secondaryLinks">
|
||||
<v-subheader v-if="secondaryHeader" :to="secondaryHeaderLink" class="pb-0">
|
||||
{{ secondaryHeader }}
|
||||
</v-subheader>
|
||||
<v-divider></v-divider>
|
||||
<router-link v-if="secondaryHeader && secondaryHeaderLink" :to="secondaryHeaderLink" style="text-decoration: none;">
|
||||
<v-subheader :to="secondaryHeaderLink" class="pb-0">
|
||||
{{ secondaryHeader }}
|
||||
</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>
|
||||
<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 -->
|
||||
<v-list-group
|
||||
v-if="nav.children"
|
||||
@ -116,7 +123,7 @@
|
||||
<v-list nav dense>
|
||||
<v-list-item-group v-model="bottomSelected" color="primary">
|
||||
<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
|
||||
:key="nav.title"
|
||||
exact
|
||||
@ -141,6 +148,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
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 UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||
|
||||
@ -198,7 +206,10 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
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({
|
||||
dropDowns: {},
|
||||
@ -210,8 +221,11 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
userFavoritesLink,
|
||||
userProfileLink,
|
||||
drawer,
|
||||
loggedIn,
|
||||
isOwnGroup,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -37,8 +37,8 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
setup(_, context) {
|
||||
const router = useRouter();
|
||||
const { i18n } = useContext();
|
||||
const router = useRouter();
|
||||
|
||||
const headers = [
|
||||
{ text: i18n.t("category.category"), value: "category" },
|
||||
@ -49,7 +49,7 @@ export default defineComponent({
|
||||
];
|
||||
|
||||
function handleRowClick(item: ReportSummary) {
|
||||
router.push("/group/reports/" + item.id);
|
||||
router.push(`/group/reports/${item.id}`);
|
||||
}
|
||||
|
||||
function capitalize(str: string) {
|
||||
|
@ -151,12 +151,12 @@ export function usePageUser(): { user: UserOut } {
|
||||
id: "",
|
||||
group: "",
|
||||
groupId: "",
|
||||
groupSlug: "",
|
||||
cacheKey: "",
|
||||
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 as UserOut };
|
||||
return { user: $auth.user };
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Ref, ref } from "@nuxtjs/composition-api";
|
||||
import { watchDebounced } from "@vueuse/core";
|
||||
import { UserApi } from "~/lib/api";
|
||||
import { ExploreApi } from "~/lib/api/public/explore";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
|
||||
export interface UseRecipeSearchReturn {
|
||||
@ -17,7 +18,7 @@ export interface UseRecipeSearchReturn {
|
||||
* on the query. Useful for searchable list views. For advanced
|
||||
* search, use the `useRecipeQuery` composable.
|
||||
*/
|
||||
export function useRecipeSearch(api: UserApi): UseRecipeSearchReturn {
|
||||
export function useRecipeSearch(api: UserApi | ExploreApi): UseRecipeSearchReturn {
|
||||
const query = ref("");
|
||||
const error = ref("");
|
||||
const loading = ref(false);
|
||||
|
@ -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 { usePublicExploreApi } from "~/composables/api/api-client";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
@ -9,6 +9,8 @@ export const allRecipes = ref<Recipe[]>([]);
|
||||
export const recentRecipes = ref<Recipe[]>([]);
|
||||
|
||||
export const useLazyRecipes = function (publicGroupSlug: string | null = null) {
|
||||
const router = useRouter();
|
||||
|
||||
// passing the group slug switches to using the public API
|
||||
const api = publicGroupSlug ? usePublicExploreApi(publicGroupSlug).explore : useUserApi();
|
||||
|
||||
@ -23,7 +25,7 @@ export const useLazyRecipes = function (publicGroupSlug: 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,
|
||||
orderDirection,
|
||||
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,
|
||||
queryFilter,
|
||||
});
|
||||
|
||||
if (error?.response?.status === 404) {
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
return data ? data.items : [];
|
||||
}
|
||||
|
||||
|
17
frontend/composables/use-logged-in-state.ts
Normal file
17
frontend/composables/use-logged-in-state.ts
Normal 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 };
|
||||
}
|
@ -9,6 +9,5 @@ import DefaultLayout from "@/components/Layout/DefaultLayout.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: { DefaultLayout },
|
||||
middleware: "auth",
|
||||
});
|
||||
</script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="ready">
|
||||
<v-card-title>
|
||||
<slot>
|
||||
<h1 class="mx-auto">{{ $t("page.404-page-not-found") }}</h1>
|
||||
@ -28,7 +28,7 @@
|
||||
</template>
|
||||
|
||||
<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({
|
||||
layout: "basic",
|
||||
@ -39,7 +39,48 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
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({
|
||||
title:
|
||||
@ -54,6 +95,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
buttons,
|
||||
ready,
|
||||
};
|
||||
},
|
||||
// Needed for useMeta
|
||||
|
@ -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>
|
@ -1,6 +1,8 @@
|
||||
import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
|
||||
import { route } from "../../base";
|
||||
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";
|
||||
|
||||
@ -16,4 +18,8 @@ export class PublicRecipeApi extends BaseCRUDAPIReadOnly<Recipe> {
|
||||
constructor(requests: ApiRequestInstance, private readonly groupSlug: string) {
|
||||
super(requests);
|
||||
}
|
||||
|
||||
async search(rsq: RecipeSearchQuery) {
|
||||
return await this.requests.get<PaginationData<Recipe>>(route(routes.recipesGroupSlug(this.groupSlug), rsq));
|
||||
}
|
||||
}
|
||||
|
@ -66,6 +66,7 @@ export interface UserOut {
|
||||
canOrganize?: boolean;
|
||||
id: string;
|
||||
groupId: string;
|
||||
groupSlug: string;
|
||||
tokens?: LongLiveTokenOut[];
|
||||
cacheKey: string;
|
||||
}
|
||||
@ -113,6 +114,7 @@ export interface PrivateUser {
|
||||
canOrganize?: boolean;
|
||||
id: string;
|
||||
groupId: string;
|
||||
groupSlug: string;
|
||||
tokens?: LongLiveTokenOut[];
|
||||
cacheKey: string;
|
||||
password: string;
|
||||
|
@ -342,7 +342,7 @@ export default {
|
||||
background_color: "#FFFFFF",
|
||||
display: "standalone",
|
||||
share_target: {
|
||||
action: "/recipe/create/url",
|
||||
action: "/r/create/url",
|
||||
method: "GET",
|
||||
params: {
|
||||
/* title and url are not currently used in Mealie. If there are issues
|
||||
|
@ -6,7 +6,7 @@
|
||||
"dev": "nuxt",
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start",
|
||||
"generate": "nuxt generate",
|
||||
"generate": "nuxt generate --spa",
|
||||
"lint:js": "eslint --ext \".ts,.js,.vue\" --ignore-path .gitignore .",
|
||||
"lint": "yarn lint:js",
|
||||
"test": "vitest",
|
||||
|
@ -105,13 +105,13 @@
|
||||
</section>
|
||||
</section>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<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 { AllBackups } from "~/lib/api/types/admin";
|
||||
|
||||
@ -119,6 +119,8 @@ export default defineComponent({
|
||||
layout: "admin",
|
||||
setup() {
|
||||
const { i18n, $auth } = useContext();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const adminApi = useAdminApi();
|
||||
const selected = ref("");
|
||||
@ -192,6 +194,7 @@ export default defineComponent({
|
||||
onMounted(refreshBackups);
|
||||
|
||||
return {
|
||||
groupSlug,
|
||||
restoreBackup,
|
||||
selected,
|
||||
...toRefs(state),
|
||||
|
@ -77,7 +77,7 @@ export default defineComponent({
|
||||
const refUserDialog = ref();
|
||||
const { $auth } = useContext();
|
||||
|
||||
const user = computed(() => $auth.user as UserOut | null);
|
||||
const user = computed(() => $auth.user);
|
||||
|
||||
const { $globals, i18n } = useContext();
|
||||
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -90,14 +90,22 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { defineComponent, useRouter } from "@nuxtjs/composition-api";
|
||||
import draggable from "vuedraggable";
|
||||
import { useCookbooks } from "@/composables/use-group-cookbooks";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: { draggable, RecipeOrganizerSelector },
|
||||
setup() {
|
||||
const { isOwnGroup, loggedIn } = useLoggedInState();
|
||||
const router = useRouter();
|
||||
|
||||
if (!(loggedIn.value && isOwnGroup.value)) {
|
||||
router.back();
|
||||
}
|
||||
|
||||
const { cookbooks, actions } = useCookbooks();
|
||||
|
||||
return {
|
14
frontend/pages/g/_groupSlug/index.vue
Normal file
14
frontend/pages/g/_groupSlug/index.vue
Normal 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>
|
59
frontend/pages/g/_groupSlug/r/_slug/index.vue
Normal file
59
frontend/pages/g/_groupSlug/r/_slug/index.vue
Normal 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>
|
@ -94,7 +94,7 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
CreateIngredientFood,
|
||||
@ -124,9 +124,12 @@ export default defineComponent({
|
||||
RecipeIngredientEditor,
|
||||
},
|
||||
setup() {
|
||||
const { $auth } = useContext();
|
||||
const panels = ref<number[]>([]);
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const router = useRouter();
|
||||
const slug = route.value.params.slug;
|
||||
const api = useUserApi();
|
||||
@ -324,7 +327,7 @@ export default defineComponent({
|
||||
const { response } = await api.recipes.updateOne(recipe.value.slug, recipe.value);
|
||||
|
||||
if (response?.status === 200) {
|
||||
router.push("/recipe/" + recipe.value.slug);
|
||||
router.push(`/g/${groupSlug.value}/r/${recipe.value.slug}`);
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
<AdvancedOnly>
|
||||
<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>
|
||||
</AdvancedOnly>
|
||||
</div>
|
||||
@ -34,7 +34,7 @@ import AdvancedOnly from "~/components/global/AdvancedOnly.vue";
|
||||
export default defineComponent({
|
||||
components: { AdvancedOnly },
|
||||
setup() {
|
||||
const { $globals, i18n } = useContext();
|
||||
const { $auth, $globals, i18n } = useContext();
|
||||
|
||||
const subpages: MenuItem[] = [
|
||||
{
|
||||
@ -71,10 +71,11 @@ export default defineComponent({
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const subpage = computed({
|
||||
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() {
|
||||
return route.value.path.split("/").pop() ?? "url";
|
||||
@ -82,6 +83,7 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
return {
|
||||
groupSlug,
|
||||
subpages,
|
||||
subpage,
|
||||
};
|
@ -10,7 +10,7 @@ export default defineComponent({
|
||||
const router = useRouter();
|
||||
onMounted(() => {
|
||||
// Force redirect to first valid page
|
||||
router.replace("/recipe/create/url");
|
||||
router.replace("/r/create/url");
|
||||
});
|
||||
return {};
|
||||
},
|
@ -35,7 +35,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<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 { useUserApi } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
@ -47,6 +47,10 @@ export default defineComponent({
|
||||
error: false,
|
||||
loading: false,
|
||||
});
|
||||
const { $auth } = useContext();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const api = useUserApi();
|
||||
const router = useRouter();
|
||||
|
||||
@ -56,7 +60,7 @@ export default defineComponent({
|
||||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
router.push(`/recipe/${response.data}?edit=${edit.toString()}`);
|
||||
router.push(`/g/${groupSlug.value}/r/${response.data}?edit=${edit.toString()}`);
|
||||
}
|
||||
|
||||
const newRecipeName = ref("");
|
@ -32,7 +32,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<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 { useUserApi } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
@ -45,6 +45,10 @@ export default defineComponent({
|
||||
loading: false,
|
||||
makeFileRecipeImage: false,
|
||||
});
|
||||
const { $auth } = useContext();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const api = useUserApi();
|
||||
const router = useRouter();
|
||||
|
||||
@ -56,7 +60,7 @@ export default defineComponent({
|
||||
state.loading = false;
|
||||
return;
|
||||
}
|
||||
router.push(`/recipe/${response.data}/ocr-editor`);
|
||||
router.push(`/g/${groupSlug.value}/r/${response.data}/ocr-editor`);
|
||||
}
|
||||
|
||||
const domCreateByOcr = ref<VForm | null>(null);
|
@ -69,6 +69,7 @@ import {
|
||||
ref,
|
||||
useRouter,
|
||||
computed,
|
||||
useContext,
|
||||
useRoute,
|
||||
onMounted,
|
||||
} from "@nuxtjs/composition-api";
|
||||
@ -85,8 +86,11 @@ export default defineComponent({
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const { $auth } = useContext();
|
||||
const api = useUserApi();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const router = useRouter();
|
||||
const tags = useTagStore();
|
||||
|
||||
@ -99,7 +103,7 @@ export default defineComponent({
|
||||
if (refreshTags) {
|
||||
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({
|
@ -30,7 +30,7 @@
|
||||
</template>
|
||||
|
||||
<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 { useUserApi } from "~/composables/api";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
@ -41,6 +41,10 @@ export default defineComponent({
|
||||
error: false,
|
||||
loading: false,
|
||||
});
|
||||
const { $auth } = useContext();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const api = useUserApi();
|
||||
const router = useRouter();
|
||||
|
||||
@ -50,7 +54,7 @@ export default defineComponent({
|
||||
state.loading = false;
|
||||
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);
|
@ -7,7 +7,7 @@
|
||||
</template>
|
||||
|
||||
<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 { usePublicApi } from "~/composables/api/api-client";
|
||||
|
||||
@ -15,7 +15,10 @@ export default defineComponent({
|
||||
components: { RecipePage },
|
||||
layout: "basic",
|
||||
setup() {
|
||||
const { $auth } = useContext();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const router = useRouter();
|
||||
const recipeId = route.value.params.id;
|
||||
const api = usePublicApi();
|
||||
@ -27,7 +30,7 @@ export default defineComponent({
|
||||
|
||||
if (error) {
|
||||
console.error("error loading recipe -> ", error);
|
||||
router.push("/");
|
||||
router.push(`/g/${groupSlug.value}`);
|
||||
}
|
||||
|
||||
if (data) {
|
@ -46,7 +46,9 @@ export default defineComponent({
|
||||
labels: i18n.tc("data-pages.labels.labels"),
|
||||
};
|
||||
|
||||
const DATA_TYPE_OPTIONS = [
|
||||
const route = useRoute();
|
||||
|
||||
const DATA_TYPE_OPTIONS = computed(() => [
|
||||
{
|
||||
text: i18n.t("general.recipes"),
|
||||
value: "new",
|
||||
@ -67,9 +69,7 @@ export default defineComponent({
|
||||
value: "new",
|
||||
to: "/group/data/labels",
|
||||
},
|
||||
];
|
||||
|
||||
const route = useRoute();
|
||||
]);
|
||||
|
||||
const buttonText = computed(() => {
|
||||
const last = route.value.path.split("/").pop();
|
||||
|
@ -26,10 +26,10 @@
|
||||
|
||||
<div class="d-flex align-center justify-space-between mb-2">
|
||||
<v-tabs>
|
||||
<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/view`">{{ $t('meal-plan.meal-planner') }}</v-tab>
|
||||
<v-tab :to="`/group/mealplan/planner/edit`">{{ $t('general.edit') }}</v-tab>
|
||||
</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>
|
||||
|
@ -14,7 +14,7 @@
|
||||
</template>
|
||||
</i18n>
|
||||
<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>
|
||||
</BasePageTitle>
|
||||
<v-data-table
|
||||
|
@ -1,29 +1,21 @@
|
||||
<template>
|
||||
<div v-if="groupSlug">
|
||||
<RecipeExplorerPage :group-slug="groupSlug" />
|
||||
</div>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import { invoke } from "@vueuse/core";
|
||||
import { useUserApi } from "~/composables/api/api-client";
|
||||
import RecipeExplorerPage from "~/components/Domain/Recipe/RecipeExplorerPage.vue";
|
||||
|
||||
import { computed, defineComponent, useContext, useRouter } from "@nuxtjs/composition-api";
|
||||
export default defineComponent({
|
||||
components: { RecipeExplorerPage },
|
||||
layout: "blank",
|
||||
setup() {
|
||||
const api = useUserApi();
|
||||
const groupSlug = ref<string>();
|
||||
const { $auth } = useContext();
|
||||
const router = useRouter();
|
||||
const groupSlug = computed(() => $auth.user?.groupSlug);
|
||||
|
||||
invoke(async () => {
|
||||
const { data } = await api.users.getSelfGroup();
|
||||
groupSlug.value = data?.slug;
|
||||
});
|
||||
|
||||
return {
|
||||
groupSlug,
|
||||
};
|
||||
},
|
||||
if (groupSlug.value) {
|
||||
router.push(`/g/${groupSlug.value}`);
|
||||
} else {
|
||||
router.push("/login");
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -103,6 +103,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, useContext, computed, reactive, useRouter } from "@nuxtjs/composition-api";
|
||||
import { useDark, whenever } from "@vueuse/core";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useAppInfo } from "~/composables/api";
|
||||
import { usePasswordField } from "~/composables/use-passwords";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
@ -115,11 +116,13 @@ export default defineComponent({
|
||||
|
||||
const router = useRouter();
|
||||
const { $auth, i18n } = useContext();
|
||||
const { loggedIn } = useLoggedInState();
|
||||
const groupSlug = computed(() => $auth.user?.groupSlug);
|
||||
|
||||
whenever(
|
||||
() => $auth.loggedIn,
|
||||
() => loggedIn.value && groupSlug.value,
|
||||
() => {
|
||||
router.push("/");
|
||||
router.push(`/g/${groupSlug.value || ""}`);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
@ -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>
|
@ -199,7 +199,7 @@
|
||||
|
||||
<v-lazy>
|
||||
<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>
|
||||
</v-lazy>
|
||||
</v-container>
|
||||
@ -236,6 +236,7 @@ export default defineComponent({
|
||||
ShoppingListItemEditor,
|
||||
},
|
||||
setup() {
|
||||
const { $auth, i18n } = useContext();
|
||||
const preferences = useShoppingListPreferences();
|
||||
|
||||
const { idle } = useIdle(5 * 60 * 1000) // 5 minutes
|
||||
@ -247,10 +248,9 @@ export default defineComponent({
|
||||
const reorderLabelsDialog = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
const id = route.value.params.id;
|
||||
|
||||
const { i18n } = useContext();
|
||||
|
||||
// ===============================================================
|
||||
// Shopping List Actions
|
||||
|
||||
@ -755,6 +755,7 @@ export default defineComponent({
|
||||
deleteListItem,
|
||||
edit,
|
||||
getLabelColor,
|
||||
groupSlug,
|
||||
itemsByLabel,
|
||||
listItems,
|
||||
loadingCounter,
|
||||
|
@ -33,19 +33,22 @@
|
||||
</v-card>
|
||||
</section>
|
||||
<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>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<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 { useAsyncKey } from "~/composables/use-utils";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { $auth } = useContext();
|
||||
const userApi = useUserApi();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.value.params.groupSlug || $auth.user?.groupSlug || "");
|
||||
|
||||
const state = reactive({
|
||||
createName: "",
|
||||
@ -95,6 +98,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
groupSlug,
|
||||
shoppingLists,
|
||||
createOne,
|
||||
deleteOne,
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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>
|
||||
</v-container>
|
||||
</template>
|
||||
@ -8,6 +8,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api";
|
||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
|
||||
@ -16,6 +17,7 @@ export default defineComponent({
|
||||
setup() {
|
||||
const api = useUserApi();
|
||||
const route = useRoute();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
|
||||
const userId = route.value.params.id;
|
||||
|
||||
@ -26,6 +28,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
user,
|
||||
isOwnGroup,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
|
||||
@ -16,6 +16,6 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
</style>
|
||||
|
@ -108,9 +108,9 @@
|
||||
:label="$t('profile.show-advanced-description')"
|
||||
@change="updateUser"
|
||||
></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">
|
||||
<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>
|
||||
{{ $globals.icons.backArrow }}
|
||||
</v-icon>
|
||||
|
@ -16,17 +16,6 @@
|
||||
</v-icon>
|
||||
{{ $t('profile.get-invite-link') }}
|
||||
</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>
|
||||
<div v-show="generatedSignupLink !== ''">
|
||||
<v-card-text>
|
||||
@ -114,7 +103,7 @@
|
||||
<v-row tag="section">
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<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')"
|
||||
>
|
||||
<template #title> {{ $t('profile.user-settings') }} </template>
|
||||
@ -124,7 +113,7 @@
|
||||
<AdvancedOnly>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<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')"
|
||||
>
|
||||
<template #title> {{ $t('settings.token.api-tokens') }} </template>
|
||||
@ -143,7 +132,7 @@
|
||||
<v-row tag="section">
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<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')"
|
||||
>
|
||||
<template #title> {{ $t('profile.group-settings') }} </template>
|
||||
@ -152,7 +141,7 @@
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<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')"
|
||||
>
|
||||
<template #title> {{ $t('sidebar.cookbooks') }} </template>
|
||||
@ -161,7 +150,7 @@
|
||||
</v-col>
|
||||
<v-col v-if="user.canManage" cols="12" sm="12" md="6">
|
||||
<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')"
|
||||
>
|
||||
<template #title> {{ $t('profile.members') }} </template>
|
||||
@ -171,7 +160,7 @@
|
||||
<AdvancedOnly>
|
||||
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
|
||||
<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')"
|
||||
>
|
||||
<template #title> {{ $t('settings.webhooks.webhooks') }} </template>
|
||||
@ -182,7 +171,7 @@
|
||||
<AdvancedOnly>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<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')"
|
||||
>
|
||||
<template #title> {{ $t('profile.notifiers') }} </template>
|
||||
@ -193,7 +182,7 @@
|
||||
<AdvancedOnly>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<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')"
|
||||
>
|
||||
<template #title> {{ $t('profile.manage-data') }} </template>
|
||||
@ -204,7 +193,7 @@
|
||||
<AdvancedOnly>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<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')"
|
||||
>
|
||||
<template #title>{{ $t('profile.data-migrations') }} </template>
|
||||
@ -218,7 +207,7 @@
|
||||
</template>
|
||||
|
||||
<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 UserProfileLinkCard from "@/components/Domain/User/UserProfileLinkCard.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
@ -239,6 +228,8 @@ export default defineComponent({
|
||||
scrollToTop: true,
|
||||
setup() {
|
||||
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
|
||||
const user = computed<UserOut | null>(() => $auth.user);
|
||||
@ -261,14 +252,6 @@ export default defineComponent({
|
||||
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() {
|
||||
const { data } = await api.groups.createInvitation({ uses: 1 });
|
||||
if (data) {
|
||||
@ -350,16 +333,16 @@ export default defineComponent({
|
||||
return iconText[key] ?? $globals.icons.primary;
|
||||
}
|
||||
|
||||
const statsTo: { [key: string]: string } = {
|
||||
totalRecipes: "/",
|
||||
const statsTo = computed<{ [key: string]: string }>(() => { return {
|
||||
totalRecipes: `/g/${groupSlug.value}/`,
|
||||
totalUsers: "/group/members",
|
||||
totalCategories: "/recipes/categories",
|
||||
totalTags: "/recipes/tags",
|
||||
totalTools: "/recipes/tools",
|
||||
};
|
||||
totalCategories: `/g/${groupSlug.value}/recipes/categories`,
|
||||
totalTags: `/g/${groupSlug.value}/recipes/tags`,
|
||||
totalTools: `/g/${groupSlug.value}/recipes/tools`,
|
||||
}});
|
||||
|
||||
function getStatsTo(key: string) {
|
||||
return statsTo[key] ?? "unknown";
|
||||
return statsTo.value[key] ?? "unknown";
|
||||
}
|
||||
|
||||
const storage = useAsync(async () => {
|
||||
@ -386,6 +369,7 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
return {
|
||||
groupSlug,
|
||||
storageText,
|
||||
storageUsedPercentage,
|
||||
getStatsTitle,
|
||||
@ -399,7 +383,6 @@ export default defineComponent({
|
||||
showPublicLink,
|
||||
publicLink,
|
||||
getSignupLink,
|
||||
getPublicLink,
|
||||
sendInvite,
|
||||
validators,
|
||||
validEmail,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Plugin } from "@nuxt/types";
|
||||
import { Auth } from "@nuxtjs/auth-next";
|
||||
import { Auth as NuxtAuth } from "@nuxtjs/auth-next";
|
||||
import { Framework } from "vuetify";
|
||||
import { UserOut } from "~/lib/api/types/user";
|
||||
import { icons } from "~/lib/icons";
|
||||
import { Icon } from "~/lib/icons/icon-type";
|
||||
|
||||
@ -15,6 +16,11 @@ declare module "vue/types/vue" {
|
||||
}
|
||||
|
||||
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 {
|
||||
$globals: Globals;
|
||||
$vuetify: Framework;
|
||||
|
@ -58,7 +58,7 @@ async def get_public_group(group_slug: str = fastapi.Path(...), session=Depends(
|
||||
repos = get_repositories(session)
|
||||
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")
|
||||
else:
|
||||
return group
|
||||
|
@ -3,6 +3,7 @@ from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
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 mealie.core.config import get_app_settings
|
||||
@ -94,6 +95,10 @@ class User(SqlAlchemyBase, BaseMixins):
|
||||
"group",
|
||||
}
|
||||
|
||||
@hybrid_property
|
||||
def group_slug(self) -> str:
|
||||
return self.group.slug
|
||||
|
||||
@auto_init()
|
||||
def __init__(self, session, full_name, password, group: str | None = None, **kwargs) -> None:
|
||||
if group is None:
|
||||
|
@ -187,7 +187,7 @@ class RecipeController(BaseRecipeController):
|
||||
message=self.t(
|
||||
"notifications.generic-created-with-url",
|
||||
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(
|
||||
"notifications.generic-created-with-url",
|
||||
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(
|
||||
"notifications.generic-updated-with-url",
|
||||
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(
|
||||
"notifications.generic-updated-with-url",
|
||||
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(
|
||||
"notifications.generic-updated-with-url",
|
||||
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),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -72,7 +72,7 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
message=self.t(
|
||||
"notifications.generic-updated-with-url",
|
||||
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(
|
||||
"notifications.generic-updated-with-url",
|
||||
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(
|
||||
"notifications.generic-updated-with-url",
|
||||
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(
|
||||
"notifications.generic-updated-with-url",
|
||||
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),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -33,9 +33,9 @@ __app_settings = get_app_settings()
|
||||
__contents = ""
|
||||
|
||||
|
||||
def content_with_meta(recipe: Recipe) -> str:
|
||||
def content_with_meta(group_slug: str, recipe: Recipe) -> str:
|
||||
# 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}"
|
||||
|
||||
ingredients: list[str] = []
|
||||
@ -122,28 +122,29 @@ def serve_recipe_with_meta_public(
|
||||
return response_404()
|
||||
|
||||
# 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:
|
||||
return response_404()
|
||||
|
||||
|
||||
async def serve_recipe_with_meta(
|
||||
slug: str,
|
||||
user: PrivateUser = Depends(try_get_current_user),
|
||||
group_slug: str,
|
||||
recipe_slug: str,
|
||||
user: PrivateUser | None = Depends(try_get_current_user),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
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:
|
||||
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:
|
||||
return response_404()
|
||||
|
||||
# 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:
|
||||
return response_404()
|
||||
|
||||
@ -155,6 +156,5 @@ def mount_spa(app: FastAPI):
|
||||
global __contents
|
||||
__contents = pathlib.Path(__app_settings.STATIC_FILES).joinpath("index.html").read_text()
|
||||
|
||||
app.get("/recipe/{slug}")(serve_recipe_with_meta)
|
||||
app.get("/explore/recipes/{group_slug}/{recipe_slug}")(serve_recipe_with_meta_public)
|
||||
app.get("/g/{group_slug}/r/{recipe_slug}")(serve_recipe_with_meta)
|
||||
app.mount("/", SPAStaticFiles(directory=__app_settings.STATIC_FILES, html=True), name="spa")
|
||||
|
@ -104,6 +104,7 @@ class UserOut(UserBase):
|
||||
id: UUID4
|
||||
group: str
|
||||
group_id: UUID4
|
||||
group_slug: str
|
||||
tokens: list[LongLiveTokenOut] | None
|
||||
cache_key: str
|
||||
favorite_recipes: list[str] | None = []
|
||||
|
@ -11,9 +11,9 @@ def _base_or(base_url: str | None) -> str:
|
||||
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)
|
||||
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:
|
||||
|
@ -24,6 +24,7 @@ def test_get_all_cookbooks(
|
||||
assert group and group.preferences
|
||||
|
||||
group.preferences.private_group = is_private_group
|
||||
group.preferences.recipe_public = not is_private_group
|
||||
database.group_preferences.update(group.id, group.preferences)
|
||||
|
||||
## Set Up Cookbooks
|
||||
@ -88,6 +89,7 @@ def test_get_one_cookbook(
|
||||
assert group and group.preferences
|
||||
|
||||
group.preferences.private_group = is_private_group
|
||||
group.preferences.recipe_public = not is_private_group
|
||||
database.group_preferences.update(group.id, group.preferences)
|
||||
|
||||
## Set Up Cookbook
|
||||
@ -116,6 +118,7 @@ def test_get_cookbooks_with_recipes(api_client: TestClient, unique_user: TestUse
|
||||
assert group and group.preferences
|
||||
|
||||
group.preferences.private_group = False
|
||||
group.preferences.recipe_public = True
|
||||
database.group_preferences.update(group.id, group.preferences)
|
||||
|
||||
tag = database.tags.create(TagSave(name=random_string(), group_id=unique_user.group_id))
|
||||
|
@ -20,6 +20,7 @@ def test_get_all_foods(
|
||||
assert group and group.preferences
|
||||
|
||||
group.preferences.private_group = is_private_group
|
||||
group.preferences.recipe_public = not is_private_group
|
||||
database.group_preferences.update(group.id, group.preferences)
|
||||
|
||||
## Set Up Foods
|
||||
@ -53,6 +54,7 @@ def test_get_one_food(
|
||||
assert group and group.preferences
|
||||
|
||||
group.preferences.private_group = is_private_group
|
||||
group.preferences.recipe_public = not is_private_group
|
||||
database.group_preferences.update(group.id, group.preferences)
|
||||
|
||||
## Set Up Food
|
||||
|
@ -48,6 +48,7 @@ def test_get_all_organizers(
|
||||
assert group and group.preferences
|
||||
|
||||
group.preferences.private_group = is_private_group
|
||||
group.preferences.recipe_public = not is_private_group
|
||||
database.group_preferences.update(group.id, group.preferences)
|
||||
|
||||
## Set Up Organizers
|
||||
@ -113,6 +114,7 @@ def test_get_one_organizer(
|
||||
assert group and group.preferences
|
||||
|
||||
group.preferences.private_group = is_private_group
|
||||
group.preferences.recipe_public = not is_private_group
|
||||
database.group_preferences.update(group.id, group.preferences)
|
||||
|
||||
## Set Up Organizer
|
||||
|
@ -32,6 +32,7 @@ def test_get_all_public_recipes(
|
||||
assert group and group.preferences
|
||||
|
||||
group.preferences.private_group = is_private_group
|
||||
group.preferences.recipe_public = not is_private_group
|
||||
database.group_preferences.update(group.id, group.preferences)
|
||||
|
||||
default_recipes = database.recipes.create_many(
|
||||
@ -106,6 +107,7 @@ def test_get_all_public_recipes_filtered(
|
||||
assert group and group.preferences
|
||||
|
||||
group.preferences.private_group = False
|
||||
group.preferences.recipe_public = True
|
||||
database.group_preferences.update(group.id, group.preferences)
|
||||
|
||||
assert random_recipe.settings
|
||||
@ -140,6 +142,7 @@ def test_public_recipe_success(
|
||||
assert group and group.preferences
|
||||
|
||||
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)
|
||||
|
||||
# Set Recipe `settings.public` attribute
|
||||
|
Loading…
x
Reference in New Issue
Block a user