create-feedbax 0.1.0 → 0.2.0
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/LICENSE +201 -21
- package/README.md +11 -1
- package/dist/cli.js +1261 -26
- package/dist/index.js +1402 -160
- package/package.json +40 -14
- package/template/.feedbax-template.sha256 +1 -0
- package/template/.gitignore.template +6 -0
- package/template/.npmignore.template +5 -0
- package/template/.prettierignore.template +4 -0
- package/template/LICENSE +201 -0
- package/template/components.json +25 -0
- package/template/eslint.config.mjs +25 -0
- package/template/package.json +70 -14
- package/template/public/feedbax-mark.svg +4 -0
- package/template/public/robots.txt +3 -0
- package/template/src/authorized-draft-post.test.tsx +212 -0
- package/template/src/authorized-draft-post.tsx +204 -0
- package/template/src/authorized-draft-query.ts +74 -0
- package/template/src/browser-post-state.test.ts +120 -0
- package/template/src/browser-post-state.ts +134 -0
- package/template/src/browser-vote-state.test.ts +14 -0
- package/template/src/browser-vote-state.ts +27 -0
- package/template/src/capability-authorization-error.ts +3 -0
- package/template/src/changelog-page.test.tsx +139 -0
- package/template/src/changelog-page.tsx +219 -0
- package/template/src/changelog-query.ts +13 -0
- package/template/src/changelog-runtime.ts +27 -0
- package/template/src/cloudflare-turnstile.ts +31 -0
- package/template/src/cloudflare-workers-test.ts +3 -0
- package/template/src/cloudflare-workers.d.ts +18 -0
- package/template/src/comment-creation.test.tsx +270 -0
- package/template/src/comment-profile.ts +5 -0
- package/template/src/comment-thread-page.ts +18 -0
- package/template/src/components/command-palette-context.tsx +71 -0
- package/template/src/components/command-palette.tsx +246 -0
- package/template/src/components/create-post-trigger.tsx +44 -0
- package/template/src/components/device-profile-provider.tsx +309 -0
- package/template/src/components/header.tsx +95 -0
- package/template/src/components/loader.tsx +10 -0
- package/template/src/components/profile-menu.tsx +89 -0
- package/template/src/components/ui/components/alert-dialog.tsx +185 -0
- package/template/src/components/ui/components/attachment.tsx +207 -0
- package/template/src/components/ui/components/avatar.tsx +107 -0
- package/template/src/components/ui/components/badge.tsx +52 -0
- package/template/src/components/ui/components/bubble.tsx +128 -0
- package/template/src/components/ui/components/button-group.tsx +87 -0
- package/template/src/components/ui/components/button.tsx +56 -0
- package/template/src/components/ui/components/card.tsx +103 -0
- package/template/src/components/ui/components/checkbox.tsx +26 -0
- package/template/src/components/ui/components/command.tsx +194 -0
- package/template/src/components/ui/components/dialog.tsx +154 -0
- package/template/src/components/ui/components/drawer.tsx +229 -0
- package/template/src/components/ui/components/dropdown-menu.tsx +270 -0
- package/template/src/components/ui/components/empty.tsx +101 -0
- package/template/src/components/ui/components/field.tsx +236 -0
- package/template/src/components/ui/components/input-group.tsx +155 -0
- package/template/src/components/ui/components/input.tsx +20 -0
- package/template/src/components/ui/components/item.tsx +201 -0
- package/template/src/components/ui/components/kbd.tsx +26 -0
- package/template/src/components/ui/components/label.tsx +18 -0
- package/template/src/components/ui/components/marker.tsx +71 -0
- package/template/src/components/ui/components/message-scroller.tsx +128 -0
- package/template/src/components/ui/components/message.tsx +92 -0
- package/template/src/components/ui/components/native-select.tsx +65 -0
- package/template/src/components/ui/components/popover.tsx +88 -0
- package/template/src/components/ui/components/portal-primitives.test.tsx +85 -0
- package/template/src/components/ui/components/radio-group.tsx +36 -0
- package/template/src/components/ui/components/scroll-area.tsx +52 -0
- package/template/src/components/ui/components/secondary-form-actions.test.tsx +23 -0
- package/template/src/components/ui/components/select.tsx +199 -0
- package/template/src/components/ui/components/separator.tsx +23 -0
- package/template/src/components/ui/components/sheet.tsx +135 -0
- package/template/src/components/ui/components/skeleton.tsx +13 -0
- package/template/src/components/ui/components/sonner.tsx +43 -0
- package/template/src/components/ui/components/spinner.tsx +16 -0
- package/template/src/components/ui/components/tabs.tsx +80 -0
- package/template/src/components/ui/components/textarea.tsx +18 -0
- package/template/src/components/ui/components/tooltip.tsx +64 -0
- package/template/src/components/ui/hooks/.gitkeep +0 -0
- package/template/src/components/ui/lib/utils.ts +6 -0
- package/template/src/components/ui/styles/globals.css +151 -0
- package/template/src/create-post-overlay.tsx +82 -0
- package/template/src/draft-post-list.tsx +39 -0
- package/template/src/feedback-cache-invalidation.test.ts +185 -0
- package/template/src/feedback-cache-invalidation.ts +97 -0
- package/template/src/feedback-runtime.ts +38 -0
- package/template/src/feedbax/changelog/index.test.ts +156 -0
- package/template/src/feedbax/changelog/index.ts +206 -0
- package/template/src/feedbax/changelog/notion-changelog.test.ts +395 -0
- package/template/src/feedbax/changelog/notion-changelog.ts +400 -0
- package/template/src/feedbax/config/index.test.ts +111 -0
- package/template/src/feedbax/config/index.ts +271 -0
- package/template/src/feedbax/feedback/feedback-storage.ts +43 -0
- package/template/src/feedbax/feedback/index.test.ts +900 -0
- package/template/src/feedbax/feedback/index.ts +942 -0
- package/template/src/feedbax/feedback/notion-comments.test.ts +171 -0
- package/template/src/feedbax/feedback/notion-comments.ts +231 -0
- package/template/src/feedbax/feedback/notion-data-source.test.ts +364 -0
- package/template/src/feedbax/feedback/notion-data-source.ts +458 -0
- package/template/src/feedbax/feedback/notion-feedback.test.ts +998 -0
- package/template/src/feedbax/feedback/notion-feedback.ts +651 -0
- package/template/src/feedbax/feedback/notion-request.ts +74 -0
- package/template/src/feedbax.test-fixture.ts +64 -0
- package/template/src/feedbax.ts +69 -0
- package/template/src/index.css +2138 -0
- package/template/src/lazy-create-post-overlay.tsx +6 -0
- package/template/src/lazy-post-detail-overlay.tsx +10 -0
- package/template/src/masked-post-link.tsx +27 -0
- package/template/src/overlay-post-detail.test.tsx +130 -0
- package/template/src/overlay-post-detail.tsx +92 -0
- package/template/src/portal-comments.test.ts +375 -0
- package/template/src/portal-comments.ts +384 -0
- package/template/src/portal-features.test.tsx +150 -0
- package/template/src/portal-feedback-form.tsx +515 -0
- package/template/src/portal-feedback-runtime.test.ts +32 -0
- package/template/src/portal-feedback-runtime.ts +55 -0
- package/template/src/portal-feedback-server-function.ts +93 -0
- package/template/src/portal-feedback-submission.test.ts +307 -0
- package/template/src/portal-feedback-submission.ts +214 -0
- package/template/src/portal-head.ts +58 -0
- package/template/src/portal-server-entry.ts +64 -0
- package/template/src/portal-voting.test.ts +106 -0
- package/template/src/portal-voting.ts +149 -0
- package/template/src/post-badges.tsx +27 -0
- package/template/src/post-creation-flow.test.tsx +230 -0
- package/template/src/post-creation-flow.tsx +88 -0
- package/template/src/post-detail-overlay.test.tsx +114 -0
- package/template/src/post-detail-overlay.tsx +67 -0
- package/template/src/post-feed-controls.test.tsx +239 -0
- package/template/src/post-feed-controls.tsx +298 -0
- package/template/src/post-presentation.ts +37 -0
- package/template/src/post-queries.test.ts +158 -0
- package/template/src/post-queries.ts +26 -0
- package/template/src/post-results.tsx +165 -0
- package/template/src/public-cache-policy.test.ts +141 -0
- package/template/src/public-cache-policy.ts +119 -0
- package/template/src/public-cache-tags.ts +7 -0
- package/template/src/public-changelog-page.ts +15 -0
- package/template/src/public-changelog-server-function.ts +27 -0
- package/template/src/public-date.ts +11 -0
- package/template/src/public-failure-state.test.tsx +69 -0
- package/template/src/public-feedback-index.tsx +124 -0
- package/template/src/public-feedback-page.test.tsx +290 -0
- package/template/src/public-feedback-page.ts +75 -0
- package/template/src/public-feedback-server-function.ts +22 -0
- package/template/src/public-portal-error.tsx +27 -0
- package/template/src/public-post-detail.tsx +682 -0
- package/template/src/public-post-page.test.tsx +173 -0
- package/template/src/public-post-page.ts +15 -0
- package/template/src/public-post-route.test.tsx +214 -0
- package/template/src/public-post-route.tsx +171 -0
- package/template/src/public-post-server-function.ts +48 -0
- package/template/src/public-post-unavailable.tsx +10 -0
- package/template/src/public-roadmap-page.test.tsx +139 -0
- package/template/src/public-roadmap-page.ts +24 -0
- package/template/src/public-roadmap-route.test.tsx +218 -0
- package/template/src/public-roadmap-route.tsx +50 -0
- package/template/src/public-roadmap-server-function.ts +44 -0
- package/template/src/public-roadmap-view.tsx +378 -0
- package/template/src/public-shell.test.tsx +518 -0
- package/template/src/roadmap-query.ts +21 -0
- package/template/src/root-branding.test.ts +53 -0
- package/template/src/routeTree.gen.ts +131 -0
- package/template/src/routed-post-creation-form.tsx +28 -0
- package/template/src/router.tsx +25 -0
- package/template/src/routes/__root.tsx +116 -0
- package/template/src/routes/changelog.tsx +39 -0
- package/template/src/routes/index.tsx +61 -0
- package/template/src/routes/p.$slug.tsx +16 -0
- package/template/src/routes/roadmap.tsx +11 -0
- package/template/src/routes/submit.tsx +28 -0
- package/template/src/safe-public-failure.ts +45 -0
- package/template/src/server.ts +111 -0
- package/template/src/similar-posts.tsx +45 -0
- package/template/src/submit-route.test.tsx +134 -0
- package/template/src/trusted-feedback-handler.test.ts +425 -0
- package/template/src/trusted-feedback-handler.ts +181 -0
- package/template/src/trusted-feedback-runtime.ts +17 -0
- package/template/src/turnstile-configuration.test.ts +37 -0
- package/template/src/turnstile-configuration.ts +19 -0
- package/template/src/use-media-query.ts +17 -0
- package/template/src/vote-toggle.test.tsx +171 -0
- package/template/src/vote-toggle.tsx +211 -0
- package/template/tsconfig.json +45 -0
- package/template/vite.config.ts +20 -0
- package/template/vitest.config.ts +4 -0
- package/template/vitest.setup.ts +3 -0
- package/template/wrangler.jsonc +35 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts +0 -21
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/template/.env.example +0 -4
- package/template/README.md +0 -7
- package/template/apps/portal/components.json +0 -20
- package/template/apps/portal/package.json +0 -31
- package/template/apps/portal/src/lib/utils.ts +0 -6
- package/template/apps/portal/src/routeTree.gen.ts +0 -59
- package/template/apps/portal/src/router.tsx +0 -10
- package/template/apps/portal/src/routes/__root.tsx +0 -31
- package/template/apps/portal/src/routes/index.tsx +0 -35
- package/template/apps/portal/src/styles.css +0 -91
- package/template/apps/portal/tsconfig.json +0 -15
- package/template/apps/portal/vite.config.ts +0 -13
- package/template/feedbax.config.ts +0 -13
- package/template/feedbax.jsonc +0 -11
- package/template/pnpm-workspace.yaml +0 -3
- package/template/turbo.json +0 -8
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { createFeedbackModule } from "@feedbax/feedback";
|
|
4
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
5
|
+
import { cleanup, render, screen, within } from "@testing-library/react";
|
|
6
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
7
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
PublicPostFeedSkeleton,
|
|
11
|
+
PublicPostIndex,
|
|
12
|
+
} from "./public-feedback-index";
|
|
13
|
+
import { loadPublicPostPage } from "./public-feedback-page";
|
|
14
|
+
import { authorizedDraftPostsQueryKey } from "./authorized-draft-query";
|
|
15
|
+
|
|
16
|
+
afterEach(cleanup);
|
|
17
|
+
|
|
18
|
+
describe("public feedback home page", () => {
|
|
19
|
+
it("loads 25 Published Posts and an opaque next-page cursor", async () => {
|
|
20
|
+
const feedback = createFeedbackModule({
|
|
21
|
+
initialItems: Array.from({ length: 26 }, (_, index) => ({
|
|
22
|
+
id: `feedback-${index + 1}`,
|
|
23
|
+
slug: `feedback-${index + 1}`,
|
|
24
|
+
title: `Feedback ${index + 1}`,
|
|
25
|
+
description: `Description ${index + 1}`,
|
|
26
|
+
type: "Feature Request" as const,
|
|
27
|
+
status: "New" as const,
|
|
28
|
+
published: true,
|
|
29
|
+
createdAt: new Date(Date.UTC(2026, 6, index + 1)),
|
|
30
|
+
updatedAt: new Date(Date.UTC(2026, 6, index + 1)),
|
|
31
|
+
})),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const page = await loadPublicPostPage({
|
|
35
|
+
feedback,
|
|
36
|
+
search: {},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(page.items).toHaveLength(25);
|
|
40
|
+
expect(page.items.map((item) => item.title)).toEqual([
|
|
41
|
+
"Feedback 26",
|
|
42
|
+
...Array.from({ length: 24 }, (_, index) => `Feedback ${25 - index}`),
|
|
43
|
+
]);
|
|
44
|
+
expect(page.nextCursor).toEqual(expect.any(String));
|
|
45
|
+
expect(page.nextCursor).not.toContain("feedback-");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("keeps each public feed ordering deterministic across route pages", async () => {
|
|
49
|
+
const feedback = createFeedbackModule({
|
|
50
|
+
initialItems: Array.from({ length: 27 }, (_, index) => ({
|
|
51
|
+
id: `post-${index}`,
|
|
52
|
+
slug: `post-${index}`,
|
|
53
|
+
title: `Post ${index}`,
|
|
54
|
+
description: "Visible.",
|
|
55
|
+
type: "Feature Request" as const,
|
|
56
|
+
status: "Planned" as const,
|
|
57
|
+
published: true,
|
|
58
|
+
voteCount: index < 2 ? 100 : index,
|
|
59
|
+
createdAt: new Date(Date.UTC(2026, 7, index + 1)),
|
|
60
|
+
updatedAt: new Date(Date.UTC(2026, 7, index + 1)),
|
|
61
|
+
})),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const top = await loadPublicPostPage({
|
|
65
|
+
feedback,
|
|
66
|
+
search: { sort: "top" },
|
|
67
|
+
});
|
|
68
|
+
const trending = await loadPublicPostPage({
|
|
69
|
+
feedback,
|
|
70
|
+
search: { sort: "trending" },
|
|
71
|
+
});
|
|
72
|
+
const newest = await loadPublicPostPage({
|
|
73
|
+
feedback,
|
|
74
|
+
search: { sort: "new" },
|
|
75
|
+
});
|
|
76
|
+
const topNext = await loadPublicPostPage({
|
|
77
|
+
feedback,
|
|
78
|
+
search: { sort: "top", cursor: top.nextCursor },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(top.items.map(({ slug }) => slug)).toEqual(
|
|
82
|
+
trending.items.map(({ slug }) => slug),
|
|
83
|
+
);
|
|
84
|
+
expect(top.items.slice(0, 2).map(({ slug }) => slug)).toEqual([
|
|
85
|
+
"post-1",
|
|
86
|
+
"post-0",
|
|
87
|
+
]);
|
|
88
|
+
expect(newest.items[0]?.slug).toBe("post-26");
|
|
89
|
+
expect([...top.items, ...topNext.items]).toHaveLength(27);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("applies Feedback Type and Feedback Status filters from the page URL", async () => {
|
|
93
|
+
const feedback = createFeedbackModule({
|
|
94
|
+
initialItems: [
|
|
95
|
+
{
|
|
96
|
+
id: "matching-feedback",
|
|
97
|
+
slug: "matching-feedback",
|
|
98
|
+
title: "Matching feedback",
|
|
99
|
+
description: "A planned feature request.",
|
|
100
|
+
type: "Feature Request",
|
|
101
|
+
status: "Planned",
|
|
102
|
+
published: true,
|
|
103
|
+
createdAt: new Date("2026-08-20T10:00:00.000Z"),
|
|
104
|
+
updatedAt: new Date("2026-08-20T10:00:00.000Z"),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "wrong-status",
|
|
108
|
+
slug: "wrong-status",
|
|
109
|
+
title: "New feature request",
|
|
110
|
+
description: "This should be filtered out.",
|
|
111
|
+
type: "Feature Request",
|
|
112
|
+
status: "New",
|
|
113
|
+
published: true,
|
|
114
|
+
createdAt: new Date("2026-08-19T10:00:00.000Z"),
|
|
115
|
+
updatedAt: new Date("2026-08-19T10:00:00.000Z"),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "wrong-type",
|
|
119
|
+
slug: "wrong-type",
|
|
120
|
+
title: "Planned bug report",
|
|
121
|
+
description: "This should also be filtered out.",
|
|
122
|
+
type: "Bug Report",
|
|
123
|
+
status: "Planned",
|
|
124
|
+
published: true,
|
|
125
|
+
createdAt: new Date("2026-08-18T10:00:00.000Z"),
|
|
126
|
+
updatedAt: new Date("2026-08-18T10:00:00.000Z"),
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const page = await loadPublicPostPage({
|
|
132
|
+
feedback,
|
|
133
|
+
search: { types: ["Feature Request"], statuses: ["Planned"] },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(page.items.map((item) => item.title)).toEqual(["Matching feedback"]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("renders the registry empty state with a clear reset action", () => {
|
|
140
|
+
const html = renderIndex(
|
|
141
|
+
<PublicPostIndex
|
|
142
|
+
page={{ items: [], nextCursor: "opaque-next-page" }}
|
|
143
|
+
search={{ types: ["Bug Report"], statuses: ["Reviewing"] }}
|
|
144
|
+
/>,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(html).toContain("No Posts match this view");
|
|
148
|
+
expect(html).toContain("Clear filters");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("renders the feedback workspace described by the visual brief", () => {
|
|
152
|
+
const html = renderIndex(
|
|
153
|
+
<PublicPostIndex page={{ items: [publishedPost] }} search={{}} />,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(html).toContain("Share ideas and vote on what matters.");
|
|
157
|
+
expect(html).toContain("Boards");
|
|
158
|
+
expect(html).toContain("All posts");
|
|
159
|
+
expect(html).toContain("Status");
|
|
160
|
+
expect(html).not.toContain("Trending");
|
|
161
|
+
expect(html).toContain("Top");
|
|
162
|
+
expect(html).toContain("Top");
|
|
163
|
+
expect(html).toContain("New post");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("renders the working Vote toggle on eligible public feed cards", () => {
|
|
167
|
+
renderIndexIntoDocument(
|
|
168
|
+
<PublicPostIndex page={{ items: [publishedPost] }} search={{}} />,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const post = screen.getByRole("link", { name: /Keyboard-first search!/ });
|
|
172
|
+
const metadata = within(post).getByRole("group", {
|
|
173
|
+
name: "Post metadata",
|
|
174
|
+
});
|
|
175
|
+
expect(within(metadata).getByText("Feature Request")).toBeTruthy();
|
|
176
|
+
expect(within(metadata).getByText("Planned")).toBeTruthy();
|
|
177
|
+
expect(
|
|
178
|
+
screen.getByRole("button", { name: "Add Vote, 7 Votes" }),
|
|
179
|
+
).toBeTruthy();
|
|
180
|
+
expect(within(metadata).queryByText(/comment/i)).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("links each Published Post by immutable slug", () => {
|
|
184
|
+
const html = renderIndex(
|
|
185
|
+
<PublicPostIndex
|
|
186
|
+
page={{
|
|
187
|
+
items: [
|
|
188
|
+
{
|
|
189
|
+
slug: "keyboard-first-search",
|
|
190
|
+
title: "Keyboard-first search!",
|
|
191
|
+
description: "Open search without reaching for the mouse.",
|
|
192
|
+
type: "Feature Request",
|
|
193
|
+
status: "Planned",
|
|
194
|
+
createdAt: new Date("2026-08-20T10:00:00.000Z"),
|
|
195
|
+
updatedAt: new Date("2026-08-20T12:00:00.000Z"),
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
}}
|
|
199
|
+
search={{}}
|
|
200
|
+
/>,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(html).toContain('href="/p/keyboard-first-search"');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("keeps visible Posts and offers retry when Load More fails", () => {
|
|
207
|
+
const html = renderIndex(
|
|
208
|
+
<PublicPostIndex
|
|
209
|
+
page={{ items: [publishedPost], nextCursor: "opaque-next-page" }}
|
|
210
|
+
search={{}}
|
|
211
|
+
loadMore={() => undefined}
|
|
212
|
+
loadMoreError
|
|
213
|
+
/>,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(html).toContain("Keyboard-first search!");
|
|
217
|
+
expect(html).toContain("Couldn’t load more Posts");
|
|
218
|
+
expect(html).toContain("Try again");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("renders a contextual feed skeleton while the route is loading", () => {
|
|
222
|
+
const html = renderToStaticMarkup(<PublicPostFeedSkeleton />);
|
|
223
|
+
|
|
224
|
+
expect(html).toContain('aria-label="Loading Posts"');
|
|
225
|
+
expect(html).toContain('aria-busy="true"');
|
|
226
|
+
expect(html.match(/data-post-skeleton-row/g)).toHaveLength(3);
|
|
227
|
+
expect(html).toContain("Loading Posts…");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("pins recognizable matching Draft Posts above Published Posts", () => {
|
|
231
|
+
const draft = {
|
|
232
|
+
id: "draft-id",
|
|
233
|
+
slug: "keyboard-draft",
|
|
234
|
+
title: "Keyboard draft",
|
|
235
|
+
description: "Keyboard access still being drafted.",
|
|
236
|
+
type: "Feature Request" as const,
|
|
237
|
+
status: "New" as const,
|
|
238
|
+
submitter: {},
|
|
239
|
+
createdAt: new Date("2026-08-22T10:00:00.000Z"),
|
|
240
|
+
updatedAt: new Date("2026-08-22T10:00:00.000Z"),
|
|
241
|
+
};
|
|
242
|
+
const html = renderIndex(
|
|
243
|
+
<PublicPostIndex
|
|
244
|
+
page={{ items: [publishedPost] }}
|
|
245
|
+
search={{ search: "keyboard", types: ["Feature Request"] }}
|
|
246
|
+
/>,
|
|
247
|
+
[draft],
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
expect(html.indexOf("Keyboard draft")).toBeLessThan(
|
|
251
|
+
html.indexOf("Keyboard-first search!"),
|
|
252
|
+
);
|
|
253
|
+
expect(html.match(/<ol class="divide-y/g)).toHaveLength(1);
|
|
254
|
+
expect(html).toContain("draft-post-link");
|
|
255
|
+
expect(html).toContain(">Draft</span>");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
function renderIndex(
|
|
260
|
+
element: React.ReactElement,
|
|
261
|
+
drafts: unknown[] = [],
|
|
262
|
+
): string {
|
|
263
|
+
return renderToStaticMarkup(withIndexQueryClient(element, drafts));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function renderIndexIntoDocument(element: React.ReactElement) {
|
|
267
|
+
render(withIndexQueryClient(element));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function withIndexQueryClient(
|
|
271
|
+
element: React.ReactElement,
|
|
272
|
+
drafts: unknown[] = [],
|
|
273
|
+
) {
|
|
274
|
+
const queryClient = new QueryClient();
|
|
275
|
+
queryClient.setQueryData(authorizedDraftPostsQueryKey({}), drafts);
|
|
276
|
+
return (
|
|
277
|
+
<QueryClientProvider client={queryClient}>{element}</QueryClientProvider>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const publishedPost = {
|
|
282
|
+
slug: "keyboard-first-search",
|
|
283
|
+
title: "Keyboard-first search!",
|
|
284
|
+
description: "Open search without reaching for the mouse.",
|
|
285
|
+
type: "Feature Request" as const,
|
|
286
|
+
status: "Planned" as const,
|
|
287
|
+
voteCount: 7,
|
|
288
|
+
createdAt: new Date("2026-08-20T10:00:00.000Z"),
|
|
289
|
+
updatedAt: new Date("2026-08-20T12:00:00.000Z"),
|
|
290
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FeedbackModule,
|
|
3
|
+
PostStatus,
|
|
4
|
+
PostType,
|
|
5
|
+
PublicPostQuery,
|
|
6
|
+
PublicPostPage,
|
|
7
|
+
} from "@feedbax/feedback";
|
|
8
|
+
import type { PortalFeatures } from "@feedbax/config";
|
|
9
|
+
import feedbax from "./feedbax";
|
|
10
|
+
|
|
11
|
+
export const postTypes = [
|
|
12
|
+
"Feature Request",
|
|
13
|
+
"Bug Report",
|
|
14
|
+
"General Feedback",
|
|
15
|
+
] as const satisfies readonly PostType[];
|
|
16
|
+
export const postStatuses = [
|
|
17
|
+
"New",
|
|
18
|
+
"Reviewing",
|
|
19
|
+
"Planned",
|
|
20
|
+
"In Progress",
|
|
21
|
+
"Shipped",
|
|
22
|
+
"Closed",
|
|
23
|
+
] as const satisfies readonly PostStatus[];
|
|
24
|
+
export const postSorts = ["top", "new"] as const;
|
|
25
|
+
|
|
26
|
+
export async function loadPublicPostPage({
|
|
27
|
+
feedback,
|
|
28
|
+
search,
|
|
29
|
+
}: {
|
|
30
|
+
feedback: Pick<FeedbackModule, "listPublicPosts">;
|
|
31
|
+
search: Record<string, unknown>;
|
|
32
|
+
}): Promise<PublicPostPage> {
|
|
33
|
+
return feedback.listPublicPosts(publicPostSearch(search));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function publicPostSearch(
|
|
37
|
+
search: Record<string, unknown>,
|
|
38
|
+
features: PortalFeatures = feedbax.features,
|
|
39
|
+
): PublicPostQuery & { pageSize?: string; schema?: string } {
|
|
40
|
+
const cursor = text(search.cursor);
|
|
41
|
+
const query = text(search.search);
|
|
42
|
+
const sortValue = text(search.sort);
|
|
43
|
+
const types = values(search.types).filter((value): value is PostType =>
|
|
44
|
+
postTypes.includes(value as PostType),
|
|
45
|
+
);
|
|
46
|
+
const statuses = values(search.statuses).filter(
|
|
47
|
+
(value): value is PostStatus => postStatuses.includes(value as PostStatus),
|
|
48
|
+
);
|
|
49
|
+
return {
|
|
50
|
+
...(text(search.pageSize) ? { pageSize: text(search.pageSize) } : {}),
|
|
51
|
+
...(text(search.schema) ? { schema: text(search.schema) } : {}),
|
|
52
|
+
...(cursor ? { cursor } : {}),
|
|
53
|
+
...(query ? { search: query } : {}),
|
|
54
|
+
sort: !features.voting
|
|
55
|
+
? "new"
|
|
56
|
+
: sortValue === "trending"
|
|
57
|
+
? "top"
|
|
58
|
+
: postSorts.includes(sortValue as (typeof postSorts)[number])
|
|
59
|
+
? (sortValue as PublicPostQuery["sort"])
|
|
60
|
+
: "top",
|
|
61
|
+
...(types.length ? { types } : {}),
|
|
62
|
+
...(statuses.length ? { statuses } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function text(value: unknown) {
|
|
67
|
+
return typeof value === "string" && value ? value : undefined;
|
|
68
|
+
}
|
|
69
|
+
function values(value: unknown): string[] {
|
|
70
|
+
return Array.isArray(value)
|
|
71
|
+
? value.filter((entry): entry is string => typeof entry === "string")
|
|
72
|
+
: typeof value === "string"
|
|
73
|
+
? value.split(",").filter(Boolean)
|
|
74
|
+
: [];
|
|
75
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
|
|
3
|
+
import { createConfiguredFeedbackModule } from "./feedback-runtime";
|
|
4
|
+
import { loadPublicPostPage, publicPostSearch } from "./public-feedback-page";
|
|
5
|
+
import { runSafePublicRead } from "./safe-public-failure";
|
|
6
|
+
|
|
7
|
+
export const getPublicPostPage = createServerFn({ method: "GET" })
|
|
8
|
+
.validator((input: unknown) =>
|
|
9
|
+
publicPostSearch(
|
|
10
|
+
typeof input === "object" && input !== null
|
|
11
|
+
? (input as Record<string, unknown>)
|
|
12
|
+
: {},
|
|
13
|
+
),
|
|
14
|
+
)
|
|
15
|
+
.handler(({ data }) =>
|
|
16
|
+
runSafePublicRead(() =>
|
|
17
|
+
loadPublicPostPage({
|
|
18
|
+
feedback: createConfiguredFeedbackModule(),
|
|
19
|
+
search: data,
|
|
20
|
+
}),
|
|
21
|
+
),
|
|
22
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Button } from "@feedbax/ui/components/button";
|
|
2
|
+
|
|
3
|
+
export function PublicPortalError({
|
|
4
|
+
reset,
|
|
5
|
+
}: {
|
|
6
|
+
error: Error;
|
|
7
|
+
reset: () => void;
|
|
8
|
+
}) {
|
|
9
|
+
return (
|
|
10
|
+
<main className="public-error">
|
|
11
|
+
<p className="feedback-eyebrow">Temporary interruption</p>
|
|
12
|
+
<h1>Posts are temporarily unavailable.</h1>
|
|
13
|
+
<p>
|
|
14
|
+
We couldn’t reach the Post service after several attempts. Your private
|
|
15
|
+
information has not been displayed.
|
|
16
|
+
</p>
|
|
17
|
+
<div className="public-error-actions">
|
|
18
|
+
<Button type="button" onClick={reset}>
|
|
19
|
+
Try again
|
|
20
|
+
</Button>
|
|
21
|
+
<Button render={<a href="/" />} variant="outline">
|
|
22
|
+
Return to the portal
|
|
23
|
+
</Button>
|
|
24
|
+
</div>
|
|
25
|
+
</main>
|
|
26
|
+
);
|
|
27
|
+
}
|