@base44/app-plugin-commerce 0.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.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/base44/agents/commerce/StoreAdmin.jsonc +64 -0
- package/base44/entities/commerce.Cart.jsonc +73 -0
- package/base44/entities/commerce.Coupon.jsonc +113 -0
- package/base44/entities/commerce.Customer.jsonc +96 -0
- package/base44/entities/commerce.DownloadPermission.jsonc +54 -0
- package/base44/entities/commerce.EmailLog.jsonc +43 -0
- package/base44/entities/commerce.Order.jsonc +287 -0
- package/base44/entities/commerce.OrderNote.jsonc +31 -0
- package/base44/entities/commerce.OrderRefund.jsonc +64 -0
- package/base44/entities/commerce.PaymentGateway.jsonc +48 -0
- package/base44/entities/commerce.Product.jsonc +291 -0
- package/base44/entities/commerce.ProductAttribute.jsonc +39 -0
- package/base44/entities/commerce.ProductAttributeTerm.jsonc +38 -0
- package/base44/entities/commerce.ProductCategory.jsonc +51 -0
- package/base44/entities/commerce.ProductReview.jsonc +48 -0
- package/base44/entities/commerce.ProductTag.jsonc +30 -0
- package/base44/entities/commerce.ProductVariation.jsonc +167 -0
- package/base44/entities/commerce.ShippingClass.jsonc +30 -0
- package/base44/entities/commerce.ShippingZone.jsonc +41 -0
- package/base44/entities/commerce.ShippingZoneMethod.jsonc +84 -0
- package/base44/entities/commerce.StoreSettings.jsonc +23 -0
- package/base44/entities/commerce.TaxClass.jsonc +23 -0
- package/base44/entities/commerce.TaxRate.jsonc +68 -0
- package/base44/entities/commerce.Webhook.jsonc +57 -0
- package/base44/entities/commerce.WebhookDelivery.jsonc +45 -0
- package/base44/functions/commerce/admin-coupons/entry.ts +100 -0
- package/base44/functions/commerce/admin-customers/entry.ts +141 -0
- package/base44/functions/commerce/admin-orders/entry.ts +396 -0
- package/base44/functions/commerce/admin-orders/helpers.ts +246 -0
- package/base44/functions/commerce/admin-products/entry.ts +506 -0
- package/base44/functions/commerce/admin-refunds/entry.ts +158 -0
- package/base44/functions/commerce/admin-reports/entry.ts +283 -0
- package/base44/functions/commerce/admin-reviews/entry.ts +66 -0
- package/base44/functions/commerce/admin-tools/entry.ts +261 -0
- package/base44/functions/commerce/admin-webhooks/entry.ts +52 -0
- package/base44/functions/commerce/payment-webhook/entry.ts +135 -0
- package/base44/functions/commerce/payments/entry.ts +238 -0
- package/base44/functions/commerce/seed-store/defaults.ts +162 -0
- package/base44/functions/commerce/seed-store/entry.ts +310 -0
- package/base44/functions/commerce/seed-store/sample-data.ts +349 -0
- package/base44/functions/commerce/storefront-account/entry.ts +207 -0
- package/base44/functions/commerce/storefront-cart/cart-pricing.ts +258 -0
- package/base44/functions/commerce/storefront-cart/entry.ts +283 -0
- package/base44/functions/commerce/storefront-catalog/entry.ts +459 -0
- package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +258 -0
- package/base44/functions/commerce/storefront-checkout/entry.ts +485 -0
- package/base44/shared/commerce/auth.ts +60 -0
- package/base44/shared/commerce/coupons.ts +257 -0
- package/base44/shared/commerce/data/continents.ts +75 -0
- package/base44/shared/commerce/data/countries.ts +307 -0
- package/base44/shared/commerce/data/currencies.ts +46 -0
- package/base44/shared/commerce/email-templates.ts +240 -0
- package/base44/shared/commerce/emails.ts +225 -0
- package/base44/shared/commerce/money.ts +66 -0
- package/base44/shared/commerce/orders.ts +251 -0
- package/base44/shared/commerce/payments.ts +495 -0
- package/base44/shared/commerce/reviews.ts +36 -0
- package/base44/shared/commerce/scan.ts +57 -0
- package/base44/shared/commerce/sequence.ts +35 -0
- package/base44/shared/commerce/settings.ts +57 -0
- package/base44/shared/commerce/shipping.ts +215 -0
- package/base44/shared/commerce/stock.ts +227 -0
- package/base44/shared/commerce/stripe.ts +463 -0
- package/base44/shared/commerce/tax.ts +136 -0
- package/base44/shared/commerce/totals.ts +314 -0
- package/base44/shared/commerce/webhooks.ts +116 -0
- package/package.json +37 -0
- package/scripts/install.js +156 -0
- package/skills/commerce/SKILL.md +62 -0
- package/skills/commerce/docs/api-admin.md +186 -0
- package/skills/commerce/docs/api-storefront.md +408 -0
- package/skills/commerce/installation-guidelines.md +91 -0
- package/skills/commerce/post-installation.md +157 -0
- package/skills/commerce/references/emails.md +13 -0
- package/skills/commerce/references/guest-access-security.md +18 -0
- package/skills/commerce/references/limits-and-performance.md +16 -0
- package/skills/commerce/references/media-and-downloads.md +4 -0
- package/skills/commerce/references/online-payments.md +201 -0
- package/skills/commerce/references/product-render.md +87 -0
- package/skills/commerce/references/scheduled-work.md +19 -0
- package/skills/commerce/references/storefront-product-page.md +83 -0
- package/skills/commerce/references/webhooks.md +8 -0
- package/src/commerce/admin/README.md +107 -0
- package/src/commerce/admin/bot/Markdown.jsx +138 -0
- package/src/commerce/admin/bot/StoreAdminBot.jsx +249 -0
- package/src/commerce/admin/bot/pipe-tables.js +116 -0
- package/src/commerce/admin/components/AddressForm.jsx +78 -0
- package/src/commerce/admin/components/ConfirmDialog.jsx +52 -0
- package/src/commerce/admin/components/CountrySelect.jsx +81 -0
- package/src/commerce/admin/components/DataTable.jsx +192 -0
- package/src/commerce/admin/components/DateRangePicker.jsx +91 -0
- package/src/commerce/admin/components/EmptyState.jsx +17 -0
- package/src/commerce/admin/components/MediaUploader.jsx +116 -0
- package/src/commerce/admin/components/MetaDataEditor.jsx +45 -0
- package/src/commerce/admin/components/MoneyInput.jsx +50 -0
- package/src/commerce/admin/components/PageHeader.jsx +29 -0
- package/src/commerce/admin/components/RichTextarea.jsx +21 -0
- package/src/commerce/admin/components/SearchSelect.jsx +142 -0
- package/src/commerce/admin/components/StatusBadge.jsx +17 -0
- package/src/commerce/admin/context/BasePathContext.jsx +26 -0
- package/src/commerce/admin/context/SettingsContext.jsx +207 -0
- package/src/commerce/admin/hooks/useAsync.js +46 -0
- package/src/commerce/admin/hooks/useDebounce.js +11 -0
- package/src/commerce/admin/hooks/useMoney.js +52 -0
- package/src/commerce/admin/hooks/usePagedList.js +83 -0
- package/src/commerce/admin/hooks/usePaymentProvider.js +27 -0
- package/src/commerce/admin/hooks/useRealtime.js +129 -0
- package/src/commerce/admin/index.jsx +34 -0
- package/src/commerce/admin/layout/AccessDenied.jsx +54 -0
- package/src/commerce/admin/layout/AdminLayout.jsx +33 -0
- package/src/commerce/admin/layout/AuthGuard.jsx +84 -0
- package/src/commerce/admin/layout/Sidebar.jsx +130 -0
- package/src/commerce/admin/layout/Topbar.jsx +94 -0
- package/src/commerce/admin/lib/api.js +55 -0
- package/src/commerce/admin/lib/constants.js +157 -0
- package/src/commerce/admin/lib/format.js +27 -0
- package/src/commerce/admin/lib/geo-data.js +125 -0
- package/src/commerce/admin/lib/order-utils.js +147 -0
- package/src/commerce/admin/lib/paths.js +35 -0
- package/src/commerce/admin/lib/product-utils.js +55 -0
- package/src/commerce/admin/pages/Dashboard.jsx +245 -0
- package/src/commerce/admin/pages/coupons/CouponEditor.jsx +565 -0
- package/src/commerce/admin/pages/coupons/CouponsList.jsx +172 -0
- package/src/commerce/admin/pages/customers/CustomerEditor.jsx +318 -0
- package/src/commerce/admin/pages/customers/CustomersList.jsx +169 -0
- package/src/commerce/admin/pages/orders/OrderEditor.jsx +952 -0
- package/src/commerce/admin/pages/orders/OrdersList.jsx +227 -0
- package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +149 -0
- package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +119 -0
- package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +208 -0
- package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +123 -0
- package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +199 -0
- package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +239 -0
- package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +52 -0
- package/src/commerce/admin/pages/products/AttributeTerms.jsx +180 -0
- package/src/commerce/admin/pages/products/Attributes.jsx +183 -0
- package/src/commerce/admin/pages/products/Categories.jsx +236 -0
- package/src/commerce/admin/pages/products/ProductEditor.jsx +267 -0
- package/src/commerce/admin/pages/products/ProductsList.jsx +391 -0
- package/src/commerce/admin/pages/products/Reviews.jsx +255 -0
- package/src/commerce/admin/pages/products/Tags.jsx +150 -0
- package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +132 -0
- package/src/commerce/admin/pages/products/components/PublishBox.jsx +101 -0
- package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +243 -0
- package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +48 -0
- package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +208 -0
- package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +91 -0
- package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +41 -0
- package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +103 -0
- package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +93 -0
- package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +102 -0
- package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +86 -0
- package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +377 -0
- package/src/commerce/admin/pages/reports/Reports.jsx +416 -0
- package/src/commerce/admin/pages/settings/EmailsSettings.jsx +240 -0
- package/src/commerce/admin/pages/settings/GeneralSettings.jsx +232 -0
- package/src/commerce/admin/pages/settings/InventorySettings.jsx +146 -0
- package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +260 -0
- package/src/commerce/admin/pages/settings/ProductsSettings.jsx +118 -0
- package/src/commerce/admin/pages/settings/SettingsLayout.jsx +53 -0
- package/src/commerce/admin/pages/settings/ShippingSettings.jsx +304 -0
- package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +514 -0
- package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +231 -0
- package/src/commerce/admin/pages/settings/TaxSettings.jsx +281 -0
- package/src/commerce/admin/pages/settings/useGroupForm.jsx +76 -0
- package/src/commerce/admin/pages/status/WebhookEditor.jsx +296 -0
- package/src/commerce/admin/pages/status/Webhooks.jsx +53 -0
- package/src/commerce/admin/routes.jsx +151 -0
- package/src/commerce/utils/index.js +19 -0
- package/src/commerce/utils/shipping-promos.js +99 -0
- package/src/commerce/utils/variants.js +411 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import React, { useRef, useState } from "react";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import { ArrowDown, ArrowUp, ImagePlus, Loader2, X } from "lucide-react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import { base44 } from "../lib/api";
|
|
6
|
+
|
|
7
|
+
/** Upload a file via the Core integration; returns the public URL. */
|
|
8
|
+
export async function uploadFile(file) {
|
|
9
|
+
const res = await base44.integrations.Core.UploadFile({ file });
|
|
10
|
+
return res?.file_url || res?.data?.file_url;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Image uploader.
|
|
15
|
+
*
|
|
16
|
+
* Single mode (multiple=false): value = {src, name, alt} | null
|
|
17
|
+
* Gallery mode (multiple=true): value = [{src, name, alt}], reorder via up/down
|
|
18
|
+
*
|
|
19
|
+
* Props: { value, onChange, multiple?, accept?, label? }
|
|
20
|
+
*/
|
|
21
|
+
export default function MediaUploader({
|
|
22
|
+
value,
|
|
23
|
+
onChange,
|
|
24
|
+
multiple = false,
|
|
25
|
+
accept = "image/*",
|
|
26
|
+
label = multiple ? "Add images" : "Set image",
|
|
27
|
+
}) {
|
|
28
|
+
const inputRef = useRef(null);
|
|
29
|
+
const [uploading, setUploading] = useState(false);
|
|
30
|
+
|
|
31
|
+
const items = multiple ? value || [] : value ? [value] : [];
|
|
32
|
+
|
|
33
|
+
const handleFiles = async (files) => {
|
|
34
|
+
if (!files?.length) return;
|
|
35
|
+
setUploading(true);
|
|
36
|
+
try {
|
|
37
|
+
const uploaded = [];
|
|
38
|
+
for (const file of Array.from(files)) {
|
|
39
|
+
const src = await uploadFile(file);
|
|
40
|
+
if (src) uploaded.push({ src, name: file.name, alt: "" });
|
|
41
|
+
}
|
|
42
|
+
if (multiple) onChange([...(value || []), ...uploaded]);
|
|
43
|
+
else onChange(uploaded[0] || null);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
toast.error(err.response?.data?.error || err.message || "Upload failed");
|
|
46
|
+
} finally {
|
|
47
|
+
setUploading(false);
|
|
48
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const removeAt = (i) => {
|
|
53
|
+
if (multiple) onChange(items.filter((_, idx) => idx !== i));
|
|
54
|
+
else onChange(null);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const move = (i, dir) => {
|
|
58
|
+
const next = [...items];
|
|
59
|
+
const j = i + dir;
|
|
60
|
+
if (j < 0 || j >= next.length) return;
|
|
61
|
+
[next[i], next[j]] = [next[j], next[i]];
|
|
62
|
+
onChange(next);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="space-y-2">
|
|
67
|
+
{items.length > 0 && (
|
|
68
|
+
<div className={multiple ? "grid grid-cols-3 gap-2" : ""}>
|
|
69
|
+
{items.map((img, i) => (
|
|
70
|
+
<div key={`${img.src}-${i}`} className="group relative overflow-hidden rounded-md border">
|
|
71
|
+
<img
|
|
72
|
+
src={img.src}
|
|
73
|
+
alt={img.alt || img.name || ""}
|
|
74
|
+
className={`w-full object-cover ${multiple ? "aspect-square" : "max-h-56"}`}
|
|
75
|
+
/>
|
|
76
|
+
<div className="absolute inset-x-0 top-0 flex justify-end gap-0.5 bg-gradient-to-b from-black/50 to-transparent p-1 opacity-0 transition-opacity group-hover:opacity-100">
|
|
77
|
+
{multiple && (
|
|
78
|
+
<>
|
|
79
|
+
<Button type="button" size="icon" variant="secondary" className="h-6 w-6" onClick={() => move(i, -1)}>
|
|
80
|
+
<ArrowUp className="h-3 w-3" />
|
|
81
|
+
</Button>
|
|
82
|
+
<Button type="button" size="icon" variant="secondary" className="h-6 w-6" onClick={() => move(i, 1)}>
|
|
83
|
+
<ArrowDown className="h-3 w-3" />
|
|
84
|
+
</Button>
|
|
85
|
+
</>
|
|
86
|
+
)}
|
|
87
|
+
<Button type="button" size="icon" variant="destructive" className="h-6 w-6" onClick={() => removeAt(i)}>
|
|
88
|
+
<X className="h-3 w-3" />
|
|
89
|
+
</Button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
<input
|
|
96
|
+
ref={inputRef}
|
|
97
|
+
type="file"
|
|
98
|
+
accept={accept}
|
|
99
|
+
multiple={multiple}
|
|
100
|
+
className="hidden"
|
|
101
|
+
onChange={(e) => handleFiles(e.target.files)}
|
|
102
|
+
/>
|
|
103
|
+
<Button
|
|
104
|
+
type="button"
|
|
105
|
+
variant="outline"
|
|
106
|
+
size="sm"
|
|
107
|
+
disabled={uploading}
|
|
108
|
+
onClick={() => inputRef.current?.click()}
|
|
109
|
+
className="w-full"
|
|
110
|
+
>
|
|
111
|
+
{uploading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <ImagePlus className="mr-2 h-4 w-4" />}
|
|
112
|
+
{label}
|
|
113
|
+
</Button>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import { Input } from "@/components/ui/input";
|
|
4
|
+
import { Plus, X } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Key/value editor for `meta_data` arrays.
|
|
8
|
+
* Props: { value: [{key, value}], onChange }
|
|
9
|
+
*/
|
|
10
|
+
export default function MetaDataEditor({ value = [], onChange }) {
|
|
11
|
+
const setRow = (i, field, v) => {
|
|
12
|
+
const next = value.map((row, idx) => (idx === i ? { ...row, [field]: v } : row));
|
|
13
|
+
onChange(next);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const removeRow = (i) => onChange(value.filter((_, idx) => idx !== i));
|
|
17
|
+
const addRow = () => onChange([...(value || []), { key: "", value: "" }]);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="space-y-2">
|
|
21
|
+
{(value || []).map((row, i) => (
|
|
22
|
+
<div key={i} className="flex items-center gap-2">
|
|
23
|
+
<Input
|
|
24
|
+
placeholder="Key"
|
|
25
|
+
value={row.key || ""}
|
|
26
|
+
onChange={(e) => setRow(i, "key", e.target.value)}
|
|
27
|
+
className="w-1/3"
|
|
28
|
+
/>
|
|
29
|
+
<Input
|
|
30
|
+
placeholder="Value"
|
|
31
|
+
value={row.value || ""}
|
|
32
|
+
onChange={(e) => setRow(i, "value", e.target.value)}
|
|
33
|
+
className="flex-1"
|
|
34
|
+
/>
|
|
35
|
+
<Button type="button" variant="ghost" size="icon" className="h-8 w-8 shrink-0" onClick={() => removeRow(i)}>
|
|
36
|
+
<X className="h-4 w-4" />
|
|
37
|
+
</Button>
|
|
38
|
+
</div>
|
|
39
|
+
))}
|
|
40
|
+
<Button type="button" variant="outline" size="sm" onClick={addRow}>
|
|
41
|
+
<Plus className="mr-1 h-4 w-4" /> Add field
|
|
42
|
+
</Button>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { Input } from "@/components/ui/input";
|
|
3
|
+
import useMoney from "../hooks/useMoney";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Numeric input with the store currency symbol.
|
|
7
|
+
* Props: { value: number|null, onChange(number|null), placeholder?, disabled?, className? }
|
|
8
|
+
*/
|
|
9
|
+
export default function MoneyInput({ value, onChange, placeholder = "0.00", disabled, className = "" }) {
|
|
10
|
+
const { symbol } = useMoney();
|
|
11
|
+
const [text, setText] = useState(value ?? value === 0 ? String(value) : "");
|
|
12
|
+
|
|
13
|
+
// Keep local text in sync when the outer value changes (e.g. after save).
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const asNum = text === "" ? null : Number(text);
|
|
16
|
+
if ((value ?? null) !== (isNaN(asNum) ? null : asNum)) {
|
|
17
|
+
setText(value === null || value === undefined ? "" : String(value));
|
|
18
|
+
}
|
|
19
|
+
// eslint-disable-next-line -- deps intentionally partial
|
|
20
|
+
}, [value]);
|
|
21
|
+
|
|
22
|
+
const handleChange = (e) => {
|
|
23
|
+
const raw = e.target.value;
|
|
24
|
+
setText(raw);
|
|
25
|
+
if (raw.trim() === "") {
|
|
26
|
+
onChange(null);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const num = Number(raw);
|
|
30
|
+
if (!isNaN(num)) onChange(num);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={`relative ${className}`}>
|
|
35
|
+
<span className="pointer-events-none absolute inset-y-0 left-3 flex items-center text-sm text-muted-foreground">
|
|
36
|
+
{symbol}
|
|
37
|
+
</span>
|
|
38
|
+
<Input
|
|
39
|
+
type="number"
|
|
40
|
+
step="any"
|
|
41
|
+
inputMode="decimal"
|
|
42
|
+
className="pl-9"
|
|
43
|
+
value={text}
|
|
44
|
+
onChange={handleChange}
|
|
45
|
+
placeholder={placeholder}
|
|
46
|
+
disabled={disabled}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { ArrowLeft } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Page title row.
|
|
8
|
+
* Props: { title, description?, actions? (node), backHref? }
|
|
9
|
+
*/
|
|
10
|
+
export default function PageHeader({ title, description, actions, backHref }) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="mb-6 flex flex-wrap items-start justify-between gap-3">
|
|
13
|
+
<div className="flex items-start gap-2">
|
|
14
|
+
{backHref && (
|
|
15
|
+
<Button variant="ghost" size="icon" asChild className="mt-0.5 h-8 w-8">
|
|
16
|
+
<Link to={backHref}>
|
|
17
|
+
<ArrowLeft className="h-4 w-4" />
|
|
18
|
+
</Link>
|
|
19
|
+
</Button>
|
|
20
|
+
)}
|
|
21
|
+
<div>
|
|
22
|
+
<h1 className="text-xl font-semibold tracking-tight">{title}</h1>
|
|
23
|
+
{description && <p className="mt-0.5 text-sm text-muted-foreground">{description}</p>}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Description editor. Persists plain text / HTML as-is.
|
|
6
|
+
* Deliberately a plain textarea to avoid a WYSIWYG dependency — swap in your
|
|
7
|
+
* editor of choice (e.g. TipTap) here if you need rich editing.
|
|
8
|
+
*
|
|
9
|
+
* Props: { value, onChange(string), rows?, placeholder? }
|
|
10
|
+
*/
|
|
11
|
+
export default function RichTextarea({ value, onChange, rows = 6, placeholder }) {
|
|
12
|
+
return (
|
|
13
|
+
<Textarea
|
|
14
|
+
value={value || ""}
|
|
15
|
+
onChange={(e) => onChange(e.target.value)}
|
|
16
|
+
rows={rows}
|
|
17
|
+
placeholder={placeholder}
|
|
18
|
+
className="font-mono text-sm"
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
3
|
+
import {
|
|
4
|
+
Command,
|
|
5
|
+
CommandEmpty,
|
|
6
|
+
CommandGroup,
|
|
7
|
+
CommandInput,
|
|
8
|
+
CommandItem,
|
|
9
|
+
CommandList,
|
|
10
|
+
} from "@/components/ui/command";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { Badge } from "@/components/ui/badge";
|
|
13
|
+
import { Check, ChevronsUpDown, Loader2, X } from "lucide-react";
|
|
14
|
+
import useDebounce from "../hooks/useDebounce";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Async searchable picker (single or multi).
|
|
18
|
+
*
|
|
19
|
+
* Props:
|
|
20
|
+
* - search(q) → Promise<[{ value, label, meta? }]>
|
|
21
|
+
* - value: {value,label} | null (single) | [{value,label}] (multiple)
|
|
22
|
+
* - onChange(next)
|
|
23
|
+
* - multiple?: boolean
|
|
24
|
+
* - placeholder?: string
|
|
25
|
+
* - disabled?, className?
|
|
26
|
+
*/
|
|
27
|
+
export default function SearchSelect({
|
|
28
|
+
search,
|
|
29
|
+
value,
|
|
30
|
+
onChange,
|
|
31
|
+
multiple = false,
|
|
32
|
+
placeholder = "Search…",
|
|
33
|
+
disabled = false,
|
|
34
|
+
className = "",
|
|
35
|
+
}) {
|
|
36
|
+
const [open, setOpen] = useState(false);
|
|
37
|
+
const [query, setQuery] = useState("");
|
|
38
|
+
const [options, setOptions] = useState([]);
|
|
39
|
+
const [loading, setLoading] = useState(false);
|
|
40
|
+
const debouncedQuery = useDebounce(query, 300);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!open) return;
|
|
44
|
+
let cancelled = false;
|
|
45
|
+
setLoading(true);
|
|
46
|
+
Promise.resolve(search(debouncedQuery))
|
|
47
|
+
.then((opts) => !cancelled && setOptions(opts || []))
|
|
48
|
+
.catch(() => !cancelled && setOptions([]))
|
|
49
|
+
.finally(() => !cancelled && setLoading(false));
|
|
50
|
+
return () => {
|
|
51
|
+
cancelled = true;
|
|
52
|
+
};
|
|
53
|
+
}, [open, debouncedQuery, search]);
|
|
54
|
+
|
|
55
|
+
const selectedValues = multiple ? (value || []).map((v) => v.value) : value ? [value.value] : [];
|
|
56
|
+
|
|
57
|
+
const pick = (opt) => {
|
|
58
|
+
if (multiple) {
|
|
59
|
+
const exists = (value || []).some((v) => v.value === opt.value);
|
|
60
|
+
onChange(exists ? (value || []).filter((v) => v.value !== opt.value) : [...(value || []), opt]);
|
|
61
|
+
} else {
|
|
62
|
+
onChange(value?.value === opt.value ? null : opt);
|
|
63
|
+
setOpen(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const removeChip = (v) => onChange((value || []).filter((x) => x.value !== v));
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className={className}>
|
|
71
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
72
|
+
<PopoverTrigger asChild>
|
|
73
|
+
<Button
|
|
74
|
+
type="button"
|
|
75
|
+
variant="outline"
|
|
76
|
+
role="combobox"
|
|
77
|
+
disabled={disabled}
|
|
78
|
+
className="w-full justify-between font-normal"
|
|
79
|
+
>
|
|
80
|
+
<span className="truncate text-left">
|
|
81
|
+
{multiple
|
|
82
|
+
? (value || []).length
|
|
83
|
+
? `${value.length} selected`
|
|
84
|
+
: placeholder
|
|
85
|
+
: value?.label || placeholder}
|
|
86
|
+
</span>
|
|
87
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
88
|
+
</Button>
|
|
89
|
+
</PopoverTrigger>
|
|
90
|
+
<PopoverContent className="w-(--radix-popover-trigger-width) min-w-64 p-0" align="start">
|
|
91
|
+
<Command shouldFilter={false}>
|
|
92
|
+
<CommandInput placeholder={placeholder} value={query} onValueChange={setQuery} />
|
|
93
|
+
<CommandList>
|
|
94
|
+
{loading ? (
|
|
95
|
+
<div className="flex items-center justify-center py-4">
|
|
96
|
+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
97
|
+
</div>
|
|
98
|
+
) : (
|
|
99
|
+
<>
|
|
100
|
+
<CommandEmpty>No results.</CommandEmpty>
|
|
101
|
+
<CommandGroup>
|
|
102
|
+
{options.map((opt) => (
|
|
103
|
+
<CommandItem key={opt.value} value={String(opt.value)} onSelect={() => pick(opt)}>
|
|
104
|
+
<Check
|
|
105
|
+
className={`mr-2 h-4 w-4 ${
|
|
106
|
+
selectedValues.includes(opt.value) ? "opacity-100" : "opacity-0"
|
|
107
|
+
}`}
|
|
108
|
+
/>
|
|
109
|
+
<div className="min-w-0">
|
|
110
|
+
<div className="truncate">{opt.label}</div>
|
|
111
|
+
{opt.meta && (
|
|
112
|
+
<div className="truncate text-xs text-muted-foreground">{opt.meta}</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
</CommandItem>
|
|
116
|
+
))}
|
|
117
|
+
</CommandGroup>
|
|
118
|
+
</>
|
|
119
|
+
)}
|
|
120
|
+
</CommandList>
|
|
121
|
+
</Command>
|
|
122
|
+
</PopoverContent>
|
|
123
|
+
</Popover>
|
|
124
|
+
{multiple && (value || []).length > 0 && (
|
|
125
|
+
<div className="mt-2 flex flex-wrap gap-1">
|
|
126
|
+
{value.map((v) => (
|
|
127
|
+
<Badge key={v.value} variant="secondary" className="gap-1 pr-1">
|
|
128
|
+
{v.label}
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={() => removeChip(v.value)}
|
|
132
|
+
className="rounded-full p-0.5 hover:bg-muted-foreground/20"
|
|
133
|
+
>
|
|
134
|
+
<X className="h-3 w-3" />
|
|
135
|
+
</button>
|
|
136
|
+
</Badge>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Badge } from "@/components/ui/badge";
|
|
3
|
+
import { ORDER_STATUSES, statusMeta } from "../lib/constants";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Colored status pill.
|
|
7
|
+
* Props: { status, map? } — `map` defaults to ORDER_STATUSES; pass
|
|
8
|
+
* REVIEW_STATUSES / WEBHOOK_STATUSES / STOCK_STATUSES for other domains.
|
|
9
|
+
*/
|
|
10
|
+
export default function StatusBadge({ status, map = ORDER_STATUSES, className = "" }) {
|
|
11
|
+
const meta = statusMeta(map, status);
|
|
12
|
+
return (
|
|
13
|
+
<Badge variant="outline" className={`${meta.color} ${className}`}>
|
|
14
|
+
{meta.label}
|
|
15
|
+
</Badge>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
import { normalizeBasePath } from "../lib/paths";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The URL prefix the admin app is mounted under (default `/admin`).
|
|
6
|
+
* All internal links are built from this, so the admin folder works no matter
|
|
7
|
+
* where the consumer mounts it: `<AdminApp basePath="/backoffice" />`.
|
|
8
|
+
*/
|
|
9
|
+
const BasePathContext = createContext("/admin");
|
|
10
|
+
|
|
11
|
+
export function BasePathProvider({ value = "/admin", children }) {
|
|
12
|
+
// normalizeBasePath also tolerates the route *pattern* being passed in
|
|
13
|
+
// (`basePath="/admin/*"`), an easy copy/paste slip from the mount — every
|
|
14
|
+
// internal link would otherwise carry a literal "*" segment.
|
|
15
|
+
return <BasePathContext.Provider value={normalizeBasePath(value)}>{children}</BasePathContext.Provider>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useBasePath() {
|
|
19
|
+
return useContext(BasePathContext);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Returns a builder: href("orders/123") → "/admin/orders/123"; href() → "/admin". */
|
|
23
|
+
export function useAdminHref() {
|
|
24
|
+
const base = useBasePath();
|
|
25
|
+
return (path = "") => (path ? `${base}/${String(path).replace(/^\/+/, "")}` : base);
|
|
26
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
7
|
+
import { Label } from "@/components/ui/label";
|
|
8
|
+
import { Loader2, Store } from "lucide-react";
|
|
9
|
+
import { toast } from "sonner";
|
|
10
|
+
import { base44, call } from "../lib/api";
|
|
11
|
+
|
|
12
|
+
const SettingsCtx = createContext(null);
|
|
13
|
+
|
|
14
|
+
export function useSettings() {
|
|
15
|
+
return useContext(SettingsCtx);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loads all StoreSettings groups once and exposes:
|
|
20
|
+
* { settings, recordIds, get(group, key, fallback), saveGroup(group, values), refresh, isSeeded, loading }
|
|
21
|
+
* When the store is not seeded (no `general` group), renders the first-run
|
|
22
|
+
* SetupScreen instead of children.
|
|
23
|
+
*/
|
|
24
|
+
export function SettingsProvider({ children }) {
|
|
25
|
+
const [state, setState] = useState({ loading: true, groups: {}, ids: {} });
|
|
26
|
+
|
|
27
|
+
const load = useCallback(async () => {
|
|
28
|
+
try {
|
|
29
|
+
const records = await base44.entities["commerce.StoreSettings"].list(undefined, 100);
|
|
30
|
+
const groups = {};
|
|
31
|
+
const ids = {};
|
|
32
|
+
(records || []).forEach((r) => {
|
|
33
|
+
groups[r.group_id] = r.values || {};
|
|
34
|
+
ids[r.group_id] = r.id;
|
|
35
|
+
});
|
|
36
|
+
setState({ loading: false, groups, ids });
|
|
37
|
+
} catch (err) {
|
|
38
|
+
setState({ loading: false, groups: {}, ids: {}, error: err });
|
|
39
|
+
}
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
load();
|
|
44
|
+
}, [load]);
|
|
45
|
+
|
|
46
|
+
const get = useCallback(
|
|
47
|
+
(group, key, fallback) => {
|
|
48
|
+
const v = state.groups?.[group]?.[key];
|
|
49
|
+
return v === undefined || v === null ? fallback : v;
|
|
50
|
+
},
|
|
51
|
+
[state.groups]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const saveGroup = useCallback(
|
|
55
|
+
async (group, values) => {
|
|
56
|
+
const id = state.ids[group];
|
|
57
|
+
if (id) await base44.entities["commerce.StoreSettings"].update(id, { values });
|
|
58
|
+
else await base44.entities["commerce.StoreSettings"].create({ group_id: group, values });
|
|
59
|
+
await load();
|
|
60
|
+
},
|
|
61
|
+
[state.ids, load]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const isSeeded = !!state.groups.general;
|
|
65
|
+
const value = {
|
|
66
|
+
loading: state.loading,
|
|
67
|
+
settings: state.groups,
|
|
68
|
+
recordIds: state.ids,
|
|
69
|
+
get,
|
|
70
|
+
saveGroup,
|
|
71
|
+
refresh: load,
|
|
72
|
+
isSeeded,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (state.loading) {
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex h-screen items-center justify-center">
|
|
78
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!isSeeded) {
|
|
84
|
+
return (
|
|
85
|
+
<SettingsCtx.Provider value={value}>
|
|
86
|
+
<SetupScreen onDone={load} />
|
|
87
|
+
</SettingsCtx.Provider>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return <SettingsCtx.Provider value={value}>{children}</SettingsCtx.Provider>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The app's name as the browser knows it, ignoring the platform's placeholder title. */
|
|
95
|
+
function appName() {
|
|
96
|
+
const title = (typeof document === "undefined" ? "" : document.title || "").trim();
|
|
97
|
+
return /^base44( app)?$/i.test(title) ? "" : title;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** First-run screen: initialize store defaults via the commerce/seed-store function. */
|
|
101
|
+
function SetupScreen({ onDone }) {
|
|
102
|
+
const [hasProducts, setHasProducts] = useState(null);
|
|
103
|
+
const [withSample, setWithSample] = useState(false);
|
|
104
|
+
// Nothing the backend can read carries the app's name (a function's env is
|
|
105
|
+
// only BASE44_APP_ID), so the store name has to come from here.
|
|
106
|
+
const [storeName, setStoreName] = useState(() => appName());
|
|
107
|
+
const [running, setRunning] = useState(false);
|
|
108
|
+
const [schemaErrors, setSchemaErrors] = useState(null);
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
base44.entities["commerce.Product"].list(undefined, 1)
|
|
112
|
+
.then((rows) => setHasProducts((rows || []).length > 0))
|
|
113
|
+
.catch(() => setHasProducts(true)); // on error, hide the sample-data option
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
const run = async () => {
|
|
117
|
+
setRunning(true);
|
|
118
|
+
setSchemaErrors(null);
|
|
119
|
+
try {
|
|
120
|
+
await call("seed-store", null, { with_sample_data: withSample, store_name: storeName.trim() }, { silent: true });
|
|
121
|
+
toast.success("Store defaults initialized");
|
|
122
|
+
onDone();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err.code === "schema_incompatible") {
|
|
125
|
+
setSchemaErrors(err.details?.errors || [{ entity: "unknown", error: err.message }]);
|
|
126
|
+
} else {
|
|
127
|
+
toast.error(err.message);
|
|
128
|
+
}
|
|
129
|
+
} finally {
|
|
130
|
+
setRunning(false);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className="flex min-h-screen items-center justify-center bg-muted/30 p-4">
|
|
136
|
+
<Card className="w-full max-w-lg">
|
|
137
|
+
<CardHeader>
|
|
138
|
+
<div className="mb-2 flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
|
139
|
+
<Store className="h-5 w-5 text-primary" />
|
|
140
|
+
</div>
|
|
141
|
+
<CardTitle>Set up your store</CardTitle>
|
|
142
|
+
<CardDescription>
|
|
143
|
+
This looks like a fresh installation. Initialize the store with default
|
|
144
|
+
settings (currency, tax classes, payment methods, shipping zone and
|
|
145
|
+
email configuration) to start using the admin.
|
|
146
|
+
</CardDescription>
|
|
147
|
+
</CardHeader>
|
|
148
|
+
<CardContent className="space-y-4">
|
|
149
|
+
<div className="space-y-1.5">
|
|
150
|
+
<Label htmlFor="store-name">Store name</Label>
|
|
151
|
+
<Input
|
|
152
|
+
id="store-name"
|
|
153
|
+
value={storeName}
|
|
154
|
+
placeholder="Your store's name"
|
|
155
|
+
onChange={(e) => setStoreName(e.target.value)}
|
|
156
|
+
/>
|
|
157
|
+
<p className="text-xs text-muted-foreground">
|
|
158
|
+
Shown to customers and used as the sender name on every transactional email.
|
|
159
|
+
Editable later in Settings → General.
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
{hasProducts === false && (
|
|
163
|
+
<div className="flex items-start gap-2 rounded-md border p-3">
|
|
164
|
+
<Checkbox
|
|
165
|
+
id="with-sample"
|
|
166
|
+
checked={withSample}
|
|
167
|
+
onCheckedChange={(v) => setWithSample(!!v)}
|
|
168
|
+
/>
|
|
169
|
+
<div className="grid gap-1">
|
|
170
|
+
<Label htmlFor="with-sample" className="cursor-pointer">
|
|
171
|
+
Include sample data
|
|
172
|
+
</Label>
|
|
173
|
+
<p className="text-xs text-muted-foreground">
|
|
174
|
+
Adds demo categories, products (simple, variable, downloadable,
|
|
175
|
+
external, grouped) and coupons so you can explore the admin.
|
|
176
|
+
</p>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
{schemaErrors && (
|
|
181
|
+
<Alert variant="destructive">
|
|
182
|
+
<AlertTitle>Entity schemas are incompatible</AlertTitle>
|
|
183
|
+
<AlertDescription>
|
|
184
|
+
<p className="mb-2">
|
|
185
|
+
The entity schemas in this app have been modified in a way that
|
|
186
|
+
prevents seeding. Restore the template schemas or fix the fields
|
|
187
|
+
below, then retry.
|
|
188
|
+
</p>
|
|
189
|
+
<ul className="list-disc space-y-1 pl-4 text-xs">
|
|
190
|
+
{schemaErrors.map((e, i) => (
|
|
191
|
+
<li key={i}>
|
|
192
|
+
<span className="font-semibold">{e.entity}</span>: {e.error}
|
|
193
|
+
</li>
|
|
194
|
+
))}
|
|
195
|
+
</ul>
|
|
196
|
+
</AlertDescription>
|
|
197
|
+
</Alert>
|
|
198
|
+
)}
|
|
199
|
+
<Button onClick={run} disabled={running || !storeName.trim()} className="w-full">
|
|
200
|
+
{running && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
201
|
+
Initialize store defaults
|
|
202
|
+
</Button>
|
|
203
|
+
</CardContent>
|
|
204
|
+
</Card>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|