@base44/app-plugin-commerce 0.7.0 → 0.8.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 (81) hide show
  1. package/README.md +4 -10
  2. package/base44/entities/commerce.ProductReview.jsonc +2 -2
  3. package/base44/functions/commerce/storefront-catalog/entry.ts +7 -3
  4. package/package.json +1 -1
  5. package/scripts/install.js +2 -4
  6. package/skills/commerce/SKILL.md +9 -3
  7. package/skills/commerce/docs/api-storefront.md +3 -3
  8. package/skills/commerce/install/01-install.md +18 -30
  9. package/skills/commerce/install/02-storefront.md +23 -9
  10. package/skills/commerce/references/admin-localization.md +95 -0
  11. package/skills/commerce/references/reviews.md +7 -6
  12. package/src/commerce/admin/README.md +36 -15
  13. package/src/commerce/admin/bot/StoreAdminBot.jsx +12 -11
  14. package/src/commerce/admin/components/AddressForm.jsx +13 -12
  15. package/src/commerce/admin/components/ConfirmDialog.jsx +5 -4
  16. package/src/commerce/admin/components/CountrySelect.jsx +12 -8
  17. package/src/commerce/admin/components/DataTable.jsx +3 -2
  18. package/src/commerce/admin/components/DateRangePicker.jsx +11 -10
  19. package/src/commerce/admin/components/EmptyState.jsx +2 -1
  20. package/src/commerce/admin/components/MediaUploader.jsx +3 -2
  21. package/src/commerce/admin/components/MetaDataEditor.jsx +4 -3
  22. package/src/commerce/admin/components/SearchSelect.jsx +4 -3
  23. package/src/commerce/admin/context/SettingsContext.jsx +12 -25
  24. package/src/commerce/admin/hooks/useMoney.js +3 -2
  25. package/src/commerce/admin/i18n/index.js +54 -0
  26. package/src/commerce/admin/i18n/locales/de.js +896 -0
  27. package/src/commerce/admin/i18n/locales/en.js +900 -0
  28. package/src/commerce/admin/i18n/locales/es.js +896 -0
  29. package/src/commerce/admin/i18n/locales/fr.js +896 -0
  30. package/src/commerce/admin/i18n/locales/ja.js +896 -0
  31. package/src/commerce/admin/i18n/locales/pt.js +896 -0
  32. package/src/commerce/admin/index.jsx +6 -30
  33. package/src/commerce/admin/layout/AccessDenied.jsx +8 -9
  34. package/src/commerce/admin/layout/AuthGuard.jsx +2 -1
  35. package/src/commerce/admin/layout/Sidebar.jsx +29 -18
  36. package/src/commerce/admin/layout/Topbar.jsx +4 -3
  37. package/src/commerce/admin/lib/api.js +3 -2
  38. package/src/commerce/admin/lib/constants.js +44 -43
  39. package/src/commerce/admin/lib/format.js +4 -3
  40. package/src/commerce/admin/lib/paths.js +2 -3
  41. package/src/commerce/admin/pages/Dashboard.jsx +25 -24
  42. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +55 -57
  43. package/src/commerce/admin/pages/coupons/CouponsList.jsx +22 -21
  44. package/src/commerce/admin/pages/customers/CustomerEditor.jsx +31 -30
  45. package/src/commerce/admin/pages/customers/CustomersList.jsx +19 -18
  46. package/src/commerce/admin/pages/orders/OrderEditor.jsx +75 -71
  47. package/src/commerce/admin/pages/orders/OrdersList.jsx +16 -15
  48. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +12 -11
  49. package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +14 -13
  50. package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +12 -11
  51. package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +11 -10
  52. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +22 -22
  53. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +18 -17
  54. package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +18 -17
  55. package/src/commerce/admin/pages/products/Categories.jsx +14 -13
  56. package/src/commerce/admin/pages/products/ProductEditor.jsx +16 -15
  57. package/src/commerce/admin/pages/products/ProductsList.jsx +41 -36
  58. package/src/commerce/admin/pages/products/Reviews.jsx +54 -46
  59. package/src/commerce/admin/pages/products/components/AttributesSection.jsx +45 -39
  60. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +8 -7
  61. package/src/commerce/admin/pages/products/components/PublishBox.jsx +10 -9
  62. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +18 -17
  63. package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +11 -10
  64. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +9 -10
  65. package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +3 -9
  66. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +76 -78
  67. package/src/commerce/admin/pages/reports/Reports.jsx +33 -32
  68. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +36 -39
  69. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +13 -14
  70. package/src/commerce/admin/pages/settings/InventorySettings.jsx +12 -13
  71. package/src/commerce/admin/pages/settings/LocationEditor.jsx +48 -47
  72. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +32 -33
  73. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +13 -12
  74. package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +31 -31
  75. package/src/commerce/admin/pages/settings/useGroupForm.jsx +4 -3
  76. package/src/commerce/admin/pages/status/WebhookEditor.jsx +34 -31
  77. package/src/commerce/admin/pages/status/Webhooks.jsx +11 -10
  78. package/src/commerce/admin/routes.jsx +31 -43
  79. package/src/commerce/storefront/pickers.jsx +19 -5
  80. package/src/commerce/storefront/useCheckout.jsx +28 -3
  81. package/src/commerce/utils/storefront.js +49 -2
