From cc3e45dc306f32c85dc2451e17799613d3ba1dd5 Mon Sep 17 00:00:00 2001 From: yun-zhi-ztl <15071461069@163.com> Date: Sun, 15 Mar 2026 13:44:07 +0800 Subject: [PATCH] fix(search): polish default sorting and loading states --- web/src/app/router.tsx | 2 +- web/src/features/search/search-bar.tsx | 40 +++++++++++++++----------- web/src/features/skill/skill-card.tsx | 3 +- web/src/i18n/locales/en.json | 6 ++-- web/src/i18n/locales/zh.json | 6 ++-- web/src/pages/dashboard/my-skills.tsx | 3 +- web/src/pages/search.tsx | 26 ++++++++++++++--- web/src/pages/skill-detail.tsx | 3 +- web/src/shared/lib/number-format.ts | 18 ++++++++++++ 9 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 web/src/shared/lib/number-format.ts diff --git a/web/src/app/router.tsx b/web/src/app/router.tsx index 954abbb3..4835ed8e 100644 --- a/web/src/app/router.tsx +++ b/web/src/app/router.tsx @@ -129,7 +129,7 @@ const searchRoute = createRoute({ validateSearch: (search: Record) => { return { q: (search.q as string) || '', - sort: (search.sort as string) || 'relevance', + sort: (search.sort as string) || 'newest', page: Number(search.page) || 0, starredOnly: search.starredOnly === true || search.starredOnly === 'true', } diff --git a/web/src/features/search/search-bar.tsx b/web/src/features/search/search-bar.tsx index 4d25d86e..9331ba0a 100644 --- a/web/src/features/search/search-bar.tsx +++ b/web/src/features/search/search-bar.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import { Loader2, Search, X } from 'lucide-react' import { Input } from '@/shared/ui/input' import { Button } from '@/shared/ui/button' @@ -7,11 +8,12 @@ interface SearchBarProps { defaultValue?: string value?: string placeholder?: string + isSearching?: boolean onChange?: (query: string) => void onSearch?: (query: string) => void } -export function SearchBar({ defaultValue = '', value, placeholder, onChange, onSearch }: SearchBarProps) { +export function SearchBar({ defaultValue = '', value, placeholder, isSearching = false, onChange, onSearch }: SearchBarProps) { const { t } = useTranslation() const [query, setQuery] = useState(defaultValue) const isControlled = value !== undefined @@ -37,32 +39,36 @@ export function SearchBar({ defaultValue = '', value, placeholder, onChange, onS } } + const handleClear = () => { + handleChange('') + onSearch?.('') + } + return (
- - - + handleChange(e.target.value)} placeholder={placeholder || t('searchBar.placeholder')} - className="pl-10 border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 h-12" + className="pl-10 pr-10 border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 h-12" /> + {currentQuery ? ( + + ) : null}
-
) diff --git a/web/src/features/skill/skill-card.tsx b/web/src/features/skill/skill-card.tsx index f9c5fb86..286b6764 100644 --- a/web/src/features/skill/skill-card.tsx +++ b/web/src/features/skill/skill-card.tsx @@ -6,6 +6,7 @@ import { useStar, useToggleStar } from '@/features/social/use-star' import { ConfirmDialog } from '@/shared/components/confirm-dialog' import { Card } from '@/shared/ui/card' import { NamespaceBadge } from '@/shared/components/namespace-badge' +import { formatCompactCount } from '@/shared/lib/number-format' import { Bookmark } from 'lucide-react' interface SkillCardProps { @@ -83,7 +84,7 @@ export function SkillCard({ skill, onClick, highlightStarred = true }: SkillCard - {skill.downloadCount} + {formatCompactCount(skill.downloadCount)} {{count}} results" + "resultCount": "Found <1>{{count}} results", + "loadingMore": "Updating search results..." }, "searchBar": { "placeholder": "Search skills...", - "button": "Search" + "button": "Search", + "clear": "Clear search" }, "login": { "title": "Login to SkillHub", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 6d545321..28dd6458 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -103,11 +103,13 @@ "noStarredSkills": "你还没有收藏任何技能", "enterKeyword": "请输入搜索关键词", "results": "找到 {{count}} 个技能", - "resultCount": "找到 <1>{{count}} 个结果" + "resultCount": "找到 <1>{{count}} 个结果", + "loadingMore": "正在更新搜索结果..." }, "searchBar": { "placeholder": "搜索技能...", - "button": "搜索" + "button": "搜索", + "clear": "清空搜索" }, "login": { "title": "登录 SkillHub", diff --git a/web/src/pages/dashboard/my-skills.tsx b/web/src/pages/dashboard/my-skills.tsx index eda2f56f..1278858a 100644 --- a/web/src/pages/dashboard/my-skills.tsx +++ b/web/src/pages/dashboard/my-skills.tsx @@ -4,6 +4,7 @@ import { Button } from '@/shared/ui/button' import { Card } from '@/shared/ui/card' import { EmptyState } from '@/shared/components/empty-state' import { useMySkills } from '@/shared/hooks/use-skill-queries' +import { formatCompactCount } from '@/shared/lib/number-format' export function MySkillsPage() { const navigate = useNavigate() @@ -86,7 +87,7 @@ export function MySkillsPage() { - {skill.downloadCount} + {formatCompactCount(skill.downloadCount)} diff --git a/web/src/pages/search.tsx b/web/src/pages/search.tsx index 053669b9..5556dfeb 100644 --- a/web/src/pages/search.tsx +++ b/web/src/pages/search.tsx @@ -1,6 +1,7 @@ import { startTransition, useEffect, useState } from 'react' import { useNavigate, useSearch } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' +import { Loader2 } from 'lucide-react' import type { SkillSummary } from '@/api/types' import { useAuth } from '@/features/auth/use-auth' import { SearchBar } from '@/features/search/search-bar' @@ -44,7 +45,7 @@ export function SearchPage() { const { isAuthenticated } = useAuth() const q = searchParams.q || '' - const sort = searchParams.sort || 'relevance' + const sort = searchParams.sort || 'newest' const page = searchParams.page ?? 0 const starredOnly = searchParams.starredOnly ?? false const [queryInput, setQueryInput] = useState(q) @@ -53,14 +54,18 @@ export function SearchPage() { setQueryInput(q) }, [q]) - const { data, isLoading } = useSearchSkills({ + const { data, isLoading, isFetching } = useSearchSkills({ q, sort, page, size: PAGE_SIZE, starredOnly, }) - const { data: starredSkills, isLoading: isLoadingStarred } = useMyStars(starredOnly && isAuthenticated) + const { + data: starredSkills, + isLoading: isLoadingStarred, + isFetching: isFetchingStarred, + } = useMyStars(starredOnly && isAuthenticated) useEffect(() => { const normalizedQuery = queryInput.trim() @@ -131,13 +136,19 @@ export function SearchPage() { : 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) return (
{/* Search Bar */}
- +
{/* Sort And Filters */} @@ -177,6 +188,13 @@ export function SearchPage() { )}
+ {isUpdatingResults ? ( +
+ + {t('search.loadingMore')} +
+ ) : null} +
{t('search.filters.label')}
diff --git a/web/src/shared/lib/number-format.ts b/web/src/shared/lib/number-format.ts new file mode 100644 index 00000000..a66f70cf --- /dev/null +++ b/web/src/shared/lib/number-format.ts @@ -0,0 +1,18 @@ +export function formatCompactCount(value: number): string { + if (value >= 1_000_000) { + const millions = value / 1_000_000 + return `${stripTrailingZero(millions)}M` + } + + if (value >= 1_000) { + const thousands = value / 1_000 + return `${stripTrailingZero(thousands)}K` + } + + return String(value) +} + +function stripTrailingZero(value: number): string { + const formatted = value >= 10 ? value.toFixed(0) : value.toFixed(1) + return formatted.endsWith('.0') ? formatted.slice(0, -2) : formatted +}