first commit

This commit is contained in:
Zopt 2025-09-15 17:28:58 +08:00
parent 06b9d6afe6
commit a6f9257062
42 changed files with 13880 additions and 1 deletions

22
.env.example Normal file
View File

@ -0,0 +1,22 @@
# SEO Configuration - 站点地图和 SEO 配置
NEXT_PUBLIC_BASE_URL=https://your-domain.com
NEXT_PUBLIC_SITE_NAME=Eco Life
NEXT_PUBLIC_TWITTER_HANDLE=@ecolife
# Search Engine Verification - 搜索引擎验证
GOOGLE_SITE_VERIFICATION=your-google-verification-code
BING_SITE_VERIFICATION=your-bing-verification-code
YANDEX_SITE_VERIFICATION=your-yandex-verification-code
BAIDU_SITE_VERIFICATION=your-baidu-verification-code
# Analytics - 分析工具
GOOGLE_ANALYTICS_ID=GA-XXXXXXXXX
GOOGLE_TAG_MANAGER_ID=GTM-XXXXXXX
# Sitemap Configuration - 站点地图配置
SITEMAP_CACHE_DURATION=3600
SITEMAP_MAX_URLS=50000
SITEMAP_ENABLE_VALIDATION=true
# Development - 开发环境
NODE_ENV=production

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

39
.gitignore vendored Normal file
View File

@ -0,0 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# vscode
.vscode

11
.prettierrc Normal file
View File

@ -0,0 +1,11 @@
{
"singleQuote": true,
"printWidth": 100,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"jsxSingleQuote": false,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}

85
README.md Normal file
View File

@ -0,0 +1,85 @@
# Eco-Life - A Multilingual Static Next.js Project
<p align="center">
<img src="public/images/logo-text.png" width="300" />
</p>
This project is a statically exported, multilingual website built with [Next.js](https://nextjs.org/) (App Router), [Tailwind CSS](https://tailwindcss.com/), and [ShadCN/UI](https://ui.shadcn.com/). It serves as a showcase for "Eco-Life," a fictional environmental initiative.
## Features
- **Static Site Generation (SSG)**: The entire site is statically exported for optimal performance and security. Configured with `output: 'export'`.
- **Multilingual Support (i18n)**: Fully supports multiple languages through Next.js's App Router i18n capabilities.
- Supported Languages: English (en), Simplified Chinese (zh-CN), Traditional Chinese (zh-TW), Korean (ko), and Japanese (ja).
- **Automatic Language Detection**: The root of the site automatically detects the user's browser language and redirects to the appropriate language version (e.g., `/en`, `/zh-CN`).
- **Dynamic Content**: Includes a blog with dynamically generated static pages for each post and language.
- **SEO Optimized**: Automatically generates `sitemap.xml` and `robots.ts` for better search engine visibility.
- **Modern Tech Stack**: Built with the latest features of Next.js, React Server Components, and Client Components.
## Getting Started
### Prerequisites
- [Node.js](https://nodejs.org/) (v18 or later)
- A package manager like [npm](https://www.npmjs.com/), [yarn](https://yarnpkg.com/), or [bun](https://bun.sh/)
### Installation
1. Clone the repository:
```bash
git clone <repository-url>
```
2. Navigate to the project directory:
```bash
cd <project-directory>
```
3. Install the dependencies:
```bash
npm install
# or
yarn install
# or
bun install
```
### Running the Development Server
To view the project in development mode with hot-reloading:
```bash
npm run dev
# or
yarn dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) in your browser to see the result.
## Building for Production
To build the static site for production:
```bash
npm run build
# or
yarn build
# or
bun build
```
This command will generate a `build` directory containing all the static files for the website.
### Previewing the Static Site
After building, you can preview the local static site by running a simple HTTP server in the `build` directory. For example, using `serve`:
```bash
# Install serve if you don't have it
npm install -g serve
# Serve the build directory
serve build
```
This will start a server, and you can view your statically exported site at the provided local URL (e.g., `http://localhost:3000`).

View File

@ -0,0 +1,341 @@
'use client';
import { useEffect, useRef } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import Navigation from '../../../../components/Navigation';
import FloatingLanguageSwitcher from '../../../../components/FloatingLanguageSwitcher';
import Footer from '../../../../components/Footer';
import { Locale } from '../../../../lib/i18n';
import { getTranslations, Translations } from '../../../../lib/translations';
import { updateDocumentMeta } from '../../../../lib/seo-utils';
// Gentle Particle Background Component
function GentleParticleBackground() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Set canvas size
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Gentle particle system
const particles: Array<{
x: number;
y: number;
vx: number;
vy: number;
size: number;
opacity: number;
}> = [];
// Create particles
for (let i = 0; i < 60; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 0.2,
vy: (Math.random() - 0.5) * 0.2,
size: Math.random() * 2 + 1,
opacity: Math.random() * 0.3 + 0.1,
});
}
// Animation loop
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach((particle) => {
// Update position
particle.x += particle.vx;
particle.y += particle.vy;
// Wrap around edges
if (particle.x < 0) particle.x = canvas.width;
if (particle.x > canvas.width) particle.x = 0;
if (particle.y < 0) particle.y = canvas.height;
if (particle.y > canvas.height) particle.y = 0;
// Draw particle
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 255, 255, ${particle.opacity})`;
ctx.fill();
});
requestAnimationFrame(animate);
};
animate();
return () => {
window.removeEventListener('resize', resizeCanvas);
};
}, []);
return (
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full"
style={{ pointerEvents: 'none' }}
/>
);
}
type Post = Translations['blog']['posts']['featured']; // Assuming all posts have the same structure
export default function BlogPostClient({
locale,
post,
t,
}: {
locale: Locale;
post: Post | null;
t: Translations;
}) {
const router = useRouter();
// Update meta tags when locale changes
useEffect(() => {
updateDocumentMeta(locale);
}, [locale]);
if (!post) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold text-white mb-4">Post Not Found</h1>
<Link
href={`/${locale}/blog`}
className="text-green-400 hover:text-green-300 underline"
>
Back to Blog
</Link>
</div>
</div>
);
}
const getCategoryColor = (category: string) => {
switch (category) {
case 'playerStories':
return 'bg-amber-500';
case 'development':
return 'bg-green-500';
case 'ecoFacts':
return 'bg-emerald-500';
default:
return 'bg-blue-500';
}
};
const getCategoryName = (category: string) => {
switch (category) {
case 'playerStories':
return t.blog.categories.playerStories;
case 'development':
return t.blog.categories.development;
case 'ecoFacts':
return t.blog.categories.ecoFacts;
default:
return category;
}
};
return (
<div className="min-h-screen relative overflow-hidden">
{/* Gentle Background with Overlay */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `linear-gradient(rgba(30, 58, 138, 0.8), rgba(15, 23, 42, 0.9)), url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800"><defs><linearGradient id="gentle1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:%234F46E5;stop-opacity:1" /><stop offset="50%" style="stop-color:%237C3AED;stop-opacity:1" /><stop offset="100%" style="stop-color:%231E40AF;stop-opacity:1" /></linearGradient></defs><rect width="1200" height="800" fill="url(%23gentle1)"/><path d="M0,400 Q300,300 600,350 T1200,300 L1200,800 L0,800 Z" fill="%23312E81" opacity="0.5"/><path d="M0,500 Q400,400 800,450 T1200,400 L1200,800 L0,800 Z" fill="%231E3A8A" opacity="0.3"/></svg>')`,
}}
/>
{/* Gentle Particle Background */}
<GentleParticleBackground />
{/* Navigation */}
<Navigation locale={locale} />
{/* Floating Language Switcher */}
<FloatingLanguageSwitcher locale={locale} />
{/* Article Content */}
<article className="relative z-10 pt-32 pb-20 px-4">
<div className="max-w-4xl mx-auto">
{/* Back to Blog Link */}
<div className="mb-8">
<Link
href={`/${locale}/blog`}
className="inline-flex items-center text-white/80 hover:text-white transition-colors duration-200"
>
<svg
className="w-5 h-5 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
{t.blog.backToBlog}
</Link>
</div>
{/* Article Header */}
<header className="mb-12">
<div className="mb-6">
<span
className={`${getCategoryColor(post.category)} text-white px-4 py-2 rounded-full text-sm font-medium`}
>
{getCategoryName(post.category)}
</span>
</div>
<h1 className="text-4xl md:text-5xl font-bold text-white mb-6 leading-tight">
{post.title}
</h1>
<div className="flex flex-wrap items-center text-white/70 text-sm space-x-6 mb-6">
<div className="flex items-center space-x-2">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<span>{post.author}</span>
</div>
<div className="flex items-center space-x-2">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>{post.date}</span>
</div>
<div className="flex items-center space-x-2">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{post.readTime}</span>
</div>
</div>
<p className="text-xl text-white/90 leading-relaxed">{post.excerpt}</p>
</header>
{/* Article Body */}
<div className="bg-white/10 backdrop-blur-md rounded-3xl p-8 md:p-12 border border-white/20 shadow-2xl">
<div className="prose prose-lg prose-invert max-w-none">
{post.content.split('\n\n').map((paragraph, index) => (
<p key={index} className="text-white/90 leading-relaxed mb-6 text-lg">
{paragraph}
</p>
))}
</div>
</div>
{/* Article Footer */}
<footer className="mt-12 pt-8 border-t border-white/20">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center space-y-4 md:space-y-0">
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-gradient-to-br from-green-400 to-blue-500 rounded-full flex items-center justify-center">
<span className="text-white font-bold text-lg">
{post.author.charAt(0)}
</span>
</div>
<div>
<p className="text-white font-medium">{post.author}</p>
<p className="text-white/70 text-sm">
Environmental Writer
</p>
</div>
</div>
<div className="flex space-x-4">
<button className="bg-white/20 hover:bg-white/30 text-white px-6 py-2 rounded-full transition-colors duration-200">
Share
</button>
<button className="bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded-full transition-colors duration-200">
Like
</button>
</div>
</div>
</footer>
{/* Related Posts Section */}
<section className="mt-16">
<h3 className="text-2xl font-bold text-white mb-8">Related Posts</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Link href={`/${locale}/blog/post1`}>
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-6 border border-white/20 hover:bg-white/20 transition-all duration-200 cursor-pointer">
<h4 className="text-white font-semibold mb-2">
Player Spotlight: Building a Virtual Eco-City
</h4>
<p className="text-white/70 text-sm">
Meet Alex, a player who created an incredible sustainable
city...
</p>
</div>
</Link>
<Link href={`/${locale}/blog/post2`}>
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-6 border border-white/20 hover:bg-white/20 transition-all duration-200 cursor-pointer">
<h4 className="text-white font-semibold mb-2">
10 Amazing Facts About Ocean Conservation
</h4>
<p className="text-white/70 text-sm">
Dive into fascinating facts about our oceans...
</p>
</div>
</Link>
</div>
</section>
</div>
</article>
{/* Footer */}
<Footer locale={locale} />
</div>
);
}

View File

@ -0,0 +1,36 @@
import { locales } from '../../../../lib/i18n';
import { getBlogPostSlugs } from '../../../../lib/sitemap-generator';
import { Locale } from '../../../../lib/i18n';
import { getTranslations } from '../../../../lib/translations';
import BlogPostClient from './BlogPostClient'; // Assuming the client component is in a separate file
export async function generateStaticParams() {
const slugs = getBlogPostSlugs();
const params = locales.flatMap((locale) =>
slugs.map((slug) => ({
locale,
slug,
})),
);
return params;
}
export default function BlogPostPage({ params }: { params: { locale: Locale; slug: string } }) {
const t = getTranslations(params.locale);
const getPostData = (slug: string) => {
const posts = {
featured: t.blog.posts.featured,
post1: t.blog.posts.post1,
post2: t.blog.posts.post2,
post3: t.blog.posts.post3,
post4: t.blog.posts.post4,
post5: t.blog.posts.post5,
};
return posts[slug as keyof typeof posts] || null;
};
const post = getPostData(params.slug);
return <BlogPostClient locale={params.locale} post={post} t={t} />;
}

358
app/[locale]/blog/page.tsx Normal file
View File

