@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommunityPage.js","sources":["../../../../../src/modules/communityTools/ui/CommunityPage.tsx"],"sourcesContent":["import type {\n CommunityTool,\n FeedSort,\n MatchField,\n SortDir,\n} from '@igstack/app-catalog-shared-core'\nimport { rankTools, searchTools } from '@igstack/app-catalog-shared-core'\nimport { useLocation, useNavigate } from '@tanstack/react-router'\nimport { useVirtualizer } from '@tanstack/react-virtual'\nimport { AlertTriangle, Plus, RefreshCw, Search, Star } from 'lucide-react'\nimport {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useAuthModal } from '~/modules/auth/AuthModalContext'\nimport { Skeleton } from '~/ui/skeleton'\nimport { useCommunityTools } from '../useCommunityTools'\nimport { FeedRow, ROW_THUMB } from './FeedRow'\nimport { FeedSortControl, feedHeading } from './FeedSortControl'\nimport { MarginPreview } from './MarginPreview'\nimport { MyToolsShelf } from './MyToolsShelf'\nimport { TagRow } from './TagRow'\nimport { Toast } from './Toast'\nimport type { ToastMessage } from './Toast'\nimport { ToolCard } from './ToolCard'\nimport { ToolFormDialog } from './ToolFormDialog'\nimport { ToolSearch } from './ToolSearch'\n\nconst KBD =\n 'rounded-[5px] border border-b-2 border-border bg-card px-1.5 py-px text-[11px] font-semibold text-muted-foreground'\n\ninterface ListItem {\n tool: CommunityTool\n matched?: MatchField[]\n}\n\nexport interface CommunityPageProps {\n /** `?tag=`: `<kind>:<value>`, or none for the whole feed. */\n tag: string | undefined\n sort: FeedSort\n dir: SortDir\n onTagChange: (tag: string | undefined) => void\n onSortChange: (sort: FeedSort, dir: SortDir) => void\n /** The tool whose card is open over the page (`/community/<slug>`). */\n cardSlug?: string\n}\n\nconst RESULTS_ID = 'community-results-grid'\n\n/** Stable per tool, so the active descendant survives re-renders and re-ranking. */\nfunction optionIdOf(slug: string): string {\n return `community-option-${slug}`\n}\n\nfunction openSite(url: string) {\n window.open(url, '_blank', 'noopener,noreferrer')\n}\n\nexport function CommunityPage({\n tag,\n sort,\n dir,\n onTagChange,\n onSortChange,\n cardSlug,\n}: CommunityPageProps) {\n const {\n list,\n pins,\n appNames,\n isAuthenticated,\n isAuthLoading,\n pin,\n unpin,\n pinBusySlug,\n reorder,\n like,\n unlike,\n withMyLike,\n } = useCommunityTools()\n const { open: openSignIn } = useAuthModal()\n const navigate = useNavigate()\n // Signing in comes back here: this page, or the card open over it. The\n // router's own location, so it holds under any history (and in tests).\n const here = useLocation({ select: (l) => l.href })\n const signInHere = () => openSignIn(here)\n\n const [query, setQuery] = useState('')\n const [selected, setSelected] = useState(0)\n const tokens = useMemo(\n () => query.trim().toLowerCase().split(/\\s+/).filter(Boolean),\n [query],\n )\n const searching = tokens.length > 0\n\n const [toast, setToast] = useState<ToastMessage | null>(null)\n const toastIdRef = useRef(0)\n const showToast = useCallback((message: Omit<ToastMessage, 'id'>) => {\n toastIdRef.current += 1\n setToast({ ...message, id: toastIdRef.current })\n }, [])\n const dismissToast = useCallback(() => setToast(null), [])\n\n const tools = useMemo(() => list.data ?? [], [list.data])\n const withTag = useCallback(\n (all: CommunityTool[]) =>\n tag\n ? all.filter((t) => t.tags.some((g) => `${g.kind}:${g.value}` === tag))\n : all,\n [tag],\n )\n\n const feed = useMemo<ListItem[]>(\n () => withTag(rankTools(tools, sort, dir)).map((tool) => ({ tool })),\n [tools, sort, dir, withTag],\n )\n\n // Strongest match first; archived tools still found, after the rest.\n const results = useMemo<ListItem[]>(\n () =>\n searching\n ? searchTools(withTag(tools), query, appNames, sort, dir).map(\n ({ tool, fields }) => ({ tool, matched: fields }),\n )\n : [],\n [searching, tools, query, appNames, sort, dir, withTag],\n )\n\n const items = searching ? results : feed\n const selectedIndex = Math.min(selected, results.length - 1)\n const selectedTool = results[selectedIndex]?.tool\n const activeOptionId =\n searching && selectedTool ? optionIdOf(selectedTool.slug) : undefined\n\n const pinnedSlugs = useMemo(\n () => new Set((pins.data ?? []).map((t) => t.slug)),\n [pins.data],\n )\n\n const pinError = (retry: () => void) => () =>\n showToast({\n tone: 'error',\n text: 'Could not save the pin. Check your connection.',\n action: { label: 'Try again', run: retry },\n })\n\n const pinTool = (slug: string) =>\n pin.mutate(\n { slug },\n {\n onSuccess: () =>\n showToast({\n tone: 'info',\n text: 'Pinned to My tools',\n action: { label: 'Undo', run: () => unpinTool(slug, false) },\n }),\n onError: pinError(() => pinTool(slug)),\n },\n )\n const unpinTool = (slug: string, offerUndo = true) =>\n unpin.mutate(\n { slug },\n {\n onSuccess: () => {\n if (offerUndo) {\n showToast({\n tone: 'info',\n text: 'Removed from My tools',\n action: { label: 'Undo', run: () => pinTool(slug) },\n })\n }\n },\n onError: pinError(() => unpinTool(slug, offerUndo)),\n },\n )\n const togglePin = (tool: CommunityTool) => {\n if (!isAuthenticated) {\n signInHere()\n return\n }\n if (pinnedSlugs.has(tool.slug)) unpinTool(tool.slug)\n else pinTool(tool.slug)\n }\n\n const toggleLike = (tool: CommunityTool) => {\n if (!isAuthenticated) {\n signInHere()\n return\n }\n const save = tool.likedByMe ? unlike : like\n save.mutate(\n { slug: tool.slug },\n {\n onError: () =>\n showToast({\n tone: 'error',\n text: 'Could not save the like. Check your connection.',\n action: { label: 'Try again', run: () => toggleLike(tool) },\n }),\n },\n )\n }\n\n // A failed save puts the tiles back (the pins refetch); say why they moved.\n const reorderPins = (slugs: string[]) =>\n reorder.mutate(\n { slugs },\n {\n onError: () =>\n showToast({\n tone: 'error',\n text: \"Couldn't save the new order\",\n action: { label: 'Try again', run: () => reorderPins(slugs) },\n }),\n },\n )\n\n // Open: `{}` from Add a tool, the search text from \"Add it\".\n const [adding, setAdding] = useState<{ prefillName?: string } | null>(null)\n const startAdd = (prefillName?: string) => {\n // Adding needs an identity: sign in, then come back to this page.\n if (!isAuthenticated) signInHere()\n else setAdding({ prefillName })\n }\n\n const openCard = (slug: string, replace = false) =>\n navigate({\n to: '/community/$slug',\n params: { slug },\n search: (prev) => prev,\n replace,\n })\n const closeCard = () =>\n void navigate({ to: '/community', search: (prev) => prev })\n\n // The page column scrolls (the layout pins the header), not the list: the\n // virtualizer needs where the list starts inside that column. Everything\n // above it can change height — the shelf loads, hides while searching, goes\n // from 2 to 4 columns at `md` — and all of that resizes the page, so one\n // observer on it catches each case.\n const scrollRef = useRef<HTMLDivElement>(null)\n const pageRef = useRef<HTMLDivElement>(null)\n const [listEl, setListEl] = useState<HTMLDivElement | null>(null)\n const [scrollMargin, setScrollMargin] = useState(0)\n useEffect(() => {\n const scroller = scrollRef.current\n const page = pageRef.current\n if (!scroller || !page || !listEl) return\n const measure = () =>\n setScrollMargin(\n listEl.getBoundingClientRect().top -\n scroller.getBoundingClientRect().top +\n scroller.scrollTop,\n )\n const observer = new ResizeObserver(measure)\n observer.observe(page)\n measure()\n return () => observer.disconnect()\n }, [listEl])\n // No layout, no window (jsdom, a print preview): the column measures 0 tall,\n // a virtualizer mounts nothing, so every row is rendered instead.\n const [hasLayout, setHasLayout] = useState(true)\n useLayoutEffect(() => {\n setHasLayout((scrollRef.current?.clientHeight ?? 0) > 0)\n }, [])\n // Stable on purpose: the virtualizer keys its measurement cache on this\n // function's identity, so a new arrow every render re-renders forever.\n const getItemKey = useCallback(\n (i: number) => items[i]?.tool.slug ?? i,\n [items],\n )\n // TanStack Virtual hands back functions the React Compiler cannot memoize\n // safely, so it skips this hook; registered in the root eslint.config.js.\n // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Virtual\n const virtualizer = useVirtualizer({\n getScrollElement: () => scrollRef.current,\n // The column's height before it is measured, so the first paint already\n // holds the rows that will be visible.\n initialRect: { width: 0, height: 800 },\n count: items.length,\n // The thumbnail sets the row: 150 px, 12 px padding either side, borders.\n estimateSize: () => 176,\n overscan: 6,\n gap: 8,\n scrollMargin,\n getItemKey,\n })\n\n // Keep the selected result on screen. The next row is almost always already\n // mounted (overscan), and the browser scrolls it exactly; the virtualizer's\n // own estimate-based scroll is only for a row that is not there yet.\n useEffect(() => {\n if (!searching || selectedIndex < 0) return\n const row = listEl?.querySelector(`[data-index=\"${selectedIndex}\"]`)\n if (row) row.scrollIntoView({ block: 'nearest' })\n else virtualizer.scrollToIndex(selectedIndex, { align: 'auto' })\n }, [searching, selectedIndex, virtualizer, listEl])\n\n const onSearchKey = (e: React.KeyboardEvent<HTMLInputElement>) => {\n const current = results[selectedIndex]\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n if (results.length === 0) return\n e.preventDefault()\n const step = e.key === 'ArrowDown' ? 1 : -1\n setSelected(\n Math.min(results.length - 1, Math.max(0, selectedIndex + step)),\n )\n } else if (e.key === 'Enter' && current) {\n e.preventDefault()\n openSite(current.tool.url)\n } else if (\n e.key === 'ArrowRight' &&\n current &&\n e.currentTarget.selectionStart === e.currentTarget.value.length\n ) {\n // Only at the end of the text, so → still moves the caret inside it.\n e.preventDefault()\n void openCard(current.tool.slug)\n }\n }\n\n // Without layout every row is \"virtual\", at its natural place in the flow.\n const virtualItems = hasLayout\n ? virtualizer.getVirtualItems()\n : items.map((_, index) => ({\n key: items[index]!.tool.slug,\n index,\n start: virtualizer.options.scrollMargin,\n }))\n\n return (\n <div\n ref={scrollRef}\n data-preview-scroller\n className=\"min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable]\"\n >\n <div\n ref={pageRef}\n data-preview-column\n className=\"mx-auto w-full max-w-[1000px] pt-1.5 pb-10\"\n >\n <div className=\"mb-[22px] flex items-center gap-2.5\">\n <ToolSearch\n value={query}\n onChange={(value) => {\n setQuery(value)\n setSelected(0)\n }}\n resultCount={results.length}\n resultsId={searching && results.length > 0 ? RESULTS_ID : undefined}\n activeDescendant={activeOptionId}\n onKey={onSearchKey}\n />\n <button\n type=\"button\"\n onClick={() => startAdd()}\n className=\"inline-flex h-10 shrink-0 items-center gap-[7px] rounded-[10px] border border-border bg-card px-3.5 text-[13.5px] font-bold whitespace-nowrap hover:border-primary/40 focus-visible:outline-2 focus-visible:outline-ring\"\n >\n <Plus aria-hidden=\"true\" className=\"size-4\" />\n Add a tool\n </button>\n </div>\n\n {searching ? (\n <div className=\"-mt-2 mb-3.5 ml-1 flex items-center gap-2 text-[13px] font-semibold text-muted-foreground\">\n <Star\n aria-hidden=\"true\"\n className=\"size-4 fill-amber-400 text-amber-600\"\n />\n My tools is hidden while you search\n </div>\n ) : (\n <div className=\"mb-[26px]\">\n <MyToolsShelf\n pins={pins.data ?? []}\n isLoading={isAuthLoading || (isAuthenticated && pins.isPending)}\n onUnpin={(tool) => togglePin(tool)}\n onReorder={reorderPins}\n />\n </div>\n )}\n\n <section\n aria-label={searching ? 'Search results' : feedHeading(sort, dir)}\n data-testid={searching ? 'community-results' : 'community-feed'}\n >\n {!searching && (\n <>\n <div className=\"mb-3 flex items-center justify-between\">\n <h2 className=\"font-serif text-[19px] font-semibold\">\n {feedHeading(sort, dir)}\n </h2>\n <FeedSortControl\n sort={sort}\n dir={dir}\n onChange={onSortChange}\n />\n </div>\n <TagRow\n tools={rankTools(tools, sort, dir)}\n activeTag={tag}\n appNames={appNames}\n onChoose={onTagChange}\n />\n </>\n )}\n\n {list.isPending ? (\n <FeedSkeleton />\n ) : list.isError ? (\n <FeedError onRetry={() => void list.refetch()} />\n ) : searching && results.length === 0 ? (\n <NoResults\n query={query.trim()}\n onAdd={() => startAdd(query.trim())}\n />\n ) : (\n <div\n ref={setListEl}\n id={searching ? RESULTS_ID : undefined}\n role={searching ? 'grid' : undefined}\n aria-label={searching ? 'Search results' : undefined}\n aria-activedescendant={activeOptionId}\n className={hasLayout ? 'relative' : 'flex flex-col gap-2'}\n style={\n hasLayout ? { height: virtualizer.getTotalSize() } : undefined\n }\n >\n {virtualItems.map((row) => {\n const item = items[row.index]\n if (!item) return null\n return (\n <div\n key={row.key}\n data-index={row.index}\n ref={virtualizer.measureElement}\n className={\n hasLayout ? 'absolute inset-x-0 top-0' : undefined\n }\n style={\n hasLayout\n ? {\n transform: `translateY(${row.start - virtualizer.options.scrollMargin}px)`,\n }\n : undefined\n }\n >\n <FeedRow\n tool={withMyLike(item.tool)}\n rank={searching ? undefined : row.index + 1}\n sort={sort}\n tokens={tokens}\n matched={item.matched}\n selected={searching && row.index === selectedIndex}\n optionId={\n searching ? optionIdOf(item.tool.slug) : undefined\n }\n pinned={pinnedSlugs.has(item.tool.slug)}\n pinBusy={pinBusySlug === item.tool.slug}\n onTogglePin={() => togglePin(item.tool)}\n onToggleLike={() => toggleLike(withMyLike(item.tool))}\n />\n </div>\n )\n })}\n </div>\n )}\n\n {searching && results.length > 0 && (\n <div className=\"mt-3.5 flex items-center gap-1.5 text-[12.5px] font-semibold text-muted-foreground\">\n <kbd className={KBD}>↑</kbd>\n <kbd className={KBD}>↓</kbd>\n <span className=\"mr-2.5\">move</span>\n <kbd className={KBD}>Enter</kbd>\n <span className=\"mr-2.5\">open site</span>\n <kbd className={KBD}>→</kbd>\n <span className=\"mr-2.5\">open card</span>\n <kbd className={KBD}>Esc</kbd>\n <span>clear</span>\n </div>\n )}\n </section>\n\n <MarginPreview\n list={listEl}\n tools={tools}\n disabled={!!cardSlug || !!adding}\n />\n {cardSlug && (\n <ToolCard\n slug={cardSlug}\n siblings={items.map((item) => item.tool.slug)}\n onClose={closeCard}\n // Stepping swaps the card in place; Back still leaves the card.\n onNavigate={(slug) => void openCard(slug, true)}\n pinned={pinnedSlugs.has(cardSlug)}\n pinBusy={pinBusySlug === cardSlug}\n onTogglePin={togglePin}\n appNames={appNames}\n // One step: the card closes onto the feed already filtered.\n onChooseTag={(chosen) =>\n void navigate({\n to: '/community',\n search: (prev) => ({ ...prev, tag: chosen }),\n })\n }\n />\n )}\n {adding && (\n <ToolFormDialog\n mode=\"add\"\n prefillName={adding.prefillName}\n onClose={() => setAdding(null)}\n onOpenCard={(slug) => {\n setAdding(null)\n void openCard(slug)\n }}\n onDone={(slug) => {\n setAdding(null)\n showToast({\n tone: 'ok',\n text: (\n <>\n <b>Live now</b>, in the review list\n </>\n ),\n action: { label: 'Open card', run: () => void openCard(slug) },\n })\n }}\n />\n )}\n {/* After the card, so a toast raised from it shows above its scrim. */}\n <Toast toast={toast} onDismiss={dismissToast} />\n </div>\n </div>\n )\n}\n\n/** Three placeholder rows at the real row height. */\nfunction FeedSkeleton() {\n return (\n <div\n data-testid=\"feed-skeleton\"\n aria-busy=\"true\"\n className=\"flex flex-col gap-2\"\n >\n {[0, 1, 2].map((i) => (\n <div\n key={i}\n className=\"flex min-h-[176px] items-center gap-4 rounded-[14px] border border-border bg-card py-3 pl-3\"\n >\n <Skeleton className=\"h-[22px] w-[26px]\" />\n <Skeleton className={ROW_THUMB} />\n <div className=\"flex flex-1 flex-col gap-2\">\n <Skeleton className=\"h-2.5 w-2/5\" />\n <Skeleton className=\"h-2.5 w-4/5\" />\n <Skeleton className=\"h-2.5 w-1/4\" />\n </div>\n {/* The Details panel's reserved width. */}\n <div className=\"w-[120px] shrink-0\" />\n </div>\n ))}\n </div>\n )\n}\n\nfunction FeedError({ onRetry }: { onRetry: () => void }) {\n return (\n <div\n data-testid=\"feed-error\"\n role=\"alert\"\n className=\"flex items-center gap-3.5 rounded-[14px] border border-destructive/30 bg-destructive/5 px-5 py-[18px]\"\n >\n <AlertTriangle\n aria-hidden=\"true\"\n className=\"size-[22px] shrink-0 text-destructive\"\n />\n <div>\n <b className=\"font-bold\">Could not load community tools.</b>\n <p className=\"mt-0.5 text-muted-foreground\">\n The server did not answer. Your pinned tools above still work.\n </p>\n </div>\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"ml-auto inline-flex h-10 shrink-0 items-center gap-[7px] rounded-[10px] border border-border bg-card px-3.5 text-[13.5px] font-bold hover:border-primary/40 focus-visible:outline-2 focus-visible:outline-ring\"\n >\n <RefreshCw aria-hidden=\"true\" className=\"size-4\" />\n Try again\n </button>\n </div>\n )\n}\n\nfunction NoResults({ query, onAdd }: { query: string; onAdd: () => void }) {\n return (\n <div\n data-testid=\"no-results\"\n className=\"flex gap-3.5 rounded-[14px] border border-border bg-card px-5 py-[18px]\"\n >\n <Search\n aria-hidden=\"true\"\n className=\"mt-0.5 size-5 shrink-0 text-muted-foreground\"\n />\n <div>\n <b className=\"font-bold\">Nothing matches “{query}”.</b>\n <p className=\"mt-0.5 text-muted-foreground\">\n Try a word from the page itself, or the name of the person who shared\n it. Know the tool?{' '}\n <button\n type=\"button\"\n onClick={onAdd}\n className=\"font-bold text-primary hover:underline focus-visible:outline-2 focus-visible:outline-ring\"\n >\n Add it\n </button>\n </p>\n </div>\n </div>\n )\n}\n"],"names":["_a"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CommunityPage.js","sources":["../../../../../src/modules/communityTools/ui/CommunityPage.tsx"],"sourcesContent":["import type {\n CommunityTool,\n FeedSort,\n MatchField,\n SortDir,\n} from '@igstack/app-catalog-shared-core'\nimport { rankTools, searchTools } from '@igstack/app-catalog-shared-core'\nimport { useLocation, useNavigate } from '@tanstack/react-router'\nimport { useVirtualizer } from '@tanstack/react-virtual'\nimport { AlertTriangle, Plus, RefreshCw, Search, Star } from 'lucide-react'\nimport {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { useAuthModal } from '~/modules/auth/AuthModalContext'\nimport { Skeleton } from '~/ui/skeleton'\nimport { useCommunityTools } from '../useCommunityTools'\nimport { FeedRow, ROW_THUMB } from './FeedRow'\nimport { FeedSortControl, feedHeading } from './FeedSortControl'\nimport { MarginPreview } from './MarginPreview'\nimport { MyToolsShelf } from './MyToolsShelf'\nimport { TagRow } from './TagRow'\nimport { Toast } from './Toast'\nimport type { ToastMessage } from './Toast'\nimport { ToolCard } from './ToolCard'\nimport { ToolFormDialog } from './ToolFormDialog'\nimport { ToolSearch } from './ToolSearch'\nimport { openSite } from '../siteOpen'\n\nconst KBD =\n 'rounded-[5px] border border-b-2 border-border bg-card px-1.5 py-px text-[11px] font-semibold text-muted-foreground'\n\ninterface ListItem {\n tool: CommunityTool\n matched?: MatchField[]\n}\n\nexport interface CommunityPageProps {\n /** `?tag=`: `<kind>:<value>`, or none for the whole feed. */\n tag: string | undefined\n sort: FeedSort\n dir: SortDir\n onTagChange: (tag: string | undefined) => void\n onSortChange: (sort: FeedSort, dir: SortDir) => void\n /** The tool whose card is open over the page (`/community/<slug>`). */\n cardSlug?: string\n}\n\nconst RESULTS_ID = 'community-results-grid'\n\n/** Stable per tool, so the active descendant survives re-renders and re-ranking. */\nfunction optionIdOf(slug: string): string {\n return `community-option-${slug}`\n}\n\nexport function CommunityPage({\n tag,\n sort,\n dir,\n onTagChange,\n onSortChange,\n cardSlug,\n}: CommunityPageProps) {\n const {\n list,\n pins,\n appNames,\n isAuthenticated,\n isAuthLoading,\n pin,\n unpin,\n pinBusySlug,\n reorder,\n like,\n unlike,\n withMyLike,\n } = useCommunityTools()\n const { open: openSignIn } = useAuthModal()\n const navigate = useNavigate()\n // Signing in comes back here: this page, or the card open over it. The\n // router's own location, so it holds under any history (and in tests).\n const here = useLocation({ select: (l) => l.href })\n const signInHere = () => openSignIn(here)\n\n const [query, setQuery] = useState('')\n const [selected, setSelected] = useState(0)\n const tokens = useMemo(\n () => query.trim().toLowerCase().split(/\\s+/).filter(Boolean),\n [query],\n )\n const searching = tokens.length > 0\n\n const [toast, setToast] = useState<ToastMessage | null>(null)\n const toastIdRef = useRef(0)\n const showToast = useCallback((message: Omit<ToastMessage, 'id'>) => {\n toastIdRef.current += 1\n setToast({ ...message, id: toastIdRef.current })\n }, [])\n const dismissToast = useCallback(() => setToast(null), [])\n\n const tools = useMemo(() => list.data ?? [], [list.data])\n const withTag = useCallback(\n (all: CommunityTool[]) =>\n tag\n ? all.filter((t) => t.tags.some((g) => `${g.kind}:${g.value}` === tag))\n : all,\n [tag],\n )\n\n const feed = useMemo<ListItem[]>(\n () => withTag(rankTools(tools, sort, dir)).map((tool) => ({ tool })),\n [tools, sort, dir, withTag],\n )\n\n // Strongest match first; archived tools still found, after the rest.\n const results = useMemo<ListItem[]>(\n () =>\n searching\n ? searchTools(withTag(tools), query, appNames, sort, dir).map(\n ({ tool, fields }) => ({ tool, matched: fields }),\n )\n : [],\n [searching, tools, query, appNames, sort, dir, withTag],\n )\n\n const items = searching ? results : feed\n const selectedIndex = Math.min(selected, results.length - 1)\n const selectedTool = results[selectedIndex]?.tool\n const activeOptionId =\n searching && selectedTool ? optionIdOf(selectedTool.slug) : undefined\n\n const pinnedSlugs = useMemo(\n () => new Set((pins.data ?? []).map((t) => t.slug)),\n [pins.data],\n )\n\n const pinError = (retry: () => void) => () =>\n showToast({\n tone: 'error',\n text: 'Could not save the pin. Check your connection.',\n action: { label: 'Try again', run: retry },\n })\n\n const pinTool = (slug: string) =>\n pin.mutate(\n { slug },\n {\n onSuccess: () =>\n showToast({\n tone: 'info',\n text: 'Pinned to My tools',\n action: { label: 'Undo', run: () => unpinTool(slug, false) },\n }),\n onError: pinError(() => pinTool(slug)),\n },\n )\n const unpinTool = (slug: string, offerUndo = true) =>\n unpin.mutate(\n { slug },\n {\n onSuccess: () => {\n if (offerUndo) {\n showToast({\n tone: 'info',\n text: 'Removed from My tools',\n action: { label: 'Undo', run: () => pinTool(slug) },\n })\n }\n },\n onError: pinError(() => unpinTool(slug, offerUndo)),\n },\n )\n const togglePin = (tool: CommunityTool) => {\n if (!isAuthenticated) {\n signInHere()\n return\n }\n if (pinnedSlugs.has(tool.slug)) unpinTool(tool.slug)\n else pinTool(tool.slug)\n }\n\n const toggleLike = (tool: CommunityTool) => {\n if (!isAuthenticated) {\n signInHere()\n return\n }\n const save = tool.likedByMe ? unlike : like\n save.mutate(\n { slug: tool.slug },\n {\n onError: () =>\n showToast({\n tone: 'error',\n text: 'Could not save the like. Check your connection.',\n action: { label: 'Try again', run: () => toggleLike(tool) },\n }),\n },\n )\n }\n\n // A failed save puts the tiles back (the pins refetch); say why they moved.\n const reorderPins = (slugs: string[]) =>\n reorder.mutate(\n { slugs },\n {\n onError: () =>\n showToast({\n tone: 'error',\n text: \"Couldn't save the new order\",\n action: { label: 'Try again', run: () => reorderPins(slugs) },\n }),\n },\n )\n\n // Open: `{}` from Add a tool, the search text from \"Add it\".\n const [adding, setAdding] = useState<{ prefillName?: string } | null>(null)\n const startAdd = (prefillName?: string) => {\n // Adding needs an identity: sign in, then come back to this page.\n if (!isAuthenticated) signInHere()\n else setAdding({ prefillName })\n }\n\n const openCard = (slug: string, replace = false) =>\n navigate({\n to: '/community/$slug',\n params: { slug },\n search: (prev) => prev,\n replace,\n })\n const closeCard = () =>\n void navigate({ to: '/community', search: (prev) => prev })\n\n // The page column scrolls (the layout pins the header), not the list: the\n // virtualizer needs where the list starts inside that column. Everything\n // above it can change height — the shelf loads, hides while searching, goes\n // from 2 to 4 columns at `md` — and all of that resizes the page, so one\n // observer on it catches each case.\n const scrollRef = useRef<HTMLDivElement>(null)\n const pageRef = useRef<HTMLDivElement>(null)\n const [listEl, setListEl] = useState<HTMLDivElement | null>(null)\n const [scrollMargin, setScrollMargin] = useState(0)\n useEffect(() => {\n const scroller = scrollRef.current\n const page = pageRef.current\n if (!scroller || !page || !listEl) return\n const measure = () =>\n setScrollMargin(\n listEl.getBoundingClientRect().top -\n scroller.getBoundingClientRect().top +\n scroller.scrollTop,\n )\n const observer = new ResizeObserver(measure)\n observer.observe(page)\n measure()\n return () => observer.disconnect()\n }, [listEl])\n // No layout, no window (jsdom, a print preview): the column measures 0 tall,\n // a virtualizer mounts nothing, so every row is rendered instead.\n const [hasLayout, setHasLayout] = useState(true)\n useLayoutEffect(() => {\n setHasLayout((scrollRef.current?.clientHeight ?? 0) > 0)\n }, [])\n // Stable on purpose: the virtualizer keys its measurement cache on this\n // function's identity, so a new arrow every render re-renders forever.\n const getItemKey = useCallback(\n (i: number) => items[i]?.tool.slug ?? i,\n [items],\n )\n // TanStack Virtual hands back functions the React Compiler cannot memoize\n // safely, so it skips this hook; registered in the root eslint.config.js.\n // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Virtual\n const virtualizer = useVirtualizer({\n getScrollElement: () => scrollRef.current,\n // The column's height before it is measured, so the first paint already\n // holds the rows that will be visible.\n initialRect: { width: 0, height: 800 },\n count: items.length,\n // The thumbnail sets the row: 150 px, 12 px padding either side, borders.\n estimateSize: () => 176,\n overscan: 6,\n gap: 8,\n scrollMargin,\n getItemKey,\n })\n\n // Keep the selected result on screen. The next row is almost always already\n // mounted (overscan), and the browser scrolls it exactly; the virtualizer's\n // own estimate-based scroll is only for a row that is not there yet.\n useEffect(() => {\n if (!searching || selectedIndex < 0) return\n const row = listEl?.querySelector(`[data-index=\"${selectedIndex}\"]`)\n if (row) row.scrollIntoView({ block: 'nearest' })\n else virtualizer.scrollToIndex(selectedIndex, { align: 'auto' })\n }, [searching, selectedIndex, virtualizer, listEl])\n\n const onSearchKey = (e: React.KeyboardEvent<HTMLInputElement>) => {\n const current = results[selectedIndex]\n if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {\n if (results.length === 0) return\n e.preventDefault()\n const step = e.key === 'ArrowDown' ? 1 : -1\n setSelected(\n Math.min(results.length - 1, Math.max(0, selectedIndex + step)),\n )\n } else if (e.key === 'Enter' && current) {\n e.preventDefault()\n openSite(current.tool)\n } else if (\n e.key === 'ArrowRight' &&\n current &&\n e.currentTarget.selectionStart === e.currentTarget.value.length\n ) {\n // Only at the end of the text, so → still moves the caret inside it.\n e.preventDefault()\n void openCard(current.tool.slug)\n }\n }\n\n // Without layout every row is \"virtual\", at its natural place in the flow.\n const virtualItems = hasLayout\n ? virtualizer.getVirtualItems()\n : items.map((_, index) => ({\n key: items[index]!.tool.slug,\n index,\n start: virtualizer.options.scrollMargin,\n }))\n\n return (\n <div\n ref={scrollRef}\n data-preview-scroller\n className=\"min-h-0 flex-1 overflow-y-auto [scrollbar-gutter:stable]\"\n >\n <div\n ref={pageRef}\n data-preview-column\n className=\"mx-auto w-full max-w-[1000px] pt-1.5 pb-10\"\n >\n <div className=\"mb-[22px] flex items-center gap-2.5\">\n <ToolSearch\n value={query}\n onChange={(value) => {\n setQuery(value)\n setSelected(0)\n }}\n resultCount={results.length}\n resultsId={searching && results.length > 0 ? RESULTS_ID : undefined}\n activeDescendant={activeOptionId}\n onKey={onSearchKey}\n />\n <button\n type=\"button\"\n onClick={() => startAdd()}\n className=\"inline-flex h-10 shrink-0 items-center gap-[7px] rounded-[10px] border border-border bg-card px-3.5 text-[13.5px] font-bold whitespace-nowrap hover:border-primary/40 focus-visible:outline-2 focus-visible:outline-ring\"\n >\n <Plus aria-hidden=\"true\" className=\"size-4\" />\n Add a tool\n </button>\n </div>\n\n {searching ? (\n <div className=\"-mt-2 mb-3.5 ml-1 flex items-center gap-2 text-[13px] font-semibold text-muted-foreground\">\n <Star\n aria-hidden=\"true\"\n className=\"size-4 fill-amber-400 text-amber-600\"\n />\n My tools is hidden while you search\n </div>\n ) : (\n <div className=\"mb-[26px]\">\n <MyToolsShelf\n pins={pins.data ?? []}\n isLoading={isAuthLoading || (isAuthenticated && pins.isPending)}\n onUnpin={(tool) => togglePin(tool)}\n onReorder={reorderPins}\n />\n </div>\n )}\n\n <section\n aria-label={searching ? 'Search results' : feedHeading(sort, dir)}\n data-testid={searching ? 'community-results' : 'community-feed'}\n >\n {!searching && (\n <>\n <div className=\"mb-3 flex items-center justify-between\">\n <h2 className=\"font-serif text-[19px] font-semibold\">\n {feedHeading(sort, dir)}\n </h2>\n <FeedSortControl\n sort={sort}\n dir={dir}\n onChange={onSortChange}\n />\n </div>\n <TagRow\n tools={rankTools(tools, sort, dir)}\n activeTag={tag}\n appNames={appNames}\n onChoose={onTagChange}\n />\n </>\n )}\n\n {list.isPending ? (\n <FeedSkeleton />\n ) : list.isError ? (\n <FeedError onRetry={() => void list.refetch()} />\n ) : searching && results.length === 0 ? (\n <NoResults\n query={query.trim()}\n onAdd={() => startAdd(query.trim())}\n />\n ) : (\n <div\n ref={setListEl}\n id={searching ? RESULTS_ID : undefined}\n role={searching ? 'grid' : undefined}\n aria-label={searching ? 'Search results' : undefined}\n aria-activedescendant={activeOptionId}\n className={hasLayout ? 'relative' : 'flex flex-col gap-2'}\n style={\n hasLayout ? { height: virtualizer.getTotalSize() } : undefined\n }\n >\n {virtualItems.map((row) => {\n const item = items[row.index]\n if (!item) return null\n return (\n <div\n key={row.key}\n data-index={row.index}\n ref={virtualizer.measureElement}\n className={\n hasLayout ? 'absolute inset-x-0 top-0' : undefined\n }\n style={\n hasLayout\n ? {\n transform: `translateY(${row.start - virtualizer.options.scrollMargin}px)`,\n }\n : undefined\n }\n >\n <FeedRow\n tool={withMyLike(item.tool)}\n rank={searching ? undefined : row.index + 1}\n sort={sort}\n tokens={tokens}\n matched={item.matched}\n selected={searching && row.index === selectedIndex}\n optionId={\n searching ? optionIdOf(item.tool.slug) : undefined\n }\n pinned={pinnedSlugs.has(item.tool.slug)}\n pinBusy={pinBusySlug === item.tool.slug}\n onTogglePin={() => togglePin(item.tool)}\n onToggleLike={() => toggleLike(withMyLike(item.tool))}\n />\n </div>\n )\n })}\n </div>\n )}\n\n {searching && results.length > 0 && (\n <div className=\"mt-3.5 flex items-center gap-1.5 text-[12.5px] font-semibold text-muted-foreground\">\n <kbd className={KBD}>↑</kbd>\n <kbd className={KBD}>↓</kbd>\n <span className=\"mr-2.5\">move</span>\n <kbd className={KBD}>Enter</kbd>\n <span className=\"mr-2.5\">open site</span>\n <kbd className={KBD}>→</kbd>\n <span className=\"mr-2.5\">open card</span>\n <kbd className={KBD}>Esc</kbd>\n <span>clear</span>\n </div>\n )}\n </section>\n\n <MarginPreview\n list={listEl}\n tools={tools}\n disabled={!!cardSlug || !!adding}\n />\n {cardSlug && (\n <ToolCard\n slug={cardSlug}\n siblings={items.map((item) => item.tool.slug)}\n onClose={closeCard}\n // Stepping swaps the card in place; Back still leaves the card.\n onNavigate={(slug) => void openCard(slug, true)}\n pinned={pinnedSlugs.has(cardSlug)}\n pinBusy={pinBusySlug === cardSlug}\n onTogglePin={togglePin}\n appNames={appNames}\n // One step: the card closes onto the feed already filtered.\n onChooseTag={(chosen) =>\n void navigate({\n to: '/community',\n search: (prev) => ({ ...prev, tag: chosen }),\n })\n }\n />\n )}\n {adding && (\n <ToolFormDialog\n mode=\"add\"\n prefillName={adding.prefillName}\n onClose={() => setAdding(null)}\n onOpenCard={(slug) => {\n setAdding(null)\n void openCard(slug)\n }}\n onDone={(slug) => {\n setAdding(null)\n showToast({\n tone: 'ok',\n text: (\n <>\n <b>Live now</b>, in the review list\n </>\n ),\n action: { label: 'Open card', run: () => void openCard(slug) },\n })\n }}\n />\n )}\n {/* After the card, so a toast raised from it shows above its scrim. */}\n <Toast toast={toast} onDismiss={dismissToast} />\n </div>\n </div>\n )\n}\n\n/** Three placeholder rows at the real row height. */\nfunction FeedSkeleton() {\n return (\n <div\n data-testid=\"feed-skeleton\"\n aria-busy=\"true\"\n className=\"flex flex-col gap-2\"\n >\n {[0, 1, 2].map((i) => (\n <div\n key={i}\n className=\"flex min-h-[176px] items-center gap-4 rounded-[14px] border border-border bg-card py-3 pl-3\"\n >\n <Skeleton className=\"h-[22px] w-[26px]\" />\n <Skeleton className={ROW_THUMB} />\n <div className=\"flex flex-1 flex-col gap-2\">\n <Skeleton className=\"h-2.5 w-2/5\" />\n <Skeleton className=\"h-2.5 w-4/5\" />\n <Skeleton className=\"h-2.5 w-1/4\" />\n </div>\n {/* The Details panel's reserved width. */}\n <div className=\"w-[120px] shrink-0\" />\n </div>\n ))}\n </div>\n )\n}\n\nfunction FeedError({ onRetry }: { onRetry: () => void }) {\n return (\n <div\n data-testid=\"feed-error\"\n role=\"alert\"\n className=\"flex items-center gap-3.5 rounded-[14px] border border-destructive/30 bg-destructive/5 px-5 py-[18px]\"\n >\n <AlertTriangle\n aria-hidden=\"true\"\n className=\"size-[22px] shrink-0 text-destructive\"\n />\n <div>\n <b className=\"font-bold\">Could not load community tools.</b>\n <p className=\"mt-0.5 text-muted-foreground\">\n The server did not answer. Your pinned tools above still work.\n </p>\n </div>\n <button\n type=\"button\"\n onClick={onRetry}\n className=\"ml-auto inline-flex h-10 shrink-0 items-center gap-[7px] rounded-[10px] border border-border bg-card px-3.5 text-[13.5px] font-bold hover:border-primary/40 focus-visible:outline-2 focus-visible:outline-ring\"\n >\n <RefreshCw aria-hidden=\"true\" className=\"size-4\" />\n Try again\n </button>\n </div>\n )\n}\n\nfunction NoResults({ query, onAdd }: { query: string; onAdd: () => void }) {\n return (\n <div\n data-testid=\"no-results\"\n className=\"flex gap-3.5 rounded-[14px] border border-border bg-card px-5 py-[18px]\"\n >\n <Search\n aria-hidden=\"true\"\n className=\"mt-0.5 size-5 shrink-0 text-muted-foreground\"\n />\n <div>\n <b className=\"font-bold\">Nothing matches “{query}”.</b>\n <p className=\"mt-0.5 text-muted-foreground\">\n Try a word from the page itself, or the name of the person who shared\n it. Know the tool?{' '}\n <button\n type=\"button\"\n onClick={onAdd}\n className=\"font-bold text-primary hover:underline focus-visible:outline-2 focus-visible:outline-ring\"\n >\n Add it\n </button>\n </p>\n </div>\n </div>\n )\n}\n"],"names":["_a"],"mappings":"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"}
|
|
@@ -13,7 +13,10 @@ export interface FeedRowProps {
|
|
|
13
13
|
tool: CommunityTool;
|
|
14
14
|
/** Shown in the ranked feed; search results have no rank. */
|
|
15
15
|
rank?: number;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Newest shows when it was added, Recently updated when the site changed;
|
|
18
|
+
* every other sort the site's date, or when it was added if that is unknown.
|
|
19
|
+
*/
|
|
17
20
|
sort: FeedSort;
|
|
18
21
|
tokens: string[];
|
|
19
22
|
/** Set while searching: which fields the query hit. */
|
|
@@ -34,8 +37,8 @@ export interface FeedRowProps {
|
|
|
34
37
|
export declare const ROW_THUMB = "h-[150px] w-[240px] rounded-[10px]";
|
|
35
38
|
/**
|
|
36
39
|
* One tool in the feed or the search results. Everything left of the end
|
|
37
|
-
* panel (rank, picture, name, summary, meta) is one link that opens the
|
|
38
|
-
*
|
|
40
|
+
* panel (rank, picture, name, summary, meta) is one link that opens the
|
|
41
|
+
* site; the 120 px panel opens the card. The star sits in the meta
|
|
39
42
|
* line as its own target, above that link.
|
|
40
43
|
*/
|
|
41
44
|
export declare function FeedRow({ tool, rank, sort, tokens, matched, selected, optionId, pinned, pinBusy, onTogglePin, onToggleLike, }: FeedRowProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -3,7 +3,8 @@ import { Link } from "@tanstack/react-router";
|
|
|
3
3
|
import { CornerDownLeft, Eye, MessageSquare, PanelRight, Globe } from "lucide-react";
|
|
4
4
|
import { cn } from "../../../lib/utils.js";
|
|
5
5
|
import { highlightTokens } from "../../../util/highlightMatches.js";
|
|
6
|
-
import {
|
|
6
|
+
import { recordSiteOpen } from "../siteOpen.js";
|
|
7
|
+
import { timeAgo, exactDate } from "../timeAgo.js";
|
|
7
8
|
import { LifecycleBadge, UnreviewedBadge } from "./LifecycleBadge.js";
|
|
8
9
|
import { LikeButton } from "./LikeButton.js";
|
|
9
10
|
import { PinButton } from "./PinButton.js";
|
|
@@ -84,7 +85,8 @@ function FeedRow({
|
|
|
84
85
|
const archived = tool.lifecycle.state === "archived";
|
|
85
86
|
const cell = optionId ? "gridcell" : void 0;
|
|
86
87
|
const owners = tool.owners.map((o) => o.name).join(", ");
|
|
87
|
-
const
|
|
88
|
+
const siteUpdatedAt = sort !== "newest" ? tool.siteUpdatedAt : null;
|
|
89
|
+
const dated = siteUpdatedAt ? { label: "Updated", at: siteUpdatedAt } : sort !== "updated" ? { label: "Added", at: tool.createdAt } : null;
|
|
88
90
|
return /* @__PURE__ */ jsxs(
|
|
89
91
|
"div",
|
|
90
92
|
{
|
|
@@ -114,11 +116,10 @@ function FeedRow({
|
|
|
114
116
|
"a",
|
|
115
117
|
{
|
|
116
118
|
href: tool.url,
|
|
117
|
-
|
|
118
|
-
rel: "noopener noreferrer",
|
|
119
|
+
onClick: () => recordSiteOpen(tool.slug),
|
|
119
120
|
"data-site": true,
|
|
120
121
|
"data-testid": "feed-row-name",
|
|
121
|
-
"aria-label": `${tool.name}, opens ${tool.host}
|
|
122
|
+
"aria-label": `${tool.name}, opens ${tool.host}`,
|
|
122
123
|
className: cn(
|
|
123
124
|
"truncate decoration-1 underline-offset-[3px] after:absolute after:inset-y-0 after:left-0 after:right-[120px] after:z-[1] after:rounded-l-[13px] focus-visible:outline-none focus-visible:after:outline-2 focus-visible:after:-outline-offset-[3px] focus-visible:after:outline-ring",
|
|
124
125
|
"group-hover/row:underline group-has-[[data-details]:hover]/row:no-underline",
|
|
@@ -203,12 +204,17 @@ function FeedRow({
|
|
|
203
204
|
}
|
|
204
205
|
)
|
|
205
206
|
] }),
|
|
206
|
-
dated && /* @__PURE__ */
|
|
207
|
+
dated && /* @__PURE__ */ jsxs(
|
|
207
208
|
"span",
|
|
208
209
|
{
|
|
209
210
|
"data-testid": "feed-row-date",
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
title: exactDate(dated.at),
|
|
212
|
+
className: "pointer-events-auto ml-auto shrink-0 pr-4 text-[13px] font-semibold whitespace-nowrap text-muted-foreground",
|
|
213
|
+
children: [
|
|
214
|
+
dated.label,
|
|
215
|
+
" ",
|
|
216
|
+
timeAgo(dated.at)
|
|
217
|
+
]
|
|
212
218
|
}
|
|
213
219
|
)
|
|
214
220
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FeedRow.js","sources":["../../../../../src/modules/communityTools/ui/FeedRow.tsx"],"sourcesContent":["import type {\n CommunityTool,\n FeedSort,\n MatchField,\n} from '@igstack/app-catalog-shared-core'\nimport { Link } from '@tanstack/react-router'\nimport {\n CornerDownLeft,\n Eye,\n Globe,\n MessageSquare,\n PanelRight,\n} from 'lucide-react'\nimport { cn } from '~/lib/utils'\nimport { highlightTokens } from '~/util/highlightMatches'\nimport { timeAgo } from '../timeAgo'\nimport { LifecycleBadge, UnreviewedBadge } from './LifecycleBadge'\nimport { LikeButton } from './LikeButton'\nimport { PinButton } from './PinButton'\n\nexport function screenshotUrl(id: string, size: number): string {\n return `/api/screenshots/${id}?size=${size}`\n}\n\n/** The tool's screenshot, cropped to its top-left, or a globe when there is none. */\nexport function ToolThumb({\n tool,\n size,\n className,\n}: {\n tool: CommunityTool\n size: number\n className?: string\n}) {\n return (\n <div\n className={cn(\n 'flex shrink-0 items-center justify-center overflow-hidden border border-border bg-muted text-muted-foreground',\n className,\n )}\n >\n {tool.screenshotId ? (\n <img\n src={screenshotUrl(tool.screenshotId, size)}\n alt=\"\"\n loading=\"lazy\"\n className=\"size-full object-cover object-left-top\"\n />\n ) : (\n <Globe aria-hidden=\"true\" className=\"size-1/4 min-w-4\" />\n )}\n </div>\n )\n}\n\nconst AVATAR_TONES = [\n 'bg-orange-200',\n 'bg-sky-200',\n 'bg-lime-200',\n 'bg-amber-200',\n 'bg-rose-200',\n 'bg-violet-200',\n]\n\nexport function PersonAvatar({ name }: { name: string }) {\n const initials = name\n .split(/\\s+/)\n .slice(0, 2)\n .map((w) => w.charAt(0).toUpperCase())\n .join('')\n let hash = 0\n for (const ch of name) hash = (hash * 31 + ch.charCodeAt(0)) | 0\n return (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'inline-flex size-[22px] shrink-0 items-center justify-center rounded-full text-[9.5px] font-extrabold text-stone-700',\n AVATAR_TONES[Math.abs(hash) % AVATAR_TONES.length],\n )}\n >\n {initials}\n </span>\n )\n}\n\nconst FIELD_LABELS: Record<MatchField, string> = {\n name: 'name',\n summary: 'summary',\n description: 'how to use',\n owner: 'author',\n address: 'address',\n tag: 'tag',\n}\n\nexport interface FeedRowProps {\n tool: CommunityTool\n /** Shown in the ranked feed; search results have no rank. */\n rank?: number\n /** Recently updated shows when the site changed; every other sort, when it was added. */\n sort: FeedSort\n tokens: string[]\n /** Set while searching: which fields the query hit. */\n matched?: MatchField[]\n selected?: boolean\n /**\n * Set while searching: the row is a row of the results grid, and this is the\n * id of its main cell, the one the search box points at.\n */\n optionId?: string\n pinned: boolean\n /** Its pin is being saved. */\n pinBusy?: boolean\n onTogglePin: () => void\n onToggleLike: () => void\n}\n\n/** The feed row's thumbnail: big enough to read the page title in its top left. */\nexport const ROW_THUMB = 'h-[150px] w-[240px] rounded-[10px]'\n\n/** The Details panel's width, reserved at rest so nothing moves when it shows. */\nconst PANEL = 'w-[120px]'\n\n/**\n * One tool in the feed or the search results. Everything left of the end\n * panel (rank, picture, name, summary, meta) is one link that opens the site\n * in a new tab; the 120 px panel opens the card. The star sits in the meta\n * line as its own target, above that link.\n */\nexport function FeedRow({\n tool,\n rank,\n sort,\n tokens,\n matched,\n selected = false,\n optionId,\n pinned,\n pinBusy,\n onTogglePin,\n onToggleLike,\n}: FeedRowProps) {\n const archived = tool.lifecycle.state === 'archived'\n const cell = optionId ? 'gridcell' : undefined\n const owners = tool.owners.map((o) => o.name).join(', ')\n const dated =\n sort !== 'updated'\n ? `Added ${timeAgo(tool.createdAt)}`\n : tool.siteUpdatedAt\n ? `Updated ${timeAgo(tool.siteUpdatedAt)}`\n : null\n\n return (\n <div\n data-testid=\"feed-row\"\n data-slug={tool.slug}\n data-selected={selected || undefined}\n role={optionId ? 'row' : undefined}\n className={cn(\n 'group/row relative flex min-h-[176px] items-center overflow-hidden rounded-[14px] border bg-card transition-[border-color,box-shadow]',\n selected\n ? 'border-primary ring-[3px] ring-primary/20'\n : 'border-border hover:border-primary/35 hover:shadow-sm has-[:focus-visible]:border-primary/35',\n archived && 'opacity-55',\n )}\n >\n {/* Two cells while searching: the site, and the card. A grid rather\n than a listbox, because an option's content is presentational and\n would hide the link and the star. */}\n <div\n id={optionId}\n role={cell}\n aria-selected={optionId ? selected : undefined}\n className=\"flex min-w-0 flex-1 items-center gap-4 self-stretch py-3 pl-3\"\n >\n {rank !== undefined && (\n <span className=\"w-[26px] shrink-0 text-center font-serif text-[22px] font-semibold text-muted-foreground/70\">\n {rank}\n </span>\n )}\n <ToolThumb tool={tool} size={512} className={ROW_THUMB} />\n <div className=\"min-w-0 flex-1 self-start pt-5 pr-6 pb-[52px]\">\n <div className=\"flex items-center gap-2.5 text-[15.5px] font-extrabold\">\n <a\n href={tool.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n data-site\n data-testid=\"feed-row-name\"\n aria-label={`${tool.name}, opens ${tool.host} in a new tab`}\n className={cn(\n 'truncate decoration-1 underline-offset-[3px] after:absolute after:inset-y-0 after:left-0 after:right-[120px] after:z-[1] after:rounded-l-[13px] focus-visible:outline-none focus-visible:after:outline-2 focus-visible:after:-outline-offset-[3px] focus-visible:after:outline-ring',\n 'group-hover/row:underline group-has-[[data-details]:hover]/row:no-underline',\n archived && 'text-muted-foreground',\n )}\n >\n {highlightTokens(tool.name, tokens)}\n </a>\n {selected && (\n <span className=\"inline-flex shrink-0 items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-[11.5px] font-bold text-primary\">\n <CornerDownLeft aria-hidden=\"true\" className=\"size-3\" />\n Enter opens site\n </span>\n )}\n <LifecycleBadge lifecycle={tool.lifecycle} />\n {!tool.reviewed && <UnreviewedBadge />}\n </div>\n {tool.summary && (\n <div className=\"mt-1 line-clamp-2 text-foreground/75\">\n {highlightTokens(tool.summary, tokens)}\n </div>\n )}\n {matched && matched.length > 0 && (\n <div\n data-testid=\"match-reason\"\n className=\"mt-1 text-xs font-semibold text-muted-foreground\"\n >\n Matched: {matched.map((f) => FIELD_LABELS[f]).join(', ')}\n </div>\n )}\n </div>\n {/* Meta on the left, the date on the right, flush to the panel. A\n long author list gives way first. Clicks fall through to the row\n link, except on star and like. */}\n <div\n className={cn(\n 'pointer-events-none absolute right-[120px] bottom-0 z-[2] flex h-12 items-center gap-4',\n rank !== undefined ? 'left-[310px]' : 'left-[268px]',\n )}\n >\n <div className=\"flex min-w-0 items-center gap-1.5 text-[13px] font-semibold whitespace-nowrap text-foreground/75 [&>*]:shrink-0\">\n {tool.owners[0] && <PersonAvatar name={tool.owners[0].name} />}\n <span\n data-testid=\"feed-row-owner\"\n className=\"mr-1 min-w-0 !shrink truncate\"\n >\n {highlightTokens(owners, tokens)}\n </span>\n <span className=\"pointer-events-auto -mr-1 flex items-center\">\n <PinButton\n small\n pinned={pinned}\n busy={pinBusy}\n name={tool.name}\n onToggle={onTogglePin}\n />\n <LikeButton tool={tool} onToggle={onToggleLike} />\n </span>\n <span\n data-testid=\"feed-row-views\"\n className=\"inline-flex items-center gap-1 text-muted-foreground\"\n >\n <Eye aria-hidden=\"true\" className=\"size-[15px]\" />\n {tool.views30d}\n <span className=\"sr-only\"> views this month</span>\n </span>\n {tool.comments > 0 && (\n <span\n data-testid=\"feed-row-comments\"\n className=\"inline-flex items-center gap-1 text-muted-foreground\"\n >\n <MessageSquare aria-hidden=\"true\" className=\"size-[15px]\" />\n {tool.comments}\n <span className=\"sr-only\">\n {tool.comments === 1 ? ' comment' : ' comments'}\n </span>\n </span>\n )}\n </div>\n {dated && (\n <span\n data-testid=\"feed-row-date\"\n className=\"ml-auto shrink-0 pr-4 text-[13px] font-semibold whitespace-nowrap text-muted-foreground\"\n >\n {dated}\n </span>\n )}\n </div>\n </div>\n <div role={cell} className=\"flex self-stretch\">\n <Link\n to=\"/community/$slug\"\n params={{ slug: tool.slug }}\n search={(prev) => prev}\n data-details\n aria-label={`Details for ${tool.name}`}\n className={cn(\n PANEL,\n 'relative z-[2] flex flex-none flex-col items-center justify-center gap-1.5 border-l border-border/55 text-[13.5px] font-extrabold text-muted-foreground transition-colors',\n 'group-hover/row:border-border group-hover/row:bg-primary/[0.025] group-has-[:focus-visible]/row:bg-primary/[0.025]',\n 'hover:!border-primary/30 hover:!bg-primary/12 hover:text-foreground',\n 'focus-visible:rounded-r-[13px] focus-visible:!bg-primary/12 focus-visible:text-foreground focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-ring',\n )}\n >\n {/* Shown on hover or focus; always where nothing hovers. */}\n <span className=\"flex flex-col items-center gap-1.5 opacity-0 transition-opacity group-hover/row:opacity-100 group-has-[:focus-visible]/row:opacity-100 [@media(hover:none)]:opacity-100\">\n <PanelRight aria-hidden=\"true\" className=\"size-[18px]\" />\n <span>Details</span>\n </span>\n </Link>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FeedRow.js","sources":["../../../../../src/modules/communityTools/ui/FeedRow.tsx"],"sourcesContent":["import type {\n CommunityTool,\n FeedSort,\n MatchField,\n} from '@igstack/app-catalog-shared-core'\nimport { Link } from '@tanstack/react-router'\nimport {\n CornerDownLeft,\n Eye,\n Globe,\n MessageSquare,\n PanelRight,\n} from 'lucide-react'\nimport { cn } from '~/lib/utils'\nimport { highlightTokens } from '~/util/highlightMatches'\nimport { recordSiteOpen } from '../siteOpen'\nimport { exactDate, timeAgo } from '../timeAgo'\nimport { LifecycleBadge, UnreviewedBadge } from './LifecycleBadge'\nimport { LikeButton } from './LikeButton'\nimport { PinButton } from './PinButton'\n\nexport function screenshotUrl(id: string, size: number): string {\n return `/api/screenshots/${id}?size=${size}`\n}\n\n/** The tool's screenshot, cropped to its top-left, or a globe when there is none. */\nexport function ToolThumb({\n tool,\n size,\n className,\n}: {\n tool: CommunityTool\n size: number\n className?: string\n}) {\n return (\n <div\n className={cn(\n 'flex shrink-0 items-center justify-center overflow-hidden border border-border bg-muted text-muted-foreground',\n className,\n )}\n >\n {tool.screenshotId ? (\n <img\n src={screenshotUrl(tool.screenshotId, size)}\n alt=\"\"\n loading=\"lazy\"\n className=\"size-full object-cover object-left-top\"\n />\n ) : (\n <Globe aria-hidden=\"true\" className=\"size-1/4 min-w-4\" />\n )}\n </div>\n )\n}\n\nconst AVATAR_TONES = [\n 'bg-orange-200',\n 'bg-sky-200',\n 'bg-lime-200',\n 'bg-amber-200',\n 'bg-rose-200',\n 'bg-violet-200',\n]\n\nexport function PersonAvatar({ name }: { name: string }) {\n const initials = name\n .split(/\\s+/)\n .slice(0, 2)\n .map((w) => w.charAt(0).toUpperCase())\n .join('')\n let hash = 0\n for (const ch of name) hash = (hash * 31 + ch.charCodeAt(0)) | 0\n return (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'inline-flex size-[22px] shrink-0 items-center justify-center rounded-full text-[9.5px] font-extrabold text-stone-700',\n AVATAR_TONES[Math.abs(hash) % AVATAR_TONES.length],\n )}\n >\n {initials}\n </span>\n )\n}\n\nconst FIELD_LABELS: Record<MatchField, string> = {\n name: 'name',\n summary: 'summary',\n description: 'how to use',\n owner: 'author',\n address: 'address',\n tag: 'tag',\n}\n\nexport interface FeedRowProps {\n tool: CommunityTool\n /** Shown in the ranked feed; search results have no rank. */\n rank?: number\n /**\n * Newest shows when it was added, Recently updated when the site changed;\n * every other sort the site's date, or when it was added if that is unknown.\n */\n sort: FeedSort\n tokens: string[]\n /** Set while searching: which fields the query hit. */\n matched?: MatchField[]\n selected?: boolean\n /**\n * Set while searching: the row is a row of the results grid, and this is the\n * id of its main cell, the one the search box points at.\n */\n optionId?: string\n pinned: boolean\n /** Its pin is being saved. */\n pinBusy?: boolean\n onTogglePin: () => void\n onToggleLike: () => void\n}\n\n/** The feed row's thumbnail: big enough to read the page title in its top left. */\nexport const ROW_THUMB = 'h-[150px] w-[240px] rounded-[10px]'\n\n/** The Details panel's width, reserved at rest so nothing moves when it shows. */\nconst PANEL = 'w-[120px]'\n\n/**\n * One tool in the feed or the search results. Everything left of the end\n * panel (rank, picture, name, summary, meta) is one link that opens the\n * site; the 120 px panel opens the card. The star sits in the meta\n * line as its own target, above that link.\n */\nexport function FeedRow({\n tool,\n rank,\n sort,\n tokens,\n matched,\n selected = false,\n optionId,\n pinned,\n pinBusy,\n onTogglePin,\n onToggleLike,\n}: FeedRowProps) {\n const archived = tool.lifecycle.state === 'archived'\n const cell = optionId ? 'gridcell' : undefined\n const owners = tool.owners.map((o) => o.name).join(', ')\n const siteUpdatedAt = sort !== 'newest' ? tool.siteUpdatedAt : null\n const dated = siteUpdatedAt\n ? { label: 'Updated', at: siteUpdatedAt }\n : sort !== 'updated'\n ? { label: 'Added', at: tool.createdAt }\n : null\n\n return (\n <div\n data-testid=\"feed-row\"\n data-slug={tool.slug}\n data-selected={selected || undefined}\n role={optionId ? 'row' : undefined}\n className={cn(\n 'group/row relative flex min-h-[176px] items-center overflow-hidden rounded-[14px] border bg-card transition-[border-color,box-shadow]',\n selected\n ? 'border-primary ring-[3px] ring-primary/20'\n : 'border-border hover:border-primary/35 hover:shadow-sm has-[:focus-visible]:border-primary/35',\n archived && 'opacity-55',\n )}\n >\n {/* Two cells while searching: the site, and the card. A grid rather\n than a listbox, because an option's content is presentational and\n would hide the link and the star. */}\n <div\n id={optionId}\n role={cell}\n aria-selected={optionId ? selected : undefined}\n className=\"flex min-w-0 flex-1 items-center gap-4 self-stretch py-3 pl-3\"\n >\n {rank !== undefined && (\n <span className=\"w-[26px] shrink-0 text-center font-serif text-[22px] font-semibold text-muted-foreground/70\">\n {rank}\n </span>\n )}\n <ToolThumb tool={tool} size={512} className={ROW_THUMB} />\n <div className=\"min-w-0 flex-1 self-start pt-5 pr-6 pb-[52px]\">\n <div className=\"flex items-center gap-2.5 text-[15.5px] font-extrabold\">\n <a\n href={tool.url}\n onClick={() => recordSiteOpen(tool.slug)}\n data-site\n data-testid=\"feed-row-name\"\n aria-label={`${tool.name}, opens ${tool.host}`}\n className={cn(\n 'truncate decoration-1 underline-offset-[3px] after:absolute after:inset-y-0 after:left-0 after:right-[120px] after:z-[1] after:rounded-l-[13px] focus-visible:outline-none focus-visible:after:outline-2 focus-visible:after:-outline-offset-[3px] focus-visible:after:outline-ring',\n 'group-hover/row:underline group-has-[[data-details]:hover]/row:no-underline',\n archived && 'text-muted-foreground',\n )}\n >\n {highlightTokens(tool.name, tokens)}\n </a>\n {selected && (\n <span className=\"inline-flex shrink-0 items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-[11.5px] font-bold text-primary\">\n <CornerDownLeft aria-hidden=\"true\" className=\"size-3\" />\n Enter opens site\n </span>\n )}\n <LifecycleBadge lifecycle={tool.lifecycle} />\n {!tool.reviewed && <UnreviewedBadge />}\n </div>\n {tool.summary && (\n <div className=\"mt-1 line-clamp-2 text-foreground/75\">\n {highlightTokens(tool.summary, tokens)}\n </div>\n )}\n {matched && matched.length > 0 && (\n <div\n data-testid=\"match-reason\"\n className=\"mt-1 text-xs font-semibold text-muted-foreground\"\n >\n Matched: {matched.map((f) => FIELD_LABELS[f]).join(', ')}\n </div>\n )}\n </div>\n {/* Meta on the left, the date on the right, flush to the panel. A\n long author list gives way first. Clicks fall through to the row\n link, except on star and like. */}\n <div\n className={cn(\n 'pointer-events-none absolute right-[120px] bottom-0 z-[2] flex h-12 items-center gap-4',\n rank !== undefined ? 'left-[310px]' : 'left-[268px]',\n )}\n >\n <div className=\"flex min-w-0 items-center gap-1.5 text-[13px] font-semibold whitespace-nowrap text-foreground/75 [&>*]:shrink-0\">\n {tool.owners[0] && <PersonAvatar name={tool.owners[0].name} />}\n <span\n data-testid=\"feed-row-owner\"\n className=\"mr-1 min-w-0 !shrink truncate\"\n >\n {highlightTokens(owners, tokens)}\n </span>\n <span className=\"pointer-events-auto -mr-1 flex items-center\">\n <PinButton\n small\n pinned={pinned}\n busy={pinBusy}\n name={tool.name}\n onToggle={onTogglePin}\n />\n <LikeButton tool={tool} onToggle={onToggleLike} />\n </span>\n <span\n data-testid=\"feed-row-views\"\n className=\"inline-flex items-center gap-1 text-muted-foreground\"\n >\n <Eye aria-hidden=\"true\" className=\"size-[15px]\" />\n {tool.views30d}\n <span className=\"sr-only\"> views this month</span>\n </span>\n {tool.comments > 0 && (\n <span\n data-testid=\"feed-row-comments\"\n className=\"inline-flex items-center gap-1 text-muted-foreground\"\n >\n <MessageSquare aria-hidden=\"true\" className=\"size-[15px]\" />\n {tool.comments}\n <span className=\"sr-only\">\n {tool.comments === 1 ? ' comment' : ' comments'}\n </span>\n </span>\n )}\n </div>\n {dated && (\n <span\n data-testid=\"feed-row-date\"\n title={exactDate(dated.at)}\n className=\"pointer-events-auto ml-auto shrink-0 pr-4 text-[13px] font-semibold whitespace-nowrap text-muted-foreground\"\n >\n {dated.label} {timeAgo(dated.at)}\n </span>\n )}\n </div>\n </div>\n <div role={cell} className=\"flex self-stretch\">\n <Link\n to=\"/community/$slug\"\n params={{ slug: tool.slug }}\n search={(prev) => prev}\n data-details\n aria-label={`Details for ${tool.name}`}\n className={cn(\n PANEL,\n 'relative z-[2] flex flex-none flex-col items-center justify-center gap-1.5 border-l border-border/55 text-[13.5px] font-extrabold text-muted-foreground transition-colors',\n 'group-hover/row:border-border group-hover/row:bg-primary/[0.025] group-has-[:focus-visible]/row:bg-primary/[0.025]',\n 'hover:!border-primary/30 hover:!bg-primary/12 hover:text-foreground',\n 'focus-visible:rounded-r-[13px] focus-visible:!bg-primary/12 focus-visible:text-foreground focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-ring',\n )}\n >\n {/* Shown on hover or focus; always where nothing hovers. */}\n <span className=\"flex flex-col items-center gap-1.5 opacity-0 transition-opacity group-hover/row:opacity-100 group-has-[:focus-visible]/row:opacity-100 [@media(hover:none)]:opacity-100\">\n <PanelRight aria-hidden=\"true\" className=\"size-[18px]\" />\n <span>Details</span>\n </span>\n </Link>\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { GripVertical } from "lucide-react";
|
|
3
3
|
import { cn } from "../../../lib/utils.js";
|
|
4
4
|
import { ToolThumb, PersonAvatar } from "./FeedRow.js";
|
|
5
|
+
import { recordSiteOpen } from "../siteOpen.js";
|
|
5
6
|
import { PinButton } from "./PinButton.js";
|
|
6
7
|
function ShelfTile({
|
|
7
8
|
tool,
|
|
@@ -26,8 +27,7 @@ function ShelfTile({
|
|
|
26
27
|
"a",
|
|
27
28
|
{
|
|
28
29
|
href: tool.url,
|
|
29
|
-
|
|
30
|
-
rel: "noopener noreferrer",
|
|
30
|
+
onClick: () => recordSiteOpen(tool.slug),
|
|
31
31
|
"aria-keyshortcuts": "Alt+ArrowUp Alt+ArrowDown",
|
|
32
32
|
onKeyDown: (e) => {
|
|
33
33
|
if (!e.altKey) return;
|
|
@@ -40,10 +40,7 @@ function ShelfTile({
|
|
|
40
40
|
children: [
|
|
41
41
|
/* @__PURE__ */ jsxs("span", { className: "relative block aspect-[16/9.5] border-b border-border", children: [
|
|
42
42
|
/* @__PURE__ */ jsx(ToolThumb, { tool, size: 512, className: "size-full border-0" }),
|
|
43
|
-
/* @__PURE__ */
|
|
44
|
-
/* @__PURE__ */ jsx(ExternalLink, { "aria-hidden": "true", className: "size-4" }),
|
|
45
|
-
"Open site"
|
|
46
|
-
] })
|
|
43
|
+
/* @__PURE__ */ jsx("span", { className: "absolute right-2.5 bottom-2.5 rounded-lg bg-primary px-3 py-1.5 text-sm font-extrabold text-primary-foreground opacity-0 shadow-sm transition-opacity group-hover:opacity-100 group-focus-within:opacity-100", children: "Open site" })
|
|
47
44
|
] }),
|
|
48
45
|
/* @__PURE__ */ jsxs("span", { className: "block py-2.5 pr-12 pl-3", children: [
|
|
49
46
|
/* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ShelfTile.js","sources":["../../../../../src/modules/communityTools/ui/ShelfTile.tsx"],"sourcesContent":["import type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport {
|
|
1
|
+
{"version":3,"file":"ShelfTile.js","sources":["../../../../../src/modules/communityTools/ui/ShelfTile.tsx"],"sourcesContent":["import type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport { GripVertical } from 'lucide-react'\nimport { cn } from '~/lib/utils'\nimport { PersonAvatar, ToolThumb } from './FeedRow'\nimport { recordSiteOpen } from '../siteOpen'\nimport { PinButton } from './PinButton'\n\n/**\n * A pinned tool: screenshot first, then name and author. A click opens the\n * site. Drag the tile, or press Alt+↑/↓ on it, to move it.\n */\nexport function ShelfTile({\n tool,\n dragging,\n onUnpin,\n onMove,\n dragProps,\n}: {\n tool: CommunityTool\n dragging: boolean\n onUnpin: () => void\n /** -1 moves it one place earlier, +1 one place later. */\n onMove: (step: -1 | 1) => void\n dragProps: React.HTMLAttributes<HTMLDivElement>\n}) {\n return (\n <div\n data-testid=\"shelf-tile\"\n draggable\n {...dragProps}\n className={cn(\n 'group relative overflow-hidden rounded-[14px] border border-border bg-card transition-[border-color,box-shadow]',\n 'hover:border-primary/40 hover:shadow-sm focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/30',\n dragging && 'opacity-40',\n )}\n >\n <a\n href={tool.url}\n onClick={() => recordSiteOpen(tool.slug)}\n aria-keyshortcuts=\"Alt+ArrowUp Alt+ArrowDown\"\n onKeyDown={(e) => {\n if (!e.altKey) return\n if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {\n e.preventDefault()\n onMove(e.key === 'ArrowUp' ? -1 : 1)\n }\n }}\n className=\"block focus-visible:outline-none\"\n >\n <span className=\"relative block aspect-[16/9.5] border-b border-border\">\n <ToolThumb tool={tool} size={512} className=\"size-full border-0\" />\n <span className=\"absolute right-2.5 bottom-2.5 rounded-lg bg-primary px-3 py-1.5 text-sm font-extrabold text-primary-foreground opacity-0 shadow-sm transition-opacity group-hover:opacity-100 group-focus-within:opacity-100\">\n Open site\n </span>\n </span>\n <span className=\"block py-2.5 pr-12 pl-3\">\n <span\n data-testid=\"shelf-tile-name\"\n className=\"block truncate text-sm font-bold\"\n >\n {tool.name}\n </span>\n <span className=\"mt-0.5 flex min-w-0 items-center gap-1.5 text-[12.5px] font-medium text-muted-foreground\">\n {tool.owners[0] && <PersonAvatar name={tool.owners[0].name} />}\n <span className=\"truncate\">\n {tool.owners.map((o) => o.name).join(', ')}\n </span>\n </span>\n </span>\n </a>\n <span\n aria-hidden=\"true\"\n className=\"absolute top-2 right-2 inline-flex size-7 cursor-grab items-center justify-center rounded-md bg-card/90 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100\"\n >\n <GripVertical className=\"size-4\" />\n </span>\n <div className=\"absolute right-2 bottom-2\">\n <PinButton pinned name={tool.name} onToggle={onUnpin} />\n </div>\n </div>\n )\n}\n"],"names":[],"mappings":";;;;;;AAWO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAY;AAAA,MACZ,WAAS;AAAA,MACR,GAAG;AAAA,MACJ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA,YAAY;AAAA,MAAA;AAAA,MAGd,UAAA;AAAA,QAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAM,KAAK;AAAA,YACX,SAAS,MAAM,eAAe,KAAK,IAAI;AAAA,YACvC,qBAAkB;AAAA,YAClB,WAAW,CAAC,MAAM;AAChB,kBAAI,CAAC,EAAE,OAAQ;AACf,kBAAI,EAAE,QAAQ,aAAa,EAAE,QAAQ,aAAa;AAChD,kBAAE,eAAA;AACF,uBAAO,EAAE,QAAQ,YAAY,KAAK,CAAC;AAAA,cACrC;AAAA,YACF;AAAA,YACA,WAAU;AAAA,YAEV,UAAA;AAAA,cAAA,qBAAC,QAAA,EAAK,WAAU,yDACd,UAAA;AAAA,gBAAA,oBAAC,WAAA,EAAU,MAAY,MAAM,KAAK,WAAU,sBAAqB;AAAA,gBACjE,oBAAC,QAAA,EAAK,WAAU,gNAA+M,UAAA,YAAA,CAE/N;AAAA,cAAA,GACF;AAAA,cACA,qBAAC,QAAA,EAAK,WAAU,2BACd,UAAA;AAAA,gBAAA;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,eAAY;AAAA,oBACZ,WAAU;AAAA,oBAET,UAAA,KAAK;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAER,qBAAC,QAAA,EAAK,WAAU,4FACb,UAAA;AAAA,kBAAA,KAAK,OAAO,CAAC,KAAK,oBAAC,cAAA,EAAa,MAAM,KAAK,OAAO,CAAC,EAAE,KAAA,CAAM;AAAA,kBAC5D,oBAAC,QAAA,EAAK,WAAU,YACb,eAAK,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,IAAI,EAAA,CAC3C;AAAA,gBAAA,EAAA,CACF;AAAA,cAAA,EAAA,CACF;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAEF;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAU;AAAA,YAEV,UAAA,oBAAC,cAAA,EAAa,WAAU,SAAA,CAAS;AAAA,UAAA;AAAA,QAAA;AAAA,QAEnC,oBAAC,OAAA,EAAI,WAAU,6BACb,UAAA,oBAAC,WAAA,EAAU,QAAM,MAAC,MAAM,KAAK,MAAM,UAAU,SAAS,EAAA,CACxD;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|
|
@@ -11,11 +11,12 @@ import { CommunityInput } from "../../appCatalog/ui/detail/CommunityInput.js";
|
|
|
11
11
|
import { cn } from "../../../lib/utils.js";
|
|
12
12
|
import { useAuth } from "../../auth/AuthContext.js";
|
|
13
13
|
import "../../../ui/button.js";
|
|
14
|
-
import "../../auth/AuthModalContext.js";
|
|
15
14
|
import { Skeleton } from "../../../ui/skeleton.js";
|
|
16
15
|
import { formatCardDate, CardScreenshot } from "./CardScreenshot.js";
|
|
17
16
|
import { CardTitleBlock } from "./CardTitleBlock.js";
|
|
18
17
|
import { PersonAvatar } from "./FeedRow.js";
|
|
18
|
+
import { openSite } from "../siteOpen.js";
|
|
19
|
+
import { timeAgo, exactDate } from "../timeAgo.js";
|
|
19
20
|
import { useTimeUp } from "../useTimeUp.js";
|
|
20
21
|
import { PinButton } from "./PinButton.js";
|
|
21
22
|
import { TagChip, tagLabel, tagKey } from "./TagRow.js";
|
|
@@ -118,7 +119,7 @@ function ToolCard({
|
|
|
118
119
|
} else if (e.key === "Enter" && tool) {
|
|
119
120
|
if (target.closest("a, button")) return;
|
|
120
121
|
e.preventDefault();
|
|
121
|
-
|
|
122
|
+
openSite(tool);
|
|
122
123
|
}
|
|
123
124
|
};
|
|
124
125
|
const controls = index >= 0 && siblings.length > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -233,7 +234,18 @@ function ToolCard({
|
|
|
233
234
|
/* @__PURE__ */ jsx("b", { className: "text-foreground", children: views.viewsAll.toLocaleString("en-US") }),
|
|
234
235
|
" ",
|
|
235
236
|
"all time"
|
|
236
|
-
] })
|
|
237
|
+
] }),
|
|
238
|
+
tool.siteUpdatedAt && /* @__PURE__ */ jsxs(
|
|
239
|
+
"span",
|
|
240
|
+
{
|
|
241
|
+
"data-testid": "card-site-updated",
|
|
242
|
+
title: exactDate(tool.siteUpdatedAt),
|
|
243
|
+
children: [
|
|
244
|
+
"Site updated ",
|
|
245
|
+
timeAgo(tool.siteUpdatedAt)
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
)
|
|
237
249
|
]
|
|
238
250
|
}
|
|
239
251
|
),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToolCard.js","sources":["../../../../../src/modules/communityTools/ui/ToolCard.tsx"],"sourcesContent":["import type { Claim } from '@igstack/app-catalog-backend-core'\nimport type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport {\n Archive,\n ArrowLeft,\n ArrowRight,\n Check,\n Eye,\n Pencil,\n} from 'lucide-react'\nimport { useEffect, useId, useRef, useState } from 'react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport {\n DIALOG_ROUND_BUTTON,\n DetailDialogShell,\n} from '~/modules/appCatalog/ui/catalog/DetailDialogShell'\nimport { MarkdownText } from '~/modules/appCatalog/ui/components/MarkdownText'\nimport { CommentsFeed } from '~/modules/appCatalog/ui/detail/CommentsFeed'\nimport { CommentsHelp } from '~/modules/appCatalog/ui/detail/CommentsHelp'\nimport { CommunityInput } from '~/modules/appCatalog/ui/detail/CommunityInput'\nimport type { CommunityInputHandle } from '~/modules/appCatalog/ui/detail/CommunityInput'\nimport { cn } from '~/lib/utils'\nimport { useAuth } from '~/modules/auth'\nimport { Skeleton } from '~/ui/skeleton'\nimport { CardScreenshot, formatCardDate } from './CardScreenshot'\nimport { CardTitleBlock } from './CardTitleBlock'\nimport { PersonAvatar } from './FeedRow'\nimport { useTimeUp } from '../useTimeUp'\nimport { PinButton } from './PinButton'\nimport { TagChip, tagKey, tagLabel } from './TagRow'\nimport { ArchiveConfirm, GHOST_BUTTON, ToolFormDialog } from './ToolFormDialog'\n\nconst KBD =\n 'rounded-[5px] border border-b-2 border-border bg-card px-1.5 py-px text-[11px] font-semibold text-muted-foreground'\n\nconst SECTION = 'mt-6 border-t border-border pt-[22px]'\n\nconst LABEL =\n 'text-[11.5px] font-bold tracking-wider text-muted-foreground uppercase'\n\nconst NO_CLAIMS: Claim[] = []\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\n/** How often a card whose first screenshot is on its way asks again. */\nconst CAPTURE_POLL_MS = 5000\n\n/** How long one open card waits for a first screenshot before it stops asking. */\nconst CAPTURE_WAIT_MS = 2 * 60 * 1000\n\nexport interface ToolCardProps {\n slug: string\n /** The list the card was opened from, in its order: ←/→ step through it. */\n siblings: string[]\n onClose: () => void\n onNavigate: (slug: string) => void\n pinned: boolean\n /** Its pin is being saved. */\n pinBusy?: boolean\n onTogglePin: (tool: CommunityTool) => void\n /** Display names of official apps by slug, for app tags. */\n appNames: Record<string, string>\n /** A tag chip on the card: close it and filter the feed to that tag. */\n onChooseTag: (tag: string) => void\n}\n\n/** Typing belongs to the field: the card's keys stay out of it. */\nfunction isTyping(target: HTMLElement): boolean {\n return (\n target.isContentEditable ||\n target.closest('input, textarea, select') !== null\n )\n}\n\n/**\n * A community tool over the Community page, in the official card's shell: no\n * tabs, one scroll. Title and screenshot to recognise it, how to use it, who\n * to ask, and the comments.\n */\nexport function ToolCard({\n slug,\n siblings,\n onClose,\n onNavigate,\n pinned,\n pinBusy,\n onTogglePin,\n appNames,\n onChooseTag,\n}: ToolCardProps) {\n const { user } = useAuth()\n const isAdmin = user?.isAdmin === true\n const trpc = useTRPC()\n const queryClient = useQueryClient()\n // The feed already holds every tool, so the card paints at once from it\n // and the tool's own query only refreshes it.\n const list = useQuery(trpc.communityTools.list.queryOptions())\n const fromList = list.data?.find((t) => t.slug === slug)\n // Whatever the server says, an open card stops waiting after a while: a\n // capture that never reports back must not keep it asking forever.\n const gaveUpOnCapture = useTimeUp(slug, CAPTURE_WAIT_MS)\n const get = useQuery({\n ...trpc.communityTools.get.queryOptions({ slug }),\n placeholderData: fromList,\n // Until the first visit lands, keep asking, so the screenshot replaces\n // the placeholder without a reload.\n refetchInterval: (query) =>\n query.state.data?.capturePending && !gaveUpOnCapture\n ? CAPTURE_POLL_MS\n : false,\n })\n const tool = get.data\n\n // One view per open. The ref keeps StrictMode's second effect from counting\n // twice; stepping to another tool is another open.\n const [counts, setCounts] = useState<{\n slug: string\n views30d: number\n viewsAll: number\n } | null>(null)\n // The hook's own onSuccess, not one passed to `mutate`: StrictMode's\n // remount detaches the observer a `mutate` callback hangs on, so that one\n // never fires and the count stayed one short.\n const { mutate: recordView } = useMutation(\n trpc.communityTools.recordView.mutationOptions({\n onSuccess: (c, { slug: viewed }) => {\n setCounts({ slug: viewed, ...c })\n },\n }),\n )\n const recordedForRef = useRef<string | null>(null)\n useEffect(() => {\n if (recordedForRef.current === slug) return\n recordedForRef.current = slug\n recordView({ slug })\n }, [slug, recordView])\n const views = counts?.slug === slug ? counts : tool\n\n const [editing, setEditing] = useState(false)\n const [archiving, setArchiving] = useState(false)\n const refreshTools = () =>\n queryClient.invalidateQueries({\n queryKey: trpc.communityTools.pathKey(),\n })\n const approve = useMutation(\n trpc.communityTools.approve.mutationOptions({ onSuccess: refreshTools }),\n )\n const archive = useMutation(\n trpc.communityTools.archive.mutationOptions({\n onSuccess: () => {\n setArchiving(false)\n return refreshTools()\n },\n }),\n )\n const adminError = approve.error ?? archive.error\n\n const commentsRef = useRef<CommunityInputHandle>(null)\n const commentsHeadingId = useId()\n const commentTarget = { communityToolSlug: slug }\n const comments = useQuery(trpc.feedback.list.queryOptions(commentTarget))\n const commentCount = comments.data?.items.length ?? 0\n\n const index = siblings.indexOf(slug)\n const prev = index > 0 ? siblings[index - 1] : undefined\n const next = index >= 0 ? siblings[index + 1] : undefined\n const nameOf = (s: string) => list.data?.find((t) => t.slug === s)?.name ?? s\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n // React bubbles through portals: keys in the screenshot viewer are not ours.\n const target = e.target as HTMLElement\n if (!e.currentTarget.contains(target)) return\n if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return\n if (isTyping(target)) return\n if (e.key === 'ArrowLeft' && prev) {\n e.preventDefault()\n onNavigate(prev)\n } else if (e.key === 'ArrowRight' && next) {\n e.preventDefault()\n onNavigate(next)\n } else if (e.key === 'Enter' && tool) {\n // A link or button answers Enter itself; the title block is the link\n // that opens the site.\n if (target.closest('a, button')) return\n e.preventDefault()\n window.open(tool.url, '_blank', 'noopener,noreferrer')\n }\n }\n\n const controls = index >= 0 && siblings.length > 1 && (\n <>\n <span className=\"flex items-center gap-1 text-[13px] font-semibold text-muted-foreground\">\n <button\n type=\"button\"\n aria-label={prev ? `Previous tool: ${nameOf(prev)}` : 'Previous tool'}\n disabled={!prev}\n onClick={() => prev && onNavigate(prev)}\n className={cn(DIALOG_ROUND_BUTTON, 'disabled:opacity-40')}\n >\n <ArrowLeft className=\"size-4\" />\n </button>\n <span data-testid=\"card-position\">\n {index + 1} of {siblings.length}\n </span>\n <button\n type=\"button\"\n aria-label={next ? `Next tool: ${nameOf(next)}` : 'Next tool'}\n disabled={!next}\n onClick={() => next && onNavigate(next)}\n className={cn(DIALOG_ROUND_BUTTON, 'disabled:opacity-40')}\n >\n <ArrowRight className=\"size-4\" />\n </button>\n </span>\n <i aria-hidden=\"true\" className=\"mx-1 h-5 w-px bg-border\" />\n </>\n )\n\n return (\n <DetailDialogShell\n label={`${tool?.name ?? 'Community tool'} details`}\n shownKey={slug}\n maxWidthClass=\"max-w-[min(920px,94vw)]\"\n focusSelector='[data-testid=\"card-title-block\"]'\n controls={controls}\n testId=\"tool-card\"\n onClose={onClose}\n onKeyDown={onKeyDown}\n >\n {tool ? (\n <>\n <div className=\"border-b border-border pb-[22px]\">\n <div className=\"flex items-end justify-between gap-4 pr-44\">\n <CardTitleBlock tool={tool} />\n <div className=\"-mr-40 flex shrink-0 items-center gap-2\">\n {tool.canEdit && (\n <button\n type=\"button\"\n data-testid=\"card-edit-button\"\n onClick={() => setEditing(true)}\n className={cn(GHOST_BUTTON, 'h-10 px-3.5 text-[13.5px]')}\n >\n <Pencil aria-hidden=\"true\" className=\"size-4\" />\n Edit\n </button>\n )}\n <PinButton\n labelled\n pinned={pinned}\n busy={pinBusy}\n name={tool.name}\n onToggle={() => onTogglePin(tool)}\n />\n </div>\n </div>\n <div\n className={cn(\n tool.lifecycle.state === 'archived' && 'opacity-60',\n )}\n >\n {tool.summary && (\n <p className=\"mt-3.5 text-[15px] text-foreground/80\">\n {tool.summary}\n </p>\n )}\n {tool.tags.length > 0 && (\n <div\n data-testid=\"card-tags\"\n className=\"mt-2.5 flex flex-wrap gap-1.5\"\n >\n {tool.tags.map((tag) => (\n <TagChip\n key={tagKey(tag)}\n label={tagLabel(tag, appNames)}\n onClick={() => onChooseTag(tagKey(tag))}\n />\n ))}\n </div>\n )}\n {views && (\n <p\n data-testid=\"card-views\"\n className=\"mt-2.5 flex gap-[18px] text-[13px] font-semibold text-muted-foreground\"\n >\n <span className=\"inline-flex items-center gap-1.5\">\n <Eye aria-hidden=\"true\" className=\"size-[15px]\" />\n <b className=\"text-foreground\">\n {views.views30d.toLocaleString('en-US')}\n </b>{' '}\n opens this month\n </span>\n <span>\n <b className=\"text-foreground\">\n {views.viewsAll.toLocaleString('en-US')}\n </b>{' '}\n all time\n </span>\n </p>\n )}\n {/* Who to ask, up here with the facts: below the screenshot\n it fell under the fold. */}\n <p\n data-testid=\"card-people\"\n className=\"mt-2.5 flex flex-wrap items-center gap-x-[18px] gap-y-1.5 text-[13px] font-semibold text-muted-foreground\"\n >\n {tool.owners.length > 0 && (\n <span className=\"inline-flex flex-wrap items-center gap-x-3 gap-y-1.5\">\n <span className={LABEL}>Owners</span>\n {tool.owners.map((owner) => (\n <span\n key={owner.slug ?? `named:${owner.name}`}\n className=\"inline-flex items-center gap-1.5 text-foreground\"\n >\n <PersonAvatar name={owner.name} />\n <span data-testid=\"card-owner\">{owner.name}</span>\n </span>\n ))}\n </span>\n )}\n <span className=\"inline-flex items-center gap-1.5\">\n <span className={LABEL}>Added by</span>\n <span className=\"text-foreground\">{tool.addedBy.name}</span>\n <span className=\"font-normal\">\n {formatCardDate(tool.createdAt)}\n </span>\n </span>\n </p>\n </div>\n <LifecycleNote\n tool={tool}\n onTellOwner={() => commentsRef.current?.focus()}\n />\n {isAdmin && (\n <div\n data-testid=\"card-admin\"\n className=\"mt-3.5 flex flex-col gap-2.5 rounded-[10px] border border-dashed border-border px-3 py-2.5\"\n >\n {archiving ? (\n <ArchiveConfirm\n busy={archive.isPending}\n onCancel={() => setArchiving(false)}\n onArchive={(reason) => archive.mutate({ slug, reason })}\n />\n ) : (\n <div className=\"flex flex-wrap items-center gap-2.5\">\n <span className=\"mr-auto text-[12.5px] font-bold tracking-wider text-muted-foreground uppercase\">\n Admin\n </span>\n {!tool.reviewed && (\n <button\n type=\"button\"\n disabled={approve.isPending}\n aria-busy={approve.isPending || undefined}\n onClick={() => approve.mutate({ slug })}\n className={GHOST_BUTTON}\n >\n <Check aria-hidden=\"true\" className=\"size-4\" />\n {approve.isPending ? 'Approving…' : 'Approve'}\n </button>\n )}\n {tool.lifecycle.state !== 'archived' && (\n <button\n type=\"button\"\n onClick={() => setArchiving(true)}\n className={GHOST_BUTTON}\n >\n <Archive aria-hidden=\"true\" className=\"size-4\" />\n Archive\n </button>\n )}\n </div>\n )}\n {adminError && (\n <p\n role=\"alert\"\n className=\"text-[13px] font-semibold text-destructive\"\n >\n {adminError.message}\n </p>\n )}\n </div>\n )}\n </div>\n\n <CardScreenshot\n tool={tool}\n capturing={tool.capturePending && !gaveUpOnCapture}\n />\n\n {tool.descriptionMd.trim() && (\n <section\n data-testid=\"card-how-to-use\"\n className={cn(SECTION, 'text-[14.5px]')}\n >\n <h3 className=\"mb-2 font-serif text-[17px] font-semibold\">\n How to use\n </h3>\n <div className=\"prose prose-sm dark:prose-invert max-w-[68ch]\">\n <MarkdownText>{tool.descriptionMd}</MarkdownText>\n </div>\n </section>\n )}\n\n {/* The card's own order: what was said first, the input last. The\n official card's CommunitySection puts the input on top. */}\n <section data-testid=\"card-comments\" className={SECTION}>\n <div className=\"comments-head\">\n <h2 id={commentsHeadingId} className=\"behind-group__title\">\n Comments\n </h2>\n {commentCount > 0 && (\n <span\n data-testid=\"card-comment-count\"\n className=\"rounded-full bg-muted px-2 text-xs leading-5 font-bold text-muted-foreground\"\n >\n {commentCount}\n </span>\n )}\n <CommentsHelp appName={tool.name} />\n </div>\n <CommentsFeed\n target={commentTarget}\n claims={NO_CLAIMS}\n labelledBy={commentsHeadingId}\n />\n <div className=\"mt-3.5\">\n <CommunityInput\n ref={commentsRef}\n target={commentTarget}\n appName={tool.name}\n />\n </div>\n </section>\n\n <p className=\"mt-[22px] flex flex-wrap items-center gap-x-4 gap-y-1.5 text-[12.5px] font-semibold text-muted-foreground\">\n <span className=\"inline-flex items-center gap-1.5\">\n <kbd className={KBD}>Esc</kbd> close\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <kbd className={KBD}>←</kbd>\n <kbd className={KBD}>→</kbd> previous / next tool\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <kbd className={KBD}>Enter</kbd> open site\n <span className=\"font-normal\">\n (in the comment box, Enter sends)\n </span>\n </span>\n </p>\n {editing && (\n <ToolFormDialog\n mode=\"edit\"\n initial={tool}\n onClose={() => setEditing(false)}\n onDone={() => setEditing(false)}\n />\n )}\n </>\n ) : get.isError ? (\n <p className=\"py-10 text-center text-muted-foreground\">\n This tool is not listed any more.\n </p>\n ) : (\n <div aria-busy=\"true\" className=\"flex flex-col gap-4\">\n <Skeleton className=\"h-[72px] w-2/3 rounded-[14px]\" />\n <Skeleton className=\"h-4 w-1/2\" />\n <Skeleton className=\"aspect-[16/10] w-full rounded-[14px]\" />\n </div>\n )}\n </DetailDialogShell>\n )\n}\n\n/** Why a tool that is not active is shown the way it is. */\nfunction LifecycleNote({\n tool,\n onTellOwner,\n}: {\n tool: CommunityTool\n onTellOwner: () => void\n}) {\n const { lifecycle } = tool\n const [now] = useState(Date.now)\n if (lifecycle.state === 'not-working') {\n const since = new Date(now - lifecycle.days * DAY_MS).toISOString()\n return (\n <p\n data-testid=\"lifecycle-note\"\n className=\"mt-3 rounded-[10px] bg-amber-50 px-3 py-[9px] text-[13.5px] text-amber-900 dark:bg-amber-950 dark:text-amber-200\"\n >\n The daily check has not reached this site since{' '}\n {formatCardDate(since).replace(/ \\d{4}$/, '')}. It may be down or moved.{' '}\n <button\n type=\"button\"\n onClick={onTellOwner}\n className=\"font-bold underline underline-offset-2\"\n >\n Tell the owner\n </button>\n </p>\n )\n }\n if (lifecycle.state === 'archived') {\n const on = formatCardDate(lifecycle.at)\n return (\n <p\n data-testid=\"lifecycle-note\"\n className=\"mt-3 rounded-[10px] bg-muted px-3 py-[9px] text-[13.5px] text-muted-foreground\"\n >\n {lifecycle.byName\n ? `Archived by ${lifecycle.byName} on ${on}${lifecycle.reason ? `: ${lifecycle.reason}` : ''}.`\n : `Archived automatically on ${on} after 30 days not working.`}{' '}\n Kept here so old links still land somewhere.\n </p>\n )\n }\n return null\n}\n"],"names":["_a","_b"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ToolCard.js","sources":["../../../../../src/modules/communityTools/ui/ToolCard.tsx"],"sourcesContent":["import type { Claim } from '@igstack/app-catalog-backend-core'\nimport type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'\nimport {\n Archive,\n ArrowLeft,\n ArrowRight,\n Check,\n Eye,\n Pencil,\n} from 'lucide-react'\nimport { useEffect, useId, useRef, useState } from 'react'\nimport { useTRPC } from '~/api/infra/trpc'\nimport {\n DIALOG_ROUND_BUTTON,\n DetailDialogShell,\n} from '~/modules/appCatalog/ui/catalog/DetailDialogShell'\nimport { MarkdownText } from '~/modules/appCatalog/ui/components/MarkdownText'\nimport { CommentsFeed } from '~/modules/appCatalog/ui/detail/CommentsFeed'\nimport { CommentsHelp } from '~/modules/appCatalog/ui/detail/CommentsHelp'\nimport { CommunityInput } from '~/modules/appCatalog/ui/detail/CommunityInput'\nimport type { CommunityInputHandle } from '~/modules/appCatalog/ui/detail/CommunityInput'\nimport { cn } from '~/lib/utils'\nimport { useAuth } from '~/modules/auth'\nimport { Skeleton } from '~/ui/skeleton'\nimport { CardScreenshot, formatCardDate } from './CardScreenshot'\nimport { CardTitleBlock } from './CardTitleBlock'\nimport { PersonAvatar } from './FeedRow'\nimport { openSite } from '../siteOpen'\nimport { exactDate, timeAgo } from '../timeAgo'\nimport { useTimeUp } from '../useTimeUp'\nimport { PinButton } from './PinButton'\nimport { TagChip, tagKey, tagLabel } from './TagRow'\nimport { ArchiveConfirm, GHOST_BUTTON, ToolFormDialog } from './ToolFormDialog'\n\nconst KBD =\n 'rounded-[5px] border border-b-2 border-border bg-card px-1.5 py-px text-[11px] font-semibold text-muted-foreground'\n\nconst SECTION = 'mt-6 border-t border-border pt-[22px]'\n\nconst LABEL =\n 'text-[11.5px] font-bold tracking-wider text-muted-foreground uppercase'\n\nconst NO_CLAIMS: Claim[] = []\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\n/** How often a card whose first screenshot is on its way asks again. */\nconst CAPTURE_POLL_MS = 5000\n\n/** How long one open card waits for a first screenshot before it stops asking. */\nconst CAPTURE_WAIT_MS = 2 * 60 * 1000\n\nexport interface ToolCardProps {\n slug: string\n /** The list the card was opened from, in its order: ←/→ step through it. */\n siblings: string[]\n onClose: () => void\n onNavigate: (slug: string) => void\n pinned: boolean\n /** Its pin is being saved. */\n pinBusy?: boolean\n onTogglePin: (tool: CommunityTool) => void\n /** Display names of official apps by slug, for app tags. */\n appNames: Record<string, string>\n /** A tag chip on the card: close it and filter the feed to that tag. */\n onChooseTag: (tag: string) => void\n}\n\n/** Typing belongs to the field: the card's keys stay out of it. */\nfunction isTyping(target: HTMLElement): boolean {\n return (\n target.isContentEditable ||\n target.closest('input, textarea, select') !== null\n )\n}\n\n/**\n * A community tool over the Community page, in the official card's shell: no\n * tabs, one scroll. Title and screenshot to recognise it, how to use it, who\n * to ask, and the comments.\n */\nexport function ToolCard({\n slug,\n siblings,\n onClose,\n onNavigate,\n pinned,\n pinBusy,\n onTogglePin,\n appNames,\n onChooseTag,\n}: ToolCardProps) {\n const { user } = useAuth()\n const isAdmin = user?.isAdmin === true\n const trpc = useTRPC()\n const queryClient = useQueryClient()\n // The feed already holds every tool, so the card paints at once from it\n // and the tool's own query only refreshes it.\n const list = useQuery(trpc.communityTools.list.queryOptions())\n const fromList = list.data?.find((t) => t.slug === slug)\n // Whatever the server says, an open card stops waiting after a while: a\n // capture that never reports back must not keep it asking forever.\n const gaveUpOnCapture = useTimeUp(slug, CAPTURE_WAIT_MS)\n const get = useQuery({\n ...trpc.communityTools.get.queryOptions({ slug }),\n placeholderData: fromList,\n // Until the first visit lands, keep asking, so the screenshot replaces\n // the placeholder without a reload.\n refetchInterval: (query) =>\n query.state.data?.capturePending && !gaveUpOnCapture\n ? CAPTURE_POLL_MS\n : false,\n })\n const tool = get.data\n\n // One view per open. The ref keeps StrictMode's second effect from counting\n // twice; stepping to another tool is another open.\n const [counts, setCounts] = useState<{\n slug: string\n views30d: number\n viewsAll: number\n } | null>(null)\n // The hook's own onSuccess, not one passed to `mutate`: StrictMode's\n // remount detaches the observer a `mutate` callback hangs on, so that one\n // never fires and the count stayed one short.\n const { mutate: recordView } = useMutation(\n trpc.communityTools.recordView.mutationOptions({\n onSuccess: (c, { slug: viewed }) => {\n setCounts({ slug: viewed, ...c })\n },\n }),\n )\n const recordedForRef = useRef<string | null>(null)\n useEffect(() => {\n if (recordedForRef.current === slug) return\n recordedForRef.current = slug\n recordView({ slug })\n }, [slug, recordView])\n const views = counts?.slug === slug ? counts : tool\n\n const [editing, setEditing] = useState(false)\n const [archiving, setArchiving] = useState(false)\n const refreshTools = () =>\n queryClient.invalidateQueries({\n queryKey: trpc.communityTools.pathKey(),\n })\n const approve = useMutation(\n trpc.communityTools.approve.mutationOptions({ onSuccess: refreshTools }),\n )\n const archive = useMutation(\n trpc.communityTools.archive.mutationOptions({\n onSuccess: () => {\n setArchiving(false)\n return refreshTools()\n },\n }),\n )\n const adminError = approve.error ?? archive.error\n\n const commentsRef = useRef<CommunityInputHandle>(null)\n const commentsHeadingId = useId()\n const commentTarget = { communityToolSlug: slug }\n const comments = useQuery(trpc.feedback.list.queryOptions(commentTarget))\n const commentCount = comments.data?.items.length ?? 0\n\n const index = siblings.indexOf(slug)\n const prev = index > 0 ? siblings[index - 1] : undefined\n const next = index >= 0 ? siblings[index + 1] : undefined\n const nameOf = (s: string) => list.data?.find((t) => t.slug === s)?.name ?? s\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n // React bubbles through portals: keys in the screenshot viewer are not ours.\n const target = e.target as HTMLElement\n if (!e.currentTarget.contains(target)) return\n if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return\n if (isTyping(target)) return\n if (e.key === 'ArrowLeft' && prev) {\n e.preventDefault()\n onNavigate(prev)\n } else if (e.key === 'ArrowRight' && next) {\n e.preventDefault()\n onNavigate(next)\n } else if (e.key === 'Enter' && tool) {\n // A link or button answers Enter itself; the title block is the link\n // that opens the site.\n if (target.closest('a, button')) return\n e.preventDefault()\n openSite(tool)\n }\n }\n\n const controls = index >= 0 && siblings.length > 1 && (\n <>\n <span className=\"flex items-center gap-1 text-[13px] font-semibold text-muted-foreground\">\n <button\n type=\"button\"\n aria-label={prev ? `Previous tool: ${nameOf(prev)}` : 'Previous tool'}\n disabled={!prev}\n onClick={() => prev && onNavigate(prev)}\n className={cn(DIALOG_ROUND_BUTTON, 'disabled:opacity-40')}\n >\n <ArrowLeft className=\"size-4\" />\n </button>\n <span data-testid=\"card-position\">\n {index + 1} of {siblings.length}\n </span>\n <button\n type=\"button\"\n aria-label={next ? `Next tool: ${nameOf(next)}` : 'Next tool'}\n disabled={!next}\n onClick={() => next && onNavigate(next)}\n className={cn(DIALOG_ROUND_BUTTON, 'disabled:opacity-40')}\n >\n <ArrowRight className=\"size-4\" />\n </button>\n </span>\n <i aria-hidden=\"true\" className=\"mx-1 h-5 w-px bg-border\" />\n </>\n )\n\n return (\n <DetailDialogShell\n label={`${tool?.name ?? 'Community tool'} details`}\n shownKey={slug}\n maxWidthClass=\"max-w-[min(920px,94vw)]\"\n focusSelector='[data-testid=\"card-title-block\"]'\n controls={controls}\n testId=\"tool-card\"\n onClose={onClose}\n onKeyDown={onKeyDown}\n >\n {tool ? (\n <>\n <div className=\"border-b border-border pb-[22px]\">\n <div className=\"flex items-end justify-between gap-4 pr-44\">\n <CardTitleBlock tool={tool} />\n <div className=\"-mr-40 flex shrink-0 items-center gap-2\">\n {tool.canEdit && (\n <button\n type=\"button\"\n data-testid=\"card-edit-button\"\n onClick={() => setEditing(true)}\n className={cn(GHOST_BUTTON, 'h-10 px-3.5 text-[13.5px]')}\n >\n <Pencil aria-hidden=\"true\" className=\"size-4\" />\n Edit\n </button>\n )}\n <PinButton\n labelled\n pinned={pinned}\n busy={pinBusy}\n name={tool.name}\n onToggle={() => onTogglePin(tool)}\n />\n </div>\n </div>\n <div\n className={cn(\n tool.lifecycle.state === 'archived' && 'opacity-60',\n )}\n >\n {tool.summary && (\n <p className=\"mt-3.5 text-[15px] text-foreground/80\">\n {tool.summary}\n </p>\n )}\n {tool.tags.length > 0 && (\n <div\n data-testid=\"card-tags\"\n className=\"mt-2.5 flex flex-wrap gap-1.5\"\n >\n {tool.tags.map((tag) => (\n <TagChip\n key={tagKey(tag)}\n label={tagLabel(tag, appNames)}\n onClick={() => onChooseTag(tagKey(tag))}\n />\n ))}\n </div>\n )}\n {views && (\n <p\n data-testid=\"card-views\"\n className=\"mt-2.5 flex gap-[18px] text-[13px] font-semibold text-muted-foreground\"\n >\n <span className=\"inline-flex items-center gap-1.5\">\n <Eye aria-hidden=\"true\" className=\"size-[15px]\" />\n <b className=\"text-foreground\">\n {views.views30d.toLocaleString('en-US')}\n </b>{' '}\n opens this month\n </span>\n <span>\n <b className=\"text-foreground\">\n {views.viewsAll.toLocaleString('en-US')}\n </b>{' '}\n all time\n </span>\n {tool.siteUpdatedAt && (\n <span\n data-testid=\"card-site-updated\"\n title={exactDate(tool.siteUpdatedAt)}\n >\n Site updated {timeAgo(tool.siteUpdatedAt)}\n </span>\n )}\n </p>\n )}\n {/* Who to ask, up here with the facts: below the screenshot\n it fell under the fold. */}\n <p\n data-testid=\"card-people\"\n className=\"mt-2.5 flex flex-wrap items-center gap-x-[18px] gap-y-1.5 text-[13px] font-semibold text-muted-foreground\"\n >\n {tool.owners.length > 0 && (\n <span className=\"inline-flex flex-wrap items-center gap-x-3 gap-y-1.5\">\n <span className={LABEL}>Owners</span>\n {tool.owners.map((owner) => (\n <span\n key={owner.slug ?? `named:${owner.name}`}\n className=\"inline-flex items-center gap-1.5 text-foreground\"\n >\n <PersonAvatar name={owner.name} />\n <span data-testid=\"card-owner\">{owner.name}</span>\n </span>\n ))}\n </span>\n )}\n <span className=\"inline-flex items-center gap-1.5\">\n <span className={LABEL}>Added by</span>\n <span className=\"text-foreground\">{tool.addedBy.name}</span>\n <span className=\"font-normal\">\n {formatCardDate(tool.createdAt)}\n </span>\n </span>\n </p>\n </div>\n <LifecycleNote\n tool={tool}\n onTellOwner={() => commentsRef.current?.focus()}\n />\n {isAdmin && (\n <div\n data-testid=\"card-admin\"\n className=\"mt-3.5 flex flex-col gap-2.5 rounded-[10px] border border-dashed border-border px-3 py-2.5\"\n >\n {archiving ? (\n <ArchiveConfirm\n busy={archive.isPending}\n onCancel={() => setArchiving(false)}\n onArchive={(reason) => archive.mutate({ slug, reason })}\n />\n ) : (\n <div className=\"flex flex-wrap items-center gap-2.5\">\n <span className=\"mr-auto text-[12.5px] font-bold tracking-wider text-muted-foreground uppercase\">\n Admin\n </span>\n {!tool.reviewed && (\n <button\n type=\"button\"\n disabled={approve.isPending}\n aria-busy={approve.isPending || undefined}\n onClick={() => approve.mutate({ slug })}\n className={GHOST_BUTTON}\n >\n <Check aria-hidden=\"true\" className=\"size-4\" />\n {approve.isPending ? 'Approving…' : 'Approve'}\n </button>\n )}\n {tool.lifecycle.state !== 'archived' && (\n <button\n type=\"button\"\n onClick={() => setArchiving(true)}\n className={GHOST_BUTTON}\n >\n <Archive aria-hidden=\"true\" className=\"size-4\" />\n Archive\n </button>\n )}\n </div>\n )}\n {adminError && (\n <p\n role=\"alert\"\n className=\"text-[13px] font-semibold text-destructive\"\n >\n {adminError.message}\n </p>\n )}\n </div>\n )}\n </div>\n\n <CardScreenshot\n tool={tool}\n capturing={tool.capturePending && !gaveUpOnCapture}\n />\n\n {tool.descriptionMd.trim() && (\n <section\n data-testid=\"card-how-to-use\"\n className={cn(SECTION, 'text-[14.5px]')}\n >\n <h3 className=\"mb-2 font-serif text-[17px] font-semibold\">\n How to use\n </h3>\n <div className=\"prose prose-sm dark:prose-invert max-w-[68ch]\">\n <MarkdownText>{tool.descriptionMd}</MarkdownText>\n </div>\n </section>\n )}\n\n {/* The card's own order: what was said first, the input last. The\n official card's CommunitySection puts the input on top. */}\n <section data-testid=\"card-comments\" className={SECTION}>\n <div className=\"comments-head\">\n <h2 id={commentsHeadingId} className=\"behind-group__title\">\n Comments\n </h2>\n {commentCount > 0 && (\n <span\n data-testid=\"card-comment-count\"\n className=\"rounded-full bg-muted px-2 text-xs leading-5 font-bold text-muted-foreground\"\n >\n {commentCount}\n </span>\n )}\n <CommentsHelp appName={tool.name} />\n </div>\n <CommentsFeed\n target={commentTarget}\n claims={NO_CLAIMS}\n labelledBy={commentsHeadingId}\n />\n <div className=\"mt-3.5\">\n <CommunityInput\n ref={commentsRef}\n target={commentTarget}\n appName={tool.name}\n />\n </div>\n </section>\n\n <p className=\"mt-[22px] flex flex-wrap items-center gap-x-4 gap-y-1.5 text-[12.5px] font-semibold text-muted-foreground\">\n <span className=\"inline-flex items-center gap-1.5\">\n <kbd className={KBD}>Esc</kbd> close\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <kbd className={KBD}>←</kbd>\n <kbd className={KBD}>→</kbd> previous / next tool\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <kbd className={KBD}>Enter</kbd> open site\n <span className=\"font-normal\">\n (in the comment box, Enter sends)\n </span>\n </span>\n </p>\n {editing && (\n <ToolFormDialog\n mode=\"edit\"\n initial={tool}\n onClose={() => setEditing(false)}\n onDone={() => setEditing(false)}\n />\n )}\n </>\n ) : get.isError ? (\n <p className=\"py-10 text-center text-muted-foreground\">\n This tool is not listed any more.\n </p>\n ) : (\n <div aria-busy=\"true\" className=\"flex flex-col gap-4\">\n <Skeleton className=\"h-[72px] w-2/3 rounded-[14px]\" />\n <Skeleton className=\"h-4 w-1/2\" />\n <Skeleton className=\"aspect-[16/10] w-full rounded-[14px]\" />\n </div>\n )}\n </DetailDialogShell>\n )\n}\n\n/** Why a tool that is not active is shown the way it is. */\nfunction LifecycleNote({\n tool,\n onTellOwner,\n}: {\n tool: CommunityTool\n onTellOwner: () => void\n}) {\n const { lifecycle } = tool\n const [now] = useState(Date.now)\n if (lifecycle.state === 'not-working') {\n const since = new Date(now - lifecycle.days * DAY_MS).toISOString()\n return (\n <p\n data-testid=\"lifecycle-note\"\n className=\"mt-3 rounded-[10px] bg-amber-50 px-3 py-[9px] text-[13.5px] text-amber-900 dark:bg-amber-950 dark:text-amber-200\"\n >\n The daily check has not reached this site since{' '}\n {formatCardDate(since).replace(/ \\d{4}$/, '')}. It may be down or moved.{' '}\n <button\n type=\"button\"\n onClick={onTellOwner}\n className=\"font-bold underline underline-offset-2\"\n >\n Tell the owner\n </button>\n </p>\n )\n }\n if (lifecycle.state === 'archived') {\n const on = formatCardDate(lifecycle.at)\n return (\n <p\n data-testid=\"lifecycle-note\"\n className=\"mt-3 rounded-[10px] bg-muted px-3 py-[9px] text-[13.5px] text-muted-foreground\"\n >\n {lifecycle.byName\n ? `Archived by ${lifecycle.byName} on ${on}${lifecycle.reason ? `: ${lifecycle.reason}` : ''}.`\n : `Archived automatically on ${on} after 30 days not working.`}{' '}\n Kept here so old links still land somewhere.\n </p>\n )\n }\n return null\n}\n"],"names":["_a","_b"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAM,MACJ;AAEF,MAAM,UAAU;AAEhB,MAAM,QACJ;AAEF,MAAM,YAAqB,CAAA;AAE3B,MAAM,SAAS,KAAK,KAAK,KAAK;AAG9B,MAAM,kBAAkB;AAGxB,MAAM,kBAAkB,IAAI,KAAK;AAmBjC,SAAS,SAAS,QAA8B;AAC9C,SACE,OAAO,qBACP,OAAO,QAAQ,yBAAyB,MAAM;AAElD;AAOO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAkB;;AAChB,QAAM,EAAE,KAAA,IAAS,QAAA;AACjB,QAAM,WAAU,6BAAM,aAAY;AAClC,QAAM,OAAO,QAAA;AACb,QAAM,cAAc,eAAA;AAGpB,QAAM,OAAO,SAAS,KAAK,eAAe,KAAK,cAAc;AAC7D,QAAM,YAAW,UAAK,SAAL,mBAAW,KAAK,CAAC,MAAM,EAAE,SAAS;AAGnD,QAAM,kBAAkB,UAAU,MAAM,eAAe;AACvD,QAAM,MAAM,SAAS;AAAA,IACnB,GAAG,KAAK,eAAe,IAAI,aAAa,EAAE,MAAM;AAAA,IAChD,iBAAiB;AAAA;AAAA;AAAA,IAGjB,iBAAiB,CAAC,UAAA;;AAChB,eAAAA,MAAA,MAAM,MAAM,SAAZ,gBAAAA,IAAkB,mBAAkB,CAAC,kBACjC,kBACA;AAAA;AAAA,EAAA,CACP;AACD,QAAM,OAAO,IAAI;AAIjB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAIlB,IAAI;AAId,QAAM,EAAE,QAAQ,WAAA,IAAe;AAAA,IAC7B,KAAK,eAAe,WAAW,gBAAgB;AAAA,MAC7C,WAAW,CAAC,GAAG,EAAE,MAAM,aAAa;AAClC,kBAAU,EAAE,MAAM,QAAQ,GAAG,GAAG;AAAA,MAClC;AAAA,IAAA,CACD;AAAA,EAAA;AAEH,QAAM,iBAAiB,OAAsB,IAAI;AACjD,YAAU,MAAM;AACd,QAAI,eAAe,YAAY,KAAM;AACrC,mBAAe,UAAU;AACzB,eAAW,EAAE,MAAM;AAAA,EACrB,GAAG,CAAC,MAAM,UAAU,CAAC;AACrB,QAAM,SAAQ,iCAAQ,UAAS,OAAO,SAAS;AAE/C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,eAAe,MACnB,YAAY,kBAAkB;AAAA,IAC5B,UAAU,KAAK,eAAe,QAAA;AAAA,EAAQ,CACvC;AACH,QAAM,UAAU;AAAA,IACd,KAAK,eAAe,QAAQ,gBAAgB,EAAE,WAAW,cAAc;AAAA,EAAA;AAEzE,QAAM,UAAU;AAAA,IACd,KAAK,eAAe,QAAQ,gBAAgB;AAAA,MAC1C,WAAW,MAAM;AACf,qBAAa,KAAK;AAClB,eAAO,aAAA;AAAA,MACT;AAAA,IAAA,CACD;AAAA,EAAA;AAEH,QAAM,aAAa,QAAQ,SAAS,QAAQ;AAE5C,QAAM,cAAc,OAA6B,IAAI;AACrD,QAAM,oBAAoB,MAAA;AAC1B,QAAM,gBAAgB,EAAE,mBAAmB,KAAA;AAC3C,QAAM,WAAW,SAAS,KAAK,SAAS,KAAK,aAAa,aAAa,CAAC;AACxE,QAAM,iBAAe,cAAS,SAAT,mBAAe,MAAM,WAAU;AAEpD,QAAM,QAAQ,SAAS,QAAQ,IAAI;AACnC,QAAM,OAAO,QAAQ,IAAI,SAAS,QAAQ,CAAC,IAAI;AAC/C,QAAM,OAAO,SAAS,IAAI,SAAS,QAAQ,CAAC,IAAI;AAChD,QAAM,SAAS,CAAC;;AAAc,aAAAC,OAAAD,MAAA,KAAK,SAAL,gBAAAA,IAAW,KAAK,CAAC,MAAM,EAAE,SAAS,OAAlC,gBAAAC,IAAsC,SAAQ;AAAA;AAE5E,QAAM,YAAY,CAAC,MAA2C;AAE5D,UAAM,SAAS,EAAE;AACjB,QAAI,CAAC,EAAE,cAAc,SAAS,MAAM,EAAG;AACvC,QAAI,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,SAAU;AACtD,QAAI,SAAS,MAAM,EAAG;AACtB,QAAI,EAAE,QAAQ,eAAe,MAAM;AACjC,QAAE,eAAA;AACF,iBAAW,IAAI;AAAA,IACjB,WAAW,EAAE,QAAQ,gBAAgB,MAAM;AACzC,QAAE,eAAA;AACF,iBAAW,IAAI;AAAA,IACjB,WAAW,EAAE,QAAQ,WAAW,MAAM;AAGpC,UAAI,OAAO,QAAQ,WAAW,EAAG;AACjC,QAAE,eAAA;AACF,eAAS,IAAI;AAAA,IACf;AAAA,EACF;AAEA,QAAM,WAAW,SAAS,KAAK,SAAS,SAAS,KAC/C,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA,qBAAC,QAAA,EAAK,WAAU,2EACd,UAAA;AAAA,MAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,cAAY,OAAO,kBAAkB,OAAO,IAAI,CAAC,KAAK;AAAA,UACtD,UAAU,CAAC;AAAA,UACX,SAAS,MAAM,QAAQ,WAAW,IAAI;AAAA,UACtC,WAAW,GAAG,qBAAqB,qBAAqB;AAAA,UAExD,UAAA,oBAAC,WAAA,EAAU,WAAU,SAAA,CAAS;AAAA,QAAA;AAAA,MAAA;AAAA,MAEhC,qBAAC,QAAA,EAAK,eAAY,iBACf,UAAA;AAAA,QAAA,QAAQ;AAAA,QAAE;AAAA,QAAK,SAAS;AAAA,MAAA,GAC3B;AAAA,MACA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,MAAK;AAAA,UACL,cAAY,OAAO,cAAc,OAAO,IAAI,CAAC,KAAK;AAAA,UAClD,UAAU,CAAC;AAAA,UACX,SAAS,MAAM,QAAQ,WAAW,IAAI;AAAA,UACtC,WAAW,GAAG,qBAAqB,qBAAqB;AAAA,UAExD,UAAA,oBAAC,YAAA,EAAW,WAAU,SAAA,CAAS;AAAA,QAAA;AAAA,MAAA;AAAA,IACjC,GACF;AAAA,IACA,oBAAC,KAAA,EAAE,eAAY,QAAO,WAAU,0BAAA,CAA0B;AAAA,EAAA,GAC5D;AAGF,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,OAAO,IAAG,6BAAM,SAAQ,gBAAgB;AAAA,MACxC,UAAU;AAAA,MACV,eAAc;AAAA,MACd,eAAc;AAAA,MACd;AAAA,MACA,QAAO;AAAA,MACP;AAAA,MACA;AAAA,MAEC,iBACC,qBAAA,UAAA,EACE,UAAA;AAAA,QAAA,qBAAC,OAAA,EAAI,WAAU,oCACb,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,8CACb,UAAA;AAAA,YAAA,oBAAC,kBAAe,MAAY;AAAA,YAC5B,qBAAC,OAAA,EAAI,WAAU,2CACZ,UAAA;AAAA,cAAA,KAAK,WACJ;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,MAAK;AAAA,kBACL,eAAY;AAAA,kBACZ,SAAS,MAAM,WAAW,IAAI;AAAA,kBAC9B,WAAW,GAAG,cAAc,2BAA2B;AAAA,kBAEvD,UAAA;AAAA,oBAAA,oBAAC,QAAA,EAAO,eAAY,QAAO,WAAU,UAAS;AAAA,oBAAE;AAAA,kBAAA;AAAA,gBAAA;AAAA,cAAA;AAAA,cAIpD;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,UAAQ;AAAA,kBACR;AAAA,kBACA,MAAM;AAAA,kBACN,MAAM,KAAK;AAAA,kBACX,UAAU,MAAM,YAAY,IAAI;AAAA,gBAAA;AAAA,cAAA;AAAA,YAClC,EAAA,CACF;AAAA,UAAA,GACF;AAAA,UACA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW;AAAA,gBACT,KAAK,UAAU,UAAU,cAAc;AAAA,cAAA;AAAA,cAGxC,UAAA;AAAA,gBAAA,KAAK,WACJ,oBAAC,KAAA,EAAE,WAAU,yCACV,eAAK,SACR;AAAA,gBAED,KAAK,KAAK,SAAS,KAClB;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,eAAY;AAAA,oBACZ,WAAU;AAAA,oBAET,UAAA,KAAK,KAAK,IAAI,CAAC,QACd;AAAA,sBAAC;AAAA,sBAAA;AAAA,wBAEC,OAAO,SAAS,KAAK,QAAQ;AAAA,wBAC7B,SAAS,MAAM,YAAY,OAAO,GAAG,CAAC;AAAA,sBAAA;AAAA,sBAFjC,OAAO,GAAG;AAAA,oBAAA,CAIlB;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAGJ,SACC;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,eAAY;AAAA,oBACZ,WAAU;AAAA,oBAEV,UAAA;AAAA,sBAAA,qBAAC,QAAA,EAAK,WAAU,oCACd,UAAA;AAAA,wBAAA,oBAAC,KAAA,EAAI,eAAY,QAAO,WAAU,eAAc;AAAA,wBAChD,oBAAC,OAAE,WAAU,mBACV,gBAAM,SAAS,eAAe,OAAO,GACxC;AAAA,wBAAK;AAAA,wBAAI;AAAA,sBAAA,GAEX;AAAA,2CACC,QAAA,EACC,UAAA;AAAA,wBAAA,oBAAC,OAAE,WAAU,mBACV,gBAAM,SAAS,eAAe,OAAO,GACxC;AAAA,wBAAK;AAAA,wBAAI;AAAA,sBAAA,GAEX;AAAA,sBACC,KAAK,iBACJ;AAAA,wBAAC;AAAA,wBAAA;AAAA,0BACC,eAAY;AAAA,0BACZ,OAAO,UAAU,KAAK,aAAa;AAAA,0BACpC,UAAA;AAAA,4BAAA;AAAA,4BACe,QAAQ,KAAK,aAAa;AAAA,0BAAA;AAAA,wBAAA;AAAA,sBAAA;AAAA,oBAC1C;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAMN;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,eAAY;AAAA,oBACZ,WAAU;AAAA,oBAET,UAAA;AAAA,sBAAA,KAAK,OAAO,SAAS,KACpB,qBAAC,QAAA,EAAK,WAAU,wDACd,UAAA;AAAA,wBAAA,oBAAC,QAAA,EAAK,WAAW,OAAO,UAAA,UAAM;AAAA,wBAC7B,KAAK,OAAO,IAAI,CAAC,UAChB;AAAA,0BAAC;AAAA,0BAAA;AAAA,4BAEC,WAAU;AAAA,4BAEV,UAAA;AAAA,8BAAA,oBAAC,cAAA,EAAa,MAAM,MAAM,KAAA,CAAM;AAAA,8BAChC,oBAAC,QAAA,EAAK,eAAY,cAAc,gBAAM,KAAA,CAAK;AAAA,4BAAA;AAAA,0BAAA;AAAA,0BAJtC,MAAM,QAAQ,SAAS,MAAM,IAAI;AAAA,wBAAA,CAMzC;AAAA,sBAAA,GACH;AAAA,sBAEF,qBAAC,QAAA,EAAK,WAAU,oCACd,UAAA;AAAA,wBAAA,oBAAC,QAAA,EAAK,WAAW,OAAO,UAAA,YAAQ;AAAA,4CAC/B,QAAA,EAAK,WAAU,mBAAmB,UAAA,KAAK,QAAQ,MAAK;AAAA,4CACpD,QAAA,EAAK,WAAU,eACb,UAAA,eAAe,KAAK,SAAS,EAAA,CAChC;AAAA,sBAAA,EAAA,CACF;AAAA,oBAAA;AAAA,kBAAA;AAAA,gBAAA;AAAA,cACF;AAAA,YAAA;AAAA,UAAA;AAAA,UAEF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC;AAAA,cACA,aAAa,MAAA;;AAAM,wBAAAD,MAAA,YAAY,YAAZ,gBAAAA,IAAqB;AAAA;AAAA,YAAM;AAAA,UAAA;AAAA,UAE/C,WACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,eAAY;AAAA,cACZ,WAAU;AAAA,cAET,UAAA;AAAA,gBAAA,YACC;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,MAAM,QAAQ;AAAA,oBACd,UAAU,MAAM,aAAa,KAAK;AAAA,oBAClC,WAAW,CAAC,WAAW,QAAQ,OAAO,EAAE,MAAM,QAAQ;AAAA,kBAAA;AAAA,gBAAA,IAGxD,qBAAC,OAAA,EAAI,WAAU,uCACb,UAAA;AAAA,kBAAA,oBAAC,QAAA,EAAK,WAAU,kFAAiF,UAAA,SAEjG;AAAA,kBACC,CAAC,KAAK,YACL;AAAA,oBAAC;AAAA,oBAAA;AAAA,sBACC,MAAK;AAAA,sBACL,UAAU,QAAQ;AAAA,sBAClB,aAAW,QAAQ,aAAa;AAAA,sBAChC,SAAS,MAAM,QAAQ,OAAO,EAAE,MAAM;AAAA,sBACtC,WAAW;AAAA,sBAEX,UAAA;AAAA,wBAAA,oBAAC,OAAA,EAAM,eAAY,QAAO,WAAU,UAAS;AAAA,wBAC5C,QAAQ,YAAY,eAAe;AAAA,sBAAA;AAAA,oBAAA;AAAA,kBAAA;AAAA,kBAGvC,KAAK,UAAU,UAAU,cACxB;AAAA,oBAAC;AAAA,oBAAA;AAAA,sBACC,MAAK;AAAA,sBACL,SAAS,MAAM,aAAa,IAAI;AAAA,sBAChC,WAAW;AAAA,sBAEX,UAAA;AAAA,wBAAA,oBAAC,SAAA,EAAQ,eAAY,QAAO,WAAU,UAAS;AAAA,wBAAE;AAAA,sBAAA;AAAA,oBAAA;AAAA,kBAAA;AAAA,gBAEnD,GAEJ;AAAA,gBAED,cACC;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,MAAK;AAAA,oBACL,WAAU;AAAA,oBAET,UAAA,WAAW;AAAA,kBAAA;AAAA,gBAAA;AAAA,cACd;AAAA,YAAA;AAAA,UAAA;AAAA,QAEJ,GAEJ;AAAA,QAEA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC;AAAA,YACA,WAAW,KAAK,kBAAkB,CAAC;AAAA,UAAA;AAAA,QAAA;AAAA,QAGpC,KAAK,cAAc,KAAA,KAClB;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,eAAY;AAAA,YACZ,WAAW,GAAG,SAAS,eAAe;AAAA,YAEtC,UAAA;AAAA,cAAA,oBAAC,MAAA,EAAG,WAAU,6CAA4C,UAAA,cAE1D;AAAA,cACA,oBAAC,SAAI,WAAU,iDACb,8BAAC,cAAA,EAAc,UAAA,KAAK,eAAc,EAAA,CACpC;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAMJ,qBAAC,WAAA,EAAQ,eAAY,iBAAgB,WAAW,SAC9C,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,iBACb,UAAA;AAAA,YAAA,oBAAC,MAAA,EAAG,IAAI,mBAAmB,WAAU,uBAAsB,UAAA,YAE3D;AAAA,YACC,eAAe,KACd;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,eAAY;AAAA,gBACZ,WAAU;AAAA,gBAET,UAAA;AAAA,cAAA;AAAA,YAAA;AAAA,YAGL,oBAAC,cAAA,EAAa,SAAS,KAAK,KAAA,CAAM;AAAA,UAAA,GACpC;AAAA,UACA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,QAAQ;AAAA,cACR,QAAQ;AAAA,cACR,YAAY;AAAA,YAAA;AAAA,UAAA;AAAA,UAEd,oBAAC,OAAA,EAAI,WAAU,UACb,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,KAAK;AAAA,cACL,QAAQ;AAAA,cACR,SAAS,KAAK;AAAA,YAAA;AAAA,UAAA,EAChB,CACF;AAAA,QAAA,GACF;AAAA,QAEA,qBAAC,KAAA,EAAE,WAAU,6GACX,UAAA;AAAA,UAAA,qBAAC,QAAA,EAAK,WAAU,oCACd,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAW,KAAK,UAAA,OAAG;AAAA,YAAM;AAAA,UAAA,GAChC;AAAA,UACA,qBAAC,QAAA,EAAK,WAAU,oCACd,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAW,KAAK,UAAA,KAAC;AAAA,YACtB,oBAAC,OAAA,EAAI,WAAW,KAAK,UAAA,KAAC;AAAA,YAAM;AAAA,UAAA,GAC9B;AAAA,UACA,qBAAC,QAAA,EAAK,WAAU,oCACd,UAAA;AAAA,YAAA,oBAAC,OAAA,EAAI,WAAW,KAAK,UAAA,SAAK;AAAA,YAAM;AAAA,YAChC,oBAAC,QAAA,EAAK,WAAU,eAAc,UAAA,oCAAA,CAE9B;AAAA,UAAA,EAAA,CACF;AAAA,QAAA,GACF;AAAA,QACC,WACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,SAAS,MAAM,WAAW,KAAK;AAAA,YAC/B,QAAQ,MAAM,WAAW,KAAK;AAAA,UAAA;AAAA,QAAA;AAAA,MAChC,GAEJ,IACE,IAAI,UACN,oBAAC,OAAE,WAAU,2CAA0C,UAAA,oCAAA,CAEvD,IAEA,qBAAC,OAAA,EAAI,aAAU,QAAO,WAAU,uBAC9B,UAAA;AAAA,QAAA,oBAAC,UAAA,EAAS,WAAU,gCAAA,CAAgC;AAAA,QACpD,oBAAC,UAAA,EAAS,WAAU,YAAA,CAAY;AAAA,QAChC,oBAAC,UAAA,EAAS,WAAU,uCAAA,CAAuC;AAAA,MAAA,EAAA,CAC7D;AAAA,IAAA;AAAA,EAAA;AAIR;AAGA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AACF,GAGG;AACD,QAAM,EAAE,cAAc;AACtB,QAAM,CAAC,GAAG,IAAI,SAAS,KAAK,GAAG;AAC/B,MAAI,UAAU,UAAU,eAAe;AACrC,UAAM,QAAQ,IAAI,KAAK,MAAM,UAAU,OAAO,MAAM,EAAE,YAAA;AACtD,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAU;AAAA,QACX,UAAA;AAAA,UAAA;AAAA,UACiD;AAAA,UAC/C,eAAe,KAAK,EAAE,QAAQ,WAAW,EAAE;AAAA,UAAE;AAAA,UAA2B;AAAA,UACzE;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS;AAAA,cACT,WAAU;AAAA,cACX,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QAED;AAAA,MAAA;AAAA,IAAA;AAAA,EAGN;AACA,MAAI,UAAU,UAAU,YAAY;AAClC,UAAM,KAAK,eAAe,UAAU,EAAE;AACtC,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,eAAY;AAAA,QACZ,WAAU;AAAA,QAET,UAAA;AAAA,UAAA,UAAU,SACP,eAAe,UAAU,MAAM,OAAO,EAAE,GAAG,UAAU,SAAS,KAAK,UAAU,MAAM,KAAK,EAAE,MAC1F,6BAA6B,EAAE;AAAA,UAA+B;AAAA,UAAI;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAI5E;AACA,SAAO;AACT;"}
|
|
@@ -10,7 +10,6 @@ import { useAppCatalogContext } from "../../appCatalog/context/AppCatalogContext
|
|
|
10
10
|
import { MarkdownText } from "../../appCatalog/ui/components/MarkdownText.js";
|
|
11
11
|
import { useAuth } from "../../auth/AuthContext.js";
|
|
12
12
|
import "../../../ui/button.js";
|
|
13
|
-
import "../../auth/AuthModalContext.js";
|
|
14
13
|
import { useDismissable } from "../../dismiss/DismissStack.js";
|
|
15
14
|
import { Dialog, DialogContent, DialogTitle } from "../../../ui/dialog.js";
|
|
16
15
|
import { formatCardDate } from "./CardScreenshot.js";
|