create-feedbax 0.2.0 → 0.2.1

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.
Files changed (99) hide show
  1. package/dist/cli.js +2042 -16
  2. package/dist/index.js +1997 -5
  3. package/package.json +1 -1
  4. package/template/.feedbax-template.sha256 +1 -1
  5. package/template/AGENTS.md +13 -0
  6. package/template/CONNECTED.md +404 -0
  7. package/template/README.md +58 -0
  8. package/template/components.json +1 -1
  9. package/template/eslint.config.mjs +23 -0
  10. package/template/migrations/0001_connected.sql +6 -0
  11. package/template/migrations/0002_participation.sql +8 -0
  12. package/template/migrations/0003_images.sql +3 -0
  13. package/template/migrations/0004_followed_status.sql +9 -0
  14. package/template/package.json +9 -5
  15. package/template/src/authorized-draft-post.tsx +1 -0
  16. package/template/src/changelog-page.tsx +63 -54
  17. package/template/src/cloudflare-workers.d.ts +6 -0
  18. package/template/src/comment-creation.test.tsx +42 -1
  19. package/template/src/comment-thread-page.ts +11 -1
  20. package/template/src/components/command-palette.tsx +59 -30
  21. package/template/src/components/connected-identity-provider.tsx +108 -0
  22. package/template/src/components/device-profile-provider.tsx +41 -26
  23. package/template/src/components/follow-post-button.tsx +89 -0
  24. package/template/src/components/header.tsx +2 -0
  25. package/template/src/components/image-attachments.tsx +187 -0
  26. package/template/src/components/loader.tsx +48 -5
  27. package/template/src/components/notification-inbox.tsx +232 -0
  28. package/template/src/components/portal-footer.tsx +14 -0
  29. package/template/src/components/profile-menu.tsx +30 -0
  30. package/template/src/components/ui/components/command.tsx +30 -29
  31. package/template/src/components/ui/components/message-scroller.tsx +2 -2
  32. package/template/src/components/ui/components/scroll-area.tsx +0 -1
  33. package/template/src/components/ui/components/switch.tsx +29 -0
  34. package/template/src/components/ui/styles/globals.css +1 -1
  35. package/template/src/connected-identity.test.ts +144 -0
  36. package/template/src/connected-identity.ts +122 -0
  37. package/template/src/connected-participation.test.tsx +67 -0
  38. package/template/src/connected-runtime.test.ts +241 -0
  39. package/template/src/connected-runtime.ts +176 -0
  40. package/template/src/connected-social.test.tsx +158 -0
  41. package/template/src/feedback-cache-invalidation.ts +1 -0
  42. package/template/src/feedback-runtime.ts +79 -12
  43. package/template/src/feedbax/config/index.test.ts +28 -0
  44. package/template/src/feedbax/config/index.ts +93 -0
  45. package/template/src/feedbax/feedback/connected-feedback.ts +206 -0
  46. package/template/src/feedbax/feedback/connected.test.ts +272 -0
  47. package/template/src/feedbax/feedback/connected.ts +215 -0
  48. package/template/src/feedbax/feedback/email-delivery.test.ts +65 -0
  49. package/template/src/feedbax/feedback/email-delivery.ts +66 -0
  50. package/template/src/feedbax/feedback/identity-handoff.test.ts +81 -0
  51. package/template/src/feedbax/feedback/identity-handoff.ts +119 -0
  52. package/template/src/feedbax/feedback/image-feedback.ts +101 -0
  53. package/template/src/feedbax/feedback/images.test.ts +223 -0
  54. package/template/src/feedbax/feedback/images.ts +324 -0
  55. package/template/src/feedbax/feedback/index.ts +55 -1
  56. package/template/src/feedbax/feedback/notion-image-review.ts +55 -0
  57. package/template/src/feedbax/feedback/participation.test.ts +653 -0
  58. package/template/src/feedbax/feedback/participation.ts +502 -0
  59. package/template/src/images-runtime.test.ts +133 -0
  60. package/template/src/images-runtime.ts +125 -0
  61. package/template/src/index.css +97 -440
  62. package/template/src/lazy-post-detail-overlay.tsx +13 -2
  63. package/template/src/overlay-post-detail.test.tsx +59 -8
  64. package/template/src/overlay-post-detail.tsx +20 -69
  65. package/template/src/participation-client.ts +63 -0
  66. package/template/src/participation-runtime.test.ts +35 -0
  67. package/template/src/participation-runtime.ts +255 -0
  68. package/template/src/portal-comments.ts +24 -5
  69. package/template/src/portal-feedback-form.tsx +18 -3
  70. package/template/src/portal-feedback-runtime.ts +3 -0
  71. package/template/src/portal-feedback-server-function.ts +6 -3
  72. package/template/src/portal-feedback-submission.ts +10 -0
  73. package/template/src/portal-server-entry.ts +4 -0
  74. package/template/src/post-creation-flow.tsx +2 -2
  75. package/template/src/post-detail-content.test.tsx +233 -0
  76. package/template/src/post-detail-content.tsx +162 -0
  77. package/template/src/post-detail-query.ts +34 -0
  78. package/template/src/post-results.tsx +1 -1
  79. package/template/src/public-cache-policy.test.ts +11 -4
  80. package/template/src/public-cache-policy.ts +1 -1
  81. package/template/src/public-feedback-index.tsx +9 -39
  82. package/template/src/public-feedback-page.test.tsx +65 -0
  83. package/template/src/public-feedback-page.ts +12 -1
  84. package/template/src/public-post-detail-skeleton.tsx +54 -0
  85. package/template/src/public-post-detail.tsx +298 -208
  86. package/template/src/public-post-route.test.tsx +39 -3
  87. package/template/src/public-post-route.tsx +15 -28
  88. package/template/src/public-roadmap-route.test.tsx +38 -2
  89. package/template/src/public-roadmap-view.tsx +56 -23
  90. package/template/src/public-shell.test.tsx +26 -0
  91. package/template/src/routeTree.gen.ts +49 -3
  92. package/template/src/router.tsx +2 -0
  93. package/template/src/routes/__root.tsx +3 -5
  94. package/template/src/routes/email-preferences.tsx +77 -0
  95. package/template/src/routes/submit.tsx +1 -1
  96. package/template/src/safe-public-failure.ts +3 -1
  97. package/template/src/server.ts +41 -1
  98. package/template/src/trusted-feedback-handler.test.ts +1 -1
  99. package/template/src/vote-toggle.tsx +42 -9
