feat(apps): read products by id instead of loading the whole catalogue
Step 3 of docs/CATALOG-PAGINATION-PLAN.md. Step 2 added the targeted routes; these are the five callers that were fetching all ~16k products to find a handful of rows: product screen whole catalogue, then find(id) -> GET /products/:id cart re-sync whole catalogue for 2-3 lines -> ?ids= favourites whole catalogue, filtered here -> ?ids= home categories categories derived from it -> /products/facets order row labels joined on variantId -> included with the order Three of these were not merely slow, they were going to be wrong. Favourites filtered the loaded catalogue, so once the list pages a favourite outside the loaded slice silently disappears. Home categories were derived the same way, so a category whose products all sit further down would vanish from the home screen. Order rows looked their title up in the catalogue, which ALREADY failed for a product that had since left the storefront — exactly the old orders history exists for — and rendered a blank row. Order labels went to the server rather than to another id lookup: the item carries productVariantId, not a product id, so ?ids= could not answer it. One nested include in getOrders returns the variant, its product and the product's first image. Deliberately NOT filtered by storefront visibility, so a de-listed product still shows what was bought; asserted in the new spec. OrderItemCard is synchronous now — no fetch, no onMounted, the row renders complete on its first frame. variantLookup is deleted from catalogStore rather than left behind: it existed only to label order rows, and keeping it would have kept a reason to load the full catalogue. getProductsByIds chunks at 100 because the route caps ids there and drops the rest. A partial answer reads to callers as "these products are no longer on the storefront" — the cart would silently drop valid lines. Favourites already run to 874 rows in the imported data (FAV-01). A failed cart re-sync now leaves the cart alone: it is the person's own data in localStorage, and dropping lines because the network blinked is worse than a slightly stale price. The product screen tells a real 404 from a failed request, so only the former renders the not-found page. One bug introduced and caught while doing this: moving the home categories to /facets meant memory.init() no longer loaded the catalogue as a side effect, and the home rails read catalog.products — the home screen would have rendered empty rails. HomeView now loads it explicitly. Found by grepping every caller of configStore.init, which is also how I know HomeView is the only one. Still on the unpaged list by design: the home rails and the search/newest lists. Moving those onto paged reads is step 4, kept separate because it is the risky part — debounce, loading state, and telling "no results" from "still loading". api 334/334 across 30 suites; both apps vue-tsc clean. getOrders has a spec now (3 cases) because its include is a contract both apps depend on and the method had no test at all. Nothing click-tested — the cart re-sync and the favourites screen are the two worth a manual pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cbb93dec5d
commit
9fc0886a57
@@ -247,14 +247,23 @@ function goNewest() {
|
||||
// The three reads are independent, so they run side by side. Each one is
|
||||
// cached until pull-to-refresh asks the server again.
|
||||
async function loadData(force = false) {
|
||||
const [catalogResult, priceResult, slidesResult] = await Promise.allSettled([
|
||||
// memory.init() reads the same cached catalogue, so this is one request.
|
||||
const [catalogResult, catsResult, priceResult, slidesResult] = await Promise.allSettled([
|
||||
// The home rails read `catalog.products`, so the catalogue is loaded
|
||||
// explicitly. It used to come in as a side effect of `memory.init()`, which
|
||||
// now reads the small facets endpoint instead of the whole catalogue — so
|
||||
// without this call the home screen would render empty rails.
|
||||
//
|
||||
// Still the unpaged list: the rails slice it locally («جدیدترین», per
|
||||
// category). Moving them onto paged reads is step 4 of
|
||||
// docs/CATALOG-PAGINATION-PLAN.md.
|
||||
catalog.load(force),
|
||||
memory.init(force),
|
||||
reads.goldPrice({ force }),
|
||||
reads.homeSlider({ force }),
|
||||
])
|
||||
|
||||
if (catalogResult.status === 'rejected') console.log(catalogResult.reason)
|
||||
if (catsResult.status === 'rejected') console.log(catsResult.reason)
|
||||
goldPrice.value = priceResult.status === 'fulfilled' ? priceResult.value : null
|
||||
// No admin slides configured, or the endpoint failed — the category-derived
|
||||
// hero below covers this silently, same as before this existed.
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
restyled into the redesigned design language (see components/home/* and
|
||||
HomeView).
|
||||
|
||||
Real, not stubbed: favorites are the local favoritesStore (per-device, no
|
||||
backend persistence — see favoritesStore.ts from an earlier batch), filtered
|
||||
against the real product catalog client-side. No pagination — the whole
|
||||
catalog is fetched once (same "small catalog" approach as everywhere else).
|
||||
That logic is carried over unchanged; only the chrome and the card change.
|
||||
Favourites are server-side (FAV-01: GET/POST customer/me/favorites), and the
|
||||
products behind them are fetched BY ID — the saved ids go straight to
|
||||
`?ids=…`, one request for the lot.
|
||||
|
||||
It used to fetch the whole catalogue and filter it in the browser, which with
|
||||
~16k products meant downloading all of them to show a handful. It was also
|
||||
wrong beyond the first page once paging existed: a favourite outside the
|
||||
loaded slice would simply not appear.
|
||||
-->
|
||||
<template>
|
||||
<div class="favs">
|
||||
@@ -35,7 +38,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
@@ -43,26 +46,41 @@ import Spinner from '../../components/Spinner.vue'
|
||||
import SectionHead from '../../components/home/SectionHead.vue'
|
||||
import ProductCard from '../../components/product/ProductCard.vue'
|
||||
import { useFavoritesStore } from '../../stores/favoritesStore'
|
||||
import { useCatalogStore } from '../../stores/catalogStore'
|
||||
import { api, type ProductSummary } from '../../api/client'
|
||||
|
||||
const router = useRouter()
|
||||
const favorites = useFavoritesStore()
|
||||
const catalog = useCatalogStore()
|
||||
const loading = ref(false)
|
||||
const allProducts = computed(() => catalog.products)
|
||||
const fetched = ref<ProductSummary[]>([])
|
||||
|
||||
const favoriteProducts = computed(() => allProducts.value.filter((p) => favorites.isFavorite(p.id)))
|
||||
/**
|
||||
* Still filtered against the store, so un-hearting a product removes its card
|
||||
* at once without waiting for a refetch — the fetched list is the source of the
|
||||
* product DATA, the store is the source of truth for what is saved.
|
||||
*/
|
||||
const favoriteProducts = computed(() => fetched.value.filter((p) => favorites.isFavorite(p.id)))
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = !catalog.loaded
|
||||
async function load() {
|
||||
const ids = favorites.ids
|
||||
if (ids.length === 0) {
|
||||
fetched.value = []
|
||||
return
|
||||
}
|
||||
loading.value = fetched.value.length === 0
|
||||
try {
|
||||
await catalog.load()
|
||||
fetched.value = await api.getProductsByIds(ids)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
|
||||
// The ids arrive from the server just after sign-in, and change when the person
|
||||
// hearts something elsewhere in the app — both land here as a new id list.
|
||||
watch(() => favorites.ids.join(','), load)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
Reference in New Issue
Block a user