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:
+51
-26
@@ -1,7 +1,9 @@
|
|||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { setRequestLocale } from 'next-intl/server'
|
import { setRequestLocale, getTranslations } from 'next-intl/server'
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { LocaleSwitcher } from "@/components/locale/LocaleSwitcher"
|
import { LocaleSwitcher } from "@/components/locale/LocaleSwitcher"
|
||||||
|
import { AnnouncementBar } from "@/components/layout/AnnouncementBar"
|
||||||
|
import type { Metadata } from "next"
|
||||||
|
|
||||||
const locales = ['zh', 'en']
|
const locales = ['zh', 'en']
|
||||||
|
|
||||||
@@ -9,6 +11,20 @@ export async function generateStaticParams() {
|
|||||||
return locales.map((locale) => ({ locale }))
|
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({
|
export default async function LocaleLayout({
|
||||||
children,
|
children,
|
||||||
params
|
params
|
||||||
@@ -23,13 +39,15 @@ export default async function LocaleLayout({
|
|||||||
|
|
||||||
setRequestLocale(locale)
|
setRequestLocale(locale)
|
||||||
|
|
||||||
|
// Get translations
|
||||||
|
const t = await getTranslations('layout')
|
||||||
|
const tNav = await getTranslations('navigation')
|
||||||
|
const tHome = await getTranslations('home')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
{/* Top announcement bar */}
|
{/* 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">
|
<AnnouncementBar text={t('announcement')} href={t('announcementHref')} locale={locale} />
|
||||||
<span>DISCOVER THE FUTURE OF AI AGENTS</span>
|
|
||||||
<span className="material-icons text-sm align-middle ml-1">arrow_forward</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* 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">
|
<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"
|
className="font-display text-sm font-bold hover:text-secondary dark:hover:text-primary transition-colors"
|
||||||
href={`/${locale}`}
|
href={`/${locale}`}
|
||||||
>
|
>
|
||||||
HOME
|
{tNav('home')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
className="font-display text-sm font-bold hover:text-secondary dark:hover:text-primary transition-colors"
|
className="font-display text-sm font-bold hover:text-secondary dark:hover:text-primary transition-colors"
|
||||||
href={`/${locale}/projects`}
|
href={`/${locale}/projects`}
|
||||||
>
|
>
|
||||||
PROJECTS
|
{tNav('projects')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
className="font-display text-sm font-bold hover:text-secondary dark:hover:text-primary transition-colors"
|
className="font-display text-sm font-bold hover:text-secondary dark:hover:text-primary transition-colors"
|
||||||
href="#"
|
href="#"
|
||||||
>
|
>
|
||||||
BLOG
|
{tNav('blog')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
className="font-display text-sm font-bold hover:text-secondary dark:hover:text-primary transition-colors"
|
className="font-display text-sm font-bold hover:text-secondary dark:hover:text-primary transition-colors"
|
||||||
href="#"
|
href="#"
|
||||||
>
|
>
|
||||||
ABOUT
|
{tNav('about')}
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Right side */}
|
{/* Right side */}
|
||||||
<div className="hidden md:flex items-center space-x-4">
|
<div className="hidden md:flex items-center space-x-4">
|
||||||
<LocaleSwitcher currentLocale={locale} />
|
<LocaleSwitcher
|
||||||
|
currentLocale={locale}
|
||||||
|
switchUrl={`/${locale === 'zh' ? 'en' : 'zh'}`}
|
||||||
|
/>
|
||||||
<Link
|
<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"
|
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="#"
|
href="#"
|
||||||
>
|
>
|
||||||
SUBMIT PROJECT
|
{tNav('submitProject')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -98,27 +119,31 @@ export default async function LocaleLayout({
|
|||||||
<section className="border-t-2 border-black dark:border-gray-600">
|
<section className="border-t-2 border-black dark:border-gray-600">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2">
|
<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">
|
<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">
|
<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>
|
</p>
|
||||||
<form className="space-y-4 max-w-md">
|
<form className="space-y-4 max-w-md">
|
||||||
<input
|
<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"
|
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"
|
type="email"
|
||||||
|
id="newsletter-email"
|
||||||
|
name="email"
|
||||||
|
autoComplete="email"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
<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">
|
<label className="text-xs font-bold font-display text-black" htmlFor="consent">
|
||||||
Subscribe to Agent Park updates
|
{t('subscribeConsent')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<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"
|
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"
|
type="submit"
|
||||||
>
|
>
|
||||||
SUBSCRIBE
|
{t('subscribe')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,26 +169,26 @@ export default async function LocaleLayout({
|
|||||||
<span className="font-display font-bold text-lg tracking-tight">Agent Park</span>
|
<span className="font-display font-bold text-lg tracking-tight">Agent Park</span>
|
||||||
</Link>
|
</Link>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<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="#">Newsletter</Link></li>
|
||||||
<li><Link className="hover:text-black dark:hover:text-white" href="#">Documentation</Link></li>
|
<li><Link className="hover:text-black dark:hover:text-white" href="#">Documentation</Link></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<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="#">{t('privacyPolicy')}</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('termsOfService')}</Link></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<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="#">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>
|
<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>
|
</div>
|
||||||
<div className="border-t border-black dark:border-gray-700 pt-8 flex flex-col md:flex-row justify-between items-center">
|
<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">
|
<div className="mt-4 md:mt-0 font-display text-sm font-bold">
|
||||||
DESIGNED FOR AI BUILDERS
|
{t('designedFor')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
@@ -3,15 +3,19 @@
|
|||||||
"search": "Search",
|
"search": "Search",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"noResults": "No results found",
|
"noResults": "No results found",
|
||||||
|
"noProjects": "No projects yet",
|
||||||
"viewMore": "View More",
|
"viewMore": "View More",
|
||||||
"backToHome": "Back to Home"
|
"backToHome": "Back to Home",
|
||||||
|
"viewDetails": "VIEW DETAILS"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "AI Project Navigator",
|
"title": "AI Project Navigator",
|
||||||
"subtitle": "Discover and explore AI projects",
|
"subtitle": "Discover and explore AI projects",
|
||||||
"featuredProjects": "Featured Projects",
|
"featuredProjects": "Featured Projects",
|
||||||
"browseByTag": "Browse by Tag",
|
"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": {
|
"project": {
|
||||||
"details": "Project Details",
|
"details": "Project Details",
|
||||||
@@ -21,11 +25,34 @@
|
|||||||
"website": "Website",
|
"website": "Website",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"huggingface": "HuggingFace",
|
"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": {
|
"navigation": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"projects": "Projects",
|
"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
@@ -3,15 +3,19 @@
|
|||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"noResults": "未找到结果",
|
"noResults": "未找到结果",
|
||||||
|
"noProjects": "暂无项目",
|
||||||
"viewMore": "查看更多",
|
"viewMore": "查看更多",
|
||||||
"backToHome": "返回首页"
|
"backToHome": "返回首页",
|
||||||
|
"viewDetails": "查看详情"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "AI 项目导航",
|
"title": "AI 项目导航",
|
||||||
"subtitle": "发现和探索全网优质 AI 项目",
|
"subtitle": "发现和探索全网优质 AI 项目",
|
||||||
"featuredProjects": "精选项目",
|
"featuredProjects": "精选项目",
|
||||||
"browseByTag": "按标签浏览",
|
"browseByTag": "按标签浏览",
|
||||||
"searchPlaceholder": "搜索 AI 项目..."
|
"searchPlaceholder": "搜索 AI 项目...",
|
||||||
|
"metaTitle": "Agent Park - AI 项目导航",
|
||||||
|
"metaDescription": "发现和探索全网优质 AI 项目"
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
"details": "项目详情",
|
"details": "项目详情",
|
||||||
@@ -21,11 +25,34 @@
|
|||||||
"website": "官网",
|
"website": "官网",
|
||||||
"github": "GitHub",
|
"github": "GitHub",
|
||||||
"huggingface": "HuggingFace",
|
"huggingface": "HuggingFace",
|
||||||
"paper": "论文"
|
"paper": "论文",
|
||||||
|
"submitYourProject": "提交你的项目",
|
||||||
|
"submitProjectTitle": "提交你的项目",
|
||||||
|
"submitProjectDescription": "有值得分享的 AI 工具吗?将其添加到 Agent Park。",
|
||||||
|
"submitNow": "立即提交"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"home": "首页",
|
"home": "首页",
|
||||||
"projects": "项目列表",
|
"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 构建者设计"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user