@ -0,0 +1,358 @@
import { Metadata } from 'next';
import Link from 'next/link';
import { Locale } from '../../../lib/i18n';
import { getTranslations } from '../../../lib/translations';
import { generateCanonicalUrl, generateAlternateLinks } from '../../../lib/seo-utils';
interface BlogPageProps {
params: {
locale: Locale;
};
}
export async function generateMetadata({ params }: BlogPageProps): Promise<Metadata> {
const t = getTranslations(params.locale);
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://your-domain.com';
const canonicalUrl = generateCanonicalUrl('/blog', params.locale, baseUrl);
const alternateLinks = generateAlternateLinks('/blog', baseUrl);
return {
title: t.blog.title,
description: t.blog.subtitle,
alternates: {
canonical: canonicalUrl,
languages: Object.fromEntries(alternateLinks.map((link) => [link.hrefLang, link.href])),
},
openGraph: {
title: t.blog.title,
description: t.blog.subtitle,
url: canonicalUrl,
type: 'website',
},
};
}
export default function BlogPage({ params }: BlogPageProps) {
const t = getTranslations(params.locale);
// 获取所有博客文章
const blogPosts = [
{
id: 'featured',
...t.blog.posts.featured,
image: 'featured',
featured: true,
},
{
id: 'post1',
...t.blog.posts.post1,
image: 'post1',
featured: false,
},
{
id: 'post2',
...t.blog.posts.post2,
image: 'post2',
featured: false,
},
{
id: 'post3',
...t.blog.posts.post3,
image: 'post3',
featured: false,
},
{
id: 'post4',
...t.blog.posts.post4,
image: 'post4',
featured: false,
},
{
id: 'post5',
...t.blog.posts.post5,
image: 'post5',
featured: false,
},
];
const getCategoryColor = (category: string) => {
switch (category) {
case 'playerStories':
return 'bg-amber-500';
case 'development':
return 'bg-green-500';
case 'ecoFacts':
return 'bg-emerald-500';
default:
return 'bg-blue-500';
}
};
const getCategoryName = (category: string) => {
switch (category) {
case 'playerStories':
return t.blog.categories.playerStories;
case 'development':
return t.blog.categories.development;
case 'ecoFacts':
return t.blog.categories.ecoFacts;
default:
return category;
}
};
const getImageGradient = (imageId: string) => {
const gradients = {
featured: 'from-purple-500 via-blue-500 to-indigo-600',
post1: 'from-amber-500 via-orange-500 to-red-500',
post2: 'from-cyan-500 via-blue-500 to-indigo-500',
post3: 'from-green-500 via-emerald-500 to-teal-500',
post4: 'from-pink-500 via-rose-500 to-red-500',
post5: 'from-violet-500 via-purple-500 to-indigo-500',
};
return gradients[imageId as keyof typeof gradients] || 'from-gray-500 to-gray-600';
};
const featuredPost = blogPosts.find((post) => post.featured);
const regularPosts = blogPosts.filter((post) => !post.featured);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-black">
{/* Hero Section */}
<section className="relative py-20 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-16">
<h1 className="text-5xl md:text-6xl font-bold text-white mb-6">
{t.blog.title}
</h1>
<p className="text-xl text-white/80 max-w-3xl mx-auto leading-relaxed">
{t.blog.subtitle}
</p>
</div>
{/* Category Filter */}
<div className="flex flex-wrap justify-center gap-4 mb-12">
<button className="bg-white/10 backdrop-blur-md text-white px-6 py-3 rounded-full border border-white/20 hover:bg-white/20 transition-all duration-200">
{t.blog.categories.all}
</button>
<button className="bg-amber-500/20 backdrop-blur-md text-amber-300 px-6 py-3 rounded-full border border-amber-500/30 hover:bg-amber-500/30 transition-all duration-200">
{t.blog.categories.playerStories}
</button>
<button className="bg-green-500/20 backdrop-blur-md text-green-300 px-6 py-3 rounded-full border border-green-500/30 hover:bg-green-500/30 transition-all duration-200">
{t.blog.categories.development}
</button>
<button className="bg-emerald-500/20 backdrop-blur-md text-emerald-300 px-6 py-3 rounded-full border border-emerald-500/30 hover:bg-emerald-500/30 transition-all duration-200">
{t.blog.categories.ecoFacts}
</button>
</div>
</div>
</section>
{/* Featured Post */}
{featuredPost && (
<section className="px-4 sm:px-6 lg:px-8 mb-20">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-white mb-4">
{t.blog.featured}
</h2>
</div>
<Link href={`/${params.locale}/blog/${featuredPost.id}`}>
<article className="group cursor-pointer">
<div className="bg-white/10 backdrop-blur-md rounded-3xl overflow-hidden border border-white/20 shadow-2xl hover:shadow-3xl transition-all duration-500 transform hover:scale-[1.02]">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0">
{/* Featured Post Image */}
<div className="relative h-80 lg:h-96 overflow-hidden">
<div
className={`absolute inset-0 bg-gradient-to-br ${getImageGradient(featuredPost.image)} opacity-90`}
>
<div className="absolute inset-0 bg-black/20"></div>
{/* Animated particles */}
<div className="absolute inset-0">
{[...Array(8)].map((_, i) => (
<div
key={i}
className="absolute w-2 h-2 bg-white/30 rounded-full animate-pulse group-hover:animate-bounce"
style={{
left: `${20 + i * 10}%`,
top: `${30 + (i % 3) * 20}%`,
animationDelay: `${i * 0.2}s`,
}}
/>
))}
</div>
</div>
<div className="absolute top-6 left-6">
<span
className={`${getCategoryColor(featuredPost.category)} text-white px-4 py-2 rounded-full text-sm font-medium shadow-lg`}
>
{getCategoryName(featuredPost.category)}
</span>
</div>
<div className="absolute top-6 right-6">
<div className="bg-white/20 backdrop-blur-sm rounded-full px-3 py-1">
<span className="text-white text-sm font-medium">
{t.blog.featured}
</span>
</div>
</div>
</div>
{/* Featured Post Content */}
<div className="p-8 lg:p-12 flex flex-col justify-center">
<div className="text-white/70 text-sm mb-4">
<div className="flex items-center space-x-4">
<span>{featuredPost.author}</span>
<span></span>
<span>{featuredPost.date}</span>
<span></span>
<span>{featuredPost.readTime}</span>
</div>
</div>
<h3 className="text-3xl font-bold text-white mb-6 group-hover:text-green-300 transition-colors duration-200 leading-tight">
{featuredPost.title}
</h3>
<p className="text-white/80 mb-8 leading-relaxed text-lg">
{featuredPost.excerpt}
</p>
<div className="flex items-center">
<span className="text-green-300 font-medium group-hover:text-green-200 transition-colors duration-200 flex items-center">
{t.blog.readMore}
<svg
className="w-5 h-5 ml-2 transform group-hover:translate-x-1 transition-transform duration-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
</article>
</Link>
</div>
</section>
)}
{/* Regular Posts Grid */}
<section className="px-4 sm:px-6 lg:px-8 pb-20">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-white mb-4">
{t.blog.categories.all}
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{regularPosts.map((post) => (
<Link key={post.id} href={`/${params.locale}/blog/${post.id}`}>
<article className="group cursor-pointer h-full">
<div className="bg-white/10 backdrop-blur-md rounded-2xl overflow-hidden border border-white/20 shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:scale-[1.02] h-full flex flex-col">
{/* Post Image */}
<div className="relative h-48 overflow-hidden">
<div
className={`absolute inset-0 bg-gradient-to-br ${getImageGradient(post.image)} opacity-90`}
>
<div className="absolute inset-0 bg-black/20"></div>
{/* Mini particles */}
<div className="absolute inset-0">
{[...Array(4)].map((_, i) => (
<div
key={i}
className="absolute w-1 h-1 bg-white/40 rounded-full animate-pulse group-hover:animate-ping"
style={{
left: `${30 + i * 15}%`,
top: `${40 + i * 10}%`,
animationDelay: `${i * 0.3}s`,
}}
/>
))}
</div>
</div>
<div className="absolute top-4 left-4">
<span
className={`${getCategoryColor(post.category)} text-white px-3 py-1 rounded-full text-xs font-medium`}
>
{getCategoryName(post.category)}
</span>
</div>
</div>
{/* Post Content */}
<div className="p-6 flex-1 flex flex-col">
<div className="text-white/60 text-sm mb-3">
<div className="flex items-center space-x-3">
<span>{post.author}</span>
<span></span>
<span>{post.readTime}</span>
</div>
</div>
<h3 className="text-xl font-bold text-white mb-3 group-hover:text-green-300 transition-colors duration-200 line-clamp-2 flex-shrink-0">
{post.title}
</h3>
<p className="text-white/70 mb-4 line-clamp-3 flex-1">
{post.excerpt}
</p>
<div className="flex items-center justify-between mt-auto">
<span className="text-white/50 text-sm">
{post.date}
</span>
<span className="text-green-300 font-medium group-hover:text-green-200 transition-colors duration-200 flex items-center">
{t.blog.readMore}
<svg
className="w-4 h-4 ml-1 transform group-hover:translate-x-1 transition-transform duration-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</span>
</div>
</div>
</div>
</article>
</Link>
))}
</div>
</div>
</section>
{/* Newsletter Subscription */}
<section className="px-4 sm:px-6 lg:px-8 pb-20">
<div className="max-w-4xl mx-auto">
<div className="bg-gradient-to-r from-green-500/20 to-emerald-500/20 backdrop-blur-md rounded-3xl p-8 lg:p-12 border border-green-500/30 text-center">
<h2 className="text-3xl font-bold text-white mb-4">{t.blog.stayUpdated}</h2>
<p className="text-white/80 mb-8 text-lg max-w-2xl mx-auto">
{t.blog.stayUpdatedDesc}
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center max-w-md mx-auto">
<input
type="email"
placeholder="your@email.com"
className="flex-1 px-6 py-3 rounded-full bg-white/10 backdrop-blur-md border border-white/20 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
<button className="bg-green-500 hover:bg-green-600 text-white px-8 py-3 rounded-full font-semibold transition-all duration-200 transform hover:scale-105 whitespace-nowrap">
{t.blog.subscribe}
</button>
</div>
</div>
</div>
</section>
</div>
);
}

View File

