create-feedbax 0.2.0 → 0.2.2
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/README.md +17 -5
- package/dist/cli.js +2610 -291
- package/dist/index.js +2375 -200
- package/package.json +7 -7
- package/template/.feedbax-template.sha256 +1 -1
- package/template/.gitignore.template +1 -0
- package/template/.npmignore.template +1 -0
- package/template/.nvmrc +1 -0
- package/template/AGENTS.md +13 -0
- package/template/CONNECTED.md +418 -0
- package/template/README.md +97 -0
- package/template/components.json +1 -1
- package/template/eslint.config.mjs +23 -0
- package/template/feedbax-vite.ts +79 -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/migrations/0005_github.sql +4 -0
- package/template/migrations/0006_integration.sql +3 -0
- package/template/migrations/0007_ownership.sql +2 -0
- package/template/migrations/0008_integration_recovery.sql +5 -0
- package/template/migrations/0009_observed_history.sql +3 -0
- package/template/package.json +39 -34
- package/template/src/authorized-draft-post.tsx +1 -0
- package/template/src/changelog-page.tsx +63 -54
- package/template/src/changelog-runtime.ts +2 -1
- package/template/src/cloudflare-workers.d.ts +9 -0
- package/template/src/comment-creation.test.tsx +95 -7
- 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 +135 -0
- package/template/src/components/device-profile-provider.tsx +41 -26
- package/template/src/components/follow-post-button.tsx +78 -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/post-history.tsx +101 -0
- package/template/src/components/profile-menu.tsx +37 -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/configured-feedback.test.ts +82 -0
- package/template/src/configured-feedback.ts +61 -0
- package/template/src/connected-identity.test.ts +148 -0
- package/template/src/connected-identity.ts +127 -0
- package/template/src/connected-participation.test.tsx +67 -0
- package/template/src/connected-runtime.test.ts +244 -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 +3 -0
- package/template/src/feedback-runtime.ts +123 -13
- package/template/src/feedbax/changelog/index.test.ts +35 -0
- package/template/src/feedbax/changelog/index.ts +5 -1
- package/template/src/feedbax/changelog/notion-changelog.test.ts +1 -1
- package/template/src/feedbax/changelog/notion-changelog.ts +1 -1
- package/template/src/feedbax/config/index.test.ts +73 -1
- package/template/src/feedbax/config/index.ts +205 -9
- package/template/src/feedbax/feedback/comment-storage.ts +38 -0
- package/template/src/feedbax/feedback/connected-feedback.ts +235 -0
- package/template/src/feedbax/feedback/connected.test.ts +272 -0
- package/template/src/feedbax/feedback/connected.ts +225 -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/feedback-storage.ts +1 -0
- package/template/src/feedbax/feedback/github-comments.ts +390 -0
- package/template/src/feedbax/feedback/github-feedback.test.ts +1138 -0
- package/template/src/feedbax/feedback/github-feedback.ts +581 -0
- package/template/src/feedbax/feedback/github-request.ts +120 -0
- package/template/src/feedbax/feedback/github-service-client.ts +56 -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 +114 -0
- package/template/src/feedbax/feedback/images.test.ts +223 -0
- package/template/src/feedbax/feedback/images.ts +350 -0
- package/template/src/feedbax/feedback/index.ts +226 -16
- package/template/src/feedbax/feedback/integration-auth.ts +138 -0
- package/template/src/feedbax/feedback/notion-comments.ts +2 -35
- package/template/src/feedbax/feedback/notion-image-review.ts +55 -0
- package/template/src/feedbax/feedback/observed-history.test.ts +150 -0
- package/template/src/feedbax/feedback/observed-history.ts +219 -0
- package/template/src/feedbax/feedback/ownership.ts +170 -0
- package/template/src/feedbax/feedback/participation.test.ts +653 -0
- package/template/src/feedbax/feedback/participation.ts +502 -0
- package/template/src/history-runtime.test.ts +80 -0
- package/template/src/history-runtime.ts +37 -0
- package/template/src/images-runtime.test.ts +133 -0
- package/template/src/images-runtime.ts +125 -0
- package/template/src/index.css +103 -440
- package/template/src/integration-runtime.test.ts +365 -0
- package/template/src/integration-runtime.ts +289 -0
- package/template/src/lazy-post-detail-overlay.tsx +13 -2
- package/template/src/my-feedback-page.test.tsx +152 -0
- package/template/src/my-feedback-page.tsx +398 -0
- package/template/src/my-feedback-runtime.test.ts +93 -0
- package/template/src/my-feedback-runtime.ts +154 -0
- 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 +35 -5
- package/template/src/portal-feedback-form.tsx +29 -6
- 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 +19 -0
- package/template/src/portal-server-entry.ts +13 -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-portal-error.tsx +1 -1
- package/template/src/public-post-detail-skeleton.tsx +54 -0
- package/template/src/public-post-detail.tsx +437 -271
- package/template/src/public-post-route.test.tsx +39 -3
- package/template/src/public-post-route.tsx +15 -28
- package/template/src/public-release-links.test.ts +120 -0
- package/template/src/public-release-links.ts +130 -0
- 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 +70 -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/my-feedback.tsx +16 -0
- package/template/src/routes/submit.tsx +1 -1
- package/template/src/safe-public-failure.ts +3 -1
- package/template/src/server.ts +87 -2
- package/template/src/submission-intent.ts +26 -0
- package/template/src/trusted-feedback-handler.test.ts +1 -1
- package/template/src/vote-toggle.tsx +42 -9
- package/template/vite.config.ts +2 -0
|
@@ -1,10 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Skeleton } from "@feedbax/ui/components/skeleton";
|
|
2
2
|
|
|
3
|
-
export default function Loader(
|
|
3
|
+
export default function Loader({
|
|
4
|
+
label = "Loading content",
|
|
5
|
+
}: { label?: string } = {}) {
|
|
4
6
|
return (
|
|
5
|
-
<div
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
<div
|
|
8
|
+
className="portal-loading"
|
|
9
|
+
role="status"
|
|
10
|
+
aria-label={label}
|
|
11
|
+
aria-busy="true"
|
|
12
|
+
>
|
|
13
|
+
<span className="sr-only">{label}…</span>
|
|
14
|
+
<div
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
className="mx-auto grid w-full max-w-7xl gap-10 px-6 py-10 md:grid-cols-[1fr_16rem] md:px-10"
|
|
17
|
+
>
|
|
18
|
+
<div className="space-y-8">
|
|
19
|
+
<div className="space-y-3">
|
|
20
|
+
<Skeleton className="h-8 w-40" />
|
|
21
|
+
<Skeleton className="h-4 w-64 max-w-full" />
|
|
22
|
+
</div>
|
|
23
|
+
<div className="flex gap-3">
|
|
24
|
+
<Skeleton className="h-10 w-24" />
|
|
25
|
+
<Skeleton className="h-10 w-24" />
|
|
26
|
+
</div>
|
|
27
|
+
<div className="divide-y border-y">
|
|
28
|
+
{[0, 1, 2].map((row) => (
|
|
29
|
+
<div
|
|
30
|
+
key={row}
|
|
31
|
+
data-post-skeleton-row
|
|
32
|
+
className="flex items-start gap-5 py-7"
|
|
33
|
+
>
|
|
34
|
+
<Skeleton className="size-10 shrink-0" />
|
|
35
|
+
<div className="flex-1 space-y-3">
|
|
36
|
+
<Skeleton className="h-4 w-3/5" />
|
|
37
|
+
<Skeleton className="h-3 w-4/5" />
|
|
38
|
+
<Skeleton className="h-3 w-24" />
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="hidden space-y-6 pt-3 md:block">
|
|
45
|
+
<Skeleton className="h-4 w-20" />
|
|
46
|
+
<Skeleton className="h-40 w-full" />
|
|
47
|
+
<Skeleton className="h-4 w-20" />
|
|
48
|
+
<Skeleton className="h-28 w-full" />
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
8
51
|
</div>
|
|
9
52
|
);
|
|
10
53
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Bell, Loader2 } from "lucide-react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
useInfiniteQuery,
|
|
5
|
+
useQuery,
|
|
6
|
+
useQueryClient,
|
|
7
|
+
} from "@tanstack/react-query";
|
|
8
|
+
import { Button } from "@feedbax/ui/components/button";
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
DialogTrigger,
|
|
16
|
+
} from "@feedbax/ui/components/dialog";
|
|
17
|
+
import { Label } from "@feedbax/ui/components/label";
|
|
18
|
+
import { Switch } from "@feedbax/ui/components/switch";
|
|
19
|
+
import { useConnectedIdentity } from "./connected-identity-provider";
|
|
20
|
+
import {
|
|
21
|
+
notificationQuery,
|
|
22
|
+
participationRequest,
|
|
23
|
+
preferencesQuery,
|
|
24
|
+
type EmailPreferences,
|
|
25
|
+
} from "../participation-client";
|
|
26
|
+
import { formatPublicDate } from "../public-date";
|
|
27
|
+
|
|
28
|
+
export function NotificationInbox() {
|
|
29
|
+
const identity = useConnectedIdentity();
|
|
30
|
+
if (!identity.enabled || !identity.displayName) return null;
|
|
31
|
+
return <ConnectedNotificationInbox />;
|
|
32
|
+
}
|
|
33
|
+
function ConnectedNotificationInbox() {
|
|
34
|
+
const identity = useConnectedIdentity();
|
|
35
|
+
const [open, setOpen] = useState(false);
|
|
36
|
+
const [error, setError] = useState<string>();
|
|
37
|
+
const [pending, setPending] = useState(false);
|
|
38
|
+
const queryClient = useQueryClient();
|
|
39
|
+
const enabled = identity.enabled && Boolean(identity.displayName);
|
|
40
|
+
const inbox = useInfiniteQuery({
|
|
41
|
+
...notificationQuery,
|
|
42
|
+
enabled,
|
|
43
|
+
refetchInterval: enabled ? 60000 : false,
|
|
44
|
+
});
|
|
45
|
+
const preferences = useQuery({
|
|
46
|
+
...preferencesQuery(),
|
|
47
|
+
enabled: enabled && open,
|
|
48
|
+
});
|
|
49
|
+
if (!enabled) return null;
|
|
50
|
+
const items = inbox.data?.pages.flatMap((page) => page.items) ?? [];
|
|
51
|
+
const unread = inbox.data?.pages[0]?.unreadCount ?? 0;
|
|
52
|
+
async function read(id?: string) {
|
|
53
|
+
setError(undefined);
|
|
54
|
+
try {
|
|
55
|
+
await participationRequest("notifications", id ? { id } : {});
|
|
56
|
+
await queryClient.invalidateQueries({
|
|
57
|
+
queryKey: notificationQuery.queryKey,
|
|
58
|
+
});
|
|
59
|
+
} catch (cause) {
|
|
60
|
+
setError(
|
|
61
|
+
cause instanceof Error
|
|
62
|
+
? cause.message
|
|
63
|
+
: "Couldn’t mark updates as read.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function email(enabled: boolean) {
|
|
68
|
+
setError(undefined);
|
|
69
|
+
setPending(true);
|
|
70
|
+
try {
|
|
71
|
+
const next = await participationRequest<EmailPreferences>("preferences", {
|
|
72
|
+
emailEnabled: enabled,
|
|
73
|
+
});
|
|
74
|
+
queryClient.setQueryData(preferencesQuery().queryKey, next);
|
|
75
|
+
} catch (cause) {
|
|
76
|
+
setError(
|
|
77
|
+
cause instanceof Error
|
|
78
|
+
? cause.message
|
|
79
|
+
: "Couldn’t save email preferences.",
|
|
80
|
+
);
|
|
81
|
+
} finally {
|
|
82
|
+
setPending(false);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return (
|
|
86
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
87
|
+
<DialogTrigger
|
|
88
|
+
render={
|
|
89
|
+
<Button
|
|
90
|
+
variant="ghost"
|
|
91
|
+
size="icon"
|
|
92
|
+
aria-label={
|
|
93
|
+
unread ? `Notifications, ${unread} unread` : "Notifications"
|
|
94
|
+
}
|
|
95
|
+
className="relative"
|
|
96
|
+
/>
|
|
97
|
+
}
|
|
98
|
+
>
|
|
99
|
+
<Bell aria-hidden="true" />
|
|
100
|
+
{unread ? (
|
|
101
|
+
<span className="absolute top-1 right-1 size-2 rounded-full bg-primary" />
|
|
102
|
+
) : null}
|
|
103
|
+
</DialogTrigger>
|
|
104
|
+
<DialogContent className="max-h-[85svh] gap-0 overflow-hidden p-0 sm:max-w-lg">
|
|
105
|
+
<DialogHeader className="border-b p-5">
|
|
106
|
+
<DialogTitle className="text-lg">Notifications</DialogTitle>
|
|
107
|
+
<DialogDescription className="text-sm">
|
|
108
|
+
Updates on Posts you follow.
|
|
109
|
+
</DialogDescription>
|
|
110
|
+
</DialogHeader>
|
|
111
|
+
<div className="flex items-center justify-between gap-3 border-b px-5 py-3">
|
|
112
|
+
<span className="text-sm text-muted-foreground">{unread} unread</span>
|
|
113
|
+
<Button
|
|
114
|
+
size="sm"
|
|
115
|
+
variant="ghost"
|
|
116
|
+
disabled={!unread}
|
|
117
|
+
onClick={() => void read()}
|
|
118
|
+
>
|
|
119
|
+
Mark all read
|
|
120
|
+
</Button>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="max-h-[45svh] overflow-y-auto">
|
|
123
|
+
{inbox.isPending ? (
|
|
124
|
+
<p role="status" className="flex items-center gap-2 p-5 text-sm">
|
|
125
|
+
<Loader2 className="size-4 animate-spin" />
|
|
126
|
+
Loading updates…
|
|
127
|
+
</p>
|
|
128
|
+
) : inbox.isError ? (
|
|
129
|
+
<div className="space-y-3 p-5">
|
|
130
|
+
<p className="text-sm">Couldn’t load updates.</p>
|
|
131
|
+
<Button
|
|
132
|
+
size="sm"
|
|
133
|
+
variant="outline"
|
|
134
|
+
onClick={() => void inbox.refetch()}
|
|
135
|
+
>
|
|
136
|
+
Try again
|
|
137
|
+
</Button>
|
|
138
|
+
</div>
|
|
139
|
+
) : items.length ? (
|
|
140
|
+
<ul className="divide-y">
|
|
141
|
+
{items.map((item) => (
|
|
142
|
+
<li
|
|
143
|
+
key={item.id}
|
|
144
|
+
className={item.read ? "p-5" : "bg-muted/40 p-5"}
|
|
145
|
+
>
|
|
146
|
+
<a
|
|
147
|
+
href={`/p/${encodeURIComponent(item.slug)}`}
|
|
148
|
+
className="block space-y-1 text-sm hover:underline"
|
|
149
|
+
onClick={() => {
|
|
150
|
+
void read(item.id);
|
|
151
|
+
setOpen(false);
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<span className="block font-medium">{item.title}</span>
|
|
155
|
+
<span className="block text-muted-foreground">
|
|
156
|
+
{item.message}
|
|
157
|
+
</span>
|
|
158
|
+
<time
|
|
159
|
+
className="block text-xs text-muted-foreground"
|
|
160
|
+
dateTime={new Date(item.createdAt).toISOString()}
|
|
161
|
+
>
|
|
162
|
+
{formatPublicDate(new Date(item.createdAt))}
|
|
163
|
+
</time>
|
|
164
|
+
</a>
|
|
165
|
+
{!item.read ? (
|
|
166
|
+
<Button
|
|
167
|
+
size="sm"
|
|
168
|
+
variant="ghost"
|
|
169
|
+
className="mt-2"
|
|
170
|
+
onClick={() => void read(item.id)}
|
|
171
|
+
>
|
|
172
|
+
Mark read
|
|
173
|
+
</Button>
|
|
174
|
+
) : null}
|
|
175
|
+
</li>
|
|
176
|
+
))}
|
|
177
|
+
</ul>
|
|
178
|
+
) : (
|
|
179
|
+
<p className="p-5 text-sm text-muted-foreground">
|
|
180
|
+
Follow a Post to receive updates here.
|
|
181
|
+
</p>
|
|
182
|
+
)}
|
|
183
|
+
{inbox.hasNextPage ? (
|
|
184
|
+
<Button
|
|
185
|
+
variant="outline"
|
|
186
|
+
size="sm"
|
|
187
|
+
className="m-5"
|
|
188
|
+
disabled={inbox.isFetchingNextPage}
|
|
189
|
+
onClick={() => void inbox.fetchNextPage()}
|
|
190
|
+
>
|
|
191
|
+
{inbox.isFetchingNextPage ? "Loading…" : "View more"}
|
|
192
|
+
</Button>
|
|
193
|
+
) : null}
|
|
194
|
+
</div>
|
|
195
|
+
<div className="space-y-2 border-t p-5">
|
|
196
|
+
<div className="flex items-center justify-between gap-4">
|
|
197
|
+
<Label htmlFor="notification-email">Email notifications</Label>
|
|
198
|
+
<Switch
|
|
199
|
+
id="notification-email"
|
|
200
|
+
checked={preferences.data?.emailEnabled ?? false}
|
|
201
|
+
disabled={pending || !preferences.data?.emailAvailable}
|
|
202
|
+
onCheckedChange={email}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
{preferences.isError ? (
|
|
206
|
+
<Button
|
|
207
|
+
size="sm"
|
|
208
|
+
variant="ghost"
|
|
209
|
+
onClick={() => void preferences.refetch()}
|
|
210
|
+
>
|
|
211
|
+
Retry email preferences
|
|
212
|
+
</Button>
|
|
213
|
+
) : !preferences.data?.emailAvailable && !preferences.isPending ? (
|
|
214
|
+
<p className="text-xs text-muted-foreground">
|
|
215
|
+
Email requires a verified address from your product account and a
|
|
216
|
+
configured sender.
|
|
217
|
+
</p>
|
|
218
|
+
) : (
|
|
219
|
+
<p className="text-xs text-muted-foreground">
|
|
220
|
+
You can unsubscribe at any time.
|
|
221
|
+
</p>
|
|
222
|
+
)}
|
|
223
|
+
{error ? (
|
|
224
|
+
<p role="alert" className="text-sm text-destructive">
|
|
225
|
+
{error}
|
|
226
|
+
</p>
|
|
227
|
+
) : null}
|
|
228
|
+
</div>
|
|
229
|
+
</DialogContent>
|
|
230
|
+
</Dialog>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function PortalFooter() {
|
|
2
|
+
return (
|
|
3
|
+
<footer className="mt-auto flex justify-end border-t px-6 py-4 text-xs text-muted-foreground lg:px-10">
|
|
4
|
+
<a
|
|
5
|
+
href="https://github.com/jessequartey/feedbax"
|
|
6
|
+
target="_blank"
|
|
7
|
+
rel="noopener noreferrer"
|
|
8
|
+
className="transition-colors hover:text-foreground"
|
|
9
|
+
>
|
|
10
|
+
Powered by Feedbax
|
|
11
|
+
</a>
|
|
12
|
+
</footer>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ObservedHistoryPage } from "@feedbax/feedback";
|
|
2
|
+
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { Button } from "@feedbax/ui/components/button";
|
|
4
|
+
import { Skeleton } from "@feedbax/ui/components/skeleton";
|
|
5
|
+
import { formatPublicDate } from "../public-date";
|
|
6
|
+
|
|
7
|
+
export function PostHistory({ slug }: { slug: string }) {
|
|
8
|
+
const client = useQueryClient();
|
|
9
|
+
const clearPublicPost = () => {
|
|
10
|
+
client.setQueryData(["public-post", slug], null);
|
|
11
|
+
client.setQueryData(["public-post-comments", slug], {
|
|
12
|
+
pages: [{ items: [] }],
|
|
13
|
+
pageParams: [undefined],
|
|
14
|
+
});
|
|
15
|
+
void client.invalidateQueries({ queryKey: ["public-post-roadmap"] });
|
|
16
|
+
};
|
|
17
|
+
const query = useInfiniteQuery({
|
|
18
|
+
queryKey: ["public-post-history", slug],
|
|
19
|
+
initialPageParam: undefined as string | undefined,
|
|
20
|
+
queryFn: async ({ pageParam }) => {
|
|
21
|
+
const params = new URLSearchParams({
|
|
22
|
+
slug,
|
|
23
|
+
...(pageParam ? { cursor: pageParam } : {}),
|
|
24
|
+
});
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(`/internal/post-history?${params}`, {
|
|
27
|
+
cache: "no-store",
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) throw Error("Progress history is unavailable.");
|
|
30
|
+
const page = (await response.json()) as ObservedHistoryPage;
|
|
31
|
+
if (!page.available) clearPublicPost();
|
|
32
|
+
return page;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
clearPublicPost();
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
getNextPageParam: (page) => page.nextCursor,
|
|
39
|
+
staleTime: 0,
|
|
40
|
+
retry: false,
|
|
41
|
+
});
|
|
42
|
+
const events = query.isError
|
|
43
|
+
? []
|
|
44
|
+
: (query.data?.pages.flatMap((page) => page.items) ?? []);
|
|
45
|
+
return (
|
|
46
|
+
<section className="space-y-3" aria-label="Activities">
|
|
47
|
+
<h2 className="sr-only">Activities</h2>
|
|
48
|
+
{query.isPending ? (
|
|
49
|
+
<Skeleton className="h-12 w-full" />
|
|
50
|
+
) : query.isError ? (
|
|
51
|
+
<div role="alert" className="space-y-2">
|
|
52
|
+
<p className="text-sm">
|
|
53
|
+
Progress history is temporarily unavailable.
|
|
54
|
+
</p>
|
|
55
|
+
<Button
|
|
56
|
+
variant="outline"
|
|
57
|
+
size="sm"
|
|
58
|
+
onClick={() => void query.refetch()}
|
|
59
|
+
>
|
|
60
|
+
Retry history
|
|
61
|
+
</Button>
|
|
62
|
+
</div>
|
|
63
|
+
) : events.length ? (
|
|
64
|
+
<ol className="space-y-4 border-l pl-4">
|
|
65
|
+
{events.map((event) => (
|
|
66
|
+
<li key={event.id} className="space-y-1 text-sm">
|
|
67
|
+
<time
|
|
68
|
+
dateTime={new Date(event.observedAt).toISOString()}
|
|
69
|
+
className="text-xs text-muted-foreground"
|
|
70
|
+
>
|
|
71
|
+
Observed {formatPublicDate(new Date(event.observedAt))}
|
|
72
|
+
</time>
|
|
73
|
+
{event.kind === "status" ? (
|
|
74
|
+
<p>
|
|
75
|
+
Moved to <span className="font-medium">{event.status}</span>
|
|
76
|
+
</p>
|
|
77
|
+
) : (
|
|
78
|
+
<>
|
|
79
|
+
<p className="font-medium">Product Team update</p>
|
|
80
|
+
<p className="whitespace-pre-wrap">{event.body}</p>
|
|
81
|
+
</>
|
|
82
|
+
)}
|
|
83
|
+
</li>
|
|
84
|
+
))}
|
|
85
|
+
</ol>
|
|
86
|
+
) : (
|
|
87
|
+
<p className="text-sm text-muted-foreground">No activities yet.</p>
|
|
88
|
+
)}
|
|
89
|
+
{query.hasNextPage && !query.isError ? (
|
|
90
|
+
<Button
|
|
91
|
+
variant="outline"
|
|
92
|
+
size="sm"
|
|
93
|
+
disabled={query.isFetchingNextPage}
|
|
94
|
+
onClick={() => void query.fetchNextPage()}
|
|
95
|
+
>
|
|
96
|
+
{query.isFetchingNextPage ? "Loading history…" : "More history"}
|
|
97
|
+
</Button>
|
|
98
|
+
) : null}
|
|
99
|
+
</section>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useConnectedIdentity } from "./connected-identity-provider";
|
|
1
2
|
import { useTheme } from "next-themes";
|
|
2
3
|
import { UserCircle } from "lucide-react";
|
|
3
4
|
|
|
@@ -20,8 +21,44 @@ import { useDeviceProfile } from "./device-profile-provider";
|
|
|
20
21
|
export function ProfileMenu() {
|
|
21
22
|
const { profile, openProfileSetup, clearProfile } = useDeviceProfile();
|
|
22
23
|
|
|
24
|
+
const identity = useConnectedIdentity();
|
|
25
|
+
|
|
23
26
|
const initials = profile ? deriveDeviceProfileInitials(profile) : undefined;
|
|
24
27
|
|
|
28
|
+
if (identity.enabled)
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<Button variant="ghost" onClick={openProfileSetup}>
|
|
32
|
+
Contact profile
|
|
33
|
+
</Button>
|
|
34
|
+
{identity.displayName ? (
|
|
35
|
+
<>
|
|
36
|
+
<Button
|
|
37
|
+
nativeButton={false}
|
|
38
|
+
variant="ghost"
|
|
39
|
+
render={<a href="/my-feedback" />}
|
|
40
|
+
>
|
|
41
|
+
My feedback
|
|
42
|
+
</Button>
|
|
43
|
+
<span>{identity.displayName}</span>
|
|
44
|
+
<Button variant="outline" onClick={() => void identity.signOut()}>
|
|
45
|
+
Sign out
|
|
46
|
+
</Button>
|
|
47
|
+
</>
|
|
48
|
+
) : (
|
|
49
|
+
<Button
|
|
50
|
+
variant="outline"
|
|
51
|
+
disabled={!identity.ready}
|
|
52
|
+
onClick={() => {
|
|
53
|
+
window.location.href = "/internal/identity/start";
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
Sign in
|
|
57
|
+
</Button>
|
|
58
|
+
)}
|
|
59
|
+
{identity.error ? <p role="alert">{identity.error}</p> : null}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
25
62
|
return profile ? (
|
|
26
63
|
<DropdownMenu>
|
|
27
64
|
<DropdownMenuTrigger
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { Command as CommandPrimitive } from "cmdk";
|
|
3
|
-
|
|
4
3
|
import { cn } from "@feedbax/ui/lib/utils";
|
|
4
|
+
|
|
5
5
|
import {
|
|
6
6
|
Dialog,
|
|
7
7
|
DialogContent,
|
|
@@ -9,10 +9,6 @@ import {
|
|
|
9
9
|
DialogHeader,
|
|
10
10
|
DialogTitle,
|
|
11
11
|
} from "@feedbax/ui/components/dialog";
|
|
12
|
-
import {
|
|
13
|
-
InputGroup,
|
|
14
|
-
InputGroupAddon,
|
|
15
|
-
} from "@feedbax/ui/components/input-group";
|
|
16
12
|
import { SearchIcon, CheckIcon } from "lucide-react";
|
|
17
13
|
|
|
18
14
|
function Command({
|
|
@@ -47,17 +43,17 @@ function CommandDialog({
|
|
|
47
43
|
}) {
|
|
48
44
|
return (
|
|
49
45
|
<Dialog {...props}>
|
|
50
|
-
<DialogHeader className="sr-only">
|
|
51
|
-
<DialogTitle>{title}</DialogTitle>
|
|
52
|
-
<DialogDescription>{description}</DialogDescription>
|
|
53
|
-
</DialogHeader>
|
|
54
46
|
<DialogContent
|
|
55
47
|
className={cn(
|
|
56
|
-
"top-
|
|
48
|
+
"top-24 translate-y-0 overflow-hidden rounded-none p-0 sm:top-32",
|
|
57
49
|
className,
|
|
58
50
|
)}
|
|
59
51
|
showCloseButton={showCloseButton}
|
|
60
52
|
>
|
|
53
|
+
<DialogHeader className="sr-only">
|
|
54
|
+
<DialogTitle>{title}</DialogTitle>
|
|
55
|
+
<DialogDescription>{description}</DialogDescription>
|
|
56
|
+
</DialogHeader>
|
|
61
57
|
{children}
|
|
62
58
|
</DialogContent>
|
|
63
59
|
</Dialog>
|
|
@@ -69,20 +65,22 @@ function CommandInput({
|
|
|
69
65
|
...props
|
|
70
66
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
|
71
67
|
return (
|
|
72
|
-
<div
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
68
|
+
<div
|
|
69
|
+
data-slot="command-input-wrapper"
|
|
70
|
+
className="flex h-14 shrink-0 items-center gap-3 border-b px-4"
|
|
71
|
+
>
|
|
72
|
+
<SearchIcon
|
|
73
|
+
className="size-5 shrink-0 text-muted-foreground"
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
/>
|
|
76
|
+
<CommandPrimitive.Input
|
|
77
|
+
data-slot="command-input"
|
|
78
|
+
className={cn(
|
|
79
|
+
"h-full w-full min-w-0 border-0 bg-transparent py-4 text-base text-foreground outline-none placeholder:text-muted-foreground focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm",
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
86
84
|
</div>
|
|
87
85
|
);
|
|
88
86
|
}
|
|
@@ -95,7 +93,7 @@ function CommandList({
|
|
|
95
93
|
<CommandPrimitive.List
|
|
96
94
|
data-slot="command-list"
|
|
97
95
|
className={cn(
|
|
98
|
-
"no-scrollbar max-h-
|
|
96
|
+
"no-scrollbar max-h-80 scroll-py-2 overflow-x-hidden overflow-y-auto outline-none",
|
|
99
97
|
className,
|
|
100
98
|
)}
|
|
101
99
|
{...props}
|
|
@@ -110,7 +108,10 @@ function CommandEmpty({
|
|
|
110
108
|
return (
|
|
111
109
|
<CommandPrimitive.Empty
|
|
112
110
|
data-slot="command-empty"
|
|
113
|
-
className={cn(
|
|
111
|
+
className={cn(
|
|
112
|
+
"px-6 py-10 text-center text-sm text-muted-foreground",
|
|
113
|
+
className,
|
|
114
|
+
)}
|
|
114
115
|
{...props}
|
|
115
116
|
/>
|
|
116
117
|
);
|
|
@@ -124,7 +125,7 @@ function CommandGroup({
|
|
|
124
125
|
<CommandPrimitive.Group
|
|
125
126
|
data-slot="command-group"
|
|
126
127
|
className={cn(
|
|
127
|
-
"overflow-hidden text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-
|
|
128
|
+
"overflow-hidden text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-2 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:text-muted-foreground",
|
|
128
129
|
className,
|
|
129
130
|
)}
|
|
130
131
|
{...props}
|
|
@@ -139,7 +140,7 @@ function CommandSeparator({
|
|
|
139
140
|
return (
|
|
140
141
|
<CommandPrimitive.Separator
|
|
141
142
|
data-slot="command-separator"
|
|
142
|
-
className={cn("-
|
|
143
|
+
className={cn("my-1 h-px bg-border", className)}
|
|
143
144
|
{...props}
|
|
144
145
|
/>
|
|
145
146
|
);
|
|
@@ -154,7 +155,7 @@ function CommandItem({
|
|
|
154
155
|
<CommandPrimitive.Item
|
|
155
156
|
data-slot="command-item"
|
|
156
157
|
className={cn(
|
|
157
|
-
"group/command-item relative flex cursor-default items-center gap-2 rounded-none px-
|
|
158
|
+
"group/command-item relative flex cursor-default items-center gap-2 rounded-none px-3 py-3 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-none! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-selected:bg-muted data-selected:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-selected:*:[svg]:text-foreground",
|
|
158
159
|
className,
|
|
159
160
|
)}
|
|
160
161
|
{...props}
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
useMessageScrollerScrollable,
|
|
6
6
|
useMessageScrollerVisibility,
|
|
7
7
|
} from "@shadcn/react/message-scroller";
|
|
8
|
-
|
|
9
8
|
import { cn } from "@feedbax/ui/lib/utils";
|
|
9
|
+
|
|
10
10
|
import { Button } from "@feedbax/ui/components/button";
|
|
11
11
|
import { ArrowDownIcon } from "lucide-react";
|
|
12
12
|
|
|
@@ -40,7 +40,7 @@ function MessageScrollerViewport({
|
|
|
40
40
|
<MessageScrollerPrimitive.Viewport
|
|
41
41
|
data-slot="message-scroller-viewport"
|
|
42
42
|
className={cn(
|
|
43
|
-
"size-full min-h-0 min-w-0 scroll-fade-b scrollbar-thin scrollbar-gutter-stable overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-thumb-transparent data-autoscrolling:scrollbar-track-transparent",
|
|
43
|
+
"size-full min-h-0 min-w-0 scroll-fade-b scrollbar-thin scrollbar-gutter-stable overflow-y-auto overscroll-contain contain-content data-autoscrolling:scrollbar-thumb-transparent data-autoscrolling:scrollbar-track-transparent data-pending-scroll:invisible",
|
|
44
44
|
className,
|
|
45
45
|
)}
|
|
46
46
|
{...props}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
|
|
2
|
+
import { cn } from "@feedbax/ui/lib/utils";
|
|
3
|
+
|
|
4
|
+
function Switch({
|
|
5
|
+
className,
|
|
6
|
+
size = "default",
|
|
7
|
+
...props
|
|
8
|
+
}: SwitchPrimitive.Root.Props & {
|
|
9
|
+
size?: "sm" | "default";
|
|
10
|
+
}) {
|
|
11
|
+
return (
|
|
12
|
+
<SwitchPrimitive.Root
|
|
13
|
+
data-slot="switch"
|
|
14
|
+
data-size={size}
|
|
15
|
+
className={cn(
|
|
16
|
+
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<SwitchPrimitive.Thumb
|
|
22
|
+
data-slot="switch-thumb"
|
|
23
|
+
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
|
|
24
|
+
/>
|
|
25
|
+
</SwitchPrimitive.Root>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { Switch };
|