feat: persist customer product and shop favorites through the live API
Replace mall fixture favorites with customer-scoped endpoints, and send signed-out shoppers back to the page they left after sign-in. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
94a64ec712
commit
6c1357ec4d
@@ -1,11 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ApiError } from "@vmall/shared";
|
||||
import { t as pick } from "@vmall/shared";
|
||||
import type { Paged, Product } from "@vmall/shared";
|
||||
import { lowestSku } from "~/utils/product";
|
||||
import { useSessionStore } from "~/stores/session";
|
||||
|
||||
const route = useRoute();
|
||||
const { locale, t } = useI18n();
|
||||
const { $api } = useNuxtApp();
|
||||
const session = useSessionStore();
|
||||
const signInThenReturn = useSignInRedirect();
|
||||
|
||||
const slug = computed(() => {
|
||||
const raw = route.params.id;
|
||||
@@ -21,6 +25,10 @@ const { data: shop } = await useAsyncData("shop-profile", () => $api.getShop(slu
|
||||
const store = computed(() => shop.value);
|
||||
|
||||
const favorite = ref(false);
|
||||
const favoriteLoading = ref(false);
|
||||
const favoriteBusy = ref(false);
|
||||
const favoriteError = ref("");
|
||||
let favoriteRequestVersion = 0;
|
||||
// Only sorts the catalog model can answer; sales and comments have no model.
|
||||
const sortMode = ref<"default" | "price">("default");
|
||||
const sortOrder = ref<"asc" | "desc">("desc");
|
||||
@@ -80,6 +88,67 @@ function chooseSort(mode: "default" | "price"): void {
|
||||
}
|
||||
page.value = 1;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [store.value?.id, session.isLoggedIn] as const,
|
||||
async ([id, loggedIn]) => {
|
||||
const version = ++favoriteRequestVersion;
|
||||
favorite.value = false;
|
||||
favoriteLoading.value = Boolean(id && loggedIn);
|
||||
favoriteBusy.value = false;
|
||||
favoriteError.value = "";
|
||||
if (!id || !loggedIn) return;
|
||||
try {
|
||||
const result = await $api.listFavorites({ kind: "shop", target_id: id, per_page: 1 });
|
||||
if (version === favoriteRequestVersion && store.value?.id === id) {
|
||||
favorite.value = result.items.length > 0;
|
||||
}
|
||||
} catch {
|
||||
if (version === favoriteRequestVersion && store.value?.id === id) {
|
||||
favoriteError.value = t("stores.favoriteLoadFailed");
|
||||
}
|
||||
} finally {
|
||||
if (version === favoriteRequestVersion) favoriteLoading.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const toggleFavorite = async (): Promise<void> => {
|
||||
const current = store.value;
|
||||
if (!current || favoriteLoading.value || favoriteBusy.value) return;
|
||||
if (!session.isLoggedIn) {
|
||||
await signInThenReturn();
|
||||
return;
|
||||
}
|
||||
const version = ++favoriteRequestVersion;
|
||||
favoriteBusy.value = true;
|
||||
favoriteError.value = "";
|
||||
try {
|
||||
if (favorite.value) {
|
||||
await $api.removeShopFavorite(current.id);
|
||||
if (version === favoriteRequestVersion && store.value?.id === current.id) {
|
||||
favorite.value = false;
|
||||
}
|
||||
} else {
|
||||
await $api.addShopFavorite(current.id);
|
||||
if (version === favoriteRequestVersion && store.value?.id === current.id) {
|
||||
favorite.value = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.status === 401) {
|
||||
await signInThenReturn();
|
||||
return;
|
||||
}
|
||||
if (version === favoriteRequestVersion && store.value?.id === current.id) {
|
||||
favoriteError.value = t("stores.favoriteFailed");
|
||||
}
|
||||
} finally {
|
||||
if (version === favoriteRequestVersion) favoriteBusy.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -112,9 +181,16 @@ function chooseSort(mode: "default" | "price"): void {
|
||||
<div v-if="store.region"><dt>{{ t("stores.region") }}</dt><dd>{{ store.region }}</dd></div>
|
||||
<div v-if="store.address"><dt>{{ t("stores.address") }}</dt><dd>{{ pick(store.address, locale) }}</dd></div>
|
||||
</dl>
|
||||
<button type="button" class="mbtn favorite-button" :class="{ selected: favorite }" @click="favorite = !favorite">
|
||||
<button
|
||||
type="button"
|
||||
class="mbtn favorite-button"
|
||||
:class="{ selected: favorite }"
|
||||
:disabled="favoriteLoading || favoriteBusy"
|
||||
@click="toggleFavorite"
|
||||
>
|
||||
{{ favorite ? t("stores.favorited") : t("stores.favorite") }}
|
||||
</button>
|
||||
<p v-if="favoriteError" class="favorite-error">{{ favoriteError }}</p>
|
||||
</section>
|
||||
|
||||
<section v-if="store.notice || store.after_sale" class="mpanel notice-card">
|
||||
@@ -253,6 +329,11 @@ function chooseSort(mode: "default" | "price"): void {
|
||||
border-color: var(--mall-red);
|
||||
color: var(--mall-red);
|
||||
}
|
||||
.favorite-error {
|
||||
margin: 6px 0 0;
|
||||
color: var(--mall-red);
|
||||
font-size: 12px;
|
||||
}
|
||||
.rail-title {
|
||||
margin: 0 0 12px;
|
||||
border-left: 3px solid var(--mall-red);
|
||||
|
||||
Reference in New Issue
Block a user