Skip to content

Commit 24dce0f

Browse files
feat(VCST-4084): saved for later list page (#2008)
## Description A separate page for the "Saved for later" list. **"Saved for later" list page under the account section (only available for authorized users):** <img width="1446" height="469" alt="image" src="https://github.com/user-attachments/assets/b29dfa99-ef11-4cdf-b9bf-662adf0b545f" /> <img width="1542" height="465" alt="image" src="https://github.com/user-attachments/assets/8cfdbbfd-eeae-42eb-9319-862c9cfc382a" /> **"See all" button in "Saved for later" cart block:** <img width="1113" height="403" alt="image" src="https://github.com/user-attachments/assets/7188b82f-52ed-4d82-a10d-ed5d35cdf8ad" /> ## References ### Jira-link: https://virtocommerce.atlassian.net/browse/VCST-4084 ### Artifact URL: https://vc3prerelease.blob.core.windows.net/packages/vc-theme-b2b-vue-2.34.0-pr-2008-d832-d8321133.zip --------- Co-authored-by: Aleksandra-Mitricheva <[email protected]>
1 parent 006b578 commit 24dce0f

File tree

23 files changed

+199
-60
lines changed

23 files changed

+199
-60
lines changed

client-app/config/menu.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@
101101
"icon": "clipboard-list",
102102
"priority": 50
103103
},
104+
{
105+
"id": "saved-for-later",
106+
"route": {
107+
"name": "SavedForLater"
108+
},
109+
"title": "shared.layout.header.mobile.account_menu.saved_for_later",
110+
"icon": "list-v2",
111+
"priority": 55
112+
},
104113
{
105114
"id": "lists",
106115
"route": {
@@ -236,6 +245,14 @@
236245
"icon": "clipboard-list",
237246
"priority": 40
238247
},
248+
{
249+
"route": {
250+
"name": "SavedForLater"
251+
},
252+
"title": "shared.layout.header.mobile.account_menu.saved_for_later",
253+
"icon": "list-v2",
254+
"priority": 45
255+
},
239256
{
240257
"route": {
241258
"name": "Lists"

client-app/pages/account/list-details.vue

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
<div class="flex flex-col">
88
<!-- Title block -->
99
<div class="contents md:flex md:flex-wrap md:items-center md:justify-between md:gap-3">
10-
<VcTypography v-if="list?.name" tag="h1" truncate>
11-
{{ list.name }}
10+
<VcTypography v-if="actualListName" tag="h1" truncate>
11+
{{ actualListName }}
1212
</VcTypography>
1313

1414
<!-- Title skeleton -->
15-
<div v-else class="w-2/3 bg-neutral-200 text-3xl md:w-1/3">&nbsp;</div>
15+
<div v-else class="w-2/3 bg-neutral-200 text-3xl md:w-1/3">{{ props.listName ?? "&nbsp;" }}</div>
1616

1717
<div class="order-last mt-8 flex flex-wrap gap-3 md:ms-0 md:mt-0 md:shrink-0 lg:my-0">
1818
<VcButton
@@ -49,6 +49,7 @@
4949
</VcButton>
5050

5151
<VcButton
52+
v-if="!hideSettings"
5253
:disabled="loading || !list"
5354
size="sm"
5455
variant="outline"
@@ -63,15 +64,7 @@
6364

6465
<div ref="listElement" class="mt-5 w-full">
6566
<!-- Skeletons -->
66-
<template v-if="listLoading">
67-
<div v-if="isMobile" class="grid grid-cols-2 gap-x-4 gap-y-6">
68-
<ProductSkeletonGrid v-for="i in actualPageRowsCount" :key="i" />
69-
</div>
70-
71-
<div v-else class="flex flex-col rounded border bg-additional-50 shadow-sm">
72-
<WishlistProductItemSkeleton v-for="i in actualPageRowsCount" :key="i" class="even:bg-neutral-50" />
73-
</div>
74-
</template>
67+
<WishlistProductsSkeleton v-if="listLoading" :itemsCount="actualPageRowsCount" />
7568

7669
<!-- List details -->
7770
<template v-else-if="!listLoading && !!list?.items?.length">
@@ -137,7 +130,6 @@ import { prepareLineItem, Logger } from "@/core/utilities";
137130
import { ROUTES } from "@/router/routes/constants";
138131
import { dataChangedEvent, useBroadcast } from "@/shared/broadcast";
139132
import { useShortCart, getItemsForAddBulkItemsToCartResultsModal } from "@/shared/cart";
140-
import { ProductSkeletonGrid } from "@/shared/catalog";
141133
import { SaveChangesModal } from "@/shared/common";
142134
import { BackButtonInHeader } from "@/shared/layout";
143135
import { useModal } from "@/shared/modal";
@@ -146,7 +138,7 @@ import {
146138
AddOrUpdateWishlistModal,
147139
DeleteWishlistProductModal,
148140
WishlistLineItems,
149-
WishlistProductItemSkeleton,
141+
WishlistProductsSkeleton,
150142
} from "@/shared/wishlists";
151143
import type {
152144
InputUpdateWishlistItemsType,
@@ -160,9 +152,13 @@ import AddBulkItemsToCartResultsModal from "@/shared/cart/components/add-bulk-it
160152
161153
interface IProps {
162154
listId: string;
155+
hideSettings?: boolean;
156+
listName?: string;
163157
}
164158
165-
const props = defineProps<IProps>();
159+
const props = withDefaults(defineProps<IProps>(), {
160+
listName: undefined,
161+
});
166162
167163
const Error404 = defineAsyncComponent(() => import("@/pages/404.vue"));
168164
@@ -224,6 +220,8 @@ const wishlistListProperties = computed(() => ({
224220
related_type: "wishlist",
225221
}));
226222
223+
const actualListName = computed(() => props.listName ?? list.value?.name);
224+
227225
const isMobile = breakpoints.smaller("lg");
228226
229227
function openListSettingsModal(): void {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<template>
2+
<template v-if="saveForLaterLoading">
3+
<div class="contents md:flex md:flex-wrap md:items-center md:justify-between md:gap-3">
4+
<VcTypography tag="h1">
5+
{{ $t("pages.cart.saved_for_later") }}
6+
</VcTypography>
7+
</div>
8+
9+
<div class="mt-5 w-full">
10+
<WishlistProductsSkeleton :itemsCount="6" />
11+
</div>
12+
</template>
13+
14+
<ListDetails
15+
v-else-if="savedForLaterListId && !savedForLaterListIsEmpty"
16+
:list-id="savedForLaterListId"
17+
:list-name="$t('pages.cart.saved_for_later')"
18+
hide-settings
19+
/>
20+
21+
<div v-else>
22+
<VcTypography tag="h1">
23+
{{ $t("pages.cart.saved_for_later") }}
24+
</VcTypography>
25+
26+
<VcEmptyView :text="$t('pages.cart.saved_for_later_not_found')" icon="list-v2" />
27+
</div>
28+
</template>
29+
30+
<script lang="ts" setup>
31+
import { computed, onMounted } from "vue";
32+
import { useSavedForLater } from "@/shared/cart/composables/useSaveForLater";
33+
import { WishlistProductsSkeleton } from "@/shared/wishlists";
34+
import ListDetails from "./list-details.vue";
35+
36+
const { savedForLaterList, loading: saveForLaterLoading, getSavedForLater } = useSavedForLater();
37+
38+
const savedForLaterListId = computed(() => savedForLaterList.value?.id);
39+
const savedForLaterListIsEmpty = computed(() => !savedForLaterList.value?.items?.length);
40+
41+
onMounted(() => {
42+
void getSavedForLater();
43+
});
44+
</script>

client-app/pages/shared-list.vue

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,7 @@
66

77
<div ref="listElement" class="shared-list__content">
88
<!-- Skeletons -->
9-
<template v-if="listLoading">
10-
<div v-if="isMobile" class="shared-list__skeleton-mobile">
11-
<ProductSkeletonGrid v-for="i in actualPageRowsCount" :key="i" />
12-
</div>
13-
14-
<div v-else class="shared-list__skeleton-desktop">
15-
<WishlistProductItemSkeleton
16-
v-for="i in actualPageRowsCount"
17-
:key="i"
18-
class="shared-list__skeleton-desktop-item"
19-
/>
20-
</div>
21-
</template>
9+
<WishlistProductsSkeleton v-if="listLoading" :itemsCount="actualPageRowsCount" />
2210

2311
<!-- List details -->
2412
<template v-else-if="!listLoading && !!list?.items?.length">
@@ -74,7 +62,6 @@
7462
</template>
7563

7664
<script setup lang="ts">
77-
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
7865
import cloneDeep from "lodash/cloneDeep";
7966
import keyBy from "lodash/keyBy";
8067
import { computed, ref, watchEffect, defineAsyncComponent } from "vue";
@@ -85,11 +72,9 @@ import { PAGE_LIMIT } from "@/core/constants";
8572
import { MODULE_XAPI_KEYS } from "@/core/constants/modules";
8673
import { prepareLineItem } from "@/core/utilities";
8774
import { useShortCart } from "@/shared/cart";
88-
import { ProductSkeletonGrid } from "@/shared/catalog";
89-
import { useWishlists, WishlistLineItems, WishlistProductItemSkeleton } from "@/shared/wishlists";
75+
import { useWishlists, WishlistLineItems, WishlistProductsSkeleton, WishlistSummary } from "@/shared/wishlists";
9076
import type { LineItemType, Product } from "@/core/api/graphql/types";
9177
import type { PreparedLineItemType } from "@/core/types";
92-
import WishlistSummary from "@/shared/wishlists/components/wishlist-summary.vue";
9378
9479
const props = defineProps<IProps>();
9580
@@ -108,7 +93,6 @@ const { cart } = useShortCart();
10893
const { continue_shopping_link } = getModuleSettings({
10994
[MODULE_XAPI_KEYS.CONTINUE_SHOPPING_LINK]: "continue_shopping_link",
11095
});
111-
const breakpoints = useBreakpoints(breakpointsTailwind);
11296
11397
usePageHead({
11498
title: computed(() => t("pages.account.list_details.meta.title", [list.value?.name])),
@@ -120,7 +104,6 @@ const wishlistListProperties = computed(() => ({
120104
related_id: list.value?.id,
121105
related_type: "wishlist",
122106
}));
123-
const isMobile = breakpoints.smaller("lg");
124107
125108
const listElement = ref<HTMLElement | undefined>();
126109
const pendingItems = ref<Record<string, boolean>>({});
@@ -175,18 +158,6 @@ watchEffect(() => {
175158
@apply mt-5 w-full;
176159
}
177160
178-
&__skeleton-mobile {
179-
@apply grid grid-cols-2 gap-x-4 gap-y-6;
180-
}
181-
182-
&__skeleton-desktop {
183-
@apply flex flex-col rounded border bg-additional-50 shadow-sm;
184-
}
185-
186-
&__skeleton-desktop-item {
187-
@apply even:bg-neutral-50;
188-
}
189-
190161
&__items {
191162
@apply flex flex-col gap-6;
192163
}

client-app/router/routes/account.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ROUTES } from "@/router/routes/constants";
12
import { useUser } from "@/shared/account";
23
import type { RouteRecordRaw } from "vue-router";
34

@@ -10,6 +11,7 @@ const OrderDetails = () => import("@/pages/account/order-details.vue");
1011
const OrderPayment = () => import("@/pages/account/order-payment.vue");
1112
const Lists = () => import("@/pages/account/lists.vue");
1213
const ListDetails = () => import("@/pages/account/list-details.vue");
14+
const SavedForLaterDetails = () => import("@/pages/account/saved-for-later-details.vue");
1315
const SavedCreditCards = () => import("@/pages/account/saved-credit-cards.vue");
1416
const Impersonate = () => import("@/pages/account/impersonate.vue");
1517

@@ -56,6 +58,10 @@ export const accountRoutes: RouteRecordRaw[] = [
5658
},
5759
],
5860
},
61+
{
62+
path: "saved-for-later",
63+
children: [{ path: "", name: ROUTES.SAVED_FOR_LATER.NAME, component: SavedForLaterDetails }],
64+
},
5965
{
6066
path: "lists",
6167
children: [

client-app/router/routes/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,7 @@ export const ROUTES = {
2323
NAME: "ChangePassword",
2424
PATH: "/change-password",
2525
},
26+
SAVED_FOR_LATER: {
27+
NAME: "SavedForLater",
28+
},
2629
} as const;

client-app/shared/cart/components/cart-for-later.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
<template>
22
<VcWidget :title="$t('pages.cart.saved_for_later')" prepend-icon="bookmark" size="lg">
3+
<template v-slot:append>
4+
<VcButton variant="outline" :to="{ name: ROUTES.SAVED_FOR_LATER.NAME }" size="sm">
5+
{{ $t("common.buttons.see_all") }}
6+
</VcButton>
7+
</template>
8+
39
<VcProductsGrid short>
410
<CartItemForLater
511
v-for="(item, index) in savedForLaterList?.items"
@@ -19,6 +25,7 @@
1925
import { computed, toRef } from "vue";
2026
import { useI18n } from "vue-i18n";
2127
import { useAnalytics } from "@/core/composables/useAnalytics";
28+
import { ROUTES } from "@/router/routes/constants";
2229
import type { SavedForLaterListFragment, Product } from "@/core/api/graphql/types";
2330
import CartItemForLater from "@/shared/cart/components/cart-item-for-later.vue";
2431

client-app/shared/cart/composables/useSaveForLater.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,15 @@ function _useSavedForLater() {
7777
}
7878
}
7979

80+
const _getSavedForLaterLoading = ref(false);
8081
async function getSavedForLater() {
8182
try {
83+
_getSavedForLaterLoading.value = true;
8284
savedForLaterList.value = await getSavedForLaterQuery();
8385
} catch (err) {
8486
Logger.error(`useSavedForLater.${getSavedForLater.name}`, err);
87+
} finally {
88+
_getSavedForLaterLoading.value = false;
8589
}
8690
}
8791

@@ -99,7 +103,8 @@ function _useSavedForLater() {
99103
_moveToSavedForLaterLoading.value ||
100104
_moveToSavedForLaterBatchedLoading.value ||
101105
_moveFromSavedForLaterLoading.value ||
102-
_moveFromSavedForLaterBatchedLoading.value,
106+
_moveFromSavedForLaterBatchedLoading.value ||
107+
_getSavedForLaterLoading.value,
103108
),
104109
};
105110
}

client-app/shared/wishlists/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ export { default as WishlistCard } from "./wishlist-card.vue";
88
export { default as WishlistCardSkeleton } from "./wishlist-card-skeleton.vue";
99
export { default as WishlistLineItems } from "./wishlist-line-items.vue";
1010
export { default as WishlistProductItemSkeleton } from "./wishlist-product-item-skeleton.vue";
11+
export { default as WishlistProductsSkeleton } from "./wishlist-products-skeleton.vue";
12+
export { default as WishlistSummary } from "./wishlist-summary.vue";
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<template>
2+
<div v-if="isMobile" class="wishlist-products-skeleton wishlist-products-skeleton--mobile">
3+
<ProductSkeletonGrid v-for="i in itemsCount" :key="i" class="wishlist-products-skeleton__item" />
4+
</div>
5+
6+
<div v-else class="wishlist-products-skeleton wishlist-products-skeleton--desktop">
7+
<WishlistProductItemSkeleton v-for="i in itemsCount" :key="i" class="wishlist-products-skeleton__item" />
8+
</div>
9+
</template>
10+
11+
<script lang="ts" setup>
12+
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
13+
import { ProductSkeletonGrid } from "@/shared/catalog";
14+
import { WishlistProductItemSkeleton } from "@/shared/wishlists";
15+
16+
interface IProps {
17+
itemsCount: number;
18+
}
19+
20+
defineProps<IProps>();
21+
22+
const breakpoints = useBreakpoints(breakpointsTailwind);
23+
24+
const isMobile = breakpoints.smaller("lg");
25+
</script>
26+
27+
<style lang="scss">
28+
.wishlist-products-skeleton {
29+
$desktop: "";
30+
31+
&--mobile {
32+
@apply grid grid-cols-2 gap-x-4 gap-y-6;
33+
}
34+
35+
&--desktop {
36+
@apply flex flex-col rounded border bg-additional-50 shadow-sm;
37+
38+
$desktop: &;
39+
}
40+
41+
&__item {
42+
#{$desktop} & {
43+
@apply even:bg-neutral-50;
44+
}
45+
}
46+
}
47+
</style>

0 commit comments

Comments
 (0)