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,177 @@
1
+ /* eslint-disable */
2
+
3
+ // @ts-nocheck
4
+
5
+ // noinspection JSUnusedGlobalSymbols
6
+
7
+ // This file was automatically generated by TanStack Router.
8
+ // You should NOT make any changes in this file as it will be overwritten.
9
+ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10
+
11
+ import { Route as rootRouteImport } from './routes/__root'
12
+ import { Route as IndexRouteImport } from './routes/index'
13
+ import { Route as ChangelogRouteImport } from './routes/changelog'
14
+ import { Route as EmailPreferencesRouteImport } from './routes/email-preferences'
15
+ import { Route as RoadmapRouteImport } from './routes/roadmap'
16
+ import { Route as SubmitRouteImport } from './routes/submit'
17
+ import { Route as PSlugRouteImport } from './routes/p.$slug'
18
+
19
+ const IndexRoute = IndexRouteImport.update({
20
+ id: '/',
21
+ path: '/',
22
+ getParentRoute: () => rootRouteImport,
23
+ } as any)
24
+ const ChangelogRoute = ChangelogRouteImport.update({
25
+ id: '/changelog',
26
+ path: '/changelog',
27
+ getParentRoute: () => rootRouteImport,
28
+ } as any)
29
+ const EmailPreferencesRoute = EmailPreferencesRouteImport.update({
30
+ id: '/email-preferences',
31
+ path: '/email-preferences',
32
+ getParentRoute: () => rootRouteImport,
33
+ } as any)
34
+ const RoadmapRoute = RoadmapRouteImport.update({
35
+ id: '/roadmap',
36
+ path: '/roadmap',
37
+ getParentRoute: () => rootRouteImport,
38
+ } as any)
39
+ const SubmitRoute = SubmitRouteImport.update({
40
+ id: '/submit',
41
+ path: '/submit',
42
+ getParentRoute: () => rootRouteImport,
43
+ } as any)
44
+ const PSlugRoute = PSlugRouteImport.update({
45
+ id: '/p/$slug',
46
+ path: '/p/$slug',
47
+ getParentRoute: () => rootRouteImport,
48
+ } as any)
49
+
50
+ export interface FileRoutesByFullPath {
51
+ '/': typeof IndexRoute
52
+ '/changelog': typeof ChangelogRoute
53
+ '/email-preferences': typeof EmailPreferencesRoute
54
+ '/roadmap': typeof RoadmapRoute
55
+ '/submit': typeof SubmitRoute
56
+ '/p/$slug': typeof PSlugRoute
57
+ }
58
+ export interface FileRoutesByTo {
59
+ '/': typeof IndexRoute
60
+ '/changelog': typeof ChangelogRoute
61
+ '/email-preferences': typeof EmailPreferencesRoute
62
+ '/roadmap': typeof RoadmapRoute
63
+ '/submit': typeof SubmitRoute
64
+ '/p/$slug': typeof PSlugRoute
65
+ }
66
+ export interface FileRoutesById {
67
+ __root__: typeof rootRouteImport
68
+ '/': typeof IndexRoute
69
+ '/changelog': typeof ChangelogRoute
70
+ '/email-preferences': typeof EmailPreferencesRoute
71
+ '/roadmap': typeof RoadmapRoute
72
+ '/submit': typeof SubmitRoute
73
+ '/p/$slug': typeof PSlugRoute
74
+ }
75
+ export interface FileRouteTypes {
76
+ fileRoutesByFullPath: FileRoutesByFullPath
77
+ fullPaths:
78
+ | '/'
79
+ | '/changelog'
80
+ | '/email-preferences'
81
+ | '/roadmap'
82
+ | '/submit'
83
+ | '/p/$slug'
84
+ fileRoutesByTo: FileRoutesByTo
85
+ to:
86
+ | '/'
87
+ | '/changelog'
88
+ | '/email-preferences'
89
+ | '/roadmap'
90
+ | '/submit'
91
+ | '/p/$slug'
92
+ id:
93
+ | '__root__'
94
+ | '/'
95
+ | '/changelog'
96
+ | '/email-preferences'
97
+ | '/roadmap'
98
+ | '/submit'
99
+ | '/p/$slug'
100
+ fileRoutesById: FileRoutesById
101
+ }
102
+ export interface RootRouteChildren {
103
+ IndexRoute: typeof IndexRoute
104
+ ChangelogRoute: typeof ChangelogRoute
105
+ EmailPreferencesRoute: typeof EmailPreferencesRoute
106
+ RoadmapRoute: typeof RoadmapRoute
107
+ SubmitRoute: typeof SubmitRoute
108
+ PSlugRoute: typeof PSlugRoute
109
+ }
110
+
111
+ declare module '@tanstack/react-router' {
112
+ interface FileRoutesByPath {
113
+ '/': {
114
+ id: '/'
115
+ path: '/'
116
+ fullPath: '/'
117
+ preLoaderRoute: typeof IndexRouteImport
118
+ parentRoute: typeof rootRouteImport
119
+ }
120
+ '/changelog': {
121
+ id: '/changelog'
122
+ path: '/changelog'
123
+ fullPath: '/changelog'
124
+ preLoaderRoute: typeof ChangelogRouteImport
125
+ parentRoute: typeof rootRouteImport
126
+ }
127
+ '/email-preferences': {
128
+ id: '/email-preferences'
129
+ path: '/email-preferences'
130
+ fullPath: '/email-preferences'
131
+ preLoaderRoute: typeof EmailPreferencesRouteImport
132
+ parentRoute: typeof rootRouteImport
133
+ }
134
+ '/roadmap': {
135
+ id: '/roadmap'
136
+ path: '/roadmap'
137
+ fullPath: '/roadmap'
138
+ preLoaderRoute: typeof RoadmapRouteImport
139
+ parentRoute: typeof rootRouteImport
140
+ }
141
+ '/submit': {
142
+ id: '/submit'
143
+ path: '/submit'
144
+ fullPath: '/submit'
145
+ preLoaderRoute: typeof SubmitRouteImport
146
+ parentRoute: typeof rootRouteImport
147
+ }
148
+ '/p/$slug': {
149
+ id: '/p/$slug'
150
+ path: '/p/$slug'
151
+ fullPath: '/p/$slug'
152
+ preLoaderRoute: typeof PSlugRouteImport
153
+ parentRoute: typeof rootRouteImport
154
+ }
155
+ }
156
+ }
157
+
158
+ const rootRouteChildren: RootRouteChildren = {
159
+ IndexRoute: IndexRoute,
160
+ ChangelogRoute: ChangelogRoute,
161
+ EmailPreferencesRoute: EmailPreferencesRoute,
162
+ RoadmapRoute: RoadmapRoute,
163
+ SubmitRoute: SubmitRoute,
164
+ PSlugRoute: PSlugRoute,
165
+ }
166
+ export const routeTree = rootRouteImport
167
+ ._addFileChildren(rootRouteChildren)
168
+ ._addFileTypes<FileRouteTypes>()
169
+
170
+ import type { getRouter } from './router.tsx'
171
+ import type { createStart } from '@tanstack/react-start'
172
+ declare module '@tanstack/react-start' {
173
+ interface Register {
174
+ ssr: true
175
+ router: Awaited<ReturnType<typeof getRouter>>
176
+ }
177
+ }
@@ -0,0 +1,28 @@
1
+ import {
2
+ editPortalFeedbackDraft,
3
+ submitPortalPost,
4
+ withdrawPortalFeedbackDraft,
5
+ } from "./portal-feedback-server-function";
6
+ import { PostCreationFlow } from "./post-creation-flow";
7
+
8
+ export const portalFeedbackMutations = {
9
+ submitPost: submitPortalPost,
10
+ editDraftPost: editPortalFeedbackDraft,
11
+ withdrawDraftPost: withdrawPortalFeedbackDraft,
12
+ };
13
+
14
+ export function RoutedPostCreationForm({
15
+ display = "page",
16
+ onCancel,
17
+ }: {
18
+ display?: "page" | "overlay";
19
+ onCancel?: () => void;
20
+ }) {
21
+ return (
22
+ <PostCreationFlow
23
+ display={display}
24
+ onCancel={onCancel}
25
+ mutations={portalFeedbackMutations}
26
+ />
27
+ );
28
+ }
@@ -0,0 +1,27 @@
1
+ import { createRouter as createTanStackRouter } from "@tanstack/react-router";
2
+ import { QueryClient } from "@tanstack/react-query";
3
+
4
+ import Loader from "./components/loader";
5
+ import { routeTree } from "./routeTree.gen";
6
+
7
+ export const getRouter = () => {
8
+ const queryClient = new QueryClient();
9
+ const router = createTanStackRouter({
10
+ routeTree,
11
+ scrollRestoration: true,
12
+ defaultPreloadStaleTime: 0,
13
+ context: { queryClient },
14
+ defaultPendingMs: 250,
15
+ defaultPendingMinMs: 200,
16
+ defaultPendingComponent: () => <Loader />,
17
+ defaultNotFoundComponent: () => <div>Not Found</div>,
18
+ });
19
+
20
+ return router;
21
+ };
22
+
23
+ declare module "@tanstack/react-router" {
24
+ interface Register {
25
+ router: ReturnType<typeof getRouter>;
26
+ }
27
+ }
@@ -0,0 +1,114 @@
1
+ import { Toaster } from "@feedbax/ui/components/sonner";
2
+ import {
3
+ HeadContent,
4
+ Outlet,
5
+ Scripts,
6
+ createRootRouteWithContext,
7
+ useRouterState,
8
+ } from "@tanstack/react-router";
9
+ import { lazy, Suspense } from "react";
10
+ import { ThemeProvider } from "next-themes";
11
+ import { QueryClientProvider } from "@tanstack/react-query";
12
+
13
+ import Header from "../components/header";
14
+ import { PortalFooter } from "../components/portal-footer";
15
+ import { LazyPostDetailOverlay as PostDetailOverlay } from "../lazy-post-detail-overlay";
16
+ import { DeviceProfileProvider } from "../components/device-profile-provider";
17
+ import {
18
+ CommandPaletteProvider,
19
+ useCommandPalette,
20
+ } from "../components/command-palette-context";
21
+ import feedbax from "../feedbax";
22
+ import { createPortalHead } from "../portal-head";
23
+
24
+ import appCss from "../index.css?url";
25
+
26
+ const RouterDevtools = import.meta.env.DEV
27
+ ? lazy(() =>
28
+ import("@tanstack/react-router-devtools").then((module) => ({
29
+ default: module.TanStackRouterDevtools,
30
+ })),
31
+ )
32
+ : null;
33
+
34
+ const CommandPalette = lazy(() =>
35
+ import("../components/command-palette").then((module) => ({
36
+ default: module.CommandPalette,
37
+ })),
38
+ );
39
+ const CreatePostOverlay = lazy(() =>
40
+ import("../lazy-create-post-overlay").then((module) => ({
41
+ default: module.LazyCreatePostOverlay,
42
+ })),
43
+ );
44
+
45
+ export type RouterAppContext = {
46
+ queryClient: import("@tanstack/react-query").QueryClient;
47
+ };
48
+
49
+ export const Route = createRootRouteWithContext<RouterAppContext>()({
50
+ head: () => createPortalHead(feedbax.product, appCss),
51
+
52
+ component: RootDocument,
53
+ });
54
+
55
+ function OptionalPortalSurfaces() {
56
+ const { open: commandPaletteOpen } = useCommandPalette();
57
+ const overlayState = useRouterState({
58
+ select: (state) => state.location.state,
59
+ });
60
+
61
+ return (
62
+ <Suspense fallback={null}>
63
+ {overlayState.createPostOverlay ? <CreatePostOverlay /> : null}
64
+ {overlayState.postDetailOverlay ? <PostDetailOverlay /> : null}
65
+ {commandPaletteOpen ? (
66
+ <CommandPalette
67
+ searchPosts={async (term) => {
68
+ const { getPublicPostPage } =
69
+ await import("../public-feedback-server-function");
70
+ return getPublicPostPage({
71
+ data: { search: term || undefined },
72
+ });
73
+ }}
74
+ />
75
+ ) : null}
76
+ </Suspense>
77
+ );
78
+ }
79
+
80
+ function RootDocument() {
81
+ const { queryClient } = Route.useRouteContext();
82
+ return (
83
+ <html lang="en" suppressHydrationWarning>
84
+ <head>
85
+ <HeadContent />
86
+ </head>
87
+ <body>
88
+ <QueryClientProvider client={queryClient}>
89
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
90
+ <DeviceProfileProvider>
91
+ <CommandPaletteProvider>
92
+ <div className="portal-shell">
93
+ <Header />
94
+ <div id="main-content" tabIndex={-1}>
95
+ <Outlet />
96
+ </div>
97
+ <PortalFooter />
98
+ <OptionalPortalSurfaces />
99
+ </div>
100
+ </CommandPaletteProvider>
101
+ </DeviceProfileProvider>
102
+ </ThemeProvider>
103
+ </QueryClientProvider>
104
+ <Toaster richColors />
105
+ {RouterDevtools ? (
106
+ <Suspense fallback={null}>
107
+ <RouterDevtools position="bottom-left" />
108
+ </Suspense>
109
+ ) : null}
110
+ <Scripts />
111
+ </body>
112
+ </html>
113
+ );
114
+ }
@@ -0,0 +1,39 @@
1
+ import { createFileRoute, notFound } from "@tanstack/react-router";
2
+ import type { PortalFeatures } from "@feedbax/config";
3
+ import { useSuspenseQuery } from "@tanstack/react-query";
4
+
5
+ import { ChangelogPage } from "../changelog-page";
6
+ import { changelogQuery } from "../changelog-query";
7
+ import feedbax from "../feedbax";
8
+ import { getPublicChangelogPage } from "../public-changelog-server-function";
9
+ import { createRouteHead } from "../portal-head";
10
+
11
+ export function ensureChangelogEnabled(features: PortalFeatures) {
12
+ if (!features.changelog) throw notFound();
13
+ }
14
+
15
+ const query = changelogQuery(getPublicChangelogPage);
16
+
17
+ export const Route = createFileRoute("/changelog")({
18
+ head: () =>
19
+ createRouteHead(feedbax.product, {
20
+ title: "Changelog",
21
+ description: `Product updates from ${feedbax.product.name}.`,
22
+ path: "/changelog",
23
+ }),
24
+ beforeLoad: () => {
25
+ ensureChangelogEnabled(feedbax.features);
26
+ },
27
+ loader: ({ context }) => context.queryClient.ensureQueryData(query),
28
+ component: ChangelogRoute,
29
+ });
30
+
31
+ function ChangelogRoute() {
32
+ const initialPage = useSuspenseQuery(query).data;
33
+ return (
34
+ <ChangelogPage
35
+ initialPage={initialPage}
36
+ loadPage={(data) => getPublicChangelogPage({ data })}
37
+ />
38
+ );
39
+ }
@@ -0,0 +1,77 @@
1
+ import { createFileRoute, notFound } from "@tanstack/react-router";
2
+ import { useState } from "react";
3
+ import { Button } from "@feedbax/ui/components/button";
4
+ import feedbax from "../feedbax";
5
+ export const Route = createFileRoute("/email-preferences")({
6
+ validateSearch: (search: Record<string, unknown>) => ({
7
+ token: typeof search.token === "string" ? search.token : "",
8
+ }),
9
+ beforeLoad: () => {
10
+ if (!feedbax.connected?.email) throw notFound();
11
+ },
12
+ head: () => ({
13
+ meta: [
14
+ { name: "robots", content: "noindex,nofollow" },
15
+ { name: "referrer", content: "no-referrer" },
16
+ ],
17
+ }),
18
+ component: EmailPreferencesPage,
19
+ });
20
+ function EmailPreferencesPage() {
21
+ const { token } = Route.useSearch();
22
+ const [pending, setPending] = useState(false);
23
+ const [done, setDone] = useState(false);
24
+ const [error, setError] = useState<string>();
25
+ async function unsubscribe() {
26
+ setPending(true);
27
+ setError(undefined);
28
+ try {
29
+ const response = await fetch(
30
+ `/internal/connected/unsubscribe?token=${encodeURIComponent(token)}`,
31
+ { method: "POST" },
32
+ );
33
+ if (!response.ok)
34
+ throw new Error(
35
+ "This link could not be used. Try again or change email preferences in your notification inbox.",
36
+ );
37
+ setDone(true);
38
+ } catch (cause) {
39
+ setError(
40
+ cause instanceof Error
41
+ ? cause.message
42
+ : "Couldn’t update email preferences.",
43
+ );
44
+ } finally {
45
+ setPending(false);
46
+ }
47
+ }
48
+ return (
49
+ <main className="mx-auto max-w-lg space-y-5 px-6 py-16">
50
+ <h1 className="text-2xl font-semibold tracking-tight">
51
+ {done ? "Email notifications are off" : "Stop email notifications"}
52
+ </h1>
53
+ <p className="text-sm leading-relaxed text-muted-foreground">
54
+ {done
55
+ ? "You can turn them on again in your notification inbox."
56
+ : "You will keep receiving updates inside Feedbax."}
57
+ </p>
58
+ {done ? (
59
+ <Button nativeButton={false} render={<a href="/" />} variant="outline">
60
+ Back to feedback
61
+ </Button>
62
+ ) : (
63
+ <Button
64
+ onClick={unsubscribe}
65
+ disabled={pending || !/^[a-f0-9]{32}\.[A-Za-z0-9_-]{43}$/.test(token)}
66
+ >
67
+ {pending ? "Updating…" : "Stop email notifications"}
68
+ </Button>
69
+ )}
70
+ {error ? (
71
+ <p role="alert" className="text-sm text-destructive">
72
+ {error}
73
+ </p>
74
+ ) : null}
75
+ </main>
76
+ );
77
+ }
@@ -0,0 +1,61 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+
3
+ import {
4
+ PublicPostFeedSkeleton,
5
+ PublicPostIndex,
6
+ } from "../public-feedback-index";
7
+ import { PublicPortalError } from "../public-portal-error";
8
+ import { publicPostSearch } from "../public-feedback-page";
9
+ import { publicPostsQuery, type FetchPublicPostsPage } from "../post-queries";
10
+ import { getPublicPostPage } from "../public-feedback-server-function";
11
+ import { useSuspenseInfiniteQuery } from "@tanstack/react-query";
12
+ import { useTransition } from "react";
13
+ import feedbax from "../feedbax";
14
+ import { createRouteHead } from "../portal-head";
15
+
16
+ const fetchPublicPostsPage: FetchPublicPostsPage = (search) =>
17
+ getPublicPostPage({ data: search });
18
+
19
+ export const Route = createFileRoute("/")({
20
+ head: () => createRouteHead(feedbax.product, { path: "/" }),
21
+ validateSearch: publicPostSearch,
22
+ loaderDeps: ({ search }) => search,
23
+ loader: ({ deps, context }) =>
24
+ context.queryClient.ensureInfiniteQueryData(
25
+ publicPostsQuery(deps, fetchPublicPostsPage),
26
+ ),
27
+ component: HomeComponent,
28
+ pendingComponent: PublicPostFeedSkeleton,
29
+ errorComponent: PublicPortalError,
30
+ });
31
+
32
+ function HomeComponent() {
33
+ const search = Route.useSearch();
34
+ const navigate = Route.useNavigate();
35
+ const [pending, startTransition] = useTransition();
36
+ const query = useSuspenseInfiniteQuery(
37
+ publicPostsQuery(search, fetchPublicPostsPage),
38
+ );
39
+ const pages = query.data.pages;
40
+ const page = {
41
+ items: pages.flatMap((page) => page.items),
42
+ nextCursor: pages.at(-1)?.nextCursor,
43
+ };
44
+
45
+ return (
46
+ <PublicPostIndex
47
+ page={page}
48
+ search={search}
49
+ pending={pending}
50
+ loadMore={() => query.fetchNextPage()}
51
+ loadingMore={query.isFetchingNextPage}
52
+ loadMoreError={query.isFetchNextPageError}
53
+ onSearchChange={(nextSearch) =>
54
+ startTransition(() => {
55
+ void navigate({ search: nextSearch, replace: false });
56
+ })
57
+ }
58
+ maskPostLinks
59
+ />
60
+ );
61
+ }
@@ -0,0 +1,16 @@
1
+ import { createPublicPostRoute } from "../public-post-route";
2
+ import { PublicPortalError } from "../public-portal-error";
3
+ import {
4
+ getPublicPost,
5
+ getPublicPostComments,
6
+ } from "../public-post-server-function";
7
+ import { AuthorizedDraftPost } from "../authorized-draft-post";
8
+
9
+ export const Route = createPublicPostRoute({
10
+ loadPost: (slug) => getPublicPost({ data: { slug } }),
11
+ loadComments: (slug, cursor) =>
12
+ getPublicPostComments({ data: { slug, cursor } }),
13
+ renderUnavailable: (slug) => <AuthorizedDraftPost slug={slug} />,
14
+ }).update({
15
+ errorComponent: PublicPortalError,
16
+ });
@@ -0,0 +1,11 @@
1
+ import { createPublicRoadmapRoute } from "../public-roadmap-route";
2
+ import {
3
+ getPublicRoadmapPage,
4
+ getPublicRoadmapStatusPage,
5
+ } from "../public-roadmap-server-function";
6
+
7
+ export const Route = createPublicRoadmapRoute({
8
+ fetchRoadmap: () => getPublicRoadmapPage(),
9
+ fetchRoadmapStatusPage: (query) =>
10
+ getPublicRoadmapStatusPage({ data: query }),
11
+ });
@@ -0,0 +1,28 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+
3
+ import { RoutedPostCreationForm } from "../routed-post-creation-form";
4
+ import { PublicPortalError } from "../public-portal-error";
5
+ import { PostCreationSkeleton } from "../post-creation-flow";
6
+ import feedbax from "../feedbax";
7
+ import { createRouteHead } from "../portal-head";
8
+
9
+ export const Route = createFileRoute("/submit")({
10
+ head: () =>
11
+ createRouteHead(feedbax.product, {
12
+ title: "Share feedback",
13
+ description: `Share feedback with ${feedbax.product.name}.`,
14
+ path: "/submit",
15
+ noIndex: true,
16
+ }),
17
+ component: SubmitComponent,
18
+ pendingComponent: PostCreationSkeleton,
19
+ errorComponent: PublicPortalError,
20
+ });
21
+
22
+ function SubmitComponent() {
23
+ return (
24
+ <main className="feedback-index">
25
+ <RoutedPostCreationForm />
26
+ </main>
27
+ );
28
+ }
@@ -0,0 +1,47 @@
1
+ import {
2
+ BrowserCapabilityAuthorizationError,
3
+ VoteEligibilityError,
4
+ ImageAttachmentError,
5
+ } from "@feedbax/feedback";
6
+
7
+ export async function runSafePublicRead<Value>(
8
+ operation: () => Promise<Value>,
9
+ ): Promise<Value> {
10
+ try {
11
+ return await operation();
12
+ } catch {
13
+ throw new Error("Public feedback is temporarily unavailable.");
14
+ }
15
+ }
16
+
17
+ export class ActionablePortalFailure extends Error {
18
+ constructor(
19
+ message: string,
20
+ readonly code?: string,
21
+ ) {
22
+ super(message);
23
+ this.name = "ActionablePortalFailure";
24
+ }
25
+ }
26
+
27
+ export async function runSafePortalMutation<Value>(
28
+ operation: () => Promise<Value>,
29
+ ): Promise<Value> {
30
+ try {
31
+ return await operation();
32
+ } catch (error) {
33
+ if (
34
+ error instanceof ActionablePortalFailure ||
35
+ error instanceof BrowserCapabilityAuthorizationError ||
36
+ error instanceof VoteEligibilityError ||
37
+ error instanceof ImageAttachmentError
38
+ ) {
39
+ throw error;
40
+ }
41
+ throwSafePortalMutationFailure();
42
+ }
43
+ }
44
+
45
+ function throwSafePortalMutationFailure(): never {
46
+ throw new Error("Public feedback mutation is temporarily unavailable.");
47
+ }