@@ -1,3 +1,9 @@
1
+ import { handleImageRequest } from "./images-runtime";
2
+ import {
3
+ handleParticipationRequest,
4
+ synchronizeConnectedUpdates,
5
+ } from "./participation-runtime";
6
+ import { handleConnectedRequest } from "./connected-runtime";
1
7
  import { configuredTurnstileSecret } from "./turnstile-configuration";
2
8
  import { createPortalServerEntry } from "./portal-server-entry";
3
9
  import { createConfiguredTrustedFeedbackHandler } from "./trusted-feedback-runtime";
@@ -13,7 +19,11 @@ import {
13
19
  createPortalCommentRequestHandler,
14
20
  } from "./portal-comments";
15
21
 
16
- export default createPortalServerEntry({
22
+ const application = createPortalServerEntry({
23
+ connectedHandler: async (request) =>
24
+ (await handleImageRequest(request)) ??
25
+ (await handleParticipationRequest(request)) ??
26
+ handleConnectedRequest(request),
17
27
  commentHandler: (request) => {
18
28
  const turnstileSecret = configuredTurnstileSecret(env.TURNSTILE_SECRET_KEY);
19
29
  return createPortalCommentRequestHandler({
@@ -109,3 +119,33 @@ export default createPortalServerEntry({
109
119
  }
110
120
  },
111
121
  });
122
+
123
+ export default {
124
+ ...application,
125
+ async scheduled() {
126
+ if (!feedbax.connected) return;
127
+ if (feedbax.connected.processing === "standard") {
128
+ await synchronizeConnectedUpdates();
129
+ return;
130
+ }
131
+ if (!env.FEEDBAX_UPDATES)
132
+ throw new Error(
133
+ "Bind the Connected processing queue as FEEDBAX_UPDATES.",
134
+ );
135
+ await env.FEEDBAX_UPDATES.send("feedbax-connected-tick-v1");
136
+ },
137
+ async queue(
138
+ batch: import("@cloudflare/workers-types").MessageBatch<unknown>,
139
+ ) {
140
+ if (!feedbax.connected || feedbax.connected.processing === "standard")
141
+ throw new Error("Queue delivery requires small Connected processing.");
142
+ for (const message of batch.messages) {
143
+ if (message.body !== "feedbax-connected-tick-v1") {
144
+ message.ack();
145
+ continue;
146
+ }
147
+ await synchronizeConnectedUpdates();
148
+ message.ack();
149
+ }
150
+ },
151
+ };
@@ -43,7 +43,7 @@ describe("trusted feedback Worker handler", () => {
43
43
 
44
44
  expect(response.status).toBe(308);
45
45
  expect(response.headers.get("location")).toBe(
46
- "https://feedback.example.com/?types=%5B%22Bug+Report%22%5D&statuses=%5B%22New%22%5D&pageSize=25&sort=top&schema=1",
46
+ "https://feedback.example.com/?types=%5B%22Bug+Report%22%5D&statuses=%5B%22New%22%5D&pageSize=25&sort=top&schema=2",
47
47
  );
48
48
  expect(applicationHandler).not.toHaveBeenCalled();
49
49
  });
@@ -1,3 +1,5 @@
1
+ import { publicPostQueryKey } from "./post-detail-query";
2
+ import { useConnectedIdentity } from "./components/connected-identity-provider";
1
3
  import { configuredTurnstileSiteKey } from "./turnstile-configuration";
2
4
  import type { PublicPost } from "@feedbax/feedback";
3
5
  import { ChevronUp } from "lucide-react";
@@ -27,10 +29,12 @@ export function VoteToggle({
27
29
  turnstileSiteKey?: string;
28
30
  requestTimeoutMs?: number;
29
31
  }) {
32
+ const identity = useConnectedIdentity();
30
33
  const queryClient = useContext(QueryClientContext);
31
34
  const [voted, setVoted] = useState(false);
32
35
  const [count, setCount] = useState(post.voteCount ?? 0);
33
36
  const [pending, setPending] = useState(false);
37
+ const [voteStateReady, setVoteStateReady] = useState(!identity.enabled);
34
38
  const [error, setError] = useState<string>();
35
39
  const [participationPass, setParticipationPass] = useState<string>();
36
40
  const form = useRef<HTMLFormElement>(null);
@@ -38,12 +42,34 @@ export function VoteToggle({
38
42
  const turnstileWidget = useRef<string | undefined>(undefined);
39
43
 
40
44
  useEffect(() => {
41
- setVoted(readVotedPostSlugs(localStorage).has(post.slug));
45
+ if (!identity.enabled)
46
+ setVoted(readVotedPostSlugs(localStorage).has(post.slug));
42
47
  setCount(post.voteCount ?? 0);
43
48
  setParticipationPass(
44
49
  sessionStorage.getItem("feedbax:participation-pass") ?? undefined,
45
50
  );
46
- }, [post.slug, post.voteCount]);
51
+ }, [post.slug, post.voteCount, identity.enabled]);
52
+ useEffect(() => {
53
+ if (!identity.enabled || !identity.displayName) return;
54
+ const controller = new AbortController();
55
+ setVoteStateReady(false);
56
+ fetch(`/internal/connected/votes?slug=${encodeURIComponent(post.slug)}`, {
57
+ signal: controller.signal,
58
+ cache: "no-store",
59
+ })
60
+ .then(async (response) => {
61
+ if (!response.ok)
62
+ throw new Error("Could not load your Vote. Refresh and try again.");
63
+ const state = await response.json();
64
+ setVoteStateReady(true);
65
+ setVoted(state.voted);
66
+ setCount(state.voteCount);
67
+ })
68
+ .catch((cause) => {
69
+ if (!controller.signal.aborted) setError(cause.message);
70
+ });
71
+ return () => controller.abort();
72
+ }, [identity.enabled, identity.displayName, post.slug]);
47
73
  useEffect(() => {
48
74
  const reconcile = (event: Event) => {
49
75
  const detail = (
@@ -95,6 +121,10 @@ export function VoteToggle({
95
121
  }, [participationPass, turnstileSiteKey]);
96
122
 
97
123
  async function toggle() {
124
+ if (identity.enabled && !identity.displayName) {
125
+ window.location.href = "/internal/identity/start";
126
+ return;
127
+ }
98
128
  const previous = { voted, count };
99
129
  const nextVoted = !voted;
100
130
  setVoted(nextVoted);
@@ -138,7 +168,8 @@ export function VoteToggle({
138
168
  throw new Error(result.error ?? "Vote failed. Try again.");
139
169
  }
140
170
  setCount(result.voteCount);
141
- persistVotedPost(localStorage, post.slug, nextVoted);
171
+ if (!identity.enabled)
172
+ persistVotedPost(localStorage, post.slug, nextVoted);
142
173
  window.dispatchEvent(
143
174
  new CustomEvent(voteConfirmedEvent, {
144
175
  detail: {
@@ -151,6 +182,7 @@ export function VoteToggle({
151
182
  if (queryClient) {
152
183
  void Promise.all([
153
184
  queryClient.invalidateQueries({ queryKey: publicPostsQueryKey }),
185
+ queryClient.invalidateQueries({ queryKey: publicPostQueryKey }),
154
186
  queryClient.invalidateQueries({ queryKey: publicRoadmapQueryKey }),
155
187
  ]);
156
188
  }
@@ -177,17 +209,18 @@ export function VoteToggle({
177
209
  }
178
210
 
179
211
  return (
180
- <form
181
- ref={form}
182
- className="vote-toggle"
183
- onSubmit={(event) => event.preventDefault()}
184
- >
212
+ <form ref={form} onSubmit={(event) => event.preventDefault()}>
185
213
  <button
186
214
  className="vote-toggle-button"
187
215
  type="button"
188
216
  aria-pressed={voted}
189
217
  aria-label={`${voted ? "Remove Vote" : "Add Vote"}, ${count} Votes`}
190
- disabled={pending}
218
+ disabled={
219
+ pending ||
220
+ (identity.enabled &&
221
+ (!identity.ready ||
222
+ (Boolean(identity.displayName) && !voteStateReady)))
223
+ }
191
224
  onClick={toggle}
192
225
  >
193
226
  <ChevronUp aria-hidden="true" /> {count}