@igstack/app-catalog-frontend-core 6.10.3 → 6.11.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/dist/esm/__tests__/modules/auth/AuthContext.test.d.ts +1 -0
- package/dist/esm/__tests__/modules/communityTools/likedBy.test.d.ts +1 -0
- package/dist/esm/__tests__/modules/communityTools/timeAgo.test.d.ts +1 -0
- package/dist/esm/__tests__/modules/communityTools/useTimeUp.test.d.ts +1 -0
- package/dist/esm/api/infra/trpc.d.ts +419 -0
- package/dist/esm/api/infra/trpc.js.map +1 -1
- package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.d.ts +2 -4
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +29 -101
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/catalog/DetailDialogShell.d.ts +26 -0
- package/dist/esm/modules/appCatalog/ui/catalog/DetailDialogShell.js +108 -0
- package/dist/esm/modules/appCatalog/ui/catalog/DetailDialogShell.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.d.ts +2 -1
- package/dist/esm/modules/appCatalog/ui/components/ScreenshotGallery.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/components/SectionTabs.d.ts +8 -0
- package/dist/esm/modules/appCatalog/ui/components/SectionTabs.js +73 -0
- package/dist/esm/modules/appCatalog/ui/components/SectionTabs.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +19 -2
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.d.ts +3 -2
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js +4 -4
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.d.ts +2 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js +2 -2
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.d.ts +3 -2
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js +13 -9
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.d.ts +4 -2
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js +10 -4
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.d.ts +12 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js +4 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/composeComments.js +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/composeComments.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/useCommunityToolCount.d.ts +6 -0
- package/dist/esm/modules/appCatalog/ui/detail/useCommunityToolCount.js +16 -0
- package/dist/esm/modules/appCatalog/ui/detail/useCommunityToolCount.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/layout/AppCatalogLayout.js +2 -2
- package/dist/esm/modules/appCatalog/ui/layout/AppCatalogLayout.js.map +1 -1
- package/dist/esm/modules/auth/AuthContext.js +7 -3
- package/dist/esm/modules/auth/AuthContext.js.map +1 -1
- package/dist/esm/modules/auth/ui/LoginModal.js +16 -8
- package/dist/esm/modules/auth/ui/LoginModal.js.map +1 -1
- package/dist/esm/modules/communityTools/likedBy.d.ts +8 -0
- package/dist/esm/modules/communityTools/likedBy.js +23 -0
- package/dist/esm/modules/communityTools/likedBy.js.map +1 -0
- package/dist/esm/modules/communityTools/timeAgo.d.ts +2 -0
- package/dist/esm/modules/communityTools/timeAgo.js +18 -0
- package/dist/esm/modules/communityTools/timeAgo.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/CardScreenshot.d.ts +13 -0
- package/dist/esm/modules/communityTools/ui/CardScreenshot.js +112 -0
- package/dist/esm/modules/communityTools/ui/CardScreenshot.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/CardTitleBlock.d.ts +8 -0
- package/dist/esm/modules/communityTools/ui/CardTitleBlock.js +59 -0
- package/dist/esm/modules/communityTools/ui/CardTitleBlock.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/CommunityPage.d.ts +12 -0
- package/dist/esm/modules/communityTools/ui/CommunityPage.js +547 -0
- package/dist/esm/modules/communityTools/ui/CommunityPage.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/FeedRow.d.ts +42 -0
- package/dist/esm/modules/communityTools/ui/FeedRow.js +260 -0
- package/dist/esm/modules/communityTools/ui/FeedRow.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/FeedSortControl.d.ts +17 -0
- package/dist/esm/modules/communityTools/ui/FeedSortControl.js +84 -0
- package/dist/esm/modules/communityTools/ui/FeedSortControl.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/LifecycleBadge.d.ts +7 -0
- package/dist/esm/modules/communityTools/ui/LifecycleBadge.js +50 -0
- package/dist/esm/modules/communityTools/ui/LifecycleBadge.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/LikeButton.d.ts +9 -0
- package/dist/esm/modules/communityTools/ui/LikeButton.js +56 -0
- package/dist/esm/modules/communityTools/ui/LikeButton.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/MarginPreview.d.ts +18 -0
- package/dist/esm/modules/communityTools/ui/MarginPreview.js +115 -0
- package/dist/esm/modules/communityTools/ui/MarginPreview.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/MyToolsShelf.d.ts +12 -0
- package/dist/esm/modules/communityTools/ui/MyToolsShelf.js +88 -0
- package/dist/esm/modules/communityTools/ui/MyToolsShelf.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/OwnerPicker.d.ts +56 -0
- package/dist/esm/modules/communityTools/ui/OwnerPicker.js +245 -0
- package/dist/esm/modules/communityTools/ui/OwnerPicker.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/PinButton.d.ts +14 -0
- package/dist/esm/modules/communityTools/ui/PinButton.js +72 -0
- package/dist/esm/modules/communityTools/ui/PinButton.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/ShelfTile.d.ts +13 -0
- package/dist/esm/modules/communityTools/ui/ShelfTile.js +81 -0
- package/dist/esm/modules/communityTools/ui/ShelfTile.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/TagPicker.d.ts +21 -0
- package/dist/esm/modules/communityTools/ui/TagPicker.js +87 -0
- package/dist/esm/modules/communityTools/ui/TagPicker.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/TagRow.d.ts +22 -0
- package/dist/esm/modules/communityTools/ui/TagRow.js +92 -0
- package/dist/esm/modules/communityTools/ui/TagRow.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/Toast.d.ts +19 -0
- package/dist/esm/modules/communityTools/ui/Toast.js +87 -0
- package/dist/esm/modules/communityTools/ui/Toast.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/ToolCard.d.ts +22 -0
- package/dist/esm/modules/communityTools/ui/ToolCard.js +467 -0
- package/dist/esm/modules/communityTools/ui/ToolCard.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/ToolFormDialog.d.ts +32 -0
- package/dist/esm/modules/communityTools/ui/ToolFormDialog.js +765 -0
- package/dist/esm/modules/communityTools/ui/ToolFormDialog.js.map +1 -0
- package/dist/esm/modules/communityTools/ui/ToolSearch.d.ts +15 -0
- package/dist/esm/modules/communityTools/ui/ToolSearch.js +92 -0
- package/dist/esm/modules/communityTools/ui/ToolSearch.js.map +1 -0
- package/dist/esm/modules/communityTools/useCommunityTools.d.ts +157 -0
- package/dist/esm/modules/communityTools/useCommunityTools.js +122 -0
- package/dist/esm/modules/communityTools/useCommunityTools.js.map +1 -0
- package/dist/esm/modules/communityTools/useCommunityToolsConfig.d.ts +10 -0
- package/dist/esm/modules/communityTools/useCommunityToolsConfig.js +20 -0
- package/dist/esm/modules/communityTools/useCommunityToolsConfig.js.map +1 -0
- package/dist/esm/modules/communityTools/useTimeUp.d.ts +5 -0
- package/dist/esm/modules/communityTools/useTimeUp.js +13 -0
- package/dist/esm/modules/communityTools/useTimeUp.js.map +1 -0
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/classic/schemas.js +2 -2
- package/dist/esm/node_modules/.pnpm/zod@4.3.5/node_modules/zod/v4/core/schemas.js +1 -1
- package/dist/esm/routeTree.gen.d.ts +96 -3
- package/dist/esm/routeTree.gen.js +19 -0
- package/dist/esm/routeTree.gen.js.map +1 -1
- package/dist/esm/routes/_layout/community.$slug.d.ts +22 -0
- package/dist/esm/routes/_layout/community._slug.js +10 -0
- package/dist/esm/routes/_layout/community._slug.js.map +1 -0
- package/dist/esm/routes/_layout/community.d.ts +23 -0
- package/dist/esm/routes/_layout/community.js +71 -0
- package/dist/esm/routes/_layout/community.js.map +1 -0
- package/dist/esm/ui/components/header/Header.js +7 -3
- package/dist/esm/ui/components/header/Header.js.map +1 -1
- package/dist/esm/util/highlightMatches.d.ts +2 -0
- package/dist/esm/util/highlightMatches.js +32 -0
- package/dist/esm/util/highlightMatches.js.map +1 -0
- package/dist/index.css +6 -0
- package/package.json +5 -5
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.enter.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.behindThisCard.test.tsx +4 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.knowMore.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +3 -0
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.feed.test.tsx +10 -3
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.tsx +4 -1
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.tsx +12 -2
- package/src/__tests__/modules/appCatalog/ui/detail/composeComments.test.ts +1 -0
- package/src/__tests__/modules/auth/AuthContext.test.tsx +55 -0
- package/src/__tests__/modules/communityTools/likedBy.test.ts +45 -0
- package/src/__tests__/modules/communityTools/timeAgo.test.ts +28 -0
- package/src/__tests__/modules/communityTools/useTimeUp.test.ts +31 -0
- package/src/__tests__/ui/components/header/Header.login.test.tsx +4 -4
- package/src/api/infra/trpc.ts +2 -0
- package/src/index.css +6 -0
- package/src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx +33 -176
- package/src/modules/appCatalog/ui/catalog/DetailDialogShell.tsx +199 -0
- package/src/modules/appCatalog/ui/components/ScreenshotGallery.tsx +2 -1
- package/src/modules/appCatalog/ui/components/SectionTabs.tsx +81 -0
- package/src/modules/appCatalog/ui/detail/AppDetails.tsx +17 -2
- package/src/modules/appCatalog/ui/detail/CommentsFeed.tsx +9 -4
- package/src/modules/appCatalog/ui/detail/CommentsHelp.tsx +6 -3
- package/src/modules/appCatalog/ui/detail/CommunityInput.tsx +15 -10
- package/src/modules/appCatalog/ui/detail/CommunitySection.tsx +10 -5
- package/src/modules/appCatalog/ui/detail/communityDraft.ts +16 -0
- package/src/modules/appCatalog/ui/detail/composeComments.ts +1 -0
- package/src/modules/appCatalog/ui/detail/useCommunityToolCount.ts +18 -0
- package/src/modules/appCatalog/ui/layout/AppCatalogLayout.tsx +2 -2
- package/src/modules/auth/AuthContext.tsx +9 -3
- package/src/modules/auth/ui/LoginModal.tsx +7 -2
- package/src/modules/communityTools/likedBy.ts +32 -0
- package/src/modules/communityTools/timeAgo.ts +16 -0
- package/src/modules/communityTools/ui/CardScreenshot.tsx +124 -0
- package/src/modules/communityTools/ui/CardTitleBlock.tsx +58 -0
- package/src/modules/communityTools/ui/CommunityPage.tsx +626 -0
- package/src/modules/communityTools/ui/FeedRow.tsx +310 -0
- package/src/modules/communityTools/ui/FeedSortControl.tsx +97 -0
- package/src/modules/communityTools/ui/LifecycleBadge.tsx +44 -0
- package/src/modules/communityTools/ui/LikeButton.tsx +56 -0
- package/src/modules/communityTools/ui/MarginPreview.tsx +143 -0
- package/src/modules/communityTools/ui/MyToolsShelf.tsx +112 -0
- package/src/modules/communityTools/ui/OwnerPicker.tsx +311 -0
- package/src/modules/communityTools/ui/PinButton.tsx +87 -0
- package/src/modules/communityTools/ui/ShelfTile.tsx +83 -0
- package/src/modules/communityTools/ui/TagPicker.tsx +118 -0
- package/src/modules/communityTools/ui/TagRow.tsx +118 -0
- package/src/modules/communityTools/ui/Toast.tsx +105 -0
- package/src/modules/communityTools/ui/ToolCard.tsx +519 -0
- package/src/modules/communityTools/ui/ToolFormDialog.tsx +922 -0
- package/src/modules/communityTools/ui/ToolSearch.tsx +111 -0
- package/src/modules/communityTools/useCommunityTools.ts +146 -0
- package/src/modules/communityTools/useCommunityToolsConfig.ts +26 -0
- package/src/modules/communityTools/useTimeUp.ts +14 -0
- package/src/routeTree.gen.ts +52 -0
- package/src/routes/_layout/community.$slug.tsx +7 -0
- package/src/routes/_layout/community.tsx +97 -0
- package/src/ui/components/header/Header.tsx +9 -3
- package/src/util/highlightMatches.tsx +34 -0
- package/dist/esm/modules/appCatalog/ui/components/ViewToggle.d.ts +0 -12
- package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js +0 -56
- package/dist/esm/modules/appCatalog/ui/components/ViewToggle.js.map +0 -1
- package/src/modules/appCatalog/ui/components/ViewToggle.tsx +0 -59
|
@@ -6,12 +6,13 @@ import { cn } from '~/lib/utils'
|
|
|
6
6
|
import { Button } from '~/ui/button'
|
|
7
7
|
import {
|
|
8
8
|
attachmentUrl,
|
|
9
|
+
draftSlugOf,
|
|
9
10
|
readAttachments,
|
|
10
11
|
readDraft,
|
|
11
12
|
writeAttachments,
|
|
12
13
|
writeDraft,
|
|
13
14
|
} from './communityDraft'
|
|
14
|
-
import type { PendingImage } from './communityDraft'
|
|
15
|
+
import type { CommentTarget, PendingImage } from './communityDraft'
|
|
15
16
|
import { formatCommunityError } from './communityError'
|
|
16
17
|
|
|
17
18
|
const MAX_HEIGHT = 200
|
|
@@ -71,20 +72,21 @@ async function upload(files: File[]): Promise<string[] | string> {
|
|
|
71
72
|
* paperclip, paste or drop, and a screenshot alone is a valid note.
|
|
72
73
|
*/
|
|
73
74
|
export function CommunityInput({
|
|
74
|
-
|
|
75
|
+
target,
|
|
75
76
|
appName,
|
|
76
77
|
ref,
|
|
77
78
|
}: {
|
|
78
|
-
|
|
79
|
+
target: CommentTarget
|
|
79
80
|
appName: string
|
|
80
81
|
ref?: React.Ref<CommunityInputHandle>
|
|
81
82
|
}) {
|
|
82
83
|
const trpc = useTRPC()
|
|
83
84
|
const queryClient = useQueryClient()
|
|
85
|
+
const draftSlug = draftSlugOf(target)
|
|
84
86
|
// Seeded from storage so a draft survives the tab panel unmounting under it.
|
|
85
|
-
const [text, setText] = React.useState(() => readDraft(
|
|
87
|
+
const [text, setText] = React.useState(() => readDraft(draftSlug))
|
|
86
88
|
const [images, setImages] = React.useState<PendingImage[]>(() =>
|
|
87
|
-
readAttachments(
|
|
89
|
+
readAttachments(draftSlug),
|
|
88
90
|
)
|
|
89
91
|
const [sent, setSent] = React.useState(false)
|
|
90
92
|
// Counts successful sends, so a click on Send hands focus back every time.
|
|
@@ -121,8 +123,11 @@ export function CommunityInput({
|
|
|
121
123
|
return () => mq.removeEventListener('change', handler)
|
|
122
124
|
}, [])
|
|
123
125
|
|
|
124
|
-
React.useEffect(() => writeDraft(
|
|
125
|
-
React.useEffect(
|
|
126
|
+
React.useEffect(() => writeDraft(draftSlug, text), [draftSlug, text])
|
|
127
|
+
React.useEffect(
|
|
128
|
+
() => writeAttachments(draftSlug, images),
|
|
129
|
+
[draftSlug, images],
|
|
130
|
+
)
|
|
126
131
|
|
|
127
132
|
// The class goes on the form, whose className React never rewrites, and
|
|
128
133
|
// is removed and re-added with a reflow between so a second call restarts
|
|
@@ -153,7 +158,7 @@ export function CommunityInput({
|
|
|
153
158
|
setSent(true)
|
|
154
159
|
setSends((n) => n + 1)
|
|
155
160
|
void queryClient.invalidateQueries({
|
|
156
|
-
queryKey: trpc.feedback.list.queryKey(
|
|
161
|
+
queryKey: trpc.feedback.list.queryKey(target),
|
|
157
162
|
})
|
|
158
163
|
},
|
|
159
164
|
}),
|
|
@@ -227,7 +232,7 @@ export function CommunityInput({
|
|
|
227
232
|
// Merge into both state and persisted draft so unmount restores it.
|
|
228
233
|
setImages((current) => {
|
|
229
234
|
const updated = [...current, ...newImages].slice(0, MAX_ATTACHMENTS)
|
|
230
|
-
writeAttachments(
|
|
235
|
+
writeAttachments(draftSlug, updated)
|
|
231
236
|
return updated
|
|
232
237
|
})
|
|
233
238
|
if (files.length > room)
|
|
@@ -242,7 +247,7 @@ export function CommunityInput({
|
|
|
242
247
|
if (!canSend || add.isPending || uploading) return
|
|
243
248
|
const body = text.trim()
|
|
244
249
|
add.mutate({
|
|
245
|
-
|
|
250
|
+
...target,
|
|
246
251
|
...(body ? { body } : {}),
|
|
247
252
|
...(images.length > 0 ? { attachmentIds: images.map((i) => i.id) } : {}),
|
|
248
253
|
})
|
|
@@ -3,6 +3,7 @@ import React from 'react'
|
|
|
3
3
|
import { CommentsFeed } from './CommentsFeed'
|
|
4
4
|
import { CommentsHelp } from './CommentsHelp'
|
|
5
5
|
import { CommunityInput } from './CommunityInput'
|
|
6
|
+
import type { CommentTarget } from './communityDraft'
|
|
6
7
|
import type { CommunityInputHandle } from './CommunityInput'
|
|
7
8
|
|
|
8
9
|
export type CommunitySectionHandle = CommunityInputHandle
|
|
@@ -15,12 +16,13 @@ const NO_CLAIMS: Claim[] = []
|
|
|
15
16
|
* as one feed.
|
|
16
17
|
*/
|
|
17
18
|
export function CommunitySection({
|
|
18
|
-
|
|
19
|
+
target,
|
|
19
20
|
appName,
|
|
20
21
|
claims = NO_CLAIMS,
|
|
21
22
|
ref,
|
|
22
23
|
}: {
|
|
23
|
-
|
|
24
|
+
target: CommentTarget
|
|
25
|
+
/** The entry's or the tool's name, for the input's label. */
|
|
24
26
|
appName: string
|
|
25
27
|
claims?: Claim[]
|
|
26
28
|
ref?: React.Ref<CommunitySectionHandle>
|
|
@@ -28,14 +30,17 @@ export function CommunitySection({
|
|
|
28
30
|
const headingId = React.useId()
|
|
29
31
|
return (
|
|
30
32
|
<section className="behind-group">
|
|
31
|
-
<CommunityInput ref={ref}
|
|
33
|
+
<CommunityInput ref={ref} target={target} appName={appName} />
|
|
32
34
|
<div className="comments-head">
|
|
33
35
|
<h2 id={headingId} className="behind-group__title">
|
|
34
36
|
Comments
|
|
35
37
|
</h2>
|
|
36
|
-
<CommentsHelp
|
|
38
|
+
<CommentsHelp
|
|
39
|
+
appSlug={'resourceSlug' in target ? target.resourceSlug : undefined}
|
|
40
|
+
appName={appName}
|
|
41
|
+
/>
|
|
37
42
|
</div>
|
|
38
|
-
<CommentsFeed
|
|
43
|
+
<CommentsFeed target={target} claims={claims} labelledBy={headingId} />
|
|
39
44
|
</section>
|
|
40
45
|
)
|
|
41
46
|
}
|
|
@@ -78,3 +78,19 @@ export function writeAttachments(slug: string, images: PendingImage[]): void {
|
|
|
78
78
|
export function attachmentUrl(id: string): string {
|
|
79
79
|
return `/api/feedback-attachments/${id}`
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
/** What a comment is about: an official catalog entry, or a community tool. */
|
|
83
|
+
export type CommentTarget =
|
|
84
|
+
| { resourceSlug: string }
|
|
85
|
+
| { communityToolSlug: string }
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The draft's storage slug. An entry keeps its bare slug, so drafts written
|
|
89
|
+
* before tools had comments still load; a tool's is prefixed, so a tool and an
|
|
90
|
+
* entry that share a slug never share a draft.
|
|
91
|
+
*/
|
|
92
|
+
export function draftSlugOf(target: CommentTarget): string {
|
|
93
|
+
return 'resourceSlug' in target
|
|
94
|
+
? target.resourceSlug
|
|
95
|
+
: `tool:${target.communityToolSlug}`
|
|
96
|
+
}
|
|
@@ -23,6 +23,7 @@ export interface CommentEntry {
|
|
|
23
23
|
const FEEDBACK_ORIGIN = 'feedback:'
|
|
24
24
|
|
|
25
25
|
function noteBadge(note: Note, linked: Claim[]): CommentBadge {
|
|
26
|
+
if (note.isSystem) return null
|
|
26
27
|
if (note.status === null) return 'pending'
|
|
27
28
|
if (linked.some((c) => c.status === 'review')) return 'pending'
|
|
28
29
|
if (note.status === 'acknowledged') return null
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { useTRPC } from '~/api/infra/trpc'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How many live community tools are tagged with this app. One query for the
|
|
6
|
+
* whole catalog, so switching cards asks nothing new. Zero while loading, and
|
|
7
|
+
* when the deployment has community tools turned off.
|
|
8
|
+
*/
|
|
9
|
+
export function useCommunityToolCount(appSlug: string): number {
|
|
10
|
+
const trpc = useTRPC()
|
|
11
|
+
const { data: config } = useQuery(trpc.communityTools.config.queryOptions())
|
|
12
|
+
const { data: counts } = useQuery(
|
|
13
|
+
trpc.communityTools.countByApp.queryOptions(undefined, {
|
|
14
|
+
enabled: config?.enabled === true,
|
|
15
|
+
}),
|
|
16
|
+
)
|
|
17
|
+
return counts?.[appSlug] ?? 0
|
|
18
|
+
}
|
|
@@ -3,7 +3,7 @@ import type { QueryClient } from '@tanstack/react-query'
|
|
|
3
3
|
import type { TRPCClient } from '@trpc/client'
|
|
4
4
|
import { AppCatalogProvider } from '~/modules/appCatalog/context/AppCatalogContext'
|
|
5
5
|
import { AppCatalogFiltersProvider } from '~/modules/appCatalog/ui/context/AppCatalogFiltersContext'
|
|
6
|
-
import {
|
|
6
|
+
import { SectionTabs } from '~/modules/appCatalog/ui/components/SectionTabs'
|
|
7
7
|
import { MainLayout } from '~/ui/layout/MainLayout'
|
|
8
8
|
import { TopLevelProviders } from '~/ui/layout/TopLevelProviders'
|
|
9
9
|
|
|
@@ -24,7 +24,7 @@ export function AppCatalogLayout({
|
|
|
24
24
|
<TopLevelProviders queryClient={queryClient} trpcClient={trpcClient}>
|
|
25
25
|
<AppCatalogProvider>
|
|
26
26
|
<AppCatalogFiltersProvider>
|
|
27
|
-
<MainLayout headerMiddle={headerMiddle ?? <
|
|
27
|
+
<MainLayout headerMiddle={headerMiddle ?? <SectionTabs />}>
|
|
28
28
|
{/* <Breadcrumb className="pb-4">*/}
|
|
29
29
|
{/* <BreadcrumbList>*/}
|
|
30
30
|
{/* <BreadcrumbItem>*/}
|
|
@@ -56,11 +56,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
|
56
56
|
if (response.ok) {
|
|
57
57
|
const data = await response.json()
|
|
58
58
|
setSession(data)
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
const userWithAdmin = data?.user
|
|
60
|
+
? {
|
|
61
|
+
...data.user,
|
|
62
|
+
isAdmin: data.isAdmin ?? data.user.isAdmin ?? false,
|
|
63
|
+
}
|
|
64
|
+
: null
|
|
65
|
+
setUser(userWithAdmin)
|
|
66
|
+
if (userWithAdmin) {
|
|
61
67
|
localStorage.setItem(
|
|
62
68
|
AUTH_USER_STORAGE_KEY,
|
|
63
|
-
JSON.stringify(
|
|
69
|
+
JSON.stringify(userWithAdmin),
|
|
64
70
|
)
|
|
65
71
|
} else {
|
|
66
72
|
localStorage.removeItem(AUTH_USER_STORAGE_KEY)
|
|
@@ -14,7 +14,7 @@ import { LoginPage } from './LoginPage'
|
|
|
14
14
|
* Can be opened from anywhere using useAuthModal hook
|
|
15
15
|
*/
|
|
16
16
|
export function LoginModal() {
|
|
17
|
-
const { isOpen, close } = useAuthModal()
|
|
17
|
+
const { isOpen, close, redirectUrl } = useAuthModal()
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
20
|
<Dialog
|
|
@@ -25,7 +25,12 @@ export function LoginModal() {
|
|
|
25
25
|
}
|
|
26
26
|
}}
|
|
27
27
|
>
|
|
28
|
-
<DialogContent
|
|
28
|
+
<DialogContent
|
|
29
|
+
className="w-full max-w-md"
|
|
30
|
+
data-testid="login-modal"
|
|
31
|
+
// Where the provider returns the visitor; readable in tests and devtools.
|
|
32
|
+
data-return-to={redirectUrl}
|
|
33
|
+
>
|
|
29
34
|
<DialogHeader>
|
|
30
35
|
<DialogTitle>Sign In</DialogTitle>
|
|
31
36
|
<DialogDescription>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CommunityTool } from '@igstack/app-catalog-shared-core'
|
|
2
|
+
|
|
3
|
+
/** How many likers the tooltip names; the rest are a count. */
|
|
4
|
+
const NAMED = 8
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* "Liked by you, A, B and 3 others": you first, then up to eight colleagues,
|
|
8
|
+
* newest first, then how many more.
|
|
9
|
+
*/
|
|
10
|
+
export function likedByText(
|
|
11
|
+
tool: Pick<CommunityTool, 'likes' | 'likedByMe' | 'likedBy'>,
|
|
12
|
+
): string {
|
|
13
|
+
if (tool.likes === 0) return 'No likes yet'
|
|
14
|
+
const names = [
|
|
15
|
+
...(tool.likedByMe ? ['you'] : []),
|
|
16
|
+
...tool.likedBy.slice(0, NAMED),
|
|
17
|
+
]
|
|
18
|
+
const others = tool.likes - names.length
|
|
19
|
+
if (others > 0) {
|
|
20
|
+
return `Liked by ${names.join(', ')} and ${others} ${others === 1 ? 'other' : 'others'}`
|
|
21
|
+
}
|
|
22
|
+
const last = names.pop()
|
|
23
|
+
return names.length > 0
|
|
24
|
+
? `Liked by ${names.join(', ')} and ${last}`
|
|
25
|
+
: `Liked by ${last}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The tool as it reads once I liked it, or took the like back. */
|
|
29
|
+
export function withLike(tool: CommunityTool, liked: boolean): CommunityTool {
|
|
30
|
+
if (tool.likedByMe === liked) return tool
|
|
31
|
+
return { ...tool, likedByMe: liked, likes: tool.likes + (liked ? 1 : -1) }
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
2
|
+
|
|
3
|
+
/** How long ago, in calendar days and up: "today", "3 days ago", "2 months ago". */
|
|
4
|
+
export function timeAgo(iso: string, now: Date = new Date()): string {
|
|
5
|
+
const day = (d: Date) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())
|
|
6
|
+
const days = Math.max(0, Math.round((day(now) - day(new Date(iso))) / DAY_MS))
|
|
7
|
+
if (days === 0) return 'today'
|
|
8
|
+
if (days === 1) return 'yesterday'
|
|
9
|
+
if (days < 31) return `${days} days ago`
|
|
10
|
+
if (days < 365) {
|
|
11
|
+
const months = Math.floor(days / 30)
|
|
12
|
+
return months === 1 ? '1 month ago' : `${months} months ago`
|
|
13
|
+
}
|
|
14
|
+
const years = Math.floor(days / 365)
|
|
15
|
+
return years === 1 ? '1 year ago' : `${years} years ago`
|
|
16
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { CommunityTool } from '@igstack/app-catalog-shared-core'
|
|
2
|
+
import { Camera, ExternalLink, ImageOff, Maximize2 } from 'lucide-react'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { ScreenshotGallery } from '~/modules/appCatalog/ui/components/ScreenshotGallery'
|
|
5
|
+
import { cn } from '~/lib/utils'
|
|
6
|
+
import { screenshotUrl } from './FeedRow'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The 16:10 box every state shares, so swapping one for another moves nothing.
|
|
10
|
+
* Capped in height, so how to use it and who to ask stay above the fold; the
|
|
11
|
+
* top of the page, where its title is, is what shows.
|
|
12
|
+
*/
|
|
13
|
+
const BOX =
|
|
14
|
+
'relative flex aspect-[16/10] max-h-[320px] w-full items-center justify-center overflow-hidden rounded-[14px] border border-border bg-muted shadow-sm'
|
|
15
|
+
|
|
16
|
+
export function formatCardDate(iso: string): string {
|
|
17
|
+
return new Date(iso).toLocaleDateString('en-GB', {
|
|
18
|
+
day: 'numeric',
|
|
19
|
+
month: 'short',
|
|
20
|
+
year: 'numeric',
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The tool's screenshot, full width: a click opens the site, like the title
|
|
26
|
+
* block, and the corner button enlarges it in the screenshot viewer. A tool
|
|
27
|
+
* with none yet shows a placeholder in the same box, not a link: "still
|
|
28
|
+
* capturing" while its first visit is to come, otherwise "no screenshot".
|
|
29
|
+
*/
|
|
30
|
+
export function CardScreenshot({
|
|
31
|
+
tool,
|
|
32
|
+
capturing,
|
|
33
|
+
}: {
|
|
34
|
+
tool: CommunityTool
|
|
35
|
+
/** A first screenshot is on its way and still worth waiting for. */
|
|
36
|
+
capturing: boolean
|
|
37
|
+
}) {
|
|
38
|
+
const [open, setOpen] = useState(false)
|
|
39
|
+
const { screenshotId, capturedAt } = tool
|
|
40
|
+
|
|
41
|
+
if (!screenshotId) {
|
|
42
|
+
return (
|
|
43
|
+
<figure data-testid="card-screenshot" className="mt-[22px]">
|
|
44
|
+
<div
|
|
45
|
+
data-testid="card-screenshot-box"
|
|
46
|
+
data-state={capturing ? 'capturing' : 'missing'}
|
|
47
|
+
aria-busy={capturing || undefined}
|
|
48
|
+
className={cn(
|
|
49
|
+
BOX,
|
|
50
|
+
'flex-col gap-2.5 px-6 text-center text-sm font-semibold text-muted-foreground',
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
{capturing ? (
|
|
54
|
+
<>
|
|
55
|
+
<Camera aria-hidden="true" className="size-7 animate-pulse" />
|
|
56
|
+
Taking a screenshot of the site…
|
|
57
|
+
</>
|
|
58
|
+
) : (
|
|
59
|
+
<>
|
|
60
|
+
<ImageOff aria-hidden="true" className="size-7" />
|
|
61
|
+
No screenshot. The site may need a login, or it did not load.
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
<figcaption className="mt-2 text-[12.5px] font-semibold text-muted-foreground">
|
|
66
|
+
{capturing ? 'Usually under a minute' : 'Open the site to see it'}
|
|
67
|
+
</figcaption>
|
|
68
|
+
</figure>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<figure data-testid="card-screenshot" className="mt-[22px]">
|
|
74
|
+
<div
|
|
75
|
+
data-testid="card-screenshot-box"
|
|
76
|
+
data-state="captured"
|
|
77
|
+
className={BOX}
|
|
78
|
+
>
|
|
79
|
+
<a
|
|
80
|
+
href={tool.url}
|
|
81
|
+
target="_blank"
|
|
82
|
+
rel="noopener noreferrer"
|
|
83
|
+
aria-label={`Open ${tool.name} in a new tab`}
|
|
84
|
+
data-testid="card-screenshot-link"
|
|
85
|
+
className="group/shot block size-full rounded-[14px] focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-ring"
|
|
86
|
+
>
|
|
87
|
+
<img
|
|
88
|
+
src={screenshotUrl(screenshotId, 1280)}
|
|
89
|
+
alt={`Screenshot of ${tool.name}`}
|
|
90
|
+
className="size-full object-cover object-left-top"
|
|
91
|
+
/>
|
|
92
|
+
<span className="absolute inset-0 flex items-center justify-center bg-black/0 transition-colors group-focus-visible/shot:bg-black/20 group-hover/shot:bg-black/20">
|
|
93
|
+
<span className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-3.5 py-2 text-sm font-extrabold text-primary-foreground opacity-0 shadow-sm transition-opacity group-focus-visible/shot:opacity-100 group-hover/shot:opacity-100">
|
|
94
|
+
Open site
|
|
95
|
+
<ExternalLink aria-hidden="true" className="size-4" />
|
|
96
|
+
</span>
|
|
97
|
+
</span>
|
|
98
|
+
</a>
|
|
99
|
+
{/* Beside the link, not in it: enlarging never opens the site. */}
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
aria-label={`Enlarge screenshot of ${tool.name}`}
|
|
103
|
+
title="Enlarge"
|
|
104
|
+
onClick={() => setOpen(true)}
|
|
105
|
+
className="absolute top-3 right-3 inline-flex size-8 items-center justify-center rounded-lg bg-black/65 text-white hover:bg-black/80 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
106
|
+
>
|
|
107
|
+
<Maximize2 aria-hidden="true" className="size-4" />
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
{capturedAt && (
|
|
111
|
+
<figcaption className="mt-2 text-[12.5px] font-semibold text-muted-foreground">
|
|
112
|
+
Screenshot captured {formatCardDate(capturedAt)}
|
|
113
|
+
</figcaption>
|
|
114
|
+
)}
|
|
115
|
+
<ScreenshotGallery
|
|
116
|
+
app={{ displayName: tool.name }}
|
|
117
|
+
screenshotIds={[screenshotId]}
|
|
118
|
+
open={open}
|
|
119
|
+
onOpenChange={setOpen}
|
|
120
|
+
title={`${tool.name} screenshot`}
|
|
121
|
+
/>
|
|
122
|
+
</figure>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CommunityTool } from '@igstack/app-catalog-shared-core'
|
|
2
|
+
import { ExternalLink, Globe } from 'lucide-react'
|
|
3
|
+
import { cn } from '~/lib/utils'
|
|
4
|
+
import { screenshotUrl } from './FeedRow'
|
|
5
|
+
import { LifecycleBadge, UnreviewedBadge } from './LifecycleBadge'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The card's door: favicon (a globe when the site has none), name, badges and
|
|
9
|
+
* address, all one link that opens the site in a new tab.
|
|
10
|
+
*/
|
|
11
|
+
export function CardTitleBlock({ tool }: { tool: CommunityTool }) {
|
|
12
|
+
const archived = tool.lifecycle.state === 'archived'
|
|
13
|
+
return (
|
|
14
|
+
<a
|
|
15
|
+
href={tool.url}
|
|
16
|
+
target="_blank"
|
|
17
|
+
rel="noopener noreferrer"
|
|
18
|
+
data-testid="card-title-block"
|
|
19
|
+
className={cn(
|
|
20
|
+
'group/door inline-flex min-w-0 items-center gap-[15px] rounded-[14px] border border-border bg-primary/[0.025] px-[17px] py-[11px] transition-colors',
|
|
21
|
+
'hover:border-primary/40 hover:bg-primary/[0.07] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
|
|
22
|
+
archived && 'opacity-70 grayscale',
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
<span className="inline-flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-border bg-card text-muted-foreground">
|
|
26
|
+
{tool.faviconId ? (
|
|
27
|
+
<img
|
|
28
|
+
src={screenshotUrl(tool.faviconId, 64)}
|
|
29
|
+
alt=""
|
|
30
|
+
className="size-7"
|
|
31
|
+
/>
|
|
32
|
+
) : (
|
|
33
|
+
<Globe aria-hidden="true" className="size-6" strokeWidth={1.75} />
|
|
34
|
+
)}
|
|
35
|
+
</span>
|
|
36
|
+
<span className="flex min-w-0 flex-col gap-0.5">
|
|
37
|
+
<span className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
38
|
+
<span
|
|
39
|
+
data-testid="card-tool-name"
|
|
40
|
+
className="font-serif text-[26px] leading-tight font-semibold"
|
|
41
|
+
>
|
|
42
|
+
{tool.name}
|
|
43
|
+
</span>
|
|
44
|
+
<LifecycleBadge lifecycle={tool.lifecycle} />
|
|
45
|
+
{!tool.reviewed && <UnreviewedBadge />}
|
|
46
|
+
</span>
|
|
47
|
+
<span className="truncate font-mono text-[13px] text-muted-foreground">
|
|
48
|
+
{tool.host}
|
|
49
|
+
</span>
|
|
50
|
+
<span className="sr-only">(opens in a new tab)</span>
|
|
51
|
+
</span>
|
|
52
|
+
<ExternalLink
|
|
53
|
+
aria-hidden="true"
|
|
54
|
+
className="size-[18px] shrink-0 self-start text-muted-foreground group-hover/door:text-primary"
|
|
55
|
+
/>
|
|
56
|
+
</a>
|
|
57
|
+
)
|
|
58
|
+
}
|