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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PublicPostQuery, PublicPostPage } from "@feedbax/feedback";
|
|
2
2
|
import type { PortalFeatures } from "@feedbax/config";
|
|
3
|
-
import
|
|
3
|
+
import Loader from "./components/loader";
|
|
4
4
|
import { useAuthorizedDraftPosts } from "./draft-post-list";
|
|
5
5
|
import {
|
|
6
6
|
BoardNavigation,
|
|
@@ -39,11 +39,11 @@ export function PublicPostIndex({
|
|
|
39
39
|
<section aria-labelledby="feedback-heading">
|
|
40
40
|
<h1
|
|
41
41
|
id="feedback-heading"
|
|
42
|
-
className="text-
|
|
42
|
+
className="text-2xl font-semibold tracking-tight"
|
|
43
43
|
>
|
|
44
44
|
Feedback
|
|
45
45
|
</h1>
|
|
46
|
-
<p className="mt-2 text-
|
|
46
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
47
47
|
{features.voting
|
|
48
48
|
? "Share ideas and vote on what matters."
|
|
49
49
|
: "Share ideas and follow public progress."}
|
|
@@ -58,10 +58,11 @@ export function PublicPostIndex({
|
|
|
58
58
|
features={features}
|
|
59
59
|
/>
|
|
60
60
|
</div>
|
|
61
|
-
<section
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
<section
|
|
62
|
+
className="mt-8"
|
|
63
|
+
aria-live="polite"
|
|
64
|
+
aria-label="Feedback posts"
|
|
65
|
+
>
|
|
65
66
|
<PostResults
|
|
66
67
|
drafts={drafts}
|
|
67
68
|
initialItems={page.items}
|
|
@@ -89,36 +90,5 @@ export function PublicPostIndex({
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
export function PublicPostFeedSkeleton() {
|
|
92
|
-
return
|
|
93
|
-
<main
|
|
94
|
-
className="feedback-index feedback-feed-skeleton"
|
|
95
|
-
aria-label="Loading Posts"
|
|
96
|
-
aria-busy="true"
|
|
97
|
-
>
|
|
98
|
-
<section className="feedback-intro" aria-hidden="true">
|
|
99
|
-
<Skeleton className="feedback-skeleton-heading" />
|
|
100
|
-
<Skeleton className="feedback-skeleton-deck" />
|
|
101
|
-
</section>
|
|
102
|
-
<div className="post-controls" aria-hidden="true">
|
|
103
|
-
<Skeleton className="feedback-skeleton-control" />
|
|
104
|
-
<Skeleton className="feedback-skeleton-control" />
|
|
105
|
-
<Skeleton className="feedback-skeleton-control" />
|
|
106
|
-
</div>
|
|
107
|
-
<section className="feedback-results" aria-hidden="true">
|
|
108
|
-
<Skeleton className="feedback-skeleton-section-heading" />
|
|
109
|
-
<ol className="feedback-list">
|
|
110
|
-
{Array.from({ length: 3 }, (_, index) => (
|
|
111
|
-
<li key={index} data-post-skeleton-row>
|
|
112
|
-
<div className="feedback-skeleton-row">
|
|
113
|
-
<Skeleton className="feedback-skeleton-meta" />
|
|
114
|
-
<Skeleton className="feedback-skeleton-title" />
|
|
115
|
-
<Skeleton className="feedback-skeleton-copy" />
|
|
116
|
-
</div>
|
|
117
|
-
</li>
|
|
118
|
-
))}
|
|
119
|
-
</ol>
|
|
120
|
-
</section>
|
|
121
|
-
<span className="sr-only">Loading Posts…</span>
|
|
122
|
-
</main>
|
|
123
|
-
);
|
|
93
|
+
return <Loader label="Loading Posts" />;
|
|
124
94
|
}
|
|
@@ -16,6 +16,71 @@ import { authorizedDraftPostsQueryKey } from "./authorized-draft-query";
|
|
|
16
16
|
afterEach(cleanup);
|
|
17
17
|
|
|
18
18
|
describe("public feedback home page", () => {
|
|
19
|
+
it("keeps Shipped Posts out of default Top before pagination while preserving explicit discovery", async () => {
|
|
20
|
+
const feedback = createFeedbackModule({
|
|
21
|
+
initialItems: [
|
|
22
|
+
...Array.from({ length: 26 }, (_, index) => ({
|
|
23
|
+
id: `active-${index}`,
|
|
24
|
+
slug: `active-${index}`,
|
|
25
|
+
title: `Active ${index}`,
|
|
26
|
+
description: "Feedback",
|
|
27
|
+
type: "Feature Request" as const,
|
|
28
|
+
status: "Planned" as const,
|
|
29
|
+
published: true,
|
|
30
|
+
voteCount: index,
|
|
31
|
+
createdAt: new Date(index * 1000),
|
|
32
|
+
updatedAt: new Date(index * 1000),
|
|
33
|
+
})),
|
|
34
|
+
...Array.from({ length: 27 }, (_, index) => ({
|
|
35
|
+
id: `shipped-${index}`,
|
|
36
|
+
slug: `shipped-${index}`,
|
|
37
|
+
title: `Shipped ${index}`,
|
|
38
|
+
description: "Feedback",
|
|
39
|
+
type: "Feature Request" as const,
|
|
40
|
+
status: "Shipped" as const,
|
|
41
|
+
published: true,
|
|
42
|
+
voteCount: 1000,
|
|
43
|
+
createdAt: new Date(100000 + index * 1000),
|
|
44
|
+
updatedAt: new Date(100000 + index * 1000),
|
|
45
|
+
})),
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
const first = await loadPublicPostPage({
|
|
49
|
+
feedback,
|
|
50
|
+
search: { sort: "top" },
|
|
51
|
+
});
|
|
52
|
+
expect(first.items).toHaveLength(25);
|
|
53
|
+
expect(first.items.every((post) => post.status !== "Shipped")).toBe(true);
|
|
54
|
+
const second = await loadPublicPostPage({
|
|
55
|
+
feedback,
|
|
56
|
+
search: { sort: "top", cursor: first.nextCursor },
|
|
57
|
+
});
|
|
58
|
+
expect(second.items).toHaveLength(1);
|
|
59
|
+
const legacy = await loadPublicPostPage({
|
|
60
|
+
feedback,
|
|
61
|
+
search: { sort: "trending" },
|
|
62
|
+
});
|
|
63
|
+
expect(legacy.items.map((post) => post.slug)).toEqual(
|
|
64
|
+
first.items.map((post) => post.slug),
|
|
65
|
+
);
|
|
66
|
+
const shipped = await loadPublicPostPage({
|
|
67
|
+
feedback,
|
|
68
|
+
search: { sort: "top", statuses: ["Shipped"] },
|
|
69
|
+
});
|
|
70
|
+
expect(shipped.items).toHaveLength(25);
|
|
71
|
+
expect(shipped.items.every((post) => post.status === "Shipped")).toBe(true);
|
|
72
|
+
const search = await loadPublicPostPage({
|
|
73
|
+
feedback,
|
|
74
|
+
search: { sort: "top", search: "Shipped" },
|
|
75
|
+
});
|
|
76
|
+
expect(search.items[0]?.status).toBe("Shipped");
|
|
77
|
+
const newest = await loadPublicPostPage({
|
|
78
|
+
feedback,
|
|
79
|
+
search: { sort: "new" },
|
|
80
|
+
});
|
|
81
|
+
expect(newest.items[0]?.status).toBe("Shipped");
|
|
82
|
+
});
|
|
83
|
+
|
|
19
84
|
it("loads 25 Published Posts and an opaque next-page cursor", async () => {
|
|
20
85
|
const feedback = createFeedbackModule({
|
|
21
86
|
initialItems: Array.from({ length: 26 }, (_, index) => ({
|
|
@@ -30,7 +30,18 @@ export async function loadPublicPostPage({
|
|
|
30
30
|
feedback: Pick<FeedbackModule, "listPublicPosts">;
|
|
31
31
|
search: Record<string, unknown>;
|
|
32
32
|
}): Promise<PublicPostPage> {
|
|
33
|
-
|
|
33
|
+
const query = publicPostSearch(search);
|
|
34
|
+
const defaultTop =
|
|
35
|
+
query.sort !== "new" &&
|
|
36
|
+
!query.search &&
|
|
37
|
+
!query.status &&
|
|
38
|
+
!query.statuses?.length;
|
|
39
|
+
return feedback.listPublicPosts({
|
|
40
|
+
...query,
|
|
41
|
+
...(defaultTop
|
|
42
|
+
? { statuses: postStatuses.filter((status) => status !== "Shipped") }
|
|
43
|
+
: {}),
|
|
44
|
+
});
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
export function publicPostSearch(
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Skeleton } from "@feedbax/ui/components/skeleton";
|
|
2
|
+
|
|
3
|
+
export function PublicPostDetailSkeleton({
|
|
4
|
+
display = "page",
|
|
5
|
+
}: { display?: "overlay" | "page" } = {}) {
|
|
6
|
+
return (
|
|
7
|
+
<main
|
|
8
|
+
className="feedback-detail public-post-detail"
|
|
9
|
+
data-display={display}
|
|
10
|
+
aria-label="Loading Post details"
|
|
11
|
+
aria-busy="true"
|
|
12
|
+
>
|
|
13
|
+
{display === "page" ? (
|
|
14
|
+
<Skeleton aria-hidden="true" className="h-4 w-32" />
|
|
15
|
+
) : null}
|
|
16
|
+
<div aria-hidden="true" className="post-detail-layout">
|
|
17
|
+
<div className="post-detail-primary space-y-7">
|
|
18
|
+
<div className="flex gap-2">
|
|
19
|
+
<Skeleton className="h-5 w-28" />
|
|
20
|
+
<Skeleton className="h-5 w-24" />
|
|
21
|
+
</div>
|
|
22
|
+
<div className="space-y-3">
|
|
23
|
+
<Skeleton
|
|
24
|
+
className={display === "overlay" ? "h-5 w-4/5" : "h-7 w-4/5"}
|
|
25
|
+
/>
|
|
26
|
+
<Skeleton
|
|
27
|
+
className={display === "overlay" ? "h-5 w-3/5" : "h-7 w-3/5"}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
<div className="space-y-3">
|
|
31
|
+
<Skeleton className="h-4 w-full" />
|
|
32
|
+
<Skeleton className="h-4 w-full" />
|
|
33
|
+
<Skeleton className="h-4 w-2/3" />
|
|
34
|
+
</div>
|
|
35
|
+
<Skeleton className="h-10 w-16" />
|
|
36
|
+
<div className="space-y-5 border-t pt-6">
|
|
37
|
+
<Skeleton className="h-4 w-24" />
|
|
38
|
+
<Skeleton className="h-24 w-full" />
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div className="post-detail-sidebar space-y-7">
|
|
42
|
+
<Skeleton className="h-4 w-20" />
|
|
43
|
+
{[0, 1, 2, 3].map((row) => (
|
|
44
|
+
<div className="flex justify-between gap-4" key={row}>
|
|
45
|
+
<Skeleton className="h-3 w-16" />
|
|
46
|
+
<Skeleton className="h-3 w-24" />
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<span className="sr-only">Loading Post details…</span>
|
|
52
|
+
</main>
|
|
53
|
+
);
|
|
54
|
+
}
|