@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.
Files changed (94) hide show
  1. package/AGENTS.md +65 -0
  2. package/COMPONENTS.md +231 -0
  3. package/README.md +201 -0
  4. package/package.json +46 -0
  5. package/src/__tests__/controllers.test.ts +2227 -0
  6. package/src/__tests__/state.test.ts +138 -0
  7. package/src/admin/components/categories-admin.mdx +3 -0
  8. package/src/admin/components/categories-admin.tsx +449 -0
  9. package/src/admin/components/category-form.mdx +9 -0
  10. package/src/admin/components/category-form.tsx +490 -0
  11. package/src/admin/components/category-list.mdx +75 -0
  12. package/src/admin/components/category-list.tsx +168 -0
  13. package/src/admin/components/collections-admin.mdx +3 -0
  14. package/src/admin/components/collections-admin.tsx +771 -0
  15. package/src/admin/components/index.tsx +8 -0
  16. package/src/admin/components/product-detail.mdx +12 -0
  17. package/src/admin/components/product-detail.tsx +790 -0
  18. package/src/admin/components/product-edit.tsx +60 -0
  19. package/src/admin/components/product-form.tsx +793 -0
  20. package/src/admin/components/product-list.mdx +3 -0
  21. package/src/admin/components/product-list.tsx +1125 -0
  22. package/src/admin/components/product-new.tsx +38 -0
  23. package/src/admin/endpoints/add-collection-product.ts +17 -0
  24. package/src/admin/endpoints/bulk-action.ts +43 -0
  25. package/src/admin/endpoints/create-category.ts +52 -0
  26. package/src/admin/endpoints/create-collection.ts +35 -0
  27. package/src/admin/endpoints/create-product.ts +50 -0
  28. package/src/admin/endpoints/create-variant.ts +45 -0
  29. package/src/admin/endpoints/delete-category.ts +27 -0
  30. package/src/admin/endpoints/delete-collection.ts +12 -0
  31. package/src/admin/endpoints/delete-product.ts +27 -0
  32. package/src/admin/endpoints/delete-variant.ts +27 -0
  33. package/src/admin/endpoints/get-product.ts +23 -0
  34. package/src/admin/endpoints/import-products.ts +47 -0
  35. package/src/admin/endpoints/index.ts +43 -0
  36. package/src/admin/endpoints/list-categories.ts +21 -0
  37. package/src/admin/endpoints/list-collections.ts +20 -0
  38. package/src/admin/endpoints/list-products.ts +25 -0
  39. package/src/admin/endpoints/remove-collection-product.ts +15 -0
  40. package/src/admin/endpoints/update-category.ts +82 -0
  41. package/src/admin/endpoints/update-collection.ts +22 -0
  42. package/src/admin/endpoints/update-product.ts +67 -0
  43. package/src/admin/endpoints/update-variant.ts +41 -0
  44. package/src/controllers.ts +1410 -0
  45. package/src/index.ts +120 -0
  46. package/src/markdown.ts +150 -0
  47. package/src/mdx.d.ts +5 -0
  48. package/src/schema.ts +352 -0
  49. package/src/state.ts +84 -0
  50. package/src/store/components/_hooks.ts +78 -0
  51. package/src/store/components/_types.ts +73 -0
  52. package/src/store/components/_utils.ts +14 -0
  53. package/src/store/components/back-in-stock-notify.tsx +97 -0
  54. package/src/store/components/collection-card.mdx +42 -0
  55. package/src/store/components/collection-card.tsx +12 -0
  56. package/src/store/components/collection-detail.mdx +12 -0
  57. package/src/store/components/collection-detail.tsx +149 -0
  58. package/src/store/components/collection-grid.mdx +9 -0
  59. package/src/store/components/collection-grid.tsx +80 -0
  60. package/src/store/components/featured-products.mdx +9 -0
  61. package/src/store/components/featured-products.tsx +75 -0
  62. package/src/store/components/filter-chip.mdx +25 -0
  63. package/src/store/components/filter-chip.tsx +12 -0
  64. package/src/store/components/index.tsx +39 -0
  65. package/src/store/components/product-card.mdx +69 -0
  66. package/src/store/components/product-card.tsx +71 -0
  67. package/src/store/components/product-detail.mdx +30 -0
  68. package/src/store/components/product-detail.tsx +488 -0
  69. package/src/store/components/product-listing.mdx +7 -0
  70. package/src/store/components/product-listing.tsx +423 -0
  71. package/src/store/components/product-reviews-section.mdx +21 -0
  72. package/src/store/components/product-reviews-section.tsx +372 -0
  73. package/src/store/components/recently-viewed.tsx +100 -0
  74. package/src/store/components/related-products.mdx +6 -0
  75. package/src/store/components/related-products.tsx +62 -0
  76. package/src/store/components/star-display.mdx +18 -0
  77. package/src/store/components/star-display.tsx +27 -0
  78. package/src/store/components/star-picker.mdx +21 -0
  79. package/src/store/components/star-picker.tsx +21 -0
  80. package/src/store/components/stock-badge.mdx +12 -0
  81. package/src/store/components/stock-badge.tsx +19 -0
  82. package/src/store/endpoints/get-category.ts +61 -0
  83. package/src/store/endpoints/get-collection.ts +46 -0
  84. package/src/store/endpoints/get-featured.ts +18 -0
  85. package/src/store/endpoints/get-product.ts +52 -0
  86. package/src/store/endpoints/get-related.ts +20 -0
  87. package/src/store/endpoints/index.ts +23 -0
  88. package/src/store/endpoints/list-categories.ts +13 -0
  89. package/src/store/endpoints/list-collections.ts +22 -0
  90. package/src/store/endpoints/list-products.ts +28 -0
  91. package/src/store/endpoints/search-products.ts +18 -0
  92. package/src/store/endpoints/store-search.ts +111 -0
  93. package/tsconfig.json +9 -0
  94. 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
+ );