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.
- package/LICENSE +201 -21
- package/README.md +11 -1
- package/dist/cli.js +3283 -22
- package/dist/index.js +3394 -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/AGENTS.md +13 -0
- package/template/CONNECTED.md +404 -0
- package/template/LICENSE +201 -0
- package/template/README.md +56 -5
- package/template/components.json +25 -0
- package/template/eslint.config.mjs +48 -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 +74 -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 +205 -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 +228 -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 +24 -0
- package/template/src/comment-creation.test.tsx +311 -0
- package/template/src/comment-profile.ts +5 -0
- package/template/src/comment-thread-page.ts +28 -0
- package/template/src/components/command-palette-context.tsx +71 -0
- package/template/src/components/command-palette.tsx +275 -0
- package/template/src/components/connected-identity-provider.tsx +108 -0
- package/template/src/components/create-post-trigger.tsx +44 -0
- package/template/src/components/device-profile-provider.tsx +324 -0
- package/template/src/components/follow-post-button.tsx +89 -0
- package/template/src/components/header.tsx +97 -0
- package/template/src/components/image-attachments.tsx +187 -0
- package/template/src/components/loader.tsx +53 -0
- 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 +119 -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 +195 -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 +51 -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/switch.tsx +29 -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/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/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 +98 -0
- package/template/src/feedback-runtime.ts +105 -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 +139 -0
- package/template/src/feedbax/config/index.ts +364 -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/feedback-storage.ts +43 -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.test.ts +900 -0
- package/template/src/feedbax/feedback/index.ts +996 -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-image-review.ts +55 -0
- package/template/src/feedbax/feedback/notion-request.ts +74 -0
- package/template/src/feedbax/feedback/participation.test.ts +653 -0
- package/template/src/feedbax/feedback/participation.ts +502 -0
- package/template/src/feedbax.test-fixture.ts +64 -0
- package/template/src/feedbax.ts +69 -0
- package/template/src/images-runtime.test.ts +133 -0
- package/template/src/images-runtime.ts +125 -0
- package/template/src/index.css +1795 -0
- package/template/src/lazy-create-post-overlay.tsx +6 -0
- package/template/src/lazy-post-detail-overlay.tsx +21 -0
- package/template/src/masked-post-link.tsx +27 -0
- package/template/src/overlay-post-detail.test.tsx +181 -0
- package/template/src/overlay-post-detail.tsx +43 -0
- 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.test.ts +375 -0
- package/template/src/portal-comments.ts +403 -0
- package/template/src/portal-features.test.tsx +150 -0
- package/template/src/portal-feedback-form.tsx +530 -0
- package/template/src/portal-feedback-runtime.test.ts +32 -0
- package/template/src/portal-feedback-runtime.ts +58 -0
- package/template/src/portal-feedback-server-function.ts +96 -0
- package/template/src/portal-feedback-submission.test.ts +307 -0
- package/template/src/portal-feedback-submission.ts +224 -0
- package/template/src/portal-head.ts +58 -0
- package/template/src/portal-server-entry.ts +68 -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-content.test.tsx +233 -0
- package/template/src/post-detail-content.tsx +162 -0
- package/template/src/post-detail-overlay.test.tsx +114 -0
- package/template/src/post-detail-overlay.tsx +67 -0
- package/template/src/post-detail-query.ts +34 -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 +148 -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 +94 -0
- package/template/src/public-feedback-page.test.tsx +355 -0
- package/template/src/public-feedback-page.ts +86 -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-skeleton.tsx +54 -0
- package/template/src/public-post-detail.tsx +772 -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 +250 -0
- package/template/src/public-post-route.tsx +158 -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 +254 -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 +411 -0
- package/template/src/public-shell.test.tsx +544 -0
- package/template/src/roadmap-query.ts +21 -0
- package/template/src/root-branding.test.ts +53 -0
- package/template/src/routeTree.gen.ts +177 -0
- package/template/src/routed-post-creation-form.tsx +28 -0
- package/template/src/router.tsx +27 -0
- package/template/src/routes/__root.tsx +114 -0
- package/template/src/routes/changelog.tsx +39 -0
- package/template/src/routes/email-preferences.tsx +77 -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 +47 -0
- package/template/src/server.ts +151 -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 +244 -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/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,177 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
|
|
5
|
+
// noinspection JSUnusedGlobalSymbols
|
|
6
|
+
|
|
7
|
+
// This file was automatically generated by TanStack Router.
|
|
8
|
+
// You should NOT make any changes in this file as it will be overwritten.
|
|
9
|
+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
10
|
+
|
|
11
|
+
import { Route as rootRouteImport } from './routes/__root'
|
|
12
|
+
import { Route as IndexRouteImport } from './routes/index'
|
|
13
|
+
import { Route as ChangelogRouteImport } from './routes/changelog'
|
|
14
|
+
import { Route as EmailPreferencesRouteImport } from './routes/email-preferences'
|
|
15
|
+
import { Route as RoadmapRouteImport } from './routes/roadmap'
|
|
16
|
+
import { Route as SubmitRouteImport } from './routes/submit'
|
|
17
|
+
import { Route as PSlugRouteImport } from './routes/p.$slug'
|
|
18
|
+
|
|
19
|
+
const IndexRoute = IndexRouteImport.update({
|
|
20
|
+
id: '/',
|
|
21
|
+
path: '/',
|
|
22
|
+
getParentRoute: () => rootRouteImport,
|
|
23
|
+
} as any)
|
|
24
|
+
const ChangelogRoute = ChangelogRouteImport.update({
|
|
25
|
+
id: '/changelog',
|
|
26
|
+
path: '/changelog',
|
|
27
|
+
getParentRoute: () => rootRouteImport,
|
|
28
|
+
} as any)
|
|
29
|
+
const EmailPreferencesRoute = EmailPreferencesRouteImport.update({
|
|
30
|
+
id: '/email-preferences',
|
|
31
|
+
path: '/email-preferences',
|
|
32
|
+
getParentRoute: () => rootRouteImport,
|
|
33
|
+
} as any)
|
|
34
|
+
const RoadmapRoute = RoadmapRouteImport.update({
|
|
35
|
+
id: '/roadmap',
|
|
36
|
+
path: '/roadmap',
|
|
37
|
+
getParentRoute: () => rootRouteImport,
|
|
38
|
+
} as any)
|
|
39
|
+
const SubmitRoute = SubmitRouteImport.update({
|
|
40
|
+
id: '/submit',
|
|
41
|
+
path: '/submit',
|
|
42
|
+
getParentRoute: () => rootRouteImport,
|
|
43
|
+
} as any)
|
|
44
|
+
const PSlugRoute = PSlugRouteImport.update({
|
|
45
|
+
id: '/p/$slug',
|
|
46
|
+
path: '/p/$slug',
|
|
47
|
+
getParentRoute: () => rootRouteImport,
|
|
48
|
+
} as any)
|
|
49
|
+
|
|
50
|
+
export interface FileRoutesByFullPath {
|
|
51
|
+
'/': typeof IndexRoute
|
|
52
|
+
'/changelog': typeof ChangelogRoute
|
|
53
|
+
'/email-preferences': typeof EmailPreferencesRoute
|
|
54
|
+
'/roadmap': typeof RoadmapRoute
|
|
55
|
+
'/submit': typeof SubmitRoute
|
|
56
|
+
'/p/$slug': typeof PSlugRoute
|
|
57
|
+
}
|
|
58
|
+
export interface FileRoutesByTo {
|
|
59
|
+
'/': typeof IndexRoute
|
|
60
|
+
'/changelog': typeof ChangelogRoute
|
|
61
|
+
'/email-preferences': typeof EmailPreferencesRoute
|
|
62
|
+
'/roadmap': typeof RoadmapRoute
|
|
63
|
+
'/submit': typeof SubmitRoute
|
|
64
|
+
'/p/$slug': typeof PSlugRoute
|
|
65
|
+
}
|
|
66
|
+
export interface FileRoutesById {
|
|
67
|
+
__root__: typeof rootRouteImport
|
|
68
|
+
'/': typeof IndexRoute
|
|
69
|
+
'/changelog': typeof ChangelogRoute
|
|
70
|
+
'/email-preferences': typeof EmailPreferencesRoute
|
|
71
|
+
'/roadmap': typeof RoadmapRoute
|
|
72
|
+
'/submit': typeof SubmitRoute
|
|
73
|
+
'/p/$slug': typeof PSlugRoute
|
|
74
|
+
}
|
|
75
|
+
export interface FileRouteTypes {
|
|
76
|
+
fileRoutesByFullPath: FileRoutesByFullPath
|
|
77
|
+
fullPaths:
|
|
78
|
+
| '/'
|
|
79
|
+
| '/changelog'
|
|
80
|
+
| '/email-preferences'
|
|
81
|
+
| '/roadmap'
|
|
82
|
+
| '/submit'
|
|
83
|
+
| '/p/$slug'
|
|
84
|
+
fileRoutesByTo: FileRoutesByTo
|
|
85
|
+
to:
|
|
86
|
+
| '/'
|
|
87
|
+
| '/changelog'
|
|
88
|
+
| '/email-preferences'
|
|
89
|
+
| '/roadmap'
|
|
90
|
+
| '/submit'
|
|
91
|
+
| '/p/$slug'
|
|
92
|
+
id:
|
|
93
|
+
| '__root__'
|
|
94
|
+
| '/'
|
|
95
|
+
| '/changelog'
|
|
96
|
+
| '/email-preferences'
|
|
97
|
+
| '/roadmap'
|
|
98
|
+
| '/submit'
|
|
99
|
+
| '/p/$slug'
|
|
100
|
+
fileRoutesById: FileRoutesById
|
|
101
|
+
}
|
|
102
|
+
export interface RootRouteChildren {
|
|
103
|
+
IndexRoute: typeof IndexRoute
|
|
104
|
+
ChangelogRoute: typeof ChangelogRoute
|
|
105
|
+
EmailPreferencesRoute: typeof EmailPreferencesRoute
|
|
106
|
+
RoadmapRoute: typeof RoadmapRoute
|
|
107
|
+
SubmitRoute: typeof SubmitRoute
|
|
108
|
+
PSlugRoute: typeof PSlugRoute
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
declare module '@tanstack/react-router' {
|
|
112
|
+
interface FileRoutesByPath {
|
|
113
|
+
'/': {
|
|
114
|
+
id: '/'
|
|
115
|
+
path: '/'
|
|
116
|
+
fullPath: '/'
|
|
117
|
+
preLoaderRoute: typeof IndexRouteImport
|
|
118
|
+
parentRoute: typeof rootRouteImport
|
|
119
|
+
}
|
|
120
|
+
'/changelog': {
|
|
121
|
+
id: '/changelog'
|
|
122
|
+
path: '/changelog'
|
|
123
|
+
fullPath: '/changelog'
|
|
124
|
+
preLoaderRoute: typeof ChangelogRouteImport
|
|
125
|
+
parentRoute: typeof rootRouteImport
|
|
126
|
+
}
|
|
127
|
+
'/email-preferences': {
|
|
128
|
+
id: '/email-preferences'
|
|
129
|
+
path: '/email-preferences'
|
|
130
|
+
fullPath: '/email-preferences'
|
|
131
|
+
preLoaderRoute: typeof EmailPreferencesRouteImport
|
|
132
|
+
parentRoute: typeof rootRouteImport
|
|
133
|
+
}
|
|
134
|
+
'/roadmap': {
|
|
135
|
+
id: '/roadmap'
|
|
136
|
+
path: '/roadmap'
|
|
137
|
+
fullPath: '/roadmap'
|
|
138
|
+
preLoaderRoute: typeof RoadmapRouteImport
|
|
139
|
+
parentRoute: typeof rootRouteImport
|
|
140
|
+
}
|
|
141
|
+
'/submit': {
|
|
142
|
+
id: '/submit'
|
|
143
|
+
path: '/submit'
|
|
144
|
+
fullPath: '/submit'
|
|
145
|
+
preLoaderRoute: typeof SubmitRouteImport
|
|
146
|
+
parentRoute: typeof rootRouteImport
|
|
147
|
+
}
|
|
148
|
+
'/p/$slug': {
|
|
149
|
+
id: '/p/$slug'
|
|
150
|
+
path: '/p/$slug'
|
|
151
|
+
fullPath: '/p/$slug'
|
|
152
|
+
preLoaderRoute: typeof PSlugRouteImport
|
|
153
|
+
parentRoute: typeof rootRouteImport
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const rootRouteChildren: RootRouteChildren = {
|
|
159
|
+
IndexRoute: IndexRoute,
|
|
160
|
+
ChangelogRoute: ChangelogRoute,
|
|
161
|
+
EmailPreferencesRoute: EmailPreferencesRoute,
|
|
162
|
+
RoadmapRoute: RoadmapRoute,
|
|
163
|
+
SubmitRoute: SubmitRoute,
|
|
164
|
+
PSlugRoute: PSlugRoute,
|
|
165
|
+
}
|
|
166
|
+
export const routeTree = rootRouteImport
|
|
167
|
+
._addFileChildren(rootRouteChildren)
|
|
168
|
+
._addFileTypes<FileRouteTypes>()
|
|
169
|
+
|
|
170
|
+
import type { getRouter } from './router.tsx'
|
|
171
|
+
import type { createStart } from '@tanstack/react-start'
|
|
172
|
+
declare module '@tanstack/react-start' {
|
|
173
|
+
interface Register {
|
|
174
|
+
ssr: true
|
|
175
|
+
router: Awaited<ReturnType<typeof getRouter>>
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
editPortalFeedbackDraft,
|
|
3
|
+
submitPortalPost,
|
|
4
|
+
withdrawPortalFeedbackDraft,
|
|
5
|
+
} from "./portal-feedback-server-function";
|
|
6
|
+
import { PostCreationFlow } from "./post-creation-flow";
|
|
7
|
+
|
|
8
|
+
export const portalFeedbackMutations = {
|
|
9
|
+
submitPost: submitPortalPost,
|
|
10
|
+
editDraftPost: editPortalFeedbackDraft,
|
|
11
|
+
withdrawDraftPost: withdrawPortalFeedbackDraft,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function RoutedPostCreationForm({
|
|
15
|
+
display = "page",
|
|
16
|
+
onCancel,
|
|
17
|
+
}: {
|
|
18
|
+
display?: "page" | "overlay";
|
|
19
|
+
onCancel?: () => void;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<PostCreationFlow
|
|
23
|
+
display={display}
|
|
24
|
+
onCancel={onCancel}
|
|
25
|
+
mutations={portalFeedbackMutations}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
|
2
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
3
|
+
|
|
4
|
+
import Loader from "./components/loader";
|
|
5
|
+
import { routeTree } from "./routeTree.gen";
|
|
6
|
+
|
|
7
|
+
export const getRouter = () => {
|
|
8
|
+
const queryClient = new QueryClient();
|
|
9
|
+
const router = createTanStackRouter({
|
|
10
|
+
routeTree,
|
|
11
|
+
scrollRestoration: true,
|
|
12
|
+
defaultPreloadStaleTime: 0,
|
|
13
|
+
context: { queryClient },
|
|
14
|
+
defaultPendingMs: 250,
|
|
15
|
+
defaultPendingMinMs: 200,
|
|
16
|
+
defaultPendingComponent: () => <Loader />,
|
|
17
|
+
defaultNotFoundComponent: () => <div>Not Found</div>,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return router;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
declare module "@tanstack/react-router" {
|
|
24
|
+
interface Register {
|
|
25
|
+
router: ReturnType<typeof getRouter>;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Toaster } from "@feedbax/ui/components/sonner";
|
|
2
|
+
import {
|
|
3
|
+
HeadContent,
|
|
4
|
+
Outlet,
|
|
5
|
+
Scripts,
|
|
6
|
+
createRootRouteWithContext,
|
|
7
|
+
useRouterState,
|
|
8
|
+
} from "@tanstack/react-router";
|
|
9
|
+
import { lazy, Suspense } from "react";
|
|
10
|
+
import { ThemeProvider } from "next-themes";
|
|
11
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
12
|
+
|
|
13
|
+
import Header from "../components/header";
|
|
14
|
+
import { PortalFooter } from "../components/portal-footer";
|
|
15
|
+
import { LazyPostDetailOverlay as PostDetailOverlay } from "../lazy-post-detail-overlay";
|
|
16
|
+
import { DeviceProfileProvider } from "../components/device-profile-provider";
|
|
17
|
+
import {
|
|
18
|
+
CommandPaletteProvider,
|
|
19
|
+
useCommandPalette,
|
|
20
|
+
} from "../components/command-palette-context";
|
|
21
|
+
import feedbax from "../feedbax";
|
|
22
|
+
import { createPortalHead } from "../portal-head";
|
|
23
|
+
|
|
24
|
+
import appCss from "../index.css?url";
|
|
25
|
+
|
|
26
|
+
const RouterDevtools = import.meta.env.DEV
|
|
27
|
+
? lazy(() =>
|
|
28
|
+
import("@tanstack/react-router-devtools").then((module) => ({
|
|
29
|
+
default: module.TanStackRouterDevtools,
|
|
30
|
+
})),
|
|
31
|
+
)
|
|
32
|
+
: null;
|
|
33
|
+
|
|
34
|
+
const CommandPalette = lazy(() =>
|
|
35
|
+
import("../components/command-palette").then((module) => ({
|
|
36
|
+
default: module.CommandPalette,
|
|
37
|
+
})),
|
|
38
|
+
);
|
|
39
|
+
const CreatePostOverlay = lazy(() =>
|
|
40
|
+
import("../lazy-create-post-overlay").then((module) => ({
|
|
41
|
+
default: module.LazyCreatePostOverlay,
|
|
42
|
+
})),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export type RouterAppContext = {
|
|
46
|
+
queryClient: import("@tanstack/react-query").QueryClient;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Route = createRootRouteWithContext<RouterAppContext>()({
|
|
50
|
+
head: () => createPortalHead(feedbax.product, appCss),
|
|
51
|
+
|
|
52
|
+
component: RootDocument,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function OptionalPortalSurfaces() {
|
|
56
|
+
const { open: commandPaletteOpen } = useCommandPalette();
|
|
57
|
+
const overlayState = useRouterState({
|
|
58
|
+
select: (state) => state.location.state,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Suspense fallback={null}>
|
|
63
|
+
{overlayState.createPostOverlay ? <CreatePostOverlay /> : null}
|
|
64
|
+
{overlayState.postDetailOverlay ? <PostDetailOverlay /> : null}
|
|
65
|
+
{commandPaletteOpen ? (
|
|
66
|
+
<CommandPalette
|
|
67
|
+
searchPosts={async (term) => {
|
|
68
|
+
const { getPublicPostPage } =
|
|
69
|
+
await import("../public-feedback-server-function");
|
|
70
|
+
return getPublicPostPage({
|
|
71
|
+
data: { search: term || undefined },
|
|
72
|
+
});
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
) : null}
|
|
76
|
+
</Suspense>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function RootDocument() {
|
|
81
|
+
const { queryClient } = Route.useRouteContext();
|
|
82
|
+
return (
|
|
83
|
+
<html lang="en" suppressHydrationWarning>
|
|
84
|
+
<head>
|
|
85
|
+
<HeadContent />
|
|
86
|
+
</head>
|
|
87
|
+
<body>
|
|
88
|
+
<QueryClientProvider client={queryClient}>
|
|
89
|
+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
|
90
|
+
<DeviceProfileProvider>
|
|
91
|
+
<CommandPaletteProvider>
|
|
92
|
+
<div className="portal-shell">
|
|
93
|
+
<Header />
|
|
94
|
+
<div id="main-content" tabIndex={-1}>
|
|
95
|
+
<Outlet />
|
|
96
|
+
</div>
|
|
97
|
+
<PortalFooter />
|
|
98
|
+
<OptionalPortalSurfaces />
|
|
99
|
+
</div>
|
|
100
|
+
</CommandPaletteProvider>
|
|
101
|
+
</DeviceProfileProvider>
|
|
102
|
+
</ThemeProvider>
|
|
103
|
+
</QueryClientProvider>
|
|
104
|
+
<Toaster richColors />
|
|
105
|
+
{RouterDevtools ? (
|
|
106
|
+
<Suspense fallback={null}>
|
|
107
|
+
<RouterDevtools position="bottom-left" />
|
|
108
|
+
</Suspense>
|
|
109
|
+
) : null}
|
|
110
|
+
<Scripts />
|
|
111
|
+
</body>
|
|
112
|
+
</html>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createFileRoute, notFound } from "@tanstack/react-router";
|
|
2
|
+
import type { PortalFeatures } from "@feedbax/config";
|
|
3
|
+
import { useSuspenseQuery } from "@tanstack/react-query";
|
|
4
|
+
|
|
5
|
+
import { ChangelogPage } from "../changelog-page";
|
|
6
|
+
import { changelogQuery } from "../changelog-query";
|
|
7
|
+
import feedbax from "../feedbax";
|
|
8
|
+
import { getPublicChangelogPage } from "../public-changelog-server-function";
|
|
9
|
+
import { createRouteHead } from "../portal-head";
|
|
10
|
+
|
|
11
|
+
export function ensureChangelogEnabled(features: PortalFeatures) {
|
|
12
|
+
if (!features.changelog) throw notFound();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const query = changelogQuery(getPublicChangelogPage);
|
|
16
|
+
|
|
17
|
+
export const Route = createFileRoute("/changelog")({
|
|
18
|
+
head: () =>
|
|
19
|
+
createRouteHead(feedbax.product, {
|
|
20
|
+
title: "Changelog",
|
|
21
|
+
description: `Product updates from ${feedbax.product.name}.`,
|
|
22
|
+
path: "/changelog",
|
|
23
|
+
}),
|
|
24
|
+
beforeLoad: () => {
|
|
25
|
+
ensureChangelogEnabled(feedbax.features);
|
|
26
|
+
},
|
|
27
|
+
loader: ({ context }) => context.queryClient.ensureQueryData(query),
|
|
28
|
+
component: ChangelogRoute,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function ChangelogRoute() {
|
|
32
|
+
const initialPage = useSuspenseQuery(query).data;
|
|
33
|
+
return (
|
|
34
|
+
<ChangelogPage
|
|
35
|
+
initialPage={initialPage}
|
|
36
|
+
loadPage={(data) => getPublicChangelogPage({ data })}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createFileRoute, notFound } from "@tanstack/react-router";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Button } from "@feedbax/ui/components/button";
|
|
4
|
+
import feedbax from "../feedbax";
|
|
5
|
+
export const Route = createFileRoute("/email-preferences")({
|
|
6
|
+
validateSearch: (search: Record<string, unknown>) => ({
|
|
7
|
+
token: typeof search.token === "string" ? search.token : "",
|
|
8
|
+
}),
|
|
9
|
+
beforeLoad: () => {
|
|
10
|
+
if (!feedbax.connected?.email) throw notFound();
|
|
11
|
+
},
|
|
12
|
+
head: () => ({
|
|
13
|
+
meta: [
|
|
14
|
+
{ name: "robots", content: "noindex,nofollow" },
|
|
15
|
+
{ name: "referrer", content: "no-referrer" },
|
|
16
|
+
],
|
|
17
|
+
}),
|
|
18
|
+
component: EmailPreferencesPage,
|
|
19
|
+
});
|
|
20
|
+
function EmailPreferencesPage() {
|
|
21
|
+
const { token } = Route.useSearch();
|
|
22
|
+
const [pending, setPending] = useState(false);
|
|
23
|
+
const [done, setDone] = useState(false);
|
|
24
|
+
const [error, setError] = useState<string>();
|
|
25
|
+
async function unsubscribe() {
|
|
26
|
+
setPending(true);
|
|
27
|
+
setError(undefined);
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(
|
|
30
|
+
`/internal/connected/unsubscribe?token=${encodeURIComponent(token)}`,
|
|
31
|
+
{ method: "POST" },
|
|
32
|
+
);
|
|
33
|
+
if (!response.ok)
|
|
34
|
+
throw new Error(
|
|
35
|
+
"This link could not be used. Try again or change email preferences in your notification inbox.",
|
|
36
|
+
);
|
|
37
|
+
setDone(true);
|
|
38
|
+
} catch (cause) {
|
|
39
|
+
setError(
|
|
40
|
+
cause instanceof Error
|
|
41
|
+
? cause.message
|
|
42
|
+
: "Couldn’t update email preferences.",
|
|
43
|
+
);
|
|
44
|
+
} finally {
|
|
45
|
+
setPending(false);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return (
|
|
49
|
+
<main className="mx-auto max-w-lg space-y-5 px-6 py-16">
|
|
50
|
+
<h1 className="text-2xl font-semibold tracking-tight">
|
|
51
|
+
{done ? "Email notifications are off" : "Stop email notifications"}
|
|
52
|
+
</h1>
|
|
53
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
54
|
+
{done
|
|
55
|
+
? "You can turn them on again in your notification inbox."
|
|
56
|
+
: "You will keep receiving updates inside Feedbax."}
|
|
57
|
+
</p>
|
|
58
|
+
{done ? (
|
|
59
|
+
<Button nativeButton={false} render={<a href="/" />} variant="outline">
|
|
60
|
+
Back to feedback
|
|
61
|
+
</Button>
|
|
62
|
+
) : (
|
|
63
|
+
<Button
|
|
64
|
+
onClick={unsubscribe}
|
|
65
|
+
disabled={pending || !/^[a-f0-9]{32}\.[A-Za-z0-9_-]{43}$/.test(token)}
|
|
66
|
+
>
|
|
67
|
+
{pending ? "Updating…" : "Stop email notifications"}
|
|
68
|
+
</Button>
|
|
69
|
+
)}
|
|
70
|
+
{error ? (
|
|
71
|
+
<p role="alert" className="text-sm text-destructive">
|
|
72
|
+
{error}
|
|
73
|
+
</p>
|
|
74
|
+
) : null}
|
|
75
|
+
</main>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PublicPostFeedSkeleton,
|
|
5
|
+
PublicPostIndex,
|
|
6
|
+
} from "../public-feedback-index";
|
|
7
|
+
import { PublicPortalError } from "../public-portal-error";
|
|
8
|
+
import { publicPostSearch } from "../public-feedback-page";
|
|
9
|
+
import { publicPostsQuery, type FetchPublicPostsPage } from "../post-queries";
|
|
10
|
+
import { getPublicPostPage } from "../public-feedback-server-function";
|
|
11
|
+
import { useSuspenseInfiniteQuery } from "@tanstack/react-query";
|
|
12
|
+
import { useTransition } from "react";
|
|
13
|
+
import feedbax from "../feedbax";
|
|
14
|
+
import { createRouteHead } from "../portal-head";
|
|
15
|
+
|
|
16
|
+
const fetchPublicPostsPage: FetchPublicPostsPage = (search) =>
|
|
17
|
+
getPublicPostPage({ data: search });
|
|
18
|
+
|
|
19
|
+
export const Route = createFileRoute("/")({
|
|
20
|
+
head: () => createRouteHead(feedbax.product, { path: "/" }),
|
|
21
|
+
validateSearch: publicPostSearch,
|
|
22
|
+
loaderDeps: ({ search }) => search,
|
|
23
|
+
loader: ({ deps, context }) =>
|
|
24
|
+
context.queryClient.ensureInfiniteQueryData(
|
|
25
|
+
publicPostsQuery(deps, fetchPublicPostsPage),
|
|
26
|
+
),
|
|
27
|
+
component: HomeComponent,
|
|
28
|
+
pendingComponent: PublicPostFeedSkeleton,
|
|
29
|
+
errorComponent: PublicPortalError,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function HomeComponent() {
|
|
33
|
+
const search = Route.useSearch();
|
|
34
|
+
const navigate = Route.useNavigate();
|
|
35
|
+
const [pending, startTransition] = useTransition();
|
|
36
|
+
const query = useSuspenseInfiniteQuery(
|
|
37
|
+
publicPostsQuery(search, fetchPublicPostsPage),
|
|
38
|
+
);
|
|
39
|
+
const pages = query.data.pages;
|
|
40
|
+
const page = {
|
|
41
|
+
items: pages.flatMap((page) => page.items),
|
|
42
|
+
nextCursor: pages.at(-1)?.nextCursor,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<PublicPostIndex
|
|
47
|
+
page={page}
|
|
48
|
+
search={search}
|
|
49
|
+
pending={pending}
|
|
50
|
+
loadMore={() => query.fetchNextPage()}
|
|
51
|
+
loadingMore={query.isFetchingNextPage}
|
|
52
|
+
loadMoreError={query.isFetchNextPageError}
|
|
53
|
+
onSearchChange={(nextSearch) =>
|
|
54
|
+
startTransition(() => {
|
|
55
|
+
void navigate({ search: nextSearch, replace: false });
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
maskPostLinks
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createPublicPostRoute } from "../public-post-route";
|
|
2
|
+
import { PublicPortalError } from "../public-portal-error";
|
|
3
|
+
import {
|
|
4
|
+
getPublicPost,
|
|
5
|
+
getPublicPostComments,
|
|
6
|
+
} from "../public-post-server-function";
|
|
7
|
+
import { AuthorizedDraftPost } from "../authorized-draft-post";
|
|
8
|
+
|
|
9
|
+
export const Route = createPublicPostRoute({
|
|
10
|
+
loadPost: (slug) => getPublicPost({ data: { slug } }),
|
|
11
|
+
loadComments: (slug, cursor) =>
|
|
12
|
+
getPublicPostComments({ data: { slug, cursor } }),
|
|
13
|
+
renderUnavailable: (slug) => <AuthorizedDraftPost slug={slug} />,
|
|
14
|
+
}).update({
|
|
15
|
+
errorComponent: PublicPortalError,
|
|
16
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createPublicRoadmapRoute } from "../public-roadmap-route";
|
|
2
|
+
import {
|
|
3
|
+
getPublicRoadmapPage,
|
|
4
|
+
getPublicRoadmapStatusPage,
|
|
5
|
+
} from "../public-roadmap-server-function";
|
|
6
|
+
|
|
7
|
+
export const Route = createPublicRoadmapRoute({
|
|
8
|
+
fetchRoadmap: () => getPublicRoadmapPage(),
|
|
9
|
+
fetchRoadmapStatusPage: (query) =>
|
|
10
|
+
getPublicRoadmapStatusPage({ data: query }),
|
|
11
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
|
|
3
|
+
import { RoutedPostCreationForm } from "../routed-post-creation-form";
|
|
4
|
+
import { PublicPortalError } from "../public-portal-error";
|
|
5
|
+
import { PostCreationSkeleton } from "../post-creation-flow";
|
|
6
|
+
import feedbax from "../feedbax";
|
|
7
|
+
import { createRouteHead } from "../portal-head";
|
|
8
|
+
|
|
9
|
+
export const Route = createFileRoute("/submit")({
|
|
10
|
+
head: () =>
|
|
11
|
+
createRouteHead(feedbax.product, {
|
|
12
|
+
title: "Share feedback",
|
|
13
|
+
description: `Share feedback with ${feedbax.product.name}.`,
|
|
14
|
+
path: "/submit",
|
|
15
|
+
noIndex: true,
|
|
16
|
+
}),
|
|
17
|
+
component: SubmitComponent,
|
|
18
|
+
pendingComponent: PostCreationSkeleton,
|
|
19
|
+
errorComponent: PublicPortalError,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function SubmitComponent() {
|
|
23
|
+
return (
|
|
24
|
+
<main className="feedback-index">
|
|
25
|
+
<RoutedPostCreationForm />
|
|
26
|
+
</main>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BrowserCapabilityAuthorizationError,
|
|
3
|
+
VoteEligibilityError,
|
|
4
|
+
ImageAttachmentError,
|
|
5
|
+
} from "@feedbax/feedback";
|
|
6
|
+
|
|
7
|
+
export async function runSafePublicRead<Value>(
|
|
8
|
+
operation: () => Promise<Value>,
|
|
9
|
+
): Promise<Value> {
|
|
10
|
+
try {
|
|
11
|
+
return await operation();
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error("Public feedback is temporarily unavailable.");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ActionablePortalFailure extends Error {
|
|
18
|
+
constructor(
|
|
19
|
+
message: string,
|
|
20
|
+
readonly code?: string,
|
|
21
|
+
) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "ActionablePortalFailure";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runSafePortalMutation<Value>(
|
|
28
|
+
operation: () => Promise<Value>,
|
|
29
|
+
): Promise<Value> {
|
|
30
|
+
try {
|
|
31
|
+
return await operation();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (
|
|
34
|
+
error instanceof ActionablePortalFailure ||
|
|
35
|
+
error instanceof BrowserCapabilityAuthorizationError ||
|
|
36
|
+
error instanceof VoteEligibilityError ||
|
|
37
|
+
error instanceof ImageAttachmentError
|
|
38
|
+
) {
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
throwSafePortalMutationFailure();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function throwSafePortalMutationFailure(): never {
|
|
46
|
+
throw new Error("Public feedback mutation is temporarily unavailable.");
|
|
47
|
+
}
|