@@ -16,6 +16,7 @@ import { Loader2, Plus } from "lucide-react";
16
16
  import { toast } from "sonner";
17
17
 
18
18
  import { base44 } from "../../../lib/api";
19
+ import { t } from "../../../i18n";
19
20
  import SearchSelect from "../../../components/SearchSelect";
20
21
  import { slugify } from "../../../lib/product-utils";
21
22
 
@@ -87,14 +88,14 @@ function CategoriesCard({ categories, selected, onChange, refresh }) {
87
88
  slug: slugify(name),
88
89
  parent_id: parent === NO_PARENT ? "" : parent,
89
90
  });
90
- toast.success("Category created");
91
+ toast.success(t("products.category_created"));
91
92
  setName("");
92
93
  setParent(NO_PARENT);
93
94
  setAdding(false);
94
95
  await refresh?.();
95
96
  if (created?.id) onChange([...selected, created.id]);
96
97
  } catch (e) {
97
- toast.error(e.message || "Failed to create category");
98
+ toast.error(e.message || t("products.category_create_failed"));
98
99
  } finally {
99
100
  setSaving(false);
100
101
  }
@@ -103,7 +104,7 @@ function CategoriesCard({ categories, selected, onChange, refresh }) {
103
104
  return (
104
105
  <Card>
105
106
  <CardHeader className="pb-2">
106
- <CardTitle className="text-base">Categories</CardTitle>
107
+ <CardTitle className="text-base">{t("routes.categories")}</CardTitle>
107
108
  </CardHeader>
108
109
  <CardContent className="space-y-2">
109
110
  {tree.length > 0 ? (
@@ -122,21 +123,21 @@ function CategoriesCard({ categories, selected, onChange, refresh }) {
122
123
  </div>
123
124
  </ScrollArea>
124
125
  ) : (
125
- <p className="text-sm text-muted-foreground">No categories yet.</p>
126
+ <p className="text-sm text-muted-foreground">{t("products.no_categories")}</p>
126
127
  )}
127
128
 
128
129
  {adding ? (
129
130
  <div className="space-y-2 rounded-md border p-2">
130
131
  <div className="space-y-1">
131
- <Label className="text-xs">Name</Label>
132
- <Input value={name} onChange={(e) => setName(e.target.value)} placeholder="Category name" />
132
+ <Label className="text-xs">{t("common.name")}</Label>
133
+ <Input value={name} onChange={(e) => setName(e.target.value)} placeholder={t("products.category_name_placeholder")} />
133
134
  </div>
134
135
  <div className="space-y-1">
135
- <Label className="text-xs">Parent</Label>
136
+ <Label className="text-xs">{t("products.parent")}</Label>
136
137
  <Select value={parent} onValueChange={setParent}>
137
138
  <SelectTrigger><SelectValue /></SelectTrigger>
138
139
  <SelectContent>
139
- <SelectItem value={NO_PARENT}>— None —</SelectItem>
140
+ <SelectItem value={NO_PARENT}>{t("products.no_parent")}</SelectItem>
140
141
  {categories.map((c) => (
141
142
  <SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
142
143
  ))}
@@ -145,16 +146,16 @@ function CategoriesCard({ categories, selected, onChange, refresh }) {
145
146
  </div>
146
147
  <div className="flex gap-2">
147
148
  <Button size="sm" onClick={create} disabled={saving || !name.trim()}>
148
- {saving && <Loader2 className="mr-2 h-3 w-3 animate-spin" />}Add
149
+ {saving && <Loader2 className="mr-2 h-3 w-3 animate-spin" />}{t("common.add")}
149
150
  </Button>
150
151
  <Button size="sm" variant="ghost" onClick={() => setAdding(false)} disabled={saving}>
151
- Cancel
152
+ {t("common.cancel")}
152
153
  </Button>
153
154
  </div>
154
155
  </div>
155
156
  ) : (
156
157
  <Button variant="link" size="sm" className="h-auto p-0 text-xs" onClick={() => setAdding(true)}>
157
- <Plus className="mr-1 h-3 w-3" /> Add new category
158
+ <Plus className="mr-1 h-3 w-3" /> {t("products.add_new_category")}
158
159
  </Button>
159
160
  )}
160
161
  </CardContent>
@@ -196,12 +197,12 @@ function RibbonsCard({ ribbons, selected, onChange, refresh }) {
196
197
  setSaving(true);
197
198
  try {
198
199
  const created = await base44.entities["commerce.ProductRibbon"].create({ name: label, count: 0 });
199
- toast.success("Ribbon created");
200
+ toast.success(t("products.ribbon_created"));
200
201
  setNewRibbon("");
201
202
  await refresh?.();
202
203
  if (created?.id) onChange([...selected, created.id]);
203
204
  } catch (e) {
204
- toast.error(e.message || "Failed to create ribbon");
205
+ toast.error(e.message || t("products.ribbon_create_failed"));
205
206
  } finally {
206
207
  setSaving(false);
207
208
  }
@@ -210,12 +211,12 @@ function RibbonsCard({ ribbons, selected, onChange, refresh }) {
210
211
  return (
211
212
  <Card>
212
213
  <CardHeader className="pb-2">
213
- <CardTitle className="text-base">Ribbons</CardTitle>
214
+ <CardTitle className="text-base">{t("products.ribbons")}</CardTitle>
214
215
  </CardHeader>
215
216
  <CardContent className="space-y-2">
216
217
  <SearchSelect
217
218
  multiple
218
- placeholder="Select ribbons"
219
+ placeholder={t("products.select_ribbons")}
219
220
  value={value}
220
221
  onChange={(next) => onChange(next.map((o) => o.value))}
221
222
  search={search}
@@ -230,11 +231,11 @@ function RibbonsCard({ ribbons, selected, onChange, refresh }) {
230
231
  createRibbon();
231
232
  }
232
233
  }}
233
- placeholder="Add new ribbon"
234
+ placeholder={t("products.add_new_ribbon")}
234
235
  className="h-8 text-sm"
235
236
  />
236
237
  <Button size="sm" variant="outline" onClick={createRibbon} disabled={saving || !newRibbon.trim()}>
237
- {saving ? <Loader2 className="h-3 w-3 animate-spin" /> : "Add"}
238
+ {saving ? <Loader2 className="h-3 w-3 animate-spin" /> : t("common.add")}
238
239
  </Button>
239
240
  </div>
240
241
  </CardContent>
@@ -5,6 +5,7 @@ import { Label } from "@/components/ui/label";
5
5
  import { Plus, Trash2 } from "lucide-react";
6
6
 
7
7
  import MediaUploader from "../../../../components/MediaUploader";
8
+ import { t } from "../../../../i18n";
8
9
 
9
10
  const num = (v, fallback) => (v === "" ? fallback : Number(v));
10
11
 
@@ -18,19 +19,19 @@ export default function DownloadsTab({ product, up }) {
18
19
  return (
19
20
  <div className="max-w-xl space-y-4">
20
21
  <div className="space-y-3">
21
- <Label>Downloadable files</Label>
22
+ <Label>{t("products.downloadable_files")}</Label>
22
23
  {downloads.map((d, i) => (
23
24
  <div key={i} className="grid gap-2 rounded-md border p-3 sm:grid-cols-[1fr_1fr_auto]">
24
25
  <div className="space-y-1.5">
25
- <Label className="text-xs">Name</Label>
26
+ <Label className="text-xs">{t("common.name")}</Label>
26
27
  <Input
27
- placeholder="e.g. User manual"
28
+ placeholder={t("products.download_name_placeholder")}
28
29
  value={d.name || ""}
29
30
  onChange={(e) => setRow(i, { name: e.target.value })}
30
31
  />
31
32
  </div>
32
33
  <div className="space-y-1.5">
33
- <Label className="text-xs">File</Label>
34
+ <Label className="text-xs">{t("products.file")}</Label>
34
35
  {d.file_url ? (
35
36
  <div className="flex items-center gap-2">
36
37
  <Input value={d.file_url} onChange={(e) => setRow(i, { file_url: e.target.value })} />
@@ -38,7 +39,7 @@ export default function DownloadsTab({ product, up }) {
38
39
  ) : (
39
40
  <MediaUploader
40
41
  accept="*/*"
41
- label="Upload file"
42
+ label={t("products.upload_file")}
42
43
  value={null}
43
44
  onChange={(img) => img && setRow(i, { file_url: img.src, name: d.name || img.name })}
44
45
  />
@@ -62,28 +63,28 @@ export default function DownloadsTab({ product, up }) {
62
63
  size="sm"
63
64
  onClick={() => up({ downloads: [...downloads, { name: "", file_url: "" }] })}
64
65
  >
65
- <Plus className="mr-1 h-4 w-4" /> Add file
66
+ <Plus className="mr-1 h-4 w-4" /> {t("products.add_file")}
66
67
  </Button>
67
68
  </div>
68
69
 
69
70
  <div className="grid max-w-md grid-cols-2 gap-2">
70
71
  <div className="space-y-1.5">
71
- <Label>Download limit</Label>
72
+ <Label>{t("products.download_limit")}</Label>
72
73
  <Input
73
74
  type="number"
74
75
  value={product.download_limit ?? -1}
75
76
  onChange={(e) => up({ download_limit: num(e.target.value, -1) })}
76
77
  />
77
- <p className="text-xs text-muted-foreground">-1 for unlimited downloads.</p>
78
+ <p className="text-xs text-muted-foreground">{t("products.download_limit_hint")}</p>
78
79
  </div>
79
80
  <div className="space-y-1.5">
80
- <Label>Download expiry (days)</Label>
81
+ <Label>{t("products.download_expiry")}</Label>
81
82
  <Input
82
83
  type="number"
83
84
  value={product.download_expiry ?? -1}
84
85
  onChange={(e) => up({ download_expiry: num(e.target.value, -1) })}
85
86
  />
86
- <p className="text-xs text-muted-foreground">-1 for no expiry.</p>
87
+ <p className="text-xs text-muted-foreground">{t("products.download_expiry_hint")}</p>
87
88
  </div>
88
89
  </div>
89
90
  </div>
@@ -3,6 +3,7 @@ import { Label } from "@/components/ui/label";
3
3
 
4
4
  import { base44, call } from "../../../../lib/api";
5
5
  import SearchSelect from "../../../../components/SearchSelect";
6
+ import { t } from "../../../../i18n";
6
7
 
7
8
  /** Search products server-side for the pickers. */
8
9
  const searchProducts = async (q) => {
@@ -29,8 +30,8 @@ function ProductIdsPicker({ ids, onChange, placeholder }) {
29
30
  Promise.all(
30
31
  missing.map((id) =>
31
32
  base44.entities["commerce.Product"].get(id)
32
- .then((p) => ({ value: id, label: p?.name || "(deleted product)" }))
33
- .catch(() => ({ value: id, label: "(deleted product)" }))
33
+ .then((p) => ({ value: id, label: p?.name || t("products.deleted_product") }))
34
+ .catch(() => ({ value: id, label: t("products.deleted_product") }))
34
35
  )
35
36
  ).then((resolved) => {
36
37
  if (!cancelled) setOptions([...(options || []), ...resolved]);
@@ -64,25 +65,23 @@ export default function LinkedTab({ product, up }) {
64
65
  return (
65
66
  <div className="max-w-md space-y-4">
66
67
  <div className="space-y-1.5">
67
- <Label>Upsells</Label>
68
+ <Label>{t("products.upsells")}</Label>
68
69
  <ProductIdsPicker
69
70
  ids={product.upsell_ids}
70
71
  onChange={(upsell_ids) => up({ upsell_ids })}
71
- placeholder="Search for products to upsell…"
72
+ placeholder={t("products.upsells_placeholder")}
72
73
  />
73
- <p className="text-xs text-muted-foreground">
74
- Recommended instead of this product, e.g. premium alternatives.
75
- </p>
74
+ <p className="text-xs text-muted-foreground">{t("products.upsells_hint")}</p>
76
75
  </div>
77
76
 
78
77
  <div className="space-y-1.5">
79
- <Label>Cross-sells</Label>
78
+ <Label>{t("products.cross_sells")}</Label>
80
79
  <ProductIdsPicker
81
80
  ids={product.cross_sell_ids}
82
81
  onChange={(cross_sell_ids) => up({ cross_sell_ids })}
83
- placeholder="Search for products to cross-sell…"
82
+ placeholder={t("products.cross_sells_placeholder")}
84
83
  />
85
- <p className="text-xs text-muted-foreground">Promoted in the cart alongside this product.</p>
84
+ <p className="text-xs text-muted-foreground">{t("products.cross_sells_hint")}</p>
86
85
  </div>
87
86
  </div>
88
87
  );
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
 
3
3
  import MetaDataEditor from "../../../../components/MetaDataEditor";
4
+ import { t } from "../../../../i18n";
4
5
 
5
6
  /**
6
7
  * Descriptive properties — Material, Care, GTIN. They are `meta_data`, never
@@ -10,16 +11,9 @@ export default function ModifiersTab({ product, up }) {
10
11
  const empty = !(product.meta_data || []).length;
11
12
  return (
12
13
  <div className="max-w-xl space-y-2">
13
- <p className="text-xs text-muted-foreground">
14
- Descriptive details shown on the product page (e.g. ingredients, allergens, care
15
- instructions).
16
- </p>
14
+ <p className="text-xs text-muted-foreground">{t("products.modifiers_hint")}</p>
17
15
  <MetaDataEditor value={product.meta_data || []} onChange={(meta_data) => up({ meta_data })} />
18
- {empty && (
19
- <p className="text-xs text-muted-foreground">
20
- Want the product to vary by something? Add it as an option under Price &amp; Inventory.
21
- </p>
22
- )}
16
+ {empty && <p className="text-xs text-muted-foreground">{t("products.modifiers_empty_hint")}</p>}
23
17
  </div>
24
18
  );
25
19
  }