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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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} /> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
Reference in New Issue
Block a user