@ -0,0 +1,513 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import Navigation from '../../../components/Navigation';
import FloatingLanguageSwitcher from '../../../components/FloatingLanguageSwitcher';
import Footer from '../../../components/Footer';
import { Locale } from '../../../lib/i18n';
import { getTranslations } from '../../../lib/translations';
import { updateDocumentMeta } from '../../../lib/seo-utils';
// Arctic Particle Background Component
function ArcticParticleBackground() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Set canvas size
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Particle system for arctic theme
const particles: Array<{
x: number;
y: number;
vx: number;
vy: number;
size: number;
opacity: number;
type: 'snow' | 'ice' | 'aurora';
}> = [];
// Create particles
for (let i = 0; i < 150; i++) {
const type = Math.random() < 0.7 ? 'snow' : Math.random() < 0.9 ? 'ice' : 'aurora';
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * (type === 'snow' ? 0.3 : 0.1),
vy: type === 'snow' ? Math.random() * 0.5 + 0.2 : (Math.random() - 0.5) * 0.1,
size:
type === 'snow'
? Math.random() * 3 + 1
: type === 'ice'
? Math.random() * 2 + 0.5
: Math.random() * 4 + 2,
opacity: type === 'aurora' ? Math.random() * 0.3 + 0.1 : Math.random() * 0.6 + 0.2,
type,
});
}
// Animation loop
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach((particle) => {
// Update position
particle.x += particle.vx;
particle.y += particle.vy;
// Wrap around edges
if (particle.x < -10) particle.x = canvas.width + 10;
if (particle.x > canvas.width + 10) particle.x = -10;
if (particle.y < -10) particle.y = canvas.height + 10;
if (particle.y > canvas.height + 10) particle.y = -10;
// Draw particle based on type
ctx.beginPath();
if (particle.type === 'snow') {
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 255, 255, ${particle.opacity})`;
ctx.fill();
} else if (particle.type === 'ice') {
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(173, 216, 230, ${particle.opacity})`;
ctx.fill();
} else if (particle.type === 'aurora') {
const gradient = ctx.createRadialGradient(
particle.x,
particle.y,
0,
particle.x,
particle.y,
particle.size * 3,
);
gradient.addColorStop(0, `rgba(0, 255, 150, ${particle.opacity})`);
gradient.addColorStop(0.5, `rgba(0, 150, 255, ${particle.opacity * 0.5})`);
gradient.addColorStop(1, `rgba(150, 0, 255, 0)`);
ctx.arc(particle.x, particle.y, particle.size * 3, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
}
});
// Draw connections between ice particles
particles
.filter((p) => p.type === 'ice')
.forEach((particle, i, iceParticles) => {
iceParticles.slice(i + 1).forEach((otherParticle) => {
const dx = particle.x - otherParticle.x;
const dy = particle.y - otherParticle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 80) {
ctx.beginPath();
ctx.moveTo(particle.x, particle.y);
ctx.lineTo(otherParticle.x, otherParticle.y);
ctx.strokeStyle = `rgba(173, 216, 230, ${0.1 * (1 - distance / 80)})`;
ctx.lineWidth = 1;
ctx.stroke();
}
});
});
requestAnimationFrame(animate);
};
animate();
return () => {
window.removeEventListener('resize', resizeCanvas);
};
}, []);
return (
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full"
style={{ pointerEvents: 'none' }}
data-oid="vdts.76"
/>
);
}
export default function ContactPage({ params }: { params: { locale: Locale } }) {
const t = getTranslations(params.locale);
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
phone: '',
message: '',
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
// Update meta tags when locale changes
useEffect(() => {
updateDocumentMeta(params.locale);
}, [params.locale]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Simulate form submission
try {
await new Promise((resolve) => setTimeout(resolve, 2000));
setSubmitStatus('success');
setFormData({
firstName: '',
lastName: '',
email: '',
phone: '',
message: '',
});
} catch (error) {
setSubmitStatus('error');
} finally {
setIsSubmitting(false);
setTimeout(() => setSubmitStatus('idle'), 5000);
}
};
return (
<div className="min-h-screen relative overflow-hidden" data-oid="cxtwh67">
{/* Arctic Background with Overlay */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `linear-gradient(rgba(0, 50, 100, 0.7), rgba(0, 30, 60, 0.8)), url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800"><defs><linearGradient id="arctic1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:%2387CEEB;stop-opacity:1" /><stop offset="50%" style="stop-color:%234682B4;stop-opacity:1" /><stop offset="100%" style="stop-color:%23191970;stop-opacity:1" /></linearGradient></defs><rect width="1200" height="800" fill="url(%23arctic1)"/><path d="M0,600 Q300,500 600,550 T1200,500 L1200,800 L0,800 Z" fill="%23F0F8FF"/><path d="M0,650 Q400,580 800,620 T1200,600 L1200,800 L0,800 Z" fill="%23E6F3FF"/><ellipse cx="200" cy="200" rx="60" ry="30" fill="%23FFFFFF" opacity="0.8"/><ellipse cx="800" cy="150" rx="80" ry="40" fill="%23FFFFFF" opacity="0.6"/><ellipse cx="1000" cy="250" rx="50" ry="25" fill="%23FFFFFF" opacity="0.7"/></svg>')`,
}}
data-oid="g8y9fqe"
/>
{/* Arctic Particle Background */}
<ArcticParticleBackground data-oid="cn3_8yw" />
{/* Navigation */}
<Navigation locale={params.locale} data-oid="88otgww" />
{/* Floating Language Switcher */}
<FloatingLanguageSwitcher locale={params.locale} data-oid="lzqtg48" />
{/* Contact Section */}
<section className="relative z-10 py-20 px-4" data-oid="wlyxybl">
<div className="max-w-6xl mx-auto" data-oid="8rk2.0s">
{/* Header */}
<div className="text-center mb-16" data-oid="q1kwgw8">
<h1
className="text-5xl md:text-6xl font-bold text-white mb-6 leading-tight"
data-oid="ek48zik"
>
{t.contact.title}
</h1>
<p
className="text-xl md:text-2xl text-white/90 max-w-3xl mx-auto leading-relaxed"
data-oid="1k-t_ie"
>
{t.contact.subtitle}
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12" data-oid="_xqiofz">
{/* Contact Form */}
<div
className="bg-white/10 backdrop-blur-md rounded-3xl p-8 border border-white/20 shadow-2xl"
data-oid="pkudnst"
>
<form onSubmit={handleSubmit} className="space-y-6" data-oid="dcdmf-y">
{/* Name Fields */}
<div
className="grid grid-cols-1 md:grid-cols-2 gap-4"
data-oid="p34ti:p"
>
<div data-oid="u_cyg_s">
<input
type="text"
name="firstName"
value={formData.firstName}
onChange={handleInputChange}
placeholder={t.contact.form.firstName}
required
className="w-full px-4 py-3 rounded-xl bg-white/20 border border-white/30 text-white placeholder-white/70 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200"
data-oid="-mrpkhk"
/>
</div>
<div data-oid="q:lwf4n">
<input
type="text"
name="lastName"
value={formData.lastName}
onChange={handleInputChange}
placeholder={t.contact.form.lastName}
required
className="w-full px-4 py-3 rounded-xl bg-white/20 border border-white/30 text-white placeholder-white/70 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200"
data-oid="s6mgi:f"
/>
</div>
</div>
{/* Email and Phone */}
<div
className="grid grid-cols-1 md:grid-cols-2 gap-4"
data-oid="qlzghax"
>
<div data-oid="9eu.6rr">
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder={t.contact.form.email}
required
className="w-full px-4 py-3 rounded-xl bg-white/20 border border-white/30 text-white placeholder-white/70 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200"
data-oid="0fgza5i"
/>
</div>
<div data-oid="d8--iet">
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleInputChange}
placeholder={t.contact.form.phone}
className="w-full px-4 py-3 rounded-xl bg-white/20 border border-white/30 text-white placeholder-white/70 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200"
data-oid="wwwvzhx"
/>
</div>
</div>
{/* Message */}
<div data-oid="59jnejx">
<textarea
name="message"
value={formData.message}
onChange={handleInputChange}
placeholder={t.contact.form.message}
required
rows={6}
className="w-full px-4 py-3 rounded-xl bg-white/20 border border-white/30 text-white placeholder-white/70 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition-all duration-200 resize-none"
data-oid="u:fwgn0"
/>
</div>
{/* Submit Button */}
<div data-oid="rh_o0-0">
<button
type="submit"
disabled={isSubmitting}
className={`w-full py-4 px-8 rounded-xl font-semibold text-lg transition-all duration-200 transform hover:scale-105 ${
isSubmitting
? 'bg-gray-500 cursor-not-allowed'
: submitStatus === 'success'
? 'bg-green-500 hover:bg-green-600'
: submitStatus === 'error'
? 'bg-red-500 hover:bg-red-600'
: 'bg-blue-500 hover:bg-blue-600'
} text-white shadow-lg`}
data-oid="l.p.45q"
>
{isSubmitting
? 'Sending...'
: submitStatus === 'success'
? 'Message Sent!'
: submitStatus === 'error'
? 'Try Again'
: t.contact.form.send}
</button>
</div>
</form>
</div>
{/* Contact Information */}
<div className="space-y-8" data-oid="gcnlszq">
<div
className="bg-white/10 backdrop-blur-md rounded-3xl p-8 border border-white/20 shadow-2xl"
data-oid="dk_e_44"
>
<h3
className="text-2xl font-bold text-white mb-6"
data-oid="p5r03zc"
>
{t.contact.info.title}
</h3>
<p
className="text-white/90 mb-8 leading-relaxed"
data-oid="b3:lk4-"
>
{t.contact.info.description}
</p>
<div className="space-y-6" data-oid="hl4u4je">
{/* Email */}
<div className="flex items-center space-x-4" data-oid="g4na330">
<div
className="w-12 h-12 bg-blue-500/20 rounded-full flex items-center justify-center"
data-oid="kl7x59e"
>
<svg
className="w-6 h-6 text-blue-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="runsvm_"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
data-oid="ccc37jv"
/>
</svg>
</div>
<div data-oid="9kfzma8">
<p className="text-white/70 text-sm" data-oid="vk.z7te">
Email
</p>
<p
className="text-white font-medium"
data-oid="mk6ei1x"
>
{t.contact.info.email}
</p>
</div>
</div>
{/* Phone */}
<div className="flex items-center space-x-4" data-oid=".1a0g8l">
<div
className="w-12 h-12 bg-green-500/20 rounded-full flex items-center justify-center"
data-oid="4m5q0ec"
>
<svg
className="w-6 h-6 text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="8amai42"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
data-oid="7oc0h_5"
/>
</svg>
</div>
<div data-oid="knv5:6o">
<p className="text-white/70 text-sm" data-oid=".:vq7ax">
Phone
</p>
<p
className="text-white font-medium"
data-oid="3wxvnf4"
>
{t.contact.info.phone}
</p>
</div>
</div>
{/* Address */}
<div className="flex items-center space-x-4" data-oid="0is872f">
<div
className="w-12 h-12 bg-purple-500/20 rounded-full flex items-center justify-center"
data-oid="s:hqv83"
>
<svg
className="w-6 h-6 text-purple-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="svaynoq"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
data-oid="i1luz2w"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
data-oid="c0qo67."
/>
</svg>
</div>
<div data-oid="nf5h5sm">
<p className="text-white/70 text-sm" data-oid="uramiy4">
Address
</p>
<p
className="text-white font-medium"
data-oid="k56c61v"
>
{t.contact.info.address}
</p>
</div>
</div>
</div>
</div>
{/* Additional Info Card */}
<div
className="bg-gradient-to-br from-blue-500/20 to-purple-500/20 backdrop-blur-md rounded-3xl p-8 border border-white/20 shadow-2xl"
data-oid="_.1cxld"
>
<h4
className="text-xl font-bold text-white mb-4"
data-oid="m83e9xn"
>
Response Time
</h4>
<p className="text-white/90 mb-4" data-oid="tjr_3e3">
We typically respond within 24 hours during business days.
</p>
<div className="flex items-center space-x-2" data-oid="ooel29n">
<div
className="w-3 h-3 bg-green-400 rounded-full animate-pulse"
data-oid="5l9ximw"
></div>
<span
className="text-green-400 text-sm font-medium"
data-oid="l6d:_3y"
>
Online Now
</span>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Footer */}
<Footer locale={params.locale} data-oid="t66xypj" />
</div>
);
}

109
app/[locale]/layout.tsx Normal file
View File

@ -0,0 +1,109 @@
import type { Metadata } from 'next';
import '../globals.css';
import { locales, Locale, defaultLocale } from '../../lib/i18n';
import { getTranslations } from '../../lib/translations';
import {
generateAlternateLinks,
generateCanonicalUrl,
generateStructuredData,
} from '../../lib/seo-utils';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export async function generateMetadata({
params,
}: {
params: { locale: Locale };
}): Promise<Metadata> {
const translations = getTranslations(params.locale);
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://your-domain.com';
const alternateLinks = generateAlternateLinks('', baseUrl);
const canonicalUrl = generateCanonicalUrl('', params.locale, baseUrl);
return {
title: {
default: translations.meta.title,
template: `%s | ${translations.meta.title}`,
},
description: translations.meta.description,
keywords: translations.meta.keywords,
authors: [{ name: translations.meta.author }],
creator: translations.meta.author,
publisher: translations.meta.author,
formatDetection: {
email: false,
address: false,
telephone: false,
},
metadataBase: new URL(baseUrl),
alternates: {
canonical: canonicalUrl,
languages: Object.fromEntries(alternateLinks.map((link) => [link.hrefLang, link.href])),
},
openGraph: {
title: translations.meta.title,
description: translations.meta.description,
url: canonicalUrl,
siteName: translations.meta.title,
locale: params.locale,
alternateLocale: locales.filter((loc) => loc !== params.locale),
type: 'website',
images: [
{
url: `${baseUrl}/og-image.jpg`,
width: 1200,
height: 630,
alt: translations.meta.title,
},
],
},
twitter: {
card: 'summary_large_image',
title: translations.meta.title,
description: translations.meta.description,
images: [`${baseUrl}/og-image.jpg`],
creator: '@yourtwitterhandle',
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
verification: {
google: 'your-google-verification-code',
// Add other search engine verification codes as needed
},
};
}
export default function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: { locale: Locale };
}) {
// Validate that the incoming `locale` parameter is valid
if (!locales.includes(params.locale)) {
notFound();
}
return (
<html lang={params.locale} data-oid="9-vbv6i">
<body className="" data-oid="2snbkeg">
{children}
</body>
</html>
);
}

210
app/[locale]/page.tsx Normal file
View File

