109 lines
No EOL
4 KiB
TypeScript
109 lines
No EOL
4 KiB
TypeScript
import React, { Suspense } from 'react';
|
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Helmet } from 'react-helmet-async';
|
|
import { ThemeProvider } from './contexts/ThemeContext';
|
|
import Navbar from './components/Navbar';
|
|
import Breadcrumb from './components/Breadcrumb';
|
|
import LoadingSpinner from './components/LoadingSpinner';
|
|
import iconMap from './utils/iconMap';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
|
|
// 懒加载路由组件
|
|
const HomePage = React.lazy(() => import('./pages/HomePage'));
|
|
const ProjectsPage = React.lazy(() => import('./pages/ProjectsPage'));
|
|
const UpdatesPage = React.lazy(() => import('./pages/UpdatesPage'));
|
|
const UpdateDetailPage = React.lazy(() => import('./pages/UpdateDetailPage'));
|
|
const ContributorsPage = React.lazy(() => import('./pages/ContributorsPage'));
|
|
const AboutPage = React.lazy(() => import('./pages/AboutPage'));
|
|
|
|
function App() {
|
|
const { t, i18n } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
const socialLinks = t('social.links', { returnObjects: true });
|
|
|
|
// 在窗口关闭时清除缓存
|
|
React.useEffect(() => {
|
|
const handleBeforeUnload = () => {
|
|
queryClient.clear();
|
|
};
|
|
|
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
return () => {
|
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
};
|
|
}, [queryClient]);
|
|
|
|
// 构建 Schema.org 结构化数据
|
|
const schemaOrgWebsite = {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'WebSite',
|
|
name: 'STARSET Mirror',
|
|
url: 'https://mirror.starset.fans',
|
|
description: t('site.description'),
|
|
inLanguage: ['zh-Hans', 'en', 'zh-Hant'],
|
|
potentialAction: {
|
|
'@type': 'SearchAction',
|
|
target: 'https://mirror.starset.fans/search?q={search_term_string}',
|
|
'query-input': 'required name=search_term_string'
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ThemeProvider>
|
|
<Router>
|
|
<div className="min-h-screen bg-white dark:bg-gray-900 transition-colors">
|
|
<Helmet>
|
|
<script type="application/ld+json">
|
|
{JSON.stringify(schemaOrgWebsite)}
|
|
</script>
|
|
</Helmet>
|
|
|
|
<Navbar />
|
|
<Breadcrumb />
|
|
|
|
<main className="pt-14" role="main" aria-label={t('aria.mainContent')}>
|
|
<Suspense fallback={<LoadingSpinner />}>
|
|
<Routes>
|
|
<Route path="/" element={<HomePage />} />
|
|
<Route path="/projects" element={<ProjectsPage />} />
|
|
<Route path="/updates" element={<UpdatesPage />} />
|
|
<Route path="/updates/:id" element={<UpdateDetailPage />} />
|
|
<Route path="/contributors" element={<ContributorsPage />} />
|
|
<Route path="/about" element={<AboutPage />} />
|
|
</Routes>
|
|
</Suspense>
|
|
</main>
|
|
|
|
<footer className="bg-gray-900 dark:bg-gray-950 text-white py-6" role="contentinfo">
|
|
<div className="container mx-auto px-4">
|
|
<div className="flex flex-col items-center">
|
|
<p className="text-gray-400 mb-4"> {t('footer.copyright', { currentYear: new Date().getFullYear() })}</p>
|
|
<div className="flex space-x-6">
|
|
{socialLinks.map((link: any) => {
|
|
const Icon = iconMap[link.icon as keyof typeof iconMap];
|
|
return (
|
|
<a
|
|
key={link.name}
|
|
href={link.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={link.color}
|
|
aria-label={link.name}
|
|
title={link.name}
|
|
>
|
|
<Icon className="h-6 w-6" />
|
|
</a>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</Router>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
export default App; |