@pol-studios/features 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.
@@ -0,0 +1,325 @@
1
+ // src/comments/useComments.tsx
2
+ import moment from "moment";
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useMemo,
8
+ useState
9
+ } from "react";
10
+ import {
11
+ useDbDelete,
12
+ useQuery,
13
+ useDbRealtimeQuery,
14
+ useDbUpsert,
15
+ useSupabase
16
+ } from "@pol-studios/db";
17
+ import { useAuth } from "@pol-studios/auth";
18
+ import { isUsable } from "@pol-studios/utils";
19
+ import { Comment } from "@pol-studios/db";
20
+ import { jsx } from "react/jsx-runtime";
21
+ var CommentContext = createContext(null);
22
+ function timeSinceOpened(timestamp) {
23
+ if (!timestamp) {
24
+ return "";
25
+ }
26
+ const openedTime = moment(timestamp);
27
+ return preciseHumanize(moment.duration(moment().diff(openedTime))) + " ago";
28
+ }
29
+ var preciseHumanize = (duration) => {
30
+ const years = duration.years();
31
+ const months = duration.months();
32
+ const days = duration.days();
33
+ const hours = duration.hours();
34
+ const minutes = duration.minutes();
35
+ if (duration.asMinutes() < 1) {
36
+ return "a few seconds";
37
+ } else if (duration.years() > 0) {
38
+ return `${years} year${years !== 1 ? "s" : ""}`;
39
+ } else if (duration.months() > 0) {
40
+ return `${months} month${months !== 1 ? "s" : ""}`;
41
+ } else if (duration.days() > 0) {
42
+ return `${days} day${days !== 1 ? "s" : ""}`;
43
+ }
44
+ return `${hours > 0 ? `${hours} hour${hours !== 1 ? "s" : ""} ` : ""}${minutes > 0 ? `${minutes} minute${minutes !== 1 ? "s" : ""} ` : ""}`;
45
+ };
46
+ function CommentProvider({
47
+ children,
48
+ entityTableName,
49
+ entityId,
50
+ schemaName = "public"
51
+ }) {
52
+ const auth = useAuth();
53
+ const supabase = useSupabase();
54
+ const tagUpsert = useDbUpsert(
55
+ { schema: "core", table: "CommentTag" },
56
+ ["id"],
57
+ "*"
58
+ );
59
+ const upsert = useDbUpsert({ schema: "core", table: "Comment" }, ["id"], "*");
60
+ const upsertSection = useDbUpsert(
61
+ { schema: "core", table: "CommentSection" },
62
+ ["id"],
63
+ "*"
64
+ );
65
+ const reaction = useDbUpsert(
66
+ { schema: "core", table: "CommentReaction" },
67
+ ["id"],
68
+ "*"
69
+ );
70
+ const reactionDelete = useDbDelete(
71
+ { schema: "core", table: "CommentReaction" },
72
+ ["id"]
73
+ );
74
+ const readUpsert = useDbUpsert(
75
+ { schema: "core", table: "CommentRead" },
76
+ ["id"],
77
+ "*"
78
+ );
79
+ const deleteCommentMutation = useDbDelete(
80
+ { schema: "core", table: "Comment" },
81
+ ["id"]
82
+ );
83
+ const [quote, setQuote] = useState(null);
84
+ const [message, setMessage] = useState("");
85
+ const [showMore, setShowMore] = useState(false);
86
+ const [taggedUserIds, setTaggedUserIds] = useState([]);
87
+ const shouldLoadProfiles = useMemo(
88
+ () => Boolean(auth?.user?.id),
89
+ [auth?.user?.id]
90
+ );
91
+ const usersRequest = useQuery(
92
+ supabase.schema("core").from("Profile").select("*"),
93
+ { enabled: shouldLoadProfiles }
94
+ );
95
+ const commentSectionRequest = useDbRealtimeQuery(
96
+ supabase.schema("core").from("CommentSection").select("*", { count: "exact" }).eq("entityId", entityId).eq("tableName", entityTableName).eq("schemaName", schemaName).maybeSingle()
97
+ );
98
+ const commentSectionId = commentSectionRequest.data?.id;
99
+ const hasCommentSectionId = isUsable(commentSectionId);
100
+ const commentsRequest = useDbRealtimeQuery(
101
+ supabase.schema("core").from("Comment").select(
102
+ `${Comment.defaultQuery}, CommentReaction(id, value, commentId, userId), CommentRead(id, userId, commentId)`,
103
+ { count: "exact" }
104
+ ).eq("commentSectionId", commentSectionId ?? "").order("createdAt", { ascending: false }),
105
+ { enabled: hasCommentSectionId }
106
+ );
107
+ const comments = useMemo(() => {
108
+ if (!isUsable(commentsRequest?.data)) return [];
109
+ return commentsRequest.data;
110
+ }, [commentsRequest?.data]);
111
+ const sendComment = useCallback(async () => {
112
+ if (message === "") return;
113
+ let nextCommentSectionId = commentSectionId;
114
+ if (!hasCommentSectionId) {
115
+ const newSection = {
116
+ entityId,
117
+ tableName: entityTableName,
118
+ schemaName
119
+ };
120
+ try {
121
+ const sectionResponse = await upsertSection.mutateAsync(newSection);
122
+ if (!sectionResponse?.id) {
123
+ throw new Error("Failed to create comment section: No ID returned.");
124
+ }
125
+ nextCommentSectionId = sectionResponse.id;
126
+ await commentSectionRequest.refetch();
127
+ } catch (error) {
128
+ console.error(
129
+ "Error creating comment section - full error:",
130
+ JSON.stringify(error, null, 2)
131
+ );
132
+ console.error("Error creating comment section - error object:", error);
133
+ let errorMessage = "Failed to create comment section.";
134
+ let errorDetails = "";
135
+ if (error && typeof error === "object") {
136
+ const supabaseError = error;
137
+ if (supabaseError.code) {
138
+ errorDetails += `[${supabaseError.code}] `;
139
+ }
140
+ if (supabaseError.message) {
141
+ errorMessage = supabaseError.message;
142
+ }
143
+ if (supabaseError.details) {
144
+ errorDetails += `Details: ${supabaseError.details}. `;
145
+ }
146
+ if (supabaseError.hint) {
147
+ errorDetails += `Hint: ${supabaseError.hint}. `;
148
+ }
149
+ } else if (error instanceof Error) {
150
+ errorMessage = error.message;
151
+ }
152
+ const fullErrorMessage = errorDetails ? `${errorMessage} ${errorDetails}`.trim() : errorMessage;
153
+ throw new Error(fullErrorMessage);
154
+ }
155
+ }
156
+ if (!nextCommentSectionId) {
157
+ throw new Error("Comment section ID is missing. Please try again.");
158
+ }
159
+ const newComment = {
160
+ commentSectionId: nextCommentSectionId,
161
+ message
162
+ };
163
+ if (quote !== null) {
164
+ newComment.parentId = quote.id;
165
+ }
166
+ const commentResponse = await upsert.mutateAsync(newComment);
167
+ await Promise.all(
168
+ taggedUserIds.map(
169
+ (userId) => tagUpsert.mutateAsync({
170
+ userId,
171
+ commentId: commentResponse.id
172
+ })
173
+ )
174
+ );
175
+ setShowMore(true);
176
+ setTaggedUserIds([]);
177
+ setMessage("");
178
+ setQuote(null);
179
+ }, [
180
+ commentSectionId,
181
+ commentSectionRequest.refetch,
182
+ entityId,
183
+ entityTableName,
184
+ hasCommentSectionId,
185
+ message,
186
+ quote,
187
+ schemaName,
188
+ setMessage,
189
+ setShowMore,
190
+ setTaggedUserIds,
191
+ setQuote,
192
+ tagUpsert,
193
+ taggedUserIds,
194
+ upsert,
195
+ upsertSection
196
+ ]);
197
+ const tagUser = useCallback(
198
+ (userId) => {
199
+ setTaggedUserIds((prev) => {
200
+ if (prev.includes(userId)) {
201
+ return prev.filter((user) => user !== userId);
202
+ }
203
+ return [...prev, userId];
204
+ });
205
+ },
206
+ [setTaggedUserIds]
207
+ );
208
+ const tryCommentReaction = useCallback(
209
+ async (value, commentId) => {
210
+ if (!auth?.user?.id || comments.length === 0) return;
211
+ const comment = comments.find((c) => c.id === commentId);
212
+ if (!comment) return;
213
+ if (comment.CommentReaction.some(
214
+ (x) => x.userId === auth.user.id && x.value === value
215
+ ))
216
+ return;
217
+ const reactionData = {
218
+ commentId,
219
+ value
220
+ };
221
+ await reaction.mutateAsync(reactionData);
222
+ },
223
+ [auth?.user?.id, comments, reaction]
224
+ );
225
+ const deleteReaction = useCallback(
226
+ async (reactionId) => {
227
+ await reactionDelete.mutateAsync({ id: reactionId });
228
+ },
229
+ [reactionDelete]
230
+ );
231
+ const readComment = useCallback(
232
+ async (commentId) => {
233
+ if (!auth?.user?.id || comments.length === 0) return;
234
+ const comment = comments.find((c) => c.id === commentId);
235
+ const reads = comment?.CommentRead;
236
+ if (!reads || reads.some((x) => x.userId === auth.user.id)) return;
237
+ await readUpsert.mutateAsync({ commentId });
238
+ await commentsRequest.refetch();
239
+ },
240
+ [auth?.user?.id, comments, commentsRequest, readUpsert]
241
+ );
242
+ const deleteComment = useCallback(
243
+ async (commentId) => {
244
+ await deleteCommentMutation.mutateAsync({ id: commentId });
245
+ },
246
+ [deleteCommentMutation]
247
+ );
248
+ const quoteInfo = useMemo(() => {
249
+ if (quote === null) return null;
250
+ return "- replying to " + quote.userName + "'s comment created " + timeSinceOpened(quote.createdAt);
251
+ }, [quote]);
252
+ const taggableUsers = useMemo(() => {
253
+ if (!auth?.user?.id || !isUsable(usersRequest.data)) return [];
254
+ return usersRequest.data.filter((user) => {
255
+ return user.id !== auth.user.id && !taggedUserIds.includes(user.id);
256
+ });
257
+ }, [auth?.user?.id, taggedUserIds, usersRequest.data]);
258
+ const taggedUsers = useMemo(() => {
259
+ if (!isUsable(usersRequest.data)) return [];
260
+ return usersRequest.data.filter((user) => taggedUserIds.includes(user.id));
261
+ }, [taggedUserIds, usersRequest.data]);
262
+ const commentSectionExists = useMemo(() => {
263
+ return hasCommentSectionId;
264
+ }, [hasCommentSectionId]);
265
+ const unreadCommentCount = useMemo(() => {
266
+ if (!auth?.user?.id || comments.length === 0) return 0;
267
+ return comments.filter(
268
+ (c) => !c.CommentRead.some((x) => x.userId === auth.user.id)
269
+ ).length;
270
+ }, [auth?.user?.id, comments]);
271
+ const contextValue = useMemo(
272
+ () => ({
273
+ deleteComment,
274
+ deleteReaction,
275
+ tryCommentReaction,
276
+ showMore,
277
+ setShowMore,
278
+ quote,
279
+ setQuote,
280
+ setMessage,
281
+ message,
282
+ sendComment,
283
+ quoteInfo,
284
+ comments,
285
+ commentSectionExists,
286
+ taggableUsers,
287
+ tagUser,
288
+ taggedUsers,
289
+ readComment,
290
+ unreadCommentCount
291
+ }),
292
+ [
293
+ commentSectionExists,
294
+ comments,
295
+ deleteComment,
296
+ deleteReaction,
297
+ message,
298
+ quote,
299
+ quoteInfo,
300
+ readComment,
301
+ sendComment,
302
+ showMore,
303
+ tagUser,
304
+ taggableUsers,
305
+ taggedUsers,
306
+ tryCommentReaction,
307
+ unreadCommentCount
308
+ ]
309
+ );
310
+ return /* @__PURE__ */ jsx(CommentContext.Provider, { value: contextValue, children });
311
+ }
312
+ var useComments = () => {
313
+ const context = useContext(CommentContext);
314
+ if (!context) {
315
+ throw new Error(
316
+ "useComments must be used within a CommentProvider"
317
+ );
318
+ }
319
+ return context;
320
+ };
321
+ export {
322
+ CommentContext,
323
+ CommentProvider,
324
+ useComments
325
+ };
@@ -0,0 +1,283 @@
1
+ // src/filter-utils/useFilterBuilder.tsx
2
+ import { createContext, useContext } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var hookOnChange = (options, onChange, curr, value, tableName, property) => {
5
+ if (!tableName) {
6
+ onChange({ ...curr, value });
7
+ return;
8
+ }
9
+ if (options) {
10
+ let filteredEntities;
11
+ switch (curr.op) {
12
+ case "=":
13
+ filteredEntities = options.filter((entity) => {
14
+ if (typeof property === "string") {
15
+ const nestedProperty = getPropertyKey(property);
16
+ return getProperty(nestedProperty, entity) === value;
17
+ }
18
+ return property.some((prop) => {
19
+ const nestedProperty = getPropertyKey(prop);
20
+ return getProperty(nestedProperty, entity) === value;
21
+ });
22
+ });
23
+ break;
24
+ case "contains":
25
+ case "ilike":
26
+ filteredEntities = options.filter((entity) => {
27
+ if (typeof property === "string") {
28
+ const nestedProperty = getPropertyKey(property);
29
+ const propValue = getProperty(nestedProperty, entity);
30
+ return propValue?.toString().toLowerCase().includes(value.toLowerCase());
31
+ }
32
+ return property.some((prop) => {
33
+ const nestedProperty = getPropertyKey(prop);
34
+ const propValue = getProperty(nestedProperty, entity);
35
+ return propValue?.toString().toLowerCase().includes(value.toLowerCase());
36
+ });
37
+ });
38
+ break;
39
+ case ">":
40
+ filteredEntities = options.filter((entity) => {
41
+ if (typeof property === "string") {
42
+ const nestedProperty = getPropertyKey(property);
43
+ return getProperty(nestedProperty, entity) > value;
44
+ }
45
+ return property.some((prop) => {
46
+ const nestedProperty = getPropertyKey(prop);
47
+ return getProperty(nestedProperty, entity) > value;
48
+ });
49
+ });
50
+ break;
51
+ case ">=":
52
+ filteredEntities = options.filter((entity) => {
53
+ if (typeof property === "string") {
54
+ const nestedProperty = getPropertyKey(property);
55
+ return getProperty(nestedProperty, entity) >= value;
56
+ }
57
+ return property.some((prop) => {
58
+ const nestedProperty = getPropertyKey(prop);
59
+ return getProperty(nestedProperty, entity) >= value;
60
+ });
61
+ });
62
+ break;
63
+ case "<":
64
+ filteredEntities = options.filter((entity) => {
65
+ if (typeof property === "string") {
66
+ const nestedProperty = getPropertyKey(property);
67
+ return getProperty(nestedProperty, entity) < value;
68
+ }
69
+ return property.some((prop) => {
70
+ const nestedProperty = getPropertyKey(prop);
71
+ return getProperty(nestedProperty, entity) < value;
72
+ });
73
+ });
74
+ break;
75
+ case "<=":
76
+ filteredEntities = options.filter((entity) => {
77
+ if (typeof property === "string") {
78
+ const nestedProperty = getPropertyKey(property);
79
+ return getProperty(nestedProperty, entity) <= value;
80
+ }
81
+ return property.some((prop) => {
82
+ const nestedProperty = getPropertyKey(prop);
83
+ return getProperty(nestedProperty, entity) <= value;
84
+ });
85
+ });
86
+ break;
87
+ case "is":
88
+ filteredEntities = options.filter((entity) => {
89
+ if (typeof property === "string") {
90
+ const nestedProperty = getPropertyKey(property);
91
+ const propValue = getProperty(nestedProperty, entity);
92
+ if (value === "null") return propValue === null || propValue === void 0;
93
+ if (value === "not null") return propValue !== null && propValue !== void 0;
94
+ return propValue === value;
95
+ }
96
+ return property.some((prop) => {
97
+ const nestedProperty = getPropertyKey(prop);
98
+ const propValue = getProperty(nestedProperty, entity);
99
+ if (value === "null") return propValue === null || propValue === void 0;
100
+ if (value === "not null") return propValue !== null && propValue !== void 0;
101
+ return propValue === value;
102
+ });
103
+ });
104
+ break;
105
+ case "in":
106
+ filteredEntities = options.filter((entity) => {
107
+ const valuesArray = Array.isArray(value) ? value : [value];
108
+ if (typeof property === "string") {
109
+ const nestedProperty = getPropertyKey(property);
110
+ const propValue = getProperty(nestedProperty, entity);
111
+ return valuesArray.includes(propValue);
112
+ }
113
+ return property.some((prop) => {
114
+ const nestedProperty = getPropertyKey(prop);
115
+ const propValue = getProperty(nestedProperty, entity);
116
+ return valuesArray.includes(propValue);
117
+ });
118
+ });
119
+ break;
120
+ case "ai_search":
121
+ filteredEntities = options;
122
+ break;
123
+ default:
124
+ console.error(`${curr.op} condition not handled`);
125
+ filteredEntities = [];
126
+ break;
127
+ }
128
+ onChange({
129
+ ...curr,
130
+ value: [
131
+ ...filteredEntities?.map((x) => {
132
+ if (curr.info.manyToOneTableInfo?.outputProperty) {
133
+ return x[curr.info.manyToOneTableInfo.outputProperty];
134
+ }
135
+ return x.id;
136
+ }) ?? []
137
+ ],
138
+ op: curr.op
139
+ });
140
+ }
141
+ };
142
+ var getDefaultValue = (filter) => {
143
+ if (filter.options && filter.propertyType !== "boolean") {
144
+ return filter.options[0].value;
145
+ } else if (filter.manyToOneTableInfo) {
146
+ if (filter.propertyType === "string") {
147
+ return "";
148
+ } else if (filter.propertyType === "number") {
149
+ return 0;
150
+ } else if (filter.propertyType === "boolean") {
151
+ return "true";
152
+ } else if (filter.propertyType === "date") {
153
+ return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
154
+ }
155
+ return "";
156
+ } else if (filter.propertyType === "string") {
157
+ return "";
158
+ } else if (filter.propertyType === "date") {
159
+ return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
160
+ } else if (filter.propertyType === "boolean") {
161
+ return "true";
162
+ } else {
163
+ return 0;
164
+ }
165
+ };
166
+ var getComparisonOptions = (filter) => {
167
+ if (filter.options) {
168
+ return [{ display: "=", value: "=" }];
169
+ }
170
+ switch (filter.propertyType) {
171
+ case "boolean":
172
+ return [
173
+ { display: "=", value: "=" },
174
+ { display: "is", value: "is" }
175
+ ];
176
+ case "string":
177
+ return [
178
+ { display: "=", value: "=" },
179
+ { display: "contains", value: "contains" },
180
+ { display: "ilike", value: "ilike" },
181
+ { display: "in", value: "in" },
182
+ { display: "is", value: "is" },
183
+ { display: "ai_search", value: "ai_search" }
184
+ ];
185
+ case "number":
186
+ return [
187
+ { display: "=", value: "=" },
188
+ { display: ">", value: ">" },
189
+ { display: "<", value: "<" },
190
+ { display: ">=", value: ">=" },
191
+ { display: "<=", value: "<=" },
192
+ { display: "in", value: "in" },
193
+ { display: "is", value: "is" }
194
+ ];
195
+ case "date":
196
+ return [
197
+ { display: "=", value: "=" },
198
+ { display: ">", value: ">" },
199
+ { display: "<", value: "<" },
200
+ { display: ">=", value: ">=" },
201
+ { display: "<=", value: "<=" },
202
+ { display: "is", value: "is" }
203
+ ];
204
+ default:
205
+ return [{ display: "=", value: "=" }];
206
+ }
207
+ };
208
+ var getDefaultCondition = (filter) => {
209
+ if (filter.options) return "=";
210
+ switch (filter.propertyType) {
211
+ case "boolean":
212
+ return "=";
213
+ case "string":
214
+ return "ilike";
215
+ // Case insensitive by default
216
+ case "number":
217
+ return "=";
218
+ case "date":
219
+ return "=";
220
+ }
221
+ };
222
+ var genId = () => {
223
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
224
+ var r = Math.random() * 16 | 0, v = c == "x" ? r : r & 3 | 8;
225
+ return v.toString(16);
226
+ });
227
+ };
228
+ var getPropertyKey = (val) => {
229
+ if (val.includes(".")) {
230
+ return val.split(".");
231
+ }
232
+ return val;
233
+ };
234
+ var recurseToProperty = (properties, i, entity) => {
235
+ if (i === properties.length) {
236
+ return entity;
237
+ }
238
+ return recurseToProperty(properties, i + 1, entity[properties[i]]);
239
+ };
240
+ var getProperty = (property, entity) => {
241
+ if (typeof property === "string") {
242
+ return entity[property];
243
+ }
244
+ return recurseToProperty(property, 0, entity);
245
+ };
246
+ var FilterContext = createContext(void 0);
247
+ var FilterProvider = ({ children, value }) => {
248
+ return /* @__PURE__ */ jsx(FilterContext.Provider, { value, children });
249
+ };
250
+ var useFilterContext = () => {
251
+ const context = useContext(FilterContext);
252
+ if (!context) {
253
+ throw new Error("useFilterContext must be used within a FilterProvider");
254
+ }
255
+ return context;
256
+ };
257
+
258
+ // src/filter-utils/useNestedFilterOptions.ts
259
+ function useNestedFilterOptions(value, onChange) {
260
+ const setFilterValue = (val) => {
261
+ onChange({ ...value, value: val });
262
+ };
263
+ const deleteSelf = () => {
264
+ onChange(value.id);
265
+ };
266
+ return {
267
+ setFilterValue,
268
+ deleteSelf
269
+ };
270
+ }
271
+ export {
272
+ FilterProvider,
273
+ genId,
274
+ getComparisonOptions,
275
+ getDefaultCondition,
276
+ getDefaultValue,
277
+ getProperty,
278
+ getPropertyKey,
279
+ hookOnChange,
280
+ recurseToProperty,
281
+ useFilterContext,
282
+ useNestedFilterOptions
283
+ };