mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-10-26 15:54:20 -04:00
fix: Optimize Recipe Favorites/Ratings (#6075)
This commit is contained in:
@@ -55,12 +55,9 @@
|
||||
/>
|
||||
<div v-else class="px-1" /> <!-- Empty div to keep the layout consistent -->
|
||||
|
||||
<RecipeRating
|
||||
class="ml-n2"
|
||||
<RecipeCardRating
|
||||
:model-value="rating"
|
||||
:recipe-id="recipeId"
|
||||
:slug="slug"
|
||||
small
|
||||
/>
|
||||
<v-spacer />
|
||||
<RecipeChips
|
||||
@@ -105,7 +102,7 @@ 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 RecipeCardRating from "./RecipeCardRating.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -87,13 +87,11 @@
|
||||
class="ma-0 pa-0"
|
||||
/>
|
||||
<div v-else class="my-0 px-1 py-0" /> <!-- Empty div to keep the layout consistent -->
|
||||
<RecipeRating
|
||||
<RecipeCardRating
|
||||
v-if="showRecipeContent"
|
||||
:class="[{ 'pb-2': !isOwnGroup }, 'ml-n2']"
|
||||
:value="rating"
|
||||
:model-value="rating"
|
||||
:recipe-id="recipeId"
|
||||
:slug="slug"
|
||||
small
|
||||
/>
|
||||
|
||||
<!-- If we're not logged-in, no items display, so we hide this menu -->
|
||||
@@ -130,7 +128,7 @@
|
||||
import RecipeFavoriteBadge from "./RecipeFavoriteBadge.vue";
|
||||
import RecipeContextMenu from "./RecipeContextMenu.vue";
|
||||
import RecipeCardImage from "./RecipeCardImage.vue";
|
||||
import RecipeRating from "./RecipeRating.vue";
|
||||
import RecipeCardRating from "./RecipeCardRating.vue";
|
||||
import RecipeChips from "./RecipeChips.vue";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
|
||||
|
||||
101
frontend/components/Domain/Recipe/RecipeCardRating.vue
Normal file
101
frontend/components/Domain/Recipe/RecipeCardRating.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="rating-display">
|
||||
<span
|
||||
v-for="(star, index) in ratingDisplay"
|
||||
:key="index"
|
||||
class="star"
|
||||
:class="{
|
||||
'star-half': star === 'half',
|
||||
'text-secondary': !useGroupStyle,
|
||||
'text-grey-darken-1': useGroupStyle,
|
||||
}"
|
||||
>
|
||||
<!-- We render both the full and empty stars for "half" stars because they're layered over each other -->
|
||||
<span
|
||||
v-if="star === 'empty' || star === 'half'"
|
||||
class="star-empty"
|
||||
>
|
||||
☆
|
||||
</span>
|
||||
<span
|
||||
v-if="star === 'full' || star === 'half'"
|
||||
class="star-full"
|
||||
>
|
||||
★
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useUserSelfRatings } from "~/composables/use-users";
|
||||
|
||||
type Star = "full" | "half" | "empty";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
recipeId: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const { userRatings } = useUserSelfRatings();
|
||||
|
||||
const userRating = computed(() => {
|
||||
return userRatings.value.find(r => r.recipeId === props.recipeId)?.rating ?? undefined;
|
||||
});
|
||||
|
||||
const ratingValue = computed(() => userRating.value || props.modelValue || 0);
|
||||
const useGroupStyle = computed(() => isOwnGroup.value && !userRating.value && props.modelValue);
|
||||
const ratingDisplay = computed<Star[]>(
|
||||
() => {
|
||||
const stars: Star[] = [];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const diff = ratingValue.value - i;
|
||||
if (diff >= 1) {
|
||||
stars.push("full");
|
||||
}
|
||||
else if (diff >= 0.25) { // round to half star if rating is at least 0.25 but not quite a full star
|
||||
stars.push("half");
|
||||
}
|
||||
else {
|
||||
stars.push("empty");
|
||||
}
|
||||
}
|
||||
|
||||
return stars;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rating-display {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
|
||||
.star {
|
||||
font-size: 18px;
|
||||
transition: color 0.2s ease;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
&.star-half {
|
||||
.star-full {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -43,8 +43,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
buttonStyle: false,
|
||||
});
|
||||
|
||||
const api = useUserApi();
|
||||
const $auth = useMealieAuth();
|
||||
const { userRatings, refreshUserRatings } = useUserSelfRatings();
|
||||
|
||||
const isFavorite = computed(() => {
|
||||
@@ -53,6 +51,9 @@ const isFavorite = computed(() => {
|
||||
});
|
||||
|
||||
async function toggleFavorite() {
|
||||
const api = useUserApi();
|
||||
const $auth = useMealieAuth();
|
||||
|
||||
if (!$auth.user.value) return;
|
||||
if (!isFavorite.value) {
|
||||
await api.users.addFavorite($auth.user.value?.id, props.recipeId);
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Composer } from "vue-i18n";
|
||||
import type { ApiRequestInstance, RequestResponse } from "~/lib/api/types/non-generated";
|
||||
import { AdminAPI, PublicApi, UserApi } from "~/lib/api";
|
||||
import { PublicExploreApi } from "~/lib/api/client-public";
|
||||
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||
|
||||
const request = {
|
||||
async safe<T, U>(
|
||||
@@ -56,8 +57,7 @@ function getRequests(axiosInstance: AxiosInstance): ApiRequestInstance {
|
||||
export const useRequests = function (i18n?: Composer): ApiRequestInstance {
|
||||
const { $axios } = useNuxtApp();
|
||||
if (!i18n) {
|
||||
// Only works in a setup block
|
||||
i18n = useI18n();
|
||||
i18n = useGlobalI18n();
|
||||
}
|
||||
|
||||
$axios.defaults.headers.common["Accept-Language"] = i18n.locale.value;
|
||||
|
||||
10
frontend/composables/use-global-i18n.ts
Normal file
10
frontend/composables/use-global-i18n.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Composer } from "vue-i18n";
|
||||
|
||||
let i18n: Composer | null = null;
|
||||
|
||||
export function useGlobalI18n() {
|
||||
if (!i18n) {
|
||||
i18n = useI18n();
|
||||
}
|
||||
return i18n;
|
||||
}
|
||||
@@ -5,26 +5,31 @@ const userRatings = ref<UserRatingSummary[]>([]);
|
||||
const loading = ref(false);
|
||||
const ready = ref(false);
|
||||
|
||||
export const useUserSelfRatings = function () {
|
||||
const $auth = useMealieAuth();
|
||||
const api = useUserApi();
|
||||
const $auth = useMealieAuth();
|
||||
|
||||
export const useUserSelfRatings = function () {
|
||||
async function refreshUserRatings() {
|
||||
if (!$auth.user.value || loading.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const api = useUserApi();
|
||||
|
||||
const { data } = await api.users.getSelfRatings();
|
||||
userRatings.value = data?.ratings || [];
|
||||
|
||||
loading.value = false;
|
||||
ready.value = true;
|
||||
}
|
||||
|
||||
async function setRating(slug: string, rating: number | null, isFavorite: boolean | null) {
|
||||
loading.value = true;
|
||||
const api = useUserApi();
|
||||
|
||||
const userId = $auth.user.value?.id || "";
|
||||
await api.users.setRating(userId, slug, rating, isFavorite);
|
||||
|
||||
loading.value = false;
|
||||
await refreshUserRatings();
|
||||
}
|
||||
|
||||
@@ -36,8 +36,9 @@ import AppHeader from "@/components/Layout/LayoutParts/AppHeader.vue";
|
||||
import AppSidebar from "@/components/Layout/LayoutParts/AppSidebar.vue";
|
||||
import TheSnackbar from "~/components/Layout/LayoutParts/TheSnackbar.vue";
|
||||
import type { SidebarLinks } from "~/types/application-types";
|
||||
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||
|
||||
const i18n = useI18n();
|
||||
const i18n = useGlobalI18n();
|
||||
const display = useDisplay();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
|
||||
@@ -13,11 +13,10 @@
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import TheSnackbar from "~/components/Layout/LayoutParts/TheSnackbar.vue";
|
||||
import AppHeader from "@/components/Layout/LayoutParts/AppHeader.vue";
|
||||
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { TheSnackbar, AppHeader },
|
||||
});
|
||||
useGlobalI18n(); // ensure i18n is initialized
|
||||
</script>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<script lang="ts">
|
||||
import TheSnackbar from "~/components/Layout/LayoutParts/TheSnackbar.vue";
|
||||
import { useAppInfo } from "~/composables/api";
|
||||
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { TheSnackbar },
|
||||
@@ -32,7 +33,7 @@ export default defineNuxtComponent({
|
||||
|
||||
const isDemo = computed(() => appInfo?.value?.demoStatus || false);
|
||||
|
||||
const i18n = useI18n();
|
||||
const i18n = useGlobalI18n();
|
||||
const version = computed(() => appInfo?.value?.version || i18n.t("about.unknown-version"));
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
<DefaultLayout />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import DefaultLayout from "@/components/Layout/DefaultLayout.vue";
|
||||
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: { DefaultLayout },
|
||||
});
|
||||
useGlobalI18n(); // ensure i18n is initialized
|
||||
</script>
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
error: {
|
||||
@@ -58,7 +60,7 @@ export default defineNuxtComponent({
|
||||
layout: "basic",
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const i18n = useGlobalI18n();
|
||||
const $auth = useMealieAuth();
|
||||
const { $globals } = useNuxtApp();
|
||||
const ready = ref(false);
|
||||
|
||||
Reference in New Issue
Block a user