This commit is contained in:
Nông Đức Huy
2026-08-13 23:20:22 +07:00
parent 3e5d38ec18
commit 3d6b0e0d4e
145 changed files with 7817 additions and 801 deletions
+3
View File
@@ -1,4 +1,5 @@
import { HttpClient, type HttpClientOptions } from './http-client';
import { createCatalogResource, type CatalogResource } from './resources/catalog';
import { createHealthResource, type HealthResource } from './resources/health';
/**
@@ -10,6 +11,7 @@ import { createHealthResource, type HealthResource } from './resources/health';
export interface ApiClient {
readonly http: HttpClient;
readonly health: HealthResource;
readonly catalog: CatalogResource;
}
export function createApiClient(options: HttpClientOptions): ApiClient {
@@ -18,5 +20,6 @@ export function createApiClient(options: HttpClientOptions): ApiClient {
return {
http,
health: createHealthResource(http),
catalog: createCatalogResource(http),
};
}
+1
View File
@@ -15,4 +15,5 @@ export { ApiClientError, isApiClientError } from './errors';
export { HttpClient } from './http-client';
export type { HttpClientOptions, RequestOptions } from './http-client';
export { createApiClient } from './create-client';
export type { CatalogResource, ProductListQuery } from './resources/catalog';
export type { ApiClient } from './create-client';
@@ -0,0 +1,120 @@
import type {
Brand,
Category,
CategoryNode,
Collection,
Locale,
NavigationMenu,
ProductListResult,
StorefrontProduct,
} from '@sport/types';
import type { HttpClient, RequestOptions } from '../http-client';
/**
* Filters accepted by the listing endpoint. Mirrors `productFilterSchema` in
* @sport/validation — the server re-validates, so this type is a convenience
* for callers, never the enforcement point.
*/
export interface ProductListQuery {
q?: string;
categorySlug?: string;
collectionSlug?: string;
brandSlugs?: readonly string[];
gender?: readonly string[];
sport?: readonly string[];
colors?: readonly string[];
sizes?: readonly string[];
minPrice?: number;
maxPrice?: number;
onSale?: boolean;
inStockOnly?: boolean;
sort?: 'newest' | 'price_asc' | 'price_desc' | 'best_selling' | 'relevance';
cursor?: string | null;
limit?: number;
}
/**
* Cache policy is the *caller's* decision, not this package's — a listing on
* the homepage and the same listing behind a filter want different revalidation.
* Every method therefore accepts RequestOptions and applies no default beyond
* what the endpoint semantically requires.
*/
export interface CatalogResource {
listProducts(
locale: Locale,
query?: ProductListQuery,
options?: RequestOptions,
): Promise<ProductListResult>;
getProduct(locale: Locale, slug: string, options?: RequestOptions): Promise<StorefrontProduct>;
listProductSlugs(
locale: Locale,
options?: RequestOptions,
): Promise<{ slug: string; updatedAt: string }[]>;
listBrands(locale: Locale, options?: RequestOptions): Promise<Brand[]>;
getBrand(locale: Locale, slug: string, options?: RequestOptions): Promise<Brand>;
getCategoryTree(locale: Locale, options?: RequestOptions): Promise<CategoryNode[]>;
getCategory(locale: Locale, slug: string, options?: RequestOptions): Promise<Category>;
listCollections(locale: Locale, options?: RequestOptions): Promise<Collection[]>;
getCollection(locale: Locale, slug: string, options?: RequestOptions): Promise<Collection>;
getNavigation(locale: Locale, options?: RequestOptions): Promise<NavigationMenu>;
}
export function createCatalogResource(http: HttpClient): CatalogResource {
/** Locale rides along on every catalog request; callers never forget it. */
const withLocale = (
locale: Locale,
query: Record<string, unknown> = {},
options?: RequestOptions,
): RequestOptions => ({
...options,
query: { locale, ...(query as RequestOptions['query']), ...options?.query },
});
return {
listProducts: (locale, query = {}, options) =>
http.get<ProductListResult>('/products', withLocale(locale, { ...query }, options)),
getProduct: (locale, slug, options) =>
http.get<StorefrontProduct>(
`/products/${encodeURIComponent(slug)}`,
withLocale(locale, {}, options),
),
listProductSlugs: (locale, options) =>
http.get<{ slug: string; updatedAt: string }[]>(
'/products/slugs',
withLocale(locale, {}, options),
),
listBrands: (locale, options) => http.get<Brand[]>('/brands', withLocale(locale, {}, options)),
getBrand: (locale, slug, options) =>
http.get<Brand>(`/brands/${encodeURIComponent(slug)}`, withLocale(locale, {}, options)),
getCategoryTree: (locale, options) =>
http.get<CategoryNode[]>('/categories', withLocale(locale, {}, options)),
getCategory: (locale, slug, options) =>
http.get<Category>(
`/categories/${encodeURIComponent(slug)}`,
withLocale(locale, {}, options),
),
listCollections: (locale, options) =>
http.get<Collection[]>('/collections', withLocale(locale, {}, options)),
getCollection: (locale, slug, options) =>
http.get<Collection>(
`/collections/${encodeURIComponent(slug)}`,
withLocale(locale, {}, options),
),
getNavigation: (locale, options) =>
http.get<NavigationMenu>('/navigation', withLocale(locale, {}, options)),
};
}