feat: paginate customer orders and polish the dashboard hero
Add paged orders API and infinite scroll, short order numbers, and align the guest invite card with the logged-in hero shell. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
// Server-side catalogue paging (STUB-03 step 4 / docs/CATALOG-PAGINATION-PLAN.md).
|
||||
// Replaces "download everything, slice in the browser" for list screens.
|
||||
import { computed, ref } from 'vue'
|
||||
import { api, type ProductQuery, type ProductSummary } from '../api/client'
|
||||
|
||||
export const CATALOG_PAGE_SIZE = 20
|
||||
|
||||
export function usePagedProducts() {
|
||||
const rows = ref<ProductSummary[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(0)
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
|
||||
const hasMore = computed(() => rows.value.length < total.value)
|
||||
|
||||
/**
|
||||
* @param opts.preserveRows — pull-to-refresh: keep the current list on screen
|
||||
* until the new page arrives (avoids an empty flash / stuck full-page spinner).
|
||||
*/
|
||||
async function resetAndLoad(query: ProductQuery, opts: { preserveRows?: boolean } = {}) {
|
||||
const showFullSpinner = !opts.preserveRows || rows.value.length === 0
|
||||
if (showFullSpinner) loading.value = true
|
||||
page.value = 1
|
||||
try {
|
||||
const res = await api.listProductsPaged({
|
||||
...query,
|
||||
page: 1,
|
||||
limit: query.limit ?? CATALOG_PAGE_SIZE,
|
||||
})
|
||||
rows.value = res.rows
|
||||
total.value = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore(query: ProductQuery) {
|
||||
if (loading.value || loadingMore.value || !hasMore.value) return
|
||||
loadingMore.value = true
|
||||
const next = page.value + 1
|
||||
try {
|
||||
const res = await api.listProductsPaged({
|
||||
...query,
|
||||
page: next,
|
||||
limit: query.limit ?? CATALOG_PAGE_SIZE,
|
||||
})
|
||||
page.value = next
|
||||
// Append; drop accidental duplicates if the catalogue shifted between pages.
|
||||
const seen = new Set(rows.value.map((p) => p.id))
|
||||
rows.value = [...rows.value, ...res.rows.filter((p) => !seen.has(p.id))]
|
||||
total.value = res.total
|
||||
} finally {
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
page.value = 0
|
||||
}
|
||||
|
||||
return { rows, total, page, loading, loadingMore, hasMore, resetAndLoad, loadMore, clear }
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
// Adapted from legacy henza_pwa/src/Mixins/getProducts.ts. Legacy paginated
|
||||
// against POST /get-products (page/limit/count). The new API's GET
|
||||
// /catalog/products returns the full active catalog in one flat response with
|
||||
// no pagination — so `isPull` (legacy's infinite-scroll "next page" trigger)
|
||||
// is a no-op here, and `totalPages` is always 0. A caller that wants new
|
||||
// products at the end of the list passes `force` to refresh the cached
|
||||
// catalogue instead of fabricating fake pages.
|
||||
import { reactive, watch } from 'vue'
|
||||
// Newest / list screens: true server paging (STUB-03 step 4).
|
||||
// Legacy paginated against POST /get-products; the early Nest port returned the
|
||||
// whole catalogue and treated `@fetch` as a no-op. This restores real pages.
|
||||
import { reactive } from 'vue'
|
||||
import type { ProductSummary } from '../api/client'
|
||||
import { useCatalogStore } from '../stores/catalogStore'
|
||||
import { usePagedProducts, CATALOG_PAGE_SIZE } from './usePagedProducts'
|
||||
|
||||
export interface ProductsData {
|
||||
totalPages: number
|
||||
@@ -16,28 +12,38 @@ export interface ProductsData {
|
||||
}
|
||||
|
||||
export function useProducts() {
|
||||
const catalog = useCatalogStore()
|
||||
const paged = usePagedProducts()
|
||||
const productsData: ProductsData = reactive({
|
||||
totalPages: 0,
|
||||
productList: catalog.products,
|
||||
productList: [] as ProductSummary[],
|
||||
flag: false,
|
||||
})
|
||||
|
||||
// A refresh from another screen (pull-to-refresh) lands here too.
|
||||
watch(
|
||||
() => catalog.products,
|
||||
(list) => {
|
||||
productsData.productList = list
|
||||
productsData.flag = list.length < 1
|
||||
},
|
||||
)
|
||||
function sync() {
|
||||
productsData.productList = paged.rows.value
|
||||
productsData.totalPages = Math.ceil(paged.total.value / CATALOG_PAGE_SIZE) || 0
|
||||
productsData.flag = paged.total.value < 1 && !paged.loading.value
|
||||
}
|
||||
|
||||
const getProducts = async (loading: { value: boolean }, isPull = false, force = false) => {
|
||||
if (isPull) return // no further pages exist server-side yet
|
||||
loading.value = force || !catalog.loaded
|
||||
void force
|
||||
if (isPull) {
|
||||
if (!paged.hasMore.value || paged.loadingMore.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
await paged.loadMore({ sort: 'newest' })
|
||||
sync()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
productsData.productList = await catalog.load(force)
|
||||
productsData.flag = productsData.productList.length < 1
|
||||
await paged.resetAndLoad({ sort: 'newest' })
|
||||
sync()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
} finally {
|
||||
@@ -45,5 +51,5 @@ export function useProducts() {
|
||||
}
|
||||
}
|
||||
|
||||
return { productsData, getProducts }
|
||||
return { productsData, getProducts, hasMore: paged.hasMore }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user