@hamzasaleemorg/convex-comments 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/LICENSE +201 -0
- package/README.md +581 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +745 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +579 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +44 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +673 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/lib.d.ts +17 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +18 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/messages.d.ts +173 -0
- package/dist/component/messages.d.ts.map +1 -0
- package/dist/component/messages.js +410 -0
- package/dist/component/messages.js.map +1 -0
- package/dist/component/reactions.d.ts +51 -0
- package/dist/component/reactions.d.ts.map +1 -0
- package/dist/component/reactions.js +191 -0
- package/dist/component/reactions.js.map +1 -0
- package/dist/component/schema.d.ts +274 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +159 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/threads.d.ts +110 -0
- package/dist/component/threads.d.ts.map +1 -0
- package/dist/component/threads.js +276 -0
- package/dist/component/threads.js.map +1 -0
- package/dist/component/typing.d.ts +31 -0
- package/dist/component/typing.d.ts.map +1 -0
- package/dist/component/typing.js +147 -0
- package/dist/component/typing.js.map +1 -0
- package/dist/component/zones.d.ts +63 -0
- package/dist/component/zones.d.ts.map +1 -0
- package/dist/component/zones.js +159 -0
- package/dist/component/zones.js.map +1 -0
- package/dist/react/AddComment.d.ts +57 -0
- package/dist/react/AddComment.d.ts.map +1 -0
- package/dist/react/AddComment.js +285 -0
- package/dist/react/AddComment.js.map +1 -0
- package/dist/react/Comment.d.ts +70 -0
- package/dist/react/Comment.d.ts.map +1 -0
- package/dist/react/Comment.js +259 -0
- package/dist/react/Comment.js.map +1 -0
- package/dist/react/Comments.d.ts +74 -0
- package/dist/react/Comments.d.ts.map +1 -0
- package/dist/react/Comments.js +108 -0
- package/dist/react/Comments.js.map +1 -0
- package/dist/react/CommentsProvider.d.ts +104 -0
- package/dist/react/CommentsProvider.d.ts.map +1 -0
- package/dist/react/CommentsProvider.js +98 -0
- package/dist/react/CommentsProvider.js.map +1 -0
- package/dist/react/ReactionPicker.d.ts +28 -0
- package/dist/react/ReactionPicker.d.ts.map +1 -0
- package/dist/react/ReactionPicker.js +56 -0
- package/dist/react/ReactionPicker.js.map +1 -0
- package/dist/react/Thread.d.ts +84 -0
- package/dist/react/Thread.d.ts.map +1 -0
- package/dist/react/Thread.js +124 -0
- package/dist/react/Thread.js.map +1 -0
- package/dist/react/TypingIndicator.d.ts +25 -0
- package/dist/react/TypingIndicator.d.ts.map +1 -0
- package/dist/react/TypingIndicator.js +99 -0
- package/dist/react/TypingIndicator.js.map +1 -0
- package/dist/react/index.d.ts +15 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +15 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +106 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.ts +813 -0
- package/src/component/_generated/api.ts +60 -0
- package/src/component/_generated/component.ts +784 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/lib.ts +57 -0
- package/src/component/messages.ts +476 -0
- package/src/component/reactions.ts +222 -0
- package/src/component/schema.ts +169 -0
- package/src/component/threads.ts +319 -0
- package/src/component/typing.ts +168 -0
- package/src/component/zones.ts +180 -0
- package/src/react/AddComment.tsx +463 -0
- package/src/react/Comment.tsx +519 -0
- package/src/react/Comments.tsx +276 -0
- package/src/react/CommentsProvider.tsx +197 -0
- package/src/react/ReactionPicker.tsx +95 -0
- package/src/react/Thread.tsx +336 -0
- package/src/react/TypingIndicator.tsx +144 -0
- package/src/react/index.ts +45 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comments Component
|
|
3
|
+
*
|
|
4
|
+
* Top-level component that displays all threads in a zone.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type ReactNode } from "react";
|
|
8
|
+
import { useComments } from "./CommentsProvider";
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export interface ThreadData {
|
|
15
|
+
thread: {
|
|
16
|
+
_id: string;
|
|
17
|
+
zoneId: string;
|
|
18
|
+
resolved: boolean;
|
|
19
|
+
resolvedBy?: string;
|
|
20
|
+
resolvedAt?: number;
|
|
21
|
+
createdAt: number;
|
|
22
|
+
lastActivityAt: number;
|
|
23
|
+
position?: { x: number; y: number; anchor?: string };
|
|
24
|
+
metadata?: unknown;
|
|
25
|
+
};
|
|
26
|
+
firstMessage: {
|
|
27
|
+
_id: string;
|
|
28
|
+
body: string;
|
|
29
|
+
authorId: string;
|
|
30
|
+
createdAt: number;
|
|
31
|
+
} | null;
|
|
32
|
+
messageCount: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CommentsProps {
|
|
36
|
+
/** Threads data from getThreads query */
|
|
37
|
+
threads: ThreadData[];
|
|
38
|
+
/** Whether there are more threads to load */
|
|
39
|
+
hasMore?: boolean;
|
|
40
|
+
/** Loading state */
|
|
41
|
+
isLoading?: boolean;
|
|
42
|
+
/** Callback when load more is clicked */
|
|
43
|
+
onLoadMore?: () => void;
|
|
44
|
+
/** Callback when a thread is clicked */
|
|
45
|
+
onThreadClick?: (threadId: string) => void;
|
|
46
|
+
/** Callback when new thread button is clicked */
|
|
47
|
+
onNewThread?: () => void;
|
|
48
|
+
/** Callback when thread is resolved */
|
|
49
|
+
onResolveThread?: (threadId: string) => void;
|
|
50
|
+
/** Callback when thread is unresolve */
|
|
51
|
+
onUnresolveThread?: (threadId: string) => void;
|
|
52
|
+
/** Whether to show resolved threads */
|
|
53
|
+
showResolved?: boolean;
|
|
54
|
+
/** Custom thread renderer */
|
|
55
|
+
renderThread?: (thread: ThreadData) => ReactNode;
|
|
56
|
+
/** Empty state message */
|
|
57
|
+
emptyMessage?: string;
|
|
58
|
+
/** CSS class name */
|
|
59
|
+
className?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Component
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Main comments component that displays all threads in a zone.
|
|
68
|
+
*
|
|
69
|
+
* Usage:
|
|
70
|
+
* ```tsx
|
|
71
|
+
* const { threads, hasMore } = useQuery(api.comments.getThreads, { zoneId });
|
|
72
|
+
*
|
|
73
|
+
* <Comments
|
|
74
|
+
* threads={threads}
|
|
75
|
+
* hasMore={hasMore}
|
|
76
|
+
* onLoadMore={loadMore}
|
|
77
|
+
* onThreadClick={(id) => setSelectedThread(id)}
|
|
78
|
+
* onNewThread={() => createThread()}
|
|
79
|
+
* />
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function Comments({
|
|
83
|
+
threads,
|
|
84
|
+
hasMore = false,
|
|
85
|
+
isLoading = false,
|
|
86
|
+
onLoadMore,
|
|
87
|
+
onThreadClick,
|
|
88
|
+
onNewThread,
|
|
89
|
+
onResolveThread,
|
|
90
|
+
onUnresolveThread,
|
|
91
|
+
showResolved = true,
|
|
92
|
+
renderThread,
|
|
93
|
+
emptyMessage = "No comments yet. Start a conversation!",
|
|
94
|
+
className = "",
|
|
95
|
+
}: CommentsProps) {
|
|
96
|
+
const { userId, styles } = useComments();
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
// Filter threads based on showResolved
|
|
100
|
+
const visibleThreads = showResolved
|
|
101
|
+
? threads
|
|
102
|
+
: threads.filter((t) => !t.thread.resolved);
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
// Default thread renderer
|
|
107
|
+
const defaultRenderThread = (data: ThreadData) => {
|
|
108
|
+
const { thread, firstMessage, messageCount } = data;
|
|
109
|
+
const isResolved = thread.resolved;
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div
|
|
113
|
+
key={thread._id}
|
|
114
|
+
className={`comments-thread-preview ${isResolved ? "resolved" : ""}`}
|
|
115
|
+
onClick={() => onThreadClick?.(thread._id)}
|
|
116
|
+
style={{
|
|
117
|
+
padding: "12px 16px",
|
|
118
|
+
borderBottom: "1px solid #e5e7eb",
|
|
119
|
+
cursor: "pointer",
|
|
120
|
+
opacity: isResolved ? 0.6 : 1,
|
|
121
|
+
backgroundColor: isResolved ? "#f9fafb" : "white",
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
|
|
125
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
126
|
+
{firstMessage ? (
|
|
127
|
+
<>
|
|
128
|
+
<div
|
|
129
|
+
style={{
|
|
130
|
+
fontSize: "14px",
|
|
131
|
+
color: "#111827",
|
|
132
|
+
whiteSpace: "nowrap",
|
|
133
|
+
overflow: "hidden",
|
|
134
|
+
textOverflow: "ellipsis",
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
{firstMessage.body}
|
|
138
|
+
</div>
|
|
139
|
+
<div style={{ fontSize: "12px", color: "#6b7280", marginTop: "4px" }}>
|
|
140
|
+
{messageCount} {messageCount === 1 ? "message" : "messages"}
|
|
141
|
+
{isResolved && " • Resolved"}
|
|
142
|
+
</div>
|
|
143
|
+
</>
|
|
144
|
+
) : (
|
|
145
|
+
<div style={{ fontSize: "14px", color: "#6b7280", fontStyle: "italic" }}>
|
|
146
|
+
Empty thread
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
{isResolved && onUnresolveThread && (
|
|
151
|
+
<button
|
|
152
|
+
onClick={(e) => {
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
onUnresolveThread(thread._id);
|
|
155
|
+
}}
|
|
156
|
+
style={{
|
|
157
|
+
padding: "4px 8px",
|
|
158
|
+
fontSize: "12px",
|
|
159
|
+
border: "1px solid #d1d5db",
|
|
160
|
+
borderRadius: "4px",
|
|
161
|
+
background: "white",
|
|
162
|
+
cursor: "pointer",
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
Reopen
|
|
166
|
+
</button>
|
|
167
|
+
)}
|
|
168
|
+
{!isResolved && onResolveThread && (
|
|
169
|
+
<button
|
|
170
|
+
onClick={(e) => {
|
|
171
|
+
e.stopPropagation();
|
|
172
|
+
onResolveThread(thread._id);
|
|
173
|
+
}}
|
|
174
|
+
style={{
|
|
175
|
+
padding: "4px 8px",
|
|
176
|
+
fontSize: "12px",
|
|
177
|
+
border: "1px solid #d1d5db",
|
|
178
|
+
borderRadius: "4px",
|
|
179
|
+
background: "white",
|
|
180
|
+
cursor: "pointer",
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
Resolve
|
|
184
|
+
</button>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div
|
|
193
|
+
className={`comments-container ${className}`}
|
|
194
|
+
style={{
|
|
195
|
+
fontFamily: styles?.fontFamily ?? "system-ui, -apple-system, sans-serif",
|
|
196
|
+
border: "1px solid #e5e7eb",
|
|
197
|
+
borderRadius: styles?.borderRadius ?? "8px",
|
|
198
|
+
overflow: "hidden",
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{/* Header */}
|
|
202
|
+
<div
|
|
203
|
+
style={{
|
|
204
|
+
padding: "12px 16px",
|
|
205
|
+
borderBottom: "1px solid #e5e7eb",
|
|
206
|
+
display: "flex",
|
|
207
|
+
justifyContent: "space-between",
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
backgroundColor: "#f9fafb",
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<span style={{ fontWeight: 600, fontSize: "14px" }}>
|
|
213
|
+
Comments ({threads.length})
|
|
214
|
+
</span>
|
|
215
|
+
{onNewThread && userId && (
|
|
216
|
+
<button
|
|
217
|
+
onClick={onNewThread}
|
|
218
|
+
style={{
|
|
219
|
+
padding: "6px 12px",
|
|
220
|
+
fontSize: "13px",
|
|
221
|
+
fontWeight: 500,
|
|
222
|
+
border: "none",
|
|
223
|
+
borderRadius: "6px",
|
|
224
|
+
backgroundColor: styles?.accentColor ?? "#3b82f6",
|
|
225
|
+
color: "white",
|
|
226
|
+
cursor: "pointer",
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
+ New Thread
|
|
230
|
+
</button>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{/* Thread list */}
|
|
235
|
+
<div style={{ maxHeight: "400px", overflowY: "auto" }}>
|
|
236
|
+
{visibleThreads.length === 0 ? (
|
|
237
|
+
<div
|
|
238
|
+
style={{
|
|
239
|
+
padding: "40px 16px",
|
|
240
|
+
textAlign: "center",
|
|
241
|
+
color: "#6b7280",
|
|
242
|
+
fontSize: "14px",
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
{emptyMessage}
|
|
246
|
+
</div>
|
|
247
|
+
) : (
|
|
248
|
+
visibleThreads.map((data) =>
|
|
249
|
+
renderThread ? renderThread(data) : defaultRenderThread(data)
|
|
250
|
+
)
|
|
251
|
+
)}
|
|
252
|
+
|
|
253
|
+
{/* Load more */}
|
|
254
|
+
{hasMore && (
|
|
255
|
+
<div style={{ padding: "12px 16px", textAlign: "center" }}>
|
|
256
|
+
<button
|
|
257
|
+
onClick={onLoadMore}
|
|
258
|
+
disabled={isLoading}
|
|
259
|
+
style={{
|
|
260
|
+
padding: "8px 16px",
|
|
261
|
+
fontSize: "13px",
|
|
262
|
+
border: "1px solid #d1d5db",
|
|
263
|
+
borderRadius: "6px",
|
|
264
|
+
background: "white",
|
|
265
|
+
cursor: isLoading ? "not-allowed" : "pointer",
|
|
266
|
+
opacity: isLoading ? 0.6 : 1,
|
|
267
|
+
}}
|
|
268
|
+
>
|
|
269
|
+
{isLoading ? "Loading..." : "Load More"}
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comments Context Provider
|
|
3
|
+
*
|
|
4
|
+
* Provides the Comments API instance to all child components.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createContext, useContext, useMemo, type ReactNode, type CSSProperties } from "react";
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface CommentsStyles {
|
|
14
|
+
/** Primary accent color (buttons, links, highlights) */
|
|
15
|
+
accentColor?: string;
|
|
16
|
+
/** Background color for the comments container */
|
|
17
|
+
backgroundColor?: string;
|
|
18
|
+
/** Text color */
|
|
19
|
+
textColor?: string;
|
|
20
|
+
/** Secondary/muted text color */
|
|
21
|
+
textColorMuted?: string;
|
|
22
|
+
/** Border color */
|
|
23
|
+
borderColor?: string;
|
|
24
|
+
/** Border radius for cards and buttons */
|
|
25
|
+
borderRadius?: string;
|
|
26
|
+
/** Font family */
|
|
27
|
+
fontFamily?: string;
|
|
28
|
+
/** Base font size */
|
|
29
|
+
fontSize?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CommentsContextValue {
|
|
33
|
+
/** Current user ID */
|
|
34
|
+
userId: string | null;
|
|
35
|
+
/** Function to resolve user display info */
|
|
36
|
+
resolveUser?: (userId: string) => Promise<{ name: string; avatar?: string }> | { name: string; avatar?: string };
|
|
37
|
+
/** Available reaction emojis */
|
|
38
|
+
reactionChoices: string[];
|
|
39
|
+
/** Whether the current user can edit/delete messages */
|
|
40
|
+
canModerate?: boolean;
|
|
41
|
+
/** Custom styles */
|
|
42
|
+
styles?: CommentsStyles;
|
|
43
|
+
/** CSS variables for theming */
|
|
44
|
+
cssVars: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Default Theme
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
const defaultStyles: Required<CommentsStyles> = {
|
|
52
|
+
accentColor: "#3b82f6",
|
|
53
|
+
backgroundColor: "#ffffff",
|
|
54
|
+
textColor: "#1f2937",
|
|
55
|
+
textColorMuted: "#6b7280",
|
|
56
|
+
borderColor: "#e5e7eb",
|
|
57
|
+
borderRadius: "8px",
|
|
58
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
59
|
+
fontSize: "14px",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Context
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
const CommentsContext = createContext<CommentsContextValue | null>(null);
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Provider
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
export interface CommentsProviderProps {
|
|
73
|
+
/** Current user ID (null if not authenticated) */
|
|
74
|
+
userId: string | null;
|
|
75
|
+
/** Function to resolve user info for display */
|
|
76
|
+
resolveUser?: (userId: string) => Promise<{ name: string; avatar?: string }> | { name: string; avatar?: string };
|
|
77
|
+
/** Available reaction emojis (defaults to common emojis) */
|
|
78
|
+
reactionChoices?: string[];
|
|
79
|
+
/** Whether the current user has moderator privileges */
|
|
80
|
+
canModerate?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Custom styling options. Can also use CSS variables:
|
|
83
|
+
* --comments-accent-color, --comments-bg-color, etc.
|
|
84
|
+
*/
|
|
85
|
+
styles?: CommentsStyles;
|
|
86
|
+
/** Optional class name for the wrapper */
|
|
87
|
+
className?: string;
|
|
88
|
+
/** Child components */
|
|
89
|
+
children: ReactNode;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Provider component that supplies comments configuration to all child components.
|
|
94
|
+
*
|
|
95
|
+
* Usage:
|
|
96
|
+
* ```tsx
|
|
97
|
+
* <CommentsProvider
|
|
98
|
+
* userId={currentUserId}
|
|
99
|
+
* resolveUser={async (id) => ({ name: users[id].name, avatar: users[id].avatar })}
|
|
100
|
+
* reactionChoices={["👍", "❤️", "😄", "🎉", "😮", "😢"]}
|
|
101
|
+
* styles={{
|
|
102
|
+
* accentColor: "#8b5cf6",
|
|
103
|
+
* borderRadius: "12px",
|
|
104
|
+
* }}
|
|
105
|
+
* >
|
|
106
|
+
* <Comments threads={threads} />
|
|
107
|
+
* </CommentsProvider>
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* CSS Variables (can be set in your CSS):
|
|
111
|
+
* - --comments-accent-color
|
|
112
|
+
* - --comments-bg-color
|
|
113
|
+
* - --comments-text-color
|
|
114
|
+
* - --comments-text-color-muted
|
|
115
|
+
* - --comments-border-color
|
|
116
|
+
* - --comments-border-radius
|
|
117
|
+
* - --comments-font-family
|
|
118
|
+
* - --comments-font-size
|
|
119
|
+
*/
|
|
120
|
+
export function CommentsProvider({
|
|
121
|
+
userId,
|
|
122
|
+
resolveUser,
|
|
123
|
+
reactionChoices = ["👍", "❤️", "😄", "🎉", "😮", "😢", "👀", "🚀"],
|
|
124
|
+
canModerate = false,
|
|
125
|
+
styles,
|
|
126
|
+
className,
|
|
127
|
+
children,
|
|
128
|
+
}: CommentsProviderProps) {
|
|
129
|
+
const mergedStyles = useMemo<Required<CommentsStyles>>(
|
|
130
|
+
() => ({
|
|
131
|
+
...defaultStyles,
|
|
132
|
+
...styles,
|
|
133
|
+
}),
|
|
134
|
+
[styles]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const cssVars = useMemo<Record<string, string>>(
|
|
138
|
+
() => ({
|
|
139
|
+
"--comments-accent-color": mergedStyles.accentColor,
|
|
140
|
+
"--comments-bg-color": mergedStyles.backgroundColor,
|
|
141
|
+
"--comments-text-color": mergedStyles.textColor,
|
|
142
|
+
"--comments-text-color-muted": mergedStyles.textColorMuted,
|
|
143
|
+
"--comments-border-color": mergedStyles.borderColor,
|
|
144
|
+
"--comments-border-radius": mergedStyles.borderRadius,
|
|
145
|
+
"--comments-font-family": mergedStyles.fontFamily,
|
|
146
|
+
"--comments-font-size": mergedStyles.fontSize,
|
|
147
|
+
}),
|
|
148
|
+
[mergedStyles]
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const value = useMemo<CommentsContextValue>(
|
|
152
|
+
() => ({
|
|
153
|
+
userId,
|
|
154
|
+
resolveUser,
|
|
155
|
+
reactionChoices,
|
|
156
|
+
canModerate,
|
|
157
|
+
styles: mergedStyles,
|
|
158
|
+
cssVars,
|
|
159
|
+
}),
|
|
160
|
+
[userId, resolveUser, reactionChoices, canModerate, mergedStyles, cssVars]
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const wrapperStyle: CSSProperties = {
|
|
164
|
+
...cssVars as CSSProperties,
|
|
165
|
+
fontFamily: mergedStyles.fontFamily,
|
|
166
|
+
fontSize: mergedStyles.fontSize,
|
|
167
|
+
color: mergedStyles.textColor,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<CommentsContext.Provider value={value}>
|
|
172
|
+
<div
|
|
173
|
+
className={`comments-provider ${className ?? ""}`}
|
|
174
|
+
style={wrapperStyle}
|
|
175
|
+
data-comments-provider=""
|
|
176
|
+
>
|
|
177
|
+
{children}
|
|
178
|
+
</div>
|
|
179
|
+
</CommentsContext.Provider>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Hook
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Hook to access the comments context.
|
|
189
|
+
* Must be used within a CommentsProvider.
|
|
190
|
+
*/
|
|
191
|
+
export function useComments(): CommentsContextValue {
|
|
192
|
+
const context = useContext(CommentsContext);
|
|
193
|
+
if (!context) {
|
|
194
|
+
throw new Error("useComments must be used within a CommentsProvider");
|
|
195
|
+
}
|
|
196
|
+
return context;
|
|
197
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReactionPicker Component
|
|
3
|
+
*
|
|
4
|
+
* A popup picker for selecting emoji reactions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useComments } from "./CommentsProvider";
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface ReactionPickerProps {
|
|
14
|
+
/** Callback when an emoji is selected */
|
|
15
|
+
onSelect: (emoji: string) => void;
|
|
16
|
+
/** Callback when the picker is closed */
|
|
17
|
+
onClose?: () => void;
|
|
18
|
+
/** Custom reaction choices (overrides provider) */
|
|
19
|
+
reactionChoices?: string[];
|
|
20
|
+
/** CSS class name */
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Component
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Emoji reaction picker popup.
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* ```tsx
|
|
33
|
+
* <ReactionPicker
|
|
34
|
+
* onSelect={(emoji) => toggleReaction({ messageId, emoji })}
|
|
35
|
+
* onClose={() => setShowPicker(false)}
|
|
36
|
+
* />
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function ReactionPicker({
|
|
40
|
+
onSelect,
|
|
41
|
+
onClose,
|
|
42
|
+
reactionChoices: customChoices,
|
|
43
|
+
className = "",
|
|
44
|
+
}: ReactionPickerProps) {
|
|
45
|
+
const { reactionChoices: providerChoices, styles } = useComments();
|
|
46
|
+
const choices = customChoices ?? providerChoices;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
className={`reaction-picker ${className}`}
|
|
51
|
+
style={{
|
|
52
|
+
display: "flex",
|
|
53
|
+
flexWrap: "wrap",
|
|
54
|
+
gap: "4px",
|
|
55
|
+
padding: "8px",
|
|
56
|
+
backgroundColor: "white",
|
|
57
|
+
borderRadius: styles?.borderRadius ?? "8px",
|
|
58
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
|
59
|
+
border: "1px solid #e5e7eb",
|
|
60
|
+
maxWidth: "200px",
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{choices.map((emoji) => (
|
|
64
|
+
<button
|
|
65
|
+
key={emoji}
|
|
66
|
+
onClick={() => {
|
|
67
|
+
onSelect(emoji);
|
|
68
|
+
onClose?.();
|
|
69
|
+
}}
|
|
70
|
+
style={{
|
|
71
|
+
width: "32px",
|
|
72
|
+
height: "32px",
|
|
73
|
+
display: "flex",
|
|
74
|
+
alignItems: "center",
|
|
75
|
+
justifyContent: "center",
|
|
76
|
+
border: "none",
|
|
77
|
+
background: "transparent",
|
|
78
|
+
borderRadius: "4px",
|
|
79
|
+
cursor: "pointer",
|
|
80
|
+
fontSize: "18px",
|
|
81
|
+
transition: "background-color 0.15s",
|
|
82
|
+
}}
|
|
83
|
+
onMouseEnter={(e) => {
|
|
84
|
+
e.currentTarget.style.backgroundColor = "#f3f4f6";
|
|
85
|
+
}}
|
|
86
|
+
onMouseLeave={(e) => {
|
|
87
|
+
e.currentTarget.style.backgroundColor = "transparent";
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{emoji}
|
|
91
|
+
</button>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|