@base44/app-plugin-commerce 0.7.1 → 0.8.1

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 (82) 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 +8 -24
  9. package/skills/commerce/install/02-storefront.md +22 -9
  10. package/skills/commerce/references/admin-localization.md +95 -0
  11. package/skills/commerce/references/catalog-rendering.md +1 -1
  12. package/skills/commerce/references/reviews.md +7 -6
  13. package/src/commerce/admin/README.md +36 -14
  14. package/src/commerce/admin/bot/StoreAdminBot.jsx +12 -11
  15. package/src/commerce/admin/components/AddressForm.jsx +13 -12
  16. package/src/commerce/admin/components/ConfirmDialog.jsx +5 -4
  17. package/src/commerce/admin/components/CountrySelect.jsx +12 -8
  18. package/src/commerce/admin/components/DataTable.jsx +3 -2
  19. package/src/commerce/admin/components/DateRangePicker.jsx +11 -10
  20. package/src/commerce/admin/components/EmptyState.jsx +2 -1
  21. package/src/commerce/admin/components/MediaUploader.jsx +3 -2
  22. package/src/commerce/admin/components/MetaDataEditor.jsx +4 -3
  23. package/src/commerce/admin/components/SearchSelect.jsx +4 -3
  24. package/src/commerce/admin/context/SettingsContext.jsx +12 -25
  25. package/src/commerce/admin/hooks/useMoney.js +3 -2
  26. package/src/commerce/admin/i18n/index.js +54 -0
  27. package/src/commerce/admin/i18n/locales/de.js +896 -0
  28. package/src/commerce/admin/i18n/locales/en.js +900 -0
  29. package/src/commerce/admin/i18n/locales/es.js +896 -0
  30. package/src/commerce/admin/i18n/locales/fr.js +896 -0
  31. package/src/commerce/admin/i18n/locales/ja.js +896 -0
  32. package/src/commerce/admin/i18n/locales/pt.js +896 -0
  33. package/src/commerce/admin/index.jsx +6 -30
  34. package/src/commerce/admin/layout/AccessDenied.jsx +8 -9
  35. package/src/commerce/admin/layout/AuthGuard.jsx +2 -1
  36. package/src/commerce/admin/layout/Sidebar.jsx +29 -18
  37. package/src/commerce/admin/layout/Topbar.jsx +4 -3
  38. package/src/commerce/admin/lib/api.js +3 -2
  39. package/src/commerce/admin/lib/constants.js +44 -43
  40. package/src/commerce/admin/lib/format.js +4 -3
  41. package/src/commerce/admin/lib/paths.js +2 -3
  42. package/src/commerce/admin/pages/Dashboard.jsx +25 -24
  43. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +55 -57
  44. package/src/commerce/admin/pages/coupons/CouponsList.jsx +22 -21
  45. package/src/commerce/admin/pages/customers/CustomerEditor.jsx +31 -30
  46. package/src/commerce/admin/pages/customers/CustomersList.jsx +19 -18
  47. package/src/commerce/admin/pages/orders/OrderEditor.jsx +75 -71
  48. package/src/commerce/admin/pages/orders/OrdersList.jsx +16 -15
  49. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +12 -11
  50. package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +14 -13
  51. package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +12 -11
  52. package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +11 -10
  53. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +22 -22
  54. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +18 -17
  55. package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +18 -17
  56. package/src/commerce/admin/pages/products/Categories.jsx +14 -13
  57. package/src/commerce/admin/pages/products/ProductEditor.jsx +16 -15
  58. package/src/commerce/admin/pages/products/ProductsList.jsx +41 -36
  59. package/src/commerce/admin/pages/products/Reviews.jsx +54 -46
  60. package/src/commerce/admin/pages/products/components/AttributesSection.jsx +45 -39
  61. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +8 -7
  62. package/src/commerce/admin/pages/products/components/PublishBox.jsx +10 -9
  63. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +18 -17
  64. package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +11 -10
  65. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +9 -10
  66. package/src/commerce/admin/pages/products/components/tabs/ModifiersTab.jsx +3 -9
  67. package/src/commerce/admin/pages/products/components/tabs/PriceInventoryTab.jsx +76 -78
  68. package/src/commerce/admin/pages/reports/Reports.jsx +33 -32
  69. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +36 -39
  70. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +13 -14
  71. package/src/commerce/admin/pages/settings/InventorySettings.jsx +12 -13
  72. package/src/commerce/admin/pages/settings/LocationEditor.jsx +48 -47
  73. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +32 -33
  74. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +13 -12
  75. package/src/commerce/admin/pages/settings/ShippingTaxSettings.jsx +31 -31
  76. package/src/commerce/admin/pages/settings/useGroupForm.jsx +4 -3
  77. package/src/commerce/admin/pages/status/WebhookEditor.jsx +34 -31
  78. package/src/commerce/admin/pages/status/Webhooks.jsx +11 -10
  79. package/src/commerce/admin/routes.jsx +31 -43
  80. package/src/commerce/storefront/pickers.jsx +19 -5
  81. package/src/commerce/storefront/useCheckout.jsx +28 -3
  82. package/src/commerce/utils/storefront.js +49 -2
