@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,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ProductForm } from "./product-form";
|
|
4
|
+
|
|
5
|
+
export function ProductNew() {
|
|
6
|
+
return (
|
|
7
|
+
<div>
|
|
8
|
+
<div className="mb-6 flex items-center gap-3">
|
|
9
|
+
<a
|
|
10
|
+
href="/admin/products"
|
|
11
|
+
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
12
|
+
>
|
|
13
|
+
<svg
|
|
14
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
15
|
+
width="20"
|
|
16
|
+
height="20"
|
|
17
|
+
viewBox="0 0 24 24"
|
|
18
|
+
fill="none"
|
|
19
|
+
stroke="currentColor"
|
|
20
|
+
strokeWidth="2"
|
|
21
|
+
strokeLinecap="round"
|
|
22
|
+
strokeLinejoin="round"
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
>
|
|
25
|
+
<path d="m15 18-6-6 6-6" />
|
|
26
|
+
</svg>
|
|
27
|
+
<span className="sr-only">Back to products</span>
|
|
28
|
+
</a>
|
|
29
|
+
<h1 className="font-semibold text-foreground text-lg">New product</h1>
|
|
30
|
+
</div>
|
|
31
|
+
<ProductForm
|
|
32
|
+
onNavigate={(path) => {
|
|
33
|
+
window.location.href = path;
|
|
34
|
+
}}
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createAdminEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const addCollectionProduct = createAdminEndpoint(
|
|
4
|
+
"/admin/collections/:id/products",
|
|
5
|
+
{
|
|
6
|
+
method: "POST",
|
|
7
|
+
params: z.object({ id: z.string() }),
|
|
8
|
+
body: z.object({
|
|
9
|
+
productId: z.string().min(1),
|
|
10
|
+
position: z.number().int().min(0).optional(),
|
|
11
|
+
}),
|
|
12
|
+
},
|
|
13
|
+
async (ctx) => {
|
|
14
|
+
const link = await ctx.context.controllers.collection.addProduct(ctx);
|
|
15
|
+
return { link, status: 201 };
|
|
16
|
+
},
|
|
17
|
+
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createAdminEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const bulkAction = createAdminEndpoint(
|
|
4
|
+
"/admin/products/bulk",
|
|
5
|
+
{
|
|
6
|
+
method: "POST",
|
|
7
|
+
body: z.object({
|
|
8
|
+
action: z.enum(["updateStatus", "delete"]),
|
|
9
|
+
ids: z.array(z.string()).min(1),
|
|
10
|
+
status: z.enum(["draft", "active", "archived"]).optional(),
|
|
11
|
+
}),
|
|
12
|
+
},
|
|
13
|
+
async (ctx) => {
|
|
14
|
+
const controllers = ctx.context.controllers;
|
|
15
|
+
const { action, ids, status } = ctx.body as {
|
|
16
|
+
action: "updateStatus" | "delete";
|
|
17
|
+
ids: string[];
|
|
18
|
+
status?: "draft" | "active" | "archived" | undefined;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (action === "updateStatus") {
|
|
22
|
+
if (!status) {
|
|
23
|
+
return {
|
|
24
|
+
error: "Status is required for updateStatus action",
|
|
25
|
+
status: 400,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return controllers.bulk.updateStatus({
|
|
29
|
+
...ctx,
|
|
30
|
+
body: { ids, status },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (action === "delete") {
|
|
35
|
+
return controllers.bulk.deleteMany({
|
|
36
|
+
...ctx,
|
|
37
|
+
body: { ids },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { error: "Unknown action", status: 400 };
|
|
42
|
+
},
|
|
43
|
+
);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createAdminEndpoint, sanitizeText, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const createCategory = createAdminEndpoint(
|
|
4
|
+
"/admin/categories",
|
|
5
|
+
{
|
|
6
|
+
method: "POST",
|
|
7
|
+
body: z.object({
|
|
8
|
+
name: z.string().min(1).max(200).transform(sanitizeText),
|
|
9
|
+
slug: z.string().min(1).max(200),
|
|
10
|
+
description: z.string().max(2000).transform(sanitizeText).optional(),
|
|
11
|
+
parentId: z.string().optional(),
|
|
12
|
+
image: z.string().optional(),
|
|
13
|
+
position: z.number().int().min(0).optional(),
|
|
14
|
+
isVisible: z.boolean().optional(),
|
|
15
|
+
metadata: z.record(z.string(), z.any()).optional(),
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
async (ctx) => {
|
|
19
|
+
const { body } = ctx;
|
|
20
|
+
const controllers = ctx.context.controllers;
|
|
21
|
+
|
|
22
|
+
// Check if slug is unique
|
|
23
|
+
const existingCategory = await controllers.category.getBySlug({
|
|
24
|
+
...ctx,
|
|
25
|
+
query: { slug: body.slug },
|
|
26
|
+
});
|
|
27
|
+
if (existingCategory) {
|
|
28
|
+
return {
|
|
29
|
+
error: "A category with this slug already exists",
|
|
30
|
+
status: 400,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// If parentId is provided, check if parent exists
|
|
35
|
+
if (body.parentId) {
|
|
36
|
+
const parentCategory = await controllers.category.getById({
|
|
37
|
+
...ctx,
|
|
38
|
+
params: { id: body.parentId },
|
|
39
|
+
});
|
|
40
|
+
if (!parentCategory) {
|
|
41
|
+
return {
|
|
42
|
+
error: "Parent category not found",
|
|
43
|
+
status: 400,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const category = await controllers.category.create(ctx);
|
|
49
|
+
|
|
50
|
+
return { category, status: 201 };
|
|
51
|
+
},
|
|
52
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createAdminEndpoint, sanitizeText, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const createCollection = createAdminEndpoint(
|
|
4
|
+
"/admin/collections",
|
|
5
|
+
{
|
|
6
|
+
method: "POST",
|
|
7
|
+
body: z.object({
|
|
8
|
+
name: z.string().min(1).max(200).transform(sanitizeText),
|
|
9
|
+
slug: z.string().min(1).max(200),
|
|
10
|
+
description: z.string().max(5000).transform(sanitizeText).optional(),
|
|
11
|
+
image: z.string().max(2000).optional(),
|
|
12
|
+
isFeatured: z.boolean().optional(),
|
|
13
|
+
isVisible: z.boolean().optional(),
|
|
14
|
+
position: z.number().int().min(0).optional(),
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
async (ctx) => {
|
|
18
|
+
const { body } = ctx;
|
|
19
|
+
|
|
20
|
+
// Check slug uniqueness
|
|
21
|
+
const existing = await ctx.context.controllers.collection.getBySlug({
|
|
22
|
+
...ctx,
|
|
23
|
+
query: { slug: body.slug },
|
|
24
|
+
});
|
|
25
|
+
if (existing) {
|
|
26
|
+
return {
|
|
27
|
+
error: "A collection with this slug already exists",
|
|
28
|
+
status: 400,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const collection = await ctx.context.controllers.collection.create(ctx);
|
|
33
|
+
return { collection, status: 201 };
|
|
34
|
+
},
|
|
35
|
+
);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createAdminEndpoint, sanitizeText, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const createProduct = createAdminEndpoint(
|
|
4
|
+
"/admin/products",
|
|
5
|
+
{
|
|
6
|
+
method: "POST",
|
|
7
|
+
body: z.object({
|
|
8
|
+
name: z.string().min(1).max(200).transform(sanitizeText),
|
|
9
|
+
slug: z.string().min(1).max(200),
|
|
10
|
+
description: z.string().max(10000).transform(sanitizeText).optional(),
|
|
11
|
+
shortDescription: z.string().max(500).transform(sanitizeText).optional(),
|
|
12
|
+
price: z.number().positive(),
|
|
13
|
+
compareAtPrice: z.number().positive().optional(),
|
|
14
|
+
costPrice: z.number().positive().optional(),
|
|
15
|
+
sku: z.string().max(100).optional(),
|
|
16
|
+
barcode: z.string().max(100).optional(),
|
|
17
|
+
inventory: z.number().int().min(0).optional(),
|
|
18
|
+
trackInventory: z.boolean().optional(),
|
|
19
|
+
allowBackorder: z.boolean().optional(),
|
|
20
|
+
status: z.enum(["draft", "active", "archived"]).optional(),
|
|
21
|
+
categoryId: z.string().optional(),
|
|
22
|
+
images: z.array(z.string()).optional(),
|
|
23
|
+
tags: z.array(z.string()).optional(),
|
|
24
|
+
metadata: z.record(z.string(), z.any()).optional(),
|
|
25
|
+
weight: z.number().positive().optional(),
|
|
26
|
+
weightUnit: z.enum(["kg", "lb", "oz", "g"]).optional(),
|
|
27
|
+
isFeatured: z.boolean().optional(),
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
async (ctx) => {
|
|
31
|
+
const { body } = ctx;
|
|
32
|
+
const controllers = ctx.context.controllers;
|
|
33
|
+
|
|
34
|
+
// Check if slug is unique
|
|
35
|
+
const existingProduct = await controllers.product.getBySlug({
|
|
36
|
+
...ctx,
|
|
37
|
+
query: { slug: body.slug },
|
|
38
|
+
});
|
|
39
|
+
if (existingProduct) {
|
|
40
|
+
return {
|
|
41
|
+
error: "A product with this slug already exists",
|
|
42
|
+
status: 400,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const product = await controllers.product.create(ctx);
|
|
47
|
+
|
|
48
|
+
return { product, status: 201 };
|
|
49
|
+
},
|
|
50
|
+
);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createAdminEndpoint, sanitizeText, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const createVariant = createAdminEndpoint(
|
|
4
|
+
"/admin/products/:productId/variants",
|
|
5
|
+
{
|
|
6
|
+
method: "POST",
|
|
7
|
+
params: z.object({
|
|
8
|
+
productId: z.string(),
|
|
9
|
+
}),
|
|
10
|
+
body: z.object({
|
|
11
|
+
name: z.string().min(1).max(200).transform(sanitizeText),
|
|
12
|
+
sku: z.string().max(100).optional(),
|
|
13
|
+
barcode: z.string().max(100).optional(),
|
|
14
|
+
price: z.number().positive(),
|
|
15
|
+
compareAtPrice: z.number().positive().optional(),
|
|
16
|
+
costPrice: z.number().positive().optional(),
|
|
17
|
+
inventory: z.number().int().min(0).optional(),
|
|
18
|
+
options: z.record(z.string(), z.string()),
|
|
19
|
+
images: z.array(z.string()).optional(),
|
|
20
|
+
weight: z.number().positive().optional(),
|
|
21
|
+
weightUnit: z.enum(["kg", "lb", "oz", "g"]).optional(),
|
|
22
|
+
position: z.number().int().min(0).optional(),
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
async (ctx) => {
|
|
26
|
+
const { params } = ctx;
|
|
27
|
+
const controllers = ctx.context.controllers;
|
|
28
|
+
|
|
29
|
+
// Check if product exists
|
|
30
|
+
const existingProduct = await controllers.product.getById({
|
|
31
|
+
...ctx,
|
|
32
|
+
params: { id: params.productId },
|
|
33
|
+
});
|
|
34
|
+
if (!existingProduct) {
|
|
35
|
+
return {
|
|
36
|
+
error: "Product not found",
|
|
37
|
+
status: 404,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const variant = await controllers.variant.create(ctx);
|
|
42
|
+
|
|
43
|
+
return { variant, status: 201 };
|
|
44
|
+
},
|
|
45
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createAdminEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const deleteCategory = createAdminEndpoint(
|
|
4
|
+
"/admin/categories/:id",
|
|
5
|
+
{
|
|
6
|
+
method: "DELETE",
|
|
7
|
+
params: z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
}),
|
|
10
|
+
},
|
|
11
|
+
async (ctx) => {
|
|
12
|
+
const controllers = ctx.context.controllers;
|
|
13
|
+
|
|
14
|
+
// Check if category exists
|
|
15
|
+
const existingCategory = await controllers.category.getById(ctx);
|
|
16
|
+
if (!existingCategory) {
|
|
17
|
+
return {
|
|
18
|
+
error: "Category not found",
|
|
19
|
+
status: 404,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await controllers.category.delete(ctx);
|
|
24
|
+
|
|
25
|
+
return { success: true, message: "Category deleted successfully" };
|
|
26
|
+
},
|
|
27
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createAdminEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const deleteCollection = createAdminEndpoint(
|
|
4
|
+
"/admin/collections/:id",
|
|
5
|
+
{
|
|
6
|
+
method: "DELETE",
|
|
7
|
+
params: z.object({ id: z.string() }),
|
|
8
|
+
},
|
|
9
|
+
async (ctx) => {
|
|
10
|
+
return ctx.context.controllers.collection.delete(ctx);
|
|
11
|
+
},
|
|
12
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createAdminEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const deleteProduct = createAdminEndpoint(
|
|
4
|
+
"/admin/products/:id",
|
|
5
|
+
{
|
|
6
|
+
method: "DELETE",
|
|
7
|
+
params: z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
}),
|
|
10
|
+
},
|
|
11
|
+
async (ctx) => {
|
|
12
|
+
const controllers = ctx.context.controllers;
|
|
13
|
+
|
|
14
|
+
// Check if product exists
|
|
15
|
+
const existingProduct = await controllers.product.getById(ctx);
|
|
16
|
+
if (!existingProduct) {
|
|
17
|
+
return {
|
|
18
|
+
error: "Product not found",
|
|
19
|
+
status: 404,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await controllers.product.delete(ctx);
|
|
24
|
+
|
|
25
|
+
return { success: true, message: "Product deleted successfully" };
|
|
26
|
+
},
|
|
27
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createAdminEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const deleteVariant = createAdminEndpoint(
|
|
4
|
+
"/admin/variants/:id",
|
|
5
|
+
{
|
|
6
|
+
method: "DELETE",
|
|
7
|
+
params: z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
}),
|
|
10
|
+
},
|
|
11
|
+
async (ctx) => {
|
|
12
|
+
const controllers = ctx.context.controllers;
|
|
13
|
+
|
|
14
|
+
// Check if variant exists
|
|
15
|
+
const existingVariant = await controllers.variant.getById(ctx);
|
|
16
|
+
if (!existingVariant) {
|
|
17
|
+
return {
|
|
18
|
+
error: "Variant not found",
|
|
19
|
+
status: 404,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await controllers.variant.delete(ctx);
|
|
24
|
+
|
|
25
|
+
return { success: true, message: "Variant deleted successfully" };
|
|
26
|
+
},
|
|
27
|
+
);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createAdminEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const adminGetProduct = createAdminEndpoint(
|
|
4
|
+
"/admin/products/:id",
|
|
5
|
+
{
|
|
6
|
+
method: "GET",
|
|
7
|
+
params: z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
}),
|
|
10
|
+
},
|
|
11
|
+
async (ctx) => {
|
|
12
|
+
const product = await ctx.context.controllers.product.getWithVariants(ctx);
|
|
13
|
+
|
|
14
|
+
if (!product) {
|
|
15
|
+
return {
|
|
16
|
+
error: "Product not found",
|
|
17
|
+
status: 404,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { product };
|
|
22
|
+
},
|
|
23
|
+
);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createAdminEndpoint, sanitizeText, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const importProducts = createAdminEndpoint(
|
|
4
|
+
"/admin/products/import",
|
|
5
|
+
{
|
|
6
|
+
method: "POST",
|
|
7
|
+
body: z.object({
|
|
8
|
+
products: z
|
|
9
|
+
.array(
|
|
10
|
+
z.object({
|
|
11
|
+
name: z.string().min(1).max(200).transform(sanitizeText),
|
|
12
|
+
slug: z.string().max(200).optional(),
|
|
13
|
+
price: z.union([z.number(), z.string()]),
|
|
14
|
+
sku: z.string().max(100).optional(),
|
|
15
|
+
barcode: z.string().max(100).optional(),
|
|
16
|
+
description: z
|
|
17
|
+
.string()
|
|
18
|
+
.max(10000)
|
|
19
|
+
.transform(sanitizeText)
|
|
20
|
+
.optional(),
|
|
21
|
+
shortDescription: z
|
|
22
|
+
.string()
|
|
23
|
+
.max(500)
|
|
24
|
+
.transform(sanitizeText)
|
|
25
|
+
.optional(),
|
|
26
|
+
compareAtPrice: z.union([z.number(), z.string()]).optional(),
|
|
27
|
+
costPrice: z.union([z.number(), z.string()]).optional(),
|
|
28
|
+
inventory: z.union([z.number(), z.string()]).optional(),
|
|
29
|
+
status: z.enum(["draft", "active", "archived"]).optional(),
|
|
30
|
+
category: z.string().optional(),
|
|
31
|
+
tags: z.array(z.string()).optional(),
|
|
32
|
+
weight: z.union([z.number(), z.string()]).optional(),
|
|
33
|
+
weightUnit: z.enum(["kg", "lb", "oz", "g"]).optional(),
|
|
34
|
+
featured: z.boolean().optional(),
|
|
35
|
+
trackInventory: z.boolean().optional(),
|
|
36
|
+
allowBackorder: z.boolean().optional(),
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
.min(1)
|
|
40
|
+
.max(500),
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
async (ctx) => {
|
|
44
|
+
const result = await ctx.context.controllers.import.importProducts(ctx);
|
|
45
|
+
return result;
|
|
46
|
+
},
|
|
47
|
+
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { addCollectionProduct } from "./add-collection-product";
|
|
2
|
+
import { bulkAction } from "./bulk-action";
|
|
3
|
+
import { createCategory } from "./create-category";
|
|
4
|
+
import { createCollection } from "./create-collection";
|
|
5
|
+
import { createProduct } from "./create-product";
|
|
6
|
+
import { createVariant } from "./create-variant";
|
|
7
|
+
import { deleteCategory } from "./delete-category";
|
|
8
|
+
import { deleteCollection } from "./delete-collection";
|
|
9
|
+
import { deleteProduct } from "./delete-product";
|
|
10
|
+
import { deleteVariant } from "./delete-variant";
|
|
11
|
+
import { adminGetProduct } from "./get-product";
|
|
12
|
+
import { importProducts } from "./import-products";
|
|
13
|
+
import { adminListCategories } from "./list-categories";
|
|
14
|
+
import { adminListCollections } from "./list-collections";
|
|
15
|
+
import { adminListProducts } from "./list-products";
|
|
16
|
+
import { removeCollectionProduct } from "./remove-collection-product";
|
|
17
|
+
import { updateCategory } from "./update-category";
|
|
18
|
+
import { updateCollection } from "./update-collection";
|
|
19
|
+
import { updateProduct } from "./update-product";
|
|
20
|
+
import { updateVariant } from "./update-variant";
|
|
21
|
+
|
|
22
|
+
export const adminEndpoints = {
|
|
23
|
+
"/admin/products/list": adminListProducts,
|
|
24
|
+
"/admin/products/create": createProduct,
|
|
25
|
+
"/admin/products/:id": adminGetProduct,
|
|
26
|
+
"/admin/products/:id/update": updateProduct,
|
|
27
|
+
"/admin/products/:id/delete": deleteProduct,
|
|
28
|
+
"/admin/products/import": importProducts,
|
|
29
|
+
"/admin/products/bulk": bulkAction,
|
|
30
|
+
"/admin/products/:productId/variants": createVariant,
|
|
31
|
+
"/admin/variants/:id/update": updateVariant,
|
|
32
|
+
"/admin/variants/:id/delete": deleteVariant,
|
|
33
|
+
"/admin/categories/list": adminListCategories,
|
|
34
|
+
"/admin/categories/create": createCategory,
|
|
35
|
+
"/admin/categories/:id/update": updateCategory,
|
|
36
|
+
"/admin/categories/:id/delete": deleteCategory,
|
|
37
|
+
"/admin/collections/list": adminListCollections,
|
|
38
|
+
"/admin/collections/create": createCollection,
|
|
39
|
+
"/admin/collections/:id/update": updateCollection,
|
|
40
|
+
"/admin/collections/:id/delete": deleteCollection,
|
|
41
|
+
"/admin/collections/:id/products": addCollectionProduct,
|
|
42
|
+
"/admin/collections/:id/products/:productId/remove": removeCollectionProduct,
|
|
43
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createAdminEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const adminListCategories = createAdminEndpoint(
|
|
4
|
+
"/admin/categories",
|
|
5
|
+
{
|
|
6
|
+
method: "GET",
|
|
7
|
+
query: z
|
|
8
|
+
.object({
|
|
9
|
+
page: z.string().optional(),
|
|
10
|
+
limit: z.string().optional(),
|
|
11
|
+
parent: z.string().optional(),
|
|
12
|
+
visible: z.string().optional(),
|
|
13
|
+
})
|
|
14
|
+
.optional(),
|
|
15
|
+
},
|
|
16
|
+
async (ctx) => {
|
|
17
|
+
// Call the controller directly - it handles query parsing internally
|
|
18
|
+
const result = await ctx.context.controllers.category.list(ctx);
|
|
19
|
+
return result;
|
|
20
|
+
},
|
|
21
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createAdminEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const adminListCollections = createAdminEndpoint(
|
|
4
|
+
"/admin/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
|
+
visible: z.string().optional(),
|
|
13
|
+
})
|
|
14
|
+
.optional(),
|
|
15
|
+
},
|
|
16
|
+
async (ctx) => {
|
|
17
|
+
const result = await ctx.context.controllers.collection.list(ctx);
|
|
18
|
+
return result;
|
|
19
|
+
},
|
|
20
|
+
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createAdminEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const adminListProducts = createAdminEndpoint(
|
|
4
|
+
"/admin/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
|
+
status: z.enum(["draft", "active", "archived"]).optional(),
|
|
13
|
+
featured: z.string().optional(),
|
|
14
|
+
search: z.string().optional(),
|
|
15
|
+
sort: z.enum(["name", "price", "createdAt", "updatedAt"]).optional(),
|
|
16
|
+
order: z.enum(["asc", "desc"]).optional(),
|
|
17
|
+
})
|
|
18
|
+
.optional(),
|
|
19
|
+
},
|
|
20
|
+
async (ctx) => {
|
|
21
|
+
// Call the controller directly - it handles query parsing internally
|
|
22
|
+
const result = await ctx.context.controllers.product.list(ctx);
|
|
23
|
+
return result;
|
|
24
|
+
},
|
|
25
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createAdminEndpoint, z } from "@86d-app/core";
|
|
2
|
+
|
|
3
|
+
export const removeCollectionProduct = createAdminEndpoint(
|
|
4
|
+
"/admin/collections/:id/products/:productId",
|
|
5
|
+
{
|
|
6
|
+
method: "DELETE",
|
|
7
|
+
params: z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
productId: z.string(),
|
|
10
|
+
}),
|
|
11
|
+
},
|
|
12
|
+
async (ctx) => {
|
|
13
|
+
return ctx.context.controllers.collection.removeProduct(ctx);
|
|
14
|
+
},
|
|
15
|
+
);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createAdminEndpoint, sanitizeText, z } from "@86d-app/core";
|
|
2
|
+
import type { Category } from "../../controllers";
|
|
3
|
+
|
|
4
|
+
export const updateCategory = createAdminEndpoint(
|
|
5
|
+
"/admin/categories/:id",
|
|
6
|
+
{
|
|
7
|
+
method: "PUT",
|
|
8
|
+
params: z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
}),
|
|
11
|
+
body: z.object({
|
|
12
|
+
name: z.string().min(1).max(200).transform(sanitizeText).optional(),
|
|
13
|
+
slug: z.string().min(1).max(200).optional(),
|
|
14
|
+
description: z
|
|
15
|
+
.string()
|
|
16
|
+
.max(2000)
|
|
17
|
+
.transform(sanitizeText)
|
|
18
|
+
.nullable()
|
|
19
|
+
.optional(),
|
|
20
|
+
parentId: z.string().nullable().optional(),
|
|
21
|
+
image: z.string().max(2048).nullable().optional(),
|
|
22
|
+
position: z.number().int().min(0).optional(),
|
|
23
|
+
isVisible: z.boolean().optional(),
|
|
24
|
+
metadata: z.record(z.string(), z.any()).optional(),
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
async (ctx) => {
|
|
28
|
+
const { params, body } = ctx;
|
|
29
|
+
const controllers = ctx.context.controllers;
|
|
30
|
+
|
|
31
|
+
// Check if category exists
|
|
32
|
+
const existingCategory = (await controllers.category.getById(
|
|
33
|
+
ctx,
|
|
34
|
+
)) as Category | null;
|
|
35
|
+
if (!existingCategory) {
|
|
36
|
+
return {
|
|
37
|
+
error: "Category not found",
|
|
38
|
+
status: 404,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If slug is being changed, check uniqueness
|
|
43
|
+
if (body.slug && body.slug !== existingCategory.slug) {
|
|
44
|
+
const categoryWithSlug = await controllers.category.getBySlug({
|
|
45
|
+
...ctx,
|
|
46
|
+
query: { slug: body.slug },
|
|
47
|
+
});
|
|
48
|
+
if (categoryWithSlug) {
|
|
49
|
+
return {
|
|
50
|
+
error: "A category with this slug already exists",
|
|
51
|
+
status: 400,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Prevent circular parent reference
|
|
57
|
+
if (body.parentId === params.id) {
|
|
58
|
+
return {
|
|
59
|
+
error: "A category cannot be its own parent",
|
|
60
|
+
status: 400,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If parentId is provided, check if parent exists
|
|
65
|
+
if (body.parentId) {
|
|
66
|
+
const parentCategory = await controllers.category.getById({
|
|
67
|
+
...ctx,
|
|
68
|
+
params: { id: body.parentId },
|
|
69
|
+
});
|
|
70
|
+
if (!parentCategory) {
|
|
71
|
+
return {
|
|
72
|
+
error: "Parent category not found",
|
|
73
|
+
status: 400,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const category = await controllers.category.update(ctx);
|
|
79
|
+
|
|
80
|
+
return { category };
|
|
81
|
+
},
|
|
82
|
+
);
|