@86d-app/products 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +65 -0
- package/COMPONENTS.md +231 -0
- package/README.md +201 -0
- package/package.json +46 -0
- package/src/__tests__/controllers.test.ts +2227 -0
- package/src/__tests__/state.test.ts +138 -0
- package/src/admin/components/categories-admin.mdx +3 -0
- package/src/admin/components/categories-admin.tsx +449 -0
- package/src/admin/components/category-form.mdx +9 -0
- package/src/admin/components/category-form.tsx +490 -0
- package/src/admin/components/category-list.mdx +75 -0
- package/src/admin/components/category-list.tsx +168 -0
- package/src/admin/components/collections-admin.mdx +3 -0
- package/src/admin/components/collections-admin.tsx +771 -0
- package/src/admin/components/index.tsx +8 -0
- package/src/admin/components/product-detail.mdx +12 -0
- package/src/admin/components/product-detail.tsx +790 -0
- package/src/admin/components/product-edit.tsx +60 -0
- package/src/admin/components/product-form.tsx +793 -0
- package/src/admin/components/product-list.mdx +3 -0
- package/src/admin/components/product-list.tsx +1125 -0
- package/src/admin/components/product-new.tsx +38 -0
- package/src/admin/endpoints/add-collection-product.ts +17 -0
- package/src/admin/endpoints/bulk-action.ts +43 -0
- package/src/admin/endpoints/create-category.ts +52 -0
- package/src/admin/endpoints/create-collection.ts +35 -0
- package/src/admin/endpoints/create-product.ts +50 -0
- package/src/admin/endpoints/create-variant.ts +45 -0
- package/src/admin/endpoints/delete-category.ts +27 -0
- package/src/admin/endpoints/delete-collection.ts +12 -0
- package/src/admin/endpoints/delete-product.ts +27 -0
- package/src/admin/endpoints/delete-variant.ts +27 -0
- package/src/admin/endpoints/get-product.ts +23 -0
- package/src/admin/endpoints/import-products.ts +47 -0
- package/src/admin/endpoints/index.ts +43 -0
- package/src/admin/endpoints/list-categories.ts +21 -0
- package/src/admin/endpoints/list-collections.ts +20 -0
- package/src/admin/endpoints/list-products.ts +25 -0
- package/src/admin/endpoints/remove-collection-product.ts +15 -0
- package/src/admin/endpoints/update-category.ts +82 -0
- package/src/admin/endpoints/update-collection.ts +22 -0
- package/src/admin/endpoints/update-product.ts +67 -0
- package/src/admin/endpoints/update-variant.ts +41 -0
- package/src/controllers.ts +1410 -0
- package/src/index.ts +120 -0
- package/src/markdown.ts +150 -0
- package/src/mdx.d.ts +5 -0
- package/src/schema.ts +352 -0
- package/src/state.ts +84 -0
- package/src/store/components/_hooks.ts +78 -0
- package/src/store/components/_types.ts +73 -0
- package/src/store/components/_utils.ts +14 -0
- package/src/store/components/back-in-stock-notify.tsx +97 -0
- package/src/store/components/collection-card.mdx +42 -0
- package/src/store/components/collection-card.tsx +12 -0
- package/src/store/components/collection-detail.mdx +12 -0
- package/src/store/components/collection-detail.tsx +149 -0
- package/src/store/components/collection-grid.mdx +9 -0
- package/src/store/components/collection-grid.tsx +80 -0
- package/src/store/components/featured-products.mdx +9 -0
- package/src/store/components/featured-products.tsx +75 -0
- package/src/store/components/filter-chip.mdx +25 -0
- package/src/store/components/filter-chip.tsx +12 -0
- package/src/store/components/index.tsx +39 -0
- package/src/store/components/product-card.mdx +69 -0
- package/src/store/components/product-card.tsx +71 -0
- package/src/store/components/product-detail.mdx +30 -0
- package/src/store/components/product-detail.tsx +488 -0
- package/src/store/components/product-listing.mdx +7 -0
- package/src/store/components/product-listing.tsx +423 -0
- package/src/store/components/product-reviews-section.mdx +21 -0
- package/src/store/components/product-reviews-section.tsx +372 -0
- package/src/store/components/recently-viewed.tsx +100 -0
- package/src/store/components/related-products.mdx +6 -0
- package/src/store/components/related-products.tsx +62 -0
- package/src/store/components/star-display.mdx +18 -0
- package/src/store/components/star-display.tsx +27 -0
- package/src/store/components/star-picker.mdx +21 -0
- package/src/store/components/star-picker.tsx +21 -0
- package/src/store/components/stock-badge.mdx +12 -0
- package/src/store/components/stock-badge.tsx +19 -0
- package/src/store/endpoints/get-category.ts +61 -0
- package/src/store/endpoints/get-collection.ts +46 -0
- package/src/store/endpoints/get-featured.ts +18 -0
- package/src/store/endpoints/get-product.ts +52 -0
- package/src/store/endpoints/get-related.ts +20 -0
- package/src/store/endpoints/index.ts +23 -0
- package/src/store/endpoints/list-categories.ts +13 -0
- package/src/store/endpoints/list-collections.ts +22 -0
- package/src/store/endpoints/list-products.ts +28 -0
- package/src/store/endpoints/search-products.ts +18 -0
- package/src/store/endpoints/store-search.ts +111 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createStoreEndpoint, z } from "@86d-app/core";
|
|
2
|
+
import type { Category, Product } from "../../controllers";
|
|
3
|
+
|
|
4
|
+
export const getCategory = createStoreEndpoint(
|
|
5
|
+
"/categories/:id",
|
|
6
|
+
{
|
|
7
|
+
method: "GET",
|
|
8
|
+
params: z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
}),
|
|
11
|
+
query: z
|
|
12
|
+
.object({
|
|
13
|
+
includeProducts: z.string().optional(),
|
|
14
|
+
})
|
|
15
|
+
.optional(),
|
|
16
|
+
},
|
|
17
|
+
async (ctx) => {
|
|
18
|
+
const { params, query = {} } = ctx;
|
|
19
|
+
const controllers = ctx.context.controllers;
|
|
20
|
+
|
|
21
|
+
// Try to get by ID first
|
|
22
|
+
let category = (await controllers.category.getById(ctx)) as Category | null;
|
|
23
|
+
|
|
24
|
+
// If not found, try by slug
|
|
25
|
+
if (!category) {
|
|
26
|
+
category = (await controllers.category.getBySlug({
|
|
27
|
+
...ctx,
|
|
28
|
+
query: { slug: params.id },
|
|
29
|
+
})) as Category | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!category) {
|
|
33
|
+
return {
|
|
34
|
+
error: "Category not found",
|
|
35
|
+
status: 404,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Only return visible categories to store
|
|
40
|
+
if (!category.isVisible) {
|
|
41
|
+
return {
|
|
42
|
+
error: "Category not found",
|
|
43
|
+
status: 404,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Optionally include products
|
|
48
|
+
let products: unknown[] | undefined;
|
|
49
|
+
if (query.includeProducts === "true") {
|
|
50
|
+
products = (await controllers.product.getByCategory({
|
|
51
|
+
...ctx,
|
|
52
|
+
params: { categoryId: category.id },
|
|
53
|
+
})) as Product[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
category,
|
|
58
|
+
products,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createStoreEndpoint, z } from "@86d-app/core";
|
|
2
|
+
import type { Collection, CollectionWithProducts } from "../../controllers";
|
|
3
|
+
|
|
4
|
+
export const getCollection = createStoreEndpoint(
|
|
5
|
+
"/collections/:id",
|
|
6
|
+
{
|
|
7
|
+
method: "GET",
|
|
8
|
+
params: z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
}),
|
|
11
|
+
},
|
|
12
|
+
async (ctx) => {
|
|
13
|
+
const { params } = ctx;
|
|
14
|
+
const controllers = ctx.context.controllers;
|
|
15
|
+
|
|
16
|
+
// Try by ID first
|
|
17
|
+
let collection = (await controllers.collection.getWithProducts(
|
|
18
|
+
ctx,
|
|
19
|
+
)) as CollectionWithProducts | null;
|
|
20
|
+
|
|
21
|
+
// If not found, try by slug
|
|
22
|
+
if (!collection) {
|
|
23
|
+
const bySlug = (await controllers.collection.getBySlug({
|
|
24
|
+
...ctx,
|
|
25
|
+
query: { slug: params.id },
|
|
26
|
+
})) as Collection | null;
|
|
27
|
+
if (bySlug) {
|
|
28
|
+
collection = (await controllers.collection.getWithProducts({
|
|
29
|
+
...ctx,
|
|
30
|
+
params: { id: bySlug.id },
|
|
31
|
+
})) as CollectionWithProducts | null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!collection) {
|
|
36
|
+
return { error: "Collection not found", status: 404 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Only return visible collections to store
|
|
40
|
+
if (!collection.isVisible) {
|
|
41
|
+
return { error: "Collection not found", status: 404 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { collection };
|
|
45
|
+
},
|
|
46
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createStoreEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const getFeaturedProducts = createStoreEndpoint(
|
|
4
|
+
"/products/featured",
|
|
5
|
+
{
|
|
6
|
+
method: "GET",
|
|
7
|
+
query: z
|
|
8
|
+
.object({
|
|
9
|
+
limit: z.string().optional(),
|
|
10
|
+
})
|
|
11
|
+
.optional(),
|
|
12
|
+
},
|
|
13
|
+
async (ctx) => {
|
|
14
|
+
// Call the controller directly
|
|
15
|
+
const products = await ctx.context.controllers.product.getFeatured(ctx);
|
|
16
|
+
return { products };
|
|
17
|
+
},
|
|
18
|
+
);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createStoreEndpoint, z } from "@86d-app/core";
|
|
2
|
+
import type { Product, ProductWithVariants } from "../../controllers";
|
|
3
|
+
|
|
4
|
+
export const getProduct = createStoreEndpoint(
|
|
5
|
+
"/products/:id",
|
|
6
|
+
{
|
|
7
|
+
method: "GET",
|
|
8
|
+
params: z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
}),
|
|
11
|
+
},
|
|
12
|
+
async (ctx) => {
|
|
13
|
+
const { params } = ctx;
|
|
14
|
+
const controllers = ctx.context.controllers;
|
|
15
|
+
|
|
16
|
+
// Try to get by ID first
|
|
17
|
+
let product = (await controllers.product.getWithVariants(
|
|
18
|
+
ctx,
|
|
19
|
+
)) as ProductWithVariants | null;
|
|
20
|
+
|
|
21
|
+
// If not found, try by slug
|
|
22
|
+
if (!product) {
|
|
23
|
+
const bySlug = (await controllers.product.getBySlug({
|
|
24
|
+
...ctx,
|
|
25
|
+
query: { slug: params.id },
|
|
26
|
+
})) as Product | null;
|
|
27
|
+
if (bySlug) {
|
|
28
|
+
product = (await controllers.product.getWithVariants({
|
|
29
|
+
...ctx,
|
|
30
|
+
params: { id: bySlug.id },
|
|
31
|
+
})) as ProductWithVariants | null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!product) {
|
|
36
|
+
return {
|
|
37
|
+
error: "Product not found",
|
|
38
|
+
status: 404,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Only return active products to store
|
|
43
|
+
if (product.status !== "active") {
|
|
44
|
+
return {
|
|
45
|
+
error: "Product not found",
|
|
46
|
+
status: 404,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { product };
|
|
51
|
+
},
|
|
52
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createStoreEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const getRelatedProducts = createStoreEndpoint(
|
|
4
|
+
"/products/:id/related",
|
|
5
|
+
{
|
|
6
|
+
method: "GET",
|
|
7
|
+
params: z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
}),
|
|
10
|
+
query: z
|
|
11
|
+
.object({
|
|
12
|
+
limit: z.string().optional(),
|
|
13
|
+
})
|
|
14
|
+
.optional(),
|
|
15
|
+
},
|
|
16
|
+
async (ctx) => {
|
|
17
|
+
const result = await ctx.context.controllers.product.getRelated(ctx);
|
|
18
|
+
return result;
|
|
19
|
+
},
|
|
20
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getCategory } from "./get-category";
|
|
2
|
+
import { getCollection } from "./get-collection";
|
|
3
|
+
import { getFeaturedProducts } from "./get-featured";
|
|
4
|
+
import { getProduct } from "./get-product";
|
|
5
|
+
import { getRelatedProducts } from "./get-related";
|
|
6
|
+
import { listCategories } from "./list-categories";
|
|
7
|
+
import { listCollections } from "./list-collections";
|
|
8
|
+
import { listProducts } from "./list-products";
|
|
9
|
+
import { searchProducts } from "./search-products";
|
|
10
|
+
import { storeSearch } from "./store-search";
|
|
11
|
+
|
|
12
|
+
export const storeEndpoints = {
|
|
13
|
+
"/products": listProducts,
|
|
14
|
+
"/products/featured": getFeaturedProducts,
|
|
15
|
+
"/products/search": searchProducts,
|
|
16
|
+
"/products/store-search": storeSearch,
|
|
17
|
+
"/products/:id": getProduct,
|
|
18
|
+
"/products/:id/related": getRelatedProducts,
|
|
19
|
+
"/categories": listCategories,
|
|
20
|
+
"/categories/:id": getCategory,
|
|
21
|
+
"/collections": listCollections,
|
|
22
|
+
"/collections/:id": getCollection,
|
|
23
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createStoreEndpoint } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const listCategories = createStoreEndpoint(
|
|
4
|
+
"/categories",
|
|
5
|
+
{
|
|
6
|
+
method: "GET",
|
|
7
|
+
},
|
|
8
|
+
async (ctx) => {
|
|
9
|
+
// Call the controller directly to get category tree
|
|
10
|
+
const categories = await ctx.context.controllers.category.getTree(ctx);
|
|
11
|
+
return { categories };
|
|
12
|
+
},
|
|
13
|
+
);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createStoreEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const listCollections = createStoreEndpoint(
|
|
4
|
+
"/collections",
|
|
5
|
+
{
|
|
6
|
+
method: "GET",
|
|
7
|
+
query: z
|
|
8
|
+
.object({
|
|
9
|
+
page: z.string().optional(),
|
|
10
|
+
limit: z.string().optional(),
|
|
11
|
+
featured: z.string().optional(),
|
|
12
|
+
})
|
|
13
|
+
.optional(),
|
|
14
|
+
},
|
|
15
|
+
async (ctx) => {
|
|
16
|
+
const result = await ctx.context.controllers.collection.list({
|
|
17
|
+
...ctx,
|
|
18
|
+
query: { ...ctx.query, visible: "true" },
|
|
19
|
+
});
|
|
20
|
+
return result;
|
|
21
|
+
},
|
|
22
|
+
);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createStoreEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const listProducts = createStoreEndpoint(
|
|
4
|
+
"/products",
|
|
5
|
+
{
|
|
6
|
+
method: "GET",
|
|
7
|
+
query: z
|
|
8
|
+
.object({
|
|
9
|
+
page: z.string().optional(),
|
|
10
|
+
limit: z.string().optional(),
|
|
11
|
+
category: z.string().optional(),
|
|
12
|
+
featured: z.string().optional(),
|
|
13
|
+
search: z.string().optional(),
|
|
14
|
+
sort: z.enum(["name", "price", "createdAt", "updatedAt"]).optional(),
|
|
15
|
+
order: z.enum(["asc", "desc"]).optional(),
|
|
16
|
+
minPrice: z.string().optional(),
|
|
17
|
+
maxPrice: z.string().optional(),
|
|
18
|
+
inStock: z.string().optional(),
|
|
19
|
+
tag: z.string().optional(),
|
|
20
|
+
})
|
|
21
|
+
.optional(),
|
|
22
|
+
},
|
|
23
|
+
async (ctx) => {
|
|
24
|
+
// Call the controller directly - it handles query parsing internally
|
|
25
|
+
const result = await ctx.context.controllers.product.list(ctx);
|
|
26
|
+
return result;
|
|
27
|
+
},
|
|
28
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createStoreEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const searchProducts = createStoreEndpoint(
|
|
4
|
+
"/products/search",
|
|
5
|
+
{
|
|
6
|
+
method: "GET",
|
|
7
|
+
query: z.object({
|
|
8
|
+
q: z.string().min(1).max(500),
|
|
9
|
+
limit: z.string().optional(),
|
|
10
|
+
}),
|
|
11
|
+
},
|
|
12
|
+
async (ctx) => {
|
|
13
|
+
const { query } = ctx;
|
|
14
|
+
// Call the controller directly
|
|
15
|
+
const products = await ctx.context.controllers.product.search(ctx);
|
|
16
|
+
return { products, query: query.q };
|
|
17
|
+
},
|
|
18
|
+
);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createStoreEndpoint, z } from "@86d-app/core";
|
|
2
|
+
import type { Collection, Product } from "../../controllers";
|
|
3
|
+
|
|
4
|
+
/** Quick links shown in store command search (label and href). */
|
|
5
|
+
const QUICK_LINKS = [
|
|
6
|
+
{ id: "products", label: "All products", href: "/products", group: "Pages" },
|
|
7
|
+
{
|
|
8
|
+
id: "collections",
|
|
9
|
+
label: "Collections",
|
|
10
|
+
href: "/collections",
|
|
11
|
+
group: "Pages",
|
|
12
|
+
},
|
|
13
|
+
{ id: "blog", label: "Blog", href: "/blog", group: "Pages" },
|
|
14
|
+
{ id: "about", label: "About", href: "/about", group: "Pages" },
|
|
15
|
+
{ id: "contact", label: "Contact", href: "/contact", group: "Pages" },
|
|
16
|
+
{ id: "terms", label: "Terms", href: "/terms", group: "Pages" },
|
|
17
|
+
{ id: "privacy", label: "Privacy", href: "/privacy", group: "Pages" },
|
|
18
|
+
{ id: "account", label: "Account", href: "/account", group: "Account" },
|
|
19
|
+
{
|
|
20
|
+
id: "profile",
|
|
21
|
+
label: "Profile",
|
|
22
|
+
href: "/account/profile",
|
|
23
|
+
group: "Account",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "addresses",
|
|
27
|
+
label: "Addresses",
|
|
28
|
+
href: "/account/addresses",
|
|
29
|
+
group: "Account",
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const storeSearch = createStoreEndpoint(
|
|
34
|
+
"/products/store-search",
|
|
35
|
+
{
|
|
36
|
+
method: "GET",
|
|
37
|
+
query: z.object({
|
|
38
|
+
q: z.string().min(0).max(500),
|
|
39
|
+
limit: z.string().optional(),
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
async (ctx) => {
|
|
43
|
+
const { query } = ctx;
|
|
44
|
+
const q = query.q.trim();
|
|
45
|
+
const limit = query.limit ? parseInt(query.limit, 10) : 15;
|
|
46
|
+
const queryLower = q.toLowerCase();
|
|
47
|
+
|
|
48
|
+
const results: Array<{
|
|
49
|
+
id: string;
|
|
50
|
+
label: string;
|
|
51
|
+
href: string;
|
|
52
|
+
image?: string;
|
|
53
|
+
subtitle?: string;
|
|
54
|
+
group?: string;
|
|
55
|
+
}> = [];
|
|
56
|
+
|
|
57
|
+
if (q.length > 0) {
|
|
58
|
+
const [products, collections] = (await Promise.all([
|
|
59
|
+
ctx.context.controllers.product.search({
|
|
60
|
+
...ctx,
|
|
61
|
+
query: { q, limit: String(Math.min(limit, 8)) },
|
|
62
|
+
}),
|
|
63
|
+
ctx.context.controllers.collection.search({
|
|
64
|
+
...ctx,
|
|
65
|
+
query: { q, limit: String(Math.min(limit, 5)) },
|
|
66
|
+
}),
|
|
67
|
+
])) as [Product[], Collection[]];
|
|
68
|
+
|
|
69
|
+
for (const p of products) {
|
|
70
|
+
const image = p.images?.[0];
|
|
71
|
+
results.push({
|
|
72
|
+
id: `product-${p.id}`,
|
|
73
|
+
label: p.name,
|
|
74
|
+
href: `/products/${p.slug}`,
|
|
75
|
+
...(image ? { image } : {}),
|
|
76
|
+
...(p.price != null ? { subtitle: formatCents(p.price) } : {}),
|
|
77
|
+
group: "Products",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
for (const c of collections) {
|
|
81
|
+
results.push({
|
|
82
|
+
id: `collection-${c.id}`,
|
|
83
|
+
label: c.name,
|
|
84
|
+
href: `/collections/${c.slug}`,
|
|
85
|
+
...(c.image ? { image: c.image } : {}),
|
|
86
|
+
group: "Collections",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const link of QUICK_LINKS) {
|
|
92
|
+
if (q.length === 0 || link.label.toLowerCase().includes(queryLower)) {
|
|
93
|
+
results.push({
|
|
94
|
+
id: link.id,
|
|
95
|
+
label: link.label,
|
|
96
|
+
href: link.href,
|
|
97
|
+
group: link.group,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { results };
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
function formatCents(cents: number): string {
|
|
107
|
+
return new Intl.NumberFormat("en-US", {
|
|
108
|
+
style: "currency",
|
|
109
|
+
currency: "USD",
|
|
110
|
+
}).format(cents / 100);
|
|
111
|
+
}
|
package/tsconfig.json
ADDED