feat: 添加顶部公告栏跳转链接和关闭按钮功能

- 新增 AnnouncementBar 客户端组件支持点击跳转
- 添加关闭按钮,状态保存至 localStorage
- 刷新页面后记住用户关闭状态
- 支持 i18n 配置公告内容和链接

🤖 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:57:47 +08:00
co-authored by Claude
parent a8a847beb2
commit 3ab2d2057c
4 changed files with 165 additions and 34 deletions
+51 -26
View File
@@ -1,7 +1,9 @@
import { notFound } from "next/navigation"
import { setRequestLocale } from 'next-intl/server'
import { setRequestLocale, getTranslations } from 'next-intl/server'
import Link from "next/link"
import { LocaleSwitcher } from "@/components/locale/LocaleSwitcher"
import { AnnouncementBar } from "@/components/layout/AnnouncementBar"
import type { Metadata } from "next"
const locales = ['zh', 'en']
@@ -9,6 +11,20 @@ export async function generateStaticParams() {
return locales.map((locale) => ({ locale }))
}
export async function generateMetadata({
params
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations('home')
return {
title: t('metaTitle'),
description: t('metaDescription'),
}
}
export default async function LocaleLayout({
children,
params
@@ -23,13 +39,15 @@ export default async function LocaleLayout({
setRequestLocale(locale)
// Get translations
const t = await getTranslations('layout')
const tNav = await getTranslations('navigation')
const tHome = await getTranslations('home')
return (
<div className="min-h-screen flex flex-col">
{/* Top announcement bar */}
<div className="bg-primary w-full py-2 px-4 border-b-2 border-black dark:border-gray-600 flex justify-center items-center text-xs font-bold font-display tracking-wide text-black">
<span>DISCOVER THE FUTURE OF AI AGENTS</span>
<span className="material-icons text-sm align-middle ml-1">arrow_forward</span>
</div>
<AnnouncementBar text={t('announcement')} href={t('announcementHref')} locale={locale} />
{/* Navigation */}
<header className="w-full border-b-2 border-black dark:border-gray-600 bg-background-light dark:bg-background-dark sticky top-0 z-50">
@@ -49,36 +67,39 @@ export default async function LocaleLayout({
className="font-display text-sm font-bold hover:text-secondary dark:hover:text-primary transition-colors"
href={`/${locale}`}
>
HOME
{tNav('home')}
</Link>
<Link
className="font-display text-sm font-bold hover:text-secondary dark:hover:text-primary transition-colors"
href={`/${locale}/projects`}
>
PROJECTS
{tNav('projects')}
</Link>
<Link
className="font-display text-sm font-bold hover:text-secondary dark:hover:text-primary transition-colors"
href="#"
>
BLOG
{tNav('blog')}
</Link>
<Link
className="font-display text-sm font-bold hover:text-secondary dark:hover:text-primary transition-colors"
href="#"
>
ABOUT
{tNav('about')}
</Link>
</nav>
{/* Right side */}
<div className="hidden md:flex items-center space-x-4">
<LocaleSwitcher currentLocale={locale} />
<LocaleSwitcher
currentLocale={locale}
switchUrl={`/${locale === 'zh' ? 'en' : 'zh'}`}
/>
<Link
className="bg-white dark:bg-surface-dark border-2 border-black dark:border-white px-4 py-2 font-display text-sm font-bold shadow-neo-sm hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] transition-all"
href="#"
>
SUBMIT PROJECT
{tNav('submitProject')}
</Link>
</div>
@@ -98,27 +119,31 @@ export default async function LocaleLayout({
<section className="border-t-2 border-black dark:border-gray-600">
<div className="grid grid-cols-1 md:grid-cols-2">
<div className="bg-primary dark:bg-yellow-600 p-12 md:p-20 border-b-2 md:border-b-0 md:border-r-2 border-black dark:border-gray-600 flex flex-col justify-center">
<h2 className="font-display text-4xl font-bold text-black mb-4">STAY UPDATED</h2>
<h2 className="font-display text-4xl font-bold text-black mb-4">{t('stayUpdated')}</h2>
<p className="font-sans text-black mb-8 max-w-md">
Get the latest AI tools and trends delivered straight to your inbox. No spam, just intelligence.
{t('stayUpdatedDesc')}
</p>
<form className="space-y-4 max-w-md">
<input
className="w-full bg-background-light border-2 border-black p-3 font-display text-sm placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-black"
placeholder="E-mail"
placeholder={t('email')}
type="email"
id="newsletter-email"
name="email"
autoComplete="email"
required
/>
<div className="flex items-center gap-2">
<input className="w-4 h-4 border-2 border-black text-black focus:ring-0" id="consent" type="checkbox" />
<input className="w-4 h-4 border-2 border-black text-black focus:ring-0" id="consent" name="consent" type="checkbox" required />
<label className="text-xs font-bold font-display text-black" htmlFor="consent">
Subscribe to Agent Park updates
{t('subscribeConsent')}
</label>
</div>
<button
className="bg-white text-black font-display font-bold py-3 px-8 border-2 border-black shadow-neo hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] transition-all"
type="submit"
>
SUBSCRIBE
{t('subscribe')}
</button>
</form>
</div>
@@ -144,26 +169,26 @@ export default async function LocaleLayout({
<span className="font-display font-bold text-lg tracking-tight">Agent Park</span>
</Link>
<p className="text-sm text-gray-600 dark:text-gray-400">
Discover and explore high-quality AI projects from across the web. Curated for developers and enthusiasts.
{t('footerDesc')}
</p>
</div>
<div>
<h4 className="font-display font-bold text-lg mb-4">Resources</h4>
<h4 className="font-display font-bold text-lg mb-4">{t('resources')}</h4>
<ul className="space-y-2 text-sm font-sans text-gray-600 dark:text-gray-400">
<li><Link className="hover:text-black dark:hover:text-white" href="#">Blog</Link></li>
<li><Link className="hover:text-black dark:hover:text-white" href="#">{tNav('blog')}</Link></li>
<li><Link className="hover:text-black dark:hover:text-white" href="#">Newsletter</Link></li>
<li><Link className="hover:text-black dark:hover:text-white" href="#">Documentation</Link></li>
</ul>
</div>
<div>
<h4 className="font-display font-bold text-lg mb-4">Legal</h4>
<h4 className="font-display font-bold text-lg mb-4">{t('legal')}</h4>
<ul className="space-y-2 text-sm font-sans text-gray-600 dark:text-gray-400">
<li><Link className="hover:text-black dark:hover:text-white" href="#">Privacy Policy</Link></li>
<li><Link className="hover:text-black dark:hover:text-white" href="#">Terms of Service</Link></li>
<li><Link className="hover:text-black dark:hover:text-white" href="#">{t('privacyPolicy')}</Link></li>
<li><Link className="hover:text-black dark:hover:text-white" href="#">{t('termsOfService')}</Link></li>
</ul>
</div>
<div>
<h4 className="font-display font-bold text-lg mb-4">Follow Us</h4>
<h4 className="font-display font-bold text-lg mb-4">{t('followUs')}</h4>
<div className="flex space-x-4">
<Link className="w-10 h-10 border-2 border-black dark:border-gray-500 flex items-center justify-center hover:bg-primary hover:text-black transition-colors font-display font-bold" href="#">X</Link>
<Link className="w-10 h-10 border-2 border-black dark:border-gray-500 flex items-center justify-center hover:bg-primary hover:text-black transition-colors font-display font-bold" href="#">Li</Link>
@@ -172,9 +197,9 @@ export default async function LocaleLayout({
</div>
</div>
<div className="border-t border-black dark:border-gray-700 pt-8 flex flex-col md:flex-row justify-between items-center">
<p className="text-sm text-gray-500 font-display">© 2025 Agent Park. All rights reserved.</p>
<p className="text-sm text-gray-500 font-display">{t('copyright')}</p>
<div className="mt-4 md:mt-0 font-display text-sm font-bold">
DESIGNED FOR AI BUILDERS
{t('designedFor')}
</div>
</div>
</div>
+52
View File
@@ -0,0 +1,52 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
interface AnnouncementBarProps {
text: string
href: string
locale: string
}
export function AnnouncementBar({ text, href, locale }: AnnouncementBarProps) {
const [isVisible, setIsVisible] = useState(true)
useEffect(() => {
// Check if user has closed the announcement before
const hasClosed = localStorage.getItem('announcement-closed')
if (hasClosed === 'true') {
setIsVisible(false)
}
}, [])
const handleClose = (e: React.MouseEvent) => {
e.preventDefault() // Prevent link navigation
e.stopPropagation()
setIsVisible(false)
localStorage.setItem('announcement-closed', 'true')
}
if (!isVisible) {
return null
}
return (
<Link
href={`/${locale}${href}`}
className="bg-primary w-full py-2 px-4 border-b-2 border-black dark:border-gray-600 flex justify-center items-center text-xs font-bold font-display tracking-wide text-black hover:opacity-90 transition-opacity relative"
>
<span>{text}</span>
<span className="material-icons text-sm align-middle ml-1">arrow_forward</span>
{/* Close button */}
<button
onClick={handleClose}
className="absolute right-4 hover:bg-black/10 rounded-full p-1 transition-colors"
aria-label="Close announcement"
>
<span className="material-icons text-sm">close</span>
</button>
</Link>
)
}
+31 -4
View File
@@ -3,15 +3,19 @@
"search": "Search",
"loading": "Loading...",
"noResults": "No results found",
"noProjects": "No projects yet",
"viewMore": "View More",
"backToHome": "Back to Home"
"backToHome": "Back to Home",
"viewDetails": "VIEW DETAILS"
},
"home": {
"title": "AI Project Navigator",
"subtitle": "Discover and explore AI projects",
"featuredProjects": "Featured Projects",
"browseByTag": "Browse by Tag",
"searchPlaceholder": "Search AI projects..."
"searchPlaceholder": "Search AI projects...",
"metaTitle": "Agent Park - AI Project Navigator",
"metaDescription": "Discover and explore quality AI projects from across the web"
},
"project": {
"details": "Project Details",
@@ -21,11 +25,34 @@
"website": "Website",
"github": "GitHub",
"huggingface": "HuggingFace",
"paper": "Paper"
"paper": "Paper",
"submitYourProject": "Submit Your Project",
"submitProjectTitle": "Submit Your Project",
"submitProjectDescription": "Have an AI tool worth sharing? Add it to Agent Park.",
"submitNow": "SUBMIT NOW"
},
"navigation": {
"home": "Home",
"projects": "Projects",
"about": "About"
"blog": "Blog",
"about": "About",
"submitProject": "SUBMIT PROJECT"
},
"layout": {
"announcement": "DISCOVER THE FUTURE OF AI AGENTS",
"announcementHref": "/projects",
"stayUpdated": "STAY UPDATED",
"stayUpdatedDesc": "Get the latest AI tools and trends delivered straight to your inbox. No spam, just intelligence.",
"email": "E-mail",
"subscribe": "SUBSCRIBE",
"subscribeConsent": "Subscribe to Agent Park updates",
"footerDesc": "Discover and explore high-quality AI projects from across the web. Curated for developers and enthusiasts.",
"resources": "Resources",
"legal": "Legal",
"privacyPolicy": "Privacy Policy",
"termsOfService": "Terms of Service",
"followUs": "Follow Us",
"copyright": "© 2025 Agent Park. All rights reserved.",
"designedFor": "DESIGNED FOR AI BUILDERS"
}
}
+31 -4
View File
@@ -3,15 +3,19 @@
"search": "搜索",
"loading": "加载中...",
"noResults": "未找到结果",
"noProjects": "暂无项目",
"viewMore": "查看更多",
"backToHome": "返回首页"
"backToHome": "返回首页",
"viewDetails": "查看详情"
},
"home": {
"title": "AI 项目导航",
"subtitle": "发现和探索全网优质 AI 项目",
"featuredProjects": "精选项目",
"browseByTag": "按标签浏览",
"searchPlaceholder": "搜索 AI 项目..."
"searchPlaceholder": "搜索 AI 项目...",
"metaTitle": "Agent Park - AI 项目导航",
"metaDescription": "发现和探索全网优质 AI 项目"
},
"project": {
"details": "项目详情",
@@ -21,11 +25,34 @@
"website": "官网",
"github": "GitHub",
"huggingface": "HuggingFace",
"paper": "论文"
"paper": "论文",
"submitYourProject": "提交你的项目",
"submitProjectTitle": "提交你的项目",
"submitProjectDescription": "有值得分享的 AI 工具吗?将其添加到 Agent Park。",
"submitNow": "立即提交"
},
"navigation": {
"home": "首页",
"projects": "项目列表",
"about": "关于"
"blog": "博客",
"about": "关于",
"submitProject": "提交项目"
},
"layout": {
"announcement": "发现 AI 代理的未来",
"announcementHref": "/projects",
"stayUpdated": "保持更新",
"stayUpdatedDesc": "获取最新的 AI 工具和趋势,直接发送到您的收件箱。没有垃圾邮件,只有智能。",
"email": "电子邮箱",
"subscribe": "订阅",
"subscribeConsent": "订阅 Agent Park 更新",
"footerDesc": "发现和探索来自全网的高质量 AI 项目。为开发者和爱好者精心策划。",
"resources": "资源",
"legal": "法律",
"privacyPolicy": "隐私政策",
"termsOfService": "服务条款",
"followUs": "关注我们",
"copyright": "© 2025 Agent Park. 保留所有权利。",
"designedFor": "专为 AI 构建者设计"
}
}