@lodashventure/medusa-brand 1.1.0

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 (44) hide show
  1. package/README.md +95 -0
  2. package/dist/admin/components/brand-form.d.ts +19 -0
  3. package/dist/admin/components/brand-form.js +182 -0
  4. package/dist/admin/components/brand-image-uploader.d.ts +14 -0
  5. package/dist/admin/components/brand-image-uploader.js +217 -0
  6. package/dist/admin/lib/sdk.d.ts +1 -0
  7. package/dist/admin/lib/sdk.js +14 -0
  8. package/dist/admin/routes/brands/page.d.ts +4 -0
  9. package/dist/admin/routes/brands/page.js +253 -0
  10. package/dist/admin/widgets/product-brand-widget.d.ts +8 -0
  11. package/dist/admin/widgets/product-brand-widget.js +207 -0
  12. package/dist/api/admin/brands/[id]/image/route.d.ts +5 -0
  13. package/dist/api/admin/brands/[id]/image/route.js +118 -0
  14. package/dist/api/admin/brands/[id]/logo/route.d.ts +5 -0
  15. package/dist/api/admin/brands/[id]/logo/route.js +118 -0
  16. package/dist/api/admin/brands/[id]/products/route.d.ts +2 -0
  17. package/dist/api/admin/brands/[id]/products/route.js +51 -0
  18. package/dist/api/admin/brands/[id]/route.d.ts +5 -0
  19. package/dist/api/admin/brands/[id]/route.js +111 -0
  20. package/dist/api/admin/brands/route.d.ts +4 -0
  21. package/dist/api/admin/brands/route.js +75 -0
  22. package/dist/api/admin/products/[id]/brand/route.d.ts +5 -0
  23. package/dist/api/admin/products/[id]/brand/route.js +116 -0
  24. package/dist/api/middlewares/attach-brand-to-products.d.ts +2 -0
  25. package/dist/api/middlewares/attach-brand-to-products.js +104 -0
  26. package/dist/api/middlewares.d.ts +6 -0
  27. package/dist/api/middlewares.js +26 -0
  28. package/dist/api/store/brands/route.d.ts +2 -0
  29. package/dist/api/store/brands/route.js +50 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +6 -0
  32. package/dist/modules/brand/index.d.ts +35 -0
  33. package/dist/modules/brand/index.js +12 -0
  34. package/dist/modules/brand/migrations/Migration20251021070648.d.ts +5 -0
  35. package/dist/modules/brand/migrations/Migration20251021070648.js +27 -0
  36. package/dist/modules/brand/models/brand.d.ts +16 -0
  37. package/dist/modules/brand/models/brand.js +42 -0
  38. package/dist/modules/brand/service.d.ts +21 -0
  39. package/dist/modules/brand/service.js +10 -0
  40. package/dist/services/gcs-direct-upload.d.ts +8 -0
  41. package/dist/services/gcs-direct-upload.js +54 -0
  42. package/dist/workflows/upload-brand-image.d.ts +15 -0
  43. package/dist/workflows/upload-brand-image.js +56 -0
  44. package/package.json +58 -0
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Medusa Brand Plugin
2
+
3
+ A comprehensive brand management plugin for Medusa v2 that allows you to manage brands, associate them with products, and provide brand-based filtering for your e-commerce store.
4
+
5
+ ## Features
6
+
7
+ - **Brand Management**: Full CRUD operations for managing brands
8
+ - **Product Association**: Associate brands with products
9
+ - **Image Management**: Upload and manage brand images and logos
10
+ - **Admin UI**: Complete admin interface for brand management
11
+ - **Store API**: Public API for displaying brands in your storefront
12
+ - **Middleware**: Automatic brand data enrichment for product responses
13
+ - **GCS Integration**: Google Cloud Storage support for image uploads
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @lodashventure/medusa-brand
19
+ # or
20
+ yarn add @lodashventure/medusa-brand
21
+ ```
22
+
23
+ ## Configuration
24
+
25
+ Add the plugin to your `medusa-config.ts`:
26
+
27
+ ```typescript
28
+ module.exports = defineConfig({
29
+ plugins: [
30
+ {
31
+ resolve: "@lodashventure/medusa-brand",
32
+ options: {},
33
+ },
34
+ ],
35
+ modules: [
36
+ {
37
+ resolve: "@lodashventure/medusa-brand/modules/brand",
38
+ options: {},
39
+ },
40
+ ],
41
+ });
42
+ ```
43
+
44
+ ## Environment Variables
45
+
46
+ The plugin requires the following environment variables for GCS integration:
47
+
48
+ ```env
49
+ CLIENT_EMAIL=your-gcs-client-email
50
+ PRIVATE_KEY=your-gcs-private-key
51
+ BUCKET_NAME=your-gcs-bucket-name
52
+ GCP_STORAGE_BASE_PUBLIC_URL=https://storage.googleapis.com
53
+ ```
54
+
55
+ ## API Routes
56
+
57
+ ### Admin Routes
58
+
59
+ - `GET /admin/brands` - List all brands
60
+ - `POST /admin/brands` - Create a new brand
61
+ - `GET /admin/brands/:id` - Get a specific brand
62
+ - `PUT /admin/brands/:id` - Update a brand
63
+ - `DELETE /admin/brands/:id` - Delete a brand
64
+ - `POST /admin/brands/:id/image` - Upload brand image
65
+ - `DELETE /admin/brands/:id/image` - Delete brand image
66
+ - `POST /admin/brands/:id/logo` - Upload brand logo
67
+ - `DELETE /admin/brands/:id/logo` - Delete brand logo
68
+ - `GET /admin/brands/:id/products` - Get products for a brand
69
+ - `GET /admin/products/:id/brand` - Get brand for a product
70
+ - `POST /admin/products/:id/brand` - Assign brand to product
71
+ - `DELETE /admin/products/:id/brand` - Remove brand from product
72
+
73
+ ### Store Routes
74
+
75
+ - `GET /store/brands` - List active brands
76
+
77
+ ## Admin UI
78
+
79
+ The plugin includes a complete admin interface with:
80
+
81
+ - Brand management page at `/app/brands`
82
+ - Product brand widget in product details
83
+ - Image and logo upload capabilities
84
+ - Search and filtering
85
+
86
+ ## Database Schema
87
+
88
+ The plugin creates two tables:
89
+
90
+ - `brand` - Stores brand information
91
+ - `product_brand` - Junction table for product-brand associations
92
+
93
+ ## License
94
+
95
+ MIT
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ interface Brand {
3
+ id: string;
4
+ name: string;
5
+ slug: string;
6
+ description?: string;
7
+ image?: string;
8
+ logo?: string;
9
+ website?: string;
10
+ is_active: boolean;
11
+ metadata?: any;
12
+ }
13
+ interface BrandFormProps {
14
+ brand?: Brand | null;
15
+ isCreating: boolean;
16
+ onClose: () => void;
17
+ }
18
+ export declare const BrandForm: ({ brand, isCreating, onClose }: BrandFormProps) => React.JSX.Element;
19
+ export {};
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BrandForm = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const sdk_1 = require("../lib/sdk");
39
+ if (["$(basename $file)" = "page.tsx"] || "../lib/sdk")
40
+ ;
41
+ const ui_1 = require("@medusajs/ui");
42
+ const icons_1 = require("@medusajs/icons");
43
+ const BrandForm = ({ brand, isCreating, onClose }) => {
44
+ const [formData, setFormData] = (0, react_1.useState)({
45
+ name: "",
46
+ slug: "",
47
+ description: "",
48
+ website: "",
49
+ is_active: true,
50
+ metadata: {},
51
+ });
52
+ const [loading, setLoading] = (0, react_1.useState)(false);
53
+ const [error, setError] = (0, react_1.useState)(null);
54
+ const [slugError, setSlugError] = (0, react_1.useState)(null);
55
+ (0, react_1.useEffect)(() => {
56
+ if (brand) {
57
+ setFormData({
58
+ name: brand.name || "",
59
+ slug: brand.slug || "",
60
+ description: brand.description || "",
61
+ website: brand.website || "",
62
+ is_active: brand.is_active !== false,
63
+ metadata: brand.metadata || {},
64
+ });
65
+ }
66
+ }, [brand]);
67
+ const generateSlug = (name) => {
68
+ return name
69
+ .toLowerCase()
70
+ .replace(/[^a-z0-9]+/g, "-")
71
+ .replace(/^-+|-+$/g, "");
72
+ };
73
+ const handleNameChange = (value) => {
74
+ setFormData(prev => ({
75
+ ...prev,
76
+ name: value,
77
+ slug: isCreating ? generateSlug(value) : prev.slug,
78
+ }));
79
+ };
80
+ const handleSlugChange = (value) => {
81
+ const cleanSlug = value
82
+ .toLowerCase()
83
+ .replace(/[^a-z0-9-]/g, "")
84
+ .replace(/--+/g, "-");
85
+ setFormData(prev => ({ ...prev, slug: cleanSlug }));
86
+ // Clear slug error when user types
87
+ if (slugError)
88
+ setSlugError(null);
89
+ };
90
+ const validateForm = () => {
91
+ if (!formData.name.trim()) {
92
+ setError("Brand name is required");
93
+ return false;
94
+ }
95
+ if (!formData.slug.trim()) {
96
+ setError("Brand slug is required");
97
+ return false;
98
+ }
99
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(formData.slug)) {
100
+ setSlugError("Slug must contain only lowercase letters, numbers, and hyphens");
101
+ return false;
102
+ }
103
+ if (formData.website && !formData.website.match(/^https?:\/\/.+/)) {
104
+ setError("Website must be a valid URL (starting with http:// or https://)");
105
+ return false;
106
+ }
107
+ return true;
108
+ };
109
+ const handleSubmit = async (e) => {
110
+ e.preventDefault();
111
+ if (!validateForm())
112
+ return;
113
+ setLoading(true);
114
+ setError(null);
115
+ try {
116
+ const url = isCreating
117
+ ? "/admin/brands"
118
+ : `/admin/brands/${brand?.id}`;
119
+ const method = isCreating ? "POST" : "PUT";
120
+ const response = await sdk_1.sdk.client.fetch(url, {
121
+ method,
122
+ headers: {
123
+ "Content-Type": "application/json",
124
+ },
125
+ body: JSON.stringify(formData),
126
+ });
127
+ const data = await response.json();
128
+ if (response.ok) {
129
+ onClose();
130
+ }
131
+ else {
132
+ setError(data.error || "Failed to save brand");
133
+ if (data.error?.includes("slug")) {
134
+ setSlugError("This slug is already in use");
135
+ }
136
+ }
137
+ }
138
+ catch (err) {
139
+ setError("An error occurred while saving the brand");
140
+ console.error("Error saving brand:", err);
141
+ }
142
+ finally {
143
+ setLoading(false);
144
+ }
145
+ };
146
+ return (react_1.default.createElement("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50" },
147
+ react_1.default.createElement("div", { className: "w-full max-w-2xl rounded-lg bg-ui-bg-base p-6 max-h-[90vh] overflow-y-auto" },
148
+ react_1.default.createElement("div", { className: "mb-6 flex items-center justify-between" },
149
+ react_1.default.createElement("div", null,
150
+ react_1.default.createElement(ui_1.Heading, { level: "h2" }, isCreating ? "Create New Brand" : "Edit Brand"),
151
+ !isCreating && brand && (react_1.default.createElement(ui_1.Text, { className: "text-ui-fg-subtle" },
152
+ "Editing: ",
153
+ brand.name))),
154
+ react_1.default.createElement(ui_1.Button, { variant: "secondary", size: "small", onClick: onClose },
155
+ react_1.default.createElement(icons_1.X, null))),
156
+ error && (react_1.default.createElement(ui_1.Alert, { variant: "error", dismissible: true, className: "mb-4" }, error)),
157
+ react_1.default.createElement("form", { onSubmit: handleSubmit, className: "space-y-4" },
158
+ react_1.default.createElement("div", null,
159
+ react_1.default.createElement(ui_1.Label, { htmlFor: "name", className: "mb-2" }, "Brand Name *"),
160
+ react_1.default.createElement(ui_1.Input, { id: "name", placeholder: "e.g., Nike", value: formData.name, onChange: (e) => handleNameChange(e.target.value), required: true })),
161
+ react_1.default.createElement("div", null,
162
+ react_1.default.createElement(ui_1.Label, { htmlFor: "slug", className: "mb-2" }, "Slug *"),
163
+ react_1.default.createElement(ui_1.Input, { id: "slug", placeholder: "e.g., nike", value: formData.slug, onChange: (e) => handleSlugChange(e.target.value), required: true }),
164
+ slugError && (react_1.default.createElement(ui_1.Text, { className: "mt-1 text-sm text-ui-fg-error" }, slugError)),
165
+ react_1.default.createElement(ui_1.Text, { className: "mt-1 text-xs text-ui-fg-subtle" }, "Used in URLs and must be unique")),
166
+ react_1.default.createElement("div", null,
167
+ react_1.default.createElement(ui_1.Label, { htmlFor: "description", className: "mb-2" }, "Description"),
168
+ react_1.default.createElement(ui_1.Textarea, { id: "description", placeholder: "Enter brand description...", value: formData.description, onChange: (e) => setFormData(prev => ({ ...prev, description: e.target.value })), rows: 4 })),
169
+ react_1.default.createElement("div", null,
170
+ react_1.default.createElement(ui_1.Label, { htmlFor: "website", className: "mb-2" }, "Website"),
171
+ react_1.default.createElement(ui_1.Input, { id: "website", type: "url", placeholder: "https://example.com", value: formData.website, onChange: (e) => setFormData(prev => ({ ...prev, website: e.target.value })) }),
172
+ react_1.default.createElement(ui_1.Text, { className: "mt-1 text-xs text-ui-fg-subtle" }, "Include the full URL with http:// or https://")),
173
+ react_1.default.createElement("div", { className: "flex items-center justify-between rounded-lg border p-4" },
174
+ react_1.default.createElement("div", null,
175
+ react_1.default.createElement(ui_1.Label, { htmlFor: "is_active", className: "font-medium" }, "Active Status"),
176
+ react_1.default.createElement(ui_1.Text, { className: "text-sm text-ui-fg-subtle" }, "Active brands are visible in your store")),
177
+ react_1.default.createElement(ui_1.Switch, { id: "is_active", checked: formData.is_active, onCheckedChange: (checked) => setFormData(prev => ({ ...prev, is_active: checked })) })),
178
+ react_1.default.createElement("div", { className: "flex items-center justify-end gap-3 pt-4 border-t" },
179
+ react_1.default.createElement(ui_1.Button, { type: "button", variant: "secondary", onClick: onClose }, "Cancel"),
180
+ react_1.default.createElement(ui_1.Button, { type: "submit", disabled: loading }, loading ? "Saving..." : isCreating ? "Create Brand" : "Update Brand"))))));
181
+ };
182
+ exports.BrandForm = BrandForm;
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ interface Brand {
3
+ id: string;
4
+ name: string;
5
+ image?: string;
6
+ logo?: string;
7
+ }
8
+ interface BrandImageUploaderProps {
9
+ brand: Brand;
10
+ imageType: "image" | "logo";
11
+ onClose: () => void;
12
+ }
13
+ export declare const BrandImageUploader: ({ brand, imageType, onClose, }: BrandImageUploaderProps) => React.JSX.Element;
14
+ export {};
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.BrandImageUploader = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const sdk_1 = require("../lib/sdk");
39
+ if (["$(basename $file)" = "page.tsx"] || "../lib/sdk")
40
+ ;
41
+ const ui_1 = require("@medusajs/ui");
42
+ const icons_1 = require("@medusajs/icons");
43
+ const BrandImageUploader = ({ brand, imageType, onClose, }) => {
44
+ const currentImage = imageType === "image" ? brand.image : brand.logo;
45
+ const [displayImage, setDisplayImage] = (0, react_1.useState)(currentImage || null);
46
+ const [isUploading, setIsUploading] = (0, react_1.useState)(false);
47
+ const [isDragging, setIsDragging] = (0, react_1.useState)(false);
48
+ const [error, setError] = (0, react_1.useState)(null);
49
+ const [previewUrl, setPreviewUrl] = (0, react_1.useState)(null);
50
+ const isLogo = imageType === "logo";
51
+ const maxSize = isLogo ? 2 : 5; // 2MB for logos, 5MB for images
52
+ const allowedTypes = isLogo
53
+ ? ["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"]
54
+ : ["image/jpeg", "image/png", "image/gif", "image/webp"];
55
+ const formatFileTypes = () => {
56
+ return isLogo
57
+ ? "JPEG, PNG, GIF, WebP, or SVG"
58
+ : "JPEG, PNG, GIF, or WebP";
59
+ };
60
+ const validateFile = (file) => {
61
+ if (!allowedTypes.includes(file.type)) {
62
+ setError(`Please upload a valid image file (${formatFileTypes()})`);
63
+ return false;
64
+ }
65
+ if (file.size > maxSize * 1024 * 1024) {
66
+ setError(`File size must be less than ${maxSize}MB`);
67
+ return false;
68
+ }
69
+ return true;
70
+ };
71
+ const handleFileUpload = async (file) => {
72
+ if (!validateFile(file))
73
+ return;
74
+ setError(null);
75
+ // Show preview immediately
76
+ const reader = new FileReader();
77
+ reader.onloadend = () => {
78
+ setPreviewUrl(reader.result);
79
+ };
80
+ reader.readAsDataURL(file);
81
+ // Upload to server
82
+ setIsUploading(true);
83
+ const formData = new FormData();
84
+ formData.append("file", file);
85
+ try {
86
+ const response = await sdk_1.sdk.client.fetch(`/admin/brands/${brand.id}/${imageType}`, {
87
+ method: "POST",
88
+ body: formData,
89
+ });
90
+ if (response.ok) {
91
+ const result = await response.json();
92
+ const newImageUrl = result.brand[imageType];
93
+ setDisplayImage(newImageUrl);
94
+ setPreviewUrl(null);
95
+ // Show success for a moment before closing
96
+ setTimeout(() => {
97
+ onClose();
98
+ }, 1000);
99
+ }
100
+ else {
101
+ const errorData = await response.json();
102
+ setError(errorData.error || `Failed to upload ${imageType}`);
103
+ setPreviewUrl(null);
104
+ }
105
+ }
106
+ catch (err) {
107
+ setError(`Error uploading ${imageType}`);
108
+ setPreviewUrl(null);
109
+ console.error(`Error uploading ${imageType}:`, err);
110
+ }
111
+ finally {
112
+ setIsUploading(false);
113
+ }
114
+ };
115
+ const handleDelete = async () => {
116
+ if (!confirm(`Are you sure you want to delete the ${imageType}?`))
117
+ return;
118
+ setIsUploading(true);
119
+ setError(null);
120
+ try {
121
+ const response = await sdk_1.sdk.client.fetch(`/admin/brands/${brand.id}/${imageType}`, {
122
+ method: "DELETE",
123
+ });
124
+ if (response.ok) {
125
+ setDisplayImage(null);
126
+ setPreviewUrl(null);
127
+ setTimeout(() => {
128
+ onClose();
129
+ }, 500);
130
+ }
131
+ else {
132
+ const errorData = await response.json();
133
+ setError(errorData.error || `Failed to delete ${imageType}`);
134
+ }
135
+ }
136
+ catch (err) {
137
+ setError(`Error deleting ${imageType}`);
138
+ console.error(`Error deleting ${imageType}:`, err);
139
+ }
140
+ finally {
141
+ setIsUploading(false);
142
+ }
143
+ };
144
+ const handleDragEnter = (0, react_1.useCallback)((e) => {
145
+ e.preventDefault();
146
+ e.stopPropagation();
147
+ setIsDragging(true);
148
+ }, []);
149
+ const handleDragLeave = (0, react_1.useCallback)((e) => {
150
+ e.preventDefault();
151
+ e.stopPropagation();
152
+ setIsDragging(false);
153
+ }, []);
154
+ const handleDragOver = (0, react_1.useCallback)((e) => {
155
+ e.preventDefault();
156
+ e.stopPropagation();
157
+ }, []);
158
+ const handleDrop = (0, react_1.useCallback)((e) => {
159
+ e.preventDefault();
160
+ e.stopPropagation();
161
+ setIsDragging(false);
162
+ const file = e.dataTransfer.files?.[0];
163
+ if (file) {
164
+ handleFileUpload(file);
165
+ }
166
+ }, []);
167
+ const imageToDisplay = previewUrl || displayImage;
168
+ return (react_1.default.createElement("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50" },
169
+ react_1.default.createElement("div", { className: "w-full max-w-2xl rounded-lg bg-ui-bg-base p-6" },
170
+ react_1.default.createElement("div", { className: "mb-6 flex items-center justify-between" },
171
+ react_1.default.createElement("div", null,
172
+ react_1.default.createElement(ui_1.Heading, { level: "h2" },
173
+ "Upload Brand ",
174
+ isLogo ? "Logo" : "Image"),
175
+ react_1.default.createElement(ui_1.Text, { className: "text-ui-fg-subtle" }, brand.name)),
176
+ react_1.default.createElement(ui_1.Button, { variant: "secondary", size: "small", onClick: onClose },
177
+ react_1.default.createElement(icons_1.X, null))),
178
+ error && (react_1.default.createElement(ui_1.Alert, { variant: "error", dismissible: true, className: "mb-4" }, error)),
179
+ react_1.default.createElement("div", { className: "space-y-4" },
180
+ imageToDisplay ? (react_1.default.createElement("div", { className: "space-y-4" },
181
+ react_1.default.createElement("div", { className: "relative overflow-hidden rounded-lg border border-ui-border-base bg-ui-bg-subtle" },
182
+ react_1.default.createElement("img", { src: imageToDisplay, alt: `Brand ${imageType}`, className: (0, ui_1.clx)("w-full object-contain", isLogo ? "h-32" : "h-64") }),
183
+ isUploading && (react_1.default.createElement("div", { className: "absolute inset-0 flex items-center justify-center bg-black/50" },
184
+ react_1.default.createElement("div", { className: "text-white" }, "Uploading...")))),
185
+ react_1.default.createElement("div", { className: "flex gap-2" },
186
+ react_1.default.createElement(ui_1.Button, { variant: "secondary", disabled: isUploading, onClick: () => document.getElementById(`${imageType}-file-input`)?.click() },
187
+ react_1.default.createElement(icons_1.CloudArrowUp, { className: "mr-2" }),
188
+ "Replace"),
189
+ react_1.default.createElement(ui_1.Button, { variant: "danger", disabled: isUploading, onClick: handleDelete },
190
+ react_1.default.createElement(icons_1.Trash, { className: "mr-2" }),
191
+ "Delete")))) : (react_1.default.createElement("div", { className: (0, ui_1.clx)("flex flex-col items-center justify-center rounded-lg border-2 border-dashed transition-colors", isLogo ? "h-48" : "h-64", isDragging
192
+ ? "border-ui-border-interactive bg-ui-bg-highlight"
193
+ : "border-ui-border-base bg-ui-bg-subtle", !isUploading && "cursor-pointer"), onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, onClick: () => !isUploading && document.getElementById(`${imageType}-file-input`)?.click() },
194
+ isLogo ? (react_1.default.createElement(icons_1.BuildingStorefront, { className: "mb-4 h-12 w-12 text-ui-fg-subtle" })) : (react_1.default.createElement(icons_1.PhotoSolid, { className: "mb-4 h-12 w-12 text-ui-fg-subtle" })),
195
+ react_1.default.createElement(ui_1.Text, { className: "mb-2 text-lg font-medium" }, isDragging ? `Drop ${imageType} here` : `Upload ${isLogo ? "Logo" : "Image"}`),
196
+ react_1.default.createElement(ui_1.Text, { className: "mb-4 text-center text-ui-fg-subtle" }, "Drag and drop an image here, or click to select"),
197
+ react_1.default.createElement(ui_1.Text, { className: "text-sm text-ui-fg-subtle" },
198
+ formatFileTypes(),
199
+ " \u2022 Max ",
200
+ maxSize,
201
+ "MB"),
202
+ isLogo && (react_1.default.createElement(ui_1.Text, { className: "mt-2 text-xs text-ui-fg-subtle" }, "Recommended: Square image, minimum 200x200px")),
203
+ isUploading && (react_1.default.createElement("div", { className: "mt-4" },
204
+ react_1.default.createElement(ui_1.Text, null, "Uploading..."))))),
205
+ react_1.default.createElement("input", { id: `${imageType}-file-input`, type: "file", accept: allowedTypes.join(","), onChange: (e) => {
206
+ const file = e.target.files?.[0];
207
+ if (file)
208
+ handleFileUpload(file);
209
+ e.target.value = ""; // Reset input
210
+ }, className: "hidden" })),
211
+ react_1.default.createElement("div", { className: "mt-6 flex items-center justify-between border-t pt-4" },
212
+ react_1.default.createElement(ui_1.Text, { className: "text-sm text-ui-fg-subtle" }, isLogo
213
+ ? "Logo will be displayed in brand lists and product pages"
214
+ : "Image can be used for brand pages and marketing"),
215
+ react_1.default.createElement(ui_1.Button, { variant: "secondary", onClick: onClose }, "Close")))));
216
+ };
217
+ exports.BrandImageUploader = BrandImageUploader;
@@ -0,0 +1 @@
1
+ export declare const sdk: any;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.sdk = void 0;
7
+ const js_sdk_1 = __importDefault(require("@medusajs/js-sdk"));
8
+ exports.sdk = new js_sdk_1.default({
9
+ baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
10
+ debug: import.meta.env.DEV,
11
+ auth: {
12
+ type: "session",
13
+ },
14
+ });
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ declare const BrandsPage: () => React.JSX.Element;
3
+ export declare const config: import("@medusajs/admin-sdk").RouteConfig;
4
+ export default BrandsPage;