@fewchurs/react 1.4.0 → 1.6.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 +235 -67
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +246 -79
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +22 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ __export(src_exports, {
|
|
|
38
38
|
FewchursProvider: () => FewchursProvider,
|
|
39
39
|
FollowForm: () => FollowForm,
|
|
40
40
|
LIMITS: () => LIMITS,
|
|
41
|
+
NOT_CONFIGURED_MESSAGE: () => NOT_CONFIGURED_MESSAGE,
|
|
41
42
|
ROADMAP_STATUSES: () => ROADMAP_STATUSES,
|
|
42
43
|
SDK_HEADER: () => SDK_HEADER,
|
|
43
44
|
SDK_VERSION: () => SDK_VERSION,
|
|
@@ -85,7 +86,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
85
86
|
var import_react = require("react");
|
|
86
87
|
|
|
87
88
|
// src/client/version.ts
|
|
88
|
-
var SDK_VERSION = "1.
|
|
89
|
+
var SDK_VERSION = "1.6.0";
|
|
89
90
|
|
|
90
91
|
// src/client/constants.ts
|
|
91
92
|
var SDK_HEADER = `react/${SDK_VERSION}`;
|
|
@@ -103,17 +104,28 @@ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
103
104
|
var ROADMAP_STATUSES = ["planned", "in_progress", "done"];
|
|
104
105
|
|
|
105
106
|
// src/client/errors.ts
|
|
107
|
+
var NOT_CONFIGURED_MESSAGE = "Fewchurs is not configured.";
|
|
106
108
|
var USER_MESSAGES = {
|
|
107
|
-
invalid_key: "
|
|
108
|
-
validation_failed: "
|
|
109
|
-
rate_limited: "You
|
|
110
|
-
not_found: "
|
|
111
|
-
limit_reached: "
|
|
112
|
-
server: "Something went wrong.
|
|
113
|
-
network: "
|
|
109
|
+
invalid_key: "Invalid API key. Check the key in your Fewchurs dashboard.",
|
|
110
|
+
validation_failed: "Invalid input.",
|
|
111
|
+
rate_limited: "You've reached today's limit. Try again tomorrow.",
|
|
112
|
+
not_found: "This request no longer exists.",
|
|
113
|
+
limit_reached: "This board isn't accepting requests right now.",
|
|
114
|
+
server: "Something went wrong. Try again.",
|
|
115
|
+
network: "You're offline."
|
|
114
116
|
};
|
|
117
|
+
function isWrongApp(error) {
|
|
118
|
+
return error.code === "invalid_key" && error.status === 403;
|
|
119
|
+
}
|
|
120
|
+
function isNotConfigured(error) {
|
|
121
|
+
return error.code === "server" && error.message === NOT_CONFIGURED_MESSAGE;
|
|
122
|
+
}
|
|
123
|
+
function notConfiguredError() {
|
|
124
|
+
return { code: "server", status: 500, message: NOT_CONFIGURED_MESSAGE };
|
|
125
|
+
}
|
|
115
126
|
function userMessage(error) {
|
|
116
127
|
if (error.code === "validation_failed" && error.message) return error.message;
|
|
128
|
+
if (isWrongApp(error) && error.message) return error.message;
|
|
117
129
|
return USER_MESSAGES[error.code];
|
|
118
130
|
}
|
|
119
131
|
function networkError(message = "Network request failed.") {
|
|
@@ -124,17 +136,17 @@ function validationError(message) {
|
|
|
124
136
|
}
|
|
125
137
|
function errorFromResponse(status, body) {
|
|
126
138
|
const code = body?.error?.code;
|
|
127
|
-
const
|
|
139
|
+
const serverMessage = body?.error?.message;
|
|
140
|
+
const message = serverMessage ?? `Request failed (${status}).`;
|
|
128
141
|
switch (code) {
|
|
129
142
|
case "invalid_key":
|
|
130
143
|
return { code: "invalid_key", status: 401, message };
|
|
131
144
|
case "wrong_app":
|
|
132
145
|
return { code: "invalid_key", status: 403, message };
|
|
133
|
-
// missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,
|
|
134
|
-
// so it collapses into validation_failed rather than widening the public union.
|
|
135
146
|
case "missing_device_id":
|
|
147
|
+
return { code: "server", status: 400, message };
|
|
136
148
|
case "validation_failed":
|
|
137
|
-
return { code: "validation_failed", status: 400, message };
|
|
149
|
+
return { code: "validation_failed", status: 400, message: serverMessage ?? "Invalid input." };
|
|
138
150
|
case "rate_limited":
|
|
139
151
|
return { code: "rate_limited", status: 429, message };
|
|
140
152
|
case "not_found":
|
|
@@ -228,11 +240,8 @@ function normalizeVote(value) {
|
|
|
228
240
|
// src/client/validate.ts
|
|
229
241
|
function validateFeatureInput(input) {
|
|
230
242
|
const title = input.title.trim();
|
|
231
|
-
if (title.length < LIMITS.titleMin) {
|
|
232
|
-
return validationError(`Title must be
|
|
233
|
-
}
|
|
234
|
-
if (title.length > LIMITS.titleMax) {
|
|
235
|
-
return validationError(`Title must be at most ${LIMITS.titleMax} characters.`);
|
|
243
|
+
if (title.length < LIMITS.titleMin || title.length > LIMITS.titleMax) {
|
|
244
|
+
return validationError(`Title must be ${LIMITS.titleMin}\u2013${LIMITS.titleMax} characters.`);
|
|
236
245
|
}
|
|
237
246
|
if ((input.description ?? "").trim().length > LIMITS.descriptionMax) {
|
|
238
247
|
return validationError(`Description must be at most ${LIMITS.descriptionMax} characters.`);
|
|
@@ -244,9 +253,8 @@ function validateFeatureInput(input) {
|
|
|
244
253
|
}
|
|
245
254
|
function validateCommentInput(input) {
|
|
246
255
|
const text = input.text.trim();
|
|
247
|
-
if (text.length < LIMITS.commentMin
|
|
248
|
-
|
|
249
|
-
return validationError(`Comment must be at most ${LIMITS.commentMax} characters.`);
|
|
256
|
+
if (text.length < LIMITS.commentMin || text.length > LIMITS.commentMax) {
|
|
257
|
+
return validationError(`Comment must be ${LIMITS.commentMin}\u2013${LIMITS.commentMax} characters.`);
|
|
250
258
|
}
|
|
251
259
|
if ((input.authorName ?? "").trim().length > LIMITS.authorNameMax) {
|
|
252
260
|
return validationError(`Name must be at most ${LIMITS.authorNameMax} characters.`);
|
|
@@ -266,6 +274,7 @@ function createFewchursClient(options = {}) {
|
|
|
266
274
|
const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
267
275
|
const retries = options.retries ?? 1;
|
|
268
276
|
const retryDelayMs = options.retryDelayMs ?? 250;
|
|
277
|
+
if (options.apiKey && !options.apiKey.startsWith("fr_")) logOnce(BAD_KEY_PREFIX);
|
|
269
278
|
function getDeviceId() {
|
|
270
279
|
return typeof options.deviceId === "function" ? options.deviceId() : options.deviceId ?? null;
|
|
271
280
|
}
|
|
@@ -292,6 +301,11 @@ function createFewchursClient(options = {}) {
|
|
|
292
301
|
}
|
|
293
302
|
async function request(path, map, { method = "GET", body, query, signal } = {}) {
|
|
294
303
|
if (!doFetch) return { ok: false, error: networkError("No fetch implementation available.") };
|
|
304
|
+
if (!options.apiKey && options.baseUrl === void 0) {
|
|
305
|
+
const failure = notConfiguredError();
|
|
306
|
+
logError(failure);
|
|
307
|
+
return { ok: false, error: failure };
|
|
308
|
+
}
|
|
295
309
|
const target = url(path, query);
|
|
296
310
|
const init = { method, headers: headers(body !== void 0) };
|
|
297
311
|
if (body !== void 0) init.body = JSON.stringify(body);
|
|
@@ -411,16 +425,21 @@ function delay(ms) {
|
|
|
411
425
|
function messageOf(error) {
|
|
412
426
|
return error instanceof Error ? error.message : "Network request failed.";
|
|
413
427
|
}
|
|
414
|
-
var INVALID_KEY_HELP = "
|
|
428
|
+
var INVALID_KEY_HELP = "Fewchurs rejected the API key. Check the key in your dashboard (Apps \u2192 API keys).";
|
|
429
|
+
var NOT_CONFIGURED_HELP = "Fewchurs is not configured. Pass apiKey to FewchursProvider, or set FEWCHURS_API_KEY on your server.";
|
|
430
|
+
var BAD_KEY_PREFIX = 'The Fewchurs API key should start with "fr_". Copy it from your dashboard.';
|
|
415
431
|
var loggedErrors = /* @__PURE__ */ new Set();
|
|
416
|
-
function
|
|
432
|
+
function logOnce(detail) {
|
|
417
433
|
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
418
|
-
const isInvalidKey = error.code === "invalid_key" && error.status === 401;
|
|
419
|
-
const detail = isInvalidKey ? INVALID_KEY_HELP : error.message;
|
|
420
434
|
if (loggedErrors.has(detail)) return;
|
|
421
435
|
loggedErrors.add(detail);
|
|
422
436
|
console.warn(`[fewchurs] : ${detail}`);
|
|
423
437
|
}
|
|
438
|
+
function logError(error) {
|
|
439
|
+
if (isNotConfigured(error)) return logOnce(NOT_CONFIGURED_HELP);
|
|
440
|
+
const isInvalidKey = error.code === "invalid_key" && error.status === 401;
|
|
441
|
+
logOnce(isInvalidKey ? INVALID_KEY_HELP : error.message);
|
|
442
|
+
}
|
|
424
443
|
|
|
425
444
|
// src/identity/deviceId.ts
|
|
426
445
|
var COOKIE_NAME = "fewchurs_did";
|
|
@@ -797,6 +816,7 @@ function useFewchurs() {
|
|
|
797
816
|
|
|
798
817
|
// src/components/Board.tsx
|
|
799
818
|
var import_react15 = require("react");
|
|
819
|
+
var import_react_dom = require("react-dom");
|
|
800
820
|
|
|
801
821
|
// src/hooks/useFewchursConfig.ts
|
|
802
822
|
var import_react3 = require("react");
|
|
@@ -850,11 +870,19 @@ var DEFAULT_LABELS = {
|
|
|
850
870
|
loadMore: "Load more",
|
|
851
871
|
retry: "Try again",
|
|
852
872
|
errorInvalidKeyTitle: "Invalid API key",
|
|
853
|
-
errorInvalidKeyDetail: "
|
|
854
|
-
errorWrongAppTitle: "
|
|
855
|
-
errorWrongAppDetail: "
|
|
856
|
-
errorTitle: "
|
|
857
|
-
errorDetail: "
|
|
873
|
+
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.",
|
|
874
|
+
errorWrongAppTitle: "Invalid API key",
|
|
875
|
+
errorWrongAppDetail: "",
|
|
876
|
+
errorTitle: "Couldn't load requests",
|
|
877
|
+
errorDetail: "Something went wrong. Try again.",
|
|
878
|
+
errorOfflineTitle: "You're offline",
|
|
879
|
+
errorOfflineDetail: "Check your connection and try again.",
|
|
880
|
+
errorNotSetUpTitle: "Fewchurs isn't set up",
|
|
881
|
+
errorNotSetUpDetail: "Pass apiKey to FewchursProvider, or set FEWCHURS_API_KEY on your server.",
|
|
882
|
+
errorLoadMore: "Couldn't load more.",
|
|
883
|
+
errorComments: "Couldn't load comments.",
|
|
884
|
+
fieldTitleTooShort: "At least 3 characters.",
|
|
885
|
+
fieldEmailInvalid: "Enter a valid email address.",
|
|
858
886
|
comments: "Comments",
|
|
859
887
|
commentPlaceholder: "Add a comment",
|
|
860
888
|
postComment: "Post",
|
|
@@ -1053,6 +1081,7 @@ function useComments(featureId, { enabled = true } = {}) {
|
|
|
1053
1081
|
store.ensureComments(featureId);
|
|
1054
1082
|
}, [store, featureId, enabled, isReady]);
|
|
1055
1083
|
const loadMore = (0, import_react9.useCallback)(() => store.loadMoreComments(featureId), [store, featureId]);
|
|
1084
|
+
const refresh = (0, import_react9.useCallback)(() => store.refreshComments(featureId), [store, featureId]);
|
|
1056
1085
|
const add = (0, import_react9.useCallback)(
|
|
1057
1086
|
async (text, authorName) => {
|
|
1058
1087
|
setIsSubmitting(true);
|
|
@@ -1073,6 +1102,7 @@ function useComments(featureId, { enabled = true } = {}) {
|
|
|
1073
1102
|
submitError,
|
|
1074
1103
|
hasMore: comments.cursor !== null,
|
|
1075
1104
|
loadMore,
|
|
1105
|
+
refresh,
|
|
1076
1106
|
add
|
|
1077
1107
|
};
|
|
1078
1108
|
}
|
|
@@ -1092,10 +1122,15 @@ function formatDate(iso) {
|
|
|
1092
1122
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1093
1123
|
function CommentList({ featureId }) {
|
|
1094
1124
|
const { labels, cx } = useBoard();
|
|
1095
|
-
const { comments, isLoading, isLoadingMore, error, hasMore, loadMore } = useComments(featureId);
|
|
1125
|
+
const { comments, isLoading, isLoadingMore, error, hasMore, loadMore, refresh } = useComments(featureId);
|
|
1096
1126
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx("fw-comments"), children: [
|
|
1097
1127
|
isLoading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-muted"), children: labels.loading }),
|
|
1098
|
-
error && /* @__PURE__ */ (0, import_jsx_runtime3.
|
|
1128
|
+
error && comments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx("fw-empty", "fw-empty--compact"), role: "alert", children: [
|
|
1129
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-empty__title"), children: labels.errorComments }),
|
|
1130
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-empty__detail"), children: isNotConfigured(error) ? labels.errorNotSetUpTitle : userMessage(error) }),
|
|
1131
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
|
|
1132
|
+
] }),
|
|
1133
|
+
error && comments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) }),
|
|
1099
1134
|
!isLoading && !error && comments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-muted"), children: labels.noComments }),
|
|
1100
1135
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { className: cx("fw-comment-list"), children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: cx("fw-comment"), children: [
|
|
1101
1136
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: cx("fw-comment__author"), children: comment.authorName }),
|
|
@@ -1166,7 +1201,7 @@ function CommentForm({ featureId }) {
|
|
|
1166
1201
|
}
|
|
1167
1202
|
)
|
|
1168
1203
|
] }),
|
|
1169
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isSubmitting, children: labels.postComment }),
|
|
1204
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isSubmitting || !text.trim(), children: labels.postComment }),
|
|
1170
1205
|
submitError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(submitError) })
|
|
1171
1206
|
]
|
|
1172
1207
|
}
|
|
@@ -1182,20 +1217,24 @@ function FollowForm({ feature }) {
|
|
|
1182
1217
|
const [email, setEmail] = (0, import_react11.useState)(userEmail ?? "");
|
|
1183
1218
|
const [isOpen, setIsOpen] = (0, import_react11.useState)(false);
|
|
1184
1219
|
if (isFollowing) {
|
|
1185
|
-
return /* @__PURE__ */ (0, import_jsx_runtime4.
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1220
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
1221
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1222
|
+
"button",
|
|
1223
|
+
{
|
|
1224
|
+
type: "button",
|
|
1225
|
+
className: cx("fw-link"),
|
|
1226
|
+
disabled: isPending,
|
|
1227
|
+
onClick: () => void unfollow(),
|
|
1228
|
+
children: labels.following
|
|
1229
|
+
}
|
|
1230
|
+
),
|
|
1231
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) })
|
|
1232
|
+
] });
|
|
1195
1233
|
}
|
|
1196
1234
|
if (!isOpen) {
|
|
1197
1235
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: cx("fw-link"), onClick: () => setIsOpen(true), children: labels.follow });
|
|
1198
1236
|
}
|
|
1237
|
+
const isEmailValid = EMAIL_PATTERN.test(email.trim());
|
|
1199
1238
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1200
1239
|
"form",
|
|
1201
1240
|
{
|
|
@@ -1219,7 +1258,8 @@ function FollowForm({ feature }) {
|
|
|
1219
1258
|
required: true
|
|
1220
1259
|
}
|
|
1221
1260
|
),
|
|
1222
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("
|
|
1261
|
+
email !== "" && !isEmailValid && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cx("fw-error"), children: labels.fieldEmailInvalid }),
|
|
1262
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending || !isEmailValid, children: labels.follow }),
|
|
1223
1263
|
error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) })
|
|
1224
1264
|
]
|
|
1225
1265
|
}
|
|
@@ -1262,7 +1302,7 @@ function FeatureCard({
|
|
|
1262
1302
|
defaultCommentsOpen = false
|
|
1263
1303
|
}) {
|
|
1264
1304
|
const { labels, allowComments, cx } = useBoard();
|
|
1265
|
-
const { isPending, toggle } = useVote(feature.id);
|
|
1305
|
+
const { isPending, toggle, error: voteError } = useVote(feature.id);
|
|
1266
1306
|
const [showComments, setShowComments] = (0, import_react12.useState)(defaultCommentsOpen);
|
|
1267
1307
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("li", { className: cx("fw-card"), children: [
|
|
1268
1308
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VoteButton, { feature, isPending, onToggle: () => void toggle() }),
|
|
@@ -1290,6 +1330,7 @@ function FeatureCard({
|
|
|
1290
1330
|
),
|
|
1291
1331
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(FollowForm, { feature })
|
|
1292
1332
|
] }),
|
|
1333
|
+
voteError && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(voteError) }),
|
|
1293
1334
|
allowComments && showComments && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { id: `fw-comments-${feature.id}`, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CommentList, { featureId: feature.id }) })
|
|
1294
1335
|
] })
|
|
1295
1336
|
] });
|
|
@@ -1331,10 +1372,25 @@ function Bar({ width, height }) {
|
|
|
1331
1372
|
// src/components/FeatureList.tsx
|
|
1332
1373
|
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
1333
1374
|
function errorCopy(error, labels) {
|
|
1334
|
-
if (error
|
|
1335
|
-
return { title: labels.
|
|
1375
|
+
if (isNotConfigured(error)) {
|
|
1376
|
+
return { title: labels.errorNotSetUpTitle, detail: labels.errorNotSetUpDetail };
|
|
1377
|
+
}
|
|
1378
|
+
if (error.code === "network") {
|
|
1379
|
+
return { title: labels.errorOfflineTitle, detail: labels.errorOfflineDetail };
|
|
1380
|
+
}
|
|
1381
|
+
if (isWrongApp(error)) {
|
|
1382
|
+
return {
|
|
1383
|
+
title: labels.errorWrongAppTitle,
|
|
1384
|
+
detail: labels.errorWrongAppDetail || userMessage(error)
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
if (error.code === "invalid_key") {
|
|
1388
|
+
return { title: labels.errorInvalidKeyTitle, detail: labels.errorInvalidKeyDetail };
|
|
1336
1389
|
}
|
|
1337
|
-
return
|
|
1390
|
+
return {
|
|
1391
|
+
title: labels.errorTitle,
|
|
1392
|
+
detail: error.code === "server" ? labels.errorDetail : userMessage(error)
|
|
1393
|
+
};
|
|
1338
1394
|
}
|
|
1339
1395
|
function FeatureList() {
|
|
1340
1396
|
const { initialFeatureId } = useBoard();
|
|
@@ -1345,8 +1401,11 @@ function FeatureList() {
|
|
|
1345
1401
|
}
|
|
1346
1402
|
function PinnedFeature({ featureId }) {
|
|
1347
1403
|
const { cx } = useBoard();
|
|
1348
|
-
const { feature, isLoading } = useFeature(featureId);
|
|
1404
|
+
const { feature, isLoading, error } = useFeature(featureId);
|
|
1349
1405
|
if (isLoading) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FeatureListSkeleton, { rows: 1 });
|
|
1406
|
+
if (!feature && error) {
|
|
1407
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: cx("fw-error", "fw-pinned"), role: "alert", children: userMessage(error) });
|
|
1408
|
+
}
|
|
1350
1409
|
if (!feature) return null;
|
|
1351
1410
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cx("fw-pinned"), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { className: cx("fw-list"), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FeatureCard, { feature, defaultCommentsOpen: true }) }) });
|
|
1352
1411
|
}
|
|
@@ -1369,7 +1428,7 @@ function Listing({ excludeId }) {
|
|
|
1369
1428
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cx("fw-empty"), role: "alert", children: [
|
|
1370
1429
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: cx("fw-empty__title"), children: title }),
|
|
1371
1430
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: cx("fw-empty__detail"), children: detail }),
|
|
1372
|
-
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
|
|
1431
|
+
!isNotConfigured(error) && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
|
|
1373
1432
|
] });
|
|
1374
1433
|
}
|
|
1375
1434
|
if (isLoading && visible.length === 0) {
|
|
@@ -1384,7 +1443,11 @@ function Listing({ excludeId }) {
|
|
|
1384
1443
|
}
|
|
1385
1444
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
1386
1445
|
isRoadmap ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RoadmapColumns, { features: visible }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Rows2, { features: visible }),
|
|
1387
|
-
hasMore && /* @__PURE__ */ (0, import_jsx_runtime9.
|
|
1446
|
+
hasMore && error && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cx("fw-pagination"), role: "alert", children: [
|
|
1447
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: labels.errorLoadMore }),
|
|
1448
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void loadMore(), children: labels.retry })
|
|
1449
|
+
] }),
|
|
1450
|
+
hasMore && !error && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cx("fw-pagination"), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1388
1451
|
"button",
|
|
1389
1452
|
{
|
|
1390
1453
|
type: "button",
|
|
@@ -1460,11 +1523,24 @@ function useSubmitFeature() {
|
|
|
1460
1523
|
// src/components/SubmitForm.tsx
|
|
1461
1524
|
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
1462
1525
|
function SubmitForm() {
|
|
1463
|
-
const {
|
|
1526
|
+
const {
|
|
1527
|
+
labels,
|
|
1528
|
+
emailField,
|
|
1529
|
+
userEmail,
|
|
1530
|
+
isSubscriber,
|
|
1531
|
+
isFormOpen,
|
|
1532
|
+
setFormOpen,
|
|
1533
|
+
setJustSubmitted,
|
|
1534
|
+
cx
|
|
1535
|
+
} = useBoard();
|
|
1464
1536
|
const { submit, isPending, error } = useSubmitFeature();
|
|
1465
1537
|
const [title, setTitle] = (0, import_react14.useState)("");
|
|
1466
1538
|
const [description, setDescription] = (0, import_react14.useState)("");
|
|
1467
1539
|
const [email, setEmail] = (0, import_react14.useState)(userEmail ?? "");
|
|
1540
|
+
const isTitleValid = title.trim().length >= LIMITS.titleMin;
|
|
1541
|
+
const isEmailInvalid = emailField !== "off" && email !== "" && !EMAIL_PATTERN.test(email.trim());
|
|
1542
|
+
const isEmailMissing = emailField === "required" && !email.trim();
|
|
1543
|
+
const canSubmit = isTitleValid && !isEmailInvalid && !isEmailMissing;
|
|
1468
1544
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
|
|
1469
1545
|
"form",
|
|
1470
1546
|
{
|
|
@@ -1478,8 +1554,10 @@ function SubmitForm() {
|
|
|
1478
1554
|
isSubscriber
|
|
1479
1555
|
});
|
|
1480
1556
|
if (!created) return;
|
|
1481
|
-
|
|
1482
|
-
|
|
1557
|
+
if (!isFormOpen) {
|
|
1558
|
+
setTitle("");
|
|
1559
|
+
setDescription("");
|
|
1560
|
+
}
|
|
1483
1561
|
setJustSubmitted(true);
|
|
1484
1562
|
setFormOpen(false);
|
|
1485
1563
|
},
|
|
@@ -1498,7 +1576,15 @@ function SubmitForm() {
|
|
|
1498
1576
|
onChange: (event) => setTitle(event.target.value),
|
|
1499
1577
|
required: true
|
|
1500
1578
|
}
|
|
1501
|
-
)
|
|
1579
|
+
),
|
|
1580
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field__foot"), children: [
|
|
1581
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: cx("fw-error"), children: title !== "" && !isTitleValid ? labels.fieldTitleTooShort : "" }),
|
|
1582
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: cx("fw-muted"), children: [
|
|
1583
|
+
title.length,
|
|
1584
|
+
"/",
|
|
1585
|
+
LIMITS.titleMax
|
|
1586
|
+
] })
|
|
1587
|
+
] })
|
|
1502
1588
|
] }),
|
|
1503
1589
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field"), children: [
|
|
1504
1590
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { className: cx("fw-label"), htmlFor: "fw-description", children: labels.fieldDescription }),
|
|
@@ -1512,7 +1598,15 @@ function SubmitForm() {
|
|
|
1512
1598
|
placeholder: labels.fieldDescriptionPlaceholder,
|
|
1513
1599
|
onChange: (event) => setDescription(event.target.value)
|
|
1514
1600
|
}
|
|
1515
|
-
)
|
|
1601
|
+
),
|
|
1602
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field__foot"), children: [
|
|
1603
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", {}),
|
|
1604
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: cx("fw-muted"), children: [
|
|
1605
|
+
description.length,
|
|
1606
|
+
"/",
|
|
1607
|
+
LIMITS.descriptionMax
|
|
1608
|
+
] })
|
|
1609
|
+
] })
|
|
1516
1610
|
] }),
|
|
1517
1611
|
emailField !== "off" && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field"), children: [
|
|
1518
1612
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { className: cx("fw-label"), htmlFor: "fw-email", children: labels.fieldEmail }),
|
|
@@ -1529,11 +1623,11 @@ function SubmitForm() {
|
|
|
1529
1623
|
required: emailField === "required"
|
|
1530
1624
|
}
|
|
1531
1625
|
),
|
|
1532
|
-
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { id: "fw-email-hint", className: cx("fw-muted"), children: labels.fieldEmailHint })
|
|
1626
|
+
isEmailInvalid ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { id: "fw-email-hint", className: cx("fw-error"), children: labels.fieldEmailInvalid }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { id: "fw-email-hint", className: cx("fw-muted"), children: labels.fieldEmailHint })
|
|
1533
1627
|
] }),
|
|
1534
1628
|
error && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) }),
|
|
1535
1629
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-form__actions"), children: [
|
|
1536
|
-
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending, children: isPending ? labels.submitting : labels.submit }),
|
|
1630
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending || !canSubmit, children: isPending ? labels.submitting : labels.submit }),
|
|
1537
1631
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
1538
1632
|
"button",
|
|
1539
1633
|
{
|
|
@@ -1657,19 +1751,92 @@ function BoardRoot({
|
|
|
1657
1751
|
) });
|
|
1658
1752
|
}
|
|
1659
1753
|
function DefaultBoard() {
|
|
1660
|
-
const {
|
|
1754
|
+
const { renderHeader } = useBoard();
|
|
1661
1755
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
|
|
1662
1756
|
renderHeader ? renderHeader() : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(BoardHeading, {}),
|
|
1663
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.
|
|
1664
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(BoardTabs, {}),
|
|
1665
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SubmitTrigger, {})
|
|
1666
|
-
] }),
|
|
1667
|
-
justSubmitted && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: cx("fw-alert"), role: "status", children: labels.submitted }),
|
|
1668
|
-
isFormOpen && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SubmitForm, {}),
|
|
1669
|
-
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(FeatureList, {}),
|
|
1757
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(BoardPanels, {}),
|
|
1670
1758
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Branding, {})
|
|
1671
1759
|
] });
|
|
1672
1760
|
}
|
|
1761
|
+
var LEAVE_MS = 110;
|
|
1762
|
+
var ENTER_MS = 300;
|
|
1763
|
+
var ENTER_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
1764
|
+
function canAnimate(element) {
|
|
1765
|
+
if (!element || typeof element.animate !== "function") return false;
|
|
1766
|
+
return !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
1767
|
+
}
|
|
1768
|
+
function BoardPanels() {
|
|
1769
|
+
const { isFormOpen, setFormOpen, justSubmitted, setJustSubmitted, labels, unstyled, cx } = useBoard();
|
|
1770
|
+
const [showForm, setShowForm] = (0, import_react15.useState)(isFormOpen);
|
|
1771
|
+
const panel = (0, import_react15.useRef)(null);
|
|
1772
|
+
const trigger = (0, import_react15.useRef)(null);
|
|
1773
|
+
const entering = (0, import_react15.useRef)(null);
|
|
1774
|
+
const hasSwapped = (0, import_react15.useRef)(false);
|
|
1775
|
+
(0, import_react15.useEffect)(() => {
|
|
1776
|
+
if (isFormOpen === showForm) return;
|
|
1777
|
+
hasSwapped.current = true;
|
|
1778
|
+
const element = panel.current;
|
|
1779
|
+
if (unstyled || !canAnimate(element)) {
|
|
1780
|
+
setShowForm(isFormOpen);
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
entering.current?.cancel();
|
|
1784
|
+
const leaving = element.animate(
|
|
1785
|
+
{ opacity: [1, 0], transform: ["translateY(0)", "translateY(-4px)"] },
|
|
1786
|
+
{ duration: LEAVE_MS, easing: "ease-in", fill: "forwards" }
|
|
1787
|
+
);
|
|
1788
|
+
leaving.onfinish = () => {
|
|
1789
|
+
const from = element.offsetHeight;
|
|
1790
|
+
(0, import_react_dom.flushSync)(() => setShowForm(isFormOpen));
|
|
1791
|
+
leaving.cancel();
|
|
1792
|
+
const to = element.offsetHeight;
|
|
1793
|
+
element.style.overflow = "hidden";
|
|
1794
|
+
const enter = element.animate(
|
|
1795
|
+
{
|
|
1796
|
+
opacity: [0, 1],
|
|
1797
|
+
transform: ["translateY(6px)", "translateY(0)"],
|
|
1798
|
+
height: [`${from}px`, `${to}px`]
|
|
1799
|
+
},
|
|
1800
|
+
{ duration: ENTER_MS, easing: ENTER_EASING }
|
|
1801
|
+
);
|
|
1802
|
+
const finish = () => {
|
|
1803
|
+
element.style.overflow = "";
|
|
1804
|
+
};
|
|
1805
|
+
enter.onfinish = finish;
|
|
1806
|
+
enter.oncancel = finish;
|
|
1807
|
+
entering.current = enter;
|
|
1808
|
+
};
|
|
1809
|
+
return () => leaving.cancel();
|
|
1810
|
+
}, [isFormOpen, showForm, unstyled]);
|
|
1811
|
+
(0, import_react15.useEffect)(() => {
|
|
1812
|
+
if (!hasSwapped.current) return;
|
|
1813
|
+
const target = showForm ? panel.current?.querySelector("#fw-title") : trigger.current;
|
|
1814
|
+
target?.focus({ preventScroll: true });
|
|
1815
|
+
}, [showForm]);
|
|
1816
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { ref: panel, children: [
|
|
1817
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { hidden: showForm, children: [
|
|
1818
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: cx("fw-toolbar"), children: [
|
|
1819
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(BoardTabs, {}),
|
|
1820
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1821
|
+
"button",
|
|
1822
|
+
{
|
|
1823
|
+
ref: trigger,
|
|
1824
|
+
type: "button",
|
|
1825
|
+
className: cx("fw-btn"),
|
|
1826
|
+
onClick: () => {
|
|
1827
|
+
setJustSubmitted(false);
|
|
1828
|
+
setFormOpen(true);
|
|
1829
|
+
},
|
|
1830
|
+
children: labels.newRequest
|
|
1831
|
+
}
|
|
1832
|
+
)
|
|
1833
|
+
] }),
|
|
1834
|
+
justSubmitted && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { className: cx("fw-alert"), role: "status", children: labels.submitted }),
|
|
1835
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(FeatureList, {})
|
|
1836
|
+
] }),
|
|
1837
|
+
showForm && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SubmitForm, {})
|
|
1838
|
+
] });
|
|
1839
|
+
}
|
|
1673
1840
|
function BoardHeading() {
|
|
1674
1841
|
const { labels, cx } = useBoard();
|
|
1675
1842
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("h2", { className: cx("fw-heading"), children: labels.title });
|
|
@@ -1781,7 +1948,7 @@ function FewchursBoardDialog({
|
|
|
1781
1948
|
|
|
1782
1949
|
// src/components/BoardButton.tsx
|
|
1783
1950
|
var import_react17 = require("react");
|
|
1784
|
-
var
|
|
1951
|
+
var import_react_dom2 = require("react-dom");
|
|
1785
1952
|
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
1786
1953
|
function launcherClasses(floating) {
|
|
1787
1954
|
if (!floating) return "fw-launcher";
|
|
@@ -1812,7 +1979,7 @@ function FewchursButton({
|
|
|
1812
1979
|
children
|
|
1813
1980
|
}
|
|
1814
1981
|
),
|
|
1815
|
-
open ? (0,
|
|
1982
|
+
open ? (0, import_react_dom2.createPortal)(
|
|
1816
1983
|
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
1817
1984
|
FewchursBoardDialog,
|
|
1818
1985
|
{
|
|
@@ -1846,6 +2013,7 @@ function FewchursButton({
|
|
|
1846
2013
|
FewchursProvider,
|
|
1847
2014
|
FollowForm,
|
|
1848
2015
|
LIMITS,
|
|
2016
|
+
NOT_CONFIGURED_MESSAGE,
|
|
1849
2017
|
ROADMAP_STATUSES,
|
|
1850
2018
|
SDK_HEADER,
|
|
1851
2019
|
SDK_VERSION,
|