Files
novalon-website/src/app/(marketing)/news/page.tsx
T
张翔 2f45818724 feat(analytics): enhance Google Analytics with privacy compliance and comprehensive tracking
- Add automatic route change tracking for SPA navigation
- Implement Cookie consent banner for GDPR compliance
- Add performance tracking (LCP, FID, CLS Web Vitals)
- Add outbound link click tracking
- Integrate contact form submission tracking with conversion events
- Add CTA button click tracking in hero section
- Integrate error tracking in ErrorBoundary component
- Extend analytics utility library with 15+ tracking functions
- Configure IP anonymization and privacy settings
- Remove unused test files and deployment scripts
- Update case studies to include only specified cases
- Fix mobile navigation active state issues
- Fix lint errors in test files and components

BREAKING CHANGE: Google Analytics now requires user consent before tracking
2026-04-22 07:19:29 +08:00

209 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useMemo, useRef, ChangeEvent } from 'react';
import { useInView } from 'framer-motion';
import { NEWS } from '@/lib/constants';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { PageHeader } from '@/components/ui/page-header';
import { Search, Calendar, Filter, ChevronLeft, ChevronRight, ArrowRight } from 'lucide-react';
import { StaticLink } from '@/components/ui/static-link';
import { motion } from 'framer-motion';
const categories = ['全部', '公司新闻', '产品发布'];
const ITEMS_PER_PAGE = 9;
export default function NewsListPage() {
const [selectedCategory, setSelectedCategory] = useState('全部');
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const contentRef = useRef(null);
const isContentInView = useInView(contentRef, { once: true, margin: '-100px' });
const filteredNews = useMemo(() => {
return NEWS.filter((newsItem) => {
const matchesCategory = selectedCategory === '全部' || newsItem.category === selectedCategory;
const matchesSearch =
newsItem.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
newsItem.excerpt.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
}, [selectedCategory, searchQuery]);
const totalPages = Math.ceil(filteredNews.length / ITEMS_PER_PAGE);
const paginatedNews = useMemo(() => {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
return filteredNews.slice(startIndex, endIndex);
}, [filteredNews, currentPage]);
const handlePageChange = (page: number) => {
setCurrentPage(page);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const handleCategoryChange = (category: string) => {
setSelectedCategory(category);
setCurrentPage(1);
};
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
setCurrentPage(1);
};
return (
<div className="min-h-screen bg-white">
<PageHeader
title="新闻动态"
description="了解睿新致远最新动态,把握行业发展脉搏"
/>
<div className="container-wide relative z-10 py-12" ref={contentRef}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="mb-8 space-y-4"
>
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center">
<div className="flex items-center gap-2 text-[#1C1C1C]">
<Filter className="w-5 h-5" />
<span className="font-medium"></span>
</div>
<div className="flex flex-wrap gap-2">
{categories.map((category) => (
<Button
key={category}
variant={selectedCategory === category ? 'default' : 'outline'}
onClick={() => handleCategoryChange(category)}
className={
selectedCategory === category
? 'bg-[#C41E3A] hover:bg-[#A01830] text-white'
: ''
}
>
{category}
</Button>
))}
</div>
</div>
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[#5C5C5C] w-5 h-5" />
<Input
type="text"
placeholder="搜索新闻..."
value={searchQuery}
onChange={handleSearchChange}
className="pl-10"
aria-label="搜索新闻"
/>
</div>
</motion.div>
{paginatedNews.length === 0 ? (
<div className="text-center py-20">
<p className="text-xl text-[#5C5C5C]"></p>
</div>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{paginatedNews.map((newsItem, index) => (
<motion.div
key={newsItem.id}
initial={{ opacity: 0, y: 20 }}
animate={isContentInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.2 + index * 0.1 }}
>
<StaticLink href={`/news/${newsItem.id}`}>
<Card className="h-full hover:shadow-lg transition-shadow cursor-pointer border-[#E5E5E5] hover:border-[#C41E3A]">
<CardContent className="p-0">
{newsItem.image ? (
<div className="aspect-video bg-gray-100 overflow-hidden">
<img
src={newsItem.image}
alt={newsItem.title}
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="aspect-video bg-gradient-to-br from-[#C41E3A]/10 to-[#1C1C1C]/10 flex items-center justify-center mb-4">
<span className="text-4xl">📰</span>
</div>
)}
<div className="p-6">
<div className="flex items-center gap-2 mb-3">
<Badge variant="secondary">{newsItem.category}</Badge>
<div className="flex items-center gap-1 text-sm text-[#5C5C5C]">
<Calendar className="w-4 h-4" />
{newsItem.date}
</div>
</div>
<h3 className="text-xl font-semibold text-[#1C1C1C] mb-3 line-clamp-2">
{newsItem.title}
</h3>
<p className="text-[#5C5C5C] text-sm line-clamp-3 mb-4">
{newsItem.excerpt}
</p>
<div className="flex items-center text-[#C41E3A] text-sm font-medium group">
<ArrowRight className="ml-1 w-4 h-4" />
</div>
</div>
</CardContent>
</Card>
</StaticLink>
</motion.div>
))}
</div>
{totalPages > 1 && (
<div className="flex justify-center items-center gap-2 mt-8">
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
<ChevronLeft className="w-4 h-4" />
</Button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<Button
key={page}
variant={currentPage === page ? 'default' : 'outline'}
size="icon"
onClick={() => handlePageChange(page)}
className={
currentPage === page
? 'bg-[#C41E3A] hover:bg-[#A01830] text-white'
: ''
}
>
{page}
</Button>
))}
<Button
variant="outline"
size="icon"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
)}
<div className="text-center mt-4 text-[#5C5C5C] text-sm">
{paginatedNews.length} {filteredNews.length}
</div>
</>
)}
</div>
</div>
);
}