Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5c13a3c
feat: saved for later menu item
yuskithedeveloper Oct 16, 2025
92e3083
feat: see all button in cart saved for later section
yuskithedeveloper Oct 17, 2025
e4e1939
feat: saved for later list not found fallback
yuskithedeveloper Oct 17, 2025
668f850
refactor: constants usage
yuskithedeveloper Oct 17, 2025
67bbb6b
feat: saved for later list empty fallback
yuskithedeveloper Oct 17, 2025
0788f5e
Merge branch 'dev' into feat/VCST-4084-saved-for-later-page
yuskithedeveloper Oct 20, 2025
f91cf32
Merge branch 'dev' into feat/VCST-4084-saved-for-later-page
Aleksandra-Mitricheva Oct 23, 2025
89e7e44
feat: saved for later list skeletons
yuskithedeveloper Oct 23, 2025
fba59ce
fix: skeleton component import
yuskithedeveloper Oct 23, 2025
9d387c0
fix: non-existing property
yuskithedeveloper Oct 24, 2025
b5f10dd
feat: bem
yuskithedeveloper Oct 24, 2025
ec9da63
Merge branch 'dev' into feat/VCST-4084-saved-for-later-page
yuskithedeveloper Oct 24, 2025
75a94a0
refactor: scss rules
yuskithedeveloper Oct 28, 2025
3e8a612
refactor: import block
yuskithedeveloper Oct 28, 2025
4029d72
Merge branch 'dev' into feat/VCST-4084-saved-for-later-page
yuskithedeveloper Oct 28, 2025
1345cfd
refactor: variable declaration moved before usage
yuskithedeveloper Oct 28, 2025
f2b3402
Merge branch 'dev' into feat/VCST-4084-saved-for-later-page
Aleksandra-Mitricheva Oct 29, 2025
e5717ee
Merge branch 'dev' into feat/VCST-4084-saved-for-later-page
yuskithedeveloper Oct 29, 2025
d832113
Merge branch 'dev' into feat/VCST-4084-saved-for-later-page
yuskithedeveloper Oct 30, 2025
bcb08c8
Merge branch 'dev' into feat/VCST-4084-saved-for-later-page
yuskithedeveloper Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions client-app/config/menu.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@
"icon": "clipboard-list",
"priority": 50
},
{
"id": "saved-for-later",
"route": {
"name": "SavedForLater"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Route Naming Inconsistency

The route name "SavedForLater" is hardcoded as a string instead of using the constant ROUTES.SAVED_FOR_LATER.NAME. Based on the PR discussion comment, this should use the constant from routes/constants.ts for consistency.

Fix in Cursor Fix in Web

},
"title": "shared.layout.header.mobile.account_menu.saved_for_later",
"icon": "list-v2",
"priority": 55
},
{
"id": "lists",
"route": {
Expand Down Expand Up @@ -236,6 +245,14 @@
"icon": "clipboard-list",
"priority": 40
},
{
"route": {
"name": "SavedForLater"
},
"title": "shared.layout.header.mobile.account_menu.saved_for_later",
"icon": "list-v2",
"priority": 45
},
{
"route": {
"name": "Lists"
Expand Down
28 changes: 13 additions & 15 deletions client-app/pages/account/list-details.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
<div class="flex flex-col">
<!-- Title block -->
<div class="contents md:flex md:flex-wrap md:items-center md:justify-between md:gap-3">
<VcTypography v-if="list?.name" tag="h1" truncate>
{{ list.name }}
<VcTypography v-if="actualListName" tag="h1" truncate>
{{ actualListName }}
</VcTypography>

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Template Interpolation Renders HTML Entity

When props.listName is not provided, the skeleton title displays the literal string " ". This happens because &nbsp; is now within a template interpolation, causing it to render as text instead of an HTML non-breaking space.

Fix in Cursor Fix in Web


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

<VcButton
v-if="!hideSettings"
:disabled="loading || !list"
size="sm"
variant="outline"
Expand All @@ -63,15 +64,7 @@

<div ref="listElement" class="mt-5 w-full">
<!-- Skeletons -->
<template v-if="listLoading">
<div v-if="isMobile" class="grid grid-cols-2 gap-x-4 gap-y-6">
<ProductSkeletonGrid v-for="i in actualPageRowsCount" :key="i" />
</div>

<div v-else class="flex flex-col rounded border bg-additional-50 shadow-sm">
<WishlistProductItemSkeleton v-for="i in actualPageRowsCount" :key="i" class="even:bg-neutral-50" />
</div>
</template>
<WishlistProductsSkeleton v-if="listLoading" :itemsCount="actualPageRowsCount" />

<!-- List details -->
<template v-else-if="!listLoading && !!list?.items?.length">
Expand Down Expand Up @@ -137,7 +130,6 @@ import { prepareLineItem, Logger } from "@/core/utilities";
import { ROUTES } from "@/router/routes/constants";
import { dataChangedEvent, useBroadcast } from "@/shared/broadcast";
import { useShortCart, getItemsForAddBulkItemsToCartResultsModal } from "@/shared/cart";
import { ProductSkeletonGrid } from "@/shared/catalog";
import { SaveChangesModal } from "@/shared/common";
import { BackButtonInHeader } from "@/shared/layout";
import { useModal } from "@/shared/modal";
Expand All @@ -146,7 +138,7 @@ import {
AddOrUpdateWishlistModal,
DeleteWishlistProductModal,
WishlistLineItems,
WishlistProductItemSkeleton,
WishlistProductsSkeleton,
} from "@/shared/wishlists";
import type {
InputUpdateWishlistItemsType,
Expand All @@ -160,9 +152,13 @@ import AddBulkItemsToCartResultsModal from "@/shared/cart/components/add-bulk-it

interface IProps {
listId: string;
hideSettings?: boolean;
listName?: string;
}

const props = defineProps<IProps>();
const props = withDefaults(defineProps<IProps>(), {
listName: undefined,
});

const Error404 = defineAsyncComponent(() => import("@/pages/404.vue"));

Expand Down Expand Up @@ -224,6 +220,8 @@ const wishlistListProperties = computed(() => ({
related_type: "wishlist",
}));

const actualListName = computed(() => props.listName ?? list.value?.name);

const isMobile = breakpoints.smaller("lg");

function openListSettingsModal(): void {
Expand Down
44 changes: 44 additions & 0 deletions client-app/pages/account/saved-for-later-details.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<template v-if="saveForLaterLoading">
<div class="contents md:flex md:flex-wrap md:items-center md:justify-between md:gap-3">
<VcTypography tag="h1">
{{ $t("pages.cart.saved_for_later") }}
</VcTypography>
</div>

<div class="mt-5 w-full">
<WishlistProductsSkeleton :itemsCount="6" />
</div>
</template>

<ListDetails
v-else-if="savedForLaterListId && !savedForLaterListIsEmpty"
:list-id="savedForLaterListId"
:list-name="$t('pages.cart.saved_for_later')"
hide-settings
/>

<div v-else>
<VcTypography tag="h1">
{{ $t("pages.cart.saved_for_later") }}
</VcTypography>

<VcEmptyView :text="$t('pages.cart.saved_for_later_not_found')" icon="list-v2" />
</div>
</template>

<script lang="ts" setup>
import { computed, onMounted } from "vue";
import { useSavedForLater } from "@/shared/cart/composables/useSaveForLater";
import { WishlistProductsSkeleton } from "@/shared/wishlists";
import ListDetails from "./list-details.vue";

const { savedForLaterList, loading: saveForLaterLoading, getSavedForLater } = useSavedForLater();

const savedForLaterListId = computed(() => savedForLaterList.value?.id);
const savedForLaterListIsEmpty = computed(() => !savedForLaterList.value?.items?.length);

onMounted(() => {
void getSavedForLater();
});
</script>
33 changes: 2 additions & 31 deletions client-app/pages/shared-list.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,7 @@

<div ref="listElement" class="shared-list__content">
<!-- Skeletons -->
<template v-if="listLoading">
<div v-if="isMobile" class="shared-list__skeleton-mobile">
<ProductSkeletonGrid v-for="i in actualPageRowsCount" :key="i" />
</div>

<div v-else class="shared-list__skeleton-desktop">
<WishlistProductItemSkeleton
v-for="i in actualPageRowsCount"
:key="i"
class="shared-list__skeleton-desktop-item"
/>
</div>
</template>
<WishlistProductsSkeleton v-if="listLoading" :itemsCount="actualPageRowsCount" />

<!-- List details -->
<template v-else-if="!listLoading && !!list?.items?.length">
Expand Down Expand Up @@ -74,7 +62,6 @@
</template>

<script setup lang="ts">
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
import cloneDeep from "lodash/cloneDeep";
import keyBy from "lodash/keyBy";
import { computed, ref, watchEffect, defineAsyncComponent } from "vue";
Expand All @@ -85,11 +72,9 @@ import { PAGE_LIMIT } from "@/core/constants";
import { MODULE_XAPI_KEYS } from "@/core/constants/modules";
import { prepareLineItem } from "@/core/utilities";
import { useShortCart } from "@/shared/cart";
import { ProductSkeletonGrid } from "@/shared/catalog";
import { useWishlists, WishlistLineItems, WishlistProductItemSkeleton } from "@/shared/wishlists";
import { useWishlists, WishlistLineItems, WishlistProductsSkeleton, WishlistSummary } from "@/shared/wishlists";
import type { LineItemType, Product } from "@/core/api/graphql/types";
import type { PreparedLineItemType } from "@/core/types";
import WishlistSummary from "@/shared/wishlists/components/wishlist-summary.vue";

const props = defineProps<IProps>();

Expand All @@ -108,7 +93,6 @@ const { cart } = useShortCart();
const { continue_shopping_link } = getModuleSettings({
[MODULE_XAPI_KEYS.CONTINUE_SHOPPING_LINK]: "continue_shopping_link",
});
const breakpoints = useBreakpoints(breakpointsTailwind);

usePageHead({
title: computed(() => t("pages.account.list_details.meta.title", [list.value?.name])),
Expand All @@ -120,7 +104,6 @@ const wishlistListProperties = computed(() => ({
related_id: list.value?.id,
related_type: "wishlist",
}));
const isMobile = breakpoints.smaller("lg");

const listElement = ref<HTMLElement | undefined>();
const pendingItems = ref<Record<string, boolean>>({});
Expand Down Expand Up @@ -175,18 +158,6 @@ watchEffect(() => {
@apply mt-5 w-full;
}

&__skeleton-mobile {
@apply grid grid-cols-2 gap-x-4 gap-y-6;
}

&__skeleton-desktop {
@apply flex flex-col rounded border bg-additional-50 shadow-sm;
}

&__skeleton-desktop-item {
@apply even:bg-neutral-50;
}

&__items {
@apply flex flex-col gap-6;
}
Expand Down
6 changes: 6 additions & 0 deletions client-app/router/routes/account.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ROUTES } from "@/router/routes/constants";
import { useUser } from "@/shared/account";
import type { RouteRecordRaw } from "vue-router";

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

Expand Down Expand Up @@ -56,6 +58,10 @@ export const accountRoutes: RouteRecordRaw[] = [
},
],
},
{
path: "saved-for-later",
children: [{ path: "", name: ROUTES.SAVED_FOR_LATER.NAME, component: SavedForLaterDetails }],
},
{
path: "lists",
children: [
Expand Down
3 changes: 3 additions & 0 deletions client-app/router/routes/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ export const ROUTES = {
NAME: "ChangePassword",
PATH: "/change-password",
},
SAVED_FOR_LATER: {
NAME: "SavedForLater",
},
} as const;
7 changes: 7 additions & 0 deletions client-app/shared/cart/components/cart-for-later.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<template>
<VcWidget :title="$t('pages.cart.saved_for_later')" prepend-icon="bookmark" size="lg">
<template v-slot:append>
<VcButton variant="outline" :to="{ name: ROUTES.SAVED_FOR_LATER.NAME }" size="sm">
{{ $t("common.buttons.see_all") }}
</VcButton>
</template>

<VcProductsGrid short>
<CartItemForLater
v-for="(item, index) in savedForLaterList?.items"
Expand All @@ -19,6 +25,7 @@
import { computed, toRef } from "vue";
import { useI18n } from "vue-i18n";
import { useAnalytics } from "@/core/composables/useAnalytics";
import { ROUTES } from "@/router/routes/constants";
import type { SavedForLaterListFragment, Product } from "@/core/api/graphql/types";
import CartItemForLater from "@/shared/cart/components/cart-item-for-later.vue";

Expand Down
7 changes: 6 additions & 1 deletion client-app/shared/cart/composables/useSaveForLater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,15 @@ function _useSavedForLater() {
}
}

const _getSavedForLaterLoading = ref(false);
async function getSavedForLater() {
try {
_getSavedForLaterLoading.value = true;
savedForLaterList.value = await getSavedForLaterQuery();
} catch (err) {
Logger.error(`useSavedForLater.${getSavedForLater.name}`, err);
} finally {
_getSavedForLaterLoading.value = false;
}
}

Expand All @@ -99,7 +103,8 @@ function _useSavedForLater() {
_moveToSavedForLaterLoading.value ||
_moveToSavedForLaterBatchedLoading.value ||
_moveFromSavedForLaterLoading.value ||
_moveFromSavedForLaterBatchedLoading.value,
_moveFromSavedForLaterBatchedLoading.value ||
_getSavedForLaterLoading.value,
),
};
}
Expand Down
2 changes: 2 additions & 0 deletions client-app/shared/wishlists/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export { default as WishlistCard } from "./wishlist-card.vue";
export { default as WishlistCardSkeleton } from "./wishlist-card-skeleton.vue";
export { default as WishlistLineItems } from "./wishlist-line-items.vue";
export { default as WishlistProductItemSkeleton } from "./wishlist-product-item-skeleton.vue";
export { default as WishlistProductsSkeleton } from "./wishlist-products-skeleton.vue";
export { default as WishlistSummary } from "./wishlist-summary.vue";
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<template>
<div v-if="isMobile" class="wishlist-products-skeleton wishlist-products-skeleton--mobile">
<ProductSkeletonGrid v-for="i in itemsCount" :key="i" class="wishlist-products-skeleton__item" />
</div>

<div v-else class="wishlist-products-skeleton wishlist-products-skeleton--desktop">
<WishlistProductItemSkeleton v-for="i in itemsCount" :key="i" class="wishlist-products-skeleton__item" />
</div>
</template>

<script lang="ts" setup>
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
import { ProductSkeletonGrid } from "@/shared/catalog";
import { WishlistProductItemSkeleton } from "@/shared/wishlists";

interface IProps {
itemsCount: number;
}

defineProps<IProps>();

const breakpoints = useBreakpoints(breakpointsTailwind);

const isMobile = breakpoints.smaller("lg");
</script>

<style lang="scss">
.wishlist-products-skeleton {
$desktop: "";

&--mobile {
@apply grid grid-cols-2 gap-x-4 gap-y-6;
}

&--desktop {
@apply flex flex-col rounded border bg-additional-50 shadow-sm;

$desktop: &;
}

&__item {
#{$desktop} & {
@apply even:bg-neutral-50;
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: CSS Class Naming Violates BEM Convention

The CSS class naming does not follow the BEM naming convention as specifically requested by the reviewer. The classes should be named based on the component name (e.g., "wishlist-products-skeleton wishlist-products-skeleton--mobile") instead of generic names like "skeleton-mobile" and "skeleton-desktop". This violates the project's naming standards as indicated in the PR discussion.

Fix in Cursor Fix in Web

</style>
5 changes: 4 additions & 1 deletion locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
"show_less": "Weniger anzeigen",
"expand_all": "Alle ausklappen",
"all_products": "Alle Produkte",
"buy_now": "Jetzt kaufen"
"buy_now": "Jetzt kaufen",
"see_all": "Alle anzeigen"
},
"labels": {
"actions": "Aktionen",
Expand Down Expand Up @@ -335,6 +336,7 @@
"change_password": "@:common.links.change_password",
"addresses": "@:common.links.addresses",
"orders": "@:common.links.orders",
"saved_for_later": "Für später gespeichert",
"lists": "@:common.links.lists",
"saved_credit_cards": "@:common.links.saved_credit_cards"
},
Expand Down Expand Up @@ -1310,6 +1312,7 @@
"recently_browsed_products": "Kürzlich angesehen",
"save_for_later": "Für später speichern",
"saved_for_later": "Für später gespeichert",
"saved_for_later_not_found": "Sie haben noch keine Produkte für später gespeichert",
"move_to_cart": "In den Warenkorb"
},
"checkout": {
Expand Down
Loading
Loading