@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.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/base44/agents/commerce/StoreAdmin.jsonc +64 -0
  4. package/base44/entities/commerce.Cart.jsonc +73 -0
  5. package/base44/entities/commerce.Coupon.jsonc +113 -0
  6. package/base44/entities/commerce.Customer.jsonc +96 -0
  7. package/base44/entities/commerce.DownloadPermission.jsonc +54 -0
  8. package/base44/entities/commerce.EmailLog.jsonc +43 -0
  9. package/base44/entities/commerce.Order.jsonc +287 -0
  10. package/base44/entities/commerce.OrderNote.jsonc +31 -0
  11. package/base44/entities/commerce.OrderRefund.jsonc +64 -0
  12. package/base44/entities/commerce.PaymentGateway.jsonc +48 -0
  13. package/base44/entities/commerce.Product.jsonc +291 -0
  14. package/base44/entities/commerce.ProductAttribute.jsonc +39 -0
  15. package/base44/entities/commerce.ProductAttributeTerm.jsonc +38 -0
  16. package/base44/entities/commerce.ProductCategory.jsonc +51 -0
  17. package/base44/entities/commerce.ProductReview.jsonc +48 -0
  18. package/base44/entities/commerce.ProductTag.jsonc +30 -0
  19. package/base44/entities/commerce.ProductVariation.jsonc +167 -0
  20. package/base44/entities/commerce.ShippingClass.jsonc +30 -0
  21. package/base44/entities/commerce.ShippingZone.jsonc +41 -0
  22. package/base44/entities/commerce.ShippingZoneMethod.jsonc +84 -0
  23. package/base44/entities/commerce.StoreSettings.jsonc +23 -0
  24. package/base44/entities/commerce.TaxClass.jsonc +23 -0
  25. package/base44/entities/commerce.TaxRate.jsonc +68 -0
  26. package/base44/entities/commerce.Webhook.jsonc +57 -0
  27. package/base44/entities/commerce.WebhookDelivery.jsonc +45 -0
  28. package/base44/functions/commerce/admin-coupons/entry.ts +100 -0
  29. package/base44/functions/commerce/admin-customers/entry.ts +141 -0
  30. package/base44/functions/commerce/admin-orders/entry.ts +396 -0
  31. package/base44/functions/commerce/admin-orders/helpers.ts +246 -0
  32. package/base44/functions/commerce/admin-products/entry.ts +506 -0
  33. package/base44/functions/commerce/admin-refunds/entry.ts +158 -0
  34. package/base44/functions/commerce/admin-reports/entry.ts +283 -0
  35. package/base44/functions/commerce/admin-reviews/entry.ts +66 -0
  36. package/base44/functions/commerce/admin-tools/entry.ts +261 -0
  37. package/base44/functions/commerce/admin-webhooks/entry.ts +52 -0
  38. package/base44/functions/commerce/payment-webhook/entry.ts +135 -0
  39. package/base44/functions/commerce/payments/entry.ts +238 -0
  40. package/base44/functions/commerce/seed-store/defaults.ts +162 -0
  41. package/base44/functions/commerce/seed-store/entry.ts +310 -0
  42. package/base44/functions/commerce/seed-store/sample-data.ts +349 -0
  43. package/base44/functions/commerce/storefront-account/entry.ts +207 -0
  44. package/base44/functions/commerce/storefront-cart/cart-pricing.ts +258 -0
  45. package/base44/functions/commerce/storefront-cart/entry.ts +283 -0
  46. package/base44/functions/commerce/storefront-catalog/entry.ts +459 -0
  47. package/base44/functions/commerce/storefront-checkout/cart-pricing.ts +258 -0
  48. package/base44/functions/commerce/storefront-checkout/entry.ts +485 -0
  49. package/base44/shared/commerce/auth.ts +60 -0
  50. package/base44/shared/commerce/coupons.ts +257 -0
  51. package/base44/shared/commerce/data/continents.ts +75 -0
  52. package/base44/shared/commerce/data/countries.ts +307 -0
  53. package/base44/shared/commerce/data/currencies.ts +46 -0
  54. package/base44/shared/commerce/email-templates.ts +240 -0
  55. package/base44/shared/commerce/emails.ts +225 -0
  56. package/base44/shared/commerce/money.ts +66 -0
  57. package/base44/shared/commerce/orders.ts +251 -0
  58. package/base44/shared/commerce/payments.ts +495 -0
  59. package/base44/shared/commerce/reviews.ts +36 -0
  60. package/base44/shared/commerce/scan.ts +57 -0
  61. package/base44/shared/commerce/sequence.ts +35 -0
  62. package/base44/shared/commerce/settings.ts +57 -0
  63. package/base44/shared/commerce/shipping.ts +215 -0
  64. package/base44/shared/commerce/stock.ts +227 -0
  65. package/base44/shared/commerce/stripe.ts +463 -0
  66. package/base44/shared/commerce/tax.ts +136 -0
  67. package/base44/shared/commerce/totals.ts +314 -0
  68. package/base44/shared/commerce/webhooks.ts +116 -0
  69. package/package.json +37 -0
  70. package/scripts/install.js +156 -0
  71. package/skills/commerce/SKILL.md +62 -0
  72. package/skills/commerce/docs/api-admin.md +186 -0
  73. package/skills/commerce/docs/api-storefront.md +408 -0
  74. package/skills/commerce/installation-guidelines.md +91 -0
  75. package/skills/commerce/post-installation.md +157 -0
  76. package/skills/commerce/references/emails.md +13 -0
  77. package/skills/commerce/references/guest-access-security.md +18 -0
  78. package/skills/commerce/references/limits-and-performance.md +16 -0
  79. package/skills/commerce/references/media-and-downloads.md +4 -0
  80. package/skills/commerce/references/online-payments.md +201 -0
  81. package/skills/commerce/references/product-render.md +87 -0
  82. package/skills/commerce/references/scheduled-work.md +19 -0
  83. package/skills/commerce/references/storefront-product-page.md +83 -0
  84. package/skills/commerce/references/webhooks.md +8 -0
  85. package/src/commerce/admin/README.md +107 -0
  86. package/src/commerce/admin/bot/Markdown.jsx +138 -0
  87. package/src/commerce/admin/bot/StoreAdminBot.jsx +249 -0
  88. package/src/commerce/admin/bot/pipe-tables.js +116 -0
  89. package/src/commerce/admin/components/AddressForm.jsx +78 -0
  90. package/src/commerce/admin/components/ConfirmDialog.jsx +52 -0
  91. package/src/commerce/admin/components/CountrySelect.jsx +81 -0
  92. package/src/commerce/admin/components/DataTable.jsx +192 -0
  93. package/src/commerce/admin/components/DateRangePicker.jsx +91 -0
  94. package/src/commerce/admin/components/EmptyState.jsx +17 -0
  95. package/src/commerce/admin/components/MediaUploader.jsx +116 -0
  96. package/src/commerce/admin/components/MetaDataEditor.jsx +45 -0
  97. package/src/commerce/admin/components/MoneyInput.jsx +50 -0
  98. package/src/commerce/admin/components/PageHeader.jsx +29 -0
  99. package/src/commerce/admin/components/RichTextarea.jsx +21 -0
  100. package/src/commerce/admin/components/SearchSelect.jsx +142 -0
  101. package/src/commerce/admin/components/StatusBadge.jsx +17 -0
  102. package/src/commerce/admin/context/BasePathContext.jsx +26 -0
  103. package/src/commerce/admin/context/SettingsContext.jsx +207 -0
  104. package/src/commerce/admin/hooks/useAsync.js +46 -0
  105. package/src/commerce/admin/hooks/useDebounce.js +11 -0
  106. package/src/commerce/admin/hooks/useMoney.js +52 -0
  107. package/src/commerce/admin/hooks/usePagedList.js +83 -0
  108. package/src/commerce/admin/hooks/usePaymentProvider.js +27 -0
  109. package/src/commerce/admin/hooks/useRealtime.js +129 -0
  110. package/src/commerce/admin/index.jsx +34 -0
  111. package/src/commerce/admin/layout/AccessDenied.jsx +54 -0
  112. package/src/commerce/admin/layout/AdminLayout.jsx +33 -0
  113. package/src/commerce/admin/layout/AuthGuard.jsx +84 -0
  114. package/src/commerce/admin/layout/Sidebar.jsx +130 -0
  115. package/src/commerce/admin/layout/Topbar.jsx +94 -0
  116. package/src/commerce/admin/lib/api.js +55 -0
  117. package/src/commerce/admin/lib/constants.js +157 -0
  118. package/src/commerce/admin/lib/format.js +27 -0
  119. package/src/commerce/admin/lib/geo-data.js +125 -0
  120. package/src/commerce/admin/lib/order-utils.js +147 -0
  121. package/src/commerce/admin/lib/paths.js +35 -0
  122. package/src/commerce/admin/lib/product-utils.js +55 -0
  123. package/src/commerce/admin/pages/Dashboard.jsx +245 -0
  124. package/src/commerce/admin/pages/coupons/CouponEditor.jsx +565 -0
  125. package/src/commerce/admin/pages/coupons/CouponsList.jsx +172 -0
  126. package/src/commerce/admin/pages/customers/CustomerEditor.jsx +318 -0
  127. package/src/commerce/admin/pages/customers/CustomersList.jsx +169 -0
  128. package/src/commerce/admin/pages/orders/OrderEditor.jsx +952 -0
  129. package/src/commerce/admin/pages/orders/OrdersList.jsx +227 -0
  130. package/src/commerce/admin/pages/orders/components/AddProductDialog.jsx +149 -0
  131. package/src/commerce/admin/pages/orders/components/DownloadPermissionsPanel.jsx +119 -0
  132. package/src/commerce/admin/pages/orders/components/LineItemsTable.jsx +208 -0
  133. package/src/commerce/admin/pages/orders/components/OrderNotesPanel.jsx +123 -0
  134. package/src/commerce/admin/pages/orders/components/PaymentPanel.jsx +199 -0
  135. package/src/commerce/admin/pages/orders/components/RefundPanel.jsx +239 -0
  136. package/src/commerce/admin/pages/orders/components/TotalsBox.jsx +52 -0
  137. package/src/commerce/admin/pages/products/AttributeTerms.jsx +180 -0
  138. package/src/commerce/admin/pages/products/Attributes.jsx +183 -0
  139. package/src/commerce/admin/pages/products/Categories.jsx +236 -0
  140. package/src/commerce/admin/pages/products/ProductEditor.jsx +267 -0
  141. package/src/commerce/admin/pages/products/ProductsList.jsx +391 -0
  142. package/src/commerce/admin/pages/products/Reviews.jsx +255 -0
  143. package/src/commerce/admin/pages/products/Tags.jsx +150 -0
  144. package/src/commerce/admin/pages/products/components/ProductDataPanel.jsx +132 -0
  145. package/src/commerce/admin/pages/products/components/PublishBox.jsx +101 -0
  146. package/src/commerce/admin/pages/products/components/TaxonomyPanel.jsx +243 -0
  147. package/src/commerce/admin/pages/products/components/tabs/AdvancedTab.jsx +48 -0
  148. package/src/commerce/admin/pages/products/components/tabs/AttributesTab.jsx +208 -0
  149. package/src/commerce/admin/pages/products/components/tabs/DownloadsTab.jsx +91 -0
  150. package/src/commerce/admin/pages/products/components/tabs/ExternalTab.jsx +41 -0
  151. package/src/commerce/admin/pages/products/components/tabs/GeneralTab.jsx +103 -0
  152. package/src/commerce/admin/pages/products/components/tabs/InventoryTab.jsx +93 -0
  153. package/src/commerce/admin/pages/products/components/tabs/LinkedTab.jsx +102 -0
  154. package/src/commerce/admin/pages/products/components/tabs/ShippingTab.jsx +86 -0
  155. package/src/commerce/admin/pages/products/components/tabs/VariationsTab.jsx +377 -0
  156. package/src/commerce/admin/pages/reports/Reports.jsx +416 -0
  157. package/src/commerce/admin/pages/settings/EmailsSettings.jsx +240 -0
  158. package/src/commerce/admin/pages/settings/GeneralSettings.jsx +232 -0
  159. package/src/commerce/admin/pages/settings/InventorySettings.jsx +146 -0
  160. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +260 -0
  161. package/src/commerce/admin/pages/settings/ProductsSettings.jsx +118 -0
  162. package/src/commerce/admin/pages/settings/SettingsLayout.jsx +53 -0
  163. package/src/commerce/admin/pages/settings/ShippingSettings.jsx +304 -0
  164. package/src/commerce/admin/pages/settings/ShippingZoneEditor.jsx +514 -0
  165. package/src/commerce/admin/pages/settings/TaxRatesTable.jsx +231 -0
  166. package/src/commerce/admin/pages/settings/TaxSettings.jsx +281 -0
  167. package/src/commerce/admin/pages/settings/useGroupForm.jsx +76 -0
  168. package/src/commerce/admin/pages/status/WebhookEditor.jsx +296 -0
  169. package/src/commerce/admin/pages/status/Webhooks.jsx +53 -0
  170. package/src/commerce/admin/routes.jsx +151 -0
  171. package/src/commerce/utils/index.js +19 -0
  172. package/src/commerce/utils/shipping-promos.js +99 -0
  173. package/src/commerce/utils/variants.js +411 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Minimal GitHub-flavored pipe-table support for the bot's markdown renderer.
