Files
EpicNext-Cms/src/features/catalog/domain/offer-discovery.ts
T
Simo 93a7e9a7c2
CI / check (push) Successful in 1m14s
CI / deploy (push) Successful in 1m29s
feat(catalog): improve visual manager search and offer discovery
2026-09-06 20:58:53 +02:00

79 lines
1.8 KiB
TypeScript

import type { CatalogItemData } from "@/app/admin/catalog/[id]/catalog-items-table/types";
export const OFFER_FILTERS = [
"all",
"free",
"paid",
"bundles",
"limited",
] as const;
export type OfferFilter = (typeof OFFER_FILTERS)[number];
type DiscoverableOffer = Pick<
CatalogItemData,
| "id"
| "catalogName"
| "baseItemName"
| "baseName"
| "itemIds"
| "costCredits"
| "costPoints"
| "limitedStack"
>;
function matchesFilter(item: DiscoverableOffer, filter: OfferFilter) {
switch (filter) {
case "all":
return true;
case "free":
return item.costCredits === 0 && item.costPoints === 0;
case "paid":
return item.costCredits > 0 || item.costPoints > 0;
case "bundles":
return (
item.itemIds.split(";").filter((part) => part.trim().length > 0)
.length > 1
);
case "limited":
return item.limitedStack > 0;
}
}
/** Facet counts apply to the text search so changing a facet never hides its available matches. */
export function discoverOffers<T extends DiscoverableOffer>(
items: readonly T[],
query: string,
filter: OfferFilter,
) {
const normalized = query.trim().toLowerCase();
const exactId = /^#\d+$/.test(normalized) ? normalized.slice(1) : null;
const matches =
exactId !== null
? items.filter((item) => String(item.id) === exactId)
: normalized
? items.filter((item) =>
[
item.catalogName,
item.baseItemName,
item.baseName,
item.itemIds,
String(item.id),
].some((value) => value.toLowerCase().includes(normalized)),
)
: [...items];
const counts = {
all: matches.length,
free: 0,
paid: 0,
bundles: 0,
limited: 0,
};
for (const item of matches)
for (const facet of OFFER_FILTERS) {
if (facet !== "all" && matchesFilter(item, facet)) counts[facet] += 1;
}
return {
items: matches.filter((item) => matchesFilter(item, filter)),
counts,
};
}