@ -0,0 +1,210 @@
'use client';
import { useEffect, useRef } from 'react';
import Navigation from '../../components/Navigation';
import FloatingLanguageSwitcher from '../../components/FloatingLanguageSwitcher';
import BlogShowcase from '../../components/BlogShowcase';
import Footer from '../../components/Footer';
import { Locale } from '../../lib/i18n';
import { getTranslations } from '../../lib/translations';
import { updateDocumentMeta } from '../../lib/seo-utils';
// Particle Background Component
function ParticleBackground() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Set canvas size
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Particle system
const particles: Array<{
x: number;
y: number;
vx: number;
vy: number;
size: number;
opacity: number;
}> = [];
// Create particles
for (let i = 0; i < 100; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
size: Math.random() * 3 + 1,
opacity: Math.random() * 0.5 + 0.2,
});
}
// Animation loop
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach((particle) => {
// Update position
particle.x += particle.vx;
particle.y += particle.vy;
// Wrap around edges
if (particle.x < 0) particle.x = canvas.width;
if (particle.x > canvas.width) particle.x = 0;
if (particle.y < 0) particle.y = canvas.height;
if (particle.y > canvas.height) particle.y = 0;
// Draw particle
ctx.beginPath();
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 255, 255, ${particle.opacity})`;
ctx.fill();
});
// Draw connections
particles.forEach((particle, i) => {
particles.slice(i + 1).forEach((otherParticle) => {
const dx = particle.x - otherParticle.x;
const dy = particle.y - otherParticle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
ctx.beginPath();
ctx.moveTo(particle.x, particle.y);
ctx.lineTo(otherParticle.x, otherParticle.y);
ctx.strokeStyle = `rgba(255, 255, 255, ${0.1 * (1 - distance / 100)})`;
ctx.stroke();
}
});
});
requestAnimationFrame(animate);
};
animate();
return () => {
window.removeEventListener('resize', resizeCanvas);
};
}, []);
return (
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full"
style={{ pointerEvents: 'none' }}
data-oid="iiiegs:"
/>
);
}
export default function Page({ params }: { params: { locale: Locale } }) {
const t = getTranslations(params.locale);
// Update meta tags when locale changes
useEffect(() => {
updateDocumentMeta(params.locale);
}, [params.locale]);
return (
<div className="min-h-screen relative overflow-hidden" data-oid="krtdxh9">
{/* Background Image with Overlay */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6)), url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800"><defs><linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:%23d4a574;stop-opacity:1" /><stop offset="50%" style="stop-color:%23b8956a;stop-opacity:1" /><stop offset="100%" style="stop-color:%23a08660;stop-opacity:1" /></linearGradient></defs><rect width="1200" height="800" fill="url(%23grad1)"/><path d="M0,400 Q300,200 600,350 T1200,300 L1200,800 L0,800 Z" fill="%23967d56"/><path d="M0,500 Q400,300 800,450 T1200,400 L1200,800 L0,800 Z" fill="%238a7350"/><circle cx="200" cy="150" r="80" fill="%23f4e4c1" opacity="0.3"/><circle cx="1000" cy="200" r="60" fill="%23f4e4c1" opacity="0.2"/></svg>')`,
}}
data-oid="t35j3uz"
/>
{/* Particle Background */}
<ParticleBackground data-oid="1eq47iz" />
{/* Navigation */}
<Navigation locale={params.locale} data-oid=".-cxh3." />
{/* Floating Language Switcher */}
<FloatingLanguageSwitcher locale={params.locale} data-oid="usi6f7v" />
{/* Hero Section */}
<section
id="hero"
className="relative z-10 flex items-center justify-center min-h-screen text-center px-4"
data-oid="2n7qgj-"
>
<div className="max-w-4xl mx-auto" data-oid="qdxxu5.">
<h1
className="text-5xl md:text-7xl font-bold text-white mb-8 leading-tight"
data-oid="pf:lgs4"
>
{t.hero.title}
</h1>
<p
className="text-xl md:text-2xl text-white/90 mb-12 max-w-3xl mx-auto leading-relaxed"
data-oid="vm1tz7h"
>
{t.hero.subtitle}
</p>
<div
className="flex flex-col sm:flex-row gap-6 justify-center"
data-oid="mbx0eri"
>
<button
className="bg-green-500 hover:bg-green-600 text-white px-8 py-4 rounded-full font-semibold text-lg transform hover:scale-105 transition-all duration-200 shadow-lg"
data-oid="e2cv0mq"
>
{t.hero.startJourney}
</button>
<button
className="border-2 border-white text-white hover:bg-white hover:text-gray-900 px-8 py-4 rounded-full font-semibold text-lg transform hover:scale-105 transition-all duration-200"
data-oid="sv:-vjb"
>
{t.hero.learnMore}
</button>
</div>
</div>
</section>
{/* Blog Showcase Section */}
<BlogShowcase locale={params.locale} data-oid=":qdpiz:" />
{/* Floating Action Elements */}
<div className="fixed bottom-8 left-8 z-50" data-oid="f2x6cuz">
<button
className="bg-green-500 hover:bg-green-600 text-white p-4 rounded-full shadow-lg transform hover:scale-110 transition-all duration-200"
data-oid="jye6ey7"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="ggzwwl-"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 10l7-7m0 0l7 7m-7-7v18"
data-oid="cngsmmd"
/>
</svg>
</button>
</div>
{/* Footer */}
<Footer locale={params.locale} data-oid="_rnutw_" />
</div>
);
}

View File

