@igstack/app-catalog-frontend-core 6.12.0 → 6.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/api/infra/trpc.d.ts +27 -0
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +0 -1
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +0 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/communityTools/siteOpen.d.ts +10 -0
- package/dist/esm/modules/communityTools/siteOpen.js +18 -0
- package/dist/esm/modules/communityTools/siteOpen.js.map +1 -0
- package/dist/esm/modules/communityTools/timeAgo.d.ts +2 -0
- package/dist/esm/modules/communityTools/timeAgo.js +11 -0
- package/dist/esm/modules/communityTools/timeAgo.js.map +1 -1
- package/dist/esm/modules/communityTools/ui/CardScreenshot.js +10 -12
- package/dist/esm/modules/communityTools/ui/CardScreenshot.js.map +1 -1
- package/dist/esm/modules/communityTools/ui/CardTitleBlock.d.ts +1 -1
- package/dist/esm/modules/communityTools/ui/CardTitleBlock.js +5 -13
- package/dist/esm/modules/communityTools/ui/CardTitleBlock.js.map +1 -1
- package/dist/esm/modules/communityTools/ui/CommunityPage.js +2 -4
- package/dist/esm/modules/communityTools/ui/CommunityPage.js.map +1 -1
- package/dist/esm/modules/communityTools/ui/FeedRow.d.ts +6 -3
- package/dist/esm/modules/communityTools/ui/FeedRow.js +14 -8
- package/dist/esm/modules/communityTools/ui/FeedRow.js.map +1 -1
- package/dist/esm/modules/communityTools/ui/ShelfTile.js +4 -7
- package/dist/esm/modules/communityTools/ui/ShelfTile.js.map +1 -1
- package/dist/esm/modules/communityTools/ui/ToolCard.js +15 -3
- package/dist/esm/modules/communityTools/ui/ToolCard.js.map +1 -1
- package/dist/esm/modules/communityTools/ui/ToolFormDialog.js +0 -1
- package/dist/esm/modules/communityTools/ui/ToolFormDialog.js.map +1 -1
- package/dist/esm/modules/communityTools/useCommunityTools.d.ts +2 -0
- package/dist/esm/modules/communityTools/useCommunityTools.js +0 -1
- package/dist/esm/modules/communityTools/useCommunityTools.js.map +1 -1
- package/package.json +4 -4
- package/src/modules/communityTools/siteOpen.ts +20 -0
- package/src/modules/communityTools/timeAgo.ts +12 -0
- package/src/modules/communityTools/ui/CardScreenshot.tsx +21 -9
- package/src/modules/communityTools/ui/CardTitleBlock.tsx +4 -9
- package/src/modules/communityTools/ui/CommunityPage.tsx +2 -5
- package/src/modules/communityTools/ui/FeedRow.tsx +19 -15
- package/src/modules/communityTools/ui/ShelfTile.tsx +4 -5
- package/src/modules/communityTools/ui/ToolCard.tsx +11 -1
|
@@ -133,6 +133,15 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
133
133
|
};
|
|
134
134
|
meta: object;
|
|
135
135
|
}>;
|
|
136
|
+
recordSiteOpen: import('@trpc/server').TRPCMutationProcedure<{
|
|
137
|
+
input: {
|
|
138
|
+
slug: string;
|
|
139
|
+
};
|
|
140
|
+
output: {
|
|
141
|
+
ok: boolean;
|
|
142
|
+
};
|
|
143
|
+
meta: object;
|
|
144
|
+
}>;
|
|
136
145
|
pins: import('@trpc/server').TRPCQueryProcedure<{
|
|
137
146
|
input: void;
|
|
138
147
|
output: import('@igstack/app-catalog-shared-core').CommunityTool[];
|
|
@@ -395,6 +404,15 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
395
404
|
};
|
|
396
405
|
meta: object;
|
|
397
406
|
}>;
|
|
407
|
+
recordSiteOpen: import('@trpc/server').TRPCMutationProcedure<{
|
|
408
|
+
input: {
|
|
409
|
+
slug: string;
|
|
410
|
+
};
|
|
411
|
+
output: {
|
|
412
|
+
ok: boolean;
|
|
413
|
+
};
|
|
414
|
+
meta: object;
|
|
415
|
+
}>;
|
|
398
416
|
pins: import('@trpc/server').TRPCQueryProcedure<{
|
|
399
417
|
input: void;
|
|
400
418
|
output: import('@igstack/app-catalog-shared-core').CommunityTool[];
|
|
@@ -656,6 +674,15 @@ declare const TRPCProvider: import('react').FunctionComponent<{
|
|
|
656
674
|
};
|
|
657
675
|
meta: object;
|
|
658
676
|
}>;
|
|
677
|
+
recordSiteOpen: import('@trpc/server').TRPCMutationProcedure<{
|
|
678
|
+
input: {
|
|
679
|
+
slug: string;
|
|
680
|
+
};
|
|
681
|
+
output: {
|
|
682
|
+
ok: boolean;
|
|
683
|
+
};
|
|
684
|
+
meta: object;
|
|
685
|
+
}>;
|
|
659
686
|
pins: import('@trpc/server').TRPCQueryProcedure<{
|
|
660
687
|
input: void;
|
|
661
688
|
output: import('@igstack/app-catalog-shared-core').CommunityTool[];
|
|
@@ -4,7 +4,6 @@ import { useAppCatalogContext } from "../../context/AppCatalogContext.js";
|
|
|
4
4
|
import { useOptionalUser } from "../../../auth/AuthContext.js";
|
|
5
5
|
import "@tanstack/react-query";
|
|
6
6
|
import "../../../../ui/button.js";
|
|
7
|
-
import "../../../auth/AuthModalContext.js";
|
|
8
7
|
import { getChildResources } from "../../utils/resolveHelpers.js";
|
|
9
8
|
import { SubResourceDetailPanel } from "../components/SubResourceDetailPanel.js";
|
|
10
9
|
import { AppDetails } from "../detail/AppDetails.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { useMemo } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailProvider,\n} from '~/modules/extensions'\nimport { useOptionalUser } from '~/modules/auth'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'\nimport { AppDetails } from '../detail/AppDetails'\nimport { DetailDialogShell } from './DetailDialogShell'\n\n/**\n * Centered detail card over the catalog (#38, item B). Renders the rich\n * AppDetails (access-hero, sub-resources, tiers) as a large centered modal card\n * over the catalog backdrop.\n *\n * When a sub-resource is selected (`?sub=<slug>` — e.g. the user clicked a\n * matched sub-resource row in the search results) the card shows that\n * sub-resource's own detail, with its two-step access chain and a back link to\n * the parent.\n *\n * The overlay itself — scrim, centring, Widen, Close, Escape — is\n * `DetailDialogShell`, shared with the community tool card.\n */\nexport function AppDetailPanel({\n app,\n subResource,\n onClose,\n onAppClick,\n onBackToParent,\n}: {\n app: Resource\n /** Selected child of `app`, when the URL carries `?sub=<slug>`. */\n subResource?: Resource | null\n onClose: () => void\n onAppClick?: (app: Resource) => void\n /** Clears `?sub=` and returns to the parent's detail. */\n onBackToParent?: () => void\n}) {\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const user = useOptionalUser()\n\n // The sub-resource branch has no band of its own, so its slot payload is\n // assembled here, where the catalog context already is, and handed down.\n const subResourceActionsCtx = useMemo(\n () => ({\n resource: subResource ?? app,\n parent: subResource ? app : undefined,\n subResources: getChildResources(allResources, app.slug),\n user: user ? { email: user.email, isAdmin: user.isAdmin ?? false } : null,\n }),\n [subResource, app, allResources, user],\n )\n const shownSlug = subResource?.slug ?? app.slug\n\n return (\n <DetailDialogShell\n label={`${subResource?.displayName ?? app.displayName} details`}\n shownKey={shownSlug}\n // Wide enough for data tables (sub-resources have 5 columns incl.\n // \"Access Contacts\"/\"AWS Account\" that wrapped at 760px); prose inside\n // AppDetails is width-capped separately so it stays readable. Caps at\n // 94vw so it never touches the edges.\n maxWidthClass=\"max-w-[min(1120px,94vw)]\"\n focusSelector=\".app-door\"\n onClose={onClose}\n >\n {/* Inside the key, so anything a plugin provides here is scoped to\n the resource on screen and is discarded when the card navigates.\n Renders its children untouched when no plugin contributes. */}\n <ResourceDetailProvider\n resource={subResource ?? app}\n parent={subResource ? app : undefined}\n >\n {subResource ? (\n <SubResourceDetailPanel\n subResource={subResource}\n parent={app}\n approvalMethods={approvalMethods}\n onBack={onBackToParent ?? onClose}\n actions={<ResourceDetailAccessActions {...subResourceActionsCtx} />}\n />\n ) : (\n <AppDetails app={app} onAppClick={onAppClick} />\n )}\n </ResourceDetailProvider>\n </DetailDialogShell>\n )\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AppDetailPanel.js","sources":["../../../../../../src/modules/appCatalog/ui/catalog/AppDetailPanel.tsx"],"sourcesContent":["import type { Resource } from '@igstack/app-catalog-backend-core'\nimport { useMemo } from 'react'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailProvider,\n} from '~/modules/extensions'\nimport { useOptionalUser } from '~/modules/auth'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport { SubResourceDetailPanel } from '../components/SubResourceDetailPanel'\nimport { AppDetails } from '../detail/AppDetails'\nimport { DetailDialogShell } from './DetailDialogShell'\n\n/**\n * Centered detail card over the catalog (#38, item B). Renders the rich\n * AppDetails (access-hero, sub-resources, tiers) as a large centered modal card\n * over the catalog backdrop.\n *\n * When a sub-resource is selected (`?sub=<slug>` — e.g. the user clicked a\n * matched sub-resource row in the search results) the card shows that\n * sub-resource's own detail, with its two-step access chain and a back link to\n * the parent.\n *\n * The overlay itself — scrim, centring, Widen, Close, Escape — is\n * `DetailDialogShell`, shared with the community tool card.\n */\nexport function AppDetailPanel({\n app,\n subResource,\n onClose,\n onAppClick,\n onBackToParent,\n}: {\n app: Resource\n /** Selected child of `app`, when the URL carries `?sub=<slug>`. */\n subResource?: Resource | null\n onClose: () => void\n onAppClick?: (app: Resource) => void\n /** Clears `?sub=` and returns to the parent's detail. */\n onBackToParent?: () => void\n}) {\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const user = useOptionalUser()\n\n // The sub-resource branch has no band of its own, so its slot payload is\n // assembled here, where the catalog context already is, and handed down.\n const subResourceActionsCtx = useMemo(\n () => ({\n resource: subResource ?? app,\n parent: subResource ? app : undefined,\n subResources: getChildResources(allResources, app.slug),\n user: user ? { email: user.email, isAdmin: user.isAdmin ?? false } : null,\n }),\n [subResource, app, allResources, user],\n )\n const shownSlug = subResource?.slug ?? app.slug\n\n return (\n <DetailDialogShell\n label={`${subResource?.displayName ?? app.displayName} details`}\n shownKey={shownSlug}\n // Wide enough for data tables (sub-resources have 5 columns incl.\n // \"Access Contacts\"/\"AWS Account\" that wrapped at 760px); prose inside\n // AppDetails is width-capped separately so it stays readable. Caps at\n // 94vw so it never touches the edges.\n maxWidthClass=\"max-w-[min(1120px,94vw)]\"\n focusSelector=\".app-door\"\n onClose={onClose}\n >\n {/* Inside the key, so anything a plugin provides here is scoped to\n the resource on screen and is discarded when the card navigates.\n Renders its children untouched when no plugin contributes. */}\n <ResourceDetailProvider\n resource={subResource ?? app}\n parent={subResource ? app : undefined}\n >\n {subResource ? (\n <SubResourceDetailPanel\n subResource={subResource}\n parent={app}\n approvalMethods={approvalMethods}\n onBack={onBackToParent ?? onClose}\n actions={<ResourceDetailAccessActions {...subResourceActionsCtx} />}\n />\n ) : (\n <AppDetails app={app} onAppClick={onAppClick} />\n )}\n </ResourceDetailProvider>\n </DetailDialogShell>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;AA0BO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,EAAE,iBAAiB,WAAW,aAAA,IAAiB,qBAAA;AACrD,QAAM,OAAO,gBAAA;AAIb,QAAM,wBAAwB;AAAA,IAC5B,OAAO;AAAA,MACL,UAAU,eAAe;AAAA,MACzB,QAAQ,cAAc,MAAM;AAAA,MAC5B,cAAc,kBAAkB,cAAc,IAAI,IAAI;AAAA,MACtD,MAAM,OAAO,EAAE,OAAO,KAAK,OAAO,SAAS,KAAK,WAAW,UAAU;AAAA,IAAA;AAAA,IAEvE,CAAC,aAAa,KAAK,cAAc,IAAI;AAAA,EAAA;AAEvC,QAAM,aAAY,2CAAa,SAAQ,IAAI;AAE3C,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,OAAO,IAAG,2CAAa,gBAAe,IAAI,WAAW;AAAA,MACrD,UAAU;AAAA,MAKV,eAAc;AAAA,MACd,eAAc;AAAA,MACd;AAAA,MAKA,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,UAAU,eAAe;AAAA,UACzB,QAAQ,cAAc,MAAM;AAAA,UAE3B,UAAA,cACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC;AAAA,cACA,QAAQ;AAAA,cACR;AAAA,cACA,QAAQ,kBAAkB;AAAA,cAC1B,SAAS,oBAAC,6BAAA,EAA6B,GAAG,sBAAA,CAAuB;AAAA,YAAA;AAAA,UAAA,IAGnE,oBAAC,YAAA,EAAW,KAAU,WAAA,CAAwB;AAAA,QAAA;AAAA,MAAA;AAAA,IAElD;AAAA,EAAA;AAGN;"}
|
|
@@ -12,7 +12,6 @@ import { useAppCatalogFilters } from "../context/AppCatalogFiltersContext.js";
|
|
|
12
12
|
import { PersonBadge } from "../components/PersonBadge.js";
|
|
13
13
|
import { useUser } from "../../../auth/AuthContext.js";
|
|
14
14
|
import "@tanstack/react-query";
|
|
15
|
-
import "../../../auth/AuthModalContext.js";
|
|
16
15
|
import { InlineEditableField } from "../components/InlineEditableField.js";
|
|
17
16
|
import { MarkdownText } from "../components/MarkdownText.js";
|
|
18
17
|
import { ScreenshotGallery } from "../components/ScreenshotGallery.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppDetails.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/AppDetails.tsx"],"sourcesContent":["import type {\n Resource,\n SourceReference,\n} from '@igstack/app-catalog-backend-core'\nimport { Link, useSearch } from '@tanstack/react-router'\nimport { ExternalLink, Plus, Trash2 } from 'lucide-react'\nimport React from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { AccessPrerequisiteChain } from '../components/AccessPrerequisiteChain'\nimport { QuickJumpBar, hasQuickJumps } from '../components/QuickJumpBar'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { PersonBadge } from '../components/PersonBadge'\nimport { useUser } from '~/modules/auth'\nimport { InlineEditableField } from '../components/InlineEditableField'\nimport { MarkdownText } from '../components/MarkdownText'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { SourcePulse } from '../components/SourcePulse'\nimport { useUpdateApp } from '../../hooks/useUpdateApp'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { formatRelativeTime } from '../../utils/formatRelativeTime'\nimport { TierVariantsSection } from '../components/TierVariantsSection'\nimport { SubResourcesSection } from '../components/SubResourcesSection'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailCardSource,\n} from '~/modules/extensions'\nimport { CommunitySection } from './CommunitySection'\nimport type { CommunitySectionHandle } from './CommunitySection'\nimport { usePendingCount } from './usePendingCount'\nimport { useCommunityToolCount } from './useCommunityToolCount'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\nimport { accessTabLabel } from '~/modules/appCatalog/utils/accessTabLabel'\nimport { AppDoor } from './AppDoor'\nimport { DetailTabs } from './DetailTabs'\nimport type { DetailTab } from './DetailTabs'\n\n/** Tab ids. Public — they land in `?tab=`, so renaming one breaks links. */\nconst TAB = {\n overview: 'overview',\n access: 'access',\n resources: 'resources',\n documentation: 'documentation',\n behind: 'behind',\n} as const\n\n/**\n * One of Behind this card's top-level groups. A level above `Section`: its\n * title is a plain heading, and a hairline separates it from the group before.\n */\nfunction Group({\n title,\n children,\n}: {\n title: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"behind-group\">\n <h2 className=\"behind-group__title\">{title}</h2>\n {children}\n </section>\n )\n}\n\n/** A labelled block inside a tab. One register for all of them. */\nfunction Section({\n title,\n hint,\n first,\n children,\n}: {\n title: string\n hint?: string\n /** Drops the top margin for the first block in a panel. */\n first?: boolean\n children: React.ReactNode\n}) {\n return (\n <div className={first ? '' : 'mt-7'}>\n <h3 className=\"section-label mb-1.5\">{title}</h3>\n {hint && <p className=\"mb-2 text-xs text-muted-foreground\">{hint}</p>}\n {children}\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: Resource }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.abbreviation || app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nexport function AppDetails({\n app,\n onAppClick,\n}: {\n app: Resource\n onAppClick?: (app: Resource) => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n const updateApp = useUpdateApp()\n const [draftSource, setDraftSource] = React.useState<string | null>(null)\n const user = useUser()\n const isAdmin = user?.isAdmin ?? false\n const { state: filterState } = useAppCatalogFilters()\n // Agrees with the Pending badges in the feed: composed by the same rules.\n const behindCount = usePendingCount(app.slug, app.claims)\n const communityCount = useCommunityToolCount(app.slug)\n\n // \"Last checked 10 hours ago · added 7 months ago\" under the sources.\n const lastChecked = app.freshness?.lastCheckedAt\n const dateParts = []\n if (lastChecked) {\n const suffix = app.freshness?.isStale ? ' (may be out of date)' : ''\n dateParts.push(`Last checked ${formatRelativeTime(lastChecked)}${suffix}`)\n }\n if (app.createdAt) {\n const addedLabel = dateParts.length > 0 ? 'added' : 'Added'\n dateParts.push(`${addedLabel} ${formatRelativeTime(app.createdAt)}`)\n }\n const datesLine = dateParts.join(' · ')\n const datesTitle = [\n app.freshness?.lastContentChangeAt &&\n `Content last changed ${app.freshness.lastContentChangeAt}`,\n lastChecked && `Last checked ${lastChecked}`,\n app.createdAt && `Added ${app.createdAt}`,\n ]\n .filter(Boolean)\n .join(' · ')\n\n // \"Share what you know\" asks for the input by bumping this. A counter, not a\n // flag, so a second press while already on the tab still re-focuses. The\n // effect runs after the click's render has mounted the tab's panel, so the\n // ref is attached by then; opening the tab by hand bumps nothing.\n const knowMoreRef = React.useRef<CommunitySectionHandle>(null)\n const [shareAsk, setShareAsk] = React.useState(0)\n React.useEffect(() => {\n if (shareAsk) knowMoreRef.current?.focus()\n }, [shareAsk])\n\n // Arriving with `?sub=` means the visitor picked one child out of a search —\n // so the card has to open ON that child's tab. Landing on Overview and\n // leaving them to find the tab is the bug this scenario exists to prevent.\n const search = useSearch({ strict: false })\n const subSlug = search.sub\n\n const displayTags = (app.tags ?? []).filter((tag) => !tag.includes(':'))\n const sourceUrls: string[] =\n app.sources?.map((s) => (typeof s === 'string' ? s : s.url)) ?? []\n const displaySources =\n draftSource !== null ? [...sourceUrls, draftSource] : sourceUrls\n // The list renders bare URLs (they are editable as text), so the pulse has to\n // be looked up rather than carried alongside. Absent until the autoupdate loop\n // has seen the source — a freshly added one simply gets no mark.\n const pulseByUrl = React.useMemo(() => {\n const byUrl = new Map<string, NonNullable<SourceReference['pulse']>>()\n for (const source of app.sources ?? []) {\n if (typeof source !== 'string' && source.pulse) {\n byUrl.set(source.url, source.pulse)\n }\n }\n return byUrl\n }, [app.sources])\n\n const children = React.useMemo(\n () => getChildResources(allResources, app.slug),\n [allResources, app.slug],\n )\n\n // `app` is whatever resource the card shows, a nested one included, so the\n // parent has to be looked up rather than assumed absent. A plugin uses its\n // presence to tell a sub-resource view from a top-level one.\n const parent = React.useMemo(\n () =>\n app.parentSlug\n ? allResources.find((r) => r.slug === app.parentSlug)\n : undefined,\n [allResources, app.parentSlug],\n )\n\n // What a plugin slot in the band receives. Memoised so a plugin that\n // memoises on its props is not defeated by a new object every render.\n const accessActionsCtx = React.useMemo(\n () => ({\n resource: app,\n parent,\n subResources: parent\n ? getChildResources(allResources, parent.slug)\n : children,\n user: user ? { email: user.email, isAdmin } : null,\n }),\n [app, parent, allResources, children, user, isAdmin],\n )\n\n // Enter: open screenshot gallery. Only reached on a card with no door — where\n // there is one it holds focus, and Enter there is the link's own.\n useHotkeys(\n 'enter',\n () => {\n const tag = document.activeElement?.tagName\n if (\n tag === 'BUTTON' ||\n tag === 'A' ||\n tag === 'INPUT' ||\n tag === 'SELECT' ||\n tag === 'TEXTAREA'\n )\n return\n\n if (app.screenshotIds && app.screenshotIds.length > 0) {\n setGalleryInitialIndex(0)\n setIsGalleryOpen(true)\n }\n },\n { enabled: !isGalleryOpen },\n [app, isGalleryOpen],\n )\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? allResources.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n const access = accessTabLabel(app, approvalMethods)\n\n const hasResourcesTab = children.length > 0 || (app.tiers?.length ?? 0) > 0\n const resourcesLabel = children.length\n ? (app.childrenLabel ?? 'Resources')\n : 'Environments'\n\n const hasDocumentationTab = (app.links?.length ?? 0) > 0 || !!app.background\n\n const tabs: DetailTab[] = [\n {\n id: TAB.overview,\n label: 'Overview',\n render: () => (\n <>\n <Section title=\"Description\" first>\n {isAdmin ? (\n <InlineEditableField\n value={app.description ?? ''}\n onSave={(description) =>\n updateApp.mutate({ id: app.id, data: { description } })\n }\n multiline\n placeholder=\"Description\"\n className=\"min-h-[4rem] resize-y text-sm text-muted-foreground\"\n />\n ) : app.description ? (\n <MarkdownText className=\"prose prose-sm max-w-none text-sm text-muted-foreground [&_p]:m-0\">\n {app.description}\n </MarkdownText>\n ) : (\n <p className=\"text-sm text-muted-foreground\">—</p>\n )}\n </Section>\n\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <Section title={`Screenshots (${app.screenshotIds.length})`}>\n {/* A button, not a clickable div: the gallery was unreachable by\n keyboard, and a role + label gives it a stable handle. */}\n <button\n type=\"button\"\n aria-label={`View screenshots of ${app.displayName}`}\n className=\"block w-full cursor-pointer text-left hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </button>\n </Section>\n )}\n\n {/* Owner — who is responsible for this resource. Distinct from the\n access approver (who decides access requests); see domain model. */}\n {app.ownerPersonSlug && (\n <Section title=\"Owner\" hint=\"Who is responsible for this resource\">\n <PersonBadge slug={app.ownerPersonSlug} />\n </Section>\n )}\n\n {/* Tags, minus the indexing machinery: `namespace:value` tags drive\n grouping, faceting and placement, are about half of all tag\n references, and say nothing about what an app is for. The colon is\n the test. Search still matches them — only the display drops them. */}\n {displayTags.length > 0 && (\n <Section title=\"Tags\">\n <div className=\"flex flex-wrap gap-2\">\n {displayTags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n\n {app.teams && app.teams.length > 0 && (\n <Section title=\"Teams\">\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n </>\n ),\n },\n {\n id: TAB.access,\n label: `Access — ${access.route}`,\n chip: access.chip,\n render: () => (\n <>\n {/* Two-step access: for a nested resource, show the parent-first\n prerequisite chain before this resource's own instructions. */}\n <AccessPrerequisiteChain resource={app} onOpenParent={onAppClick} />\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n </>\n ),\n },\n ...(hasResourcesTab\n ? [\n {\n id: TAB.resources,\n label: resourcesLabel,\n chip: children.length ? String(children.length) : undefined,\n render: () => (\n <>\n {app.tiers && app.tiers.length > 0 && (\n <TierVariantsSection tiers={app.tiers} />\n )}\n {children.length > 0 && (\n <div className={app.tiers?.length ? 'mt-7' : ''}>\n {/* The active catalog search term seeds the filter, so a\n visitor who got here by searching something that matched\n a child sees that child rather than all of its siblings. */}\n <SubResourcesSection\n subResources={children}\n parentSlug={app.slug}\n parent={app}\n initialSearch={filterState.searchValue}\n childrenLabel={app.childrenLabel}\n />\n </div>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n ...(hasDocumentationTab\n ? [\n {\n id: TAB.documentation,\n label: 'Documentation',\n render: () => (\n <>\n {app.links && app.links.length > 0 && (\n <Section title=\"Documentation\" first>\n <div className=\"space-y-0.5\">\n {app.links.map((link) => (\n <a\n key={link.url}\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate\"\n >\n <ExternalLink className=\"size-3 shrink-0\" />\n {link.title || displayUrl(link.url)}\n </a>\n ))}\n </div>\n </Section>\n )}\n {/* Background: why the app exists at all. Deliberately quiet — a\n reader who came to get access has already passed everything they\n need, and history should not compete with the description. */}\n {app.background && (\n <Section title=\"Background\" first={!app.links?.length}>\n <MarkdownText className=\"prose prose-sm text-muted-foreground max-w-none text-sm [&_p]:mt-0 [&_p]:mb-2 [&_p:last-child]:mb-0\">\n {app.background}\n </MarkdownText>\n </Section>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n {\n id: TAB.behind,\n label: 'Behind this card',\n chip: behindCount ? String(behindCount) : undefined,\n ariaLabel: behindCount\n ? `Behind this card, ${behindCount} waiting for review`\n : undefined,\n render: () => (\n <div className=\"behind-panel\">\n <CommunitySection\n ref={knowMoreRef}\n target={{ resourceSlug: app.slug }}\n appName={app.displayName}\n claims={app.claims}\n />\n\n {(isAdmin ||\n sourceUrls.length > 0 ||\n app.createdAt ||\n app.freshness?.lastCheckedAt) && (\n <Group title=\"Kept up to date from\">\n {isAdmin ? (\n <>\n <ul className=\"space-y-2\">\n {displaySources.map((url, index) => {\n const isDraft =\n draftSource !== null && index === sourceUrls.length\n return (\n <li\n key={isDraft ? 'draft' : `${index}-${url}`}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n <InlineEditableField\n value={url}\n initialEditMode={isDraft}\n onCancel={\n isDraft ? () => setDraftSource(null) : undefined\n }\n onSave={(newUrl) => {\n if (isDraft) {\n setDraftSource(null)\n if (newUrl) {\n updateApp.mutate({\n id: app.id,\n data: { sources: [...sourceUrls, newUrl] },\n })\n }\n } else {\n const next = [...sourceUrls]\n next[index] = newUrl\n updateApp.mutate({\n id: app.id,\n data: { sources: next.filter(Boolean) },\n })\n }\n }}\n placeholder=\"https://...\"\n viewClassName=\"flex-1 min-w-0\"\n renderView={(val) =>\n val ? (\n <a\n href={val}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {displayUrl(val)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n {!isDraft && pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n {!isDraft && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Remove source\"\n className=\"shrink-0 text-muted-foreground hover:text-destructive\"\n onClick={() => {\n const next = sourceUrls.filter(\n (_, i) => i !== index,\n )\n updateApp.mutate({\n id: app.id,\n data: { sources: next },\n })\n }}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n )}\n </li>\n )\n })}\n </ul>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-2 gap-1 text-muted-foreground\"\n onClick={() => setDraftSource('')}\n >\n <Plus className=\"size-3.5\" />\n Add source\n </Button>\n </>\n ) : (\n <ul className=\"space-y-2\">\n {sourceUrls.map((url, index) => (\n // `group` so hovering anywhere on the row fades that source's\n // track in — the mark sits in a right-hand column, and row\n // hover is what ties the two ends of a wide row together.\n <li\n key={index}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex min-w-0 items-center gap-1 truncate\"\n >\n {displayUrl(url)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n {pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n </li>\n ))}\n </ul>\n )}\n {datesLine && (\n <p className=\"behind-dates\" title={datesTitle}>\n {datesLine}\n </p>\n )}\n </Group>\n )}\n\n {(app.aiPrompt || app.aiMemory) && (\n <Group title=\"How the AI maintains this card\">\n {app.aiPrompt && (\n <>\n <p className=\"behind-ai__label\">Prompt</p>\n <p className=\"behind-ai\">{app.aiPrompt}</p>\n </>\n )}\n {app.aiMemory && (\n <>\n <p className=\"behind-ai__label\">Memory</p>\n <p className=\"behind-ai\">{app.aiMemory}</p>\n </>\n )}\n </Group>\n )}\n\n <div className=\"behind-card-source\">\n <ResourceDetailCardSource\n appSlug={app.slug}\n appName={app.displayName}\n />\n </div>\n </div>\n ),\n },\n ]\n\n // The hero band: identity, the way in, and the typed-id jumps. It does NOT\n // scroll away with a tab, so \"take me to the app\" is reachable from every one\n // of them.\n //\n // Deliberately holds nothing else. An owner/teams/freshness strip was drawn\n // and dropped: `ownerPersonSlug` is set on a tiny fraction of entries so the\n // owner slot would almost always be empty, teams already has its own block in\n // Overview, and on an entry with quick jumps the band had grown past 250px\n // before any content — on exactly the apps where people most want to type an\n // id and leave.\n const band = (\n <>\n <div className=\"flex flex-wrap items-center gap-x-7 gap-y-2\">\n <AppDoor app={app} onOpen={() => recordClick(app.slug)} />\n {app.deprecated && (\n <Badge\n variant={\n app.deprecated.type === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {app.deprecated.type === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )}\n {/* Plugin actions live in the band because it is mounted on every tab and\n never scrolls away; a slot in a tab panel would be invisible until a\n second click. Renders nothing when no plugin contributes.\n `mr-10` keeps them clear of the dialog's close button at top-right. */}\n <div className=\"ml-auto mr-10 flex items-center gap-2\">\n <ResourceDetailAccessActions {...accessActionsCtx} />\n </div>\n </div>\n\n {app.nicknames && app.nicknames.length > 0 && (\n <p className=\"mt-2.5 text-xs text-muted-foreground\">\n Also known as {app.nicknames.join(', ')}\n </p>\n )}\n\n {communityCount > 0 && (\n <p className=\"mt-2.5 text-xs\">\n <Link\n to=\"/community\"\n search={{ tag: `app:${app.slug}` }}\n data-testid=\"app-community-tools\"\n className=\"text-muted-foreground hover:text-primary hover:underline\"\n >\n Community tools for {app.displayName} · {communityCount}\n </Link>\n </p>\n )}\n\n {isAdmin && (\n <div className=\"mt-3 flex flex-wrap items-center gap-x-6 gap-y-1\">\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Slug:</span>\n <InlineEditableField\n value={app.slug}\n onSave={(slug) =>\n updateApp.mutate({ id: app.id, data: { slug } })\n }\n className=\"text-sm\"\n />\n </span>\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">URL:</span>\n <InlineEditableField\n value={app.appUrl ?? ''}\n onSave={(appUrl) =>\n updateApp.mutate({ id: app.id, data: { appUrl } })\n }\n placeholder=\"App URL\"\n className=\"text-sm\"\n />\n </span>\n </div>\n )}\n\n {hasQuickJumps(app) && (\n <div className=\"mt-4\">\n {/* Named, where the bare control was not: \"or go straight to a\n record\" says what the field is for before you reach it. */}\n <p className=\"mb-1.5 text-xs text-muted-foreground\">\n Or go straight to a record:\n </p>\n <QuickJumpBar app={app} />\n </div>\n )}\n\n {/* Deprecation stays in the band, not in a tab: \"do not use this\"\n must not be something you have to go looking for. */}\n {app.deprecated &&\n (() => {\n const isDiscouraged = app.deprecated.type === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-4 rounded-lg border border-yellow-500/50 bg-yellow-50 p-4 dark:bg-yellow-950/20'\n : 'mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'section-label mb-1.5 text-yellow-700 dark:text-yellow-500'\n : 'section-label mb-1.5 text-destructive'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n type=\"button\"\n onClick={() => onAppClick?.(replacementApp)}\n className=\"mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n </>\n )\n\n return (\n <>\n <div className=\"flex h-full flex-col\">\n <DetailTabs\n tabs={tabs}\n band={band}\n // Cancels the dialog's own padding so the tint reaches the card's\n // edges, then re-applies it inside.\n bandClassName=\"-mx-6 -mt-5 px-6 pt-5 sm:-mx-8 sm:-mt-7 sm:px-8 sm:pt-7\"\n // `?sub=` means a child was picked out of a search, so the card opens\n // on the tab that holds it rather than making them hunt for it.\n forcedTab={subSlug && hasResourcesTab ? TAB.resources : undefined}\n trailing={(select) => (\n <button\n type=\"button\"\n className=\"detail-share\"\n aria-label=\"Share what you know\"\n onClick={() => {\n select(TAB.behind)\n setShareAsk((n) => n + 1)\n }}\n >\n <span className=\"max-sm:sr-only\">Share what you know</span>\n <Plus className=\"size-4 sm:hidden\" aria-hidden=\"true\" />\n </button>\n )}\n />\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.abbreviation || app.displayName} - Screenshots`}\n />\n </>\n )\n}\n"],"names":["React","_a"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AppDetails.js","sources":["../../../../../../src/modules/appCatalog/ui/detail/AppDetails.tsx"],"sourcesContent":["import type {\n Resource,\n SourceReference,\n} from '@igstack/app-catalog-backend-core'\nimport { Link, useSearch } from '@tanstack/react-router'\nimport { ExternalLink, Plus, Trash2 } from 'lucide-react'\nimport React from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\nimport { Badge } from '~/ui/badge'\nimport { Button } from '~/ui/button'\nimport { AccessRequestSection } from '../components/AccessRequestSection'\nimport { AccessPrerequisiteChain } from '../components/AccessPrerequisiteChain'\nimport { QuickJumpBar, hasQuickJumps } from '../components/QuickJumpBar'\nimport { useAppCatalogFilters } from '../context/AppCatalogFiltersContext'\nimport { PersonBadge } from '../components/PersonBadge'\nimport { useUser } from '~/modules/auth'\nimport { InlineEditableField } from '../components/InlineEditableField'\nimport { MarkdownText } from '../components/MarkdownText'\nimport { ScreenshotGallery } from '../components/ScreenshotGallery'\nimport { SourcePulse } from '../components/SourcePulse'\nimport { useUpdateApp } from '../../hooks/useUpdateApp'\nimport { useAppCatalogContext } from '../../context/AppCatalogContext'\nimport { useAppClickHistory } from '../../hooks/useAppClickHistory'\nimport { formatRelativeTime } from '../../utils/formatRelativeTime'\nimport { TierVariantsSection } from '../components/TierVariantsSection'\nimport { SubResourcesSection } from '../components/SubResourcesSection'\nimport { getChildResources } from '../../utils/resolveHelpers'\nimport {\n ResourceDetailAccessActions,\n ResourceDetailCardSource,\n} from '~/modules/extensions'\nimport { CommunitySection } from './CommunitySection'\nimport type { CommunitySectionHandle } from './CommunitySection'\nimport { usePendingCount } from './usePendingCount'\nimport { useCommunityToolCount } from './useCommunityToolCount'\nimport { displayUrl } from '~/modules/appCatalog/utils/displayUrl'\nimport { accessTabLabel } from '~/modules/appCatalog/utils/accessTabLabel'\nimport { AppDoor } from './AppDoor'\nimport { DetailTabs } from './DetailTabs'\nimport type { DetailTab } from './DetailTabs'\n\n/** Tab ids. Public — they land in `?tab=`, so renaming one breaks links. */\nconst TAB = {\n overview: 'overview',\n access: 'access',\n resources: 'resources',\n documentation: 'documentation',\n behind: 'behind',\n} as const\n\n/**\n * One of Behind this card's top-level groups. A level above `Section`: its\n * title is a plain heading, and a hairline separates it from the group before.\n */\nfunction Group({\n title,\n children,\n}: {\n title: string\n children: React.ReactNode\n}) {\n return (\n <section className=\"behind-group\">\n <h2 className=\"behind-group__title\">{title}</h2>\n {children}\n </section>\n )\n}\n\n/** A labelled block inside a tab. One register for all of them. */\nfunction Section({\n title,\n hint,\n first,\n children,\n}: {\n title: string\n hint?: string\n /** Drops the top margin for the first block in a panel. */\n first?: boolean\n children: React.ReactNode\n}) {\n return (\n <div className={first ? '' : 'mt-7'}>\n <h3 className=\"section-label mb-1.5\">{title}</h3>\n {hint && <p className=\"mb-2 text-xs text-muted-foreground\">{hint}</p>}\n {children}\n </div>\n )\n}\n\nfunction AppScreenshot({ app }: { app: Resource }) {\n const [imageError, setImageError] = React.useState(false)\n const [isLoadingImage, setIsLoadingImage] = React.useState(true)\n\n const screenshotId = app.screenshotIds?.[0]\n if (!screenshotId) {\n return (\n <div className=\"w-full bg-muted/50 rounded-lg overflow-hidden flex items-center justify-center min-h-64\">\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n No screenshot available\n </div>\n </div>\n )\n }\n\n const screenshotImageUrl = `/api/screenshots/${screenshotId}?size=512`\n\n return (\n <div className=\"w-full flex justify-center\">\n <div className=\"rounded-lg overflow-hidden inline-flex items-center justify-center min-h-64\">\n {!imageError ? (\n <img\n src={screenshotImageUrl}\n alt={`${app.abbreviation || app.displayName} screenshot`}\n className=\"h-64 object-contain\"\n onError={() => {\n setImageError(true)\n setIsLoadingImage(false)\n }}\n onLoad={() => setIsLoadingImage(false)}\n />\n ) : null}\n {(imageError || isLoadingImage) && (\n <div className=\"w-full h-64 bg-muted/30 flex items-center justify-center text-muted-foreground text-sm\">\n {isLoadingImage\n ? 'Loading screenshot...'\n : 'No screenshot available'}\n </div>\n )}\n </div>\n </div>\n )\n}\n\nexport function AppDetails({\n app,\n onAppClick,\n}: {\n app: Resource\n onAppClick?: (app: Resource) => void\n}) {\n const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)\n const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)\n\n const { approvalMethods, resources: allResources } = useAppCatalogContext()\n const { recordClick } = useAppClickHistory()\n const updateApp = useUpdateApp()\n const [draftSource, setDraftSource] = React.useState<string | null>(null)\n const user = useUser()\n const isAdmin = user?.isAdmin ?? false\n const { state: filterState } = useAppCatalogFilters()\n // Agrees with the Pending badges in the feed: composed by the same rules.\n const behindCount = usePendingCount(app.slug, app.claims)\n const communityCount = useCommunityToolCount(app.slug)\n\n // \"Last checked 10 hours ago · added 7 months ago\" under the sources.\n const lastChecked = app.freshness?.lastCheckedAt\n const dateParts = []\n if (lastChecked) {\n const suffix = app.freshness?.isStale ? ' (may be out of date)' : ''\n dateParts.push(`Last checked ${formatRelativeTime(lastChecked)}${suffix}`)\n }\n if (app.createdAt) {\n const addedLabel = dateParts.length > 0 ? 'added' : 'Added'\n dateParts.push(`${addedLabel} ${formatRelativeTime(app.createdAt)}`)\n }\n const datesLine = dateParts.join(' · ')\n const datesTitle = [\n app.freshness?.lastContentChangeAt &&\n `Content last changed ${app.freshness.lastContentChangeAt}`,\n lastChecked && `Last checked ${lastChecked}`,\n app.createdAt && `Added ${app.createdAt}`,\n ]\n .filter(Boolean)\n .join(' · ')\n\n // \"Share what you know\" asks for the input by bumping this. A counter, not a\n // flag, so a second press while already on the tab still re-focuses. The\n // effect runs after the click's render has mounted the tab's panel, so the\n // ref is attached by then; opening the tab by hand bumps nothing.\n const knowMoreRef = React.useRef<CommunitySectionHandle>(null)\n const [shareAsk, setShareAsk] = React.useState(0)\n React.useEffect(() => {\n if (shareAsk) knowMoreRef.current?.focus()\n }, [shareAsk])\n\n // Arriving with `?sub=` means the visitor picked one child out of a search —\n // so the card has to open ON that child's tab. Landing on Overview and\n // leaving them to find the tab is the bug this scenario exists to prevent.\n const search = useSearch({ strict: false })\n const subSlug = search.sub\n\n const displayTags = (app.tags ?? []).filter((tag) => !tag.includes(':'))\n const sourceUrls: string[] =\n app.sources?.map((s) => (typeof s === 'string' ? s : s.url)) ?? []\n const displaySources =\n draftSource !== null ? [...sourceUrls, draftSource] : sourceUrls\n // The list renders bare URLs (they are editable as text), so the pulse has to\n // be looked up rather than carried alongside. Absent until the autoupdate loop\n // has seen the source — a freshly added one simply gets no mark.\n const pulseByUrl = React.useMemo(() => {\n const byUrl = new Map<string, NonNullable<SourceReference['pulse']>>()\n for (const source of app.sources ?? []) {\n if (typeof source !== 'string' && source.pulse) {\n byUrl.set(source.url, source.pulse)\n }\n }\n return byUrl\n }, [app.sources])\n\n const children = React.useMemo(\n () => getChildResources(allResources, app.slug),\n [allResources, app.slug],\n )\n\n // `app` is whatever resource the card shows, a nested one included, so the\n // parent has to be looked up rather than assumed absent. A plugin uses its\n // presence to tell a sub-resource view from a top-level one.\n const parent = React.useMemo(\n () =>\n app.parentSlug\n ? allResources.find((r) => r.slug === app.parentSlug)\n : undefined,\n [allResources, app.parentSlug],\n )\n\n // What a plugin slot in the band receives. Memoised so a plugin that\n // memoises on its props is not defeated by a new object every render.\n const accessActionsCtx = React.useMemo(\n () => ({\n resource: app,\n parent,\n subResources: parent\n ? getChildResources(allResources, parent.slug)\n : children,\n user: user ? { email: user.email, isAdmin } : null,\n }),\n [app, parent, allResources, children, user, isAdmin],\n )\n\n // Enter: open screenshot gallery. Only reached on a card with no door — where\n // there is one it holds focus, and Enter there is the link's own.\n useHotkeys(\n 'enter',\n () => {\n const tag = document.activeElement?.tagName\n if (\n tag === 'BUTTON' ||\n tag === 'A' ||\n tag === 'INPUT' ||\n tag === 'SELECT' ||\n tag === 'TEXTAREA'\n )\n return\n\n if (app.screenshotIds && app.screenshotIds.length > 0) {\n setGalleryInitialIndex(0)\n setIsGalleryOpen(true)\n }\n },\n { enabled: !isGalleryOpen },\n [app, isGalleryOpen],\n )\n\n const handleScreenshotClick = (index: number) => {\n setGalleryInitialIndex(index)\n setIsGalleryOpen(true)\n }\n\n // Find replacement app if deprecated\n const replacementApp = app.deprecated?.replacementSlug\n ? allResources.find((a) => a.slug === app.deprecated?.replacementSlug)\n : null\n\n const access = accessTabLabel(app, approvalMethods)\n\n const hasResourcesTab = children.length > 0 || (app.tiers?.length ?? 0) > 0\n const resourcesLabel = children.length\n ? (app.childrenLabel ?? 'Resources')\n : 'Environments'\n\n const hasDocumentationTab = (app.links?.length ?? 0) > 0 || !!app.background\n\n const tabs: DetailTab[] = [\n {\n id: TAB.overview,\n label: 'Overview',\n render: () => (\n <>\n <Section title=\"Description\" first>\n {isAdmin ? (\n <InlineEditableField\n value={app.description ?? ''}\n onSave={(description) =>\n updateApp.mutate({ id: app.id, data: { description } })\n }\n multiline\n placeholder=\"Description\"\n className=\"min-h-[4rem] resize-y text-sm text-muted-foreground\"\n />\n ) : app.description ? (\n <MarkdownText className=\"prose prose-sm max-w-none text-sm text-muted-foreground [&_p]:m-0\">\n {app.description}\n </MarkdownText>\n ) : (\n <p className=\"text-sm text-muted-foreground\">—</p>\n )}\n </Section>\n\n {app.screenshotIds && app.screenshotIds.length > 0 && (\n <Section title={`Screenshots (${app.screenshotIds.length})`}>\n {/* A button, not a clickable div: the gallery was unreachable by\n keyboard, and a role + label gives it a stable handle. */}\n <button\n type=\"button\"\n aria-label={`View screenshots of ${app.displayName}`}\n className=\"block w-full cursor-pointer text-left hover:opacity-80 transition-opacity\"\n onClick={() => handleScreenshotClick(0)}\n >\n <AppScreenshot app={app} />\n {app.screenshotIds.length > 1 && (\n <p className=\"text-xs text-muted-foreground mt-2 text-center\">\n Click to view all {app.screenshotIds.length} screenshots\n </p>\n )}\n </button>\n </Section>\n )}\n\n {/* Owner — who is responsible for this resource. Distinct from the\n access approver (who decides access requests); see domain model. */}\n {app.ownerPersonSlug && (\n <Section title=\"Owner\" hint=\"Who is responsible for this resource\">\n <PersonBadge slug={app.ownerPersonSlug} />\n </Section>\n )}\n\n {/* Tags, minus the indexing machinery: `namespace:value` tags drive\n grouping, faceting and placement, are about half of all tag\n references, and say nothing about what an app is for. The colon is\n the test. Search still matches them — only the display drops them. */}\n {displayTags.length > 0 && (\n <Section title=\"Tags\">\n <div className=\"flex flex-wrap gap-2\">\n {displayTags.map((tag) => (\n <Badge key={tag} variant=\"secondary\" className=\"text-xs\">\n {tag}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n\n {app.teams && app.teams.length > 0 && (\n <Section title=\"Teams\">\n <div className=\"flex flex-wrap gap-2\">\n {app.teams.map((team) => (\n <Badge key={team} variant=\"outline\" className=\"text-xs\">\n {team}\n </Badge>\n ))}\n </div>\n </Section>\n )}\n </>\n ),\n },\n {\n id: TAB.access,\n label: `Access — ${access.route}`,\n chip: access.chip,\n render: () => (\n <>\n {/* Two-step access: for a nested resource, show the parent-first\n prerequisite chain before this resource's own instructions. */}\n <AccessPrerequisiteChain resource={app} onOpenParent={onAppClick} />\n <AccessRequestSection app={app} approvalMethods={approvalMethods} />\n </>\n ),\n },\n ...(hasResourcesTab\n ? [\n {\n id: TAB.resources,\n label: resourcesLabel,\n chip: children.length ? String(children.length) : undefined,\n render: () => (\n <>\n {app.tiers && app.tiers.length > 0 && (\n <TierVariantsSection tiers={app.tiers} />\n )}\n {children.length > 0 && (\n <div className={app.tiers?.length ? 'mt-7' : ''}>\n {/* The active catalog search term seeds the filter, so a\n visitor who got here by searching something that matched\n a child sees that child rather than all of its siblings. */}\n <SubResourcesSection\n subResources={children}\n parentSlug={app.slug}\n parent={app}\n initialSearch={filterState.searchValue}\n childrenLabel={app.childrenLabel}\n />\n </div>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n ...(hasDocumentationTab\n ? [\n {\n id: TAB.documentation,\n label: 'Documentation',\n render: () => (\n <>\n {app.links && app.links.length > 0 && (\n <Section title=\"Documentation\" first>\n <div className=\"space-y-0.5\">\n {app.links.map((link) => (\n <a\n key={link.url}\n href={link.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate\"\n >\n <ExternalLink className=\"size-3 shrink-0\" />\n {link.title || displayUrl(link.url)}\n </a>\n ))}\n </div>\n </Section>\n )}\n {/* Background: why the app exists at all. Deliberately quiet — a\n reader who came to get access has already passed everything they\n need, and history should not compete with the description. */}\n {app.background && (\n <Section title=\"Background\" first={!app.links?.length}>\n <MarkdownText className=\"prose prose-sm text-muted-foreground max-w-none text-sm [&_p]:mt-0 [&_p]:mb-2 [&_p:last-child]:mb-0\">\n {app.background}\n </MarkdownText>\n </Section>\n )}\n </>\n ),\n } satisfies DetailTab,\n ]\n : []),\n {\n id: TAB.behind,\n label: 'Behind this card',\n chip: behindCount ? String(behindCount) : undefined,\n ariaLabel: behindCount\n ? `Behind this card, ${behindCount} waiting for review`\n : undefined,\n render: () => (\n <div className=\"behind-panel\">\n <CommunitySection\n ref={knowMoreRef}\n target={{ resourceSlug: app.slug }}\n appName={app.displayName}\n claims={app.claims}\n />\n\n {(isAdmin ||\n sourceUrls.length > 0 ||\n app.createdAt ||\n app.freshness?.lastCheckedAt) && (\n <Group title=\"Kept up to date from\">\n {isAdmin ? (\n <>\n <ul className=\"space-y-2\">\n {displaySources.map((url, index) => {\n const isDraft =\n draftSource !== null && index === sourceUrls.length\n return (\n <li\n key={isDraft ? 'draft' : `${index}-${url}`}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n <InlineEditableField\n value={url}\n initialEditMode={isDraft}\n onCancel={\n isDraft ? () => setDraftSource(null) : undefined\n }\n onSave={(newUrl) => {\n if (isDraft) {\n setDraftSource(null)\n if (newUrl) {\n updateApp.mutate({\n id: app.id,\n data: { sources: [...sourceUrls, newUrl] },\n })\n }\n } else {\n const next = [...sourceUrls]\n next[index] = newUrl\n updateApp.mutate({\n id: app.id,\n data: { sources: next.filter(Boolean) },\n })\n }\n }}\n placeholder=\"https://...\"\n viewClassName=\"flex-1 min-w-0\"\n renderView={(val) =>\n val ? (\n <a\n href={val}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex items-center gap-1 truncate\"\n >\n {displayUrl(val)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )\n }\n />\n {!isDraft && pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n {!isDraft && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label=\"Remove source\"\n className=\"shrink-0 text-muted-foreground hover:text-destructive\"\n onClick={() => {\n const next = sourceUrls.filter(\n (_, i) => i !== index,\n )\n updateApp.mutate({\n id: app.id,\n data: { sources: next },\n })\n }}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n )}\n </li>\n )\n })}\n </ul>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"mt-2 gap-1 text-muted-foreground\"\n onClick={() => setDraftSource('')}\n >\n <Plus className=\"size-3.5\" />\n Add source\n </Button>\n </>\n ) : (\n <ul className=\"space-y-2\">\n {sourceUrls.map((url, index) => (\n // `group` so hovering anywhere on the row fades that source's\n // track in — the mark sits in a right-hand column, and row\n // hover is what ties the two ends of a wide row together.\n <li\n key={index}\n className=\"group flex items-center gap-2 text-xs\"\n >\n <span className=\"text-muted-foreground shrink-0 tabular-nums\">\n {index + 1}.\n </span>\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"hover:text-primary inline-flex min-w-0 items-center gap-1 truncate\"\n >\n {displayUrl(url)}\n <ExternalLink className=\"size-3 shrink-0\" />\n </a>\n ) : (\n <span className=\"text-muted-foreground\">—</span>\n )}\n {pulseByUrl.has(url) && (\n <SourcePulse\n pulse={pulseByUrl.get(url)!}\n className=\"ml-auto\"\n />\n )}\n </li>\n ))}\n </ul>\n )}\n {datesLine && (\n <p className=\"behind-dates\" title={datesTitle}>\n {datesLine}\n </p>\n )}\n </Group>\n )}\n\n {(app.aiPrompt || app.aiMemory) && (\n <Group title=\"How the AI maintains this card\">\n {app.aiPrompt && (\n <>\n <p className=\"behind-ai__label\">Prompt</p>\n <p className=\"behind-ai\">{app.aiPrompt}</p>\n </>\n )}\n {app.aiMemory && (\n <>\n <p className=\"behind-ai__label\">Memory</p>\n <p className=\"behind-ai\">{app.aiMemory}</p>\n </>\n )}\n </Group>\n )}\n\n <div className=\"behind-card-source\">\n <ResourceDetailCardSource\n appSlug={app.slug}\n appName={app.displayName}\n />\n </div>\n </div>\n ),\n },\n ]\n\n // The hero band: identity, the way in, and the typed-id jumps. It does NOT\n // scroll away with a tab, so \"take me to the app\" is reachable from every one\n // of them.\n //\n // Deliberately holds nothing else. An owner/teams/freshness strip was drawn\n // and dropped: `ownerPersonSlug` is set on a tiny fraction of entries so the\n // owner slot would almost always be empty, teams already has its own block in\n // Overview, and on an entry with quick jumps the band had grown past 250px\n // before any content — on exactly the apps where people most want to type an\n // id and leave.\n const band = (\n <>\n <div className=\"flex flex-wrap items-center gap-x-7 gap-y-2\">\n <AppDoor app={app} onOpen={() => recordClick(app.slug)} />\n {app.deprecated && (\n <Badge\n variant={\n app.deprecated.type === 'discouraged'\n ? 'secondary'\n : 'destructive'\n }\n >\n {app.deprecated.type === 'discouraged'\n ? 'Discouraged'\n : 'Deprecated'}\n </Badge>\n )}\n {/* Plugin actions live in the band because it is mounted on every tab and\n never scrolls away; a slot in a tab panel would be invisible until a\n second click. Renders nothing when no plugin contributes.\n `mr-10` keeps them clear of the dialog's close button at top-right. */}\n <div className=\"ml-auto mr-10 flex items-center gap-2\">\n <ResourceDetailAccessActions {...accessActionsCtx} />\n </div>\n </div>\n\n {app.nicknames && app.nicknames.length > 0 && (\n <p className=\"mt-2.5 text-xs text-muted-foreground\">\n Also known as {app.nicknames.join(', ')}\n </p>\n )}\n\n {communityCount > 0 && (\n <p className=\"mt-2.5 text-xs\">\n <Link\n to=\"/community\"\n search={{ tag: `app:${app.slug}` }}\n data-testid=\"app-community-tools\"\n className=\"text-muted-foreground hover:text-primary hover:underline\"\n >\n Community tools for {app.displayName} · {communityCount}\n </Link>\n </p>\n )}\n\n {isAdmin && (\n <div className=\"mt-3 flex flex-wrap items-center gap-x-6 gap-y-1\">\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">Slug:</span>\n <InlineEditableField\n value={app.slug}\n onSave={(slug) =>\n updateApp.mutate({ id: app.id, data: { slug } })\n }\n className=\"text-sm\"\n />\n </span>\n <span className=\"inline-flex items-center gap-2\">\n <span className=\"text-xs text-muted-foreground\">URL:</span>\n <InlineEditableField\n value={app.appUrl ?? ''}\n onSave={(appUrl) =>\n updateApp.mutate({ id: app.id, data: { appUrl } })\n }\n placeholder=\"App URL\"\n className=\"text-sm\"\n />\n </span>\n </div>\n )}\n\n {hasQuickJumps(app) && (\n <div className=\"mt-4\">\n {/* Named, where the bare control was not: \"or go straight to a\n record\" says what the field is for before you reach it. */}\n <p className=\"mb-1.5 text-xs text-muted-foreground\">\n Or go straight to a record:\n </p>\n <QuickJumpBar app={app} />\n </div>\n )}\n\n {/* Deprecation stays in the band, not in a tab: \"do not use this\"\n must not be something you have to go looking for. */}\n {app.deprecated &&\n (() => {\n const isDiscouraged = app.deprecated.type === 'discouraged'\n return (\n <div\n className={\n isDiscouraged\n ? 'mt-4 rounded-lg border border-yellow-500/50 bg-yellow-50 p-4 dark:bg-yellow-950/20'\n : 'mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4'\n }\n >\n <h3\n className={\n isDiscouraged\n ? 'section-label mb-1.5 text-yellow-700 dark:text-yellow-500'\n : 'section-label mb-1.5 text-destructive'\n }\n >\n {isDiscouraged\n ? 'Usage discouraged'\n : 'This application is deprecated'}\n </h3>\n <p className=\"text-sm text-muted-foreground\">\n {app.deprecated.comment}\n </p>\n {replacementApp && (\n <button\n type=\"button\"\n onClick={() => onAppClick?.(replacementApp)}\n className=\"mt-3 inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline\"\n >\n View replacement: {replacementApp.displayName}\n <ExternalLink className=\"size-3\" />\n </button>\n )}\n </div>\n )\n })()}\n </>\n )\n\n return (\n <>\n <div className=\"flex h-full flex-col\">\n <DetailTabs\n tabs={tabs}\n band={band}\n // Cancels the dialog's own padding so the tint reaches the card's\n // edges, then re-applies it inside.\n bandClassName=\"-mx-6 -mt-5 px-6 pt-5 sm:-mx-8 sm:-mt-7 sm:px-8 sm:pt-7\"\n // `?sub=` means a child was picked out of a search, so the card opens\n // on the tab that holds it rather than making them hunt for it.\n forcedTab={subSlug && hasResourcesTab ? TAB.resources : undefined}\n trailing={(select) => (\n <button\n type=\"button\"\n className=\"detail-share\"\n aria-label=\"Share what you know\"\n onClick={() => {\n select(TAB.behind)\n setShareAsk((n) => n + 1)\n }}\n >\n <span className=\"max-sm:sr-only\">Share what you know</span>\n <Plus className=\"size-4 sm:hidden\" aria-hidden=\"true\" />\n </button>\n )}\n />\n </div>\n\n {/* Screenshot Gallery Dialog */}\n <ScreenshotGallery\n app={app}\n screenshotIds={app.screenshotIds || []}\n open={isGalleryOpen}\n onOpenChange={setIsGalleryOpen}\n initialIndex={galleryInitialIndex}\n title={`${app.abbreviation || app.displayName} - Screenshots`}\n />\n </>\n )\n}\n"],"names":["React","_a"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts a click through to a tool's site. The page leaves in the same tab, which
|
|
3
|
+
* would cancel a tRPC call, so this posts with `keepalive` and never waits for it.
|
|
4
|
+
*/
|
|
5
|
+
export declare function recordSiteOpen(slug: string): void;
|
|
6
|
+
/** Goes to the tool's site in this tab, counting the click. */
|
|
7
|
+
export declare function openSite(tool: {
|
|
8
|
+
slug: string;
|
|
9
|
+
url: string;
|
|
10
|
+
}): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function recordSiteOpen(slug) {
|
|
2
|
+
fetch(`${window.location.origin}/api/trpc/communityTools.recordSiteOpen`, {
|
|
3
|
+
method: "POST",
|
|
4
|
+
keepalive: true,
|
|
5
|
+
headers: { "content-type": "application/json" },
|
|
6
|
+
body: JSON.stringify({ slug })
|
|
7
|
+
}).catch(() => {
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function openSite(tool) {
|
|
11
|
+
recordSiteOpen(tool.slug);
|
|
12
|
+
window.location.assign(tool.url);
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
openSite,
|
|
16
|
+
recordSiteOpen
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=siteOpen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"siteOpen.js","sources":["../../../../src/modules/communityTools/siteOpen.ts"],"sourcesContent":["/**\n * Counts a click through to a tool's site. The page leaves in the same tab, which\n * would cancel a tRPC call, so this posts with `keepalive` and never waits for it.\n */\nexport function recordSiteOpen(slug: string): void {\n fetch(`${window.location.origin}/api/trpc/communityTools.recordSiteOpen`, {\n method: 'POST',\n keepalive: true,\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ slug }),\n }).catch(() => {\n // A lost count never stops anyone reaching the site.\n })\n}\n\n/** Goes to the tool's site in this tab, counting the click. */\nexport function openSite(tool: { slug: string; url: string }): void {\n recordSiteOpen(tool.slug)\n window.location.assign(tool.url)\n}\n"],"names":[],"mappings":"AAIO,SAAS,eAAe,MAAoB;AACjD,QAAM,GAAG,OAAO,SAAS,MAAM,2CAA2C;AAAA,IACxE,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,SAAS,EAAE,gBAAgB,mBAAA;AAAA,IAC3B,MAAM,KAAK,UAAU,EAAE,MAAM;AAAA,EAAA,CAC9B,EAAE,MAAM,MAAM;AAAA,EAEf,CAAC;AACH;AAGO,SAAS,SAAS,MAA2C;AAClE,iBAAe,KAAK,IAAI;AACxB,SAAO,SAAS,OAAO,KAAK,GAAG;AACjC;"}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
/** How long ago, in calendar days and up: "today", "3 days ago", "2 months ago". */
|
|
2
2
|
export declare function timeAgo(iso: string, now?: Date): string;
|
|
3
|
+
/** The exact moment, for the tooltip of a timeAgo: "22 Oct 2026, 15:04". */
|
|
4
|
+
export declare function exactDate(iso: string): string;
|
|
@@ -12,7 +12,18 @@ function timeAgo(iso, now = /* @__PURE__ */ new Date()) {
|
|
|
12
12
|
const years = Math.floor(days / 365);
|
|
13
13
|
return years === 1 ? "1 year ago" : `${years} years ago`;
|
|
14
14
|
}
|
|
15
|
+
function exactDate(iso) {
|
|
16
|
+
return new Date(iso).toLocaleString("en-GB", {
|
|
17
|
+
day: "numeric",
|
|
18
|
+
month: "short",
|
|
19
|
+
year: "numeric",
|
|
20
|
+
hour: "2-digit",
|
|
21
|
+
minute: "2-digit",
|
|
22
|
+
hour12: false
|
|
23
|
+
});
|
|
24
|
+
}
|
|
15
25
|
export {
|
|
26
|
+
exactDate,
|
|
16
27
|
timeAgo
|
|
17
28
|
};
|
|
18
29
|
//# sourceMappingURL=timeAgo.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeAgo.js","sources":["../../../../src/modules/communityTools/timeAgo.ts"],"sourcesContent":["const DAY_MS = 24 * 60 * 60 * 1000\n\n/** How long ago, in calendar days and up: \"today\", \"3 days ago\", \"2 months ago\". */\nexport function timeAgo(iso: string, now: Date = new Date()): string {\n const day = (d: Date) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n const days = Math.max(0, Math.round((day(now) - day(new Date(iso))) / DAY_MS))\n if (days === 0) return 'today'\n if (days === 1) return 'yesterday'\n if (days < 31) return `${days} days ago`\n if (days < 365) {\n const months = Math.floor(days / 30)\n return months === 1 ? '1 month ago' : `${months} months ago`\n }\n const years = Math.floor(days / 365)\n return years === 1 ? '1 year ago' : `${years} years ago`\n}\n"],"names":[],"mappings":"AAAA,MAAM,SAAS,KAAK,KAAK,KAAK;AAGvB,SAAS,QAAQ,KAAa,MAAY,oBAAI,QAAgB;AACnE,QAAM,MAAM,CAAC,MAAY,KAAK,IAAI,EAAE,YAAA,GAAe,EAAE,SAAA,GAAY,EAAE,SAAS;AAC5E,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,IAAI,IAAI,IAAI,KAAK,GAAG,CAAC,KAAK,MAAM,CAAC;AAC7E,MAAI,SAAS,EAAG,QAAO;AACvB,MAAI,SAAS,EAAG,QAAO;AACvB,MAAI,OAAO,GAAI,QAAO,GAAG,IAAI;AAC7B,MAAI,OAAO,KAAK;AACd,UAAM,SAAS,KAAK,MAAM,OAAO,EAAE;AACnC,WAAO,WAAW,IAAI,gBAAgB,GAAG,MAAM;AAAA,EACjD;AACA,QAAM,QAAQ,KAAK,MAAM,OAAO,GAAG;AACnC,SAAO,UAAU,IAAI,eAAe,GAAG,KAAK;AAC9C;"}
|
|
1
|
+
{"version":3,"file":"timeAgo.js","sources":["../../../../src/modules/communityTools/timeAgo.ts"],"sourcesContent":["const DAY_MS = 24 * 60 * 60 * 1000\n\n/** How long ago, in calendar days and up: \"today\", \"3 days ago\", \"2 months ago\". */\nexport function timeAgo(iso: string, now: Date = new Date()): string {\n const day = (d: Date) => Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())\n const days = Math.max(0, Math.round((day(now) - day(new Date(iso))) / DAY_MS))\n if (days === 0) return 'today'\n if (days === 1) return 'yesterday'\n if (days < 31) return `${days} days ago`\n if (days < 365) {\n const months = Math.floor(days / 30)\n return months === 1 ? '1 month ago' : `${months} months ago`\n }\n const years = Math.floor(days / 365)\n return years === 1 ? '1 year ago' : `${years} years ago`\n}\n\n/** The exact moment, for the tooltip of a timeAgo: \"22 Oct 2026, 15:04\". */\nexport function exactDate(iso: string): string {\n return new Date(iso).toLocaleString('en-GB', {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n })\n}\n"],"names":[],"mappings":"AAAA,MAAM,SAAS,KAAK,KAAK,KAAK;AAGvB,SAAS,QAAQ,KAAa,MAAY,oBAAI,QAAgB;AACnE,QAAM,MAAM,CAAC,MAAY,KAAK,IAAI,EAAE,YAAA,GAAe,EAAE,SAAA,GAAY,EAAE,SAAS;AAC5E,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,IAAI,IAAI,IAAI,KAAK,GAAG,CAAC,KAAK,MAAM,CAAC;AAC7E,MAAI,SAAS,EAAG,QAAO;AACvB,MAAI,SAAS,EAAG,QAAO;AACvB,MAAI,OAAO,GAAI,QAAO,GAAG,IAAI;AAC7B,MAAI,OAAO,KAAK;AACd,UAAM,SAAS,KAAK,MAAM,OAAO,EAAE;AACnC,WAAO,WAAW,IAAI,gBAAgB,GAAG,MAAM;AAAA,EACjD;AACA,QAAM,QAAQ,KAAK,MAAM,OAAO,GAAG;AACnC,SAAO,UAAU,IAAI,eAAe,GAAG,KAAK;AAC9C;AAGO,SAAS,UAAU,KAAqB;AAC7C,SAAO,IAAI,KAAK,GAAG,EAAE,eAAe,SAAS;AAAA,IAC3C,KAAK;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,EAAA,CACT;AACH;"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Camera, ImageOff,
|
|
2
|
+
import { Camera, ImageOff, Maximize2 } from "lucide-react";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { ScreenshotGallery } from "../../appCatalog/ui/components/ScreenshotGallery.js";
|
|
5
5
|
import { cn } from "../../../lib/utils.js";
|
|
6
|
+
import { recordSiteOpen } from "../siteOpen.js";
|
|
6
7
|
import { screenshotUrl } from "./FeedRow.js";
|
|
7
8
|
const BOX = "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";
|
|
9
|
+
const UNREACHABLE = "Screenshot unavailable: the catalog's server can't reach this site.";
|
|
8
10
|
function formatCardDate(iso) {
|
|
9
11
|
return new Date(iso).toLocaleDateString("en-GB", {
|
|
10
12
|
day: "numeric",
|
|
@@ -35,7 +37,7 @@ function CardScreenshot({
|
|
|
35
37
|
"Taking a screenshot of the site…"
|
|
36
38
|
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
37
39
|
/* @__PURE__ */ jsx(ImageOff, { "aria-hidden": "true", className: "size-7" }),
|
|
38
|
-
"No screenshot. The site may need a login, or it did not load."
|
|
40
|
+
tool.serverUnreachable ? /* @__PURE__ */ jsx("span", { "data-testid": "card-server-unreachable", children: UNREACHABLE }) : "No screenshot. The site may need a login, or it did not load."
|
|
39
41
|
] })
|
|
40
42
|
}
|
|
41
43
|
),
|
|
@@ -54,9 +56,8 @@ function CardScreenshot({
|
|
|
54
56
|
"a",
|
|
55
57
|
{
|
|
56
58
|
href: tool.url,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"aria-label": `Open ${tool.name} in a new tab`,
|
|
59
|
+
onClick: () => recordSiteOpen(tool.slug),
|
|
60
|
+
"aria-label": `Open ${tool.name}`,
|
|
60
61
|
"data-testid": "card-screenshot-link",
|
|
61
62
|
className: "group/shot block size-full rounded-[14px] focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-ring",
|
|
62
63
|
children: [
|
|
@@ -68,10 +69,7 @@ function CardScreenshot({
|
|
|
68
69
|
className: "size-full object-cover object-left-top"
|
|
69
70
|
}
|
|
70
71
|
),
|
|
71
|
-
/* @__PURE__ */ jsx("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", children: /* @__PURE__ */
|
|
72
|
-
"Open site",
|
|
73
|
-
/* @__PURE__ */ jsx(ExternalLink, { "aria-hidden": "true", className: "size-4" })
|
|
74
|
-
] }) })
|
|
72
|
+
/* @__PURE__ */ jsx("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", children: /* @__PURE__ */ jsx("span", { className: "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", children: "Open site" }) })
|
|
75
73
|
]
|
|
76
74
|
}
|
|
77
75
|
),
|
|
@@ -89,9 +87,9 @@ function CardScreenshot({
|
|
|
89
87
|
]
|
|
90
88
|
}
|
|
91
89
|
),
|
|
92
|
-
capturedAt && /* @__PURE__ */ jsxs("figcaption", { className: "mt-2 text-[12.5px] font-semibold text-muted-foreground", children: [
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
(capturedAt || tool.serverUnreachable) && /* @__PURE__ */ jsxs("figcaption", { className: "mt-2 text-[12.5px] font-semibold text-muted-foreground", children: [
|
|
91
|
+
capturedAt && `Screenshot captured ${formatCardDate(capturedAt)}`,
|
|
92
|
+
tool.serverUnreachable && /* @__PURE__ */ jsx("span", { "data-testid": "card-server-unreachable", className: "block", children: UNREACHABLE })
|
|
95
93
|
] }),
|
|
96
94
|
/* @__PURE__ */ jsx(
|
|
97
95
|
ScreenshotGallery,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CardScreenshot.js","sources":["../../../../../src/modules/communityTools/ui/CardScreenshot.tsx"],"sourcesContent":["import type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport { Camera,
|
|
1
|
+
{"version":3,"file":"CardScreenshot.js","sources":["../../../../../src/modules/communityTools/ui/CardScreenshot.tsx"],"sourcesContent":["import type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport { Camera, ImageOff, Maximize2 } from 'lucide-react'\nimport { useState } from 'react'\nimport { ScreenshotGallery } from '~/modules/appCatalog/ui/components/ScreenshotGallery'\nimport { cn } from '~/lib/utils'\nimport { recordSiteOpen } from '../siteOpen'\nimport { screenshotUrl } from './FeedRow'\n\n/**\n * The 16:10 box every state shares, so swapping one for another moves nothing.\n * Capped in height, so how to use it and who to ask stay above the fold; the\n * top of the page, where its title is, is what shows.\n */\nconst BOX =\n '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'\n\n/** The server's checks get no answer, so there is no fresh screenshot to show. */\nconst UNREACHABLE =\n \"Screenshot unavailable: the catalog's server can't reach this site.\"\n\nexport function formatCardDate(iso: string): string {\n return new Date(iso).toLocaleDateString('en-GB', {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n })\n}\n\n/**\n * The tool's screenshot, full width: a click opens the site, like the title\n * block, and the corner button enlarges it in the screenshot viewer. A tool\n * with none yet shows a placeholder in the same box, not a link: \"still\n * capturing\" while its first visit is to come, otherwise \"no screenshot\".\n */\nexport function CardScreenshot({\n tool,\n capturing,\n}: {\n tool: CommunityTool\n /** A first screenshot is on its way and still worth waiting for. */\n capturing: boolean\n}) {\n const [open, setOpen] = useState(false)\n const { screenshotId, capturedAt } = tool\n\n if (!screenshotId) {\n return (\n <figure data-testid=\"card-screenshot\" className=\"mt-[22px]\">\n <div\n data-testid=\"card-screenshot-box\"\n data-state={capturing ? 'capturing' : 'missing'}\n aria-busy={capturing || undefined}\n className={cn(\n BOX,\n 'flex-col gap-2.5 px-6 text-center text-sm font-semibold text-muted-foreground',\n )}\n >\n {capturing ? (\n <>\n <Camera aria-hidden=\"true\" className=\"size-7 animate-pulse\" />\n Taking a screenshot of the site…\n </>\n ) : (\n <>\n <ImageOff aria-hidden=\"true\" className=\"size-7\" />\n {tool.serverUnreachable ? (\n <span data-testid=\"card-server-unreachable\">{UNREACHABLE}</span>\n ) : (\n 'No screenshot. The site may need a login, or it did not load.'\n )}\n </>\n )}\n </div>\n <figcaption className=\"mt-2 text-[12.5px] font-semibold text-muted-foreground\">\n {capturing ? 'Usually under a minute' : 'Open the site to see it'}\n </figcaption>\n </figure>\n )\n }\n\n return (\n <figure data-testid=\"card-screenshot\" className=\"mt-[22px]\">\n <div\n data-testid=\"card-screenshot-box\"\n data-state=\"captured\"\n className={BOX}\n >\n <a\n href={tool.url}\n onClick={() => recordSiteOpen(tool.slug)}\n aria-label={`Open ${tool.name}`}\n data-testid=\"card-screenshot-link\"\n className=\"group/shot block size-full rounded-[14px] focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-ring\"\n >\n <img\n src={screenshotUrl(screenshotId, 1280)}\n alt={`Screenshot of ${tool.name}`}\n className=\"size-full object-cover object-left-top\"\n />\n <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\">\n <span className=\"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\">\n Open site\n </span>\n </span>\n </a>\n {/* Beside the link, not in it: enlarging never opens the site. */}\n <button\n type=\"button\"\n aria-label={`Enlarge screenshot of ${tool.name}`}\n title=\"Enlarge\"\n onClick={() => setOpen(true)}\n 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\"\n >\n <Maximize2 aria-hidden=\"true\" className=\"size-4\" />\n </button>\n </div>\n {(capturedAt || tool.serverUnreachable) && (\n <figcaption className=\"mt-2 text-[12.5px] font-semibold text-muted-foreground\">\n {capturedAt && `Screenshot captured ${formatCardDate(capturedAt)}`}\n {tool.serverUnreachable && (\n <span data-testid=\"card-server-unreachable\" className=\"block\">\n {UNREACHABLE}\n </span>\n )}\n </figcaption>\n )}\n <ScreenshotGallery\n app={{ displayName: tool.name }}\n screenshotIds={[screenshotId]}\n open={open}\n onOpenChange={setOpen}\n title={`${tool.name} screenshot`}\n />\n </figure>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CommunityTool } from '@igstack/app-catalog-shared-core';
|
|
2
2
|
/**
|
|
3
3
|
* The card's door: favicon (a globe when the site has none), name, badges and
|
|
4
|
-
* address, all one link that opens the site
|
|
4
|
+
* address, all one link that opens the site.
|
|
5
5
|
*/
|
|
6
6
|
export declare function CardTitleBlock({ tool }: {
|
|
7
7
|
tool: CommunityTool;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Globe
|
|
2
|
+
import { Globe } from "lucide-react";
|
|
3
3
|
import { cn } from "../../../lib/utils.js";
|
|
4
|
+
import { recordSiteOpen } from "../siteOpen.js";
|
|
4
5
|
import { screenshotUrl } from "./FeedRow.js";
|
|
5
6
|
import { LifecycleBadge, UnreviewedBadge } from "./LifecycleBadge.js";
|
|
6
7
|
function CardTitleBlock({ tool }) {
|
|
@@ -9,8 +10,7 @@ function CardTitleBlock({ tool }) {
|
|
|
9
10
|
"a",
|
|
10
11
|
{
|
|
11
12
|
href: tool.url,
|
|
12
|
-
|
|
13
|
-
rel: "noopener noreferrer",
|
|
13
|
+
onClick: () => recordSiteOpen(tool.slug),
|
|
14
14
|
"data-testid": "card-title-block",
|
|
15
15
|
className: cn(
|
|
16
16
|
"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",
|
|
@@ -39,16 +39,8 @@ function CardTitleBlock({ tool }) {
|
|
|
39
39
|
/* @__PURE__ */ jsx(LifecycleBadge, { lifecycle: tool.lifecycle }),
|
|
40
40
|
!tool.reviewed && /* @__PURE__ */ jsx(UnreviewedBadge, {})
|
|
41
41
|
] }),
|
|
42
|
-
/* @__PURE__ */ jsx("span", { className: "truncate font-mono text-[13px] text-muted-foreground", children: tool.host })
|
|
43
|
-
|
|
44
|
-
] }),
|
|
45
|
-
/* @__PURE__ */ jsx(
|
|
46
|
-
ExternalLink,
|
|
47
|
-
{
|
|
48
|
-
"aria-hidden": "true",
|
|
49
|
-
className: "size-[18px] shrink-0 self-start text-muted-foreground group-hover/door:text-primary"
|
|
50
|
-
}
|
|
51
|
-
)
|
|
42
|
+
/* @__PURE__ */ jsx("span", { className: "truncate font-mono text-[13px] text-muted-foreground", children: tool.host })
|
|
43
|
+
] })
|
|
52
44
|
]
|
|
53
45
|
}
|
|
54
46
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CardTitleBlock.js","sources":["../../../../../src/modules/communityTools/ui/CardTitleBlock.tsx"],"sourcesContent":["import type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport {
|
|
1
|
+
{"version":3,"file":"CardTitleBlock.js","sources":["../../../../../src/modules/communityTools/ui/CardTitleBlock.tsx"],"sourcesContent":["import type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport { Globe } from 'lucide-react'\nimport { cn } from '~/lib/utils'\nimport { recordSiteOpen } from '../siteOpen'\nimport { screenshotUrl } from './FeedRow'\nimport { LifecycleBadge, UnreviewedBadge } from './LifecycleBadge'\n\n/**\n * The card's door: favicon (a globe when the site has none), name, badges and\n * address, all one link that opens the site.\n */\nexport function CardTitleBlock({ tool }: { tool: CommunityTool }) {\n const archived = tool.lifecycle.state === 'archived'\n return (\n <a\n href={tool.url}\n onClick={() => recordSiteOpen(tool.slug)}\n data-testid=\"card-title-block\"\n className={cn(\n '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',\n 'hover:border-primary/40 hover:bg-primary/[0.07] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',\n archived && 'opacity-70 grayscale',\n )}\n >\n <span className=\"inline-flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-border bg-card text-muted-foreground\">\n {tool.faviconId ? (\n <img\n src={screenshotUrl(tool.faviconId, 64)}\n alt=\"\"\n className=\"size-7\"\n />\n ) : (\n <Globe aria-hidden=\"true\" className=\"size-6\" strokeWidth={1.75} />\n )}\n </span>\n <span className=\"flex min-w-0 flex-col gap-0.5\">\n <span className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n <span\n data-testid=\"card-tool-name\"\n className=\"font-serif text-[26px] leading-tight font-semibold\"\n >\n {tool.name}\n </span>\n <LifecycleBadge lifecycle={tool.lifecycle} />\n {!tool.reviewed && <UnreviewedBadge />}\n </span>\n <span className=\"truncate font-mono text-[13px] text-muted-foreground\">\n {tool.host}\n </span>\n </span>\n </a>\n )\n}\n"],"names":[],"mappings":";;;;;;AAWO,SAAS,eAAe,EAAE,QAAiC;AAChE,QAAM,WAAW,KAAK,UAAU,UAAU;AAC1C,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAM,KAAK;AAAA,MACX,SAAS,MAAM,eAAe,KAAK,IAAI;AAAA,MACvC,eAAY;AAAA,MACZ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,YAAY;AAAA,MAAA;AAAA,MAGd,UAAA;AAAA,QAAA,oBAAC,QAAA,EAAK,WAAU,0IACb,UAAA,KAAK,YACJ;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,KAAK,cAAc,KAAK,WAAW,EAAE;AAAA,YACrC,KAAI;AAAA,YACJ,WAAU;AAAA,UAAA;AAAA,QAAA,wBAGX,OAAA,EAAM,eAAY,QAAO,WAAU,UAAS,aAAa,KAAA,CAAM,GAEpE;AAAA,QACA,qBAAC,QAAA,EAAK,WAAU,iCACd,UAAA;AAAA,UAAA,qBAAC,QAAA,EAAK,WAAU,+CACd,UAAA;AAAA,YAAA;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,eAAY;AAAA,gBACZ,WAAU;AAAA,gBAET,UAAA,KAAK;AAAA,cAAA;AAAA,YAAA;AAAA,YAER,oBAAC,gBAAA,EAAe,WAAW,KAAK,UAAA,CAAW;AAAA,YAC1C,CAAC,KAAK,YAAY,oBAAC,iBAAA,CAAA,CAAgB;AAAA,UAAA,GACtC;AAAA,UACA,oBAAC,QAAA,EAAK,WAAU,wDACb,eAAK,KAAA,CACR;AAAA,QAAA,EAAA,CACF;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|
|
@@ -16,14 +16,12 @@ import { Toast } from "./Toast.js";
|
|
|
16
16
|
import { ToolCard } from "./ToolCard.js";
|
|
17
17
|
import { ToolFormDialog } from "./ToolFormDialog.js";
|
|
18
18
|
import { ToolSearch } from "./ToolSearch.js";
|
|
19
|
+
import { openSite } from "../siteOpen.js";
|
|
19
20
|
const KBD = "rounded-[5px] border border-b-2 border-border bg-card px-1.5 py-px text-[11px] font-semibold text-muted-foreground";
|
|
20
21
|
const RESULTS_ID = "community-results-grid";
|
|
21
22
|
function optionIdOf(slug) {
|
|
22
23
|
return `community-option-${slug}`;
|
|
23
24
|
}
|
|
24
|
-
function openSite(url) {
|
|
25
|
-
window.open(url, "_blank", "noopener,noreferrer");
|
|
26
|
-
}
|
|
27
25
|
function CommunityPage({
|
|
28
26
|
tag,
|
|
29
27
|
sort,
|
|
@@ -224,7 +222,7 @@ function CommunityPage({
|
|
|
224
222
|
);
|
|
225
223
|
} else if (e.key === "Enter" && current) {
|
|
226
224
|
e.preventDefault();
|
|
227
|
-
openSite(current.tool
|
|
225
|
+
openSite(current.tool);
|
|
228
226
|
} else if (e.key === "ArrowRight" && current && e.currentTarget.selectionStart === e.currentTarget.value.length) {
|
|
229
227
|
e.preventDefault();
|
|
230
228
|
void openCard(current.tool.slug);
|