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