@fewchurs/react 1.4.0 → 1.5.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/dist/core.d.mts +5 -4
- package/dist/core.d.ts +5 -4
- package/dist/core.js +44 -24
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +43 -24
- package/dist/core.mjs.map +1 -1
- package/dist/index.d.mts +14 -4
- package/dist/index.d.ts +14 -4
- package/dist/index.js +137 -54
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +146 -64
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +21 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
|
|
6
6
|
// src/client/version.ts
|
|
7
|
-
var SDK_VERSION = "1.
|
|
7
|
+
var SDK_VERSION = "1.5.0";
|
|
8
8
|
|
|
9
9
|
// src/client/constants.ts
|
|
10
10
|
var SDK_HEADER = `react/${SDK_VERSION}`;
|
|
@@ -22,17 +22,28 @@ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
22
22
|
var ROADMAP_STATUSES = ["planned", "in_progress", "done"];
|
|
23
23
|
|
|
24
24
|
// src/client/errors.ts
|
|
25
|
+
var NOT_CONFIGURED_MESSAGE = "Fewchurs is not configured.";
|
|
25
26
|
var USER_MESSAGES = {
|
|
26
|
-
invalid_key: "
|
|
27
|
-
validation_failed: "
|
|
28
|
-
rate_limited: "You
|
|
29
|
-
not_found: "
|
|
30
|
-
limit_reached: "
|
|
31
|
-
server: "Something went wrong.
|
|
32
|
-
network: "
|
|
27
|
+
invalid_key: "Invalid API key. Check the key in your Fewchurs dashboard.",
|
|
28
|
+
validation_failed: "Invalid input.",
|
|
29
|
+
rate_limited: "You've reached today's limit. Try again tomorrow.",
|
|
30
|
+
not_found: "This request no longer exists.",
|
|
31
|
+
limit_reached: "This board isn't accepting requests right now.",
|
|
32
|
+
server: "Something went wrong. Try again.",
|
|
33
|
+
network: "You're offline."
|
|
33
34
|
};
|
|
35
|
+
function isWrongApp(error) {
|
|
36
|
+
return error.code === "invalid_key" && error.status === 403;
|
|
37
|
+
}
|
|
38
|
+
function isNotConfigured(error) {
|
|
39
|
+
return error.code === "server" && error.message === NOT_CONFIGURED_MESSAGE;
|
|
40
|
+
}
|
|
41
|
+
function notConfiguredError() {
|
|
42
|
+
return { code: "server", status: 500, message: NOT_CONFIGURED_MESSAGE };
|
|
43
|
+
}
|
|
34
44
|
function userMessage(error) {
|
|
35
45
|
if (error.code === "validation_failed" && error.message) return error.message;
|
|
46
|
+
if (isWrongApp(error) && error.message) return error.message;
|
|
36
47
|
return USER_MESSAGES[error.code];
|
|
37
48
|
}
|
|
38
49
|
function networkError(message = "Network request failed.") {
|
|
@@ -43,17 +54,17 @@ function validationError(message) {
|
|
|
43
54
|
}
|
|
44
55
|
function errorFromResponse(status, body) {
|
|
45
56
|
const code = body?.error?.code;
|
|
46
|
-
const
|
|
57
|
+
const serverMessage = body?.error?.message;
|
|
58
|
+
const message = serverMessage ?? `Request failed (${status}).`;
|
|
47
59
|
switch (code) {
|
|
48
60
|
case "invalid_key":
|
|
49
61
|
return { code: "invalid_key", status: 401, message };
|
|
50
62
|
case "wrong_app":
|
|
51
63
|
return { code: "invalid_key", status: 403, message };
|
|
52
|
-
// missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,
|
|
53
|
-
// so it collapses into validation_failed rather than widening the public union.
|
|
54
64
|
case "missing_device_id":
|
|
65
|
+
return { code: "server", status: 400, message };
|
|
55
66
|
case "validation_failed":
|
|
56
|
-
return { code: "validation_failed", status: 400, message };
|
|
67
|
+
return { code: "validation_failed", status: 400, message: serverMessage ?? "Invalid input." };
|
|
57
68
|
case "rate_limited":
|
|
58
69
|
return { code: "rate_limited", status: 429, message };
|
|
59
70
|
case "not_found":
|
|
@@ -147,11 +158,8 @@ function normalizeVote(value) {
|
|
|
147
158
|
// src/client/validate.ts
|
|
148
159
|
function validateFeatureInput(input) {
|
|
149
160
|
const title = input.title.trim();
|
|
150
|
-
if (title.length < LIMITS.titleMin) {
|
|
151
|
-
return validationError(`Title must be
|
|
152
|
-
}
|
|
153
|
-
if (title.length > LIMITS.titleMax) {
|
|
154
|
-
return validationError(`Title must be at most ${LIMITS.titleMax} characters.`);
|
|
161
|
+
if (title.length < LIMITS.titleMin || title.length > LIMITS.titleMax) {
|
|
162
|
+
return validationError(`Title must be ${LIMITS.titleMin}\u2013${LIMITS.titleMax} characters.`);
|
|
155
163
|
}
|
|
156
164
|
if ((input.description ?? "").trim().length > LIMITS.descriptionMax) {
|
|
157
165
|
return validationError(`Description must be at most ${LIMITS.descriptionMax} characters.`);
|
|
@@ -163,9 +171,8 @@ function validateFeatureInput(input) {
|
|
|
163
171
|
}
|
|
164
172
|
function validateCommentInput(input) {
|
|
165
173
|
const text = input.text.trim();
|
|
166
|
-
if (text.length < LIMITS.commentMin
|
|
167
|
-
|
|
168
|
-
return validationError(`Comment must be at most ${LIMITS.commentMax} characters.`);
|
|
174
|
+
if (text.length < LIMITS.commentMin || text.length > LIMITS.commentMax) {
|
|
175
|
+
return validationError(`Comment must be ${LIMITS.commentMin}\u2013${LIMITS.commentMax} characters.`);
|
|
169
176
|
}
|
|
170
177
|
if ((input.authorName ?? "").trim().length > LIMITS.authorNameMax) {
|
|
171
178
|
return validationError(`Name must be at most ${LIMITS.authorNameMax} characters.`);
|
|
@@ -185,6 +192,7 @@ function createFewchursClient(options = {}) {
|
|
|
185
192
|
const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
186
193
|
const retries = options.retries ?? 1;
|
|
187
194
|
const retryDelayMs = options.retryDelayMs ?? 250;
|
|
195
|
+
if (options.apiKey && !options.apiKey.startsWith("fr_")) logOnce(BAD_KEY_PREFIX);
|
|
188
196
|
function getDeviceId() {
|
|
189
197
|
return typeof options.deviceId === "function" ? options.deviceId() : options.deviceId ?? null;
|
|
190
198
|
}
|
|
@@ -211,6 +219,11 @@ function createFewchursClient(options = {}) {
|
|
|
211
219
|
}
|
|
212
220
|
async function request(path, map, { method = "GET", body, query, signal } = {}) {
|
|
213
221
|
if (!doFetch) return { ok: false, error: networkError("No fetch implementation available.") };
|
|
222
|
+
if (!options.apiKey && options.baseUrl === void 0) {
|
|
223
|
+
const failure = notConfiguredError();
|
|
224
|
+
logError(failure);
|
|
225
|
+
return { ok: false, error: failure };
|
|
226
|
+
}
|
|
214
227
|
const target = url(path, query);
|
|
215
228
|
const init = { method, headers: headers(body !== void 0) };
|
|
216
229
|
if (body !== void 0) init.body = JSON.stringify(body);
|
|
@@ -330,16 +343,21 @@ function delay(ms) {
|
|
|
330
343
|
function messageOf(error) {
|
|
331
344
|
return error instanceof Error ? error.message : "Network request failed.";
|
|
332
345
|
}
|
|
333
|
-
var INVALID_KEY_HELP = "
|
|
346
|
+
var INVALID_KEY_HELP = "Fewchurs rejected the API key. Check the key in your dashboard (Apps \u2192 API keys).";
|
|
347
|
+
var NOT_CONFIGURED_HELP = "Fewchurs is not configured. Pass apiKey to FewchursProvider, or set FEWCHURS_API_KEY on your server.";
|
|
348
|
+
var BAD_KEY_PREFIX = 'The Fewchurs API key should start with "fr_". Copy it from your dashboard.';
|
|
334
349
|
var loggedErrors = /* @__PURE__ */ new Set();
|
|
335
|
-
function
|
|
350
|
+
function logOnce(detail) {
|
|
336
351
|
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
337
|
-
const isInvalidKey = error.code === "invalid_key" && error.status === 401;
|
|
338
|
-
const detail = isInvalidKey ? INVALID_KEY_HELP : error.message;
|
|
339
352
|
if (loggedErrors.has(detail)) return;
|
|
340
353
|
loggedErrors.add(detail);
|
|
341
354
|
console.warn(`[fewchurs] : ${detail}`);
|
|
342
355
|
}
|
|
356
|
+
function logError(error) {
|
|
357
|
+
if (isNotConfigured(error)) return logOnce(NOT_CONFIGURED_HELP);
|
|
358
|
+
const isInvalidKey = error.code === "invalid_key" && error.status === 401;
|
|
359
|
+
logOnce(isInvalidKey ? INVALID_KEY_HELP : error.message);
|
|
360
|
+
}
|
|
343
361
|
|
|
344
362
|
// src/identity/deviceId.ts
|
|
345
363
|
var COOKIE_NAME = "fewchurs_did";
|
|
@@ -769,11 +787,19 @@ var DEFAULT_LABELS = {
|
|
|
769
787
|
loadMore: "Load more",
|
|
770
788
|
retry: "Try again",
|
|
771
789
|
errorInvalidKeyTitle: "Invalid API key",
|
|
772
|
-
errorInvalidKeyDetail: "
|
|
773
|
-
errorWrongAppTitle: "
|
|
774
|
-
errorWrongAppDetail: "
|
|
775
|
-
errorTitle: "
|
|
776
|
-
errorDetail: "
|
|
790
|
+
errorInvalidKeyDetail: "Copy the key from your Fewchurs dashboard (Apps \u2192 API keys) and pass it to FewchursProvider, or set FEWCHURS_API_KEY on your server.",
|
|
791
|
+
errorWrongAppTitle: "Invalid API key",
|
|
792
|
+
errorWrongAppDetail: "",
|
|
793
|
+
errorTitle: "Couldn't load requests",
|
|
794
|
+
errorDetail: "Something went wrong. Try again.",
|
|
795
|
+
errorOfflineTitle: "You're offline",
|
|
796
|
+
errorOfflineDetail: "Check your connection and try again.",
|
|
797
|
+
errorNotSetUpTitle: "Fewchurs isn't set up",
|
|
798
|
+
errorNotSetUpDetail: "Pass apiKey to FewchursProvider, or set FEWCHURS_API_KEY on your server.",
|
|
799
|
+
errorLoadMore: "Couldn't load more.",
|
|
800
|
+
errorComments: "Couldn't load comments.",
|
|
801
|
+
fieldTitleTooShort: "At least 3 characters.",
|
|
802
|
+
fieldEmailInvalid: "Enter a valid email address.",
|
|
777
803
|
comments: "Comments",
|
|
778
804
|
commentPlaceholder: "Add a comment",
|
|
779
805
|
postComment: "Post",
|
|
@@ -972,6 +998,7 @@ function useComments(featureId, { enabled = true } = {}) {
|
|
|
972
998
|
store.ensureComments(featureId);
|
|
973
999
|
}, [store, featureId, enabled, isReady]);
|
|
974
1000
|
const loadMore = useCallback5(() => store.loadMoreComments(featureId), [store, featureId]);
|
|
1001
|
+
const refresh = useCallback5(() => store.refreshComments(featureId), [store, featureId]);
|
|
975
1002
|
const add = useCallback5(
|
|
976
1003
|
async (text, authorName) => {
|
|
977
1004
|
setIsSubmitting(true);
|
|
@@ -992,6 +1019,7 @@ function useComments(featureId, { enabled = true } = {}) {
|
|
|
992
1019
|
submitError,
|
|
993
1020
|
hasMore: comments.cursor !== null,
|
|
994
1021
|
loadMore,
|
|
1022
|
+
refresh,
|
|
995
1023
|
add
|
|
996
1024
|
};
|
|
997
1025
|
}
|
|
@@ -1011,10 +1039,15 @@ function formatDate(iso) {
|
|
|
1011
1039
|
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
1012
1040
|
function CommentList({ featureId }) {
|
|
1013
1041
|
const { labels, cx } = useBoard();
|
|
1014
|
-
const { comments, isLoading, isLoadingMore, error, hasMore, loadMore } = useComments(featureId);
|
|
1042
|
+
const { comments, isLoading, isLoadingMore, error, hasMore, loadMore, refresh } = useComments(featureId);
|
|
1015
1043
|
return /* @__PURE__ */ jsxs("div", { className: cx("fw-comments"), children: [
|
|
1016
1044
|
isLoading && /* @__PURE__ */ jsx3("p", { className: cx("fw-muted"), children: labels.loading }),
|
|
1017
|
-
error && /* @__PURE__ */
|
|
1045
|
+
error && comments.length === 0 && /* @__PURE__ */ jsxs("div", { className: cx("fw-empty", "fw-empty--compact"), role: "alert", children: [
|
|
1046
|
+
/* @__PURE__ */ jsx3("p", { className: cx("fw-empty__title"), children: labels.errorComments }),
|
|
1047
|
+
/* @__PURE__ */ jsx3("p", { className: cx("fw-empty__detail"), children: isNotConfigured(error) ? labels.errorNotSetUpTitle : userMessage(error) }),
|
|
1048
|
+
/* @__PURE__ */ jsx3("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
|
|
1049
|
+
] }),
|
|
1050
|
+
error && comments.length > 0 && /* @__PURE__ */ jsx3("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) }),
|
|
1018
1051
|
!isLoading && !error && comments.length === 0 && /* @__PURE__ */ jsx3("p", { className: cx("fw-muted"), children: labels.noComments }),
|
|
1019
1052
|
/* @__PURE__ */ jsx3("ul", { className: cx("fw-comment-list"), children: comments.map((comment) => /* @__PURE__ */ jsxs("li", { className: cx("fw-comment"), children: [
|
|
1020
1053
|
/* @__PURE__ */ jsx3("span", { className: cx("fw-comment__author"), children: comment.authorName }),
|
|
@@ -1085,7 +1118,7 @@ function CommentForm({ featureId }) {
|
|
|
1085
1118
|
}
|
|
1086
1119
|
)
|
|
1087
1120
|
] }),
|
|
1088
|
-
/* @__PURE__ */ jsx3("button", { type: "submit", className: cx("fw-btn"), disabled: isSubmitting, children: labels.postComment }),
|
|
1121
|
+
/* @__PURE__ */ jsx3("button", { type: "submit", className: cx("fw-btn"), disabled: isSubmitting || !text.trim(), children: labels.postComment }),
|
|
1089
1122
|
submitError && /* @__PURE__ */ jsx3("p", { className: cx("fw-error"), role: "alert", children: userMessage(submitError) })
|
|
1090
1123
|
]
|
|
1091
1124
|
}
|
|
@@ -1094,27 +1127,31 @@ function CommentForm({ featureId }) {
|
|
|
1094
1127
|
|
|
1095
1128
|
// src/components/FollowForm.tsx
|
|
1096
1129
|
import { useState as useState6 } from "react";
|
|
1097
|
-
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1130
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1098
1131
|
function FollowForm({ feature }) {
|
|
1099
1132
|
const { labels, userEmail, cx } = useBoard();
|
|
1100
1133
|
const { isFollowing, isPending, error, follow, unfollow } = useFollow(feature.id);
|
|
1101
1134
|
const [email, setEmail] = useState6(userEmail ?? "");
|
|
1102
1135
|
const [isOpen, setIsOpen] = useState6(false);
|
|
1103
1136
|
if (isFollowing) {
|
|
1104
|
-
return /* @__PURE__ */
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1137
|
+
return /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1138
|
+
/* @__PURE__ */ jsx4(
|
|
1139
|
+
"button",
|
|
1140
|
+
{
|
|
1141
|
+
type: "button",
|
|
1142
|
+
className: cx("fw-link"),
|
|
1143
|
+
disabled: isPending,
|
|
1144
|
+
onClick: () => void unfollow(),
|
|
1145
|
+
children: labels.following
|
|
1146
|
+
}
|
|
1147
|
+
),
|
|
1148
|
+
error && /* @__PURE__ */ jsx4("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) })
|
|
1149
|
+
] });
|
|
1114
1150
|
}
|
|
1115
1151
|
if (!isOpen) {
|
|
1116
1152
|
return /* @__PURE__ */ jsx4("button", { type: "button", className: cx("fw-link"), onClick: () => setIsOpen(true), children: labels.follow });
|
|
1117
1153
|
}
|
|
1154
|
+
const isEmailValid = EMAIL_PATTERN.test(email.trim());
|
|
1118
1155
|
return /* @__PURE__ */ jsxs2(
|
|
1119
1156
|
"form",
|
|
1120
1157
|
{
|
|
@@ -1138,7 +1175,8 @@ function FollowForm({ feature }) {
|
|
|
1138
1175
|
required: true
|
|
1139
1176
|
}
|
|
1140
1177
|
),
|
|
1141
|
-
/* @__PURE__ */ jsx4("
|
|
1178
|
+
email !== "" && !isEmailValid && /* @__PURE__ */ jsx4("p", { className: cx("fw-error"), children: labels.fieldEmailInvalid }),
|
|
1179
|
+
/* @__PURE__ */ jsx4("button", { type: "submit", className: cx("fw-btn"), disabled: isPending || !isEmailValid, children: labels.follow }),
|
|
1142
1180
|
error && /* @__PURE__ */ jsx4("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) })
|
|
1143
1181
|
]
|
|
1144
1182
|
}
|
|
@@ -1181,7 +1219,7 @@ function FeatureCard({
|
|
|
1181
1219
|
defaultCommentsOpen = false
|
|
1182
1220
|
}) {
|
|
1183
1221
|
const { labels, allowComments, cx } = useBoard();
|
|
1184
|
-
const { isPending, toggle } = useVote(feature.id);
|
|
1222
|
+
const { isPending, toggle, error: voteError } = useVote(feature.id);
|
|
1185
1223
|
const [showComments, setShowComments] = useState7(defaultCommentsOpen);
|
|
1186
1224
|
return /* @__PURE__ */ jsxs4("li", { className: cx("fw-card"), children: [
|
|
1187
1225
|
/* @__PURE__ */ jsx7(VoteButton, { feature, isPending, onToggle: () => void toggle() }),
|
|
@@ -1209,6 +1247,7 @@ function FeatureCard({
|
|
|
1209
1247
|
),
|
|
1210
1248
|
/* @__PURE__ */ jsx7(FollowForm, { feature })
|
|
1211
1249
|
] }),
|
|
1250
|
+
voteError && /* @__PURE__ */ jsx7("p", { className: cx("fw-error"), role: "alert", children: userMessage(voteError) }),
|
|
1212
1251
|
allowComments && showComments && /* @__PURE__ */ jsx7("div", { id: `fw-comments-${feature.id}`, children: /* @__PURE__ */ jsx7(CommentList, { featureId: feature.id }) })
|
|
1213
1252
|
] })
|
|
1214
1253
|
] });
|
|
@@ -1248,24 +1287,42 @@ function Bar({ width, height }) {
|
|
|
1248
1287
|
}
|
|
1249
1288
|
|
|
1250
1289
|
// src/components/FeatureList.tsx
|
|
1251
|
-
import { Fragment, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1290
|
+
import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1252
1291
|
function errorCopy(error, labels) {
|
|
1253
|
-
if (error
|
|
1254
|
-
return { title: labels.
|
|
1292
|
+
if (isNotConfigured(error)) {
|
|
1293
|
+
return { title: labels.errorNotSetUpTitle, detail: labels.errorNotSetUpDetail };
|
|
1294
|
+
}
|
|
1295
|
+
if (error.code === "network") {
|
|
1296
|
+
return { title: labels.errorOfflineTitle, detail: labels.errorOfflineDetail };
|
|
1297
|
+
}
|
|
1298
|
+
if (isWrongApp(error)) {
|
|
1299
|
+
return {
|
|
1300
|
+
title: labels.errorWrongAppTitle,
|
|
1301
|
+
detail: labels.errorWrongAppDetail || userMessage(error)
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
if (error.code === "invalid_key") {
|
|
1305
|
+
return { title: labels.errorInvalidKeyTitle, detail: labels.errorInvalidKeyDetail };
|
|
1255
1306
|
}
|
|
1256
|
-
return
|
|
1307
|
+
return {
|
|
1308
|
+
title: labels.errorTitle,
|
|
1309
|
+
detail: error.code === "server" ? labels.errorDetail : userMessage(error)
|
|
1310
|
+
};
|
|
1257
1311
|
}
|
|
1258
1312
|
function FeatureList() {
|
|
1259
1313
|
const { initialFeatureId } = useBoard();
|
|
1260
|
-
return /* @__PURE__ */ jsxs6(
|
|
1314
|
+
return /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
1261
1315
|
initialFeatureId && /* @__PURE__ */ jsx9(PinnedFeature, { featureId: initialFeatureId }),
|
|
1262
1316
|
/* @__PURE__ */ jsx9(Listing, { excludeId: initialFeatureId })
|
|
1263
1317
|
] });
|
|
1264
1318
|
}
|
|
1265
1319
|
function PinnedFeature({ featureId }) {
|
|
1266
1320
|
const { cx } = useBoard();
|
|
1267
|
-
const { feature, isLoading } = useFeature(featureId);
|
|
1321
|
+
const { feature, isLoading, error } = useFeature(featureId);
|
|
1268
1322
|
if (isLoading) return /* @__PURE__ */ jsx9(FeatureListSkeleton, { rows: 1 });
|
|
1323
|
+
if (!feature && error) {
|
|
1324
|
+
return /* @__PURE__ */ jsx9("p", { className: cx("fw-error", "fw-pinned"), role: "alert", children: userMessage(error) });
|
|
1325
|
+
}
|
|
1269
1326
|
if (!feature) return null;
|
|
1270
1327
|
return /* @__PURE__ */ jsx9("div", { className: cx("fw-pinned"), children: /* @__PURE__ */ jsx9("ul", { className: cx("fw-list"), children: /* @__PURE__ */ jsx9(FeatureCard, { feature, defaultCommentsOpen: true }) }) });
|
|
1271
1328
|
}
|
|
@@ -1288,22 +1345,26 @@ function Listing({ excludeId }) {
|
|
|
1288
1345
|
return /* @__PURE__ */ jsxs6("div", { className: cx("fw-empty"), role: "alert", children: [
|
|
1289
1346
|
/* @__PURE__ */ jsx9("p", { className: cx("fw-empty__title"), children: title }),
|
|
1290
1347
|
/* @__PURE__ */ jsx9("p", { className: cx("fw-empty__detail"), children: detail }),
|
|
1291
|
-
/* @__PURE__ */ jsx9("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
|
|
1348
|
+
!isNotConfigured(error) && /* @__PURE__ */ jsx9("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
|
|
1292
1349
|
] });
|
|
1293
1350
|
}
|
|
1294
1351
|
if (isLoading && visible.length === 0) {
|
|
1295
1352
|
return /* @__PURE__ */ jsx9(FeatureListSkeleton, { roadmap: isRoadmap });
|
|
1296
1353
|
}
|
|
1297
1354
|
if (visible.length === 0) {
|
|
1298
|
-
if (renderEmpty) return /* @__PURE__ */ jsx9(
|
|
1355
|
+
if (renderEmpty) return /* @__PURE__ */ jsx9(Fragment2, { children: renderEmpty() });
|
|
1299
1356
|
return /* @__PURE__ */ jsxs6("div", { className: cx("fw-empty"), children: [
|
|
1300
1357
|
/* @__PURE__ */ jsx9("p", { className: cx("fw-empty__title"), children: labels.emptyTitle }),
|
|
1301
1358
|
/* @__PURE__ */ jsx9("p", { children: labels.emptyBody })
|
|
1302
1359
|
] });
|
|
1303
1360
|
}
|
|
1304
|
-
return /* @__PURE__ */ jsxs6(
|
|
1361
|
+
return /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
1305
1362
|
isRoadmap ? /* @__PURE__ */ jsx9(RoadmapColumns, { features: visible }) : /* @__PURE__ */ jsx9(Rows2, { features: visible }),
|
|
1306
|
-
hasMore && /* @__PURE__ */
|
|
1363
|
+
hasMore && error && /* @__PURE__ */ jsxs6("div", { className: cx("fw-pagination"), role: "alert", children: [
|
|
1364
|
+
/* @__PURE__ */ jsx9("span", { children: labels.errorLoadMore }),
|
|
1365
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void loadMore(), children: labels.retry })
|
|
1366
|
+
] }),
|
|
1367
|
+
hasMore && !error && /* @__PURE__ */ jsx9("div", { className: cx("fw-pagination"), children: /* @__PURE__ */ jsx9(
|
|
1307
1368
|
"button",
|
|
1308
1369
|
{
|
|
1309
1370
|
type: "button",
|
|
@@ -1346,7 +1407,7 @@ function FeatureSlot({ feature }) {
|
|
|
1346
1407
|
const { renderFeature } = useBoard();
|
|
1347
1408
|
const { toggle } = useVote(feature.id);
|
|
1348
1409
|
const { follow, unfollow } = useFollow(feature.id);
|
|
1349
|
-
return /* @__PURE__ */ jsx9(
|
|
1410
|
+
return /* @__PURE__ */ jsx9(Fragment2, { children: renderFeature?.(feature, { toggleVote: toggle, follow, unfollow }) });
|
|
1350
1411
|
}
|
|
1351
1412
|
|
|
1352
1413
|
// src/components/SubmitForm.tsx
|
|
@@ -1384,6 +1445,10 @@ function SubmitForm() {
|
|
|
1384
1445
|
const [title, setTitle] = useState9("");
|
|
1385
1446
|
const [description, setDescription] = useState9("");
|
|
1386
1447
|
const [email, setEmail] = useState9(userEmail ?? "");
|
|
1448
|
+
const isTitleValid = title.trim().length >= LIMITS.titleMin;
|
|
1449
|
+
const isEmailInvalid = emailField !== "off" && email !== "" && !EMAIL_PATTERN.test(email.trim());
|
|
1450
|
+
const isEmailMissing = emailField === "required" && !email.trim();
|
|
1451
|
+
const canSubmit = isTitleValid && !isEmailInvalid && !isEmailMissing;
|
|
1387
1452
|
return /* @__PURE__ */ jsxs7(
|
|
1388
1453
|
"form",
|
|
1389
1454
|
{
|
|
@@ -1417,7 +1482,15 @@ function SubmitForm() {
|
|
|
1417
1482
|
onChange: (event) => setTitle(event.target.value),
|
|
1418
1483
|
required: true
|
|
1419
1484
|
}
|
|
1420
|
-
)
|
|
1485
|
+
),
|
|
1486
|
+
/* @__PURE__ */ jsxs7("div", { className: cx("fw-field__foot"), children: [
|
|
1487
|
+
/* @__PURE__ */ jsx10("span", { className: cx("fw-error"), children: title !== "" && !isTitleValid ? labels.fieldTitleTooShort : "" }),
|
|
1488
|
+
/* @__PURE__ */ jsxs7("span", { className: cx("fw-muted"), children: [
|
|
1489
|
+
title.length,
|
|
1490
|
+
"/",
|
|
1491
|
+
LIMITS.titleMax
|
|
1492
|
+
] })
|
|
1493
|
+
] })
|
|
1421
1494
|
] }),
|
|
1422
1495
|
/* @__PURE__ */ jsxs7("div", { className: cx("fw-field"), children: [
|
|
1423
1496
|
/* @__PURE__ */ jsx10("label", { className: cx("fw-label"), htmlFor: "fw-description", children: labels.fieldDescription }),
|
|
@@ -1431,7 +1504,15 @@ function SubmitForm() {
|
|
|
1431
1504
|
placeholder: labels.fieldDescriptionPlaceholder,
|
|
1432
1505
|
onChange: (event) => setDescription(event.target.value)
|
|
1433
1506
|
}
|
|
1434
|
-
)
|
|
1507
|
+
),
|
|
1508
|
+
/* @__PURE__ */ jsxs7("div", { className: cx("fw-field__foot"), children: [
|
|
1509
|
+
/* @__PURE__ */ jsx10("span", {}),
|
|
1510
|
+
/* @__PURE__ */ jsxs7("span", { className: cx("fw-muted"), children: [
|
|
1511
|
+
description.length,
|
|
1512
|
+
"/",
|
|
1513
|
+
LIMITS.descriptionMax
|
|
1514
|
+
] })
|
|
1515
|
+
] })
|
|
1435
1516
|
] }),
|
|
1436
1517
|
emailField !== "off" && /* @__PURE__ */ jsxs7("div", { className: cx("fw-field"), children: [
|
|
1437
1518
|
/* @__PURE__ */ jsx10("label", { className: cx("fw-label"), htmlFor: "fw-email", children: labels.fieldEmail }),
|
|
@@ -1448,11 +1529,11 @@ function SubmitForm() {
|
|
|
1448
1529
|
required: emailField === "required"
|
|
1449
1530
|
}
|
|
1450
1531
|
),
|
|
1451
|
-
/* @__PURE__ */ jsx10("p", { id: "fw-email-hint", className: cx("fw-muted"), children: labels.fieldEmailHint })
|
|
1532
|
+
isEmailInvalid ? /* @__PURE__ */ jsx10("p", { id: "fw-email-hint", className: cx("fw-error"), children: labels.fieldEmailInvalid }) : /* @__PURE__ */ jsx10("p", { id: "fw-email-hint", className: cx("fw-muted"), children: labels.fieldEmailHint })
|
|
1452
1533
|
] }),
|
|
1453
1534
|
error && /* @__PURE__ */ jsx10("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) }),
|
|
1454
1535
|
/* @__PURE__ */ jsxs7("div", { className: cx("fw-form__actions"), children: [
|
|
1455
|
-
/* @__PURE__ */ jsx10("button", { type: "submit", className: cx("fw-btn"), disabled: isPending, children: isPending ? labels.submitting : labels.submit }),
|
|
1536
|
+
/* @__PURE__ */ jsx10("button", { type: "submit", className: cx("fw-btn"), disabled: isPending || !canSubmit, children: isPending ? labels.submitting : labels.submit }),
|
|
1456
1537
|
/* @__PURE__ */ jsx10(
|
|
1457
1538
|
"button",
|
|
1458
1539
|
{
|
|
@@ -1490,7 +1571,7 @@ function themeToStyle(theme) {
|
|
|
1490
1571
|
}
|
|
1491
1572
|
|
|
1492
1573
|
// src/components/Board.tsx
|
|
1493
|
-
import { Fragment as
|
|
1574
|
+
import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1494
1575
|
var DEFAULT_TABS = ["top", "new"];
|
|
1495
1576
|
function BoardRoot({
|
|
1496
1577
|
tabs = DEFAULT_TABS,
|
|
@@ -1577,7 +1658,7 @@ function BoardRoot({
|
|
|
1577
1658
|
}
|
|
1578
1659
|
function DefaultBoard() {
|
|
1579
1660
|
const { isFormOpen, justSubmitted, labels, renderHeader, cx } = useBoard();
|
|
1580
|
-
return /* @__PURE__ */ jsxs8(
|
|
1661
|
+
return /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
1581
1662
|
renderHeader ? renderHeader() : /* @__PURE__ */ jsx11(BoardHeading, {}),
|
|
1582
1663
|
/* @__PURE__ */ jsxs8("div", { className: cx("fw-toolbar"), children: [
|
|
1583
1664
|
/* @__PURE__ */ jsx11(BoardTabs, {}),
|
|
@@ -1701,7 +1782,7 @@ function FewchursBoardDialog({
|
|
|
1701
1782
|
// src/components/BoardButton.tsx
|
|
1702
1783
|
import { useCallback as useCallback7, useState as useState11 } from "react";
|
|
1703
1784
|
import { createPortal } from "react-dom";
|
|
1704
|
-
import { Fragment as
|
|
1785
|
+
import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1705
1786
|
function launcherClasses(floating) {
|
|
1706
1787
|
if (!floating) return "fw-launcher";
|
|
1707
1788
|
const side = floating === "left" ? " fw-launcher--left" : "";
|
|
@@ -1718,7 +1799,7 @@ function FewchursButton({
|
|
|
1718
1799
|
const [open, setOpen] = useState11(false);
|
|
1719
1800
|
const close = useCallback7(() => setOpen(false), []);
|
|
1720
1801
|
const classes = [board.unstyled ? "" : launcherClasses(floating), className].filter(Boolean).join(" ");
|
|
1721
|
-
return /* @__PURE__ */ jsxs10(
|
|
1802
|
+
return /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
1722
1803
|
/* @__PURE__ */ jsx13(
|
|
1723
1804
|
"button",
|
|
1724
1805
|
{
|
|
@@ -1764,6 +1845,7 @@ export {
|
|
|
1764
1845
|
FewchursProvider,
|
|
1765
1846
|
FollowForm,
|
|
1766
1847
|
LIMITS,
|
|
1848
|
+
NOT_CONFIGURED_MESSAGE,
|
|
1767
1849
|
ROADMAP_STATUSES,
|
|
1768
1850
|
SDK_HEADER,
|
|
1769
1851
|
SDK_VERSION,
|