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,151 @@
1
+ import { handleImageRequest } from "./images-runtime";
2
+ import {
3
+ handleParticipationRequest,
4
+ synchronizeConnectedUpdates,
5
+ } from "./participation-runtime";
6
+ import { handleConnectedRequest } from "./connected-runtime";
7
+ import { configuredTurnstileSecret } from "./turnstile-configuration";
8
+ import { createPortalServerEntry } from "./portal-server-entry";
9
+ import { createConfiguredTrustedFeedbackHandler } from "./trusted-feedback-runtime";
10
+ import { env } from "cloudflare:workers";
11
+ import { createConfiguredFeedbackModule } from "./feedback-runtime";
12
+ import { createPortalVoteHandler } from "./portal-voting";
13
+ import { createCloudflareTurnstileVerifier } from "./cloudflare-turnstile";
14
+ import feedbax from "./feedbax";
15
+ import { ActionablePortalFailure } from "./safe-public-failure";
16
+ import { VoteEligibilityError } from "@feedbax/feedback";
17
+ import {
18
+ createPortalCommentMutationRequestHandler,
19
+ createPortalCommentRequestHandler,
20
+ } from "./portal-comments";
21
+
22
+ const application = createPortalServerEntry({
23
+ connectedHandler: async (request) =>
24
+ (await handleImageRequest(request)) ??
25
+ (await handleParticipationRequest(request)) ??
26
+ handleConnectedRequest(request),
27
+ commentHandler: (request) => {
28
+ const turnstileSecret = configuredTurnstileSecret(env.TURNSTILE_SECRET_KEY);
29
+ return createPortalCommentRequestHandler({
30
+ feedback: createConfiguredFeedbackModule(),
31
+ commentsEnabled: feedbax.features.comments,
32
+ rateLimiter: env.PORTAL_SUBMISSIONS_RATE_LIMITER,
33
+ rateLimitKey:
34
+ request.headers.get("CF-Connecting-IP") ?? "unknown-participant-ip",
35
+ ...(turnstileSecret
36
+ ? {
37
+ turnstileVerifier: createCloudflareTurnstileVerifier({
38
+ secretKey: turnstileSecret,
39
+ }),
40
+ participationSigningSecret: env.PARTICIPATION_SIGNING_SECRET,
41
+ }
42
+ : {}),
43
+ participationSigningSecret: env.PARTICIPATION_SIGNING_SECRET,
44
+ })(request);
45
+ },
46
+ commentMutationHandler: (request) =>
47
+ createPortalCommentMutationRequestHandler({
48
+ feedback: createConfiguredFeedbackModule(),
49
+ commentsEnabled: feedbax.features.comments,
50
+ rateLimiter: env.PORTAL_SUBMISSIONS_RATE_LIMITER,
51
+ rateLimitKey:
52
+ request.headers.get("CF-Connecting-IP") ?? "unknown-participant-ip",
53
+ ...(configuredTurnstileSecret(env.TURNSTILE_SECRET_KEY)
54
+ ? {
55
+ turnstileVerifier: createCloudflareTurnstileVerifier({
56
+ secretKey: configuredTurnstileSecret(env.TURNSTILE_SECRET_KEY)!,
57
+ }),
58
+ }
59
+ : {}),
60
+ participationSigningSecret: env.PARTICIPATION_SIGNING_SECRET,
61
+ })(request),
62
+ voteHandler: async (request) => {
63
+ try {
64
+ const turnstileSecret = configuredTurnstileSecret(
65
+ env.TURNSTILE_SECRET_KEY,
66
+ );
67
+ const result = await createPortalVoteHandler({
68
+ feedback: createConfiguredFeedbackModule(),
69
+ votingEnabled: feedbax.features.voting,
70
+ rateLimiter: env.PORTAL_SUBMISSIONS_RATE_LIMITER,
71
+ rateLimitKey:
72
+ request.headers.get("CF-Connecting-IP") ?? "unknown-participant-ip",
73
+ ...(turnstileSecret
74
+ ? {
75
+ turnstileVerifier: createCloudflareTurnstileVerifier({
76
+ secretKey: turnstileSecret,
77
+ }),
78
+ participationSigningSecret: env.PARTICIPATION_SIGNING_SECRET,
79
+ }
80
+ : {}),
81
+ })(await request.json());
82
+ return Response.json(result);
83
+ } catch (error) {
84
+ const actionable =
85
+ error instanceof ActionablePortalFailure ||
86
+ error instanceof VoteEligibilityError;
87
+ return Response.json(
88
+ {
89
+ error: actionable
90
+ ? error.message
91
+ : "Public feedback mutation is temporarily unavailable.",
92
+ code:
93
+ error instanceof ActionablePortalFailure
94
+ ? (error.code ?? "action_failed")
95
+ : error instanceof VoteEligibilityError
96
+ ? "ineligible_post"
97
+ : "temporarily_unavailable",
98
+ },
99
+ {
100
+ status:
101
+ error instanceof ActionablePortalFailure &&
102
+ error.code === "rate_limited"
103
+ ? 429
104
+ : actionable
105
+ ? 400
106
+ : 503,
107
+ },
108
+ );
109
+ }
110
+ },
111
+ trustedFeedbackHandler: async (request) => {
112
+ try {
113
+ return await createConfiguredTrustedFeedbackHandler()(request);
114
+ } catch {
115
+ return Response.json(
116
+ { error: "Trusted submission is unavailable." },
117
+ { status: 503 },
118
+ );
119
+ }
120
+ },
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
+ };
@@ -0,0 +1,45 @@
1
+ import type { PublicPostPage } from "@feedbax/feedback";
2
+ import { useEffect, useRef, useState } from "react";
3
+
4
+ import { getPublicPostPage } from "./public-feedback-server-function";
5
+
6
+ export const similarPostMinimumTitleLength = 4;
7
+
8
+ export function SimilarPosts({ title }: { title: string }) {
9
+ const [posts, setPosts] = useState<PublicPostPage["items"]>([]);
10
+ const requestSequence = useRef(0);
11
+
12
+ useEffect(() => {
13
+ const query = title.trim();
14
+ const sequence = ++requestSequence.current;
15
+ if (query.length < similarPostMinimumTitleLength) {
16
+ setPosts([]);
17
+ return;
18
+ }
19
+ const timeout = window.setTimeout(() => {
20
+ void getPublicPostPage({ data: { search: query, sort: "top" } })
21
+ .then((page) => {
22
+ if (sequence === requestSequence.current)
23
+ setPosts(page.items.slice(0, 3));
24
+ })
25
+ .catch(() => {
26
+ if (sequence === requestSequence.current) setPosts([]);
27
+ });
28
+ }, 300);
29
+ return () => window.clearTimeout(timeout);
30
+ }, [title]);
31
+
32
+ if (!posts.length) return null;
33
+ return (
34
+ <aside aria-label="Potentially related Posts">
35
+ <p className="feedback-eyebrow">Potentially related</p>
36
+ <ul>
37
+ {posts.map((post) => (
38
+ <li key={post.slug}>
39
+ <a href={`/p/${encodeURIComponent(post.slug)}`}>{post.title}</a>
40
+ </li>
41
+ ))}
42
+ </ul>
43
+ </aside>
44
+ );
45
+ }
@@ -0,0 +1,134 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
4
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5
+ import {
6
+ Link,
7
+ Outlet,
8
+ RouterProvider,
9
+ createMemoryHistory,
10
+ createRootRoute,
11
+ createRoute,
12
+ createRouter,
13
+ } from "@tanstack/react-router";
14
+ import { afterEach, describe, expect, it, vi } from "vitest";
15
+
16
+ import { CreatePostOverlay } from "./create-post-overlay";
17
+ import { PostCreationSkeleton } from "./post-creation-flow";
18
+ import { renderToStaticMarkup } from "react-dom/server";
19
+
20
+ const mutations = {
21
+ submitPost: vi.fn(),
22
+ editDraftPost: vi.fn(),
23
+ withdrawDraftPost: vi.fn(),
24
+ };
25
+
26
+ afterEach(() => {
27
+ cleanup();
28
+ vi.restoreAllMocks();
29
+ vi.unstubAllGlobals();
30
+ });
31
+
32
+ describe("/submit", () => {
33
+ it("opens from the feed at the canonical URL without discarding the feed", async () => {
34
+ const history = createMemoryHistory({ initialEntries: ["/"] });
35
+ const router = createSubmitRouter(history);
36
+
37
+ renderSubmitRouter(router);
38
+ fireEvent.click(await screen.findByRole("link", { name: "Create Post" }));
39
+
40
+ expect(
41
+ await screen.findByRole("dialog", { name: "Create a Post" }),
42
+ ).toBeTruthy();
43
+ expect(document.body.textContent).toContain("Posts");
44
+ expect(history.location.pathname).toBe("/submit");
45
+ });
46
+
47
+ it("renders a complete creation page when entered directly", async () => {
48
+ const history = createMemoryHistory({ initialEntries: ["/submit"] });
49
+ const router = createSubmitRouter(history);
50
+
51
+ renderSubmitRouter(router);
52
+
53
+ expect(await screen.findByRole("main")).toBeTruthy();
54
+ expect(screen.getByRole("heading", { name: "Create a Post" })).toBeTruthy();
55
+ expect(screen.queryByRole("dialog")).toBeNull();
56
+ });
57
+
58
+ it("uses a Drawer on mobile and Back restores the feed route", async () => {
59
+ vi.stubGlobal("matchMedia", (query: string) => ({
60
+ matches: query === "(max-width: 720px)",
61
+ media: query,
62
+ addEventListener: vi.fn(),
63
+ removeEventListener: vi.fn(),
64
+ }));
65
+ const history = createMemoryHistory({ initialEntries: ["/"] });
66
+ const router = createSubmitRouter(history);
67
+
68
+ renderSubmitRouter(router);
69
+ fireEvent.click(await screen.findByRole("link", { name: "Create Post" }));
70
+
71
+ const drawer = await screen.findByRole("dialog", { name: "Create a Post" });
72
+ expect(drawer.closest('[data-slot="drawer-popup"]')).toBeTruthy();
73
+ history.back();
74
+ await vi.waitFor(() => expect(history.location.pathname).toBe("/"));
75
+ await vi.waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
76
+ });
77
+
78
+ it("has a contextual creation skeleton for routed loading", () => {
79
+ const html = renderToStaticMarkup(<PostCreationSkeleton />);
80
+
81
+ expect(html).toContain('aria-label="Loading Post creation"');
82
+ expect(html).toContain('aria-busy="true"');
83
+ expect(html).toContain("Loading Post creation…");
84
+ });
85
+ });
86
+
87
+ function renderSubmitRouter(router: ReturnType<typeof createSubmitRouter>) {
88
+ render(
89
+ <QueryClientProvider client={new QueryClient()}>
90
+ <RouterProvider router={router} />
91
+ </QueryClientProvider>,
92
+ );
93
+ }
94
+
95
+ function createSubmitRouter(history: ReturnType<typeof createMemoryHistory>) {
96
+ const root = createRootRoute({
97
+ component: () => (
98
+ <>
99
+ <Outlet />
100
+ <CreatePostOverlay mutations={mutations} />
101
+ </>
102
+ ),
103
+ });
104
+ const index = createRoute({
105
+ getParentRoute: () => root,
106
+ path: "/",
107
+ component: () => (
108
+ <main>
109
+ <h1>Posts</h1>
110
+ <Link
111
+ to="/"
112
+ state={{ createPostOverlay: true }}
113
+ mask={{ to: "/submit", unmaskOnReload: true }}
114
+ >
115
+ Create Post
116
+ </Link>
117
+ </main>
118
+ ),
119
+ });
120
+ const submit = createRoute({
121
+ getParentRoute: () => root,
122
+ path: "/submit",
123
+ component: () => (
124
+ <main>
125
+ <h1>Create a Post</h1>
126
+ </main>
127
+ ),
128
+ });
129
+
130
+ return createRouter({
131
+ history,
132
+ routeTree: root.addChildren([index, submit]),
133
+ });
134
+ }