create-feedbax 0.1.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 (251) hide show
  1. package/LICENSE +201 -21
  2. package/README.md +11 -1
  3. package/dist/cli.js +3283 -22
  4. package/dist/index.js +3394 -160
  5. package/package.json +40 -14
  6. package/template/.feedbax-template.sha256 +1 -0
  7. package/template/.gitignore.template +6 -0
  8. package/template/.npmignore.template +5 -0
  9. package/template/.prettierignore.template +4 -0
  10. package/template/AGENTS.md +13 -0
  11. package/template/CONNECTED.md +404 -0
  12. package/template/LICENSE +201 -0
  13. package/template/README.md +56 -5
  14. package/template/components.json +25 -0
  15. package/template/eslint.config.mjs +48 -0
  16. package/template/migrations/0001_connected.sql +6 -0
  17. package/template/migrations/0002_participation.sql +8 -0
  18. package/template/migrations/0003_images.sql +3 -0
  19. package/template/migrations/0004_followed_status.sql +9 -0
  20. package/template/package.json +74 -14
  21. package/template/public/feedbax-mark.svg +4 -0
  22. package/template/public/robots.txt +3 -0
  23. package/template/src/authorized-draft-post.test.tsx +212 -0
  24. package/template/src/authorized-draft-post.tsx +205 -0
  25. package/template/src/authorized-draft-query.ts +74 -0
  26. package/template/src/browser-post-state.test.ts +120 -0
  27. package/template/src/browser-post-state.ts +134 -0
  28. package/template/src/browser-vote-state.test.ts +14 -0
  29. package/template/src/browser-vote-state.ts +27 -0
  30. package/template/src/capability-authorization-error.ts +3 -0
  31. package/template/src/changelog-page.test.tsx +139 -0
  32. package/template/src/changelog-page.tsx +228 -0
  33. package/template/src/changelog-query.ts +13 -0
  34. package/template/src/changelog-runtime.ts +27 -0
  35. package/template/src/cloudflare-turnstile.ts +31 -0
  36. package/template/src/cloudflare-workers-test.ts +3 -0
  37. package/template/src/cloudflare-workers.d.ts +24 -0
  38. package/template/src/comment-creation.test.tsx +311 -0
  39. package/template/src/comment-profile.ts +5 -0
  40. package/template/src/comment-thread-page.ts +28 -0
  41. package/template/src/components/command-palette-context.tsx +71 -0
  42. package/template/src/components/command-palette.tsx +275 -0
  43. package/template/src/components/connected-identity-provider.tsx +108 -0
  44. package/template/src/components/create-post-trigger.tsx +44 -0
  45. package/template/src/components/device-profile-provider.tsx +324 -0
  46. package/template/src/components/follow-post-button.tsx +89 -0
  47. package/template/src/components/header.tsx +97 -0
  48. package/template/src/components/image-attachments.tsx +187 -0
  49. package/template/src/components/loader.tsx +53 -0
  50. package/template/src/components/notification-inbox.tsx +232 -0
  51. package/template/src/components/portal-footer.tsx +14 -0
  52. package/template/src/components/profile-menu.tsx +119 -0
  53. package/template/src/components/ui/components/alert-dialog.tsx +185 -0
  54. package/template/src/components/ui/components/attachment.tsx +207 -0
  55. package/template/src/components/ui/components/avatar.tsx +107 -0
  56. package/template/src/components/ui/components/badge.tsx +52 -0
  57. package/template/src/components/ui/components/bubble.tsx +128 -0
  58. package/template/src/components/ui/components/button-group.tsx +87 -0
  59. package/template/src/components/ui/components/button.tsx +56 -0
  60. package/template/src/components/ui/components/card.tsx +103 -0
  61. package/template/src/components/ui/components/checkbox.tsx +26 -0
  62. package/template/src/components/ui/components/command.tsx +195 -0
  63. package/template/src/components/ui/components/dialog.tsx +154 -0
  64. package/template/src/components/ui/components/drawer.tsx +229 -0
  65. package/template/src/components/ui/components/dropdown-menu.tsx +270 -0
  66. package/template/src/components/ui/components/empty.tsx +101 -0
  67. package/template/src/components/ui/components/field.tsx +236 -0
  68. package/template/src/components/ui/components/input-group.tsx +155 -0
  69. package/template/src/components/ui/components/input.tsx +20 -0
  70. package/template/src/components/ui/components/item.tsx +201 -0
  71. package/template/src/components/ui/components/kbd.tsx +26 -0
  72. package/template/src/components/ui/components/label.tsx +18 -0
  73. package/template/src/components/ui/components/marker.tsx +71 -0
  74. package/template/src/components/ui/components/message-scroller.tsx +128 -0
  75. package/template/src/components/ui/components/message.tsx +92 -0
  76. package/template/src/components/ui/components/native-select.tsx +65 -0
  77. package/template/src/components/ui/components/popover.tsx +88 -0
  78. package/template/src/components/ui/components/portal-primitives.test.tsx +85 -0
  79. package/template/src/components/ui/components/radio-group.tsx +36 -0
  80. package/template/src/components/ui/components/scroll-area.tsx +51 -0
  81. package/template/src/components/ui/components/secondary-form-actions.test.tsx +23 -0
  82. package/template/src/components/ui/components/select.tsx +199 -0
  83. package/template/src/components/ui/components/separator.tsx +23 -0
  84. package/template/src/components/ui/components/sheet.tsx +135 -0
  85. package/template/src/components/ui/components/skeleton.tsx +13 -0
  86. package/template/src/components/ui/components/sonner.tsx +43 -0
  87. package/template/src/components/ui/components/spinner.tsx +16 -0
  88. package/template/src/components/ui/components/switch.tsx +29 -0
  89. package/template/src/components/ui/components/tabs.tsx +80 -0
  90. package/template/src/components/ui/components/textarea.tsx +18 -0
  91. package/template/src/components/ui/components/tooltip.tsx +64 -0
  92. package/template/src/components/ui/hooks/.gitkeep +0 -0
  93. package/template/src/components/ui/lib/utils.ts +6 -0
  94. package/template/src/components/ui/styles/globals.css +151 -0
  95. package/template/src/connected-identity.test.ts +144 -0
  96. package/template/src/connected-identity.ts +122 -0
  97. package/template/src/connected-participation.test.tsx +67 -0
  98. package/template/src/connected-runtime.test.ts +241 -0
  99. package/template/src/connected-runtime.ts +176 -0
  100. package/template/src/connected-social.test.tsx +158 -0
  101. package/template/src/create-post-overlay.tsx +82 -0
  102. package/template/src/draft-post-list.tsx +39 -0
  103. package/template/src/feedback-cache-invalidation.test.ts +185 -0
  104. package/template/src/feedback-cache-invalidation.ts +98 -0
  105. package/template/src/feedback-runtime.ts +105 -0
  106. package/template/src/feedbax/changelog/index.test.ts +156 -0
  107. package/template/src/feedbax/changelog/index.ts +206 -0
  108. package/template/src/feedbax/changelog/notion-changelog.test.ts +395 -0
  109. package/template/src/feedbax/changelog/notion-changelog.ts +400 -0
  110. package/template/src/feedbax/config/index.test.ts +139 -0
  111. package/template/src/feedbax/config/index.ts +364 -0
  112. package/template/src/feedbax/feedback/connected-feedback.ts +206 -0
  113. package/template/src/feedbax/feedback/connected.test.ts +272 -0
  114. package/template/src/feedbax/feedback/connected.ts +215 -0
  115. package/template/src/feedbax/feedback/email-delivery.test.ts +65 -0
  116. package/template/src/feedbax/feedback/email-delivery.ts +66 -0
  117. package/template/src/feedbax/feedback/feedback-storage.ts +43 -0
  118. package/template/src/feedbax/feedback/identity-handoff.test.ts +81 -0
  119. package/template/src/feedbax/feedback/identity-handoff.ts +119 -0
  120. package/template/src/feedbax/feedback/image-feedback.ts +101 -0
  121. package/template/src/feedbax/feedback/images.test.ts +223 -0
  122. package/template/src/feedbax/feedback/images.ts +324 -0
  123. package/template/src/feedbax/feedback/index.test.ts +900 -0
  124. package/template/src/feedbax/feedback/index.ts +996 -0
  125. package/template/src/feedbax/feedback/notion-comments.test.ts +171 -0
  126. package/template/src/feedbax/feedback/notion-comments.ts +231 -0
  127. package/template/src/feedbax/feedback/notion-data-source.test.ts +364 -0
  128. package/template/src/feedbax/feedback/notion-data-source.ts +458 -0
  129. package/template/src/feedbax/feedback/notion-feedback.test.ts +998 -0
  130. package/template/src/feedbax/feedback/notion-feedback.ts +651 -0
  131. package/template/src/feedbax/feedback/notion-image-review.ts +55 -0
  132. package/template/src/feedbax/feedback/notion-request.ts +74 -0
  133. package/template/src/feedbax/feedback/participation.test.ts +653 -0
  134. package/template/src/feedbax/feedback/participation.ts +502 -0
  135. package/template/src/feedbax.test-fixture.ts +64 -0
  136. package/template/src/feedbax.ts +69 -0
  137. package/template/src/images-runtime.test.ts +133 -0
  138. package/template/src/images-runtime.ts +125 -0
  139. package/template/src/index.css +1795 -0
  140. package/template/src/lazy-create-post-overlay.tsx +6 -0
  141. package/template/src/lazy-post-detail-overlay.tsx +21 -0
  142. package/template/src/masked-post-link.tsx +27 -0
  143. package/template/src/overlay-post-detail.test.tsx +181 -0
  144. package/template/src/overlay-post-detail.tsx +43 -0
  145. package/template/src/participation-client.ts +63 -0
  146. package/template/src/participation-runtime.test.ts +35 -0
  147. package/template/src/participation-runtime.ts +255 -0
  148. package/template/src/portal-comments.test.ts +375 -0
  149. package/template/src/portal-comments.ts +403 -0
  150. package/template/src/portal-features.test.tsx +150 -0
  151. package/template/src/portal-feedback-form.tsx +530 -0
  152. package/template/src/portal-feedback-runtime.test.ts +32 -0
  153. package/template/src/portal-feedback-runtime.ts +58 -0
  154. package/template/src/portal-feedback-server-function.ts +96 -0
  155. package/template/src/portal-feedback-submission.test.ts +307 -0
  156. package/template/src/portal-feedback-submission.ts +224 -0
  157. package/template/src/portal-head.ts +58 -0
  158. package/template/src/portal-server-entry.ts +68 -0
  159. package/template/src/portal-voting.test.ts +106 -0
  160. package/template/src/portal-voting.ts +149 -0
  161. package/template/src/post-badges.tsx +27 -0
  162. package/template/src/post-creation-flow.test.tsx +230 -0
  163. package/template/src/post-creation-flow.tsx +88 -0
  164. package/template/src/post-detail-content.test.tsx +233 -0
  165. package/template/src/post-detail-content.tsx +162 -0
  166. package/template/src/post-detail-overlay.test.tsx +114 -0
  167. package/template/src/post-detail-overlay.tsx +67 -0
  168. package/template/src/post-detail-query.ts +34 -0
  169. package/template/src/post-feed-controls.test.tsx +239 -0
  170. package/template/src/post-feed-controls.tsx +298 -0
  171. package/template/src/post-presentation.ts +37 -0
  172. package/template/src/post-queries.test.ts +158 -0
  173. package/template/src/post-queries.ts +26 -0
  174. package/template/src/post-results.tsx +165 -0
  175. package/template/src/public-cache-policy.test.ts +148 -0
  176. package/template/src/public-cache-policy.ts +119 -0
  177. package/template/src/public-cache-tags.ts +7 -0
  178. package/template/src/public-changelog-page.ts +15 -0
  179. package/template/src/public-changelog-server-function.ts +27 -0
  180. package/template/src/public-date.ts +11 -0
  181. package/template/src/public-failure-state.test.tsx +69 -0
  182. package/template/src/public-feedback-index.tsx +94 -0
  183. package/template/src/public-feedback-page.test.tsx +355 -0
  184. package/template/src/public-feedback-page.ts +86 -0
  185. package/template/src/public-feedback-server-function.ts +22 -0
  186. package/template/src/public-portal-error.tsx +27 -0
  187. package/template/src/public-post-detail-skeleton.tsx +54 -0
  188. package/template/src/public-post-detail.tsx +772 -0
  189. package/template/src/public-post-page.test.tsx +173 -0
  190. package/template/src/public-post-page.ts +15 -0
  191. package/template/src/public-post-route.test.tsx +250 -0
  192. package/template/src/public-post-route.tsx +158 -0
  193. package/template/src/public-post-server-function.ts +48 -0
  194. package/template/src/public-post-unavailable.tsx +10 -0
  195. package/template/src/public-roadmap-page.test.tsx +139 -0
  196. package/template/src/public-roadmap-page.ts +24 -0
  197. package/template/src/public-roadmap-route.test.tsx +254 -0
  198. package/template/src/public-roadmap-route.tsx +50 -0
  199. package/template/src/public-roadmap-server-function.ts +44 -0
  200. package/template/src/public-roadmap-view.tsx +411 -0
  201. package/template/src/public-shell.test.tsx +544 -0
  202. package/template/src/roadmap-query.ts +21 -0
  203. package/template/src/root-branding.test.ts +53 -0
  204. package/template/src/routeTree.gen.ts +177 -0
  205. package/template/src/routed-post-creation-form.tsx +28 -0
  206. package/template/src/router.tsx +27 -0
  207. package/template/src/routes/__root.tsx +114 -0
  208. package/template/src/routes/changelog.tsx +39 -0
  209. package/template/src/routes/email-preferences.tsx +77 -0
  210. package/template/src/routes/index.tsx +61 -0
  211. package/template/src/routes/p.$slug.tsx +16 -0
  212. package/template/src/routes/roadmap.tsx +11 -0
  213. package/template/src/routes/submit.tsx +28 -0
  214. package/template/src/safe-public-failure.ts +47 -0
  215. package/template/src/server.ts +151 -0
  216. package/template/src/similar-posts.tsx +45 -0
  217. package/template/src/submit-route.test.tsx +134 -0
  218. package/template/src/trusted-feedback-handler.test.ts +425 -0
  219. package/template/src/trusted-feedback-handler.ts +181 -0
  220. package/template/src/trusted-feedback-runtime.ts +17 -0
  221. package/template/src/turnstile-configuration.test.ts +37 -0
  222. package/template/src/turnstile-configuration.ts +19 -0
  223. package/template/src/use-media-query.ts +17 -0
  224. package/template/src/vote-toggle.test.tsx +171 -0
  225. package/template/src/vote-toggle.tsx +244 -0
  226. package/template/tsconfig.json +45 -0
  227. package/template/vite.config.ts +20 -0
  228. package/template/vitest.config.ts +4 -0
  229. package/template/vitest.setup.ts +3 -0
  230. package/template/wrangler.jsonc +35 -0
  231. package/dist/cli.d.ts +0 -3
  232. package/dist/cli.d.ts.map +0 -1
  233. package/dist/cli.js.map +0 -1
  234. package/dist/index.d.ts +0 -21
  235. package/dist/index.d.ts.map +0 -1
  236. package/dist/index.js.map +0 -1
  237. package/template/.env.example +0 -4
  238. package/template/apps/portal/components.json +0 -20
  239. package/template/apps/portal/package.json +0 -31
  240. package/template/apps/portal/src/lib/utils.ts +0 -6
  241. package/template/apps/portal/src/routeTree.gen.ts +0 -59
  242. package/template/apps/portal/src/router.tsx +0 -10
  243. package/template/apps/portal/src/routes/__root.tsx +0 -31
  244. package/template/apps/portal/src/routes/index.tsx +0 -35
  245. package/template/apps/portal/src/styles.css +0 -91
  246. package/template/apps/portal/tsconfig.json +0 -15
  247. package/template/apps/portal/vite.config.ts +0 -13
  248. package/template/feedbax.config.ts +0 -13
  249. package/template/feedbax.jsonc +0 -11
  250. package/template/pnpm-workspace.yaml +0 -3
  251. package/template/turbo.json +0 -8
