@fewchurs/react 1.3.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 +52 -29
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +51 -29
- package/dist/core.mjs.map +1 -1
- package/dist/index.d.mts +20 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.js +152 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +161 -70
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +41 -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);
|
|
@@ -226,7 +239,9 @@ function createFewchursClient(options = {}) {
|
|
|
226
239
|
await delay(retryDelayMs);
|
|
227
240
|
continue;
|
|
228
241
|
}
|
|
229
|
-
|
|
242
|
+
const failure2 = networkError(messageOf(error));
|
|
243
|
+
logError(failure2);
|
|
244
|
+
return { ok: false, error: failure2 };
|
|
230
245
|
}
|
|
231
246
|
if (response.ok) {
|
|
232
247
|
const payload = await readJson(response);
|
|
@@ -237,7 +252,7 @@ function createFewchursClient(options = {}) {
|
|
|
237
252
|
continue;
|
|
238
253
|
}
|
|
239
254
|
const failure = errorFromResponse(response.status, await readJson(response));
|
|
240
|
-
|
|
255
|
+
logError(failure);
|
|
241
256
|
return { ok: false, error: failure };
|
|
242
257
|
}
|
|
243
258
|
return { ok: false, error: networkError() };
|
|
@@ -328,14 +343,20 @@ function delay(ms) {
|
|
|
328
343
|
function messageOf(error) {
|
|
329
344
|
return error instanceof Error ? error.message : "Network request failed.";
|
|
330
345
|
}
|
|
331
|
-
var
|
|
332
|
-
|
|
333
|
-
|
|
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.';
|
|
349
|
+
var loggedErrors = /* @__PURE__ */ new Set();
|
|
350
|
+
function logOnce(detail) {
|
|
334
351
|
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
352
|
+
if (loggedErrors.has(detail)) return;
|
|
353
|
+
loggedErrors.add(detail);
|
|
354
|
+
console.warn(`[fewchurs] : ${detail}`);
|
|
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);
|
|
339
360
|
}
|
|
340
361
|
|
|
341
362
|
// src/identity/deviceId.ts
|
|
@@ -765,6 +786,20 @@ var DEFAULT_LABELS = {
|
|
|
765
786
|
loading: "Loading\u2026",
|
|
766
787
|
loadMore: "Load more",
|
|
767
788
|
retry: "Try again",
|
|
789
|
+
errorInvalidKeyTitle: "Invalid API key",
|
|
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.",
|
|
768
803
|
comments: "Comments",
|
|
769
804
|
commentPlaceholder: "Add a comment",
|
|
770
805
|
postComment: "Post",
|
|
@@ -963,6 +998,7 @@ function useComments(featureId, { enabled = true } = {}) {
|
|
|
963
998
|
store.ensureComments(featureId);
|
|
964
999
|
}, [store, featureId, enabled, isReady]);
|
|
965
1000
|
const loadMore = useCallback5(() => store.loadMoreComments(featureId), [store, featureId]);
|
|
1001
|
+
const refresh = useCallback5(() => store.refreshComments(featureId), [store, featureId]);
|
|
966
1002
|
const add = useCallback5(
|
|
967
1003
|
async (text, authorName) => {
|
|
968
1004
|
setIsSubmitting(true);
|
|
@@ -983,6 +1019,7 @@ function useComments(featureId, { enabled = true } = {}) {
|
|
|
983
1019
|
submitError,
|
|
984
1020
|
hasMore: comments.cursor !== null,
|
|
985
1021
|
loadMore,
|
|
1022
|
+
refresh,
|
|
986
1023
|
add
|
|
987
1024
|
};
|
|
988
1025
|
}
|
|
@@ -1002,10 +1039,15 @@ function formatDate(iso) {
|
|
|
1002
1039
|
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
1003
1040
|
function CommentList({ featureId }) {
|
|
1004
1041
|
const { labels, cx } = useBoard();
|
|
1005
|
-
const { comments, isLoading, isLoadingMore, error, hasMore, loadMore } = useComments(featureId);
|
|
1042
|
+
const { comments, isLoading, isLoadingMore, error, hasMore, loadMore, refresh } = useComments(featureId);
|
|
1006
1043
|
return /* @__PURE__ */ jsxs("div", { className: cx("fw-comments"), children: [
|
|
1007
1044
|
isLoading && /* @__PURE__ */ jsx3("p", { className: cx("fw-muted"), children: labels.loading }),
|
|
1008
|
-
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) }),
|
|
1009
1051
|
!isLoading && !error && comments.length === 0 && /* @__PURE__ */ jsx3("p", { className: cx("fw-muted"), children: labels.noComments }),
|
|
1010
1052
|
/* @__PURE__ */ jsx3("ul", { className: cx("fw-comment-list"), children: comments.map((comment) => /* @__PURE__ */ jsxs("li", { className: cx("fw-comment"), children: [
|
|
1011
1053
|
/* @__PURE__ */ jsx3("span", { className: cx("fw-comment__author"), children: comment.authorName }),
|
|
@@ -1076,7 +1118,7 @@ function CommentForm({ featureId }) {
|
|
|
1076
1118
|
}
|
|
1077
1119
|
)
|
|
1078
1120
|
] }),
|
|
1079
|
-
/* @__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 }),
|
|
1080
1122
|
submitError && /* @__PURE__ */ jsx3("p", { className: cx("fw-error"), role: "alert", children: userMessage(submitError) })
|
|
1081
1123
|
]
|
|
1082
1124
|
}
|
|
@@ -1085,27 +1127,31 @@ function CommentForm({ featureId }) {
|
|
|
1085
1127
|
|
|
1086
1128
|
// src/components/FollowForm.tsx
|
|
1087
1129
|
import { useState as useState6 } from "react";
|
|
1088
|
-
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1130
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1089
1131
|
function FollowForm({ feature }) {
|
|
1090
1132
|
const { labels, userEmail, cx } = useBoard();
|
|
1091
1133
|
const { isFollowing, isPending, error, follow, unfollow } = useFollow(feature.id);
|
|
1092
1134
|
const [email, setEmail] = useState6(userEmail ?? "");
|
|
1093
1135
|
const [isOpen, setIsOpen] = useState6(false);
|
|
1094
1136
|
if (isFollowing) {
|
|
1095
|
-
return /* @__PURE__ */
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
+
] });
|
|
1105
1150
|
}
|
|
1106
1151
|
if (!isOpen) {
|
|
1107
1152
|
return /* @__PURE__ */ jsx4("button", { type: "button", className: cx("fw-link"), onClick: () => setIsOpen(true), children: labels.follow });
|
|
1108
1153
|
}
|
|
1154
|
+
const isEmailValid = EMAIL_PATTERN.test(email.trim());
|
|
1109
1155
|
return /* @__PURE__ */ jsxs2(
|
|
1110
1156
|
"form",
|
|
1111
1157
|
{
|
|
@@ -1129,7 +1175,8 @@ function FollowForm({ feature }) {
|
|
|
1129
1175
|
required: true
|
|
1130
1176
|
}
|
|
1131
1177
|
),
|
|
1132
|
-
/* @__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 }),
|
|
1133
1180
|
error && /* @__PURE__ */ jsx4("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) })
|
|
1134
1181
|
]
|
|
1135
1182
|
}
|
|
@@ -1172,7 +1219,7 @@ function FeatureCard({
|
|
|
1172
1219
|
defaultCommentsOpen = false
|
|
1173
1220
|
}) {
|
|
1174
1221
|
const { labels, allowComments, cx } = useBoard();
|
|
1175
|
-
const { isPending, toggle } = useVote(feature.id);
|
|
1222
|
+
const { isPending, toggle, error: voteError } = useVote(feature.id);
|
|
1176
1223
|
const [showComments, setShowComments] = useState7(defaultCommentsOpen);
|
|
1177
1224
|
return /* @__PURE__ */ jsxs4("li", { className: cx("fw-card"), children: [
|
|
1178
1225
|
/* @__PURE__ */ jsx7(VoteButton, { feature, isPending, onToggle: () => void toggle() }),
|
|
@@ -1200,6 +1247,7 @@ function FeatureCard({
|
|
|
1200
1247
|
),
|
|
1201
1248
|
/* @__PURE__ */ jsx7(FollowForm, { feature })
|
|
1202
1249
|
] }),
|
|
1250
|
+
voteError && /* @__PURE__ */ jsx7("p", { className: cx("fw-error"), role: "alert", children: userMessage(voteError) }),
|
|
1203
1251
|
allowComments && showComments && /* @__PURE__ */ jsx7("div", { id: `fw-comments-${feature.id}`, children: /* @__PURE__ */ jsx7(CommentList, { featureId: feature.id }) })
|
|
1204
1252
|
] })
|
|
1205
1253
|
] });
|
|
@@ -1239,18 +1287,42 @@ function Bar({ width, height }) {
|
|
|
1239
1287
|
}
|
|
1240
1288
|
|
|
1241
1289
|
// src/components/FeatureList.tsx
|
|
1242
|
-
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";
|
|
1291
|
+
function errorCopy(error, 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 };
|
|
1306
|
+
}
|
|
1307
|
+
return {
|
|
1308
|
+
title: labels.errorTitle,
|
|
1309
|
+
detail: error.code === "server" ? labels.errorDetail : userMessage(error)
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1243
1312
|
function FeatureList() {
|
|
1244
1313
|
const { initialFeatureId } = useBoard();
|
|
1245
|
-
return /* @__PURE__ */ jsxs6(
|
|
1314
|
+
return /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
1246
1315
|
initialFeatureId && /* @__PURE__ */ jsx9(PinnedFeature, { featureId: initialFeatureId }),
|
|
1247
1316
|
/* @__PURE__ */ jsx9(Listing, { excludeId: initialFeatureId })
|
|
1248
1317
|
] });
|
|
1249
1318
|
}
|
|
1250
1319
|
function PinnedFeature({ featureId }) {
|
|
1251
1320
|
const { cx } = useBoard();
|
|
1252
|
-
const { feature, isLoading } = useFeature(featureId);
|
|
1321
|
+
const { feature, isLoading, error } = useFeature(featureId);
|
|
1253
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
|
+
}
|
|
1254
1326
|
if (!feature) return null;
|
|
1255
1327
|
return /* @__PURE__ */ jsx9("div", { className: cx("fw-pinned"), children: /* @__PURE__ */ jsx9("ul", { className: cx("fw-list"), children: /* @__PURE__ */ jsx9(FeatureCard, { feature, defaultCommentsOpen: true }) }) });
|
|
1256
1328
|
}
|
|
@@ -1269,32 +1341,30 @@ function Listing({ excludeId }) {
|
|
|
1269
1341
|
return showDeclined || feature.status !== "declined";
|
|
1270
1342
|
});
|
|
1271
1343
|
if (error && visible.length === 0) {
|
|
1344
|
+
const { title, detail } = errorCopy(error, labels);
|
|
1272
1345
|
return /* @__PURE__ */ jsxs6("div", { className: cx("fw-empty"), role: "alert", children: [
|
|
1273
|
-
/* @__PURE__ */ jsx9("p", {
|
|
1274
|
-
/* @__PURE__ */ jsx9(
|
|
1275
|
-
|
|
1276
|
-
{
|
|
1277
|
-
type: "button",
|
|
1278
|
-
className: cx("fw-btn", "fw-btn--ghost"),
|
|
1279
|
-
onClick: () => void refresh(),
|
|
1280
|
-
children: labels.retry
|
|
1281
|
-
}
|
|
1282
|
-
)
|
|
1346
|
+
/* @__PURE__ */ jsx9("p", { className: cx("fw-empty__title"), children: title }),
|
|
1347
|
+
/* @__PURE__ */ jsx9("p", { className: cx("fw-empty__detail"), children: detail }),
|
|
1348
|
+
!isNotConfigured(error) && /* @__PURE__ */ jsx9("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
|
|
1283
1349
|
] });
|
|
1284
1350
|
}
|
|
1285
1351
|
if (isLoading && visible.length === 0) {
|
|
1286
1352
|
return /* @__PURE__ */ jsx9(FeatureListSkeleton, { roadmap: isRoadmap });
|
|
1287
1353
|
}
|
|
1288
1354
|
if (visible.length === 0) {
|
|
1289
|
-
if (renderEmpty) return /* @__PURE__ */ jsx9(
|
|
1355
|
+
if (renderEmpty) return /* @__PURE__ */ jsx9(Fragment2, { children: renderEmpty() });
|
|
1290
1356
|
return /* @__PURE__ */ jsxs6("div", { className: cx("fw-empty"), children: [
|
|
1291
1357
|
/* @__PURE__ */ jsx9("p", { className: cx("fw-empty__title"), children: labels.emptyTitle }),
|
|
1292
1358
|
/* @__PURE__ */ jsx9("p", { children: labels.emptyBody })
|
|
1293
1359
|
] });
|
|
1294
1360
|
}
|
|
1295
|
-
return /* @__PURE__ */ jsxs6(
|
|
1361
|
+
return /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
1296
1362
|
isRoadmap ? /* @__PURE__ */ jsx9(RoadmapColumns, { features: visible }) : /* @__PURE__ */ jsx9(Rows2, { features: visible }),
|
|
1297
|
-
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(
|
|
1298
1368
|
"button",
|
|
1299
1369
|
{
|
|
1300
1370
|
type: "button",
|
|
@@ -1337,7 +1407,7 @@ function FeatureSlot({ feature }) {
|
|
|
1337
1407
|
const { renderFeature } = useBoard();
|
|
1338
1408
|
const { toggle } = useVote(feature.id);
|
|
1339
1409
|
const { follow, unfollow } = useFollow(feature.id);
|
|
1340
|
-
return /* @__PURE__ */ jsx9(
|
|
1410
|
+
return /* @__PURE__ */ jsx9(Fragment2, { children: renderFeature?.(feature, { toggleVote: toggle, follow, unfollow }) });
|
|
1341
1411
|
}
|
|
1342
1412
|
|
|
1343
1413
|
// src/components/SubmitForm.tsx
|
|
@@ -1375,6 +1445,10 @@ function SubmitForm() {
|
|
|
1375
1445
|
const [title, setTitle] = useState9("");
|
|
1376
1446
|
const [description, setDescription] = useState9("");
|
|
1377
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;
|
|
1378
1452
|
return /* @__PURE__ */ jsxs7(
|
|
1379
1453
|
"form",
|
|
1380
1454
|
{
|
|
@@ -1408,7 +1482,15 @@ function SubmitForm() {
|
|
|
1408
1482
|
onChange: (event) => setTitle(event.target.value),
|
|
1409
1483
|
required: true
|
|
1410
1484
|
}
|
|
1411
|
-
)
|
|
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
|
+
] })
|
|
1412
1494
|
] }),
|
|
1413
1495
|
/* @__PURE__ */ jsxs7("div", { className: cx("fw-field"), children: [
|
|
1414
1496
|
/* @__PURE__ */ jsx10("label", { className: cx("fw-label"), htmlFor: "fw-description", children: labels.fieldDescription }),
|
|
@@ -1422,7 +1504,15 @@ function SubmitForm() {
|
|
|
1422
1504
|
placeholder: labels.fieldDescriptionPlaceholder,
|
|
1423
1505
|
onChange: (event) => setDescription(event.target.value)
|
|
1424
1506
|
}
|
|
1425
|
-
)
|
|
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
|
+
] })
|
|
1426
1516
|
] }),
|
|
1427
1517
|
emailField !== "off" && /* @__PURE__ */ jsxs7("div", { className: cx("fw-field"), children: [
|
|
1428
1518
|
/* @__PURE__ */ jsx10("label", { className: cx("fw-label"), htmlFor: "fw-email", children: labels.fieldEmail }),
|
|
@@ -1439,11 +1529,11 @@ function SubmitForm() {
|
|
|
1439
1529
|
required: emailField === "required"
|
|
1440
1530
|
}
|
|
1441
1531
|
),
|
|
1442
|
-
/* @__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 })
|
|
1443
1533
|
] }),
|
|
1444
1534
|
error && /* @__PURE__ */ jsx10("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) }),
|
|
1445
1535
|
/* @__PURE__ */ jsxs7("div", { className: cx("fw-form__actions"), children: [
|
|
1446
|
-
/* @__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 }),
|
|
1447
1537
|
/* @__PURE__ */ jsx10(
|
|
1448
1538
|
"button",
|
|
1449
1539
|
{
|
|
@@ -1481,7 +1571,7 @@ function themeToStyle(theme) {
|
|
|
1481
1571
|
}
|
|
1482
1572
|
|
|
1483
1573
|
// src/components/Board.tsx
|
|
1484
|
-
import { Fragment as
|
|
1574
|
+
import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1485
1575
|
var DEFAULT_TABS = ["top", "new"];
|
|
1486
1576
|
function BoardRoot({
|
|
1487
1577
|
tabs = DEFAULT_TABS,
|
|
@@ -1568,7 +1658,7 @@ function BoardRoot({
|
|
|
1568
1658
|
}
|
|
1569
1659
|
function DefaultBoard() {
|
|
1570
1660
|
const { isFormOpen, justSubmitted, labels, renderHeader, cx } = useBoard();
|
|
1571
|
-
return /* @__PURE__ */ jsxs8(
|
|
1661
|
+
return /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
1572
1662
|
renderHeader ? renderHeader() : /* @__PURE__ */ jsx11(BoardHeading, {}),
|
|
1573
1663
|
/* @__PURE__ */ jsxs8("div", { className: cx("fw-toolbar"), children: [
|
|
1574
1664
|
/* @__PURE__ */ jsx11(BoardTabs, {}),
|
|
@@ -1692,7 +1782,7 @@ function FewchursBoardDialog({
|
|
|
1692
1782
|
// src/components/BoardButton.tsx
|
|
1693
1783
|
import { useCallback as useCallback7, useState as useState11 } from "react";
|
|
1694
1784
|
import { createPortal } from "react-dom";
|
|
1695
|
-
import { Fragment as
|
|
1785
|
+
import { Fragment as Fragment4, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1696
1786
|
function launcherClasses(floating) {
|
|
1697
1787
|
if (!floating) return "fw-launcher";
|
|
1698
1788
|
const side = floating === "left" ? " fw-launcher--left" : "";
|
|
@@ -1709,7 +1799,7 @@ function FewchursButton({
|
|
|
1709
1799
|
const [open, setOpen] = useState11(false);
|
|
1710
1800
|
const close = useCallback7(() => setOpen(false), []);
|
|
1711
1801
|
const classes = [board.unstyled ? "" : launcherClasses(floating), className].filter(Boolean).join(" ");
|
|
1712
|
-
return /* @__PURE__ */ jsxs10(
|
|
1802
|
+
return /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
1713
1803
|
/* @__PURE__ */ jsx13(
|
|
1714
1804
|
"button",
|
|
1715
1805
|
{
|
|
@@ -1755,6 +1845,7 @@ export {
|
|
|
1755
1845
|
FewchursProvider,
|
|
1756
1846
|
FollowForm,
|
|
1757
1847
|
LIMITS,
|
|
1848
|
+
NOT_CONFIGURED_MESSAGE,
|
|
1758
1849
|
ROADMAP_STATUSES,
|
|
1759
1850
|
SDK_HEADER,
|
|
1760
1851
|
SDK_VERSION,
|