From a6f925706228054eca30eedbe91e52463b3f40c7 Mon Sep 17 00:00:00 2001 From: Zopt <100939012+Zopt@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:28:58 +0800 Subject: [PATCH] first commit --- .env.example | 22 + .eslintrc.json | 3 + .gitignore | 39 + .prettierrc | 11 + README.md | 85 + app/[locale]/blog/[slug]/BlogPostClient.tsx | 341 ++ app/[locale]/blog/[slug]/page.tsx | 36 + app/[locale]/blog/page.tsx | 358 ++ app/[locale]/contact/page.tsx | 513 ++ app/[locale]/layout.tsx | 109 + app/[locale]/page.tsx | 210 + app/[locale]/products/page.tsx | 700 +++ app/favicon.ico | Bin 0 -> 179344 bytes app/globals.css | 132 + app/layout.tsx | 16 + app/page.tsx | 44 + app/robots.ts | 22 + app/sitemap.ts | 16 + bun.lockb | Bin 0 -> 184880 bytes components.json | 20 + components/BlogShowcase.tsx | 287 + components/FloatingLanguageSwitcher.tsx | 271 + components/Footer.tsx | 172 + components/Navigation.tsx | 130 + lib/i18n.ts | 20 + lib/seo-utils.ts | 88 + lib/sitemap-generator.ts | 125 + lib/sitemap-utils.ts | 289 + lib/translations.ts | 1091 ++++ lib/utils.ts | 6 + middleware.ts | 54 + next.config.mjs | 6 + package-lock.json | 5582 +++++++++++++++++++ package.json | 34 + postcss.config.mjs | 8 + public/images/logo-text.png | Bin 0 -> 1174 bytes public/manifest.json | 27 + public/robots.txt | 19 + tailwind.config.ts | 67 + tsconfig.json | 40 + txt.txt | 1 - yarn.lock | 2887 ++++++++++ 42 files changed, 13880 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 README.md create mode 100644 app/[locale]/blog/[slug]/BlogPostClient.tsx create mode 100644 app/[locale]/blog/[slug]/page.tsx create mode 100644 app/[locale]/blog/page.tsx create mode 100644 app/[locale]/contact/page.tsx create mode 100644 app/[locale]/layout.tsx create mode 100644 app/[locale]/page.tsx create mode 100644 app/[locale]/products/page.tsx create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/robots.ts create mode 100644 app/sitemap.ts create mode 100644 bun.lockb create mode 100644 components.json create mode 100644 components/BlogShowcase.tsx create mode 100644 components/FloatingLanguageSwitcher.tsx create mode 100644 components/Footer.tsx create mode 100644 components/Navigation.tsx create mode 100644 lib/i18n.ts create mode 100644 lib/seo-utils.ts create mode 100644 lib/sitemap-generator.ts create mode 100644 lib/sitemap-utils.ts create mode 100644 lib/translations.ts create mode 100644 lib/utils.ts create mode 100644 middleware.ts create mode 100644 next.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 public/images/logo-text.png create mode 100644 public/manifest.json create mode 100644 public/robots.txt create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json delete mode 100644 txt.txt create mode 100644 yarn.lock diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8355661 --- /dev/null +++ b/.env.example @@ -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 \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b45fcb9 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..36f0850 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "printWidth": 100, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "jsxSingleQuote": false, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fef17c4 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Eco-Life - A Multilingual Static Next.js Project + +

+ +