@@ -10,6 +10,7 @@ import { Separator } from "@/components/ui/separator";
10
10
  import { Copy, Loader2, RefreshCw } from "lucide-react";
11
11
 
12
12
  import { call, base44 } from "../../lib/api";
13
+ import { t } from "../../i18n";
13
14
  import { formatDate } from "../../lib/format";
14
15
  import { round2 } from "../../lib/order-utils";
15
16
  import useMoney from "../../hooks/useMoney";
@@ -71,17 +72,17 @@ export default function CustomerEditor() {
71
72
 
72
73
  const save = async () => {
73
74
  if (!customer.email?.trim()) {
74
- toast.error("An email address is required");
75
+ toast.error(t("customers.email_required"));
75
76
  return;
76
77
  }
77
78
  setSaving(true);
78
79
  try {
79
80
  const saved = await call("admin-customers", "save", { customer });
80
- toast.success("Customer saved");
81
+ toast.success(t("customers.saved"));
81
82
  if (isNew && sendInvite && saved?.email) {
82
83
  try {
83
84
  await call("admin-customers", "invite", { email: saved.email });
84
- toast.success("Invite sent");
85
+ toast.success(t("customers.invite_sent"));
85
86
  } catch {
86
87
  /* toast shown by call() */
87
88
  }
@@ -99,7 +100,7 @@ export default function CustomerEditor() {
99
100
  setRecalculating(true);
100
101
  try {
101
102
  const updated = await call("admin-customers", "recalculate-stats", { id });
102
- toast.success("Stats recalculated");
103
+ toast.success(t("customers.stats_recalculated"));
103
104
  if (updated) setCustomer((c) => ({ ...c, ...updated }));
104
105
  } catch {
105
106
  /* toast shown by call() */
@@ -112,7 +113,7 @@ export default function CustomerEditor() {
112
113
  setDeleting(true);
113
114
  try {
114
115
  await call("admin-customers", "delete", { id, reassign_orders_to_guest: true });
115
- toast.success("Customer deleted");
116
+ toast.success(t("customers.deleted"));
116
117
  navigate(href("customers"));
117
118
  } catch {
118
119
  setDeleting(false);
@@ -134,7 +135,7 @@ export default function CustomerEditor() {
134
135
  return (
135
136
  <div>
136
137
  <PageHeader
137
- title={isNew ? "Add customer" : name || customer.email}
138
+ title={isNew ? t("routes.customers_new") : name || customer.email}
138
139
  backHref={href("customers")}
139
140
  actions={
140
141
  <>
@@ -144,12 +145,12 @@ export default function CustomerEditor() {
144
145
  className="text-destructive hover:text-destructive"
145
146
  onClick={() => setConfirmDelete(true)}
146
147
  >
147
- Delete
148
+ {t("common.delete")}
148
149
  </Button>
149
150
  )}
150
151
  <Button onClick={save} disabled={saving}>
151
152
  {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
152
- {isNew ? "Create customer" : "Save customer"}
153
+ {isNew ? t("customers.create") : t("customers.save")}
153
154
  </Button>
154
155
  </>
155
156
  }
@@ -160,29 +161,29 @@ export default function CustomerEditor() {
160
161
  <div className="space-y-4">
161
162
  <Card>
162
163
  <CardHeader className="pb-3">
163
- <CardTitle className="text-base">Account</CardTitle>
164
+ <CardTitle className="text-base">{t("customers.account")}</CardTitle>
164
165
  </CardHeader>
165
166
  <CardContent className="grid grid-cols-1 gap-3 sm:grid-cols-2">
166
167
  <div className="grid gap-1.5 sm:col-span-2">
167
- <Label>Email address *</Label>
168
+ <Label>{t("customers.email_address_required")}</Label>
168
169
  <Input type="email" value={customer.email} onChange={setInput("email")} />
169
170
  </div>
170
171
  <div className="grid gap-1.5">
171
- <Label>First name</Label>
172
+ <Label>{t("address.first_name")}</Label>
172
173
  <Input value={customer.first_name || ""} onChange={setInput("first_name")} />
173
174
  </div>
174
175
  <div className="grid gap-1.5">
175
- <Label>Last name</Label>
176
+ <Label>{t("address.last_name")}</Label>
176
177
  <Input value={customer.last_name || ""} onChange={setInput("last_name")} />
177
178
  </div>
178
179
  <div className="grid gap-1.5 sm:col-span-2">
179
- <Label>Username</Label>
180
+ <Label>{t("customers.username")}</Label>
180
181
  <Input value={customer.username || ""} onChange={setInput("username")} />
181
182
  </div>
182
183
  {isNew && (
183
184
  <label className="flex items-center gap-2 text-sm sm:col-span-2">
184
185
  <Checkbox checked={sendInvite} onCheckedChange={(v) => setSendInvite(!!v)} />
185
- Send the new user an account invite
186
+ {t("customers.send_invite")}
186
187
  </label>
187
188
  )}
188
189
  </CardContent>
@@ -190,7 +191,7 @@ export default function CustomerEditor() {
190
191
 
191
192
  <Card>
192
193
  <CardHeader className="pb-3">
193
- <CardTitle className="text-base">Billing address</CardTitle>
194
+ <CardTitle className="text-base">{t("customers.billing_address")}</CardTitle>
194
195
  </CardHeader>
195
196
  <CardContent>
196
197
  <AddressForm
@@ -204,7 +205,7 @@ export default function CustomerEditor() {
204
205
 
205
206
  <Card>
206
207
  <CardHeader className="flex-row items-center justify-between space-y-0 pb-3">
207
- <CardTitle className="text-base">Shipping address</CardTitle>
208
+ <CardTitle className="text-base">{t("customers.shipping_address")}</CardTitle>
208
209
  <Button
209
210
  type="button"
210
211
  variant="outline"
@@ -212,10 +213,10 @@ export default function CustomerEditor() {
212
213
  onClick={() => {
213
214
  const { email: _email, ...rest } = customer.billing || {};
214
215
  set("shipping")(rest);
215
- toast.success("Copied from billing");
216
+ toast.success(t("customers.copied_from_billing"));
216
217
  }}
217
218
  >
218
- <Copy className="mr-1 h-4 w-4" /> Copy from billing
219
+ <Copy className="mr-1 h-4 w-4" /> {t("customers.copy_from_billing")}
219
220
  </Button>
220
221
  </CardHeader>
221
222
  <CardContent>
@@ -230,7 +231,7 @@ export default function CustomerEditor() {
230
231
 
231
232
  <Card>
232
233
  <CardHeader className="pb-3">
233
- <CardTitle className="text-base">Custom fields</CardTitle>
234
+ <CardTitle className="text-base">{t("customers.custom_fields")}</CardTitle>
234
235
  </CardHeader>
235
236
  <CardContent>
236
237
  <MetaDataEditor value={customer.meta_data || []} onChange={set("meta_data")} />
@@ -243,7 +244,7 @@ export default function CustomerEditor() {
243
244
  <div className="space-y-4">
244
245
  <Card>
245
246
  <CardHeader className="flex-row items-center justify-between space-y-0 pb-3">
246
- <CardTitle className="text-base">Stats</CardTitle>
247
+ <CardTitle className="text-base">{t("customers.stats")}</CardTitle>
247
248
  <Button
248
249
  variant="ghost"
249
250
  size="sm"
@@ -252,35 +253,35 @@ export default function CustomerEditor() {
252
253
  disabled={recalculating}
253
254
  >
254
255
  <RefreshCw className={`mr-1 h-3.5 w-3.5 ${recalculating ? "animate-spin" : ""}`} />
255
- Recalculate
256
+ {t("customers.recalculate")}
256
257
  </Button>
257
258
  </CardHeader>
258
259
  <CardContent className="space-y-2 text-sm">
259
260
  <div className="flex justify-between">
260
- <span className="text-muted-foreground">Total spend</span>
261
+ <span className="text-muted-foreground">{t("customers.total_spend")}</span>
261
262
  <span className="font-medium">{money.format(customer.total_spent ?? 0)}</span>
262
263
  </div>
263
264
  <div className="flex justify-between">
264
- <span className="text-muted-foreground">Orders</span>
265
+ <span className="text-muted-foreground">{t("routes.orders")}</span>
265
266
  <span className="font-medium">{customer.orders_count ?? 0}</span>
266
267
  </div>
267
268
  <div className="flex justify-between">
268
- <span className="text-muted-foreground">Average order value</span>
269
+ <span className="text-muted-foreground">{t("customers.average_order_value")}</span>
269
270
  <span className="font-medium">{money.format(aov)}</span>
270
271
  </div>
271
272
  {customer.is_paying_customer && (
272
- <p className="pt-1 text-xs text-muted-foreground">Paying customer ✓</p>
273
+ <p className="pt-1 text-xs text-muted-foreground">{t("customers.paying_customer")}</p>
273
274
  )}
274
275
  </CardContent>
275
276
  </Card>
276
277
 
277
278
  <Card>
278
279
  <CardHeader className="pb-3">
279
- <CardTitle className="text-base">Recent orders</CardTitle>
280
+ <CardTitle className="text-base">{t("customers.recent_orders")}</CardTitle>
280
281
  </CardHeader>
281
282
  <CardContent className="space-y-1">
282
283
  {recentOrders.length === 0 && (
283
- <p className="text-sm text-muted-foreground">No orders yet.</p>
284
+ <p className="text-sm text-muted-foreground">{t("customers.no_orders_yet")}</p>
284
285
  )}
285
286
  {recentOrders.map((o) => (
286
287
  <div key={o.id}>
@@ -307,9 +308,9 @@ export default function CustomerEditor() {
307
308
  <ConfirmDialog
308
309
  open={confirmDelete}
309
310
  onOpenChange={setConfirmDelete}
310
- title="Delete this customer?"
311
- description="Their orders are kept and reassigned to Guest. This cannot be undone."
312
- confirmLabel="Delete"
311
+ title={t("customers.confirm_delete_title")}
312
+ description={t("customers.confirm_delete_description")}
313
+ confirmLabel={t("common.delete")}
313
314
  loading={deleting}
314
315
  onConfirm={doDelete}
315
316
  />
@@ -6,6 +6,7 @@ import { Input } from "@/components/ui/input";
6
6
  import { Plus, Search, Users, Trash2, Pencil } from "lucide-react";
7
7
 
8
8
  import { call, base44 } from "../../lib/api";
9
+ import { t } from "../../i18n";
9
10
  import { countryName } from "../../lib/geo-data";
10
11
  import usePagedList from "../../hooks/usePagedList";
11
12
  import useDebounce from "../../hooks/useDebounce";
@@ -46,7 +47,7 @@ export default function CustomersList() {
46
47
  id: pendingDelete.id,
47
48
  reassign_orders_to_guest: true,
48
49
  });
49
- toast.success("Customer deleted");
50
+ toast.success(t("customers.deleted"));
50
51
  setPendingDelete(null);
51
52
  list.refetch();
52
53
  } catch {
@@ -59,7 +60,7 @@ export default function CustomersList() {
59
60
  const columns = [
60
61
  {
61
62
  key: "name",
62
- label: "Name",
63
+ label: t("common.name"),
63
64
  render: (c) => {
64
65
  const name = [c.first_name, c.last_name].filter(Boolean).join(" ");
65
66
  return (
@@ -71,7 +72,7 @@ export default function CustomersList() {
71
72
  {name || c.email}
72
73
  {c.is_guest && (
73
74
  <span className="ml-2 rounded bg-muted px-1.5 py-0.5 text-[10px] uppercase text-muted-foreground">
74
- guest
75
+ {t("customers.guest_badge")}
75
76
  </span>
76
77
  )}
77
78
  </Link>
@@ -80,7 +81,7 @@ export default function CustomersList() {
80
81
  },
81
82
  {
82
83
  key: "email",
83
- label: "Email / Username",
84
+ label: t("customers.email_username"),
84
85
  render: (c) => (
85
86
  <div className="min-w-0">
86
87
  <div className="truncate">{c.email}</div>
@@ -90,7 +91,7 @@ export default function CustomersList() {
90
91
  },
91
92
  {
92
93
  key: "location",
93
- label: "Location",
94
+ label: t("customers.location"),
94
95
  render: (c) => {
95
96
  const b = c.billing || {};
96
97
  return [b.city, countryName(b.country)].filter(Boolean).join(", ") || "—";
@@ -98,13 +99,13 @@ export default function CustomersList() {
98
99
  },
99
100
  {
100
101
  key: "orders_count",
101
- label: "Orders",
102
+ label: t("routes.orders"),
102
103
  sortable: !debouncedQuery,
103
104
  render: (c) => c.orders_count ?? 0,
104
105
  },
105
106
  {
106
107
  key: "total_spent",
107
- label: "Total spend",
108
+ label: t("customers.total_spend"),
108
109
  sortable: !debouncedQuery,
109
110
  render: (c) => money.format(c.total_spent ?? 0),
110
111
  },
@@ -113,12 +114,12 @@ export default function CustomersList() {
113
114
  return (
114
115
  <div>
115
116
  <PageHeader
116
- title="Customers"
117
- description="Registered customers and checkout guests."
117
+ title={t("routes.customers")}
118
+ description={t("customers.subtitle")}
118
119
  actions={
119
120
  <Button asChild>
120
121
  <Link to={href("customers/new")}>
121
- <Plus className="mr-1 h-4 w-4" /> Add customer
122
+ <Plus className="mr-1 h-4 w-4" /> {t("routes.customers_new")}
122
123
  </Link>
123
124
  </Button>
124
125
  }
@@ -129,7 +130,7 @@ export default function CustomersList() {
129
130
  <Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
130
131
  <Input
131
132
  className="pl-8"
132
- placeholder="Search by name, email or username…"
133
+ placeholder={t("customers.search_placeholder")}
133
134
  value={query}
134
135
  onChange={(e) => setQuery(e.target.value)}
135
136
  />
@@ -144,23 +145,23 @@ export default function CustomersList() {
144
145
  onSort={debouncedQuery ? undefined : list.setSort}
145
146
  onRowClick={(c) => navigate(href(`customers/${c.id}`))}
146
147
  rowActions={(c) => [
147
- { label: "Edit", icon: Pencil, onClick: () => navigate(href(`customers/${c.id}`)) },
148
- { label: "Delete", icon: Trash2, destructive: true, onClick: () => setPendingDelete(c) },
148
+ { label: t("common.edit"), icon: Pencil, onClick: () => navigate(href(`customers/${c.id}`)) },
149
+ { label: t("common.delete"), icon: Trash2, destructive: true, onClick: () => setPendingDelete(c) },
149
150
  ]}
150
151
  pagination={{ page: list.page, hasNext: list.hasNext, onNext: list.next, onPrev: list.prev }}
151
152
  empty={{
152
153
  icon: Users,
153
- title: "No customers yet",
154
- description: "Customers appear here after checkout or when you add them manually.",
154
+ title: t("customers.empty_title"),
155
+ description: t("customers.empty_description"),
155
156
  }}
156
157
  />
157
158
 
158
159
  <ConfirmDialog
159
160
  open={!!pendingDelete}
160
161
  onOpenChange={(o) => !o && setPendingDelete(null)}
161
- title="Delete this customer?"
162
- description="Their orders are kept and reassigned to Guest. This cannot be undone."
163
- confirmLabel="Delete"
162
+ title={t("customers.confirm_delete_title")}
163
+ description={t("customers.confirm_delete_description")}
164
+ confirmLabel={t("common.delete")}
164
165
  loading={deleting}
165
166
  onConfirm={doDelete}
166
167
  />