@ -0,0 +1,700 @@
'use client';
import { useEffect, useRef } from 'react';
import Navigation from '../../../components/Navigation';
import FloatingLanguageSwitcher from '../../../components/FloatingLanguageSwitcher';
import Footer from '../../../components/Footer';
import { Locale } from '../../../lib/i18n';
import { getTranslations } from '../../../lib/translations';
import { updateDocumentMeta } from '../../../lib/seo-utils';
// Enhanced Particle Background Component for Products
function ProductParticleBackground() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Set canvas size
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Enhanced particle system with different types
const particles: Array<{
x: number;
y: number;
vx: number;
vy: number;
size: number;
opacity: number;
type: 'circle' | 'square' | 'triangle';
color: string;
rotation: number;
rotationSpeed: number;
}> = [];
const colors = ['#22c55e', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6'];
// Create particles
for (let i = 0; i < 80; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 0.8,
vy: (Math.random() - 0.5) * 0.8,
size: Math.random() * 4 + 2,
opacity: Math.random() * 0.6 + 0.2,
type: ['circle', 'square', 'triangle'][Math.floor(Math.random() * 3)] as
| 'circle'
| 'square'
| 'triangle',
color: colors[Math.floor(Math.random() * colors.length)],
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * 0.02,
});
}
// Animation loop
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach((particle) => {
// Update position
particle.x += particle.vx;
particle.y += particle.vy;
particle.rotation += particle.rotationSpeed;
// Wrap around edges
if (particle.x < -particle.size) particle.x = canvas.width + particle.size;
if (particle.x > canvas.width + particle.size) particle.x = -particle.size;
if (particle.y < -particle.size) particle.y = canvas.height + particle.size;
if (particle.y > canvas.height + particle.size) particle.y = -particle.size;
// Set style
ctx.fillStyle = particle.color;
ctx.globalAlpha = particle.opacity;
ctx.save();
ctx.translate(particle.x, particle.y);
ctx.rotate(particle.rotation);
// Draw different shapes
ctx.beginPath();
switch (particle.type) {
case 'circle':
ctx.arc(0, 0, particle.size, 0, Math.PI * 2);
break;
case 'square':
ctx.rect(
-particle.size,
-particle.size,
particle.size * 2,
particle.size * 2,
);
break;
case 'triangle':
ctx.moveTo(0, -particle.size);
ctx.lineTo(-particle.size, particle.size);
ctx.lineTo(particle.size, particle.size);
ctx.closePath();
break;
}
ctx.fill();
ctx.restore();
});
// Draw connections with different colors
particles.forEach((particle, i) => {
particles.slice(i + 1).forEach((otherParticle) => {
const dx = particle.x - otherParticle.x;
const dy = particle.y - otherParticle.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 120) {
ctx.beginPath();
ctx.moveTo(particle.x, particle.y);
ctx.lineTo(otherParticle.x, otherParticle.y);
const gradient = ctx.createLinearGradient(
particle.x,
particle.y,
otherParticle.x,
otherParticle.y,
);
gradient.addColorStop(0, particle.color);
gradient.addColorStop(1, otherParticle.color);
ctx.strokeStyle = gradient;
ctx.globalAlpha = 0.1 * (1 - distance / 120);
ctx.lineWidth = 1;
ctx.stroke();
}
});
});
ctx.globalAlpha = 1;
requestAnimationFrame(animate);
};
animate();
return () => {
window.removeEventListener('resize', resizeCanvas);
};
}, []);
return (
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full"
style={{ pointerEvents: 'none' }}
data-oid="j7rncr6"
/>
);
}
export default function ProductsPage({ params }: { params: { locale: Locale } }) {
const t = getTranslations(params.locale);
// Update meta tags when locale changes
useEffect(() => {
updateDocumentMeta(params.locale);
}, [params.locale]);
return (
<div
className="min-h-screen relative overflow-hidden bg-gradient-to-br from-slate-900 via-blue-900 to-indigo-900"
data-oid="cvpywwb"
>
{/* Enhanced Particle Background */}
<ProductParticleBackground data-oid="uc9k0a8" />
{/* Navigation */}
<Navigation locale={params.locale} data-oid="531iitl" />
{/* Floating Language Switcher */}
<FloatingLanguageSwitcher locale={params.locale} data-oid="kpdg95h" />
{/* Hero Section */}
<section className="relative z-10 pt-20 pb-16 px-4" data-oid="lm2ytxy">
<div className="max-w-7xl mx-auto text-center" data-oid="j5e12u3">
<h1
className="text-5xl md:text-7xl font-bold text-white mb-6 leading-tight"
data-oid="5baiugk"
>
{t.products?.title || 'Our Products'}
</h1>
<p
className="text-xl md:text-2xl text-white/80 mb-12 max-w-3xl mx-auto"
data-oid=":evsigy"
>
{t.products?.subtitle ||
'Discover innovative solutions for a sustainable future'}
</p>
</div>
</section>
{/* Featured Product */}
<section className="relative z-10 py-16 px-4" data-oid="3djucnf">
<div className="max-w-7xl mx-auto" data-oid="jffsbbq">
<div
className="bg-white/10 backdrop-blur-md rounded-3xl p-8 md:p-12 border border-white/20"
data-oid="5j0vqo-"
>
<div
className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center"
data-oid="w:qoogs"
>
<div data-oid="jp:93sh">
<div
className="inline-block bg-green-500 text-white px-4 py-2 rounded-full text-sm font-medium mb-6"
data-oid="_2jwbsf"
>
Featured Product
</div>
<h2
className="text-4xl md:text-5xl font-bold text-white mb-6"
data-oid="42kynsq"
>
{t.products?.featured?.title || 'Eco-Life Simulator'}
</h2>
<p className="text-xl text-white/80 mb-8" data-oid=":6k38y3">
{t.products?.featured?.description ||
'Experience the ultimate environmental simulation game'}
</p>
<div className="space-y-4 mb-8" data-oid="gakgmck">
{(t.products?.featured?.features || []).map(
(feature, index) => (
<div
key={index}
className="flex items-center space-x-3"
data-oid="f4az8h0"
>
<div
className="w-6 h-6 bg-green-500 rounded-full flex items-center justify-center"
data-oid="nwia:z7"
>
<svg
className="w-4 h-4 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="to48jui"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
data-oid="hqp78tt"
/>
</svg>
</div>
<span className="text-white/90" data-oid="a.d5.o0">
{feature}
</span>
</div>
),
)}
</div>
<div className="flex items-center space-x-6" data-oid="ciy3l:k">
<span
className="text-3xl font-bold text-green-400"
data-oid="j3m:k.g"
>
{t.products?.featured?.price || '$29.99'}
</span>
<button
className="bg-green-500 hover:bg-green-600 text-white px-8 py-4 rounded-full font-semibold text-lg transform hover:scale-105 transition-all duration-200 shadow-lg"
data-oid="3vyrpk7"
>
{t.products?.featured?.cta || 'Get Started Now'}
</button>
</div>
</div>
<div className="relative" data-oid="2frqox_">
<div
className="aspect-square bg-gradient-to-br from-green-400 to-blue-500 rounded-3xl p-8 relative overflow-hidden"
data-oid="sv04vkp"
>
{/* Product mockup with particles */}
<div
className="absolute inset-0 bg-black/20 rounded-3xl"
data-oid="7mphq74"
></div>
<div
className="relative z-10 h-full flex items-center justify-center"
data-oid="::y3wwb"
>
<div className="text-center text-white" data-oid="_a3bv_z">
<div
className="w-24 h-24 bg-white/20 rounded-full mx-auto mb-4 flex items-center justify-center"
data-oid="xoz1gv-"
>
<svg
className="w-12 h-12"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="htmw:.5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
data-oid="qm_gi7x"
/>
</svg>
</div>
<h3
className="text-2xl font-bold mb-2"
data-oid="._-sk9i"
>
Interactive Demo
</h3>
<p className="text-white/80" data-oid="_yx_77f">
Experience the simulation
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Product Categories */}
<section className="relative z-10 py-16 px-4" data-oid="dth.vfh">
<div className="max-w-7xl mx-auto" data-oid="b_yrju2">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8" data-oid="deq5x:y">
<div
className="bg-white/10 backdrop-blur-md rounded-2xl p-8 border border-white/20 hover:bg-white/15 transition-all duration-300"
data-oid="vrhxdx5"
>
<div
className="w-16 h-16 bg-green-500 rounded-2xl flex items-center justify-center mb-6"
data-oid="fbph6.s"
>
<svg
className="w-8 h-8 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="i-5t_6a"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1a3 3 0 000-6h-1m0 6V4m0 6h6m-7 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
data-oid="21l8sg_"
/>
</svg>
</div>
<h3 className="text-2xl font-bold text-white mb-4" data-oid="zh43ndf">
{t.products.categories.games.title}
</h3>
<p className="text-white/70" data-oid=".xj-6y.">
{t.products.categories.games.description}
</p>
</div>
<div
className="bg-white/10 backdrop-blur-md rounded-2xl p-8 border border-white/20 hover:bg-white/15 transition-all duration-300"
data-oid="_r4xti3"
>
<div
className="w-16 h-16 bg-blue-500 rounded-2xl flex items-center justify-center mb-6"
data-oid="2xc--or"
>
<svg
className="w-8 h-8 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="xr0zrf:"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
data-oid="e6ly5m_"
/>
</svg>
</div>
<h3 className="text-2xl font-bold text-white mb-4" data-oid="7vi9m0h">
{t.products.categories.tools.title}
</h3>
<p className="text-white/70" data-oid="f1:5c.n">
{t.products.categories.tools.description}
</p>
</div>
<div
className="bg-white/10 backdrop-blur-md rounded-2xl p-8 border border-white/20 hover:bg-white/15 transition-all duration-300"
data-oid="tc7x7dh"
>
<div
className="w-16 h-16 bg-purple-500 rounded-2xl flex items-center justify-center mb-6"
data-oid="q2r2se9"
>
<svg
className="w-8 h-8 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="ahfdmqs"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
data-oid="w05eav4"
/>
</svg>
</div>
<h3 className="text-2xl font-bold text-white mb-4" data-oid="b8tkf9z">
{t.products.categories.education.title}
</h3>
<p className="text-white/70" data-oid="ac0dw_k">
{t.products.categories.education.description}
</p>
</div>
</div>
</div>
</section>
{/* Product Grid */}
<section className="relative z-10 py-16 px-4" data-oid=":df3vby">
<div className="max-w-7xl mx-auto" data-oid="1fm-ile">
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8"
data-oid="mq9zk93"
>
{/* Product 1 */}
<div
className="bg-white/10 backdrop-blur-md rounded-2xl p-6 border border-white/20 hover:bg-white/15 transition-all duration-300 group"
data-oid="4m47ygl"
>
<div
className="aspect-square bg-gradient-to-br from-green-400 to-green-600 rounded-xl mb-6 relative overflow-hidden"
data-oid="ni7-0vh"
>
<div
className="absolute inset-0 bg-black/20"
data-oid="qqzr2r2"
></div>
<div
className="absolute inset-0 flex items-center justify-center"
data-oid="kg2s6ba"
>
<svg
className="w-12 h-12 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid=".exeex5"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
data-oid="6vhjg1_"
/>
</svg>
</div>
</div>
<h3 className="text-xl font-bold text-white mb-2" data-oid="27_ijij">
{t.products.productList.ecoSimulator.title}
</h3>
<p className="text-white/70 mb-4 text-sm" data-oid="5x:wp2_">
{t.products.productList.ecoSimulator.description}
</p>
<div className="flex items-center justify-between" data-oid="ad00jbx">
<span className="text-green-400 font-bold" data-oid="6q1_97d">
{t.products.productList.ecoSimulator.price}
</span>
<button
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors duration-200"
data-oid="e.r9qu-"
>
View Details
</button>
</div>
</div>
{/* Product 2 */}
<div
className="bg-white/10 backdrop-blur-md rounded-2xl p-6 border border-white/20 hover:bg-white/15 transition-all duration-300 group"
data-oid="5rbr4p8"
>
<div
className="aspect-square bg-gradient-to-br from-blue-400 to-blue-600 rounded-xl mb-6 relative overflow-hidden"
data-oid="m1w2yx2"
>
<div
className="absolute inset-0 bg-black/20"
data-oid="w8uva_n"
></div>
<div
className="absolute inset-0 flex items-center justify-center"
data-oid="igj8g:d"
>
<svg
className="w-12 h-12 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="zsd1xns"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
data-oid="c7izslw"
/>
</svg>
</div>
</div>
<h3 className="text-xl font-bold text-white mb-2" data-oid="16315pd">
{t.products.productList.carbonTracker.title}
</h3>
<p className="text-white/70 mb-4 text-sm" data-oid="fxx3xzs">
{t.products.productList.carbonTracker.description}
</p>
<div className="flex items-center justify-between" data-oid="3iqh5.-">
<span className="text-blue-400 font-bold" data-oid="afep:s-">
{t.products.productList.carbonTracker.price}
</span>
<button
className="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors duration-200"
data-oid="v:84d.f"
>
View Details
</button>
</div>
</div>
{/* Product 3 */}
<div
className="bg-white/10 backdrop-blur-md rounded-2xl p-6 border border-white/20 hover:bg-white/15 transition-all duration-300 group"
data-oid="ylps7.f"
>
<div
className="aspect-square bg-gradient-to-br from-purple-400 to-purple-600 rounded-xl mb-6 relative overflow-hidden"
data-oid="sgmy4a-"
>
<div
className="absolute inset-0 bg-black/20"
data-oid="1jrvfuf"
></div>
<div
className="absolute inset-0 flex items-center justify-center"
data-oid="giashnw"
>
<svg
className="w-12 h-12 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid=":haulu-"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
data-oid="gg2vvsi"
/>
</svg>
</div>
</div>
<h3 className="text-xl font-bold text-white mb-2" data-oid="6itbonk">
{t.products.productList.greenGuide.title}
</h3>
<p className="text-white/70 mb-4 text-sm" data-oid="94_u:5r">
{t.products.productList.greenGuide.description}
</p>
<div className="flex items-center justify-between" data-oid="im08pp2">
<span className="text-purple-400 font-bold" data-oid="hxtwl9f">
{t.products.productList.greenGuide.price}
</span>
<button
className="bg-purple-500 hover:bg-purple-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors duration-200"
data-oid="3e9u9t0"
>
View Details
</button>
</div>
</div>
{/* Product 4 */}
<div
className="bg-white/10 backdrop-blur-md rounded-2xl p-6 border border-white/20 hover:bg-white/15 transition-all duration-300 group"
data-oid="od:zoot"
>
<div
className="aspect-square bg-gradient-to-br from-orange-400 to-orange-600 rounded-xl mb-6 relative overflow-hidden"
data-oid=".09wskk"
>
<div
className="absolute inset-0 bg-black/20"
data-oid="xb_lxfj"
></div>
<div
className="absolute inset-0 flex items-center justify-center"
data-oid="gbvwymw"
>
<svg
className="w-12 h-12 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="1m6hjyl"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
data-oid=".sikv1l"
/>
</svg>
</div>
</div>
<h3 className="text-xl font-bold text-white mb-2" data-oid="gfqu2:k">
{t.products.productList.ecoChallenge.title}
</h3>
<p className="text-white/70 mb-4 text-sm" data-oid="rrsnl8c">
{t.products.productList.ecoChallenge.description}
</p>
<div className="flex items-center justify-between" data-oid="s5jbagy">
<span className="text-orange-400 font-bold" data-oid="rmdip20">
{t.products.productList.ecoChallenge.price}
</span>
<button
className="bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors duration-200"
data-oid="gjhr-2m"
>
View Details
</button>
</div>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="relative z-10 py-20 px-4" data-oid="9d:srtj">
<div className="max-w-4xl mx-auto text-center" data-oid="ehinuqy">
<div
className="bg-white/10 backdrop-blur-md rounded-3xl p-12 border border-white/20"
data-oid="bszxslt"
>
<h2
className="text-4xl md:text-5xl font-bold text-white mb-6"
data-oid="y3mxv4e"
>
Ready to Make a Difference?
</h2>
<p className="text-xl text-white/80 mb-8" data-oid="4rdacti">
Join thousands of users already making sustainable choices with our
products.
</p>
<div
className="flex flex-col sm:flex-row gap-4 justify-center"
data-oid="d-iudla"
>
<button
className="bg-green-500 hover:bg-green-600 text-white px-8 py-4 rounded-full font-semibold text-lg transform hover:scale-105 transition-all duration-200 shadow-lg"
data-oid="wn7ixwn"
>
Explore All Products
</button>
<button
className="border-2 border-white text-white hover:bg-white hover:text-gray-900 px-8 py-4 rounded-full font-semibold text-lg transform hover:scale-105 transition-all duration-200"
data-oid="1iho.r8"
>
Contact Sales
</button>
</div>
</div>
</div>
</section>
{/* Footer */}
<Footer locale={params.locale} data-oid="0vh4cx:" />
</div>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

132
app/globals.css Normal file
View File

@ -0,0 +1,132 @@
@tailwind base;
@tailwind components;
@tailwind utilities; /* Line clamp utilities */
@layer utilities {
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.line-clamp-3 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.line-clamp-4 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
}
} /* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb {
background: rgba(34, 197, 94, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(34, 197, 94, 0.7);
} /* Smooth animations */
* {
scroll-behavior: smooth;
} /* Focus styles for accessibility */
button:focus,
a:focus,
input:focus,
textarea:focus {
outline: 2px solid #22c55e;
outline-offset: 2px;
}
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

16
app/layout.tsx Normal file
View File

@ -0,0 +1,16 @@
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'Eco-Life - Rebuild Earth, Starting With Your Choices',
description:
'Join the movement to create a sustainable future. Every choice you make today shapes the world of tomorrow.',
};
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" data-oid="ki63-os">
<body className="" data-oid="i_q8qsb">
{children}
</body>
</html>
);
}

44
app/page.tsx Normal file
View File

@ -0,0 +1,44 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { locales, defaultLocale } from '../lib/i18n';
export default function RootPage() {
const router = useRouter();
useEffect(() => {
// Get user's preferred language from the browser
const userLang = navigator.language;
// Find a matching supported language
let targetLang = defaultLocale;
const matchedFullLang = locales.find((lang) => lang === userLang);
if (matchedFullLang) {
targetLang = matchedFullLang;
} else {
const baseLang = userLang.split('-')[0];
const matchedBaseLang = locales.find((lang) => lang.startsWith(baseLang));
if (matchedBaseLang) {
targetLang = matchedBaseLang;
}
}
// Redirect to the appropriate language-specific page
router.replace(`/${targetLang}`);
}, [router]);
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
backgroundColor: '#111827',
color: 'white',
fontFamily: 'sans-serif'
}}>
<p>Redirecting to your preferred language...</p>
</div>
);
}

22
app/robots.ts Normal file
View File

@ -0,0 +1,22 @@
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://your-domain.com';
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/_next/', '/admin/', '*.json'],
},
{
userAgent: 'Googlebot',
allow: '/',
disallow: ['/api/', '/_next/', '/admin/'],
},
],
sitemap: `${baseUrl}/sitemap.xml`,
host: baseUrl,
};
}

16
app/sitemap.ts Normal file
View File

@ -0,0 +1,16 @@
import { MetadataRoute } from 'next';
import { generateSitemap } from '../lib/sitemap-generator';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://your-domain.com';
const sitemapEntries = generateSitemap(baseUrl);
return sitemapEntries.map((entry) => ({
url: entry.url,
lastModified: entry.lastModified,
changeFrequency: entry.changeFrequency,
priority: entry.priority,
alternates: entry.alternates,
}));
}

