diff --git a/web/src/features/skill/skill-card.tsx b/web/src/features/skill/skill-card.tsx index fa0c2980..ff8fcd1b 100644 --- a/web/src/features/skill/skill-card.tsx +++ b/web/src/features/skill/skill-card.tsx @@ -21,59 +21,72 @@ export function SkillCard({ skill, onClick, highlightStarred = true }: SkillCard const { data: starStatus } = useStar(skill.id, highlightStarred && isAuthenticated) const showStarredHighlight = highlightStarred && isAuthenticated && starStatus?.starred const headlineVersion = getHeadlineVersion(skill) + const isInteractive = typeof onClick === 'function' return ( -
-
-
-

- {skill.displayName} -

-
-
- -
+ className="h-full p-5 cursor-pointer group relative overflow-hidden bg-white border shadow-sm transition-shadow hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/70 focus-visible:ring-offset-2" + style={{ borderColor: 'hsl(var(--border-card))' }} + onClick={onClick} + onKeyDown={(event) => { + if (!isInteractive) { + return + } + + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + onClick() + } + }} + role={isInteractive ? 'link' : undefined} + tabIndex={isInteractive ? 0 : undefined} + > +
+
+
+

+ {skill.displayName} +

- - {skill.summary && ( -

- {skill.summary} -

- )} - -
- {headlineVersion && ( - - v{headlineVersion.version} - - )} - - - - - {formatCompactCount(skill.downloadCount)} - - - - {skill.starCount} - - {skill.ratingAvg !== undefined && skill.ratingCount > 0 && ( - - - - - {skill.ratingAvg.toFixed(1)} - - )} +
+
- + + {skill.summary && ( +

+ {skill.summary} +

+ )} + +
+ {headlineVersion && ( + + v{headlineVersion.version} + + )} + + + + + {formatCompactCount(skill.downloadCount)} + + + + {skill.starCount} + + {skill.ratingAvg !== undefined && skill.ratingCount > 0 && ( + + + + + {skill.ratingAvg.toFixed(1)} + + )} +
+
+ ) } diff --git a/web/src/pages/search.tsx b/web/src/pages/search.tsx index 58137415..5e01a375 100644 --- a/web/src/pages/search.tsx +++ b/web/src/pages/search.tsx @@ -1,4 +1,4 @@ -import { startTransition, useEffect, useState } from 'react' +import { startTransition, useEffect, useRef, useState } from 'react' import { useNavigate, useSearch } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { Loader2 } from 'lucide-react' @@ -60,11 +60,38 @@ export function SearchPage() { const page = searchParams.page ?? 0 const starredOnly = searchParams.starredOnly ?? false const [queryInput, setQueryInput] = useState(q) + const previousPageRef = useRef(page) useEffect(() => { setQueryInput(q) }, [q]) + useEffect(() => { + if (previousPageRef.current !== page) { + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur() + } + + let secondFrame = 0 + const firstFrame = window.requestAnimationFrame(() => { + window.scrollTo({ top: 0, behavior: 'auto' }) + secondFrame = window.requestAnimationFrame(() => { + window.scrollTo({ top: 0, behavior: 'auto' }) + }) + }) + + previousPageRef.current = page + return () => { + window.cancelAnimationFrame(firstFrame) + if (secondFrame) { + window.cancelAnimationFrame(secondFrame) + } + } + } + + previousPageRef.current = page + }, [page]) + const { data, isLoading, isFetching } = useSearchSkills({ q, label: selectedLabel || undefined, @@ -79,6 +106,7 @@ export function SearchPage() { isLoading: isLoadingStarred, isFetching: isFetchingStarred, } = useMyStars(starredOnly && isAuthenticated) + const shouldShowGuidance = !starredOnly && !q && !selectedLabel useEffect(() => { // Debounce URL updates while the user is typing so query state stays shareable without @@ -117,6 +145,9 @@ export function SearchPage() { } const handlePageChange = (newPage: number) => { + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur() + } navigate({ to: '/search', search: { q, label: selectedLabel, sort, page: newPage, starredOnly } }) } @@ -154,10 +185,10 @@ export function SearchPage() { : data ? Math.ceil(data.total / data.size) : 0 - const displayItems = starredOnly ? starredPageItems : (data?.items ?? []) - const isPageLoading = starredOnly ? isLoadingStarred : isLoading - const isUpdatingResults = starredOnly ? isFetchingStarred && !isLoadingStarred : isFetching && !isLoading - const resultCount = starredOnly ? filteredStarredSkills.length : (data?.total ?? 0) + const displayItems = shouldShowGuidance ? [] : (starredOnly ? starredPageItems : (data?.items ?? [])) + const isPageLoading = shouldShowGuidance ? false : (starredOnly ? isLoadingStarred : isLoading) + const isUpdatingResults = shouldShowGuidance ? false : (starredOnly ? isFetchingStarred && !isLoadingStarred : isFetching && !isLoading) + const resultCount = shouldShowGuidance ? 0 : (starredOnly ? filteredStarredSkills.length : (data?.total ?? 0)) return (
@@ -265,7 +296,9 @@ export function SearchPage() { searchSkills(params), - enabled: params.starredOnly !== true, + enabled: params.starredOnly !== true && Boolean(params.q || params.label), }) }