skillhub/web/src/pages/namespace.tsx
dongmucat 0020989ccb fix(namespace): add pagination to namespace skill list
The namespace detail page hardcoded a limit of 20 skills with no
pagination controls, so namespaces with more than 20 skills appeared
truncated. Add page state, wire it into the search query, and render
the shared Pagination component when total exceeds PAGE_SIZE. Also
reset page to 0 when the namespace param changes.

Closes #350
2026-04-27 10:54:57 +08:00

88 lines
2.9 KiB
TypeScript

import { useState, useEffect } from 'react'
import { useNavigate, useParams } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { NamespaceHeader } from '@/features/namespace/namespace-header'
import { SkillCard } from '@/features/skill/skill-card'
import { SkeletonList } from '@/shared/components/skeleton-loader'
import { EmptyState } from '@/shared/components/empty-state'
import { Pagination } from '@/shared/components/pagination'
import { useSearchSkills } from '@/shared/hooks/use-skill-queries'
import { useNamespaceDetail } from '@/shared/hooks/use-namespace-queries'
const PAGE_SIZE = 20
/**
* Public namespace page showing namespace metadata and the skills currently discoverable inside it.
*/
export function NamespacePage() {
const { t } = useTranslation()
const navigate = useNavigate()
const { namespace } = useParams({ from: '/space/$namespace' })
const [page, setPage] = useState(0)
// Reset page when namespace changes
useEffect(() => {
setPage(0)
}, [namespace])
const { data: namespaceData, isLoading: isLoadingNamespace } = useNamespaceDetail(namespace)
const { data: skillsData, isLoading: isLoadingSkills } = useSearchSkills({
namespace,
page,
size: PAGE_SIZE,
})
const totalPages = skillsData ? Math.max(Math.ceil(skillsData.total / skillsData.size), 1) : 1
const handleSkillClick = (slug: string) => {
navigate({ to: `/space/${namespace}/${encodeURIComponent(slug)}` })
}
if (isLoadingNamespace) {
return (
<div className="space-y-6 animate-fade-up">
<div className="h-12 w-48 animate-shimmer rounded-lg" />
<div className="h-6 w-96 animate-shimmer rounded-md" />
</div>
)
}
if (!namespaceData) {
return <EmptyState title={t('namespace.notFound')} />
}
return (
<div className="space-y-8 animate-fade-up">
<NamespaceHeader namespace={namespaceData} />
<div className="space-y-6">
<h2 className="text-2xl font-bold font-heading">{t('namespace.skillList')}</h2>
{isLoadingSkills ? (
<SkeletonList count={6} />
) : skillsData && skillsData.items.length > 0 ? (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{skillsData.items.map((skill, idx) => (
<div key={skill.id} className={`animate-fade-up delay-${Math.min(idx + 1, 6)}`}>
<SkillCard
skill={skill}
onClick={() => handleSkillClick(skill.slug)}
/>
</div>
))}
</div>
{skillsData.total > PAGE_SIZE ? (
<Pagination page={page} totalPages={totalPages} onPageChange={setPage} />
) : null}
</>
) : (
<EmptyState
title={t('namespace.emptyTitle')}
description={t('namespace.emptyDescription')}
/>
)}
</div>
</div>
)
}