@sqlrooms/discuss 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright 2025 Ilya Boyandin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ A simple discussion system for SQLRooms applications.
@@ -0,0 +1,375 @@
1
+ import { type ProjectBuilderState, type StateCreator, BaseProjectConfig } from '@sqlrooms/project-builder';
2
+ import { z } from 'zod';
3
+ export declare const CommentBase: z.ZodObject<{
4
+ id: z.ZodString;
5
+ userId: z.ZodString;
6
+ text: z.ZodString;
7
+ timestamp: z.ZodDate;
8
+ }, "strip", z.ZodTypeAny, {
9
+ id: string;
10
+ userId: string;
11
+ text: string;
12
+ timestamp: Date;
13
+ }, {
14
+ id: string;
15
+ userId: string;
16
+ text: string;
17
+ timestamp: Date;
18
+ }>;
19
+ export type CommentBase = z.infer<typeof CommentBase>;
20
+ export declare const Comment: z.ZodObject<z.objectUtil.extendShape<{
21
+ id: z.ZodString;
22
+ userId: z.ZodString;
23
+ text: z.ZodString;
24
+ timestamp: z.ZodDate;
25
+ }, {
26
+ parentId: z.ZodOptional<z.ZodString>;
27
+ }>, "strip", z.ZodTypeAny, {
28
+ id: string;
29
+ userId: string;
30
+ text: string;
31
+ timestamp: Date;
32
+ parentId?: string | undefined;
33
+ }, {
34
+ id: string;
35
+ userId: string;
36
+ text: string;
37
+ timestamp: Date;
38
+ parentId?: string | undefined;
39
+ }>;
40
+ export type Comment = z.infer<typeof Comment>;
41
+ export declare const Discussion: z.ZodObject<{
42
+ id: z.ZodString;
43
+ anchorId: z.ZodOptional<z.ZodString>;
44
+ rootComment: z.ZodObject<z.objectUtil.extendShape<{
45
+ id: z.ZodString;
46
+ userId: z.ZodString;
47
+ text: z.ZodString;
48
+ timestamp: z.ZodDate;
49
+ }, {
50
+ parentId: z.ZodOptional<z.ZodString>;
51
+ }>, "strip", z.ZodTypeAny, {
52
+ id: string;
53
+ userId: string;
54
+ text: string;
55
+ timestamp: Date;
56
+ parentId?: string | undefined;
57
+ }, {
58
+ id: string;
59
+ userId: string;
60
+ text: string;
61
+ timestamp: Date;
62
+ parentId?: string | undefined;
63
+ }>;
64
+ comments: z.ZodArray<z.ZodObject<z.objectUtil.extendShape<{
65
+ id: z.ZodString;
66
+ userId: z.ZodString;
67
+ text: z.ZodString;
68
+ timestamp: z.ZodDate;
69
+ }, {
70
+ parentId: z.ZodOptional<z.ZodString>;
71
+ }>, "strip", z.ZodTypeAny, {
72
+ id: string;
73
+ userId: string;
74
+ text: string;
75
+ timestamp: Date;
76
+ parentId?: string | undefined;
77
+ }, {
78
+ id: string;
79
+ userId: string;
80
+ text: string;
81
+ timestamp: Date;
82
+ parentId?: string | undefined;
83
+ }>, "many">;
84
+ }, "strip", z.ZodTypeAny, {
85
+ id: string;
86
+ rootComment: {
87
+ id: string;
88
+ userId: string;
89
+ text: string;
90
+ timestamp: Date;
91
+ parentId?: string | undefined;
92
+ };
93
+ comments: {
94
+ id: string;
95
+ userId: string;
96
+ text: string;
97
+ timestamp: Date;
98
+ parentId?: string | undefined;
99
+ }[];
100
+ anchorId?: string | undefined;
101
+ }, {
102
+ id: string;
103
+ rootComment: {
104
+ id: string;
105
+ userId: string;
106
+ text: string;
107
+ timestamp: Date;
108
+ parentId?: string | undefined;
109
+ };
110
+ comments: {
111
+ id: string;
112
+ userId: string;
113
+ text: string;
114
+ timestamp: Date;
115
+ parentId?: string | undefined;
116
+ }[];
117
+ anchorId?: string | undefined;
118
+ }>;
119
+ export type Discussion = z.infer<typeof Discussion>;
120
+ export declare const DiscussSliceConfig: z.ZodObject<{
121
+ discuss: z.ZodObject<{
122
+ discussions: z.ZodArray<z.ZodObject<{
123
+ id: z.ZodString;
124
+ anchorId: z.ZodOptional<z.ZodString>;
125
+ rootComment: z.ZodObject<z.objectUtil.extendShape<{
126
+ id: z.ZodString;
127
+ userId: z.ZodString;
128
+ text: z.ZodString;
129
+ timestamp: z.ZodDate;
130
+ }, {
131
+ parentId: z.ZodOptional<z.ZodString>;
132
+ }>, "strip", z.ZodTypeAny, {
133
+ id: string;
134
+ userId: string;
135
+ text: string;
136
+ timestamp: Date;
137
+ parentId?: string | undefined;
138
+ }, {
139
+ id: string;
140
+ userId: string;
141
+ text: string;
142
+ timestamp: Date;
143
+ parentId?: string | undefined;
144
+ }>;
145
+ comments: z.ZodArray<z.ZodObject<z.objectUtil.extendShape<{
146
+ id: z.ZodString;
147
+ userId: z.ZodString;
148
+ text: z.ZodString;
149
+ timestamp: z.ZodDate;
150
+ }, {
151
+ parentId: z.ZodOptional<z.ZodString>;
152
+ }>, "strip", z.ZodTypeAny, {
153
+ id: string;
154
+ userId: string;
155
+ text: string;
156
+ timestamp: Date;
157
+ parentId?: string | undefined;
158
+ }, {
159
+ id: string;
160
+ userId: string;
161
+ text: string;
162
+ timestamp: Date;
163
+ parentId?: string | undefined;
164
+ }>, "many">;
165
+ }, "strip", z.ZodTypeAny, {
166
+ id: string;
167
+ rootComment: {
168
+ id: string;
169
+ userId: string;
170
+ text: string;
171
+ timestamp: Date;
172
+ parentId?: string | undefined;
173
+ };
174
+ comments: {
175
+ id: string;
176
+ userId: string;
177
+ text: string;
178
+ timestamp: Date;
179
+ parentId?: string | undefined;
180
+ }[];
181
+ anchorId?: string | undefined;
182
+ }, {
183
+ id: string;
184
+ rootComment: {
185
+ id: string;
186
+ userId: string;
187
+ text: string;
188
+ timestamp: Date;
189
+ parentId?: string | undefined;
190
+ };
191
+ comments: {
192
+ id: string;
193
+ userId: string;
194
+ text: string;
195
+ timestamp: Date;
196
+ parentId?: string | undefined;
197
+ }[];
198
+ anchorId?: string | undefined;
199
+ }>, "many">;
200
+ }, "strip", z.ZodTypeAny, {
201
+ discussions: {
202
+ id: string;
203
+ rootComment: {
204
+ id: string;
205
+ userId: string;
206
+ text: string;
207
+ timestamp: Date;
208
+ parentId?: string | undefined;
209
+ };
210
+ comments: {
211
+ id: string;
212
+ userId: string;
213
+ text: string;
214
+ timestamp: Date;
215
+ parentId?: string | undefined;
216
+ }[];
217
+ anchorId?: string | undefined;
218
+ }[];
219
+ }, {
220
+ discussions: {
221
+ id: string;
222
+ rootComment: {
223
+ id: string;
224
+ userId: string;
225
+ text: string;
226
+ timestamp: Date;
227
+ parentId?: string | undefined;
228
+ };
229
+ comments: {
230
+ id: string;
231
+ userId: string;
232
+ text: string;
233
+ timestamp: Date;
234
+ parentId?: string | undefined;
235
+ }[];
236
+ anchorId?: string | undefined;
237
+ }[];
238
+ }>;
239
+ }, "strip", z.ZodTypeAny, {
240
+ discuss: {
241
+ discussions: {
242
+ id: string;
243
+ rootComment: {
244
+ id: string;
245
+ userId: string;
246
+ text: string;
247
+ timestamp: Date;
248
+ parentId?: string | undefined;
249
+ };
250
+ comments: {
251
+ id: string;
252
+ userId: string;
253
+ text: string;
254
+ timestamp: Date;
255
+ parentId?: string | undefined;
256
+ }[];
257
+ anchorId?: string | undefined;
258
+ }[];
259
+ };
260
+ }, {
261
+ discuss: {
262
+ discussions: {
263
+ id: string;
264
+ rootComment: {
265
+ id: string;
266
+ userId: string;
267
+ text: string;
268
+ timestamp: Date;
269
+ parentId?: string | undefined;
270
+ };
271
+ comments: {
272
+ id: string;
273
+ userId: string;
274
+ text: string;
275
+ timestamp: Date;
276
+ parentId?: string | undefined;
277
+ }[];
278
+ anchorId?: string | undefined;
279
+ }[];
280
+ };
281
+ }>;
282
+ export type DiscussSliceConfig = z.infer<typeof DiscussSliceConfig>;
283
+ export declare function createDefaultDiscussConfig(): DiscussSliceConfig;
284
+ export type ReplyToItem = {
285
+ discussionId: string;
286
+ commentId?: string;
287
+ };
288
+ export type EditingItem = {
289
+ discussionId: string;
290
+ commentId?: string;
291
+ };
292
+ export type DeleteItem = {
293
+ discussionId: string;
294
+ commentId?: string;
295
+ itemType: string;
296
+ };
297
+ export type DiscussSliceState = {
298
+ discuss: {
299
+ userId: string;
300
+ /**
301
+ * Submit content based on current UI state (add discussion, reply to discussion/comment, or edit).
302
+ * This automatically handles state management and is the preferred way to submit content.
303
+ */
304
+ submitEdit: (text: string) => void;
305
+ /**
306
+ * Current discussion or comment being replied to.
307
+ * Used by the form to determine context when submitting.
308
+ */
309
+ replyToItem: ReplyToItem | undefined;
310
+ /**
311
+ * Sets the discussion or comment being replied to.
312
+ * Will clear editing state if set.
313
+ */
314
+ setReplyToItem: (replyToItem: ReplyToItem | undefined) => void;
315
+ /**
316
+ * Current discussion or comment being edited.
317
+ * Used by the form to determine context when submitting.
318
+ */
319
+ editingItem: EditingItem | undefined;
320
+ /**
321
+ * Sets the discussion or comment being edited.
322
+ * Will clear replyTo state if set.
323
+ */
324
+ setEditingItem: (editingItem: EditingItem | undefined) => void;
325
+ /**
326
+ * Item currently targeted for deletion.
327
+ * Used by the delete confirmation dialog.
328
+ */
329
+ itemToDelete: DeleteItem | undefined;
330
+ /**
331
+ * Sets the discussion or comment to be deleted.
332
+ * Should be used before showing the confirmation dialog.
333
+ */
334
+ setItemToDelete: (item: DeleteItem | undefined) => void;
335
+ /**
336
+ * Currently highlighted discussion.
337
+ * Used to visually highlight a discussion in the UI.
338
+ */
339
+ highlightedDiscussionId: string | undefined;
340
+ /**
341
+ * Sets the highlighted discussion.
342
+ */
343
+ setHighlightedDiscussionId: (discussionId: string | undefined) => void;
344
+ /**
345
+ * Handles the confirmation of a delete operation.
346
+ * Should be called after the user confirms deletion in the UI.
347
+ */
348
+ handleDeleteConfirm: () => void;
349
+ /**
350
+ * Helper function to get the user ID of the entity being replied to.
351
+ * Returns '' if no reply context is set, or the user ID if a valid reply context exists.
352
+ */
353
+ getReplyToUserId: () => string;
354
+ /**
355
+ * Helper function to get the text of the item being edited.
356
+ * Returns '' if no editing context is set, or the text content if a valid editing context exists.
357
+ */
358
+ getEditingItemText: () => string;
359
+ addDiscussion: (text: string, anchorId?: string) => void;
360
+ editDiscussion: (id: string, text: string) => void;
361
+ removeDiscussion: (id: string) => void;
362
+ addComment: (discussionId: string, text: string, parentId?: string) => void;
363
+ editComment: (discussionId: string, commentId: string, text: string) => void;
364
+ removeComment: (discussionId: string, commentId: string) => void;
365
+ };
366
+ };
367
+ export type ProjectStateWithDiscussion = ProjectBuilderState<BaseProjectConfig> & DiscussSliceState;
368
+ export declare function createDiscussSlice<PC extends BaseProjectConfig & DiscussSliceConfig>({ userId }: {
369
+ userId: string;
370
+ }): StateCreator<DiscussSliceState>;
371
+ type ProjectConfigWithDiscuss = BaseProjectConfig & DiscussSliceConfig;
372
+ type ProjectStateWithDiscuss = ProjectBuilderState<ProjectConfigWithDiscuss> & DiscussSliceState;
373
+ export declare function useStoreWithDiscussion<T>(selector: (state: ProjectStateWithDiscuss) => T): T;
374
+ export {};
375
+ //# sourceMappingURL=DiscussSlice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DiscussSlice.d.ts","sourceRoot":"","sources":["../src/DiscussSlice.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,316 @@
1
+ import { createId } from '@paralleldrive/cuid2';
2
+ import { createSlice, useBaseProjectBuilderStore, } from '@sqlrooms/project-builder';
3
+ import { produce } from 'immer';
4
+ import { z } from 'zod';
5
+ // Base type with common fields
6
+ export const CommentBase = z.object({
7
+ id: z.string().cuid2(),
8
+ userId: z.string(),
9
+ text: z.string(),
10
+ timestamp: z.coerce.date(),
11
+ });
12
+ // Comment type extends base with parentId
13
+ export const Comment = CommentBase.extend({
14
+ parentId: z.string().optional(),
15
+ });
16
+ // Discussion is a container with a rootComment and collection of reply comments
17
+ export const Discussion = z.object({
18
+ id: z.string().cuid2(),
19
+ anchorId: z.string().optional(),
20
+ rootComment: Comment,
21
+ comments: z.array(Comment),
22
+ });
23
+ export const DiscussSliceConfig = z.object({
24
+ discuss: z.object({
25
+ discussions: z.array(Discussion),
26
+ }),
27
+ });
28
+ export function createDefaultDiscussConfig() {
29
+ return {
30
+ discuss: {
31
+ discussions: [],
32
+ },
33
+ };
34
+ }
35
+ export function createDiscussSlice({ userId }) {
36
+ return createSlice((set, get) => ({
37
+ discuss: {
38
+ userId,
39
+ // Direct CRUD operations - These are exposed for advanced use cases
40
+ // For normal usage with UI integration, prefer submitEdit
41
+ /**
42
+ * Directly adds a new discussion without managing UI state.
43
+ * For UI-integrated usage, prefer submitEdit.
44
+ */
45
+ addDiscussion: (text, anchorId) => {
46
+ const id = createId();
47
+ const rootComment = {
48
+ id: createId(),
49
+ userId,
50
+ text,
51
+ timestamp: new Date(),
52
+ };
53
+ const newDiscussion = {
54
+ id,
55
+ anchorId,
56
+ rootComment,
57
+ comments: [],
58
+ };
59
+ set((state) => produce(state, (draft) => {
60
+ draft.config.discuss.discussions.push(newDiscussion);
61
+ }));
62
+ },
63
+ /**
64
+ * Directly removes an discussion without managing UI state.
65
+ * For UI-integrated usage, prefer setting itemToDelete and using handleDeleteConfirm.
66
+ */
67
+ removeDiscussion: (id) => {
68
+ set((state) => produce(state, (draft) => {
69
+ const index = draft.config.discuss.discussions.findIndex((a) => a.id === id);
70
+ if (index !== -1) {
71
+ draft.config.discuss.discussions.splice(index, 1);
72
+ }
73
+ }));
74
+ },
75
+ /**
76
+ * Directly edits an discussion without managing UI state.
77
+ * For UI-integrated usage, prefer submitEdit.
78
+ */
79
+ editDiscussion: (id, text) => {
80
+ set((state) => produce(state, (draft) => {
81
+ const discussion = draft.config.discuss.discussions.find((a) => a.id === id);
82
+ if (discussion) {
83
+ discussion.rootComment.text = text;
84
+ }
85
+ }));
86
+ },
87
+ /**
88
+ * Directly adds a comment without managing UI state.
89
+ * For UI-integrated usage, prefer submitEdit.
90
+ */
91
+ addComment: (discussionId, text, parentId) => {
92
+ const newComment = {
93
+ id: createId(),
94
+ userId,
95
+ text,
96
+ timestamp: new Date(),
97
+ parentId,
98
+ };
99
+ set((state) => produce(state, (draft) => {
100
+ const discussion = draft.config.discuss.discussions.find((a) => a.id === discussionId);
101
+ if (discussion) {
102
+ discussion.comments.push(newComment);
103
+ }
104
+ }));
105
+ },
106
+ /**
107
+ * Directly edits a comment without managing UI state.
108
+ * For UI-integrated usage, prefer submitEdit.
109
+ */
110
+ editComment: (discussionId, commentId, text) => {
111
+ set((state) => produce(state, (draft) => {
112
+ const discussion = draft.config.discuss.discussions.find((a) => a.id === discussionId);
113
+ if (discussion) {
114
+ if (discussion.rootComment.id === commentId) {
115
+ discussion.rootComment.text = text;
116
+ }
117
+ else {
118
+ const comment = discussion.comments.find((c) => c.id === commentId);
119
+ if (comment) {
120
+ comment.text = text;
121
+ }
122
+ }
123
+ }
124
+ }));
125
+ },
126
+ /**
127
+ * Directly removes a comment without managing UI state.
128
+ * For UI-integrated usage, prefer setting itemToDelete and using handleDeleteConfirm.
129
+ */
130
+ removeComment: (discussionId, commentId) => {
131
+ set((state) => produce(state, (draft) => {
132
+ const discussion = draft.config.discuss.discussions.find((a) => a.id === discussionId);
133
+ if (discussion) {
134
+ // Cannot remove the root comment
135
+ if (discussion.rootComment.id !== commentId) {
136
+ const index = discussion.comments.findIndex((c) => c.id === commentId);
137
+ if (index !== -1) {
138
+ discussion.comments.splice(index, 1);
139
+ }
140
+ }
141
+ }
142
+ }));
143
+ },
144
+ // UI state management
145
+ /**
146
+ * Current discussion or comment being replied to.
147
+ * Used by the form to determine context when submitting.
148
+ */
149
+ replyToItem: undefined,
150
+ /**
151
+ * Sets the discussion or comment being replied to.
152
+ * Will clear editing state if set.
153
+ */
154
+ setReplyToItem: (replyToItem) => {
155
+ set((state) => produce(state, (draft) => {
156
+ draft.discuss.replyToItem = replyToItem;
157
+ if (replyToItem) {
158
+ draft.discuss.editingItem = undefined;
159
+ }
160
+ }));
161
+ },
162
+ /**
163
+ * Current discussion or comment being edited.
164
+ * Used by the form to determine context when submitting.
165
+ */
166
+ editingItem: undefined,
167
+ /**
168
+ * Sets the discussion or comment being edited.
169
+ * Will clear replyTo state if set.
170
+ */
171
+ setEditingItem: (editingItem) => {
172
+ set((state) => produce(state, (draft) => {
173
+ draft.discuss.editingItem = editingItem;
174
+ if (editingItem) {
175
+ draft.discuss.replyToItem = undefined;
176
+ }
177
+ }));
178
+ },
179
+ /**
180
+ * Item currently targeted for deletion.
181
+ * Used by the delete confirmation dialog.
182
+ */
183
+ itemToDelete: undefined,
184
+ /**
185
+ * Sets the discussion or comment to be deleted.
186
+ * Should be used before showing the confirmation dialog.
187
+ */
188
+ setItemToDelete: (item) => {
189
+ set((state) => produce(state, (draft) => {
190
+ draft.discuss.itemToDelete = item;
191
+ }));
192
+ },
193
+ /**
194
+ * Currently highlighted discussion.
195
+ * Used to visually highlight a discussion in the UI.
196
+ */
197
+ highlightedDiscussionId: undefined,
198
+ /**
199
+ * Sets the highlighted discussion.
200
+ */
201
+ setHighlightedDiscussionId: (discussionId) => {
202
+ set((state) => produce(state, (draft) => {
203
+ draft.discuss.highlightedDiscussionId = discussionId;
204
+ }));
205
+ },
206
+ /**
207
+ * Main form submission handler that processes content based on UI state.
208
+ * This is the preferred method to submit discussion/comment content.
209
+ *
210
+ * Will automatically:
211
+ * - Add a new discussion if no context is set
212
+ * - Add a comment as a reply if replyToItem is set
213
+ * - Edit an discussion or comment if editing is set
214
+ * - Clear UI state after submission
215
+ */
216
+ submitEdit: (text) => {
217
+ const state = get();
218
+ const { editingItem, replyToItem, addDiscussion, editDiscussion, editComment, addComment, } = state.discuss;
219
+ if (editingItem) {
220
+ if (editingItem.commentId) {
221
+ editComment(editingItem.discussionId, editingItem.commentId, text);
222
+ }
223
+ else {
224
+ editDiscussion(editingItem.discussionId, text);
225
+ }
226
+ state.discuss.setEditingItem(undefined);
227
+ }
228
+ else if (replyToItem) {
229
+ if (replyToItem.commentId) {
230
+ addComment(replyToItem.discussionId, text, replyToItem.commentId);
231
+ }
232
+ else {
233
+ addComment(replyToItem.discussionId, text);
234
+ }
235
+ state.discuss.setReplyToItem(undefined);
236
+ }
237
+ else {
238
+ addDiscussion(text);
239
+ }
240
+ },
241
+ /**
242
+ * Handles the confirmation of a delete operation.
243
+ * Should be called after the user confirms deletion in the UI.
244
+ *
245
+ * Will:
246
+ * - Delete the discussion or comment specified in itemToDelete
247
+ * - Clear the itemToDelete state
248
+ */
249
+ handleDeleteConfirm: () => {
250
+ const state = get();
251
+ const { itemToDelete, removeComment, removeDiscussion } = state.discuss;
252
+ if (itemToDelete) {
253
+ if (itemToDelete.commentId) {
254
+ removeComment(itemToDelete.discussionId, itemToDelete.commentId);
255
+ }
256
+ else {
257
+ removeDiscussion(itemToDelete.discussionId);
258
+ }
259
+ state.discuss.setItemToDelete(undefined);
260
+ }
261
+ },
262
+ /**
263
+ * Helper function to get the user ID of the entity being replied to.
264
+ * Returns '' if no reply context is set, or the user ID if a valid reply context exists.
265
+ */
266
+ getReplyToUserId: () => {
267
+ const state = get();
268
+ const { replyToItem } = state.discuss;
269
+ const { discussions } = state.config.discuss;
270
+ if (!replyToItem)
271
+ return '';
272
+ const discussion = discussions.find((a) => a.id === replyToItem.discussionId);
273
+ if (discussion) {
274
+ if (replyToItem.commentId) {
275
+ if (discussion.rootComment.id === replyToItem.commentId) {
276
+ return discussion.rootComment.userId;
277
+ }
278
+ const comment = discussion.comments.find((c) => c.id === replyToItem.commentId);
279
+ if (comment)
280
+ return comment.userId;
281
+ }
282
+ else {
283
+ return discussion.rootComment.userId;
284
+ }
285
+ }
286
+ return '';
287
+ },
288
+ /**
289
+ * Helper function to get the text of the item being edited.
290
+ * Returns '' if no editing context is set, or the text content if a valid editing context exists.
291
+ */
292
+ getEditingItemText: () => {
293
+ const state = get();
294
+ const { editingItem } = state.discuss;
295
+ const { discussions } = state.config.discuss;
296
+ if (!editingItem)
297
+ return '';
298
+ // Look for the discussion
299
+ const discussion = discussions.find((a) => a.id === editingItem.discussionId);
300
+ if (!discussion)
301
+ return '';
302
+ // If editing a comment, find the comment
303
+ if (editingItem.commentId) {
304
+ const comment = discussion.comments.find((c) => c.id === editingItem.commentId);
305
+ return comment ? comment.text : '';
306
+ }
307
+ // If editing the discussion itself
308
+ return discussion.rootComment.text;
309
+ },
310
+ },
311
+ }));
312
+ }
313
+ export function useStoreWithDiscussion(selector) {
314
+ return useBaseProjectBuilderStore((state) => selector(state));
315
+ }
316
+ //# sourceMappingURL=DiscussSlice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DiscussSlice.js","sourceRoot":"","sources":["../src/DiscussSlice.ts"],"names":[],"mappings":"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","sourcesContent":["import {createId} from '@paralleldrive/cuid2';\nimport {\n createSlice,\n useBaseProjectBuilderStore,\n type ProjectBuilderState,\n type StateCreator,\n BaseProjectConfig,\n} from '@sqlrooms/project-builder';\nimport {produce} from 'immer';\nimport {z} from 'zod';\n\n// Base type with common fields\nexport const CommentBase = z.object({\n id: z.string().cuid2(),\n userId: z.string(),\n text: z.string(),\n timestamp: z.coerce.date(),\n});\nexport type CommentBase = z.infer<typeof CommentBase>;\n\n// Comment type extends base with parentId\nexport const Comment = CommentBase.extend({\n parentId: z.string().optional(),\n});\nexport type Comment = z.infer<typeof Comment>;\n\n// Discussion is a container with a rootComment and collection of reply comments\nexport const Discussion = z.object({\n id: z.string().cuid2(),\n anchorId: z.string().optional(),\n rootComment: Comment,\n comments: z.array(Comment),\n});\nexport type Discussion = z.infer<typeof Discussion>;\n\nexport const DiscussSliceConfig = z.object({\n discuss: z.object({\n discussions: z.array(Discussion),\n }),\n});\nexport type DiscussSliceConfig = z.infer<typeof DiscussSliceConfig>;\n\nexport function createDefaultDiscussConfig(): DiscussSliceConfig {\n return {\n discuss: {\n discussions: [],\n },\n };\n}\n\n// UI state for discussions\nexport type ReplyToItem = {\n discussionId: string;\n commentId?: string;\n};\n\nexport type EditingItem = {\n discussionId: string;\n commentId?: string;\n};\n\nexport type DeleteItem = {\n discussionId: string;\n commentId?: string;\n itemType: string;\n};\n\nexport type DiscussSliceState = {\n discuss: {\n userId: string;\n\n // UI-connected actions - preferred API for most use cases\n /**\n * Submit content based on current UI state (add discussion, reply to discussion/comment, or edit).\n * This automatically handles state management and is the preferred way to submit content.\n */\n submitEdit: (text: string) => void;\n\n // UI state management\n /**\n * Current discussion or comment being replied to.\n * Used by the form to determine context when submitting.\n */\n replyToItem: ReplyToItem | undefined;\n /**\n * Sets the discussion or comment being replied to.\n * Will clear editing state if set.\n */\n setReplyToItem: (replyToItem: ReplyToItem | undefined) => void;\n\n /**\n * Current discussion or comment being edited.\n * Used by the form to determine context when submitting.\n */\n editingItem: EditingItem | undefined;\n /**\n * Sets the discussion or comment being edited.\n * Will clear replyTo state if set.\n */\n setEditingItem: (editingItem: EditingItem | undefined) => void;\n\n /**\n * Item currently targeted for deletion.\n * Used by the delete confirmation dialog.\n */\n itemToDelete: DeleteItem | undefined;\n /**\n * Sets the discussion or comment to be deleted.\n * Should be used before showing the confirmation dialog.\n */\n setItemToDelete: (item: DeleteItem | undefined) => void;\n\n /**\n * Currently highlighted discussion.\n * Used to visually highlight a discussion in the UI.\n */\n highlightedDiscussionId: string | undefined;\n /**\n * Sets the highlighted discussion.\n */\n setHighlightedDiscussionId: (discussionId: string | undefined) => void;\n\n // Delete confirmation handler\n /**\n * Handles the confirmation of a delete operation.\n * Should be called after the user confirms deletion in the UI.\n */\n handleDeleteConfirm: () => void;\n\n // Helpers\n /**\n * Helper function to get the user ID of the entity being replied to.\n * Returns '' if no reply context is set, or the user ID if a valid reply context exists.\n */\n getReplyToUserId: () => string;\n\n /**\n * Helper function to get the text of the item being edited.\n * Returns '' if no editing context is set, or the text content if a valid editing context exists.\n */\n getEditingItemText: () => string;\n\n // Direct CRUD operations - use these only for custom integrations\n // that don't use the built-in UI state management\n addDiscussion: (text: string, anchorId?: string) => void;\n editDiscussion: (id: string, text: string) => void;\n removeDiscussion: (id: string) => void;\n addComment: (discussionId: string, text: string, parentId?: string) => void;\n editComment: (\n discussionId: string,\n commentId: string,\n text: string,\n ) => void;\n removeComment: (discussionId: string, commentId: string) => void;\n };\n};\n\nexport type ProjectStateWithDiscussion =\n ProjectBuilderState<BaseProjectConfig> & DiscussSliceState;\n\nexport function createDiscussSlice<\n PC extends BaseProjectConfig & DiscussSliceConfig,\n>({userId}: {userId: string}): StateCreator<DiscussSliceState> {\n return createSlice<PC, DiscussSliceState>((set, get) => ({\n discuss: {\n userId,\n\n // Direct CRUD operations - These are exposed for advanced use cases\n // For normal usage with UI integration, prefer submitEdit\n\n /**\n * Directly adds a new discussion without managing UI state.\n * For UI-integrated usage, prefer submitEdit.\n */\n addDiscussion: (text, anchorId) => {\n const id = createId();\n const rootComment: Comment = {\n id: createId(),\n userId,\n text,\n timestamp: new Date(),\n };\n\n const newDiscussion: Discussion = {\n id,\n anchorId,\n rootComment,\n comments: [],\n };\n\n set((state) =>\n produce(state, (draft) => {\n draft.config.discuss.discussions.push(newDiscussion);\n }),\n );\n },\n\n /**\n * Directly removes an discussion without managing UI state.\n * For UI-integrated usage, prefer setting itemToDelete and using handleDeleteConfirm.\n */\n removeDiscussion: (id) => {\n set((state) =>\n produce(state, (draft) => {\n const index = draft.config.discuss.discussions.findIndex(\n (a) => a.id === id,\n );\n if (index !== -1) {\n draft.config.discuss.discussions.splice(index, 1);\n }\n }),\n );\n },\n\n /**\n * Directly edits an discussion without managing UI state.\n * For UI-integrated usage, prefer submitEdit.\n */\n editDiscussion: (id, text) => {\n set((state) =>\n produce(state, (draft) => {\n const discussion = draft.config.discuss.discussions.find(\n (a) => a.id === id,\n );\n if (discussion) {\n discussion.rootComment.text = text;\n }\n }),\n );\n },\n\n /**\n * Directly adds a comment without managing UI state.\n * For UI-integrated usage, prefer submitEdit.\n */\n addComment: (discussionId, text, parentId) => {\n const newComment: Comment = {\n id: createId(),\n userId,\n text,\n timestamp: new Date(),\n parentId,\n };\n\n set((state) =>\n produce(state, (draft) => {\n const discussion = draft.config.discuss.discussions.find(\n (a) => a.id === discussionId,\n );\n if (discussion) {\n discussion.comments.push(newComment);\n }\n }),\n );\n },\n\n /**\n * Directly edits a comment without managing UI state.\n * For UI-integrated usage, prefer submitEdit.\n */\n editComment: (discussionId, commentId, text) => {\n set((state) =>\n produce(state, (draft) => {\n const discussion = draft.config.discuss.discussions.find(\n (a) => a.id === discussionId,\n );\n if (discussion) {\n if (discussion.rootComment.id === commentId) {\n discussion.rootComment.text = text;\n } else {\n const comment = discussion.comments.find(\n (c) => c.id === commentId,\n );\n if (comment) {\n comment.text = text;\n }\n }\n }\n }),\n );\n },\n\n /**\n * Directly removes a comment without managing UI state.\n * For UI-integrated usage, prefer setting itemToDelete and using handleDeleteConfirm.\n */\n removeComment: (discussionId, commentId) => {\n set((state) =>\n produce(state, (draft) => {\n const discussion = draft.config.discuss.discussions.find(\n (a) => a.id === discussionId,\n );\n if (discussion) {\n // Cannot remove the root comment\n if (discussion.rootComment.id !== commentId) {\n const index = discussion.comments.findIndex(\n (c) => c.id === commentId,\n );\n if (index !== -1) {\n discussion.comments.splice(index, 1);\n }\n }\n }\n }),\n );\n },\n\n // UI state management\n /**\n * Current discussion or comment being replied to.\n * Used by the form to determine context when submitting.\n */\n replyToItem: undefined,\n /**\n * Sets the discussion or comment being replied to.\n * Will clear editing state if set.\n */\n setReplyToItem: (replyToItem) => {\n set((state) =>\n produce(state, (draft) => {\n draft.discuss.replyToItem = replyToItem;\n if (replyToItem) {\n draft.discuss.editingItem = undefined;\n }\n }),\n );\n },\n\n /**\n * Current discussion or comment being edited.\n * Used by the form to determine context when submitting.\n */\n editingItem: undefined,\n /**\n * Sets the discussion or comment being edited.\n * Will clear replyTo state if set.\n */\n setEditingItem: (editingItem) => {\n set((state) =>\n produce(state, (draft) => {\n draft.discuss.editingItem = editingItem;\n if (editingItem) {\n draft.discuss.replyToItem = undefined;\n }\n }),\n );\n },\n\n /**\n * Item currently targeted for deletion.\n * Used by the delete confirmation dialog.\n */\n itemToDelete: undefined,\n /**\n * Sets the discussion or comment to be deleted.\n * Should be used before showing the confirmation dialog.\n */\n setItemToDelete: (item) => {\n set((state) =>\n produce(state, (draft) => {\n draft.discuss.itemToDelete = item;\n }),\n );\n },\n\n /**\n * Currently highlighted discussion.\n * Used to visually highlight a discussion in the UI.\n */\n highlightedDiscussionId: undefined,\n /**\n * Sets the highlighted discussion.\n */\n setHighlightedDiscussionId: (discussionId) => {\n set((state) =>\n produce(state, (draft) => {\n draft.discuss.highlightedDiscussionId = discussionId;\n }),\n );\n },\n\n /**\n * Main form submission handler that processes content based on UI state.\n * This is the preferred method to submit discussion/comment content.\n *\n * Will automatically:\n * - Add a new discussion if no context is set\n * - Add a comment as a reply if replyToItem is set\n * - Edit an discussion or comment if editing is set\n * - Clear UI state after submission\n */\n submitEdit: (text) => {\n const state = get();\n const {\n editingItem,\n replyToItem,\n addDiscussion,\n editDiscussion,\n editComment,\n addComment,\n } = state.discuss;\n\n if (editingItem) {\n if (editingItem.commentId) {\n editComment(editingItem.discussionId, editingItem.commentId, text);\n } else {\n editDiscussion(editingItem.discussionId, text);\n }\n state.discuss.setEditingItem(undefined);\n } else if (replyToItem) {\n if (replyToItem.commentId) {\n addComment(replyToItem.discussionId, text, replyToItem.commentId);\n } else {\n addComment(replyToItem.discussionId, text);\n }\n state.discuss.setReplyToItem(undefined);\n } else {\n addDiscussion(text);\n }\n },\n\n /**\n * Handles the confirmation of a delete operation.\n * Should be called after the user confirms deletion in the UI.\n *\n * Will:\n * - Delete the discussion or comment specified in itemToDelete\n * - Clear the itemToDelete state\n */\n handleDeleteConfirm: () => {\n const state = get();\n const {itemToDelete, removeComment, removeDiscussion} = state.discuss;\n\n if (itemToDelete) {\n if (itemToDelete.commentId) {\n removeComment(itemToDelete.discussionId, itemToDelete.commentId);\n } else {\n removeDiscussion(itemToDelete.discussionId);\n }\n state.discuss.setItemToDelete(undefined);\n }\n },\n\n /**\n * Helper function to get the user ID of the entity being replied to.\n * Returns '' if no reply context is set, or the user ID if a valid reply context exists.\n */\n getReplyToUserId: () => {\n const state = get();\n const {replyToItem} = state.discuss;\n const {discussions} = state.config.discuss;\n\n if (!replyToItem) return '';\n\n const discussion = discussions.find(\n (a) => a.id === replyToItem.discussionId,\n );\n\n if (discussion) {\n if (replyToItem.commentId) {\n if (discussion.rootComment.id === replyToItem.commentId) {\n return discussion.rootComment.userId;\n }\n const comment = discussion.comments.find(\n (c) => c.id === replyToItem.commentId,\n );\n if (comment) return comment.userId;\n } else {\n return discussion.rootComment.userId;\n }\n }\n\n return '';\n },\n\n /**\n * Helper function to get the text of the item being edited.\n * Returns '' if no editing context is set, or the text content if a valid editing context exists.\n */\n getEditingItemText: () => {\n const state = get();\n const {editingItem} = state.discuss;\n const {discussions} = state.config.discuss;\n\n if (!editingItem) return '';\n\n // Look for the discussion\n const discussion = discussions.find(\n (a) => a.id === editingItem.discussionId,\n );\n if (!discussion) return '';\n\n // If editing a comment, find the comment\n if (editingItem.commentId) {\n const comment = discussion.comments.find(\n (c) => c.id === editingItem.commentId,\n );\n return comment ? comment.text : '';\n }\n\n // If editing the discussion itself\n return discussion.rootComment.text;\n },\n },\n }));\n}\n\ntype ProjectConfigWithDiscuss = BaseProjectConfig & DiscussSliceConfig;\ntype ProjectStateWithDiscuss = ProjectBuilderState<ProjectConfigWithDiscuss> &\n DiscussSliceState;\n\nexport function useStoreWithDiscussion<T>(\n selector: (state: ProjectStateWithDiscuss) => T,\n): T {\n return useBaseProjectBuilderStore<\n BaseProjectConfig & DiscussSliceConfig,\n ProjectStateWithDiscuss,\n T\n >((state) => selector(state as ProjectStateWithDiscuss));\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import { ReactNode } from 'react';
2
+ import { CommentItemProps } from './components/CommentItem';
3
+ import { DiscussionItemProps } from './components/DiscussionItem';
4
+ export declare const DiscussionList: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
5
+ renderDiscussion?: (props: DiscussionItemProps) => ReactNode;
6
+ renderComment?: (props: CommentItemProps) => ReactNode;
7
+ } & import("react").RefAttributes<HTMLDivElement>>;
8
+ //# sourceMappingURL=DiscussionList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DiscussionList.d.ts","sourceRoot":"","sources":["../src/DiscussionList.tsx"],"names":[],"mappings":"AACA,OAAO,EAIL,SAAS,EAGV,MAAM,OAAO,CAAC;AACf,OAAO,EAAC,gBAAgB,EAAuB,MAAM,0BAA0B,CAAC;AAEhF,OAAO,EAAiB,mBAAmB,EAAC,MAAM,6BAA6B,CAAC;AAchF,eAAO,MAAM,cAAc;uBARN,CAAC,KAAK,EAAE,mBAAmB,KAAK,SAAS;oBAC5C,CAAC,KAAK,EAAE,gBAAgB,KAAK,SAAS;kDAiNvD,CAAC"}
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from '@sqlrooms/ui';
3
+ import { forwardRef, Fragment, useEffect, useRef, } from 'react';
4
+ import { defaultRenderComment } from './components/CommentItem';
5
+ import { DeleteConfirmDialog } from './components/DeleteConfirmDialog';
6
+ import { DiscussionItem } from './components/DiscussionItem';
7
+ import { EditCommentForm } from './components/EditCommentForm';
8
+ import { useStoreWithDiscussion } from './DiscussSlice';
9
+ const defaultRenderDiscussion = (props) => (_jsx(DiscussionItem, { ...props }));
10
+ export const DiscussionList = forwardRef(({ className, renderComment = defaultRenderComment, renderDiscussion = defaultRenderDiscussion, ...props }, ref) => {
11
+ const highlightedDiscussionId = useStoreWithDiscussion((state) => state.discuss.highlightedDiscussionId);
12
+ const discussions = useStoreWithDiscussion((state) => state.config.discuss.discussions);
13
+ const replyToItem = useStoreWithDiscussion((state) => state.discuss.replyToItem);
14
+ const editingItem = useStoreWithDiscussion((state) => state.discuss.editingItem);
15
+ const itemToDelete = useStoreWithDiscussion((state) => state.discuss.itemToDelete);
16
+ const setReplyToItem = useStoreWithDiscussion((state) => state.discuss.setReplyToItem);
17
+ const setEditingItem = useStoreWithDiscussion((state) => state.discuss.setEditingItem);
18
+ const setItemToDelete = useStoreWithDiscussion((state) => state.discuss.setItemToDelete);
19
+ const submitEdit = useStoreWithDiscussion((state) => state.discuss.submitEdit);
20
+ const handleDeleteConfirm = useStoreWithDiscussion((state) => state.discuss.handleDeleteConfirm);
21
+ const getEditingItemText = useStoreWithDiscussion((state) => state.discuss.getEditingItemText);
22
+ // Reference to highlighted discussion
23
+ const highlightedRef = useRef(null);
24
+ // Scroll to highlighted discussion
25
+ useEffect(() => {
26
+ if (highlightedDiscussionId && highlightedRef.current) {
27
+ highlightedRef.current.scrollIntoView({
28
+ behavior: 'smooth',
29
+ block: 'start',
30
+ });
31
+ }
32
+ }, [highlightedDiscussionId]);
33
+ // Get the context content (the post being replied to or edited)
34
+ const getContextContent = () => {
35
+ if (editingItem) {
36
+ // When editing, show the original content
37
+ const discussion = discussions.find((d) => d.id === editingItem.discussionId);
38
+ if (!discussion)
39
+ return null;
40
+ if (editingItem.commentId) {
41
+ // Editing a comment
42
+ const comment = discussion.comments.find((c) => c.id === editingItem.commentId);
43
+ if (!comment)
44
+ return null;
45
+ return renderComment({ comment, discussion });
46
+ }
47
+ else {
48
+ // Editing the root discussion
49
+ return renderComment({
50
+ comment: discussion.rootComment,
51
+ discussion,
52
+ });
53
+ }
54
+ }
55
+ if (replyToItem) {
56
+ // When replying, show what we're replying to
57
+ const discussion = discussions.find((d) => d.id === replyToItem.discussionId);
58
+ if (!discussion)
59
+ return null;
60
+ if (replyToItem.commentId) {
61
+ // Replying to a comment
62
+ const comment = discussion.comments.find((c) => c.id === replyToItem.commentId);
63
+ if (!comment)
64
+ return null;
65
+ return renderComment({ comment, discussion });
66
+ }
67
+ else {
68
+ // Replying to the root discussion
69
+ return renderComment({
70
+ comment: discussion.rootComment,
71
+ discussion,
72
+ });
73
+ }
74
+ }
75
+ return null;
76
+ };
77
+ const editingContext = getContextContent();
78
+ return (_jsxs("div", { ref: ref, className: cn('flex h-full flex-col overflow-hidden', className), ...props, children: [_jsx("div", { className: "flex-1 overflow-y-auto", children: _jsxs("div", { className: "flex flex-col gap-4 p-2", children: [discussions.map((discussion) => (_jsx(Fragment, { children: renderDiscussion({
79
+ discussion,
80
+ renderComment,
81
+ ref: highlightedDiscussionId === discussion.id
82
+ ? highlightedRef
83
+ : undefined,
84
+ className: cn('flex flex-col gap-4 rounded border p-2', highlightedDiscussionId === discussion.id &&
85
+ 'border-blue-500 shadow-md transition-all duration-500'),
86
+ }) }, discussion.id))), _jsx("div", { className: "h-20" })] }) }), _jsxs("div", { className: "bg-background sticky bottom-0 border-t p-2 shadow-lg", children: [editingContext ? (_jsxs("div", { className: "flex flex-col gap-2 p-1", children: [_jsx("div", { className: "text-muted-foreground text-xs", children: replyToItem ? 'Replying to:' : 'Editing:' }), _jsx("div", { className: "bg-muted rounded border p-2", children: editingContext })] })) : null, editingItem && (_jsx(EditCommentForm, { onSubmit: submitEdit, initialText: getEditingItemText(), submitLabel: "Save", onCancel: () => {
87
+ setEditingItem(undefined);
88
+ } })), !editingItem && replyToItem && (_jsx(EditCommentForm, { onSubmit: submitEdit, initialText: "", submitLabel: "Reply", onCancel: () => {
89
+ setReplyToItem(undefined);
90
+ } })), !editingItem && !replyToItem && (_jsx(EditCommentForm, { onSubmit: submitEdit, initialText: "", submitLabel: "Post" }))] }), _jsx(DeleteConfirmDialog, { open: !!itemToDelete, onOpenChange: (open) => {
91
+ if (!open)
92
+ setItemToDelete(undefined);
93
+ }, onConfirm: handleDeleteConfirm, itemType: itemToDelete?.itemType || 'Item' })] }));
94
+ });
95
+ DiscussionList.displayName = 'DiscussionList';
96
+ //# sourceMappingURL=DiscussionList.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DiscussionList.js","sourceRoot":"","sources":["../src/DiscussionList.tsx"],"names":[],"mappings":"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","sourcesContent":["import {cn} from '@sqlrooms/ui';\nimport {\n ComponentPropsWithoutRef,\n forwardRef,\n Fragment,\n ReactNode,\n useEffect,\n useRef,\n} from 'react';\nimport {CommentItemProps, defaultRenderComment} from './components/CommentItem';\nimport {DeleteConfirmDialog} from './components/DeleteConfirmDialog';\nimport {DiscussionItem, DiscussionItemProps} from './components/DiscussionItem';\nimport {EditCommentForm} from './components/EditCommentForm';\nimport {useStoreWithDiscussion} from './DiscussSlice';\n\n// Main DiscussionList component\ntype DiscussionListProps = ComponentPropsWithoutRef<'div'> & {\n renderDiscussion?: (props: DiscussionItemProps) => ReactNode;\n renderComment?: (props: CommentItemProps) => ReactNode;\n};\n\nconst defaultRenderDiscussion = (props: DiscussionItemProps) => (\n <DiscussionItem {...props} />\n);\n\nexport const DiscussionList = forwardRef<HTMLDivElement, DiscussionListProps>(\n (\n {\n className,\n renderComment = defaultRenderComment,\n renderDiscussion = defaultRenderDiscussion,\n\n ...props\n },\n ref,\n ) => {\n const highlightedDiscussionId = useStoreWithDiscussion(\n (state) => state.discuss.highlightedDiscussionId,\n );\n const discussions = useStoreWithDiscussion(\n (state) => state.config.discuss.discussions,\n );\n const replyToItem = useStoreWithDiscussion(\n (state) => state.discuss.replyToItem,\n );\n const editingItem = useStoreWithDiscussion(\n (state) => state.discuss.editingItem,\n );\n const itemToDelete = useStoreWithDiscussion(\n (state) => state.discuss.itemToDelete,\n );\n const setReplyToItem = useStoreWithDiscussion(\n (state) => state.discuss.setReplyToItem,\n );\n const setEditingItem = useStoreWithDiscussion(\n (state) => state.discuss.setEditingItem,\n );\n const setItemToDelete = useStoreWithDiscussion(\n (state) => state.discuss.setItemToDelete,\n );\n const submitEdit = useStoreWithDiscussion(\n (state) => state.discuss.submitEdit,\n );\n const handleDeleteConfirm = useStoreWithDiscussion(\n (state) => state.discuss.handleDeleteConfirm,\n );\n const getEditingItemText = useStoreWithDiscussion(\n (state) => state.discuss.getEditingItemText,\n );\n\n // Reference to highlighted discussion\n const highlightedRef = useRef<HTMLDivElement>(null);\n\n // Scroll to highlighted discussion\n useEffect(() => {\n if (highlightedDiscussionId && highlightedRef.current) {\n highlightedRef.current.scrollIntoView({\n behavior: 'smooth',\n block: 'start',\n });\n }\n }, [highlightedDiscussionId]);\n\n // Get the context content (the post being replied to or edited)\n const getContextContent = (): ReactNode => {\n if (editingItem) {\n // When editing, show the original content\n const discussion = discussions.find(\n (d) => d.id === editingItem.discussionId,\n );\n if (!discussion) return null;\n\n if (editingItem.commentId) {\n // Editing a comment\n const comment = discussion.comments.find(\n (c) => c.id === editingItem.commentId,\n );\n if (!comment) return null;\n\n return renderComment({comment, discussion});\n } else {\n // Editing the root discussion\n return renderComment({\n comment: discussion.rootComment,\n discussion,\n });\n }\n }\n\n if (replyToItem) {\n // When replying, show what we're replying to\n const discussion = discussions.find(\n (d) => d.id === replyToItem.discussionId,\n );\n if (!discussion) return null;\n\n if (replyToItem.commentId) {\n // Replying to a comment\n const comment = discussion.comments.find(\n (c) => c.id === replyToItem.commentId,\n );\n if (!comment) return null;\n\n return renderComment({comment, discussion});\n } else {\n // Replying to the root discussion\n return renderComment({\n comment: discussion.rootComment,\n discussion,\n });\n }\n }\n\n return null;\n };\n\n const editingContext = getContextContent();\n\n return (\n <div\n ref={ref}\n className={cn('flex h-full flex-col overflow-hidden', className)}\n {...props}\n >\n {/* Scrollable discussion list */}\n <div className=\"flex-1 overflow-y-auto\">\n <div className=\"flex flex-col gap-4 p-2\">\n {discussions.map((discussion) => (\n <Fragment key={discussion.id}>\n {renderDiscussion({\n discussion,\n renderComment,\n ref:\n highlightedDiscussionId === discussion.id\n ? highlightedRef\n : undefined,\n className: cn(\n 'flex flex-col gap-4 rounded border p-2',\n highlightedDiscussionId === discussion.id &&\n 'border-blue-500 shadow-md transition-all duration-500',\n ),\n })}\n </Fragment>\n ))}\n\n {/* Add padding at the bottom to prevent content from being hidden behind sticky form */}\n <div className=\"h-20\" />\n </div>\n </div>\n\n {/* Sticky form at the bottom */}\n <div className=\"bg-background sticky bottom-0 border-t p-2 shadow-lg\">\n {editingContext ? (\n <div className=\"flex flex-col gap-2 p-1\">\n <div className=\"text-muted-foreground text-xs\">\n {replyToItem ? 'Replying to:' : 'Editing:'}\n </div>\n <div className=\"bg-muted rounded border p-2\">\n {editingContext}\n </div>\n </div>\n ) : null}\n {/* Show editing form when editing */}\n {editingItem && (\n <EditCommentForm\n onSubmit={submitEdit}\n initialText={getEditingItemText()}\n submitLabel=\"Save\"\n onCancel={() => {\n setEditingItem(undefined);\n }}\n />\n )}\n\n {/* Show reply form when replying */}\n {!editingItem && replyToItem && (\n <EditCommentForm\n onSubmit={submitEdit}\n initialText=\"\"\n submitLabel=\"Reply\"\n onCancel={() => {\n setReplyToItem(undefined);\n }}\n />\n )}\n\n {/* Show form for new discussions only when not replying or editing */}\n {!editingItem && !replyToItem && (\n <EditCommentForm\n onSubmit={submitEdit}\n initialText=\"\"\n submitLabel=\"Post\"\n />\n )}\n </div>\n\n <DeleteConfirmDialog\n open={!!itemToDelete}\n onOpenChange={(open) => {\n if (!open) setItemToDelete(undefined);\n }}\n onConfirm={handleDeleteConfirm}\n itemType={itemToDelete?.itemType || 'Item'}\n />\n </div>\n );\n },\n);\nDiscussionList.displayName = 'DiscussionList';\n"]}
@@ -0,0 +1,11 @@
1
+ import { ComponentPropsWithRef, ReactNode } from 'react';
2
+ import type { Comment, Discussion } from '../DiscussSlice';
3
+ export type CommentItemProps = ComponentPropsWithRef<'div'> & {
4
+ discussion: Discussion;
5
+ comment: Comment;
6
+ isRootComment?: boolean;
7
+ className?: string;
8
+ };
9
+ export declare const defaultRenderComment: (props: CommentItemProps) => ReactNode;
10
+ export declare const CommentItem: import("react").ForwardRefExoticComponent<Omit<CommentItemProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
11
+ //# sourceMappingURL=CommentItem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentItem.d.ts","sourceRoot":"","sources":["../../src/components/CommentItem.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAC,qBAAqB,EAAc,SAAS,EAAC,MAAM,OAAO,CAAC;AACnE,OAAO,KAAK,EAAC,OAAO,EAAE,UAAU,EAAC,MAAM,iBAAiB,CAAC;AAGzD,MAAM,MAAM,gBAAgB,GAAG,qBAAqB,CAAC,KAAK,CAAC,GAAG;IAC5D,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAGF,eAAO,MAAM,oBAAoB,GAAI,OAAO,gBAAgB,KAAG,SAY9D,CAAC;AAEF,eAAO,MAAM,WAAW,0HAsFvB,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Button, cn, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@sqlrooms/ui';
3
+ import { formatTimeRelative } from '@sqlrooms/utils';
4
+ import { Edit, MessageSquareReply, Trash2 } from 'lucide-react';
5
+ import { forwardRef } from 'react';
6
+ import { useStoreWithDiscussion } from '../DiscussSlice';
7
+ // Default implementation for rendering a comment's content
8
+ export const defaultRenderComment = (props) => {
9
+ const { comment } = props;
10
+ return (_jsx(CommentItem, { ...props, children: _jsxs("div", { className: "flex flex-col gap-1", children: [_jsxs("div", { className: "text-muted-foreground text-xs", children: ["Anonymous - ", formatTimeRelative(comment.timestamp)] }), _jsx("div", { className: "whitespace-pre-wrap text-sm", children: comment.text })] }) }));
11
+ };
12
+ export const CommentItem = forwardRef(({ discussion, comment, isRootComment = false, className, children }, ref) => {
13
+ const userId = useStoreWithDiscussion((state) => state.discuss.userId);
14
+ const discussionId = discussion.id;
15
+ const setReplyToItem = useStoreWithDiscussion((state) => state.discuss.setReplyToItem);
16
+ const setEditingItem = useStoreWithDiscussion((state) => state.discuss.setEditingItem);
17
+ const setItemToDelete = useStoreWithDiscussion((state) => state.discuss.setItemToDelete);
18
+ const handleReply = () => {
19
+ if (isRootComment) {
20
+ setReplyToItem({ discussionId });
21
+ }
22
+ else {
23
+ setReplyToItem({ discussionId, commentId: comment.id });
24
+ }
25
+ };
26
+ const handleEdit = () => {
27
+ if (isRootComment) {
28
+ setEditingItem({ discussionId });
29
+ }
30
+ else {
31
+ setEditingItem({ discussionId, commentId: comment.id });
32
+ }
33
+ };
34
+ const handleDelete = () => {
35
+ if (isRootComment) {
36
+ setItemToDelete({ discussionId, itemType: 'Discussion' });
37
+ }
38
+ else {
39
+ setItemToDelete({
40
+ discussionId,
41
+ commentId: comment.id,
42
+ itemType: 'Comment',
43
+ });
44
+ }
45
+ };
46
+ return (_jsx(TooltipProvider, { children: _jsxs("div", { ref: ref, className: cn('flex flex-col gap-2', className), children: [children, _jsxs("div", { className: "mt-1 flex justify-end gap-1 text-xs", children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "xs", onClick: handleReply, children: _jsx(MessageSquareReply, { className: "h-3 w-3" }) }) }), _jsx(TooltipContent, { children: _jsx("p", { children: "Reply" }) })] }), comment.userId === userId && (_jsxs(_Fragment, { children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "xs", onClick: handleEdit, children: _jsx(Edit, { className: "h-3 w-3" }) }) }), _jsx(TooltipContent, { children: _jsx("p", { children: "Edit" }) })] }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "xs", onClick: handleDelete, children: _jsx(Trash2, { className: "h-3 w-3" }) }) }), _jsx(TooltipContent, { children: _jsx("p", { children: "Delete" }) })] })] }))] })] }) }));
47
+ });
48
+ CommentItem.displayName = 'CommentItem';
49
+ //# sourceMappingURL=CommentItem.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentItem.js","sourceRoot":"","sources":["../../src/components/CommentItem.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n Button,\n cn,\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from '@sqlrooms/ui';\nimport {formatTimeRelative} from '@sqlrooms/utils';\nimport {Edit, MessageSquareReply, Trash2} from 'lucide-react';\nimport {ComponentPropsWithRef, forwardRef, ReactNode} from 'react';\nimport type {Comment, Discussion} from '../DiscussSlice';\nimport {useStoreWithDiscussion} from '../DiscussSlice';\n\nexport type CommentItemProps = ComponentPropsWithRef<'div'> & {\n discussion: Discussion;\n comment: Comment;\n isRootComment?: boolean;\n className?: string;\n};\n\n// Default implementation for rendering a comment's content\nexport const defaultRenderComment = (props: CommentItemProps): ReactNode => {\n const {comment} = props;\n return (\n <CommentItem {...props}>\n <div className=\"flex flex-col gap-1\">\n <div className=\"text-muted-foreground text-xs\">\n Anonymous - {formatTimeRelative(comment.timestamp)}\n </div>\n <div className=\"whitespace-pre-wrap text-sm\">{comment.text}</div>\n </div>\n </CommentItem>\n );\n};\n\nexport const CommentItem = forwardRef<HTMLDivElement, CommentItemProps>(\n ({discussion, comment, isRootComment = false, className, children}, ref) => {\n const userId = useStoreWithDiscussion((state) => state.discuss.userId);\n const discussionId = discussion.id;\n const setReplyToItem = useStoreWithDiscussion(\n (state) => state.discuss.setReplyToItem,\n );\n const setEditingItem = useStoreWithDiscussion(\n (state) => state.discuss.setEditingItem,\n );\n const setItemToDelete = useStoreWithDiscussion(\n (state) => state.discuss.setItemToDelete,\n );\n\n const handleReply = () => {\n if (isRootComment) {\n setReplyToItem({discussionId});\n } else {\n setReplyToItem({discussionId, commentId: comment.id});\n }\n };\n\n const handleEdit = () => {\n if (isRootComment) {\n setEditingItem({discussionId});\n } else {\n setEditingItem({discussionId, commentId: comment.id});\n }\n };\n\n const handleDelete = () => {\n if (isRootComment) {\n setItemToDelete({discussionId, itemType: 'Discussion'});\n } else {\n setItemToDelete({\n discussionId,\n commentId: comment.id,\n itemType: 'Comment',\n });\n }\n };\n\n return (\n <TooltipProvider>\n <div ref={ref} className={cn('flex flex-col gap-2', className)}>\n {children}\n <div className=\"mt-1 flex justify-end gap-1 text-xs\">\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"ghost\" size=\"xs\" onClick={handleReply}>\n <MessageSquareReply className=\"h-3 w-3\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>\n <p>Reply</p>\n </TooltipContent>\n </Tooltip>\n {comment.userId === userId && (\n <>\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"ghost\" size=\"xs\" onClick={handleEdit}>\n <Edit className=\"h-3 w-3\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>\n <p>Edit</p>\n </TooltipContent>\n </Tooltip>\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"ghost\" size=\"xs\" onClick={handleDelete}>\n <Trash2 className=\"h-3 w-3\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>\n <p>Delete</p>\n </TooltipContent>\n </Tooltip>\n </>\n )}\n </div>\n </div>\n </TooltipProvider>\n );\n },\n);\nCommentItem.displayName = 'CommentItem';\n"]}
@@ -0,0 +1,8 @@
1
+ export type DeleteConfirmDialogProps = {
2
+ open: boolean;
3
+ onOpenChange: (open: boolean) => void;
4
+ onConfirm: () => void;
5
+ itemType: string;
6
+ };
7
+ export declare const DeleteConfirmDialog: ({ open, onOpenChange, onConfirm, itemType, }: DeleteConfirmDialogProps) => import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=DeleteConfirmDialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DeleteConfirmDialog.d.ts","sourceRoot":"","sources":["../../src/components/DeleteConfirmDialog.tsx"],"names":[],"mappings":"AAUA,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,8CAKjC,wBAAwB,4CA4B1B,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Button } from '@sqlrooms/ui';
3
+ import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, } from '@sqlrooms/ui';
4
+ export const DeleteConfirmDialog = ({ open, onOpenChange, onConfirm, itemType, }) => {
5
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Delete ", itemType] }), _jsxs(DialogDescription, { children: ["Are you sure you want to delete this ", itemType.toLowerCase(), "? This action cannot be undone."] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: "Cancel" }), _jsx(Button, { variant: "destructive", onClick: () => {
6
+ onConfirm();
7
+ onOpenChange(false);
8
+ }, children: "Delete" })] })] }) }));
9
+ };
10
+ //# sourceMappingURL=DeleteConfirmDialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DeleteConfirmDialog.js","sourceRoot":"","sources":["../../src/components/DeleteConfirmDialog.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,cAAc,CAAC;AACpC,OAAO,EACL,MAAM,EACN,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAStB,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,QAAQ,GACiB,EAAE,EAAE;IAC7B,OAAO,CACL,KAAC,MAAM,IAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,YAC5C,MAAC,aAAa,eACZ,MAAC,YAAY,eACX,MAAC,WAAW,0BAAS,QAAQ,IAAe,EAC5C,MAAC,iBAAiB,wDACsB,QAAQ,CAAC,WAAW,EAAE,uCAE1C,IACP,EACf,MAAC,YAAY,eACX,KAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,uBAEnD,EACT,KAAC,MAAM,IACL,OAAO,EAAC,aAAa,EACrB,OAAO,EAAE,GAAG,EAAE;gCACZ,SAAS,EAAE,CAAC;gCACZ,YAAY,CAAC,KAAK,CAAC,CAAC;4BACtB,CAAC,uBAGM,IACI,IACD,GACT,CACV,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import {Button} from '@sqlrooms/ui';\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogFooter,\n DialogTitle,\n DialogDescription,\n} from '@sqlrooms/ui';\n\nexport type DeleteConfirmDialogProps = {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n onConfirm: () => void;\n itemType: string;\n};\n\nexport const DeleteConfirmDialog = ({\n open,\n onOpenChange,\n onConfirm,\n itemType,\n}: DeleteConfirmDialogProps) => {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>Delete {itemType}</DialogTitle>\n <DialogDescription>\n Are you sure you want to delete this {itemType.toLowerCase()}? This\n action cannot be undone.\n </DialogDescription>\n </DialogHeader>\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button\n variant=\"destructive\"\n onClick={() => {\n onConfirm();\n onOpenChange(false);\n }}\n >\n Delete\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n};\n"]}
@@ -0,0 +1,10 @@
1
+ import React, { ComponentPropsWithRef, ReactNode } from 'react';
2
+ import { type Discussion } from '../DiscussSlice';
3
+ import { CommentItemProps } from './CommentItem';
4
+ export type DiscussionItemProps = ComponentPropsWithRef<'div'> & {
5
+ discussion: Discussion;
6
+ className?: string;
7
+ renderComment?: (props: CommentItemProps) => ReactNode;
8
+ };
9
+ export declare const DiscussionItem: React.ForwardRefExoticComponent<Omit<DiscussionItemProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
10
+ //# sourceMappingURL=DiscussionItem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DiscussionItem.d.ts","sourceRoot":"","sources":["../../src/components/DiscussionItem.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAC,qBAAqB,EAAc,SAAS,EAAC,MAAM,OAAO,CAAC;AAC1E,OAAO,EAAyB,KAAK,UAAU,EAAC,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAC,gBAAgB,EAAuB,MAAM,eAAe,CAAC;AAErE,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,CAAC,KAAK,CAAC,GAAG;IAC/D,UAAU,EAAE,UAAU,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,SAAS,CAAC;CACxD,CAAC;AAEF,eAAO,MAAM,cAAc,yGAsC1B,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from '@sqlrooms/ui';
3
+ import React, { forwardRef } from 'react';
4
+ import { useStoreWithDiscussion } from '../DiscussSlice';
5
+ import { defaultRenderComment } from './CommentItem';
6
+ export const DiscussionItem = forwardRef(({ discussion, className, renderComment = defaultRenderComment }, ref) => {
7
+ const setHighlightedDiscussionId = useStoreWithDiscussion((state) => state.discuss.setHighlightedDiscussionId);
8
+ return (_jsxs("div", { ref: ref, className: cn('flex flex-col gap-4 rounded border p-2 shadow-md', 'transition-all duration-200 hover:border-blue-300', className), onClick: () => {
9
+ setHighlightedDiscussionId(discussion.id);
10
+ }, children: [renderComment({
11
+ discussion,
12
+ comment: discussion.rootComment,
13
+ isRootComment: true,
14
+ }), discussion.comments.length > 0 && (_jsx("div", { className: "border-muted ml-6 flex flex-col gap-4 border-l-2 pl-4", children: discussion.comments.map((comment) => (_jsx(React.Fragment, { children: renderComment({ discussion, comment, isRootComment: false }) }, comment.id))) }))] }));
15
+ });
16
+ DiscussionItem.displayName = 'DiscussionItem';
17
+ //# sourceMappingURL=DiscussionItem.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DiscussionItem.js","sourceRoot":"","sources":["../../src/components/DiscussionItem.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAC,EAAE,EAAC,MAAM,cAAc,CAAC;AAChC,OAAO,KAAK,EAAE,EAAwB,UAAU,EAAY,MAAM,OAAO,CAAC;AAC1E,OAAO,EAAC,sBAAsB,EAAkB,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAmB,oBAAoB,EAAC,MAAM,eAAe,CAAC;AAQrE,MAAM,CAAC,MAAM,cAAc,GAAG,UAAU,CACtC,CAAC,EAAC,UAAU,EAAE,SAAS,EAAE,aAAa,GAAG,oBAAoB,EAAC,EAAE,GAAG,EAAE,EAAE;IACrE,MAAM,0BAA0B,GAAG,sBAAsB,CACvD,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,0BAA0B,CACpD,CAAC;IAEF,OAAO,CACL,eACE,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,EAAE,CACX,kDAAkD,EAClD,mDAAmD,EACnD,SAAS,CACV,EACD,OAAO,EAAE,GAAG,EAAE;YACZ,0BAA0B,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;QAC5C,CAAC,aAGA,aAAa,CAAC;gBACb,UAAU;gBACV,OAAO,EAAE,UAAU,CAAC,WAAW;gBAC/B,aAAa,EAAE,IAAI;aACpB,CAAC,EAGD,UAAU,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,CACjC,cAAK,SAAS,EAAC,uDAAuD,YACnE,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CACpC,KAAC,KAAK,CAAC,QAAQ,cACZ,aAAa,CAAC,EAAC,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,EAAC,CAAC,IADxC,OAAO,CAAC,EAAE,CAEd,CAClB,CAAC,GACE,CACP,IACG,CACP,CAAC;AACJ,CAAC,CACF,CAAC;AACF,cAAc,CAAC,WAAW,GAAG,gBAAgB,CAAC","sourcesContent":["import {cn} from '@sqlrooms/ui';\nimport React, {ComponentPropsWithRef, forwardRef, ReactNode} from 'react';\nimport {useStoreWithDiscussion, type Discussion} from '../DiscussSlice';\nimport {CommentItemProps, defaultRenderComment} from './CommentItem';\n\nexport type DiscussionItemProps = ComponentPropsWithRef<'div'> & {\n discussion: Discussion;\n className?: string;\n renderComment?: (props: CommentItemProps) => ReactNode;\n};\n\nexport const DiscussionItem = forwardRef<HTMLDivElement, DiscussionItemProps>(\n ({discussion, className, renderComment = defaultRenderComment}, ref) => {\n const setHighlightedDiscussionId = useStoreWithDiscussion(\n (state) => state.discuss.setHighlightedDiscussionId,\n );\n\n return (\n <div\n ref={ref}\n className={cn(\n 'flex flex-col gap-4 rounded border p-2 shadow-md',\n 'transition-all duration-200 hover:border-blue-300',\n className,\n )}\n onClick={() => {\n setHighlightedDiscussionId(discussion.id);\n }}\n >\n {/* Root comment (the discussion itself) */}\n {renderComment({\n discussion,\n comment: discussion.rootComment,\n isRootComment: true,\n })}\n\n {/* Replies */}\n {discussion.comments.length > 0 && (\n <div className=\"border-muted ml-6 flex flex-col gap-4 border-l-2 pl-4\">\n {discussion.comments.map((comment) => (\n <React.Fragment key={comment.id}>\n {renderComment({discussion, comment, isRootComment: false})}\n </React.Fragment>\n ))}\n </div>\n )}\n </div>\n );\n },\n);\nDiscussionItem.displayName = 'DiscussionItem';\n"]}
@@ -0,0 +1,14 @@
1
+ import { ComponentPropsWithoutRef } from 'react';
2
+ export type EditCommentFormProps = Omit<ComponentPropsWithoutRef<'div'>, 'onSubmit'> & {
3
+ onSubmit: (text: string) => void;
4
+ initialText?: string;
5
+ submitLabel?: string;
6
+ onCancel?: () => void;
7
+ };
8
+ export declare const EditCommentForm: import("react").ForwardRefExoticComponent<Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "onSubmit"> & {
9
+ onSubmit: (text: string) => void;
10
+ initialText?: string;
11
+ submitLabel?: string;
12
+ onCancel?: () => void;
13
+ } & import("react").RefAttributes<HTMLDivElement>>;
14
+ //# sourceMappingURL=EditCommentForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EditCommentForm.d.ts","sourceRoot":"","sources":["../../src/components/EditCommentForm.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,wBAAwB,EAKzB,MAAM,OAAO,CAAC;AAEf,MAAM,MAAM,oBAAoB,GAAG,IAAI,CACrC,wBAAwB,CAAC,KAAK,CAAC,EAC/B,UAAU,CACX,GAAG;IACF,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AAEF,eAAO,MAAM,eAAe;cANhB,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;kBAClB,MAAM;kBACN,MAAM;eACT,MAAM,IAAI;kDAqEtB,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, cn, Textarea } from '@sqlrooms/ui';
3
+ import { SendHorizonalIcon } from 'lucide-react';
4
+ import { forwardRef, useEffect, useRef, useState, } from 'react';
5
+ export const EditCommentForm = forwardRef(({ onSubmit, initialText = '', submitLabel = 'Post', onCancel, className, ...props }, ref) => {
6
+ const [text, setText] = useState(initialText);
7
+ const textareaRef = useRef(null);
8
+ // Update text state when initialText changes
9
+ useEffect(() => {
10
+ setText(initialText);
11
+ }, [initialText]);
12
+ // Focus textarea when mounted
13
+ useEffect(() => {
14
+ textareaRef.current?.focus();
15
+ }, []);
16
+ const handleSubmit = () => {
17
+ if (!text.trim())
18
+ return;
19
+ onSubmit(text);
20
+ setText('');
21
+ };
22
+ return (_jsxs("div", { ref: ref, className: cn('mt-2 flex flex-col gap-2', className), ...props, children: [_jsx(Textarea, { ref: textareaRef, className: "min-h-[60px]", value: text, onChange: (e) => setText(e.target.value), onKeyDown: (e) => {
23
+ if (e.key === 'Enter' && !e.shiftKey) {
24
+ e.preventDefault();
25
+ handleSubmit();
26
+ }
27
+ else if (e.key === 'Escape' && onCancel) {
28
+ e.preventDefault();
29
+ onCancel();
30
+ }
31
+ } }), _jsxs("div", { className: "flex items-center gap-2 self-end", children: [onCancel && (_jsx(Button, { variant: "ghost", size: "xs", onClick: onCancel, children: "Cancel" })), _jsxs(Button, { onClick: handleSubmit, variant: "outline", size: "xs", children: [_jsx(SendHorizonalIcon, { className: "h-4 w-4" }), submitLabel] })] })] }));
32
+ });
33
+ EditCommentForm.displayName = 'EditCommentForm';
34
+ //# sourceMappingURL=EditCommentForm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EditCommentForm.js","sourceRoot":"","sources":["../../src/components/EditCommentForm.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAC,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAC,MAAM,cAAc,CAAC;AAClD,OAAO,EAAC,iBAAiB,EAAC,MAAM,cAAc,CAAC;AAC/C,OAAO,EAEL,UAAU,EACV,SAAS,EACT,MAAM,EACN,QAAQ,GACT,MAAM,OAAO,CAAC;AAYf,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CACvC,CACE,EACE,QAAQ,EACR,WAAW,GAAG,EAAE,EAChB,WAAW,GAAG,MAAM,EACpB,QAAQ,EACR,SAAS,EACT,GAAG,KAAK,EACT,EACD,GAAG,EACH,EAAE;IACF,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAG,MAAM,CAAsB,IAAI,CAAC,CAAC;IAEtD,6CAA6C;IAC7C,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,CAAC,WAAW,CAAC,CAAC;IACvB,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IAElB,8BAA8B;IAC9B,SAAS,CAAC,GAAG,EAAE;QACb,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;IAC/B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,OAAO;QACzB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,OAAO,CAAC,EAAE,CAAC,CAAC;IACd,CAAC,CAAC;IAEF,OAAO,CACL,eACE,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,EAAE,CAAC,0BAA0B,EAAE,SAAS,CAAC,KAChD,KAAK,aAET,KAAC,QAAQ,IACP,GAAG,EAAE,WAAW,EAChB,SAAS,EAAC,cAAc,EACxB,KAAK,EAAE,IAAI,EACX,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACxC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE;oBACf,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;wBACrC,CAAC,CAAC,cAAc,EAAE,CAAC;wBACnB,YAAY,EAAE,CAAC;oBACjB,CAAC;yBAAM,IAAI,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,QAAQ,EAAE,CAAC;wBAC1C,CAAC,CAAC,cAAc,EAAE,CAAC;wBACnB,QAAQ,EAAE,CAAC;oBACb,CAAC;gBACH,CAAC,GACD,EACF,eAAK,SAAS,EAAC,kCAAkC,aAC9C,QAAQ,IAAI,CACX,KAAC,MAAM,IAAC,OAAO,EAAC,OAAO,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,QAAQ,uBAE1C,CACV,EACD,MAAC,MAAM,IAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,aACxD,KAAC,iBAAiB,IAAC,SAAS,EAAC,SAAS,GAAG,EACxC,WAAW,IACL,IACL,IACF,CACP,CAAC;AACJ,CAAC,CACF,CAAC;AACF,eAAe,CAAC,WAAW,GAAG,iBAAiB,CAAC","sourcesContent":["import {Button, cn, Textarea} from '@sqlrooms/ui';\nimport {SendHorizonalIcon} from 'lucide-react';\nimport {\n ComponentPropsWithoutRef,\n forwardRef,\n useEffect,\n useRef,\n useState,\n} from 'react';\n\nexport type EditCommentFormProps = Omit<\n ComponentPropsWithoutRef<'div'>,\n 'onSubmit'\n> & {\n onSubmit: (text: string) => void;\n initialText?: string;\n submitLabel?: string;\n onCancel?: () => void;\n};\n\nexport const EditCommentForm = forwardRef<HTMLDivElement, EditCommentFormProps>(\n (\n {\n onSubmit,\n initialText = '',\n submitLabel = 'Post',\n onCancel,\n className,\n ...props\n },\n ref,\n ) => {\n const [text, setText] = useState(initialText);\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n // Update text state when initialText changes\n useEffect(() => {\n setText(initialText);\n }, [initialText]);\n\n // Focus textarea when mounted\n useEffect(() => {\n textareaRef.current?.focus();\n }, []);\n\n const handleSubmit = () => {\n if (!text.trim()) return;\n onSubmit(text);\n setText('');\n };\n\n return (\n <div\n ref={ref}\n className={cn('mt-2 flex flex-col gap-2', className)}\n {...props}\n >\n <Textarea\n ref={textareaRef}\n className=\"min-h-[60px]\"\n value={text}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSubmit();\n } else if (e.key === 'Escape' && onCancel) {\n e.preventDefault();\n onCancel();\n }\n }}\n />\n <div className=\"flex items-center gap-2 self-end\">\n {onCancel && (\n <Button variant=\"ghost\" size=\"xs\" onClick={onCancel}>\n Cancel\n </Button>\n )}\n <Button onClick={handleSubmit} variant=\"outline\" size=\"xs\">\n <SendHorizonalIcon className=\"h-4 w-4\" />\n {submitLabel}\n </Button>\n </div>\n </div>\n );\n },\n);\nEditCommentForm.displayName = 'EditCommentForm';\n"]}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * {@include ../README.md}
3
+ * @packageDocumentation
4
+ */
5
+ export { DiscussionList } from './DiscussionList';
6
+ export { Comment, createDefaultDiscussConfig, createDiscussSlice, Discussion, DiscussSliceConfig, useStoreWithDiscussion, type DiscussSliceState, type ProjectStateWithDiscussion, } from './DiscussSlice';
7
+ export { CommentItem } from './components/CommentItem';
8
+ export { DeleteConfirmDialog } from './components/DeleteConfirmDialog';
9
+ export { DiscussionItem } from './components/DiscussionItem';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACL,OAAO,EACP,0BAA0B,EAC1B,kBAAkB,EAClB,UAAU,EACV,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,iBAAiB,EACtB,KAAK,0BAA0B,GAChC,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAC,mBAAmB,EAAC,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAC,cAAc,EAAC,MAAM,6BAA6B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * {@include ../README.md}
3
+ * @packageDocumentation
4
+ */
5
+ export { DiscussionList } from './DiscussionList';
6
+ export { Comment, createDefaultDiscussConfig, createDiscussSlice, Discussion, DiscussSliceConfig, useStoreWithDiscussion, } from './DiscussSlice';
7
+ export { CommentItem } from './components/CommentItem';
8
+ export { DeleteConfirmDialog } from './components/DeleteConfirmDialog';
9
+ export { DiscussionItem } from './components/DiscussionItem';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACL,OAAO,EACP,0BAA0B,EAC1B,kBAAkB,EAClB,UAAU,EACV,kBAAkB,EAClB,sBAAsB,GAGvB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAC,mBAAmB,EAAC,MAAM,kCAAkC,CAAC;AACrE,OAAO,EAAC,cAAc,EAAC,MAAM,6BAA6B,CAAC","sourcesContent":["/**\n * {@include ../README.md}\n * @packageDocumentation\n */\n\nexport {DiscussionList} from './DiscussionList';\nexport {\n Comment,\n createDefaultDiscussConfig,\n createDiscussSlice,\n Discussion,\n DiscussSliceConfig,\n useStoreWithDiscussion,\n type DiscussSliceState,\n type ProjectStateWithDiscussion,\n} from './DiscussSlice';\n\nexport {CommentItem} from './components/CommentItem';\nexport {DeleteConfirmDialog} from './components/DeleteConfirmDialog';\nexport {DiscussionItem} from './components/DiscussionItem';\n"]}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@sqlrooms/discuss",
3
+ "version": "0.14.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "module": "dist/index.js",
9
+ "author": "Ilya Boyandin <ilya@boyandin.me>",
10
+ "license": "MIT",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "dependencies": {
18
+ "@paralleldrive/cuid2": "^2.2.2",
19
+ "@sqlrooms/project-builder": "0.14.0",
20
+ "@sqlrooms/ui": "0.14.0",
21
+ "@sqlrooms/utils": "0.14.0",
22
+ "immer": "^10.1.1",
23
+ "zod": "^3.24.1"
24
+ },
25
+ "peerDependencies": {
26
+ "react": "*",
27
+ "react-dom": "*"
28
+ },
29
+ "scripts": {
30
+ "dev": "tsc -w",
31
+ "build": "tsc",
32
+ "lint": "eslint ."
33
+ },
34
+ "gitHead": "fb82a7e1be842348246bafdfeb93b6c41f5f9eb5"
35
+ }