@@ -0,0 +1,171 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import {
4
+ act,
5
+ cleanup,
6
+ fireEvent,
7
+ render,
8
+ screen,
9
+ waitFor,
10
+ } from "@testing-library/react";
11
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
12
+ import {
13
+ QueryClient,
14
+ QueryClientProvider,
15
+ useQuery,
16
+ } from "@tanstack/react-query";
17
+ import { VoteToggle } from "./vote-toggle";
18
+
19
+ const post = {
20
+ slug: "keyboard-navigation",
21
+ title: "Keyboard navigation",
22
+ description: "Navigate without a mouse.",
23
+ type: "Feature Request" as const,
24
+ status: "Planned" as const,
25
+ voteCount: 2,
26
+ createdAt: new Date("2026-08-25T00:00:00.000Z"),
27
+ updatedAt: new Date("2026-08-25T00:00:00.000Z"),
28
+ };
29
+
30
+ beforeEach(() => {
31
+ localStorage.clear();
32
+ sessionStorage.clear();
33
+ });
34
+
35
+ afterEach(() => {
36
+ cleanup();
37
+ vi.restoreAllMocks();
38
+ vi.useRealTimers();
39
+ delete window.turnstile;
40
+ sessionStorage.clear();
41
+ });
42
+
43
+ describe("Vote toggle", () => {
44
+ it("presents a caret in an interactive Vote control", () => {
45
+ renderVoteToggle();
46
+
47
+ const button = screen.getByRole("button", { name: "Add Vote, 2 Votes" });
48
+ expect(button.classList.contains("vote-toggle-button")).toBe(true);
49
+ expect(button.querySelector(".lucide-chevron-up")).not.toBeNull();
50
+ expect(button.querySelector(".lucide-arrow-up")).toBeNull();
51
+ });
52
+
53
+ it("reconciles every affected browser projection only after confirmation", async () => {
54
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(
55
+ Response.json({ voteCount: 3 }),
56
+ );
57
+ const queryClient = new QueryClient();
58
+ const fetchPosts = vi.fn().mockResolvedValue({ items: [post] });
59
+ const fetchRoadmap = vi
60
+ .fn()
61
+ .mockResolvedValue({ Planned: { items: [post] } });
62
+ function VisibleProjections() {
63
+ useQuery({
64
+ queryKey: ["public-posts", { sort: "top" }],
65
+ queryFn: fetchPosts,
66
+ staleTime: Infinity,
67
+ });
68
+ useQuery({
69
+ queryKey: ["public-post-roadmap"],
70
+ queryFn: fetchRoadmap,
71
+ staleTime: Infinity,
72
+ });
73
+ return null;
74
+ }
75
+ render(
76
+ <QueryClientProvider client={queryClient}>
77
+ <VisibleProjections />
78
+ <VoteToggle post={post} />
79
+ </QueryClientProvider>,
80
+ );
81
+ await waitFor(() => {
82
+ expect(fetchPosts).toHaveBeenCalledOnce();
83
+ expect(fetchRoadmap).toHaveBeenCalledOnce();
84
+ });
85
+
86
+ fireEvent.click(screen.getByRole("button", { name: "Add Vote, 2 Votes" }));
87
+
88
+ await waitFor(() =>
89
+ expect(localStorage.getItem("feedbax:voted-posts")).toContain(post.slug),
90
+ );
91
+ await waitFor(() => {
92
+ expect(fetchPosts).toHaveBeenCalledTimes(2);
93
+ expect(fetchRoadmap).toHaveBeenCalledTimes(2);
94
+ });
95
+ });
96
+
97
+ it("keeps mounted feed and Post detail toggles consistent after confirmation", async () => {
98
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(
99
+ Response.json({ voteCount: 3 }),
100
+ );
101
+ render(
102
+ <QueryClientProvider client={new QueryClient()}>
103
+ <VoteToggle post={post} />
104
+ <VoteToggle post={post} />
105
+ </QueryClientProvider>,
106
+ );
107
+
108
+ fireEvent.click(
109
+ screen.getAllByRole("button", { name: "Add Vote, 2 Votes" })[0]!,
110
+ );
111
+
112
+ await waitFor(() =>
113
+ expect(
114
+ screen.getAllByRole("button", { name: "Remove Vote, 3 Votes" }),
115
+ ).toHaveLength(2),
116
+ );
117
+ });
118
+
119
+ it("renders and resets an explicit Turnstile challenge after verification failure", async () => {
120
+ const reset = vi.fn();
121
+ window.turnstile = { render: vi.fn().mockReturnValue("widget-1"), reset };
122
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(
123
+ Response.json(
124
+ { error: "Vote verification failed.", code: "verification_failed" },
125
+ { status: 400 },
126
+ ),
127
+ );
128
+ renderVoteToggle({ turnstileSiteKey: "public-site-key" });
129
+ await waitFor(() => expect(window.turnstile?.render).toHaveBeenCalled());
130
+
131
+ fireEvent.click(screen.getByRole("button", { name: "Add Vote, 2 Votes" }));
132
+
133
+ await waitFor(() => expect(reset).toHaveBeenCalledWith("widget-1"));
134
+ expect(screen.getByRole("alert").textContent).toContain(
135
+ "verification failed",
136
+ );
137
+ });
138
+
139
+ it("rolls back and gives retry guidance when a Vote times out", async () => {
140
+ vi.useFakeTimers();
141
+ vi.spyOn(globalThis, "fetch").mockImplementation(
142
+ (_input, init) =>
143
+ new Promise((_resolve, reject) => {
144
+ init?.signal?.addEventListener("abort", () =>
145
+ reject(new DOMException("Aborted", "AbortError")),
146
+ );
147
+ }),
148
+ );
149
+ renderVoteToggle({ requestTimeoutMs: 25 });
150
+ const button = screen.getByRole("button", { name: "Add Vote, 2 Votes" });
151
+ fireEvent.click(button);
152
+ expect(button.textContent).toContain("3");
153
+
154
+ await act(async () => vi.advanceTimersByTime(25));
155
+
156
+ expect(button.textContent).toContain("2");
157
+ expect(screen.getByRole("alert").textContent).toContain(
158
+ "Check your connection and try again",
159
+ );
160
+ });
161
+ });
162
+
163
+ function renderVoteToggle(
164
+ props: Partial<React.ComponentProps<typeof VoteToggle>> = {},
165
+ ) {
166
+ return render(
167
+ <QueryClientProvider client={new QueryClient()}>
168
+ <VoteToggle post={post} {...props} />
169
+ </QueryClientProvider>,
170
+ );
171
+ }
@@ -0,0 +1,244 @@
1
+ import { publicPostQueryKey } from "./post-detail-query";
2
+ import { useConnectedIdentity } from "./components/connected-identity-provider";
3
+ import { configuredTurnstileSiteKey } from "./turnstile-configuration";
4
+ import type { PublicPost } from "@feedbax/feedback";
5
+ import { ChevronUp } from "lucide-react";
6
+ import { useContext, useEffect, useRef, useState } from "react";
7
+ import { QueryClientContext } from "@tanstack/react-query";
8
+ import { readVotedPostSlugs, persistVotedPost } from "./browser-vote-state";
9
+ import { publicPostsQueryKey } from "./post-queries";
10
+ import { publicRoadmapQueryKey } from "./roadmap-query";
11
+
12
+ const voteConfirmedEvent = "feedbax:vote-confirmed";
13
+
14
+ declare global {
15
+ interface Window {
16
+ turnstile?: {
17
+ render(element: HTMLElement, options: { sitekey: string }): string;
18
+ reset(widgetId: string): void;
19
+ };
20
+ }
21
+ }
22
+
23
+ export function VoteToggle({
24
+ post,
25
+ turnstileSiteKey = configuredTurnstileSiteKey(),
26
+ requestTimeoutMs = 10_000,
27
+ }: {
28
+ post: PublicPost;
29
+ turnstileSiteKey?: string;
30
+ requestTimeoutMs?: number;
31
+ }) {
32
+ const identity = useConnectedIdentity();
33
+ const queryClient = useContext(QueryClientContext);
34
+ const [voted, setVoted] = useState(false);
35
+ const [count, setCount] = useState(post.voteCount ?? 0);
36
+ const [pending, setPending] = useState(false);
37
+ const [voteStateReady, setVoteStateReady] = useState(!identity.enabled);
38
+ const [error, setError] = useState<string>();
39
+ const [participationPass, setParticipationPass] = useState<string>();
40
+ const form = useRef<HTMLFormElement>(null);
41
+ const turnstileContainer = useRef<HTMLDivElement>(null);
42
+ const turnstileWidget = useRef<string | undefined>(undefined);
43
+
44
+ useEffect(() => {
45
+ if (!identity.enabled)
46
+ setVoted(readVotedPostSlugs(localStorage).has(post.slug));
47
+ setCount(post.voteCount ?? 0);
48
+ setParticipationPass(
49
+ sessionStorage.getItem("feedbax:participation-pass") ?? undefined,
50
+ );
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]);
73
+ useEffect(() => {
74
+ const reconcile = (event: Event) => {
75
+ const detail = (
76
+ event as CustomEvent<{
77
+ slug: string;
78
+ voteCount: number;
79
+ voted: boolean;
80
+ }>
81
+ ).detail;
82
+ if (detail.slug !== post.slug) return;
83
+ setCount(detail.voteCount);
84
+ setVoted(detail.voted);
85
+ };
86
+ window.addEventListener(voteConfirmedEvent, reconcile);
87
+ return () => window.removeEventListener(voteConfirmedEvent, reconcile);
88
+ }, [post.slug]);
89
+ useEffect(() => {
90
+ if (!turnstileSiteKey || participationPass || !turnstileContainer.current)
91
+ return;
92
+ const render = () => {
93
+ if (
94
+ !turnstileWidget.current &&
95
+ window.turnstile &&
96
+ turnstileContainer.current
97
+ ) {
98
+ turnstileWidget.current = window.turnstile.render(
99
+ turnstileContainer.current,
100
+ { sitekey: turnstileSiteKey },
101
+ );
102
+ }
103
+ };
104
+ const existing = document.querySelector<HTMLScriptElement>(
105
+ "script[data-turnstile]",
106
+ );
107
+ if (existing) {
108
+ if (window.turnstile) render();
109
+ else existing.addEventListener("load", render, { once: true });
110
+ return () => existing.removeEventListener("load", render);
111
+ }
112
+ const script = document.createElement("script");
113
+ script.src =
114
+ "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
115
+ script.async = true;
116
+ script.defer = true;
117
+ script.dataset.turnstile = "true";
118
+ script.addEventListener("load", render, { once: true });
119
+ document.head.append(script);
120
+ return () => script.removeEventListener("load", render);
121
+ }, [participationPass, turnstileSiteKey]);
122
+
123
+ async function toggle() {
124
+ if (identity.enabled && !identity.displayName) {
125
+ window.location.href = "/internal/identity/start";
126
+ return;
127
+ }
128
+ const previous = { voted, count };
129
+ const nextVoted = !voted;
130
+ setVoted(nextVoted);
131
+ setCount(Math.max(0, count + (nextVoted ? 1 : -1)));
132
+ setPending(true);
133
+ setError(undefined);
134
+ try {
135
+ const turnstileToken = form.current
136
+ ? new FormData(form.current).get("cf-turnstile-response")
137
+ : undefined;
138
+ const controller = new AbortController();
139
+ const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
140
+ const response = await fetch("/internal/votes", {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/json" },
143
+ signal: controller.signal,
144
+ body: JSON.stringify({
145
+ slug: post.slug,
146
+ intention: nextVoted ? "add" : "remove",
147
+ participationPass,
148
+ ...(turnstileToken ? { turnstileToken: String(turnstileToken) } : {}),
149
+ }),
150
+ }).finally(() => clearTimeout(timeout));
151
+ const result = (await response.json()) as {
152
+ voteCount: number;
153
+ participationPass?: string;
154
+ error?: string;
155
+ code?: string;
156
+ };
157
+ if (!response.ok) {
158
+ if (
159
+ result.code === "verification_failed" ||
160
+ result.code === "verification_required"
161
+ ) {
162
+ sessionStorage.removeItem("feedbax:participation-pass");
163
+ setParticipationPass(undefined);
164
+ if (turnstileWidget.current)
165
+ window.turnstile?.reset(turnstileWidget.current);
166
+ turnstileWidget.current = undefined;
167
+ }
168
+ throw new Error(result.error ?? "Vote failed. Try again.");
169
+ }
170
+ setCount(result.voteCount);
171
+ if (!identity.enabled)
172
+ persistVotedPost(localStorage, post.slug, nextVoted);
173
+ window.dispatchEvent(
174
+ new CustomEvent(voteConfirmedEvent, {
175
+ detail: {
176
+ slug: post.slug,
177
+ voteCount: result.voteCount,
178
+ voted: nextVoted,
179
+ },
180
+ }),
181
+ );
182
+ if (queryClient) {
183
+ void Promise.all([
184
+ queryClient.invalidateQueries({ queryKey: publicPostsQueryKey }),
185
+ queryClient.invalidateQueries({ queryKey: publicPostQueryKey }),
186
+ queryClient.invalidateQueries({ queryKey: publicRoadmapQueryKey }),
187
+ ]);
188
+ }
189
+ if (result.participationPass) {
190
+ sessionStorage.setItem(
191
+ "feedbax:participation-pass",
192
+ result.participationPass,
193
+ );
194
+ setParticipationPass(result.participationPass);
195
+ }
196
+ } catch (cause) {
197
+ setVoted(previous.voted);
198
+ setCount(previous.count);
199
+ setError(
200
+ cause instanceof DOMException && cause.name === "AbortError"
201
+ ? "Vote timed out. Check your connection and try again."
202
+ : cause instanceof Error
203
+ ? cause.message
204
+ : "Vote failed. Try again.",
205
+ );
206
+ } finally {
207
+ setPending(false);
208
+ }
209
+ }
210
+
211
+ return (
212
+ <form ref={form} onSubmit={(event) => event.preventDefault()}>
213
+ <button
214
+ className="vote-toggle-button"
215
+ type="button"
216
+ aria-pressed={voted}
217
+ aria-label={`${voted ? "Remove Vote" : "Add Vote"}, ${count} Votes`}
218
+ disabled={
219
+ pending ||
220
+ (identity.enabled &&
221
+ (!identity.ready ||
222
+ (Boolean(identity.displayName) && !voteStateReady)))
223
+ }
224
+ onClick={toggle}
225
+ >
226
+ <ChevronUp aria-hidden="true" /> {count}
227
+ </button>
228
+ {turnstileSiteKey && !participationPass ? (
229
+ <div
230
+ ref={(element) => {
231
+ turnstileContainer.current = element;
232
+ if (element && window.turnstile && !turnstileWidget.current) {
233
+ turnstileWidget.current = window.turnstile.render(element, {
234
+ sitekey: turnstileSiteKey,
235
+ });
236
+ }
237
+ }}
238
+ className="cf-turnstile"
239
+ />
240
+ ) : null}
241
+ {error ? <p role="alert">{error}</p> : null}
242
+ </form>
243
+ );
244
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "include": [
3
+ "src/**/*.ts",
4
+ "src/**/*.tsx"
5
+ ],
6
+ "compilerOptions": {
7
+ "target": "ES2022",
8
+ "jsx": "react-jsx",
9
+ "module": "ESNext",
10
+ "lib": [
11
+ "ES2022",
12
+ "DOM",
13
+ "DOM.Iterable"
14
+ ],
15
+ "types": [
16
+ "vite/client",
17
+ "node"
18
+ ],
19
+ "moduleResolution": "bundler",
20
+ "allowImportingTsExtensions": true,
21
+ "noEmit": true,
22
+ "skipLibCheck": true,
23
+ "strict": true,
24
+ "paths": {
25
+ "@/*": [
26
+ "./src/*"
27
+ ],
28
+ "@feedbax/feedback": [
29
+ "./src/feedbax/feedback/index.ts"
30
+ ],
31
+ "@feedbax/changelog": [
32
+ "./src/feedbax/changelog/index.ts"
33
+ ],
34
+ "@feedbax/config": [
35
+ "./src/feedbax/config/index.ts"
36
+ ],
37
+ "@feedbax/ui/*": [
38
+ "./src/components/ui/*"
39
+ ],
40
+ "@feedbax/ui/globals.css": [
41
+ "./src/components/ui/styles/globals.css"
42
+ ]
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,20 @@
1
+ import { cloudflare } from "@cloudflare/vite-plugin";
2
+ import tailwindcss from "@tailwindcss/vite";
3
+ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
4
+ import viteReact from "@vitejs/plugin-react";
5
+ import { defineConfig } from "vite";
6
+
7
+ export default defineConfig({
8
+ server: {
9
+ port: 3001,
10
+ },
11
+ resolve: {
12
+ tsconfigPaths: true,
13
+ },
14
+ plugins: [
15
+ cloudflare({ viteEnvironment: { name: "ssr" } }),
16
+ tailwindcss(),
17
+ tanstackStart({ server: { entry: "./server.ts" } }),
18
+ viteReact(),
19
+ ],
20
+ });
@@ -0,0 +1,4 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { defineConfig } from "vitest/config";
3
+ const src = fileURLToPath(new URL("./src/", import.meta.url));
4
+ export default defineConfig({ test: { setupFiles: ["./vitest.setup.ts"] }, resolve: { alias: [{ find: "@feedbax/feedback", replacement: `${src}feedbax/feedback/index.ts` }, { find: "@feedbax/changelog", replacement: `${src}feedbax/changelog/index.ts` }, { find: "@feedbax/config", replacement: `${src}feedbax/config/index.ts` }, { find: "cloudflare:workers", replacement: `${src}cloudflare-workers-test.ts` }, { find: /^@feedbax\/ui\/(.*)$/, replacement: `${src}components/ui/$1` }] } });
@@ -0,0 +1,3 @@
1
+ import { vi } from "vitest";
2
+
3
+ vi.mock("./src/feedbax", () => import("./src/feedbax.test-fixture"));
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "node_modules/wrangler/config-schema.json",
3
+ "name": "feedbax-installation",
4
+ "compatibility_date": "2026-08-20",
5
+ "compatibility_flags": [
6
+ "nodejs_compat"
7
+ ],
8
+ "main": "./src/server.ts",
9
+ "workers_dev": true,
10
+ "preview_urls": true,
11
+ "cache": {
12
+ "enabled": true
13
+ },
14
+ "ratelimits": [
15
+ {
16
+ "name": "TRUSTED_SUBMISSIONS_RATE_LIMITER",
17
+ "namespace_id": "1001",
18
+ "simple": {
19
+ "limit": 60,
20
+ "period": 60
21
+ }
22
+ },
23
+ {
24
+ "name": "PORTAL_SUBMISSIONS_RATE_LIMITER",
25
+ "namespace_id": "1002",
26
+ "simple": {
27
+ "limit": 5,
28
+ "period": 60
29
+ }
30
+ }
31
+ ],
32
+ "observability": {
33
+ "enabled": true
34
+ }
35
+ }
package/dist/cli.d.ts DELETED
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
3
- //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,+BAA+B,GAChC,MAAM,YAAY,CAAA;AAEnB,IAAI,CAAC;IACH,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAClC,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAA;IAC/D,MAAM,OAAO,GAAG,WAAW;QACzB,CAAC,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE;YAChB,MAAM,QAAQ,GAAG,eAAe,CAAC;gBAC/B,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,MAAM,EAAE,OAAO,CAAC,MAAM;aACvB,CAAC,CAAA;YACF,IAAI,CAAC;gBACH,OAAO,MAAM,+BAA+B,CAC1C,IAAI,EACJ,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,CAC/B,QAAQ,CAAC,QAAQ,CAAC,GAAG,QAAQ,KAAK,YAAY,KAAK,CAAC,CACvD,CAAA;YACH,CAAC;oBAAS,CAAC;gBACT,QAAQ,CAAC,KAAK,EAAE,CAAA;YAClB,CAAC;QACH,CAAC,CAAC,EAAE;QACN,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAA;IAC9B,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,CAAA;IAC3C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,sBAAsB,MAAM,CAAC,WAAW,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAC/E,CAAA;AACH,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAC9D,CAAA;IACD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;AACtB,CAAC"}
package/dist/index.d.ts DELETED
@@ -1,21 +0,0 @@
1
- type IdentityMode = 'anonymous' | 'email' | 'handoff';
2
- export type Deployment = 'cloudflare' | 'vercel' | 'node';
3
- export type PackageManager = 'npm' | 'pnpm';
4
- export interface CreateOptions {
5
- readonly directory: string;
6
- readonly identity: IdentityMode;
7
- readonly deploy: Deployment;
8
- readonly packageManager: PackageManager;
9
- readonly preset: string;
10
- readonly install: boolean;
11
- readonly git: boolean;
12
- }
13
- export declare function resolveCreateOptions(argv: readonly string[]): CreateOptions;
14
- export type CreatePrompt = (question: string, defaultValue: string) => Promise<string>;
15
- export declare function resolveInteractiveCreateOptions(argv: readonly string[], prompt: CreatePrompt): Promise<CreateOptions>;
16
- export declare function createProject(options: CreateOptions): Promise<{
17
- destination: string;
18
- nextSteps: string[];
19
- }>;
20
- export {};
21
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,KAAK,YAAY,GAAG,WAAW,GAAG,OAAO,GAAG,SAAS,CAAA;AAErD,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,QAAQ,GAAG,MAAM,CAAA;AACzD,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,MAAM,CAAA;AAC3C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;IAC3B,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAA;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;CACtB;AAoBD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,aAAa,CA6C3E;AAED,MAAM,MAAM,YAAY,GAAG,CACzB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,KACjB,OAAO,CAAC,MAAM,CAAC,CAAA;AAEpB,wBAAsB,+BAA+B,CACnD,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC,aAAa,CAAC,CAaxB;AAyCD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa;;;GA8DzD"}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,4 +0,0 @@
1
- # Server-only Notion integration token. Never expose this to browser code.
2
- NOTION_TOKEN=
3
- # At least 32 random bytes used to sign opaque visitor sessions.
4
- FEEDBAX_SESSION_SECRET=
@@ -1,20 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema.json",
3
- "style": "new-york",
4
- "rsc": false,
5
- "tsx": true,
6
- "tailwind": {
7
- "config": "",
8
- "css": "src/styles.css",
9
- "baseColor": "zinc",
10
- "cssVariables": true
11
- },
12
- "iconLibrary": "lucide",
13
- "aliases": {
14
- "components": "@/components",
15
- "utils": "@/lib/utils",
16
- "ui": "@/components/ui",
17
- "lib": "@/lib",
18
- "hooks": "@/hooks"
19
- }
20
- }
@@ -1,31 +0,0 @@
1
- {
2
- "name": "@feedbax/portal",
3
- "private": true,
4
- "type": "module",
5
- "scripts": {
6
- "dev": "vite dev --port 3000",
7
- "build": "vite build",
8
- "type-check": "tsc --noEmit"
9
- },
10
- "dependencies": {
11
- "@feedbax/core": "^0.1.0",
12
- "@feedbax/notion": "^0.1.0",
13
- "@tanstack/react-router": "1.170.18",
14
- "@tanstack/react-start": "1.168.20",
15
- "class-variance-authority": "0.7.1",
16
- "clsx": "2.1.1",
17
- "react": "^19.2.4",
18
- "react-dom": "^19.2.4",
19
- "tailwind-merge": "3.5.0"
20
- },
21
- "devDependencies": {
22
- "@tailwindcss/vite": "4.1.18",
23
- "@tanstack/router-plugin": "1.168.20",
24
- "@types/react": "^19.2.14",
25
- "@types/react-dom": "^19.2.3",
26
- "@vitejs/plugin-react": "5.1.4",
27
- "nitro": "3.0.260610-beta",
28
- "tailwindcss": "4.1.18",
29
- "vite": "7.3.6"
30
- }
31
- }
@@ -1,6 +0,0 @@
1
- import { clsx, type ClassValue } from 'clsx'
2
- import { twMerge } from 'tailwind-merge'
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs))
6
- }