3
+ *
4
+ * The StoreAdmin agent is told to format lists and reports as GFM tables
5
+ * (`| col | col |` over a `|---|` separator), which CommonMark does not
6
+ * understand — plain react-markdown renders them as literal pipes. Rather than
7
+ * add `remark-gfm` (and make every install run an npm install for one feature),
8
+ * tables are pulled out of the source here and rendered directly; everything
9
+ * else still goes through react-markdown untouched.
10
+ *
11
+ * Covers what agents actually emit: optional leading/trailing pipes, alignment
12
+ * markers, `\|` escapes, and ragged rows (padded/truncated to the header, as GFM
13
+ * specifies). Not covered: tables nested inside a list or blockquote, and the
14
+ * other GFM extensions (strikethrough, task lists, bare-URL autolinks).
15
+ */
16
+
17
+ /** `|---|:--:|---:|` — the row that turns the line above it into a table header. */
18
+ export function isDelimiterRow(line) {
19
+ const trimmed = String(line ?? "").trim();
20
+ if (!trimmed.includes("-")) return false;
21
+ return /^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?$/.test(trimmed);
22
+ }
23
+
24
+ /**
25
+ * Split one row into cells on unescaped pipes, dropping the optional outer
26
+ * pipes and unescaping `\|` into a literal pipe.
27
+ */
28
+ export function splitCells(row) {
29
+ const line = String(row ?? "").trim();
30
+ const cells = [];
31
+ let cell = "";
32
+ for (let i = 0; i < line.length; i += 1) {
33
+ const ch = line[i];
34
+ if (ch === "\\" && line[i + 1] === "|") {
35
+ cell += "|";
36
+ i += 1;
37
+ } else if (ch === "|") {
38
+ cells.push(cell);
39
+ cell = "";
40
+ } else {
41
+ cell += ch;
42
+ }
43
+ }
44
+ cells.push(cell);
45
+ // A row written with outer pipes yields an empty cell at each end.
46
+ if (line.startsWith("|")) cells.shift();
47
+ if (line.endsWith("|") && !line.endsWith("\\|") && cells.length) cells.pop();
48
+ return cells.map((c) => c.trim());
49
+ }
50
+
51
+ /** Per-column alignment from the delimiter row: "left" | "center" | "right" | null. */
52
+ export function parseAlignments(delimiterRow) {
53
+ return splitCells(delimiterRow).map((cell) => {
54
+ const left = cell.startsWith(":");
55
+ const right = cell.endsWith(":");
56
+ if (left && right) return "center";
57
+ if (right) return "right";
58
+ if (left) return "left";
59
+ return null;
60
+ });
61
+ }
62
+
63
+ /** Force a body row to the header's column count (GFM pads/truncates). */
64
+ const fitRow = (cells, width) =>
65
+ Array.from({ length: width }, (_, i) => cells[i] ?? "");
66
+
67
+ /**
68
+ * Split markdown into an ordered list of segments:
69
+ * { type: "markdown", value }
70
+ * { type: "table", head: string[], align: (string|null)[], rows: string[][] }
71
+ *
72
+ * Non-table text keeps its original shape, so react-markdown still sees whole
73
+ * blocks rather than fragments.
74
+ */
75
+ export function splitPipeTables(source) {
76
+ const lines = String(source ?? "").split("\n");
77
+ const segments = [];
78
+ let buffer = [];
79
+
80
+ const flush = () => {
81
+ if (!buffer.length) return;
82
+ const value = buffer.join("\n");
83
+ if (value.trim()) segments.push({ type: "markdown", value });
84
+ buffer = [];
85
+ };
86
+
87
+ for (let i = 0; i < lines.length; i += 1) {
88
+ const line = lines[i];
89
+ const next = lines[i + 1];
90
+ const head = line.includes("|") ? splitCells(line) : null;
91
+
92
+ // A table starts only where a row containing pipes is followed by a
93
+ // delimiter row with the same number of columns. That pairing is what
94
+ // separates a table from a paragraph that happens to contain a pipe — and
95
+ // from a setext heading, where `Orders | Today` over `---` gives one
96
+ // delimiter column against two header cells.
97
+ if (head && next !== undefined && isDelimiterRow(next) && parseAlignments(next).length === head.length) {
98
+ flush();
99
+ const align = parseAlignments(next);
100
+ const rows = [];
101
+ let j = i + 2;
102
+ for (; j < lines.length; j += 1) {
103
+ const row = lines[j];
104
+ if (!row.trim() || !row.includes("|")) break; // blank line or prose ends the table
105
+ rows.push(fitRow(splitCells(row), head.length));
106
+ }
107
+ segments.push({ type: "table", head, align, rows });
108
+ i = j - 1;
109
+ continue;
110
+ }
111
+ buffer.push(line);
112
+ }
113
+
114
+ flush();
115
+ return segments;
116
+ }
@@ -0,0 +1,78 @@
1
+ import React from "react";
2
+ import { Input } from "@/components/ui/input";
3
+ import { Label } from "@/components/ui/label";
4
+ import {
5
+ Select,
6
+ SelectContent,
7
+ SelectItem,
8
+ SelectTrigger,
9
+ SelectValue,
10
+ } from "@/components/ui/select";
11
+ import CountrySelect from "./CountrySelect";
12
+ import { statesFor } from "../lib/geo-data";
13
+
14
+ /**
15
+ * Billing/shipping address editor (Order billing shape).
16
+ * Props: { value, onChange, showEmail?, showPhone?, disabled? }
17
+ */
18
+ export default function AddressForm({
19
+ value = {},
20
+ onChange,
21
+ showEmail = false,
22
+ showPhone = true,
23
+ disabled = false,
24
+ }) {
25
+ const set = (key) => (v) => onChange({ ...value, [key]: v });
26
+ const setInput = (key) => (e) => set(key)(e.target.value);
27
+ const states = statesFor(value.country);
28
+
29
+ const field = (label, node) => (
30
+ <div className="grid gap-1.5">
31
+ <Label className="text-xs text-muted-foreground">{label}</Label>
32
+ {node}
33
+ </div>
34
+ );
35
+
36
+ return (
37
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
38
+ {field("First name", <Input value={value.first_name || ""} onChange={setInput("first_name")} disabled={disabled} />)}
39
+ {field("Last name", <Input value={value.last_name || ""} onChange={setInput("last_name")} disabled={disabled} />)}
40
+ <div className="sm:col-span-2">
41
+ {field("Company", <Input value={value.company || ""} onChange={setInput("company")} disabled={disabled} />)}
42
+ </div>
43
+ <div className="sm:col-span-2">
44
+ {field("Address line 1", <Input value={value.address_1 || ""} onChange={setInput("address_1")} disabled={disabled} />)}
45
+ </div>
46
+ <div className="sm:col-span-2">
47
+ {field("Address line 2", <Input value={value.address_2 || ""} onChange={setInput("address_2")} disabled={disabled} />)}
48
+ </div>
49
+ {field("City", <Input value={value.city || ""} onChange={setInput("city")} disabled={disabled} />)}
50
+ {field("Postcode / ZIP", <Input value={value.postcode || ""} onChange={setInput("postcode")} disabled={disabled} />)}
51
+ {field(
52
+ "Country",
53
+ <CountrySelect value={value.country || ""} onChange={(code) => onChange({ ...value, country: code, state: "" })} disabled={disabled} />
54
+ )}
55
+ {field(
56
+ "State / Province",
57
+ states ? (
58
+ <Select value={value.state || ""} onValueChange={set("state")} disabled={disabled}>
59
+ <SelectTrigger>
60
+ <SelectValue placeholder="Select state…" />
61
+ </SelectTrigger>
62
+ <SelectContent>
63
+ {states.map((s) => (
64
+ <SelectItem key={s.code} value={s.code}>
65
+ {s.name}
66
+ </SelectItem>
67
+ ))}
68
+ </SelectContent>
69
+ </Select>
70
+ ) : (
71
+ <Input value={value.state || ""} onChange={setInput("state")} disabled={disabled} />
72
+ )
73
+ )}
74
+ {showEmail && field("Email", <Input type="email" value={value.email || ""} onChange={setInput("email")} disabled={disabled} />)}
75
+ {showPhone && field("Phone", <Input value={value.phone || ""} onChange={setInput("phone")} disabled={disabled} />)}
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import {
3
+ AlertDialog,
4
+ AlertDialogAction,
5
+ AlertDialogCancel,
6
+ AlertDialogContent,
7
+ AlertDialogDescription,
8
+ AlertDialogFooter,
9
+ AlertDialogHeader,
10
+ AlertDialogTitle,
11
+ } from "@/components/ui/alert-dialog";
12
+ import { Loader2 } from "lucide-react";
13
+
14
+ /**
15
+ * Confirmation dialog for destructive actions.
16
+ * Props: { open, onOpenChange, title?, description?, confirmLabel?, destructive?, onConfirm, loading? }
17
+ */
18
+ export default function ConfirmDialog({
19
+ open,
20
+ onOpenChange,
21
+ title = "Are you sure?",
22
+ description = "This action cannot be undone.",
23
+ confirmLabel = "Confirm",
24
+ destructive = true,
25
+ onConfirm,
26
+ loading = false,
27
+ }) {
28
+ return (
29
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
30
+ <AlertDialogContent>
31
+ <AlertDialogHeader>
32
+ <AlertDialogTitle>{title}</AlertDialogTitle>
33
+ <AlertDialogDescription>{description}</AlertDialogDescription>
34
+ </AlertDialogHeader>
35
+ <AlertDialogFooter>
36
+ <AlertDialogCancel disabled={loading}>Cancel</AlertDialogCancel>
37
+ <AlertDialogAction
38
+ disabled={loading}
39
+ className={destructive ? "bg-destructive text-destructive-foreground hover:bg-destructive/90" : ""}
40
+ onClick={(e) => {
41
+ e.preventDefault();
42
+ onConfirm();
43
+ }}
44
+ >
45
+ {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
46
+ {confirmLabel}
47
+ </AlertDialogAction>
48
+ </AlertDialogFooter>
49
+ </AlertDialogContent>
50
+ </AlertDialog>
51
+ );
52
+ }
@@ -0,0 +1,81 @@
1
+ import React, { 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 { Check, ChevronsUpDown } from "lucide-react";
13
+ import { COUNTRIES } from "../lib/geo-data";
14
+
15
+ /**
16
+ * Searchable country picker.
17
+ * Props: { value (ISO code), onChange(code), placeholder?, allowEmpty?, disabled? }
18
+ */
19
+ export default function CountrySelect({
20
+ value,
21
+ onChange,
22
+ placeholder = "Select country…",
23
+ allowEmpty = false,
24
+ disabled = false,
25
+ className = "",
26
+ }) {
27
+ const [open, setOpen] = useState(false);
28
+ const selected = COUNTRIES.find((c) => c.code === value);
29
+
30
+ return (
31
+ <Popover open={open} onOpenChange={setOpen}>
32
+ <PopoverTrigger asChild>
33
+ <Button
34
+ type="button"
35
+ variant="outline"
36
+ role="combobox"
37
+ disabled={disabled}
38
+ className={`w-full justify-between font-normal ${className}`}
39
+ >
40
+ <span className="truncate">{selected?.name || placeholder}</span>
41
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
42
+ </Button>
43
+ </PopoverTrigger>
44
+ <PopoverContent className="w-(--radix-popover-trigger-width) min-w-64 p-0" align="start">
45
+ <Command>
46
+ <CommandInput placeholder="Search countries…" />
47
+ <CommandList>
48
+ <CommandEmpty>No country found.</CommandEmpty>
49
+ <CommandGroup>
50
+ {allowEmpty && (
51
+ <CommandItem
52
+ value="__any__"
53
+ onSelect={() => {
54
+ onChange("");
55
+ setOpen(false);
56
+ }}
57
+ >
58
+ <Check className={`mr-2 h-4 w-4 ${!value ? "opacity-100" : "opacity-0"}`} />
59
+ Any country
60
+ </CommandItem>
61
+ )}
62
+ {COUNTRIES.map((c) => (
63
+ <CommandItem
64
+ key={c.code}
65
+ value={`${c.name} ${c.code}`}
66
+ onSelect={() => {
67
+ onChange(c.code);
68
+ setOpen(false);
69
+ }}
70
+ >
71
+ <Check className={`mr-2 h-4 w-4 ${value === c.code ? "opacity-100" : "opacity-0"}`} />
72
+ {c.name}
73
+ </CommandItem>
74
+ ))}
75
+ </CommandGroup>
76
+ </CommandList>
77
+ </Command>
78
+ </PopoverContent>
79
+ </Popover>
80
+ );
81
+ }
@@ -0,0 +1,192 @@
1
+ import React from "react";
2
+ import {
3
+ Table,
4
+ TableBody,
5
+ TableCell,
6
+ TableHead,
7
+ TableHeader,
8
+ TableRow,
9
+ } from "@/components/ui/table";
10
+ import { Button } from "@/components/ui/button";
11
+ import { Checkbox } from "@/components/ui/checkbox";
12
+ import { Skeleton } from "@/components/ui/skeleton";
13
+ import {
14
+ DropdownMenu,
15
+ DropdownMenuContent,
16
+ DropdownMenuItem,
17
+ DropdownMenuTrigger,
18
+ } from "@/components/ui/dropdown-menu";
19
+ import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
20
+ import EmptyState from "./EmptyState";
21
+
22
+ /**
23
+ * Generic list table.
24
+ *
25
+ * Props:
26
+ * - columns: [{ key, label, sortable?, render?(row), className? }]
27
+ * - rows: array of records
28
+ * - rowKey: field used as key (default "id")
29
+ * - sort / onSort: SDK sort string, e.g. "-created_date"; clicking a sortable
30
+ * column toggles `key` ↔ `-key`
31
+ * - selectable + selected (array of ids) + onSelectChange(ids)
32
+ * - rowActions(row) → [{ label, onClick, icon?, destructive? }]
33
+ * - onRowClick(row)
34
+ * - pagination: { page, hasNext, onNext, onPrev }
35
+ * - loading, empty: { icon, title, description, action }
36
+ */
37
+ export default function DataTable({
38
+ columns,
39
+ rows = [],
40
+ rowKey = "id",
41
+ sort,
42
+ onSort,
43
+ selectable = false,
44
+ selected = [],
45
+ onSelectChange,
46
+ rowActions,
47
+ onRowClick,
48
+ pagination,
49
+ loading = false,
50
+ empty,
51
+ }) {
52
+ const allSelected = rows.length > 0 && rows.every((r) => selected.includes(r[rowKey]));
53
+
54
+ const toggleAll = () => {
55
+ if (!onSelectChange) return;
56
+ onSelectChange(allSelected ? [] : rows.map((r) => r[rowKey]));
57
+ };
58
+
59
+ const toggleOne = (id) => {
60
+ if (!onSelectChange) return;
61
+ onSelectChange(
62
+ selected.includes(id) ? selected.filter((x) => x !== id) : [...selected, id]
63
+ );
64
+ };
65
+
66
+ const sortIcon = (key) => {
67
+ if (sort === key) return <ArrowUp className="ml-1 inline h-3.5 w-3.5" />;
68
+ if (sort === `-${key}`) return <ArrowDown className="ml-1 inline h-3.5 w-3.5" />;
69
+ return <ArrowUpDown className="ml-1 inline h-3.5 w-3.5 opacity-40" />;
70
+ };
71
+
72
+ const handleSort = (col) => {
73
+ if (!col.sortable || !onSort) return;
74
+ onSort(sort === `-${col.key}` ? col.key : `-${col.key}`);
75
+ };
76
+
77
+ const colCount = columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0);
78
+
79
+ return (
80
+ <div className="rounded-lg border bg-background">
81
+ <div className="overflow-x-auto">
82
+ <Table>
83
+ <TableHeader>
84
+ <TableRow>
85
+ {selectable && (
86
+ <TableHead className="w-10">
87
+ <Checkbox checked={allSelected} onCheckedChange={toggleAll} />
88
+ </TableHead>
89
+ )}
90
+ {columns.map((col) => (
91
+ <TableHead
92
+ key={col.key}
93
+ className={`${col.className || ""} ${col.sortable ? "cursor-pointer select-none" : ""}`}
94
+ onClick={() => handleSort(col)}
95
+ >
96
+ {col.label}
97
+ {col.sortable && sortIcon(col.key)}
98
+ </TableHead>
99
+ ))}
100
+ {rowActions && <TableHead className="w-12" />}
101
+ </TableRow>
102
+ </TableHeader>
103
+ <TableBody>
104
+ {loading ? (
105
+ Array.from({ length: 5 }).map((_, i) => (
106
+ <TableRow key={i}>
107
+ <TableCell colSpan={colCount}>
108
+ <Skeleton className="h-6 w-full" />
109
+ </TableCell>
110
+ </TableRow>
111
+ ))
112
+ ) : rows.length === 0 ? (
113
+ <TableRow>
114
+ <TableCell colSpan={colCount} className="py-12">
115
+ <EmptyState {...(empty || { title: "Nothing here yet" })} />
116
+ </TableCell>
117
+ </TableRow>
118
+ ) : (
119
+ rows.map((row) => (
120
+ <TableRow
121
+ key={row[rowKey]}
122
+ className={onRowClick ? "cursor-pointer" : ""}
123
+ onClick={onRowClick ? () => onRowClick(row) : undefined}
124
+ >
125
+ {selectable && (
126
+ <TableCell className="w-10" onClick={(e) => e.stopPropagation()}>
127
+ <Checkbox
128
+ checked={selected.includes(row[rowKey])}
129
+ onCheckedChange={() => toggleOne(row[rowKey])}
130
+ />
131
+ </TableCell>
132
+ )}
133
+ {columns.map((col) => (
134
+ <TableCell key={col.key} className={col.className}>
135
+ {col.render ? col.render(row) : row[col.key] ?? "—"}
136
+ </TableCell>
137
+ ))}
138
+ {rowActions && (
139
+ <TableCell className="w-12" onClick={(e) => e.stopPropagation()}>
140
+ <DropdownMenu>
141
+ <DropdownMenuTrigger asChild>
142
+ <Button variant="ghost" size="icon" className="h-8 w-8">
143
+ <MoreHorizontal className="h-4 w-4" />
144
+ </Button>
145
+ </DropdownMenuTrigger>
146
+ <DropdownMenuContent align="end">
147
+ {rowActions(row).map((action, i) => (
148
+ <DropdownMenuItem
149
+ key={i}
150
+ onClick={action.onClick}
151
+ className={action.destructive ? "text-destructive focus:text-destructive" : ""}
152
+ >
153
+ {action.icon && <action.icon className="mr-2 h-4 w-4" />}
154
+ {action.label}
155
+ </DropdownMenuItem>
156
+ ))}
157
+ </DropdownMenuContent>
158
+ </DropdownMenu>
159
+ </TableCell>
160
+ )}
161
+ </TableRow>
162
+ ))
163
+ )}
164
+ </TableBody>
165
+ </Table>
166
+ </div>
167
+ {pagination && (
168
+ <div className="flex items-center justify-end gap-2 border-t px-3 py-2 text-sm text-muted-foreground">
169
+ <span>Page {pagination.page}</span>
170
+ <Button
171
+ variant="outline"
172
+ size="icon"
173
+ className="h-7 w-7"
174
+ disabled={pagination.page <= 1 || loading}
175
+ onClick={pagination.onPrev}
176
+ >
177
+ <ChevronLeft className="h-4 w-4" />
178
+ </Button>
179
+ <Button
180
+ variant="outline"
181
+ size="icon"
182
+ className="h-7 w-7"
183
+ disabled={!pagination.hasNext || loading}
184
+ onClick={pagination.onNext}
185
+ >
186
+ <ChevronRight className="h-4 w-4" />
187
+ </Button>
188
+ </div>
189
+ )}
190
+ </div>
191
+ );
192
+ }
@@ -0,0 +1,91 @@
1
+ import React, { useState } from "react";
2
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Calendar } from "@/components/ui/calendar";
5
+ import { CalendarIcon, X } from "lucide-react";
6
+
7
+ const toISO = (d) => (d ? d.toISOString().slice(0, 10) : null);
8
+
9
+ function presetRanges() {
10
+ const today = new Date();
11
+ const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
12
+ const t = startOfDay(today);
13
+ const daysAgo = (n) => new Date(t.getFullYear(), t.getMonth(), t.getDate() - n);
14
+ return [
15
+ { label: "Today", from: t, to: t },
16
+ { label: "Last 7 days", from: daysAgo(6), to: t },
17
+ { label: "This month", from: new Date(t.getFullYear(), t.getMonth(), 1), to: t },
18
+ {
19
+ label: "Last month",
20
+ from: new Date(t.getFullYear(), t.getMonth() - 1, 1),
21
+ to: new Date(t.getFullYear(), t.getMonth(), 0),
22
+ },
23
+ { label: "This year", from: new Date(t.getFullYear(), 0, 1), to: t },
24
+ ];
25
+ }
26
+
27
+ /**
28
+ * Date range picker with common date presets.
29
+ * Props: { value: {from, to} (ISO yyyy-mm-dd) | null, onChange({from,to}|null), align? }
30
+ */
31
+ export default function DateRangePicker({ value, onChange, align = "start" }) {
32
+ const [open, setOpen] = useState(false);
33
+ const presets = presetRanges();
34
+
35
+ const label = value?.from
36
+ ? `${value.from}${value.to && value.to !== value.from ? ` → ${value.to}` : ""}`
37
+ : "All time";
38
+
39
+ const applyPreset = (p) => {
40
+ onChange({ from: toISO(p.from), to: toISO(p.to) });
41
+ setOpen(false);
42
+ };
43
+
44
+ const applyCalendar = (range) => {
45
+ if (!range?.from) return;
46
+ onChange({ from: toISO(range.from), to: toISO(range.to || range.from) });
47
+ };
48
+
49
+ return (
50
+ <div className="flex items-center gap-1">
51
+ <Popover open={open} onOpenChange={setOpen}>
52
+ <PopoverTrigger asChild>
53
+ <Button variant="outline" className="justify-start font-normal">
54
+ <CalendarIcon className="mr-2 h-4 w-4" />
55
+ {label}
56
+ </Button>
57
+ </PopoverTrigger>
58
+ <PopoverContent className="flex w-auto p-0" align={align}>
59
+ <div className="flex flex-col gap-0.5 border-r p-2">
60
+ {presets.map((p) => (
61
+ <Button
62
+ key={p.label}
63
+ variant="ghost"
64
+ size="sm"
65
+ className="justify-start"
66
+ onClick={() => applyPreset(p)}
67
+ >
68
+ {p.label}
69
+ </Button>
70
+ ))}
71
+ </div>
72
+ <Calendar
73
+ mode="range"
74
+ numberOfMonths={2}
75
+ selected={
76
+ value?.from
77
+ ? { from: new Date(value.from), to: value.to ? new Date(value.to) : undefined }
78
+ : undefined
79
+ }
80
+ onSelect={applyCalendar}
81
+ />
82
+ </PopoverContent>
83
+ </Popover>
84
+ {value?.from && (
85
+ <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => onChange(null)}>
86
+ <X className="h-4 w-4" />
87
+ </Button>
88
+ )}
89
+ </div>
90
+ );
91
+ }
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ import { Inbox } from "lucide-react";
3
+
4
+ /**
5
+ * Empty-list placeholder.
6
+ * Props: { icon?: LucideIcon, title?, description?, action?: node }
7
+ */
8
+ export default function EmptyState({ icon: Icon = Inbox, title = "Nothing here yet", description, action }) {
9
+ return (
10
+ <div className="flex flex-col items-center justify-center gap-2 py-8 text-center">
11
+ <Icon className="h-8 w-8 text-muted-foreground/40" />
12
+ <div className="text-sm font-medium">{title}</div>
13
+ {description && <p className="max-w-sm text-xs text-muted-foreground">{description}</p>}
14
+ {action && <div className="mt-2">{action}</div>}
15
+ </div>
16
+ );
17
+ }