@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/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.3.0";
89
+ var SDK_VERSION = "1.5.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: "Feedback is unavailable right now.",
108
- validation_failed: "Please check what you entered and try again.",
109
- rate_limited: "You have reached today's limit. Try again tomorrow.",
110
- not_found: "That request no longer exists.",
111
- limit_reached: "Feedback is unavailable right now.",
112
- server: "Something went wrong. Please try again.",
113
- network: "Could not reach the server. Check your connection."
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 message = body?.error?.message ?? `Request failed (${status}).`;
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 at least ${LIMITS.titleMin} characters.`);
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) return validationError("Write a comment first.");
248
- if (text.length > LIMITS.commentMax) {
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);
@@ -307,7 +321,9 @@ function createFewchursClient(options = {}) {
307
321
  await delay(retryDelayMs);
308
322
  continue;
309
323
  }
310
- return { ok: false, error: networkError(messageOf(error)) };
324
+ const failure2 = networkError(messageOf(error));
325
+ logError(failure2);
326
+ return { ok: false, error: failure2 };
311
327
  }
312
328
  if (response.ok) {
313
329
  const payload = await readJson(response);
@@ -318,7 +334,7 @@ function createFewchursClient(options = {}) {
318
334
  continue;
319
335
  }
320
336
  const failure = errorFromResponse(response.status, await readJson(response));
321
- warnAboutKey(failure);
337
+ logError(failure);
322
338
  return { ok: false, error: failure };
323
339
  }
324
340
  return { ok: false, error: networkError() };
@@ -409,14 +425,20 @@ function delay(ms) {
409
425
  function messageOf(error) {
410
426
  return error instanceof Error ? error.message : "Network request failed.";
411
427
  }
412
- var warnedAboutKey = false;
413
- function warnAboutKey(error) {
414
- if (error.code !== "invalid_key" || warnedAboutKey) return;
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.';
431
+ var loggedErrors = /* @__PURE__ */ new Set();
432
+ function logOnce(detail) {
415
433
  if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
416
- warnedAboutKey = true;
417
- console.warn(
418
- error.status === 403 ? `[fewchurs] ${error.message}` : "[fewchurs] The API key was rejected. Check FEWCHURS_API_KEY (server) or the apiKey passed to FewchursProvider, and that the key is active in your Fewchurs dashboard."
419
- );
434
+ if (loggedErrors.has(detail)) return;
435
+ loggedErrors.add(detail);
436
+ console.warn(`[fewchurs] : ${detail}`);
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);
420
442
  }
421
443
 
422
444
  // src/identity/deviceId.ts
@@ -846,6 +868,20 @@ var DEFAULT_LABELS = {
846
868
  loading: "Loading\u2026",
847
869
  loadMore: "Load more",
848
870
  retry: "Try again",
871
+ errorInvalidKeyTitle: "Invalid API key",
872
+ 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.",
873
+ errorWrongAppTitle: "Invalid API key",
874
+ errorWrongAppDetail: "",
875
+ errorTitle: "Couldn't load requests",
876
+ errorDetail: "Something went wrong. Try again.",
877
+ errorOfflineTitle: "You're offline",
878
+ errorOfflineDetail: "Check your connection and try again.",
879
+ errorNotSetUpTitle: "Fewchurs isn't set up",
880
+ errorNotSetUpDetail: "Pass apiKey to FewchursProvider, or set FEWCHURS_API_KEY on your server.",
881
+ errorLoadMore: "Couldn't load more.",
882
+ errorComments: "Couldn't load comments.",
883
+ fieldTitleTooShort: "At least 3 characters.",
884
+ fieldEmailInvalid: "Enter a valid email address.",
849
885
  comments: "Comments",
850
886
  commentPlaceholder: "Add a comment",
851
887
  postComment: "Post",
@@ -1044,6 +1080,7 @@ function useComments(featureId, { enabled = true } = {}) {
1044
1080
  store.ensureComments(featureId);
1045
1081
  }, [store, featureId, enabled, isReady]);
1046
1082
  const loadMore = (0, import_react9.useCallback)(() => store.loadMoreComments(featureId), [store, featureId]);
1083
+ const refresh = (0, import_react9.useCallback)(() => store.refreshComments(featureId), [store, featureId]);
1047
1084
  const add = (0, import_react9.useCallback)(
1048
1085
  async (text, authorName) => {
1049
1086
  setIsSubmitting(true);
@@ -1064,6 +1101,7 @@ function useComments(featureId, { enabled = true } = {}) {
1064
1101
  submitError,
1065
1102
  hasMore: comments.cursor !== null,
1066
1103
  loadMore,
1104
+ refresh,
1067
1105
  add
1068
1106
  };
1069
1107
  }
@@ -1083,10 +1121,15 @@ function formatDate(iso) {
1083
1121
  var import_jsx_runtime3 = require("react/jsx-runtime");
1084
1122
  function CommentList({ featureId }) {
1085
1123
  const { labels, cx } = useBoard();
1086
- const { comments, isLoading, isLoadingMore, error, hasMore, loadMore } = useComments(featureId);
1124
+ const { comments, isLoading, isLoadingMore, error, hasMore, loadMore, refresh } = useComments(featureId);
1087
1125
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx("fw-comments"), children: [
1088
1126
  isLoading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-muted"), children: labels.loading }),
1089
- error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) }),
1127
+ error && comments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx("fw-empty", "fw-empty--compact"), role: "alert", children: [
1128
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-empty__title"), children: labels.errorComments }),
1129
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-empty__detail"), children: isNotConfigured(error) ? labels.errorNotSetUpTitle : userMessage(error) }),
1130
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
1131
+ ] }),
1132
+ error && comments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) }),
1090
1133
  !isLoading && !error && comments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-muted"), children: labels.noComments }),
1091
1134
  /* @__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: [
1092
1135
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: cx("fw-comment__author"), children: comment.authorName }),
@@ -1157,7 +1200,7 @@ function CommentForm({ featureId }) {
1157
1200
  }
1158
1201
  )
1159
1202
  ] }),
1160
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isSubmitting, children: labels.postComment }),
1203
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isSubmitting || !text.trim(), children: labels.postComment }),
1161
1204
  submitError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(submitError) })
1162
1205
  ]
1163
1206
  }
@@ -1173,20 +1216,24 @@ function FollowForm({ feature }) {
1173
1216
  const [email, setEmail] = (0, import_react11.useState)(userEmail ?? "");
1174
1217
  const [isOpen, setIsOpen] = (0, import_react11.useState)(false);
1175
1218
  if (isFollowing) {
1176
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1177
- "button",
1178
- {
1179
- type: "button",
1180
- className: cx("fw-link"),
1181
- disabled: isPending,
1182
- onClick: () => void unfollow(),
1183
- children: labels.following
1184
- }
1185
- );
1219
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1220
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1221
+ "button",
1222
+ {
1223
+ type: "button",
1224
+ className: cx("fw-link"),
1225
+ disabled: isPending,
1226
+ onClick: () => void unfollow(),
1227
+ children: labels.following
1228
+ }
1229
+ ),
1230
+ error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) })
1231
+ ] });
1186
1232
  }
1187
1233
  if (!isOpen) {
1188
1234
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: cx("fw-link"), onClick: () => setIsOpen(true), children: labels.follow });
1189
1235
  }
1236
+ const isEmailValid = EMAIL_PATTERN.test(email.trim());
1190
1237
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1191
1238
  "form",
1192
1239
  {
@@ -1210,7 +1257,8 @@ function FollowForm({ feature }) {
1210
1257
  required: true
1211
1258
  }
1212
1259
  ),
1213
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending, children: labels.follow }),
1260
+ email !== "" && !isEmailValid && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cx("fw-error"), children: labels.fieldEmailInvalid }),
1261
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending || !isEmailValid, children: labels.follow }),
1214
1262
  error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) })
1215
1263
  ]
1216
1264
  }
@@ -1253,7 +1301,7 @@ function FeatureCard({
1253
1301
  defaultCommentsOpen = false
1254
1302
  }) {
1255
1303
  const { labels, allowComments, cx } = useBoard();
1256
- const { isPending, toggle } = useVote(feature.id);
1304
+ const { isPending, toggle, error: voteError } = useVote(feature.id);
1257
1305
  const [showComments, setShowComments] = (0, import_react12.useState)(defaultCommentsOpen);
1258
1306
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("li", { className: cx("fw-card"), children: [
1259
1307
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VoteButton, { feature, isPending, onToggle: () => void toggle() }),
@@ -1281,6 +1329,7 @@ function FeatureCard({
1281
1329
  ),
1282
1330
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(FollowForm, { feature })
1283
1331
  ] }),
1332
+ voteError && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(voteError) }),
1284
1333
  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 }) })
1285
1334
  ] })
1286
1335
  ] });
@@ -1321,6 +1370,27 @@ function Bar({ width, height }) {
1321
1370
 
1322
1371
  // src/components/FeatureList.tsx
1323
1372
  var import_jsx_runtime9 = require("react/jsx-runtime");
1373
+ function errorCopy(error, labels) {
1374
+ if (isNotConfigured(error)) {
1375
+ return { title: labels.errorNotSetUpTitle, detail: labels.errorNotSetUpDetail };
1376
+ }
1377
+ if (error.code === "network") {
1378
+ return { title: labels.errorOfflineTitle, detail: labels.errorOfflineDetail };
1379
+ }
1380
+ if (isWrongApp(error)) {
1381
+ return {
1382
+ title: labels.errorWrongAppTitle,
1383
+ detail: labels.errorWrongAppDetail || userMessage(error)
1384
+ };
1385
+ }
1386
+ if (error.code === "invalid_key") {
1387
+ return { title: labels.errorInvalidKeyTitle, detail: labels.errorInvalidKeyDetail };
1388
+ }
1389
+ return {
1390
+ title: labels.errorTitle,
1391
+ detail: error.code === "server" ? labels.errorDetail : userMessage(error)
1392
+ };
1393
+ }
1324
1394
  function FeatureList() {
1325
1395
  const { initialFeatureId } = useBoard();
1326
1396
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
@@ -1330,8 +1400,11 @@ function FeatureList() {
1330
1400
  }
1331
1401
  function PinnedFeature({ featureId }) {
1332
1402
  const { cx } = useBoard();
1333
- const { feature, isLoading } = useFeature(featureId);
1403
+ const { feature, isLoading, error } = useFeature(featureId);
1334
1404
  if (isLoading) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FeatureListSkeleton, { rows: 1 });
1405
+ if (!feature && error) {
1406
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: cx("fw-error", "fw-pinned"), role: "alert", children: userMessage(error) });
1407
+ }
1335
1408
  if (!feature) return null;
1336
1409
  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 }) }) });
1337
1410
  }
@@ -1350,17 +1423,11 @@ function Listing({ excludeId }) {
1350
1423
  return showDeclined || feature.status !== "declined";
1351
1424
  });
1352
1425
  if (error && visible.length === 0) {
1426
+ const { title, detail } = errorCopy(error, labels);
1353
1427
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cx("fw-empty"), role: "alert", children: [
1354
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { children: userMessage(error) }),
1355
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1356
- "button",
1357
- {
1358
- type: "button",
1359
- className: cx("fw-btn", "fw-btn--ghost"),
1360
- onClick: () => void refresh(),
1361
- children: labels.retry
1362
- }
1363
- )
1428
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: cx("fw-empty__title"), children: title }),
1429
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: cx("fw-empty__detail"), children: detail }),
1430
+ !isNotConfigured(error) && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
1364
1431
  ] });
1365
1432
  }
1366
1433
  if (isLoading && visible.length === 0) {
@@ -1375,7 +1442,11 @@ function Listing({ excludeId }) {
1375
1442
  }
1376
1443
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
1377
1444
  isRoadmap ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RoadmapColumns, { features: visible }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Rows2, { features: visible }),
1378
- hasMore && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cx("fw-pagination"), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1445
+ hasMore && error && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cx("fw-pagination"), role: "alert", children: [
1446
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: labels.errorLoadMore }),
1447
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void loadMore(), children: labels.retry })
1448
+ ] }),
1449
+ hasMore && !error && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cx("fw-pagination"), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1379
1450
  "button",
1380
1451
  {
1381
1452
  type: "button",
@@ -1456,6 +1527,10 @@ function SubmitForm() {
1456
1527
  const [title, setTitle] = (0, import_react14.useState)("");
1457
1528
  const [description, setDescription] = (0, import_react14.useState)("");
1458
1529
  const [email, setEmail] = (0, import_react14.useState)(userEmail ?? "");
1530
+ const isTitleValid = title.trim().length >= LIMITS.titleMin;
1531
+ const isEmailInvalid = emailField !== "off" && email !== "" && !EMAIL_PATTERN.test(email.trim());
1532
+ const isEmailMissing = emailField === "required" && !email.trim();
1533
+ const canSubmit = isTitleValid && !isEmailInvalid && !isEmailMissing;
1459
1534
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1460
1535
  "form",
1461
1536
  {
@@ -1489,7 +1564,15 @@ function SubmitForm() {
1489
1564
  onChange: (event) => setTitle(event.target.value),
1490
1565
  required: true
1491
1566
  }
1492
- )
1567
+ ),
1568
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field__foot"), children: [
1569
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: cx("fw-error"), children: title !== "" && !isTitleValid ? labels.fieldTitleTooShort : "" }),
1570
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: cx("fw-muted"), children: [
1571
+ title.length,
1572
+ "/",
1573
+ LIMITS.titleMax
1574
+ ] })
1575
+ ] })
1493
1576
  ] }),
1494
1577
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field"), children: [
1495
1578
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { className: cx("fw-label"), htmlFor: "fw-description", children: labels.fieldDescription }),
@@ -1503,7 +1586,15 @@ function SubmitForm() {
1503
1586
  placeholder: labels.fieldDescriptionPlaceholder,
1504
1587
  onChange: (event) => setDescription(event.target.value)
1505
1588
  }
1506
- )
1589
+ ),
1590
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field__foot"), children: [
1591
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", {}),
1592
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: cx("fw-muted"), children: [
1593
+ description.length,
1594
+ "/",
1595
+ LIMITS.descriptionMax
1596
+ ] })
1597
+ ] })
1507
1598
  ] }),
1508
1599
  emailField !== "off" && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field"), children: [
1509
1600
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { className: cx("fw-label"), htmlFor: "fw-email", children: labels.fieldEmail }),
@@ -1520,11 +1611,11 @@ function SubmitForm() {
1520
1611
  required: emailField === "required"
1521
1612
  }
1522
1613
  ),
1523
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { id: "fw-email-hint", className: cx("fw-muted"), children: labels.fieldEmailHint })
1614
+ 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 })
1524
1615
  ] }),
1525
1616
  error && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) }),
1526
1617
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-form__actions"), children: [
1527
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending, children: isPending ? labels.submitting : labels.submit }),
1618
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending || !canSubmit, children: isPending ? labels.submitting : labels.submit }),
1528
1619
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1529
1620
  "button",
1530
1621
  {
@@ -1837,6 +1928,7 @@ function FewchursButton({
1837
1928
  FewchursProvider,
1838
1929
  FollowForm,
1839
1930
  LIMITS,
1931
+ NOT_CONFIGURED_MESSAGE,
1840
1932
  ROADMAP_STATUSES,
1841
1933
  SDK_HEADER,
1842
1934
  SDK_VERSION,