@better-zap/react 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +3 -0
- package/dist/index.cjs +614 -0
- package/dist/index.d.cts +146 -0
- package/dist/index.d.mts +146 -0
- package/dist/index.mjs +578 -0
- package/dist/tailwind.css +11 -0
- package/dist/wpp-bg.webp +0 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Better Zap
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let react = require("react");
|
|
25
|
+
react = __toESM(react);
|
|
26
|
+
let clsx = require("clsx");
|
|
27
|
+
let tailwind_merge = require("tailwind-merge");
|
|
28
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
29
|
+
let _hugeicons_react = require("@hugeicons/react");
|
|
30
|
+
let _hugeicons_core_free_icons = require("@hugeicons/core-free-icons");
|
|
31
|
+
let class_variance_authority = require("class-variance-authority");
|
|
32
|
+
//#region src/react/utils.ts
|
|
33
|
+
function cn(...inputs) {
|
|
34
|
+
return (0, tailwind_merge.twMerge)((0, clsx.clsx)(inputs));
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/react/whatsapp-dashboard.tsx
|
|
38
|
+
const MOBILE_BREAKPOINT = 1024;
|
|
39
|
+
function useIsMobile() {
|
|
40
|
+
const [isMobile, setIsMobile] = (0, react.useState)(false);
|
|
41
|
+
(0, react.useEffect)(() => {
|
|
42
|
+
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
43
|
+
const onChange = (e) => {
|
|
44
|
+
setIsMobile(e.matches);
|
|
45
|
+
};
|
|
46
|
+
onChange(mql);
|
|
47
|
+
mql.addEventListener("change", onChange);
|
|
48
|
+
return () => mql.removeEventListener("change", onChange);
|
|
49
|
+
}, []);
|
|
50
|
+
return isMobile;
|
|
51
|
+
}
|
|
52
|
+
const WhatsappDashboardContext = (0, react.createContext)(null);
|
|
53
|
+
function useWhatsappDashboard() {
|
|
54
|
+
const ctx = (0, react.useContext)(WhatsappDashboardContext);
|
|
55
|
+
if (!ctx) throw new Error("useWhatsappDashboard must be used within <WhatsappDashboard>");
|
|
56
|
+
return ctx;
|
|
57
|
+
}
|
|
58
|
+
function WhatsappDashboard({ children, className, defaultMobileView = "list", ...props }) {
|
|
59
|
+
const isMobile = useIsMobile();
|
|
60
|
+
const [mobileView, setMobileView] = (0, react.useState)(defaultMobileView);
|
|
61
|
+
const handleSetMobileView = (0, react.useCallback)((view) => {
|
|
62
|
+
setMobileView(view);
|
|
63
|
+
}, []);
|
|
64
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WhatsappDashboardContext.Provider, {
|
|
65
|
+
value: {
|
|
66
|
+
isMobile,
|
|
67
|
+
mobileView,
|
|
68
|
+
setMobileView: handleSetMobileView
|
|
69
|
+
},
|
|
70
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
71
|
+
className: cn("bg-background flex h-full w-full overflow-hidden", className),
|
|
72
|
+
...props,
|
|
73
|
+
children
|
|
74
|
+
})
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/react/message-bubble.tsx
|
|
79
|
+
const bubbleVariants = (0, class_variance_authority.cva)("relative shadow-[0_1px_0.5px_rgba(11,20,26,0.13)] rounded-lg px-3 py-2 max-w-[65%]", { variants: { variant: {
|
|
80
|
+
outgoing: "bg-green-100 text-green-900 rounded-tr-none",
|
|
81
|
+
incoming: "bg-gray-100 text-gray-900 rounded-tl-none",
|
|
82
|
+
failed: "bg-red-100 text-red-900 rounded-tr-none border border-red-200"
|
|
83
|
+
} } });
|
|
84
|
+
const statusVariants = (0, class_variance_authority.cva)("text-[13px] leading-none", {
|
|
85
|
+
variants: { variant: {
|
|
86
|
+
default: "opacity-60",
|
|
87
|
+
read: "text-blue-500",
|
|
88
|
+
failed: "text-red-500"
|
|
89
|
+
} },
|
|
90
|
+
defaultVariants: { variant: "default" }
|
|
91
|
+
});
|
|
92
|
+
function MessageBubble({ content, sender, timestamp, status, templateName, label, className, ...props }) {
|
|
93
|
+
const isIncoming = sender === "user";
|
|
94
|
+
const isFailed = status === "failed";
|
|
95
|
+
const bubbleVariant = isIncoming ? "incoming" : isFailed ? "failed" : "outgoing";
|
|
96
|
+
const statusVariant = status === "read" ? "read" : isFailed ? "failed" : "default";
|
|
97
|
+
const displayContent = content || (templateName ? `[Template: ${templateName}]` : "[Conteúdo não disponível]");
|
|
98
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
99
|
+
className: cn("flex w-full mb-1", isIncoming ? "justify-start" : "justify-end", className),
|
|
100
|
+
...props,
|
|
101
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
102
|
+
className: bubbleVariants({ variant: bubbleVariant }),
|
|
103
|
+
children: [
|
|
104
|
+
label && !isIncoming && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
105
|
+
className: "mb-1 block text-xs font-medium opacity-70",
|
|
106
|
+
children: label
|
|
107
|
+
}),
|
|
108
|
+
templateName && !label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
109
|
+
className: "mb-1 border-b border-black/5 pb-1",
|
|
110
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
111
|
+
className: "text-[11px] font-medium opacity-70",
|
|
112
|
+
children: ["📋 ", templateName]
|
|
113
|
+
})
|
|
114
|
+
}),
|
|
115
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
116
|
+
className: "text-[14.5px] leading-normal whitespace-pre-wrap break-words select-text",
|
|
117
|
+
children: [
|
|
118
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormattedMessage, { text: displayContent }),
|
|
119
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
120
|
+
className: "float-right -mb-1 ml-2 mt-1.5 flex items-center justify-end gap-1 shrink-0 h-[15px] select-none",
|
|
121
|
+
children: [timestamp && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
122
|
+
className: "text-[11px] opacity-60 leading-none whitespace-nowrap",
|
|
123
|
+
children: timestamp
|
|
124
|
+
}), !isIncoming && status && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
125
|
+
className: statusVariants({ variant: statusVariant }),
|
|
126
|
+
children: status === "read" || status === "delivered" ? "✓✓" : isFailed ? "✕" : "✓"
|
|
127
|
+
})]
|
|
128
|
+
}),
|
|
129
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "clear-both" })
|
|
130
|
+
]
|
|
131
|
+
})
|
|
132
|
+
]
|
|
133
|
+
})
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function FormattedMessage({ text }) {
|
|
137
|
+
if (!text) return null;
|
|
138
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: text.split(/(\n)/g).map((line, lineIndex) => {
|
|
139
|
+
if (line === "\n") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}, lineIndex);
|
|
140
|
+
if (!line) return null;
|
|
141
|
+
const parts = line.split(/(```[\s\S]*?```|`[^`]+`|\*[^\s*](?:[^*]*[^\s*])?\*|_[^\s_](?:[^_]*[^\s_])?_|~[^\s~](?:[^~]*[^\s~])?~|https?:\/\/[^\s]+)/g);
|
|
142
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.default.Fragment, { children: parts.map((part, index) => {
|
|
143
|
+
if (!part) return null;
|
|
144
|
+
if (part.startsWith("```") && part.endsWith("```")) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
145
|
+
className: "my-1 block whitespace-pre-wrap rounded bg-black/5 px-1 py-0.5 font-mono text-[13px]",
|
|
146
|
+
children: part.slice(3, -3)
|
|
147
|
+
}, index);
|
|
148
|
+
if (part.startsWith("`") && part.endsWith("`")) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
149
|
+
className: "rounded bg-black/5 px-1 font-mono text-[13px] text-[#df0165]",
|
|
150
|
+
children: part.slice(1, -1)
|
|
151
|
+
}, index);
|
|
152
|
+
if (part.startsWith("*") && part.endsWith("*")) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
153
|
+
className: "font-bold",
|
|
154
|
+
children: part.slice(1, -1)
|
|
155
|
+
}, index);
|
|
156
|
+
if (part.startsWith("_") && part.endsWith("_")) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", {
|
|
157
|
+
className: "italic",
|
|
158
|
+
children: part.slice(1, -1)
|
|
159
|
+
}, index);
|
|
160
|
+
if (part.startsWith("~") && part.endsWith("~")) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("del", {
|
|
161
|
+
className: "text-gray-500 line-through",
|
|
162
|
+
children: part.slice(1, -1)
|
|
163
|
+
}, index);
|
|
164
|
+
if (part.match(/^https?:\/\/[^\s]+$/)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
165
|
+
href: part,
|
|
166
|
+
target: "_blank",
|
|
167
|
+
rel: "noopener noreferrer",
|
|
168
|
+
className: "text-[#027eb5] hover:underline",
|
|
169
|
+
children: part
|
|
170
|
+
}, index);
|
|
171
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: part }, index);
|
|
172
|
+
}) }, lineIndex);
|
|
173
|
+
}) });
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/date.ts
|
|
177
|
+
function getDisplayDate(dateStr) {
|
|
178
|
+
const dateObj = new Date(dateStr);
|
|
179
|
+
const now = /* @__PURE__ */ new Date();
|
|
180
|
+
const isToday = dateObj.toDateString() === now.toDateString();
|
|
181
|
+
const yesterday = new Date(now);
|
|
182
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
183
|
+
const isYesterday = dateObj.toDateString() === yesterday.toDateString();
|
|
184
|
+
if (isToday) return "HOJE";
|
|
185
|
+
else if (isYesterday) return "ONTEM";
|
|
186
|
+
else return dateObj.toLocaleDateString("pt-BR", {
|
|
187
|
+
day: "2-digit",
|
|
188
|
+
month: "2-digit",
|
|
189
|
+
year: "numeric"
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/react/message-view.tsx
|
|
194
|
+
function MessageView({ children, className, ...props }) {
|
|
195
|
+
const { isMobile, mobileView } = useWhatsappDashboard();
|
|
196
|
+
const hasContent = react.default.Children.count(children) > 0;
|
|
197
|
+
const isVisible = !isMobile || mobileView === "chat";
|
|
198
|
+
if (!hasContent) {
|
|
199
|
+
if (isMobile) return null;
|
|
200
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageViewEmpty, {
|
|
201
|
+
className,
|
|
202
|
+
...props
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
206
|
+
className: cn("relative flex flex-1 flex-col bg-[#efeae2]", className),
|
|
207
|
+
style: isVisible ? void 0 : { display: "none" },
|
|
208
|
+
...props,
|
|
209
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
|
|
210
|
+
position: "absolute",
|
|
211
|
+
backgroundImage: `url(${new URL("./wpp-bg.webp", require("url").pathToFileURL(__filename).href).href})`,
|
|
212
|
+
backgroundRepeat: "repeat",
|
|
213
|
+
opacity: .15,
|
|
214
|
+
inset: 0
|
|
215
|
+
} }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
216
|
+
className: "relative flex flex-1 flex-col min-h-0",
|
|
217
|
+
children
|
|
218
|
+
})]
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function MessageViewHeader({ conversation, onBack, onInfoClick, className }) {
|
|
222
|
+
const { isMobile, setMobileView } = useWhatsappDashboard();
|
|
223
|
+
const handleBack = () => {
|
|
224
|
+
setMobileView("list");
|
|
225
|
+
onBack?.();
|
|
226
|
+
};
|
|
227
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
228
|
+
className: cn("flex h-16 shrink-0 items-center justify-between border-b bg-[#f0f2f5] px-4 z-20", className),
|
|
229
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
230
|
+
className: "flex items-center gap-3",
|
|
231
|
+
children: [isMobile && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
232
|
+
type: "button",
|
|
233
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded-md hover:bg-black/5",
|
|
234
|
+
onClick: handleBack,
|
|
235
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
|
|
236
|
+
icon: _hugeicons_core_free_icons.ArrowLeft02Icon,
|
|
237
|
+
size: 20
|
|
238
|
+
})
|
|
239
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
240
|
+
className: "flex flex-col",
|
|
241
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
242
|
+
className: "text-[15px] font-medium text-[#111b21] leading-tight",
|
|
243
|
+
children: conversation.contactName || conversation.phone
|
|
244
|
+
}), conversation.contactName && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
245
|
+
className: "text-sm text-[#667781] leading-tight",
|
|
246
|
+
children: conversation.phone
|
|
247
|
+
})]
|
|
248
|
+
})]
|
|
249
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
250
|
+
type: "button",
|
|
251
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded-md hover:bg-black/5",
|
|
252
|
+
onClick: onInfoClick,
|
|
253
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
|
|
254
|
+
icon: _hugeicons_core_free_icons.InformationCircleIcon,
|
|
255
|
+
size: 20
|
|
256
|
+
})
|
|
257
|
+
})]
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const SCROLL_TOP_THRESHOLD = 50;
|
|
261
|
+
function MessageViewContent({ children, autoScroll = true, onScrollTop, className, ...props }) {
|
|
262
|
+
const scrollRef = (0, react.useRef)(null);
|
|
263
|
+
const prevScrollHeightRef = (0, react.useRef)(0);
|
|
264
|
+
(0, react.useEffect)(() => {
|
|
265
|
+
if (autoScroll && scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
266
|
+
}, [children, autoScroll]);
|
|
267
|
+
(0, react.useEffect)(() => {
|
|
268
|
+
const el = scrollRef.current;
|
|
269
|
+
if (!el) return;
|
|
270
|
+
const newScrollHeight = el.scrollHeight;
|
|
271
|
+
const prevScrollHeight = prevScrollHeightRef.current;
|
|
272
|
+
if (prevScrollHeight > 0 && newScrollHeight > prevScrollHeight) {
|
|
273
|
+
const addedHeight = newScrollHeight - prevScrollHeight;
|
|
274
|
+
if (el.scrollTop < SCROLL_TOP_THRESHOLD + addedHeight) el.scrollTop = addedHeight;
|
|
275
|
+
}
|
|
276
|
+
prevScrollHeightRef.current = newScrollHeight;
|
|
277
|
+
});
|
|
278
|
+
const handleScroll = (0, react.useCallback)(() => {
|
|
279
|
+
const el = scrollRef.current;
|
|
280
|
+
if (!el || !onScrollTop) return;
|
|
281
|
+
if (el.scrollTop <= SCROLL_TOP_THRESHOLD) onScrollTop();
|
|
282
|
+
}, [onScrollTop]);
|
|
283
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
284
|
+
ref: scrollRef,
|
|
285
|
+
className: cn("flex flex-1 flex-col overflow-y-auto p-4 pb-0 chat-scrollbar", className),
|
|
286
|
+
onScroll: handleScroll,
|
|
287
|
+
...props,
|
|
288
|
+
children
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function MessageList({ messages, renderMessageLabel, className }) {
|
|
292
|
+
const messageGroups = (0, react.useMemo)(() => {
|
|
293
|
+
const groups = [];
|
|
294
|
+
let currentGroup = null;
|
|
295
|
+
messages.forEach((msg) => {
|
|
296
|
+
const displayDate = getDisplayDate(msg.sentAt);
|
|
297
|
+
if (!currentGroup || currentGroup.date !== displayDate) {
|
|
298
|
+
currentGroup = {
|
|
299
|
+
date: displayDate,
|
|
300
|
+
messages: []
|
|
301
|
+
};
|
|
302
|
+
groups.push(currentGroup);
|
|
303
|
+
}
|
|
304
|
+
currentGroup.messages.push(msg);
|
|
305
|
+
});
|
|
306
|
+
return groups;
|
|
307
|
+
}, [messages]);
|
|
308
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
309
|
+
className: cn("flex flex-col pb-4", className),
|
|
310
|
+
children: messageGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
311
|
+
className: "flex flex-col gap-1",
|
|
312
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DateDivider, { date: group.date }), group.messages.map((msg) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageBubble, {
|
|
313
|
+
content: msg.content || "",
|
|
314
|
+
sender: msg.direction === "incoming" ? "user" : "bot",
|
|
315
|
+
timestamp: new Date(msg.sentAt).toLocaleTimeString("pt-BR", {
|
|
316
|
+
hour: "2-digit",
|
|
317
|
+
minute: "2-digit"
|
|
318
|
+
}),
|
|
319
|
+
status: msg.status,
|
|
320
|
+
templateName: msg.templateName || void 0,
|
|
321
|
+
label: renderMessageLabel?.(msg)
|
|
322
|
+
}, msg.id))]
|
|
323
|
+
}, group.date))
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function DateDivider({ date }) {
|
|
327
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
328
|
+
className: "sticky top-0 z-10 flex justify-center w-full py-2 pointer-events-none",
|
|
329
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
330
|
+
className: "bg-white border border-[#e9edef] shadow-[0_1px_0.5px_rgba(11,20,26,0.13)] text-[#54656f] text-[12.5px] font-medium px-3 py-1.5 rounded-lg uppercase pointer-events-auto",
|
|
331
|
+
children: date
|
|
332
|
+
})
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function MessageViewEmpty({ className, ...props }) {
|
|
336
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
337
|
+
className: cn("relative flex flex-1 flex-col items-center justify-center bg-[#f8f9fa] text-[#667781] p-6", className),
|
|
338
|
+
...props,
|
|
339
|
+
children: [
|
|
340
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
341
|
+
className: "mb-8 flex h-48 w-48 items-center justify-center rounded-full bg-[#f0f2f5] shadow-sm",
|
|
342
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
|
|
343
|
+
icon: _hugeicons_core_free_icons.Message01Icon,
|
|
344
|
+
size: 80,
|
|
345
|
+
className: "text-[#bbc5cb]"
|
|
346
|
+
})
|
|
347
|
+
}),
|
|
348
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
349
|
+
className: "mb-3 text-2xl font-semibold text-[#41525d]",
|
|
350
|
+
children: "Better Zap"
|
|
351
|
+
}),
|
|
352
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
353
|
+
className: "max-w-sm space-y-3 text-center",
|
|
354
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
355
|
+
className: "text-[15px] leading-relaxed",
|
|
356
|
+
children: [
|
|
357
|
+
"Esta é uma interface dedicada para visualização e monitoramento de mensagens da ",
|
|
358
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "API Oficial do WhatsApp" }),
|
|
359
|
+
"."
|
|
360
|
+
]
|
|
361
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
362
|
+
className: "text-sm opacity-80",
|
|
363
|
+
children: "Acompanhe o histórico de conversas, verifique o status de entrega e gerencie as interações do Cloud API de forma profissional."
|
|
364
|
+
})]
|
|
365
|
+
})
|
|
366
|
+
]
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/react/conversation-search.tsx
|
|
371
|
+
function ConversationSearch({ value, onChange, className }) {
|
|
372
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
373
|
+
className: cn("border-b border-[#e9edef] shrink-0 p-2", className),
|
|
374
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
375
|
+
className: "flex items-center bg-[#f0f2f5] rounded-full px-4 h-[38px] gap-3 focus-within:bg-white focus-within:ring-1 focus-within:ring-green-200 focus-within:shadow-md transition-all border border-transparent focus-within:border-transparent",
|
|
376
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
|
|
377
|
+
icon: _hugeicons_core_free_icons.Search01Icon,
|
|
378
|
+
size: 18,
|
|
379
|
+
className: "text-[#54656f] shrink-0"
|
|
380
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
381
|
+
type: "text",
|
|
382
|
+
placeholder: "Buscar conversa",
|
|
383
|
+
value,
|
|
384
|
+
onChange: (e) => onChange(e.target.value),
|
|
385
|
+
className: "flex-1 border-none bg-transparent text-[15px] text-[#111b21] focus:outline-none h-full placeholder:text-[#667781]"
|
|
386
|
+
})]
|
|
387
|
+
})
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
//#endregion
|
|
391
|
+
//#region src/react/conversation-list.tsx
|
|
392
|
+
function ConversationList({ conversations, isLoading, isError, selectedConversationId, onSelect, className }) {
|
|
393
|
+
const { isMobile, mobileView, setMobileView } = useWhatsappDashboard();
|
|
394
|
+
const [search, setSearch] = (0, react.useState)("");
|
|
395
|
+
const filtered = conversations.filter((c) => c.phone.includes(search) || c.contactName?.toLowerCase().includes(search.toLowerCase()));
|
|
396
|
+
const handleSelect = (id) => {
|
|
397
|
+
onSelect(id);
|
|
398
|
+
setMobileView("chat");
|
|
399
|
+
};
|
|
400
|
+
const isVisible = !isMobile || mobileView === "list";
|
|
401
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
402
|
+
className: cn("flex flex-col h-full bg-white border-r border-[#e9edef]", isMobile ? "w-full" : "min-w-[320px] max-w-105", className),
|
|
403
|
+
style: isVisible ? void 0 : { display: "none" },
|
|
404
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationSearch, {
|
|
405
|
+
value: search,
|
|
406
|
+
onChange: setSearch
|
|
407
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
408
|
+
className: "flex-1 overflow-y-auto overflow-x-hidden chat-scrollbar",
|
|
409
|
+
children: isLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
410
|
+
className: "flex items-center justify-center h-full text-sm text-[#667781]",
|
|
411
|
+
children: "Carregando..."
|
|
412
|
+
}) : isError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
413
|
+
className: "flex items-center justify-center h-full text-sm text-red-500",
|
|
414
|
+
children: "Erro ao carregar conversas"
|
|
415
|
+
}) : filtered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
416
|
+
className: "flex flex-col items-center justify-center h-full gap-2 text-[#667781]",
|
|
417
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
|
|
418
|
+
icon: _hugeicons_core_free_icons.Message01Icon,
|
|
419
|
+
size: 32
|
|
420
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
421
|
+
className: "text-sm",
|
|
422
|
+
children: "Nenhuma conversa encontrada"
|
|
423
|
+
})]
|
|
424
|
+
}) : filtered.map((conversation) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConversationItem, {
|
|
425
|
+
conversation,
|
|
426
|
+
isSelected: selectedConversationId === conversation.id,
|
|
427
|
+
onClick: () => handleSelect(conversation.id)
|
|
428
|
+
}, conversation.id))
|
|
429
|
+
})]
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function ConversationItem({ conversation, isSelected, onClick }) {
|
|
433
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
434
|
+
onClick,
|
|
435
|
+
"data-selected": isSelected,
|
|
436
|
+
className: "group flex items-center w-full h-[72px] px-3 gap-3 transition-all cursor-pointer text-left relative overflow-hidden hover:bg-[#f5f6f6] data-[selected=true]:bg-[#075e54] data-[selected=true]:hover:bg-[#064940]",
|
|
437
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
438
|
+
className: "w-[49px] h-[49px] rounded-full bg-[#dfe5e7] flex items-center justify-center shrink-0 group-data-[selected=true]:bg-white/20",
|
|
439
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
|
|
440
|
+
icon: _hugeicons_core_free_icons.UserIcon,
|
|
441
|
+
size: 28,
|
|
442
|
+
className: "text-[#aebac1] group-data-[selected=true]:text-white/80"
|
|
443
|
+
})
|
|
444
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
445
|
+
className: "flex-1 min-w-0 border-b border-[#f2f2f2] h-full flex flex-col justify-center pr-1 group-last:border-none group-data-[selected=true]:border-transparent",
|
|
446
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
447
|
+
className: "flex justify-between items-baseline mb-0.5",
|
|
448
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
449
|
+
className: "text-[17px] font-normal text-[#111b21] truncate group-data-[selected=true]:text-white",
|
|
450
|
+
children: conversation.contactName || formatPhone(conversation.phone)
|
|
451
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
452
|
+
className: "text-xs text-[#667781] shrink-0 group-data-[selected=true]:text-white/90",
|
|
453
|
+
children: formatTime(conversation.lastMessageAt)
|
|
454
|
+
})]
|
|
455
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
456
|
+
className: "flex justify-between items-center gap-2",
|
|
457
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
458
|
+
className: "text-[14px] text-[#667781] truncate group-data-[selected=true]:text-white/90",
|
|
459
|
+
children: [conversation.lastDirection === "incoming" ? "" : "Você: ", conversation.lastMessagePreview || "Sem mensagem"]
|
|
460
|
+
}), conversation.unreadCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
461
|
+
className: "bg-[#25d366] text-white text-[11px] font-bold rounded-full min-w-[20px] h-[20px] flex items-center justify-center px-1.5 shrink-0 group-data-[selected=true]:bg-white",
|
|
462
|
+
children: conversation.unreadCount
|
|
463
|
+
})]
|
|
464
|
+
})]
|
|
465
|
+
})]
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function formatPhone(phone) {
|
|
469
|
+
if (phone.length === 13 && phone.startsWith("55")) return `(${phone.slice(2, 4)}) ${phone.slice(4, 9)}-${phone.slice(9)}`;
|
|
470
|
+
return phone;
|
|
471
|
+
}
|
|
472
|
+
function formatTime(dateStr) {
|
|
473
|
+
try {
|
|
474
|
+
const date = new Date(dateStr);
|
|
475
|
+
const now = /* @__PURE__ */ new Date();
|
|
476
|
+
if (date.toDateString() === now.toDateString()) return date.toLocaleTimeString("pt-BR", {
|
|
477
|
+
hour: "2-digit",
|
|
478
|
+
minute: "2-digit"
|
|
479
|
+
});
|
|
480
|
+
const yesterday = new Date(now);
|
|
481
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
482
|
+
if (date.toDateString() === yesterday.toDateString()) return "Ontem";
|
|
483
|
+
return date.toLocaleDateString("pt-BR", {
|
|
484
|
+
day: "2-digit",
|
|
485
|
+
month: "2-digit"
|
|
486
|
+
});
|
|
487
|
+
} catch {
|
|
488
|
+
return "";
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/react/message-input.tsx
|
|
493
|
+
function MessageInput({ onSend, disabled, placeholder = "Digite uma mensagem", className, contextWindowOpen = true }) {
|
|
494
|
+
const [text, setText] = (0, react.useState)("");
|
|
495
|
+
const [isSending, setIsSending] = (0, react.useState)(false);
|
|
496
|
+
const textareaRef = (0, react.useRef)(null);
|
|
497
|
+
const isDisabled = disabled || isSending || !contextWindowOpen;
|
|
498
|
+
const handleSend = async () => {
|
|
499
|
+
const trimmedText = text.trim();
|
|
500
|
+
if (!trimmedText || isDisabled) return;
|
|
501
|
+
setIsSending(true);
|
|
502
|
+
try {
|
|
503
|
+
await onSend(trimmedText);
|
|
504
|
+
setText("");
|
|
505
|
+
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
506
|
+
} finally {
|
|
507
|
+
setIsSending(false);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
const handleKeyDown = (e) => {
|
|
511
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
512
|
+
e.preventDefault();
|
|
513
|
+
handleSend();
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
(0, react.useEffect)(() => {
|
|
517
|
+
if (textareaRef.current) {
|
|
518
|
+
textareaRef.current.style.height = "auto";
|
|
519
|
+
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
|
520
|
+
}
|
|
521
|
+
}, [text]);
|
|
522
|
+
const hasText = text.trim().length > 0;
|
|
523
|
+
if (!contextWindowOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
524
|
+
className: cn("w-full flex flex-col p-4 z-10 shrink-0", className),
|
|
525
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
526
|
+
className: "flex items-center gap-2 px-4 py-3 min-h-[62px] bg-[#fef3c7] rounded-2xl border border-[#f59e0b]/20",
|
|
527
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
|
|
528
|
+
icon: _hugeicons_core_free_icons.Clock01Icon,
|
|
529
|
+
size: 20,
|
|
530
|
+
className: "text-[#92400e] shrink-0"
|
|
531
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
532
|
+
className: "text-[13px] leading-[18px] text-[#92400e]",
|
|
533
|
+
children: "A janela de 24h expirou. Mensagens de texto livre só podem ser enviadas dentro de 24h após a última mensagem do contato."
|
|
534
|
+
})]
|
|
535
|
+
})
|
|
536
|
+
});
|
|
537
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
538
|
+
className: cn("w-full flex flex-col p-4 z-10 shrink-0", className),
|
|
539
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
540
|
+
className: "flex items-center gap-1 px-2 py-1 min-h-[62px] bg-white rounded-2xl shadow-lg border border-gray-100",
|
|
541
|
+
children: [
|
|
542
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
543
|
+
className: "flex items-center gap-1 text-[#54656f] shrink-0",
|
|
544
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
545
|
+
type: "button",
|
|
546
|
+
"aria-label": "Emojis",
|
|
547
|
+
className: "p-2 rounded-full hover:bg-black/5 transition-colors",
|
|
548
|
+
title: "Emojis",
|
|
549
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
|
|
550
|
+
icon: _hugeicons_core_free_icons.SmileIcon,
|
|
551
|
+
size: 24
|
|
552
|
+
})
|
|
553
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
554
|
+
type: "button",
|
|
555
|
+
"aria-label": "Anexar arquivo",
|
|
556
|
+
className: "p-2 rounded-full hover:bg-black/5 transition-colors",
|
|
557
|
+
title: "Anexar",
|
|
558
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
|
|
559
|
+
icon: _hugeicons_core_free_icons.Add01Icon,
|
|
560
|
+
size: 24
|
|
561
|
+
})
|
|
562
|
+
})]
|
|
563
|
+
}),
|
|
564
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
565
|
+
className: "flex-1 flex items-center min-h-[42px] py-3 px-2",
|
|
566
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
567
|
+
ref: textareaRef,
|
|
568
|
+
className: "flex-1 bg-transparent border-none text-[15px] leading-[22px] text-[#111b21] resize-none focus:outline-none max-h-[120px] placeholder:text-[#8696a0]",
|
|
569
|
+
name: "message",
|
|
570
|
+
"aria-label": "Mensagem",
|
|
571
|
+
autoComplete: "off",
|
|
572
|
+
placeholder: isSending ? "Enviando..." : placeholder,
|
|
573
|
+
value: text,
|
|
574
|
+
onChange: (e) => setText(e.target.value),
|
|
575
|
+
onKeyDown: handleKeyDown,
|
|
576
|
+
rows: 1,
|
|
577
|
+
disabled: isDisabled
|
|
578
|
+
})
|
|
579
|
+
}),
|
|
580
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
581
|
+
className: "flex items-center pr-1 shrink-0",
|
|
582
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
583
|
+
type: "button",
|
|
584
|
+
"aria-label": hasText ? "Enviar" : "Gravar áudio",
|
|
585
|
+
className: cn("p-2 rounded-full transition-colors disabled:opacity-40 disabled:cursor-not-allowed", hasText ? "text-[#00a884]" : "text-[#54656f] hover:bg-black/5"),
|
|
586
|
+
onClick: hasText ? handleSend : void 0,
|
|
587
|
+
disabled: isDisabled,
|
|
588
|
+
children: hasText ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
|
|
589
|
+
icon: _hugeicons_core_free_icons.Sent02Icon,
|
|
590
|
+
size: 24
|
|
591
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_hugeicons_react.HugeiconsIcon, {
|
|
592
|
+
icon: _hugeicons_core_free_icons.Mic01Icon,
|
|
593
|
+
size: 24
|
|
594
|
+
})
|
|
595
|
+
})
|
|
596
|
+
})
|
|
597
|
+
]
|
|
598
|
+
})
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
//#endregion
|
|
602
|
+
exports.ConversationItem = ConversationItem;
|
|
603
|
+
exports.ConversationList = ConversationList;
|
|
604
|
+
exports.FormattedMessage = FormattedMessage;
|
|
605
|
+
exports.MessageBubble = MessageBubble;
|
|
606
|
+
exports.MessageInput = MessageInput;
|
|
607
|
+
exports.MessageList = MessageList;
|
|
608
|
+
exports.MessageView = MessageView;
|
|
609
|
+
exports.MessageViewContent = MessageViewContent;
|
|
610
|
+
exports.MessageViewEmpty = MessageViewEmpty;
|
|
611
|
+
exports.MessageViewHeader = MessageViewHeader;
|
|
612
|
+
exports.WhatsappDashboard = WhatsappDashboard;
|
|
613
|
+
exports.cn = cn;
|
|
614
|
+
exports.useWhatsappDashboard = useWhatsappDashboard;
|