BIN
bun.lockb Normal file

Binary file not shown.

20
components.json Normal file
View File

@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

287
components/BlogShowcase.tsx Normal file
View File

@ -0,0 +1,287 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { Locale } from '../lib/i18n';
import { getTranslations } from '../lib/translations';
interface BlogShowcaseProps {
locale: Locale;
}
export default function BlogShowcase({ locale }: BlogShowcaseProps) {
const t = getTranslations(locale);
const [hoveredPost, setHoveredPost] = useState<string | null>(null);
// 获取博客文章数据
const blogPosts = [
{
id: 'featured',
...t.blog.posts.featured,
image: 'featured',
},
{
id: 'post1',
...t.blog.posts.post1,
image: 'post1',
},
{
id: 'post2',
...t.blog.posts.post2,
image: 'post2',
},
{
id: 'post3',
...t.blog.posts.post3,
image: 'post3',
},
];
const getCategoryColor = (category: string) => {
switch (category) {
case 'playerStories':
return 'bg-amber-500';
case 'development':
return 'bg-green-500';
case 'ecoFacts':
return 'bg-emerald-500';
default:
return 'bg-blue-500';
}
};
const getCategoryName = (category: string) => {
switch (category) {
case 'playerStories':
return t.blog.categories.playerStories;
case 'development':
return t.blog.categories.development;
case 'ecoFacts':
return t.blog.categories.ecoFacts;
default:
return category;
}
};
const getImageGradient = (imageId: string) => {
const gradients = {
featured: 'from-purple-500 via-blue-500 to-indigo-600',
post1: 'from-amber-500 via-orange-500 to-red-500',
post2: 'from-cyan-500 via-blue-500 to-indigo-500',
post3: 'from-green-500 via-emerald-500 to-teal-500',
};
return gradients[imageId as keyof typeof gradients] || 'from-gray-500 to-gray-600';
};
return (
<section className="relative z-10 py-20 bg-gradient-to-br from-slate-800 via-slate-900 to-black">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<div className="text-center mb-16">
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">
{t.blog.title}
</h2>
<p className="text-xl text-white/80 max-w-3xl mx-auto leading-relaxed">
{t.blog.subtitle}
</p>
<div className="mt-8">
<Link
href={`/${locale}/blog`}
className="inline-flex items-center bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white px-8 py-3 rounded-full font-semibold transition-all duration-200 transform hover:scale-105 shadow-lg"
>
{t.blog.categories.all}
<svg
className="w-5 h-5 ml-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg>
</Link>
</div>
</div>
{/* Blog Posts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Featured Post */}
<div className="lg:row-span-2">
<Link href={`/${locale}/blog/${blogPosts[0].id}`}>
<article
className="group cursor-pointer h-full"
onMouseEnter={() => setHoveredPost(blogPosts[0].id)}
onMouseLeave={() => setHoveredPost(null)}
>
<div className="bg-white/10 backdrop-blur-md rounded-3xl overflow-hidden border border-white/20 shadow-2xl hover:shadow-3xl transition-all duration-500 transform hover:scale-[1.02] h-full">
{/* Featured Post Image */}
<div className="relative h-80 overflow-hidden">
<div
className={`absolute inset-0 bg-gradient-to-br ${getImageGradient(blogPosts[0].image)} opacity-90`}
>
<div className="absolute inset-0 bg-black/20"></div>
{/* Animated particles for featured post */}
<div className="absolute inset-0">
{[...Array(6)].map((_, i) => (
<div
key={i}
className={`absolute w-2 h-2 bg-white/30 rounded-full animate-pulse ${
hoveredPost === blogPosts[0].id
? 'animate-bounce'
: ''
}`}
style={{
left: `${20 + i * 15}%`,
top: `${30 + (i % 2) * 20}%`,
animationDelay: `${i * 0.2}s`,
}}
/>
))}
</div>
</div>
<div className="absolute top-6 left-6">
<span
className={`${getCategoryColor(blogPosts[0].category)} text-white px-4 py-2 rounded-full text-sm font-medium shadow-lg`}
>
{getCategoryName(blogPosts[0].category)}
</span>
</div>
<div className="absolute top-6 right-6">
<div className="bg-white/20 backdrop-blur-sm rounded-full px-3 py-1">
<span className="text-white text-sm font-medium">
{t.blog.featured}
</span>
</div>
</div>
<div className="absolute bottom-6 left-6 right-6">
<div className="text-white/90 text-sm mb-2">
<div className="flex items-center space-x-4">
<span>{blogPosts[0].author}</span>
<span></span>
<span>{blogPosts[0].date}</span>
<span></span>
<span>{blogPosts[0].readTime}</span>
</div>
</div>
</div>
</div>
{/* Featured Post Content */}
<div className="p-8">
<h3 className="text-2xl font-bold text-white mb-4 group-hover:text-green-300 transition-colors duration-200 leading-tight">
{blogPosts[0].title}
</h3>
<p className="text-white/80 mb-6 leading-relaxed line-clamp-3">
{blogPosts[0].excerpt}
</p>
<div className="flex items-center justify-between">
<span className="text-green-300 font-medium group-hover:text-green-200 transition-colors duration-200">
{t.blog.readMore}
</span>
</div>
</div>
</div>
</article>
</Link>
</div>
{/* Other Posts */}
<div className="space-y-8">
{blogPosts.slice(1).map((post, index) => (
<Link key={post.id} href={`/${locale}/blog/${post.id}`}>
<article
className="group cursor-pointer"
onMouseEnter={() => setHoveredPost(post.id)}
onMouseLeave={() => setHoveredPost(null)}
>
<div className="bg-white/10 backdrop-blur-md rounded-2xl overflow-hidden border border-white/20 shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:scale-[1.02]">
<div className="flex">
{/* Post Image */}
<div className="relative w-32 h-32 flex-shrink-0 overflow-hidden">
<div
className={`absolute inset-0 bg-gradient-to-br ${getImageGradient(post.image)} opacity-90`}
>
<div className="absolute inset-0 bg-black/20"></div>
{/* Mini particles */}
<div className="absolute inset-0">
{[...Array(3)].map((_, i) => (
<div
key={i}
className={`absolute w-1 h-1 bg-white/40 rounded-full ${
hoveredPost === post.id
? 'animate-ping'
: 'animate-pulse'
}`}
style={{
left: `${30 + i * 20}%`,
top: `${40 + i * 10}%`,
animationDelay: `${i * 0.3}s`,
}}
/>
))}
</div>
</div>
<div className="absolute top-2 left-2">
<span
className={`${getCategoryColor(post.category)} text-white px-2 py-1 rounded-full text-xs font-medium`}
>
{getCategoryName(post.category)}
</span>
</div>
</div>
{/* Post Content */}
<div className="flex-1 p-6">
<h4 className="text-lg font-bold text-white mb-2 group-hover:text-green-300 transition-colors duration-200 line-clamp-2">
{post.title}
</h4>
<p className="text-white/70 text-sm mb-3 line-clamp-2">
{post.excerpt}
</p>
<div className="flex items-center justify-between">
<div className="text-white/60 text-xs">
<span>{post.author}</span>
<span className="mx-2"></span>
<span>{post.readTime}</span>
</div>
<span className="text-green-300 text-sm font-medium group-hover:text-green-200 transition-colors duration-200">
</span>
</div>
</div>
</div>
</div>
</article>
</Link>
))}
</div>
</div>
{/* Bottom CTA */}
<div className="text-center mt-16">
<div className="bg-gradient-to-r from-green-500/20 to-emerald-500/20 backdrop-blur-md rounded-2xl p-8 border border-green-500/30">
<h3 className="text-2xl font-bold text-white mb-4">{t.blog.stayUpdated}</h3>
<p className="text-white/80 mb-6 max-w-2xl mx-auto">
{t.blog.stayUpdatedDesc}
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href={`/${locale}/blog`}
className="bg-green-500 hover:bg-green-600 text-white px-8 py-3 rounded-full font-semibold transition-all duration-200 transform hover:scale-105"
>
{t.blog.exploreAll}
</Link>
<button className="border-2 border-green-500 text-green-400 hover:bg-green-500 hover:text-white px-8 py-3 rounded-full font-semibold transition-all duration-200">
{t.blog.subscribe}
</button>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,271 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { Locale, locales, localeNames, localeFlags } from '../lib/i18n';
interface FloatingLanguageSwitcherProps {
locale: Locale;
}
export default function FloatingLanguageSwitcher({ locale }: FloatingLanguageSwitcherProps) {
const [isOpen, setIsOpen] = useState(false);
const [isVisible, setIsVisible] = useState(true);
const [isMounted, setIsMounted] = useState(false);
const router = useRouter();
const pathname = usePathname();
const switcherRef = useRef<HTMLDivElement>(null);
const lastScrollY = useRef(0);
// Ensure component is mounted before rendering to avoid hydration issues
useEffect(() => {
setIsMounted(true);
}, []);
// Handle scroll to show/hide the switcher (temporarily disabled for visibility)
useEffect(() => {
// Keep the switcher always visible for now
setIsVisible(true);
// Uncomment below to re-enable scroll hiding
/*
const handleScroll = () => {
const currentScrollY = window.scrollY;
if (currentScrollY > lastScrollY.current && currentScrollY > 100) {
// Scrolling down
setIsVisible(false);
} else {
// Scrolling up
setIsVisible(true);
}
lastScrollY.current = currentScrollY;
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
*/
}, []);
// Close menu when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
// Add a small delay to ensure button click is processed first
setTimeout(() => {
if (switcherRef.current && !switcherRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}, 10);
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
const switchLanguage = (newLocale: Locale) => {
console.log('Switching language from', locale, 'to', newLocale);
console.log('Current pathname:', pathname);
try {
// Get the current path segments
const pathSegments = pathname.split('/').filter(Boolean);
console.log('Path segments:', pathSegments);
// Remove the current locale from the beginning if it exists
if (pathSegments[0] && locales.includes(pathSegments[0] as Locale)) {
pathSegments.shift();
}
// Construct the new path with the new locale
const newPath = `/${newLocale}${pathSegments.length > 0 ? '/' + pathSegments.join('/') : ''}`;
console.log('New path:', newPath);
// Use replace instead of push to avoid history issues
router.replace(newPath);
setIsOpen(false);
} catch (error) {
console.error('Error switching language:', error);
// Fallback to simple navigation
router.replace(`/${newLocale}`);
setIsOpen(false);
}
};
// Don't render until mounted to avoid hydration issues
if (!isMounted) {
return null;
}
return (
<div
ref={switcherRef}
className={`fixed right-4 md:right-6 top-1/2 -translate-y-1/2 transition-all duration-300 ${
isVisible ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
}`}
style={{
pointerEvents: 'auto',
isolation: 'isolate',
zIndex: 999999,
position: 'fixed',
}}
data-oid="taerglg"
>
{/* Main Switcher Button */}
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log('Button clicked, current isOpen:', isOpen);
const newState = !isOpen;
setIsOpen(newState);
console.log('Setting isOpen to:', newState);
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
className="group relative bg-slate-800/90 border-2 border-green-400/50 rounded-full p-3 shadow-2xl hover:bg-slate-700/90 hover:border-green-400 transition-all duration-200 cursor-pointer"
title={`Current: ${localeNames[locale]}`}
type="button"
style={{
pointerEvents: 'auto',
touchAction: 'manipulation',
zIndex: 1000000,
position: 'relative',
}}
data-oid="_:fx8.e"
>
<div className="flex items-center justify-center w-8 h-8" data-oid="di.5x8v">
<span className="text-2xl" data-oid="ll7ebjo">
{localeFlags[locale]}
</span>
</div>
{/* Tooltip */}
<div
className="absolute right-full mr-3 top-1/2 -translate-y-1/2 bg-slate-800/95 backdrop-blur-md text-white px-3 py-2 rounded-lg text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap"
data-oid=":kya.x4"
>
{localeNames[locale]}
<div
className="absolute left-full top-1/2 -translate-y-1/2 border-4 border-transparent border-l-slate-800/95"
data-oid="01-w6mc"
></div>
</div>
</button>
{/* Language Options */}
{isOpen && (
<div
className="absolute right-full mr-3 md:mr-4 top-1/2 -translate-y-1/2 bg-slate-800/98 rounded-2xl shadow-2xl border-2 border-green-400/30 overflow-hidden min-w-[180px] md:min-w-[200px]"
style={{
zIndex: 1000001,
pointerEvents: 'auto',
}}
data-oid="e1v:81b"
>
<div className="p-2" data-oid="xcuqg9t">
<div
className="px-3 py-2 text-xs font-medium text-white/60 uppercase tracking-wider border-b border-white/10 mb-2"
data-oid="_jrtjyl"
>
Select Language
</div>
<div className="space-y-1" data-oid="tg0y3c4">
{locales.map((loc) => (
<button
key={loc}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log('Language option clicked:', loc);
// Add immediate feedback
e.currentTarget.style.backgroundColor =
'rgba(34, 197, 94, 0.3)';
setTimeout(() => {
switchLanguage(loc);
}, 100);
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
className={`w-full text-left px-3 py-3 rounded-xl flex items-center space-x-3 transition-all duration-200 cursor-pointer ${
locale === loc
? 'bg-green-500/30 text-green-400 border border-green-500/50'
: 'text-white hover:bg-green-500/20 hover:text-green-400'
}`}
type="button"
style={{
pointerEvents: 'auto',
touchAction: 'manipulation',
zIndex: 1000002,
}}
data-oid="i3cq3gh"
>
<span className="text-lg" data-oid="kl5sqzg">
{localeFlags[loc]}
</span>
<div className="flex-1" data-oid="ump7m:6">
<span className="font-medium" data-oid="lsnf7ck">
{localeNames[loc]}
</span>
{locale === loc && (
<div
className="text-xs text-green-400/70 mt-0.5"
data-oid="6e9d:gj"
>
Current
</div>
)}
</div>
{locale === loc && (
<svg
className="w-4 h-4 text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
data-oid="v2bjs6b"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
data-oid="mmqle:5"
/>
</svg>
)}
</button>
))}
</div>
</div>
{/* Arrow pointing to the button */}
<div
className="absolute left-full top-1/2 -translate-y-1/2 border-8 border-transparent border-l-slate-800/95"
data-oid="0gukon1"
></div>
</div>
)}
{/* Subtle glow effect */}
<div
className="absolute inset-0 rounded-full bg-green-400/20 animate-pulse pointer-events-none"
style={{ zIndex: -1 }}
data-oid="g55eh56"
></div>
{/* Floating indicator */}
<div
className="absolute -top-1 -right-1 w-3 h-3 bg-green-400 rounded-full animate-bounce pointer-events-none"
style={{ zIndex: -1 }}
data-oid="1t6b6v9"
></div>
</div>
);
}

172
components/Footer.tsx Normal file
View File

@ -0,0 +1,172 @@
'use client';
import { useRouter, usePathname } from 'next/navigation';
import { Locale, locales, localeNames, localeFlags } from '../lib/i18n';
import { getTranslations } from '../lib/translations';
interface FooterProps {
locale: Locale;
}
export default function Footer({ locale }: FooterProps) {
const router = useRouter();
const pathname = usePathname();
const t = getTranslations(locale);
const switchLanguage = (newLocale: Locale) => {
// Get the current path segments
const pathSegments = pathname.split('/').filter(Boolean);
// Remove the current locale from the beginning if it exists
if (pathSegments[0] && locales.includes(pathSegments[0] as Locale)) {
pathSegments.shift();
}
// Construct the new path with the new locale
const newPath = `/${newLocale}${pathSegments.length > 0 ? '/' + pathSegments.join('/') : ''}`;
router.push(newPath);
};
return (
<footer className="relative z-10 bg-slate-900/95 backdrop-blur-md border-t border-white/10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Brand Section */}
<div className="col-span-1 md:col-span-2">
<div className="text-2xl font-bold text-white mb-4">
Nebula <span className="text-green-400">Cloud</span>
</div>
<p className="text-white/70 mb-6 max-w-md">{t.hero.subtitle}</p>
{/* Language Switcher in Footer */}
<div className="mb-6">
<h4 className="text-white font-medium mb-3">
Language / / /
</h4>
<div className="flex flex-wrap gap-2">
{locales.map((loc) => (
<button
key={loc}
onClick={() => switchLanguage(loc)}
className={`flex items-center space-x-2 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
locale === loc
? 'bg-green-500/20 text-green-400 border border-green-500/30'
: 'text-white/70 hover:text-white hover:bg-white/10'
}`}
>
<span>{localeFlags[loc]}</span>
<span>{localeNames[loc]}</span>
</button>
))}
</div>
</div>
</div>
{/* Quick Links */}
<div>
<h4 className="text-white font-medium mb-4">Quick Links</h4>
<div className="space-y-2">
<a
href={`/${locale}#hero`}
className="block text-white/70 hover:text-green-400 transition-colors duration-200"
>
{t.nav.heroSection}
</a>
<a
href={`/${locale}#about`}
className="block text-white/70 hover:text-green-400 transition-colors duration-200"
>
{t.nav.aboutEcoLife}
</a>
<a
href={`/${locale}/products`}
className="block text-white/70 hover:text-green-400 transition-colors duration-200"
>
{t.nav.products}
</a>
<a
href={`/${locale}#blog`}
className="block text-white/70 hover:text-green-400 transition-colors duration-200"
>
{t.nav.blog}
</a>
</div>
</div>
{/* Contact Info */}
<div>
<h4 className="text-white font-medium mb-4">{t.nav.contact}</h4>
<div className="space-y-2 text-white/70">
<p>support@eco-life.com</p>
<p>+1 (555) 123-4567</p>
<div className="flex space-x-4 mt-4">
<a
href="#"
className="text-white/70 hover:text-green-400 transition-colors duration-200"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z" />
</svg>
</a>
<a
href="#"
className="text-white/70 hover:text-green-400 transition-colors duration-200"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M22.46 6c-.77.35-1.6.58-2.46.69.88-.53 1.56-1.37 1.88-2.38-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29 0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15 0 1.49.75 2.81 1.91 3.56-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07 4.28 4.28 0 0 0 4 2.98 8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21 16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56.84-.6 1.56-1.36 2.14-2.23z" />
</svg>
</a>
<a
href="#"
className="text-white/70 hover:text-green-400 transition-colors duration-200"
>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
</svg>
</a>
</div>
</div>
</div>
</div>
{/* Bottom Bar */}
<div className="border-t border-white/10 mt-8 pt-8 flex flex-col md:flex-row justify-between items-center">
<p className="text-white/60 text-sm">© 2024 Nebula Cloud. All rights reserved.</p>
<div className="flex space-x-6 mt-4 md:mt-0">
<a
href="#"
className="text-white/60 hover:text-white text-sm transition-colors duration-200"
>
Privacy Policy
</a>
<a
href="#"
className="text-white/60 hover:text-white text-sm transition-colors duration-200"
>
Terms of Service
</a>
<a
href="#"
className="text-white/60 hover:text-white text-sm transition-colors duration-200"
>
Cookie Policy
</a>
</div>
</div>
</div>
</footer>
);
}

130
components/Navigation.tsx Normal file
View File

@ -0,0 +1,130 @@
'use client';
import { useState, useEffect } from 'react';
import { Locale } from '../lib/i18n';
import { getTranslations } from '../lib/translations';
interface NavigationProps {
locale?: Locale;
}
export default function Navigation({ locale = 'en' }: NavigationProps) {
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const t = getTranslations(locale);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 50);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const navItems = [
{ href: `/${locale}#hero`, label: t.nav.heroSection },
{ href: `/${locale}#about`, label: t.nav.aboutEcoLife },
{ href: `/${locale}/products`, label: t.nav.products },
{ href: `/${locale}/blog`, label: t.nav.blog },
{ href: `/${locale}/contact`, label: t.nav.contact },
];
return (
<nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
isScrolled
? 'bg-slate-900/95 backdrop-blur-md border-b border-white/10'
: 'bg-transparent'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<div className="flex-shrink-0">
<a
href={`/${locale}`}
className="text-2xl font-bold text-white hover:text-green-400 transition-colors duration-200"
>
Nebula <span className="text-green-400">Cloud</span>
</a>
</div>
{/* Desktop Navigation */}
<div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-8">
{navItems.map((item) => (
<a
key={item.href}
href={item.href}
className="text-white/90 hover:text-green-400 px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200"
>
{item.label}
</a>
))}
</div>
</div>
{/* Download Button */}
<div className="hidden md:block">
<button className="bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded-full font-medium transition-all duration-200 transform hover:scale-105">
{t.nav.download}
</button>
</div>
{/* Mobile menu button */}
<div className="md:hidden">
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="text-white hover:text-green-400 p-2"
>
<svg
className="h-6 w-6"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
{isMobileMenuOpen ? (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
) : (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
)}
</svg>
</button>
</div>
</div>
{/* Mobile Navigation */}
{isMobileMenuOpen && (
<div className="md:hidden">
<div className="px-2 pt-2 pb-3 space-y-1 bg-slate-900/95 backdrop-blur-md rounded-lg mt-2">
{navItems.map((item) => (
<a
key={item.href}
href={item.href}
className="text-white/90 hover:text-green-400 block px-3 py-2 rounded-md text-base font-medium transition-colors duration-200"
onClick={() => setIsMobileMenuOpen(false)}
>
{item.label}
</a>
))}
<button className="w-full bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded-full font-medium transition-all duration-200 mt-4">
{t.nav.download}
</button>
</div>
</div>
)}
</div>
</nav>
);
}

20
lib/i18n.ts Normal file
View File

@ -0,0 +1,20 @@
export const locales = ['en', 'zh-CN', 'zh-TW', 'ko', 'ja'] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'en';
export const localeNames: Record<Locale, string> = {
en: 'English',
'zh-CN': '简体中文',
'zh-TW': '繁體中文',
ko: '한국어',
ja: '日本語',
};
export const localeFlags: Record<Locale, string> = {
en: '🇺🇸',
'zh-CN': '🇨🇳',
'zh-TW': '🇹🇼',
ko: '🇰🇷',
ja: '🇯🇵',
};

88
lib/seo-utils.ts Normal file
View File

@ -0,0 +1,88 @@
import { Locale, locales, defaultLocale } from './i18n';
import { getTranslations } from './translations';
export function updateDocumentMeta(locale: Locale) {
const translations = getTranslations(locale);
// Update document title
document.title = translations.meta.title;
// Update meta description
updateMetaTag('description', translations.meta.description);
// Update HTML lang attribute
document.documentElement.lang = locale;
}
function updateMetaTag(name: string, content: string) {
let meta = document.querySelector(`meta[name="${name}"]`) as HTMLMetaElement;
if (!meta) {
meta = document.createElement('meta');
meta.name = name;
document.head.appendChild(meta);
}
meta.content = content;
}
export function generateAlternateLinks(pathname: string, baseUrl: string = '') {
return locales.map((locale) => ({
hrefLang: locale,
href:
locale === defaultLocale ? `${baseUrl}${pathname}` : `${baseUrl}/${locale}${pathname}`,
}));
}
export function generateCanonicalUrl(pathname: string, locale: Locale, baseUrl: string = '') {
return locale === defaultLocale ? `${baseUrl}${pathname}` : `${baseUrl}/${locale}${pathname}`;
}
export function generateStructuredData(
type: 'WebSite' | 'BlogPosting' | 'Organization',
data: any,
locale: Locale,
) {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://your-domain.com';
const translations = getTranslations(locale);
const baseStructuredData = {
'@context': 'https://schema.org',
'@type': type,
};
switch (type) {
case 'WebSite':
return {
...baseStructuredData,
name: translations.meta.title,
description: translations.meta.description,
url: baseUrl,
inLanguage: locale,
availableLanguage: locales,
};
case 'BlogPosting':
return {
...baseStructuredData,
headline: data.title,
description: data.excerpt,
author: {
'@type': 'Person',
name: data.author,
},
datePublished: data.date,
dateModified: data.date,
inLanguage: locale,
};
case 'Organization':
return {
...baseStructuredData,
name: translations.meta.title,
description: translations.meta.description,
url: baseUrl,
};
default:
return baseStructuredData;
}
}

125
lib/sitemap-generator.ts Normal file
View File

@ -0,0 +1,125 @@
import { locales, defaultLocale, Locale } from './i18n';
import { getTranslations } from './translations';
export interface SitemapEntry {
url: string;
lastModified: Date;
changeFrequency: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
priority: number;
alternates?: {
languages: Record<string, string>;
};
}
export function generateStaticPages(baseUrl: string): SitemapEntry[] {
const staticPages = [
{ path: '', priority: 1.0, changeFrequency: 'daily' as const },
{ path: '/blog', priority: 0.8, changeFrequency: 'weekly' as const },
{ path: '/contact', priority: 0.6, changeFrequency: 'monthly' as const },
{ path: '/products', priority: 0.7, changeFrequency: 'weekly' as const },
];
const entries: SitemapEntry[] = [];
locales.forEach((locale) => {
staticPages.forEach((page) => {
const url =
locale === defaultLocale
? `${baseUrl}${page.path}`
: `${baseUrl}/${locale}${page.path}`;
entries.push({
url,
lastModified: new Date(),
changeFrequency: page.changeFrequency,
priority: page.priority,
alternates: {
languages: Object.fromEntries(
locales.map((loc) => [
loc,
loc === defaultLocale
? `${baseUrl}${page.path}`
: `${baseUrl}/${loc}${page.path}`,
]),
),
},
});
});
});
return entries;
}
export function generateBlogPages(baseUrl: string): SitemapEntry[] {
// Get blog post slugs from your translations or database
const blogSlugs = ['featured', 'post1', 'post2', 'post3', 'post4', 'post5'];
const entries: SitemapEntry[] = [];
locales.forEach((locale) => {
blogSlugs.forEach((slug) => {
const url =
locale === defaultLocale
? `${baseUrl}/blog/${slug}`
: `${baseUrl}/${locale}/blog/${slug}`;
entries.push({
url,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
alternates: {
languages: Object.fromEntries(
locales.map((loc) => [
loc,
loc === defaultLocale
? `${baseUrl}/blog/${slug}`
: `${baseUrl}/${loc}/blog/${slug}`,
]),
),
},
});
});
});
return entries;
}
export function generateSitemap(baseUrl: string): SitemapEntry[] {
const staticPages = generateStaticPages(baseUrl);
const blogPages = generateBlogPages(baseUrl);
return [...staticPages, ...blogPages];
}
// Helper function to get all available blog post slugs
export function getBlogPostSlugs(): string[] {
// In a real application, you might fetch this from a CMS or database
// For now, we'll use the static list from your existing blog structure
return ['featured', 'post1', 'post2', 'post3', 'post4', 'post5'];
}
// Helper function to validate if a blog post exists
export function validateBlogPost(slug: string, locale: Locale): boolean {
const translations = getTranslations(locale);
const posts = translations.blog.posts;
return slug in posts;
}
export function getSitemapStats(baseUrl: string): {
urlCount: number;
staticPages: number;
blogPages: number;
languages: number;
} {
const staticPages = generateStaticPages(baseUrl);
const blogPages = generateBlogPages(baseUrl);
const sitemap = [...staticPages, ...blogPages];
return {
urlCount: sitemap.length,
staticPages: staticPages.length,
blogPages: blogPages.length,
languages: locales.length,
};
}

289
lib/sitemap-utils.ts Normal file
View File

@ -0,0 +1,289 @@
import { locales, defaultLocale } from './i18n';
import { getSitemapStats, generateSitemap } from './sitemap-generator';
/**
*
*/
export class SitemapValidator {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
/**
* URL 访
*/
async validateUrls(): Promise<{
valid: string[];
invalid: string[];
errors: Array<{ url: string; error: string }>;
}> {
const sitemap = generateSitemap(this.baseUrl);
const valid: string[] = [];
const invalid: string[] = [];
const errors: Array<{ url: string; error: string }> = [];
for (const entry of sitemap) {
try {
const response = await fetch(entry.url, { method: 'HEAD' });
if (response.ok) {
valid.push(entry.url);
} else {
invalid.push(entry.url);
errors.push({
url: entry.url,
error: `HTTP ${response.status}: ${response.statusText}`,
});
}
} catch (error) {
invalid.push(entry.url);
errors.push({
url: entry.url,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
return { valid, invalid, errors };
}
/**
* URL
*/
checkDuplicates(): string[] {
const sitemap = generateSitemap(this.baseUrl);
const urls = sitemap.map((entry) => entry.url);
const duplicates: string[] = [];
const seen = new Set<string>();
for (const url of urls) {
if (seen.has(url)) {
duplicates.push(url);
} else {
seen.add(url);
}
}
return duplicates;
}
/**
*
*/
generateReport(): {
stats: ReturnType<typeof getSitemapStats>;
duplicates: string[];
recommendations: string[];
} {
const stats = getSitemapStats(this.baseUrl);
const duplicates = this.checkDuplicates();
const recommendations: string[] = [];
// 生成建议
if (duplicates.length > 0) {
recommendations.push(`发现 ${duplicates.length} 个重复的 URL请检查站点地图生成逻辑`);
}
if (stats.urlCount > 50000) {
recommendations.push('站点地图包含超过 50,000 个 URL建议分割为多个站点地图文件');
}
if (stats.blogPages === 0) {
recommendations.push('未发现博客页面,请确认博客内容是否正确配置');
}
return {
stats,
duplicates,
recommendations,
};
}
}
/**
*
*/
export class SitemapSubmitter {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
/**
* URL
*/
getSubmissionUrls(): Record<string, string> {
const sitemapUrl = `${this.baseUrl}/sitemap.xml`;
return {
google: `https://www.google.com/ping?sitemap=${encodeURIComponent(sitemapUrl)}`,
bing: `https://www.bing.com/ping?sitemap=${encodeURIComponent(sitemapUrl)}`,
yandex: `https://webmaster.yandex.com/ping?sitemap=${encodeURIComponent(sitemapUrl)}`,
baidu: `https://ping.baidu.com/ping/RPC2?sitemap=${encodeURIComponent(sitemapUrl)}`,
};
}
/**
*
*/
async submitToSearchEngines(): Promise<{
success: string[];
failed: Array<{ engine: string; error: string }>;
}> {
const urls = this.getSubmissionUrls();
const success: string[] = [];
const failed: Array<{ engine: string; error: string }> = [];
for (const [engine, url] of Object.entries(urls)) {
try {
const response = await fetch(url, { method: 'GET' });
if (response.ok) {
success.push(engine);
} else {
failed.push({
engine,
error: `HTTP ${response.status}: ${response.statusText}`,
});
}
} catch (error) {
failed.push({
engine,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
return { success, failed };
}
}
/**
*
*/
export function generateSitemapIndex(baseUrl: string): string {
const lastmod = new Date().toISOString();
return `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>${baseUrl}/sitemap.xml</loc>
<lastmod>${lastmod}</lastmod>
</sitemap>
</sitemapindex>`;
}
/**
* HTML
*/
export function generateHtmlSitemap(baseUrl: string): string {
const sitemap = generateSitemap(baseUrl);
const stats = getSitemapStats(baseUrl);
// 按页面类型分组
const groupedPages = sitemap.reduce(
(acc, entry) => {
let category = 'other';
if (entry.url.includes('/blog/') && !entry.url.endsWith('/blog')) {
category = 'blog-posts';
} else if (entry.url.endsWith('/blog')) {
category = 'blog';
} else if (entry.url.endsWith('/contact')) {
category = 'contact';
} else if (entry.url.endsWith('/products')) {
category = 'products';
} else if (entry.url === baseUrl || entry.url.match(/\/[a-z-]+$/)) {
category = 'main';
}
if (!acc[category]) acc[category] = [];
acc[category].push(entry);
return acc;
},
{} as Record<string, typeof sitemap>,
);
const categoryNames = {
main: '主要页面',
blog: '博客',
'blog-posts': '博客文章',
contact: '联系我们',
products: '产品',
other: '其他页面',
};
let html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> - Eco Life</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; line-height: 1.6; }
.header { border-bottom: 2px solid #10b981; padding-bottom: 20px; margin-bottom: 30px; }
.stats { background: #f0fdf4; padding: 20px; border-radius: 8px; margin-bottom: 30px; }
.category { margin-bottom: 30px; }
.category h2 { color: #10b981; border-bottom: 1px solid #d1d5db; padding-bottom: 10px; }
.url-list { list-style: none; padding: 0; }
.url-item { margin: 8px 0; padding: 12px; background: #f9fafb; border-radius: 6px; }
.url-item a { text-decoration: none; color: #1f2937; font-weight: 500; }
.url-item a:hover { color: #10b981; }
.url-meta { font-size: 0.875rem; color: #6b7280; margin-top: 4px; }
.priority { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; }
.priority-high { background: #fef3c7; color: #92400e; }
.priority-medium { background: #dbeafe; color: #1e40af; }
.priority-low { background: #f3f4f6; color: #374151; }
</style>
</head>
<body>
<div class="header">
<h1></h1>
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
</div>
<div class="stats">
<h3></h3>
<p><strong>:</strong> ${stats.urlCount}</p>
<p><strong>:</strong> ${stats.staticPages}</p>
<p><strong>:</strong> ${stats.blogPages}</p>
<p><strong>:</strong> ${stats.languages} (${locales.join(', ')})</p>
</div>`;
Object.entries(groupedPages).forEach(([category, pages]) => {
html += `
<div class="category">
<h2>${categoryNames[category as keyof typeof categoryNames] || category}</h2>
<ul class="url-list">`;
pages.forEach((page) => {
const priorityClass =
page.priority >= 0.8
? 'priority-high'
: page.priority >= 0.6
? 'priority-medium'
: 'priority-low';
html += `
<li class="url-item">
<a href="${page.url}" target="_blank">${page.url}</a>
<div class="url-meta">
<span class="priority ${priorityClass}">优先级: ${page.priority}</span>
<span>更新频率: ${page.changeFrequency}</span>
<span>最后修改: ${page.lastModified.toLocaleDateString('zh-CN')}</span>
</div>
</li>`;
});
html += `
</ul>
</div>`;
});
html += `
</body>
</html>`;
return html;
}

1091
lib/translations.ts Normal file

File diff suppressed because it is too large Load Diff

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

54
middleware.ts Normal file
View File

@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from 'next/server';
import { locales, defaultLocale } from './lib/i18n';
export function middleware(request: NextRequest) {
// Check if there is any supported locale in the pathname
const pathname = request.nextUrl.pathname;
// Check if the pathname already includes a locale
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
);
// Redirect if there is no locale
if (pathnameIsMissingLocale) {
// Try to get locale from Accept-Language header
const locale = getLocale(request) || defaultLocale;
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}
}
function getLocale(request: NextRequest): string | undefined {
// Get locale from Accept-Language header
const acceptLanguage = request.headers.get('accept-language');
if (!acceptLanguage) return undefined;
// Parse Accept-Language header
const languages = acceptLanguage.split(',').map((lang) => lang.split(';')[0].trim());
// Find matching locale
for (const lang of languages) {
if (locales.includes(lang as any)) {
return lang;
}
// Check for language without region (e.g., 'zh' matches 'zh-CN')
const langWithoutRegion = lang.split('-')[0];
const matchingLocale = locales.find((locale) => locale.startsWith(langWithoutRegion));
if (matchingLocale) {
return matchingLocale;
}
}
return undefined;
}
export const config = {
matcher: [
// Skip all internal paths (_next)
'/((?!_next|api|favicon.ico|.*\\..*).*)',
],
};

6
next.config.mjs Normal file
View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
distDir: 'build',
};
export default nextConfig;

5582
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "@onlook/next-template",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"format": "prettier --write ."
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.438.0",
"next": "14.2.23",
"react": "^18",
"react-dom": "^18",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "^15.1.6",
"postcss": "^8",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

BIN
public/images/logo-text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

27
public/manifest.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "Eco-Life - Rebuild Earth",
"short_name": "Eco-Life",
"description": "Join the movement to create a sustainable future through interactive environmental gaming",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#22c55e",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"categories": ["games", "education", "environment"],
"lang": "en",
"dir": "ltr"
}

19
public/robots.txt Normal file
View File

@ -0,0 +1,19 @@
User-agent: *
Allow: /
# Sitemaps
Sitemap: https://eco-life.com/sitemap.xml
# Specific rules for different locales
Allow: /en/
Allow: /zh-CN/
Allow: /zh-TW/
Allow: /ko/
Allow: /ja/
# Block admin areas (if any)
Disallow: /admin/
Disallow: /api/
# Crawl delay
Crawl-delay: 1

67
tailwind.config.ts Normal file
View File

@ -0,0 +1,67 @@
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))'
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
}
}
},
plugins: [require("tailwindcss-animate")],
};
export default config;

40
tsconfig.json Normal file
View File

@ -0,0 +1,40 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"build/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

@ -1 +0,0 @@
t ext

2887
yarn.lock Normal file

File diff suppressed because it is too large Load Diff