+ +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 + ``` +2. Navigate to the project directory: + ```bash + cd + ``` +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`). diff --git a/app/[locale]/blog/[slug]/BlogPostClient.tsx b/app/[locale]/blog/[slug]/BlogPostClient.tsx new file mode 100644 index 0000000..73782f4 --- /dev/null +++ b/app/[locale]/blog/[slug]/BlogPostClient.tsx @@ -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(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 ( + + ); +} + +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 ( +
+
+

Post Not Found

+ + Back to Blog + +
+
+ ); + } + + 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 ( +
+ {/* Gentle Background with Overlay */} +
')`, + }} + /> + + {/* Gentle Particle Background */} + + + {/* Navigation */} + + + {/* Floating Language Switcher */} + + + {/* Article Content */} +
+
+ {/* Back to Blog Link */} +
+ + + + + {t.blog.backToBlog} + +
+ + {/* Article Header */} +
+
+ + {getCategoryName(post.category)} + +
+ +

+ {post.title} +

+ +
+
+ + + + {post.author} +
+
+ + + + {post.date} +
+
+ + + + {post.readTime} +
+
+ +

{post.excerpt}

+
+ + {/* Article Body */} +
+
+ {post.content.split('\n\n').map((paragraph, index) => ( +

+ {paragraph} +

+ ))} +
+
+ + {/* Article Footer */} +
+
+
+
+ + {post.author.charAt(0)} + +
+
+

{post.author}

+

+ Environmental Writer +

+
+
+ +
+ + +
+
+
+ + {/* Related Posts Section */} +
+

Related Posts

+
+ +
+

+ Player Spotlight: Building a Virtual Eco-City +

+

+ Meet Alex, a player who created an incredible sustainable + city... +

+
+ + +
+

+ 10 Amazing Facts About Ocean Conservation +

+

+ Dive into fascinating facts about our oceans... +

+
+ +
+
+
+
+ + {/* Footer */} +
+
+ ); +} diff --git a/app/[locale]/blog/[slug]/page.tsx b/app/[locale]/blog/[slug]/page.tsx new file mode 100644 index 0000000..99cc1b3 --- /dev/null +++ b/app/[locale]/blog/[slug]/page.tsx @@ -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 ; +} diff --git a/app/[locale]/blog/page.tsx b/app/[locale]/blog/page.tsx new file mode 100644 index 0000000..1e3533d --- /dev/null +++ b/app/[locale]/blog/page.tsx @@ -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 { + 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 ( +
+ {/* Hero Section */} +
+
+
+

+ {t.blog.title} +

+

+ {t.blog.subtitle} +

+
+ + {/* Category Filter */} +
+ + + + +
+
+
+ + {/* Featured Post */} + {featuredPost && ( +
+
+
+

+ {t.blog.featured} +

+
+ + +
+
+
+ {/* Featured Post Image */} +
+
+
+ {/* Animated particles */} +
+ {[...Array(8)].map((_, i) => ( +
+ ))} +
+
+
+ + {getCategoryName(featuredPost.category)} + +
+
+
+ + {t.blog.featured} + +
+
+
+ + {/* Featured Post Content */} +
+
+
+ {featuredPost.author} + + {featuredPost.date} + + {featuredPost.readTime} +
+
+

+ {featuredPost.title} +

+

+ {featuredPost.excerpt} +

+
+ + {t.blog.readMore} + + + + +
+
+
+
+
+ +
+
+ )} + + {/* Regular Posts Grid */} +
+
+
+

+ {t.blog.categories.all} +

+
+ +
+ {regularPosts.map((post) => ( + +
+
+ {/* Post Image */} +
+
+
+ {/* Mini particles */} +
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+
+
+ + {getCategoryName(post.category)} + +
+
+ + {/* Post Content */} +
+
+
+ {post.author} + + {post.readTime} +
+
+

+ {post.title} +

+

+ {post.excerpt} +

+
+ + {post.date} + + + {t.blog.readMore} + + + + +
+
+
+
+ + ))} +
+
+
+ + {/* Newsletter Subscription */} +
+
+
+

{t.blog.stayUpdated}

+

+ {t.blog.stayUpdatedDesc} +

+
+ + +
+
+
+
+
+ ); +} diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx new file mode 100644 index 0000000..356a157 --- /dev/null +++ b/app/[locale]/contact/page.tsx @@ -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(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 ( + + ); +} + +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) => { + 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 ( +
+ {/* Arctic Background with Overlay */} +
')`, + }} + data-oid="g8y9fqe" + /> + + {/* Arctic Particle Background */} + + + {/* Navigation */} + + + {/* Floating Language Switcher */} + + + {/* Contact Section */} +
+
+ {/* Header */} +
+

+ {t.contact.title} +

+

+ {t.contact.subtitle} +

+
+ +
+ {/* Contact Form */} +
+
+ {/* Name Fields */} +
+
+ +
+
+ +
+
+ + {/* Email and Phone */} +
+
+ +
+
+ +
+
+ + {/* Message */} +
+