diff --git a/scripts/list-tags.js b/scripts/list-tags.js
new file mode 100644
index 0000000..f1f4fe2
--- /dev/null
+++ b/scripts/list-tags.js
@@ -0,0 +1,39 @@
+const { PrismaClient } = require('@prisma/client');
+
+const prisma = new PrismaClient();
+
+async function main() {
+ const tags = await prisma.tag.findMany({
+ include: {
+ _count: {
+ select: { projects: true }
+ }
+ }
+ });
+
+ console.log('=== Tag Statistics ===\n');
+ console.log('Total tags:', tags.length);
+
+ const sortedTags = tags.sort((a, b) => b._count.projects - a._count.projects);
+
+ console.log('\n=== Tags by Project Count ===');
+ sortedTags.forEach((tag, idx) => {
+ console.log(`${idx + 1}. ${tag.name}: ${tag._count.projects} projects`);
+ });
+
+ const unusedTags = tags.filter(t => t._count.projects === 0);
+ console.log(`\n=== Unused Tags (${unusedTags.length}) ===`);
+ unusedTags.forEach(tag => console.log(`- ${tag.name}`));
+
+ const lowActivityTags = tags.filter(t => t._count.projects > 0 && t._count.projects <= 2);
+ console.log(`\n=== Low Activity Tags (1-2 projects) (${lowActivityTags.length}) ===`);
+ lowActivityTags.forEach(tag => console.log(`- ${tag.name}: ${tag._count.projects} projects`));
+
+ const highActivityTags = tags.filter(t => t._count.projects >= 5);
+ console.log(`\n=== High Activity Tags (5+ projects) (${highActivityTags.length}) ===`);
+ highActivityTags.forEach(tag => console.log(`- ${tag.name}: ${tag._count.projects} projects`));
+}
+
+main()
+ .catch(console.error)
+ .finally(() => prisma.$disconnect());
diff --git a/src/app/[locale]/projects/page.tsx b/src/app/[locale]/projects/page.tsx
index 57eff01..f8b77b0 100644
--- a/src/app/[locale]/projects/page.tsx
+++ b/src/app/[locale]/projects/page.tsx
@@ -1,6 +1,6 @@
import { Suspense } from 'react'
import { getTranslations } from 'next-intl/server'
-import { getProjects, getAllTags } from '@/hooks/useProjects'
+import { getProjects, getAllTags, getTopTags } from '@/hooks/useProjects'
import { ProjectList } from '@/components/project/ProjectList'
import { TagCloud } from '@/components/project/TagCloud'
import { SearchBar } from '@/components/search/SearchBar'
@@ -23,9 +23,10 @@ export default async function ProjectsPage({
const tag = resolvedSearchParams.tag || ''
const page = Number(resolvedSearchParams.page) || 1
- const [projectsData, tags] = await Promise.all([
+ const [projectsData, topTags, allTags] = await Promise.all([
getProjects({ search, tag, page }),
- getAllTags(),
+ getTopTags(10), // 获取前10个标签
+ getAllTags(), // 获取所有标签(用于展开)
])
return (
@@ -44,7 +45,12 @@ export default async function ProjectsPage({
Browse by Tags
-
+
diff --git a/src/components/project/TagCloud.tsx b/src/components/project/TagCloud.tsx
index ef47ed7..3530c9e 100644
--- a/src/components/project/TagCloud.tsx
+++ b/src/components/project/TagCloud.tsx
@@ -1,3 +1,6 @@
+'use client'
+
+import { useState, useMemo } from 'react'
import Link from 'next/link'
interface TagCloudProps {
@@ -10,41 +13,119 @@ interface TagCloudProps {
projects: number
}
}>
+ allTags?: Array<{
+ id: string
+ name: string
+ nameEn?: string | null
+ slug: string
+ _count?: {
+ projects: number
+ }
+ }>
locale: string
activeTag?: string
}
-export function TagCloud({ tags, locale, activeTag }: TagCloudProps) {
- return (
-
- {tags.map((tag) => {
- const count = tag._count?.projects || 0
- const isActive = activeTag === tag.slug
- const displayName = locale === 'en' && tag.nameEn ? tag.nameEn : tag.name
+export function TagCloud({ tags, allTags, locale, activeTag }: TagCloudProps) {
+ const [searchQuery, setSearchQuery] = useState('')
+ const [showAll, setShowAll] = useState(false)
- return (
-
- {displayName}
- {count > 0 && (
-
- {count}
-
- )}
-
- )
- })}
+ // 搜索过滤逻辑
+ const filteredTags = useMemo(() => {
+ const sourceTags = allTags || tags
+
+ if (!searchQuery.trim()) {
+ return showAll ? sourceTags : tags
+ }
+
+ const query = searchQuery.toLowerCase()
+ return sourceTags.filter(tag =>
+ tag.name.toLowerCase().includes(query) ||
+ (tag.nameEn && tag.nameEn.toLowerCase().includes(query))
+ )
+ }, [searchQuery, showAll, tags, allTags])
+
+ const hasMoreTags = allTags && allTags.length > tags.length
+ const displayName = (tag: typeof tags[0]) =>
+ locale === 'en' && tag.nameEn ? tag.nameEn : tag.name
+
+ return (
+
+ {/* 搜索框 */}
+
+
setSearchQuery(e.target.value)}
+ placeholder={locale === 'zh' ? '搜索标签...' : 'Search tags...'}
+ className="w-full px-4 py-2 pl-10 bg-white dark:bg-surface-dark border-2 border-gray-300 dark:border-gray-600 focus:border-primary text-sm font-display focus:outline-none transition-colors"
+ />
+
+
+
+ {/* 标签列表 */}
+
+ {filteredTags.length > 0 ? (
+ filteredTags.map((tag) => {
+ const count = tag._count?.projects || 0
+ const isActive = activeTag === tag.slug
+
+ return (
+
+ {displayName(tag)}
+
+ {count}
+
+
+ )
+ })
+ ) : (
+
+ {locale === 'zh' ? '未找到匹配的标签' : 'No matching tags found'}
+
+ )}
+
+
+ {/* 显示全部按钮 */}
+ {!searchQuery && hasMoreTags && !showAll && (
+
+ )}
+
+ {/* 收起按钮 */}
+ {showAll && !searchQuery && (
+
+ )}
)
}
diff --git a/src/hooks/useProjects.ts b/src/hooks/useProjects.ts
index b124099..b225452 100644
--- a/src/hooks/useProjects.ts
+++ b/src/hooks/useProjects.ts
@@ -156,3 +156,28 @@ export async function getTagsWithProjectCounts(): Promise
return tags.filter(tag => tag._count.projects > 0)
}
+
+export async function getTopTags(limit: number = 10): Promise {
+ const tags = await prisma.tag.findMany({
+ include: {
+ _count: {
+ select: { projects: true }
+ }
+ },
+ where: {
+ projects: {
+ some: {} // 只返回有项目的标签
+ }
+ }
+ })
+
+ // 按项目数量降序、名称升序排序
+ return tags
+ .filter(tag => tag._count.projects > 0)
+ .sort((a, b) => {
+ const countDiff = b._count.projects - a._count.projects
+ if (countDiff !== 0) return countDiff
+ return a.name.localeCompare(b.name, 'zh')
+ })
+ .slice(0, limit)
+}
diff --git a/src/messages/en.json b/src/messages/en.json
index 019a7ec..d7b8d6b 100644
--- a/src/messages/en.json
+++ b/src/messages/en.json
@@ -8,6 +8,13 @@
"backToHome": "Back to Home",
"viewDetails": "VIEW DETAILS"
},
+ "tags": {
+ "searchPlaceholder": "Search tags...",
+ "showAll": "Show all tags ({count})",
+ "showLess": "↑ Show less",
+ "noResults": "No matching tags found",
+ "projectCount": "projects"
+ },
"home": {
"title": "AI Project Navigator",
"subtitle": "Discover and explore AI projects",
diff --git a/src/messages/zh.json b/src/messages/zh.json
index 2af85a5..2ad3166 100644
--- a/src/messages/zh.json
+++ b/src/messages/zh.json
@@ -8,6 +8,13 @@
"backToHome": "返回首页",
"viewDetails": "查看详情"
},
+ "tags": {
+ "searchPlaceholder": "搜索标签...",
+ "showAll": "显示全部标签 ({count})",
+ "showLess": "↑ 收起",
+ "noResults": "未找到匹配的标签",
+ "projectCount": "个项目"
+ },
"home": {
"title": "AI 项目导航",
"subtitle": "发现和探索全网优质 AI 项目",