@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
|
+
/**
|
|
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
|
+
}
|