@voyantjs/ui 0.6.7
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/README.md +30 -0
- package/dist/components/accordion.d.ts +7 -0
- package/dist/components/accordion.d.ts.map +1 -0
- package/dist/components/accordion.js +17 -0
- package/dist/components/alert-dialog.d.ts +19 -0
- package/dist/components/alert-dialog.d.ts.map +1 -0
- package/dist/components/alert-dialog.js +42 -0
- package/dist/components/alert.d.ts +11 -0
- package/dist/components/alert.d.ts.map +1 -0
- package/dist/components/alert.js +27 -0
- package/dist/components/aspect-ratio.d.ts +5 -0
- package/dist/components/aspect-ratio.d.ts.map +1 -0
- package/dist/components/aspect-ratio.js +8 -0
- package/dist/components/avatar.d.ts +12 -0
- package/dist/components/avatar.d.ts.map +1 -0
- package/dist/components/avatar.js +22 -0
- package/dist/components/badge.d.ts +8 -0
- package/dist/components/badge.d.ts.map +1 -0
- package/dist/components/badge.js +33 -0
- package/dist/components/breadcrumb.d.ts +11 -0
- package/dist/components/breadcrumb.d.ts.map +1 -0
- package/dist/components/breadcrumb.js +36 -0
- package/dist/components/button-group.d.ts +11 -0
- package/dist/components/button-group.d.ts.map +1 -0
- package/dist/components/button-group.js +36 -0
- package/dist/components/button.d.ts +9 -0
- package/dist/components/button.d.ts.map +1 -0
- package/dist/components/button.js +34 -0
- package/dist/components/calendar.d.ts +11 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +76 -0
- package/dist/components/card.d.ts +12 -0
- package/dist/components/card.d.ts.map +1 -0
- package/dist/components/card.js +24 -0
- package/dist/components/carousel.d.ts +29 -0
- package/dist/components/carousel.d.ts.map +1 -0
- package/dist/components/carousel.js +91 -0
- package/dist/components/chart.d.ts +45 -0
- package/dist/components/chart.d.ts.map +1 -0
- package/dist/components/chart.js +121 -0
- package/dist/components/checkbox.d.ts +4 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox.js +8 -0
- package/dist/components/collapsible.d.ts +6 -0
- package/dist/components/collapsible.d.ts.map +1 -0
- package/dist/components/collapsible.js +12 -0
- package/dist/components/combobox.d.ts +25 -0
- package/dist/components/combobox.d.ts.map +1 -0
- package/dist/components/combobox.js +57 -0
- package/dist/components/command.d.ts +20 -0
- package/dist/components/command.d.ts.map +1 -0
- package/dist/components/command.js +35 -0
- package/dist/components/confirm-action-button.d.ts +14 -0
- package/dist/components/confirm-action-button.d.ts.map +1 -0
- package/dist/components/confirm-action-button.js +21 -0
- package/dist/components/context-menu.d.ts +30 -0
- package/dist/components/context-menu.d.ts.map +1 -0
- package/dist/components/context-menu.js +50 -0
- package/dist/components/contract-template-authoring-help.d.ts +32 -0
- package/dist/components/contract-template-authoring-help.d.ts.map +1 -0
- package/dist/components/contract-template-authoring-help.js +37 -0
- package/dist/components/country-combobox.d.ts +9 -0
- package/dist/components/country-combobox.d.ts.map +1 -0
- package/dist/components/country-combobox.js +47 -0
- package/dist/components/currency-combobox.d.ts +14 -0
- package/dist/components/currency-combobox.d.ts.map +1 -0
- package/dist/components/currency-combobox.js +53 -0
- package/dist/components/dashboard-widgets.d.ts +66 -0
- package/dist/components/dashboard-widgets.d.ts.map +1 -0
- package/dist/components/dashboard-widgets.js +64 -0
- package/dist/components/data-table-column-header.d.ts +9 -0
- package/dist/components/data-table-column-header.d.ts.map +1 -0
- package/dist/components/data-table-column-header.js +12 -0
- package/dist/components/data-table-pagination.d.ts +7 -0
- package/dist/components/data-table-pagination.d.ts.map +1 -0
- package/dist/components/data-table-pagination.js +11 -0
- package/dist/components/data-table.d.ts +22 -0
- package/dist/components/data-table.d.ts.map +1 -0
- package/dist/components/data-table.js +55 -0
- package/dist/components/date-picker.d.ts +38 -0
- package/dist/components/date-picker.d.ts.map +1 -0
- package/dist/components/date-picker.js +120 -0
- package/dist/components/date-time-picker.d.ts +30 -0
- package/dist/components/date-time-picker.d.ts.map +1 -0
- package/dist/components/date-time-picker.js +75 -0
- package/dist/components/dialog.d.ts +18 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.js +37 -0
- package/dist/components/direction.d.ts +2 -0
- package/dist/components/direction.d.ts.map +1 -0
- package/dist/components/direction.js +1 -0
- package/dist/components/drawer.d.ts +14 -0
- package/dist/components/drawer.d.ts.map +1 -0
- package/dist/components/drawer.js +34 -0
- package/dist/components/dropdown-menu.d.ts +30 -0
- package/dist/components/dropdown-menu.d.ts.map +1 -0
- package/dist/components/dropdown-menu.js +50 -0
- package/dist/components/empty.d.ts +12 -0
- package/dist/components/empty.d.ts.map +1 -0
- package/dist/components/empty.js +33 -0
- package/dist/components/field.d.ts +25 -0
- package/dist/components/field.d.ts.map +1 -0
- package/dist/components/field.js +65 -0
- package/dist/components/hover-card.d.ts +6 -0
- package/dist/components/hover-card.d.ts.map +1 -0
- package/dist/components/hover-card.js +13 -0
- package/dist/components/index.d.ts +86 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +85 -0
- package/dist/components/input-group.d.ts +19 -0
- package/dist/components/input-group.d.ts.map +1 -0
- package/dist/components/input-group.js +73 -0
- package/dist/components/input-otp.d.ts +12 -0
- package/dist/components/input-otp.d.ts.map +1 -0
- package/dist/components/input-otp.js +20 -0
- package/dist/components/input.d.ts +4 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +7 -0
- package/dist/components/item.d.ts +23 -0
- package/dist/components/item.d.ts.map +1 -0
- package/dist/components/item.js +78 -0
- package/dist/components/kbd.d.ts +4 -0
- package/dist/components/kbd.d.ts.map +1 -0
- package/dist/components/kbd.js +9 -0
- package/dist/components/label.d.ts +4 -0
- package/dist/components/label.d.ts.map +1 -0
- package/dist/components/label.js +8 -0
- package/dist/components/menubar.d.ts +30 -0
- package/dist/components/menubar.d.ts.map +1 -0
- package/dist/components/menubar.js +56 -0
- package/dist/components/native-select.d.ts +9 -0
- package/dist/components/native-select.d.ts.map +1 -0
- package/dist/components/native-select.js +13 -0
- package/dist/components/navigation-menu.d.ts +12 -0
- package/dist/components/navigation-menu.d.ts.map +1 -0
- package/dist/components/navigation-menu.js +31 -0
- package/dist/components/notification-deliveries-page.d.ts +2 -0
- package/dist/components/notification-deliveries-page.d.ts.map +1 -0
- package/dist/components/notification-deliveries-page.js +22 -0
- package/dist/components/notification-delivery-detail-dialog.d.ts +8 -0
- package/dist/components/notification-delivery-detail-dialog.d.ts.map +1 -0
- package/dist/components/notification-delivery-detail-dialog.js +29 -0
- package/dist/components/notification-reminder-rule-dialog.d.ts +10 -0
- package/dist/components/notification-reminder-rule-dialog.d.ts.map +1 -0
- package/dist/components/notification-reminder-rule-dialog.js +123 -0
- package/dist/components/notification-reminder-rules-page.d.ts +2 -0
- package/dist/components/notification-reminder-rules-page.d.ts.map +1 -0
- package/dist/components/notification-reminder-rules-page.js +35 -0
- package/dist/components/notification-reminder-runs-page.d.ts +2 -0
- package/dist/components/notification-reminder-runs-page.d.ts.map +1 -0
- package/dist/components/notification-reminder-runs-page.js +20 -0
- package/dist/components/notification-template-authoring-help.d.ts +11 -0
- package/dist/components/notification-template-authoring-help.d.ts.map +1 -0
- package/dist/components/notification-template-authoring-help.js +6 -0
- package/dist/components/notification-template-detail-page.d.ts +6 -0
- package/dist/components/notification-template-detail-page.d.ts.map +1 -0
- package/dist/components/notification-template-detail-page.js +145 -0
- package/dist/components/notification-template-dialog.d.ts +10 -0
- package/dist/components/notification-template-dialog.d.ts.map +1 -0
- package/dist/components/notification-template-dialog.js +296 -0
- package/dist/components/notification-templates-page.d.ts +2 -0
- package/dist/components/notification-templates-page.d.ts.map +1 -0
- package/dist/components/notification-templates-page.js +33 -0
- package/dist/components/overview-metric.d.ts +12 -0
- package/dist/components/overview-metric.d.ts.map +1 -0
- package/dist/components/overview-metric.js +6 -0
- package/dist/components/pagination.d.ts +18 -0
- package/dist/components/pagination.d.ts.map +1 -0
- package/dist/components/pagination.js +26 -0
- package/dist/components/popover.d.ts +10 -0
- package/dist/components/popover.d.ts.map +1 -0
- package/dist/components/popover.js +22 -0
- package/dist/components/progress.d.ts +8 -0
- package/dist/components/progress.d.ts.map +1 -0
- package/dist/components/progress.js +19 -0
- package/dist/components/radio-group.d.ts +6 -0
- package/dist/components/radio-group.d.ts.map +1 -0
- package/dist/components/radio-group.js +11 -0
- package/dist/components/resizable.d.ts +8 -0
- package/dist/components/resizable.d.ts.map +1 -0
- package/dist/components/resizable.js +13 -0
- package/dist/components/rich-text-editor.d.ts +13 -0
- package/dist/components/rich-text-editor.d.ts.map +1 -0
- package/dist/components/rich-text-editor.js +71 -0
- package/dist/components/rich-text-variable-extension.d.ts +6 -0
- package/dist/components/rich-text-variable-extension.d.ts.map +1 -0
- package/dist/components/rich-text-variable-extension.js +117 -0
- package/dist/components/scroll-area.d.ts +5 -0
- package/dist/components/scroll-area.d.ts.map +1 -0
- package/dist/components/scroll-area.js +10 -0
- package/dist/components/select.d.ts +16 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.js +33 -0
- package/dist/components/selection-action-bar.d.ts +8 -0
- package/dist/components/selection-action-bar.d.ts.map +1 -0
- package/dist/components/selection-action-bar.js +7 -0
- package/dist/components/separator.d.ts +4 -0
- package/dist/components/separator.d.ts.map +1 -0
- package/dist/components/separator.js +7 -0
- package/dist/components/sheet.d.ts +15 -0
- package/dist/components/sheet.d.ts.map +1 -0
- package/dist/components/sheet.js +37 -0
- package/dist/components/sidebar-core.d.ts +34 -0
- package/dist/components/sidebar-core.d.ts.map +1 -0
- package/dist/components/sidebar-core.js +112 -0
- package/dist/components/sidebar-menu.d.ts +33 -0
- package/dist/components/sidebar-menu.d.ts.map +1 -0
- package/dist/components/sidebar-menu.js +128 -0
- package/dist/components/sidebar.d.ts +3 -0
- package/dist/components/sidebar.d.ts.map +1 -0
- package/dist/components/sidebar.js +2 -0
- package/dist/components/skeleton.d.ts +7 -0
- package/dist/components/skeleton.d.ts.map +1 -0
- package/dist/components/skeleton.js +6 -0
- package/dist/components/slider.d.ts +4 -0
- package/dist/components/slider.d.ts.map +1 -0
- package/dist/components/slider.js +9 -0
- package/dist/components/sonner.d.ts +4 -0
- package/dist/components/sonner.d.ts.map +1 -0
- package/dist/components/sonner.js +24 -0
- package/dist/components/spinner.d.ts +3 -0
- package/dist/components/spinner.d.ts.map +1 -0
- package/dist/components/spinner.js +7 -0
- package/dist/components/switch.d.ts +6 -0
- package/dist/components/switch.d.ts.map +1 -0
- package/dist/components/switch.js +7 -0
- package/dist/components/table.d.ts +11 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.js +27 -0
- package/dist/components/tabs.d.ts +11 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.js +28 -0
- package/dist/components/textarea.d.ts +4 -0
- package/dist/components/textarea.d.ts.map +1 -0
- package/dist/components/textarea.js +6 -0
- package/dist/components/toggle-group.d.ts +11 -0
- package/dist/components/toggle-group.d.ts.map +1 -0
- package/dist/components/toggle-group.js +24 -0
- package/dist/components/toggle.d.ts +9 -0
- package/dist/components/toggle.d.ts.map +1 -0
- package/dist/components/toggle.js +25 -0
- package/dist/components/tooltip.d.ts +7 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.js +16 -0
- package/dist/components/typography.d.ts +18 -0
- package/dist/components/typography.d.ts.map +1 -0
- package/dist/components/typography.js +48 -0
- package/dist/hooks/use-mobile.d.ts +2 -0
- package/dist/hooks/use-mobile.d.ts.map +1 -0
- package/dist/hooks/use-mobile.js +15 -0
- package/dist/lib/crop-image.d.ts +4 -0
- package/dist/lib/crop-image.d.ts.map +1 -0
- package/dist/lib/crop-image.js +30 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +5 -0
- package/dist/lib/zod-resolver.d.ts +4 -0
- package/dist/lib/zod-resolver.d.ts.map +1 -0
- package/dist/lib/zod-resolver.js +39 -0
- package/package.json +108 -0
- package/postcss.config.mjs +6 -0
- package/src/styles/globals.css +157 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useNotificationDeliveries, useNotificationTemplate, useNotificationTemplateAuthoring, useNotificationTemplateTools, } from "@voyantjs/notifications-react";
|
|
4
|
+
import { ArrowLeft, Loader2, Pencil } from "lucide-react";
|
|
5
|
+
import { useMemo, useState } from "react";
|
|
6
|
+
import { toast } from "sonner";
|
|
7
|
+
import { Badge } from "./badge";
|
|
8
|
+
import { Button } from "./button";
|
|
9
|
+
import { Card, CardContent, CardHeader, CardTitle } from "./card";
|
|
10
|
+
import { Label } from "./label";
|
|
11
|
+
import { NotificationDeliveryDetailDialog } from "./notification-delivery-detail-dialog";
|
|
12
|
+
import { NotificationTemplateDialog } from "./notification-template-dialog";
|
|
13
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
|
|
14
|
+
import { Textarea } from "./textarea";
|
|
15
|
+
function parsePath(path) {
|
|
16
|
+
return path
|
|
17
|
+
.replace(/\[(\d+)\]/g, ".$1")
|
|
18
|
+
.split(".")
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
function setDeepValue(target, path, value) {
|
|
22
|
+
const segments = parsePath(path);
|
|
23
|
+
let current = target;
|
|
24
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
25
|
+
const segment = segments[index];
|
|
26
|
+
const isLast = index === segments.length - 1;
|
|
27
|
+
const nextSegment = segments[index + 1];
|
|
28
|
+
const nextIsIndex = nextSegment ? /^\d+$/.test(nextSegment) : false;
|
|
29
|
+
if (Array.isArray(current)) {
|
|
30
|
+
const arrayIndex = Number(segment);
|
|
31
|
+
if (Number.isNaN(arrayIndex))
|
|
32
|
+
return;
|
|
33
|
+
if (isLast) {
|
|
34
|
+
current[arrayIndex] = value;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (current[arrayIndex] == null) {
|
|
38
|
+
current[arrayIndex] = nextIsIndex ? [] : {};
|
|
39
|
+
}
|
|
40
|
+
current = current[arrayIndex];
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (typeof current !== "object" || current == null)
|
|
44
|
+
return;
|
|
45
|
+
const record = current;
|
|
46
|
+
if (isLast) {
|
|
47
|
+
record[segment] = value;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (record[segment] == null) {
|
|
51
|
+
record[segment] = nextIsIndex ? [] : {};
|
|
52
|
+
}
|
|
53
|
+
current = record[segment];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function buildSamplePayload(variableGroups) {
|
|
57
|
+
const sample = {};
|
|
58
|
+
for (const group of variableGroups) {
|
|
59
|
+
for (const variable of group.variables) {
|
|
60
|
+
setDeepValue(sample, variable.key, variable.example);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return sample;
|
|
64
|
+
}
|
|
65
|
+
export function NotificationTemplateDetailPage({ id }) {
|
|
66
|
+
const [editOpen, setEditOpen] = useState(false);
|
|
67
|
+
const [previewDataInput, setPreviewDataInput] = useState("");
|
|
68
|
+
const [selectedDeliveryId, setSelectedDeliveryId] = useState(null);
|
|
69
|
+
const { data: template, isPending, error, refetch } = useNotificationTemplate(id);
|
|
70
|
+
const { variableCatalog } = useNotificationTemplateAuthoring();
|
|
71
|
+
const variableGroups = useMemo(() => variableCatalog.map((group) => ({
|
|
72
|
+
...group,
|
|
73
|
+
variables: group.variables.map((variable) => ({
|
|
74
|
+
...variable,
|
|
75
|
+
example: String(variable.example),
|
|
76
|
+
})),
|
|
77
|
+
})), [variableCatalog]);
|
|
78
|
+
const defaultPreviewData = useMemo(() => JSON.stringify(buildSamplePayload(variableGroups), null, 2), [variableGroups]);
|
|
79
|
+
const { preview } = useNotificationTemplateTools();
|
|
80
|
+
const deliveries = useNotificationDeliveries({
|
|
81
|
+
templateSlug: template?.slug,
|
|
82
|
+
limit: 20,
|
|
83
|
+
offset: 0,
|
|
84
|
+
enabled: Boolean(template?.slug),
|
|
85
|
+
});
|
|
86
|
+
const parsePreviewData = () => {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = previewDataInput.trim() ? JSON.parse(previewDataInput) : {};
|
|
89
|
+
if (typeof parsed !== "object" || parsed == null || Array.isArray(parsed)) {
|
|
90
|
+
throw new Error("Preview data must be a JSON object.");
|
|
91
|
+
}
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
catch (previewError) {
|
|
95
|
+
throw new Error(previewError instanceof Error ? previewError.message : "Preview data is invalid JSON.");
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const handlePreview = async () => {
|
|
99
|
+
if (!template)
|
|
100
|
+
return;
|
|
101
|
+
try {
|
|
102
|
+
const data = parsePreviewData();
|
|
103
|
+
await preview.mutateAsync({
|
|
104
|
+
channel: template.channel,
|
|
105
|
+
provider: null,
|
|
106
|
+
fromAddress: template.fromAddress,
|
|
107
|
+
subjectTemplate: template.subjectTemplate,
|
|
108
|
+
htmlTemplate: template.htmlTemplate,
|
|
109
|
+
textTemplate: template.textTemplate,
|
|
110
|
+
data,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (previewError) {
|
|
114
|
+
toast.error(previewError instanceof Error ? previewError.message : "Preview failed");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
if (isPending) {
|
|
118
|
+
return (_jsx("div", { className: "flex items-center justify-center py-16", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) }));
|
|
119
|
+
}
|
|
120
|
+
if (error || !template) {
|
|
121
|
+
return (_jsxs("div", { className: "flex flex-col gap-4 p-6", children: [_jsxs("a", { href: "/notifications/templates", className: "inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back to templates"] }), _jsx("div", { className: "rounded-md border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive", children: error instanceof Error ? error.message : "Notification template not found." })] }));
|
|
122
|
+
}
|
|
123
|
+
const renderedPreview = preview.data;
|
|
124
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsxs("a", { href: "/notifications/templates", className: "inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back to templates"] }), _jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: template.name }), _jsx("p", { className: "font-mono text-xs text-muted-foreground", children: template.slug })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx(Badge, { variant: "outline", children: template.channel }), _jsx(Badge, { variant: template.status === "active" ? "default" : "secondary", children: template.status })] })] }), _jsxs(Button, { onClick: () => setEditOpen(true), children: [_jsx(Pencil, { className: "mr-2 h-4 w-4" }), "Edit Template"] })] }), _jsxs("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-4", children: [_jsx(MetaCard, { label: "Channel", value: template.channel }), _jsx(MetaCard, { label: "From", value: template.fromAddress ?? "Default sender" }), _jsx(MetaCard, { label: "Updated", value: new Date(template.updatedAt).toLocaleString() })] }), _jsxs(Tabs, { defaultValue: "overview", children: [_jsxs(TabsList, { className: "w-full", children: [_jsx(TabsTrigger, { value: "overview", children: "Overview" }), _jsx(TabsTrigger, { value: "preview", children: "Preview" }), _jsx(TabsTrigger, { value: "deliveries", children: "Recent deliveries" })] }), _jsx(TabsContent, { value: "overview", className: "mt-4 space-y-4", children: _jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Message structure" }) }), _jsxs(CardContent, { className: "space-y-3 text-sm", children: [_jsx(KeyValue, { label: "Subject", value: template.subjectTemplate ?? "—" }), _jsx(KeyValue, { label: "Text fallback", value: template.textTemplate ?? "—" }), _jsx(KeyValue, { label: "Description", value: template.metadata ? JSON.stringify(template.metadata) : "—" })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "HTML body" }) }), _jsx(CardContent, { children: template.htmlTemplate ? (_jsx("div", { className: "prose prose-sm max-w-none rounded-md border bg-background px-4 py-4 dark:prose-invert",
|
|
125
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: Notification template HTML is rendered for preview.
|
|
126
|
+
dangerouslySetInnerHTML: { __html: template.htmlTemplate } })) : (_jsx("div", { className: "rounded-md border bg-muted/20 px-4 py-3 text-sm text-muted-foreground", children: "No HTML body configured." })) })] })] }) }), _jsx(TabsContent, { value: "preview", className: "mt-4", children: _jsxs("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]", children: [_jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Sample data" }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsx(Label, { children: "Render with custom JSON" }), _jsx(Textarea, { value: previewDataInput || defaultPreviewData, onChange: (event) => setPreviewDataInput(event.target.value), rows: 16, className: "font-mono text-xs" }), _jsxs(Button, { type: "button", variant: "outline", onClick: handlePreview, disabled: preview.isPending, children: [preview.isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, "Render Preview"] })] })] }), _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Rendered output" }) }), _jsxs(CardContent, { className: "space-y-3", children: [_jsx(KeyValue, { label: "Subject", value: renderedPreview?.subject ?? "Not rendered yet." }), template.channel === "email" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "HTML body" }), renderedPreview?.html ? (_jsx("div", { className: "prose prose-sm max-w-none rounded-md border bg-background px-4 py-4 dark:prose-invert",
|
|
127
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: Rendered preview HTML is generated server-side for preview.
|
|
128
|
+
dangerouslySetInnerHTML: { __html: renderedPreview.html } })) : (_jsx("div", { className: "rounded-md border bg-muted/20 px-4 py-3 text-sm text-muted-foreground", children: "No rendered HTML yet." }))] }), _jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Text fallback" }), _jsx("pre", { className: "whitespace-pre-wrap rounded-md border bg-muted/20 px-3 py-3 text-xs", children: renderedPreview?.text ?? "No rendered text yet." })] })] })) : (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "SMS body" }), _jsx("pre", { className: "whitespace-pre-wrap rounded-md border bg-muted/20 px-3 py-3 text-xs", children: renderedPreview?.text ?? "No rendered text yet." })] }))] })] })] }) }), _jsx(TabsContent, { value: "deliveries", className: "mt-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { children: "Recent deliveries" }) }), _jsx(CardContent, { children: deliveries.isPending ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : deliveries.data?.data && deliveries.data.data.length > 0 ? (_jsx("div", { className: "rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-left text-xs uppercase tracking-wide text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-4 py-3", children: "Recipient" }), _jsx("th", { className: "px-4 py-3", children: "Provider" }), _jsx("th", { className: "px-4 py-3", children: "Status" }), _jsx("th", { className: "px-4 py-3", children: "Created" }), _jsx("th", { className: "px-4 py-3 text-right", children: "View" })] }) }), _jsx("tbody", { children: deliveries.data.data.map((delivery) => (_jsxs("tr", { className: "border-t", children: [_jsxs("td", { className: "px-4 py-3", children: [_jsx("div", { children: delivery.toAddress }), delivery.subject ? (_jsx("div", { className: "text-xs text-muted-foreground", children: delivery.subject })) : null] }), _jsx("td", { className: "px-4 py-3", children: delivery.provider }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: delivery.status === "sent"
|
|
129
|
+
? "default"
|
|
130
|
+
: delivery.status === "failed"
|
|
131
|
+
? "destructive"
|
|
132
|
+
: "secondary", children: delivery.status }) }), _jsx("td", { className: "px-4 py-3", children: new Date(delivery.createdAt).toLocaleString() }), _jsx("td", { className: "px-4 py-3 text-right", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: () => setSelectedDeliveryId(delivery.id), children: "Inspect" }) })] }, delivery.id))) })] }) })) : (_jsx("div", { className: "rounded-md border border-dashed px-4 py-8 text-center text-sm text-muted-foreground", children: "No deliveries recorded for this template yet." })) })] }) })] }), _jsx(NotificationTemplateDialog, { open: editOpen, onOpenChange: setEditOpen, template: template, onSuccess: () => {
|
|
133
|
+
setEditOpen(false);
|
|
134
|
+
void refetch();
|
|
135
|
+
} }), _jsx(NotificationDeliveryDetailDialog, { deliveryId: selectedDeliveryId, open: Boolean(selectedDeliveryId), onOpenChange: (open) => {
|
|
136
|
+
if (!open)
|
|
137
|
+
setSelectedDeliveryId(null);
|
|
138
|
+
} })] }));
|
|
139
|
+
}
|
|
140
|
+
function MetaCard({ label, value }) {
|
|
141
|
+
return (_jsxs("div", { className: "rounded-md border p-4", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: label }), _jsx("div", { className: "mt-2 text-sm", children: value })] }));
|
|
142
|
+
}
|
|
143
|
+
function KeyValue({ label, value }) {
|
|
144
|
+
return (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: label }), _jsx("div", { className: "break-words text-sm", children: value })] }));
|
|
145
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type NotificationTemplateRecord } from "@voyantjs/notifications-react";
|
|
2
|
+
type NotificationTemplateDialogProps = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onOpenChange: (open: boolean) => void;
|
|
5
|
+
template?: NotificationTemplateRecord;
|
|
6
|
+
onSuccess: () => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function NotificationTemplateDialog({ open, onOpenChange, template, onSuccess, }: NotificationTemplateDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=notification-template-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-template-dialog.d.ts","sourceRoot":"","sources":["../../src/components/notification-template-dialog.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,0BAA0B,EAIhC,MAAM,+BAA+B,CAAA;AAkDtC,KAAK,+BAA+B,GAAG;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,CAAC,EAAE,0BAA0B,CAAA;IACrC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB,CAAA;AAuED,wBAAgB,0BAA0B,CAAC,EACzC,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,SAAS,GACV,EAAE,+BAA+B,2CAghBjC"}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useNotificationTemplateAuthoring, useNotificationTemplateMutation, useNotificationTemplateTools, } from "@voyantjs/notifications-react";
|
|
4
|
+
import { Loader2 } from "lucide-react";
|
|
5
|
+
import { useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { useForm } from "react-hook-form";
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
import { z } from "zod/v4";
|
|
9
|
+
import { zodResolver } from "../lib/zod-resolver";
|
|
10
|
+
import { Button } from "./button";
|
|
11
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "./index";
|
|
12
|
+
import { Input } from "./input";
|
|
13
|
+
import { Label } from "./label";
|
|
14
|
+
import { NotificationTemplateAuthoringHelp } from "./notification-template-authoring-help";
|
|
15
|
+
import { RichTextEditor } from "./rich-text-editor";
|
|
16
|
+
import { insertPlainText, insertVariableToken } from "./rich-text-variable-extension";
|
|
17
|
+
import { ScrollArea } from "./scroll-area";
|
|
18
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
|
|
19
|
+
import { Switch } from "./switch";
|
|
20
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
|
|
21
|
+
import { Textarea } from "./textarea";
|
|
22
|
+
const CHANNEL_ITEMS = [
|
|
23
|
+
{ label: "Email", value: "email" },
|
|
24
|
+
{ label: "SMS", value: "sms" },
|
|
25
|
+
];
|
|
26
|
+
const STATUS_ITEMS = [
|
|
27
|
+
{ label: "Draft", value: "draft" },
|
|
28
|
+
{ label: "Active", value: "active" },
|
|
29
|
+
{ label: "Archived", value: "archived" },
|
|
30
|
+
];
|
|
31
|
+
const templateFormSchema = z.object({
|
|
32
|
+
name: z.string().min(1, "Name is required"),
|
|
33
|
+
slug: z
|
|
34
|
+
.string()
|
|
35
|
+
.min(1, "Slug is required")
|
|
36
|
+
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Must be kebab-case"),
|
|
37
|
+
channel: z.enum(["email", "sms"]),
|
|
38
|
+
status: z.enum(["draft", "active", "archived"]).default("draft"),
|
|
39
|
+
subjectTemplate: z.string().optional(),
|
|
40
|
+
htmlTemplate: z.string().optional(),
|
|
41
|
+
textTemplate: z.string().optional(),
|
|
42
|
+
fromAddress: z.string().optional(),
|
|
43
|
+
active: z.boolean(),
|
|
44
|
+
});
|
|
45
|
+
function parsePath(path) {
|
|
46
|
+
return path
|
|
47
|
+
.replace(/\[(\d+)\]/g, ".$1")
|
|
48
|
+
.split(".")
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
function setDeepValue(target, path, value) {
|
|
52
|
+
const segments = parsePath(path);
|
|
53
|
+
let current = target;
|
|
54
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
55
|
+
const segment = segments[index];
|
|
56
|
+
const isLast = index === segments.length - 1;
|
|
57
|
+
const nextSegment = segments[index + 1];
|
|
58
|
+
const nextIsIndex = nextSegment ? /^\d+$/.test(nextSegment) : false;
|
|
59
|
+
if (Array.isArray(current)) {
|
|
60
|
+
const arrayIndex = Number(segment);
|
|
61
|
+
if (Number.isNaN(arrayIndex))
|
|
62
|
+
return;
|
|
63
|
+
if (isLast) {
|
|
64
|
+
current[arrayIndex] = value;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (current[arrayIndex] == null) {
|
|
68
|
+
current[arrayIndex] = nextIsIndex ? [] : {};
|
|
69
|
+
}
|
|
70
|
+
current = current[arrayIndex];
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (typeof current !== "object" || current == null)
|
|
74
|
+
return;
|
|
75
|
+
const record = current;
|
|
76
|
+
if (isLast) {
|
|
77
|
+
record[segment] = value;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (record[segment] == null) {
|
|
81
|
+
record[segment] = nextIsIndex ? [] : {};
|
|
82
|
+
}
|
|
83
|
+
current = record[segment];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function buildSamplePayload(variableGroups) {
|
|
87
|
+
const sample = {};
|
|
88
|
+
for (const group of variableGroups) {
|
|
89
|
+
for (const variable of group.variables) {
|
|
90
|
+
setDeepValue(sample, variable.key, variable.example);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return sample;
|
|
94
|
+
}
|
|
95
|
+
function appendTemplateValue(current, addition) {
|
|
96
|
+
if (!current?.trim())
|
|
97
|
+
return addition;
|
|
98
|
+
return `${current}${current.endsWith("\n") ? "" : "\n"}${addition}`;
|
|
99
|
+
}
|
|
100
|
+
function variableReference(key) {
|
|
101
|
+
return `{{ ${key} }}`;
|
|
102
|
+
}
|
|
103
|
+
export function NotificationTemplateDialog({ open, onOpenChange, template, onSuccess, }) {
|
|
104
|
+
const isEditing = Boolean(template);
|
|
105
|
+
const { create, update } = useNotificationTemplateMutation();
|
|
106
|
+
const { preview, testSend } = useNotificationTemplateTools();
|
|
107
|
+
const { variableCatalog, liquidSnippets } = useNotificationTemplateAuthoring();
|
|
108
|
+
const [editorInstance, setEditorInstance] = useState(null);
|
|
109
|
+
const [insertionTarget, setInsertionTarget] = useState("body");
|
|
110
|
+
const [previewDataInput, setPreviewDataInput] = useState("{}");
|
|
111
|
+
const [testRecipient, setTestRecipient] = useState("");
|
|
112
|
+
const variableGroups = useMemo(() => variableCatalog.map((group) => ({
|
|
113
|
+
...group,
|
|
114
|
+
variables: group.variables.map((variable) => ({
|
|
115
|
+
...variable,
|
|
116
|
+
example: String(variable.example),
|
|
117
|
+
})),
|
|
118
|
+
})), [variableCatalog]);
|
|
119
|
+
const defaultPreviewData = useMemo(() => JSON.stringify(buildSamplePayload(variableGroups), null, 2), [variableGroups]);
|
|
120
|
+
const form = useForm({
|
|
121
|
+
resolver: zodResolver(templateFormSchema),
|
|
122
|
+
defaultValues: {
|
|
123
|
+
name: "",
|
|
124
|
+
slug: "",
|
|
125
|
+
channel: "email",
|
|
126
|
+
status: "draft",
|
|
127
|
+
subjectTemplate: "",
|
|
128
|
+
htmlTemplate: "",
|
|
129
|
+
textTemplate: "",
|
|
130
|
+
fromAddress: "",
|
|
131
|
+
active: true,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
const channel = form.watch("channel");
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (open && template) {
|
|
137
|
+
form.reset({
|
|
138
|
+
name: template.name,
|
|
139
|
+
slug: template.slug,
|
|
140
|
+
channel: template.channel,
|
|
141
|
+
status: template.status,
|
|
142
|
+
subjectTemplate: template.subjectTemplate ?? "",
|
|
143
|
+
htmlTemplate: template.htmlTemplate ?? "",
|
|
144
|
+
textTemplate: template.textTemplate ?? "",
|
|
145
|
+
fromAddress: template.fromAddress ?? "",
|
|
146
|
+
active: template.status === "active",
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (open) {
|
|
151
|
+
form.reset();
|
|
152
|
+
}
|
|
153
|
+
}, [open, template, form]);
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!open)
|
|
156
|
+
return;
|
|
157
|
+
setInsertionTarget((current) => channel === "sms" ? "text" : current === "text" ? "body" : current);
|
|
158
|
+
}, [channel, open]);
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!open)
|
|
161
|
+
return;
|
|
162
|
+
setPreviewDataInput(defaultPreviewData);
|
|
163
|
+
setTestRecipient("");
|
|
164
|
+
preview.reset();
|
|
165
|
+
testSend.reset();
|
|
166
|
+
}, [defaultPreviewData, open, preview, testSend]);
|
|
167
|
+
const onSubmit = async (values) => {
|
|
168
|
+
const payload = {
|
|
169
|
+
name: values.name,
|
|
170
|
+
slug: values.slug,
|
|
171
|
+
channel: values.channel,
|
|
172
|
+
provider: null,
|
|
173
|
+
status: values.active ? (values.status === "archived" ? "active" : values.status) : "draft",
|
|
174
|
+
subjectTemplate: values.channel === "email" ? values.subjectTemplate || null : null,
|
|
175
|
+
htmlTemplate: values.channel === "email" ? values.htmlTemplate || null : null,
|
|
176
|
+
textTemplate: values.textTemplate || null,
|
|
177
|
+
fromAddress: values.channel === "email" ? values.fromAddress || null : null,
|
|
178
|
+
isSystem: template?.isSystem ?? false,
|
|
179
|
+
metadata: template?.metadata ?? null,
|
|
180
|
+
};
|
|
181
|
+
if (isEditing && template) {
|
|
182
|
+
await update.mutateAsync({ id: template.id, input: payload });
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
await create.mutateAsync(payload);
|
|
186
|
+
}
|
|
187
|
+
onSuccess();
|
|
188
|
+
};
|
|
189
|
+
const isPending = create.isPending || update.isPending;
|
|
190
|
+
const parsePreviewData = () => {
|
|
191
|
+
try {
|
|
192
|
+
const parsed = previewDataInput.trim() ? JSON.parse(previewDataInput) : {};
|
|
193
|
+
if (typeof parsed !== "object" || parsed == null || Array.isArray(parsed)) {
|
|
194
|
+
throw new Error("Preview data must be a JSON object.");
|
|
195
|
+
}
|
|
196
|
+
return parsed;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
throw new Error(error instanceof Error ? error.message : "Preview data is invalid JSON.");
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const insertIntoTarget = (content, kind) => {
|
|
203
|
+
if (insertionTarget === "body" && channel === "email" && editorInstance) {
|
|
204
|
+
if (kind === "variable") {
|
|
205
|
+
insertVariableToken(editorInstance, content);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
insertPlainText(editorInstance, content);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const fieldName = insertionTarget === "subject" ? "subjectTemplate" : "textTemplate";
|
|
213
|
+
const current = form.getValues(fieldName) ?? "";
|
|
214
|
+
const nextValue = kind === "variable"
|
|
215
|
+
? `${current}${current ? " " : ""}${variableReference(content)}`
|
|
216
|
+
: appendTemplateValue(current, content);
|
|
217
|
+
form.setValue(fieldName, nextValue, {
|
|
218
|
+
shouldDirty: true,
|
|
219
|
+
shouldTouch: true,
|
|
220
|
+
shouldValidate: true,
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
const handlePreview = async () => {
|
|
224
|
+
try {
|
|
225
|
+
const data = parsePreviewData();
|
|
226
|
+
await preview.mutateAsync({
|
|
227
|
+
channel,
|
|
228
|
+
provider: null,
|
|
229
|
+
fromAddress: channel === "email" ? form.getValues("fromAddress") || null : null,
|
|
230
|
+
subjectTemplate: channel === "email" ? form.getValues("subjectTemplate") || null : null,
|
|
231
|
+
htmlTemplate: channel === "email" ? form.getValues("htmlTemplate") || null : null,
|
|
232
|
+
textTemplate: form.getValues("textTemplate") || null,
|
|
233
|
+
data,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
toast.error(error instanceof Error ? error.message : "Preview failed");
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const handleTestSend = async () => {
|
|
241
|
+
if (!testRecipient.trim()) {
|
|
242
|
+
toast.error(channel === "email" ? "Recipient email is required." : "Recipient phone is required.");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const data = parsePreviewData();
|
|
247
|
+
await testSend.mutateAsync({
|
|
248
|
+
to: testRecipient.trim(),
|
|
249
|
+
channel,
|
|
250
|
+
provider: null,
|
|
251
|
+
from: channel === "email" ? form.getValues("fromAddress") || null : null,
|
|
252
|
+
subject: channel === "email" ? form.getValues("subjectTemplate") || null : null,
|
|
253
|
+
html: channel === "email" ? form.getValues("htmlTemplate") || null : null,
|
|
254
|
+
text: form.getValues("textTemplate") || null,
|
|
255
|
+
data,
|
|
256
|
+
targetType: "other",
|
|
257
|
+
});
|
|
258
|
+
toast.success(`Test ${channel === "email" ? "email" : "SMS"} queued successfully.`);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
toast.error(error instanceof Error ? error.message : "Test send failed");
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsx(DialogContent, { size: "xl", className: "h-[calc(100vh-2rem)]", children: _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "grid min-h-0 flex-1 grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? "Edit Notification Template" : "New Notification Template" }) }), _jsx(ScrollArea, { className: "min-h-0 flex-1", children: _jsxs("div", { className: "grid gap-4 py-4 pr-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Name" }), _jsx(Input, { ...form.register("name"), placeholder: "Booking confirmation" }), form.formState.errors.name ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.name.message })) : null] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Slug" }), _jsx(Input, { ...form.register("slug"), placeholder: "booking-confirmation" }), form.formState.errors.slug ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.slug.message })) : null] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Channel" }), _jsxs(Select, { items: CHANNEL_ITEMS, value: form.watch("channel"), onValueChange: (value) => {
|
|
265
|
+
if (!value)
|
|
266
|
+
return;
|
|
267
|
+
form.setValue("channel", value);
|
|
268
|
+
}, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: CHANNEL_ITEMS.map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Status" }), _jsxs(Select, { items: STATUS_ITEMS, value: form.watch("status"), onValueChange: (value) => {
|
|
269
|
+
if (!value)
|
|
270
|
+
return;
|
|
271
|
+
form.setValue("status", value);
|
|
272
|
+
}, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: STATUS_ITEMS.map((item) => (_jsx(SelectItem, { value: item.value, children: item.label }, item.value))) })] })] })] }), channel === "email" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "From address" }), _jsx(Input, { ...form.register("fromAddress"), placeholder: "reservations@example.com" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Subject" }), _jsx(Input, { ...form.register("subjectTemplate"), placeholder: "Your booking {{ booking.reference }}" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "HTML body" }), _jsx(RichTextEditor, { value: form.watch("htmlTemplate") ?? "", onChange: (value) => form.setValue("htmlTemplate", value, {
|
|
273
|
+
shouldDirty: true,
|
|
274
|
+
shouldTouch: true,
|
|
275
|
+
shouldValidate: true,
|
|
276
|
+
}), placeholder: "Compose the email body using Liquid variables...", enableVariables: true, onEditorReady: setEditorInstance })] })] })) : null, _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: channel === "sms" ? "SMS body" : "Plain-text fallback" }), _jsx(Textarea, { ...form.register("textTemplate"), placeholder: channel === "sms"
|
|
277
|
+
? 'Hi {{ traveler.firstName | default: "traveler" }}, your booking is confirmed.'
|
|
278
|
+
: "Optional plain-text version for email clients.", rows: 6, className: "font-mono text-xs" })] }), _jsxs(Tabs, { defaultValue: "authoring", children: [_jsxs(TabsList, { className: "w-full", children: [_jsx(TabsTrigger, { value: "authoring", children: "Authoring" }), _jsx(TabsTrigger, { value: "preview", children: "Preview & Test" })] }), _jsxs(TabsContent, { value: "authoring", className: "mt-4 space-y-4", children: [_jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-[180px_1fr] sm:items-center", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: "Insert into" }), _jsxs(Select, { items: [
|
|
279
|
+
...(channel === "email"
|
|
280
|
+
? [
|
|
281
|
+
{ label: "Subject", value: "subject" },
|
|
282
|
+
{ label: "HTML body", value: "body" },
|
|
283
|
+
]
|
|
284
|
+
: []),
|
|
285
|
+
{
|
|
286
|
+
label: channel === "sms" ? "SMS body" : "Plain-text fallback",
|
|
287
|
+
value: "text",
|
|
288
|
+
},
|
|
289
|
+
], value: insertionTarget, onValueChange: (value) => {
|
|
290
|
+
if (!value)
|
|
291
|
+
return;
|
|
292
|
+
setInsertionTarget(value);
|
|
293
|
+
}, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [channel === "email" ? (_jsx(SelectItem, { value: "subject", children: "Subject" })) : null, channel === "email" ? (_jsx(SelectItem, { value: "body", children: "HTML body" })) : null, _jsx(SelectItem, { value: "text", children: channel === "sms" ? "SMS body" : "Plain-text fallback" })] })] })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Variables insert as Liquid tags in text fields and as inline chips in the rich-text HTML body." })] }), _jsx(NotificationTemplateAuthoringHelp, { variableGroups: variableGroups, snippets: liquidSnippets, onInsertVariable: (variable) => insertIntoTarget(variable.key, "variable"), onInsertSnippet: (snippet) => insertIntoTarget(snippet.code, "snippet") })] }), _jsx(TabsContent, { value: "preview", className: "mt-4 space-y-4", children: _jsxs("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_320px]", children: [_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Preview data (JSON)" }), _jsx(Textarea, { value: previewDataInput, onChange: (event) => setPreviewDataInput(event.target.value), rows: 14, className: "font-mono text-xs", placeholder: '{"booking":{"reference":"BKG-2026-00125"}}' }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Use sample JSON to preview Liquid rendering and send a safe test message." })] }), _jsx("div", { className: "flex gap-2", children: _jsxs(Button, { type: "button", variant: "outline", onClick: handlePreview, disabled: preview.isPending, children: [preview.isPending ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : null, "Refresh Preview"] }) }), _jsxs("div", { className: "space-y-3 rounded-md border p-4", children: [_jsx("div", { className: "text-sm font-medium", children: "Rendered preview" }), channel === "email" ? (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Subject" }), _jsx("div", { className: "rounded-md border bg-muted/20 px-3 py-2 text-sm", children: preview.data?.subject || "No subject rendered yet." })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "HTML body" }), _jsx("div", { className: "rounded-md border bg-background", children: preview.data?.html ? (_jsx("div", { className: "prose prose-sm max-w-none px-3 py-3 dark:prose-invert",
|
|
294
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: Preview HTML is generated server-side for template preview.
|
|
295
|
+
dangerouslySetInnerHTML: { __html: preview.data.html } })) : (_jsx("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: "No HTML content rendered yet." })) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "Plain-text fallback" }), _jsx("pre", { className: "whitespace-pre-wrap rounded-md border bg-muted/20 px-3 py-3 text-xs", children: preview.data?.text || "No plain-text content rendered yet." })] })] })) : (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: "SMS body" }), _jsx("pre", { className: "whitespace-pre-wrap rounded-md border bg-muted/20 px-3 py-3 text-xs", children: preview.data?.text || "No SMS content rendered yet." })] }))] })] }), _jsxs("div", { className: "space-y-4 rounded-md border p-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-sm font-medium", children: "Test send" }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Sends the current unsaved content through the configured provider path." })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: channel === "email" ? "Recipient email" : "Recipient phone" }), _jsx(Input, { value: testRecipient, onChange: (event) => setTestRecipient(event.target.value), placeholder: channel === "email" ? "qa@example.com" : "+40 721 111 222" })] }), _jsxs("div", { className: "space-y-1 text-xs text-muted-foreground", children: [_jsx("div", { children: "Provider is selected automatically by the app runtime." }), channel === "email" ? (_jsxs("div", { children: ["From: ", form.watch("fromAddress") || "Default sender"] })) : null] }), _jsxs(Button, { type: "button", className: "w-full", onClick: handleTestSend, disabled: testSend.isPending, children: [testSend.isPending ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : null, "Send Test ", channel === "email" ? "Email" : "SMS"] }), testSend.data ? (_jsxs("div", { className: "rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-200", children: ["Delivery queued with status ", _jsx("strong", { children: testSend.data.status }), testSend.data.provider ? ` via ${testSend.data.provider}` : "", "."] })) : null] })] }) })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { checked: form.watch("active"), onCheckedChange: (checked) => form.setValue("active", checked) }), _jsx(Label, { className: "cursor-pointer", children: "Mark template active after saving" })] })] }) }), _jsxs(DialogFooter, { className: "mt-0", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: "Cancel" }), _jsxs(Button, { type: "submit", disabled: isPending, children: [isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? "Save Changes" : "Create Template"] })] })] }) }) }));
|
|
296
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-templates-page.d.ts","sourceRoot":"","sources":["../../src/components/notification-templates-page.tsx"],"names":[],"mappings":"AAgBA,wBAAgB,yBAAyB,4CAgJxC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useNotificationTemplates, } from "@voyantjs/notifications-react";
|
|
4
|
+
import { Loader2, Pencil, Plus, Search } from "lucide-react";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { Badge } from "./badge";
|
|
7
|
+
import { Button } from "./button";
|
|
8
|
+
import { Input } from "./input";
|
|
9
|
+
import { NotificationTemplateDialog } from "./notification-template-dialog";
|
|
10
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
|
|
11
|
+
export function NotificationTemplatesPage() {
|
|
12
|
+
const [search, setSearch] = useState("");
|
|
13
|
+
const [channel, setChannel] = useState("all");
|
|
14
|
+
const [status, setStatus] = useState("all");
|
|
15
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
16
|
+
const [editing, setEditing] = useState();
|
|
17
|
+
const { data, isPending, refetch } = useNotificationTemplates({
|
|
18
|
+
search,
|
|
19
|
+
channel: channel === "all" ? undefined : channel,
|
|
20
|
+
status: status === "all" ? undefined : status,
|
|
21
|
+
});
|
|
22
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: "Notification Templates" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Manage email and SMS templates rendered with Liquid." })] }), _jsxs(Button, { onClick: () => {
|
|
23
|
+
setEditing(undefined);
|
|
24
|
+
setDialogOpen(true);
|
|
25
|
+
}, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), "New Template"] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "relative max-w-sm flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: "Search templates...", value: search, onChange: (event) => setSearch(event.target.value), className: "pl-9" })] }), _jsxs(Select, { value: channel, onValueChange: (value) => setChannel(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Channel" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All channels" }), _jsx(SelectItem, { value: "email", children: "Email" }), _jsx(SelectItem, { value: "sms", children: "SMS" })] })] }), _jsxs(Select, { value: status, onValueChange: (value) => setStatus(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: "Status" }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: "All statuses" }), _jsx(SelectItem, { value: "draft", children: "Draft" }), _jsx(SelectItem, { value: "active", children: "Active" }), _jsx(SelectItem, { value: "archived", children: "Archived" })] })] })] }), isPending ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : null, !isPending && (!data?.data || data.data.length === 0) ? (_jsx("div", { className: "rounded-md border border-dashed p-8 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "No notification templates yet. Create one to start sending branded emails and SMS messages." }) })) : null, !isPending && data?.data && data.data.length > 0 ? (_jsx("div", { className: "rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-left text-xs uppercase tracking-wide text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-4 py-3", children: "Template" }), _jsx("th", { className: "px-4 py-3", children: "Channel" }), _jsx("th", { className: "px-4 py-3", children: "Status" }), _jsx("th", { className: "px-4 py-3", children: "Updated" }), _jsx("th", { className: "px-4 py-3 text-right", children: "Actions" })] }) }), _jsx("tbody", { children: data.data.map((template) => (_jsxs("tr", { className: "border-t", children: [_jsx("td", { className: "px-4 py-3", children: _jsxs("a", { href: `/notifications/templates/${template.id}`, className: "block rounded-sm outline-none transition-colors hover:text-primary focus-visible:text-primary", children: [_jsx("div", { className: "font-medium", children: template.name }), _jsx("div", { className: "font-mono text-xs text-muted-foreground", children: template.slug })] }) }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: "outline", children: template.channel }) }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: template.status === "active" ? "default" : "secondary", children: template.status }) }), _jsx("td", { className: "px-4 py-3", children: new Date(template.updatedAt).toLocaleString() }), _jsx("td", { className: "px-4 py-3 text-right", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
|
|
26
|
+
setEditing(template);
|
|
27
|
+
setDialogOpen(true);
|
|
28
|
+
}, children: _jsx(Pencil, { className: "h-4 w-4" }) }) })] }, template.id))) })] }) })) : null, _jsx(NotificationTemplateDialog, { open: dialogOpen, onOpenChange: setDialogOpen, template: editing, onSuccess: () => {
|
|
29
|
+
setDialogOpen(false);
|
|
30
|
+
setEditing(undefined);
|
|
31
|
+
void refetch();
|
|
32
|
+
} })] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
type IconComponent = React.ComponentType<{
|
|
3
|
+
className?: string;
|
|
4
|
+
}>;
|
|
5
|
+
declare function OverviewMetric({ title, value, description, icon: Icon, }: {
|
|
6
|
+
title: string;
|
|
7
|
+
value: string | number;
|
|
8
|
+
description: string;
|
|
9
|
+
icon: IconComponent;
|
|
10
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export { OverviewMetric };
|
|
12
|
+
//# sourceMappingURL=overview-metric.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overview-metric.d.ts","sourceRoot":"","sources":["../../src/components/overview-metric.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAGnC,KAAK,aAAa,GAAG,KAAK,CAAC,aAAa,CAAC;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAEhE,iBAAS,cAAc,CAAC,EACtB,KAAK,EACL,KAAK,EACL,WAAW,EACX,IAAI,EAAE,IAAI,GACX,EAAE;IACD,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,aAAa,CAAA;CACpB,2CAaA;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Card, CardContent, CardHeader, CardTitle } from "./card";
|
|
3
|
+
function OverviewMetric({ title, value, description, icon: Icon, }) {
|
|
4
|
+
return (_jsxs(Card, { size: "sm", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between gap-3", children: [_jsx(CardTitle, { className: "text-sm text-muted-foreground", children: title }), _jsx(Icon, { className: "h-4 w-4 text-muted-foreground" })] }), _jsxs(CardContent, { className: "space-y-1", children: [_jsx("div", { className: "text-2xl font-semibold tracking-tight", children: value }), _jsx("p", { className: "text-xs text-muted-foreground", children: description })] })] }));
|
|
5
|
+
}
|
|
6
|
+
export { OverviewMetric };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import { Button } from "./button";
|
|
3
|
+
declare function Pagination({ className, ...props }: React.ComponentProps<"nav">): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
declare function PaginationContent({ className, ...props }: React.ComponentProps<"ul">): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
declare function PaginationItem({ ...props }: React.ComponentProps<"li">): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
type PaginationLinkProps = {
|
|
7
|
+
isActive?: boolean;
|
|
8
|
+
} & Pick<React.ComponentProps<typeof Button>, "size"> & React.ComponentProps<"a">;
|
|
9
|
+
declare function PaginationLink({ className, isActive, size, ...props }: PaginationLinkProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
declare function PaginationPrevious({ className, text, ...props }: React.ComponentProps<typeof PaginationLink> & {
|
|
11
|
+
text?: string;
|
|
12
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
declare function PaginationNext({ className, text, ...props }: React.ComponentProps<typeof PaginationLink> & {
|
|
14
|
+
text?: string;
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
declare function PaginationEllipsis({ className, ...props }: React.ComponentProps<"span">): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, };
|
|
18
|
+
//# sourceMappingURL=pagination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pagination.d.ts","sourceRoot":"","sources":["../../src/components/pagination.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAA;AAEnC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAEjC,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,2CASvE;AAED,iBAAS,iBAAiB,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,2CAQ7E;AAED,iBAAS,cAAc,CAAC,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,2CAE/D;AAED,KAAK,mBAAmB,GAAG;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,MAAM,CAAC,EAAE,MAAM,CAAC,GACnD,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,CAAA;AAE3B,iBAAS,cAAc,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAa,EAAE,GAAG,KAAK,EAAE,EAAE,mBAAmB,2CAiB5F;AAED,iBAAS,kBAAkB,CAAC,EAC1B,SAAS,EACT,IAAiB,EACjB,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,cAAc,CAAC,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,2CAYjE;AAED,iBAAS,cAAc,CAAC,EACtB,SAAS,EACT,IAAa,EACb,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,cAAc,CAAC,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,2CAYjE;AAED,iBAAS,kBAAkB,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,2CAehF;AAED,OAAO,EACL,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,cAAc,EACd,kBAAkB,GACnB,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
function Pagination({ className, ...props }) {
|
|
6
|
+
return (_jsx("nav", { "aria-label": "pagination", "data-slot": "pagination", className: cn("mx-auto flex w-full justify-center", className), ...props }));
|
|
7
|
+
}
|
|
8
|
+
function PaginationContent({ className, ...props }) {
|
|
9
|
+
return (_jsx("ul", { "data-slot": "pagination-content", className: cn("flex items-center gap-1", className), ...props }));
|
|
10
|
+
}
|
|
11
|
+
function PaginationItem({ ...props }) {
|
|
12
|
+
return _jsx("li", { "data-slot": "pagination-item", ...props });
|
|
13
|
+
}
|
|
14
|
+
function PaginationLink({ className, isActive, size = "icon", ...props }) {
|
|
15
|
+
return (_jsx(Button, { variant: isActive ? "outline" : "ghost", size: size, className: cn(className), nativeButton: false, render: _jsx("a", { "aria-current": isActive ? "page" : undefined, "data-slot": "pagination-link", "data-active": isActive, ...props }) }));
|
|
16
|
+
}
|
|
17
|
+
function PaginationPrevious({ className, text = "Previous", ...props }) {
|
|
18
|
+
return (_jsxs(PaginationLink, { "aria-label": "Go to previous page", size: "default", className: cn("pl-2!", className), ...props, children: [_jsx(ChevronLeftIcon, { "data-icon": "inline-start" }), _jsx("span", { className: "hidden sm:block", children: text })] }));
|
|
19
|
+
}
|
|
20
|
+
function PaginationNext({ className, text = "Next", ...props }) {
|
|
21
|
+
return (_jsxs(PaginationLink, { "aria-label": "Go to next page", size: "default", className: cn("pr-2!", className), ...props, children: [_jsx("span", { className: "hidden sm:block", children: text }), _jsx(ChevronRightIcon, { "data-icon": "inline-end" })] }));
|
|
22
|
+
}
|
|
23
|
+
function PaginationEllipsis({ className, ...props }) {
|
|
24
|
+
return (_jsxs("span", { "aria-hidden": true, "data-slot": "pagination-ellipsis", className: cn("flex size-9 items-center justify-center [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [_jsx(MoreHorizontalIcon, {}), _jsx("span", { className: "sr-only", children: "More pages" })] }));
|
|
25
|
+
}
|
|
26
|
+
export { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
declare function Popover({ ...props }: PopoverPrimitive.Root.Props): import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
declare function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
declare function PopoverContent({ className, align, alignOffset, side, sideOffset, ...props }: PopoverPrimitive.Popup.Props & Pick<PopoverPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
declare function PopoverHeader({ className, ...props }: React.ComponentProps<"div">): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
declare function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
declare function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger };
|
|
10
|
+
//# sourceMappingURL=popover.d.ts.map
|