refactor: 优化组件国际化实现

- 将客户端组件改为服务端组件,使用 getTranslations
- LocaleSwitcher 简化为接收 switchUrl prop
- ProjectCard/ProjectList/ExternalLinkCard 等组件支持 i18n
- 优化 webhook 去重查询,关联 links 数据
- 移除 ProjectDetail 中的客户端交互逻辑(ShareButtons 暂时禁用)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-29 19:58:23 +08:00
co-authored by Claude
parent 6303fd7ae6
commit 3869bb23bb
10 changed files with 143 additions and 93 deletions
+6 -1
View File
@@ -12,6 +12,7 @@ interface HomePageProps {
export default async function HomePage({ params }: HomePageProps) {
const { locale } = await params
const t = await getTranslations('home')
const tCommon = await getTranslations('common')
const [projectsData, tags] = await Promise.all([
getProjects({ limit: 6 }),
@@ -33,7 +34,11 @@ export default async function HomePage({ params }: HomePageProps) {
Discover and explore high-quality AI projects from across the web. Curated for developers and enthusiasts.
</p>
<Suspense fallback={<div className="h-16"></div>}>
<SearchBar locale={locale} />
<SearchBar
locale={locale}
searchPlaceholder={t('searchPlaceholder')}
searchLabel={tCommon('search')}
/>
</Suspense>
</section>
+6 -1
View File
@@ -15,6 +15,7 @@ export default async function ProjectsPage({
searchParams,
}: ProjectsPageProps) {
const t = await getTranslations('home')
const tCommon = await getTranslations('common')
const [resolvedParams, resolvedSearchParams] = await Promise.all([params, searchParams])
const { locale } = resolvedParams
@@ -47,7 +48,11 @@ export default async function ProjectsPage({
{/* Search and Filter Section */}
<div className="bg-surface-light dark:bg-surface-dark border-2 border-black dark:border-white/20 p-6 md:p-8 mb-12 shadow-neo dark:shadow-none">
<Suspense fallback={<div className="h-20"></div>}>
<SearchBar locale={locale} />
<SearchBar
locale={locale}
searchPlaceholder={t('searchPlaceholder')}
searchLabel={tCommon('search')}
/>
</Suspense>
<div className="border-t-2 border-gray-100 dark:border-gray-800 pt-6 mt-8">
+13 -2
View File
@@ -28,7 +28,11 @@ async function findExistingProject(projectData: ProjectInput) {
url: githubLink.url,
},
include: {
project: true,
project: {
include: {
links: true,
},
},
},
})
@@ -51,7 +55,11 @@ async function findExistingProject(projectData: ProjectInput) {
url: websiteLink.url,
},
include: {
project: true,
project: {
include: {
links: true,
},
},
},
})
@@ -70,6 +78,9 @@ async function findExistingProject(projectData: ProjectInput) {
const existingBySlug = await prisma.project.findUnique({
where: { slug },
include: {
links: true,
},
})
if (existingBySlug) {
+2 -21
View File
@@ -1,6 +1,5 @@
'use client'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
const locales = ['zh', 'en'] as const
@@ -9,30 +8,12 @@ const localeNames: Record<string, string> = {
en: 'EN'
}
export function LocaleSwitcher({ currentLocale }: { currentLocale: string }) {
const pathname = usePathname()
// 移除当前语言前缀,获取基础路径
const getBasePathname = () => {
const segments = pathname.split('/').filter(Boolean)
if (segments[0] && locales.includes(segments[0] as any)) {
return '/' + segments.slice(1).join('/')
}
return pathname
}
const basePathname = getBasePathname()
const switchLocale = (newLocale: string) => {
// 切换到另一种语言
return `/${newLocale}${basePathname}`
}
export function LocaleSwitcher({ currentLocale, switchUrl }: { currentLocale: string; switchUrl: string }) {
const otherLocale = currentLocale === 'zh' ? 'en' : 'zh'
return (
<Link
href={switchLocale(otherLocale)}
href={switchUrl}
className="font-display text-sm font-bold hover:opacity-70 transition-opacity"
>
{localeNames[currentLocale]} / {localeNames[otherLocale]}
+24 -11
View File
@@ -1,26 +1,39 @@
import { ExternalLink } from '@prisma/client'
import { ExternalLink, LinkType } from '@prisma/client'
import { getTranslations } from 'next-intl/server'
interface ExternalLinkCardProps {
links: ExternalLink[]
locale: string
}
const linkTypeNames = {
WEBSITE: '官网',
GITHUB: 'GitHub',
HUGGINGFACE: 'HuggingFace',
PAPER: '论文',
}
export async function ExternalLinkCard({ links, locale }: ExternalLinkCardProps) {
const t = await getTranslations('project')
export function ExternalLinkCard({ links }: ExternalLinkCardProps) {
if (links.length === 0) {
return null
}
// Build type map for all link types
const typeMap: Record<LinkType, string> = {
WEBSITE: t('website'),
GITHUB: t('github'),
HUGGINGFACE: t('huggingface'),
PAPER: t('paper'),
}
// Pre-resolve all link names
const linkItems = await Promise.all(
links.map(async (link) => ({
...link,
displayName: link.title || typeMap[link.type]
}))
)
return (
<div className="border rounded-lg p-6">
<h3 className="text-lg font-semibold mb-4"></h3>
<h3 className="text-lg font-semibold mb-4">{t('externalLinks')}</h3>
<div className="space-y-3">
{links.map((link) => (
{linkItems.map((link) => (
<a
key={link.id}
href={link.url}
@@ -30,7 +43,7 @@ export function ExternalLinkCard({ links }: ExternalLinkCardProps) {
>
<div>
<div className="font-medium">
{link.title || linkTypeNames[link.type]}
{link.displayName}
</div>
<div className="text-sm text-muted-foreground">{link.url}</div>
</div>
+15 -7
View File
@@ -1,4 +1,5 @@
import Link from 'next/link'
import { getTranslations } from 'next-intl/server'
interface ProjectCardProps {
project: {
@@ -30,11 +31,17 @@ function getProjectIcon(tags: Array<{ name: string }>): string {
return '✨'
}
export function ProjectCard({ project, locale, featured = false }: ProjectCardProps) {
export async function ProjectCard({ project, locale, featured = false }: ProjectCardProps) {
const t = await getTranslations('common')
const icon = getProjectIcon(project.tags)
const displayName = locale === 'en' && project.nameEn ? project.nameEn : project.name
const displayDescription = locale === 'en' && project.descriptionEn ? project.descriptionEn : project.description
// Helper to get display name for tag based on locale
const getTagName = (tag: { name: string; nameEn?: string | null }) => {
return locale === 'en' && tag.nameEn ? tag.nameEn : tag.name
}
return (
<article
className={`bg-white dark:bg-surface-dark border-2 border-black dark:border-gray-600 p-6 ${
@@ -61,7 +68,7 @@ export function ProjectCard({ project, locale, featured = false }: ProjectCardPr
key={tag.id}
className="bg-gray-100 dark:bg-gray-800 px-2 py-1 text-[10px] uppercase font-display font-bold border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300"
>
{tag.name}
{getTagName(tag)}
</span>
))}
</div>
@@ -71,7 +78,7 @@ export function ProjectCard({ project, locale, featured = false }: ProjectCardPr
href={`/${locale}/projects/${project.slug}`}
className="inline-flex items-center font-display font-bold text-sm uppercase hover:text-secondary dark:hover:text-primary group"
>
VIEW DETAILS{' '}
{t('viewDetails')}{' '}
<span className="ml-1 transform group-hover:translate-x-1 transition-transform"></span>
</Link>
</div>
@@ -80,21 +87,22 @@ export function ProjectCard({ project, locale, featured = false }: ProjectCardPr
}
// Special card for "Submit Your Project" CTA
export function SubmitProjectCard({ locale }: { locale: string }) {
export async function SubmitProjectCard({ locale }: { locale: string }) {
const t = await getTranslations('project')
return (
<article className="bg-primary border-2 border-black dark:border-gray-600 p-6 shadow-neo hover:shadow-neo-hover hover:-translate-y-1 transition-all duration-200 flex flex-col h-full justify-center items-center text-center group">
<div className="bg-white dark:bg-black p-4 rounded-full border-2 border-black mb-4 group-hover:scale-110 transition-transform">
<span className="text-3xl dark:text-primary">+</span>
</div>
<h3 className="font-display text-xl md:text-2xl font-bold text-black mb-2">Submit Your Project</h3>
<h3 className="font-display text-xl md:text-2xl font-bold text-black mb-2">{t('submitProjectTitle')}</h3>
<p className="text-black mb-6 font-sans text-sm px-4">
Have an AI tool worth sharing? Add it to Agent Park.
{t('submitProjectDescription')}
</p>
<Link
href="#"
className="bg-black text-white dark:bg-black dark:text-primary border-2 border-black dark:border-black px-6 py-2 font-display text-sm font-bold uppercase hover:bg-white hover:text-black dark:hover:bg-white dark:hover:text-black transition-colors"
>
SUBMIT NOW
{t('submitNow')}
</Link>
</article>
)
+15 -45
View File
@@ -1,7 +1,6 @@
'use client'
import Link from 'next/link'
import { MarkdownContent } from './MarkdownContent'
import { ShareButtons } from './ShareButtons'
interface ProjectDetailProps {
project: {
@@ -31,14 +30,22 @@ interface ProjectDetailProps {
locale: string
}
// Helper function to format date
// Helper function to format date - using ISO format for consistency
function formatDate(date: Date | string, locale: string): string {
const dateObj = typeof date === 'string' ? new Date(date) : date
const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short', day: 'numeric' }
return dateObj.toLocaleDateString(locale === 'en' ? 'en-US' : 'zh-CN', options)
const year = dateObj.getFullYear()
const month = dateObj.getMonth() + 1
const day = dateObj.getDate()
if (locale === 'en') {
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
return `${monthNames[month - 1]} ${day}, ${year}`
} else {
return `${year}${month}${day}`
}
}
export function ProjectDetail({ project, locale }: ProjectDetailProps) {
export async function ProjectDetail({ project, locale }: ProjectDetailProps) {
const displayName = locale === 'en' && project.nameEn ? project.nameEn : project.name
const displayDescription =
locale === 'en' && project.descriptionEn ? project.descriptionEn : project.description
@@ -49,20 +56,6 @@ export function ProjectDetail({ project, locale }: ProjectDetailProps) {
const categoryEn = project.tags[0]?.nameEn || project.tags[0]?.name || 'AI Agent'
const displayCategory = locale === 'en' ? categoryEn : category
const handleShare = () => {
if (typeof window !== 'undefined') {
const url = encodeURIComponent(window.location.href)
const text = encodeURIComponent(`Check out ${displayName} on Agent Park`)
window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank')
}
}
const handleCopyLink = () => {
if (typeof window !== 'undefined') {
navigator.clipboard.writeText(window.location.href)
}
}
return (
<>
{/* Header Section */}
@@ -153,31 +146,8 @@ npm install`}</code>
)}
</article>
{/* Share and Feedback Section */}
<div className="mt-12 pt-8 border-t border-gray-300 dark:border-gray-700 flex flex-col sm:flex-row justify-between items-center gap-6">
<div className="flex gap-4">
<button
className="p-2 border border-black dark:border-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
title="Share on X"
onClick={handleShare}
>
<span className="material-icons text-lg">share</span>
</button>
<button
className="p-2 border border-black dark:border-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
title="Copy Link"
onClick={handleCopyLink}
>
<span className="material-icons text-lg">link</span>
</button>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-display font-bold text-gray-500">DID THIS AGENT HELP YOU?</span>
<button className="px-4 py-2 bg-primary text-black font-display font-bold text-xs uppercase border border-black hover:bg-yellow-400 transition-colors shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:shadow-none active:translate-x-[2px] active:translate-y-[2px]">
Yes, it rocks
</button>
</div>
</div>
{/* Share and Feedback Section - Temporarily disabled for debugging */}
{/* <ShareButtons displayName={displayName} /> */}
</>
)
}
+5 -2
View File
@@ -1,4 +1,5 @@
import { ProjectCard, SubmitProjectCard } from './ProjectCard'
import { getTranslations } from 'next-intl/server'
interface ProjectListProps {
projects: Array<{
@@ -19,11 +20,13 @@ interface ProjectListProps {
featured?: boolean
}
export function ProjectList({ projects, locale, featured = false }: ProjectListProps) {
export async function ProjectList({ projects, locale, featured = false }: ProjectListProps) {
const t = await getTranslations('common')
if (projects.length === 0) {
return (
<div className="text-center py-12">
<p className="text-gray-500 dark:text-gray-400 font-display"></p>
<p className="text-gray-500 dark:text-gray-400 font-display">{t('noProjects')}</p>
</div>
)
}
+52
View File
@@ -0,0 +1,52 @@
'use client'
interface ShareButtonsProps {
displayName: string
}
export function ShareButtons({ displayName }: ShareButtonsProps) {
const handleShare = () => {
const url = encodeURIComponent(window.location.href)
const text = encodeURIComponent(`Check out ${displayName} on Agent Park`)
window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank')
}
const handleCopyLink = () => {
navigator.clipboard.writeText(window.location.href)
}
return (
<div className="mt-12 pt-8 border-t border-gray-300 dark:border-gray-700 flex flex-col sm:flex-row justify-between items-center gap-6">
<div className="flex gap-4">
<button
className="p-2 border border-black dark:border-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
title="Share on X"
onClick={handleShare}
id="share-button"
name="share"
>
<span className="material-icons text-lg">share</span>
</button>
<button
className="p-2 border border-black dark:border-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
title="Copy Link"
onClick={handleCopyLink}
id="copy-link-button"
name="copyLink"
>
<span className="material-icons text-lg">link</span>
</button>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-display font-bold text-gray-500">DID THIS AGENT HELP YOU?</span>
<button
className="px-4 py-2 bg-primary text-black font-display font-bold text-xs uppercase border border-black hover:bg-yellow-400 transition-colors shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:shadow-none active:translate-x-[2px] active:translate-y-[2px]"
id="feedback-button"
name="feedback"
>
Yes, it rocks
</button>
</div>
</div>
)
}
+5 -3
View File
@@ -5,9 +5,11 @@ import { useRouter, useSearchParams } from 'next/navigation'
interface SearchBarProps {
locale: string
searchPlaceholder: string
searchLabel: string
}
export function SearchBar({ locale }: SearchBarProps) {
export function SearchBar({ locale, searchPlaceholder, searchLabel }: SearchBarProps) {
const router = useRouter()
const searchParams = useSearchParams()
const [query, setQuery] = useState(searchParams.get('search') || '')
@@ -36,7 +38,7 @@ export function SearchBar({ locale }: SearchBarProps) {
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search AI projects..."
placeholder={searchPlaceholder}
className="block w-full pl-12 pr-32 py-4 bg-surface-light dark:bg-surface-dark border-2 border-black dark:border-gray-600 text-text-light dark:text-text-dark placeholder-gray-500 focus:ring-0 focus:border-black dark:focus:border-primary font-display shadow-neo transition-all"
/>
@@ -45,7 +47,7 @@ export function SearchBar({ locale }: SearchBarProps) {
type="submit"
className="absolute inset-y-2 right-2 px-4 bg-primary text-black font-bold font-display text-sm border-2 border-black hover:bg-yellow-400 transition-colors shadow-neo-sm active:shadow-none active:translate-x-[2px] active:translate-y-[2px]"
>
SEARCH
{searchLabel}
</button>
</div>
</div>